@zelgadis87/utils-core 5.2.10 → 5.2.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/dist/time/TimeInstant.d.ts +9 -9
- package/dist/time/TimeInstantBuilder.d.ts +23 -14
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/esbuild/index.cjs +196 -184
- package/esbuild/index.cjs.map +3 -3
- package/esbuild/index.mjs +196 -184
- package/esbuild/index.mjs.map +3 -3
- package/package.json +1 -1
- package/src/time/TimeInstant.ts +271 -200
- package/src/time/TimeInstantBuilder.ts +17 -7
package/package.json
CHANGED
package/src/time/TimeInstant.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { ICancelable, ICancelablePromise } from "../async/Deferred.js";
|
|
|
2
2
|
import { TComparisonResult } from "../sorting/_index.js";
|
|
3
3
|
import TimeBase from "./TimeBase";
|
|
4
4
|
import TimeDuration from "./TimeDuration";
|
|
5
|
-
import { TTimeInstantBuilder, TTimeInstantCreationParameters, createTimeInstantFromParameters, timeInstantBuilder } from "./TimeInstantBuilder.js";
|
|
5
|
+
import { TTimeInstantBuilder, TTimeInstantCreationParameters, createTimeInstantFromParameters, timeInstantBuilder, type TTimeInstantParameters } from "./TimeInstantBuilder.js";
|
|
6
6
|
import { TimeUnit } from "./TimeUnit";
|
|
7
7
|
import { TDayOfMonth, TDayOfWeek, THourOfDay, TIso8601DateString, TIso8601DateUtcString, TMillisecondOfSecond, TMinuteOfHour, TMonth, TSecondOfMinute, TWeekNumber } from "./types";
|
|
8
8
|
|
|
@@ -47,7 +47,10 @@ export class TimeInstant extends TimeBase<TimeInstant> {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
public distanceFromStartOfDay(): TimeDuration {
|
|
50
|
-
|
|
50
|
+
// Calculate milliseconds since start of day directly without creating intermediate TimeInstant
|
|
51
|
+
const params = this.toParameters();
|
|
52
|
+
const msInDay = params.hours * 3600000 + params.minutes * 60000 + params.seconds * 1000 + params.milliseconds;
|
|
53
|
+
return TimeDuration.fromMs( msInDay );
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
public atStartOfDay(): TimeInstant {
|
|
@@ -55,7 +58,11 @@ export class TimeInstant extends TimeBase<TimeInstant> {
|
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
public distanceFromEndOfDay(): TimeDuration {
|
|
58
|
-
|
|
61
|
+
// Calculate milliseconds until end of day directly
|
|
62
|
+
const params = this.toParameters();
|
|
63
|
+
const msInDay = params.hours * 3600000 + params.minutes * 60000 + params.seconds * 1000 + params.milliseconds;
|
|
64
|
+
const msUntilEndOfDay = 86399999 - msInDay; // 23:59:59.999 in ms
|
|
65
|
+
return TimeDuration.fromMs( msUntilEndOfDay );
|
|
59
66
|
}
|
|
60
67
|
|
|
61
68
|
public atEndOfDay(): TimeInstant {
|
|
@@ -89,7 +96,8 @@ export class TimeInstant extends TimeBase<TimeInstant> {
|
|
|
89
96
|
}
|
|
90
97
|
|
|
91
98
|
public isToday(): boolean {
|
|
92
|
-
|
|
99
|
+
// Calculate days directly from timestamps without creating TimeInstant object
|
|
100
|
+
return Math.floor( this.ms / 86400000 ) === Math.floor( Date.now() / 86400000 );
|
|
93
101
|
}
|
|
94
102
|
|
|
95
103
|
/**
|
|
@@ -158,10 +166,14 @@ export class TimeInstant extends TimeBase<TimeInstant> {
|
|
|
158
166
|
* @throws Error if the string doesn't match the pattern or contains invalid basic values
|
|
159
167
|
* @todo Add calendar-aware validation to reject dates like February 30th, April 31st
|
|
160
168
|
*/
|
|
161
|
-
public static
|
|
169
|
+
public static fromString( dateString: string, pattern: string, base: TimeInstant = TimeInstant.now(), config: { locale?: string, timeZone?: string } = {} ): TimeInstant {
|
|
162
170
|
return parseTimeInstant( dateString, pattern, base, config );
|
|
163
171
|
}
|
|
164
172
|
|
|
173
|
+
public static parse( dateString: string, pattern: string, config: { locale?: string, timeZone?: string } = {} ): Partial<TTimeInstantCreationParameters> {
|
|
174
|
+
return parseTimeInstantComponents( dateString, pattern, config );
|
|
175
|
+
}
|
|
176
|
+
|
|
165
177
|
/**
|
|
166
178
|
* @returns this instant, in ISO 8601 format (eg, 2024-11-01T15:49:22.024Z). The format is meant to always be realiable.
|
|
167
179
|
*/
|
|
@@ -177,7 +189,7 @@ export class TimeInstant extends TimeBase<TimeInstant> {
|
|
|
177
189
|
}
|
|
178
190
|
|
|
179
191
|
public toUnixTimestamp(): number {
|
|
180
|
-
//
|
|
192
|
+
// Syntactic sugar for this.ms;
|
|
181
193
|
return this.ms;
|
|
182
194
|
}
|
|
183
195
|
|
|
@@ -185,8 +197,17 @@ export class TimeInstant extends TimeBase<TimeInstant> {
|
|
|
185
197
|
return new Date( this.ms );
|
|
186
198
|
}
|
|
187
199
|
|
|
188
|
-
public
|
|
189
|
-
|
|
200
|
+
public toParameters(): TTimeInstantParameters {
|
|
201
|
+
const d = this.toDate();
|
|
202
|
+
return {
|
|
203
|
+
year: d.getUTCFullYear(),
|
|
204
|
+
month: d.getUTCMonth() + 1 as TMonth,
|
|
205
|
+
date: d.getUTCDate() as TDayOfMonth,
|
|
206
|
+
hours: d.getUTCHours() as THourOfDay,
|
|
207
|
+
minutes: d.getUTCMinutes() as TMinuteOfHour,
|
|
208
|
+
seconds: d.getUTCSeconds() as TSecondOfMinute,
|
|
209
|
+
milliseconds: d.getUTCMilliseconds() as TMillisecondOfSecond,
|
|
210
|
+
};
|
|
190
211
|
}
|
|
191
212
|
|
|
192
213
|
/**
|
|
@@ -351,39 +372,31 @@ export class TimeInstant extends TimeBase<TimeInstant> {
|
|
|
351
372
|
return TimeInstant.fromUnixTimestamp( timestamp );
|
|
352
373
|
}
|
|
353
374
|
|
|
354
|
-
public get dayOfMonth(): TDayOfMonth {
|
|
355
|
-
return this.toDate().getUTCDate() as TDayOfMonth;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
public get dayOfWeek(): TDayOfWeek {
|
|
359
|
-
return this.toDate().getUTCDay() + 1 as TDayOfWeek;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
public get month(): TMonth {
|
|
363
|
-
return this.toDate().getUTCMonth() + 1 as TMonth;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
public get year(): number {
|
|
367
|
-
return this.toDate().getUTCFullYear();
|
|
368
|
-
}
|
|
369
|
-
|
|
370
375
|
/**
|
|
371
376
|
* Returns the week number represented by this instant, according to the ISO 8601 standard.
|
|
372
|
-
|
|
377
|
+
* Please note that the instant and the week number could be of two different years, eg the friday 1st january is actually part of week 52 of the previous year.
|
|
373
378
|
*/
|
|
374
379
|
public get weekNumber(): { weekNumber: TWeekNumber, year: number } {
|
|
375
380
|
/**
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
381
|
+
* According to the ISO 8601 Standard, week number 1 is defined as the week containing the 4th of january (or, equivalently, the week that contains the first Thursday of the year).
|
|
382
|
+
* As such, we basically have to count how many Thursdays there has been in this year.
|
|
383
|
+
* Please note that the thursdayOfThisWeek could be in the previous year.
|
|
379
384
|
*/
|
|
380
385
|
const date = this.toDate();
|
|
381
386
|
const oneDay = 1000 * 60 * 60 * 24;
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
const
|
|
387
|
+
|
|
388
|
+
// Get thursday of this week
|
|
389
|
+
const dayOfWeek = date.getUTCDay() || 7; // Sunday = 7 in ISO
|
|
390
|
+
const thursdayMs = this.ms + ( 4 - dayOfWeek ) * oneDay;
|
|
391
|
+
const thursdayDate = new Date( thursdayMs );
|
|
392
|
+
const thursdayYear = thursdayDate.getUTCFullYear();
|
|
393
|
+
|
|
394
|
+
// First of January at noon UTC
|
|
395
|
+
const firstOfJanMs = Date.UTC( thursdayYear, 0, 1, 12, 0, 0 );
|
|
396
|
+
const dayOfTheYear = Math.round( ( thursdayMs - firstOfJanMs ) / oneDay );
|
|
385
397
|
const weekNumber = Math.floor( dayOfTheYear / 7 ) + 1;
|
|
386
|
-
|
|
398
|
+
|
|
399
|
+
return { weekNumber: weekNumber as TWeekNumber, year: thursdayYear };
|
|
387
400
|
}
|
|
388
401
|
|
|
389
402
|
/**
|
|
@@ -445,130 +458,156 @@ function isValidMillisecond( num: number ): num is TMillisecondOfSecond {
|
|
|
445
458
|
return num >= 0 && num <= 999;
|
|
446
459
|
}
|
|
447
460
|
|
|
448
|
-
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
const optionNames = {
|
|
452
|
-
y: 'year',
|
|
453
|
-
M: 'month',
|
|
454
|
-
d: 'day',
|
|
455
|
-
D: 'weekday',
|
|
456
|
-
S: 'fractionalSecondDigits',
|
|
457
|
-
G: 'era',
|
|
458
|
-
Z: 'timeZoneName',
|
|
459
|
-
P: 'dayPeriod',
|
|
460
|
-
a: 'hour12',
|
|
461
|
-
h: 'hour',
|
|
462
|
-
H: 'hour',
|
|
463
|
-
m: 'minute',
|
|
464
|
-
s: 'second',
|
|
465
|
-
} as const;
|
|
466
|
-
|
|
467
|
-
const values = {
|
|
468
|
-
y: [ 'numeric', '2-digit', undefined, 'numeric' ],
|
|
469
|
-
M: [ 'narrow', '2-digit', 'short', 'long' ],
|
|
470
|
-
d: [ 'numeric', '2-digit' ],
|
|
471
|
-
D: [ 'narrow', 'short', 'long' ],
|
|
472
|
-
S: [ 1, 2, 3 ],
|
|
473
|
-
G: [ 'narrow', 'short', 'long' ],
|
|
474
|
-
Z: [ 'short', 'long' ],
|
|
475
|
-
P: [ 'narrow', 'short', 'long' ],
|
|
476
|
-
a: [ true ],
|
|
477
|
-
h: [ "numeric", "2-digit" ],
|
|
478
|
-
H: [ "numeric", "2-digit" ],
|
|
479
|
-
m: [ "numeric", "2-digit" ],
|
|
480
|
-
s: [ "numeric", "2-digit" ],
|
|
481
|
-
} as const;
|
|
482
|
-
|
|
483
|
-
function padIf( condition: boolean, value: string | number, length: number ): string {
|
|
484
|
-
return condition && length === 2 && Number( value ) / 10 < 1 ? '0' + value : String( value );
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
function formatType( instant: TimeInstant, type: string, length: number, { locale, timeZone }: { locale?: string, timeZone?: string } = {} ): string | undefined {
|
|
488
|
-
// For timezone-sensitive formatting, we still need to use Intl.DateTimeFormat with a Date
|
|
489
|
-
// But for basic numeric formatting, we can use the TimeInstant's properties directly
|
|
490
|
-
|
|
491
|
-
// Handle basic numeric formatting directly from TimeInstant properties (UTC-based)
|
|
492
|
-
if ( type === 'y' ) {
|
|
493
|
-
const year = instant.year;
|
|
494
|
-
if ( length === 2 ) {
|
|
495
|
-
return String( year ).slice( -2 );
|
|
496
|
-
}
|
|
497
|
-
return String( year );
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
if ( type === 'M' && ( length === 1 || length === 2 ) ) {
|
|
501
|
-
const month = instant.month;
|
|
502
|
-
return length === 2 && month < 10 ? '0' + month : String( month );
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
if ( type === 'd' && ( length === 1 || length === 2 ) ) {
|
|
506
|
-
const day = instant.dayOfMonth;
|
|
507
|
-
return length === 2 && day < 10 ? '0' + day : String( day );
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// Handle time components directly from UTC Date to avoid timezone conversion
|
|
511
|
-
if ( type === 'H' && ( length === 1 || length === 2 ) ) {
|
|
512
|
-
const hours = instant.toDate().getUTCHours();
|
|
513
|
-
return length === 2 && hours < 10 ? '0' + hours : String( hours );
|
|
514
|
-
}
|
|
461
|
+
// Month names cache by locale to avoid regenerating on every call
|
|
462
|
+
const monthNamesCache = new Map<string, { short: string[], long: string[] }>();
|
|
515
463
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
464
|
+
function getMonthNames( locale: string = 'en' ): { short: string[], long: string[] } {
|
|
465
|
+
// Check cache first
|
|
466
|
+
const cached = monthNamesCache.get( locale );
|
|
467
|
+
if ( cached ) {
|
|
468
|
+
return cached;
|
|
520
469
|
}
|
|
521
470
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
}
|
|
471
|
+
// Use Intl.DateTimeFormat to generate month names for any locale
|
|
472
|
+
const shortFormatter = new Intl.DateTimeFormat( locale, { month: 'short' } );
|
|
473
|
+
const longFormatter = new Intl.DateTimeFormat( locale, { month: 'long' } );
|
|
526
474
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
return length === 2 && seconds < 10 ? '0' + seconds : String( seconds );
|
|
530
|
-
}
|
|
475
|
+
const short: string[] = [];
|
|
476
|
+
const long: string[] = [];
|
|
531
477
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
478
|
+
// Generate names for all 12 months (0-11 in Date constructor)
|
|
479
|
+
for ( let month = 0; month < 12; month++ ) {
|
|
480
|
+
// Use January 1st of year 2000 as a reference date (arbitrary non-leap year)
|
|
481
|
+
const date = new Date( 2000, month, 1 );
|
|
482
|
+
short.push( normalizeMonthName( shortFormatter.format( date ) ) );
|
|
483
|
+
long.push( normalizeMonthName( longFormatter.format( date ) ) );
|
|
538
484
|
}
|
|
539
485
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
486
|
+
const result = { short, long };
|
|
487
|
+
monthNamesCache.set( locale, result );
|
|
488
|
+
return result;
|
|
489
|
+
}
|
|
544
490
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
491
|
+
// Normalize month names by removing accents, periods, and other special characters
|
|
492
|
+
function normalizeMonthName( name: string ): string {
|
|
493
|
+
return name
|
|
494
|
+
// Remove periods and other punctuation
|
|
495
|
+
.replace( /[.,;:!?]/g, '' )
|
|
496
|
+
// Normalize Unicode (decompose accented characters)
|
|
497
|
+
.normalize( 'NFD' )
|
|
498
|
+
// Remove diacritical marks (accents)
|
|
499
|
+
.replace( /[\u0300-\u036f]/g, '' )
|
|
500
|
+
// Trim whitespace
|
|
501
|
+
.trim();
|
|
502
|
+
}
|
|
548
503
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
timeZone,
|
|
552
|
-
} as Intl.DateTimeFormatOptions;
|
|
504
|
+
const PATTERN_REGEX = /(M|y|d|D|h|H|m|s|S|G|Z|P|a)+/g;
|
|
505
|
+
const ESCAPE_REGEX = /\\"|"((?:\\"|[^"])*)"/g;
|
|
553
506
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
507
|
+
// Formatter configuration with optional extraction metadata
|
|
508
|
+
type TFormatterConfig = {
|
|
509
|
+
options: Intl.DateTimeFormatOptions;
|
|
510
|
+
extract?: {
|
|
511
|
+
partType: Intl.DateTimeFormatPartTypes;
|
|
512
|
+
transform?: ( value: string ) => string;
|
|
513
|
+
};
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
// Formatter configuration mapping: token pattern -> configuration with optional extraction rules
|
|
517
|
+
const formatterConfigs: Record<string, TFormatterConfig> = {
|
|
518
|
+
// Year
|
|
519
|
+
'y': { options: { year: 'numeric' } },
|
|
520
|
+
'yy': { options: { year: '2-digit' } },
|
|
521
|
+
'yyyy': { options: { year: 'numeric' } },
|
|
522
|
+
|
|
523
|
+
// Month
|
|
524
|
+
'M': { options: { month: 'numeric' } },
|
|
525
|
+
'MM': { options: { month: '2-digit' } },
|
|
526
|
+
'MMM': { options: { month: 'short' } },
|
|
527
|
+
'MMMM': { options: { month: 'long' } },
|
|
528
|
+
|
|
529
|
+
// Day
|
|
530
|
+
'd': { options: { day: 'numeric' } },
|
|
531
|
+
'dd': { options: { day: '2-digit' } },
|
|
532
|
+
|
|
533
|
+
// Hours (24-hour) - extract and pad manually due to Intl quirk
|
|
534
|
+
'H': { options: { hour: 'numeric', hour12: false }, extract: { partType: 'hour' } },
|
|
535
|
+
'HH': { options: { hour: '2-digit', hour12: false }, extract: { partType: 'hour', transform: ( v ) => v.padStart( 2, '0' ) } },
|
|
536
|
+
|
|
537
|
+
// Hours (12-hour) - extract and pad manually due to Intl quirk
|
|
538
|
+
'h': { options: { hour: 'numeric', hour12: true }, extract: { partType: 'hour' } },
|
|
539
|
+
'hh': { options: { hour: '2-digit', hour12: true }, extract: { partType: 'hour', transform: ( v ) => v.padStart( 2, '0' ) } },
|
|
540
|
+
|
|
541
|
+
// Minutes - extract and pad manually due to Intl quirk
|
|
542
|
+
'm': { options: { minute: 'numeric' }, extract: { partType: 'minute' } },
|
|
543
|
+
'mm': { options: { minute: '2-digit' }, extract: { partType: 'minute', transform: ( v ) => v.padStart( 2, '0' ) } },
|
|
544
|
+
|
|
545
|
+
// Seconds - extract and pad manually due to Intl quirk
|
|
546
|
+
's': { options: { second: 'numeric' }, extract: { partType: 'second' } },
|
|
547
|
+
'ss': { options: { second: '2-digit' }, extract: { partType: 'second', transform: ( v ) => v.padStart( 2, '0' ) } },
|
|
548
|
+
|
|
549
|
+
// Milliseconds - extract manually
|
|
550
|
+
'S': { options: { fractionalSecondDigits: 1 }, extract: { partType: 'fractionalSecond' } },
|
|
551
|
+
'SS': { options: { fractionalSecondDigits: 2 }, extract: { partType: 'fractionalSecond' } },
|
|
552
|
+
'SSS': { options: { fractionalSecondDigits: 3 }, extract: { partType: 'fractionalSecond' } },
|
|
553
|
+
|
|
554
|
+
// Weekday
|
|
555
|
+
'D': { options: { weekday: 'narrow' } },
|
|
556
|
+
'DD': { options: { weekday: 'short' } },
|
|
557
|
+
'DDD': { options: { weekday: 'long' } },
|
|
558
|
+
|
|
559
|
+
// Era
|
|
560
|
+
'G': { options: { era: 'narrow' } },
|
|
561
|
+
'GG': { options: { era: 'short' } },
|
|
562
|
+
'GGG': { options: { era: 'long' } },
|
|
563
|
+
|
|
564
|
+
// Timezone
|
|
565
|
+
'Z': { options: { timeZoneName: 'short' } },
|
|
566
|
+
'ZZ': { options: { timeZoneName: 'long' } },
|
|
567
|
+
|
|
568
|
+
// Day period
|
|
569
|
+
'P': { options: { dayPeriod: 'narrow' } },
|
|
570
|
+
'PP': { options: { dayPeriod: 'short' } },
|
|
571
|
+
'PPP': { options: { dayPeriod: 'long' } },
|
|
572
|
+
|
|
573
|
+
// Meridiem - extract dayPeriod and lowercase it
|
|
574
|
+
'a': { options: { hour: 'numeric', hour12: true }, extract: { partType: 'dayPeriod', transform: ( v ) => v.toLowerCase() } },
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
// Format a token using Intl.DateTimeFormat
|
|
578
|
+
function formatType( type: string, length: number, date: Date, locale: string = 'en', timeZone: string = 'UTC' ): string | undefined {
|
|
579
|
+
const tokenKey = type.repeat( length );
|
|
580
|
+
const config = formatterConfigs[ tokenKey ];
|
|
581
|
+
|
|
582
|
+
if ( !config ) {
|
|
583
|
+
return undefined;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const formatter = new Intl.DateTimeFormat( locale, { ...config.options, timeZone } );
|
|
587
|
+
|
|
588
|
+
// If extraction metadata is specified, use it
|
|
589
|
+
if ( config.extract ) {
|
|
590
|
+
const part = formatter.formatToParts( date ).find( p => p.type === config.extract!.partType );
|
|
591
|
+
const value = part?.value;
|
|
592
|
+
|
|
593
|
+
if ( !value ) {
|
|
594
|
+
return undefined;
|
|
595
|
+
}
|
|
562
596
|
|
|
563
|
-
|
|
564
|
-
return
|
|
597
|
+
// Apply transformation if specified
|
|
598
|
+
return config.extract.transform ? config.extract.transform( value ) : value;
|
|
565
599
|
}
|
|
566
600
|
|
|
567
|
-
//
|
|
568
|
-
return
|
|
601
|
+
// Otherwise, format directly
|
|
602
|
+
return formatter.format( date );
|
|
569
603
|
}
|
|
570
604
|
|
|
571
605
|
function formatTimeInstant( instant: TimeInstant, pattern: string, config: { locale?: string, timeZone?: string } = {} ): string {
|
|
606
|
+
|
|
607
|
+
const date = instant.toDate();
|
|
608
|
+
const locale = config.locale || 'en';
|
|
609
|
+
const timeZone = config.timeZone || 'UTC';
|
|
610
|
+
|
|
572
611
|
return pattern
|
|
573
612
|
.split( ESCAPE_REGEX )
|
|
574
613
|
.filter( ( sub ) => sub !== undefined )
|
|
@@ -580,13 +619,29 @@ function formatTimeInstant( instant: TimeInstant, pattern: string, config: { loc
|
|
|
580
619
|
|
|
581
620
|
return sub.replace( PATTERN_REGEX, ( match ) => {
|
|
582
621
|
const type = match.charAt( 0 );
|
|
583
|
-
|
|
622
|
+
const length = match.length;
|
|
623
|
+
|
|
624
|
+
return formatType( type, length, date, locale, timeZone ) || match;
|
|
584
625
|
} );
|
|
585
626
|
} )
|
|
586
627
|
.join( '' );
|
|
587
628
|
}
|
|
588
629
|
|
|
589
630
|
function parseTimeInstant( dateString: string, pattern: string, base: TimeInstant, config: { locale?: string, timeZone?: string } = {} ): TimeInstant {
|
|
631
|
+
// Parse the components from the date string
|
|
632
|
+
const parsed = parseTimeInstantComponents( dateString, pattern, config );
|
|
633
|
+
|
|
634
|
+
// Fill in missing values from the base instant
|
|
635
|
+
const params = {
|
|
636
|
+
...base.toParameters(),
|
|
637
|
+
...parsed
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
// Create and return the TimeInstant with complete parameters
|
|
641
|
+
return TimeInstant.fromParameters( params );
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function parseTimeInstantComponents( dateString: string, pattern: string, config: { locale?: string, timeZone?: string } = {} ): Partial<TTimeInstantParameters> {
|
|
590
645
|
// Create a regex pattern from the format pattern
|
|
591
646
|
let regexPattern = pattern;
|
|
592
647
|
const tokens: { type: string; length: number; position: number }[] = [];
|
|
@@ -613,7 +668,7 @@ function parseTimeInstant( dateString: string, pattern: string, base: TimeInstan
|
|
|
613
668
|
case 'M':
|
|
614
669
|
if ( match.length === 1 ) return '(\\d{1,2})';
|
|
615
670
|
if ( match.length === 2 ) return '(\\d{2})';
|
|
616
|
-
if ( match.length === 3 ) return '([A-Za-z]{
|
|
671
|
+
if ( match.length === 3 ) return '([A-Za-z.]{1,7})';
|
|
617
672
|
return '([A-Za-z]+)';
|
|
618
673
|
case 'd':
|
|
619
674
|
return match.length === 1 ? '(\\d{1,2})' : '(\\d{2})';
|
|
@@ -652,16 +707,12 @@ function parseTimeInstant( dateString: string, pattern: string, base: TimeInstan
|
|
|
652
707
|
throw new Error( `Date string "${dateString}" does not match pattern "${pattern}"` );
|
|
653
708
|
}
|
|
654
709
|
|
|
655
|
-
// Extract values
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
let day = base.dayOfMonth;
|
|
659
|
-
let hour = base.toDate().getUTCHours();
|
|
660
|
-
let minute = base.toDate().getUTCMinutes();
|
|
661
|
-
let second = base.toDate().getUTCSeconds();
|
|
662
|
-
let millisecond = base.toDate().getUTCMilliseconds();
|
|
710
|
+
// Extract only the values that were actually parsed (no defaults)
|
|
711
|
+
const result: Partial<TTimeInstantParameters> = {};
|
|
712
|
+
const locale = config.locale || 'en';
|
|
663
713
|
let isPM = false;
|
|
664
714
|
let is12Hour = false;
|
|
715
|
+
let hourValue: number | undefined;
|
|
665
716
|
|
|
666
717
|
tokens.forEach( ( token, index ) => {
|
|
667
718
|
const value = matches[ index + 1 ];
|
|
@@ -670,52 +721,63 @@ function parseTimeInstant( dateString: string, pattern: string, base: TimeInstan
|
|
|
670
721
|
case 'y':
|
|
671
722
|
if ( token.length === 2 ) {
|
|
672
723
|
const shortYear = parseInt( value, 10 );
|
|
673
|
-
year = shortYear < 50 ? 2000 + shortYear : 1900 + shortYear;
|
|
724
|
+
result.year = shortYear < 50 ? 2000 + shortYear : 1900 + shortYear;
|
|
674
725
|
} else {
|
|
675
|
-
year = parseInt( value, 10 );
|
|
726
|
+
result.year = parseInt( value, 10 );
|
|
676
727
|
}
|
|
677
728
|
break;
|
|
678
729
|
case 'M':
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
const
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
730
|
+
switch ( token.length ) {
|
|
731
|
+
case 1:
|
|
732
|
+
case 2:
|
|
733
|
+
result.month = parseInt( value, 10 ) as TMonth;
|
|
734
|
+
break;
|
|
735
|
+
case 3: {
|
|
736
|
+
const normalizedValue = normalizeMonthName( value );
|
|
737
|
+
const monthIndex = getMonthNames( locale ).short.findIndex( name =>
|
|
738
|
+
name.toLowerCase() === normalizedValue.toLowerCase()
|
|
739
|
+
);
|
|
740
|
+
if ( monthIndex === -1 )
|
|
741
|
+
throw new Error( `Invalid short month name in date string: ${dateString}` );
|
|
742
|
+
result.month = ( monthIndex + 1 ) as TMonth;
|
|
743
|
+
break;
|
|
744
|
+
}
|
|
745
|
+
case 4: {
|
|
746
|
+
const normalizedValue = normalizeMonthName( value );
|
|
747
|
+
const monthIndex = getMonthNames( locale ).long.findIndex( name =>
|
|
748
|
+
name.toLowerCase() === normalizedValue.toLowerCase()
|
|
749
|
+
);
|
|
750
|
+
if ( monthIndex === -1 )
|
|
751
|
+
throw new Error( `Invalid full month name in date string: ${dateString}` );
|
|
752
|
+
result.month = ( monthIndex + 1 ) as TMonth;
|
|
753
|
+
break;
|
|
754
|
+
}
|
|
755
|
+
default:
|
|
756
|
+
throw new Error( `Invalid month pattern: ${token}` );
|
|
695
757
|
}
|
|
696
758
|
break;
|
|
697
759
|
case 'd':
|
|
698
|
-
|
|
760
|
+
result.date = parseInt( value, 10 ) as TDayOfMonth;
|
|
699
761
|
break;
|
|
700
762
|
case 'H':
|
|
701
|
-
|
|
763
|
+
result.hours = parseInt( value, 10 ) as THourOfDay;
|
|
702
764
|
break;
|
|
703
765
|
case 'h':
|
|
704
|
-
|
|
766
|
+
hourValue = parseInt( value, 10 );
|
|
705
767
|
is12Hour = true;
|
|
706
768
|
break;
|
|
707
769
|
case 'm':
|
|
708
|
-
|
|
770
|
+
result.minutes = parseInt( value, 10 ) as TMinuteOfHour;
|
|
709
771
|
break;
|
|
710
772
|
case 's':
|
|
711
|
-
|
|
773
|
+
result.seconds = parseInt( value, 10 ) as TSecondOfMinute;
|
|
712
774
|
break;
|
|
713
775
|
case 'S':
|
|
714
776
|
let ms = parseInt( value, 10 );
|
|
715
777
|
// Normalize to milliseconds based on length
|
|
716
778
|
if ( token.length === 1 ) ms *= 100;
|
|
717
779
|
else if ( token.length === 2 ) ms *= 10;
|
|
718
|
-
|
|
780
|
+
result.milliseconds = ms as TMillisecondOfSecond;
|
|
719
781
|
break;
|
|
720
782
|
case 'a':
|
|
721
783
|
isPM = value.toLowerCase().includes( 'p' );
|
|
@@ -723,30 +785,39 @@ function parseTimeInstant( dateString: string, pattern: string, base: TimeInstan
|
|
|
723
785
|
}
|
|
724
786
|
} );
|
|
725
787
|
|
|
726
|
-
// Handle 12-hour format
|
|
727
|
-
if ( is12Hour &&
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
if (
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
788
|
+
// Handle 12-hour format conversion
|
|
789
|
+
if ( is12Hour && hourValue !== undefined ) {
|
|
790
|
+
if ( isPM && hourValue < 12 ) {
|
|
791
|
+
result.hours = ( hourValue + 12 ) as THourOfDay;
|
|
792
|
+
} else if ( !isPM && hourValue === 12 ) {
|
|
793
|
+
result.hours = 0 as THourOfDay;
|
|
794
|
+
} else {
|
|
795
|
+
result.hours = hourValue as THourOfDay;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Validate each extracted component (only validate primitives)
|
|
800
|
+
if ( typeof result.year === 'number' && !isValidYear( result.year ) ) {
|
|
801
|
+
throw new Error( `Invalid year in date string: ${dateString}` );
|
|
802
|
+
}
|
|
803
|
+
if ( typeof result.month === 'number' && !isValidMonth( result.month ) ) {
|
|
804
|
+
throw new Error( `Invalid month in date string: ${dateString}` );
|
|
805
|
+
}
|
|
806
|
+
if ( typeof result.date === 'number' && !isValidDayOfMonth( result.date ) ) {
|
|
807
|
+
throw new Error( `Invalid day in date string: ${dateString}` );
|
|
808
|
+
}
|
|
809
|
+
if ( typeof result.hours === 'number' && !isValidHour( result.hours ) ) {
|
|
810
|
+
throw new Error( `Invalid hour in date string: ${dateString}` );
|
|
811
|
+
}
|
|
812
|
+
if ( typeof result.minutes === 'number' && !isValidMinute( result.minutes ) ) {
|
|
813
|
+
throw new Error( `Invalid minute in date string: ${dateString}` );
|
|
814
|
+
}
|
|
815
|
+
if ( typeof result.seconds === 'number' && !isValidSecond( result.seconds ) ) {
|
|
816
|
+
throw new Error( `Invalid second in date string: ${dateString}` );
|
|
817
|
+
}
|
|
818
|
+
if ( typeof result.milliseconds === 'number' && !isValidMillisecond( result.milliseconds ) ) {
|
|
819
|
+
throw new Error( `Invalid millisecond in date string: ${dateString}` );
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
return result;
|
|
752
823
|
}
|