• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

FontoXML / fontoxpath / 16482076398

23 Jul 2025 09:20PM UTC coverage: 91.572% (-0.004%) from 91.576%
16482076398

Pull #677

github

web-flow
Merge a8664b354 into d5ac00463
Pull Request #677: Add addition and substraction of xs:dayTimeDuration()

5021 of 5832 branches covered (86.09%)

Branch coverage included in aggregate %.

60 of 69 new or added lines in 5 files covered. (86.96%)

10854 of 11504 relevant lines covered (94.35%)

95958.25 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.4
/src/expressions/dataTypes/valueTypes/DateTime.ts
1
import { ValueType, valueTypeToString } from '../Value';
2
import AbstractDuration from './AbstractDuration';
3
import DayTimeDuration from './DayTimeDuration';
15✔
4

5
function parseMatch(match: string | undefined): number | null {
6
        return match ? parseInt(match, 10) : null;
9,630✔
7
}
8

9
function convertYearToString(year: number): string {
10
        let yearString = year + '';
63✔
11
        const isNegative = yearString.startsWith('-');
63✔
12
        if (isNegative) {
63!
13
                yearString = yearString.substring(1);
×
14
        }
15
        return (isNegative ? '-' : '') + yearString.padStart(4, '0');
63!
16
}
17

18
function convertToTwoCharString(value: number): string {
19
        const valueString = value + '';
315✔
20
        return valueString.padStart(2, '0');
315✔
21
}
22

23
function convertSecondsToString(seconds: number): string {
24
        let secondsString = seconds + '';
39✔
25
        if (secondsString.split('.')[0].length === 1) {
39✔
26
                secondsString = secondsString.padStart(secondsString.length + 1, '0');
6✔
27
        }
28
        return secondsString;
39✔
29
}
30

31
function isUTC(timezone: DayTimeDuration): boolean {
32
        return timezone.getHours() === 0 && timezone.getMinutes() === 0;
96✔
33
}
34

35
function timezoneToString(timezone: DayTimeDuration): string {
36
        if (isUTC(timezone)) {
96✔
37
                return 'Z';
45✔
38
        }
39

40
        return (
51✔
41
                (timezone.isPositive() ? '+' : '-') +
51!
42
                convertToTwoCharString(Math.abs(timezone.getHours())) +
43
                ':' +
44
                convertToTwoCharString(Math.abs(timezone.getMinutes()))
45
        );
46
}
47

