-
Notifications
You must be signed in to change notification settings - Fork 152
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* add: duration scalar * add: int test for duration scalar * add: tck tests for duration scalar * update: documentation for duration scalar * cleanup: int test * fix: regex * add: error thrown on decimal value * update: documentation about decimal values * add: validation and parsing tests * refactor: for readability and performance * fix: regex to allow full ISO 8601 duration * add: duration comparison * add: {Create,Update}Info and bookmarks * fix: regex group capture * add: negative duration for unit based * cleanup * fix: documentation * fix: regex negative unit time * fix: decimal values for components * Apply suggestions from code review Co-authored-by: Darrell Warde <[email protected]> * fix: readability and unnecessary regex test * remove: unnessary type check * add: missing type check * move: duration schema test Co-authored-by: Darrell Warde <[email protected]> Co-authored-by: Oskar Hane <[email protected]>
- Loading branch information
1 parent
b2f639c
commit 8896131
Showing
11 changed files
with
1,089 additions
and
30 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
/* | ||
* Copyright (c) "Neo4j" | ||
* Neo4j Sweden AB [http://neo4j.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 { parseDuration } from "./Duration"; | ||
|
||
type ParsedDuration = ReturnType<typeof parseDuration>; | ||
|
||
describe("Duration Scalar", () => { | ||
test.each<any>([42, () => 5, { a: 3, b: 4 }, null, undefined])("should not match %p and throw error", (value) => | ||
expect(() => parseDuration(value)).toThrow(TypeError) | ||
); | ||
test.each<string>([ | ||
"P", | ||
"PT", | ||
"P233WT4H", | ||
"P5.2Y4M", | ||
"P18871104T12:00:00", | ||
"P1887-11-04T120000", | ||
])("should not match %s and throw error", (value) => expect(() => parseDuration(value)).toThrow(TypeError)); | ||
test.each<[string, ParsedDuration]>([ | ||
["P2Y", { months: 2 * 12, days: 0, seconds: 0, nanoseconds: 0 }], | ||
["P2Y-3M", { months: 2 * 12 - 3, days: 0, seconds: 0, nanoseconds: 0 }], | ||
["-P2Y-3M", { months: -2 * 12 + 3, days: 0, seconds: 0, nanoseconds: 0 }], | ||
["P3M", { months: 3, days: 0, seconds: 0, nanoseconds: 0 }], | ||
["P87D", { months: 0, days: 87, seconds: 0, nanoseconds: 0 }], | ||
["P15W", { months: 0, days: 15 * 7, seconds: 0, nanoseconds: 0 }], | ||
["P-15W", { months: 0, days: -15 * 7, seconds: 0, nanoseconds: 0 }], | ||
["-P-15W", { months: 0, days: 15 * 7, seconds: 0, nanoseconds: 0 }], | ||
["PT50H", { months: 0, days: 0, seconds: 50 * 60 * 60, nanoseconds: 0 }], | ||
["P4Y-5M-3DT5H", { months: 4 * 12 - 5, days: -3, seconds: 5 * 3600, nanoseconds: 0 }], | ||
["PT30M", { months: 0, days: 0, seconds: 30 * 60, nanoseconds: 0 }], | ||
["PT6.5S", { months: 0, days: 0, seconds: 6, nanoseconds: 500000000 }], | ||
["P34.5Y", { months: 414, days: 0, seconds: 0, nanoseconds: 0 }], | ||
["P6.5M", { months: 6, days: 15, seconds: 18873, nanoseconds: 0 }], | ||
["P3.5D", { months: 0, days: 3, seconds: 43200, nanoseconds: 0 }], | ||
["P7M-4.5D", { months: 7, days: -4, seconds: -43200, nanoseconds: 0 }], | ||
["P6M-4DT0.75H", { months: 6, days: -4, seconds: 2700, nanoseconds: 0 }], | ||
["P6Y30M16DT30M", { months: 6 * 12 + 30, days: 16, seconds: 30 * 60, nanoseconds: 0 }], | ||
["P18870605T120000", { months: 1887 * 12 + 6, days: 5, seconds: 12 * 60 * 60, nanoseconds: 0 }], | ||
["P1887-06-05T12:00:00", { months: 1887 * 12 + 6, days: 5, seconds: 12 * 60 * 60, nanoseconds: 0 }], | ||
])("should match and parse %s correctly", (duration, parsed) => | ||
expect(parseDuration(duration)).toStrictEqual(parsed) | ||
); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
/* | ||
* Copyright (c) "Neo4j" | ||
* Neo4j Sweden AB [http://neo4j.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 { GraphQLError, GraphQLScalarType, Kind } from "graphql"; | ||
import neo4j from "neo4j-driver"; | ||
|
||
// Matching P[nY][nM][nD][T[nH][nM][nS]] | PnW | PYYYYMMDDTHHMMSS | PYYYY-MM-DDTHH:MM:SS | ||
// For unit based duration a decimal value can only exist on the smallest unit(e.g. P2Y4.5M matches P2.5Y4M does not) | ||
// Similar constraint allows for only decimal seconds on date time based duration | ||
const DURATION_REGEX = /^(?<negated>-?)P(?!$)(?:(?:(?<yearUnit>-?\d+(?:\.\d+(?=Y$))?)Y)?(?:(?<monthUnit>-?\d+(?:\.\d+(?=M$))?)M)?(?:(?<dayUnit>-?\d+(?:\.\d+(?=D$))?)D)?(?:T(?=-?\d)(?:(?<hourUnit>-?\d+(?:\.\d+(?=H$))?)H)?(?:(?<minuteUnit>-?\d+(?:\.\d+(?=M$))?)M)?(?:(?<secondUnit>-?\d+(?:\.\d+(?=S$))?)S)?)?|(?<weekUnit>-?\d+(?:\.\d+)?)W|(?<yearDT>\d{4})(?<dateDelimiter>-?)(?<monthDT>[0]\d|1[0-2])\k<dateDelimiter>(?<dayDT>\d{2})T(?<hourDT>[01]\d|2[0-3])(?<timeDelimiter>(?:(?<=-\w+?):)|(?<=^-?\w+))(?<minuteDT>[0-5]\d)\k<timeDelimiter>(?<secondDT>[0-5]\d(?:\.\d+)?))$/; | ||
|
||
// Normalized components per https://neo4j.com/docs/cypher-manual/current/syntax/operators/#cypher-ordering | ||
const MONTHS_PER_YEAR = 12; | ||
const DAYS_PER_YEAR = 365.2425; | ||
const DAYS_PER_MONTH = DAYS_PER_YEAR / MONTHS_PER_YEAR; | ||
const DAYS_PER_WEEK = 7; | ||
const HOURS_PER_DAY = 24; | ||
const MINUTES_PER_HOUR = 60; | ||
const SECONDS_PER_MINUTE = 60; | ||
const SECONDS_PER_HOUR = SECONDS_PER_MINUTE * MINUTES_PER_HOUR; | ||
|
||
export const parseDuration = (value: string) => { | ||
const match = DURATION_REGEX.exec(value); | ||
|
||
if (!match) { | ||
throw new TypeError(`Value must be formatted as Duration: ${value}`); | ||
} | ||
|
||
const { | ||
negated, | ||
// P[nY][nM][nD][T[nH][nM][nS]] | ||
yearUnit = 0, | ||
monthUnit = 0, | ||
dayUnit = 0, | ||
hourUnit = 0, | ||
minuteUnit = 0, | ||
secondUnit = 0, | ||
// PnW | ||
weekUnit = 0, | ||
// PYYYYMMDDTHHMMSS | PYYYY-MM-DDTHH:MM:SS | ||
yearDT = 0, | ||
monthDT = 0, | ||
dayDT = 0, | ||
hourDT = 0, | ||
minuteDT = 0, | ||
secondDT = 0, | ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
} = match.groups!; | ||
|
||
// NOTE: xUnit and xDT cannot both be nonzero by construction => (xUnit + xDT) = xUnit | xDT | 0 | ||
const years = +yearUnit + +yearDT; | ||
const months = +monthUnit + +monthDT; | ||
const weeks = +weekUnit; | ||
const days = +dayUnit + +dayDT; | ||
const hours = +hourUnit + +hourDT; | ||
const minutes = +minuteUnit + +minuteDT; | ||
const seconds = +secondUnit + +secondDT; | ||
|
||
// Splits a component into a whole part and remainder | ||
const splitComponent = (component: number): [number, number] => [ | ||
Math.trunc(component), | ||
+(component % 1).toPrecision(9), | ||
]; | ||
|
||
// Calculate months based off of months and years | ||
const [wholeMonths, remainderMonths] = splitComponent(months + years * MONTHS_PER_YEAR); | ||
|
||
// Calculate days based off of days, weeks, and remainder of months | ||
const [wholeDays, remainderDays] = splitComponent(days + weeks * DAYS_PER_WEEK + remainderMonths * DAYS_PER_MONTH); | ||
|
||
// Calculate seconds based off of remainder of days, hours, minutes, and seconds | ||
const splitHoursInSeconds = splitComponent((hours + remainderDays * HOURS_PER_DAY) * SECONDS_PER_HOUR); | ||
const splitMinutesInSeconds = splitComponent(minutes * SECONDS_PER_MINUTE); | ||
const splitSeconds = splitComponent(seconds); | ||
// Total seconds by adding splits of hour minute second | ||
const [wholeSeconds, remainderSeconds] = splitHoursInSeconds.map( | ||
(p, i) => p + splitMinutesInSeconds[i] + splitSeconds[i] | ||
); | ||
|
||
// Calculate nanoseconds based off of remainder of seconds | ||
const wholeNanoseconds = +remainderSeconds.toFixed(9) * 1000000000; | ||
|
||
// Whether total duration is negative | ||
const coefficient = negated ? -1 : 1; | ||
// coefficient of duration and % may negate zero: converts -0 -> 0 | ||
const unsignZero = (a: number) => (Object.is(a, -0) ? 0 : a); | ||
|
||
return { | ||
months: unsignZero(coefficient * wholeMonths), | ||
days: unsignZero(coefficient * wholeDays), | ||
seconds: unsignZero(coefficient * wholeSeconds), | ||
nanoseconds: unsignZero(coefficient * wholeNanoseconds), | ||
}; | ||
}; | ||
|
||
const parse = (value: any) => { | ||
const { months, days, seconds, nanoseconds } = parseDuration(value); | ||
|
||
return new neo4j.types.Duration(months, days, seconds, nanoseconds); | ||
}; | ||
|
||
export default new GraphQLScalarType({ | ||
name: "Duration", | ||
description: "A duration, represented as an ISO 8601 duration string", | ||
serialize: (value: any) => { | ||
if (!(typeof value === "string" || value instanceof neo4j.types.Duration)) { | ||
throw new TypeError(`Value must be of type string: ${value}`); | ||
} | ||
|
||
if (value instanceof neo4j.types.Duration) { | ||
return value.toString(); | ||
} | ||
|
||
if (!DURATION_REGEX.test(value)) { | ||
throw new TypeError(`Value must be formatted as Duration: ${value}`); | ||
} | ||
|
||
return value; | ||
}, | ||
parseValue: (value) => { | ||
if (typeof value !== "string") { | ||
throw new GraphQLError(`Only strings can be validated as Duration, but received: ${value}`); | ||
} | ||
|
||
return parse(value); | ||
}, | ||
parseLiteral: (ast) => { | ||
if (ast.kind !== Kind.STRING) { | ||
throw new GraphQLError(`Only strings can be validated as Duration, but received: ${ast.kind}`); | ||
} | ||
return parse(ast.value); | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.