@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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@zelgadis87/utils-core",
4
- "version": "5.2.10",
4
+ "version": "5.2.11",
5
5
  "author": "Zelgadis87",
6
6
  "license": "ISC",
7
7
  "private": false,
@@ -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
- return this.distanceFrom( this.atStartOfDay() );
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
- return this.distanceFrom( this.atEndOfDay() );
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
- return Math.floor( this.days ) === Math.floor( TimeInstant.now().days );
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 parse( dateString: string, pattern: string, base: TimeInstant = TimeInstant.now(), config: { locale?: string, timeZone?: string } = {} ): TimeInstant {
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
- // Syntatic sugar for this.ms;
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 toDateUTC(): Date {
189
- return new Date( this.ms + new Date().getTimezoneOffset() * 60 * 1000 );
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
- * 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.
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
- * 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).
377
- * As such, we basically have to count how many Thursdays there has been in this year.
378
- * Please note that the thursdayOfThisWeek could be in the previous year.
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
- const thursdayOfThisWeek = new Date( date.getFullYear(), date.getMonth(), date.getDate() + 4 - ( date.getDay() || 7 ), 14, 0, 0 );
383
- const firstOfJanuary = new Date( thursdayOfThisWeek.getFullYear(), 0, 1, 14, 0, 0 );
384
- const dayOfTheYear = Math.round( ( thursdayOfThisWeek.getTime() - firstOfJanuary.getTime() ) / oneDay );
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
- return { weekNumber: weekNumber as TWeekNumber, year: thursdayOfThisWeek.getFullYear() };
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
- const PATTERN_REGEX = /(M|y|d|D|h|H|m|s|S|G|Z|P|a)+/g;
449
- const ESCAPE_REGEX = /\\"|"((?:\\"|[^"])*)"/g;
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
- if ( type === 'h' && ( length === 1 || length === 2 ) ) {
517
- let hours = instant.toDate().getUTCHours();
518
- hours = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours;
519
- return length === 2 && hours < 10 ? '0' + hours : String( hours );
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
- if ( type === 'm' && ( length === 1 || length === 2 ) ) {
523
- const minutes = instant.toDate().getUTCMinutes();
524
- return length === 2 && minutes < 10 ? '0' + minutes : String( minutes );
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
- if ( type === 's' && ( length === 1 || length === 2 ) ) {
528
- const seconds = instant.toDate().getUTCSeconds();
529
- return length === 2 && seconds < 10 ? '0' + seconds : String( seconds );
530
- }
475
+ const short: string[] = [];
476
+ const long: string[] = [];
531
477
 
532
- if ( type === 'S' ) {
533
- const ms = instant.toDate().getUTCMilliseconds();
534
- if ( length === 1 ) return String( Math.floor( ms / 100 ) );
535
- if ( length === 2 ) return String( Math.floor( ms / 10 ) ).padStart( 2, '0' );
536
- if ( length === 3 ) return String( ms ).padStart( 3, '0' );
537
- return String( ms );
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
- // For timezone-sensitive and locale-sensitive formatting, use Date with Intl
541
- const date = instant.toDate();
542
- const option = optionNames[ type as keyof typeof optionNames ];
543
- const value = values[ type as keyof typeof values ][ length - 1 ];
486
+ const result = { short, long };
487
+ monthNamesCache.set( locale, result );
488
+ return result;
489
+ }
544
490
 
545
- if ( !value ) {
546
- return;
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
- const options = {
550
- [ option ]: value,
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
- if ( type === 'a' ) {
555
- return Intl.DateTimeFormat( locale, {
556
- ...options,
557
- hour: 'numeric',
558
- } )
559
- .formatToParts( date )
560
- .pop()?.value;
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
- if ( type === 'G' || type === 'Z' ) {
564
- return Intl.DateTimeFormat( locale, options ).formatToParts( date ).pop()?.value;
597
+ // Apply transformation if specified
598
+ return config.extract.transform ? config.extract.transform( value ) : value;
565
599
  }
566
600
 
567
- // For other complex formatting, fall back to Intl
568
- return Intl.DateTimeFormat( locale, options ).format( date );
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
- return formatType( instant, type, match.length, config ) || match;
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]{3})';
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 from matches using base instant as default
656
- let year = base.year;
657
- let month = base.month;
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
- if ( token.length <= 2 ) {
680
- month = parseInt( value, 10 ) as TMonth;
681
- } else {
682
- // Handle month names - simplified approach
683
- const monthNames = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
684
- 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ];
685
- const longMonthNames = [ 'January', 'February', 'March', 'April', 'May', 'June',
686
- 'July', 'August', 'September', 'October', 'November', 'December' ];
687
-
688
- let monthIndex = monthNames.findIndex( name =>
689
- name.toLowerCase() === value.toLowerCase() );
690
- if ( monthIndex === -1 ) {
691
- monthIndex = longMonthNames.findIndex( name =>
692
- name.toLowerCase() === value.toLowerCase() );
693
- }
694
- month = ( monthIndex !== -1 ? monthIndex + 1 : 1 ) as TMonth;
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
- day = parseInt( value, 10 ) as TDayOfMonth;
760
+ result.date = parseInt( value, 10 ) as TDayOfMonth;
699
761
  break;
700
762
  case 'H':
701
- hour = parseInt( value, 10 );
763
+ result.hours = parseInt( value, 10 ) as THourOfDay;
702
764
  break;
703
765
  case 'h':
704
- hour = parseInt( value, 10 );
766
+ hourValue = parseInt( value, 10 );
705
767
  is12Hour = true;
706
768
  break;
707
769
  case 'm':
708
- minute = parseInt( value, 10 );
770
+ result.minutes = parseInt( value, 10 ) as TMinuteOfHour;
709
771
  break;
710
772
  case 's':
711
- second = parseInt( value, 10 );
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
- millisecond = ms;
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 && isPM && hour < 12 ) {
728
- hour += 12;
729
- } else if ( is12Hour && !isPM && hour === 12 ) {
730
- hour = 0;
731
- }
732
-
733
- // Validate each component using standalone validation functions
734
- if ( !isValidYear( year ) ) throw new Error( `Invalid year in date string: ${dateString}` );
735
- if ( !isValidMonth( month ) ) throw new Error( `Invalid month in date string: ${dateString}` );
736
- if ( !isValidDayOfMonth( day ) ) throw new Error( `Invalid day in date string: ${dateString}` );
737
- if ( !isValidHour( hour ) ) throw new Error( `Invalid hour in date string: ${dateString}` );
738
- if ( !isValidMinute( minute ) ) throw new Error( `Invalid minute in date string: ${dateString}` );
739
- if ( !isValidSecond( second ) ) throw new Error( `Invalid second in date string: ${dateString}` );
740
- if ( !isValidMillisecond( millisecond ) ) throw new Error( `Invalid millisecond in date string: ${dateString}` );
741
-
742
- // Use TimeInstant.fromParameters instead of creating a Date
743
- return TimeInstant.fromParameters( {
744
- year,
745
- month: month as TMonth,
746
- date: day as TDayOfMonth,
747
- hours: hour as THourOfDay,
748
- minutes: minute as TMinuteOfHour,
749
- seconds: second as TSecondOfMinute,
750
- milliseconds: millisecond as TMillisecondOfSecond,
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
  }