From 330a6560b3c6c2d4861096fb5dff719d2190a46a Mon Sep 17 00:00:00 2001 From: lutovich Date: Mon, 7 May 2018 18:02:57 +0200 Subject: [PATCH] Handle nanosecond normalization in duration Database normalizes nanoseconds to be in range from 0 to 999999999. `Duration` class previously did not calculations with given seconds and nanoseconds and could thus store non-normalized values. It also did not re-calculate values for the `toString()` function and returned incorrect value. This commit makes `Duration` class normalize given seconds and nanoseconds during construction. It also changes the `toString()` function to correctly format the normalized values. --- src/v1/internal/temporal-util.js | 36 +++++++++++++++---- src/v1/temporal-types.js | 18 +++++----- test/internal/temporal-util.test.js | 54 ++++++++++++++++++++++++++++- test/v1/temporal-types.test.js | 49 ++++++++++++++++++++++++-- 4 files changed, 139 insertions(+), 18 deletions(-) diff --git a/src/v1/internal/temporal-util.js b/src/v1/internal/temporal-util.js index 8c174cc67..e25551aec 100644 --- a/src/v1/internal/temporal-util.js +++ b/src/v1/internal/temporal-util.js @@ -40,6 +40,14 @@ const DAYS_0000_TO_1970 = 719528; const DAYS_PER_400_YEAR_CYCLE = 146097; const SECONDS_PER_DAY = 86400; +export function normalizeSecondsForDuration(seconds, nanoseconds) { + return int(seconds).add(floorDiv(nanoseconds, NANOS_PER_SECOND)); +} + +export function normalizeNanosecondsForDuration(nanoseconds) { + return floorMod(nanoseconds, NANOS_PER_SECOND); +} + /** * Converts given local time into a single integer representing this same time in nanoseconds of the day. * @param {Integer|number|string} hour the hour of the local time to convert. @@ -328,14 +336,30 @@ function formatSecondsAndNanosecondsForDuration(seconds, nanoseconds) { seconds = int(seconds); nanoseconds = int(nanoseconds); - const signString = seconds.isNegative() || nanoseconds.isNegative() ? '-' : ''; - seconds = seconds.isNegative() ? seconds.negate() : seconds; - nanoseconds = nanoseconds.isNegative() ? nanoseconds.negate() : nanoseconds; + let secondsString; + let nanosecondsString; + + const secondsNegative = seconds.isNegative(); + const nanosecondsGreaterThanZero = nanoseconds.greaterThan(0); + if (secondsNegative && nanosecondsGreaterThanZero) { + if (seconds.equals(-1)) { + secondsString = '-0'; + } else { + secondsString = seconds.add(1).toString(); + } + } else { + secondsString = seconds.toString(); + } - const secondsString = formatNumber(seconds); - const nanosecondsString = formatNanosecond(nanoseconds); + if (nanosecondsGreaterThanZero) { + if (secondsNegative) { + nanosecondsString = formatNanosecond(nanoseconds.negate().add(2 * NANOS_PER_SECOND).modulo(NANOS_PER_SECOND)); + } else { + nanosecondsString = formatNanosecond(nanoseconds.add(NANOS_PER_SECOND).modulo(NANOS_PER_SECOND)); + } + } - return signString + secondsString + nanosecondsString; + return nanosecondsString ? secondsString + nanosecondsString : secondsString; } /** diff --git a/src/v1/temporal-types.js b/src/v1/temporal-types.js index 8f6358b63..9d2137bed 100644 --- a/src/v1/temporal-types.js +++ b/src/v1/temporal-types.js @@ -16,7 +16,7 @@ * limitations under the License. */ -import {dateToIsoString, durationToIsoString, timeToIsoString, timeZoneOffsetToIsoString} from './internal/temporal-util'; +import * as util from './internal/temporal-util'; import {newError} from './error'; const IDENTIFIER_PROPERTY_ATTRIBUTES = { @@ -48,13 +48,13 @@ export class Duration { constructor(months, days, seconds, nanoseconds) { this.months = months; this.days = days; - this.seconds = seconds; - this.nanoseconds = nanoseconds; + this.seconds = util.normalizeSecondsForDuration(seconds, nanoseconds); + this.nanoseconds = util.normalizeNanosecondsForDuration(nanoseconds); Object.freeze(this); } toString() { - return durationToIsoString(this.months, this.days, this.seconds, this.nanoseconds); + return util.durationToIsoString(this.months, this.days, this.seconds, this.nanoseconds); } } @@ -91,7 +91,7 @@ export class LocalTime { } toString() { - return timeToIsoString(this.hour, this.minute, this.second, this.nanosecond); + return util.timeToIsoString(this.hour, this.minute, this.second, this.nanosecond); } } @@ -130,7 +130,7 @@ export class Time { } toString() { - return timeToIsoString(this.hour, this.minute, this.second, this.nanosecond) + timeZoneOffsetToIsoString(this.timeZoneOffsetSeconds); + return util.timeToIsoString(this.hour, this.minute, this.second, this.nanosecond) + util.timeZoneOffsetToIsoString(this.timeZoneOffsetSeconds); } } @@ -165,7 +165,7 @@ export class Date { } toString() { - return dateToIsoString(this.year, this.month, this.day); + return util.dateToIsoString(this.year, this.month, this.day); } } @@ -259,7 +259,7 @@ export class DateTime { toString() { const localDateTimeStr = localDateTimeToString(this.year, this.month, this.day, this.hour, this.minute, this.second, this.nanosecond); - const timeZoneStr = this.timeZoneId ? `[${this.timeZoneId}]` : timeZoneOffsetToIsoString(this.timeZoneOffsetSeconds); + const timeZoneStr = this.timeZoneId ? `[${this.timeZoneId}]` : util.timeZoneOffsetToIsoString(this.timeZoneOffsetSeconds); return localDateTimeStr + timeZoneStr; } } @@ -280,7 +280,7 @@ function hasIdentifierProperty(obj, property) { } function localDateTimeToString(year, month, day, hour, minute, second, nanosecond) { - return dateToIsoString(year, month, day) + 'T' + timeToIsoString(hour, minute, second, nanosecond); + return util.dateToIsoString(year, month, day) + 'T' + util.timeToIsoString(hour, minute, second, nanosecond); } function verifyTimeZoneArguments(timeZoneOffsetSeconds, timeZoneId) { diff --git a/test/internal/temporal-util.test.js b/test/internal/temporal-util.test.js index 10222a967..d7256d5b3 100644 --- a/test/internal/temporal-util.test.js +++ b/test/internal/temporal-util.test.js @@ -22,6 +22,58 @@ import {types} from '../../src/v1'; describe('temporal-util', () => { + it('should normalize seconds for duration', () => { + expect(util.normalizeSecondsForDuration(1, 0)).toEqual(int(1)); + expect(util.normalizeSecondsForDuration(3, 0)).toEqual(int(3)); + expect(util.normalizeSecondsForDuration(424242, 0)).toEqual(int(424242)); + + expect(util.normalizeSecondsForDuration(-1, 0)).toEqual(int(-1)); + expect(util.normalizeSecondsForDuration(-9, 0)).toEqual(int(-9)); + expect(util.normalizeSecondsForDuration(-42, 0)).toEqual(int(-42)); + + expect(util.normalizeSecondsForDuration(1, 19)).toEqual(int(1)); + expect(util.normalizeSecondsForDuration(42, 42)).toEqual(int(42)); + expect(util.normalizeSecondsForDuration(12345, 6789)).toEqual(int(12345)); + + expect(util.normalizeSecondsForDuration(-1, 42)).toEqual(int(-1)); + expect(util.normalizeSecondsForDuration(-42, 4242)).toEqual(int(-42)); + expect(util.normalizeSecondsForDuration(-123, 999)).toEqual(int(-123)); + + expect(util.normalizeSecondsForDuration(1, 1000000000)).toEqual(int(2)); + expect(util.normalizeSecondsForDuration(40, 2000000001)).toEqual(int(42)); + expect(util.normalizeSecondsForDuration(583, 7999999999)).toEqual(int(590)); + + expect(util.normalizeSecondsForDuration(1, -1000000000)).toEqual(int(0)); + expect(util.normalizeSecondsForDuration(1, -5000000000)).toEqual(int(-4)); + expect(util.normalizeSecondsForDuration(85, -42000000123)).toEqual(int(42)); + + expect(util.normalizeSecondsForDuration(-19, -1000000000)).toEqual(int(-20)); + expect(util.normalizeSecondsForDuration(-19, -11123456789)).toEqual(int(-31)); + expect(util.normalizeSecondsForDuration(-42, -2000000001)).toEqual(int(-45)); + }); + + it('should normalize nanoseconds for duration', () => { + expect(util.normalizeNanosecondsForDuration(0)).toEqual(int(0)); + + expect(util.normalizeNanosecondsForDuration(1)).toEqual(int(1)); + expect(util.normalizeNanosecondsForDuration(42)).toEqual(int(42)); + expect(util.normalizeNanosecondsForDuration(123456789)).toEqual(int(123456789)); + expect(util.normalizeNanosecondsForDuration(999999999)).toEqual(int(999999999)); + + expect(util.normalizeNanosecondsForDuration(1000000000)).toEqual(int(0)); + expect(util.normalizeNanosecondsForDuration(1000000001)).toEqual(int(1)); + expect(util.normalizeNanosecondsForDuration(1000000042)).toEqual(int(42)); + expect(util.normalizeNanosecondsForDuration(1123456789)).toEqual(int(123456789)); + expect(util.normalizeNanosecondsForDuration(42999999999)).toEqual(int(999999999)); + + expect(util.normalizeNanosecondsForDuration(-1)).toEqual(int(999999999)); + expect(util.normalizeNanosecondsForDuration(-3)).toEqual(int(999999997)); + expect(util.normalizeNanosecondsForDuration(-100)).toEqual(int(999999900)); + expect(util.normalizeNanosecondsForDuration(-999999999)).toEqual(int(1)); + expect(util.normalizeNanosecondsForDuration(-1999999999)).toEqual(int(1)); + expect(util.normalizeNanosecondsForDuration(-1123456789)).toEqual(int(876543211)); + }); + it('should convert date to ISO string', () => { expect(util.dateToIsoString(90, 2, 5)).toEqual('0090-02-05'); expect(util.dateToIsoString(int(1), 1, int(1))).toEqual('0001-01-01'); @@ -65,7 +117,7 @@ describe('temporal-util', () => { expect(util.durationToIsoString(0, 0, 0, 123)).toEqual('P0M0DT0.000000123S'); expect(util.durationToIsoString(11, 99, 100, 99901)).toEqual('P11M99DT100.000099901S'); expect(util.durationToIsoString(int(3), int(9191), int(17), int(123456789))).toEqual('P3M9191DT17.123456789S'); - expect(util.durationToIsoString(-5, 2, -13, 123)).toEqual('P-5M2DT-13.000000123S'); + expect(util.durationToIsoString(-5, 2, -13, 123)).toEqual('P-5M2DT-12.999999877S'); }); it('should convert epoch day to cypher date', () => { diff --git a/test/v1/temporal-types.test.js b/test/v1/temporal-types.test.js index f5a4eb2a7..491420b4a 100644 --- a/test/v1/temporal-types.test.js +++ b/test/v1/temporal-types.test.js @@ -530,6 +530,10 @@ describe('temporal-types', () => { } testDurationToString([ + {duration: duration(0, 0, 0, 0), expectedString: 'P0M0DT0S'}, + + {duration: duration(0, 0, 42, 0), expectedString: 'P0M0DT42S'}, + {duration: duration(0, 0, -42, 0), expectedString: 'P0M0DT-42S'}, {duration: duration(0, 0, 1, 0), expectedString: 'P0M0DT1S'}, {duration: duration(0, 0, -1, 0), expectedString: 'P0M0DT-1S'}, @@ -547,10 +551,51 @@ describe('temporal-types', () => { {duration: duration(0, 0, 1, -999999999), expectedString: 'P0M0DT0.000000001S'}, {duration: duration(0, 0, -1, 999999999), expectedString: 'P0M0DT-0.000000001S'}, - {duration: duration(0, 0, -78036, -143000000), expectedString: 'P0M0DT-78036.143000000S'} + {duration: duration(0, 0, 28, 9), expectedString: 'P0M0DT28.000000009S'}, + {duration: duration(0, 0, -28, 9), expectedString: 'P0M0DT-27.999999991S'}, + {duration: duration(0, 0, 28, -9), expectedString: 'P0M0DT27.999999991S'}, + {duration: duration(0, 0, -28, -9), expectedString: 'P0M0DT-28.000000009S'}, + + {duration: duration(0, 0, -78036, -143000000), expectedString: 'P0M0DT-78036.143000000S'}, + + {duration: duration(0, 0, 0, 1000000000), expectedString: 'P0M0DT1S'}, + {duration: duration(0, 0, 0, -1000000000), expectedString: 'P0M0DT-1S'}, + {duration: duration(0, 0, 0, 1000000007), expectedString: 'P0M0DT1.000000007S'}, + {duration: duration(0, 0, 0, -1000000007), expectedString: 'P0M0DT-1.000000007S'}, + + {duration: duration(0, 0, 40, 2123456789), expectedString: 'P0M0DT42.123456789S'}, + {duration: duration(0, 0, -40, 2123456789), expectedString: 'P0M0DT-37.876543211S'}, + {duration: duration(0, 0, 40, -2123456789), expectedString: 'P0M0DT37.876543211S'}, + {duration: duration(0, 0, -40, -2123456789), expectedString: 'P0M0DT-42.123456789S'} ], done); }); + it('should normalize created duration', () => { + const duration1 = duration(0, 0, 1, 1000000000); + expect(duration1.seconds).toEqual(neo4j.int(2)); + expect(duration1.nanoseconds).toEqual(neo4j.int(0)); + + const duration2 = duration(0, 0, 42, 1000000001); + expect(duration2.seconds).toEqual(neo4j.int(43)); + expect(duration2.nanoseconds).toEqual(neo4j.int(1)); + + const duration3 = duration(0, 0, 42, 42999111222); + expect(duration3.seconds).toEqual(neo4j.int(84)); + expect(duration3.nanoseconds).toEqual(neo4j.int(999111222)); + + const duration4 = duration(0, 0, 1, -1000000000); + expect(duration4.seconds).toEqual(neo4j.int(0)); + expect(duration4.nanoseconds).toEqual(neo4j.int(0)); + + const duration5 = duration(0, 0, 1, -1000000001); + expect(duration5.seconds).toEqual(neo4j.int(-1)); + expect(duration5.nanoseconds).toEqual(neo4j.int(999999999)); + + const duration6 = duration(0, 0, 40, -12123456999); + expect(duration6.seconds).toEqual(neo4j.int(27)); + expect(duration6.nanoseconds).toEqual(neo4j.int(876543001)); + }); + function testSendAndReceiveRandomTemporalValues(valueGenerator, done) { const asyncFunction = (index, callback) => { const next = () => callback(); @@ -635,7 +680,7 @@ describe('temporal-types', () => { sign * _.random(0, Number.MAX_SAFE_INTEGER), sign * _.random(0, Number.MAX_SAFE_INTEGER), sign * _.random(0, Number.MAX_SAFE_INTEGER), - sign * _.random(0, MAX_NANO_OF_SECOND), + _.random(0, MAX_NANO_OF_SECOND), ); }