@zelgadis87/utils-core 5.3.3 → 5.3.4
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/.rollup/index.cjs +215 -3
- package/.rollup/index.cjs.map +1 -1
- package/.rollup/index.d.ts +18 -2
- package/.rollup/index.mjs +214 -4
- package/.rollup/index.mjs.map +1 -1
- package/.rollup/tsconfig.tsbuildinfo +1 -1
- package/CHANGELOG.md +7 -1
- package/package.json +2 -2
- package/src/time/TimeInstant.ts +372 -99
package/src/time/TimeInstant.ts
CHANGED
|
@@ -4,7 +4,7 @@ import TimeBase from "./TimeBase";
|
|
|
4
4
|
import TimeDuration from "./TimeDuration";
|
|
5
5
|
import { TTimeInstantBuilder, TTimeInstantCreationParameters, createTimeInstantFromParameters, timeInstantBuilder, type TTimeInstantParameters } from "./TimeInstantBuilder.js";
|
|
6
6
|
import { TimeUnit } from "./TimeUnit";
|
|
7
|
-
import { TDayOfMonth,
|
|
7
|
+
import { TDayOfMonth, THourOfDay, TIso8601DateUtcString, TMillisecondOfSecond, TMinuteOfHour, TMonth, TSecondOfMinute, TWeekNumber, type TYear } from "./types";
|
|
8
8
|
|
|
9
9
|
export class TimeInstant extends TimeBase<TimeInstant> {
|
|
10
10
|
|
|
@@ -287,8 +287,8 @@ export class TimeInstant extends TimeBase<TimeInstant> {
|
|
|
287
287
|
}
|
|
288
288
|
|
|
289
289
|
public static fromIso8601( str: string ) {
|
|
290
|
-
// Regex to capture: YYYY-MM-DDTHH:mm:ss.
|
|
291
|
-
const iso8601Regex = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})
|
|
290
|
+
// Regex to capture: YYYY-MM-DDTHH:mm:ss[.sss]Z or YYYY-MM-DDTHH:mm:ss[.sss]±HH:mm
|
|
291
|
+
const iso8601Regex = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{3})|)(Z|[+-]\d{2}:\d{2})?$/;
|
|
292
292
|
const match = str.match( iso8601Regex );
|
|
293
293
|
|
|
294
294
|
if ( !match ) {
|
|
@@ -304,7 +304,7 @@ export class TimeInstant extends TimeBase<TimeInstant> {
|
|
|
304
304
|
const hours = parseInt( hourStr, 10 );
|
|
305
305
|
const minutes = parseInt( minuteStr, 10 );
|
|
306
306
|
const seconds = parseInt( secondStr, 10 );
|
|
307
|
-
const milliseconds = parseInt( millisecondStr, 10 );
|
|
307
|
+
const milliseconds = millisecondStr ? parseInt( millisecondStr, 10 ) : 0;
|
|
308
308
|
|
|
309
309
|
// Validate each component using standalone validation functions
|
|
310
310
|
if ( !isValidYear( year ) )
|
|
@@ -641,7 +641,7 @@ function parseTimeInstant( dateString: string, pattern: string, base: TimeInstan
|
|
|
641
641
|
return TimeInstant.fromParameters( params );
|
|
642
642
|
}
|
|
643
643
|
|
|
644
|
-
function parseTimeInstantComponents( dateString: string, pattern: string, config: { locale?: string, timeZone?: string } = {} ): Partial<TTimeInstantParameters> {
|
|
644
|
+
export function parseTimeInstantComponents( dateString: string, pattern: string, config: { locale?: string, timeZone?: string } = {} ): Partial<TTimeInstantParameters> {
|
|
645
645
|
// Create a regex pattern from the format pattern
|
|
646
646
|
let regexPattern = pattern;
|
|
647
647
|
const tokens: { type: string; length: number; position: number }[] = [];
|
|
@@ -663,39 +663,39 @@ function parseTimeInstantComponents( dateString: string, pattern: string, config
|
|
|
663
663
|
|
|
664
664
|
// Create appropriate regex for each token type
|
|
665
665
|
switch ( type ) {
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
666
|
+
case 'y':
|
|
667
|
+
return match.length === 2 ? '(\\d{2})' : '(\\d{4})';
|
|
668
|
+
case 'M':
|
|
669
|
+
if ( match.length === 1 ) return '(\\d{1,2})';
|
|
670
|
+
if ( match.length === 2 ) return '(\\d{2})';
|
|
671
|
+
if ( match.length === 3 ) return '([A-Za-z.]{1,7})';
|
|
672
|
+
return '([A-Za-z]+)';
|
|
673
|
+
case 'd':
|
|
674
|
+
return match.length === 1 ? '(\\d{1,2})' : '(\\d{2})';
|
|
675
|
+
case 'H':
|
|
676
|
+
case 'h':
|
|
677
|
+
return match.length === 1 ? '(\\d{1,2})' : '(\\d{2})';
|
|
678
|
+
case 'm':
|
|
679
|
+
case 's':
|
|
680
|
+
return match.length === 1 ? '(\\d{1,2})' : '(\\d{2})';
|
|
681
|
+
case 'S':
|
|
682
|
+
return `(\\d{${match.length}})`;
|
|
683
|
+
case 'a':
|
|
684
|
+
return '([aApP][mM])';
|
|
685
|
+
case 'D':
|
|
686
|
+
if ( match.length === 1 ) return '([A-Za-z])';
|
|
687
|
+
if ( match.length === 2 ) return '([A-Za-z]{3})';
|
|
688
|
+
return '([A-Za-z]+)';
|
|
689
|
+
case 'G':
|
|
690
|
+
if ( match.length === 1 ) return '([A-Za-z])';
|
|
691
|
+
if ( match.length === 2 ) return '([A-Za-z]{2})';
|
|
692
|
+
return '([A-Za-z\\s]+)';
|
|
693
|
+
case 'Z':
|
|
694
|
+
return match.length === 1 ? '([A-Za-z0-9+\\-:]+)' : '([A-Za-z\\s]+)';
|
|
695
|
+
case 'P':
|
|
696
|
+
return '([A-Za-z\\s]+)';
|
|
697
|
+
default:
|
|
698
|
+
return match;
|
|
699
699
|
}
|
|
700
700
|
} );
|
|
701
701
|
} ).join( '' );
|
|
@@ -718,70 +718,70 @@ function parseTimeInstantComponents( dateString: string, pattern: string, config
|
|
|
718
718
|
const value = matches[ index + 1 ];
|
|
719
719
|
|
|
720
720
|
switch ( token.type ) {
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
break;
|
|
729
|
-
case 'M':
|
|
730
|
-
switch ( token.length ) {
|
|
731
|
-
case 1:
|
|
732
|
-
case 2:
|
|
733
|
-
result.month = parseInt( value, 10 ) as TMonth;
|
|
721
|
+
case 'y':
|
|
722
|
+
if ( token.length === 2 ) {
|
|
723
|
+
const shortYear = parseInt( value, 10 );
|
|
724
|
+
result.year = shortYear < 50 ? 2000 + shortYear : 1900 + shortYear;
|
|
725
|
+
} else {
|
|
726
|
+
result.year = parseInt( value, 10 );
|
|
727
|
+
}
|
|
734
728
|
break;
|
|
735
|
-
case
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
729
|
+
case 'M':
|
|
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}` );
|
|
757
|
+
}
|
|
743
758
|
break;
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
759
|
+
case 'd':
|
|
760
|
+
result.date = parseInt( value, 10 ) as TDayOfMonth;
|
|
761
|
+
break;
|
|
762
|
+
case 'H':
|
|
763
|
+
result.hours = parseInt( value, 10 ) as THourOfDay;
|
|
764
|
+
break;
|
|
765
|
+
case 'h':
|
|
766
|
+
hourValue = parseInt( value, 10 );
|
|
767
|
+
is12Hour = true;
|
|
768
|
+
break;
|
|
769
|
+
case 'm':
|
|
770
|
+
result.minutes = parseInt( value, 10 ) as TMinuteOfHour;
|
|
771
|
+
break;
|
|
772
|
+
case 's':
|
|
773
|
+
result.seconds = parseInt( value, 10 ) as TSecondOfMinute;
|
|
774
|
+
break;
|
|
775
|
+
case 'S':
|
|
776
|
+
let ms = parseInt( value, 10 );
|
|
777
|
+
// Normalize to milliseconds based on length
|
|
778
|
+
if ( token.length === 1 ) ms *= 100;
|
|
779
|
+
else if ( token.length === 2 ) ms *= 10;
|
|
780
|
+
result.milliseconds = ms as TMillisecondOfSecond;
|
|
781
|
+
break;
|
|
782
|
+
case 'a':
|
|
783
|
+
isPM = value.toLowerCase().includes( 'p' );
|
|
753
784
|
break;
|
|
754
|
-
}
|
|
755
|
-
default:
|
|
756
|
-
throw new Error( `Invalid month pattern: ${token}` );
|
|
757
|
-
}
|
|
758
|
-
break;
|
|
759
|
-
case 'd':
|
|
760
|
-
result.date = parseInt( value, 10 ) as TDayOfMonth;
|
|
761
|
-
break;
|
|
762
|
-
case 'H':
|
|
763
|
-
result.hours = parseInt( value, 10 ) as THourOfDay;
|
|
764
|
-
break;
|
|
765
|
-
case 'h':
|
|
766
|
-
hourValue = parseInt( value, 10 );
|
|
767
|
-
is12Hour = true;
|
|
768
|
-
break;
|
|
769
|
-
case 'm':
|
|
770
|
-
result.minutes = parseInt( value, 10 ) as TMinuteOfHour;
|
|
771
|
-
break;
|
|
772
|
-
case 's':
|
|
773
|
-
result.seconds = parseInt( value, 10 ) as TSecondOfMinute;
|
|
774
|
-
break;
|
|
775
|
-
case 'S':
|
|
776
|
-
let ms = parseInt( value, 10 );
|
|
777
|
-
// Normalize to milliseconds based on length
|
|
778
|
-
if ( token.length === 1 ) ms *= 100;
|
|
779
|
-
else if ( token.length === 2 ) ms *= 10;
|
|
780
|
-
result.milliseconds = ms as TMillisecondOfSecond;
|
|
781
|
-
break;
|
|
782
|
-
case 'a':
|
|
783
|
-
isPM = value.toLowerCase().includes( 'p' );
|
|
784
|
-
break;
|
|
785
785
|
}
|
|
786
786
|
} );
|
|
787
787
|
|
|
@@ -821,3 +821,276 @@ function parseTimeInstantComponents( dateString: string, pattern: string, config
|
|
|
821
821
|
|
|
822
822
|
return result;
|
|
823
823
|
}
|
|
824
|
+
|
|
825
|
+
// Basic patterns that don't require Intl (24-hour format only)
|
|
826
|
+
export type TBasicTimePattern =
|
|
827
|
+
// Year patterns
|
|
828
|
+
| 'y' | 'yy' | 'yyyy'
|
|
829
|
+
// Month patterns (numeric only)
|
|
830
|
+
| 'M' | 'MM'
|
|
831
|
+
// Day patterns
|
|
832
|
+
| 'd' | 'dd'
|
|
833
|
+
// Hour patterns (24-hour only)
|
|
834
|
+
| 'H' | 'HH'
|
|
835
|
+
// Minute patterns
|
|
836
|
+
| 'm' | 'mm'
|
|
837
|
+
// Second patterns
|
|
838
|
+
| 's' | 'ss'
|
|
839
|
+
// Millisecond patterns
|
|
840
|
+
| 'S' | 'SS' | 'SSS'
|
|
841
|
+
// Combination patterns (basic only)
|
|
842
|
+
| 'yyyy-MM-dd'
|
|
843
|
+
| 'yyyy-MM-dd HH:mm'
|
|
844
|
+
| 'yyyy-MM-dd HH:mm:ss'
|
|
845
|
+
| 'yyyy-MM-dd HH:mm:ss.SSS'
|
|
846
|
+
| 'yyyy/MM/dd'
|
|
847
|
+
| 'yyyy/MM/dd HH:mm'
|
|
848
|
+
| 'yyyy/MM/dd HH:mm:ss'
|
|
849
|
+
| 'yyyy/MM/dd HH:mm:ss.SSS'
|
|
850
|
+
| 'MM/dd/yyyy'
|
|
851
|
+
| 'MM/dd/yyyy HH:mm'
|
|
852
|
+
| 'MM/dd/yyyy HH:mm:ss'
|
|
853
|
+
| 'MM/dd/yyyy HH:mm:ss.SSS'
|
|
854
|
+
| 'dd/MM/yyyy'
|
|
855
|
+
| 'dd/MM/yyyy HH:mm'
|
|
856
|
+
| 'dd/MM/yyyy HH:mm:ss'
|
|
857
|
+
| 'dd/MM/yyyy HH:mm:ss.SSS'
|
|
858
|
+
| 'HH:mm:ss'
|
|
859
|
+
| 'HH:mm';
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Parses a date string using only basic patterns that don't require Intl.
|
|
863
|
+
* This is a safer alternative to parseTimeInstantComponents that works even when Intl is not available.
|
|
864
|
+
*
|
|
865
|
+
* @param dateString The date string to parse
|
|
866
|
+
* @param pattern A basic pattern that doesn't require Intl (strongly typed)
|
|
867
|
+
* @returns Partial time instant parameters that were parsed from the string
|
|
868
|
+
* @throws Error if the string doesn't match the pattern or contains invalid values
|
|
869
|
+
*/
|
|
870
|
+
export function parseTimeInstantBasicComponents(
|
|
871
|
+
dateString: string,
|
|
872
|
+
pattern: TBasicTimePattern
|
|
873
|
+
): Partial<TTimeInstantParameters> {
|
|
874
|
+
|
|
875
|
+
// Check if Intl is available, if so warn the user about the existing function
|
|
876
|
+
const isIntlAvailable = typeof Intl !== 'undefined' && typeof Intl.DateTimeFormat !== 'undefined';
|
|
877
|
+
if ( isIntlAvailable )
|
|
878
|
+
console.warn( 'Intl is available, use parseTimeInstantComponents instead of parseTimeInstantBasicComponents.' );
|
|
879
|
+
|
|
880
|
+
const result: Partial<TTimeInstantParameters> = {};
|
|
881
|
+
|
|
882
|
+
let patternIndex = 0;
|
|
883
|
+
let dateStringIndex = 0;
|
|
884
|
+
|
|
885
|
+
// Helper function to check if a character is a pattern token
|
|
886
|
+
const isPatternChar = ( char: string ): boolean => [ 'y', 'M', 'd', 'H', 'm', 's', 'S' ].includes( char );
|
|
887
|
+
|
|
888
|
+
// Token extraction rules dictionary
|
|
889
|
+
const tokenRules: Record<string, {
|
|
890
|
+
maxLength?: number;
|
|
891
|
+
validator?: (value: string) => boolean;
|
|
892
|
+
description?: string;
|
|
893
|
+
}> = {
|
|
894
|
+
'': { maxLength: undefined }, // Default rule
|
|
895
|
+
'H': {
|
|
896
|
+
maxLength: 2,
|
|
897
|
+
validator: (value) => /^\d{1,2}$/.test(value) && parseInt(value, 10) >= 0 && parseInt(value, 10) <= 23,
|
|
898
|
+
description: 'Hour (0-23, 1-2 digits)'
|
|
899
|
+
},
|
|
900
|
+
'HH': {
|
|
901
|
+
maxLength: 2,
|
|
902
|
+
validator: (value) => /^\d{2}$/.test(value) && parseInt(value, 10) >= 0 && parseInt(value, 10) <= 23,
|
|
903
|
+
description: 'Hour (00-23, exactly 2 digits)'
|
|
904
|
+
},
|
|
905
|
+
'M': {
|
|
906
|
+
maxLength: 2,
|
|
907
|
+
validator: (value) => /^\d{1,2}$/.test(value) && parseInt(value, 10) >= 1 && parseInt(value, 10) <= 12,
|
|
908
|
+
description: 'Month (1-12, 1-2 digits)'
|
|
909
|
+
},
|
|
910
|
+
'MM': {
|
|
911
|
+
maxLength: 2,
|
|
912
|
+
validator: (value) => /^\d{2}$/.test(value) && parseInt(value, 10) >= 1 && parseInt(value, 10) <= 12,
|
|
913
|
+
description: 'Month (01-12, exactly 2 digits)'
|
|
914
|
+
},
|
|
915
|
+
'd': {
|
|
916
|
+
maxLength: 2,
|
|
917
|
+
validator: (value) => /^\d{1,2}$/.test(value) && parseInt(value, 10) >= 1 && parseInt(value, 10) <= 31,
|
|
918
|
+
description: 'Day (1-31, 1-2 digits)'
|
|
919
|
+
},
|
|
920
|
+
'dd': {
|
|
921
|
+
maxLength: 2,
|
|
922
|
+
validator: (value) => /^\d{2}$/.test(value) && parseInt(value, 10) >= 1 && parseInt(value, 10) <= 31,
|
|
923
|
+
description: 'Day (01-31, exactly 2 digits)'
|
|
924
|
+
},
|
|
925
|
+
'm': {
|
|
926
|
+
maxLength: 2,
|
|
927
|
+
validator: (value) => /^\d{1,2}$/.test(value) && parseInt(value, 10) >= 0 && parseInt(value, 10) <= 59,
|
|
928
|
+
description: 'Minutes (0-59, 1-2 digits)'
|
|
929
|
+
},
|
|
930
|
+
'mm': {
|
|
931
|
+
maxLength: 2,
|
|
932
|
+
validator: (value) => /^\d{2}$/.test(value) && parseInt(value, 10) >= 0 && parseInt(value, 10) <= 59,
|
|
933
|
+
description: 'Minutes (00-59, exactly 2 digits)'
|
|
934
|
+
},
|
|
935
|
+
's': {
|
|
936
|
+
maxLength: 2,
|
|
937
|
+
validator: (value) => /^\d{1,2}$/.test(value) && parseInt(value, 10) >= 0 && parseInt(value, 10) <= 59,
|
|
938
|
+
description: 'Seconds (0-59, 1-2 digits)'
|
|
939
|
+
},
|
|
940
|
+
'ss': {
|
|
941
|
+
maxLength: 2,
|
|
942
|
+
validator: (value) => /^\d{2}$/.test(value) && parseInt(value, 10) >= 0 && parseInt(value, 10) <= 59,
|
|
943
|
+
description: 'Seconds (00-59, exactly 2 digits)'
|
|
944
|
+
},
|
|
945
|
+
'S': {
|
|
946
|
+
maxLength: 3,
|
|
947
|
+
validator: (value) => /^\d{1,3}$/.test(value) && parseInt(value, 10) >= 0 && parseInt(value, 10) <= 999,
|
|
948
|
+
description: 'Milliseconds (0-999, 1-3 digits)'
|
|
949
|
+
},
|
|
950
|
+
'SS': {
|
|
951
|
+
maxLength: 2,
|
|
952
|
+
validator: (value) => /^\d{2}$/.test(value) && parseInt(value, 10) >= 0 && parseInt(value, 10) <= 99,
|
|
953
|
+
description: 'Milliseconds (0-99, exactly 2 digits)'
|
|
954
|
+
},
|
|
955
|
+
'SSS': {
|
|
956
|
+
maxLength: 3,
|
|
957
|
+
validator: (value) => /^\d{3}$/.test(value) && parseInt(value, 10) >= 0 && parseInt(value, 10) <= 999,
|
|
958
|
+
description: 'Milliseconds (000-999, exactly 3 digits)'
|
|
959
|
+
},
|
|
960
|
+
'y': {
|
|
961
|
+
maxLength: 4,
|
|
962
|
+
validator: (value) => /^\d{1,4}$/.test(value) && parseInt(value, 10) >= 0 && parseInt(value, 10) <= 9999,
|
|
963
|
+
description: 'Year (0-9999, 1-4 digits)'
|
|
964
|
+
},
|
|
965
|
+
'yy': {
|
|
966
|
+
maxLength: 2,
|
|
967
|
+
validator: (value) => /^\d{2}$/.test(value),
|
|
968
|
+
description: 'Year (2 digits)'
|
|
969
|
+
},
|
|
970
|
+
'yyyy': {
|
|
971
|
+
maxLength: 4,
|
|
972
|
+
validator: (value) => /^\d{4}$/.test(value),
|
|
973
|
+
description: 'Year (4 digits)'
|
|
974
|
+
}
|
|
975
|
+
};
|
|
976
|
+
|
|
977
|
+
// Helper function to extract a token value from the date string using token rules
|
|
978
|
+
const extractValue = ( tokenType: string, tokenLength: number ): string => {
|
|
979
|
+
const tokenKey = tokenType.repeat( tokenLength );
|
|
980
|
+
const rule = tokenRules[ tokenKey ] || tokenRules[ '' ];
|
|
981
|
+
|
|
982
|
+
// Extract consecutive digits up to maxLength
|
|
983
|
+
let endIndex = dateStringIndex;
|
|
984
|
+
let digitCount = 0;
|
|
985
|
+
const maxChars = rule.maxLength || tokenLength;
|
|
986
|
+
|
|
987
|
+
while ( endIndex < dateString.length && /\d/.test( dateString[ endIndex ] ) && digitCount < maxChars ) {
|
|
988
|
+
endIndex++;
|
|
989
|
+
digitCount++;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
const value = dateString.substring( dateStringIndex, endIndex );
|
|
993
|
+
dateStringIndex = endIndex;
|
|
994
|
+
|
|
995
|
+
// Validate the extracted value if a validator is provided
|
|
996
|
+
if ( rule.validator && !rule.validator( value ) ) {
|
|
997
|
+
// Extract field name from description for better error messages
|
|
998
|
+
const fieldName = (rule.description || tokenKey).split(' ')[0].toLowerCase();
|
|
999
|
+
throw new Error( `Invalid ${fieldName} value: "${value}"` );
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
return value;
|
|
1003
|
+
};
|
|
1004
|
+
|
|
1005
|
+
// Helper function to parse and validate a numeric value
|
|
1006
|
+
const parseNumericValue = ( value: string, min: number, max: number, field: string ): number => {
|
|
1007
|
+
const num = parseInt( value, 10 );
|
|
1008
|
+
if ( isNaN( num ) || num < min || num > max )
|
|
1009
|
+
throw new Error( `Invalid ${field} value: ${value}` );
|
|
1010
|
+
return num;
|
|
1011
|
+
};
|
|
1012
|
+
|
|
1013
|
+
while ( patternIndex < pattern.length ) {
|
|
1014
|
+
const patternChar = pattern[ patternIndex ];
|
|
1015
|
+
|
|
1016
|
+
if ( isPatternChar( patternChar ) ) {
|
|
1017
|
+
|
|
1018
|
+
// Start of a token - determine its full length
|
|
1019
|
+
let tokenLength = 0;
|
|
1020
|
+
while ( patternIndex + tokenLength < pattern.length &&
|
|
1021
|
+
pattern[ patternIndex + tokenLength ] === patternChar ) {
|
|
1022
|
+
tokenLength++;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// Extract and parse the corresponding value from the date string
|
|
1026
|
+
const value = extractValue( patternChar, tokenLength );
|
|
1027
|
+
|
|
1028
|
+
// Check if we got enough characters for this token (after extraction)
|
|
1029
|
+
if ( value.length < tokenLength ) {
|
|
1030
|
+
throw new Error( `Date string "${dateString}" does not match pattern "${pattern}"` );
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// Debug: log pattern processing (commented out for production)
|
|
1034
|
+
// console.log( `Processing patternChar="${patternChar}" with tokenLength=${tokenLength}, value="${value}"` );
|
|
1035
|
+
|
|
1036
|
+
switch ( patternChar ) {
|
|
1037
|
+
case 'y': {
|
|
1038
|
+
if ( tokenLength === 2 ) {
|
|
1039
|
+
// 2-digit year
|
|
1040
|
+
const shortYear = parseNumericValue( value, 0, 99, 'year' );
|
|
1041
|
+
result.year = shortYear < 50 ? 2000 + shortYear : 1900 + shortYear;
|
|
1042
|
+
} else {
|
|
1043
|
+
// 4-digit year
|
|
1044
|
+
result.year = parseNumericValue( value, 0, 9999, 'year' );
|
|
1045
|
+
}
|
|
1046
|
+
break;
|
|
1047
|
+
}
|
|
1048
|
+
case 'M':
|
|
1049
|
+
result.month = parseNumericValue( value, 1, 12, 'month' ) as TMonth;
|
|
1050
|
+
break;
|
|
1051
|
+
case 'd':
|
|
1052
|
+
result.date = parseNumericValue( value, 1, 31, 'day' ) as TDayOfMonth;
|
|
1053
|
+
break;
|
|
1054
|
+
case 'H':
|
|
1055
|
+
result.hours = parseNumericValue( value, 0, 23, 'hours' ) as THourOfDay;
|
|
1056
|
+
break;
|
|
1057
|
+
case 'm':
|
|
1058
|
+
result.minutes = parseNumericValue( value, 0, 59, 'minutes' ) as TMinuteOfHour;
|
|
1059
|
+
break;
|
|
1060
|
+
case 's':
|
|
1061
|
+
result.seconds = parseNumericValue( value, 0, 59, 'seconds' ) as TSecondOfMinute;
|
|
1062
|
+
break;
|
|
1063
|
+
case 'S': {
|
|
1064
|
+
let ms = parseInt( value, 10 );
|
|
1065
|
+
if ( isNaN( ms ) )
|
|
1066
|
+
throw new Error( `Invalid milliseconds value: ${value}` );
|
|
1067
|
+
|
|
1068
|
+
// Normalize to milliseconds based on length
|
|
1069
|
+
if ( tokenLength === 1 ) ms *= 100;
|
|
1070
|
+
else if ( tokenLength === 2 ) ms *= 10;
|
|
1071
|
+
result.milliseconds = ms as TMillisecondOfSecond;
|
|
1072
|
+
break;
|
|
1073
|
+
}
|
|
1074
|
+
default:
|
|
1075
|
+
throw new Error( `Unsupported pattern character: ${patternChar}` );
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
patternIndex += tokenLength;
|
|
1079
|
+
} else {
|
|
1080
|
+
// Non-pattern character (separator like '-', '/', ':', ' ', '.')
|
|
1081
|
+
if ( dateStringIndex >= dateString.length ||
|
|
1082
|
+
dateString[ dateStringIndex ] !== patternChar ) {
|
|
1083
|
+
throw new Error( `Date string "${dateString}" does not match pattern "${pattern}"` );
|
|
1084
|
+
}
|
|
1085
|
+
patternIndex++;
|
|
1086
|
+
dateStringIndex++;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// After processing the entire pattern, check if we consumed the entire date string
|
|
1091
|
+
if ( dateStringIndex < dateString.length ) {
|
|
1092
|
+
throw new Error( `Date string "${dateString}" does not match pattern "${pattern}"` );
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
return result;
|
|
1096
|
+
}
|