@zelgadis87/utils-core 5.2.8 → 5.2.10
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 +11 -2
- package/dist/Optional.d.ts +4 -2
- package/dist/time/TimeInstant.d.ts +31 -3
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utils/empties.d.ts +1 -0
- package/esbuild/index.cjs +363 -171
- package/esbuild/index.cjs.map +4 -4
- package/esbuild/index.mjs +363 -184
- package/esbuild/index.mjs.map +4 -4
- package/package.json +1 -4
- package/src/Optional.ts +3 -1
- package/src/time/TimeInstant.ts +421 -15
- package/src/time/TimeInstantBuilder.ts +2 -2
- package/src/utils/empties.ts +2 -0
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.
|
|
4
|
+
"version": "5.2.10",
|
|
5
5
|
"author": "Zelgadis87",
|
|
6
6
|
"license": "ISC",
|
|
7
7
|
"private": false,
|
|
@@ -30,8 +30,5 @@
|
|
|
30
30
|
"typescript": "5.8.3",
|
|
31
31
|
"typescript-eslint": "8.43.0",
|
|
32
32
|
"vitest": "3.2.4"
|
|
33
|
-
},
|
|
34
|
-
"dependencies": {
|
|
35
|
-
"small-date": "^2.0.1"
|
|
36
33
|
}
|
|
37
34
|
}
|
package/src/Optional.ts
CHANGED
|
@@ -221,7 +221,7 @@ export type TOptional<T> = {
|
|
|
221
221
|
|
|
222
222
|
ifEmpty( callback: TVoidFunction ): void;
|
|
223
223
|
ifPresent( callback: TConsumer<T> ): void;
|
|
224
|
-
apply<RP = void, RE = void>( callbackIfPresent: TFunction<T, RP>, callbackIfEmpty: TProducer<RE> ):
|
|
224
|
+
apply<RP = void, RE = void>( callbackIfPresent: TFunction<T, RP>, callbackIfEmpty: TProducer<RE> ): RP | RE;
|
|
225
225
|
|
|
226
226
|
throwIfPresent: ( errorGenerator: TFunction<T, Error> ) => TEmptyOptional<T>;
|
|
227
227
|
|
|
@@ -255,11 +255,13 @@ export type TEmptyOptional<T> = TOptional<T> & {
|
|
|
255
255
|
get(): never;
|
|
256
256
|
isEmpty(): true;
|
|
257
257
|
isPresent(): false;
|
|
258
|
+
apply<RP = void, RE = void>( callbackIfPresent: TFunction<T, RP>, callbackIfEmpty: TProducer<RE> ): RE;
|
|
258
259
|
}
|
|
259
260
|
export type TPresentOptional<T> = TOptional<T> & {
|
|
260
261
|
get(): T;
|
|
261
262
|
isEmpty(): false;
|
|
262
263
|
isPresent(): true;
|
|
264
|
+
apply<RP = void, RE = void>( callbackIfPresent: TFunction<T, RP>, callbackIfEmpty: TProducer<RE> ): RP;
|
|
263
265
|
}
|
|
264
266
|
|
|
265
267
|
export class ErrorGetEmptyOptional extends Error {
|
package/src/time/TimeInstant.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { format } from "small-date";
|
|
2
1
|
import { ICancelable, ICancelablePromise } from "../async/Deferred.js";
|
|
3
2
|
import { TComparisonResult } from "../sorting/_index.js";
|
|
4
3
|
import TimeBase from "./TimeBase";
|
|
5
4
|
import TimeDuration from "./TimeDuration";
|
|
6
5
|
import { TTimeInstantBuilder, TTimeInstantCreationParameters, createTimeInstantFromParameters, timeInstantBuilder } from "./TimeInstantBuilder.js";
|
|
7
6
|
import { TimeUnit } from "./TimeUnit";
|
|
8
|
-
import { TDayOfMonth, TDayOfWeek, TIso8601DateString, TIso8601DateUtcString, TMonth, TWeekNumber } from "./types";
|
|
7
|
+
import { TDayOfMonth, TDayOfWeek, THourOfDay, TIso8601DateString, TIso8601DateUtcString, TMillisecondOfSecond, TMinuteOfHour, TMonth, TSecondOfMinute, TWeekNumber } from "./types";
|
|
9
8
|
|
|
10
9
|
export class TimeInstant extends TimeBase<TimeInstant> {
|
|
11
10
|
|
|
@@ -32,7 +31,7 @@ export class TimeInstant extends TimeBase<TimeInstant> {
|
|
|
32
31
|
public distanceFromNow(): TimeDuration {
|
|
33
32
|
return TimeDuration.fromMs( Math.abs( this.ms - Date.now() ) );
|
|
34
33
|
}
|
|
35
|
-
|
|
34
|
+
|
|
36
35
|
public distanceFromUnixTimestamp( timestamp: number ): TimeDuration {
|
|
37
36
|
return TimeDuration.ms( this.ms - timestamp );
|
|
38
37
|
}
|
|
@@ -96,6 +95,8 @@ export class TimeInstant extends TimeBase<TimeInstant> {
|
|
|
96
95
|
/**
|
|
97
96
|
* Formats this instant using the given pattern. The pattern can contain the following tokens:
|
|
98
97
|
*
|
|
98
|
+
* Note: Implementation inspired by the small-date library (https://github.com/robinweser/small-date).
|
|
99
|
+
*
|
|
99
100
|
* | Token | Description | Example |
|
|
100
101
|
* |:------|:--------------------------------|:------------------------------|
|
|
101
102
|
* | D | Weekday, 1 letter | W |
|
|
@@ -135,7 +136,30 @@ export class TimeInstant extends TimeBase<TimeInstant> {
|
|
|
135
136
|
* @returns a string, formatted using the given pattern, at the given timeZone with the given locale.
|
|
136
137
|
*/
|
|
137
138
|
public format( pattern: string, config: { locale?: string, timeZone?: string } = {} ) {
|
|
138
|
-
return
|
|
139
|
+
return formatTimeInstant( this, pattern, config );
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Parses a date string using the given pattern and creates a TimeInstant.
|
|
144
|
+
* This method is the inverse of format() - parsing a formatted string should recreate the original instant.
|
|
145
|
+
*
|
|
146
|
+
* For partial patterns (e.g., time-only or date-only), the missing components are taken from the base instant.
|
|
147
|
+
* For example, parsing "14:30" with base set to yesterday at midnight will result in yesterday at 14:30.
|
|
148
|
+
*
|
|
149
|
+
* Note: Currently performs basic validation (e.g., month 1-12, day 1-31) but does not validate
|
|
150
|
+
* calendar-specific constraints (e.g., February 30th, April 31st). Invalid dates may be
|
|
151
|
+
* normalized by the underlying Date constructor.
|
|
152
|
+
*
|
|
153
|
+
* @param dateString The date string to parse
|
|
154
|
+
* @param pattern The pattern used to parse the string (same tokens as format())
|
|
155
|
+
* @param base The base TimeInstant to use for partial patterns (defaults to now)
|
|
156
|
+
* @param config An optional locale and timeZone definition to use during parsing
|
|
157
|
+
* @returns A TimeInstant parsed from the string
|
|
158
|
+
* @throws Error if the string doesn't match the pattern or contains invalid basic values
|
|
159
|
+
* @todo Add calendar-aware validation to reject dates like February 30th, April 31st
|
|
160
|
+
*/
|
|
161
|
+
public static parse( dateString: string, pattern: string, base: TimeInstant = TimeInstant.now(), config: { locale?: string, timeZone?: string } = {} ): TimeInstant {
|
|
162
|
+
return parseTimeInstant( dateString, pattern, base, config );
|
|
139
163
|
}
|
|
140
164
|
|
|
141
165
|
/**
|
|
@@ -241,16 +265,58 @@ export class TimeInstant extends TimeBase<TimeInstant> {
|
|
|
241
265
|
return timeInstantBuilder();
|
|
242
266
|
}
|
|
243
267
|
|
|
244
|
-
public static fromIso8601( str:
|
|
245
|
-
|
|
246
|
-
|
|
268
|
+
public static fromIso8601( str: string ) {
|
|
269
|
+
// Regex to capture: YYYY-MM-DDTHH:mm:ss.sssZ or YYYY-MM-DDTHH:mm:ss.sss±HH:mm
|
|
270
|
+
const iso8601Regex = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})\.(\d{3})(Z|[+-]\d{2}:\d{2})?$/;
|
|
271
|
+
const match = str.match( iso8601Regex );
|
|
272
|
+
|
|
273
|
+
if ( !match ) {
|
|
274
|
+
throw new Error( 'Invalid ISO 8601 date format: ' + str );
|
|
275
|
+
}
|
|
247
276
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
277
|
+
const [ , yearStr, monthStr, dayStr, hourStr, minuteStr, secondStr, millisecondStr ] = match;
|
|
278
|
+
|
|
279
|
+
// Parse strings to numbers
|
|
280
|
+
const year = parseInt( yearStr, 10 );
|
|
281
|
+
const month = parseInt( monthStr, 10 );
|
|
282
|
+
const date = parseInt( dayStr, 10 );
|
|
283
|
+
const hours = parseInt( hourStr, 10 );
|
|
284
|
+
const minutes = parseInt( minuteStr, 10 );
|
|
285
|
+
const seconds = parseInt( secondStr, 10 );
|
|
286
|
+
const milliseconds = parseInt( millisecondStr, 10 );
|
|
287
|
+
|
|
288
|
+
// Validate each component using standalone validation functions
|
|
289
|
+
if ( !isValidYear( year ) )
|
|
290
|
+
throw new Error( 'Invalid year in: ' + str );
|
|
291
|
+
if ( !isValidMonth( month ) )
|
|
292
|
+
throw new Error( 'Invalid month in: ' + str );
|
|
293
|
+
if ( !isValidDayOfMonth( date ) )
|
|
294
|
+
throw new Error( 'Invalid day in: ' + str );
|
|
295
|
+
if ( !isValidHour( hours ) )
|
|
296
|
+
throw new Error( 'Invalid hour in: ' + str );
|
|
297
|
+
if ( !isValidMinute( minutes ) )
|
|
298
|
+
throw new Error( 'Invalid minute in: ' + str );
|
|
299
|
+
if ( !isValidSecond( seconds ) )
|
|
300
|
+
throw new Error( 'Invalid second in: ' + str );
|
|
301
|
+
if ( !isValidMillisecond( milliseconds ) )
|
|
302
|
+
throw new Error( 'Invalid millisecond in: ' + str );
|
|
303
|
+
|
|
304
|
+
return TimeInstant.fromParameters( {
|
|
305
|
+
year,
|
|
306
|
+
month,
|
|
307
|
+
date,
|
|
308
|
+
hours,
|
|
309
|
+
minutes,
|
|
310
|
+
seconds,
|
|
311
|
+
milliseconds,
|
|
312
|
+
} );
|
|
252
313
|
}
|
|
253
314
|
|
|
315
|
+
/**
|
|
316
|
+
* @deprecated [2025.10.19]: Use fromIso8601 instead.
|
|
317
|
+
*/
|
|
318
|
+
public static tryFromIso8601 = this.fromIso8601;
|
|
319
|
+
|
|
254
320
|
public static now(): TimeInstant {
|
|
255
321
|
return TimeInstant.fromUnixTimestamp( Date.now() );
|
|
256
322
|
}
|
|
@@ -286,19 +352,19 @@ export class TimeInstant extends TimeBase<TimeInstant> {
|
|
|
286
352
|
}
|
|
287
353
|
|
|
288
354
|
public get dayOfMonth(): TDayOfMonth {
|
|
289
|
-
return this.toDate().
|
|
355
|
+
return this.toDate().getUTCDate() as TDayOfMonth;
|
|
290
356
|
}
|
|
291
357
|
|
|
292
358
|
public get dayOfWeek(): TDayOfWeek {
|
|
293
|
-
return this.toDate().
|
|
359
|
+
return this.toDate().getUTCDay() + 1 as TDayOfWeek;
|
|
294
360
|
}
|
|
295
361
|
|
|
296
362
|
public get month(): TMonth {
|
|
297
|
-
return this.toDate().
|
|
363
|
+
return this.toDate().getUTCMonth() + 1 as TMonth;
|
|
298
364
|
}
|
|
299
365
|
|
|
300
366
|
public get year(): number {
|
|
301
|
-
return this.toDate().
|
|
367
|
+
return this.toDate().getUTCFullYear();
|
|
302
368
|
}
|
|
303
369
|
|
|
304
370
|
/**
|
|
@@ -344,3 +410,343 @@ export function isTimeInstant( x: unknown ): x is TimeInstant {
|
|
|
344
410
|
}
|
|
345
411
|
|
|
346
412
|
export default TimeInstant;
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
// Utility functions for date formatting and parsing
|
|
416
|
+
|
|
417
|
+
// Standalone validation functions
|
|
418
|
+
// TODO: Add calendar-aware validation to check month-specific day limits
|
|
419
|
+
// (e.g., reject February 30th, April 31st, February 29th in non-leap years)
|
|
420
|
+
function isValidYear( num: number ): boolean {
|
|
421
|
+
return num >= 0 && num <= 9999;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function isValidMonth( num: number ): num is TMonth {
|
|
425
|
+
return num >= 1 && num <= 12;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function isValidDayOfMonth( num: number ): num is TDayOfMonth {
|
|
429
|
+
return num >= 1 && num <= 31;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function isValidHour( num: number ): num is THourOfDay {
|
|
433
|
+
return num >= 0 && num <= 23;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function isValidMinute( num: number ): num is TMinuteOfHour {
|
|
437
|
+
return num >= 0 && num <= 59;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function isValidSecond( num: number ): num is TSecondOfMinute {
|
|
441
|
+
return num >= 0 && num <= 59;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function isValidMillisecond( num: number ): num is TMillisecondOfSecond {
|
|
445
|
+
return num >= 0 && num <= 999;
|
|
446
|
+
}
|
|
447
|
+
|
|
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
|
+
}
|
|
515
|
+
|
|
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 );
|
|
520
|
+
}
|
|
521
|
+
|
|
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
|
+
}
|
|
526
|
+
|
|
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
|
+
}
|
|
531
|
+
|
|
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 );
|
|
538
|
+
}
|
|
539
|
+
|
|
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 ];
|
|
544
|
+
|
|
545
|
+
if ( !value ) {
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const options = {
|
|
550
|
+
[ option ]: value,
|
|
551
|
+
timeZone,
|
|
552
|
+
} as Intl.DateTimeFormatOptions;
|
|
553
|
+
|
|
554
|
+
if ( type === 'a' ) {
|
|
555
|
+
return Intl.DateTimeFormat( locale, {
|
|
556
|
+
...options,
|
|
557
|
+
hour: 'numeric',
|
|
558
|
+
} )
|
|
559
|
+
.formatToParts( date )
|
|
560
|
+
.pop()?.value;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if ( type === 'G' || type === 'Z' ) {
|
|
564
|
+
return Intl.DateTimeFormat( locale, options ).formatToParts( date ).pop()?.value;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// For other complex formatting, fall back to Intl
|
|
568
|
+
return Intl.DateTimeFormat( locale, options ).format( date );
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function formatTimeInstant( instant: TimeInstant, pattern: string, config: { locale?: string, timeZone?: string } = {} ): string {
|
|
572
|
+
return pattern
|
|
573
|
+
.split( ESCAPE_REGEX )
|
|
574
|
+
.filter( ( sub ) => sub !== undefined )
|
|
575
|
+
.map( ( sub, index ) => {
|
|
576
|
+
// keep escaped strings as is
|
|
577
|
+
if ( index % 2 !== 0 ) {
|
|
578
|
+
return sub;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return sub.replace( PATTERN_REGEX, ( match ) => {
|
|
582
|
+
const type = match.charAt( 0 );
|
|
583
|
+
return formatType( instant, type, match.length, config ) || match;
|
|
584
|
+
} );
|
|
585
|
+
} )
|
|
586
|
+
.join( '' );
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function parseTimeInstant( dateString: string, pattern: string, base: TimeInstant, config: { locale?: string, timeZone?: string } = {} ): TimeInstant {
|
|
590
|
+
// Create a regex pattern from the format pattern
|
|
591
|
+
let regexPattern = pattern;
|
|
592
|
+
const tokens: { type: string; length: number; position: number }[] = [];
|
|
593
|
+
let position = 0;
|
|
594
|
+
|
|
595
|
+
// Split by escape regex first, like the original implementation
|
|
596
|
+
const parts = pattern.split( ESCAPE_REGEX ).filter( ( sub ) => sub !== undefined );
|
|
597
|
+
|
|
598
|
+
// Rebuild pattern and track tokens
|
|
599
|
+
regexPattern = parts.map( ( sub, index ) => {
|
|
600
|
+
// Skip escaped strings (odd indices after split)
|
|
601
|
+
if ( index % 2 !== 0 ) {
|
|
602
|
+
return sub.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' ); // Escape special regex chars
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return sub.replace( PATTERN_REGEX, ( match ) => {
|
|
606
|
+
const type = match.charAt( 0 );
|
|
607
|
+
tokens.push( { type, length: match.length, position: position++ } );
|
|
608
|
+
|
|
609
|
+
// Create appropriate regex for each token type
|
|
610
|
+
switch ( type ) {
|
|
611
|
+
case 'y':
|
|
612
|
+
return match.length === 2 ? '(\\d{2})' : '(\\d{4})';
|
|
613
|
+
case 'M':
|
|
614
|
+
if ( match.length === 1 ) return '(\\d{1,2})';
|
|
615
|
+
if ( match.length === 2 ) return '(\\d{2})';
|
|
616
|
+
if ( match.length === 3 ) return '([A-Za-z]{3})';
|
|
617
|
+
return '([A-Za-z]+)';
|
|
618
|
+
case 'd':
|
|
619
|
+
return match.length === 1 ? '(\\d{1,2})' : '(\\d{2})';
|
|
620
|
+
case 'H':
|
|
621
|
+
case 'h':
|
|
622
|
+
return match.length === 1 ? '(\\d{1,2})' : '(\\d{2})';
|
|
623
|
+
case 'm':
|
|
624
|
+
case 's':
|
|
625
|
+
return match.length === 1 ? '(\\d{1,2})' : '(\\d{2})';
|
|
626
|
+
case 'S':
|
|
627
|
+
return `(\\d{${match.length}})`;
|
|
628
|
+
case 'a':
|
|
629
|
+
return '([aApP][mM])';
|
|
630
|
+
case 'D':
|
|
631
|
+
if ( match.length === 1 ) return '([A-Za-z])';
|
|
632
|
+
if ( match.length === 2 ) return '([A-Za-z]{3})';
|
|
633
|
+
return '([A-Za-z]+)';
|
|
634
|
+
case 'G':
|
|
635
|
+
if ( match.length === 1 ) return '([A-Za-z])';
|
|
636
|
+
if ( match.length === 2 ) return '([A-Za-z]{2})';
|
|
637
|
+
return '([A-Za-z\\s]+)';
|
|
638
|
+
case 'Z':
|
|
639
|
+
return match.length === 1 ? '([A-Za-z0-9+\\-:]+)' : '([A-Za-z\\s]+)';
|
|
640
|
+
case 'P':
|
|
641
|
+
return '([A-Za-z\\s]+)';
|
|
642
|
+
default:
|
|
643
|
+
return match;
|
|
644
|
+
}
|
|
645
|
+
} );
|
|
646
|
+
} ).join( '' );
|
|
647
|
+
|
|
648
|
+
const regex = new RegExp( '^' + regexPattern + '$' );
|
|
649
|
+
const matches = dateString.match( regex );
|
|
650
|
+
|
|
651
|
+
if ( !matches ) {
|
|
652
|
+
throw new Error( `Date string "${dateString}" does not match pattern "${pattern}"` );
|
|
653
|
+
}
|
|
654
|
+
|
|
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();
|
|
663
|
+
let isPM = false;
|
|
664
|
+
let is12Hour = false;
|
|
665
|
+
|
|
666
|
+
tokens.forEach( ( token, index ) => {
|
|
667
|
+
const value = matches[ index + 1 ];
|
|
668
|
+
|
|
669
|
+
switch ( token.type ) {
|
|
670
|
+
case 'y':
|
|
671
|
+
if ( token.length === 2 ) {
|
|
672
|
+
const shortYear = parseInt( value, 10 );
|
|
673
|
+
year = shortYear < 50 ? 2000 + shortYear : 1900 + shortYear;
|
|
674
|
+
} else {
|
|
675
|
+
year = parseInt( value, 10 );
|
|
676
|
+
}
|
|
677
|
+
break;
|
|
678
|
+
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;
|
|
695
|
+
}
|
|
696
|
+
break;
|
|
697
|
+
case 'd':
|
|
698
|
+
day = parseInt( value, 10 ) as TDayOfMonth;
|
|
699
|
+
break;
|
|
700
|
+
case 'H':
|
|
701
|
+
hour = parseInt( value, 10 );
|
|
702
|
+
break;
|
|
703
|
+
case 'h':
|
|
704
|
+
hour = parseInt( value, 10 );
|
|
705
|
+
is12Hour = true;
|
|
706
|
+
break;
|
|
707
|
+
case 'm':
|
|
708
|
+
minute = parseInt( value, 10 );
|
|
709
|
+
break;
|
|
710
|
+
case 's':
|
|
711
|
+
second = parseInt( value, 10 );
|
|
712
|
+
break;
|
|
713
|
+
case 'S':
|
|
714
|
+
let ms = parseInt( value, 10 );
|
|
715
|
+
// Normalize to milliseconds based on length
|
|
716
|
+
if ( token.length === 1 ) ms *= 100;
|
|
717
|
+
else if ( token.length === 2 ) ms *= 10;
|
|
718
|
+
millisecond = ms;
|
|
719
|
+
break;
|
|
720
|
+
case 'a':
|
|
721
|
+
isPM = value.toLowerCase().includes( 'p' );
|
|
722
|
+
break;
|
|
723
|
+
}
|
|
724
|
+
} );
|
|
725
|
+
|
|
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
|
+
} );
|
|
752
|
+
}
|
|
@@ -66,7 +66,7 @@ const timeInstantResolveValue = ( getFromDate: TFunction<Date, number>, referenc
|
|
|
66
66
|
};
|
|
67
67
|
const getFromDate = {
|
|
68
68
|
year: d => d.getFullYear(),
|
|
69
|
-
month: d => d.getMonth(),
|
|
69
|
+
month: d => d.getMonth() + 1,
|
|
70
70
|
date: d => d.getDate(),
|
|
71
71
|
hours: d => d.getHours(),
|
|
72
72
|
minutes: d => d.getMinutes(),
|
|
@@ -84,7 +84,7 @@ export function createTimeInstantFromParameters( aParameters: Partial<TTimeInsta
|
|
|
84
84
|
const referenceDate = toReferenceDate( aReferenceDate );
|
|
85
85
|
const timestamp = Date.UTC(
|
|
86
86
|
timeInstantResolveValue( getFromDate.year, referenceDate, year ),
|
|
87
|
-
timeInstantResolveValue( getFromDate.month, referenceDate, month ),
|
|
87
|
+
timeInstantResolveValue( getFromDate.month, referenceDate, month ) - 1, // Convert from 1-based to 0-based for Date.UTC()
|
|
88
88
|
timeInstantResolveValue( getFromDate.date, referenceDate, date ),
|
|
89
89
|
timeInstantResolveValue( getFromDate.hours, referenceDate, hours ),
|
|
90
90
|
timeInstantResolveValue( getFromDate.minutes, referenceDate, minutes ),
|