Skip to content

Commit e2e98ec

Browse files
aheissenbergerbwrrp
authored andcommitted
Add addition and substraction of xs:dayTimeDuration()
1 parent 9fe4afc commit e2e98ec

8 files changed

Lines changed: 345 additions & 52 deletions

File tree

src/expressions/dataTypes/valueTypes/AbstractDuration.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ abstract class AbstractDuration {
4040
public isPositive() {
4141
return true;
4242
}
43+
public negate() {
44+
return this;
45+
}
4346
}
4447

4548
export default AbstractDuration;

src/expressions/dataTypes/valueTypes/DateTime.ts

Lines changed: 101 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -466,14 +466,109 @@ export function subtract(
466466
return new DayTimeDuration(secondsOfDuration);
467467
}
468468

469-
export function addDuration(dateTime: DateTime, _duration: AbstractDuration): DateTime {
470-
throw new Error(`Not implemented: adding durations to ${valueTypeToString(dateTime.type)}`);
469+
export function evalDuration(dateTime: DateTime, duration: AbstractDuration): DateTime {
470+
const tz = dateTime.getTimezone();
471+
472+
let years = dateTime.getYear();
473+
let months = dateTime.getMonth();
474+
let days = dateTime.getDay();
475+
let hours = dateTime.getHours();
476+
let minutes = dateTime.getMinutes();
477+
let seconds = dateTime.getSeconds();
478+
const fraction = dateTime.getSecondFraction();
479+
480+
// Add years and months
481+
years += duration.getYears();
482+
months += duration.getMonths();
483+
484+
// Normalize months
485+
while (months > 12) {
486+
months -= 12;
487+
years += 1;
488+
}
489+
while (months < 1) {
490+
months += 12;
491+
years -= 1;
492+
}
493+
494+
function isLeapYear(year: number): boolean {
495+
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
496+
}
497+
function getLastDayOfMonth(year: number, month: number): number {
498+
if (month === 2) {
499+
return isLeapYear(year) ? 29 : 28;
500+
}
501+
return [4, 6, 9, 11].includes(month) ? 30 : 31;
502+
}
503+
504+
const originalLastDay = getLastDayOfMonth(dateTime.getYear(), dateTime.getMonth());
505+
const originalWasLastDay = dateTime.getDay() === originalLastDay;
506+
507+
// Clamp day to last valid day of new month/year ONLY if original date was last day of its month
508+
const newLastDay = getLastDayOfMonth(years, months);
509+
if (originalWasLastDay) {
510+
days = newLastDay;
511+
}
512+
513+
// Add days, hours, minutes, seconds, fraction
514+
days += duration.getDays();
515+
hours += duration.getHours();
516+
minutes += duration.getMinutes();
517+
seconds += duration.getSeconds();
518+
519+
// Normalize seconds
520+
if (seconds >= 60) {
521+
minutes += Math.floor(seconds / 60);
522+
seconds = seconds % 60;
523+
} else if (seconds < 0) {
524+
minutes -= Math.ceil(Math.abs(seconds) / 60);
525+
seconds = ((seconds % 60) + 60) % 60;
526+
}
527+
528+
// Normalize minutes
529+
if (minutes >= 60) {
530+
hours += Math.floor(minutes / 60);
531+
minutes = minutes % 60;
532+
} else if (minutes < 0) {
533+
hours -= Math.ceil(Math.abs(minutes) / 60);
534+
minutes = ((minutes % 60) + 60) % 60;
535+
}
536+
537+
// Normalize hours
538+
if (hours >= 24) {
539+
days += Math.floor(hours / 24);
540+
hours = hours % 24;
541+
} else if (hours < 0) {
542+
days -= Math.ceil(Math.abs(hours) / 24);
543+
hours = ((hours % 24) + 24) % 24;
544+
}
545+
546+
while (days > getLastDayOfMonth(years, months)) {
547+
days -= getLastDayOfMonth(years, months);
548+
months += 1;
549+
}
550+
while (days < 1) {
551+
months -= 1;
552+
days += getLastDayOfMonth(years, months);
553+
}
554+
555+
while (months > 12) {
556+
months -= 12;
557+
years += 1;
558+
}
559+
while (months < 1) {
560+
months += 12;
561+
years -= 1;
562+
}
563+
564+
return new DateTime(years, months, days, hours, minutes, seconds, fraction, tz, dateTime.type);
565+
}
566+
export function addDuration(dateTime: DateTime, duration: AbstractDuration): DateTime {
567+
return evalDuration(dateTime, duration);
471568
}
472569

473-
export function subtractDuration(dateTime: DateTime, _duration: AbstractDuration): DateTime {
474-
throw new Error(
475-
`Not implemented: subtracting durations from ${valueTypeToString(dateTime.type)}`,
476-
);
570+
export function subtractDuration(dateTime: DateTime, duration: AbstractDuration): DateTime {
571+
return evalDuration(dateTime, duration.negate());
477572
}
478573

479574
export default DateTime;

src/expressions/dataTypes/valueTypes/DayTimeDuration.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ class DayTimeDuration extends AbstractDuration {
5050
return Object.is(-0, this.seconds) ? false : this.seconds >= 0;
5151
}
5252

53+
public negate(): this {
54+
return new (this.constructor as any)(-this.seconds);
55+
}
56+
5357
public toString() {
5458
return (this.isPositive() ? 'P' : '-P') + this.toStringWithoutP();
5559
}

src/expressions/dataTypes/valueTypes/Duration.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,13 @@ class Duration extends AbstractDuration {
145145
return this._yearMonthDuration.isPositive() && this._dayTimeDuration.isPositive();
146146
}
147147

148+
public negate(): this {
149+
return new (this.constructor as any)(
150+
this._yearMonthDuration.negate(),
151+
this._dayTimeDuration.negate(),
152+
);
153+
}
154+
148155
public toString() {
149156
const durationString = this.isPositive() ? 'P' : '-P';
150157
const TYM = this._yearMonthDuration.toStringWithoutP();

src/expressions/dataTypes/valueTypes/YearMonthDuration.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ class YearMonthDuration extends AbstractDuration {
3333
return Object.is(-0, this.months) ? false : this.months >= 0;
3434
}
3535

36+
public negate(): this {
37+
return new (this.constructor as any)(-this.months);
38+
}
39+
3640
public toString() {
3741
return (this.isPositive() ? 'P' : '-P') + this.toStringWithoutP();
3842
}

test/assets/unrunnableTestCases.csv

Lines changed: 0 additions & 45 deletions
Large diffs are not rendered by default.

test/specs/expressions/dataTypes/valueTypes/DateTime.tests.ts

Lines changed: 199 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import * as chai from 'chai';
2-
import DateTime from 'fontoxpath/expressions/dataTypes/valueTypes/DateTime';
2+
import DateTime, {
3+
addDuration,
4+
subtractDuration,
5+
} from 'fontoxpath/expressions/dataTypes/valueTypes/DateTime';
36
import DayTimeDuration from 'fontoxpath/expressions/dataTypes/valueTypes/DayTimeDuration';
7+
import Duration from 'fontoxpath/expressions/dataTypes/valueTypes/Duration';
48

59
describe('Data type: dateTime', () => {
610
describe('DateTime.fromString()', () => {
@@ -52,5 +56,199 @@ describe('Data type: dateTime', () => {
5256
),
5357
);
5458
});
59+
60+
it('addDuration "P1Y2M" to "1999-12-31T23:00:00+10:00"', () => {
61+
const dateTime = DateTime.fromString('1999-12-31T23:00:00+10:00');
62+
const duration = Duration.fromString('P1Y2M');
63+
const newDateTime = addDuration(dateTime, duration);
64+
chai.assert.deepEqual(
65+
newDateTime,
66+
new DateTime(
67+
2001,
68+
2,
69+
28,
70+
23,
71+
0,
72+
0,
73+
0,
74+
DayTimeDuration.fromTimezoneString('+10:00'),
75+
),
76+
);
77+
});
78+
79+
it('addDuration "P3DT1H15M" to "1999-12-31T23:00:00+10:00"', () => {
80+
const dateTime = DateTime.fromString('1999-12-31T23:00:00+10:00');
81+
const duration = Duration.fromString('P3DT1H15M');
82+
const newDateTime = addDuration(dateTime, duration);
83+
chai.assert.deepEqual(
84+
newDateTime,
85+
// eslint-disable-next-line prettier/prettier
86+
new DateTime(
87+
2000,
88+
1,
89+
4,
90+
0,
91+
15,
92+
0,
93+
0,
94+
DayTimeDuration.fromTimezoneString('+10:00'),
95+
),
96+
);
97+
});
98+
99+
it('addDuration "PT505H" to "1999-12-31T23:00:00+10:00"', () => {
100+
const dateTime = DateTime.fromString('1999-12-31T23:00:00+10:00');
101+
const duration = Duration.fromString('PT505H');
102+
const newDateTime = addDuration(dateTime, duration);
103+
chai.assert.deepEqual(
104+
newDateTime,
105+
// eslint-disable-next-line prettier/prettier
106+
new DateTime(
107+
2000,
108+
1,
109+
22,
110+
0,
111+
0,
112+
0,
113+
0,
114+
DayTimeDuration.fromTimezoneString('+10:00'),
115+
),
116+
);
117+
});
118+
119+
it('addDuration "P60D" to "1999-12-31T23:00:00+10:00"', () => {
120+
const dateTime = DateTime.fromString('1999-12-31T23:00:00+10:00');
121+
const duration = Duration.fromString('P60D');
122+
const newDateTime = addDuration(dateTime, duration);
123+
chai.assert.deepEqual(
124+
newDateTime,
125+
// eslint-disable-next-line prettier/prettier
126+
new DateTime(
127+
2000,
128+
2,
129+
29,
130+
23,
131+
0,
132+
0,
133+
0,
134+
DayTimeDuration.fromTimezoneString('+10:00'),
135+
),
136+
);
137+
});
138+
139+
it('addDuration with negative "-P3DT1H15M" to "2000-01-04T00:15:00+10:00"', () => {
140+
const dateTime = DateTime.fromString('2000-01-04T00:15:00+10:00');
141+
const duration = Duration.fromString('-P3DT1H15M');
142+
const newDateTime = addDuration(dateTime, duration);
143+
chai.assert.deepEqual(
144+
newDateTime,
145+
new DateTime(
146+
1999,
147+
12,
148+
31,
149+
23,
150+
0,
151+
0,
152+
0,
153+
DayTimeDuration.fromTimezoneString('+10:00'),
154+
),
155+
);
156+
});
157+
158+
it('subDuration "P1Y2M" from "2001-02-28T23:00:00+10:00"', () => {
159+
const dateTime = DateTime.fromString('2001-02-28T23:00:00+10:00');
160+
const duration = Duration.fromString('P1Y2M');
161+
const newDateTime = subtractDuration(dateTime, duration);
162+
chai.assert.deepEqual(
163+
newDateTime,
164+
new DateTime(
165+
1999,
166+
12,
167+
31,
168+
23,
169+
0,
170+
0,
171+
0,
172+
DayTimeDuration.fromTimezoneString('+10:00'),
173+
),
174+
);
175+
});
176+
it('subDuration "P3DT1H15M" from "2000-01-04T00:15:00+10:00"', () => {
177+
const dateTime = DateTime.fromString('2000-01-04T00:15:00+10:00');
178+
const duration = Duration.fromString('P3DT1H15M');
179+
const newDateTime = subtractDuration(dateTime, duration);
180+
chai.assert.deepEqual(
181+
newDateTime,
182+
new DateTime(
183+
1999,
184+
12,
185+
31,
186+
23,
187+
0,
188+
0,
189+
0,
190+
DayTimeDuration.fromTimezoneString('+10:00'),
191+
),
192+
);
193+
});
194+
195+
it('subDuration negativ "-P3DT1H15M" to "1999-12-31T23:00:00+10:00"', () => {
196+
const dateTime = DateTime.fromString('1999-12-31T23:00:00+10:00');
197+
const duration = Duration.fromString('-P3DT1H15M');
198+
const newDateTime = subtractDuration(dateTime, duration);
199+
chai.assert.deepEqual(
200+
newDateTime,
201+
// eslint-disable-next-line prettier/prettier
202+
new DateTime(
203+
2000,
204+
1,
205+
4,
206+
0,
207+
15,
208+
0,
209+
0,
210+
DayTimeDuration.fromTimezoneString('+10:00'),
211+
),
212+
);
213+
});
214+
});
215+
it('subDuration "PT505H" to "2000-01-22T00:00:00+10:00"', () => {
216+
const dateTime = DateTime.fromString('2000-01-22T00:00:00+10:00');
217+
const duration = Duration.fromString('PT505H');
218+
const newDateTime = subtractDuration(dateTime, duration);
219+
chai.assert.deepEqual(
220+
newDateTime,
221+
// eslint-disable-next-line prettier/prettier
222+
new DateTime(
223+
1999,
224+
12,
225+
31,
226+
23,
227+
0,
228+
0,
229+
0,
230+
DayTimeDuration.fromTimezoneString('+10:00'),
231+
),
232+
);
233+
});
234+
235+
it('subDuration "P60D" to "2000-02-29T23:00:00+10:00"', () => {
236+
const dateTime = DateTime.fromString('2000-02-29T23:00:00+10:00');
237+
const duration = Duration.fromString('P60D');
238+
const newDateTime = subtractDuration(dateTime, duration);
239+
chai.assert.deepEqual(
240+
newDateTime,
241+
// eslint-disable-next-line prettier/prettier
242+
new DateTime(
243+
1999,
244+
12,
245+
31,
246+
23,
247+
0,
248+
0,
249+
0,
250+
DayTimeDuration.fromTimezoneString('+10:00'),
251+
),
252+
);
55253
});
56254
});

0 commit comments

Comments
 (0)