Skip to content

Commit 45f2b56

Browse files
committed
Fix conversion from standard Date to Time & DateTime
Time zone offset was not converted correctly. Offset exposed by the standard JavaScript `Date` is the difference, in minutes, from local time to UTC. So positive offset means local time is behind UTC and negative means it's ahead. This is different from Neo4j temporal types that support time zone offsets - `Time` and `DateTime`. They define offset as the difference, in seconds, from UTC to local time. Previous code converted offset in minutes to offset in seconds but did not change the sign of the value.
1 parent 0d259a1 commit 45f2b56

File tree

5 files changed

+82
-16
lines changed

5 files changed

+82
-16
lines changed

src/v1/internal/temporal-util.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -311,11 +311,18 @@ export function totalNanoseconds(standardDate, nanoseconds) {
311311

312312
/**
313313
* Get the time zone offset in seconds from the given standard JavaScript date.
314+
*
315+
* <b>Implementation note:</b>
316+
* Time zone offset returned by the standard JavaScript date is the difference, in minutes, from local time to UTC.
317+
* So positive value means offset is behind UTC and negative value means it is ahead.
318+
* For Neo4j temporal types, like `Time` or `DateTime` offset is in seconds and represents difference from UTC to local time.
319+
* This is different from standard JavaScript dates and that's why implementation negates the returned value.
320+
*
314321
* @param {global.Date} standardDate the standard JavaScript date.
315322
* @return {number} the time zone offset in seconds.
316323
*/
317324
export function timeZoneOffsetInSeconds(standardDate) {
318-
return standardDate.getTimezoneOffset() * SECONDS_PER_MINUTE;
325+
return -1 * standardDate.getTimezoneOffset() * SECONDS_PER_MINUTE;
319326
}
320327

321328
/**

src/v1/temporal-types.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,8 @@ export class Time {
139139
* @param {Integer|number} minute the minute for the new local time.
140140
* @param {Integer|number} second the second for the new local time.
141141
* @param {Integer|number} nanosecond the nanosecond for the new local time.
142-
* @param {Integer|number} timeZoneOffsetSeconds the time zone offset in seconds.
142+
* @param {Integer|number} timeZoneOffsetSeconds the time zone offset in seconds. Value represents the difference, in seconds, from UTC to local time.
143+
* This is different from standard JavaScript `Date.getTimezoneOffset()` which is the difference, in minutes, from local time to UTC.
143144
*/
144145
constructor(hour, minute, second, nanosecond, timeZoneOffsetSeconds) {
145146
this.hour = util.assertValidHour(hour);
@@ -312,7 +313,9 @@ export class DateTime {
312313
* @param {Integer|number} minute the minute for the new date-time.
313314
* @param {Integer|number} second the second for the new date-time.
314315
* @param {Integer|number} nanosecond the nanosecond for the new date-time.
315-
* @param {Integer|number|null} timeZoneOffsetSeconds the total time zone offset in seconds for the new date-time. Either this argument or `timeZoneId` should be defined.
316+
* @param {Integer|number} timeZoneOffsetSeconds the time zone offset in seconds. Either this argument or `timeZoneId` should be defined.
317+
* Value represents the difference, in seconds, from UTC to local time.
318+
* This is different from standard JavaScript `Date.getTimezoneOffset()` which is the difference, in minutes, from local time to UTC.
316319
* @param {string|null} timeZoneId the time zone id for the new date-time. Either this argument or `timeZoneOffsetSeconds` should be defined.
317320
*/
318321
constructor(year, month, day, hour, minute, second, nanosecond, timeZoneOffsetSeconds, timeZoneId) {

test/internal/temporal-util.test.js

+7-10
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import {int} from '../../src/v1/integer';
2121
import * as util from '../../src/v1/internal/temporal-util';
2222
import {types} from '../../src/v1';
23+
import testUtils from './test-utils';
2324

2425
describe('temporal-util', () => {
2526

@@ -189,10 +190,12 @@ describe('temporal-util', () => {
189190
});
190191

191192
it('should get timezone offset in seconds from standard date', () => {
192-
expect(util.timeZoneOffsetInSeconds(fakeStandardDateWithOffset(0))).toEqual(0);
193-
expect(util.timeZoneOffsetInSeconds(fakeStandardDateWithOffset(2))).toEqual(120);
194-
expect(util.timeZoneOffsetInSeconds(fakeStandardDateWithOffset(10))).toEqual(600);
195-
expect(util.timeZoneOffsetInSeconds(fakeStandardDateWithOffset(101))).toEqual(6060);
193+
expect(util.timeZoneOffsetInSeconds(testUtils.fakeStandardDateWithOffset(0))).toBe(0);
194+
expect(util.timeZoneOffsetInSeconds(testUtils.fakeStandardDateWithOffset(2))).toBe(-120);
195+
expect(util.timeZoneOffsetInSeconds(testUtils.fakeStandardDateWithOffset(10))).toBe(-600);
196+
expect(util.timeZoneOffsetInSeconds(testUtils.fakeStandardDateWithOffset(101))).toBe(-6060);
197+
expect(util.timeZoneOffsetInSeconds(testUtils.fakeStandardDateWithOffset(-180))).toBe(10800);
198+
expect(util.timeZoneOffsetInSeconds(testUtils.fakeStandardDateWithOffset(-600))).toBe(36000);
196199
});
197200

198201
it('should verify year', () => {
@@ -330,9 +333,3 @@ function localTime(hour, minute, second, nanosecond) {
330333
function localDateTime(year, month, day, hour, minute, second, nanosecond) {
331334
return new types.LocalDateTime(int(year), int(month), int(day), int(hour), int(minute), int(second), int(nanosecond));
332335
}
333-
334-
function fakeStandardDateWithOffset(offsetMinutes) {
335-
const date = new Date();
336-
date.getTimezoneOffset = () => offsetMinutes;
337-
return date;
338-
}

test/internal/test-utils.js

+7
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,14 @@ function isServer() {
2424
return !isClient();
2525
}
2626

27+
function fakeStandardDateWithOffset(offsetMinutes) {
28+
const date = new Date();
29+
date.getTimezoneOffset = () => offsetMinutes;
30+
return date;
31+
}
32+
2733
export default {
2834
isClient,
2935
isServer,
36+
fakeStandardDateWithOffset
3037
};

test/v1/temporal-types.test.js

+55-3
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@
1919

2020
import neo4j from '../../src';
2121
import sharedNeo4j from '../internal/shared-neo4j';
22-
import {totalNanoseconds} from '../../src/v1/internal/temporal-util';
22+
import {timeZoneOffsetInSeconds, totalNanoseconds} from '../../src/v1/internal/temporal-util';
2323
import {ServerVersion, VERSION_3_4_0} from '../../src/v1/internal/server-version';
2424
import timesSeries from 'async/timesSeries';
2525
import _ from 'lodash';
26+
import testUtils from '../internal/test-utils';
2627

2728
const RANDOM_VALUES_TO_TEST = 2000;
2829
const MIN_TEMPORAL_ARRAY_LENGTH = 20;
@@ -921,6 +922,50 @@ describe('temporal-types', () => {
921922
expect(() => dateTimeWithZoneOffset(1, 1, 1, 1, 1, 1, 1000000000, 0)).toThrow();
922923
});
923924

925+
it('should convert standard Date with offset to neo4j Time', () => {
926+
const standardDate1 = testUtils.fakeStandardDateWithOffset(0);
927+
const neo4jTime1 = neo4j.types.Time.fromStandardDate(standardDate1);
928+
verifyTimeZoneOffset(neo4jTime1, 0, 'Z');
929+
930+
const standardDate2 = testUtils.fakeStandardDateWithOffset(-600);
931+
const neo4jTime2 = neo4j.types.Time.fromStandardDate(standardDate2);
932+
verifyTimeZoneOffset(neo4jTime2, 600 * 60, '+10:00');
933+
934+
const standardDate3 = testUtils.fakeStandardDateWithOffset(480);
935+
const neo4jTime3 = neo4j.types.Time.fromStandardDate(standardDate3);
936+
verifyTimeZoneOffset(neo4jTime3, -1 * 480 * 60, '-08:00');
937+
938+
const standardDate4 = testUtils.fakeStandardDateWithOffset(-180);
939+
const neo4jTime4 = neo4j.types.Time.fromStandardDate(standardDate4);
940+
verifyTimeZoneOffset(neo4jTime4, 180 * 60, '+03:00');
941+
942+
const standardDate5 = testUtils.fakeStandardDateWithOffset(150);
943+
const neo4jTime5 = neo4j.types.Time.fromStandardDate(standardDate5);
944+
verifyTimeZoneOffset(neo4jTime5, -1 * 150 * 60, '-02:30');
945+
});
946+
947+
it('should convert standard Date with offset to neo4j DateTime', () => {
948+
const standardDate1 = testUtils.fakeStandardDateWithOffset(0);
949+
const neo4jDateTime1 = neo4j.types.DateTime.fromStandardDate(standardDate1);
950+
verifyTimeZoneOffset(neo4jDateTime1, 0, 'Z');
951+
952+
const standardDate2 = testUtils.fakeStandardDateWithOffset(-600);
953+
const neo4jDateTime2 = neo4j.types.DateTime.fromStandardDate(standardDate2);
954+
verifyTimeZoneOffset(neo4jDateTime2, 600 * 60, '+10:00');
955+
956+
const standardDate3 = testUtils.fakeStandardDateWithOffset(480);
957+
const neo4jDateTime3 = neo4j.types.DateTime.fromStandardDate(standardDate3);
958+
verifyTimeZoneOffset(neo4jDateTime3, -1 * 480 * 60, '-08:00');
959+
960+
const standardDate4 = testUtils.fakeStandardDateWithOffset(-180);
961+
const neo4jDateTime4 = neo4j.types.DateTime.fromStandardDate(standardDate4);
962+
verifyTimeZoneOffset(neo4jDateTime4, 180 * 60, '+03:00');
963+
964+
const standardDate5 = testUtils.fakeStandardDateWithOffset(150);
965+
const neo4jDateTime5 = neo4j.types.DateTime.fromStandardDate(standardDate5);
966+
verifyTimeZoneOffset(neo4jDateTime5, -1 * 150 * 60, '-02:30');
967+
});
968+
924969
function testSendAndReceiveRandomTemporalValues(valueGenerator, done) {
925970
const asyncFunction = (index, callback) => {
926971
const next = () => callback();
@@ -1117,7 +1162,7 @@ describe('temporal-types', () => {
11171162
function testStandardDateToTimeConversion(date, nanosecond) {
11181163
const converted = neo4j.types.Time.fromStandardDate(date, nanosecond);
11191164
const expected = new neo4j.types.Time(date.getHours(), date.getMinutes(), date.getSeconds(), totalNanoseconds(date, nanosecond),
1120-
date.getTimezoneOffset() * 60);
1165+
timeZoneOffsetInSeconds(date));
11211166
expect(converted).toEqual(expected);
11221167
}
11231168

@@ -1137,7 +1182,14 @@ describe('temporal-types', () => {
11371182
function testStandardDateToDateTimeConversion(date, nanosecond) {
11381183
const converted = neo4j.types.DateTime.fromStandardDate(date, nanosecond);
11391184
const expected = new neo4j.types.DateTime(date.getFullYear(), date.getMonth() + 1, date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds(),
1140-
totalNanoseconds(date, nanosecond), date.getTimezoneOffset() * 60);
1185+
totalNanoseconds(date, nanosecond), timeZoneOffsetInSeconds(date));
11411186
expect(converted).toEqual(expected);
11421187
}
1188+
1189+
function verifyTimeZoneOffset(temporal, expectedValue, expectedStringValue) {
1190+
expect(temporal.timeZoneOffsetSeconds).toBe(expectedValue);
1191+
const isoString = temporal.toString();
1192+
// assert ISO string ends with the expected suffix
1193+
expect(isoString.indexOf(expectedStringValue, isoString.length - expectedStringValue.length)).toBeGreaterThan(0);
1194+
}
11431195
});

0 commit comments

Comments
 (0)