diff --git a/src/v1/index.js b/src/v1/index.js index 8ddaf0181..8c4c53ace 100644 --- a/src/v1/index.js +++ b/src/v1/index.js @@ -29,7 +29,23 @@ import VERSION from '../version'; import {assertString, isEmptyObjectOrNull} from './internal/util'; import urlUtil from './internal/url-util'; import HttpDriver from './internal/http/http-driver'; -import {Point} from './spatial-types'; +import {isPoint, Point} from './spatial-types'; +import { + Date, + DateTimeWithZoneId, + DateTimeWithZoneOffset, + Duration, + isDate, + isDateTimeWithZoneId, + isDateTimeWithZoneOffset, + isDuration, + isLocalDateTime, + isLocalTime, + isTime, + LocalDateTime, + LocalTime, + Time +} from './temporal-types'; /** * @property {function(username: string, password: string, realm: ?string)} basic the function to create a @@ -196,7 +212,7 @@ function driver(url, authToken, config = {}) { } /** - * Object containing constructors for all graph types. + * Object containing constructors for all neo4j types. */ const types = { Node, @@ -207,7 +223,14 @@ const types = { Result, ResultSummary, Record, - Point + Point, + Date, + DateTimeWithZoneId, + DateTimeWithZoneOffset, + Duration, + LocalDateTime, + LocalTime, + Time }; /** @@ -248,7 +271,23 @@ const forExport = { auth, types, session, - error + error, + Point, + isPoint, + Date, + DateTimeWithZoneId, + DateTimeWithZoneOffset, + Duration, + LocalDateTime, + LocalTime, + Time, + isDuration, + isLocalTime, + isTime, + isDate, + isLocalDateTime, + isDateTimeWithZoneOffset, + isDateTimeWithZoneId }; export { @@ -260,6 +299,22 @@ export { auth, types, session, - error + error, + Point, + isPoint, + Date, + DateTimeWithZoneId, + DateTimeWithZoneOffset, + Duration, + LocalDateTime, + LocalTime, + Time, + isDuration, + isLocalTime, + isTime, + isDate, + isLocalDateTime, + isDateTimeWithZoneOffset, + isDateTimeWithZoneId }; export default forExport; diff --git a/src/v1/integer.js b/src/v1/integer.js index 7fe006c15..29f6468b7 100644 --- a/src/v1/integer.js +++ b/src/v1/integer.js @@ -238,7 +238,9 @@ class Integer { * @returns {boolean} * @expose */ - notEquals(other) { !this.equals(/* validates */ other); } + notEquals(other) { + return !this.equals(/* validates */ other); + } /** * Tests if this Integer's value is less than the specified's. diff --git a/src/v1/internal/packstream-v2.js b/src/v1/internal/packstream-v2.js index 05037a3d0..d50c0bdf5 100644 --- a/src/v1/internal/packstream-v2.js +++ b/src/v1/internal/packstream-v2.js @@ -19,7 +19,29 @@ import * as v1 from './packstream-v1'; import {isPoint, Point} from '../spatial-types'; +import { + Date, + DateTimeWithZoneId, + DateTimeWithZoneOffset, + Duration, + Time, + isDate, + isDateTimeWithZoneId, + isDateTimeWithZoneOffset, + isDuration, + isLocalDateTime, + isLocalTime, + isTime +} from '../temporal-types'; import {int} from '../integer'; +import { + dateToEpochDay, + localDateTimeToEpochSecond, + localTimeToNanoOfDay, + epochDayToDate, + epochSecondAndNanoToLocalDateTime, + nanoOfDayToLocalTime +} from '../internal/temporal-util'; const POINT_2D = 0x58; const POINT_2D_STRUCT_SIZE = 3; @@ -27,6 +49,27 @@ const POINT_2D_STRUCT_SIZE = 3; const POINT_3D = 0x59; const POINT_3D_STRUCT_SIZE = 4; +const DURATION = 0x45; +const DURATION_STRUCT_SIZE = 4; + +const LOCAL_TIME = 0x74; +const LOCAL_TIME_STRUCT_SIZE = 1; + +const TIME = 0x54; +const TIME_STRUCT_SIZE = 2; + +const DATE = 0x44; +const DATE_STRUCT_SIZE = 1; + +const LOCAL_DATE_TIME = 0x64; +const LOCAL_DATE_TIME_STRUCT_SIZE = 2; + +const DATE_TIME_WITH_ZONE_OFFSET = 0x46; +const DATE_TIME_WITH_ZONE_OFFSET_STRUCT_SIZE = 3; + +const DATE_TIME_WITH_ZONE_ID = 0x66; +const DATE_TIME_WITH_ZONE_ID_STRUCT_SIZE = 3; + export class Packer extends v1.Packer { /** @@ -44,6 +87,20 @@ export class Packer extends v1.Packer { packable(obj, onError) { if (isPoint(obj)) { return () => packPoint(obj, this, onError); + } else if (isDuration(obj)) { + return () => packDuration(obj, this, onError); + } else if (isLocalTime(obj)) { + return () => packLocalTime(obj, this, onError); + } else if (isTime(obj)) { + return () => packTime(obj, this, onError); + } else if (isDate(obj)) { + return () => packDate(obj, this, onError); + } else if (isLocalDateTime(obj)) { + return () => packLocalDateTime(obj, this, onError); + } else if (isDateTimeWithZoneOffset(obj)) { + return () => packDateTimeWithZoneOffset(obj, this, onError); + } else if (isDateTimeWithZoneId(obj)) { + return () => packDateTimeWithZoneId(obj, this, onError); } else { return super.packable(obj, onError); } @@ -66,6 +123,20 @@ export class Unpacker extends v1.Unpacker { return unpackPoint2D(this, structSize, buffer); } else if (signature == POINT_3D) { return unpackPoint3D(this, structSize, buffer); + } else if (signature == DURATION) { + return unpackDuration(this, structSize, buffer); + } else if (signature == LOCAL_TIME) { + return unpackLocalTime(this, structSize, buffer); + } else if (signature == TIME) { + return unpackTime(this, structSize, buffer); + } else if (signature == DATE) { + return unpackDate(this, structSize, buffer); + } else if (signature == LOCAL_DATE_TIME) { + return unpackLocalDateTime(this, structSize, buffer); + } else if (signature == DATE_TIME_WITH_ZONE_OFFSET) { + return unpackDateTimeWithZoneOffset(this, structSize, buffer); + } else if (signature == DATE_TIME_WITH_ZONE_ID) { + return unpackDateTimeWithZoneId(this, structSize, buffer); } else { return super._unpackUnknownStruct(signature, structSize, buffer); } @@ -121,3 +192,215 @@ function unpackPoint3D(unpacker, structSize, buffer) { unpacker.unpack(buffer) // z ); } + +function packDuration(value, packer, onError) { + const months = int(value.months); + const days = int(value.days); + const seconds = int(value.seconds); + const nanoseconds = int(value.nanoseconds); + + const packableStructFields = [ + packer.packable(months, onError), + packer.packable(days, onError), + packer.packable(seconds, onError), + packer.packable(nanoseconds, onError), + ]; + packer.packStruct(DURATION, packableStructFields, onError); +} + +function unpackDuration(unpacker, structSize, buffer) { + unpacker._verifyStructSize('Duration', DURATION_STRUCT_SIZE, structSize); + + const months = unpacker.unpack(buffer); + const days = unpacker.unpack(buffer); + const seconds = unpacker.unpack(buffer); + const nanoseconds = unpacker.unpack(buffer); + + return new Duration(months, days, seconds, nanoseconds); +} + +function packLocalTime(value, packer, onError) { + const nanoOfDay = localTimeToNanoOfDay(value); + + const packableStructFields = [ + packer.packable(nanoOfDay, onError) + ]; + packer.packStruct(LOCAL_TIME, packableStructFields, onError); +} + +function unpackLocalTime(unpacker, structSize, buffer) { + unpacker._verifyStructSize('LocalTime', LOCAL_TIME_STRUCT_SIZE, structSize); + + const nanoOfDay = unpacker.unpack(buffer); + return nanoOfDayToLocalTime(nanoOfDay); +} + +/** + * Pack given time. + * @param {Time} value the time value to pack. + * @param {Packer} packer the packer to use. + * @param {function} onError the error callback. + */ +function packTime(value, packer, onError) { + const nanoOfDay = localTimeToNanoOfDay(value.localTime); + const offsetSeconds = int(value.offsetSeconds); + + const packableStructFields = [ + packer.packable(nanoOfDay, onError), + packer.packable(offsetSeconds, onError) + ]; + packer.packStruct(TIME, packableStructFields, onError); +} + +/** + * Unpack time value using the given unpacker. + * @param {Unpacker} unpacker the unpacker to use. + * @param {number} structSize the retrieved struct size. + * @param {BaseBuffer} buffer the buffer to unpack from. + * @return {Time} the unpacked time value. + */ +function unpackTime(unpacker, structSize, buffer) { + unpacker._verifyStructSize('Time', TIME_STRUCT_SIZE, structSize); + + const nanoOfDay = unpacker.unpack(buffer); + const offsetSeconds = unpacker.unpack(buffer); + + const localTime = nanoOfDayToLocalTime(nanoOfDay); + return new Time(localTime, offsetSeconds); +} + +/** + * Pack given neo4j date. + * @param {Date} value the date value to pack. + * @param {Packer} packer the packer to use. + * @param {function} onError the error callback. + */ +function packDate(value, packer, onError) { + const epochDay = dateToEpochDay(value); + + const packableStructFields = [ + packer.packable(epochDay, onError) + ]; + packer.packStruct(DATE, packableStructFields, onError); +} + +/** + * Unpack neo4j date value using the given unpacker. + * @param {Unpacker} unpacker the unpacker to use. + * @param {number} structSize the retrieved struct size. + * @param {BaseBuffer} buffer the buffer to unpack from. + * @return {Date} the unpacked neo4j date value. + */ +function unpackDate(unpacker, structSize, buffer) { + unpacker._verifyStructSize('Date', DATE_STRUCT_SIZE, structSize); + + const epochDay = unpacker.unpack(buffer); + return epochDayToDate(epochDay); +} + +/** + * Pack given local date time. + * @param {LocalDateTime} value the local date time value to pack. + * @param {Packer} packer the packer to use. + * @param {function} onError the error callback. + */ +function packLocalDateTime(value, packer, onError) { + const epochSecond = localDateTimeToEpochSecond(value); + const nano = int(value.localTime.nanosecond); + + const packableStructFields = [ + packer.packable(epochSecond, onError), + packer.packable(nano, onError) + ]; + packer.packStruct(LOCAL_DATE_TIME, packableStructFields, onError); +} + +/** + * Unpack local date time value using the given unpacker. + * @param {Unpacker} unpacker the unpacker to use. + * @param {number} structSize the retrieved struct size. + * @param {BaseBuffer} buffer the buffer to unpack from. + * @return {LocalDateTime} the unpacked local date time value. + */ +function unpackLocalDateTime(unpacker, structSize, buffer) { + unpacker._verifyStructSize('LocalDateTime', LOCAL_DATE_TIME_STRUCT_SIZE, structSize); + + const epochSecond = unpacker.unpack(buffer); + const nano = unpacker.unpack(buffer); + + return epochSecondAndNanoToLocalDateTime(epochSecond, nano); +} + +/** + * Pack given date time with zone offset. + * @param {DateTimeWithZoneOffset} value the date time value to pack. + * @param {Packer} packer the packer to use. + * @param {function} onError the error callback. + */ +function packDateTimeWithZoneOffset(value, packer, onError) { + const epochSecond = localDateTimeToEpochSecond(value.localDateTime); + const nano = int(value.localDateTime.localTime.nanosecond); + const offsetSeconds = int(value.offsetSeconds); + + const packableStructFields = [ + packer.packable(epochSecond, onError), + packer.packable(nano, onError), + packer.packable(offsetSeconds, onError) + ]; + packer.packStruct(DATE_TIME_WITH_ZONE_OFFSET, packableStructFields, onError); +} + +/** + * Unpack date time with zone offset value using the given unpacker. + * @param {Unpacker} unpacker the unpacker to use. + * @param {number} structSize the retrieved struct size. + * @param {BaseBuffer} buffer the buffer to unpack from. + * @return {DateTimeWithZoneOffset} the unpacked date time with zone offset value. + */ +function unpackDateTimeWithZoneOffset(unpacker, structSize, buffer) { + unpacker._verifyStructSize('DateTimeWithZoneOffset', DATE_TIME_WITH_ZONE_OFFSET_STRUCT_SIZE, structSize); + + const epochSecond = unpacker.unpack(buffer); + const nano = unpacker.unpack(buffer); + const offsetSeconds = unpacker.unpack(buffer); + + const localDateTime = epochSecondAndNanoToLocalDateTime(epochSecond, nano); + return new DateTimeWithZoneOffset(localDateTime, offsetSeconds); +} + +/** + * Pack given date time with zone id. + * @param {DateTimeWithZoneId} value the date time value to pack. + * @param {Packer} packer the packer to use. + * @param {function} onError the error callback. + */ +function packDateTimeWithZoneId(value, packer, onError) { + const epochSecond = localDateTimeToEpochSecond(value.localDateTime); + const nano = int(value.localDateTime.localTime.nanosecond); + const zoneId = value.zoneId; + + const packableStructFields = [ + packer.packable(epochSecond, onError), + packer.packable(nano, onError), + packer.packable(zoneId, onError) + ]; + packer.packStruct(DATE_TIME_WITH_ZONE_ID, packableStructFields, onError); +} + +/** + * Unpack date time with zone id value using the given unpacker. + * @param {Unpacker} unpacker the unpacker to use. + * @param {number} structSize the retrieved struct size. + * @param {BaseBuffer} buffer the buffer to unpack from. + * @return {DateTimeWithZoneId} the unpacked date time with zone id value. + */ +function unpackDateTimeWithZoneId(unpacker, structSize, buffer) { + unpacker._verifyStructSize('DateTimeWithZoneId', DATE_TIME_WITH_ZONE_ID_STRUCT_SIZE, structSize); + + const epochSecond = unpacker.unpack(buffer); + const nano = unpacker.unpack(buffer); + const zoneId = unpacker.unpack(buffer); + + const localDateTime = epochSecondAndNanoToLocalDateTime(epochSecond, nano); + return new DateTimeWithZoneId(localDateTime, zoneId); +} diff --git a/src/v1/internal/temporal-util.js b/src/v1/internal/temporal-util.js new file mode 100644 index 000000000..d3fd63b98 --- /dev/null +++ b/src/v1/internal/temporal-util.js @@ -0,0 +1,323 @@ +/** + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {int} from '../integer'; +import {Date, LocalDateTime, LocalTime} from '../temporal-types'; + +/* + Code in this util should be compatible with code in the database that uses JSR-310 java.time APIs. + + It is based on a library called ThreeTen (https://github.com/ThreeTen/threetenbp) which was derived + from JSR-310 reference implementation previously hosted on GitHub. Code uses `Integer` type everywhere + to correctly handle large integer values that are greater than Number.MAX_SAFE_INTEGER. + + Please consult either ThreeTen or js-joda (https://github.com/js-joda/js-joda) when working with the + conversion functions. + */ + +const MINUTES_PER_HOUR = 60; +const SECONDS_PER_MINUTE = 60; +const SECONDS_PER_HOUR = SECONDS_PER_MINUTE * MINUTES_PER_HOUR; +const NANOS_PER_SECOND = 1000000000; +const NANOS_PER_MINUTE = NANOS_PER_SECOND * SECONDS_PER_MINUTE; +const NANOS_PER_HOUR = NANOS_PER_MINUTE * MINUTES_PER_HOUR; +const DAYS_0000_TO_1970 = 719528; +const DAYS_PER_400_YEAR_CYCLE = 146097; +const SECONDS_PER_DAY = 86400; + +/** + * Converts given local time into a single integer representing this same time in nanoseconds of the day. + * @param {LocalTime} localTime the time to convert. + * @return {Integer} nanoseconds representing the given local time. + */ +export function localTimeToNanoOfDay(localTime) { + const hour = int(localTime.hour); + const minute = int(localTime.minute); + const second = int(localTime.second); + const nanosecond = int(localTime.nanosecond); + + let totalNanos = hour.multiply(NANOS_PER_HOUR); + totalNanos = totalNanos.add(minute.multiply(NANOS_PER_MINUTE)); + totalNanos = totalNanos.add(second.multiply(NANOS_PER_SECOND)); + return totalNanos.add(nanosecond); +} + +/** + * Converts nanoseconds of the day into local time. + * @param {Integer|number|string} nanoOfDay the nanoseconds of the day to convert. + * @return {LocalTime} the local time representing given nanoseconds of the day. + */ +export function nanoOfDayToLocalTime(nanoOfDay) { + nanoOfDay = int(nanoOfDay); + + const hour = nanoOfDay.div(NANOS_PER_HOUR); + nanoOfDay = nanoOfDay.subtract(hour.multiply(NANOS_PER_HOUR)); + + const minute = nanoOfDay.div(NANOS_PER_MINUTE); + nanoOfDay = nanoOfDay.subtract(minute.multiply(NANOS_PER_MINUTE)); + + const second = nanoOfDay.div(NANOS_PER_SECOND); + const nanosecond = nanoOfDay.subtract(second.multiply(NANOS_PER_SECOND)); + + return new LocalTime(hour, minute, second, nanosecond); +} + +/** + * Converts given local date time into a single integer representing this same time in epoch seconds UTC. + * @param {LocalDateTime} localDateTime the local date time value to convert. + * @return {Integer} epoch second in UTC representing the given local date time. + */ +export function localDateTimeToEpochSecond(localDateTime) { + const localDate = localDateTime.localDate; + const localTime = localDateTime.localTime; + + const epochDay = dateToEpochDay(localDate); + const localTimeSeconds = localTimeToSecondOfDay(localTime); + return epochDay.multiply(SECONDS_PER_DAY).add(localTimeSeconds); +} + +/** + * Converts given epoch second and nanosecond adjustment into a local date time object. + * @param {Integer|number|string} epochSecond the epoch second to use. + * @param {Integer|number|string} nano the nanosecond to use. + * @return {LocalDateTime} the local date time representing given epoch second and nano. + */ +export function epochSecondAndNanoToLocalDateTime(epochSecond, nano) { + const epochDay = floorDiv(epochSecond, SECONDS_PER_DAY); + const secondsOfDay = floorMod(epochSecond, SECONDS_PER_DAY); + const nanoOfDay = secondsOfDay.multiply(NANOS_PER_SECOND).add(nano); + + const localDate = epochDayToDate(epochDay); + const localTime = nanoOfDayToLocalTime(nanoOfDay); + return new LocalDateTime(localDate, localTime); +} + +/** + * Converts given local date into a single integer representing it's epoch day. + * @param {Date} date the date to convert. + * @return {Integer} epoch day representing the given date. + */ +export function dateToEpochDay(date) { + const year = int(date.year); + const month = int(date.month); + const day = int(date.day); + + let epochDay = year.multiply(365); + + if (year.greaterThanOrEqual(0)) { + epochDay = epochDay.add(year.add(3).div(4).subtract(year.add(99).div(100)).add(year.add(399).div(400))); + } else { + epochDay = epochDay.subtract(year.div(-4).subtract(year.div(-100)).add(year.div(-400))); + } + + epochDay = epochDay.add(month.multiply(367).subtract(362).div(12)); + epochDay = epochDay.add(day.subtract(1)); + if (month.greaterThan(2)) { + epochDay = epochDay.subtract(1); + if (!isLeapYear(year)) { + epochDay = epochDay.subtract(1); + } + } + return epochDay.subtract(DAYS_0000_TO_1970); +} + +/** + * Converts given epoch day to a local date. + * @param {Integer|number|string} epochDay the epoch day to convert. + * @return {Date} the date representing the epoch day in years, months and days. + */ +export function epochDayToDate(epochDay) { + epochDay = int(epochDay); + + let zeroDay = epochDay.add(DAYS_0000_TO_1970).subtract(60); + let adjust = int(0); + if (zeroDay.lessThan(0)) { + const adjustCycles = zeroDay.add(1).div(DAYS_PER_400_YEAR_CYCLE).subtract(1); + adjust = adjustCycles.multiply(400); + zeroDay = zeroDay.add(adjustCycles.multiply(-DAYS_PER_400_YEAR_CYCLE)); + } + let year = zeroDay.multiply(400).add(591).div(DAYS_PER_400_YEAR_CYCLE); + let dayOfYearEst = zeroDay.subtract(year.multiply(365).add(year.div(4)).subtract(year.div(100)).add(year.div(400))); + if (dayOfYearEst.lessThan(0)) { + year = year.subtract(1); + dayOfYearEst = zeroDay.subtract(year.multiply(365).add(year.div(4)).subtract(year.div(100)).add(year.div(400))); + } + year = year.add(adjust); + let marchDayOfYear = dayOfYearEst; + + const marchMonth = marchDayOfYear.multiply(5).add(2).div(153); + const month = marchMonth.add(2).modulo(12).add(1); + const day = marchDayOfYear.subtract(marchMonth.multiply(306).add(5).div(10)).add(1); + year = year.add(marchMonth.div(10)); + + return new Date(year, month, day); +} + +/** + * Format given duration to an ISO 8601 string. + * @param {Integer|number} months the number of months. + * @param {Integer|number} days the number of days. + * @param {Integer|number} seconds the number of seconds. + * @param {Integer|number} nanoseconds the number of nanoseconds. + * @return {string} ISO string that represents given duration. + */ +export function durationToIsoString(months, days, seconds, nanoseconds) { + const monthsString = formatNumber(months); + const daysString = formatNumber(days); + const secondsString = formatNumber(seconds); + const nanosecondsString = formatNumber(nanoseconds, 9); + return `P${monthsString}M${daysString}DT${secondsString}.${nanosecondsString}S`; +} + +/** + * Formats given time to an ISO 8601 string. + * @param {Integer|number} hour the hour value. + * @param {Integer|number} minute the minute value. + * @param {Integer|number} second the second value. + * @param {Integer|number} nanosecond the nanosecond value. + * @return {string} ISO string that represents given time. + */ +export function timeToIsoString(hour, minute, second, nanosecond) { + const hourString = formatNumber(hour, 2); + const minuteString = formatNumber(minute, 2); + const secondString = formatNumber(second, 2); + const nanosecondString = formatNumber(nanosecond, 9); + return `${hourString}:${minuteString}:${secondString}.${nanosecondString}`; +} + +/** + * Formats given time zone offset in seconds to string representation like '±HH:MM', '±HH:MM:SS' or 'Z' for UTC. + * @param {Integer|number} offsetSeconds the offset in seconds. + * @return {string} ISO string that represents given offset. + */ +export function timeZoneOffsetToIsoString(offsetSeconds) { + offsetSeconds = int(offsetSeconds); + if (offsetSeconds.equals(0)) { + return 'Z'; + } + + const isNegative = offsetSeconds.isNegative(); + if (isNegative) { + offsetSeconds = offsetSeconds.multiply(-1); + } + const signPrefix = isNegative ? '-' : '+'; + + const hours = formatNumber(offsetSeconds.div(SECONDS_PER_HOUR), 2); + const minutes = formatNumber(offsetSeconds.div(SECONDS_PER_MINUTE).modulo(MINUTES_PER_HOUR), 2); + let secondsValue = offsetSeconds.modulo(SECONDS_PER_MINUTE); + const seconds = secondsValue.equals(0) ? null : formatNumber(secondsValue, 2); + + return seconds ? `${signPrefix}${hours}:${minutes}:${seconds}` : `${signPrefix}${hours}:${minutes}`; +} + +/** + * Formats given date to an ISO 8601 string. + * @param {Integer|number} year the date year. + * @param {Integer|number} month the date month. + * @param {Integer|number} day the date day. + * @return {string} ISO string that represents given date. + */ +export function dateToIsoString(year, month, day) { + year = int(year); + const isNegative = year.isNegative(); + if (isNegative) { + year = year.multiply(-1); + } + let yearString = year.toString().padStart(4, '0'); + if (isNegative) { + yearString = '-' + yearString; + } + + const monthString = formatNumber(month, 2); + const dayString = formatNumber(day, 2); + return `${yearString}-${monthString}-${dayString}`; +} + +/** + * Converts given local time into a single integer representing this same time in seconds of the day. Nanoseconds are skipped. + * @param {LocalTime} localTime the time to convert. + * @return {Integer} seconds representing the given local time. + */ +function localTimeToSecondOfDay(localTime) { + const hour = int(localTime.hour); + const minute = int(localTime.minute); + const second = int(localTime.second); + + let totalSeconds = hour.multiply(SECONDS_PER_HOUR); + totalSeconds = totalSeconds.add(minute.multiply(SECONDS_PER_MINUTE)); + return totalSeconds.add(second); +} + + +/** + * Check if given year is a leap year. Uses algorithm described here {@link https://en.wikipedia.org/wiki/Leap_year#Algorithm}. + * @param {Integer|number|string} year the year to check. Will be converted to {@link Integer} for all calculations. + * @return {boolean} true if given year is a leap year, false otherwise. + */ +function isLeapYear(year) { + year = int(year); + + if (!year.modulo(4).equals(0)) { + return false; + } else if (!year.modulo(100).equals(0)) { + return true; + } else if (!year.modulo(400).equals(0)) { + return false; + } else { + return true; + } +} + +/** + * @param {Integer|number|string} x the divident. + * @param {Integer|number|string} y the divisor. + * @return {Integer} the result. + */ +function floorDiv(x, y) { + x = int(x); + y = int(y); + + let result = x.div(y); + if (x.isPositive() !== y.isPositive() && result.multiply(y).notEquals(x)) { + result = result.subtract(1); + } + return result; +} + +/** + * @param {Integer|number|string} x the divident. + * @param {Integer|number|string} y the divisor. + * @return {Integer} the result. + */ +function floorMod(x, y) { + x = int(x); + y = int(y); + + return x.subtract(floorDiv(x, y).multiply(y)); +} + +/** + * @param {Integer|number} num the number to format. + * @param {number} [stringLength=undefined] the string length to left-pad to. + * @return {string} formatted and possibly left-padded number as string. + */ +function formatNumber(num, stringLength = undefined) { + const result = int(num).toString(); + return stringLength ? result.padStart(stringLength, '0') : result; +} diff --git a/src/v1/temporal-types.js b/src/v1/temporal-types.js new file mode 100644 index 000000000..3f8b61661 --- /dev/null +++ b/src/v1/temporal-types.js @@ -0,0 +1,279 @@ +/** + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {dateToIsoString, durationToIsoString, timeToIsoString, timeZoneOffsetToIsoString} from './internal/temporal-util'; + +const IDENTIFIER_PROPERTY_ATTRIBUTES = { + value: true, + enumerable: false, + configurable: false +}; + +const DURATION_IDENTIFIER_PROPERTY = '__isDuration__'; +const LOCAL_TIME_IDENTIFIER_PROPERTY = '__isLocalTime__'; +const TIME_IDENTIFIER_PROPERTY = '__isTime__'; +const DATE_IDENTIFIER_PROPERTY = '__isDate__'; +const LOCAL_DATE_TIME_IDENTIFIER_PROPERTY = '__isLocalDateTime__'; +const DATE_TIME_WITH_ZONE_OFFSET_IDENTIFIER_PROPERTY = '__isDateTimeWithZoneOffset__'; +const DATE_TIME_WITH_ZONE_ID_IDENTIFIER_PROPERTY = '__isDateTimeWithZoneId__'; + +/** + * Represents an ISO 8601 duration. Contains both date-based values (years, months, days) and time-based values (seconds, nanoseconds). + * Created Duration objects are frozen with {@link Object#freeze()} in constructor and thus immutable. + */ +export class Duration { + + /** + * @constructor + * @param {Integer|number} months the number of months for the new duration. + * @param {Integer|number} days the number of days for the new duration. + * @param {Integer|number} seconds the number of seconds for the new duration. + * @param {Integer|number} nanoseconds the number of nanoseconds for the new duration. + */ + constructor(months, days, seconds, nanoseconds) { + this.months = months; + this.days = days; + this.seconds = seconds; + this.nanoseconds = nanoseconds; + Object.freeze(this); + } + + toString() { + return durationToIsoString(this.months, this.days, this.seconds, this.nanoseconds); + } +} + +Object.defineProperty(Duration.prototype, DURATION_IDENTIFIER_PROPERTY, IDENTIFIER_PROPERTY_ATTRIBUTES); + +/** + * Test if given object is an instance of {@link Duration} class. + * @param {object} obj the object to test. + * @return {boolean} true if given object is a {@link Duration}, false otherwise. + */ +export function isDuration(obj) { + return hasIdentifierProperty(obj, DURATION_IDENTIFIER_PROPERTY); +} + +/** + * Represents an instant capturing the time of day, but not the date, nor the timezone. + * Created LocalTime objects are frozen with {@link Object#freeze()} in constructor and thus immutable. + */ +export class LocalTime { + + /** + * @constructor + * @param {Integer|number} hour the hour for the new local time. + * @param {Integer|number} minute the minute for the new local time. + * @param {Integer|number} second the second for the new local time. + * @param {Integer|number} nanosecond the nanosecond for the new local time. + */ + constructor(hour, minute, second, nanosecond) { + this.hour = hour; + this.minute = minute; + this.second = second; + this.nanosecond = nanosecond; + Object.freeze(this); + } + + toString() { + return timeToIsoString(this.hour, this.minute, this.second, this.nanosecond); + } +} + +Object.defineProperty(LocalTime.prototype, LOCAL_TIME_IDENTIFIER_PROPERTY, IDENTIFIER_PROPERTY_ATTRIBUTES); + +/** + * Test if given object is an instance of {@link LocalTime} class. + * @param {object} obj the object to test. + * @return {boolean} true if given object is a {@link LocalTime}, false otherwise. + */ +export function isLocalTime(obj) { + return hasIdentifierProperty(obj, LOCAL_TIME_IDENTIFIER_PROPERTY); +} + +/** + * Represents an instant capturing the time of day, and the timezone offset in seconds, but not the date. + * Created Time objects are frozen with {@link Object#freeze()} in constructor and thus immutable. + */ +export class Time { + + /** + * @constructor + * @param {LocalTime} localTime the local time for the new time with offset. + * @param {Integer|number} offsetSeconds the time zone offset in seconds. + */ + constructor(localTime, offsetSeconds) { + this.localTime = localTime; + this.offsetSeconds = offsetSeconds; + Object.freeze(this); + } + + toString() { + return this.localTime.toString() + timeZoneOffsetToIsoString(this.offsetSeconds); + } +} + +Object.defineProperty(Time.prototype, TIME_IDENTIFIER_PROPERTY, IDENTIFIER_PROPERTY_ATTRIBUTES); + +/** + * Test if given object is an instance of {@link Time} class. + * @param {object} obj the object to test. + * @return {boolean} true if given object is a {@link Time}, false otherwise. + */ +export function isTime(obj) { + return hasIdentifierProperty(obj, TIME_IDENTIFIER_PROPERTY); +} + +/** + * Represents an instant capturing the date, but not the time, nor the timezone. + * Created Date objects are frozen with {@link Object#freeze()} in constructor and thus immutable. + */ +export class Date { + + /** + * @constructor + * @param {Integer|number} year the year for the new local date. + * @param {Integer|number} month the month for the new local date. + * @param {Integer|number} day the day for the new local date. + */ + constructor(year, month, day) { + this.year = year; + this.month = month; + this.day = day; + Object.freeze(this); + } + + toString() { + return dateToIsoString(this.year, this.month, this.day); + } +} + +Object.defineProperty(Date.prototype, DATE_IDENTIFIER_PROPERTY, IDENTIFIER_PROPERTY_ATTRIBUTES); + +/** + * Test if given object is an instance of {@link Date} class. + * @param {object} obj the object to test. + * @return {boolean} true if given object is a {@link Date}, false otherwise. + */ +export function isDate(obj) { + return hasIdentifierProperty(obj, DATE_IDENTIFIER_PROPERTY); +} + +/** + * Represents an instant capturing the date and the time, but not the timezone. + * Created LocalDateTime objects are frozen with {@link Object#freeze()} in constructor and thus immutable. + */ +export class LocalDateTime { + + /** + * @constructor + * @param {Date} localDate the local date part for the new local date-time. + * @param {LocalTime} localTime the local time part for the new local date-time. + */ + constructor(localDate, localTime) { + this.localDate = localDate; + this.localTime = localTime; + Object.freeze(this); + } + + toString() { + return `${this.localDate.toString()}T${this.localTime.toString()}`; + } +} + +Object.defineProperty(LocalDateTime.prototype, LOCAL_DATE_TIME_IDENTIFIER_PROPERTY, IDENTIFIER_PROPERTY_ATTRIBUTES); + +/** + * Test if given object is an instance of {@link LocalDateTime} class. + * @param {object} obj the object to test. + * @return {boolean} true if given object is a {@link LocalDateTime}, false otherwise. + */ +export function isLocalDateTime(obj) { + return hasIdentifierProperty(obj, LOCAL_DATE_TIME_IDENTIFIER_PROPERTY); +} + +/** + * Represents an instant capturing the date, the time and the timezone identifier. + * Created DateTimeWithZoneOffset objects are frozen with {@link Object#freeze()} in constructor and thus immutable. + */ +export class DateTimeWithZoneOffset { + + /** + * @constructor + * @param {LocalDateTime} localDateTime the local date-time part for the new timezone-aware date-time. + * @param {Integer|number} offsetSeconds the timezone offset in seconds for the new timezone-aware date-time. + */ + constructor(localDateTime, offsetSeconds) { + this.localDateTime = localDateTime; + this.offsetSeconds = offsetSeconds; + Object.freeze(this); + } + + toString() { + return this.localDateTime.toString() + timeZoneOffsetToIsoString(this.offsetSeconds); + } +} + +Object.defineProperty(DateTimeWithZoneOffset.prototype, DATE_TIME_WITH_ZONE_OFFSET_IDENTIFIER_PROPERTY, IDENTIFIER_PROPERTY_ATTRIBUTES); + +/** + * Test if given object is an instance of {@link DateTimeWithZoneOffset} class. + * @param {object} obj the object to test. + * @return {boolean} true if given object is a {@link DateTimeWithZoneOffset}, false otherwise. + */ +export function isDateTimeWithZoneOffset(obj) { + return hasIdentifierProperty(obj, DATE_TIME_WITH_ZONE_OFFSET_IDENTIFIER_PROPERTY); +} + +/** + * Represents an instant capturing the date, the time and the timezone identifier. + * Created DateTimeWithZoneId objects are frozen with {@link Object#freeze()} in constructor and thus immutable. + */ +export class DateTimeWithZoneId { + + /** + * @constructor + * @param {LocalDateTime} localDateTime the local date-time part for the new timezone-aware date-time. + * @param {string} zoneId the timezone identifier for the new timezone-aware date-time. + */ + constructor(localDateTime, zoneId) { + this.localDateTime = localDateTime; + this.zoneId = zoneId; + Object.freeze(this); + } + + toString() { + return `${this.localDateTime.toString()}[${this.zoneId}]`; + } +} + +Object.defineProperty(DateTimeWithZoneId.prototype, DATE_TIME_WITH_ZONE_ID_IDENTIFIER_PROPERTY, IDENTIFIER_PROPERTY_ATTRIBUTES); + +/** + * Test if given object is an instance of {@link DateTimeWithZoneId} class. + * @param {object} obj the object to test. + * @return {boolean} true if given object is a {@link DateTimeWithZoneId}, false otherwise. + */ +export function isDateTimeWithZoneId(obj) { + return hasIdentifierProperty(obj, DATE_TIME_WITH_ZONE_ID_IDENTIFIER_PROPERTY); +} + +function hasIdentifierProperty(obj, property) { + return (obj && obj[property]) === true; +} diff --git a/test/internal/shared-neo4j.js b/test/internal/shared-neo4j.js index 50c554560..3bb205264 100644 --- a/test/internal/shared-neo4j.js +++ b/test/internal/shared-neo4j.js @@ -109,7 +109,7 @@ const additionalConfig = { }; const neoCtrlVersionParam = '-e'; -const defaultNeo4jVersion = '3.2.9'; +const defaultNeo4jVersion = '3.3.4'; const defaultNeoCtrlArgs = `${neoCtrlVersionParam} ${defaultNeo4jVersion}`; function neo4jCertPath(dir) { diff --git a/test/types/v1/temporal-types.test.ts b/test/types/v1/temporal-types.test.ts new file mode 100644 index 000000000..95562c15d --- /dev/null +++ b/test/types/v1/temporal-types.test.ts @@ -0,0 +1,101 @@ +/** + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Date, + DateTimeWithZoneOffset, + Duration, + isDate, + isDateTimeWithZoneId, + isDateTimeWithZoneOffset, + isDuration, + isLocalDateTime, + isLocalTime, + isTime, + LocalDateTime, + LocalTime, + Time +} from "../../../types/v1/temporal-types"; +import Integer, {int} from "../../../types/v1/integer"; + +const duration1: Duration = new Duration(int(1), int(1), int(1), int(1)); +const months1: Integer = duration1.months; +const days1: Integer = duration1.days; +const seconds1: Integer = duration1.seconds; +const nanoseconds1: Integer = duration1.nanoseconds; + +const duration2: Duration = new Duration(1, 1, 1, 1); +const months2: number = duration2.months; +const days2: number = duration2.days; +const seconds2: number = duration2.seconds; +const nanoseconds2: number = duration2.nanoseconds; + +const localTime1: LocalTime = new LocalTime(int(1), int(1), int(1), int(1)); +const localTime1Hour1: Integer = localTime1.hour; +const localTime1Minute1: Integer = localTime1.minute; +const localTime1Second1: Integer = localTime1.second; +const localTime1Nanosecond1: Integer = localTime1.nanosecond; + +const localTime2: LocalTime = new LocalTime(1, 1, 1, 1); +const localTime2Hour1: number = localTime2.hour; +const localTime2Minute1: number = localTime2.minute; +const localTime2Second1: number = localTime2.second; +const localTime2Nanosecond1: number = localTime2.nanosecond; + +const time1: Time = new Time(localTime1, int(1)); +const localTime3: LocalTime = time1.localTime; +const offset1: Integer = time1.offsetSeconds; + +const time2: Time = new Time(localTime2, 1); +const localTime4: LocalTime = time2.localTime; +const offset2: number = time2.offsetSeconds; + +const date1: Date = new Date(int(1), int(1), int(1)); +const date1Year1: Integer = date1.year; +const date1Month1: Integer = date1.month; +const date1Day1: Integer = date1.day; + +const date2: Date = new Date(1, 1, 1); +const date2Year1: number = date2.year; +const date2Month1: number = date2.month; +const date2Day1: number = date2.day; + +const localDateTime1: LocalDateTime = new LocalDateTime(date1, localTime1); +const date3: Date = localDateTime1.localDate; +const localTime5: LocalTime = localDateTime1.localTime; + +const localDateTime2: LocalDateTime = new LocalDateTime(date2, localTime2); +const date4: Date = localDateTime2.localDate; +const localTime6: LocalTime = localDateTime2.localTime; + +const dateTime1: DateTimeWithZoneOffset = new DateTimeWithZoneOffset(localDateTime1, int(1)); +const localDateTime3: LocalDateTime = dateTime1.localDateTime; +const offset3: Integer = dateTime1.offsetSeconds; + +const dateTime2: DateTimeWithZoneOffset = new DateTimeWithZoneOffset(localDateTime2, 1); +const localDateTime4: LocalDateTime = dateTime2.localDateTime; +const offset4: number = dateTime2.offsetSeconds; + +const isDurationValue: boolean = isDuration(duration1); +const isLocalTimeValue: boolean = isLocalTime(localTime1); +const isTimeValue: boolean = isTime(time1); +const isDateValue: boolean = isDate(date1); +const isLocalDateTimeValue: boolean = isLocalDateTime(localDateTime1); +const isDateTimeWithZoneOffsetValue: boolean = isDateTimeWithZoneOffset(dateTime1); +const isDateTimeWithZoneIdValue: boolean = isDateTimeWithZoneId(dateTime2); diff --git a/test/v1/examples.test.js b/test/v1/examples.test.js index f76ea7ccf..d63e5023d 100644 --- a/test/v1/examples.test.js +++ b/test/v1/examples.test.js @@ -44,7 +44,7 @@ describe('examples', () => { beforeAll(() => { originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; - jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 20000; driverGlobal = neo4j.driver(uri, sharedNeo4j.authToken); }); diff --git a/test/v1/tck/steps/matchacceptencesteps.js b/test/v1/tck/steps/matchacceptencesteps.js index b3d5aebfd..c23e364ba 100644 --- a/test/v1/tck/steps/matchacceptencesteps.js +++ b/test/v1/tck/steps/matchacceptencesteps.js @@ -155,9 +155,6 @@ module.exports = function () { var segment; for (var i = 0; i < segments.length; i++) { segment = segments[i]; - if (startid.notEquals(segment.start.identity)) { - throw new Error("Path segment does not make sense") - } var relationship = segment.relationship; var endId = neo4j.int(segment.end.identity.toString()); relationship.identity = neo4j.int(0); diff --git a/test/v1/temporal-types.test.js b/test/v1/temporal-types.test.js new file mode 100644 index 000000000..929721846 --- /dev/null +++ b/test/v1/temporal-types.test.js @@ -0,0 +1,517 @@ +/** + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import neo4j from '../../src'; +import sharedNeo4j from '../internal/shared-neo4j'; +import {ServerVersion, VERSION_3_4_0} from '../../src/v1/internal/server-version'; +import timesSeries from 'async/timesSeries'; +import _ from 'lodash'; + +const RANDOM_VALUES_TO_TEST = 2000; +const MAX_NANO_OF_SECOND = 999999999; +const MAX_YEAR = 999999999; +const MIN_YEAR = -MAX_YEAR; +const MAX_TIME_ZONE_OFFSET = 64800; +const MIN_TIME_ZONE_OFFSET = -MAX_TIME_ZONE_OFFSET; +const SECONDS_PER_MINUTE = 60; +const MIN_ZONE_ID = 'Etc/GMT+12'; +const MAX_ZONE_ID = 'Etc/GMT-14'; +const ZONE_IDS = ['Europe/Zaporozhye', 'America/Argentina/Mendoza', 'Etc/GMT-12', 'Asia/Jayapura', 'Pacific/Auckland', 'America/Argentina/Rio_Gallegos', + 'America/Tegucigalpa', 'Europe/Skopje', 'Africa/Lome', 'America/Eirunepe', 'Pacific/Port_Moresby', 'America/Merida', 'Asia/Qyzylorda', 'Hongkong', + 'America/Paramaribo', 'Pacific/Wallis', 'Antarctica/Mawson', 'America/Metlakatla', 'Indian/Reunion', 'Asia/Chungking', 'Canada/Central', 'Etc/GMT-6', + 'UCT', 'America/Belem', 'Europe/Belgrade', 'Singapore', 'Israel', 'Europe/London', 'America/Yellowknife', 'Europe/Uzhgorod', 'Etc/GMT+7', + 'America/Indiana/Winamac', 'Asia/Kuala_Lumpur', 'America/Cuiaba', 'Europe/Sofia', 'Asia/Kuching', 'Australia/Lord_Howe', 'America/Porto_Acre', + 'America/Indiana/Indianapolis', 'Africa/Windhoek', 'Atlantic/Cape_Verde', 'Asia/Kuwait', 'America/Barbados', 'Egypt', 'GB-Eire', 'Antarctica/South_Pole', + 'America/Kentucky/Louisville', 'Asia/Yangon', 'CET', 'Etc/GMT+11', 'Asia/Dubai', 'Europe/Stockholm']; + +describe('temporal-types', () => { + + let originalTimeout; + let driver; + let session; + let serverVersion; + + beforeAll(done => { + originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000; + + driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken); + ServerVersion.fromDriver(driver).then(version => { + serverVersion = version; + done(); + }); + }); + + afterAll(() => { + jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; + + if (driver) { + driver.close(); + driver = null; + } + }); + + beforeEach(done => { + session = driver.session(); + session.run('MATCH (n) DETACH DELETE n').then(() => { + done(); + }).catch(error => { + done.fail(error); + }); + }); + + afterEach(() => { + if (session) { + session.close(); + session = null; + } + }); + + it('should receive Duration', done => { + if (neo4jDoesNotSupportTemporalTypes(done)) { + return; + } + + const expectedValue = duration(27, 17, 91, 999); + testReceiveTemporalValue('RETURN duration({years: 2, months: 3, days: 17, seconds: 91, nanoseconds: 999})', expectedValue, done); + }); + + it('should send and receive random Duration', done => { + if (neo4jDoesNotSupportTemporalTypes(done)) { + return; + } + + const valueGenerator = () => { + const sign = _.sample([true, false]) ? 1 : -1; // duration can be negative + return duration( + 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), + ); + }; + + testSendAndReceiveRandomTemporalValues(valueGenerator, done); + }); + + it('should receive LocalTime', done => { + if (neo4jDoesNotSupportTemporalTypes(done)) { + return; + } + + const expectedValue = localTime(22, 59, 10, 999999); + testReceiveTemporalValue('RETURN localtime({hour: 22, minute: 59, second: 10, nanosecond: 999999})', expectedValue, done); + }); + + it('should send and receive max LocalTime', done => { + if (neo4jDoesNotSupportTemporalTypes(done)) { + return; + } + + const maxLocalTime = localTime(23, 59, 59, MAX_NANO_OF_SECOND); + testSendReceiveTemporalValue(maxLocalTime, done); + }); + + it('should send and receive min LocalTime', done => { + if (neo4jDoesNotSupportTemporalTypes(done)) { + return; + } + + const minLocalTime = localTime(0, 0, 0, 0); + testSendReceiveTemporalValue(minLocalTime, done); + }); + + it('should send and receive random LocalTime', done => { + if (neo4jDoesNotSupportTemporalTypes(done)) { + return; + } + + const valueGenerator = () => randomLocalTime(); + + testSendAndReceiveRandomTemporalValues(valueGenerator, done); + }); + + it('should receive Time', done => { + if (neo4jDoesNotSupportTemporalTypes(done)) { + return; + } + + const expectedValue = time(11, 42, 59, 9999, -30600); + testReceiveTemporalValue('RETURN time({hour: 11, minute: 42, second: 59, nanosecond: 9999, timezone:"-08:30"})', expectedValue, done); + }); + + it('should send and receive max Time', done => { + if (neo4jDoesNotSupportTemporalTypes(done)) { + return; + } + + const maxTime = time(23, 59, 59, MAX_NANO_OF_SECOND, MAX_TIME_ZONE_OFFSET); + testSendReceiveTemporalValue(maxTime, done); + }); + + it('should send and receive min Time', done => { + if (neo4jDoesNotSupportTemporalTypes(done)) { + return; + } + + const minTime = time(0, 0, 0, 0, MIN_TIME_ZONE_OFFSET); + testSendReceiveTemporalValue(minTime, done); + }); + + it('should send and receive random Time', done => { + if (neo4jDoesNotSupportTemporalTypes(done)) { + return; + } + + const valueGenerator = () => randomTime(); + + testSendAndReceiveRandomTemporalValues(valueGenerator, done); + }); + + it('should receive Date', done => { + if (neo4jDoesNotSupportTemporalTypes(done)) { + return; + } + + const expectedValue = date(1995, 7, 28); + testReceiveTemporalValue('RETURN date({year: 1995, month: 7, day: 28})', expectedValue, done); + }); + + it('should send and receive max Date', done => { + if (neo4jDoesNotSupportTemporalTypes(done)) { + return; + } + + const maxDate = date(MAX_YEAR, 12, 31); + testSendReceiveTemporalValue(maxDate, done); + }); + + it('should send and receive min Date', done => { + if (neo4jDoesNotSupportTemporalTypes(done)) { + return; + } + + const minDate = date(MIN_YEAR, 1, 1); + testSendReceiveTemporalValue(minDate, done); + }); + + it('should send and receive random Date', done => { + if (neo4jDoesNotSupportTemporalTypes(done)) { + return; + } + + const valueGenerator = () => randomDate(); + + testSendAndReceiveRandomTemporalValues(valueGenerator, done); + }); + + it('should receive LocalDateTime', done => { + if (neo4jDoesNotSupportTemporalTypes(done)) { + return; + } + + const expectedValue = localDateTime(1869, 9, 23, 18, 29, 59, 12349); + testReceiveTemporalValue('RETURN localdatetime({year: 1869, month: 9, day: 23, hour: 18, minute: 29, second: 59, nanosecond: 12349})', expectedValue, done); + }); + + it('should send and receive max LocalDateTime', done => { + if (neo4jDoesNotSupportTemporalTypes(done)) { + return; + } + + const maxLocalDateTime = localDateTime(MAX_YEAR, 12, 31, 23, 59, 59, MAX_NANO_OF_SECOND); + testSendReceiveTemporalValue(maxLocalDateTime, done); + }); + + it('should send and receive min LocalDateTime', done => { + if (neo4jDoesNotSupportTemporalTypes(done)) { + return; + } + + const minLocalDateTime = localDateTime(MIN_YEAR, 1, 1, 0, 0, 0, 0); + testSendReceiveTemporalValue(minLocalDateTime, done); + }); + + it('should send and receive random LocalDateTime', done => { + if (neo4jDoesNotSupportTemporalTypes(done)) { + return; + } + + const valueGenerator = () => randomLocalDateTime(); + + testSendAndReceiveRandomTemporalValues(valueGenerator, done); + }); + + it('should receive DateTimeWithZoneOffset', done => { + if (neo4jDoesNotSupportTemporalTypes(done)) { + return; + } + + const expectedValue = dateTimeWithZoneOffset(1992, 11, 24, 9, 55, 42, 999, 18000); + testReceiveTemporalValue('RETURN datetime({year: 1992, month: 11, day: 24, hour: 9, minute: 55, second: 42, nanosecond: 999, timezone: "+05:00"})', expectedValue, done); + }); + + it('should send and receive max DateTimeWithZoneOffset', done => { + if (neo4jDoesNotSupportTemporalTypes(done)) { + return; + } + + const maxDateTime = dateTimeWithZoneOffset(MAX_YEAR, 12, 31, 23, 59, 59, MAX_NANO_OF_SECOND, MAX_TIME_ZONE_OFFSET); + testSendReceiveTemporalValue(maxDateTime, done); + }); + + it('should send and receive min DateTimeWithZoneOffset', done => { + if (neo4jDoesNotSupportTemporalTypes(done)) { + return; + } + + const minDateTime = dateTimeWithZoneOffset(MIN_YEAR, 1, 1, 0, 0, 0, 0, MAX_TIME_ZONE_OFFSET); + testSendReceiveTemporalValue(minDateTime, done); + }); + + it('should send and receive random DateTimeWithZoneOffset', done => { + if (neo4jDoesNotSupportTemporalTypes(done)) { + return; + } + + const valueGenerator = () => randomDateTimeWithZoneOffset(); + + testSendAndReceiveRandomTemporalValues(valueGenerator, done); + }); + + it('should receive DateTimeWithZoneId', done => { + if (neo4jDoesNotSupportTemporalTypes(done)) { + return; + } + + const expectedValue = dateTimeWithZoneId(1992, 11, 24, 9, 55, 42, 999, 'Europe/Stockholm'); + testReceiveTemporalValue('RETURN datetime({year: 1992, month: 11, day: 24, hour: 9, minute: 55, second: 42, nanosecond: 999, timezone: "Europe/Stockholm"})', expectedValue, done); + }); + + it('should send and receive max DateTimeWithZoneId', done => { + if (neo4jDoesNotSupportTemporalTypes(done)) { + return; + } + + const maxDateTime = dateTimeWithZoneId(MAX_YEAR, 12, 31, 23, 59, 59, MAX_NANO_OF_SECOND, MAX_ZONE_ID); + testSendReceiveTemporalValue(maxDateTime, done); + }); + + it('should send and receive min DateTimeWithZoneId', done => { + if (neo4jDoesNotSupportTemporalTypes(done)) { + return; + } + + const minDateTime = dateTimeWithZoneId(MIN_YEAR, 1, 1, 0, 0, 0, 0, MIN_ZONE_ID); + testSendReceiveTemporalValue(minDateTime, done); + }); + + it('should send and receive random DateTimeWithZoneId', done => { + if (neo4jDoesNotSupportTemporalTypes(done)) { + return; + } + + const valueGenerator = () => randomDateTimeWithZoneId(); + + testSendAndReceiveRandomTemporalValues(valueGenerator, done); + }); + + it('should convert Duration to ISO string', () => { + expect(duration(13, 62, 3, 999111999).toString()).toEqual('P13M62DT3.999111999S'); + expect(duration(0, 0, 0, 0).toString()).toEqual('P0M0DT0.000000000S'); + expect(duration(-1, -2, 10, 10).toString()).toEqual('P-1M-2DT10.000000010S'); + }); + + it('should convert LocalTime to ISO string', () => { + expect(localTime(12, 19, 39, 111222333).toString()).toEqual('12:19:39.111222333'); + expect(localTime(3, 59, 2, 17).toString()).toEqual('03:59:02.000000017'); + expect(localTime(0, 0, 0, 0).toString()).toEqual('00:00:00.000000000'); + }); + + it('should convert Time to ISO string', () => { + expect(time(11, 45, 22, 333222111, 9015).toString()).toEqual('11:45:22.333222111+02:30:15'); + expect(time(23, 2, 1, 10, 0).toString()).toEqual('23:02:01.000000010Z'); + expect(time(0, 12, 59, 0, -40500).toString()).toEqual('00:12:59.000000000-11:15'); + expect(time(21, 59, 0, 123, -25200).toString()).toEqual('21:59:00.000000123-07:00'); + }); + + it('should convert Date to ISO string', () => { + expect(date(2015, 10, 12).toString()).toEqual('2015-10-12'); + expect(date(881, 1, 1).toString()).toEqual('0881-01-01'); + expect(date(-999, 12, 24).toString()).toEqual('-0999-12-24'); + expect(date(-9, 1, 1).toString()).toEqual('-0009-01-01'); + }); + + it('should convert LocalDateTime to ISO string', () => { + expect(localDateTime(1992, 11, 8, 9, 42, 17, 22).toString()).toEqual('1992-11-08T09:42:17.000000022'); + expect(localDateTime(-10, 7, 15, 8, 15, 33, 500).toString()).toEqual('-0010-07-15T08:15:33.000000500'); + expect(localDateTime(0, 0, 0, 0, 0, 0, 1).toString()).toEqual('0000-00-00T00:00:00.000000001'); + }); + + it('should convert DateTime with time zone offset to ISO string', () => { + expect(dateTimeWithZoneOffset(2025, 9, 17, 23, 22, 21, 999888, 37800).toString()).toEqual('2025-09-17T23:22:21.000999888+10:30'); + expect(dateTimeWithZoneOffset(1, 2, 3, 4, 5, 6, 7, -49376).toString()).toEqual('0001-02-03T04:05:06.000000007-13:42:56'); + expect(dateTimeWithZoneOffset(-3, 3, 9, 9, 33, 27, 999000, 15300).toString()).toEqual('-0003-03-09T09:33:27.000999000+04:15'); + }); + + it('should convert DateTime with time zone id to ISO-like string', () => { + expect(dateTimeWithZoneId(1949, 10, 7, 6, 10, 15, 15000000, 'Europe/Zaporozhye').toString()).toEqual('1949-10-07T06:10:15.015000000[Europe/Zaporozhye]'); + expect(dateTimeWithZoneId(-30455, 5, 5, 12, 24, 10, 123, 'Asia/Yangon').toString()).toEqual('-30455-05-05T12:24:10.000000123[Asia/Yangon]'); + expect(dateTimeWithZoneId(248, 12, 30, 23, 59, 59, 3, 'CET').toString()).toEqual('0248-12-30T23:59:59.000000003[CET]'); + }); + + function testSendAndReceiveRandomTemporalValues(valueGenerator, done) { + const asyncFunction = (index, callback) => { + const next = () => callback(); + next.fail = error => callback(error); + testSendReceiveTemporalValue(valueGenerator(), next); + }; + + const doneFunction = error => { + if (error) { + done.fail(error); + } else { + done(); + } + }; + + timesSeries(RANDOM_VALUES_TO_TEST, asyncFunction, doneFunction); + } + + function testReceiveTemporalValue(query, expectedValue, done) { + session.run(query).then(result => { + const records = result.records; + expect(records.length).toEqual(1); + + const value = records[0].get(0); + expect(value).toEqual(expectedValue); + + session.close(); + done(); + }).catch(error => { + done.fail(error); + }); + } + + function testSendReceiveTemporalValue(value, done) { + session.run('CREATE (n:Node {value: $value}) RETURN n.value', {value: value}).then(result => { + const records = result.records; + expect(records.length).toEqual(1); + + const receivedValue = records[0].get(0); + expect(receivedValue).toEqual(value); + + session.close(); + done(); + }).catch(error => { + done.fail(error); + }); + } + + function neo4jDoesNotSupportTemporalTypes(done) { + if (serverVersion.compareTo(VERSION_3_4_0) < 0) { + done(); + return true; + } + return false; + } + + function randomDateTimeWithZoneOffset() { + return new neo4j.DateTimeWithZoneOffset( + randomLocalDateTime(), + randomZoneOffsetSeconds() + ); + } + + function randomDateTimeWithZoneId() { + return new neo4j.DateTimeWithZoneId( + randomLocalDateTime(), + randomZoneId() + ); + } + + function randomLocalDateTime() { + return new neo4j.LocalDateTime(randomDate(), randomLocalTime()); + } + + function randomDate() { + return new neo4j.Date( + randomInt(MIN_YEAR, MAX_YEAR), + randomInt(1, 12), + randomInt(1, 28) + ); + } + + function randomTime() { + return new neo4j.Time( + randomLocalTime(), + randomZoneOffsetSeconds(), + ); + } + + function randomLocalTime() { + return new neo4j.LocalTime( + randomInt(0, 23), + randomInt(0, 59), + randomInt(0, 59), + randomInt(0, MAX_NANO_OF_SECOND) + ); + } + + function randomZoneOffsetSeconds() { + const randomOffsetWithSeconds = neo4j.int(randomInt(MIN_TIME_ZONE_OFFSET, MAX_TIME_ZONE_OFFSET)); + return randomOffsetWithSeconds.div(SECONDS_PER_MINUTE).multiply(SECONDS_PER_MINUTE); // truncate seconds + } + + function randomZoneId() { + return _.sample(ZONE_IDS); + } + + function duration(months, days, seconds, nanoseconds) { + return new neo4j.Duration(neo4j.int(months), neo4j.int(days), neo4j.int(seconds), neo4j.int(nanoseconds)); + } + + function localTime(hour, minute, second, nanosecond) { + return new neo4j.LocalTime(neo4j.int(hour), neo4j.int(minute), neo4j.int(second), neo4j.int(nanosecond)); + } + + function time(hour, minute, second, nanosecond, offsetSeconds) { + return new neo4j.Time(localTime(hour, minute, second, nanosecond), neo4j.int(offsetSeconds)); + } + + function date(year, month, day) { + return new neo4j.Date(neo4j.int(year), neo4j.int(month), neo4j.int(day)); + } + + function localDateTime(year, month, day, hour, minute, second, nanosecond) { + return new neo4j.LocalDateTime(date(year, month, day), localTime(hour, minute, second, nanosecond)); + } + + function dateTimeWithZoneOffset(year, month, day, hour, minute, second, nanosecond, offsetSeconds) { + return new neo4j.DateTimeWithZoneOffset(localDateTime(year, month, day, hour, minute, second, nanosecond), neo4j.int(offsetSeconds)); + } + + function dateTimeWithZoneId(year, month, day, hour, minute, second, nanosecond, zoneId) { + return new neo4j.DateTimeWithZoneId(localDateTime(year, month, day, hour, minute, second, nanosecond), zoneId); + } + + function randomInt(lower, upper) { + return neo4j.int(_.random(lower, upper)); + } +}); diff --git a/types/v1/index.d.ts b/types/v1/index.d.ts index 0a8aab49c..a3c07e9aa 100644 --- a/types/v1/index.d.ts +++ b/types/v1/index.d.ts @@ -19,16 +19,26 @@ import Integer, {inSafeRange, int, isInt, toNumber, toString} from "./integer"; import {Node, Path, PathSegment, Relationship, UnboundRelationship} from "./graph-types"; +import {isPoint, Point} from "./spatial-types"; +import { + Date, + DateTimeWithZoneId, + DateTimeWithZoneOffset, + Duration, + isDate, + isDateTimeWithZoneId, + isDateTimeWithZoneOffset, + isDuration, + isLocalDateTime, + isLocalTime, + isTime, + LocalDateTime, + LocalTime, + Time +} from "./temporal-types"; import {Neo4jError, PROTOCOL_ERROR, SERVICE_UNAVAILABLE, SESSION_EXPIRED} from "./error"; import Result, {Observer, StatementResult} from "./result"; -import ResultSummary, { - Notification, - NotificationPosition, - Plan, - ProfiledPlan, - ServerInfo, - StatementStatistic -} from "./result-summary"; +import ResultSummary, {Notification, NotificationPosition, Plan, ProfiledPlan, ServerInfo, StatementStatistic} from "./result-summary"; import Record from "./record"; import Session from "./session"; import {AuthToken, Config, Driver, EncryptionLevel, READ, SessionMode, TrustStrategy, WRITE} from "./driver"; @@ -54,14 +64,22 @@ declare function driver(url: string, config?: Config): Driver; declare const types: { - Node: typeof Node; - Relationship: typeof Relationship; - UnboundRelationship: typeof UnboundRelationship; - PathSegment: typeof PathSegment; - Path: typeof Path; + Node: Node; + Relationship: Relationship; + UnboundRelationship: UnboundRelationship; + PathSegment: PathSegment; + Path: Path; Result: Result; ResultSummary: ResultSummary; - Record: typeof Record; + Record: Record; + Point: Point; + Duration: Duration; + LocalTime: LocalTime; + Time: Time; + Date: Date; + LocalDateTime: LocalDateTime; + DateTimeWithZoneOffset: DateTimeWithZoneOffset; + DateTimeWithZoneId: DateTimeWithZoneId; }; declare const session: { @@ -125,6 +143,22 @@ declare const forExport: { NotificationPosition: NotificationPosition, Session: Session; Transaction: Transaction; + Point: Point; + isPoint: typeof isPoint; + Duration: Duration; + LocalTime: LocalTime; + Time: Time; + Date: Date; + LocalDateTime: LocalDateTime; + DateTimeWithZoneOffset: DateTimeWithZoneOffset; + DateTimeWithZoneId: DateTimeWithZoneId; + isDuration: typeof isDuration; + isLocalTime: typeof isLocalTime; + isTime: typeof isTime; + isDate: typeof isDate; + isLocalDateTime: typeof isLocalDateTime; + isDateTimeWithZoneOffset: typeof isDateTimeWithZoneOffset; + isDateTimeWithZoneId: typeof isDateTimeWithZoneId; }; export { @@ -161,7 +195,23 @@ export { ServerInfo, NotificationPosition, Session, - Transaction + Transaction, + Point, + isPoint, + Duration, + LocalTime, + Time, + Date, + LocalDateTime, + DateTimeWithZoneOffset, + DateTimeWithZoneId, + isDuration, + isLocalTime, + isTime, + isDate, + isLocalDateTime, + isDateTimeWithZoneOffset, + isDateTimeWithZoneId } export default forExport; diff --git a/types/v1/temporal-types.d.ts b/types/v1/temporal-types.d.ts new file mode 100644 index 000000000..dd27cae25 --- /dev/null +++ b/types/v1/temporal-types.d.ts @@ -0,0 +1,111 @@ +/** + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {NumberOrInteger} from './graph-types'; +import Integer from "./integer"; + +declare class Duration { + months: T; + days: T; + seconds: T; + nanoseconds: T; + + constructor(months: T, days: T, seconds: T, nanoseconds: T) +} + +declare class LocalTime { + hour: T; + minute: T; + second: T; + nanosecond: T; + + constructor(hour: T, minute: T, second: T, nanosecond: T); +} + +declare class Time { + + localTime: LocalTime; + offsetSeconds: T; + + constructor(localTime: LocalTime, offsetSeconds: T); +} + +declare class Date { + + year: T; + month: T; + day: T; + + constructor(year: T, month: T, day: T); +} + +declare class LocalDateTime { + + localDate: Date; + localTime: LocalTime; + + constructor(localDate: Date, localTime: LocalTime); +} + +declare class DateTimeWithZoneOffset { + + localDateTime: LocalDateTime; + offsetSeconds: T; + + constructor(localDateTime: LocalDateTime, offsetSeconds: T); +} + +declare class DateTimeWithZoneId { + + localDateTime: LocalDateTime; + zoneId: string; + + constructor(localDateTime: LocalDateTime, zoneId: string); +} + +declare function isDuration(obj: object): boolean; + +declare function isLocalTime(obj: object): boolean; + +declare function isTime(obj: object): boolean; + +declare function isDate(obj: object): boolean; + +declare function isLocalDateTime(obj: object): boolean; + +declare function isDateTimeWithZoneOffset(obj: object): boolean; + +declare function isDateTimeWithZoneId(obj: object): boolean; + +export { + Duration, + LocalTime, + Time, + Date, + LocalDateTime, + DateTimeWithZoneOffset, + DateTimeWithZoneId, + isDuration, + isLocalTime, + isTime, + isDate, + isLocalDateTime, + isDateTimeWithZoneOffset, + isDateTimeWithZoneId +}