48
class DateTime {
49
        public secondFraction: number;
50
        public type: ValueType;
51
        protected _days: number;
52
        protected _hours: number;
53
        protected _minutes: number;
54
        protected _months: number;
55
        protected _seconds: number;
56
        protected _timezone: DayTimeDuration;
57
        protected _years: number;
58
        constructor(
59
                years: number,
60
                months: number,
61
                days: number,
62
                hours: number,
63
                minutes: number,
64
                seconds: number,
65
                secondFraction: number,
66
                timezone: DayTimeDuration,
67
                type: ValueType = ValueType.XSDATETIME,
51✔
68
        ) {
69
                this._years = years;
2,151✔
70
                this._months = months;
2,151✔
71
                this._days = days + (hours === 24 ? 1 : 0);
2,151✔
72
                this._hours = hours === 24 ? 0 : hours;
2,151✔
73
                this._minutes = minutes;
2,151✔
74
                this._seconds = seconds;
2,151✔
75
                this.secondFraction = secondFraction;
2,151✔
76
                this._timezone = timezone;
2,151✔
77

78
                this.type = type;
2,151✔
79
        }
80

81
        // dateTime    | (-)yyyy-mm-ddThh:mm:ss.ss(Z|[+-]hh:mm)
82
        // time        |               hh:mm:ss.ss(Z|[+-]hh:mm)
83
        // date        | (-)yyyy-mm-dd            (Z|[+-]hh:mm)
84
        // gYearMonth  | (-)yyyy-mm               (Z|[+-]hh:mm)
85
        // gYear       | (-)yyyy                  (Z|[+-]hh:mm)
86
        // gMonthDay   |       --mm-dd            (Z|[+-]hh:mm)
87
        // gDay        |         ---dd            (Z|[+-]hh:mm)
88
        // gMonth      |       --mm               (Z|[+-]hh:mm)
89
        public static fromString(dateString: string): DateTime {
90
                const regex =
91
                        /^(?:(-?\d{4,}))?(?:--?(\d\d))?(?:-{1,3}(\d\d))?(T)?(?:(\d\d):(\d\d):(\d\d))?(\.\d+)?(Z|(?:[+-]\d\d:\d\d))?$/;
1,926✔
92
                const match = regex.exec(dateString);
1,926✔
93

94
                const years = match[1] ? parseInt(match[1], 10) : null;
1,926✔
95
                const months = parseMatch(match[2]);
1,926✔
96
                const days = parseMatch(match[3]);
1,926✔
97
                const t = match[4];
1,926✔
98
                const hours = parseMatch(match[5]);
1,926✔
99
                const minutes = parseMatch(match[6]);
1,926✔
100
                const seconds = parseMatch(match[7]);
1,926✔
101
                const secondFraction = match[8] ? parseFloat(match[8]) : 0;
1,926✔
102
                const timezone = match[9] ? DayTimeDuration.fromTimezoneString(match[9]) : null;
1,926✔
103

104
                if (years && (years < -271821 || years > 273860)) {
1,926!
105
                        // These are the JavaScript bounds for date (https://tc39.github.io/ecma262/#sec-time-values-and-time-range)
106
                        throw new Error('FODT0001: Datetime year is out of bounds');
×
107
                }
108

109
                if (t) {
1,926✔
110
                        // There is a T separating the date and time components -> dateTime
111
                        return new DateTime(
822✔
112
                                years,
113
                                months,
114
                                days,
115
                                hours,
116
                                minutes,
117
                                seconds,
118
                                secondFraction,
119
                                timezone,
120
                                ValueType.XSDATETIME,
121
                        );
122
                }
123

124
                if (hours !== null && minutes !== null && seconds !== null) {
1,104✔
125
                        // There is no T separator, but there is a time component -> time
126
                        return new DateTime(
222✔
127
                                1972,
128
                                12,
129
                                31,
130
                                hours,
131
                                minutes,
132
                                seconds,
133
                                secondFraction,
134
                                timezone,
135
                                ValueType.XSTIME,
136
                        );
137
                }
138

139
                if (years !== null && months !== null && days !== null) {
882✔
140
                        // There is no T separator, but there is a complete date component -> date
141
                        return new DateTime(years, months, days, 0, 0, 0, 0, timezone, ValueType.XSDATE);
249✔
142
                }
143

144
                if (years !== null && months !== null) {
633✔
145
                        // There is no complete date component, but there is a year and a month -> gYearMonth
146
                        return new DateTime(years, months, 1, 0, 0, 0, 0, timezone, ValueType.XSGYEARMONTH);
135✔
147
                }
148

149
                if (months !== null && days !== null) {
498✔
150
                        // There is no complete date component, but there is a month and a day -> gMonthDay
151
                        return new DateTime(1972, months, days, 0, 0, 0, 0, timezone, ValueType.XSGMONTHDAY);
135✔
152
                }
153

154
                if (years !== null) {
363✔
155
                        // There is only a year -> gYear
156
                        return new DateTime(years, 1, 1, 0, 0, 0, 0, timezone, ValueType.XSGYEAR);
129✔
157
                }
158

159
                if (months !== null) {
234✔
160
                        // There is only a month -> gMonth
161
                        return new DateTime(1972, months, 1, 0, 0, 0, 0, timezone, ValueType.XSGMONTH);
117✔
162
                }
163

164
                // There is only one option left -> gDay
165
                return new DateTime(1972, 12, days, 0, 0, 0, 0, timezone, ValueType.XSGDAY);
117✔
166
        }
167

168
        public convertToType(type: ValueType) {
169
                // xs:date       xxxx-xx-xxT00:00:00
170
                // xs:time       1972-12-31Txx:xx:xx
171
                // xs:gYearMonth xxxx-xx-01T00:00:00
172
                // xs:gYear      xxxx-01-01T00:00:00
173
                // xs:gMonthDay  1972-xx-xxT00:00:00
174
                // xs:gMonth     1972-xx-01T00:00:00
175
                // xs:gDay       1972-12-xxT00:00:00
176

177
                switch (type) {
144✔
178
                        case ValueType.XSGDAY:
179
                                return new DateTime(
15✔
180
                                        1972,
181
                                        12,
182
                                        this._days,
183
                                        0,
184
                                        0,
185
                                        0,
186
                                        0,
187
                                        this._timezone,
188
                                        ValueType.XSGDAY,
189
                                );
190
                        case ValueType.XSGMONTH:
191
                                return new DateTime(
15✔
192
                                        1972,
193
                                        this._months,
194
                                        1,
195
                                        0,
196
                                        0,
197
                                        0,
198
                                        0,
199
                                        this._timezone,
200
                                        ValueType.XSGMONTH,
201
                                );
202
                        case ValueType.XSGYEAR:
203
                                return new DateTime(
15✔
204
                                        this._years,
205
                                        1,
206
                                        1,
207
                                        0,
208
                                        0,
209
                                        0,
210
                                        0,
211
                                        this._timezone,
212
                                        ValueType.XSGYEAR,
213
                                );
214
                        case ValueType.XSGMONTHDAY:
215
                                return new DateTime(
15✔
216
                                        1972,
217
                                        this._months,
218
                                        this._days,
219
                                        0,
220
                                        0,
221
                                        0,
222
                                        0,
223
                                        this._timezone,
224
                                        ValueType.XSGMONTHDAY,
225
                                );
226
                        case ValueType.XSGYEARMONTH:
227
                                return new DateTime(
15✔
228
                                        this._years,
229
                                        this._months,
230
                                        1,
231
                                        0,
232
                                        0,
233
                                        0,
234
                                        0,
235
                                        this._timezone,
236
                                        ValueType.XSGYEARMONTH,
237
                                );
238
                        case ValueType.XSTIME:
239
                                return new DateTime(
15✔
240
                                        1972,
241
                                        12,
242
                                        31,
243
                                        this._hours,
244
                                        this._minutes,
245
                                        this._seconds,
246
                                        this.secondFraction,
247
                                        this._timezone,
248
                                        ValueType.XSTIME,
249
                                );
250
                        case ValueType.XSDATE:
251
                                return new DateTime(
42✔
252
                                        this._years,
253
                                        this._months,
254
                                        this._days,
255
                                        0,
256
                                        0,
257
                                        0,
258
                                        0,
259
                                        this._timezone,
260
                                        ValueType.XSDATE,
261
                                );
262
                        case ValueType.XSDATETIME:
263
                        default:
264
                                return new DateTime(
12✔
265
                                        this._years,
266
                                        this._months,
267
                                        this._days,
268
                                        this._hours,
269
                                        this._minutes,
270
                                        this._seconds,
271
                                        this.secondFraction,
272
                                        this._timezone,
273
                                        ValueType.XSDATETIME,
274
                                );
275
                }
276
        }
277

278
        public getDay() {
279
                return this._days;
72✔
280
        }
281

282
        public getFullSeconds() {
283
                return this._seconds + this.secondFraction;
6✔
284
        }
285

286
        public getHours() {
287
                return this._hours;
42✔
288
        }
289

290
        public getMinutes() {
291
                return this._minutes;
42✔
292
        }
293

294
        public getMonth() {
295
                return this._months;
72✔
296
        }
297

298
        public getSecondFraction() {
299
                return this.secondFraction;
36✔
300
        }
301

302
        public getSeconds() {
303
                return this._seconds;
36✔
304
        }
305

306
        public getTimezone() {
307
                return this._timezone;
51✔
308
        }
309

310
        public getYear() {
311
                return this._years;
72✔
312
        }
313

314
        public isPositive() {
315
                return this._years >= 0;
×
316
        }
317

318
        public toJavaScriptDate(implicitTimezone?: DayTimeDuration): Date {
319
                const timezoneToUse =
320
                        this._timezone || implicitTimezone || DayTimeDuration.fromTimezoneString('Z');
648✔
321
                return new Date(
648✔
322
                        Date.UTC(
323
                                this._years,
324
                                this._months - 1,
325
                                this._days,
326
                                this._hours - timezoneToUse.getHours(),
327
                                this._minutes - timezoneToUse.getMinutes(),
328
                                this._seconds,
329
                                this.secondFraction * 1000,
330
                        ),
331
                );
332
        }
333

334
        public toString() {
335
                switch (this.type) {
102✔
336
                        case ValueType.XSDATETIME:
337
                                return (
27✔
338
                                        convertYearToString(this._years) +
339
                                        '-' +
340
                                        convertToTwoCharString(this._months) +
341
                                        '-' +
342
                                        convertToTwoCharString(this._days) +
343
                                        'T' +
344
                                        convertToTwoCharString(this._hours) +
345
                                        ':' +
346
                                        convertToTwoCharString(this._minutes) +
347
                                        ':' +
348
                                        convertSecondsToString(this._seconds + this.secondFraction) +
349
                                        (this._timezone ? timezoneToString(this._timezone) : '')
27✔
350
                                );
351
                        case ValueType.XSDATE:
352
                                return (
18✔
353
                                        convertYearToString(this._years) +
354
                                        '-' +
355
                                        convertToTwoCharString(this._months) +
356
                                        '-' +
357
                                        convertToTwoCharString(this._days) +
358
                                        (this._timezone ? timezoneToString(this._timezone) : '')
18!
359
                                );
360
                        case ValueType.XSTIME:
361
                                return (
12✔
362
                                        convertToTwoCharString(this._hours) +
363
                                        ':' +
364
                                        convertToTwoCharString(this._minutes) +
365
                                        ':' +
366
                                        convertSecondsToString(this._seconds + this.secondFraction) +
367
                                        (this._timezone ? timezoneToString(this._timezone) : '')
12!
368
                                );
369
                        case ValueType.XSGDAY:
370
                                return (
9✔
371
                                        '---' +
372
                                        convertToTwoCharString(this._days) +
373
                                        (this._timezone ? timezoneToString(this._timezone) : '')
9!
374
                                );
375
                        case ValueType.XSGMONTH:
376
                                return (
9✔
377
                                        '--' +
378
                                        convertToTwoCharString(this._months) +
379
                                        (this._timezone ? timezoneToString(this._timezone) : '')
9!
380
                                );
381
                        case ValueType.XSGMONTHDAY:
382
                                return (
9✔
383
                                        '--' +
384
                                        convertToTwoCharString(this._months) +
385
                                        '-' +
386
                                        convertToTwoCharString(this._days) +
387
                                        (this._timezone ? timezoneToString(this._timezone) : '')
9!
388
                                );
389
                        case ValueType.XSGYEAR:
390
                                return (
9✔
391
                                        convertYearToString(this._years) +
392
                                        (this._timezone ? timezoneToString(this._timezone) : '')
9!
393
                                );
394
                        case ValueType.XSGYEARMONTH:
395
                                return (
9✔
396
                                        convertYearToString(this._years) +
397
                                        '-' +
398
                                        convertToTwoCharString(this._months) +
399
                                        (this._timezone ? timezoneToString(this._timezone) : '')
9!
400
                                );
401
                }
402
                throw new Error('Unexpected subType');
×
403
        }
404
}
405

