Skip to content

Commit 862161f

Browse files
committed
feat: add defaultDate option to parse/isValid for partial date string support
Introduce ParsedComponents interface in parser.ts and refactor PreparseResult to extend it. Add defaultDate option to ParserOptions so callers can supply fallback values for components missing from the format string. - toGregorianYear/getDefaultDate helpers extracted in isValid.ts for reuse - validatePreparseResult now validates defaultDate components (e.g. H, Z) - parse() merges parsed values with defaultDate, respecting precedence rules: defaultDate.Z takes precedence over options.timeZone
1 parent f63a74a commit 862161f

4 files changed

Lines changed: 135 additions & 85 deletions

File tree

src/isValid.ts

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,67 @@
11
import { preparse } from './preparse.ts';
22
import { getDaysInMonth } from './utils.ts';
33
import type { CompiledObject } from './compile.ts';
4-
import type { ParserOptions } from './parser.ts';
4+
import type { ParsedComponents, ParserOptions } from './parser.ts';
55
import type { PreparseResult } from './preparse.ts';
66

7+
/**
8+
* Converts a year to the Gregorian calendar year based on the specified calendar system in the parser options.
9+
* @param year - The year to convert, which may be in a non-Gregorian calendar system (e.g., Buddhist calendar)
10+
* @param [options] - Optional parser options that may specify the calendar system to use for conversion
11+
* @returns The corresponding Gregorian calendar year, or undefined if the input year is undefined
12+
*/
13+
export const toGregorianYear = (year: number | undefined, options?: ParserOptions) => {
14+
return year === undefined ? year : year - (options?.calendar === 'buddhist' ? 543 : 0);
15+
};
16+
17+
/**
18+
* Gets the default date components to use when certain components are missing from the input string.
19+
* @param [defaultDate] - An object containing default date components (year, month, day, hour, minute, second, millisecond, timezone offset)
20+
* @returns An object with all date components filled in, using the provided default values or fallback defaults (e.g., year defaults to 1970)
21+
*/
22+
export const getDefaultDate = (defaultDate: ParsedComponents = {}) => {
23+
return {
24+
Y: defaultDate.Y ?? 1970,
25+
M: defaultDate.M ?? 1,
26+
D: defaultDate.D ?? 1,
27+
H: defaultDate.H,
28+
A: defaultDate.A,
29+
h: defaultDate.h,
30+
m: defaultDate.m ?? 0,
31+
s: defaultDate.s ?? 0,
32+
S: defaultDate.S ?? 0,
33+
Z: defaultDate.Z
34+
};
35+
};
36+
737
/**
838
* Validates whether a preparse result object is valid.
939
* @param pr - The preparse result object to validate
1040
* @param [options] - Optional parser options
1141
* @returns True if the preparse result is valid, false otherwise
1242
*/
1343
export function validatePreparseResult(pr: PreparseResult, options?: ParserOptions) {
14-
const y = pr.Y === undefined ? 1970 : pr.Y - (options?.calendar === 'buddhist' ? 543 : 0);
1544
const [min12, max12] = options?.hour12 === 'h11' ? [0, 11] : [1, 12];
1645
const [min24, max24] = options?.hour24 === 'h24' ? [1, 24] : [0, 23];
1746
const range = (value: number | undefined, min: number, max: number) => value === undefined || value >= min && value <= max;
47+
const base = getDefaultDate(options?.defaultDate);
48+
const year = toGregorianYear(pr.Y, options) ?? base.Y;
49+
const month = pr.M ?? base.M;
1850

1951
return pr._index > 0
2052
&& pr._length > 0
2153
&& pr._index === pr._length
2254
&& pr._match > 0
23-
&& range(y, 1, 9999)
24-
&& range(pr.M, 1, 12)
25-
&& range(pr.D, 1, getDaysInMonth(y, pr.M ?? 1))
26-
&& range(pr.H, min24, max24)
27-
&& range(pr.A, 0, 1)
28-
&& range(pr.h, min12, max12)
29-
&& range(pr.m, 0, 59)
30-
&& range(pr.s, 0, 59)
31-
&& range(pr.S, 0, 999)
32-
&& range(pr.Z, -913, 956);
55+
&& range(year, 1, 9999)
56+
&& range(month, 1, 12)
57+
&& range(pr.D ?? base.D, 1, getDaysInMonth(year, month))
58+
&& range(pr.H ?? base.H, min24, max24)
59+
&& range(pr.A ?? base.A, 0, 1)
60+
&& range(pr.h ?? base.h, min12, max12)
61+
&& range(pr.m ?? base.m, 0, 59)
62+
&& range(pr.s ?? base.s, 0, 59)
63+
&& range(pr.S ?? base.S, 0, 999)
64+
&& range(pr.Z ?? base.Z, -913, 956);
3365
}
3466

