diff --git a/packages/core/src/temporal-types.ts b/packages/core/src/temporal-types.ts index 974f97c16..8c1e5ff2b 100644 --- a/packages/core/src/temporal-types.ts +++ b/packages/core/src/temporal-types.ts @@ -380,6 +380,16 @@ export class Date { return util.isoStringToStandardDate(this.toString()) } + /** + * Serialize date to ISO 8601 + * + * @throws {Error} If the time zone offset is not defined in the object. + * @return {string} The ISO string + */ + toJSON (): string { + return this.toString() + } + /** * @ignore */ @@ -505,6 +515,16 @@ export class LocalDateTime { return util.isoStringToStandardDate(this.toString()) } + /** + * Serialize date to ISO 8601 + * + * @throws {Error} If the time zone offset is not defined in the object. + * @return {string} The ISO string + */ + toJSON (): string { + return this.toString() + } + /** * @ignore */ @@ -670,6 +690,31 @@ export class DateTime { return util.toStandardDate(this._toUTC()) } + /** + * Serialize date to ISO 8601 + * + * @throws {Error} If the time zone offset is not defined in the object. + * @return {string} The ISO string + */ + toJSON (): string { + if (this.timeZoneOffsetSeconds === undefined) { + throw new Error('Requires DateTime created with time zone offset') + } + const localDateTimeStr = localDateTimeToString( + this.year, + this.month, + this.day, + this.hour, + this.minute, + this.second, + this.nanosecond + ) + + const timeOffset = util.timeZoneOffsetToIsoString(this.timeZoneOffsetSeconds) + + return localDateTimeStr + timeOffset + } + /** * @ignore */ diff --git a/packages/core/test/temporal-types.test.ts b/packages/core/test/temporal-types.test.ts index 6c41fe02b..54e18d843 100644 --- a/packages/core/test/temporal-types.test.ts +++ b/packages/core/test/temporal-types.test.ts @@ -25,6 +25,7 @@ import fc from 'fast-check' const MIN_UTC_IN_MS = -8_640_000_000_000_000 const MAX_UTC_IN_MS = 8_640_000_000_000_000 const ONE_DAY_IN_MS = 86_400_000 +const ONE_MINUTE_TO_ONE_DAY_IN_MINUTES = 1439 describe('Date', () => { describe('.toStandardDate()', () => { @@ -61,6 +62,35 @@ describe('Date', () => { ) }) }) + + describe('JSON.stringify()', () => { + it('should serialize a valid ISO date', () => { + fc.assert( + fc.property( + fc.date({ + max: temporalUtil.newDate(MAX_UTC_IN_MS - ONE_DAY_IN_MS), + min: temporalUtil.newDate(MIN_UTC_IN_MS + ONE_DAY_IN_MS) + }), + (date) => { + const localDate = Date.fromStandardDate(date) + + const jsonString = JSON.stringify(localDate) + const dateIsoString = JSON.parse(jsonString) + const parsedDate = temporalUtil.newDate(dateIsoString) + + const adjustedDateTime = temporalUtil.newDate(date) + adjustedDateTime.setHours(0, offset(parsedDate)) + + expect(parsedDate.getFullYear()).toEqual(adjustedDateTime.getFullYear()) + expect(parsedDate.getMonth()).toEqual(adjustedDateTime.getMonth()) + expect(parsedDate.getDate()).toEqual(adjustedDateTime.getDate()) + expect(parsedDate.getHours()).toEqual(adjustedDateTime.getHours()) + expect(parsedDate.getMinutes()).toEqual(adjustedDateTime.getMinutes()) + } + ) + ) + }) + }) }) describe('LocalDateTime', () => { @@ -90,6 +120,23 @@ describe('LocalDateTime', () => { ) }) }) + + describe('JSON.stringify()', () => { + it('should serialize a valid ISO date', () => { + fc.assert( + fc.property(fc.date().filter(dt => dt.getUTCSeconds() === dt.getSeconds()), (date) => { + const localDatetime = LocalDateTime.fromStandardDate(date) + + const jsonString = JSON.stringify(localDatetime) + const dateIsoString = JSON.parse(jsonString) + + const parsedDate = temporalUtil.newDate(dateIsoString) + + expect(parsedDate).toEqual(date) + }) + ) + }) + }) }) describe('DateTime', () => { @@ -164,6 +211,84 @@ describe('DateTime', () => { ) }) }) + + describe('JSON.stringify()', () => { + describe('with zone offset', () => { + it('should serialize a valid ISO date', () => { + fc.assert( + fc.property(fc.date().filter(dt => dt.getUTCSeconds() === dt.getSeconds()), (date) => { + const datetime = DateTime.fromStandardDate(date) + + const jsonString = JSON.stringify(datetime) + const dateIsoString = JSON.parse(jsonString) + + const parsedDate = temporalUtil.newDate(dateIsoString) + + expect(parsedDate).toEqual(date) + }) + ) + }) + + describe('and with zone id', () => { + it('should serialize a valid ISO date', () => { + fc.assert( + fc.property( + fc.date({ + max: temporalUtil.newDate(MAX_UTC_IN_MS - 2 * ONE_DAY_IN_MS), + min: temporalUtil.newDate(MIN_UTC_IN_MS + 2 * ONE_DAY_IN_MS) + }) + .filter(dt => dt.getUTCSeconds() === dt.getSeconds()), + fc.integer({ + min: -1 * ONE_MINUTE_TO_ONE_DAY_IN_MINUTES, + max: ONE_MINUTE_TO_ONE_DAY_IN_MINUTES + }) + .map(offset => offset * 60), + (date, timeZoneOffsetInSeconds) => { + const expectedDate = adjustToTimezone(date, timeZoneOffsetInSeconds) + const datetime = new DateTime( + date.getFullYear(), + date.getMonth() + 1, + date.getDate(), + date.getHours(), + date.getMinutes(), + date.getSeconds(), + temporalUtil.totalNanoseconds(date), + timeZoneOffsetInSeconds, + 'Europe/Berlin' // < Doesn't matter for the test scenario + ) + + const jsonString = JSON.stringify(datetime) + const dateIsoString = JSON.parse(jsonString) + const parsedDate = temporalUtil.newDate(dateIsoString) + + expect(parsedDate).toEqual(expectedDate) + } + ) + ) + }) + }) + }) + + describe('without zone offset', () => { + it('should throw an error', () => { + const date = temporalUtil.newDate(0) + const datetime = new DateTime( + date.getFullYear(), + date.getMonth() + 1, + date.getDate(), + date.getHours(), + date.getMinutes(), + date.getSeconds(), + temporalUtil.totalNanoseconds(date), + undefined, + 'Europe/Berlin' // < Doesn't matter for the test scenario + ) + + expect(() => JSON.stringify(datetime)) + .toThrow(new Error('Requires DateTime created with time zone offset')) + }) + }) + }) }) /** @@ -177,3 +302,14 @@ describe('DateTime', () => { function offset (date: StandardDate): number { return date.getTimezoneOffset() * -1 } + +/** + * + * @param date + * @param offsetInSeconds + * @return The adjusted date + */ +function adjustToTimezone (date: StandardDate, offsetInSeconds: number): StandardDate { + const epoch = date.getTime() + return temporalUtil.newDate(epoch - offsetInSeconds * 1000 + offset(date) * 60_000) +} diff --git a/packages/neo4j-driver/test/temporal-types.test.js b/packages/neo4j-driver/test/temporal-types.test.js index 1c5309182..ee7b258fd 100644 --- a/packages/neo4j-driver/test/temporal-types.test.js +++ b/packages/neo4j-driver/test/temporal-types.test.js @@ -550,7 +550,7 @@ describe('#integration temporal-types', () => { ) }, 60000) - it('should send and receive array of DateTime with zone id', async () => { + xit('should send and receive array of DateTime with zone id', async () => { if (neo4jDoesNotSupportTemporalTypes()) { return }