406
export function compare(
15✔
407
        dateTime1: DateTime,
408
        dateTime2: DateTime,
409
        implicitTimezone?: DayTimeDuration | null,
410
): number {
411
        const jsTime1 = dateTime1.toJavaScriptDate(implicitTimezone).getTime();
300✔
412
        const jsTime2 = dateTime2.toJavaScriptDate(implicitTimezone).getTime();
300✔
413

414
        if (jsTime1 === jsTime2) {
300✔
415
                // We should break the tie on the secondFraction property, which has no counterpart in JS dates
416
                if (dateTime1.secondFraction === dateTime2.secondFraction) {
138!
417
                        return 0;
138✔
418
                }
419
                if (dateTime1.secondFraction > dateTime2.secondFraction) {
×
420
                        return 1;
×
421
                }
422
                return -1;
×
423
        }
424

425
        if (jsTime1 > jsTime2) {
162✔
426
                return 1;
108✔
427
        }
428

429
        return -1;
54✔
430
}
431

432
export function equal(
15✔
433
        dateTime1: DateTime,
434
        dateTime2: DateTime,
435
        implicitTimezone?: DayTimeDuration | null,
436
): boolean {
437
        return compare(dateTime1, dateTime2, implicitTimezone) === 0;
150✔
438
}
439