3567
/**

src/parse.ts

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
1-
import { createTimezoneDate } from './zone.ts';
1+
import { createTimezoneDate, TimeZone } from './zone.ts';
22
import { isUTC } from './datetime.ts';
33
import { preparse } from './preparse.ts';
4-
import { validatePreparseResult } from './isValid.ts';
4+
import { toGregorianYear, getDefaultDate, validatePreparseResult } from './isValid.ts';
55
import type { CompiledObject } from './compile.ts';
66
import type { ParserOptions } from './parser.ts';
77

8+
const convert = (Y: number, M: number, D: number, H: number, m: number, s: number, S: number, timeZone?: string | TimeZone) => {
9+
// If a specific time zone is provided in the options, use it to create the date.
10+
if (timeZone) {
11+
const naiveUTC = Date.UTC(Y, M - 1, D, H, m, s, S);
12+
// If the specified time zone is UTC, create the date directly in UTC. Otherwise, create the date using the specified time zone.
13+
return isUTC(timeZone) ? new Date(naiveUTC) : createTimezoneDate(naiveUTC, timeZone);
14+
}
15+
// If no time zone is provided, create the date in the local time zone.
16+
return new Date(Y, M - 1, D, H, m, s, S);
17+
};
18+
819
/**
920
* Parses a date string according to the specified format.
1021
* @param dateString - The date string to parse
@@ -18,23 +29,20 @@ export function parse(dateString: string, arg: string | CompiledObject, options?
1829
if (!validatePreparseResult(pr, options)) {
1930
return new Date(NaN);
2031
}
21-
// Normalize date components (year, month, day, hour, minute, second, millisecond)
22-
pr.Y = pr.Y ? pr.Y - (options?.calendar === 'buddhist' ? 543 : 0) : 1970;
23-
pr.M = (pr.M ?? 1) - (pr.Y < 100 ? 1900 * 12 + 1 : 1);
24-
pr.D ??= 1;
25-
pr.H = ((pr.H ?? 0) % 24) || ((pr.A ?? 0) * 12 + (pr.h ?? 0) % 12);
26-
pr.m ??= 0;
27-
pr.s ??= 0;
28-
pr.S ??= 0;
2932

30-
// If the preparse result contains a timezone offset (Z), use it to create the date.
31-
if (isUTC(options?.timeZone) || 'Z' in pr) {
32-
return new Date(Date.UTC(pr.Y, pr.M, pr.D, pr.H, pr.m + (pr.Z ?? 0), pr.s, pr.S));
33-
}
34-
// If a specific time zone is provided in the options, use it to create the date.
35-
if (options?.timeZone) {
36-
return createTimezoneDate(Date.UTC(pr.Y, pr.M, pr.D, pr.H, pr.m, pr.s, pr.S), options.timeZone);
37-
}
38-
// If no time zone information is available, create the date in the local time zone.
39-
return new Date(pr.Y, pr.M, pr.D, pr.H, pr.m, pr.s, pr.S);
33+
const base = getDefaultDate(options?.defaultDate);
34+
const year = toGregorianYear(pr.Y, options) ?? base.Y;
35+
// When a Z offset exists (from the parsed string or defaultDate.Z), it takes precedence over options.timeZone.
36+
const offset = pr.Z ?? base.Z;
37+
38+
return convert(
39+
year,
40+
(pr.M ?? base.M) - (year < 100 ? 1900 * 12 : 0),
41+
pr.D ?? base.D,
42+
((pr.H ?? base.H ?? 0) % 24) || ((pr.A ?? base.A ?? 0) * 12 + (pr.h ?? base.h ?? 0) % 12),
43+
(pr.m ?? base.m) + (offset ?? 0),
44+
pr.s ?? base.s,
45+
pr.S ?? base.S,
46+
typeof offset === 'number' ? 'UTC' : options?.timeZone
47+
);
4048
}

src/parser.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,58 @@ import type { TimeZone } from './zone.ts';
55

66
type ParserToken = 'Y' | 'M' | 'D' | 'H' | 'A' | 'h' | 'm' | 's' | 'S' | 'Z';
77

8+
export interface ParsedComponents {
9+
/**
10+
* Year component
11+
*/
12+
Y?: number;
13+
14+
/**
15+
* Month component (1-12)
16+
*/
17+
M?: number;
18+
19+
/**
20+
* Day component
21+
*/
22+
D?: number;
23+
24+
/**
25+
* Hour in 24-hour format
26+
*/
27+
H?: number;
28+
29+
/**
30+
* Meridiem indicator (0:AM / 1:PM)
31+
*/
32+
A?: number;
33+
34+
/**
35+
* Hour in 12-hour format
36+
*/
37+
h?: number;
38+
39+
/**
40+
* Minute component
41+
*/
42+
m?: number;
43+
44+
/**
45+
* Second component
46+
*/
47+
s?: number;
48+
49+
/**
50+
* Millisecond component
51+
*/
52+
S?: number;
53+
54+
/**
55+
* Timezone offset in minutes
56+
*/
57+
Z?: number;
58+
}
59+
860
export interface ParserPluginOptions {
961
/**
1062
* The hour format to use for parsing.
@@ -51,6 +103,13 @@ export interface ParserPluginOptions {
51103
* This is an object that provides methods to get localized month names, day names, and meridiems.
52104
*/
53105
locale: Locale;
106+
107+
/**
108+
* Default date components to use when certain components are missing from the input string.
109+
* This allows the parser to fill in missing components with default values, which can be useful for parsing partial date strings.
110+
* For example, if the input string only contains a month and day, the parser can use the default year from this object.
111+
*/
112+
defaultDate: ParsedComponents;
54113
}
55114

