Skip to content

Commit 2e3a622

Browse files
committed
Add property-based testing to temporal-types conversion
Add this type of testing to `.toStandardDate()` ('should be the reverse operation of fromStandardDate but losing time information') helps to cover corner cases and solve special cases such: * Negative date time not being serialized 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` contribuition in the timezone offset. This is not a quite common scenarion, but there are dates with timezone offset of for example `50 minutes` and `20 seconds`. * Fix `Date.toStandardDate` for dates with offsets of seconds. Javascript Date contructor 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, the date should be re-created from the utc timestamp.
1 parent b7e2198 commit 2e3a622

File tree

6 files changed

+110
-57
lines changed

6 files changed

+110
-57
lines changed

packages/core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"esdoc": "^1.1.0",
3535
"esdoc-importpath-plugin": "^1.0.2",
3636
"esdoc-standard-plugin": "^1.0.0",
37+
"fast-check": "^3.1.3",
3738
"jest": "^27.5.1",
3839
"ts-jest": "^27.1.4",
3940
"ts-node": "^10.3.0",

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

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -288,16 +288,7 @@ 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 = formatNumber(year, 6, { usePositiveSign: true })
301292
const monthString = formatNumber(month, 2)
302293
const dayString = formatNumber(day, 2)
303294
return `${yearString}-${monthString}-${dayString}`
@@ -313,6 +304,16 @@ export function isoStringToStandardDate (isoString: string): Date {
313304
return new Date(isoString)
314305
}
315306

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+
316317
/**
317318
* Get the total number of nanoseconds from the milliseconds of the given standard JavaScript date and optional nanosecond part.
318319
* @param {global.Date} standardDate the standard JavaScript date.
@@ -341,11 +342,14 @@ export function totalNanoseconds (
341342
* @return {number} the time zone offset in seconds.
342343
*/
343344
export function timeZoneOffsetInSeconds (standardDate: Date): number {
345+
const secondsPortion = standardDate.getSeconds() >= standardDate.getUTCSeconds()
346+
? standardDate.getSeconds() - standardDate.getUTCSeconds()
347+
: standardDate.getSeconds() - standardDate.getUTCSeconds() + 60
344348
const offsetInMinutes = standardDate.getTimezoneOffset()
345349
if (offsetInMinutes === 0) {
346-
return 0
350+
return 0 + secondsPortion
347351
}
348-
return -1 * offsetInMinutes * SECONDS_PER_MINUTE
352+
return -1 * offsetInMinutes * SECONDS_PER_MINUTE + secondsPortion
349353
}
350354

351355
/**
@@ -583,7 +587,10 @@ function formatNanosecond (value: NumberOrInteger | string): string {
583587
*/
584588
function formatNumber (
585589
num: NumberOrInteger | string,
586-
stringLength?: number
590+
stringLength?: number,
591+
params?: {
592+
usePositiveSign?: boolean
593+
}
587594
): string {
588595
num = int(num)
589596
const isNegative = num.isNegative()
@@ -598,7 +605,12 @@ function formatNumber (
598605
numString = '0' + numString
599606
}
600607
}
601-
return isNegative ? '-' + numString : numString
608+
if (isNegative) {
609+
return '-' + numString
610+
} else if (params?.usePositiveSign === true) {
611+
return '+' + numString
612+
}
613+
return numString
602614
}
603615

604616
function add (x: NumberOrInteger, y: number): NumberOrInteger {

packages/core/src/temporal-types.ts

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -667,16 +667,7 @@ export class DateTime<T extends NumberOrInteger = Integer> {
667667
* @throws {Error} If the time zone offset is not defined in the object.
668668
*/
669669
toStandardDate (): StandardDate {
670-
if (this.timeZoneOffsetSeconds === undefined) {
671-
throw new Error('Requires DateTime created with time zone offset')
672-
}
673-
return util.isoStringToStandardDate(
674-
// the timezone name should be removed from the
675-
// string, otherwise the javascript parse doesn't
676-
// read the datetime correctly
677-
this.toString().replace(
678-
this.timeZoneId != null ? `[${this.timeZoneId}]` : '', '')
679-
)
670+
return util.toStandardDate(this._toUTC())
680671
}
681672

682673
/**
@@ -703,6 +694,32 @@ export class DateTime<T extends NumberOrInteger = Integer> {
703694

704695
return localDateTimeStr + timeOffset + timeZoneStr
705696
}
697+
698+
/**
699+
* @private
700+
* @returns {number}
701+
*/
702+
private _toUTC (): number {
703+
if (this.timeZoneOffsetSeconds === undefined) {
704+
throw new Error('Requires DateTime created with time zone offset')
705+
}
706+
const epochSecond = util.localDateTimeToEpochSecond(
707+
this.year,
708+
this.month,
709+
this.day,
710+
this.hour,
711+
this.minute,
712+
this.second,
713+
this.nanosecond
714+
)
715+
716+
const utcSecond = epochSecond.subtract(this.timeZoneOffsetSeconds ?? 0)
717+
718+
return int(utcSecond)
719+
.multiply(1000)
720+
.add(int(this.nanosecond).div(1_000_000))
721+
.toNumber()
722+
}
706723
}
707724

708725
Object.defineProperty(

packages/core/test/temporal-types.test.ts

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import { StandardDate } from '../src/graph-types'
2121
import { LocalDateTime, Date, DateTime } from '../src/temporal-types'
22+
import fc from 'fast-check'
2223

2324
describe('Date', () => {
2425
describe('.toStandardDate()', () => {
@@ -33,15 +34,33 @@ describe('Date', () => {
3334
})
3435

3536
it('should be the reverse operation of fromStandardDate but losing time information', () => {
36-
const standardDate = new global.Date()
37-
38-
const date = Date.fromStandardDate(standardDate)
39-
const receivedDate = date.toStandardDate()
40-
41-
// Setting 00:00:00:000 UTC
42-
standardDate.setHours(0, -1 * standardDate.getTimezoneOffset(), 0, 0)
43-
44-
expect(receivedDate).toEqual(standardDate)
37+
fc.assert(
38+
fc.property(fc.date(), (standardDate) => {
39+
// @ts-expect-error
40+
if (isNaN(standardDate)) {
41+
// Should not create from a non-valid date.
42+
expect(() => Date.fromStandardDate(standardDate)).toThrow(TypeError)
43+
return
44+
}
45+
46+
const date = Date.fromStandardDate(standardDate)
47+
const receivedDate = date.toStandardDate()
48+
49+
const hour = standardDate.setHours(0, -1 * receivedDate.getTimezoneOffset())
50+
51+
// In some situations, the setHours result in a NaN hour.
52+
// In this case, the test should be discarded
53+
if (isNaN(hour)) {
54+
return
55+
}
56+
57+
expect(receivedDate.getFullYear()).toEqual(standardDate.getFullYear())
58+
expect(receivedDate.getMonth()).toEqual(standardDate.getMonth())
59+
expect(receivedDate.getDate()).toEqual(standardDate.getDate())
60+
expect(receivedDate.getHours()).toEqual(standardDate.getHours())
61+
expect(receivedDate.getMinutes()).toEqual(standardDate.getMinutes())
62+
})
63+
)
4564
})
4665
})
4766
})
@@ -63,12 +82,14 @@ describe('LocalDateTime', () => {
6382
})
6483

6584
it('should be the reverse operation of fromStandardDate', () => {
66-
const date = new global.Date()
67-
68-
const localDatetime = LocalDateTime.fromStandardDate(date)
69-
const receivedDate = localDatetime.toStandardDate()
70-
71-
expect(receivedDate).toEqual(date)
85+
fc.assert(
86+
fc.property(fc.date(), (date) => {
87+
const localDatetime = LocalDateTime.fromStandardDate(date)
88+
const receivedDate = localDatetime.toStandardDate()
89+
90+
expect(receivedDate).toEqual(date)
91+
})
92+
)
7293
})
7394
})
7495
})
@@ -135,12 +156,14 @@ describe('DateTime', () => {
135156
})
136157

137158
it('should be the reverse operation of fromStandardDate', () => {
138-
const date = new global.Date()
139-
140-
const datetime = DateTime.fromStandardDate(date)
141-
const receivedDate = datetime.toStandardDate()
142-
143-
expect(receivedDate).toEqual(date)
159+
fc.assert(
160+
fc.property(fc.date(), (date) => {
161+
const datetime = DateTime.fromStandardDate(date)
162+
const receivedDate = datetime.toStandardDate()
163+
164+
expect(receivedDate).toEqual(date)
165+
})
166+
)
144167
})
145168
})
146169
})

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -88,19 +88,19 @@ describe('#unit temporal-util', () => {
8888
})
8989

9090
it('should convert date to ISO string', () => {
91-
expect(util.dateToIsoString(90, 2, 5)).toEqual('0090-02-05')
92-
expect(util.dateToIsoString(int(1), 1, int(1))).toEqual('0001-01-01')
93-
expect(util.dateToIsoString(-123, int(12), int(23))).toEqual('-0123-12-23')
91+
expect(util.dateToIsoString(90, 2, 5)).toEqual('+000090-02-05')
92+
expect(util.dateToIsoString(int(1), 1, int(1))).toEqual('+000001-01-01')
93+
expect(util.dateToIsoString(-123, int(12), int(23))).toEqual('-000123-12-23')
9494
expect(util.dateToIsoString(int(-999), int(9), int(10))).toEqual(
95-
'-0999-09-10'
95+
'-000999-09-10'
9696
)
97-
expect(util.dateToIsoString(1999, 12, 19)).toEqual('1999-12-19')
97+
expect(util.dateToIsoString(1999, 12, 19)).toEqual('+001999-12-19')
9898
expect(util.dateToIsoString(int(2023), int(8), int(16))).toEqual(
99-
'2023-08-16'
99+
'+002023-08-16'
100100
)
101-
expect(util.dateToIsoString(12345, 12, 31)).toEqual('12345-12-31')
101+
expect(util.dateToIsoString(12345, 12, 31)).toEqual('+012345-12-31')
102102
expect(util.dateToIsoString(int(19191919), int(11), int(30))).toEqual(
103-
'19191919-11-30'
103+
'+19191919-11-30'
104104
)
105105
expect(util.dateToIsoString(-909090, 9, 9)).toEqual('-909090-09-09')
106106
expect(util.dateToIsoString(int(-888999777), int(7), int(26))).toEqual(

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -588,10 +588,10 @@ describe('#integration temporal-types', () => {
588588
}, 60000)
589589

590590
it('should convert Date to ISO string', () => {
591-
expect(date(2015, 10, 12).toString()).toEqual('2015-10-12')
592-
expect(date(881, 1, 1).toString()).toEqual('0881-01-01')
593-
expect(date(-999, 12, 24).toString()).toEqual('-0999-12-24')
594-
expect(date(-9, 1, 1).toString()).toEqual('-0009-01-01')
591+
expect(date(2015, 10, 12).toString()).toEqual('+002015-10-12')
592+
expect(date(881, 1, 1).toString()).toEqual('+000881-01-01')
593+
expect(date(-999, 12, 24).toString()).toEqual('-000999-12-24')
594+
expect(date(-9, 1, 1).toString()).toEqual('-000009-01-01')
595595
}, 60000)
596596

597597
it('should convert LocalDateTime to ISO string', () => {

0 commit comments

Comments
 (0)