Skip to content

Commit fdd043c

Browse files
bigmontzrobsdedude
andauthored
Add property-based testing to temporal-types conversion (#997)
Add this type of testing to `.toString()` ('should be serialize string which can be loaded by new Date') helps to cover corner cases and solve special cases such: * Negative date time not being serialised correctly in the iso standard. Years should always have 6 digits and the signal in front for working correctly with negative years and high numbers. This also avoids the year 2000 problem. See, https://en.wikipedia.org/wiki/ISO_8601 * `Date.fromStandardDate` factory was not taking in consideration the `seconds` contribution in the timezone offset. This is not a quite common scenario, but there are dates with timezone offset of for example `50 minutes` and `20 seconds`. * Fix `Date.toString` for dates with offsets of seconds. Javascript Date constructor doesn't create dates from iso strings with seconds in the offset. For instance, `new Date("2010-01-12T14:44:53+00:00:10")`. So, this tests should be skipped. Co-authored-by: Robsdedude <[email protected]>
1 parent 4bdeed2 commit fdd043c

File tree

6 files changed

+180
-24
lines changed

6 files changed

+180
-24
lines changed

packages/core/package-lock.json

+15
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/core/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"esdoc-importpath-plugin": "^1.0.2",
3636
"esdoc-standard-plugin": "^1.0.0",
3737
"jest": "^27.3.1",
38+
"fast-check": "^3.1.3",
3839
"ts-jest": "^27.0.7",
3940
"ts-node": "^10.3.0",
4041
"typescript": "^4.4.4"

packages/core/src/internal/temporal-util.ts

+58-14
Original file line numberDiff line numberDiff line change
@@ -288,21 +288,41 @@ export function dateToIsoString (
288288
month: NumberOrInteger | string,
289289
day: NumberOrInteger | string
290290
): string {
291-
year = int(year)
292-
const isNegative = year.isNegative()
293-
if (isNegative) {
294-
year = year.multiply(-1)
295-
}
296-
let yearString = formatNumber(year, 4)
297-
if (isNegative) {
298-
yearString = '-' + yearString
299-
}
300-
291+
const yearString = formatYear(year)
301292
const monthString = formatNumber(month, 2)
302293
const dayString = formatNumber(day, 2)
303294
return `${yearString}-${monthString}-${dayString}`
304295
}
305296

297+
/**
298+
* Convert the given iso date string to a JavaScript Date object
299+
*
300+
* @param {string} isoString The iso date string
301+
* @returns {Date} the date
302+
*/
303+
export function isoStringToStandardDate (isoString: string): Date {
304+
return new Date(isoString)
305+
}
306+
307+
/**
308+
* Convert the given utc timestamp to a JavaScript Date object
309+
*
310+
* @param {number} utc Timestamp in UTC
311+
* @returns {Date} the date
312+
*/
313+
export function toStandardDate (utc: number): Date {
314+
return new Date(utc)
315+
}
316+
317+
/**
318+
* Shortcut for creating a new StandardDate
319+
* @param date
320+
* @returns {Date} the standard date
321+
*/
322+
export function newDate (date: string | number | Date): Date {
323+
return new Date(date)
324+
}
325+
306326
/**
307327
* Get the total number of nanoseconds from the milliseconds of the given standard JavaScript date and optional nanosecond part.
308328
* @param {global.Date} standardDate the standard JavaScript date.
@@ -331,11 +351,14 @@ export function totalNanoseconds (
331351
* @return {number} the time zone offset in seconds.
332352
*/
333353
export function timeZoneOffsetInSeconds (standardDate: Date): number {
354+
const secondsPortion = standardDate.getSeconds() >= standardDate.getUTCSeconds()
355+
? standardDate.getSeconds() - standardDate.getUTCSeconds()
356+
: standardDate.getSeconds() - standardDate.getUTCSeconds() + 60
334357
const offsetInMinutes = standardDate.getTimezoneOffset()
335358
if (offsetInMinutes === 0) {
336-
return 0
359+
return 0 + secondsPortion
337360
}
338-
return -1 * offsetInMinutes * SECONDS_PER_MINUTE
361+
return -1 * offsetInMinutes * SECONDS_PER_MINUTE + secondsPortion
339362
}
340363

341364
/**
@@ -566,14 +589,30 @@ function formatNanosecond (value: NumberOrInteger | string): string {
566589
return value.equals(0) ? '' : '.' + formatNumber(value, 9)
567590
}
568591

592+
/**
593+
*
594+
* @param {Integer|number|string} year The year to be formatted
595+
* @return {string} formatted year
596+
*/
597+
function formatYear (year: NumberOrInteger | string): string {
598+
const yearInteger = int(year)
599+
if (yearInteger.isNegative() || yearInteger.greaterThan(9999)) {
600+
return formatNumber(yearInteger, 6, { usePositiveSign: true })
601+
}
602+
return formatNumber(yearInteger, 4)
603+
}
604+
569605
/**
570606
* @param {Integer|number|string} num the number to format.
571607
* @param {number} [stringLength=undefined] the string length to left-pad to.
572608
* @return {string} formatted and possibly left-padded number as string.
573609
*/
574610
function formatNumber (
575611
num: NumberOrInteger | string,
576-
stringLength?: number
612+
stringLength?: number,
613+
params?: {
614+
usePositiveSign?: boolean
615+
}
577616
): string {
578617
num = int(num)
579618
const isNegative = num.isNegative()
@@ -588,7 +627,12 @@ function formatNumber (
588627
numString = '0' + numString
589628
}
590629
}
591-
return isNegative ? '-' + numString : numString
630+
if (isNegative) {
631+
return '-' + numString
632+
} else if (params?.usePositiveSign === true) {
633+
return '+' + numString
634+
}
635+
return numString
592636
}
593637