56115
export interface ParseResult {

src/preparse.ts

Lines changed: 4 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -4,59 +4,9 @@ import { parser as defaultParser, validateToken } from './parser.ts';
44
import en from './locales/en.ts';
55
import latn from './numerals/latn.ts';
66
import type { CompiledObject } from './compile.ts';
7-
import type { ParserOptions } from './parser.ts';
8-
9-
export interface PreparseResult {
10-
/**
11-
* Year component
12-
*/
13-
Y?: number;
14-
15-
/**
16-
* Month component (1-12)
17-
*/
18-
M?: number;
19-
20-
/**
21-
* Day component
22-
*/
23-
D?: number;
24-
25-
/**
26-
* Hour in 24-hour format
27-
*/
28-
H?: number;
29-
30-
/**
31-
* Meridiem indicator (0:AM/1:PM)
32-
*/
33-
A?: number;
34-
35-
/**
36-
* Hour in 12-hour format
37-
*/
38-
h?: number;
39-
40-
/**
41-
* Minute component
42-
*/
43-
m?: number;
44-
45-
/**
46-
* Second component
47-
*/
48-
s?: number;
49-
50-
/**
51-
* Millisecond component
52-
*/
53-
S?: number;
54-
55-
/**
56-
* Timezone offset in minutes
57-
*/
58-
Z?: number;
7+
import type { ParsedComponents, ParserOptions } from './parser.ts';
598

9+
export interface PreparseResult extends ParsedComponents {
6010
/**
6111
* Current parsing position
6212
*/
@@ -95,7 +45,8 @@ export function preparse(dateString: string, arg: string | CompiledObject, optio
9545
timeZone: isTimeZone(options?.timeZone) || typeof options?.timeZone === 'string'
9646
? options.timeZone || undefined
9747
: undefined,
98-
locale: options?.locale ?? en
48+
locale: options?.locale ?? en,
49+
defaultDate: options?.defaultDate ?? {}
9950
};
10051
const pr: PreparseResult = {
10152
_index: 0,

0 commit comments

Comments
 (0)