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

IgniteUI / igniteui-angular / 13331632524

14 Feb 2025 02:51PM CUT coverage: 22.015% (-69.6%) from 91.622%
13331632524

Pull #15372

github

web-flow
Merge d52d57714 into bcb78ae0a
Pull Request #15372: chore(*): test ci passing

1990 of 15592 branches covered (12.76%)

431 of 964 new or added lines in 18 files covered. (44.71%)

19956 existing lines in 307 files now uncovered.

6452 of 29307 relevant lines covered (22.02%)

249.17 hits per line

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

48.53
/projects/igniteui-angular/src/lib/date-common/util/date-time.util.ts
1
import { DatePart, DatePartInfo } from '../../directives/date-time-editor/date-time-editor.common';
2
import { formatDate, FormatWidth, getLocaleDateFormat } from '@angular/common';
3
import { ValidationErrors } from '@angular/forms';
4
import { isDate } from '../../core/utils';
5
import { DataType } from '../../data-operations/data-util';
6

7
/** @hidden */
8
const enum FormatDesc {
9
    Numeric = 'numeric',
10
    TwoDigits = '2-digit'
11
}
12

13
const TIME_CHARS = ['h', 'H', 'm', 's', 'S', 't', 'T', 'a'];
2✔
14
const DATE_CHARS = ['d', 'D', 'M', 'y', 'Y'];
2✔
15

16
/** @hidden */
17
const enum AmPmValues {
18
    AM = 'AM',
19
    A = 'a',
20
    PM = 'PM',
21
    P = 'p'
22
}
23

24
/** @hidden */
25
const enum DateParts {
26
    Day = 'day',
27
    Month = 'month',
28
    Year = 'year',
29
    Hour = 'hour',
30
    Minute = 'minute',
31
    Second = 'second',
32
    AmPm = 'dayPeriod'
33
}
34

35
/** Maps of the pre-defined date-time format options supported by the Angular DatePipe
36
 * - predefinedNumericFormats resolve to numeric parts only (and period) for the default 'en' culture
37
 * - predefinedNonNumericFormats usually contain non-numeric date/time parts, which cannot be
38
 *   handled for editing by the date/time editors
39
 *  Ref: https://angular.dev/api/common/DatePipe?tab=usage-notes
40
 * @hidden
41
 */
42
const predefinedNumericFormats = new Map<string, DateParts[]>([
2✔
43
    ['short', [DateParts.Month, DateParts.Day, DateParts.Year, DateParts.Hour, DateParts.Minute]],
44
    ['shortDate', [DateParts.Month, DateParts.Day, DateParts.Year]],
45
    ['shortTime', [DateParts.Hour, DateParts.Minute]],
46
    ['mediumTime', [DateParts.Hour, DateParts.Minute, DateParts.Second]],
47
]);
48

49
const predefinedNonNumericFormats = new Set<string>([
2✔
50
    'medium', 'long', 'full', 'mediumDate', 'longDate', 'fullDate', 'longTime', 'fullTime',
51
])
52