440
export function lessThan(
15✔
441
        dateTime1: DateTime,
442
        dateTime2: DateTime,
443
        implicitTimezone?: DayTimeDuration | null,
444
): boolean {
445
        return compare(dateTime1, dateTime2, implicitTimezone) < 0;
75✔
446
}
447

448
export function greaterThan(
15✔
449
        dateTime1: DateTime,
450
        dateTime2: DateTime,
451
        implicitTimezone?: DayTimeDuration | null,
452
): boolean {
453
        return compare(dateTime1, dateTime2, implicitTimezone) > 0;
75✔
454
}
455

456
export function subtract(
15✔
457
        dateTime1: DateTime,
458
        dateTime2: DateTime,
459
        implicitTimezone?: DayTimeDuration | null,
460
): DayTimeDuration {
461
        // Divided by 1000 because date subtraction results in milliseconds
462
        const secondsOfDuration =
463
                (dateTime1.toJavaScriptDate(implicitTimezone).getTime() -
3✔
464
                        dateTime2.toJavaScriptDate(implicitTimezone).getTime()) /
465
                1000;
466
        return new DayTimeDuration(secondsOfDuration);
3✔
467
}
468

469
export function evalDuration(dateTime: DateTime, duration: AbstractDuration): DateTime {
15✔
470
        const tz = dateTime.getTimezone();
30✔
471

472
        let years = dateTime.getYear();
30✔
473
        let months = dateTime.getMonth();
30✔
474
        let days = dateTime.getDay();
30✔
475
        let hours = dateTime.getHours();
30✔
476
        let minutes = dateTime.getMinutes();
30✔
477
        let seconds = dateTime.getSeconds();
30✔
478
        const fraction = dateTime.getSecondFraction();
30✔
479

480
        // Add years and months
481
        years += duration.getYears();
30✔
482
        months += duration.getMonths();
30✔
483

484
        // Normalize months
485
        while (months > 12) {
30✔
486
                months -= 12;
3✔
487
                years += 1;
3✔
488
        }
489
        while (months < 1) {
30✔
490
                months += 12;
3✔
491
                years -= 1;
3✔
492
        }
493

494
        function isLeapYear(year: number): boolean {
495
                return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
18✔
496
        }
497
        function getLastDayOfMonth(year: number, month: number): number {
498
                if (month === 2) {
135✔
499
                        return isLeapYear(year) ? 29 : 28;
18✔
500
                }
501
                return [4, 6, 9, 11].includes(month) ? 30 : 31;
117!
502
        }
503

504
        const originalLastDay = getLastDayOfMonth(dateTime.getYear(), dateTime.getMonth());
30✔
505
        const originalWasLastDay = dateTime.getDay() === originalLastDay;
30✔
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);
30✔
509
        if (originalWasLastDay) {
30✔
510
                days = newLastDay;
21✔
511
        }
