Skip to content

Commit d50f51f

Browse files
committed
Add IANA timezone string support to all date manipulation functions
- Move isUTC helper from zone.ts to datetime.ts and add dtfToParts helper - Refactor getTimezoneOffset and createTimezoneDate in zone.ts to accept IANA timezone name strings (e.g., 'America/Los_Angeles') in addition to TimeZone objects - Update ParserPluginOptions.timeZone type to TimeZone | string in parser.ts; integrate ignoreCase logic into find function and add validateToken - Update day-of-week plugin to use new find signature with options parameter - Change timeZone argument type to TimeZone | string in addDays, addMonths, addYears, parse, and preparse - Add test cases for IANA string timezones and historical timezone changes (Metlakatla, Manila); expand boundary values to -1601/+1601
1 parent def8b55 commit d50f51f

File tree

10 files changed

+288
-172
lines changed

10 files changed

+288
-172
lines changed

src/addDays.ts

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,34 @@
1-
import { toParts, fromParts } from './datetime.ts';
2-
import { isTimeZone, isUTC, createTimezoneDate } from './zone.ts';
1+
import { toParts, fromParts, isUTC } from './datetime.ts';
2+
import { isTimeZone, createTimezoneDate } from './zone.ts';
33
import type { TimeZone } from './zone.ts';
44