53
/** @hidden */
54
export abstract class DateTimeUtil {
55
    public static readonly DEFAULT_INPUT_FORMAT = 'MM/dd/yyyy';
2✔
56
    public static readonly DEFAULT_TIME_INPUT_FORMAT = 'hh:mm tt';
2✔
57
    private static readonly SEPARATOR = 'literal';
2✔
58
    private static readonly DEFAULT_LOCALE = 'en';
2✔
59

60
    /**
61
     * Parse a Date value from masked string input based on determined date parts
62
     *
63
     * @param inputData masked value to parse
64
     * @param dateTimeParts Date parts array for the mask
65
     */
66
    public static parseValueFromMask(inputData: string, dateTimeParts: DatePartInfo[], promptChar?: string): Date | null {
UNCOV
67
        const parts: { [key in DatePart]: number } = {} as any;
×
UNCOV
68
        dateTimeParts.forEach(dp => {
×
UNCOV
69
            let value = parseInt(DateTimeUtil.getCleanVal(inputData, dp, promptChar), 10);
×
UNCOV
70
            if (!value) {
×
UNCOV
71
                value = dp.type === DatePart.Date || dp.type === DatePart.Month ? 1 : 0;
×
72
            }
UNCOV
73
            parts[dp.type] = value;
×
74
        });
UNCOV
75
        parts[DatePart.Month] -= 1;
×
76

UNCOV
77
        if (parts[DatePart.Month] < 0 || 11 < parts[DatePart.Month]) {
×
UNCOV
78
            return null;
×
79
        }
80

81
        // TODO: Century threshold
UNCOV
82
        if (parts[DatePart.Year] < 50) {
×
UNCOV
83
            parts[DatePart.Year] += 2000;
×
84
        }
85

UNCOV
86
        if (parts[DatePart.Date] > DateTimeUtil.daysInMonth(parts[DatePart.Year], parts[DatePart.Month])) {
×
UNCOV
87
            return null;
×
88
        }
89

UNCOV
90
        if (parts[DatePart.Hours] > 23 || parts[DatePart.Minutes] > 59
×
91
            || parts[DatePart.Seconds] > 59 || parts[DatePart.FractionalSeconds] > 999) {
UNCOV
92
            return null;
×
93
        }
94

UNCOV
95
        const amPm = dateTimeParts.find(p => p.type === DatePart.AmPm);
×
UNCOV
96
        if (amPm) {
×
UNCOV
97
            parts[DatePart.Hours] %= 12;
×
98
        }
99

UNCOV
100
        if (amPm) {
×
UNCOV
101
            const cleanVal = DateTimeUtil.getCleanVal(inputData, amPm, promptChar);
×
UNCOV
102
            if (DateTimeUtil.isPm(cleanVal)) {
×
UNCOV
103
                parts[DatePart.Hours] += 12;
×
104
            }
105
        }
106

UNCOV
107
        return new Date(
×
108
            parts[DatePart.Year] || 2000,
×
109
            parts[DatePart.Month] || 0,
×
110
            parts[DatePart.Date] || 1,
×
111
            parts[DatePart.Hours] || 0,
×
112
            parts[DatePart.Minutes] || 0,
×
113
            parts[DatePart.Seconds] || 0,
×
114
            parts[DatePart.FractionalSeconds] || 0
×
115
        );
116
    }
117

118
    /** Parse the mask into date/time and literal parts */
119
    public static parseDateTimeFormat(mask: string, locale?: string): DatePartInfo[] {
120
        const format = mask || DateTimeUtil.getDefaultInputFormat(locale);
1,840!
121
        const dateTimeParts: DatePartInfo[] = [];
1,840✔
122
        const formatArray = Array.from(format);
1,840✔
123
        let currentPart: DatePartInfo = null;
1,840✔
124
        let position = 0;
1,840✔
125
        let lastPartAdded = false;
1,840✔
126
        for (let i = 0; i < formatArray.length; i++, position++) {
1,840✔
127
            const type = DateTimeUtil.determineDatePart(formatArray[i]);
9,363✔
128
            if (currentPart) {
9,363✔
129
                if (currentPart.type === type) {
7,523✔
130
                    currentPart.format += formatArray[i];
3,761✔
131
                    if (i < formatArray.length - 1) {
3,761✔
132
                        continue;
1,923✔
133
                    }
134
                }
135

136
                if (currentPart.type === DatePart.AmPm && currentPart.format.indexOf('a') !== -1) {
5,600✔
137
                    currentPart = DateTimeUtil.simplifyAmPmFormat(currentPart);
11✔
138
                }
139
                DateTimeUtil.addCurrentPart(currentPart, dateTimeParts);
5,600✔
140
                lastPartAdded = true;
5,600✔
141
                position = currentPart.end;
5,600✔
142
                if(i === formatArray.length - 1 && currentPart.type !== type) {
5,600✔
143
                    lastPartAdded = false;
2✔
144
                }
145
            }
146

147
            currentPart = {
7,440✔
148
                start: position,
149
                end: position + formatArray[i].length,
150
                type,
151
                format: formatArray[i]
152
            };
153
        }
154

155
        // make sure the last member of a format like H:m:s is not omitted
156
        if (!lastPartAdded) {
1,840✔
157
            if (currentPart.type === DatePart.AmPm) {
2!
UNCOV
158
                currentPart = DateTimeUtil.simplifyAmPmFormat(currentPart);
×
159
            }
160
            DateTimeUtil.addCurrentPart(currentPart, dateTimeParts);
2✔
161
        }
162
        // formats like "y" or "yyy" are treated like "yyyy" while editing
163
        const yearPart = dateTimeParts.filter(p => p.type === DatePart.Year)[0];
5,602✔
164
        if (yearPart && yearPart.format !== 'yy') {
1,840✔
165
            yearPart.end += 4 - yearPart.format.length;
9✔
166
            yearPart.format = 'yyyy';
9✔
167
        }
168

169
        return dateTimeParts;
1,840✔
170
    }
171

172
    /** Simplifies the AmPm part to as many chars as will be displayed */
173
    private static simplifyAmPmFormat(currentPart: DatePartInfo){
174
            currentPart.format = currentPart.format.length === 5 ? 'a' : 'aa';
11✔
175
            currentPart.end = currentPart.start +  currentPart.format.length;
11✔
176
            return { ...currentPart };
11✔
177
    }
178

179
    public static getPartValue(value: Date, datePartInfo: DatePartInfo, partLength: number): string {
180
        let maskedValue;
181
        const datePart = datePartInfo.type;
1,780✔
182
        switch (datePart) {
1,780!
183
            case DatePart.Date:
184
                maskedValue = value.getDate();
×
185
                break;
×
186
            case DatePart.Month:
187
                // months are zero based
188
                maskedValue = value.getMonth() + 1;
×
189
                break;
×
190
            case DatePart.Year:
191
                if (partLength === 2) {
×
192
                    maskedValue = this.prependValue(
×
193
                        parseInt(value.getFullYear().toString().slice(-2), 10), partLength, '0');
194
                } else {
195
                    maskedValue = value.getFullYear();
×
196
                }
197
                break;
×
198
            case DatePart.Hours:
199
                if (datePartInfo.format.indexOf('h') !== -1) {
900!
200
                    maskedValue = this.prependValue(
900✔
201
                        this.toTwelveHourFormat(value.getHours().toString()), partLength, '0');
202
                } else {
UNCOV
203
                    maskedValue = value.getHours();
×
204
                }
205
                break;
900✔
206
            case DatePart.Minutes:
207
                maskedValue = value.getMinutes();
880✔
208
                break;
880✔
209
            case DatePart.Seconds:
UNCOV
210
                maskedValue = value.getSeconds();
×
UNCOV
211
                break;
×
212
            case DatePart.FractionalSeconds:
213
                maskedValue = value.getMilliseconds();
×
214
                break;
×
215
            case DatePart.AmPm:
UNCOV
216
                maskedValue = DateTimeUtil.getAmPmValue(partLength, value.getHours() < 12);
×
UNCOV
217
                break;
×
218
        }
219

220
        if (datePartInfo.type !== DatePart.AmPm && datePartInfo.type !== DatePart.Literal) {
1,780✔
221
            return this.prependValue(maskedValue, partLength, '0');
1,780✔
222
        }
223

UNCOV
224
        return maskedValue;
×
225
    }
226

227
    /** Returns the AmPm part value depending on the part length and a
228
     * conditional expression indicating whether the value is AM or PM.
229
     */
230
    public static getAmPmValue(partLength: number, isAm: boolean) {
UNCOV
231
        if (isAm) {
×
UNCOV
232
            return partLength === 1 ? AmPmValues.A : AmPmValues.AM;
×
233
        } else {
UNCOV
234
            return partLength === 1 ? AmPmValues.P : AmPmValues.PM;
×
235
        }
236
    }
237

238
    /** Returns true if a string value indicates an AM period */
239
    public static isAm(value: string) {
UNCOV
240
        value = value.toLowerCase();
×
UNCOV
241
        return (value === AmPmValues.AM.toLowerCase() || value === AmPmValues.A.toLowerCase());
×
242
    }
243

244
    /** Returns true if a string value indicates a PM period */
245
    public static isPm(value: string) {
UNCOV
246
        value = value.toLowerCase();
×
UNCOV
247
        return (value === AmPmValues.PM.toLowerCase() || value === AmPmValues.P.toLowerCase());
×
248
    }
249

250
    /** Builds a date-time editor's default input format based on provided locale settings and data type. */
251
    public static getDefaultInputFormat(locale: string, dataType: DataType = DataType.Date): string {
×
252
        locale = locale || DateTimeUtil.DEFAULT_LOCALE;
2!
253
        if (!Intl || !Intl.DateTimeFormat || !Intl.DateTimeFormat.prototype.formatToParts) {
2!
254
            // TODO: fallback with Intl.format for IE?
255
            return DateTimeUtil.DEFAULT_INPUT_FORMAT;
×
256
        }
257
        const parts = DateTimeUtil.getDefaultLocaleMask(locale, dataType);
2✔
258
        parts.forEach(p => {
2✔
259
            if (p.type !== DatePart.Year && p.type !== DateTimeUtil.SEPARATOR && p.type !== DatePart.AmPm) {
26✔
260
                p.formatType = FormatDesc.TwoDigits;
12✔
261
            }
262
        });
263

264
        return DateTimeUtil.getMask(parts);
2✔
265
    }
266

267
    /** Tries to format a date using Angular's DatePipe. Fallbacks to `Intl` if no locale settings have been loaded. */
268
    public static formatDate(value: number | Date, format: string, locale: string, timezone?: string): string {
269
        let formattedDate: string;
UNCOV
270
        try {
×
UNCOV
271
            formattedDate = formatDate(value, format, locale, timezone);
×
272
        } catch {
273
            DateTimeUtil.logMissingLocaleSettings(locale);
×
274
            const formatter = new Intl.DateTimeFormat(locale);
×
275
            formattedDate = formatter.format(value);
×
276
        }
277

UNCOV
278
        return formattedDate;
×
279
    }
280

281
    /**
282
     * Returns the date format based on a provided locale.
283
     * Supports Angular's DatePipe format options such as `shortDate`, `longDate`.
284
     */
285
    public static getLocaleDateFormat(locale: string, displayFormat?: string): string {
UNCOV
286
        const formatKeys = Object.keys(FormatWidth) as (keyof FormatWidth)[];
×
UNCOV
287
        const targetKey = formatKeys.find(k => k.toLowerCase() === displayFormat?.toLowerCase().replace('date', ''));
×
UNCOV
288
        if (!targetKey) {
×
289
            // if displayFormat is not shortDate, longDate, etc.
290
            // or if it is not set by the user
UNCOV
291
            return displayFormat;
×
292
        }
293
        let format: string;
UNCOV
294
        try {
×
UNCOV
295
            format = getLocaleDateFormat(locale, FormatWidth[targetKey]);
×
296
        } catch {
297
            DateTimeUtil.logMissingLocaleSettings(locale);
×
298
            format = DateTimeUtil.getDefaultInputFormat(locale);
×
299
        }
300

UNCOV
301
        return format;
×
302
    }
303

304
    /** Determines if a given character is `d/M/y` or `h/m/s`. */
305
    public static isDateOrTimeChar(char: string): boolean {
UNCOV
306
        return DATE_CHARS.indexOf(char) !== -1 || TIME_CHARS.indexOf(char) !== -1;
×
307
    }
308

309
    /** Spins the date portion in a date-time editor. */
310
    public static spinDate(delta: number, newDate: Date, spinLoop: boolean): void {
UNCOV
311
        const maxDate = DateTimeUtil.daysInMonth(newDate.getFullYear(), newDate.getMonth());
×
UNCOV
312
        let date = newDate.getDate() + delta;
×
UNCOV
313
        if (date > maxDate) {
×
UNCOV
314
            date = spinLoop ? date % maxDate : maxDate;
×
UNCOV
315
        } else if (date < 1) {
×
UNCOV
316
            date = spinLoop ? maxDate + (date % maxDate) : 1;
×
317
        }
318

UNCOV
319
        newDate.setDate(date);
×
320
    }
321

322
    /** Spins the month portion in a date-time editor. */
323
    public static spinMonth(delta: number, newDate: Date, spinLoop: boolean): void {
UNCOV
324
        const maxDate = DateTimeUtil.daysInMonth(newDate.getFullYear(), newDate.getMonth() + delta);
×
UNCOV
325
        if (newDate.getDate() > maxDate) {
×
UNCOV
326
            newDate.setDate(maxDate);
×
327
        }
328

UNCOV
329
        const maxMonth = 11;
×
UNCOV
330
        const minMonth = 0;
×
UNCOV
331
        let month = newDate.getMonth() + delta;
×
UNCOV
332
        if (month > maxMonth) {
×
UNCOV
333
            month = spinLoop ? (month % maxMonth) - 1 : maxMonth;
×
UNCOV
334
        } else if (month < minMonth) {
×
UNCOV
335
            month = spinLoop ? maxMonth + (month % maxMonth) + 1 : minMonth;
×
336
        }
337

UNCOV
338
        newDate.setMonth(month);
×
339
    }
340

341
    /** Spins the year portion in a date-time editor. */
342
    public static spinYear(delta: number, newDate: Date): void {
UNCOV
343
        const maxDate = DateTimeUtil.daysInMonth(newDate.getFullYear() + delta, newDate.getMonth());
×
UNCOV
344
        if (newDate.getDate() > maxDate) {
×
345
            // clip to max to avoid leap year change shifting the entire value
UNCOV
346
            newDate.setDate(maxDate);
×
347
        }
UNCOV
348
        newDate.setFullYear(newDate.getFullYear() + delta);
×
349
    }
350

351
    /** Spins the hours portion in a date-time editor. */
352
    public static spinHours(delta: number, newDate: Date, spinLoop: boolean): void {
UNCOV
353
        const maxHour = 23;
×
UNCOV
354
        const minHour = 0;
×
UNCOV
355
        let hours = newDate.getHours() + delta;
×
UNCOV
356
        if (hours > maxHour) {
×
UNCOV
357
            hours = spinLoop ? hours % maxHour - 1 : maxHour;
×
UNCOV
358
        } else if (hours < minHour) {
×
UNCOV
359
            hours = spinLoop ? maxHour + (hours % maxHour) + 1 : minHour;
×
360
        }
361

UNCOV
362
        newDate.setHours(hours);
×
363
    }
364

365
    /** Spins the minutes portion in a date-time editor. */
366
    public static spinMinutes(delta: number, newDate: Date, spinLoop: boolean): void {
UNCOV
367
        const maxMinutes = 59;
×
UNCOV
368
        const minMinutes = 0;
×
UNCOV
369
        let minutes = newDate.getMinutes() + delta;
×
UNCOV
370
        if (minutes > maxMinutes) {
×
UNCOV
371
            minutes = spinLoop ? minutes % maxMinutes - 1 : maxMinutes;
×
UNCOV
372
        } else if (minutes < minMinutes) {
×
UNCOV
373
            minutes = spinLoop ? maxMinutes + (minutes % maxMinutes) + 1 : minMinutes;
×
374
        }
375

UNCOV
376
        newDate.setMinutes(minutes);
×
377
    }
378

379
    /** Spins the seconds portion in a date-time editor. */
380
    public static spinSeconds(delta: number, newDate: Date, spinLoop: boolean): void {
UNCOV
381
        const maxSeconds = 59;
×
UNCOV
382
        const minSeconds = 0;
×
UNCOV
383
        let seconds = newDate.getSeconds() + delta;
×
UNCOV
384
        if (seconds > maxSeconds) {
×
UNCOV
385
            seconds = spinLoop ? seconds % maxSeconds - 1 : maxSeconds;
×
UNCOV
386
        } else if (seconds < minSeconds) {
×
UNCOV
387
            seconds = spinLoop ? maxSeconds + (seconds % maxSeconds) + 1 : minSeconds;
×
388
        }
389

UNCOV
390
        newDate.setSeconds(seconds);
×
391
    }
392

393
     /** Spins the fractional seconds (milliseconds) portion in a date-time editor. */
394
    public static spinFractionalSeconds(delta: number, newDate: Date, spinLoop: boolean) {
UNCOV
395
        const maxMs = 999;
×
UNCOV
396
        const minMs = 0;
×
UNCOV
397
        let ms = newDate.getMilliseconds() + delta;
×
UNCOV
398
        if (ms > maxMs) {
×
UNCOV
399
            ms = spinLoop ? ms % maxMs - 1 : maxMs;
×
UNCOV
400
        } else if (ms < minMs) {
×
UNCOV
401
            ms = spinLoop ? maxMs + (ms % maxMs) + 1 : minMs;
×
402
        }
403

UNCOV
404
        newDate.setMilliseconds(ms);
×
405
    }
406

407
    /** Spins the AM/PM portion in a date-time editor. */
408
    public static spinAmPm(newDate: Date, currentDate: Date, amPmFromMask: string): Date {
UNCOV
409
        if(DateTimeUtil.isAm(amPmFromMask)) {
×
UNCOV
410
            newDate = new Date(newDate.setHours(newDate.getHours() + 12));
×
UNCOV
411
        } else if(DateTimeUtil.isPm(amPmFromMask)) {
×
UNCOV
412
            newDate = new Date(newDate.setHours(newDate.getHours() - 12));
×
413
        }
414

UNCOV
415
        if (newDate.getDate() !== currentDate.getDate()) {
×
UNCOV
416
            return currentDate;
×
417
        }
418

UNCOV
419
        return newDate;
×
420
    }
421

422
    /**
423
     * Determines whether the provided value is greater than the provided max value.
424
     *
425
     * @param includeTime set to false if you want to exclude time portion of the two dates
426
     * @param includeDate set to false if you want to exclude the date portion of the two dates
427
     * @returns true if provided value is greater than provided maxValue
428
     */
429
    public static greaterThanMaxValue(value: Date, maxValue: Date, includeTime = true, includeDate = true): boolean {
×
UNCOV
430
        if (includeTime && includeDate) {
×
UNCOV
431
            return value.getTime() > maxValue.getTime();
×
432
        }
433

UNCOV
434
        const _value = new Date(value.getTime());
×
UNCOV
435
        const _maxValue = new Date(maxValue.getTime());
×
UNCOV
436
        if (!includeTime) {
×
UNCOV
437
            _value.setHours(0, 0, 0, 0);
×
UNCOV
438
            _maxValue.setHours(0, 0, 0, 0);
×
439
        }
UNCOV
440
        if (!includeDate) {
×
UNCOV
441
            _value.setFullYear(0, 0, 0);
×
UNCOV
442
            _maxValue.setFullYear(0, 0, 0);
×
443
        }
444

UNCOV
445
        return _value.getTime() > _maxValue.getTime();
×
446
    }
447

448
    /**
449
     * Determines whether the provided value is less than the provided min value.
450
     *
451
     * @param includeTime set to false if you want to exclude time portion of the two dates
452
     * @param includeDate set to false if you want to exclude the date portion of the two dates
453
     * @returns true if provided value is less than provided minValue
454
     */
455
    public static lessThanMinValue(value: Date, minValue: Date, includeTime = true, includeDate = true): boolean {
×
UNCOV
456
        if (includeTime && includeDate) {
×
UNCOV
457
            return value.getTime() < minValue.getTime();
×
458
        }
459

UNCOV
460
        const _value = new Date(value.getTime());
×
UNCOV
461
        const _minValue = new Date(minValue.getTime());
×
UNCOV
462
        if (!includeTime) {
×
UNCOV
463
            _value.setHours(0, 0, 0, 0);
×
UNCOV
464
            _minValue.setHours(0, 0, 0, 0);
×
465
        }
UNCOV
466
        if (!includeDate) {
×
UNCOV
467
            _value.setFullYear(0, 0, 0);
×
UNCOV
468
            _minValue.setFullYear(0, 0, 0);
×
469
        }
470

UNCOV
471
        return _value.getTime() < _minValue.getTime();
×
472
    }
473

474
    /**
475
     * Validates a value within a given min and max value range.
476
     *
477
     * @param value The value to validate
478
     * @param minValue The lowest possible value that `value` can take
479
     * @param maxValue The largest possible value that `value` can take
480
     */
481
    public static validateMinMax(value: Date, minValue: Date | string, maxValue: Date | string,
482
        includeTime = true, includeDate = true): ValidationErrors {
×
UNCOV
483
        if (!value) {
×
484
            return null;
×
485
        }
UNCOV
486
        const errors = {};
×
UNCOV
487
        const min = DateTimeUtil.isValidDate(minValue) ? minValue : DateTimeUtil.parseIsoDate(minValue);
×
UNCOV
488
        const max = DateTimeUtil.isValidDate(maxValue) ? maxValue : DateTimeUtil.parseIsoDate(maxValue);
×
UNCOV
489
        if (min && value && DateTimeUtil.lessThanMinValue(value, min, includeTime, includeDate)) {
×
UNCOV
490
            Object.assign(errors, { minValue: true });
×
491
        }
UNCOV
492
        if (max && value && DateTimeUtil.greaterThanMaxValue(value, max, includeTime, includeDate)) {
×
UNCOV
493
            Object.assign(errors, { maxValue: true });
×
494
        }
495

UNCOV
496
        return errors;
×
497
    }
498

499
    /** Parse an ISO string to a Date */
500
    public static parseIsoDate(value: string): Date | null {
501
        let regex = /^\d{4}/g;
8✔
502
        const timeLiteral = 'T';
8✔
503
        if (regex.test(value)) {
8!
UNCOV
504
            return new Date(value + `${value.indexOf(timeLiteral) === -1 ? 'T00:00:00' : ''}`);
×
505
        }
506

507
        regex = /^\d{2}/g;
8✔
508
        if (regex.test(value)) {
8!
UNCOV
509
            const dateNow = new Date().toISOString();
×
510
            // eslint-disable-next-line prefer-const
UNCOV
511
            let [datePart, _timePart] = dateNow.split(timeLiteral);
×
UNCOV
512
            return new Date(`${datePart}T${value}`);
×
513
        }
514

515
        return null;
8✔
516
    }
517

518
    /**
519
     * Returns whether the input is valid date
520
     *
521
     * @param value input to check
522
     * @returns true if provided input is a valid date
523
     */
524
    public static isValidDate(value: any): value is Date {
525
        if (isDate(value)) {
12!
UNCOV
526
            return !isNaN(value.getTime());
×
527
        }
528

529
        return false;
12✔
530
    }
531

532
    public static isFormatNumeric(locale: string, inputFormat: string): boolean {
533
        const dateParts = DateTimeUtil.parseDateTimeFormat(inputFormat);
6✔
534
        if (predefinedNonNumericFormats.has(inputFormat) || dateParts.every(p => p.type === DatePart.Literal)) {
6✔
535
            return false;
2✔
536
        }
537
        for (let i = 0; i < dateParts.length; i++) {
4✔
538
            if (dateParts[i].type === DatePart.AmPm || dateParts[i].type === DatePart.Literal) {
28✔
539
                continue;
14✔
540
            }
541
            const transformedValue = formatDate(new Date(), dateParts[i].format, locale);
14✔
542
            // check if the transformed date/time part contains any kind of letter from any language
543
            if (/\p{L}+/gu.test(transformedValue)) {
14!
UNCOV
544
                return false;
×
545
            }
546
        }
547
        return true;
4✔
548
    }
549

550
    /**
551
     * Returns an input format that can be used by the date-time editors, as
552
     * - if the format is already numeric, return it as is
553
     * - if it is among the predefined numeric ones, return it as the equivalent locale-based format
554
     *   for the corresponding numeric date parts
555
     * - otherwise, return an empty string
556
     */
557
    public static getNumericInputFormat(locale: string, format: string): string {
558
        let resultFormat = '';
8✔
559
        if (!format) {
8!
UNCOV
560
            return resultFormat;
×
561
        }
562
        if (predefinedNumericFormats.has(format)) {
8✔
563
            resultFormat = DateTimeUtil.getLocaleInputFormatFromParts(locale, predefinedNumericFormats.get(format));
2✔
564

565
        } else if (DateTimeUtil.isFormatNumeric(locale, format)) {
6✔
566
            resultFormat = format;
4✔
567
        }
568
        return resultFormat;
8✔
569
    }
570

571
    /** Gets the locale-based format from an array of date parts */
572
    private static getLocaleInputFormatFromParts(locale: string, dateParts: DateParts[]): string {
573
        const options = {};
2✔
574
        dateParts.forEach(p => {
2✔
575
            if (p === DateParts.Year) {
6!
UNCOV
576
                options[p] = FormatDesc.Numeric;
×
577
            } else if (p !== DateParts.AmPm) {
6✔
578
                options[p] = FormatDesc.TwoDigits;
6✔
579
            }
580
        });
581
        const formatter = new Intl.DateTimeFormat(locale, options);
2✔
582
        const dateStruct = DateTimeUtil.getDateStructFromParts(formatter.formatToParts(new Date()), formatter);
2✔
583
        DateTimeUtil.fillDatePartsPositions(dateStruct);
2✔
584
        return DateTimeUtil.getMask(dateStruct);
2✔
585
    }
586

587
    private static addCurrentPart(currentPart: DatePartInfo, dateTimeParts: DatePartInfo[]): void {
588
        DateTimeUtil.ensureLeadingZero(currentPart);
5,602✔
589
        currentPart.end = currentPart.start + currentPart.format.length;
5,602✔
590
        dateTimeParts.push(currentPart);
5,602✔
591
    }
592

593
    private static daysInMonth(fullYear: number, month: number): number {
UNCOV
594
        return new Date(fullYear, month + 1, 0).getDate();
×
595
    }
596

597
    private static trimEmptyPlaceholders(value: string, promptChar?: string): string {
UNCOV
598
        const result = value.replace(new RegExp(promptChar || '_', 'g'), '');
×
UNCOV
599
        return result;
×
600
    }
601

602
    private static getMask(dateStruct: any[]): string {
603
        const mask = [];
4✔
604
        for (const part of dateStruct) {
4✔
605
            if (part.formatType === FormatDesc.Numeric) {
40✔
606
                switch (part.type) {
2!
607
                    case DateParts.Day:
608
                        mask.push('d');
×
609
                        break;
×
610
                    case DateParts.Month:
611
                        mask.push('M');
×
612
                        break;
×
613
                    case DateParts.Year:
614
                        mask.push('yyyy');
2✔
615
                        break;
2✔
616
                    case DateParts.Hour:
617
                        mask.push(part.hour12 ? 'h' : 'H');
×
618
                        break;
×
619
                    case DateParts.Minute:
620
                        mask.push('m');
×
621
                        break;
×
622
                    case DateParts.Second:
623
                        mask.push('s');
×
624
                        break;
×
625
                }
626
            } else if (part.formatType === FormatDesc.TwoDigits) {
38✔
627
                switch (part.type) {
18!
628
                    case DateParts.Day:
629
                        mask.push('dd');
2✔
630
                        break;
2✔
631
                    case DateParts.Month:
632
                        mask.push('MM');
2✔
633
                        break;
2✔
634
                    case DateParts.Year:
635
                        mask.push('yy');
×
636
                        break;
×
637
                    case DateParts.Hour:
638
                        mask.push(part.hour12 ? 'hh' : 'HH');
4!
639
                        break;
4✔
640
                    case DateParts.Minute:
641
                        mask.push('mm');
4✔
642
                        break;
4✔
643
                    case DateParts.Second:
644
                        mask.push('ss');
4✔
645
                        break;
4✔
646
                }
647
            }
648

649
            if (part.type === DateParts.AmPm) {
40✔
650
                mask.push('tt');
4✔
651
            }
652

653
            if (part.type === DateTimeUtil.SEPARATOR) {
40✔
654
                mask.push(part.value);
18✔
655
            }
656
        }
657

658
        return mask.join('');
4✔
659
    }
660

661
    private static logMissingLocaleSettings(locale: string): void {
662
        console.warn(`Missing locale data for the locale ${locale}. Please refer to https://angular.io/guide/i18n#i18n-pipes`);
×
663
        console.warn('Using default browser locale settings.');
×
664
    }
665

666
    private static prependValue(value: number, partLength: number, prependChar: string): string {
667
        return (prependChar + value.toString()).slice(-partLength);
2,680✔
668
    }
669

670
    private static toTwelveHourFormat(value: string, promptChar = '_'): number {
900✔
671
        let hour = parseInt(value.replace(new RegExp(promptChar, 'g'), '0'), 10);
900✔
672
        if (hour > 12) {
900✔
673
            hour -= 12;
20✔
674
        } else if (hour === 0) {
880✔
675
            hour = 12;
880✔
676
        }
677

678
        return hour;
900✔
679
    }
680

681
    private static ensureLeadingZero(part: DatePartInfo) {
682
        switch (part.type) {
5,602!
683
            case DatePart.Date:
684
            case DatePart.Month:
685
            case DatePart.Hours:
686
            case DatePart.Minutes:
687
            case DatePart.Seconds:
688
                if (part.format.length === 1) {
3,702✔
689
                    part.format = part.format.repeat(2);
8✔
690
                }
691
                break;
3,702✔
692
            case DatePart.FractionalSeconds:
UNCOV
693
                part.format = part.format[0].repeat(3);
×
UNCOV
694
                break;
×
695
        }
696
    }
697

698
    private static getCleanVal(inputData: string, datePart: DatePartInfo, promptChar?: string): string {
UNCOV
699
        return DateTimeUtil.trimEmptyPlaceholders(inputData.substring(datePart.start, datePart.end), promptChar);
×
700
    }
701

702
    private static determineDatePart(char: string): DatePart {
703
        switch (char) {
9,363!
704
            case 'd':
705
            case 'D':
706
                return DatePart.Date;
22✔
707
            case 'M':
708
                return DatePart.Month;
18✔
709
            case 'y':
710
            case 'Y':
711
                return DatePart.Year;
36✔
712
            case 'h':
713
            case 'H':
714
                return DatePart.Hours;
3,676✔
715
            case 'm':
716
                return DatePart.Minutes;
3,680✔
717
            case 's':
UNCOV
718
                return DatePart.Seconds;
×
719
            case 'S':
UNCOV
720
                return DatePart.FractionalSeconds;
×
721
            case 'a':
722
            case 't':
723
            case 'T':
724
                return DatePart.AmPm;
49✔
725
            default:
726
                return DatePart.Literal;
1,882✔
727
        }
728
    }
729

730
    private static getFormatOptions(dataType: DataType) {
731
        const dateOptions = {
2✔
732
            day: FormatDesc.TwoDigits,
733
            month: FormatDesc.TwoDigits,
734
            year: FormatDesc.Numeric
735
        };
736
        const timeOptions = {
2✔
737
            hour: FormatDesc.TwoDigits,
738
            minute: FormatDesc.TwoDigits
739
        };
740
        switch (dataType) {
2!
741
            case DataType.Date:
UNCOV
742
                return dateOptions;
×
743
            case DataType.Time:
UNCOV
744
                return timeOptions;
×
745
            case DataType.DateTime:
746
                return {
2✔
747
                    ...dateOptions,
748
                    ...timeOptions,
749
                    second: FormatDesc.TwoDigits
750
                };
751
            default:
752
                return { };
×
753
        }
754
    }
755

756
    private static getDefaultLocaleMask(locale: string, dataType: DataType = DataType.Date) {
×
757
        const options = DateTimeUtil.getFormatOptions(dataType);
2✔
758
        const formatter = new Intl.DateTimeFormat(locale, options);
2✔
759
        const formatToParts = formatter.formatToParts(new Date());
2✔
760
        const dateStruct = DateTimeUtil.getDateStructFromParts(formatToParts, formatter);
2✔
761
        DateTimeUtil.fillDatePartsPositions(dateStruct);
2✔
762
        return dateStruct;
2✔
763
    }
764

765
    private static getDateStructFromParts(parts: Intl.DateTimeFormatPart[], formatter: Intl.DateTimeFormat): any[] {
766
        const dateStruct = [];
4✔
767
        for (const part of parts) {
4✔
768
            if (part.type === DateTimeUtil.SEPARATOR) {
40✔
769
                dateStruct.push({
18✔
770
                    type: DateTimeUtil.SEPARATOR,
771
                    value: part.value
772
                });
773
            } else {
774
                dateStruct.push({
22✔
775
                    type: part.type
776
                });
777
            }
778
        }
779
        const formatterOptions = formatter.resolvedOptions();
4✔
780
        for (const part of dateStruct) {
4✔
781
            switch (part.type) {
40✔
782
                case DateParts.Day: {
783
                    part.formatType = formatterOptions.day;
2✔
784
                    break;
2✔
785
                }
786
                case DateParts.Month: {
787
                    part.formatType = formatterOptions.month;
2✔
788
                    break;
2✔
789
                }
790
                case DateParts.Year: {
791
                    part.formatType = formatterOptions.year;
2✔
792
                    break;
2✔
793
                }
794
                case DateParts.Hour: {
795
                    part.formatType = formatterOptions.hour;
4✔
796
                    if (formatterOptions.hour12) {
4✔
797
                        part.hour12 = true;
4✔
798
                    }
799
                    break;
4✔
800
                }
801
                case DateParts.Minute: {
802
                    part.formatType = formatterOptions.minute;
4✔
803
                    break;
4✔
804
                }
805
                case DateParts.Second: {
806
                    part.formatType = formatterOptions.second;
4✔
807
                    break;
4✔
808
                }
809
                case DateParts.AmPm: {
810
                    part.formatType = formatterOptions.dayPeriod;
4✔
811
                    break;
4✔
812
                }
813
            }
814
        }
815
        return dateStruct;
4✔
816
    }
817

818
    private static fillDatePartsPositions(dateArray: any[]): void {
819
        let currentPos = 0;
4✔
820

821
        for (const part of dateArray) {
4✔
822
            // Day|Month|Hour|Minute|Second|AmPm part positions
823
            if (part.type === DateParts.Day || part.type === DateParts.Month ||
40✔
824
                part.type === DateParts.Hour || part.type === DateParts.Minute || part.type === DateParts.Second ||
825
                part.type === DateParts.AmPm
826
            ) {
827
                // Offset 2 positions for number
828
                part.position = [currentPos, currentPos + 2];
20✔
829
                currentPos += 2;
20✔
830
            } else if (part.type === DateParts.Year) {
20✔
831
                // Year part positions
832
                switch (part.formatType) {
2!
833
                    case FormatDesc.Numeric: {
834
                        // Offset 4 positions for full year
835
                        part.position = [currentPos, currentPos + 4];
2✔
836
                        currentPos += 4;
2✔
837
                        break;
2✔
838
                    }
839
                    case FormatDesc.TwoDigits: {
840
                        // Offset 2 positions for short year
841
                        part.position = [currentPos, currentPos + 2];
×
842
                        currentPos += 2;
×
843
                        break;
×
844
                    }
845
                }
846
            } else if (part.type === DateTimeUtil.SEPARATOR) {
18✔
847
                // Separator positions
848
                part.position = [currentPos, currentPos + 1];
18✔
849
                currentPos++;
18✔
850
            }
851
        }
852
    }
853
}
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

© 2025 Coveralls, Inc