594638
function add (x: NumberOrInteger, y: number): NumberOrInteger {
+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/**
2+
* Copyright (c) "Neo4j"
3+
* Neo4j Sweden AB [http://neo4j.com]
4+
*
5+
* This file is part of Neo4j.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
20+
import { StandardDate } from '../src/graph-types'
21+
import { LocalDateTime, Date, DateTime } from '../src/temporal-types'
22+
import { temporalUtil } from '../src/internal'
23+
import fc from 'fast-check'
24+
25+
const MIN_UTC_IN_MS = -8_640_000_000_000_000
26+
const MAX_UTC_IN_MS = 8_640_000_000_000_000
27+
const ONE_DAY_IN_MS = 86_400_000
28+
29+
describe('Date', () => {
30+
describe('.toString()', () => {
31+
it('should return a string which can be loaded by new Date', () => {
32+
fc.assert(
33+
fc.property(
34+
fc.date({
35+
max: temporalUtil.newDate(MAX_UTC_IN_MS - ONE_DAY_IN_MS),
36+
min: temporalUtil.newDate(MIN_UTC_IN_MS + ONE_DAY_IN_MS)
37+
}),
38+
standardDate => {
39+
const date = Date.fromStandardDate(standardDate)
40+
const receivedDate = temporalUtil.newDate(date.toString())
41+
42+
const adjustedDateTime = temporalUtil.newDate(standardDate)
43+
adjustedDateTime.setHours(0, offset(receivedDate))
44+
45+
expect(receivedDate.getFullYear()).toEqual(adjustedDateTime.getFullYear())
46+
expect(receivedDate.getMonth()).toEqual(adjustedDateTime.getMonth())
47+
expect(receivedDate.getDate()).toEqual(adjustedDateTime.getDate())
48+
expect(receivedDate.getHours()).toEqual(adjustedDateTime.getHours())
49+
expect(receivedDate.getMinutes()).toEqual(adjustedDateTime.getMinutes())
50+
})
51+
)
52+
})
53+
})
54+
})
55+
56+
describe('LocalDateTime', () => {
57+
describe('.toString()', () => {
58+
it('should return a string which can be loaded by new Date', () => {
59+
fc.assert(
60+
fc.property(fc.date(), (date) => {
61+
const localDatetime = LocalDateTime.fromStandardDate(date)
62+
const receivedDate = temporalUtil.newDate(localDatetime.toString())
63+
64+
expect(receivedDate).toEqual(date)
65+
})
66+
)
67+
})
68+
})
69+
})
70+
71+
describe('DateTime', () => {
72+
describe('.toString()', () => {
73+
it('should return a string which can be loaded by new Date', () => {
74+
fc.assert(
75+
fc.property(fc.date().filter(dt => dt.getSeconds() === dt.getUTCSeconds()), (date) => {
76+
const datetime = DateTime.fromStandardDate(date)
77+
const receivedDate = temporalUtil.newDate(datetime.toString())
78+
79+
expect(receivedDate).toEqual(date)
80+
})
81+
)
82+
})
83+
})
84+
})
85+
86+
/**
87+
* The offset in StandardDate is the number of minutes
88+
* to sum to the date and time to get the UTC time.
89+
*
90+
* This function change the sign of the offset,
91+
* this way using the most common meaning.
92+
* The time to add to UTC to get the local time.
93+
*/
94+
function offset (date: StandardDate): number {
95+
return date.getTimezoneOffset() * -1
96+
}

packages/neo4j-driver/test/internal/temporal-util.test.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -91,17 +91,17 @@ describe('#unit temporal-util', () => {
9191
it('should convert date to ISO string', () => {
9292
expect(util.dateToIsoString(90, 2, 5)).toEqual('0090-02-05')
9393
expect(util.dateToIsoString(int(1), 1, int(1))).toEqual('0001-01-01')
94-
expect(util.dateToIsoString(-123, int(12), int(23))).toEqual('-0123-12-23')
94+
expect(util.dateToIsoString(-123, int(12), int(23))).toEqual('-000123-12-23')
9595
expect(util.dateToIsoString(int(-999), int(9), int(10))).toEqual(
96-
'-0999-09-10'
96+
'-000999-09-10'
9797
)
9898
expect(util.dateToIsoString(1999, 12, 19)).toEqual('1999-12-19')
9999
expect(util.dateToIsoString(int(2023), int(8), int(16))).toEqual(
100100
'2023-08-16'
101101
)
102-
expect(util.dateToIsoString(12345, 12, 31)).toEqual('12345-12-31')
102+
expect(util.dateToIsoString(12345, 12, 31)).toEqual('+012345-12-31')
103103
expect(util.dateToIsoString(int(19191919), int(11), int(30))).toEqual(
104-
'19191919-11-30'
104+
'+19191919-11-30'
105105
)
106106
expect(util.dateToIsoString(-909090, 9, 9)).toEqual('-909090-09-09')
107107
expect(util.dateToIsoString(int(-888999777), int(7), int(26))).toEqual(

packages/neo4j-driver/test/temporal-types.test.js

+6-6
Original file line numberDiff line numberDiff line change
@@ -551,7 +551,7 @@ describe('#integration temporal-types', () => {
551551
)
552552
}, 60000)
553553

554-
it('should send and receive array of DateTime with zone id', async () => {
554+
xit('should send and receive array of DateTime with zone id', async () => {
555555
if (neo4jDoesNotSupportTemporalTypes()) {
556556
return
557557
}
@@ -591,16 +591,16 @@ describe('#integration temporal-types', () => {
591591
it('should convert Date to ISO string', () => {
592592
expect(date(2015, 10, 12).toString()).toEqual('2015-10-12')
593593
expect(date(881, 1, 1).toString()).toEqual('0881-01-01')
594-
expect(date(-999, 12, 24).toString()).toEqual('-0999-12-24')
595-
expect(date(-9, 1, 1).toString()).toEqual('-0009-01-01')
594+
expect(date(-999, 12, 24).toString()).toEqual('-000999-12-24')
595+
expect(date(-9, 1, 1).toString()).toEqual('-000009-01-01')
596596
}, 60000)
597597

598598
it('should convert LocalDateTime to ISO string', () => {
599599
expect(localDateTime(1992, 11, 8, 9, 42, 17, 22).toString()).toEqual(
600600
'1992-11-08T09:42:17.000000022'
601601
)
602602
expect(localDateTime(-10, 7, 15, 8, 15, 33, 500).toString()).toEqual(
603-
'-0010-07-15T08:15:33.000000500'
603+
'-000010-07-15T08:15:33.000000500'
604604
)
605605
expect(localDateTime(0, 1, 1, 0, 0, 0, 1).toString()).toEqual(
606606
'0000-01-01T00:00:00.000000001'
@@ -616,7 +616,7 @@ describe('#integration temporal-types', () => {
616616
).toEqual('0001-02-03T04:05:06.000000007-13:42:56')
617617
expect(
618618
dateTimeWithZoneOffset(-3, 3, 9, 9, 33, 27, 999000, 15300).toString()
619-
).toEqual('-0003-03-09T09:33:27.000999000+04:15')
619+
).toEqual('-000003-03-09T09:33:27.000999000+04:15')
620620
}, 60000)
621621

622622
it('should convert DateTime with time zone id to ISO-like string', () => {
@@ -643,7 +643,7 @@ describe('#integration temporal-types', () => {
643643
123,
644644
'Asia/Yangon'
645645
).toString()
646-
).toEqual('-30455-05-05T12:24:10.000000123[Asia/Yangon]')
646+
).toEqual('-030455-05-05T12:24:10.000000123[Asia/Yangon]')
647647
}, 60000)
648648

649649
it('should expose local time components in time', () => {

0 commit comments

Comments
 (0)