55
/**
66
* Adds the specified number of days to a Date object.
77
* @param dateObj - The Date object to modify
88
* @param days - The number of days to add
9-
* @param [timeZone] - Optional time zone object or 'UTC'
9+
* @param [timeZone] - Optional time zone object, an IANA timezone name or 'UTC' to use Coordinated Universal Time.
1010
* @returns A new Date object with the specified number of days added
1111
*/
12-
export function addDays(dateObj: Date, days: number, timeZone?: TimeZone | 'UTC') {
13-
// Handle timezone-specific calculation
14-
if (isTimeZone(timeZone)) {
15-
const parts = toParts(dateObj, timeZone.zone_name);
12+
export function addDays(dateObj: Date, days: number, timeZone?: TimeZone | string) {
13+
const zoneName = isTimeZone(timeZone) ? timeZone.zone_name : timeZone ?? undefined;
1614

17-
parts.day += days;
18-
parts.timezoneOffset = 0;
15+
if (!zoneName || isUTC(zoneName)) {
16+
const d = new Date(dateObj.getTime());
1917

20-
return createTimezoneDate(fromParts(parts), timeZone);
18+
// Handle UTC calculation
19+
if (isUTC(timeZone)) {
20+
d.setUTCDate(d.getUTCDate() + days);
21+
return d;
22+
}
23+
// Handle local time calculation
24+
d.setDate(d.getDate() + days);
25+
return d;
2126
}
27+
// Handle timezone-specific calculation
28+
const parts = toParts(dateObj, zoneName);
2229

23-
const d = new Date(dateObj.getTime());
30+
parts.day += days;
31+
parts.timezoneOffset = 0;
2432

25-
// Handle UTC calculation
26-
if (isUTC(timeZone)) {
27-
d.setUTCDate(d.getUTCDate() + days);
28-
return d;
29-
}
30-
d.setDate(d.getDate() + days);
31-
return d;
33+
return createTimezoneDate(fromParts(parts), zoneName);
3234
}

src/addMonths.ts

Lines changed: 40 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,58 @@
1-
import { toParts, fromParts } from './datetime.ts';
2-
import { isTimeZone, isUTC, createTimezoneDate } from './zone.ts';
1+
import { toParts, fromParts, isUTC } from './datetime.ts';
2+
import { isTimeZone, createTimezoneDate } from './zone.ts';
33
import type { TimeZone } from './zone.ts';
44

55
/**
66
* Adds the specified number of months to a Date object.
77
* @param dateObj - The Date object to modify
88
* @param months - The number of months to add
9-
* @param [timeZone] - Optional time zone object or 'UTC'
9+
* @param [timeZone] - Optional time zone object, an IANA timezone name or 'UTC' to use Coordinated Universal Time.
1010
* @returns A new Date object with the specified number of months added
1111
*/
12-
export function addMonths(dateObj: Date, months: number, timeZone?: TimeZone | 'UTC') {
13-
// Handle timezone-specific calculation
14-
if (isTimeZone(timeZone)) {
15-
const parts = toParts(dateObj, timeZone.zone_name);
16-
17-
parts.month += months;
18-
parts.timezoneOffset = 0;
12+
export function addMonths(dateObj: Date, months: number, timeZone?: TimeZone | string) {
13+
const zoneName = isTimeZone(timeZone) ? timeZone.zone_name : timeZone ?? undefined;
1914

20-
const d = new Date(fromParts(parts));
15+
if (!zoneName || isUTC(zoneName)) {
16+
const d = new Date(dateObj.getTime());
2117

22-
if (d.getUTCDate() < parts.day) {
23-
d.setUTCDate(0);
18+
// Handle UTC calculation
19+
if (isUTC(timeZone)) {
20+
d.setUTCMonth(d.getUTCMonth() + months);
21+
// Adjust to last day of month if new month has fewer days
22+
if (d.getUTCDate() < dateObj.getUTCDate()) {
23+
d.setUTCDate(0);
24+
return d;
25+
}
26+
return d;
2427
}
25-
26-
return createTimezoneDate(
27-
fromParts({
28-
...parts,
29-
year: d.getUTCFullYear(),
30-
month: d.getUTCMonth() + 1,
31-
day: d.getUTCDate()
32-
}),
33-
timeZone
34-
);
35-
}
36-
37-
const d = new Date(dateObj.getTime());
38-
39-
// Handle UTC calculation
40-
if (isUTC(timeZone)) {
41-
d.setUTCMonth(d.getUTCMonth() + months);
28+
// Handle local time calculation
29+
d.setMonth(d.getMonth() + months);
4230
// Adjust to last day of month if new month has fewer days
43-
if (d.getUTCDate() < dateObj.getUTCDate()) {
44-
d.setUTCDate(0);
31+
if (d.getDate() < dateObj.getDate()) {
32+
d.setDate(0);
4533
return d;
4634
}
4735
return d;
4836
}
49-
d.setMonth(d.getMonth() + months);
50-
// Adjust to last day of month if new month has fewer days
51-
if (d.getDate() < dateObj.getDate()) {
52-
d.setDate(0);
53-
return d;
37+
// Handle timezone-specific calculation
38+
const parts = toParts(dateObj, zoneName);
39+
40+
parts.month += months;
41+
parts.timezoneOffset = 0;
42+
43+
const d = new Date(fromParts(parts));
44+
45+
if (d.getUTCDate() < parts.day) {
46+
d.setUTCDate(0);
5447
}
55-
return d;
48+
49+
return createTimezoneDate(
50+
fromParts({
51+
...parts,
52+
year: d.getUTCFullYear(),
53+
month: d.getUTCMonth() + 1,
54+
day: d.getUTCDate()
55+
}),
56+
zoneName
57+
);
5658
}

src/addYears.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import type { TimeZone } from './zone.ts';
55
* Adds the specified number of years to a Date object.
66
* @param dateObj - The Date object to modify
77
* @param years - The number of years to add
8-
* @param [timeZone] - Optional time zone object or 'UTC'
8+
* @param [timeZone] - Optional time zone object, an IANA timezone name or 'UTC' to use Coordinated Universal Time.
99
* @returns A new Date object with the specified number of years added
1010
*/
11-
export function addYears(dateObj: Date, years: number, timeZone?: TimeZone | 'UTC') {
11+
export function addYears(dateObj: Date, years: number, timeZone?: TimeZone | string) {
1212
return addMonths(dateObj, years * 12, timeZone);
1313
}

src/datetime.ts

Lines changed: 36 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { getDateTimeFormat } from './dtf.ts';
2-
import { isUTC } from './zone.ts';
32

43
export interface DateTimeParts {
54
weekday: number;
@@ -13,23 +12,20 @@ export interface DateTimeParts {
1312
timezoneOffset: number;
1413
}
1514

16-
export const toParts = (dateObj: Date, zoneName: string): DateTimeParts => {
17-
if (isUTC(zoneName)) {
18-
return {
19-
weekday: dateObj.getUTCDay(),
20-
year: dateObj.getUTCFullYear(),
21-
month: dateObj.getUTCMonth() + 1,
22-
day: dateObj.getUTCDate(),
23-
hour: dateObj.getUTCHours(),
24-
minute: dateObj.getUTCMinutes(),
25-
second: dateObj.getUTCSeconds(),
26-
fractionalSecond: dateObj.getUTCMilliseconds(),
27-
timezoneOffset: 0
28-
};
29-
}
15+
export const fromParts = (parts: DateTimeParts) => {
16+
return Date.UTC(
17+
parts.year,
18+
parts.month - (parts.year < 100 ? 1900 * 12 + 1 : 1),
19+
parts.day,
20+
parts.hour,
21+
parts.minute,
22+
parts.second,
23+
parts.fractionalSecond + parts.timezoneOffset * 60000
24+
);
25+
};
3026

31-
const values = getDateTimeFormat(zoneName)
32-
.formatToParts(dateObj)
27+
export const dtfToParts = (dtf: Intl.DateTimeFormat, time: number): DateTimeParts => {
28+
return dtf.formatToParts(time)
3329
.reduce((parts, { type, value }) => {
3430
switch (type) {
3531
case 'weekday':
@@ -52,25 +48,32 @@ export const toParts = (dateObj: Date, zoneName: string): DateTimeParts => {
5248
hour: 0, minute: 0, second: 0, fractionalSecond: 0,
5349
timezoneOffset: 0
5450
});
51+
};
5552

56-
values.timezoneOffset = (dateObj.getTime() - Date.UTC(
57-
values.year,
58-
values.month - (values.year < 100 ? 1900 * 12 + 1 : 1),
59-
values.day,
60-
values.hour,
61-
values.minute,
62-
values.second,
63-
values.fractionalSecond
64-
)) / 60000;
65-
66-
return values;
53+
export const isUTC = (timeZone: unknown): timeZone is 'UTC' => {
54+
return typeof timeZone === 'string' && timeZone.toUpperCase() === 'UTC';
6755
};
6856

69-
export const fromParts = (parts: DateTimeParts) => {
70-
return Date.UTC(
71-
parts.year, parts.month - (parts.year < 100 ? 1900 * 12 + 1 : 1), parts.day,
72-
parts.hour, parts.minute, parts.second, parts.fractionalSecond + parts.timezoneOffset * 60000
73-
);
57+
export const toParts = (dateObj: Date, zoneName: string): DateTimeParts => {
58+
if (isUTC(zoneName)) {
59+
return {
60+
weekday: dateObj.getUTCDay(),
61+
year: dateObj.getUTCFullYear(),
62+
month: dateObj.getUTCMonth() + 1,
63+
day: dateObj.getUTCDate(),
64+
hour: dateObj.getUTCHours(),
65+
minute: dateObj.getUTCMinutes(),
66+
second: dateObj.getUTCSeconds(),
67+
fractionalSecond: dateObj.getUTCMilliseconds(),
68+
timezoneOffset: 0
69+
};
70+
}
71+
72+
const time = dateObj.getTime();
73+
const parts = dtfToParts(getDateTimeFormat(zoneName), time);
74+
75+
parts.timezoneOffset = (time - fromParts(parts)) / 60000;
76+
return parts;
7477
};
7578

7679
export interface DateLike {

src/parse.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { isTimeZone, isUTC, createTimezoneDate } from './zone.ts';
2-
import { validatePreparseResult } from './isValid.ts';
1+
import { createTimezoneDate } from './zone.ts';
2+
import { isUTC } from './datetime.ts';
33
import { preparse } from './preparse.ts';
4+
import { validatePreparseResult } from './isValid.ts';
45
import type { CompiledObject } from './compile.ts';
56
import type { ParserOptions } from './parser.ts';
67

@@ -26,14 +27,14 @@ export function parse(dateString: string, arg: string | CompiledObject, options?
2627
pr.s ??= 0;
2728
pr.S ??= 0;
2829

29-
if (isTimeZone(options?.timeZone)) {
30-
// Handle timezone-specific calculation
31-
return createTimezoneDate(Date.UTC(pr.Y, pr.M, pr.D, pr.H, pr.m, pr.s, pr.S), options.timeZone);
32-
}
30+
// If the preparse result contains a timezone offset (Z), use it to create the date.
3331
if (isUTC(options?.timeZone) || 'Z' in pr) {
34-
// Handle UTC calculation or when 'Z' token is present
3532
return new Date(Date.UTC(pr.Y, pr.M, pr.D, pr.H, pr.m + (pr.Z ?? 0), pr.s, pr.S));
3633
}
37-
// Handle local timezone calculation
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.
3839
return new Date(pr.Y, pr.M, pr.D, pr.H, pr.m, pr.s, pr.S);
3940
}

0 commit comments

Comments
 (0)