512

513
        // Add days, hours, minutes, seconds, fraction
514
        days += duration.getDays();
30✔
515
        hours += duration.getHours();
30✔
516
        minutes += duration.getMinutes();
30✔
517
        seconds += duration.getSeconds();
30✔
518

519
        // Normalize seconds
520
        if (seconds >= 60) {
30!
NEW
521
                minutes += Math.floor(seconds / 60);
×
NEW
522
                seconds = seconds % 60;
×
523
        } else if (seconds < 0) {
30!
NEW
524
                minutes -= Math.ceil(Math.abs(seconds) / 60);
×
NEW
525
                seconds = ((seconds % 60) + 60) % 60;
×
526
        }
527

528
        // Normalize minutes
529
        if (minutes >= 60) {
30!
NEW
530
                hours += Math.floor(minutes / 60);
×
NEW
531
                minutes = minutes % 60;
×
532
        } else if (minutes < 0) {
30!
NEW
533
                hours -= Math.ceil(Math.abs(minutes) / 60);
×
NEW
534
                minutes = ((minutes % 60) + 60) % 60;
×
535
        }
536

537
        // Normalize hours
538
        if (hours >= 24) {
30✔
539
                days += Math.floor(hours / 24);
9✔
540
                hours = hours % 24;
9✔
541
        } else if (hours < 0) {
21✔
542
                days -= Math.ceil(Math.abs(hours) / 24);
9✔
543
                hours = ((hours % 24) + 24) % 24;
9✔
544
        }
545

546
        while (days > getLastDayOfMonth(years, months)) {
30✔
547
                days -= getLastDayOfMonth(years, months);
15✔
548
                months += 1;
15✔
549
        }
550
        while (days < 1) {
30✔
551
                months -= 1;
15✔
552
                days += getLastDayOfMonth(years, months);
15✔
553
        }
554

555
        while (months > 12) {
30✔
556
                months -= 12;
12✔
557
                years += 1;
12✔
558
        }
559
        while (months < 1) {
30✔
560
                months += 12;
12✔
561
                years -= 1;
12✔
562
        }
563

564
        return new DateTime(years, months, days, hours, minutes, seconds, fraction, tz, dateTime.type);
30✔
565
}
566
export function addDuration(dateTime: DateTime, duration: AbstractDuration): DateTime {
15✔
567
        return evalDuration(dateTime, duration);
15✔
568
}
569

570
export function subtractDuration(dateTime: DateTime, duration: AbstractDuration): DateTime {
15✔
571
        return evalDuration(dateTime, duration.negate());
15✔
572
}
573

574
export default DateTime;
15✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc