@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.
Files changed (48) hide show
  1. package/README.md +289 -8
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +2 -0
  4. package/dist/instant/convert/fromOffsetInstant.js +3 -11
  5. package/dist/instant/convert/toOffsetInstant.d.ts +23 -0
  6. package/dist/instant/convert/toOffsetInstant.js +2 -25
  7. package/dist/intermodal/calculate/billingTimeline.d.ts +94 -0
  8. package/dist/intermodal/calculate/billingTimeline.js +168 -0
  9. package/dist/intermodal/calculate/chargeableDays.d.ts +98 -0
  10. package/dist/intermodal/calculate/chargeableDays.js +101 -0
  11. package/dist/intermodal/calculate/demurrageClock.d.ts +93 -0
  12. package/dist/intermodal/calculate/demurrageClock.js +140 -0
  13. package/dist/intermodal/calculate/freeTimeExpiry.d.ts +66 -0
  14. package/dist/intermodal/calculate/freeTimeExpiry.js +82 -0
  15. package/dist/intermodal/calculate/index.d.ts +4 -0
  16. package/dist/intermodal/calculate/index.js +4 -0
  17. package/dist/intermodal/index.d.ts +2 -0
  18. package/dist/intermodal/index.js +3 -0
  19. package/dist/internal/freeTimeLedger.d.ts +93 -0
  20. package/dist/internal/freeTimeLedger.js +262 -0
  21. package/dist/internal/index.d.ts +4 -2
  22. package/dist/internal/index.js +4 -2
  23. package/dist/internal/isoStringBody.d.ts +26 -0
  24. package/dist/internal/isoStringBody.js +22 -2
  25. package/dist/internal/wallClockAtOffset.d.ts +22 -0
  26. package/dist/internal/wallClockAtOffset.js +26 -0
  27. package/dist/internal/zonedWallClockDifference.js +5 -4
  28. package/dist/transport/calculate/dwellTime.d.ts +72 -0
  29. package/dist/transport/calculate/dwellTime.js +126 -0
  30. package/dist/transport/calculate/index.d.ts +2 -0
  31. package/dist/transport/calculate/index.js +2 -0
  32. package/dist/transport/calculate/transitTime.d.ts +53 -0
  33. package/dist/transport/calculate/transitTime.js +118 -0
  34. package/dist/transport/convert/etaAtZone.d.ts +36 -0
  35. package/dist/transport/convert/etaAtZone.js +53 -0
  36. package/dist/transport/convert/index.d.ts +1 -0
  37. package/dist/transport/convert/index.js +1 -0
  38. package/dist/transport/index.d.ts +3 -0
  39. package/dist/transport/index.js +4 -0
  40. package/dist/types/free-time-options.d.ts +34 -0
  41. package/dist/types/free-time-options.js +0 -0
  42. package/dist/types/index.d.ts +1 -0
  43. package/dist/types/index.js +1 -0
  44. package/package.json +36 -1
  45. package/skills/gmt-arithmetic/SKILL.md +1 -1
  46. package/skills/gmt-basics/SKILL.md +1 -1
  47. package/skills/gmt-integration/SKILL.md +1 -1
  48. package/skills/gmt-timezone/SKILL.md +79 -12
@@ -0,0 +1,22 @@
1
+ import type { Temporal } from "@js-temporal/polyfill";
2
+ /**
3
+ * The wall clock an instant reads at a fixed UTC offset, including a sub-minute or fractional
4
+ * one. Callers write the offset text themselves.
5
+ *
6
+ * The shift happens on the wall clock, never on the instant: a `PlainDateTime` reaches a day
7
+ * further either side than an `Instant` does (TC39 `ISODateTimeWithinLimits` against
8
+ * `IsValidEpochNanoseconds`), so shifting the instant first would reject the last 14 hours of
9
+ * the representable range, where `+275760-09-13T00:00:00Z` at `+14:00` names a local time that
10
+ * exists. No offset time zone is built either: Temporal's offset time-zone identifiers are
11
+ * minute precision, and an offset such as `-00:44:30` or `+05:30:15.25` is not.
12
+ *
13
+ * Throws what Temporal throws; callers wrap it in their own try/catch.
14
+ *
15
+ * @param instant the exact time
16
+ * @param offsetNanoseconds UTC offset in nanoseconds, east positive
17
+ * @returns the local wall clock at that offset
18
+ *
19
+ * @example wallClockAtOffset(Temporal.Instant.from("1970-01-01T00:00:00Z"), -2_670_000_000_000n).toString() // "1969-12-31T23:15:30"
20
+ * @example wallClockAtOffset(Temporal.Instant.from("+275760-09-13T00:00:00Z"), 50_400_000_000_000n).toString() // "+275760-09-13T14:00:00"
21
+ */
22
+ export declare function wallClockAtOffset(instant: Temporal.Instant, offsetNanoseconds: bigint): Temporal.PlainDateTime;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The wall clock an instant reads at a fixed UTC offset, including a sub-minute or fractional
3
+ * one. Callers write the offset text themselves.
4
+ *
5
+ * The shift happens on the wall clock, never on the instant: a `PlainDateTime` reaches a day
6
+ * further either side than an `Instant` does (TC39 `ISODateTimeWithinLimits` against
7
+ * `IsValidEpochNanoseconds`), so shifting the instant first would reject the last 14 hours of
8
+ * the representable range, where `+275760-09-13T00:00:00Z` at `+14:00` names a local time that
9
+ * exists. No offset time zone is built either: Temporal's offset time-zone identifiers are
10
+ * minute precision, and an offset such as `-00:44:30` or `+05:30:15.25` is not.
11
+ *
12
+ * Throws what Temporal throws; callers wrap it in their own try/catch.
13
+ *
14
+ * @param instant the exact time
15
+ * @param offsetNanoseconds UTC offset in nanoseconds, east positive
16
+ * @returns the local wall clock at that offset
17
+ *
18
+ * @example wallClockAtOffset(Temporal.Instant.from("1970-01-01T00:00:00Z"), -2_670_000_000_000n).toString() // "1969-12-31T23:15:30"
19
+ * @example wallClockAtOffset(Temporal.Instant.from("+275760-09-13T00:00:00Z"), 50_400_000_000_000n).toString() // "+275760-09-13T14:00:00"
20
+ */
21
+ export function wallClockAtOffset(instant, offsetNanoseconds) {
22
+ return instant
23
+ .toZonedDateTimeISO("UTC")
24
+ .toPlainDateTime()
25
+ .add({ nanoseconds: Number(offsetNanoseconds) });
26
+ }
@@ -1,4 +1,5 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
+ import { TIME_ZONE_ANNOTATION } from "./isoStringBody.js";
2
3
  import { MAX_EPOCH_NANOSECONDS, MIN_EPOCH_NANOSECONDS, } from "./epochNanoseconds.js";
