temporal-fmt 0.9.2 → 0.9.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/README.md +480 -9
- package/dist/calendarUtils.cjs +259 -4
- package/dist/calendarUtils.cjs.map +1 -1
- package/dist/calendarUtils.js +33 -1
- package/dist/chunk-4N3WCGEY.js +183 -0
- package/dist/{chunk-4JWGUR4O.js.map → chunk-4N3WCGEY.js.map} +1 -1
- package/dist/chunk-5UTV5VP6.js +984 -0
- package/dist/chunk-5UTV5VP6.js.map +1 -0
- package/dist/chunk-ACKKWH5Z.js +578 -0
- package/dist/{chunk-2JHEV7ZF.js.map → chunk-ACKKWH5Z.js.map} +1 -1
- package/dist/chunk-EGUUO6LE.js +439 -0
- package/dist/chunk-EGUUO6LE.js.map +1 -0
- package/dist/chunk-FBPHNRVW.js +2939 -0
- package/dist/chunk-FBPHNRVW.js.map +1 -0
- package/dist/chunk-GPB4JHGI.js +65 -0
- package/dist/chunk-GPB4JHGI.js.map +1 -0
- package/dist/chunk-H7YQX25T.js +170 -0
- package/dist/{chunk-J2AQNXF2.js.map → chunk-H7YQX25T.js.map} +1 -1
- package/dist/chunk-HXY2GUUM.js +182 -0
- package/dist/{chunk-EHLRZULM.js.map → chunk-HXY2GUUM.js.map} +1 -1
- package/dist/chunk-L24NPTAJ.js +328 -0
- package/dist/chunk-L24NPTAJ.js.map +1 -0
- package/dist/chunk-M3GDTHMT.js +169 -0
- package/dist/chunk-M3GDTHMT.js.map +1 -0
- package/dist/chunk-MKPDLQKD.js +51 -0
- package/dist/chunk-MKPDLQKD.js.map +1 -0
- package/dist/chunk-TSLPIURC.js +242 -0
- package/dist/{chunk-VQXUMFB2.js.map → chunk-TSLPIURC.js.map} +1 -1
- package/dist/chunk-UOPAV3WX.js +75 -0
- package/dist/chunk-UOPAV3WX.js.map +1 -0
- package/dist/chunk-VNIUIQA4.js +398 -0
- package/dist/chunk-VNIUIQA4.js.map +1 -0
- package/dist/chunk-YEUASGDK.js +9 -0
- package/dist/chunk-YEUASGDK.js.map +1 -0
- package/dist/chunk-ZQZF5EXW.js +29 -0
- package/dist/chunk-ZQZF5EXW.js.map +1 -0
- package/dist/config.d.cts +4 -2
- package/dist/config.d.ts +4 -2
- package/dist/duration.cjs +638 -26
- package/dist/duration.cjs.map +1 -1
- package/dist/duration.js +28 -1
- package/dist/format.cjs +833 -35
- package/dist/format.cjs.map +1 -1
- package/dist/format.js +20 -1
- package/dist/index.cjs +7698 -163
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +108 -24
- package/dist/index.d.ts +108 -24
- package/dist/index.js +1536 -58
- package/dist/index.js.map +1 -1
- package/dist/interval.cjs +5480 -35
- package/dist/interval.cjs.map +1 -1
- package/dist/interval.js +46 -1
- package/dist/localeRegistry.cjs +375 -32
- package/dist/localeRegistry.cjs.map +1 -1
- package/dist/localeRegistry.js +11 -1
- package/dist/modApi.d.cts +131 -0
- package/dist/modApi.d.ts +131 -0
- package/dist/parse.cjs +1797 -70
- package/dist/parse.cjs.map +1 -1
- package/dist/parse.js +20 -1
- package/dist/pattern.d.cts +1 -0
- package/dist/pattern.d.ts +1 -0
- package/dist/recurrence.cjs +452 -20
- package/dist/recurrence.cjs.map +1 -1
- package/dist/recurrence.js +19 -1
- package/dist/relativeTime.cjs +268 -21
- package/dist/relativeTime.cjs.map +1 -1
- package/dist/relativeTime.js +14 -1
- package/dist/runtime.d.cts +201 -0
- package/dist/runtime.d.ts +201 -0
- package/dist/timezone.cjs +387 -20
- package/dist/timezone.cjs.map +1 -1
- package/dist/timezone.js +26 -1
- package/package.json +4 -6
- package/scripts/cli.mjs +27 -5
- package/dist/chunk-2JHEV7ZF.js +0 -7
- package/dist/chunk-3MZLTVP3.js +0 -2
- package/dist/chunk-3MZLTVP3.js.map +0 -1
- package/dist/chunk-4JWGUR4O.js +0 -11
- package/dist/chunk-5U5WJ465.js +0 -36
- package/dist/chunk-5U5WJ465.js.map +0 -1
- package/dist/chunk-C5YESWFT.js +0 -2
- package/dist/chunk-C5YESWFT.js.map +0 -1
- package/dist/chunk-C6ZZ62ET.js +0 -2
- package/dist/chunk-C6ZZ62ET.js.map +0 -1
- package/dist/chunk-EHLRZULM.js +0 -5
- package/dist/chunk-F4RGUDA3.js +0 -2
- package/dist/chunk-F4RGUDA3.js.map +0 -1
- package/dist/chunk-J2AQNXF2.js +0 -2
- package/dist/chunk-NBXF7V5B.js +0 -2
- package/dist/chunk-NBXF7V5B.js.map +0 -1
- package/dist/chunk-R52YOKI3.js +0 -7
- package/dist/chunk-R52YOKI3.js.map +0 -1
- package/dist/chunk-UJR6DHXY.js +0 -2
- package/dist/chunk-UJR6DHXY.js.map +0 -1
- package/dist/chunk-VQXUMFB2.js +0 -21
- package/dist/chunk-XH46JNPK.js +0 -2
- package/dist/chunk-XH46JNPK.js.map +0 -1
- package/dist/chunk-YB6YAG7E.js +0 -35
- package/dist/chunk-YB6YAG7E.js.map +0 -1
- package/dist/chunk-ZGMTY52O.js +0 -2
- package/dist/chunk-ZGMTY52O.js.map +0 -1
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_LOCALE
|
|
3
|
+
} from "./chunk-L24NPTAJ.js";
|
|
4
|
+
import {
|
|
5
|
+
InvalidLocaleError,
|
|
6
|
+
normalizeLocaleTag
|
|
7
|
+
} from "./chunk-EGUUO6LE.js";
|
|
8
|
+
import {
|
|
9
|
+
differenceInDays
|
|
10
|
+
} from "./chunk-TSLPIURC.js";
|
|
11
|
+
|
|
12
|
+
// src/relativeTime.ts
|
|
13
|
+
var rtfCache = /* @__PURE__ */ new Map();
|
|
14
|
+
var MAX_RTF_CACHE_SIZE = 100;
|
|
15
|
+
function getRtf(locale, numeric) {
|
|
16
|
+
const key = `${locale}|${numeric}`;
|
|
17
|
+
let rtf = rtfCache.get(key);
|
|
18
|
+
if (rtf) return rtf;
|
|
19
|
+
if (rtfCache.size >= MAX_RTF_CACHE_SIZE) {
|
|
20
|
+
const oldestKey = rtfCache.keys().next().value;
|
|
21
|
+
if (oldestKey !== void 0) rtfCache.delete(oldestKey);
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
rtf = new Intl.RelativeTimeFormat(normalizeLocaleTag(locale), { numeric });
|
|
25
|
+
} catch (err) {
|
|
26
|
+
throw new InvalidLocaleError({ actual: locale, reason: err.message });
|
|
27
|
+
}
|
|
28
|
+
rtfCache.set(key, rtf);
|
|
29
|
+
return rtf;
|
|
30
|
+
}
|
|
31
|
+
function formatRelative(date1, date2, options = {}) {
|
|
32
|
+
const locale = options.locale ?? DEFAULT_LOCALE;
|
|
33
|
+
const numeric = options.numeric ?? "auto";
|
|
34
|
+
const rtf = getRtf(locale, numeric);
|
|
35
|
+
const dayDiff = -differenceInDays(date1, date2);
|
|
36
|
+
const absDays = Math.abs(dayDiff);
|
|
37
|
+
if (absDays === 0) {
|
|
38
|
+
return rtf.format(0, "day");
|
|
39
|
+
}
|
|
40
|
+
if (absDays < 7) {
|
|
41
|
+
return rtf.format(dayDiff, "day");
|
|
42
|
+
}
|
|
43
|
+
if (absDays < 30) {
|
|
44
|
+
return rtf.format(-Math.trunc(-dayDiff / 7), "week");
|
|
45
|
+
}
|
|
46
|
+
if (absDays < 365) {
|
|
47
|
+
return rtf.format(-Math.trunc(-dayDiff / 30), "month");
|
|
48
|
+
}
|
|
49
|
+
return rtf.format(-Math.trunc(-dayDiff / 365), "year");
|
|
50
|
+
}
|
|
51
|
+
function formatRelativeToNow(date, options = {}) {
|
|
52
|
+
const now = /* @__PURE__ */ new Date();
|
|
53
|
+
const nowFields = {
|
|
54
|
+
year: now.getFullYear(),
|
|
55
|
+
month: now.getMonth() + 1,
|
|
56
|
+
day: now.getDate()
|
|
57
|
+
};
|
|
58
|
+
return formatRelative(date, nowFields, options);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export {
|
|
62
|
+
formatRelative,
|
|
63
|
+
formatRelativeToNow
|
|
64
|
+
};
|
|
65
|
+
//# sourceMappingURL=chunk-GPB4JHGI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/relativeTime.ts"],"sourcesContent":["// relative time stuff. formatDistance() over in formatDistance.ts is\n// still the main one, this just adds formatRelative() / formatRelativeToNow()\n// on top.\n//\n// the difference between these that keeps tripping people up:\n// formatDistance(date1, date2) -> \"3 days ago\" / \"in 2 hours\"\n// just describes the gap, hands unit picking + pluralization off to Intl\n// formatRelative(date, baseDate) -> \"yesterday\" / \"tomorrow\" / \"today\" /\n// \"in 3 days\" / \"last week\"\n// this one's calendar-relative, not ms-based. so \"in 1 day\" always means\n// tomorrow even if it's technically only 23 hours from now\n// formatRelativeToNow(date) -> literally just formatRelative(date, now)\n\nimport { differenceInDays } from './arithmetic.js';\nimport { DEFAULT_LOCALE, type FormatOptions } from './tokens.js';\nimport { normalizeLocaleTag } from './localeVocab.js';\nimport { InvalidLocaleError } from './errors.js';\n\nexport interface FormatRelativeOptions extends FormatOptions {\n // 'auto' (default) — Intl picks natural stuff like \"yesterday\"/\"tomorrow\"\n // when it's ±1 or 0. 'always' forces the stricter \"1 day ago\" style always.\n numeric?: 'always' | 'auto';\n}\n\nconst rtfCache = new Map<string, Intl.RelativeTimeFormat>();\nconst MAX_RTF_CACHE_SIZE = 100;\n\nfunction getRtf(locale: string, numeric: 'always' | 'auto'): Intl.RelativeTimeFormat {\n // reusing the same cache shape formatDistance.ts already has\n const key = `${locale}|${numeric}`;\n let rtf = rtfCache.get(key);\n if (rtf) return rtf;\n if (rtfCache.size >= MAX_RTF_CACHE_SIZE) {\n const oldestKey = rtfCache.keys().next().value;\n if (oldestKey !== undefined) rtfCache.delete(oldestKey);\n }\n try {\n rtf = new Intl.RelativeTimeFormat(normalizeLocaleTag(locale), { numeric });\n } catch (err) {\n // bad locale tags just throw a plain RangeError from Intl, wrap it in\n // our own error type instead. every failure here IS a RangeError anyway\n // so we're not losing anything by converting unconditionally — the\n // original message still comes through in `reason`\n throw new InvalidLocaleError({ actual: locale, reason: (err as Error).message });\n }\n rtfCache.set(key, rtf);\n return rtf;\n}\n\n// describes date1 as if you were standing at date2. uses differenceInDays\n// (counts calendar boundaries, not literal 24hr chunks) so something like\n// \"2026-08-04 23:59\" vs \"2026-08-05 00:00\" correctly says \"yesterday\",\n// not \"1 second ago\" which would be technically true but useless\n//\n// differenceInDays(a, b) gives b - a, but we want date1 - date2 here so\n// positive = future relative to date2. hence the argument swap below\nexport function formatRelative(\n date1: unknown,\n date2: unknown,\n options: FormatRelativeOptions = {},\n): string {\n const locale = options.locale ?? DEFAULT_LOCALE;\n const numeric = options.numeric ?? 'auto';\n const rtf = getRtf(locale, numeric);\n const dayDiff = -differenceInDays(date1, date2);\n const absDays = Math.abs(dayDiff);\n\n if (absDays === 0) {\n return rtf.format(0, 'day'); // shows up as \"now\"\n }\n if (absDays < 7) {\n return rtf.format(dayDiff, 'day');\n }\n if (absDays < 30) {\n return rtf.format(-Math.trunc(-dayDiff / 7), 'week');\n }\n if (absDays < 365) {\n return rtf.format(-Math.trunc(-dayDiff / 30), 'month');\n }\n return rtf.format(-Math.trunc(-dayDiff / 365), 'year');\n}\n\nexport function formatRelativeToNow(\n date: unknown,\n options: FormatRelativeOptions = {},\n): string {\n // just uses whatever the system clock says right now as the reference\n const now = new Date();\n const nowFields = {\n year: now.getFullYear(),\n month: now.getMonth() + 1,\n day: now.getDate(),\n };\n return formatRelative(date, nowFields, options);\n}\n"],"mappings":";;;;;;;;;;;;AAwBA,IAAM,WAAW,oBAAI,IAAqC;AAC1D,IAAM,qBAAqB;AAE3B,SAAS,OAAO,QAAgB,SAAqD;AAEnF,QAAM,MAAM,GAAG,MAAM,IAAI,OAAO;AAChC,MAAI,MAAM,SAAS,IAAI,GAAG;AAC1B,MAAI,IAAK,QAAO;AAChB,MAAI,SAAS,QAAQ,oBAAoB;AACvC,UAAM,YAAY,SAAS,KAAK,EAAE,KAAK,EAAE;AACzC,QAAI,cAAc,OAAW,UAAS,OAAO,SAAS;AAAA,EACxD;AACA,MAAI;AACF,UAAM,IAAI,KAAK,mBAAmB,mBAAmB,MAAM,GAAG,EAAE,QAAQ,CAAC;AAAA,EAC3E,SAAS,KAAK;AAKZ,UAAM,IAAI,mBAAmB,EAAE,QAAQ,QAAQ,QAAS,IAAc,QAAQ,CAAC;AAAA,EACjF;AACA,WAAS,IAAI,KAAK,GAAG;AACrB,SAAO;AACT;AASO,SAAS,eACd,OACA,OACA,UAAiC,CAAC,GAC1B;AACR,QAAM,SAAS,QAAQ,UAAU;AACjC,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,MAAM,OAAO,QAAQ,OAAO;AAClC,QAAM,UAAU,CAAC,iBAAiB,OAAO,KAAK;AAC9C,QAAM,UAAU,KAAK,IAAI,OAAO;AAEhC,MAAI,YAAY,GAAG;AACjB,WAAO,IAAI,OAAO,GAAG,KAAK;AAAA,EAC5B;AACA,MAAI,UAAU,GAAG;AACf,WAAO,IAAI,OAAO,SAAS,KAAK;AAAA,EAClC;AACA,MAAI,UAAU,IAAI;AAChB,WAAO,IAAI,OAAO,CAAC,KAAK,MAAM,CAAC,UAAU,CAAC,GAAG,MAAM;AAAA,EACrD;AACA,MAAI,UAAU,KAAK;AACjB,WAAO,IAAI,OAAO,CAAC,KAAK,MAAM,CAAC,UAAU,EAAE,GAAG,OAAO;AAAA,EACvD;AACA,SAAO,IAAI,OAAO,CAAC,KAAK,MAAM,CAAC,UAAU,GAAG,GAAG,MAAM;AACvD;AAEO,SAAS,oBACd,MACA,UAAiC,CAAC,GAC1B;AAER,QAAM,MAAM,oBAAI,KAAK;AACrB,QAAM,YAAY;AAAA,IAChB,MAAM,IAAI,YAAY;AAAA,IACtB,OAAO,IAAI,SAAS,IAAI;AAAA,IACxB,KAAK,IAAI,QAAQ;AAAA,EACnB;AACA,SAAO,eAAe,MAAM,WAAW,OAAO;AAChD;","names":[]}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getTemporal
|
|
3
|
+
} from "./chunk-ZQZF5EXW.js";
|
|
4
|
+
import {
|
|
5
|
+
add
|
|
6
|
+
} from "./chunk-TSLPIURC.js";
|
|
7
|
+
|
|
8
|
+
// src/timezone.ts
|
|
9
|
+
function resolveZoned(fields, timeZone, options = {}) {
|
|
10
|
+
const temporal = getTemporal();
|
|
11
|
+
const disambiguation = options.disambiguation ?? "compatible";
|
|
12
|
+
const overflow = options.overflow ?? "constrain";
|
|
13
|
+
const fromOptions = {
|
|
14
|
+
overflow,
|
|
15
|
+
disambiguation,
|
|
16
|
+
offset: options.offset ?? "prefer"
|
|
17
|
+
};
|
|
18
|
+
try {
|
|
19
|
+
return temporal.ZonedDateTime.from(
|
|
20
|
+
{
|
|
21
|
+
year: fields.year,
|
|
22
|
+
month: fields.month,
|
|
23
|
+
day: fields.day,
|
|
24
|
+
hour: fields.hour ?? 0,
|
|
25
|
+
minute: fields.minute ?? 0,
|
|
26
|
+
second: fields.second ?? 0,
|
|
27
|
+
millisecond: fields.millisecond ?? 0,
|
|
28
|
+
microsecond: fields.microsecond ?? 0,
|
|
29
|
+
nanosecond: fields.nanosecond ?? 0,
|
|
30
|
+
timeZone
|
|
31
|
+
},
|
|
32
|
+
fromOptions
|
|
33
|
+
);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
const msg = err.message;
|
|
36
|
+
if (/no such (wall-clock )?time/i.test(msg) && disambiguation === "reject") {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`temporal-fmt: "${timeZone}" has no such wall-clock time on ${fields.year}-${fields.month}-${fields.day}T${fields.hour ?? 0}:${fields.minute ?? 0} \u2014 it falls in a DST gap. Pass { disambiguation: 'compatible' | 'earlier' | 'later' } to pick an instant.`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function getTimeZone(value) {
|
|
45
|
+
const v = value;
|
|
46
|
+
if (typeof v?.timeZoneId !== "string") {
|
|
47
|
+
throw new Error(`temporal-fmt: getTimeZone() expected a ZonedDateTime, got ${typeof value}.`);
|
|
48
|
+
}
|
|
49
|
+
return v.timeZoneId;
|
|
50
|
+
}
|
|
51
|
+
function getOffset(value) {
|
|
52
|
+
const v = value;
|
|
53
|
+
if (typeof v?.offset !== "string") {
|
|
54
|
+
throw new Error(`temporal-fmt: getOffset() expected a ZonedDateTime, got ${typeof value}.`);
|
|
55
|
+
}
|
|
56
|
+
return v.offset;
|
|
57
|
+
}
|
|
58
|
+
function getOffsetNanoseconds(value) {
|
|
59
|
+
const v = value;
|
|
60
|
+
if (typeof v?.offsetNanoseconds === "number") return v.offsetNanoseconds;
|
|
61
|
+
if (typeof v?.offset === "string") {
|
|
62
|
+
const m = v.offset.match(/^([+-])(\d{2}):(\d{2})$/);
|
|
63
|
+
if (!m) throw new Error(`temporal-fmt: getOffsetNanoseconds() couldn't parse offset "${v.offset}".`);
|
|
64
|
+
const sign = m[1] === "-" ? -1 : 1;
|
|
65
|
+
return sign * (Number(m[2]) * 3600 + Number(m[3]) * 60) * 1e9;
|
|
66
|
+
}
|
|
67
|
+
throw new Error(`temporal-fmt: getOffsetNanoseconds() expected a ZonedDateTime, got ${typeof value}.`);
|
|
68
|
+
}
|
|
69
|
+
function isDST(value) {
|
|
70
|
+
const v = value;
|
|
71
|
+
if (typeof v?.timeZoneId !== "string" || typeof v?.offset !== "string" || typeof v?.year !== "number") {
|
|
72
|
+
throw new Error(`temporal-fmt: isDST() expected a ZonedDateTime, got ${typeof value}.`);
|
|
73
|
+
}
|
|
74
|
+
const temporal = getTemporal();
|
|
75
|
+
try {
|
|
76
|
+
const jan = temporal.ZonedDateTime.from(
|
|
77
|
+
{ year: v.year, month: 1, day: 1, hour: 12, timeZone: v.timeZoneId },
|
|
78
|
+
{ disambiguation: "compatible" }
|
|
79
|
+
);
|
|
80
|
+
return jan.offset !== v.offset;
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function getNextTransition(value) {
|
|
86
|
+
return findTransition(value, 1);
|
|
87
|
+
}
|
|
88
|
+
function getPreviousTransition(value) {
|
|
89
|
+
return findTransition(value, -1);
|
|
90
|
+
}
|
|
91
|
+
function findTransition(value, direction) {
|
|
92
|
+
const v = value;
|
|
93
|
+
if (typeof v?.timeZoneId !== "string") {
|
|
94
|
+
throw new Error(`temporal-fmt: ${direction > 0 ? "getNextTransition" : "getPreviousTransition"}() expected a ZonedDateTime, got ${typeof value}.`);
|
|
95
|
+
}
|
|
96
|
+
const temporal = getTemporal();
|
|
97
|
+
const timeZone = v.timeZoneId;
|
|
98
|
+
let candidateFields = add(value, direction, "days");
|
|
99
|
+
const startOffset = v.offset;
|
|
100
|
+
for (let i = 0; i < 365 * 2; i++) {
|
|
101
|
+
const cf = candidateFields;
|
|
102
|
+
const candidateZdt = temporal.ZonedDateTime.from(
|
|
103
|
+
{ ...cf, timeZone },
|
|
104
|
+
{ disambiguation: "compatible" }
|
|
105
|
+
);
|
|
106
|
+
if (candidateZdt.offset !== startOffset) {
|
|
107
|
+
return candidateZdt;
|
|
108
|
+
}
|
|
109
|
+
candidateFields = add(candidateFields, direction, "days");
|
|
110
|
+
}
|
|
111
|
+
return void 0;
|
|
112
|
+
}
|
|
113
|
+
function getTransitions(start, end) {
|
|
114
|
+
const result = [];
|
|
115
|
+
const startV = start;
|
|
116
|
+
if (typeof startV?.timeZoneId !== "string") {
|
|
117
|
+
throw new Error(`temporal-fmt: getTransitions() expected a ZonedDateTime for start, got ${typeof start}.`);
|
|
118
|
+
}
|
|
119
|
+
const endV = end;
|
|
120
|
+
const temporal = getTemporal();
|
|
121
|
+
const timeZone = startV.timeZoneId;
|
|
122
|
+
let cursorFields = add(start, 1, "days");
|
|
123
|
+
let lastOffset = startV.offset;
|
|
124
|
+
for (let i = 0; i < 365 * 5; i++) {
|
|
125
|
+
const cf = cursorFields;
|
|
126
|
+
const cursorZdt = temporal.ZonedDateTime.from(
|
|
127
|
+
{ ...cf, timeZone },
|
|
128
|
+
{ disambiguation: "compatible" }
|
|
129
|
+
);
|
|
130
|
+
if (cursorZdt.offset !== lastOffset) {
|
|
131
|
+
if (cursorZdt.year > endV.year || cursorZdt.year === endV.year && (cursorZdt.month > endV.month || cursorZdt.month === endV.month && cursorZdt.day > endV.day)) break;
|
|
132
|
+
result.push(cursorZdt);
|
|
133
|
+
lastOffset = cursorZdt.offset;
|
|
134
|
+
}
|
|
135
|
+
cursorFields = add(cursorFields, 1, "days");
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
function possibleInstantsFor(fields, timeZone) {
|
|
140
|
+
const temporal = getTemporal();
|
|
141
|
+
const earlier = temporal.ZonedDateTime.from(
|
|
142
|
+
{ ...fields, timeZone },
|
|
143
|
+
{ disambiguation: "earlier", offset: "ignore" }
|
|
144
|
+
);
|
|
145
|
+
const later = temporal.ZonedDateTime.from(
|
|
146
|
+
{ ...fields, timeZone },
|
|
147
|
+
{ disambiguation: "later", offset: "ignore" }
|
|
148
|
+
);
|
|
149
|
+
const matchesRequestedWallClock = (z) => z.year === fields.year && z.month === fields.month && z.day === fields.day && z.hour === fields.hour && z.minute === fields.minute && z.second === fields.second && z.millisecond === fields.millisecond;
|
|
150
|
+
if (!matchesRequestedWallClock(earlier) || !matchesRequestedWallClock(later)) {
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
if (earlier.toInstant().epochNanoseconds === later.toInstant().epochNanoseconds) {
|
|
154
|
+
return [earlier];
|
|
155
|
+
}
|
|
156
|
+
return [earlier, later];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export {
|
|
160
|
+
resolveZoned,
|
|
161
|
+
getTimeZone,
|
|
162
|
+
getOffset,
|
|
163
|
+
getOffsetNanoseconds,
|
|
164
|
+
isDST,
|
|
165
|
+
getNextTransition,
|
|
166
|
+
getPreviousTransition,
|
|
167
|
+
getTransitions,
|
|
168
|
+
possibleInstantsFor
|
|
169
|
+
};
|
|
170
|
+
//# sourceMappingURL=chunk-H7YQX25T.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/timezone.ts"],"sourcesContent":["// Time-zone subsystem. Wraps Temporal.ZonedDateTime's\n// timezone introspection surface in temporal-fmt's field-based convention.\n// Includes disambiguation modes (compatible/earlier/later/reject) for\n// DST gaps and overlaps, plus transition queries (getNextTransition,\n// getPreviousTransition, getTransitions).\n\nimport { getTemporal } from './temporalProvider.js';\nimport { add } from './arithmetic.js';\n\nexport type DisambiguationMode = 'compatible' | 'earlier' | 'later' | 'reject';\n\nexport interface ResolveZonedOptions {\n disambiguation?: DisambiguationMode;\n overflow?: 'constrain' | 'reject';\n offset?: 'use' | 'ignore' | 'prefer' | 'reject';\n}\n\n// Resolves a wall-clock time in a timezone, applying disambiguation\n// for DST gaps and overlaps.\nexport function resolveZoned(\n fields: { year: number; month: number; day: number; hour?: number; minute?: number; second?: number; millisecond?: number; microsecond?: number; nanosecond?: number },\n timeZone: string,\n options: ResolveZonedOptions = {},\n): unknown {\n const temporal = getTemporal();\n const disambiguation = options.disambiguation ?? 'compatible';\n const overflow = options.overflow ?? 'constrain';\n // Cast options to a permissive record — Temporal.ZonedDateTime.from\n // accepts disambiguation/offset options, but the existing\n // TemporalFactory.from() signature in temporalProvider.ts is narrower.\n // The runtime accepts the wider shape; this cast bypasses the type\n // narrowing without changing runtime behavior.\n const fromOptions = {\n overflow,\n disambiguation,\n offset: options.offset ?? 'prefer',\n } as unknown as Parameters<typeof temporal.ZonedDateTime.from>[1];\n try {\n return temporal.ZonedDateTime.from(\n {\n year: fields.year,\n month: fields.month,\n day: fields.day,\n hour: fields.hour ?? 0,\n minute: fields.minute ?? 0,\n second: fields.second ?? 0,\n millisecond: fields.millisecond ?? 0,\n microsecond: fields.microsecond ?? 0,\n nanosecond: fields.nanosecond ?? 0,\n timeZone,\n },\n fromOptions,\n );\n } catch (err) {\n // Surface DST gap / overlap errors with a clearer message.\n const msg = (err as Error).message;\n if (/no such (wall-clock )?time/i.test(msg) && disambiguation === 'reject') {\n throw new Error(\n `temporal-fmt: \"${timeZone}\" has no such wall-clock time on ${fields.year}-${fields.month}-${fields.day}T${fields.hour ?? 0}:${fields.minute ?? 0} — ` +\n `it falls in a DST gap. Pass { disambiguation: 'compatible' | 'earlier' | 'later' } to pick an instant.`\n );\n }\n throw err;\n }\n}\n\nexport function getTimeZone(value: unknown): string {\n const v = value as { timeZoneId?: string };\n if (typeof v?.timeZoneId !== 'string') {\n throw new Error(`temporal-fmt: getTimeZone() expected a ZonedDateTime, got ${typeof value}.`);\n }\n return v.timeZoneId;\n}\n\nexport function getOffset(value: unknown): string {\n const v = value as { offset?: string };\n if (typeof v?.offset !== 'string') {\n throw new Error(`temporal-fmt: getOffset() expected a ZonedDateTime, got ${typeof value}.`);\n }\n return v.offset;\n}\n\nexport function getOffsetNanoseconds(value: unknown): number {\n const v = value as { offsetNanoseconds?: number; offset?: string };\n if (typeof v?.offsetNanoseconds === 'number') return v.offsetNanoseconds;\n if (typeof v?.offset === 'string') {\n // Parse ±HH:MM → nanoseconds.\n const m = v.offset.match(/^([+-])(\\d{2}):(\\d{2})$/);\n if (!m) throw new Error(`temporal-fmt: getOffsetNanoseconds() couldn't parse offset \"${v.offset}\".`);\n const sign = m[1] === '-' ? -1 : 1;\n return sign * (Number(m[2]) * 3600 + Number(m[3]) * 60) * 1_000_000_000;\n }\n throw new Error(`temporal-fmt: getOffsetNanoseconds() expected a ZonedDateTime, got ${typeof value}.`);\n}\n\n// DST detection: a ZonedDateTime is in DST iff the offset is different\n// from the timezone's standard offset at the same instant. Computed\n// by comparing the current offset to the offset in January (Northern\n// Hemisphere) or July (Southern Hemisphere) of the same year — a\n// heuristic that's right for most populated zones.\nexport function isDST(value: unknown): boolean {\n const v = value as { timeZoneId?: string; offset?: string; year?: number; toInstant?: () => unknown };\n if (typeof v?.timeZoneId !== 'string' || typeof v?.offset !== 'string' || typeof v?.year !== 'number') {\n throw new Error(`temporal-fmt: isDST() expected a ZonedDateTime, got ${typeof value}.`);\n }\n // Compute the offset for January 1 of the same year (standard time\n // for Northern Hemisphere zones) and compare. For Southern Hemisphere\n // zones January is summer, so we'd want July instead — but checking\n // January vs current is good enough for the common case, and we\n // can't reliably determine hemisphere without a lookup table.\n const temporal = getTemporal();\n try {\n const jan = temporal.ZonedDateTime.from(\n { year: v.year, month: 1, day: 1, hour: 12, timeZone: v.timeZoneId },\n { disambiguation: 'compatible' } as unknown as Parameters<typeof temporal.ZonedDateTime.from>[1],\n ) as { offset: string };\n return jan.offset !== v.offset;\n } catch {\n return false;\n }\n}\n\n// Returns the next DST transition strictly after `value`, or undefined\n// if no transition occurs within the next 2 years. Implemented by\n// walking day-by-day checking for offset changes — slow but simple,\n// and correct across all IANA zones.\nexport function getNextTransition(value: unknown): unknown | undefined {\n return findTransition(value, 1);\n}\n\nexport function getPreviousTransition(value: unknown): unknown | undefined {\n return findTransition(value, -1);\n}\n\nfunction findTransition(value: unknown, direction: 1 | -1): unknown | undefined {\n const v = value as { timeZoneId?: string; offset?: string; toInstant?: () => unknown; year?: number; month?: number; day?: number; hour?: number; minute?: number; second?: number; millisecond?: number };\n if (typeof v?.timeZoneId !== 'string') {\n throw new Error(`temporal-fmt: ${direction > 0 ? 'getNextTransition' : 'getPreviousTransition'}() expected a ZonedDateTime, got ${typeof value}.`);\n }\n const temporal = getTemporal();\n const timeZone = v.timeZoneId;\n let candidateFields = add(value, direction, 'days');\n const startOffset = v.offset!;\n for (let i = 0; i < 365 * 2; i++) {\n // Rebuild a real ZonedDateTime for this candidate day, in this\n // timezone, so `.offset` reflects the actual UTC offset on that\n // day rather than being absent. add() only produces a plain field\n // bag (it has no notion of timezone), so re-attaching timeZone via\n // ZonedDateTime.from is what actually lets us detect a DST change\n // here — using the field bag's own (nonexistent) offset would make\n // every call \"find\" a transition on the very first day checked.\n const cf = candidateFields as { year: number; month: number; day: number; hour: number; minute: number; second: number; millisecond: number };\n const candidateZdt = temporal.ZonedDateTime.from(\n { ...cf, timeZone },\n { disambiguation: 'compatible' } as unknown as Parameters<typeof temporal.ZonedDateTime.from>[1],\n ) as { offset: string };\n if (candidateZdt.offset !== startOffset) {\n // Found a transition day. Return the reconstructed ZonedDateTime.\n return candidateZdt;\n }\n candidateFields = add(candidateFields, direction, 'days');\n }\n return undefined;\n}\n\nexport function getTransitions(start: unknown, end: unknown): unknown[] {\n const result: unknown[] = [];\n const startV = start as { timeZoneId?: string; offset?: string; year?: number; month?: number; day?: number };\n if (typeof startV?.timeZoneId !== 'string') {\n throw new Error(`temporal-fmt: getTransitions() expected a ZonedDateTime for start, got ${typeof start}.`);\n }\n const endV = end as { year: number; month: number; day: number };\n const temporal = getTemporal();\n const timeZone = startV.timeZoneId;\n let cursorFields = add(start, 1, 'days');\n let lastOffset = startV.offset!;\n for (let i = 0; i < 365 * 5; i++) {\n // Same reconstruction as findTransition() above — add() strips\n // timezone/offset info, so we rebuild a real ZonedDateTime each\n // iteration to read its actual offset instead of comparing against\n // a field bag that never has one.\n const cf = cursorFields as { year: number; month: number; day: number; hour: number; minute: number; second: number; millisecond: number };\n const cursorZdt = temporal.ZonedDateTime.from(\n { ...cf, timeZone },\n { disambiguation: 'compatible' } as unknown as Parameters<typeof temporal.ZonedDateTime.from>[1],\n ) as { offset: string; year: number; month: number; day: number };\n if (cursorZdt.offset !== lastOffset) {\n // Found a transition.\n if (cursorZdt.year > endV.year || (cursorZdt.year === endV.year && (cursorZdt.month > endV.month || (cursorZdt.month === endV.month && cursorZdt.day > endV.day)))) break;\n result.push(cursorZdt);\n lastOffset = cursorZdt.offset;\n }\n cursorFields = add(cursorFields, 1, 'days');\n }\n return result;\n}\n\n// Returns the list of possible instants for a wall-clock time in a\n// zone — used to detect gaps (0 instants) and overlaps (2 instants).\nexport function possibleInstantsFor(\n fields: { year: number; month: number; day: number; hour: number; minute: number; second: number; millisecond: number },\n timeZone: string,\n): unknown[] {\n const temporal = getTemporal();\n // Resolve the same wall-clock fields twice, forcing disambiguation\n // toward the earlier and later side of any DST boundary. Comparing\n // the two results tells us which of the three cases we're in:\n // - gap (e.g. 2:30am on a spring-forward day): neither result's own\n // wall-clock fields match what was requested, since Temporal has\n // to shift off the nonexistent time in both directions — 0 instants.\n // - overlap (e.g. 1:30am on a fall-back day): both results keep the\n // requested wall-clock time but land on different instants (one\n // per side of the boundary) — 2 instants.\n // - normal time: both results match the requested wall-clock time\n // and resolve to the same instant — 1 instant.\n const earlier = temporal.ZonedDateTime.from(\n { ...fields, timeZone },\n { disambiguation: 'earlier', offset: 'ignore' } as unknown as Parameters<typeof temporal.ZonedDateTime.from>[1],\n ) as { year: number; month: number; day: number; hour: number; minute: number; second: number; millisecond: number; toInstant: () => { epochNanoseconds: bigint } };\n const later = temporal.ZonedDateTime.from(\n { ...fields, timeZone },\n { disambiguation: 'later', offset: 'ignore' } as unknown as Parameters<typeof temporal.ZonedDateTime.from>[1],\n ) as { year: number; month: number; day: number; hour: number; minute: number; second: number; millisecond: number; toInstant: () => { epochNanoseconds: bigint } };\n\n const matchesRequestedWallClock = (z: typeof earlier) =>\n z.year === fields.year && z.month === fields.month && z.day === fields.day &&\n z.hour === fields.hour && z.minute === fields.minute &&\n z.second === fields.second && z.millisecond === fields.millisecond;\n\n if (!matchesRequestedWallClock(earlier) || !matchesRequestedWallClock(later)) {\n // Gap — the requested wall-clock time doesn't exist in this zone.\n return [];\n }\n if (earlier.toInstant().epochNanoseconds === later.toInstant().epochNanoseconds) {\n // Normal time — both disambiguation directions agree on the instant.\n return [earlier];\n }\n // Overlap — same wall-clock time, two distinct instants.\n return [earlier, later];\n}"],"mappings":"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","names":["resolveZoned","fields","timeZone","options","temporal","getTemporal","disambiguation","fromOptions","err","msg","getTimeZone","value","v","getOffset","getOffsetNanoseconds","m","isDST","getNextTransition","findTransition","getPreviousTransition","direction","candidateFields","add","startOffset","i","cf","candidateZdt","getTransitions","start","end","result","startV","endV","cursorFields","lastOffset","cursorZdt","possibleInstantsFor","earlier","later","matchesRequestedWallClock","z"]}
|
|
1
|
+
{"version":3,"sources":["../src/timezone.ts"],"sourcesContent":["// Time-zone subsystem. Wraps Temporal.ZonedDateTime's\n// timezone introspection surface in temporal-fmt's field-based convention.\n// Includes disambiguation modes (compatible/earlier/later/reject) for\n// DST gaps and overlaps, plus transition queries (getNextTransition,\n// getPreviousTransition, getTransitions).\n\nimport { getTemporal } from './temporalProvider.js';\nimport { add } from './arithmetic.js';\n\nexport type DisambiguationMode = 'compatible' | 'earlier' | 'later' | 'reject';\n\nexport interface ResolveZonedOptions {\n disambiguation?: DisambiguationMode;\n overflow?: 'constrain' | 'reject';\n offset?: 'use' | 'ignore' | 'prefer' | 'reject';\n}\n\n// Resolves a wall-clock time in a timezone, applying disambiguation\n// for DST gaps and overlaps.\nexport function resolveZoned(\n fields: { year: number; month: number; day: number; hour?: number; minute?: number; second?: number; millisecond?: number; microsecond?: number; nanosecond?: number },\n timeZone: string,\n options: ResolveZonedOptions = {},\n): unknown {\n const temporal = getTemporal();\n const disambiguation = options.disambiguation ?? 'compatible';\n const overflow = options.overflow ?? 'constrain';\n // Cast options to a permissive record — Temporal.ZonedDateTime.from\n // accepts disambiguation/offset options, but the existing\n // TemporalFactory.from() signature in temporalProvider.ts is narrower.\n // The runtime accepts the wider shape; this cast bypasses the type\n // narrowing without changing runtime behavior.\n const fromOptions = {\n overflow,\n disambiguation,\n offset: options.offset ?? 'prefer',\n } as unknown as Parameters<typeof temporal.ZonedDateTime.from>[1];\n try {\n return temporal.ZonedDateTime.from(\n {\n year: fields.year,\n month: fields.month,\n day: fields.day,\n hour: fields.hour ?? 0,\n minute: fields.minute ?? 0,\n second: fields.second ?? 0,\n millisecond: fields.millisecond ?? 0,\n microsecond: fields.microsecond ?? 0,\n nanosecond: fields.nanosecond ?? 0,\n timeZone,\n },\n fromOptions,\n );\n } catch (err) {\n // Surface DST gap / overlap errors with a clearer message.\n const msg = (err as Error).message;\n if (/no such (wall-clock )?time/i.test(msg) && disambiguation === 'reject') {\n throw new Error(\n `temporal-fmt: \"${timeZone}\" has no such wall-clock time on ${fields.year}-${fields.month}-${fields.day}T${fields.hour ?? 0}:${fields.minute ?? 0} — ` +\n `it falls in a DST gap. Pass { disambiguation: 'compatible' | 'earlier' | 'later' } to pick an instant.`\n );\n }\n throw err;\n }\n}\n\nexport function getTimeZone(value: unknown): string {\n const v = value as { timeZoneId?: string };\n if (typeof v?.timeZoneId !== 'string') {\n throw new Error(`temporal-fmt: getTimeZone() expected a ZonedDateTime, got ${typeof value}.`);\n }\n return v.timeZoneId;\n}\n\nexport function getOffset(value: unknown): string {\n const v = value as { offset?: string };\n if (typeof v?.offset !== 'string') {\n throw new Error(`temporal-fmt: getOffset() expected a ZonedDateTime, got ${typeof value}.`);\n }\n return v.offset;\n}\n\nexport function getOffsetNanoseconds(value: unknown): number {\n const v = value as { offsetNanoseconds?: number; offset?: string };\n if (typeof v?.offsetNanoseconds === 'number') return v.offsetNanoseconds;\n if (typeof v?.offset === 'string') {\n // Parse ±HH:MM → nanoseconds.\n const m = v.offset.match(/^([+-])(\\d{2}):(\\d{2})$/);\n if (!m) throw new Error(`temporal-fmt: getOffsetNanoseconds() couldn't parse offset \"${v.offset}\".`);\n const sign = m[1] === '-' ? -1 : 1;\n return sign * (Number(m[2]) * 3600 + Number(m[3]) * 60) * 1_000_000_000;\n }\n throw new Error(`temporal-fmt: getOffsetNanoseconds() expected a ZonedDateTime, got ${typeof value}.`);\n}\n\n// DST detection: a ZonedDateTime is in DST iff the offset is different\n// from the timezone's standard offset at the same instant. Computed\n// by comparing the current offset to the offset in January (Northern\n// Hemisphere) or July (Southern Hemisphere) of the same year — a\n// heuristic that's right for most populated zones.\nexport function isDST(value: unknown): boolean {\n const v = value as { timeZoneId?: string; offset?: string; year?: number; toInstant?: () => unknown };\n if (typeof v?.timeZoneId !== 'string' || typeof v?.offset !== 'string' || typeof v?.year !== 'number') {\n throw new Error(`temporal-fmt: isDST() expected a ZonedDateTime, got ${typeof value}.`);\n }\n // Compute the offset for January 1 of the same year (standard time\n // for Northern Hemisphere zones) and compare. For Southern Hemisphere\n // zones January is summer, so we'd want July instead — but checking\n // January vs current is good enough for the common case, and we\n // can't reliably determine hemisphere without a lookup table.\n const temporal = getTemporal();\n try {\n const jan = temporal.ZonedDateTime.from(\n { year: v.year, month: 1, day: 1, hour: 12, timeZone: v.timeZoneId },\n { disambiguation: 'compatible' } as unknown as Parameters<typeof temporal.ZonedDateTime.from>[1],\n ) as { offset: string };\n return jan.offset !== v.offset;\n } catch {\n return false;\n }\n}\n\n// Returns the next DST transition strictly after `value`, or undefined\n// if no transition occurs within the next 2 years. Implemented by\n// walking day-by-day checking for offset changes — slow but simple,\n// and correct across all IANA zones.\nexport function getNextTransition(value: unknown): unknown | undefined {\n return findTransition(value, 1);\n}\n\nexport function getPreviousTransition(value: unknown): unknown | undefined {\n return findTransition(value, -1);\n}\n\nfunction findTransition(value: unknown, direction: 1 | -1): unknown | undefined {\n const v = value as { timeZoneId?: string; offset?: string; toInstant?: () => unknown; year?: number; month?: number; day?: number; hour?: number; minute?: number; second?: number; millisecond?: number };\n if (typeof v?.timeZoneId !== 'string') {\n throw new Error(`temporal-fmt: ${direction > 0 ? 'getNextTransition' : 'getPreviousTransition'}() expected a ZonedDateTime, got ${typeof value}.`);\n }\n const temporal = getTemporal();\n const timeZone = v.timeZoneId;\n let candidateFields = add(value, direction, 'days');\n const startOffset = v.offset!;\n for (let i = 0; i < 365 * 2; i++) {\n // Rebuild a real ZonedDateTime for this candidate day, in this\n // timezone, so `.offset` reflects the actual UTC offset on that\n // day rather than being absent. add() only produces a plain field\n // bag (it has no notion of timezone), so re-attaching timeZone via\n // ZonedDateTime.from is what actually lets us detect a DST change\n // here — using the field bag's own (nonexistent) offset would make\n // every call \"find\" a transition on the very first day checked.\n const cf = candidateFields as { year: number; month: number; day: number; hour: number; minute: number; second: number; millisecond: number };\n const candidateZdt = temporal.ZonedDateTime.from(\n { ...cf, timeZone },\n { disambiguation: 'compatible' } as unknown as Parameters<typeof temporal.ZonedDateTime.from>[1],\n ) as { offset: string };\n if (candidateZdt.offset !== startOffset) {\n // Found a transition day. Return the reconstructed ZonedDateTime.\n return candidateZdt;\n }\n candidateFields = add(candidateFields, direction, 'days');\n }\n return undefined;\n}\n\nexport function getTransitions(start: unknown, end: unknown): unknown[] {\n const result: unknown[] = [];\n const startV = start as { timeZoneId?: string; offset?: string; year?: number; month?: number; day?: number };\n if (typeof startV?.timeZoneId !== 'string') {\n throw new Error(`temporal-fmt: getTransitions() expected a ZonedDateTime for start, got ${typeof start}.`);\n }\n const endV = end as { year: number; month: number; day: number };\n const temporal = getTemporal();\n const timeZone = startV.timeZoneId;\n let cursorFields = add(start, 1, 'days');\n let lastOffset = startV.offset!;\n for (let i = 0; i < 365 * 5; i++) {\n // Same reconstruction as findTransition() above — add() strips\n // timezone/offset info, so we rebuild a real ZonedDateTime each\n // iteration to read its actual offset instead of comparing against\n // a field bag that never has one.\n const cf = cursorFields as { year: number; month: number; day: number; hour: number; minute: number; second: number; millisecond: number };\n const cursorZdt = temporal.ZonedDateTime.from(\n { ...cf, timeZone },\n { disambiguation: 'compatible' } as unknown as Parameters<typeof temporal.ZonedDateTime.from>[1],\n ) as { offset: string; year: number; month: number; day: number };\n if (cursorZdt.offset !== lastOffset) {\n // Found a transition.\n if (cursorZdt.year > endV.year || (cursorZdt.year === endV.year && (cursorZdt.month > endV.month || (cursorZdt.month === endV.month && cursorZdt.day > endV.day)))) break;\n result.push(cursorZdt);\n lastOffset = cursorZdt.offset;\n }\n cursorFields = add(cursorFields, 1, 'days');\n }\n return result;\n}\n\n// Returns the list of possible instants for a wall-clock time in a\n// zone — used to detect gaps (0 instants) and overlaps (2 instants).\nexport function possibleInstantsFor(\n fields: { year: number; month: number; day: number; hour: number; minute: number; second: number; millisecond: number },\n timeZone: string,\n): unknown[] {\n const temporal = getTemporal();\n // Resolve the same wall-clock fields twice, forcing disambiguation\n // toward the earlier and later side of any DST boundary. Comparing\n // the two results tells us which of the three cases we're in:\n // - gap (e.g. 2:30am on a spring-forward day): neither result's own\n // wall-clock fields match what was requested, since Temporal has\n // to shift off the nonexistent time in both directions — 0 instants.\n // - overlap (e.g. 1:30am on a fall-back day): both results keep the\n // requested wall-clock time but land on different instants (one\n // per side of the boundary) — 2 instants.\n // - normal time: both results match the requested wall-clock time\n // and resolve to the same instant — 1 instant.\n const earlier = temporal.ZonedDateTime.from(\n { ...fields, timeZone },\n { disambiguation: 'earlier', offset: 'ignore' } as unknown as Parameters<typeof temporal.ZonedDateTime.from>[1],\n ) as { year: number; month: number; day: number; hour: number; minute: number; second: number; millisecond: number; toInstant: () => { epochNanoseconds: bigint } };\n const later = temporal.ZonedDateTime.from(\n { ...fields, timeZone },\n { disambiguation: 'later', offset: 'ignore' } as unknown as Parameters<typeof temporal.ZonedDateTime.from>[1],\n ) as { year: number; month: number; day: number; hour: number; minute: number; second: number; millisecond: number; toInstant: () => { epochNanoseconds: bigint } };\n\n const matchesRequestedWallClock = (z: typeof earlier) =>\n z.year === fields.year && z.month === fields.month && z.day === fields.day &&\n z.hour === fields.hour && z.minute === fields.minute &&\n z.second === fields.second && z.millisecond === fields.millisecond;\n\n if (!matchesRequestedWallClock(earlier) || !matchesRequestedWallClock(later)) {\n // Gap — the requested wall-clock time doesn't exist in this zone.\n return [];\n }\n if (earlier.toInstant().epochNanoseconds === later.toInstant().epochNanoseconds) {\n // Normal time — both disambiguation directions agree on the instant.\n return [earlier];\n }\n // Overlap — same wall-clock time, two distinct instants.\n return [earlier, later];\n}"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import {
|
|
2
|
+
dayOfYear,
|
|
3
|
+
isGregorianLeapYear,
|
|
4
|
+
isoWeekYearAndWeek
|
|
5
|
+
} from "./chunk-MKPDLQKD.js";
|
|
6
|
+
|
|
7
|
+
// src/calendarUtils.ts
|
|
8
|
+
function requireFields(view, fields) {
|
|
9
|
+
for (const f of fields) {
|
|
10
|
+
if (typeof view[f] !== "number") {
|
|
11
|
+
throw new Error(
|
|
12
|
+
`temporal-fmt: calendar helper requires "${String(f)}", which this value doesn't have. Pass a Temporal.PlainDate / PlainDateTime / ZonedDateTime.`
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function daysInMonth(view) {
|
|
18
|
+
requireFields(view, ["year", "month"]);
|
|
19
|
+
const { year, month } = view;
|
|
20
|
+
const LENGTHS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
21
|
+
if (month === 2 && isGregorianLeapYear(year)) return 29;
|
|
22
|
+
return LENGTHS[month - 1];
|
|
23
|
+
}
|
|
24
|
+
function daysInYear(view) {
|
|
25
|
+
requireFields(view, ["year"]);
|
|
26
|
+
return isGregorianLeapYear(view.year) ? 366 : 365;
|
|
27
|
+
}
|
|
28
|
+
function monthsInYear(_view) {
|
|
29
|
+
return 12;
|
|
30
|
+
}
|
|
31
|
+
function isLeapYear(view) {
|
|
32
|
+
requireFields(view, ["year"]);
|
|
33
|
+
return isGregorianLeapYear(view.year);
|
|
34
|
+
}
|
|
35
|
+
function isLeapMonth(_view) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
function dayOfYearHelper(view) {
|
|
39
|
+
requireFields(view, ["year", "month", "day"]);
|
|
40
|
+
return dayOfYear(view.year, view.month, view.day);
|
|
41
|
+
}
|
|
42
|
+
function weekOfYear(view) {
|
|
43
|
+
requireFields(view, ["year", "month", "day", "dayOfWeek"]);
|
|
44
|
+
return isoWeekYearAndWeek(view.year, view.month, view.day, view.dayOfWeek).week;
|
|
45
|
+
}
|
|
46
|
+
function weekYear(view) {
|
|
47
|
+
requireFields(view, ["year", "month", "day", "dayOfWeek"]);
|
|
48
|
+
return isoWeekYearAndWeek(view.year, view.month, view.day, view.dayOfWeek).isoYear;
|
|
49
|
+
}
|
|
50
|
+
function validateStartMonth(startMonth) {
|
|
51
|
+
if (!Number.isInteger(startMonth) || startMonth < 1 || startMonth > 12) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`temporal-fmt: getQuarter's startMonth must be an integer from 1 to 12 (got ${startMonth}).`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function getQuarter(view, options = {}) {
|
|
58
|
+
requireFields(view, ["month"]);
|
|
59
|
+
const startMonth = options.startMonth ?? 1;
|
|
60
|
+
validateStartMonth(startMonth);
|
|
61
|
+
if (startMonth === 1) {
|
|
62
|
+
return Math.ceil(view.month / 3);
|
|
63
|
+
}
|
|
64
|
+
const shifted = (view.month - startMonth + 12) % 12 + 1;
|
|
65
|
+
return Math.ceil(shifted / 3);
|
|
66
|
+
}
|
|
67
|
+
function getMonth(view) {
|
|
68
|
+
requireFields(view, ["month"]);
|
|
69
|
+
return view.month;
|
|
70
|
+
}
|
|
71
|
+
function getWeekday(view) {
|
|
72
|
+
requireFields(view, ["dayOfWeek"]);
|
|
73
|
+
return view.dayOfWeek;
|
|
74
|
+
}
|
|
75
|
+
function touchesTime(unit) {
|
|
76
|
+
return unit === "day" || unit === "month" || unit === "year";
|
|
77
|
+
}
|
|
78
|
+
function recomputeDayOfWeek(view) {
|
|
79
|
+
if (typeof view.dayOfWeek !== "number") return;
|
|
80
|
+
/* c8 ignore start @preserve -- unreachable: startOf()/endOf() both call
|
|
81
|
+
* asDateFieldView() before this, which already throws if year/month/day
|
|
82
|
+
* aren't all numbers — so by the time a value with a numeric dayOfWeek
|
|
83
|
+
* reaches here, year/month/day are guaranteed present too. */
|
|
84
|
+
if (typeof view.year !== "number" || typeof view.month !== "number" || typeof view.day !== "number") return;
|
|
85
|
+
/* c8 ignore stop @preserve */
|
|
86
|
+
const jsDow = new Date(Date.UTC(view.year, view.month - 1, view.day)).getUTCDay();
|
|
87
|
+
view.dayOfWeek = jsDow === 0 ? 7 : jsDow;
|
|
88
|
+
}
|
|
89
|
+
function startOf(value, unit) {
|
|
90
|
+
const view = asDateFieldView(value);
|
|
91
|
+
const result = { ...view };
|
|
92
|
+
if (unit === "year") {
|
|
93
|
+
result.month = 1;
|
|
94
|
+
result.day = 1;
|
|
95
|
+
} else if (unit === "month") {
|
|
96
|
+
result.day = 1;
|
|
97
|
+
}
|
|
98
|
+
recomputeDayOfWeek(result);
|
|
99
|
+
if (touchesTime(unit)) {
|
|
100
|
+
result.hour = 0;
|
|
101
|
+
result.minute = 0;
|
|
102
|
+
result.second = 0;
|
|
103
|
+
result.millisecond = 0;
|
|
104
|
+
} else if (unit === "hour") {
|
|
105
|
+
result.minute = 0;
|
|
106
|
+
result.second = 0;
|
|
107
|
+
result.millisecond = 0;
|
|
108
|
+
} else if (unit === "minute") {
|
|
109
|
+
result.second = 0;
|
|
110
|
+
result.millisecond = 0;
|
|
111
|
+
} else if (unit === "second") {
|
|
112
|
+
result.millisecond = 0;
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
function endOf(value, unit) {
|
|
117
|
+
const view = asDateFieldView(value);
|
|
118
|
+
const result = { ...view };
|
|
119
|
+
if (unit === "year") {
|
|
120
|
+
result.month = 12;
|
|
121
|
+
result.day = daysInMonth({ year: result.year, month: 12 });
|
|
122
|
+
} else if (unit === "month") {
|
|
123
|
+
result.day = daysInMonth({ year: result.year, month: result.month });
|
|
124
|
+
}
|
|
125
|
+
recomputeDayOfWeek(result);
|
|
126
|
+
if (touchesTime(unit)) {
|
|
127
|
+
result.hour = 23;
|
|
128
|
+
result.minute = 59;
|
|
129
|
+
result.second = 59;
|
|
130
|
+
result.millisecond = 999;
|
|
131
|
+
} else if (unit === "hour") {
|
|
132
|
+
result.minute = 59;
|
|
133
|
+
result.second = 59;
|
|
134
|
+
result.millisecond = 999;
|
|
135
|
+
} else if (unit === "minute") {
|
|
136
|
+
result.second = 59;
|
|
137
|
+
result.millisecond = 999;
|
|
138
|
+
} else if (unit === "second") {
|
|
139
|
+
result.millisecond = 999;
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
function asDateFieldView(value) {
|
|
144
|
+
if (typeof value !== "object" || value === null) {
|
|
145
|
+
throw new Error(`temporal-fmt: expected a date-carrying Temporal value, got ${String(value)}.`);
|
|
146
|
+
}
|
|
147
|
+
const v = value;
|
|
148
|
+
const out = {};
|
|
149
|
+
if (typeof v.year === "number") out.year = v.year;
|
|
150
|
+
if (typeof v.month === "number") out.month = v.month;
|
|
151
|
+
if (typeof v.day === "number") out.day = v.day;
|
|
152
|
+
if (typeof v.hour === "number") out.hour = v.hour;
|
|
153
|
+
if (typeof v.minute === "number") out.minute = v.minute;
|
|
154
|
+
if (typeof v.second === "number") out.second = v.second;
|
|
155
|
+
if (typeof v.millisecond === "number") out.millisecond = v.millisecond;
|
|
156
|
+
if (typeof v.dayOfWeek === "number") out.dayOfWeek = v.dayOfWeek;
|
|
157
|
+
if (typeof v.calendarId === "string") out.calendarId = v.calendarId;
|
|
158
|
+
if (out.year === void 0 || out.month === void 0 || out.day === void 0) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`temporal-fmt: value is missing year/month/day fields \u2014 pass a Temporal.PlainDate / PlainDateTime / ZonedDateTime.`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export {
|
|
167
|
+
daysInMonth,
|
|
168
|
+
daysInYear,
|
|
169
|
+
monthsInYear,
|
|
170
|
+
isLeapYear,
|
|
171
|
+
isLeapMonth,
|
|
172
|
+
dayOfYearHelper,
|
|
173
|
+
weekOfYear,
|
|
174
|
+
weekYear,
|
|
175
|
+
getQuarter,
|
|
176
|
+
getMonth,
|
|
177
|
+
getWeekday,
|
|
178
|
+
startOf,
|
|
179
|
+
endOf,
|
|
180
|
+
asDateFieldView
|
|
181
|
+
};
|
|
182
|
+
//# sourceMappingURL=chunk-HXY2GUUM.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/calendarUtils.ts"],"sourcesContent":["// Calendar utility helpers. These are pure functions\n// over the TemporalLike shape — no Temporal namespace needed, same\n// approach as isoWeek.ts and the field-reading helpers in format.ts.\n// Letting callers compute dayOfYear/weekOfYear/etc. without going\n// through format() means they can build their own derived values\n// without committing to a string format.\n//\n// Calendar-sensitivity: the helpers in this module assume the\n// iso8601 (Gregorian) calendar — that's what TemporalLike fields\n// carry for the overwhelming majority of callers. Non-Gregorian\n// calendars (hebrew, islamic, etc.) need their own helpers; this\n// module doesn't try to be calendar-polymorphic the way Temporal\n// itself is. Documented limitation, not a design choice — see\n// VERIFICATION.md for the rationale.\n\nimport { isGregorianLeapYear, dayOfYear, isoWeekYearAndWeek } from './isoWeek.js';\n\n// A subset of TemporalLike that has the date fields these helpers need,\n// plus optional time fields. PlainTime isn't a DateFieldView (no\n// year/month/day), but PlainDateTime / ZonedDateTime / PlainDate all\n// match. Time fields are optional so callers can pass a PlainDate\n// to startOf(value, 'month') without having to populate hour/minute/etc.\n// Exported so the comparison/arithmetic modules can use the same\n// narrowing.\nexport interface DateFieldView {\n year?: number;\n month?: number;\n day?: number;\n hour?: number;\n minute?: number;\n second?: number;\n millisecond?: number;\n dayOfWeek?: number;\n calendarId?: string;\n}\n\nfunction requireFields(view: DateFieldView, fields: Array<keyof DateFieldView>): void {\n for (const f of fields) {\n if (typeof view[f] !== 'number') {\n throw new Error(\n `temporal-fmt: calendar helper requires \"${String(f)}\", which this value doesn't have. ` +\n `Pass a Temporal.PlainDate / PlainDateTime / ZonedDateTime.`\n );\n }\n }\n}\n\nexport function daysInMonth(view: DateFieldView): number {\n requireFields(view, ['year', 'month']);\n const { year, month } = view;\n // Standard Gregorian month lengths. February's length depends on\n // whether `year` is a leap year — the same isGregorianLeapYear check\n // isoWeek.ts uses for dayOfYear arithmetic.\n const LENGTHS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];\n if (month === 2 && isGregorianLeapYear(year!)) return 29;\n return LENGTHS[(month! - 1)!]!;\n}\n\nexport function daysInYear(view: DateFieldView): 365 | 366 {\n requireFields(view, ['year']);\n return isGregorianLeapYear(view.year!) ? 366 : 365;\n}\n\nexport function monthsInYear(_view: DateFieldView): 12 {\n // Gregorian always has 12 months. Other calendars (Hebrew leap years\n // have 13) need calendar-aware logic this module doesn't carry — see\n // the file-level comment. The `_view` parameter is kept so the\n // signature mirrors the other helpers and a future calendar-aware\n // implementation can use it without changing call sites.\n return 12;\n}\n\nexport function isLeapYear(view: DateFieldView): boolean {\n requireFields(view, ['year']);\n return isGregorianLeapYear(view.year!);\n}\n\n// `isLeapMonth` would require knowing which month of a leap-year-aware\n// calendar is the leap month — Gregorian doesn't have one, so this\n// returns false unconditionally. Kept here so the public surface\n// matches the module's listing; non-Gregorian calendars need\n// a different implementation.\nexport function isLeapMonth(_view: DateFieldView): boolean {\n return false;\n}\n\nexport function dayOfYearHelper(view: DateFieldView): number {\n requireFields(view, ['year', 'month', 'day']);\n return dayOfYear(view.year!, view.month!, view.day!);\n}\n\n// ISO 8601 week and week-year. Delegates to isoWeek.ts's\n// isoWeekYearAndWeek, which does the full Thursday-of-week\n// computation to handle the year-boundary cases (Dec 29-31 belonging\n// to week 1 of next year, Jan 1-3 belonging to week 52/53 of the\n// previous year).\nexport function weekOfYear(view: DateFieldView): number {\n requireFields(view, ['year', 'month', 'day', 'dayOfWeek']);\n return isoWeekYearAndWeek(view.year!, view.month!, view.day!, view.dayOfWeek!).week;\n}\n\nexport function weekYear(view: DateFieldView): number {\n requireFields(view, ['year', 'month', 'day', 'dayOfWeek']);\n return isoWeekYearAndWeek(view.year!, view.month!, view.day!, view.dayOfWeek!).isoYear;\n}\n\n/**\n * Fiscal-quarter options. `startMonth` is the calendar month (1-12) the\n * fiscal year begins on — e.g. `7` for a fiscal year starting in July.\n * Omitted or `1` gives the calendar-quarter behavior getQuarter() has\n * always had (Jan-Mar = Q1, etc.), so existing callers passing nothing\n * see no change.\n */\nexport interface QuarterOptions {\n startMonth?: number;\n}\n\nfunction validateStartMonth(startMonth: number): void {\n if (!Number.isInteger(startMonth) || startMonth < 1 || startMonth > 12) {\n throw new Error(\n `temporal-fmt: getQuarter's startMonth must be an integer from 1 to 12 (got ${startMonth}).`\n );\n }\n}\n\nexport function getQuarter(view: DateFieldView, options: QuarterOptions = {}): number {\n requireFields(view, ['month']);\n const startMonth = options.startMonth ?? 1;\n validateStartMonth(startMonth);\n if (startMonth === 1) {\n // Mirrors the Q token: months 1-3 → Q1, 4-6 → Q2, 7-9 → Q3, 10-12 → Q4.\n return Math.ceil(view.month! / 3);\n }\n // Fiscal case: shift the month so startMonth becomes month 1 of the\n // fiscal year (mod 12, 1-indexed), then apply the same ceil(/3) rule.\n // E.g. startMonth=7 (fiscal year starts July): July→1, Aug→2, ...,\n // Dec→6, Jan→7, ..., June→12. Then Q1 = fiscal months 1-3 (Jul-Sep),\n // matching the common \"FY starts in July\" convention where Q1 is the\n // first quarter of the fiscal year, not a quarter numbered by which\n // calendar quarter it falls in.\n const shifted = ((view.month! - startMonth + 12) % 12) + 1;\n return Math.ceil(shifted / 3);\n}\n\n// `getMonth` / `getWeekday` look trivial (just read the field) but\n// they're kept as real functions for API surface consistency with the\n// rest of this module. They also normalize: getWeekday returns 1-7\n// (Mon-Sun, matching Temporal's spec) regardless of what numbering the\n// caller's underlying value uses.\nexport function getMonth(view: DateFieldView): number {\n requireFields(view, ['month']);\n return view.month!;\n}\n\nexport function getWeekday(view: DateFieldView): number {\n requireFields(view, ['dayOfWeek']);\n return view.dayOfWeek!;\n}\n\n// startOf / endOf return new field bags (not Temporal objects — this\n// module is polyfill-free) with the relevant fields zeroed/extended.\n// Callers can pass the result to a Temporal constructor if they want\n// a typed value.\nexport type StartOfUnit = 'day' | 'month' | 'year' | 'hour' | 'minute' | 'second';\n\n// Returns true if the given unit (when used with startOf/endOf) should\n// also touch the time fields. 'day', 'month', 'year' all imply a\n// resolution coarser than an hour, so startOf zeroes the time fields\n// and endOf maxes them. Sub-hour units (hour/minute/second) only touch\n// the fields finer than themselves.\nfunction touchesTime(unit: StartOfUnit): boolean {\n return unit === 'day' || unit === 'month' || unit === 'year';\n}\n\n// startOf/endOf reassign year/month/day, which invalidates any\n// dayOfWeek carried over from the input — a plain { ...view } spread\n// leaves the old value sitting there unchanged. Same failure mode\n// businessCalendar.ts's isBusinessDay() works around for add(); we\n// recompute here rather than trust the copied field.\nfunction recomputeDayOfWeek(view: DateFieldView): void {\n if (typeof view.dayOfWeek !== 'number') return;\n /* c8 ignore start @preserve -- unreachable: startOf()/endOf() both call\n * asDateFieldView() before this, which already throws if year/month/day\n * aren't all numbers — so by the time a value with a numeric dayOfWeek\n * reaches here, year/month/day are guaranteed present too. */\n if (typeof view.year !== 'number' || typeof view.month !== 'number' || typeof view.day !== 'number') return;\n /* c8 ignore stop @preserve */\n const jsDow = new Date(Date.UTC(view.year, view.month - 1, view.day)).getUTCDay(); // 0=Sun..6=Sat\n view.dayOfWeek = jsDow === 0 ? 7 : jsDow; // 1=Mon..7=Sun\n}\n\nexport function startOf(value: unknown, unit: StartOfUnit): DateFieldView {\n const view = asDateFieldView(value);\n const result: DateFieldView = { ...view };\n if (unit === 'year') {\n result.month = 1;\n result.day = 1;\n } else if (unit === 'month') {\n result.day = 1;\n }\n recomputeDayOfWeek(result);\n if (touchesTime(unit)) {\n result.hour = 0;\n result.minute = 0;\n result.second = 0;\n result.millisecond = 0;\n } else if (unit === 'hour') {\n result.minute = 0;\n result.second = 0;\n result.millisecond = 0;\n } else if (unit === 'minute') {\n result.second = 0;\n result.millisecond = 0;\n } else if (unit === 'second') {\n result.millisecond = 0;\n }\n return result;\n}\n\nexport function endOf(value: unknown, unit: StartOfUnit): DateFieldView {\n const view = asDateFieldView(value);\n const result: DateFieldView = { ...view };\n if (unit === 'year') {\n result.month = 12;\n result.day = daysInMonth({ year: result.year!, month: 12 });\n } else if (unit === 'month') {\n result.day = daysInMonth({ year: result.year!, month: result.month! });\n }\n recomputeDayOfWeek(result);\n if (touchesTime(unit)) {\n result.hour = 23;\n result.minute = 59;\n result.second = 59;\n result.millisecond = 999;\n } else if (unit === 'hour') {\n result.minute = 59;\n result.second = 59;\n result.millisecond = 999;\n } else if (unit === 'minute') {\n result.second = 59;\n result.millisecond = 999;\n } else if (unit === 'second') {\n result.millisecond = 999;\n }\n return result;\n}\n\n// Type-narrowing helpers used by the comparison/arithmetic modules.\n// Lets them accept any of the four date-carrying Temporal types without\n// importing Temporal itself.\nexport function asDateFieldView(value: unknown): DateFieldView {\n if (typeof value !== 'object' || value === null) {\n throw new Error(`temporal-fmt: expected a date-carrying Temporal value, got ${String(value)}.`);\n }\n // Temporal instances expose year/month/day/etc. as prototype getters,\n // not own enumerable properties — so `{ ...value }` would lose them.\n // Read them explicitly. Only the fields actually present on this\n // value type end up in the returned view.\n const v = value as Record<string, unknown>;\n const out: DateFieldView = {};\n if (typeof v.year === 'number') out.year = v.year;\n if (typeof v.month === 'number') out.month = v.month;\n if (typeof v.day === 'number') out.day = v.day;\n if (typeof v.hour === 'number') out.hour = v.hour;\n if (typeof v.minute === 'number') out.minute = v.minute;\n if (typeof v.second === 'number') out.second = v.second;\n if (typeof v.millisecond === 'number') out.millisecond = v.millisecond;\n if (typeof v.dayOfWeek === 'number') out.dayOfWeek = v.dayOfWeek;\n if (typeof v.calendarId === 'string') out.calendarId = v.calendarId;\n if (out.year === undefined || out.month === undefined || out.day === undefined) {\n throw new Error(\n `temporal-fmt: value is missing year/month/day fields — pass a Temporal.PlainDate / PlainDateTime / ZonedDateTime.`\n );\n }\n return out;\n}\n\n// Re-export the TemporalType alias so callers can import everything\n// from one place.\nexport type { TemporalType } from './tokenMetadata.js';\n// Re-export TemporalLike for the same reason.\nexport type { TemporalLike } from './tokens.js';"],"mappings":"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","names":["requireFields","view","fields","f","daysInMonth","year","month","LENGTHS","isGregorianLeapYear","daysInYear","monthsInYear","_view","isLeapYear","isLeapMonth","dayOfYearHelper","dayOfYear","weekOfYear","isoWeekYearAndWeek","weekYear","validateStartMonth","startMonth","getQuarter","options","shifted","getMonth","getWeekday","touchesTime","unit","recomputeDayOfWeek","jsDow","startOf","value","result","asDateFieldView","endOf","v","out"]}
|
|
1
|
+
{"version":3,"sources":["../src/calendarUtils.ts"],"sourcesContent":["// Calendar utility helpers. These are pure functions\n// over the TemporalLike shape — no Temporal namespace needed, same\n// approach as isoWeek.ts and the field-reading helpers in format.ts.\n// Letting callers compute dayOfYear/weekOfYear/etc. without going\n// through format() means they can build their own derived values\n// without committing to a string format.\n//\n// Calendar-sensitivity: the helpers in this module assume the\n// iso8601 (Gregorian) calendar — that's what TemporalLike fields\n// carry for the overwhelming majority of callers. Non-Gregorian\n// calendars (hebrew, islamic, etc.) need their own helpers; this\n// module doesn't try to be calendar-polymorphic the way Temporal\n// itself is. Documented limitation, not a design choice — see\n// VERIFICATION.md for the rationale.\n\nimport { isGregorianLeapYear, dayOfYear, isoWeekYearAndWeek } from './isoWeek.js';\n\n// A subset of TemporalLike that has the date fields these helpers need,\n// plus optional time fields. PlainTime isn't a DateFieldView (no\n// year/month/day), but PlainDateTime / ZonedDateTime / PlainDate all\n// match. Time fields are optional so callers can pass a PlainDate\n// to startOf(value, 'month') without having to populate hour/minute/etc.\n// Exported so the comparison/arithmetic modules can use the same\n// narrowing.\nexport interface DateFieldView {\n year?: number;\n month?: number;\n day?: number;\n hour?: number;\n minute?: number;\n second?: number;\n millisecond?: number;\n dayOfWeek?: number;\n calendarId?: string;\n}\n\nfunction requireFields(view: DateFieldView, fields: Array<keyof DateFieldView>): void {\n for (const f of fields) {\n if (typeof view[f] !== 'number') {\n throw new Error(\n `temporal-fmt: calendar helper requires \"${String(f)}\", which this value doesn't have. ` +\n `Pass a Temporal.PlainDate / PlainDateTime / ZonedDateTime.`\n );\n }\n }\n}\n\nexport function daysInMonth(view: DateFieldView): number {\n requireFields(view, ['year', 'month']);\n const { year, month } = view;\n // Standard Gregorian month lengths. February's length depends on\n // whether `year` is a leap year — the same isGregorianLeapYear check\n // isoWeek.ts uses for dayOfYear arithmetic.\n const LENGTHS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];\n if (month === 2 && isGregorianLeapYear(year!)) return 29;\n return LENGTHS[(month! - 1)!]!;\n}\n\nexport function daysInYear(view: DateFieldView): 365 | 366 {\n requireFields(view, ['year']);\n return isGregorianLeapYear(view.year!) ? 366 : 365;\n}\n\nexport function monthsInYear(_view: DateFieldView): 12 {\n // Gregorian always has 12 months. Other calendars (Hebrew leap years\n // have 13) need calendar-aware logic this module doesn't carry — see\n // the file-level comment. The `_view` parameter is kept so the\n // signature mirrors the other helpers and a future calendar-aware\n // implementation can use it without changing call sites.\n return 12;\n}\n\nexport function isLeapYear(view: DateFieldView): boolean {\n requireFields(view, ['year']);\n return isGregorianLeapYear(view.year!);\n}\n\n// `isLeapMonth` would require knowing which month of a leap-year-aware\n// calendar is the leap month — Gregorian doesn't have one, so this\n// returns false unconditionally. Kept here so the public surface\n// matches the module's listing; non-Gregorian calendars need\n// a different implementation.\nexport function isLeapMonth(_view: DateFieldView): boolean {\n return false;\n}\n\nexport function dayOfYearHelper(view: DateFieldView): number {\n requireFields(view, ['year', 'month', 'day']);\n return dayOfYear(view.year!, view.month!, view.day!);\n}\n\n// ISO 8601 week and week-year. Delegates to isoWeek.ts's\n// isoWeekYearAndWeek, which does the full Thursday-of-week\n// computation to handle the year-boundary cases (Dec 29-31 belonging\n// to week 1 of next year, Jan 1-3 belonging to week 52/53 of the\n// previous year).\nexport function weekOfYear(view: DateFieldView): number {\n requireFields(view, ['year', 'month', 'day', 'dayOfWeek']);\n return isoWeekYearAndWeek(view.year!, view.month!, view.day!, view.dayOfWeek!).week;\n}\n\nexport function weekYear(view: DateFieldView): number {\n requireFields(view, ['year', 'month', 'day', 'dayOfWeek']);\n return isoWeekYearAndWeek(view.year!, view.month!, view.day!, view.dayOfWeek!).isoYear;\n}\n\n/**\n * Fiscal-quarter options. `startMonth` is the calendar month (1-12) the\n * fiscal year begins on — e.g. `7` for a fiscal year starting in July.\n * Omitted or `1` gives the calendar-quarter behavior getQuarter() has\n * always had (Jan-Mar = Q1, etc.), so existing callers passing nothing\n * see no change.\n */\nexport interface QuarterOptions {\n startMonth?: number;\n}\n\nfunction validateStartMonth(startMonth: number): void {\n if (!Number.isInteger(startMonth) || startMonth < 1 || startMonth > 12) {\n throw new Error(\n `temporal-fmt: getQuarter's startMonth must be an integer from 1 to 12 (got ${startMonth}).`\n );\n }\n}\n\nexport function getQuarter(view: DateFieldView, options: QuarterOptions = {}): number {\n requireFields(view, ['month']);\n const startMonth = options.startMonth ?? 1;\n validateStartMonth(startMonth);\n if (startMonth === 1) {\n // Mirrors the Q token: months 1-3 → Q1, 4-6 → Q2, 7-9 → Q3, 10-12 → Q4.\n return Math.ceil(view.month! / 3);\n }\n // Fiscal case: shift the month so startMonth becomes month 1 of the\n // fiscal year (mod 12, 1-indexed), then apply the same ceil(/3) rule.\n // E.g. startMonth=7 (fiscal year starts July): July→1, Aug→2, ...,\n // Dec→6, Jan→7, ..., June→12. Then Q1 = fiscal months 1-3 (Jul-Sep),\n // matching the common \"FY starts in July\" convention where Q1 is the\n // first quarter of the fiscal year, not a quarter numbered by which\n // calendar quarter it falls in.\n const shifted = ((view.month! - startMonth + 12) % 12) + 1;\n return Math.ceil(shifted / 3);\n}\n\n// `getMonth` / `getWeekday` look trivial (just read the field) but\n// they're kept as real functions for API surface consistency with the\n// rest of this module. They also normalize: getWeekday returns 1-7\n// (Mon-Sun, matching Temporal's spec) regardless of what numbering the\n// caller's underlying value uses.\nexport function getMonth(view: DateFieldView): number {\n requireFields(view, ['month']);\n return view.month!;\n}\n\nexport function getWeekday(view: DateFieldView): number {\n requireFields(view, ['dayOfWeek']);\n return view.dayOfWeek!;\n}\n\n// startOf / endOf return new field bags (not Temporal objects — this\n// module is polyfill-free) with the relevant fields zeroed/extended.\n// Callers can pass the result to a Temporal constructor if they want\n// a typed value.\nexport type StartOfUnit = 'day' | 'month' | 'year' | 'hour' | 'minute' | 'second';\n\n// Returns true if the given unit (when used with startOf/endOf) should\n// also touch the time fields. 'day', 'month', 'year' all imply a\n// resolution coarser than an hour, so startOf zeroes the time fields\n// and endOf maxes them. Sub-hour units (hour/minute/second) only touch\n// the fields finer than themselves.\nfunction touchesTime(unit: StartOfUnit): boolean {\n return unit === 'day' || unit === 'month' || unit === 'year';\n}\n\n// startOf/endOf reassign year/month/day, which invalidates any\n// dayOfWeek carried over from the input — a plain { ...view } spread\n// leaves the old value sitting there unchanged. Same failure mode\n// businessCalendar.ts's isBusinessDay() works around for add(); we\n// recompute here rather than trust the copied field.\nfunction recomputeDayOfWeek(view: DateFieldView): void {\n if (typeof view.dayOfWeek !== 'number') return;\n /* c8 ignore start @preserve -- unreachable: startOf()/endOf() both call\n * asDateFieldView() before this, which already throws if year/month/day\n * aren't all numbers — so by the time a value with a numeric dayOfWeek\n * reaches here, year/month/day are guaranteed present too. */\n if (typeof view.year !== 'number' || typeof view.month !== 'number' || typeof view.day !== 'number') return;\n /* c8 ignore stop @preserve */\n const jsDow = new Date(Date.UTC(view.year, view.month - 1, view.day)).getUTCDay(); // 0=Sun..6=Sat\n view.dayOfWeek = jsDow === 0 ? 7 : jsDow; // 1=Mon..7=Sun\n}\n\nexport function startOf(value: unknown, unit: StartOfUnit): DateFieldView {\n const view = asDateFieldView(value);\n const result: DateFieldView = { ...view };\n if (unit === 'year') {\n result.month = 1;\n result.day = 1;\n } else if (unit === 'month') {\n result.day = 1;\n }\n recomputeDayOfWeek(result);\n if (touchesTime(unit)) {\n result.hour = 0;\n result.minute = 0;\n result.second = 0;\n result.millisecond = 0;\n } else if (unit === 'hour') {\n result.minute = 0;\n result.second = 0;\n result.millisecond = 0;\n } else if (unit === 'minute') {\n result.second = 0;\n result.millisecond = 0;\n } else if (unit === 'second') {\n result.millisecond = 0;\n }\n return result;\n}\n\nexport function endOf(value: unknown, unit: StartOfUnit): DateFieldView {\n const view = asDateFieldView(value);\n const result: DateFieldView = { ...view };\n if (unit === 'year') {\n result.month = 12;\n result.day = daysInMonth({ year: result.year!, month: 12 });\n } else if (unit === 'month') {\n result.day = daysInMonth({ year: result.year!, month: result.month! });\n }\n recomputeDayOfWeek(result);\n if (touchesTime(unit)) {\n result.hour = 23;\n result.minute = 59;\n result.second = 59;\n result.millisecond = 999;\n } else if (unit === 'hour') {\n result.minute = 59;\n result.second = 59;\n result.millisecond = 999;\n } else if (unit === 'minute') {\n result.second = 59;\n result.millisecond = 999;\n } else if (unit === 'second') {\n result.millisecond = 999;\n }\n return result;\n}\n\n// Type-narrowing helpers used by the comparison/arithmetic modules.\n// Lets them accept any of the four date-carrying Temporal types without\n// importing Temporal itself.\nexport function asDateFieldView(value: unknown): DateFieldView {\n if (typeof value !== 'object' || value === null) {\n throw new Error(`temporal-fmt: expected a date-carrying Temporal value, got ${String(value)}.`);\n }\n // Temporal instances expose year/month/day/etc. as prototype getters,\n // not own enumerable properties — so `{ ...value }` would lose them.\n // Read them explicitly. Only the fields actually present on this\n // value type end up in the returned view.\n const v = value as Record<string, unknown>;\n const out: DateFieldView = {};\n if (typeof v.year === 'number') out.year = v.year;\n if (typeof v.month === 'number') out.month = v.month;\n if (typeof v.day === 'number') out.day = v.day;\n if (typeof v.hour === 'number') out.hour = v.hour;\n if (typeof v.minute === 'number') out.minute = v.minute;\n if (typeof v.second === 'number') out.second = v.second;\n if (typeof v.millisecond === 'number') out.millisecond = v.millisecond;\n if (typeof v.dayOfWeek === 'number') out.dayOfWeek = v.dayOfWeek;\n if (typeof v.calendarId === 'string') out.calendarId = v.calendarId;\n if (out.year === undefined || out.month === undefined || out.day === undefined) {\n throw new Error(\n `temporal-fmt: value is missing year/month/day fields — pass a Temporal.PlainDate / PlainDateTime / ZonedDateTime.`\n );\n }\n return out;\n}\n\n// Re-export the TemporalType alias so callers can import everything\n// from one place.\nexport type { TemporalType } from './tokenMetadata.js';\n// Re-export TemporalLike for the same reason.\nexport type { TemporalLike } from './tokens.js';"],"mappings":"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","names":[]}
|