Skip to content

Commit bee5eff

Browse files
authored
Unify Duration String Representation With Cypher (#1284)
Ensures duration stringification simpliefies seconds to whole minutes and hours, and months to whole years
1 parent 022d6bd commit bee5eff

File tree

4 files changed

+167
-54
lines changed

4 files changed

+167
-54
lines changed

packages/core/src/internal/temporal-util.ts

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,52 @@ export const DAYS_0000_TO_1970 = 719528
8484
export const DAYS_PER_400_YEAR_CYCLE = 146097
8585
export const SECONDS_PER_DAY = 86400
8686

87+
export function normalizeYearsForDuration (
88+
months: NumberOrInteger | string
89+
): Integer {
90+
return int(months).div(12)
91+
}
92+
93+
export function normalizeMonthsForDuration (
94+
months: NumberOrInteger | string
95+
): Integer {
96+
return int(months).modulo(12)
97+
}
98+
99+
export function normalizeHoursForDuration (
100+
seconds: NumberOrInteger | string,
101+
nanoseconds: NumberOrInteger | string
102+
): Integer {
103+
if (int(nanoseconds).greaterThan(0) && int(seconds).lessThan(0)) {
104+
seconds = int(seconds).add(1)
105+
}
106+
return int(seconds).div(SECONDS_PER_HOUR)
107+
}
108+
109+
export function normalizeMinutesForDuration (
110+
seconds: NumberOrInteger | string,
111+
nanoseconds: NumberOrInteger | string
112+
): Integer {
113+
if (int(nanoseconds).greaterThan(0) && int(seconds).lessThan(0)) {
114+
seconds = int(seconds).add(1)
115+
}
116+
let minutes = int(seconds).div(SECONDS_PER_MINUTE)
117+
const negativeMinutes = minutes.isNegative()
118+
if (negativeMinutes) {
119+
minutes = minutes.negate()
120+
}
121+
return floorMod(minutes, MINUTES_PER_HOUR).multiply(negativeMinutes ? -1 : 1)
122+
}
123+
87124
export function normalizeSecondsForDuration (
88-
seconds: number | Integer | bigint,
89-
nanoseconds: number | Integer | bigint
125+
seconds: NumberOrInteger | string,
126+
nanoseconds: NumberOrInteger | string
90127
): Integer {
91128
return int(seconds).add(floorDiv(nanoseconds, NANOS_PER_SECOND))
92129
}
93130

94131
export function normalizeNanosecondsForDuration (
95-
nanoseconds: number | Integer | bigint
132+
nanoseconds: NumberOrInteger | string
96133
): Integer {
97134
return floorMod(nanoseconds, NANOS_PER_SECOND)
98135
}
@@ -212,13 +249,24 @@ export function durationToIsoString (
212249
seconds: NumberOrInteger | string,
213250
nanoseconds: NumberOrInteger | string
214251
): string {
215-
const monthsString = formatNumber(months)
216-
const daysString = formatNumber(days)
252+
if (int(months).equals(0) && int(days).equals(0) && int(seconds).equals(0) && int(nanoseconds).equals(0)) {
253+
return 'PT0S'
254+
}
255+
const yearString = formatNumber(normalizeYearsForDuration(months))
256+
const monthString = formatNumber(normalizeMonthsForDuration(months))
257+
const dayString = formatNumber(days)
258+
const hourString = formatNumber(normalizeHoursForDuration(seconds, nanoseconds))
259+
const minuteString = formatNumber(normalizeMinutesForDuration(seconds, nanoseconds))
217260
const secondsAndNanosecondsString = formatSecondsAndNanosecondsForDuration(
218261
seconds,
219262
nanoseconds
220263
)
221-
return `P${monthsString}M${daysString}DT${secondsAndNanosecondsString}S`
264+
return `P${yearString !== '0' ? yearString + 'Y' : ''}` +
265+
`${monthString !== '0' ? monthString + 'M' : ''}` +
266+
`${dayString !== '0' ? dayString + 'D' : ''}T` +
267+
`${hourString !== '0' ? hourString + 'H' : ''}` +
268+
`${minuteString !== '0' ? minuteString + 'M' : ''}` +
269+
`${secondsAndNanosecondsString !== '0' ? secondsAndNanosecondsString + 'S' : ''}`
222270
}
223271

224272
/**
@@ -596,13 +644,14 @@ function formatSecondsAndNanosecondsForDuration (
596644
const secondsNegative = seconds.isNegative()
597645
const nanosecondsGreaterThanZero = nanoseconds.greaterThan(0)
598646
if (secondsNegative && nanosecondsGreaterThanZero) {
599-
if (seconds.equals(-1)) {
647+
seconds = seconds.add(1).negate().modulo(60).negate()
648+
if (seconds.equals(0)) {
600649
secondsString = '-0'
601650
} else {
602-
secondsString = seconds.add(1).toString()
651+
secondsString = seconds.toString()
603652
}
604653
} else {
605-
secondsString = seconds.toString()
654+
secondsString = seconds.modulo(60).toString()
606655
}
607656

608657
if (nanosecondsGreaterThanZero) {

packages/neo4j-driver-deno/lib/core/internal/temporal-util.ts

Lines changed: 58 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/neo4j-driver/test/internal/temporal-util.test.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ describe('#unit temporal-util', () => {
3434
expect(util.normalizeSecondsForDuration(12345, 6789)).toEqual(int(12345))
3535

3636
expect(util.normalizeSecondsForDuration(-1, 42)).toEqual(int(-1))
37+
expect(util.normalizeSecondsForDuration(1, -42)).toEqual(int(0))
3738
expect(util.normalizeSecondsForDuration(-42, 4242)).toEqual(int(-42))
3839
expect(util.normalizeSecondsForDuration(-123, 999)).toEqual(int(-123))
3940

@@ -149,10 +150,13 @@ describe('#unit temporal-util', () => {
149150
})
150151

151152
it('should convert duration to ISO string', () => {
152-
expect(util.durationToIsoString(0, 0, 0, 0)).toEqual('P0M0DT0S')
153-
expect(util.durationToIsoString(0, 0, 0, 123)).toEqual('P0M0DT0.000000123S')
153+
expect(util.durationToIsoString(0, 0, 0, 0)).toEqual('PT0S')
154+
expect(util.durationToIsoString(0, 0, 0, 123)).toEqual('PT0.000000123S')
154155
expect(util.durationToIsoString(11, 99, 100, 99901)).toEqual(
155-
'P11M99DT100.000099901S'
156+
'P11M99DT1M40.000099901S'
157+
)
158+
expect(util.durationToIsoString(13, 99, 100, 99901)).toEqual(
159+
'P1Y1M99DT1M40.000099901S'
156160
)
157161
expect(
158162
util.durationToIsoString(int(3), int(9191), int(17), int(123456789))

packages/neo4j-driver/test/temporal-types.test.js

Lines changed: 44 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -558,9 +558,9 @@ describe('#integration temporal-types', () => {
558558

559559
it('should convert Duration to ISO string', () => {
560560
expect(duration(13, 62, 3, 999111999).toString()).toEqual(
561-
'P13M62DT3.999111999S'
561+
'P1Y1M62DT3.999111999S'
562562
)
563-
expect(duration(0, 0, 0, 0).toString()).toEqual('P0M0DT0S')
563+
expect(duration(0, 0, 0, 0).toString()).toEqual('PT0S')
564564
expect(duration(-1, -2, 10, 10).toString()).toEqual('P-1M-2DT10.000000010S')
565565
}, 90000)
566566

@@ -713,114 +713,125 @@ describe('#integration temporal-types', () => {
713713
}
714714

715715
await testDurationToString([
716-
{ duration: duration(0, 0, 0, 0), expectedString: 'P0M0DT0S' },
716+
{ duration: duration(0, 0, 0, 0), expectedString: 'PT0S' },
717717

718-
{ duration: duration(0, 0, 42, 0), expectedString: 'P0M0DT42S' },
719-
{ duration: duration(0, 0, -42, 0), expectedString: 'P0M0DT-42S' },
720-
{ duration: duration(0, 0, 1, 0), expectedString: 'P0M0DT1S' },
721-
{ duration: duration(0, 0, -1, 0), expectedString: 'P0M0DT-1S' },
718+
{ duration: duration(0, 0, 42, 0), expectedString: 'PT42S' },
719+
{ duration: duration(0, 0, -42, 0), expectedString: 'PT-42S' },
720+
{ duration: duration(0, 0, 1, 0), expectedString: 'PT1S' },
721+
{ duration: duration(0, 0, -1, 0), expectedString: 'PT-1S' },
722722

723723
{
724724
duration: duration(0, 0, 0, 5),
725-
expectedString: 'P0M0DT0.000000005S'
725+
expectedString: 'PT0.000000005S'
726726
},
727727
{
728728
duration: duration(0, 0, 0, -5),
729-
expectedString: 'P0M0DT-0.000000005S'
729+
expectedString: 'PT-0.000000005S'
730730
},
731731
{
732732
duration: duration(0, 0, 0, 999999999),
733-
expectedString: 'P0M0DT0.999999999S'
733+
expectedString: 'PT0.999999999S'
734734
},
735735
{
736736
duration: duration(0, 0, 0, -999999999),
737-
expectedString: 'P0M0DT-0.999999999S'
737+
expectedString: 'PT-0.999999999S'
738738
},
739739

740740
{
741741
duration: duration(0, 0, 1, 5),
742-
expectedString: 'P0M0DT1.000000005S'
742+
expectedString: 'PT1.000000005S'
743743
},
744744
{
745745
duration: duration(0, 0, -1, -5),
746-
expectedString: 'P0M0DT-1.000000005S'
746+
expectedString: 'PT-1.000000005S'
747747
},
748748
{
749749
duration: duration(0, 0, 1, -5),
750-
expectedString: 'P0M0DT0.999999995S'
750+
expectedString: 'PT0.999999995S'
751751
},
752752
{
753753
duration: duration(0, 0, -1, 5),
754-
expectedString: 'P0M0DT-0.999999995S'
754+
expectedString: 'PT-0.999999995S'
755755
},
756756
{
757757
duration: duration(0, 0, 1, 999999999),
758-
expectedString: 'P0M0DT1.999999999S'
758+
expectedString: 'PT1.999999999S'
759759
},
760760
{
761761
duration: duration(0, 0, -1, -999999999),
762-
expectedString: 'P0M0DT-1.999999999S'
762+
expectedString: 'PT-1.999999999S'
763763
},
764764
{
765765
duration: duration(0, 0, 1, -999999999),
766-
expectedString: 'P0M0DT0.000000001S'
766+
expectedString: 'PT0.000000001S'
767767
},
768768
{
769769
duration: duration(0, 0, -1, 999999999),
770-
expectedString: 'P0M0DT-0.000000001S'
770+
expectedString: 'PT-0.000000001S'
771771
},
772772

773773
{
774774
duration: duration(0, 0, 28, 9),
775-
expectedString: 'P0M0DT28.000000009S'
775+
expectedString: 'PT28.000000009S'
776776
},
777777
{
778778
duration: duration(0, 0, -28, 9),
779-
expectedString: 'P0M0DT-27.999999991S'
779+
expectedString: 'PT-27.999999991S'
780780
},
781781
{
782782
duration: duration(0, 0, 28, -9),
783-
expectedString: 'P0M0DT27.999999991S'
783+
expectedString: 'PT27.999999991S'
784784
},
785785
{
786786
duration: duration(0, 0, -28, -9),
787-
expectedString: 'P0M0DT-28.000000009S'
787+
expectedString: 'PT-28.000000009S'
788788
},
789789

790790
{
791791
duration: duration(0, 0, -78036, -143000000),
792-
expectedString: 'P0M0DT-78036.143000000S'
792+
expectedString: 'PT-21H-40M-36.143000000S'
793793
},
794794

795-
{ duration: duration(0, 0, 0, 1000000000), expectedString: 'P0M0DT1S' },
795+
{ duration: duration(0, 0, 0, 1000000000), expectedString: 'PT1S' },
796796
{
797797
duration: duration(0, 0, 0, -1000000000),
798-
expectedString: 'P0M0DT-1S'
798+
expectedString: 'PT-1S'
799799
},
800800
{
801801
duration: duration(0, 0, 0, 1000000007),
802-
expectedString: 'P0M0DT1.000000007S'
802+
expectedString: 'PT1.000000007S'
803803
},
804804
{
805805
duration: duration(0, 0, 0, -1000000007),
806-
expectedString: 'P0M0DT-1.000000007S'
806+
expectedString: 'PT-1.000000007S'
807+
},
808+
{
809+
duration: duration(0, 0, 1, -1000000007),
810+
expectedString: 'PT-0.000000007S'
811+
},
812+
{
813+
duration: duration(0, 0, -60, 1),
814+
expectedString: 'PT-59.999999999S'
815+
},
816+
{
817+
duration: duration(0, 0, -60, -1),
818+
expectedString: 'PT-1M-0.000000001S'
807819
},
808-
809820
{
810821
duration: duration(0, 0, 40, 2123456789),
811-
expectedString: 'P0M0DT42.123456789S'
822+
expectedString: 'PT42.123456789S'
812823
},
813824
{
814825
duration: duration(0, 0, -40, 2123456789),
815-
expectedString: 'P0M0DT-37.876543211S'
826+
expectedString: 'PT-37.876543211S'
816827
},
817828
{
818829
duration: duration(0, 0, 40, -2123456789),
819-
expectedString: 'P0M0DT37.876543211S'
830+
expectedString: 'PT37.876543211S'
820831
},
821832
{
822833
duration: duration(0, 0, -40, -2123456789),
823-
expectedString: 'P0M0DT-42.123456789S'
834+
expectedString: 'PT-42.123456789S'
824835
}
825836
])
826837
}, 90000)

0 commit comments

Comments
 (0)