@northguild/gmt 1.16.0 → 1.17.0
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 +289 -8
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/instant/convert/fromOffsetInstant.js +3 -11
- package/dist/instant/convert/toOffsetInstant.d.ts +23 -0
- package/dist/instant/convert/toOffsetInstant.js +2 -25
- package/dist/intermodal/calculate/billingTimeline.d.ts +94 -0
- package/dist/intermodal/calculate/billingTimeline.js +168 -0
- package/dist/intermodal/calculate/chargeableDays.d.ts +98 -0
- package/dist/intermodal/calculate/chargeableDays.js +101 -0
- package/dist/intermodal/calculate/demurrageClock.d.ts +93 -0
- package/dist/intermodal/calculate/demurrageClock.js +140 -0
- package/dist/intermodal/calculate/freeTimeExpiry.d.ts +66 -0
- package/dist/intermodal/calculate/freeTimeExpiry.js +82 -0
- package/dist/intermodal/calculate/index.d.ts +4 -0
- package/dist/intermodal/calculate/index.js +4 -0
- package/dist/intermodal/index.d.ts +2 -0
- package/dist/intermodal/index.js +3 -0
- package/dist/internal/freeTimeLedger.d.ts +93 -0
- package/dist/internal/freeTimeLedger.js +262 -0
- package/dist/internal/index.d.ts +4 -2
- package/dist/internal/index.js +4 -2
- package/dist/internal/isoStringBody.d.ts +26 -0
- package/dist/internal/isoStringBody.js +22 -2
- package/dist/internal/wallClockAtOffset.d.ts +22 -0
- package/dist/internal/wallClockAtOffset.js +26 -0
- package/dist/internal/zonedWallClockDifference.js +5 -4
- package/dist/transport/calculate/dwellTime.d.ts +72 -0
- package/dist/transport/calculate/dwellTime.js +126 -0
- package/dist/transport/calculate/index.d.ts +2 -0
- package/dist/transport/calculate/index.js +2 -0
- package/dist/transport/calculate/transitTime.d.ts +53 -0
- package/dist/transport/calculate/transitTime.js +118 -0
- package/dist/transport/convert/etaAtZone.d.ts +36 -0
- package/dist/transport/convert/etaAtZone.js +53 -0
- package/dist/transport/convert/index.d.ts +1 -0
- package/dist/transport/convert/index.js +1 -0
- package/dist/transport/index.d.ts +3 -0
- package/dist/transport/index.js +4 -0
- package/dist/types/free-time-options.d.ts +34 -0
- package/dist/types/free-time-options.js +0 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/package.json +36 -1
- package/skills/gmt-arithmetic/SKILL.md +1 -1
- package/skills/gmt-basics/SKILL.md +1 -1
- package/skills/gmt-integration/SKILL.md +1 -1
- package/skills/gmt-timezone/SKILL.md +79 -12
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { FreeTimeOptions } from "../../types/index.js";
|
|
2
|
+
/** What `freeTimeExpiry` returns: the free-time window as local dates, and the instant it ends. */
|
|
3
|
+
export interface FreeTime {
|
|
4
|
+
/** Free day one, as a local date in the counting zone. */
|
|
5
|
+
freeTimeStart: string;
|
|
6
|
+
/** The last free day, as a local date in the counting zone. */
|
|
7
|
+
lastFreeDay: string;
|
|
8
|
+
/** The instant free time ends: the start of the local day after `lastFreeDay`, as a UTC instant. */
|
|
9
|
+
expiresAt: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Lay out a container's free time from the instant its clock started, and say when it ends.
|
|
13
|
+
*
|
|
14
|
+
* Free time is the money calculation in container logistics, and none of its terms is a fact
|
|
15
|
+
* about the port: the number of free days, whether the day of discharge is free day one, and
|
|
16
|
+
* whether weekends count are all set by the carrier's tariff or the service contract. So every
|
|
17
|
+
* term is a parameter. No regulation or industry standard fixes how free time is counted (DCSA types only its unit: calendar days, working days, hours or day of discharge; GMT counts
|
|
18
|
+
* days); the result is the window every tariff states, and what an invoice can print about it:
|
|
19
|
+
* the free time allowed and the dates it starts and ends.
|
|
20
|
+
*
|
|
21
|
+
* - **Days are the terminal's local days.** `clockStart` is an instant (`Z`, an offset, or a
|
|
22
|
+
* bracketed zone, of which only the instant is read); its local date in `options.timeZone` is
|
|
23
|
+
* the event day. A container discharged at 23:00 has used a free day by 00:01 under
|
|
24
|
+
* `firstDay: "eventDay"`. Day boundaries are the zone's real ones, as `floorToZone` and
|
|
25
|
+
* `dwellTime` find them: a 23- or 25-hour day is one day, a date the zone deleted is never a
|
|
26
|
+
* free day, and a fall-back that re-enters an earlier date never moves the count backwards.
|
|
27
|
+
* - **`firstDay` has no default.** `"eventDay"` makes the event day free day one; `"nextDay"`
|
|
28
|
+
* starts free time on the following counted day. The two differ by exactly one day of charges,
|
|
29
|
+
* and a silent default here is a wrong invoice, so omitting it returns `null`.
|
|
30
|
+
* - **`basis` decides which days count.** `"calendar"` counts every local day, so a weekend
|
|
31
|
+
* burns two free days while the terminal is shut. `"working"` counts only the working days of
|
|
32
|
+
* `options.calendar` (its weekend and holidays; `calendar.timeZone` is not read), and is
|
|
33
|
+
* required to have one: a working-day count without a calendar is a guess and returns `null`.
|
|
34
|
+
* Under `"working"` a Saturday discharge starts free time on Monday under either `firstDay`.
|
|
35
|
+
* A tariff counting "calendar days excluding bank holidays" (Hapag-Lloyd Japan, MSC Vietnam) is
|
|
36
|
+
* `"working"` with an empty `weekend`. How days after expiry are charged is a separate term;
|
|
37
|
+
* see `chargeableDays`'s `chargeBasis`.
|
|
38
|
+
* - **`expiresAt` is half-open.** It is the first instant of the local day after `lastFreeDay`,
|
|
39
|
+
* as a UTC instant. A gate-out at exactly `expiresAt` is not a chargeable day; see
|
|
40
|
+
* `chargeableDays`. Under `"working"` it can fall on a weekend, which is then simply not
|
|
41
|
+
* chargeable.
|
|
42
|
+
* - `freeDays` is a whole number of at least `1`: with no free days there is no last free day
|
|
43
|
+
* to name, and `chargeableDays` accepts `0` for that tariff.
|
|
44
|
+
* - Returns `null` when `clockStart` is not an instant, `freeDays` is not a whole number of at
|
|
45
|
+
* least `1`, `options` is not an object, `basis` or `firstDay` is missing or unknown,
|
|
46
|
+
* `timeZone` is invalid, `basis` is `"working"` without a valid `BusinessCalendar`, or the walk
|
|
47
|
+
* would pass 10,000 local days.
|
|
48
|
+
*
|
|
49
|
+
* @param clockStart ISO 8601 instant string of the event that starts the clock (discharge, availability or gate-out)
|
|
50
|
+
* @param freeDays whole number of free days the tariff allows, at least 1
|
|
51
|
+
* @param options { basis: "calendar" | "working", timeZone: string, firstDay: "eventDay" | "nextDay", calendar?: BusinessCalendar }
|
|
52
|
+
* @returns the free-time start and last free day as local dates, and the expiry instant, or null on invalid input
|
|
53
|
+
*
|
|
54
|
+
* @example freeTimeExpiry("2024-06-14T19:00:00Z", 3, { basis: "calendar", timeZone: "America/New_York", firstDay: "eventDay" }) // { freeTimeStart: "2024-06-14", lastFreeDay: "2024-06-16", expiresAt: "2024-06-17T04:00:00Z" } (a Friday discharge: the weekend is consumed)
|
|
55
|
+
* @example freeTimeExpiry("2024-06-14T19:00:00Z", 3, { basis: "calendar", timeZone: "America/New_York", firstDay: "nextDay" }) // { freeTimeStart: "2024-06-15", lastFreeDay: "2024-06-17", expiresAt: "2024-06-18T04:00:00Z" } (the same tariff read the other way: one more day)
|
|
56
|
+
* @example freeTimeExpiry("2024-06-14T19:00:00Z", 3, { basis: "working", timeZone: "America/New_York", firstDay: "eventDay", calendar: { weekend: [6, 7], holidays: [], timeZone: "America/New_York" } }) // { freeTimeStart: "2024-06-14", lastFreeDay: "2024-06-18", expiresAt: "2024-06-19T04:00:00Z" } (Friday, Monday, Tuesday: the weekend does not count)
|
|
57
|
+
* @example freeTimeExpiry("2024-06-14T19:00:00Z", 3, { basis: "working", timeZone: "America/New_York", firstDay: "eventDay", calendar: { weekend: [6, 7], holidays: ["2024-06-17"], timeZone: "America/New_York" } }) // { freeTimeStart: "2024-06-14", lastFreeDay: "2024-06-19", expiresAt: "2024-06-20T04:00:00Z" } (a terminal holiday on the Monday moves expiry a day)
|
|
58
|
+
* @example freeTimeExpiry("2024-06-15T14:00:00Z", 2, { basis: "working", timeZone: "America/New_York", firstDay: "eventDay", calendar: { weekend: [6, 7], holidays: [], timeZone: "America/New_York" } }) // { freeTimeStart: "2024-06-17", lastFreeDay: "2024-06-18", expiresAt: "2024-06-19T04:00:00Z" } (a Saturday discharge: free time starts Monday)
|
|
59
|
+
* @example freeTimeExpiry("2024-06-15T03:00:00Z", 1, { basis: "calendar", timeZone: "America/New_York", firstDay: "eventDay" }) // { freeTimeStart: "2024-06-14", lastFreeDay: "2024-06-14", expiresAt: "2024-06-15T04:00:00Z" } (23:00 local on the 14th: the free day is over an hour later)
|
|
60
|
+
* @example freeTimeExpiry("2024-09-07T15:00:00Z", 1, { basis: "calendar", timeZone: "America/Santiago", firstDay: "eventDay" }) // { freeTimeStart: "2024-09-07", lastFreeDay: "2024-09-07", expiresAt: "2024-09-08T04:00:00Z" } (Santiago skipped midnight on the 8th, so the day starts at 01:00)
|
|
61
|
+
* @example freeTimeExpiry("2024-06-14T19:00:00Z", 3, { basis: "calendar", timeZone: "America/New_York" }) // null (firstDay has no default)
|
|
62
|
+
* @example freeTimeExpiry("2024-06-14T19:00:00Z", 3, { basis: "working", timeZone: "America/New_York", firstDay: "eventDay" }) // null (a working-day count needs the terminal's calendar)
|
|
63
|
+
* @example freeTimeExpiry("2024-06-14T19:00:00Z", 0, { basis: "calendar", timeZone: "America/New_York", firstDay: "eventDay" }) // null (no free days means no last free day)
|
|
64
|
+
* @example freeTimeExpiry("2024-06-14T19:00:00", 3, { basis: "calendar", timeZone: "America/New_York", firstDay: "eventDay" }) // null (no offset: not an instant)
|
|
65
|
+
*/
|
|
66
|
+
export declare function freeTimeExpiry(clockStart: string, freeDays: number, options: FreeTimeOptions): FreeTime | null;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Temporal } from "@js-temporal/polyfill";
|
|
2
|
+
import { parseFreeDays, parseFreeTimeTerms, walkFreeTime, } from "../../internal/index.js";
|
|
3
|
+
import { isValidInstant } from "../../precision/validate/isValidInstant.js";
|
|
4
|
+
/**
|
|
5
|
+
* Lay out a container's free time from the instant its clock started, and say when it ends.
|
|
6
|
+
*
|
|
7
|
+
* Free time is the money calculation in container logistics, and none of its terms is a fact
|
|
8
|
+
* about the port: the number of free days, whether the day of discharge is free day one, and
|
|
9
|
+
* whether weekends count are all set by the carrier's tariff or the service contract. So every
|
|
10
|
+
* term is a parameter. No regulation or industry standard fixes how free time is counted (DCSA types only its unit: calendar days, working days, hours or day of discharge; GMT counts
|
|
11
|
+
* days); the result is the window every tariff states, and what an invoice can print about it:
|
|
12
|
+
* the free time allowed and the dates it starts and ends.
|
|
13
|
+
*
|
|
14
|
+
* - **Days are the terminal's local days.** `clockStart` is an instant (`Z`, an offset, or a
|
|
15
|
+
* bracketed zone, of which only the instant is read); its local date in `options.timeZone` is
|
|
16
|
+
* the event day. A container discharged at 23:00 has used a free day by 00:01 under
|
|
17
|
+
* `firstDay: "eventDay"`. Day boundaries are the zone's real ones, as `floorToZone` and
|
|
18
|
+
* `dwellTime` find them: a 23- or 25-hour day is one day, a date the zone deleted is never a
|
|
19
|
+
* free day, and a fall-back that re-enters an earlier date never moves the count backwards.
|
|
20
|
+
* - **`firstDay` has no default.** `"eventDay"` makes the event day free day one; `"nextDay"`
|
|
21
|
+
* starts free time on the following counted day. The two differ by exactly one day of charges,
|
|
22
|
+
* and a silent default here is a wrong invoice, so omitting it returns `null`.
|
|
23
|
+
* - **`basis` decides which days count.** `"calendar"` counts every local day, so a weekend
|
|
24
|
+
* burns two free days while the terminal is shut. `"working"` counts only the working days of
|
|
25
|
+
* `options.calendar` (its weekend and holidays; `calendar.timeZone` is not read), and is
|
|
26
|
+
* required to have one: a working-day count without a calendar is a guess and returns `null`.
|
|
27
|
+
* Under `"working"` a Saturday discharge starts free time on Monday under either `firstDay`.
|
|
28
|
+
* A tariff counting "calendar days excluding bank holidays" (Hapag-Lloyd Japan, MSC Vietnam) is
|
|
29
|
+
* `"working"` with an empty `weekend`. How days after expiry are charged is a separate term;
|
|
30
|
+
* see `chargeableDays`'s `chargeBasis`.
|
|
31
|
+
* - **`expiresAt` is half-open.** It is the first instant of the local day after `lastFreeDay`,
|
|
32
|
+
* as a UTC instant. A gate-out at exactly `expiresAt` is not a chargeable day; see
|
|
33
|
+
* `chargeableDays`. Under `"working"` it can fall on a weekend, which is then simply not
|
|
34
|
+
* chargeable.
|
|
35
|
+
* - `freeDays` is a whole number of at least `1`: with no free days there is no last free day
|
|
36
|
+
* to name, and `chargeableDays` accepts `0` for that tariff.
|
|
37
|
+
* - Returns `null` when `clockStart` is not an instant, `freeDays` is not a whole number of at
|
|
38
|
+
* least `1`, `options` is not an object, `basis` or `firstDay` is missing or unknown,
|
|
39
|
+
* `timeZone` is invalid, `basis` is `"working"` without a valid `BusinessCalendar`, or the walk
|
|
40
|
+
* would pass 10,000 local days.
|
|
41
|
+
*
|
|
42
|
+
* @param clockStart ISO 8601 instant string of the event that starts the clock (discharge, availability or gate-out)
|
|
43
|
+
* @param freeDays whole number of free days the tariff allows, at least 1
|
|
44
|
+
* @param options { basis: "calendar" | "working", timeZone: string, firstDay: "eventDay" | "nextDay", calendar?: BusinessCalendar }
|
|
45
|
+
* @returns the free-time start and last free day as local dates, and the expiry instant, or null on invalid input
|
|
46
|
+
*
|
|
47
|
+
* @example freeTimeExpiry("2024-06-14T19:00:00Z", 3, { basis: "calendar", timeZone: "America/New_York", firstDay: "eventDay" }) // { freeTimeStart: "2024-06-14", lastFreeDay: "2024-06-16", expiresAt: "2024-06-17T04:00:00Z" } (a Friday discharge: the weekend is consumed)
|
|
48
|
+
* @example freeTimeExpiry("2024-06-14T19:00:00Z", 3, { basis: "calendar", timeZone: "America/New_York", firstDay: "nextDay" }) // { freeTimeStart: "2024-06-15", lastFreeDay: "2024-06-17", expiresAt: "2024-06-18T04:00:00Z" } (the same tariff read the other way: one more day)
|
|
49
|
+
* @example freeTimeExpiry("2024-06-14T19:00:00Z", 3, { basis: "working", timeZone: "America/New_York", firstDay: "eventDay", calendar: { weekend: [6, 7], holidays: [], timeZone: "America/New_York" } }) // { freeTimeStart: "2024-06-14", lastFreeDay: "2024-06-18", expiresAt: "2024-06-19T04:00:00Z" } (Friday, Monday, Tuesday: the weekend does not count)
|
|
50
|
+
* @example freeTimeExpiry("2024-06-14T19:00:00Z", 3, { basis: "working", timeZone: "America/New_York", firstDay: "eventDay", calendar: { weekend: [6, 7], holidays: ["2024-06-17"], timeZone: "America/New_York" } }) // { freeTimeStart: "2024-06-14", lastFreeDay: "2024-06-19", expiresAt: "2024-06-20T04:00:00Z" } (a terminal holiday on the Monday moves expiry a day)
|
|
51
|
+
* @example freeTimeExpiry("2024-06-15T14:00:00Z", 2, { basis: "working", timeZone: "America/New_York", firstDay: "eventDay", calendar: { weekend: [6, 7], holidays: [], timeZone: "America/New_York" } }) // { freeTimeStart: "2024-06-17", lastFreeDay: "2024-06-18", expiresAt: "2024-06-19T04:00:00Z" } (a Saturday discharge: free time starts Monday)
|
|
52
|
+
* @example freeTimeExpiry("2024-06-15T03:00:00Z", 1, { basis: "calendar", timeZone: "America/New_York", firstDay: "eventDay" }) // { freeTimeStart: "2024-06-14", lastFreeDay: "2024-06-14", expiresAt: "2024-06-15T04:00:00Z" } (23:00 local on the 14th: the free day is over an hour later)
|
|
53
|
+
* @example freeTimeExpiry("2024-09-07T15:00:00Z", 1, { basis: "calendar", timeZone: "America/Santiago", firstDay: "eventDay" }) // { freeTimeStart: "2024-09-07", lastFreeDay: "2024-09-07", expiresAt: "2024-09-08T04:00:00Z" } (Santiago skipped midnight on the 8th, so the day starts at 01:00)
|
|
54
|
+
* @example freeTimeExpiry("2024-06-14T19:00:00Z", 3, { basis: "calendar", timeZone: "America/New_York" }) // null (firstDay has no default)
|
|
55
|
+
* @example freeTimeExpiry("2024-06-14T19:00:00Z", 3, { basis: "working", timeZone: "America/New_York", firstDay: "eventDay" }) // null (a working-day count needs the terminal's calendar)
|
|
56
|
+
* @example freeTimeExpiry("2024-06-14T19:00:00Z", 0, { basis: "calendar", timeZone: "America/New_York", firstDay: "eventDay" }) // null (no free days means no last free day)
|
|
57
|
+
* @example freeTimeExpiry("2024-06-14T19:00:00", 3, { basis: "calendar", timeZone: "America/New_York", firstDay: "eventDay" }) // null (no offset: not an instant)
|
|
58
|
+
*/
|
|
59
|
+
export function freeTimeExpiry(clockStart, freeDays, options) {
|
|
60
|
+
try {
|
|
61
|
+
const terms = parseFreeTimeTerms(options);
|
|
62
|
+
const allowed = parseFreeDays(freeDays, 1);
|
|
63
|
+
if (!isValidInstant(clockStart) || terms === null || allowed === null) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const start = Temporal.Instant.from(clockStart).toZonedDateTimeISO(terms.timeZone);
|
|
67
|
+
const ledger = walkFreeTime(start, allowed, terms);
|
|
68
|
+
if (ledger === null) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
freeTimeStart: ledger.free[0].date,
|
|
73
|
+
lastFreeDay: ledger.free[allowed - 1].date,
|
|
74
|
+
expiresAt: ledger.expiresAt.toInstant().toString(),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Never throws (Core Rule 3): a hostile
|
|
79
|
+
// argument is invalid input, not an exception.
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Temporal } from "@js-temporal/polyfill";
|
|
2
|
+
import { type ResolvedBusinessCalendar } from "./businessCalendar.js";
|
|
3
|
+
/**
|
|
4
|
+
* Local days a free-time walk may visit before giving up: the same cap as `bucketRange`. A
|
|
5
|
+
* valid business calendar leaves at least one working day a week, so a working-day count needs
|
|
6
|
+
* at most about seven local days per free or chargeable day. A walk that runs out answers with
|
|
7
|
+
* the sentinel, never a partial ledger.
|
|
8
|
+
*/
|
|
9
|
+
export declare const MAX_WALKED_DAYS = 10000;
|
|
10
|
+
/**
|
|
11
|
+
* The tariff terms reduced to what the walk reads. `calendar` is the working-day calendar free
|
|
12
|
+
* days are counted against, `null` on the calendar basis; `chargeCalendar` is the one charged days
|
|
13
|
+
* are counted against, `null` when every local day after expiry is charged.
|
|
14
|
+
*/
|
|
15
|
+
export type FreeTimeTerms = {
|
|
16
|
+
timeZone: string;
|
|
17
|
+
firstDay: "eventDay" | "nextDay";
|
|
18
|
+
calendar: ResolvedBusinessCalendar | null;
|
|
19
|
+
chargeCalendar: ResolvedBusinessCalendar | null;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Read a `FreeTimeOptions` bag, or `null` when it is not one: a non-object, an unknown `basis`
|
|
23
|
+
* or `firstDay`, a missing `firstDay` (no default: the two conventions differ by a day of
|
|
24
|
+
* charges), an invalid `timeZone`, or a `"working"` basis without a valid `BusinessCalendar`.
|
|
25
|
+
* The `calendar` is read only when a basis that needs it is `"working"`.
|
|
26
|
+
*
|
|
27
|
+
* With `readChargeBasis`, `chargeBasis` is required too, and has no default for the same reason:
|
|
28
|
+
* where free time is in working days, tariffs mostly charge every calendar day after it, but some
|
|
29
|
+
* tariffs charge working days only, and the two differ by every closed day after expiry.
|
|
30
|
+
*/
|
|
31
|
+
export declare function parseFreeTimeTerms(options: unknown, readChargeBasis?: boolean): FreeTimeTerms | null;
|
|
32
|
+
/** A whole number of free days no smaller than `minimum`, or `null`. */
|
|
33
|
+
export declare function parseFreeDays(freeDays: unknown, minimum: 0 | 1): number | null;
|
|
34
|
+
/**
|
|
35
|
+
* Read a `tiers` option: omitted means no bands, otherwise a strictly ascending list of positive
|
|
36
|
+
* safe whole chargeable-day ordinals, each the last day of its band, with no holes. Anything
|
|
37
|
+
* else is `null`.
|
|
38
|
+
*/
|
|
39
|
+
export declare function parseTiers(tiers: unknown): number[] | null;
|
|
40
|
+
/**
|
|
41
|
+
* Split `chargeable` days into the bands `tiers` describes: `[5, 10]` is days 1–5, 6–10 and 11
|
|
42
|
+
* onward. Every band is listed, an empty one with `days: 0`, so a caller's rate table lines up
|
|
43
|
+
* by index; the last band is always open (`to: null`).
|
|
44
|
+
*/
|
|
45
|
+
export declare function bandsByTier(tiers: number[], chargeable: number): {
|
|
46
|
+
from: number;
|
|
47
|
+
to: number | null;
|
|
48
|
+
days: number;
|
|
49
|
+
}[];
|
|
50
|
+
/** One local date of the terminal: its first instant, and its ISO label. */
|
|
51
|
+
export interface LedgerDay {
|
|
52
|
+
start: Temporal.ZonedDateTime;
|
|
53
|
+
date: string;
|
|
54
|
+
/**
|
|
55
|
+
* True when the date began before the first instant Temporal represents, so `start` is the
|
|
56
|
+
* clock start standing in for it. Such a start is never output: it can only anchor the walk.
|
|
57
|
+
*/
|
|
58
|
+
clamped?: boolean;
|
|
59
|
+
}
|
|
60
|
+
/** What `walkFreeTime` returns: the free days, the expiry, and what the dwell used and owes. */
|
|
61
|
+
export interface FreeTimeLedger {
|
|
62
|
+
/** The free days in order, exactly `freeDays` of them. */
|
|
63
|
+
free: LedgerDay[];
|
|
64
|
+
/** The first instant of the local day after the last free day; with no free days, of day one. */
|
|
65
|
+
expiresAt: Temporal.ZonedDateTime;
|
|
66
|
+
/** Free days the half-open dwell `[clockStart, clockEnd)` touched; `0` with no `clockEnd`. */
|
|
67
|
+
used: number;
|
|
68
|
+
/** Counted days from `expiresAt` that the dwell touched, in order; empty with no `clockEnd`. */
|
|
69
|
+
charged: LedgerDay[];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Walk the terminal's real local days from the clock start and lay out free time and charges.
|
|
73
|
+
*
|
|
74
|
+
* Each day is a bucket `zonedUnitStart` / `nextZonedBucketStart` returns for `"day"`, the same
|
|
75
|
+
* boundaries `floorToZone` and `bucketRange` use, so a 23- or 25-hour day is one day, a date the
|
|
76
|
+
* zone deleted is never visited, and a date the clock falls back into is one day. Free days and
|
|
77
|
+
* charged days are counted against their own terms: on a working basis a weekend day or holiday is
|
|
78
|
+
* visited but not counted; on a calendar basis every day counts. The day of the event is neither
|
|
79
|
+
* free nor charged under `"nextDay"`.
|
|
80
|
+
*
|
|
81
|
+
* - Free days are the first `freeDays` days counted on `calendar`. `expiresAt` is the start of the
|
|
82
|
+
* day after the last of them, whether or not that day counts; with `freeDays` of `0` it is the
|
|
83
|
+
* start of the first counted day.
|
|
84
|
+
* - With a `clockEnd`, the dwell `[clockStart, clockEnd)` is half-open: a day is touched when it
|
|
85
|
+
* starts before `clockEnd`, and a zero-length dwell touches the day it sits on, as `dwellTime`
|
|
86
|
+
* counts. `used` is the free days touched; `charged` the days from `expiresAt` touched that count
|
|
87
|
+
* on `chargeCalendar`.
|
|
88
|
+
* - An event day that began before the first representable instant is still walked from the
|
|
89
|
+
* clock start; only an expiry that would be that unrepresentable start is refused.
|
|
90
|
+
* - Returns `null` when the walk would visit more than `MAX_WALKED_DAYS` local days, when a day
|
|
91
|
+
* boundary cannot be found, or when the expiry lies outside the representable range.
|
|
92
|
+
*/
|
|
93
|
+
export declare function walkFreeTime(clockStart: Temporal.ZonedDateTime, freeDays: number, terms: FreeTimeTerms, clockEnd?: Temporal.ZonedDateTime): FreeTimeLedger | null;
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { Temporal } from "@js-temporal/polyfill";
|
|
2
|
+
import { isValidTimeZone } from "../zoned/validate/isValidTimeZone.js";
|
|
3
|
+
import { isBusinessDate, parseBusinessCalendar, } from "./businessCalendar.js";
|
|
4
|
+
import { isObject } from "./isObject.js";
|
|
5
|
+
import { nextZonedBucketStart, zonedUnitStart } from "./zonedBucket.js";
|
|
6
|
+
/**
|
|
7
|
+
* Local days a free-time walk may visit before giving up: the same cap as `bucketRange`. A
|
|
8
|
+
* valid business calendar leaves at least one working day a week, so a working-day count needs
|
|
9
|
+
* at most about seven local days per free or chargeable day. A walk that runs out answers with
|
|
10
|
+
* the sentinel, never a partial ledger.
|
|
11
|
+
*/
|
|
12
|
+
export const MAX_WALKED_DAYS = 10_000;
|
|
13
|
+
/**
|
|
14
|
+
* Buckets stepped over while looking for a later date. A fall-back that re-enters the previous
|
|
15
|
+
* date (`America/Goose_Bay`, 2010-11-07 00:01 back into the 6th) opens a one-minute 7 November
|
|
16
|
+
* bucket, then a 6 November one, then 7 November again: two buckets in a row whose date is not
|
|
17
|
+
* later than the day being left. One transition can do no more than that, so eight is a
|
|
18
|
+
* generous bound.
|
|
19
|
+
*/
|
|
20
|
+
const MAX_REVISITED_BUCKETS = 8;
|
|
21
|
+
/** `parseFreeTimeTerms` without the guard; every read of `options` happens here. */
|
|
22
|
+
function readFreeTimeTerms(options, readChargeBasis = false) {
|
|
23
|
+
if (!isObject(options)) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const { basis, chargeBasis, firstDay, timeZone, calendar } = options;
|
|
27
|
+
const isBasis = (value) => value === "calendar" || value === "working";
|
|
28
|
+
if (!isBasis(basis)) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
if (readChargeBasis && !isBasis(chargeBasis)) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (firstDay !== "eventDay" && firstDay !== "nextDay") {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
if (typeof timeZone !== "string" || !isValidTimeZone(timeZone)) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const needsCalendar = basis === "working" || (readChargeBasis && chargeBasis === "working");
|
|
41
|
+
const resolved = needsCalendar ? parseBusinessCalendar(calendar) : null;
|
|
42
|
+
if (needsCalendar && resolved === null) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
timeZone,
|
|
47
|
+
firstDay,
|
|
48
|
+
calendar: basis === "working" ? resolved : null,
|
|
49
|
+
chargeCalendar: readChargeBasis && chargeBasis === "working" ? resolved : null,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Read a `FreeTimeOptions` bag, or `null` when it is not one: a non-object, an unknown `basis`
|
|
54
|
+
* or `firstDay`, a missing `firstDay` (no default: the two conventions differ by a day of
|
|
55
|
+
* charges), an invalid `timeZone`, or a `"working"` basis without a valid `BusinessCalendar`.
|
|
56
|
+
* The `calendar` is read only when a basis that needs it is `"working"`.
|
|
57
|
+
*
|
|
58
|
+
* With `readChargeBasis`, `chargeBasis` is required too, and has no default for the same reason:
|
|
59
|
+
* where free time is in working days, tariffs mostly charge every calendar day after it, but some
|
|
60
|
+
* tariffs charge working days only, and the two differ by every closed day after expiry.
|
|
61
|
+
*/
|
|
62
|
+
export function parseFreeTimeTerms(options, readChargeBasis = false) {
|
|
63
|
+
try {
|
|
64
|
+
return readFreeTimeTerms(options, readChargeBasis);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Never throws (Core Rule 3): a Proxy or getter that throws on access is invalid input.
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** A whole number of free days no smaller than `minimum`, or `null`. */
|
|
72
|
+
export function parseFreeDays(freeDays, minimum) {
|
|
73
|
+
return typeof freeDays === "number" &&
|
|
74
|
+
Number.isSafeInteger(freeDays) &&
|
|
75
|
+
freeDays >= minimum
|
|
76
|
+
? freeDays
|
|
77
|
+
: null;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Read a `tiers` option: omitted means no bands, otherwise a strictly ascending list of positive
|
|
81
|
+
* safe whole chargeable-day ordinals, each the last day of its band, with no holes. Anything
|
|
82
|
+
* else is `null`.
|
|
83
|
+
*/
|
|
84
|
+
export function parseTiers(tiers) {
|
|
85
|
+
if (tiers === undefined) {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
if (!Array.isArray(tiers)) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
// `Array.from` fills holes with `undefined`, which the check below rejects; `every` alone
|
|
92
|
+
// would skip them and let `[5, , ]` through to a `NaN` band.
|
|
93
|
+
const items = Array.from(tiers);
|
|
94
|
+
const ascending = items.every((tier, index) => typeof tier === "number" &&
|
|
95
|
+
Number.isSafeInteger(tier) &&
|
|
96
|
+
tier >= 1 &&
|
|
97
|
+
(index === 0 || tier > items[index - 1]));
|
|
98
|
+
return ascending ? items : null;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Split `chargeable` days into the bands `tiers` describes: `[5, 10]` is days 1–5, 6–10 and 11
|
|
102
|
+
* onward. Every band is listed, an empty one with `days: 0`, so a caller's rate table lines up
|
|
103
|
+
* by index; the last band is always open (`to: null`).
|
|
104
|
+
*/
|
|
105
|
+
export function bandsByTier(tiers, chargeable) {
|
|
106
|
+
const bands = [];
|
|
107
|
+
let from = 1;
|
|
108
|
+
for (const to of tiers) {
|
|
109
|
+
bands.push({
|
|
110
|
+
from,
|
|
111
|
+
to,
|
|
112
|
+
days: Math.max(0, Math.min(chargeable, to) - from + 1),
|
|
113
|
+
});
|
|
114
|
+
from = to + 1;
|
|
115
|
+
}
|
|
116
|
+
bands.push({ from, to: null, days: Math.max(0, chargeable - from + 1) });
|
|
117
|
+
return bands;
|
|
118
|
+
}
|
|
119
|
+
/** The local day holding `zoned`, or `null` when its start cannot be found. */
|
|
120
|
+
function localDayOf(zoned) {
|
|
121
|
+
let start;
|
|
122
|
+
try {
|
|
123
|
+
start = zonedUnitStart(zoned, "day");
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (!(error instanceof RangeError))
|
|
127
|
+
throw error;
|
|
128
|
+
// The event day began before Temporal's first instant (-271821-04-20T00:00Z in a zone west
|
|
129
|
+
// of UTC). Its date is still known, and every later day boundary is representable, so the
|
|
130
|
+
// clock start anchors the walk in its place.
|
|
131
|
+
return {
|
|
132
|
+
start: zoned,
|
|
133
|
+
date: zoned.toPlainDate().toString(),
|
|
134
|
+
clamped: true,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return start === null
|
|
138
|
+
? null
|
|
139
|
+
: { start, date: start.toPlainDate().toString() };
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The first local day after `day` whose date is later than `day`'s. A bucket that re-enters an
|
|
143
|
+
* earlier or equal date is folded into the day already laid out: a clock that starts in the
|
|
144
|
+
* one-minute first pass of Goose Bay's 7 November must not free or charge the 6th, and
|
|
145
|
+
* `countZonedLocalDates` counts a re-entered date once. `undefined` when the next day starts
|
|
146
|
+
* past the last representable instant, `null` when the walk gives up.
|
|
147
|
+
*/
|
|
148
|
+
function nextLocalDay(day) {
|
|
149
|
+
const current = day.start.toPlainDate();
|
|
150
|
+
let cursor = day.start;
|
|
151
|
+
for (let i = 0; i < MAX_REVISITED_BUCKETS; i++) {
|
|
152
|
+
let next;
|
|
153
|
+
try {
|
|
154
|
+
next = nextZonedBucketStart(cursor, "day");
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
if (error instanceof RangeError)
|
|
158
|
+
return undefined;
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
if (next === null) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
const date = next.toPlainDate();
|
|
165
|
+
if (Temporal.PlainDate.compare(date, current) > 0) {
|
|
166
|
+
return { start: next, date: date.toString() };
|
|
167
|
+
}
|
|
168
|
+
cursor = next;
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Walk the terminal's real local days from the clock start and lay out free time and charges.
|
|
174
|
+
*
|
|
175
|
+
* Each day is a bucket `zonedUnitStart` / `nextZonedBucketStart` returns for `"day"`, the same
|
|
176
|
+
* boundaries `floorToZone` and `bucketRange` use, so a 23- or 25-hour day is one day, a date the
|
|
177
|
+
* zone deleted is never visited, and a date the clock falls back into is one day. Free days and
|
|
178
|
+
* charged days are counted against their own terms: on a working basis a weekend day or holiday is
|
|
179
|
+
* visited but not counted; on a calendar basis every day counts. The day of the event is neither
|
|
180
|
+
* free nor charged under `"nextDay"`.
|
|
181
|
+
*
|
|
182
|
+
* - Free days are the first `freeDays` days counted on `calendar`. `expiresAt` is the start of the
|
|
183
|
+
* day after the last of them, whether or not that day counts; with `freeDays` of `0` it is the
|
|
184
|
+
* start of the first counted day.
|
|
185
|
+
* - With a `clockEnd`, the dwell `[clockStart, clockEnd)` is half-open: a day is touched when it
|
|
186
|
+
* starts before `clockEnd`, and a zero-length dwell touches the day it sits on, as `dwellTime`
|
|
187
|
+
* counts. `used` is the free days touched; `charged` the days from `expiresAt` touched that count
|
|
188
|
+
* on `chargeCalendar`.
|
|
189
|
+
* - An event day that began before the first representable instant is still walked from the
|
|
190
|
+
* clock start; only an expiry that would be that unrepresentable start is refused.
|
|
191
|
+
* - Returns `null` when the walk would visit more than `MAX_WALKED_DAYS` local days, when a day
|
|
192
|
+
* boundary cannot be found, or when the expiry lies outside the representable range.
|
|
193
|
+
*/
|
|
194
|
+
export function walkFreeTime(clockStart, freeDays, terms, clockEnd) {
|
|
195
|
+
// The dwell's last instant, one nanosecond before the exit; a zero-length dwell is its own point.
|
|
196
|
+
const last = clockEnd === undefined
|
|
197
|
+
? undefined
|
|
198
|
+
: Temporal.ZonedDateTime.compare(clockStart, clockEnd) < 0
|
|
199
|
+
? clockEnd.subtract({ nanoseconds: 1 })
|
|
200
|
+
: clockEnd;
|
|
201
|
+
const touched = (day) => last !== undefined && Temporal.ZonedDateTime.compare(day.start, last) <= 0;
|
|
202
|
+
const counts = (day) => terms.calendar === null ||
|
|
203
|
+
isBusinessDate(day.start.toPlainDate(), terms.calendar);
|
|
204
|
+
const charges = (day) => terms.chargeCalendar === null ||
|
|
205
|
+
isBusinessDate(day.start.toPlainDate(), terms.chargeCalendar);
|
|
206
|
+
const free = [];
|
|
207
|
+
const charged = [];
|
|
208
|
+
let used = 0;
|
|
209
|
+
let expiresAt = null;
|
|
210
|
+
let expiryPending = false;
|
|
211
|
+
let skipEventDay = terms.firstDay === "nextDay";
|
|
212
|
+
let day = localDayOf(clockStart);
|
|
213
|
+
if (day === null) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
// One more visit than the cap: the day after the last touched one is what ends the walk.
|
|
217
|
+
for (let i = 0; i <= MAX_WALKED_DAYS; i++) {
|
|
218
|
+
const eventDaySkipped = skipEventDay;
|
|
219
|
+
const counted = !skipEventDay && counts(day);
|
|
220
|
+
skipEventDay = false;
|
|
221
|
+
if (expiryPending) {
|
|
222
|
+
expiresAt = day.start;
|
|
223
|
+
expiryPending = false;
|
|
224
|
+
}
|
|
225
|
+
else if (expiresAt === null && counted) {
|
|
226
|
+
if (freeDays === 0) {
|
|
227
|
+
// With no free time the expiry is day one's start, which must be a real instant.
|
|
228
|
+
if (day.clamped)
|
|
229
|
+
return null;
|
|
230
|
+
expiresAt = day.start;
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
free.push(day);
|
|
234
|
+
if (touched(day))
|
|
235
|
+
used++;
|
|
236
|
+
expiryPending = free.length === freeDays;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (expiresAt !== null) {
|
|
240
|
+
// Free time has ended; with no clock end there is nothing more to lay out.
|
|
241
|
+
if (last === undefined) {
|
|
242
|
+
return { free, expiresAt, used, charged };
|
|
243
|
+
}
|
|
244
|
+
if (Temporal.ZonedDateTime.compare(day.start, last) > 0) {
|
|
245
|
+
return { free, expiresAt, used, charged };
|
|
246
|
+
}
|
|
247
|
+
// The day of the event under `nextDay` is never charged: free time has not started on it.
|
|
248
|
+
if (!eventDaySkipped && charges(day))
|
|
249
|
+
charged.push(day);
|
|
250
|
+
}
|
|
251
|
+
const next = nextLocalDay(day);
|
|
252
|
+
if (next === undefined) {
|
|
253
|
+
// No representable day follows: free time can still be laid out only if it has ended.
|
|
254
|
+
return expiresAt === null ? null : { free, expiresAt, used, charged };
|
|
255
|
+
}
|
|
256
|
+
if (next === null) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
day = next;
|
|
260
|
+
}
|
|
261
|
+
return null;
|
|
262
|
+
}
|
package/dist/internal/index.d.ts
CHANGED
|
@@ -15,8 +15,9 @@ export { dateCycleFieldBounds } from "./dateCycleFieldBounds.js";
|
|
|
15
15
|
export { timeCycleFieldBounds } from "./timeCycleFieldBounds.js";
|
|
16
16
|
export { durationUntilString } from "./durationUntilString.js";
|
|
17
17
|
export { formatDateInCalendar } from "./formatDateInCalendar.js";
|
|
18
|
+
export { bandsByTier, parseFreeDays, parseFreeTimeTerms, parseTiers, walkFreeTime, } from "./freeTimeLedger.js";
|
|
18
19
|
export { halfOpenAbuts, halfOpenContainsPoint, halfOpenContainsSpan, halfOpenDifference, halfOpenIntersection, halfOpenMerge, halfOpenOverlap, halfOpenUnion, halfOpenXor, } from "./halfOpenIntervals.js";
|
|
19
|
-
export { isoStringBody } from "./isoStringBody.js";
|
|
20
|
+
export { EXTENDED_UTC_OFFSET, isoStringBody, TIME_ZONE_ANNOTATION, } from "./isoStringBody.js";
|
|
20
21
|
export { formatHourDuration } from "./hourDurationString.js";
|
|
21
22
|
export { canonicalInstantIntervals } from "./instantIntervalText.js";
|
|
22
23
|
export { parseInstantNanoseconds } from "./instantNanoseconds.js";
|
|
@@ -47,7 +48,8 @@ export { parseUnixEpochInterval, parseUnixEpochIntervalList, parseUnixEpochInter
|
|
|
47
48
|
export { tileByUnit } from "./splitStep.js";
|
|
48
49
|
export { countZonedBuckets, nextZonedBucketStart, type WeekStartDay, zonedUnitEnd, zonedUnitStart, } from "./zonedBucket.js";
|
|
49
50
|
export { countZonedLocalDates } from "./zonedLocalDates.js";
|
|
50
|
-
export { zonedDateTimeFrom } from "./zonedWallClock.js";
|
|
51
|
+
export { utcOffsetStringNanoseconds, zonedDateTimeFrom, } from "./zonedWallClock.js";
|
|
52
|
+
export { wallClockAtOffset } from "./wallClockAtOffset.js";
|
|
51
53
|
export { durationCompare, durationRound, durationTotal, isCalendarDifferenceAcrossZones, plainUntilWithRounding, zonedUntil, } from "./zonedWallClockDifference.js";
|
|
52
54
|
export { addToZoned, addToZonedDisambiguated, plainToZoned, roundZonedDateTime, subtractFromZoned, withZonedFields, zonedHoursInDay, zonedNextTransition, zonedStartOfDay, } from "./zonedWallClockOperations.js";
|
|
53
55
|
export { zonelessCalendarDate } from "./zonelessCalendarDate.js";
|
package/dist/internal/index.js
CHANGED
|
@@ -15,8 +15,9 @@ export { dateCycleFieldBounds } from "./dateCycleFieldBounds.js";
|
|
|
15
15
|
export { timeCycleFieldBounds } from "./timeCycleFieldBounds.js";
|
|
16
16
|
export { durationUntilString } from "./durationUntilString.js";
|
|
17
17
|
export { formatDateInCalendar } from "./formatDateInCalendar.js";
|
|
18
|
+
export { bandsByTier, parseFreeDays, parseFreeTimeTerms, parseTiers, walkFreeTime, } from "./freeTimeLedger.js";
|
|
18
19
|
export { halfOpenAbuts, halfOpenContainsPoint, halfOpenContainsSpan, halfOpenDifference, halfOpenIntersection, halfOpenMerge, halfOpenOverlap, halfOpenUnion, halfOpenXor, } from "./halfOpenIntervals.js";
|
|
19
|
-
export { isoStringBody } from "./isoStringBody.js";
|
|
20
|
+
export { EXTENDED_UTC_OFFSET, isoStringBody, TIME_ZONE_ANNOTATION, } from "./isoStringBody.js";
|
|
20
21
|
export { formatHourDuration } from "./hourDurationString.js";
|
|
21
22
|
export { canonicalInstantIntervals } from "./instantIntervalText.js";
|
|
22
23
|
export { parseInstantNanoseconds } from "./instantNanoseconds.js";
|
|
@@ -47,7 +48,8 @@ export { parseUnixEpochInterval, parseUnixEpochIntervalList, parseUnixEpochInter
|
|
|
47
48
|
export { tileByUnit } from "./splitStep.js";
|
|
48
49
|
export { countZonedBuckets, nextZonedBucketStart, zonedUnitEnd, zonedUnitStart, } from "./zonedBucket.js";
|
|
49
50
|
export { countZonedLocalDates } from "./zonedLocalDates.js";
|
|
50
|
-
export { zonedDateTimeFrom } from "./zonedWallClock.js";
|
|
51
|
+
export { utcOffsetStringNanoseconds, zonedDateTimeFrom, } from "./zonedWallClock.js";
|
|
52
|
+
export { wallClockAtOffset } from "./wallClockAtOffset.js";
|
|
51
53
|
export { durationCompare, durationRound, durationTotal, isCalendarDifferenceAcrossZones, plainUntilWithRounding, zonedUntil, } from "./zonedWallClockDifference.js";
|
|
52
54
|
export { addToZoned, addToZonedDisambiguated, plainToZoned, roundZonedDateTime, subtractFromZoned, withZonedFields, zonedHoursInDay, zonedNextTransition, zonedStartOfDay, } from "./zonedWallClockOperations.js";
|
|
53
55
|
export { zonelessCalendarDate } from "./zonelessCalendarDate.js";
|
|
@@ -17,6 +17,32 @@
|
|
|
17
17
|
* @example isoStringBody("12:30") // "12:30"
|
|
18
18
|
*/
|
|
19
19
|
export declare function isoStringBody(value: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* An ISO 8601 extended UTC offset, as RFC 3339 §5.6 `time-numoffset` writes it (`±HH:MM`), with
|
|
22
|
+
* the seconds and fraction Temporal's `UTCOffset` grammar adds (`±HH:MM:SS[.fraction]`, the
|
|
23
|
+
* fraction after either decimal sign, `.` or `,`, as `TemporalDecimalSeparator` allows). Hours
|
|
24
|
+
* 00–23. Basic (`-0400`) and hour-only (`-04`) offsets, which Temporal also reads, do not match.
|
|
25
|
+
*
|
|
26
|
+
* A regular-expression source with no anchors and no capture groups, so callers can embed it:
|
|
27
|
+
* GMT's one statement of the offset grammar its instant and zoned validators accept.
|
|
28
|
+
*
|
|
29
|
+
* @example new RegExp(`^${EXTENDED_UTC_OFFSET}$`).test("+05:30:00,5") // true
|
|
30
|
+
* @example new RegExp(`^${EXTENDED_UTC_OFFSET}$`).test("-0400") // false (basic format)
|
|
31
|
+
*/
|
|
32
|
+
export declare const EXTENDED_UTC_OFFSET = "[+-](?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:[.,]\\d{1,9})?)?";
|
|
33
|
+
/**
|
|
34
|
+
* An RFC 9557 time-zone annotation: a bracket whose content has no `=` (§4.1 `time-zone`,
|
|
35
|
+
* against `suffix-tag`'s `key=value`), with an optional leading `!` marking it critical (§3.3).
|
|
36
|
+
* `[Europe/London]`, `[!+01:00]`; never `[u-ca=hebrew]` or `[!foo=bar]`. Every `key=value`
|
|
37
|
+
* bracket is a tagged annotation, which Temporal reads (or rejects) on its own.
|
|
38
|
+
*
|
|
39
|
+
* A regular-expression source with no anchors and no capture groups, so callers can embed or
|
|
40
|
+
* anchor it: GMT's one statement of "this bracket names a time zone".
|
|
41
|
+
*
|
|
42
|
+
* @example new RegExp(TIME_ZONE_ANNOTATION).test("2024-06-15T10:00:00Z[!Europe/London]") // true
|
|
43
|
+
* @example new RegExp(TIME_ZONE_ANNOTATION).test("2024-06-15T10:00:00Z[u-ca=gregory]") // false
|
|
44
|
+
*/
|
|
45
|
+
export declare const TIME_ZONE_ANNOTATION = "\\[!?[^\\]=]+\\]";
|
|
20
46
|
/**
|
|
21
47
|
* True when `value` is a string whose part before the first annotation is `zonedDateTimeBody`.
|
|
22
48
|
* The shape gate every GMT zoned string input passes before `Temporal.ZonedDateTime.from`.
|
|
@@ -26,10 +26,30 @@ export function isoStringBody(value) {
|
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
28
|
* An ISO 8601 extended UTC offset, as RFC 3339 §5.6 `time-numoffset` writes it (`±HH:MM`), with
|
|
29
|
-
* the seconds and fraction Temporal's `UTCOffset` grammar adds (`±HH:MM:SS[.fraction]
|
|
29
|
+
* the seconds and fraction Temporal's `UTCOffset` grammar adds (`±HH:MM:SS[.fraction]`, the
|
|
30
|
+
* fraction after either decimal sign, `.` or `,`, as `TemporalDecimalSeparator` allows). Hours
|
|
30
31
|
* 00–23. Basic (`-0400`) and hour-only (`-04`) offsets, which Temporal also reads, do not match.
|
|
32
|
+
*
|
|
33
|
+
* A regular-expression source with no anchors and no capture groups, so callers can embed it:
|
|
34
|
+
* GMT's one statement of the offset grammar its instant and zoned validators accept.
|
|
35
|
+
*
|
|
36
|
+
* @example new RegExp(`^${EXTENDED_UTC_OFFSET}$`).test("+05:30:00,5") // true
|
|
37
|
+
* @example new RegExp(`^${EXTENDED_UTC_OFFSET}$`).test("-0400") // false (basic format)
|
|
38
|
+
*/
|
|
39
|
+
export const EXTENDED_UTC_OFFSET = "[+-](?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:[.,]\\d{1,9})?)?";
|
|
40
|
+
/**
|
|
41
|
+
* An RFC 9557 time-zone annotation: a bracket whose content has no `=` (§4.1 `time-zone`,
|
|
42
|
+
* against `suffix-tag`'s `key=value`), with an optional leading `!` marking it critical (§3.3).
|
|
43
|
+
* `[Europe/London]`, `[!+01:00]`; never `[u-ca=hebrew]` or `[!foo=bar]`. Every `key=value`
|
|
44
|
+
* bracket is a tagged annotation, which Temporal reads (or rejects) on its own.
|
|
45
|
+
*
|
|
46
|
+
* A regular-expression source with no anchors and no capture groups, so callers can embed or
|
|
47
|
+
* anchor it: GMT's one statement of "this bracket names a time zone".
|
|
48
|
+
*
|
|
49
|
+
* @example new RegExp(TIME_ZONE_ANNOTATION).test("2024-06-15T10:00:00Z[!Europe/London]") // true
|
|
50
|
+
* @example new RegExp(TIME_ZONE_ANNOTATION).test("2024-06-15T10:00:00Z[u-ca=gregory]") // false
|
|
31
51
|
*/
|
|
32
|
-
const
|
|
52
|
+
export const TIME_ZONE_ANNOTATION = "\\[!?[^\\]=]+\\]";
|
|
33
53
|
/** `plainDateTime` without its anchors: `<date>T<time>` with an optional fraction. */
|
|
34
54
|
const EXTENDED_DATE_TIME = plainDateTime.source.slice(1, -1);
|
|
35
55
|
/**
|