3
4
  import { calendarDateAdd as compatCalendarDateAdd, calendarDateUntil as compatCalendarDateUntil, isCalendarArithmeticCompatNeeded, isNudgeWindowCompatNeeded, } from "./temporalCompat/index.js";
4
5
  import { epochNanosecondsFor, isNamedTimeZone, isValidEpoch, utcEpochNanoseconds, zonedDateTimeFrom, } from "./zonedWallClock.js";
@@ -43,7 +44,7 @@ const UTC_EQUIVALENT_OFFSET_ZONE = "+00:00";
43
44
  const OPTIONS_PROBE = new Temporal.ZonedDateTime(0n, "UTC");
44
45
  const ZERO_DURATION = new Temporal.Duration();
45
46
  /** An RFC 9557 time zone annotation (`[Europe/London]`, `[!+01:00]`), never a `key=value` one. */
46
- const TIME_ZONE_ANNOTATION = /\[!?[^\]=]+\]/;
47
+ const timeZoneAnnotation = new RegExp(TIME_ZONE_ANNOTATION);
47
48
  const ZERO_DATE_DURATION = {
48
49
  years: 0,
49
50
  months: 0,
@@ -766,7 +767,7 @@ function zonedRelativeTo(relativeTo) {
766
767
  return relativeTo;
767
768
  try {
768
769
  if (typeof relativeTo === "string") {
769
- return TIME_ZONE_ANNOTATION.test(relativeTo)
770
+ return timeZoneAnnotation.test(relativeTo)
770
771
  ? zonedDateTimeFrom(relativeTo)
771
772
  : null;
772
773
  }
@@ -868,7 +869,7 @@ function plainRelativeToNeedingCompat(relativeTo) {
868
869
  }
869
870
  else if (typeof relativeTo === "string" &&
870
871
  relativeTo.includes("u-ca=") &&
871
- !TIME_ZONE_ANNOTATION.test(relativeTo)) {
872
+ !timeZoneAnnotation.test(relativeTo)) {
872
873
  plain = Temporal.PlainDate.from(relativeTo);
873
874
  }
874
875
  else {
@@ -974,7 +975,7 @@ function plainRelativeTo(relativeTo) {
974
975
  return relativeTo.toPlainDate();
975
976
  }
976
977
  if (typeof relativeTo === "string" &&
977
- !TIME_ZONE_ANNOTATION.test(relativeTo)) {
978
+ !timeZoneAnnotation.test(relativeTo)) {
978
979
  return Temporal.PlainDate.from(relativeTo);
979
980
  }
980
981
  if (relativeTo !== null &&
@@ -0,0 +1,72 @@
1
+ /** What `dwellTime` returns: exact elapsed time plus the local view every charging regime counts in. */
2
+ export interface Dwell {
3
+ /** Exact elapsed time from entry to exit, as an ISO 8601 duration with hours as the largest unit. */
4
+ duration: string;
5
+ /** The entry, rendered in the dwell zone. */
6
+ enter: string;
7
+ /** The exit, rendered in the dwell zone. */
8
+ exit: string;
9
+ /** Distinct local calendar dates the dwell touched, counted half-open at the exit. */
10
+ calendarDays: number;
11
+ }
12
+ /**
13
+ * Measure how long something sat somewhere, and how many local calendar days it sat there.
14
+ *
15
+ * Terminals, depots, yards, stations, gates and berths all charge dwell, and none of them
16
+ * charge by elapsed hours. They count the local calendar days the thing was present, so a
17
+ * container that gates in at 23:00 and out at 01:00 has been there two hours and two days.
18
+ * `dwellTime` returns both, and is the one "local days crossed" primitive in the library: free
19
+ * time and demurrage (INT-12), laytime day counts (MAR-19) and hospital length of stay
20
+ * all take their day boundary from here rather than each deciding what a midnight is.
21
+ *
22
+ * - **`duration` is exact elapsed time.** A dwell spanning a DST transition reports the hours
23
+ * that actually elapsed; the local wall clocks at either end can be an hour apart from that.
24
+ * Hours is the largest unit, so the value never depends on a calendar.
25
+ * - **`calendarDays` is not `duration` rounded up.** It is the number of distinct local dates
26
+ * in `targetZone` that the half-open interval `[entry, exit)` touches: entry and exit on the
27
+ * same local date is `1`, across one local midnight is `2`, and so on. A dwell that ends
28
+ * exactly at a local midnight does not touch the new day, because the exit instant itself
29
+ * is excluded — the same half-open rule as `bucketRange` and CORE-6's intervals. A zero-length
30
+ * dwell touches one date. The count walks the zone's real transitions: between two of them
31
+ * the wall clock runs forward without a break, so each stretch touches every date from its
32
+ * first instant to its last, and the count is the size of the union of those dates. A 23- or
33
+ * 25-hour local day is one day, not 0.96 or 1.04 of one; a date the zone deleted is never
34
+ * touched (`Pacific/Apia` skipped 2011-12-30); and a date the clock falls back into is still
35
+ * one date (`America/Goose_Bay` fell back from 00:01 on 7 November 2010 into the 6th).
36
+ * - **The zone decides the count, so it has to be stated.** `targetZone` is used when given.
37
+ * Without it, the entry's bracketed IANA zone is used. As in `Temporal.Instant.from`, a
38
+ * bracketed zone is otherwise not read, so it is not validated either: the entry's is read
39
+ * (and must exist and agree with its offset) only when it supplies the dwell zone, because
40
+ * `targetZone` is omitted, and the exit's is never read. If the entry and exit are bare
41
+ * instants — `Z` or an offset — and no `targetZone` is given, the result is `null`: an
42
+ * offset is not a zone (CORE-4), and a day count in an unstated locality would be a guess.
43
+ * - `enter` and `exit` are the two instants rendered in the dwell zone, so a charging record
44
+ * shows the local times the terminal's own clock showed.
45
+ * - `targetZone` is whatever `isValidTimeZone` accepts, which is what Temporal accepts: an IANA
46
+ * identifier, or a fixed offset such as `"+02:00"`, as `etaAtZone` also takes. Days are then
47
+ * counted in that offset, which observes no DST, so pass the place's IANA zone when there is one.
48
+ * - GMT does not resolve a port, terminal or station code to a zone; the caller supplies the
49
+ * identifier.
50
+ * - Long dwells stay cheap: the walk visits zone transitions, not days. A dwell that crosses
51
+ * more than 10,000 transitions (about 5,000 years of twice-yearly DST) returns `null`.
52
+ * - Returns `null` when either instant is invalid, when `exit` is before `entry` (an inverted
53
+ * dwell is a data error, not a negative stay), or when no zone can be determined.
54
+ *
55
+ * @param entry ISO 8601 zoned datetime or instant string of the gate-in, arrival or admission
56
+ * @param exit ISO 8601 zoned datetime or instant string of the gate-out, departure or discharge
57
+ * @param targetZone IANA timeZone identifier or fixed offset the days are counted in; defaults to `entry`'s bracketed zone
58
+ * @returns exact duration, zone-local entry and exit, and the local calendar days touched, or null on invalid input
59
+ *
60
+ * @example dwellTime("2024-06-15T23:00:00-04:00[America/New_York]", "2024-06-16T01:00:00-04:00[America/New_York]") // { duration: "PT2H", enter: "2024-06-15T23:00:00-04:00[America/New_York]", exit: "2024-06-16T01:00:00-04:00[America/New_York]", calendarDays: 2 }
61
+ * @example dwellTime("2024-06-15T08:00:00Z", "2024-06-15T17:30:00Z", "Europe/Berlin") // { duration: "PT9H30M", enter: "2024-06-15T10:00:00+02:00[Europe/Berlin]", exit: "2024-06-15T19:30:00+02:00[Europe/Berlin]", calendarDays: 1 }
62
+ * @example dwellTime("2024-06-15T22:30:00Z", "2024-06-16T01:00:00Z", "Europe/London") // { duration: "PT2H30M", enter: "2024-06-15T23:30:00+01:00[Europe/London]", exit: "2024-06-16T02:00:00+01:00[Europe/London]", calendarDays: 2 }
63
+ * @example dwellTime("2024-06-15T22:30:00Z", "2024-06-16T01:00:00Z", "Europe/Amsterdam") // { duration: "PT2H30M", enter: "2024-06-16T00:30:00+02:00[Europe/Amsterdam]", exit: "2024-06-16T03:00:00+02:00[Europe/Amsterdam]", calendarDays: 1 } (the same two instants, one fewer day: the zone decides)
64
+ * @example dwellTime("2024-06-15T22:00:00Z", "2024-06-16T04:00:00Z", "America/New_York") // { duration: "PT6H", enter: "2024-06-15T18:00:00-04:00[America/New_York]", exit: "2024-06-16T00:00:00-04:00[America/New_York]", calendarDays: 1 } (an exit exactly at local midnight does not touch the new day)
65
+ * @example dwellTime("2024-03-10T05:00:00Z", "2024-03-10T12:00:00Z", "America/New_York") // { duration: "PT7H", enter: "2024-03-10T00:00:00-05:00[America/New_York]", exit: "2024-03-10T08:00:00-04:00[America/New_York]", calendarDays: 1 } (seven elapsed hours across the spring-forward, one local day)
66
+ * @example dwellTime("2011-12-30T08:00:00Z", "2011-12-30T11:00:00Z", "Pacific/Apia") // { duration: "PT3H", enter: "2011-12-29T22:00:00-10:00[Pacific/Apia]", exit: "2011-12-31T01:00:00+14:00[Pacific/Apia]", calendarDays: 2 } (29 and 31 December: Samoa deleted the 30th)
67
+ * @example dwellTime("2024-06-15T22:30:00Z", "2024-06-16T01:00:00Z", "+02:00") // { duration: "PT2H30M", enter: "2024-06-16T00:30:00+02:00[+02:00]", exit: "2024-06-16T03:00:00+02:00[+02:00]", calendarDays: 1 } (a fixed offset counts days with no DST)
68
+ * @example dwellTime("2024-06-15T22:30:00Z", "2024-06-16T01:00:00Z") // null (bare instants and no zone: the day count has no locality)
69
+ * @example dwellTime("2024-06-16T01:00:00Z", "2024-06-15T22:30:00Z", "Europe/London") // null (exit before entry)
70
+ * @example dwellTime("2024-06-15T22:30:00Z", "2024-06-16T01:00:00Z", "Europe/Londres") // null
71
+ */
72
+ export declare function dwellTime(entry: string, exit: string, targetZone?: string): Dwell | null;
@@ -0,0 +1,126 @@
1
+ import { Temporal } from "@js-temporal/polyfill";
2
+ import { countZonedLocalDates, zonedDateTimeFrom } from "../../internal/index.js";
3
+ import { isValidInstant } from "../../precision/validate/isValidInstant.js";
4
+ import { isValidTimeZone } from "../../zoned/validate/isValidTimeZone.js";
5
+ import { isValidZonedDateTime } from "../../zoned/validate/isValidZonedDateTime.js";
6
+ /**
7
+ * The zone a dwell is counted in: the caller's, or the one the entry string carries.
8
+ *
9
+ * An entry written with a bracketed IANA zone names a place; `Z` and offset-only strings name a
10
+ * moment and nothing else, so with no `targetZone` there is no locality to count days in.
11
+ */
12
+ function dwellZone(entry, targetZone) {
13
+ if (targetZone !== undefined) {
14
+ return isValidTimeZone(targetZone) ? targetZone : null;
15
+ }
16
+ if (!isValidZonedDateTime(entry)) {
17
+ return null;
18
+ }
19
+ try {
20
+ return zonedDateTimeFrom(entry).timeZoneId;
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ }
26
+ /**
27
+ * Measure how long something sat somewhere, and how many local calendar days it sat there.
28
+ *
29
+ * Terminals, depots, yards, stations, gates and berths all charge dwell, and none of them
30
+ * charge by elapsed hours. They count the local calendar days the thing was present, so a
31
+ * container that gates in at 23:00 and out at 01:00 has been there two hours and two days.
32
+ * `dwellTime` returns both, and is the one "local days crossed" primitive in the library: free
33
+ * time and demurrage (INT-12), laytime day counts (MAR-19) and hospital length of stay
34
+ * all take their day boundary from here rather than each deciding what a midnight is.
35
+ *
36
+ * - **`duration` is exact elapsed time.** A dwell spanning a DST transition reports the hours
37
+ * that actually elapsed; the local wall clocks at either end can be an hour apart from that.
38
+ * Hours is the largest unit, so the value never depends on a calendar.
39
+ * - **`calendarDays` is not `duration` rounded up.** It is the number of distinct local dates
40
+ * in `targetZone` that the half-open interval `[entry, exit)` touches: entry and exit on the
41
+ * same local date is `1`, across one local midnight is `2`, and so on. A dwell that ends
42
+ * exactly at a local midnight does not touch the new day, because the exit instant itself
43
+ * is excluded — the same half-open rule as `bucketRange` and CORE-6's intervals. A zero-length
44
+ * dwell touches one date. The count walks the zone's real transitions: between two of them
45
+ * the wall clock runs forward without a break, so each stretch touches every date from its
46
+ * first instant to its last, and the count is the size of the union of those dates. A 23- or
47
+ * 25-hour local day is one day, not 0.96 or 1.04 of one; a date the zone deleted is never
48
+ * touched (`Pacific/Apia` skipped 2011-12-30); and a date the clock falls back into is still
49
+ * one date (`America/Goose_Bay` fell back from 00:01 on 7 November 2010 into the 6th).
50
+ * - **The zone decides the count, so it has to be stated.** `targetZone` is used when given.
51
+ * Without it, the entry's bracketed IANA zone is used. As in `Temporal.Instant.from`, a
52
+ * bracketed zone is otherwise not read, so it is not validated either: the entry's is read
53
+ * (and must exist and agree with its offset) only when it supplies the dwell zone, because
54
+ * `targetZone` is omitted, and the exit's is never read. If the entry and exit are bare
55
+ * instants — `Z` or an offset — and no `targetZone` is given, the result is `null`: an
56
+ * offset is not a zone (CORE-4), and a day count in an unstated locality would be a guess.
57
+ * - `enter` and `exit` are the two instants rendered in the dwell zone, so a charging record
58
+ * shows the local times the terminal's own clock showed.
59
+ * - `targetZone` is whatever `isValidTimeZone` accepts, which is what Temporal accepts: an IANA
60
+ * identifier, or a fixed offset such as `"+02:00"`, as `etaAtZone` also takes. Days are then
61
+ * counted in that offset, which observes no DST, so pass the place's IANA zone when there is one.
62
+ * - GMT does not resolve a port, terminal or station code to a zone; the caller supplies the
63
+ * identifier.
64
+ * - Long dwells stay cheap: the walk visits zone transitions, not days. A dwell that crosses
65
+ * more than 10,000 transitions (about 5,000 years of twice-yearly DST) returns `null`.
66
+ * - Returns `null` when either instant is invalid, when `exit` is before `entry` (an inverted
67
+ * dwell is a data error, not a negative stay), or when no zone can be determined.
68
+ *
69
+ * @param entry ISO 8601 zoned datetime or instant string of the gate-in, arrival or admission
70
+ * @param exit ISO 8601 zoned datetime or instant string of the gate-out, departure or discharge
71
+ * @param targetZone IANA timeZone identifier or fixed offset the days are counted in; defaults to `entry`'s bracketed zone
72
+ * @returns exact duration, zone-local entry and exit, and the local calendar days touched, or null on invalid input
73
+ *
74
+ * @example dwellTime("2024-06-15T23:00:00-04:00[America/New_York]", "2024-06-16T01:00:00-04:00[America/New_York]") // { duration: "PT2H", enter: "2024-06-15T23:00:00-04:00[America/New_York]", exit: "2024-06-16T01:00:00-04:00[America/New_York]", calendarDays: 2 }
75
+ * @example dwellTime("2024-06-15T08:00:00Z", "2024-06-15T17:30:00Z", "Europe/Berlin") // { duration: "PT9H30M", enter: "2024-06-15T10:00:00+02:00[Europe/Berlin]", exit: "2024-06-15T19:30:00+02:00[Europe/Berlin]", calendarDays: 1 }
76
+ * @example dwellTime("2024-06-15T22:30:00Z", "2024-06-16T01:00:00Z", "Europe/London") // { duration: "PT2H30M", enter: "2024-06-15T23:30:00+01:00[Europe/London]", exit: "2024-06-16T02:00:00+01:00[Europe/London]", calendarDays: 2 }
77
+ * @example dwellTime("2024-06-15T22:30:00Z", "2024-06-16T01:00:00Z", "Europe/Amsterdam") // { duration: "PT2H30M", enter: "2024-06-16T00:30:00+02:00[Europe/Amsterdam]", exit: "2024-06-16T03:00:00+02:00[Europe/Amsterdam]", calendarDays: 1 } (the same two instants, one fewer day: the zone decides)
78
+ * @example dwellTime("2024-06-15T22:00:00Z", "2024-06-16T04:00:00Z", "America/New_York") // { duration: "PT6H", enter: "2024-06-15T18:00:00-04:00[America/New_York]", exit: "2024-06-16T00:00:00-04:00[America/New_York]", calendarDays: 1 } (an exit exactly at local midnight does not touch the new day)
79
+ * @example dwellTime("2024-03-10T05:00:00Z", "2024-03-10T12:00:00Z", "America/New_York") // { duration: "PT7H", enter: "2024-03-10T00:00:00-05:00[America/New_York]", exit: "2024-03-10T08:00:00-04:00[America/New_York]", calendarDays: 1 } (seven elapsed hours across the spring-forward, one local day)
80
+ * @example dwellTime("2011-12-30T08:00:00Z", "2011-12-30T11:00:00Z", "Pacific/Apia") // { duration: "PT3H", enter: "2011-12-29T22:00:00-10:00[Pacific/Apia]", exit: "2011-12-31T01:00:00+14:00[Pacific/Apia]", calendarDays: 2 } (29 and 31 December: Samoa deleted the 30th)
81
+ * @example dwellTime("2024-06-15T22:30:00Z", "2024-06-16T01:00:00Z", "+02:00") // { duration: "PT2H30M", enter: "2024-06-16T00:30:00+02:00[+02:00]", exit: "2024-06-16T03:00:00+02:00[+02:00]", calendarDays: 1 } (a fixed offset counts days with no DST)
82
+ * @example dwellTime("2024-06-15T22:30:00Z", "2024-06-16T01:00:00Z") // null (bare instants and no zone: the day count has no locality)
83
+ * @example dwellTime("2024-06-16T01:00:00Z", "2024-06-15T22:30:00Z", "Europe/London") // null (exit before entry)
84
+ * @example dwellTime("2024-06-15T22:30:00Z", "2024-06-16T01:00:00Z", "Europe/Londres") // null
85
+ */
86
+ export function dwellTime(entry, exit, targetZone) {
87
+ try {
88
+ if (!isValidInstant(entry) || !isValidInstant(exit)) {
89
+ return null;
90
+ }
91
+ const zone = dwellZone(entry, targetZone);
92
+ if (zone === null) {
93
+ return null;
94
+ }
95
+ const entryInstant = Temporal.Instant.from(entry);
96
+ const exitInstant = Temporal.Instant.from(exit);
97
+ if (Temporal.Instant.compare(entryInstant, exitInstant) > 0) {
98
+ return null;
99
+ }
100
+ const enter = entryInstant.toZonedDateTimeISO(zone);
101
+ const leave = exitInstant.toZonedDateTimeISO(zone);
102
+ // Half-open at the exit: the dwell's last instant is one nanosecond before it, so an exit
103
+ // exactly on a local day boundary does not touch the new day. A zero-length dwell still
104
+ // touches the one date it sits on.
105
+ const last = Temporal.Instant.compare(entryInstant, exitInstant) < 0
106
+ ? leave.subtract({ nanoseconds: 1 })
107
+ : leave;
108
+ const calendarDays = countZonedLocalDates(enter, last);
109
+ if (calendarDays === null) {
110
+ return null;
111
+ }
112
+ return {
113
+ duration: entryInstant
114
+ .until(exitInstant, { largestUnit: "hours" })
115
+ .toString(),
116
+ enter: enter.toString(),
117
+ exit: leave.toString(),
118
+ calendarDays,
119
+ };
120
+ }
121
+ catch {
122
+ // Never throws (Core Rule 3): a hostile
123
+ // argument is invalid input, not an exception.
124
+ return null;
125
+ }
126
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./dwellTime.js";
2
+ export * from "./transitTime.js";
@@ -0,0 +1,2 @@
1
+ export * from "./dwellTime.js";
2
+ export * from "./transitTime.js";
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Add a transit duration to a departure and return the arrival, in the departure's own zone.
3
+ *
4
+ * The first of the three shared transport operations: a leg is a departure plus how long it
5
+ * takes. Every mode publishes it that way, and every mode gets the same thing wrong — a leg
6
+ * that crosses a DST transition is a fixed number of *elapsed* hours, not a wall-clock
7
+ * interval, so the local arrival time reflects the shift.
8
+ *
9
+ * - **Transit time is exact time.** Hours, minutes, seconds and fractions are added as elapsed
10
+ * time, and a day component means 24 hours, exactly. That is TC39 Temporal's rule:
11
+ * `Duration.prototype.round` and `total` without a `relativeTo` treat a day as 24 hours and
12
+ * require a `relativeTo` for weeks, months and years. A 26-hour leg that starts the evening
13
+ * before a spring-forward arrives at a wall time one hour later than a wall-clock reading would
14
+ * suggest, because that is when the vessel, train or aircraft actually gets there.
15
+ * - **Calendar units are refused.** A `duration` with years, months or weeks returns `""`: no
16
+ * leg takes "a month" in a sense that time arithmetic can fix without a reference point, and
17
+ * Temporal will not add them to an instant either. Scheduled connections that are dated rather
18
+ * than timed belong to `scheduleDelivery`.
19
+ * - **The departure's zone is preserved.** A bracketed IANA zone (`…-05:00[America/New_York]`)
20
+ * stays that zone, so the arrival's offset is whatever is in force there at arrival. A `Z`
21
+ * instant returns a `Z` instant; an offset-only instant keeps its offset, written exactly as
22
+ * the departure wrote it, sub-minute offsets such as `+05:30:15` included (an offset is not a
23
+ * zone, so nothing else can be inferred from it — see `toOffsetInstant`). A bracketed zone
24
+ * that does not exist, or that contradicts its offset, is rejected as `isValidZonedDateTime`
25
+ * rejects it; it does not fall back to the offset.
26
+ * - RFC 9557 annotations are read as `Temporal.Instant.from` reads them: a bracket without `=`
27
+ * is a time zone, and a `key=value` bracket with an unknown key is ignored when elective and
28
+ * rejected when critical (`[!…]`). A calendar on a departure without a zone is accepted and
29
+ * changes nothing, since an instant has no calendar; a zoned departure's calendar must be ISO,
30
+ * as `isValidZonedDateTime` requires.
31
+ * - A negative duration is allowed and moves backwards; that is how a departure is recovered
32
+ * from an arrival.
33
+ * - Returns `""` when `departure` is not a zoned datetime or instant string, or `duration` is
34
+ * not an ISO 8601 duration.
35
+ *
36
+ * @param departure ISO 8601 zoned datetime (bracketed zone) or instant string (`Z` or offset)
37
+ * @param duration ISO 8601 duration of the leg; time units and 24-hour days only
38
+ * @returns the arrival in the departure's zone or offset form, or "" on invalid input
39
+ *
40
+ * @example transitTime("2024-06-15T10:00:00-04:00[America/New_York]", "PT2H30M") // "2024-06-15T12:30:00-04:00[America/New_York]"
41
+ * @example transitTime("2024-03-09T23:00:00-05:00[America/New_York]", "PT4H") // "2024-03-10T04:00:00-04:00[America/New_York]" (four elapsed hours across the spring-forward, so the wall clock reads 04:00, not 03:00)
42
+ * @example transitTime("2024-03-09T12:00:00-05:00[America/New_York]", "P1D") // "2024-03-10T13:00:00-04:00[America/New_York]" (a day is 24 elapsed hours, not "the same wall time tomorrow")
43
+ * @example transitTime("2024-06-15T10:00:00Z", "PT36H") // "2024-06-16T22:00:00Z"
44
+ * @example transitTime("2024-06-15T10:00:00+09:00", "PT1H") // "2024-06-15T11:00:00+09:00" (an offset stays an offset)
45
+ * @example transitTime("2024-06-15T10:00:00+05:30:15", "PT1H") // "2024-06-15T11:00:00+05:30:15" (a sub-minute offset is kept as written)
46
+ * @example transitTime("2024-06-15T10:00:00Z[foo=bar]", "PT1H") // "2024-06-15T11:00:00Z" (an elective unknown annotation is ignored)
47
+ * @example transitTime("2024-06-15T12:30:00-04:00[America/New_York]", "-PT2H30M") // "2024-06-15T10:00:00-04:00[America/New_York]"
48
+ * @example transitTime("2024-06-15T10:00:00-04:00[America/New_York]", "P1M") // "" (calendar units need a reference point)
49
+ * @example transitTime("2024-06-15T10:00:00-05:00[America/New_York]", "PT2H") // "" (New York is -04:00 in June: the offset contradicts the zone)
50
+ * @example transitTime("2024-06-15T10:00:00", "PT2H") // "" (no zone and no offset: not a moment)
51
+ * @example transitTime("2024-06-15T10:00:00Z", "2 hours") // ""
52
+ */
53
+ export declare function transitTime(departure: string, duration: string): string;
@@ -0,0 +1,118 @@
1
+ import { Temporal } from "@js-temporal/polyfill";
2
+ import { EXTENDED_UTC_OFFSET, TIME_ZONE_ANNOTATION, utcOffsetStringNanoseconds, wallClockAtOffset, zonedDateTimeFrom, } from "../../internal/index.js";
3
+ import { isValidDuration } from "../../duration/validate/isValidDuration.js";
4
+ import { isValidInstant } from "../../precision/validate/isValidInstant.js";
5
+ import { isValidZonedDateTime } from "../../zoned/validate/isValidZonedDateTime.js";
6
+ /**
7
+ * Trailing `Z` or extended offset of an instant string, before any annotation. The offset grammar
8
+ * is `isValidInstant`'s own (`EXTENDED_UTC_OFFSET`), so every offset it accepts, `,` fractions
9
+ * included, is captured as written.
10
+ */
11
+ const trailingOffset = new RegExp(`(Z|${EXTENDED_UTC_OFFSET})(?:\\[[^\\]]*\\])*$`);
12
+ /**
13
+ * A time-zone annotation anywhere in the string (`TIME_ZONE_ANNOTATION`). Every `key=value`
14
+ * bracket is a tagged annotation, which `isValidInstant` already decides.
15
+ */
16
+ const zoneAnnotation = new RegExp(TIME_ZONE_ANNOTATION);
17
+ /**
18
+ * Add a transit duration to a departure and return the arrival, in the departure's own zone.
19
+ *
20
+ * The first of the three shared transport operations: a leg is a departure plus how long it
21
+ * takes. Every mode publishes it that way, and every mode gets the same thing wrong — a leg
22
+ * that crosses a DST transition is a fixed number of *elapsed* hours, not a wall-clock
23
+ * interval, so the local arrival time reflects the shift.
24
+ *
25
+ * - **Transit time is exact time.** Hours, minutes, seconds and fractions are added as elapsed
26
+ * time, and a day component means 24 hours, exactly. That is TC39 Temporal's rule:
27
+ * `Duration.prototype.round` and `total` without a `relativeTo` treat a day as 24 hours and
28
+ * require a `relativeTo` for weeks, months and years. A 26-hour leg that starts the evening
29
+ * before a spring-forward arrives at a wall time one hour later than a wall-clock reading would
30
+ * suggest, because that is when the vessel, train or aircraft actually gets there.
31
+ * - **Calendar units are refused.** A `duration` with years, months or weeks returns `""`: no
32
+ * leg takes "a month" in a sense that time arithmetic can fix without a reference point, and
33
+ * Temporal will not add them to an instant either. Scheduled connections that are dated rather
34
+ * than timed belong to `scheduleDelivery`.
35
+ * - **The departure's zone is preserved.** A bracketed IANA zone (`…-05:00[America/New_York]`)
36
+ * stays that zone, so the arrival's offset is whatever is in force there at arrival. A `Z`
37
+ * instant returns a `Z` instant; an offset-only instant keeps its offset, written exactly as
38
+ * the departure wrote it, sub-minute offsets such as `+05:30:15` included (an offset is not a
39
+ * zone, so nothing else can be inferred from it — see `toOffsetInstant`). A bracketed zone
40
+ * that does not exist, or that contradicts its offset, is rejected as `isValidZonedDateTime`
41
+ * rejects it; it does not fall back to the offset.
42
+ * - RFC 9557 annotations are read as `Temporal.Instant.from` reads them: a bracket without `=`
43
+ * is a time zone, and a `key=value` bracket with an unknown key is ignored when elective and
44
+ * rejected when critical (`[!…]`). A calendar on a departure without a zone is accepted and
45
+ * changes nothing, since an instant has no calendar; a zoned departure's calendar must be ISO,
46
+ * as `isValidZonedDateTime` requires.
47
+ * - A negative duration is allowed and moves backwards; that is how a departure is recovered
48
+ * from an arrival.
49
+ * - Returns `""` when `departure` is not a zoned datetime or instant string, or `duration` is
50
+ * not an ISO 8601 duration.
51
+ *
52
+ * @param departure ISO 8601 zoned datetime (bracketed zone) or instant string (`Z` or offset)
53
+ * @param duration ISO 8601 duration of the leg; time units and 24-hour days only
54
+ * @returns the arrival in the departure's zone or offset form, or "" on invalid input
55
+ *
56
+ * @example transitTime("2024-06-15T10:00:00-04:00[America/New_York]", "PT2H30M") // "2024-06-15T12:30:00-04:00[America/New_York]"
57
+ * @example transitTime("2024-03-09T23:00:00-05:00[America/New_York]", "PT4H") // "2024-03-10T04:00:00-04:00[America/New_York]" (four elapsed hours across the spring-forward, so the wall clock reads 04:00, not 03:00)
58
+ * @example transitTime("2024-03-09T12:00:00-05:00[America/New_York]", "P1D") // "2024-03-10T13:00:00-04:00[America/New_York]" (a day is 24 elapsed hours, not "the same wall time tomorrow")
59
+ * @example transitTime("2024-06-15T10:00:00Z", "PT36H") // "2024-06-16T22:00:00Z"
60
+ * @example transitTime("2024-06-15T10:00:00+09:00", "PT1H") // "2024-06-15T11:00:00+09:00" (an offset stays an offset)
61
+ * @example transitTime("2024-06-15T10:00:00+05:30:15", "PT1H") // "2024-06-15T11:00:00+05:30:15" (a sub-minute offset is kept as written)
62
+ * @example transitTime("2024-06-15T10:00:00Z[foo=bar]", "PT1H") // "2024-06-15T11:00:00Z" (an elective unknown annotation is ignored)
63
+ * @example transitTime("2024-06-15T12:30:00-04:00[America/New_York]", "-PT2H30M") // "2024-06-15T10:00:00-04:00[America/New_York]"
64
+ * @example transitTime("2024-06-15T10:00:00-04:00[America/New_York]", "P1M") // "" (calendar units need a reference point)
65
+ * @example transitTime("2024-06-15T10:00:00-05:00[America/New_York]", "PT2H") // "" (New York is -04:00 in June: the offset contradicts the zone)
66
+ * @example transitTime("2024-06-15T10:00:00", "PT2H") // "" (no zone and no offset: not a moment)
67
+ * @example transitTime("2024-06-15T10:00:00Z", "2 hours") // ""
68
+ */
69
+ export function transitTime(departure, duration) {
70
+ // A non-string would reach the regex tests below, which coerce it: a throwing `toString`, a
71
+ // Symbol or a hostile Proxy would escape. Refuse it before any read.
72
+ if (typeof departure !== "string" || typeof duration !== "string") {
73
+ return "";
74
+ }
75
+ try {
76
+ return addLeg(departure, duration);
77
+ }
78
+ catch {
79
+ // Never throws (Core Rule 3): a hostile
80
+ // argument is invalid input, not an exception.
81
+ return "";
82
+ }
83
+ }
84
+ /** `transitTime` for two strings, without the guard. */
85
+ function addLeg(departure, duration) {
86
+ if (!isValidDuration(duration)) {
87
+ return "";
88
+ }
89
+ const zoned = isValidZonedDateTime(departure);
90
+ // A string that names a zone must name a real one that agrees with its offset; only a string
91
+ // that names none may fall back to the instant path.
92
+ if (!zoned &&
93
+ (zoneAnnotation.test(departure) || !isValidInstant(departure))) {
94
+ return "";
95
+ }
96
+ try {
97
+ const parsed = Temporal.Duration.from(duration);
98
+ if (parsed.years !== 0 || parsed.months !== 0 || parsed.weeks !== 0) {
99
+ return "";
100
+ }
101
+ // Days become 24 exact hours; every remaining unit is already exact time.
102
+ const exact = parsed.round({ largestUnit: "hours" });
103
+ if (zoned) {
104
+ return zonedDateTimeFrom(departure).add(exact).toString();
105
+ }
106
+ const arrival = Temporal.Instant.from(departure).add(exact);
107
+ const offset = trailingOffset.exec(departure)?.[1] ?? "Z";
108
+ if (offset === "Z") {
109
+ return arrival.toString();
110
+ }
111
+ // The arrival's wall clock in the departure's own offset, written with that offset's text.
112
+ const wall = wallClockAtOffset(arrival, utcOffsetStringNanoseconds(offset));
113
+ return `${wall.toString()}${offset}`;
114
+ }
115
+ catch {
116
+ return "";
117
+ }
118
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Render an arrival instant as local time in the zone where it will be read.
3
+ *
4
+ * Schedules are computed in UTC and displayed where the cargo lands. The zone is the caller's
5
+ * fact: GMT does not resolve a port, airport, terminal or station code to a timezone, and
6
+ * takes an IANA identifier instead (see the parked registries note in the epic's painpoints).
7
+ *
8
+ * - `arrivalUtc` is any instant `isValidInstant` accepts — `Z`, an offset, or an offset with a
9
+ * bracketed zone. Only the instant is read; a bracketed zone in the input is not the zone the
10
+ * result is rendered in, `targetZone` is.
11
+ * - **No disambiguation arises.** The input is a moment, and a moment has exactly one wall
12
+ * time in any zone, so the `disambiguation` vocabulary of `resolveLocal` does not apply here.
13
+ * On a fall-back night two moments an hour apart print the same wall time with different
14
+ * offsets, and the offset in the result is what tells them apart; on a spring-forward night
15
+ * the skipped hour simply never appears. That is also why the result carries its offset and
16
+ * zone rather than a bare wall time.
17
+ * - Returns a zoned ISO 8601 string, `Temporal.ZonedDateTime.prototype.toString()`'s form:
18
+ * wall time, offset, bracketed zone.
19
+ * - `targetZone` is whatever `isValidTimeZone` accepts, which is what Temporal accepts: an IANA
20
+ * identifier, or a fixed offset such as `"+09:00"`, which renders with the offset as its zone.
21
+ * A fixed offset is a legitimate rendering choice for a ship's log (MAR-18); it is not a place.
22
+ * - Returns `""` when the instant or the zone is invalid.
23
+ *
24
+ * @param arrivalUtc ISO 8601 instant string of the arrival
25
+ * @param targetZone IANA timeZone identifier of the place the arrival is read in
26
+ * @returns zoned ISO 8601 string of the local arrival, or "" on invalid input
27
+ *
28
+ * @example etaAtZone("2024-06-15T12:30:00Z", "Asia/Tokyo") // "2024-06-15T21:30:00+09:00[Asia/Tokyo]"
29
+ * @example etaAtZone("2024-06-15T12:30:00Z", "America/Los_Angeles") // "2024-06-15T05:30:00-07:00[America/Los_Angeles]"
30
+ * @example etaAtZone("2024-06-15T12:30:00+02:00", "Pacific/Apia") // "2024-06-15T23:30:00+13:00[Pacific/Apia]" (the offset fixes the instant; the zone renders it)
31
+ * @example etaAtZone("2024-11-03T05:30:00Z", "America/New_York") // "2024-11-03T01:30:00-04:00[America/New_York]" (the first 01:30 of the fall-back night)
32
+ * @example etaAtZone("2024-11-03T06:30:00Z", "America/New_York") // "2024-11-03T01:30:00-05:00[America/New_York]" (the second; the offset tells them apart)
33
+ * @example etaAtZone("2024-06-15T12:30:00", "Asia/Tokyo") // "" (no offset designator: not a moment)
34
+ * @example etaAtZone("2024-06-15T12:30:00Z", "Asia/Tokio") // ""
35
+ */
36
+ export declare function etaAtZone(arrivalUtc: string, targetZone: string): string;
@@ -0,0 +1,53 @@
1
+ import { Temporal } from "@js-temporal/polyfill";
2
+ import { isValidInstant } from "../../precision/validate/isValidInstant.js";
3
+ import { isValidTimeZone } from "../../zoned/validate/isValidTimeZone.js";
4
+ /**
5
+ * Render an arrival instant as local time in the zone where it will be read.
6
+ *
7
+ * Schedules are computed in UTC and displayed where the cargo lands. The zone is the caller's
8
+ * fact: GMT does not resolve a port, airport, terminal or station code to a timezone, and
9
+ * takes an IANA identifier instead (see the parked registries note in the epic's painpoints).
10
+ *
11
+ * - `arrivalUtc` is any instant `isValidInstant` accepts — `Z`, an offset, or an offset with a
12
+ * bracketed zone. Only the instant is read; a bracketed zone in the input is not the zone the
13
+ * result is rendered in, `targetZone` is.
14
+ * - **No disambiguation arises.** The input is a moment, and a moment has exactly one wall
15
+ * time in any zone, so the `disambiguation` vocabulary of `resolveLocal` does not apply here.
16
+ * On a fall-back night two moments an hour apart print the same wall time with different
17
+ * offsets, and the offset in the result is what tells them apart; on a spring-forward night
18
+ * the skipped hour simply never appears. That is also why the result carries its offset and
19
+ * zone rather than a bare wall time.
20
+ * - Returns a zoned ISO 8601 string, `Temporal.ZonedDateTime.prototype.toString()`'s form:
21
+ * wall time, offset, bracketed zone.
22
+ * - `targetZone` is whatever `isValidTimeZone` accepts, which is what Temporal accepts: an IANA
23
+ * identifier, or a fixed offset such as `"+09:00"`, which renders with the offset as its zone.
24
+ * A fixed offset is a legitimate rendering choice for a ship's log (MAR-18); it is not a place.
25
+ * - Returns `""` when the instant or the zone is invalid.
26
+ *
27
+ * @param arrivalUtc ISO 8601 instant string of the arrival
28
+ * @param targetZone IANA timeZone identifier of the place the arrival is read in
29
+ * @returns zoned ISO 8601 string of the local arrival, or "" on invalid input
30
+ *
31
+ * @example etaAtZone("2024-06-15T12:30:00Z", "Asia/Tokyo") // "2024-06-15T21:30:00+09:00[Asia/Tokyo]"
32
+ * @example etaAtZone("2024-06-15T12:30:00Z", "America/Los_Angeles") // "2024-06-15T05:30:00-07:00[America/Los_Angeles]"
33
+ * @example etaAtZone("2024-06-15T12:30:00+02:00", "Pacific/Apia") // "2024-06-15T23:30:00+13:00[Pacific/Apia]" (the offset fixes the instant; the zone renders it)
34
+ * @example etaAtZone("2024-11-03T05:30:00Z", "America/New_York") // "2024-11-03T01:30:00-04:00[America/New_York]" (the first 01:30 of the fall-back night)
35
+ * @example etaAtZone("2024-11-03T06:30:00Z", "America/New_York") // "2024-11-03T01:30:00-05:00[America/New_York]" (the second; the offset tells them apart)
36
+ * @example etaAtZone("2024-06-15T12:30:00", "Asia/Tokyo") // "" (no offset designator: not a moment)
37
+ * @example etaAtZone("2024-06-15T12:30:00Z", "Asia/Tokio") // ""
38
+ */
39
+ export function etaAtZone(arrivalUtc, targetZone) {
40
+ try {
41
+ if (!isValidInstant(arrivalUtc) || !isValidTimeZone(targetZone)) {
42
+ return "";
43
+ }
44
+ return Temporal.Instant.from(arrivalUtc)
45
+ .toZonedDateTimeISO(targetZone)
46
+ .toString();
47
+ }
48
+ catch {
49
+ // Never throws (Core Rule 3): a hostile
50
+ // argument is invalid input, not an exception.
51
+ return "";
52
+ }
53
+ }
@@ -0,0 +1 @@
1
+ export * from "./etaAtZone.js";
@@ -0,0 +1 @@
1
+ export * from "./etaAtZone.js";
@@ -0,0 +1,3 @@
1
+ export * from "@js-temporal/polyfill";
2
+ export * from "./calculate/index.js";
3
+ export * from "./convert/index.js";
@@ -0,0 +1,4 @@
1
+ // Re-export the Temporal polyfill (`Temporal`, `Intl`, `toTemporalInstant`) so every namespace subpath offers it
2
+ export * from "@js-temporal/polyfill";
3
+ export * from "./calculate/index.js";
4
+ export * from "./convert/index.js";
@@ -0,0 +1,34 @@
1
+ import type { BusinessCalendar } from "./business-calendar.js";
2
+ /**
3
+ * How days are counted: every local calendar day, or only the terminal's working days. Used for
4
+ * free time (`basis`) and, separately, for the days charged after it (`chargeBasis`), because
5
+ * published tariffs commonly count the two differently.
6
+ */
7
+ export type FreeTimeBasis = "calendar" | "working";
8
+ /**
9
+ * Whether the day of the triggering event is free day one (`"eventDay"`), or free time starts
10
+ * on the following counted day (`"nextDay"`). The two conventions differ by one full day of
11
+ * charges, so no function defaults it.
12
+ */
13
+ export type FreeTimeFirstDay = "eventDay" | "nextDay";
14
+ /**
15
+ * The tariff terms a free-time count needs. Free time is set by carrier, lane, trade and service
16
+ * contract, never by port, so every term is a parameter and no counting term has a default.
17
+ *
18
+ * - `basis`: `"calendar"` counts every local day, so a weekend burns two free days while the
19
+ * terminal is shut; `"working"` counts only the days `calendar` marks as working, and the
20
+ * clock does not run on weekends or holidays.
21
+ * - `timeZone`: the IANA identifier of the terminal, depot or yard whose local midnight ends a
22
+ * day. GMT does not resolve a port or terminal code to a zone. A fixed offset such as
23
+ * `"+02:00"` is accepted, as `isValidTimeZone` accepts it, but observes no DST.
24
+ * - `firstDay`: `"eventDay"` or `"nextDay"`, see `FreeTimeFirstDay`.
25
+ * - `calendar`: the terminal's `BusinessCalendar`, required when `basis` is `"working"` and not
26
+ * read when it is `"calendar"`. Its `timeZone` field is not read either, as the business-day
27
+ * functions never read it: `timeZone` above is the counting zone.
28
+ */
29
+ export type FreeTimeOptions = {
30
+ basis: FreeTimeBasis;
31
+ timeZone: string;
32
+ firstDay: FreeTimeFirstDay;
33
+ calendar?: BusinessCalendar;
34
+ };
File without changes