@northguild/gmt 1.16.0 → 1.18.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 +742 -21
- package/dist/calendar/calculate/bucketRange.d.ts +3 -3
- package/dist/calendar/calculate/bucketRange.js +6 -6
- package/dist/calendar/calculate/floorToZone.d.ts +5 -3
- package/dist/calendar/calculate/floorToZone.js +6 -5
- package/dist/calendar/calculate/getFiscalPeriod.d.ts +21 -8
- package/dist/calendar/calculate/getFiscalPeriod.js +9 -8
- package/dist/calendar/calculate/getIsoWeekDate.d.ts +10 -5
- package/dist/calendar/calculate/getIsoWeekDate.js +1 -5
- package/dist/calendar/calculate/getOrdinalDate.d.ts +10 -3
- package/dist/calendar/calculate/getOrdinalDate.js +2 -3
- package/dist/calendar/calculate/getQuarter.d.ts +21 -9
- package/dist/calendar/calculate/getQuarter.js +8 -12
- package/dist/calendar/hours/addOperatingTime.d.ts +53 -0
- package/dist/calendar/hours/addOperatingTime.js +59 -0
- package/dist/calendar/hours/index.d.ts +7 -0
- package/dist/calendar/hours/index.js +7 -0
- package/dist/calendar/hours/isOpenAt.d.ts +39 -0
- package/dist/calendar/hours/isOpenAt.js +44 -0
- package/dist/calendar/hours/nextCloseAt.d.ts +49 -0
- package/dist/calendar/hours/nextCloseAt.js +51 -0
- package/dist/calendar/hours/nextOpenAt.d.ts +45 -0
- package/dist/calendar/hours/nextOpenAt.js +47 -0
- package/dist/calendar/hours/operatingIntervals.d.ts +43 -0
- package/dist/calendar/hours/operatingIntervals.js +52 -0
- package/dist/calendar/hours/operatingTimeBetween.d.ts +40 -0
- package/dist/calendar/hours/operatingTimeBetween.js +49 -0
- package/dist/calendar/hours/recurringWindows.d.ts +61 -0
- package/dist/calendar/hours/recurringWindows.js +80 -0
- package/dist/calendar/index.d.ts +1 -0
- package/dist/calendar/index.js +1 -0
- package/dist/duration/calculate/durationAs.d.ts +13 -8
- package/dist/duration/calculate/durationAs.js +4 -8
- package/dist/duration/compare/compareDurations.d.ts +12 -6
- package/dist/duration/compare/compareDurations.js +3 -6
- package/dist/duration/format/formatDuration.d.ts +16 -4
- package/dist/duration/format/formatDuration.js +4 -5
- package/dist/duration/normalize/normalizeDuration.d.ts +39 -14
- package/dist/duration/normalize/normalizeDuration.js +6 -15
- package/dist/duration/parse/parseDuration.d.ts +21 -3
- package/dist/duration/parse/parseDuration.js +1 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/instant/convert/classifyLocal.d.ts +1 -1
- package/dist/instant/convert/classifyLocal.js +1 -1
- package/dist/instant/convert/fromOffsetInstant.d.ts +3 -1
- package/dist/instant/convert/fromOffsetInstant.js +6 -12
- package/dist/instant/convert/resolveLocal.d.ts +12 -7
- package/dist/instant/convert/resolveLocal.js +6 -10
- package/dist/instant/convert/toOffsetInstant.d.ts +37 -1
- package/dist/instant/convert/toOffsetInstant.js +12 -28
- package/dist/intermodal/calculate/billingTimeline.d.ts +114 -0
- package/dist/intermodal/calculate/billingTimeline.js +168 -0
- package/dist/intermodal/calculate/chargeableDays.d.ts +100 -0
- package/dist/intermodal/calculate/chargeableDays.js +96 -0
- package/dist/intermodal/calculate/demurrageClock.d.ts +102 -0
- package/dist/intermodal/calculate/demurrageClock.js +140 -0
- package/dist/intermodal/calculate/freeTimeExpiry.d.ts +67 -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/businessCalendar.d.ts +5 -0
- package/dist/internal/businessCalendar.js +5 -1
- package/dist/internal/cycleFieldValue.d.ts +6 -4
- package/dist/internal/cycleFieldValue.js +6 -4
- package/dist/internal/daylightTime.d.ts +33 -0
- package/dist/internal/daylightTime.js +164 -0
- package/dist/internal/durationUntilString.js +4 -3
- package/dist/internal/englishCalendarNames.js +1 -2
- package/dist/internal/epochNanoseconds.d.ts +2 -0
- package/dist/internal/epochNanoseconds.js +2 -0
- package/dist/internal/exactDuration.d.ts +37 -0
- package/dist/internal/exactDuration.js +64 -0
- package/dist/internal/exactMoment.d.ts +72 -0
- package/dist/internal/exactMoment.js +114 -0
- package/dist/internal/fiscalCalendar.d.ts +4 -3
- package/dist/internal/fiscalCalendar.js +4 -3
- package/dist/internal/formatCalendarDays.d.ts +2 -2
- package/dist/internal/formatRelativeDuration.d.ts +21 -1
- package/dist/internal/formatRelativeDuration.js +47 -7
- package/dist/internal/freeTimeLedger.d.ts +93 -0
- package/dist/internal/freeTimeLedger.js +262 -0
- package/dist/internal/index.d.ts +10 -3
- package/dist/internal/index.js +10 -3
- package/dist/internal/instantFormatOptions.js +2 -1
- package/dist/internal/instantNanoseconds.d.ts +75 -9
- package/dist/internal/instantNanoseconds.js +192 -11
- package/dist/internal/isoStringBody.d.ts +26 -0
- package/dist/internal/isoStringBody.js +22 -2
- package/dist/internal/joinDateTimeConnector.d.ts +1 -1
- package/dist/internal/monthGridWeekRow.d.ts +2 -1
- package/dist/internal/monthGridWeekRow.js +2 -1
- package/dist/internal/normalizeTimeZone.d.ts +2 -2
- package/dist/internal/normalizeTimeZone.js +2 -2
- package/dist/internal/operatingSchedule.d.ts +129 -0
- package/dist/internal/operatingSchedule.js +668 -0
- package/dist/internal/optionOrDefault.d.ts +18 -0
- package/dist/internal/optionOrDefault.js +20 -0
- package/dist/internal/patternToken.d.ts +2 -2
- package/dist/internal/patternToken.js +28 -13
- package/dist/internal/plainDateUntil.js +10 -6
- package/dist/internal/punctuality.d.ts +38 -0
- package/dist/internal/punctuality.js +59 -0
- package/dist/internal/readDateTimeFormatOptions.d.ts +30 -0
- package/dist/internal/readDateTimeFormatOptions.js +93 -0
- package/dist/internal/resolveDurationRelativeTo.d.ts +4 -0
- package/dist/internal/resolveDurationRelativeTo.js +11 -1
- package/dist/internal/resolveRelativeRounding.d.ts +17 -6
- package/dist/internal/resolveRelativeRounding.js +27 -7
- package/dist/internal/resolveRelativeUnit.d.ts +30 -0
- package/dist/internal/resolveRelativeUnit.js +40 -0
- package/dist/internal/splitStep.d.ts +24 -1
- package/dist/internal/splitStep.js +34 -2
- package/dist/internal/startOrEndOfZoned.js +2 -3
- package/dist/internal/temporalCompat/capabilities.d.ts +26 -0
- package/dist/internal/temporalCompat/capabilities.js +35 -3
- package/dist/internal/temporalCompat/fieldSearch.js +1 -1
- package/dist/internal/temporalCompat/index.d.ts +1 -1
- package/dist/internal/temporalCompat/index.js +1 -1
- package/dist/internal/temporalCompat/repros.d.ts +1 -1
- package/dist/internal/temporalCompat/repros.js +157 -0
- package/dist/internal/timestampEvents.d.ts +32 -0
- package/dist/internal/timestampEvents.js +76 -0
- package/dist/internal/utcZonedDateTime.d.ts +5 -2
- package/dist/internal/utcZonedDateTime.js +8 -5
- package/dist/internal/wallClockAtOffset.d.ts +22 -0
- package/dist/internal/wallClockAtOffset.js +26 -0
- package/dist/internal/zonedBucket.js +8 -6
- package/dist/internal/zonedLocalDates.js +1 -1
- package/dist/internal/zonedTransitionSearch.d.ts +27 -0
- package/dist/internal/zonedTransitionSearch.js +218 -0
- package/dist/internal/zonedWallClock.d.ts +21 -7
- package/dist/internal/zonedWallClock.js +87 -18
- package/dist/internal/zonedWallClockDifference.js +5 -4
- package/dist/internal/zonedWallClockOperations.d.ts +26 -13
- package/dist/internal/zonedWallClockOperations.js +87 -34
- package/dist/interval/validate/isValidInterval.d.ts +6 -3
- package/dist/interval/validate/isValidInterval.js +6 -3
- package/dist/plain/calculate/addDate.d.ts +11 -8
- package/dist/plain/calculate/addDate.js +5 -8
- package/dist/plain/calculate/addDateTime.d.ts +7 -4
- package/dist/plain/calculate/addDateTime.js +1 -4
- package/dist/plain/calculate/cycleDate.d.ts +16 -7
- package/dist/plain/calculate/cycleDate.js +3 -7
- package/dist/plain/calculate/cycleDateTime.d.ts +14 -4
- package/dist/plain/calculate/cycleDateTime.js +1 -4
- package/dist/plain/calculate/cycleTime.d.ts +10 -5
- package/dist/plain/calculate/cycleTime.js +3 -5
- package/dist/plain/calculate/diffDate.d.ts +1 -1
- package/dist/plain/calculate/diffDate.js +1 -1
- package/dist/plain/calculate/diffDateAsDuration.d.ts +1 -1
- package/dist/plain/calculate/diffDateAsDuration.js +1 -1
- package/dist/plain/calculate/diffDateTime.d.ts +1 -1
- package/dist/plain/calculate/diffDateTime.js +1 -1
- package/dist/plain/calculate/diffDateTimeAsDuration.d.ts +1 -1
- package/dist/plain/calculate/diffDateTimeAsDuration.js +1 -1
- package/dist/plain/calculate/diffTime.d.ts +1 -1
- package/dist/plain/calculate/diffTime.js +1 -1
- package/dist/plain/calculate/endOfDate.d.ts +8 -3
- package/dist/plain/calculate/endOfDate.js +1 -3
- package/dist/plain/calculate/endOfDateTime.d.ts +15 -5
- package/dist/plain/calculate/endOfDateTime.js +1 -5
- package/dist/plain/calculate/endOfTime.d.ts +8 -2
- package/dist/plain/calculate/endOfTime.js +1 -2
- package/dist/plain/calculate/getLocaleWeekYear.d.ts +16 -9
- package/dist/plain/calculate/getLocaleWeekYear.js +9 -9
- package/dist/plain/calculate/getWeekOfMonth.d.ts +7 -4
- package/dist/plain/calculate/getWeekOfMonth.js +7 -4
- package/dist/plain/calculate/getWeeksInLocaleWeekYear.d.ts +13 -6
- package/dist/plain/calculate/getWeeksInLocaleWeekYear.js +6 -6
- package/dist/plain/calculate/getWeeksInMonth.d.ts +5 -2
- package/dist/plain/calculate/getWeeksInMonth.js +5 -2
- package/dist/plain/calculate/nextWeekday.d.ts +7 -3
- package/dist/plain/calculate/nextWeekday.js +1 -3
- package/dist/plain/calculate/previousWeekday.d.ts +7 -3
- package/dist/plain/calculate/previousWeekday.js +1 -3
- package/dist/plain/calculate/roundDate.d.ts +20 -7
- package/dist/plain/calculate/roundDate.js +3 -10
- package/dist/plain/calculate/roundDateTime.d.ts +22 -8
- package/dist/plain/calculate/roundDateTime.js +3 -11
- package/dist/plain/calculate/roundTime.d.ts +20 -4
- package/dist/plain/calculate/roundTime.js +3 -7
- package/dist/plain/calculate/setDate.d.ts +7 -5
- package/dist/plain/calculate/setDate.js +1 -5
- package/dist/plain/calculate/setDateTime.d.ts +8 -5
- package/dist/plain/calculate/setDateTime.js +1 -5
- package/dist/plain/calculate/setTime.d.ts +10 -6
- package/dist/plain/calculate/setTime.js +4 -6
- package/dist/plain/calculate/startOfDate.d.ts +8 -2
- package/dist/plain/calculate/startOfDate.js +1 -2
- package/dist/plain/calculate/startOfDateTime.d.ts +15 -2
- package/dist/plain/calculate/startOfDateTime.js +1 -2
- package/dist/plain/calculate/startOfTime.d.ts +8 -1
- package/dist/plain/calculate/startOfTime.js +1 -1
- package/dist/plain/calculate/subtractDate.d.ts +7 -4
- package/dist/plain/calculate/subtractDate.js +1 -4
- package/dist/plain/calculate/subtractDateTime.d.ts +7 -4
- package/dist/plain/calculate/subtractDateTime.js +1 -4
- package/dist/plain/compare/areDateTimesEqualBy.d.ts +13 -5
- package/dist/plain/compare/areDateTimesEqualBy.js +6 -5
- package/dist/plain/compare/areDatesEqualBy.d.ts +15 -6
- package/dist/plain/compare/areDatesEqualBy.js +8 -6
- package/dist/plain/compare/isBetweenDate.d.ts +13 -2
- package/dist/plain/compare/isBetweenDate.js +4 -4
- package/dist/plain/compare/isBetweenDateTime.d.ts +13 -2
- package/dist/plain/compare/isBetweenDateTime.js +4 -4
- package/dist/plain/compare/isBetweenTime.d.ts +13 -2
- package/dist/plain/compare/isBetweenTime.js +4 -4
- package/dist/plain/compare/isRelativeDay.d.ts +1 -1
- package/dist/plain/compare/isRelativeDay.js +1 -1
- package/dist/plain/compare/isThisUnit.d.ts +1 -1
- package/dist/plain/compare/isThisUnit.js +1 -1
- package/dist/plain/format/formatCalendar.d.ts +21 -14
- package/dist/plain/format/formatCalendar.js +21 -16
- package/dist/plain/format/formatDate.js +5 -3
- package/dist/plain/format/formatDateRange.js +4 -2
- package/dist/plain/format/formatDateTime.js +4 -2
- package/dist/plain/format/formatDateTimeRange.js +4 -2
- package/dist/plain/format/formatDateTimeToParts.d.ts +15 -4
- package/dist/plain/format/formatDateTimeToParts.js +10 -6
- package/dist/plain/format/formatDateToParts.d.ts +13 -2
- package/dist/plain/format/formatDateToParts.js +8 -4
- package/dist/plain/format/formatRelativeDate.d.ts +16 -3
- package/dist/plain/format/formatRelativeDate.js +26 -15
- package/dist/plain/format/formatRelativeDateTime.d.ts +16 -3
- package/dist/plain/format/formatRelativeDateTime.js +8 -5
- package/dist/plain/format/formatRelativeTime.d.ts +16 -3
- package/dist/plain/format/formatRelativeTime.js +21 -13
- package/dist/plain/format/formatTime.js +4 -2
- package/dist/plain/interval/intervalDifferenceDate.d.ts +9 -0
- package/dist/plain/interval/intervalDifferenceDateTime.d.ts +9 -0
- package/dist/plain/interval/intervalDifferenceTime.d.ts +9 -0
- package/dist/plain/interval/intervalDivideEquallyDate.d.ts +19 -4
- package/dist/plain/interval/intervalDivideEquallyDate.js +3 -4
- package/dist/plain/interval/intervalDivideEquallyDateTime.d.ts +19 -4
- package/dist/plain/interval/intervalDivideEquallyDateTime.js +3 -4
- package/dist/plain/interval/intervalDivideEquallyTime.d.ts +19 -4
- package/dist/plain/interval/intervalDivideEquallyTime.js +3 -4
- package/dist/plain/interval/intervalFromDurationDate.d.ts +16 -3
- package/dist/plain/interval/intervalFromDurationDate.js +1 -3
- package/dist/plain/interval/intervalFromDurationDateTime.d.ts +16 -3
- package/dist/plain/interval/intervalFromDurationDateTime.js +1 -3
- package/dist/plain/interval/intervalFromDurationTime.d.ts +9 -0
- package/dist/plain/interval/intervalIntersectionDate.d.ts +9 -0
- package/dist/plain/interval/intervalIntersectionDateTime.d.ts +9 -0
- package/dist/plain/interval/intervalIntersectionTime.d.ts +9 -0
- package/dist/plain/interval/intervalSplitAtDate.d.ts +11 -0
- package/dist/plain/interval/intervalSplitAtDate.js +2 -0
- package/dist/plain/interval/intervalSplitAtDateTime.d.ts +11 -0
- package/dist/plain/interval/intervalSplitAtDateTime.js +2 -0
- package/dist/plain/interval/intervalSplitAtTime.d.ts +11 -0
- package/dist/plain/interval/intervalSplitAtTime.js +2 -0
- package/dist/plain/interval/intervalUnionDate.d.ts +9 -0
- package/dist/plain/interval/intervalUnionDateTime.d.ts +9 -0
- package/dist/plain/interval/intervalUnionTime.d.ts +9 -0
- package/dist/plain/interval/intervalXorAllDate.d.ts +9 -0
- package/dist/plain/interval/intervalXorAllDateTime.d.ts +9 -0
- package/dist/plain/interval/intervalXorAllTime.d.ts +9 -0
- package/dist/plain/interval/intervalXorDate.d.ts +9 -0
- package/dist/plain/interval/intervalXorDateTime.d.ts +9 -0
- package/dist/plain/interval/intervalXorTime.d.ts +9 -0
- package/dist/plain/interval/mergeIntervalsDate.d.ts +9 -0
- package/dist/plain/interval/mergeIntervalsDateTime.d.ts +9 -0
- package/dist/plain/interval/mergeIntervalsTime.d.ts +9 -0
- package/dist/plain/interval/splitIntervalByUnitDate.d.ts +23 -7
- package/dist/plain/interval/splitIntervalByUnitDate.js +7 -7
- package/dist/plain/interval/splitIntervalByUnitDateTime.d.ts +22 -6
- package/dist/plain/interval/splitIntervalByUnitDateTime.js +6 -6
- package/dist/plain/interval/splitIntervalByUnitTime.d.ts +17 -4
- package/dist/plain/interval/splitIntervalByUnitTime.js +1 -4
- package/dist/plain/map/mapDatesInRange.d.ts +8 -4
- package/dist/plain/map/mapDatesInRange.js +1 -4
- package/dist/plain/parse/parseDateTimeWithPattern.d.ts +10 -7
- package/dist/plain/parse/parseDateTimeWithPattern.js +10 -7
- package/dist/plain/parse/parseDateWithPattern.d.ts +10 -7
- package/dist/plain/parse/parseDateWithPattern.js +10 -7
- package/dist/plain/parse/parseTimeWithPattern.d.ts +10 -7
- package/dist/plain/parse/parseTimeWithPattern.js +10 -7
- package/dist/plain/parse/parseUnitFromDate.d.ts +11 -3
- package/dist/plain/parse/parseUnitFromDate.js +3 -3
- package/dist/plain/parse/parseUnitFromDateTime.d.ts +11 -3
- package/dist/plain/parse/parseUnitFromDateTime.js +3 -3
- package/dist/plain/parse/parseWeekFromDate.d.ts +10 -6
- package/dist/plain/parse/parseWeekFromDate.js +2 -6
- package/dist/plain/parse/parseWeekFromDateTime.d.ts +10 -6
- package/dist/plain/parse/parseWeekFromDateTime.js +2 -6
- package/dist/plain/validate/isValidDateRange.d.ts +19 -4
- package/dist/plain/validate/isValidDateRange.js +1 -4
- package/dist/plain/validate/isValidDateTimeRange.d.ts +20 -5
- package/dist/plain/validate/isValidDateTimeRange.js +2 -5
- package/dist/plain/validate/isValidTimeRange.d.ts +20 -5
- package/dist/plain/validate/isValidTimeRange.js +2 -5
- package/dist/precision/convert/fromExcelSerial.d.ts +10 -4
- package/dist/precision/convert/fromExcelSerial.js +4 -5
- package/dist/precision/convert/fromNanoseconds.d.ts +1 -1
- package/dist/precision/convert/fromNanoseconds.js +1 -1
- package/dist/precision/convert/toExcelSerial.d.ts +9 -3
- package/dist/precision/convert/toExcelSerial.js +3 -4
- package/dist/precision/convert/toNanoseconds.d.ts +11 -6
- package/dist/precision/convert/toNanoseconds.js +11 -6
- package/dist/precision/validate/isValidInstant.d.ts +12 -3
- package/dist/precision/validate/isValidInstant.js +12 -3
- package/dist/span/calculate/spanMs.d.ts +10 -4
- package/dist/span/calculate/spanMs.js +10 -4
- package/dist/span/calculate/spanNs.d.ts +10 -4
- package/dist/span/calculate/spanNs.js +10 -4
- package/dist/span/validate/isValidSpan.d.ts +8 -3
- package/dist/span/validate/isValidSpan.js +8 -3
- package/dist/transport/calculate/bestAvailable.d.ts +47 -0
- package/dist/transport/calculate/bestAvailable.js +76 -0
- package/dist/transport/calculate/crossingTime.d.ts +58 -0
- package/dist/transport/calculate/crossingTime.js +76 -0
- package/dist/transport/calculate/cutoffAt.d.ts +90 -0
- package/dist/transport/calculate/cutoffAt.js +209 -0
- package/dist/transport/calculate/cutoffSchedule.d.ts +70 -0
- package/dist/transport/calculate/cutoffSchedule.js +71 -0
- package/dist/transport/calculate/dwellTime.d.ts +81 -0
- package/dist/transport/calculate/dwellTime.js +130 -0
- package/dist/transport/calculate/estimateDrift.d.ts +65 -0
- package/dist/transport/calculate/estimateDrift.js +75 -0
- package/dist/transport/calculate/index.d.ts +10 -0
- package/dist/transport/calculate/index.js +10 -0
- package/dist/transport/calculate/nextDeparture.d.ts +78 -0
- package/dist/transport/calculate/nextDeparture.js +131 -0
- package/dist/transport/calculate/punctualityRate.d.ts +42 -0
- package/dist/transport/calculate/punctualityRate.js +59 -0
- package/dist/transport/calculate/scheduleDelivery.d.ts +150 -0
- package/dist/transport/calculate/scheduleDelivery.js +300 -0
- package/dist/transport/calculate/transitTime.d.ts +71 -0
- package/dist/transport/calculate/transitTime.js +101 -0
- package/dist/transport/compare/classifyPunctuality.d.ts +46 -0
- package/dist/transport/compare/classifyPunctuality.js +64 -0
- package/dist/transport/compare/index.d.ts +4 -0
- package/dist/transport/compare/index.js +4 -0
- package/dist/transport/compare/isPastCutoff.d.ts +33 -0
- package/dist/transport/compare/isPastCutoff.js +48 -0
- package/dist/transport/compare/scheduleDeviation.d.ts +37 -0
- package/dist/transport/compare/scheduleDeviation.js +53 -0
- package/dist/transport/compare/timeToCutoff.d.ts +31 -0
- package/dist/transport/compare/timeToCutoff.js +47 -0
- package/dist/transport/convert/etaAtZone.d.ts +43 -0
- package/dist/transport/convert/etaAtZone.js +58 -0
- package/dist/transport/convert/index.d.ts +1 -0
- package/dist/transport/convert/index.js +1 -0
- package/dist/transport/index.d.ts +4 -0
- package/dist/transport/index.js +5 -0
- package/dist/types/business-calendar.d.ts +19 -14
- package/dist/types/calendar-options.d.ts +15 -3
- package/dist/types/date-cycle-field.d.ts +5 -0
- package/dist/types/date-duration-unit.d.ts +4 -0
- package/dist/types/date-time-cycle-field.d.ts +4 -0
- package/dist/types/date-time-duration-units.d.ts +4 -0
- package/dist/types/date-time-unit.d.ts +4 -0
- package/dist/types/date-unit.d.ts +4 -0
- package/dist/types/disambiguation.d.ts +8 -0
- package/dist/types/duration-relative-to.d.ts +8 -0
- package/dist/types/duration-string-options.d.ts +28 -0
- package/dist/types/fiscal-calendar.d.ts +1 -1
- package/dist/types/fractional-digit.d.ts +5 -0
- package/dist/types/free-time-options.d.ts +44 -0
- package/dist/types/index.d.ts +3 -1
- package/dist/types/index.js +3 -1
- package/dist/types/interval.d.ts +8 -1
- package/dist/types/offset.d.ts +9 -0
- package/dist/types/operating-schedule.d.ts +71 -0
- package/dist/types/operating-schedule.js +0 -0
- package/dist/types/overflow.d.ts +6 -0
- package/dist/types/relative-rounding-method.d.ts +6 -0
- package/dist/types/relative-time-options.d.ts +33 -5
- package/dist/types/rounding-options.d.ts +27 -0
- package/dist/types/time-cycle-field.d.ts +4 -0
- package/dist/types/time-duration-unit.d.ts +4 -0
- package/dist/types/time-unit.d.ts +4 -0
- package/dist/types/transport-timestamps.d.ts +54 -0
- package/dist/types/transport-timestamps.js +0 -0
- package/dist/unix/calculate/addUnix.d.ts +19 -5
- package/dist/unix/calculate/addUnix.js +1 -5
- package/dist/unix/calculate/diffUnix.d.ts +13 -3
- package/dist/unix/calculate/diffUnix.js +1 -3
- package/dist/unix/calculate/diffUnixAsDuration.d.ts +13 -3
- package/dist/unix/calculate/diffUnixAsDuration.js +1 -3
- package/dist/unix/calculate/endOfQuarterForUnix.d.ts +13 -3
- package/dist/unix/calculate/endOfQuarterForUnix.js +1 -3
- package/dist/unix/calculate/endOfUnix.d.ts +20 -4
- package/dist/unix/calculate/endOfUnix.js +1 -4
- package/dist/unix/calculate/isBetweenUnix.d.ts +26 -4
- package/dist/unix/calculate/isBetweenUnix.js +4 -6
- package/dist/unix/calculate/roundUnix.d.ts +31 -3
- package/dist/unix/calculate/roundUnix.js +3 -6
- package/dist/unix/calculate/setUnix.d.ts +35 -9
- package/dist/unix/calculate/setUnix.js +4 -13
- package/dist/unix/calculate/startOfQuarterForUnix.d.ts +13 -3
- package/dist/unix/calculate/startOfQuarterForUnix.js +1 -3
- package/dist/unix/calculate/startOfUnix.d.ts +20 -4
- package/dist/unix/calculate/startOfUnix.js +1 -4
- package/dist/unix/calculate/subtractUnix.d.ts +19 -5
- package/dist/unix/calculate/subtractUnix.js +1 -5
- package/dist/unix/compare/areUnixEqual.d.ts +7 -1
- package/dist/unix/compare/areUnixEqual.js +1 -1
- package/dist/unix/compare/areUnixEqualBy.d.ts +24 -7
- package/dist/unix/compare/areUnixEqualBy.js +13 -9
- package/dist/unix/compare/isAfterUnix.d.ts +7 -1
- package/dist/unix/compare/isAfterUnix.js +1 -1
- package/dist/unix/compare/isBeforeUnix.d.ts +7 -1
- package/dist/unix/compare/isBeforeUnix.js +1 -1
- package/dist/unix/convert/convertUnixToPlainDate.d.ts +14 -4
- package/dist/unix/convert/convertUnixToPlainDate.js +2 -4
- package/dist/unix/convert/convertUnixToPlainDateTime.d.ts +14 -4
- package/dist/unix/convert/convertUnixToPlainDateTime.js +2 -4
- package/dist/unix/convert/convertUnixToPlainTime.d.ts +14 -4
- package/dist/unix/convert/convertUnixToPlainTime.js +2 -4
- package/dist/unix/convert/convertUnixToUtc.d.ts +9 -4
- package/dist/unix/convert/convertUnixToUtc.js +3 -4
- package/dist/unix/convert/convertUnixToZoned.d.ts +10 -5
- package/dist/unix/convert/convertUnixToZoned.js +4 -5
- package/dist/unix/format/formatCalendarUnix.d.ts +27 -19
- package/dist/unix/format/formatCalendarUnix.js +4 -5
- package/dist/unix/format/formatRelativeUnix.d.ts +54 -5
- package/dist/unix/format/formatRelativeUnix.js +3 -5
- package/dist/unix/format/formatUnix.d.ts +24 -6
- package/dist/unix/format/formatUnix.js +8 -7
- package/dist/unix/get/getUnixNow.d.ts +8 -3
- package/dist/unix/get/getUnixNow.js +1 -3
- package/dist/unix/interval/intervalCountUnix.d.ts +14 -5
- package/dist/unix/interval/intervalCountUnix.js +2 -5
- package/dist/unix/interval/intervalDifferenceUnix.d.ts +6 -0
- package/dist/unix/interval/intervalDivideEquallyUnix.d.ts +18 -4
- package/dist/unix/interval/intervalDivideEquallyUnix.js +5 -4
- package/dist/unix/interval/intervalFromDurationUnix.d.ts +33 -5
- package/dist/unix/interval/intervalFromDurationUnix.js +6 -5
- package/dist/unix/interval/intervalIntersectionUnix.d.ts +6 -0
- package/dist/unix/interval/intervalLengthUnix.d.ts +16 -6
- package/dist/unix/interval/intervalLengthUnix.js +4 -6
- package/dist/unix/interval/intervalOverlappingDaysUnix.d.ts +13 -3
- package/dist/unix/interval/intervalOverlappingDaysUnix.js +1 -3
- package/dist/unix/interval/intervalSplitAtUnix.d.ts +8 -0
- package/dist/unix/interval/intervalSplitAtUnix.js +2 -0
- package/dist/unix/interval/intervalUnionUnix.d.ts +6 -0
- package/dist/unix/interval/intervalXorAllUnix.d.ts +6 -0
- package/dist/unix/interval/intervalXorUnix.d.ts +6 -0
- package/dist/unix/interval/mergeIntervalsUnix.d.ts +6 -0
- package/dist/unix/interval/splitIntervalByUnitUnix.d.ts +42 -11
- package/dist/unix/interval/splitIntervalByUnitUnix.js +21 -16
- package/dist/unix/parse/parseDateFromUnix.d.ts +13 -2
- package/dist/unix/parse/parseDateFromUnix.js +1 -2
- package/dist/unix/parse/parseDayFromUnix.d.ts +14 -2
- package/dist/unix/parse/parseDayFromUnix.js +2 -2
- package/dist/unix/parse/parseDayOfWeekFromUnix.d.ts +13 -1
- package/dist/unix/parse/parseDayOfWeekFromUnix.js +1 -1
- package/dist/unix/parse/parseHourFromUnix.d.ts +14 -2
- package/dist/unix/parse/parseHourFromUnix.js +2 -2
- package/dist/unix/parse/parseMicrosecondFromUnix.d.ts +14 -2
- package/dist/unix/parse/parseMicrosecondFromUnix.js +2 -2
- package/dist/unix/parse/parseMillisecondFromUnix.d.ts +14 -2
- package/dist/unix/parse/parseMillisecondFromUnix.js +2 -2
- package/dist/unix/parse/parseMinuteFromUnix.d.ts +14 -2
- package/dist/unix/parse/parseMinuteFromUnix.js +2 -2
- package/dist/unix/parse/parseMonthFromUnix.d.ts +14 -2
- package/dist/unix/parse/parseMonthFromUnix.js +2 -2
- package/dist/unix/parse/parseNanosecondFromUnix.d.ts +14 -2
- package/dist/unix/parse/parseNanosecondFromUnix.js +2 -2
- package/dist/unix/parse/parseSecondFromUnix.d.ts +14 -2
- package/dist/unix/parse/parseSecondFromUnix.js +2 -2
- package/dist/unix/parse/parseTimeFromUnix.d.ts +13 -2
- package/dist/unix/parse/parseTimeFromUnix.js +1 -2
- package/dist/unix/parse/parseUnitFromUnix.d.ts +22 -3
- package/dist/unix/parse/parseUnitFromUnix.js +2 -3
- package/dist/unix/parse/parseWeekFromUnix.d.ts +23 -7
- package/dist/unix/parse/parseWeekFromUnix.js +3 -7
- package/dist/unix/parse/parseYearFromUnix.d.ts +14 -2
- package/dist/unix/parse/parseYearFromUnix.js +2 -2
- package/dist/unix/validate/isValidUnixRange.d.ts +20 -4
- package/dist/unix/validate/isValidUnixRange.js +1 -4
- package/dist/utc/calculate/addUtc.d.ts +7 -4
- package/dist/utc/calculate/addUtc.js +1 -4
- package/dist/utc/calculate/diffUtc.d.ts +1 -1
- package/dist/utc/calculate/diffUtc.js +1 -1
- package/dist/utc/calculate/diffUtcAsDuration.d.ts +1 -1
- package/dist/utc/calculate/diffUtcAsDuration.js +1 -1
- package/dist/utc/calculate/endOfUtc.d.ts +16 -3
- package/dist/utc/calculate/endOfUtc.js +2 -3
- package/dist/utc/calculate/isBetweenUtc.d.ts +13 -2
- package/dist/utc/calculate/isBetweenUtc.js +4 -4
- package/dist/utc/calculate/roundUtc.d.ts +26 -9
- package/dist/utc/calculate/roundUtc.js +3 -12
- package/dist/utc/calculate/setUtc.d.ts +7 -4
- package/dist/utc/calculate/setUtc.js +1 -4
- package/dist/utc/calculate/startOfUtc.d.ts +15 -2
- package/dist/utc/calculate/startOfUtc.js +1 -2
- package/dist/utc/calculate/subtractUtc.d.ts +7 -4
- package/dist/utc/calculate/subtractUtc.js +1 -4
- package/dist/utc/compare/areUtcEqualBy.d.ts +9 -3
- package/dist/utc/compare/areUtcEqualBy.js +2 -3
- package/dist/utc/convert/convertUtcToPlainDate.d.ts +8 -3
- package/dist/utc/convert/convertUtcToPlainDate.js +5 -6
- package/dist/utc/convert/convertUtcToPlainDateTime.d.ts +8 -3
- package/dist/utc/convert/convertUtcToPlainDateTime.js +5 -6
- package/dist/utc/convert/convertUtcToPlainTime.d.ts +8 -3
- package/dist/utc/convert/convertUtcToPlainTime.js +5 -6
- package/dist/utc/convert/convertUtcToUnix.d.ts +9 -4
- package/dist/utc/convert/convertUtcToUnix.js +3 -4
- package/dist/utc/convert/convertUtcToZoned.d.ts +1 -1
- package/dist/utc/convert/convertUtcToZoned.js +1 -1
- package/dist/utc/format/formatCalendarUtc.d.ts +19 -8
- package/dist/utc/format/formatCalendarUtc.js +8 -6
- package/dist/utc/format/formatHttp.d.ts +1 -1
- package/dist/utc/format/formatHttp.js +1 -1
- package/dist/utc/format/formatRelativeUtc.d.ts +30 -10
- package/dist/utc/format/formatRelativeUtc.js +9 -11
- package/dist/utc/format/formatUtc.d.ts +17 -15
- package/dist/utc/format/formatUtc.js +8 -8
- package/dist/utc/interval/intervalDifferenceUtc.d.ts +6 -0
- package/dist/utc/interval/intervalDivideEquallyUtc.d.ts +18 -4
- package/dist/utc/interval/intervalDivideEquallyUtc.js +5 -4
- package/dist/utc/interval/intervalFromDurationUtc.d.ts +13 -3
- package/dist/utc/interval/intervalFromDurationUtc.js +1 -3
- package/dist/utc/interval/intervalIntersectionUtc.d.ts +6 -0
- package/dist/utc/interval/intervalSplitAtUtc.d.ts +8 -0
- package/dist/utc/interval/intervalSplitAtUtc.js +2 -0
- package/dist/utc/interval/intervalUnionUtc.d.ts +6 -0
- package/dist/utc/interval/intervalXorAllUtc.d.ts +6 -0
- package/dist/utc/interval/intervalXorUtc.d.ts +6 -0
- package/dist/utc/interval/mergeIntervalsUtc.d.ts +6 -0
- package/dist/utc/interval/splitIntervalByUnitUtc.d.ts +19 -6
- package/dist/utc/interval/splitIntervalByUnitUtc.js +6 -6
- package/dist/utc/parse/parseDateFromUtc.d.ts +7 -3
- package/dist/utc/parse/parseDateFromUtc.js +1 -3
- package/dist/utc/parse/parseDayFromUtc.d.ts +7 -3
- package/dist/utc/parse/parseDayFromUtc.js +1 -3
- package/dist/utc/parse/parseDayOfWeekFromUtc.d.ts +7 -3
- package/dist/utc/parse/parseDayOfWeekFromUtc.js +4 -6
- package/dist/utc/parse/parseHourFromUtc.d.ts +7 -3
- package/dist/utc/parse/parseHourFromUtc.js +1 -3
- package/dist/utc/parse/parseHttp.d.ts +1 -1
- package/dist/utc/parse/parseHttp.js +1 -1
- package/dist/utc/parse/parseMicrosecondFromUtc.d.ts +7 -3
- package/dist/utc/parse/parseMicrosecondFromUtc.js +1 -3
- package/dist/utc/parse/parseMillisecondFromUtc.d.ts +7 -3
- package/dist/utc/parse/parseMillisecondFromUtc.js +1 -3
- package/dist/utc/parse/parseMinuteFromUtc.d.ts +7 -3
- package/dist/utc/parse/parseMinuteFromUtc.js +1 -3
- package/dist/utc/parse/parseMonthFromUtc.d.ts +7 -3
- package/dist/utc/parse/parseMonthFromUtc.js +1 -3
- package/dist/utc/parse/parseNanosecondFromUtc.d.ts +7 -3
- package/dist/utc/parse/parseNanosecondFromUtc.js +1 -3
- package/dist/utc/parse/parseSecondFromUtc.d.ts +7 -3
- package/dist/utc/parse/parseSecondFromUtc.js +1 -3
- package/dist/utc/parse/parseTimeFromUtc.d.ts +8 -3
- package/dist/utc/parse/parseTimeFromUtc.js +5 -6
- package/dist/utc/parse/parseUnitFromUtc.d.ts +17 -5
- package/dist/utc/parse/parseUnitFromUtc.js +6 -8
- package/dist/utc/parse/parseWeekFromUtc.d.ts +18 -10
- package/dist/utc/parse/parseWeekFromUtc.js +7 -13
- package/dist/utc/parse/parseYearFromUtc.d.ts +7 -3
- package/dist/utc/parse/parseYearFromUtc.js +1 -3
- package/dist/utc/validate/isValidUtcRange.d.ts +16 -5
- package/dist/utc/validate/isValidUtcRange.js +2 -5
- package/dist/zoned/calculate/addZoned.d.ts +20 -10
- package/dist/zoned/calculate/addZoned.js +6 -13
- package/dist/zoned/calculate/cycleZoned.d.ts +33 -8
- package/dist/zoned/calculate/cycleZoned.js +1 -8
- package/dist/zoned/calculate/diffZoned.d.ts +1 -1
- package/dist/zoned/calculate/diffZoned.js +1 -1
- package/dist/zoned/calculate/diffZonedAsDuration.d.ts +1 -1
- package/dist/zoned/calculate/diffZonedAsDuration.js +1 -1
- package/dist/zoned/calculate/endOfQuarterForZoned.d.ts +9 -2
- package/dist/zoned/calculate/endOfQuarterForZoned.js +4 -5
- package/dist/zoned/calculate/endOfZoned.d.ts +16 -3
- package/dist/zoned/calculate/endOfZoned.js +2 -3
- package/dist/zoned/calculate/getLocaleZonedEndOfWeek.d.ts +10 -3
- package/dist/zoned/calculate/getLocaleZonedEndOfWeek.js +5 -6
- package/dist/zoned/calculate/getLocaleZonedStartOfWeek.d.ts +7 -1
- package/dist/zoned/calculate/getLocaleZonedStartOfWeek.js +3 -4
- package/dist/zoned/calculate/roundZoned.d.ts +21 -4
- package/dist/zoned/calculate/roundZoned.js +3 -7
- package/dist/zoned/calculate/setZoned.d.ts +25 -8
- package/dist/zoned/calculate/setZoned.js +4 -12
- package/dist/zoned/calculate/startOfQuarterForZoned.d.ts +7 -2
- package/dist/zoned/calculate/startOfQuarterForZoned.js +3 -5
- package/dist/zoned/calculate/startOfZoned.d.ts +15 -3
- package/dist/zoned/calculate/startOfZoned.js +1 -3
- package/dist/zoned/calculate/subtractZoned.d.ts +20 -10
- package/dist/zoned/calculate/subtractZoned.js +6 -13
- package/dist/zoned/compare/areZonedEqualBy.d.ts +14 -5
- package/dist/zoned/compare/areZonedEqualBy.js +7 -5
- package/dist/zoned/compare/isBetweenZoned.d.ts +13 -1
- package/dist/zoned/compare/isBetweenZoned.js +4 -3
- package/dist/zoned/compare/isInDaylightSaving.d.ts +38 -11
- package/dist/zoned/compare/isInDaylightSaving.js +40 -44
- package/dist/zoned/convert/convertPlainDateTimeToZoned.d.ts +18 -5
- package/dist/zoned/convert/convertPlainDateTimeToZoned.js +6 -11
- package/dist/zoned/convert/convertZonedToUnix.d.ts +9 -4
- package/dist/zoned/convert/convertZonedToUnix.js +2 -4
- package/dist/zoned/convert/convertZonedToZoned.d.ts +1 -1
- package/dist/zoned/convert/convertZonedToZoned.js +1 -1
- package/dist/zoned/format/formatCalendarZoned.d.ts +18 -12
- package/dist/zoned/format/formatCalendarZoned.js +13 -9
- package/dist/zoned/format/formatRelativeZoned.d.ts +41 -12
- package/dist/zoned/format/formatRelativeZoned.js +7 -4
- package/dist/zoned/format/formatRfc2822.d.ts +2 -2
- package/dist/zoned/format/formatRfc2822.js +2 -2
- package/dist/zoned/format/formatRfc3339.d.ts +2 -3
- package/dist/zoned/format/formatRfc3339.js +2 -3
- package/dist/zoned/format/formatTimeZoneName.d.ts +12 -3
- package/dist/zoned/format/formatTimeZoneName.js +5 -4
- package/dist/zoned/format/formatZonedDateTime.js +2 -1
- package/dist/zoned/format/formatZonedRange.js +2 -1
- package/dist/zoned/format/formatZonedToParts.d.ts +15 -4
- package/dist/zoned/format/formatZonedToParts.js +8 -5
- package/dist/zoned/get/getDstTransitions.d.ts +9 -10
- package/dist/zoned/get/getDstTransitions.js +12 -3
- package/dist/zoned/get/getTimeZoneOffset.d.ts +4 -1
- package/dist/zoned/get/getTimeZoneOffset.js +6 -3
- package/dist/zoned/get/getZonedNow.d.ts +13 -1
- package/dist/zoned/get/getZonedNow.js +12 -3
- package/dist/zoned/interval/intervalDifferenceZoned.d.ts +9 -0
- package/dist/zoned/interval/intervalDivideEquallyZoned.d.ts +22 -4
- package/dist/zoned/interval/intervalDivideEquallyZoned.js +6 -4
- package/dist/zoned/interval/intervalFromDurationZoned.d.ts +29 -10
- package/dist/zoned/interval/intervalFromDurationZoned.js +6 -13
- package/dist/zoned/interval/intervalIntersectionZoned.d.ts +9 -0
- package/dist/zoned/interval/intervalSplitAtZoned.d.ts +11 -0
- package/dist/zoned/interval/intervalSplitAtZoned.js +2 -0
- package/dist/zoned/interval/intervalUnionZoned.d.ts +9 -0
- package/dist/zoned/interval/intervalXorAllZoned.d.ts +9 -0
- package/dist/zoned/interval/intervalXorZoned.d.ts +9 -0
- package/dist/zoned/interval/mergeIntervalsZoned.d.ts +9 -0
- package/dist/zoned/interval/splitIntervalByUnitZoned.d.ts +27 -6
- package/dist/zoned/interval/splitIntervalByUnitZoned.js +11 -6
- package/dist/zoned/map/mapZonedDatesInRange.d.ts +9 -4
- package/dist/zoned/map/mapZonedDatesInRange.js +2 -4
- package/dist/zoned/parse/parseRfc2822.d.ts +1 -1
- package/dist/zoned/parse/parseRfc2822.js +1 -1
- package/dist/zoned/parse/parseUnitFromZoned.d.ts +9 -3
- package/dist/zoned/parse/parseUnitFromZoned.js +1 -3
- package/dist/zoned/parse/parseWeekFromZoned.d.ts +10 -3
- package/dist/zoned/parse/parseWeekFromZoned.js +2 -3
- package/dist/zoned/validate/hasDaylightSaving.d.ts +42 -11
- package/dist/zoned/validate/hasDaylightSaving.js +47 -31
- package/dist/zoned/validate/isValidZonedDateTime.d.ts +13 -0
- package/dist/zoned/validate/isValidZonedDateTime.js +14 -1
- package/dist/zoned/validate/isValidZonedRange.d.ts +15 -4
- package/dist/zoned/validate/isValidZonedRange.js +1 -4
- package/package.json +53 -5
- package/skills/gmt-arithmetic/SKILL.md +1 -1
- package/skills/gmt-basics/SKILL.md +10 -1
- package/skills/gmt-integration/SKILL.md +10 -5
- package/skills/gmt-timezone/SKILL.md +210 -14
- package/dist/internal/resolveReadingTimeZone.d.ts +0 -11
- package/dist/internal/resolveReadingTimeZone.js +0 -18
- package/dist/types/duration-unit.d.ts +0 -5
- /package/dist/types/{duration-unit.js → free-time-options.js} +0 -0
package/README.md
CHANGED
|
@@ -17,8 +17,8 @@ Read the [docs](https://gmt-dox.northguild.workers.dev/), or ask us on [Discord]
|
|
|
17
17
|
|
|
18
18
|
- **100% Temporal, Temporal-first.** GMT is built directly on the TC39 `Temporal` standard (via `@js-temporal/polyfill`) — not a custom, homegrown date/time type system like `@internationalized/date`'s own `CalendarDate`/`ZonedDateTime` classes. No `Date` object anywhere, enforced by 3 dedicated lint packages.
|
|
19
19
|
- **A full replacement for any and all of them.** Luxon, date-fns, Moment.js, and react-aria's `@internationalized/date` don't have parity with each other — GMT covers the combined capabilities of all four in one library, plus what none of them do alone.
|
|
20
|
-
- **~
|
|
21
|
-
- **~
|
|
20
|
+
- **~59× more CI test executions than all four competitors combined**: 1,200,990 from 40,033 tests run in all 10 timezones × 3 Node versions, vs. their combined 20,190.
|
|
21
|
+
- **~104× more test cases than `@internationalized/date`**: 40,033 vs. 386 — Adobe's own library, run at its own commit.
|
|
22
22
|
- **The only one of the five that tests systematically across locales in CI at all.** Zero of the four comparison libraries run a locale-test matrix; GMT mandates all 17 locales on every locale-aware function.
|
|
23
23
|
- **The only one that runs its entire suite under a real `TZ` env var across real-world zones.** Luxon and `@internationalized/date` have no CI timezone matrix; date-fns's zone scope is unclear; Moment.js covers 6 zones but not its full suite.
|
|
24
24
|
- **Explicit DST disambiguation control on both construction _and_ arithmetic** — a control none of the others expose.
|
|
@@ -94,7 +94,7 @@ GMT's test suite balances **thoroughness** against **maintenance burden** by tes
|
|
|
94
94
|
- **Non-string input tables** — functions that guard with `typeof x !== "string"` return the same sentinel for `null`, `undefined`, `123`, `true`, `[]`, and `{}`. We test one representative non-string per argument position rather than all six types × N positions. The collapse is safe because all non-string types hit the identical early-return code path.
|
|
95
95
|
- **Redundant permutations** — adjacent/disjoint/reversed interval cases that produce identical results are not duplicated across every function variant. The `plain/`, `zoned/`, `utc/`, and `unix/` families share the same mathematical behavior; each family gets the minimum set of cases needed to prove correctness.
|
|
96
96
|
|
|
97
|
-
**Result:**
|
|
97
|
+
**Result:** 40,033 tests across 706 files that exercise real behavior differences without redundant permutations. They run in CI as 1,200,990 executions — every one of them × 3 Node versions × 10 timezones.
|
|
98
98
|
|
|
99
99
|
## How GMT is tested, vs. the libraries it targets
|
|
100
100
|
|
|
@@ -110,9 +110,9 @@ GMT is measured directly against react-aria's **`@internationalized/date`**, **L
|
|
|
110
110
|
|
|
111
111
|
| Metric | GMT | `@internationalized/date` | Luxon | date-fns | Moment.js |
|
|
112
112
|
| ------------------------------- | -------------------------------------------------- | ------------------------------ | ------------------------------------ | ----------------------------------------- | -------------------------------- |
|
|
113
|
-
| Test files |
|
|
114
|
-
| Individual test cases | **
|
|
115
|
-
| Effective CI test<br>executions | **1,
|
|
113
|
+
| Test files | 706 | 6 | 58 / 60<br>(2 didn't run<br>locally) | 256 | 191<br>(52 core +<br>139 locale) |
|
|
114
|
+
| Individual test cases | **40,033** | 386 | 1,222 | 3,213 | 3,901 |
|
|
115
|
+
| Effective CI test<br>executions | **1,200,990**<br>(40,033 × 3 Node<br>× 10 timezones) | 386<br>(×1 Node) | 4,888<br>(1,222 × 4 Node) | 3,213<br>(×1 Node) | 11,703<br>(3,901 × 3 Node) |
|
|
116
116
|
| CI Node.js matrix | 22, 24, 26 | n/a — tests<br>React 16–canary | 20, 22, 24, 25 | not explicit<br>(`node = "latest"`) | LTS, LTS-1,<br>latest |
|
|
117
117
|
| CI timezone matrix | **10 zones × 3**<br>**Node, full suite** | none found | none found | dedicated workflow,<br>zone scope unclear | 6 zones,<br>partial suite only |
|
|
118
118
|
| Locale test matrix | **17 locales**,<br>every locale fn | none found | none found | none found | none found |
|
|
@@ -148,7 +148,7 @@ Specific, sourced claims — not a repeat of the metrics above.
|
|
|
148
148
|
| Only GMT enforces a mandatory<br>17-locale test matrix on every<br>locale-aware function | No CI-level or systematic<br>locale-matrix testing found<br>in any of the four |
|
|
149
149
|
| Only GMT exposes explicit DST<br>disambiguation control on both<br>construction _and_ arithmetic | Luxon's docs call this explicitly<br>undefined; `@internationalized/date`<br>only covers construction, not arithmetic |
|
|
150
150
|
| Only GMT is Temporal-native with<br>zero `Date` usage, enforced by<br>3 dedicated lint packages | Luxon, date-fns, and Moment.js all<br>still wrap or depend on `Date` internally |
|
|
151
|
-
| GMT's effective CI test<br>executions exceed all four<br>competitors **combined**<br>by ~
|
|
151
|
+
| GMT's effective CI test<br>executions exceed all four<br>competitors **combined**<br>by ~59× | 1,200,990 vs. 386 + 4,888 + 3,213<br>+ 11,703 = 20,190 |
|
|
152
152
|
|
|
153
153
|
## Package Layout
|
|
154
154
|
|
|
@@ -160,15 +160,17 @@ namespace objects: a namespace is a subpath.
|
|
|
160
160
|
import { addDate, getNow, formatRelativeZoned, Temporal } from "@northguild/gmt";
|
|
161
161
|
```
|
|
162
162
|
|
|
163
|
-
The
|
|
163
|
+
The fourteen namespace subpaths:
|
|
164
164
|
|
|
165
|
-
- `@northguild/gmt/calendar`: ISO week and ordinal dates, quarter and fiscal periods, zone-aware bucketing,
|
|
165
|
+
- `@northguild/gmt/calendar`: ISO week and ordinal dates, quarter and fiscal periods, zone-aware bucketing, business calendars with holiday sets and roll conventions, and operating hours with open-time SLA arithmetic
|
|
166
166
|
- `@northguild/gmt/duration`: ISO 8601 duration string parsing, validation, and arithmetic
|
|
167
167
|
- `@northguild/gmt/instant`: the instant-plus-offset pair, and explicit resolution of zoneless local wall times
|
|
168
168
|
- `@northguild/gmt/interval`: half-open `[start, end)` interval algebra over instants — overlap, intersect, clamp, merge, subtract, split, sum
|
|
169
169
|
- `@northguild/gmt/plain`: timezone-free helpers
|
|
170
170
|
- `@northguild/gmt/precision`: nanosecond (`bigint`) instants, their JSON bridge, storage truncation, and foreign epoch bridges
|
|
171
171
|
- `@northguild/gmt/span`: elapsed and wall-clock durations between two timestamps, as raw numbers
|
|
172
|
+
- `@northguild/gmt/transport`: transit legs as exact elapsed time, arrivals rendered where they land, dwell measured in local calendar days, crossings read in the zone that administers them, multi-leg schedules with missed connections flagged, cut-offs counted back from an anchor event, punctuality against a stated tolerance, planned, estimated, requested and actual timestamps kept apart, and the next departure a connection can make
|
|
173
|
+
- `@northguild/gmt/intermodal`: free time, demurrage and detention counted in the terminal's local days, with the charged dates behind every count, and the invoice, dispute and resolution deadline chain with every window a caller parameter
|
|
172
174
|
- `@northguild/gmt/zoned`: timezone-aware helpers
|
|
173
175
|
- `@northguild/gmt/unix`: Unix epoch (seconds or milliseconds) helpers
|
|
174
176
|
- `@northguild/gmt/utc`: UTC instant helpers
|
|
@@ -533,7 +535,7 @@ parseDateWithPattern("02/31/2024", "MM/dd/yyyy");
|
|
|
533
535
|
// "" — shape-valid but not a real date; Temporal validates after the regex match
|
|
534
536
|
```
|
|
535
537
|
|
|
536
|
-
Supported tokens include `yyyy`/`MM`/`dd`/`HH`/`mm`/`ss`/`SSS` for fixed-width fields, `M`/`d`/`H`/`h`/`m`/`s` for variable-width, `MMMM`/`MMM`/`EEEE`/`EEE`/`a`/`GGGG`/`GG` for locale-aware names, and `'single quotes'` for literal text. A `locale` parameter (default `"en-US"`) controls name-token matching. Invalid input, no-match, and malformed patterns all return `""`.
|
|
538
|
+
Supported tokens include `yyyy`/`MM`/`dd`/`HH`/`mm`/`ss`/`SSS` for fixed-width fields, `M`/`d`/`H`/`h`/`m`/`s` for variable-width, `MMMM`/`MMM`/`EEEE`/`EEE`/`a`/`GGGG`/`GG` for locale-aware names, and `'single quotes'` for literal text. Two single quotes in a row are one literal quote, inside or outside quoted text, as UTS #35 reads them: `"MMM d, ''yy"` reads `"Mar 15, '24"`. A `locale` parameter (default `"en-US"`) controls name-token matching. Invalid input, no-match, and malformed patterns all return `""`.
|
|
537
539
|
|
|
538
540
|
### Calendar systems
|
|
539
541
|
|
|
@@ -1184,7 +1186,7 @@ mapDatesInRange("0001-01-01", "9999-12-31", 1);
|
|
|
1184
1186
|
// [] — 3,652,059 dates exceed the default
|
|
1185
1187
|
```
|
|
1186
1188
|
|
|
1187
|
-
|
|
1189
|
+
For a calendar unit (day, week, month, year) each boundary is computed from `start` (`start + k × amount`), never by stepping from the previous boundary, so month-end starts don't drift. A yearly split from February 29 likewise returns to February 29 in leap years. An exact unit (hour and smaller) steps from the previous boundary: adding an exact unit never clamps, so the two ways give the same boundaries.
|
|
1188
1190
|
|
|
1189
1191
|
```typescript
|
|
1190
1192
|
splitIntervalByUnitDate("2024-01-31", "2024-05-01", "month", 1);
|
|
@@ -1382,18 +1384,18 @@ diffZoned("2024-01-01T00:00:00-05:00[America/New_York]", "2024-01-03T00:00:00+01
|
|
|
1382
1384
|
// null — calendar unit across two time zones
|
|
1383
1385
|
```
|
|
1384
1386
|
|
|
1385
|
-
`hasDaylightSaving` reports whether an IANA timezone observes daylight saving time at
|
|
1387
|
+
`hasDaylightSaving(timeZone, { at })` reports whether an IANA timezone observes daylight saving time at a reference instant. It is `true` when the zone is in daylight time at `at`, or a daylight period begins less than 365 days after it. So a zone in its winter is `true`, and a zone that has stopped changing its clocks is `false`. `at` is an ISO 8601 instant string. With it, the answer does not depend on the day the code runs; without it, the reference is the current instant. An invalid zone, an invalid `at` and a fixed offset return `false`:
|
|
1386
1388
|
|
|
1387
1389
|
```typescript
|
|
1388
1390
|
import { hasDaylightSaving } from "@northguild/gmt";
|
|
1389
1391
|
|
|
1390
|
-
hasDaylightSaving("America/New_York");
|
|
1392
|
+
hasDaylightSaving("America/New_York", { at: "2024-01-15T12:00:00Z" });
|
|
1391
1393
|
// true
|
|
1392
1394
|
|
|
1393
|
-
hasDaylightSaving("Asia/Tokyo");
|
|
1395
|
+
hasDaylightSaving("Asia/Tokyo", { at: "2024-01-15T12:00:00Z" });
|
|
1394
1396
|
// false
|
|
1395
1397
|
|
|
1396
|
-
hasDaylightSaving("Invalid/Zone");
|
|
1398
|
+
hasDaylightSaving("Invalid/Zone", { at: "2024-01-15T12:00:00Z" });
|
|
1397
1399
|
// false
|
|
1398
1400
|
```
|
|
1399
1401
|
|
|
@@ -1421,12 +1423,18 @@ Each object's `instant` is a UTC ISO 8601 string; `offsetBefore`/`offsetAfter` a
|
|
|
1421
1423
|
|
|
1422
1424
|
| Question | Function | Scope |
|
|
1423
1425
|
| ------------------------------------------------------------------------------- | --------------------------------------------- | ---------------------------------------------------- |
|
|
1424
|
-
| Does this zone observe DST at
|
|
1426
|
+
| Does this zone observe DST at this instant, or now? | `hasDaylightSaving(timeZone, { at })` | Zone-level, judged from a reference instant |
|
|
1425
1427
|
| Where do this zone's transitions fall? | `getDstTransitions(timeZone, year)` | Enumerates instants |
|
|
1426
1428
|
| Is _this particular instant_ currently in DST? | `isInDaylightSaving(value)` | A single zoned value |
|
|
1427
1429
|
| What should happen when construction lands on an ambiguous/nonexistent instant? | `disambiguation` / `offset` options (Group C) | Orthogonal — a construction-time choice, not a query |
|
|
1428
1430
|
|
|
1429
|
-
`isInDaylightSaving` answers the third question:
|
|
1431
|
+
`isInDaylightSaving` answers the third question. It and `hasDaylightSaving` apply one rule, read from the zone's own clock changes in the runtime's time zone data. The rule is GMT's own definition, not the tz database's daylight flag: no JavaScript API exposes that flag, and it cannot be worked out from offsets.
|
|
1432
|
+
|
|
1433
|
+
- **The rule.** Daylight time runs from a forward change of the zone's clocks to the backward change of the same size that undoes it, less than 365 days later.
|
|
1434
|
+
- **A forward change never undone, or undone 365 days or more later, is a change of standard time.** `Europe/Istanbul` has been on standard time since its last advance in March 2016.
|
|
1435
|
+
- **The higher of two alternating offsets is the daylight one.** `Europe/Dublin` is in daylight time in summer, and `Africa/Casablanca` at `+01:00`.
|
|
1436
|
+
|
|
1437
|
+
The full list of what the rule reads as standard time is in [DST Disambiguation](../../docs/dst-disambiguation.md) and in the function's JSDoc.
|
|
1430
1438
|
|
|
1431
1439
|
```typescript
|
|
1432
1440
|
import { isInDaylightSaving } from "@northguild/gmt/zoned";
|
|
@@ -1442,7 +1450,13 @@ isInDaylightSaving("2024-01-15T12:00:00+11:00[Australia/Sydney]");
|
|
|
1442
1450
|
// true
|
|
1443
1451
|
|
|
1444
1452
|
isInDaylightSaving("2024-07-15T12:00:00+09:00[Asia/Tokyo]");
|
|
1445
|
-
// false — Asia/Tokyo has no DST
|
|
1453
|
+
// false — Asia/Tokyo has had no DST since 1951
|
|
1454
|
+
|
|
1455
|
+
isInDaylightSaving("2015-07-01T12:00:00+03:00[Europe/Istanbul]");
|
|
1456
|
+
// true — the clocks went back on 2015-11-08
|
|
1457
|
+
|
|
1458
|
+
isInDaylightSaving("2016-12-01T12:00:00+03:00[Europe/Istanbul]");
|
|
1459
|
+
// false — the advance of March 2016 was never undone
|
|
1446
1460
|
|
|
1447
1461
|
isInDaylightSaving("invalid");
|
|
1448
1462
|
// false
|
|
@@ -1569,7 +1583,9 @@ formatRelativeZoned("2026-03-08T01:00:00-05:00[America/New_York]", "en-US");
|
|
|
1569
1583
|
// e.g. "tomorrow"
|
|
1570
1584
|
|
|
1571
1585
|
// Auto-picked units run from second through year, with formatRelativeDate's thresholds;
|
|
1572
|
-
// pass largestUnit to cap them.
|
|
1586
|
+
// pass largestUnit to cap them. It takes a singular or plural name: "day" or "days".
|
|
1587
|
+
// Each function accepts the units its values have: formatRelativeDate day to year,
|
|
1588
|
+
// formatRelativeTime second to hour, the others second to year. Any other unit returns "".
|
|
1573
1589
|
formatRelativeUtc("2024-03-17T14:30:45Z", "en-US", {
|
|
1574
1590
|
reference: "2026-03-17T14:30:45Z",
|
|
1575
1591
|
});
|
|
@@ -1589,6 +1605,7 @@ formatRelativeUnix(1710685845000, "en-US", {
|
|
|
1589
1605
|
|
|
1590
1606
|
// roundingMethod ("floor" | "ceil" | "round", default "round") controls how the
|
|
1591
1607
|
// computed distance rounds to the display unit — every formatRelative* function accepts it.
|
|
1608
|
+
// Any other value returns "".
|
|
1592
1609
|
formatRelativeUtc(value, "en-US", { roundingMethod: "floor" });
|
|
1593
1610
|
|
|
1594
1611
|
// formatCalendar* — a relative day label + time-of-day, joined with the
|
|
@@ -1805,6 +1822,40 @@ invalid input, and accepts only values inside the range `Temporal.Instant` can r
|
|
|
1805
1822
|
first when the two must be told apart. Leap-second-aware time scales (TAI, GPS) are not
|
|
1806
1823
|
part of this namespace — these are plain instant conversions.
|
|
1807
1824
|
|
|
1825
|
+
**A zoned string names one instant in every reader.** Temporal writes a zone's offset rounded
|
|
1826
|
+
to the minute, so a zone with a sub-minute offset is written with an offset up to 30 seconds
|
|
1827
|
+
from the real one: `Africa/Monrovia` stood at −00:44:30 until 1972 and is written `-00:45`.
|
|
1828
|
+
Every GMT function that reads a moment reads such a string as `Temporal.ZonedDateTime.from`
|
|
1829
|
+
does (TC39 `ToTemporalZonedDateTime` matches a minute-precision offset by minutes), so a zoned
|
|
1830
|
+
string GMT wrote reads back as the instant it was written for:
|
|
1831
|
+
|
|
1832
|
+
```typescript
|
|
1833
|
+
import { isValidZonedDateTime, toNanoseconds } from "@northguild/gmt";
|
|
1834
|
+
|
|
1835
|
+
toNanoseconds("1960-01-01T00:20:00-00:45[Africa/Monrovia]"); // -315615330000000000n (01:04:30Z)
|
|
1836
|
+
toNanoseconds("1960-01-01T00:20:00-00:45"); // -315615300000000000n (01:05:00Z; no zone, so the offset is exact)
|
|
1837
|
+
toNanoseconds("2024-06-15T10:00:00Z[Not/AZone]"); // 1718445600000000000n (an instant reader does not validate the bracket)
|
|
1838
|
+
|
|
1839
|
+
isValidZonedDateTime("1960-01-01T00:20:00-00:45[Africa/Monrovia]"); // true (the rounded offset)
|
|
1840
|
+
isValidZonedDateTime("1960-01-01T00:20:00-00:44:30[Africa/Monrovia]"); // true (the exact offset)
|
|
1841
|
+
isValidZonedDateTime("1960-01-01T00:20:00-00:45:00[Africa/Monrovia]"); // false (an offset with seconds must be exact)
|
|
1842
|
+
```
|
|
1843
|
+
|
|
1844
|
+
- The rule applies to `precision/`, `span/`, `interval/`, `calendar/`, `intermodal/`,
|
|
1845
|
+
`transport/` and `getTimeZoneOffset` alike. Any other string keeps its written offset: one
|
|
1846
|
+
without a bracket, a `Z` instant, an offset written with seconds, and a bracket whose zone
|
|
1847
|
+
does not exist or does not fit the offset.
|
|
1848
|
+
- Every string Temporal writes for an instant in range is valid, the first and last instants
|
|
1849
|
+
included: `-271821-04-19T23:58:45-00:01[Europe/London]` is the first instant.
|
|
1850
|
+
- A function that keeps the zone (`isValidZonedDateTime`, `transitTime`, `toOffsetInstant`,
|
|
1851
|
+
every `zoned/` function, a zoned `relativeTo`) rejects a bracket that contradicts the
|
|
1852
|
+
offset, and requires an offset written with seconds to be the zone's offset exactly.
|
|
1853
|
+
- Two limits are Temporal's own. A wall time repeated inside a sub-minute offset change,
|
|
1854
|
+
written to the minute, reads as its first pass (`1952-10-15T23:59:59-11:20[Pacific/Niue]`);
|
|
1855
|
+
write the offset with seconds (`-11:20:00`) to name the second. A zoned read refuses a local
|
|
1856
|
+
date of −271821-04-19, which a zone west of Greenwich shows for the first hours of the
|
|
1857
|
+
instant range; pass that instant in `Z` form.
|
|
1858
|
+
|
|
1808
1859
|
### Foreign epoch bridges
|
|
1809
1860
|
|
|
1810
1861
|
Other systems do not count from 1970. NTP counts from 1900 and rolls over in 2036, Windows
|
|
@@ -2162,7 +2213,15 @@ isValidInterval({ start: "2024-01-01T17:00:00Z", end: "2024-01-01T09:00:00Z" });
|
|
|
2162
2213
|
|
|
2163
2214
|
- **Endpoints are instants.** An offset (`Z` or `±HH:MM`) is required and a `[Zone]` annotation is
|
|
2164
2215
|
optional. As with a TC39 `Temporal.Instant`, endpoints compare by epoch nanoseconds, so two of
|
|
2165
|
-
them may name different zones. Leap seconds are rejected, and a `[u-ca=…]` annotation is ignored, as `Temporal.Instant.from` ignores it.
|
|
2216
|
+
them may name different zones. Leap seconds are rejected, and a `[u-ca=…]` annotation is ignored, as `Temporal.Instant.from` ignores it.
|
|
2217
|
+
The offset fixes the instant; an offset written to the minute that is the bracketed zone's
|
|
2218
|
+
sub-minute offset rounded (`-00:45[Africa/Monrovia]`, for −00:44:30) names the instant the
|
|
2219
|
+
zone gives, so a zoned string GMT wrote is read back as the same moment. That holds
|
|
2220
|
+
from the first instant of the range to the last. The one exception is Temporal's own: a wall
|
|
2221
|
+
time repeated inside a sub-minute offset change, written with a minute-rounded offset, reads
|
|
2222
|
+
as its first pass (for example `1952-10-15T23:59:59-11:20[Pacific/Niue]`; several zones have
|
|
2223
|
+
such a change in their history).
|
|
2224
|
+
For local-calendar
|
|
2166
2225
|
edges, build them with `floorToZone` or `bucketRange` first.
|
|
2167
2226
|
- **Outputs are the caller's own strings**, never re-serialised. When two strings spell the same
|
|
2168
2227
|
instant, the first argument's spelling wins. No spec covers the tie, so this is a GMT rule.
|
|
@@ -2179,6 +2238,666 @@ isValidInterval({ start: "2024-01-01T17:00:00Z", end: "2024-01-01T09:00:00Z" });
|
|
|
2179
2238
|
is both a legitimate result (a fully covered subtraction) and the invalid-input sentinel.
|
|
2180
2239
|
`isValidInterval` tells them apart.
|
|
2181
2240
|
|
|
2241
|
+
### Operating hours and open-time SLAs
|
|
2242
|
+
|
|
2243
|
+
A business day answers "is Thursday a working day". A terminal gate, a support desk, a customs
|
|
2244
|
+
office and a night curfew ask a finer question: is it open right now, how many open hours have
|
|
2245
|
+
passed, and when does an SLA measured in open hours fall due. `calendar/hours` answers them from
|
|
2246
|
+
one `OperatingSchedule`: a zone, windows of local wall time for each ISO weekday, holiday dates,
|
|
2247
|
+
and dated overrides.
|
|
2248
|
+
|
|
2249
|
+
```typescript
|
|
2250
|
+
import {
|
|
2251
|
+
addOperatingTime,
|
|
2252
|
+
isOpenAt,
|
|
2253
|
+
nextCloseAt,
|
|
2254
|
+
nextOpenAt,
|
|
2255
|
+
operatingIntervals,
|
|
2256
|
+
operatingTimeBetween,
|
|
2257
|
+
recurringWindows,
|
|
2258
|
+
} from "@northguild/gmt";
|
|
2259
|
+
import type { OperatingSchedule } from "@northguild/gmt/types";
|
|
2260
|
+
|
|
2261
|
+
const nineToFive = [{ from: "09:00", to: "17:00" }];
|
|
2262
|
+
const desk: OperatingSchedule = {
|
|
2263
|
+
timeZone: "America/New_York",
|
|
2264
|
+
weekly: { 1: nineToFive, 2: nineToFive, 3: nineToFive, 4: nineToFive, 5: nineToFive },
|
|
2265
|
+
holidays: ["2024-07-04"],
|
|
2266
|
+
overrides: [{ date: "2024-07-05", windows: [{ from: "10:00", to: "12:00" }] }],
|
|
2267
|
+
};
|
|
2268
|
+
|
|
2269
|
+
operatingIntervals(desk, { start: "2024-07-04T04:00:00Z", end: "2024-07-06T04:00:00Z" });
|
|
2270
|
+
// [{ start: "2024-07-05T14:00:00Z", end: "2024-07-05T16:00:00Z" }] — the holiday is closed, the override replaces Friday
|
|
2271
|
+
|
|
2272
|
+
isOpenAt("2024-06-17T14:00:00Z", desk); // true — Monday 10:00 local
|
|
2273
|
+
nextOpenAt("2024-06-15T16:00:00Z", desk); // "2024-06-17T13:00:00Z" — Saturday noon to Monday 09:00 local
|
|
2274
|
+
nextCloseAt("2024-06-17T14:00:00Z", desk); // "2024-06-17T21:00:00Z" — 17:00 local
|
|
2275
|
+
|
|
2276
|
+
operatingTimeBetween("2024-06-14T20:00:00Z", "2024-06-17T14:00:00Z", desk); // "PT2H" — Friday 16:00 to Monday 10:00
|
|
2277
|
+
addOperatingTime("2024-06-14T20:00:00Z", "PT8H", desk); // "2024-06-17T20:00:00Z" — an 8-hour SLA from Friday 16:00: Monday 16:00
|
|
2278
|
+
addOperatingTime("2024-06-14T20:00:00Z", "PT8H", desk, { within: "P1D" }); // "" — not due within a day
|
|
2279
|
+
|
|
2280
|
+
// A curfew wraps midnight and belongs to the night it starts on.
|
|
2281
|
+
recurringWindows(
|
|
2282
|
+
{ 6: [{ from: "23:00", to: "06:00" }] },
|
|
2283
|
+
{ start: "2024-11-02T00:00:00Z", end: "2024-11-04T00:00:00Z" },
|
|
2284
|
+
"America/New_York",
|
|
2285
|
+
);
|
|
2286
|
+
// [{ start: "2024-11-03T03:00:00Z", end: "2024-11-03T11:00:00Z" }] — 8 hours across the fall-back night
|
|
2287
|
+
```
|
|
2288
|
+
|
|
2289
|
+
- **Windows are half-open local wall time.** `{ from: "09:00", to: "17:00" }` is open at 09:00
|
|
2290
|
+
and closed at 17:00. A `to` at or before its `from` wraps past midnight: `23:00`–`06:00` is a
|
|
2291
|
+
night and `00:00`–`00:00` a whole day. A window belongs to the date it starts on, so a holiday
|
|
2292
|
+
or override on Friday removes or replaces a Friday-night window, and one on Saturday does not.
|
|
2293
|
+
Windows that overlap or touch merge, so a run of whole days is one interval.
|
|
2294
|
+
- **Every edge goes through `resolveLocal`.** The default `disambiguation` is `"compatible"`: an
|
|
2295
|
+
edge in a repeated fall-back hour takes the earlier instant, and one in a skipped
|
|
2296
|
+
spring-forward hour moves forward by the gap. So a 23:00–06:00 window is 8 real hours across
|
|
2297
|
+
New York's fall-back night and 6 across its spring-forward night. Under `"earlier"`, a skipped
|
|
2298
|
+
midnight can move a date's first window onto the previous date. `"earlier"` and `"later"`
|
|
2299
|
+
pick the other instant. `"reject"` guesses nothing: it returns the sentinel when a window with
|
|
2300
|
+
an ambiguous or nonexistent edge could change the answer, and ignores one that cannot.
|
|
2301
|
+
- **Dates are the zone's real local dates.** Holidays and overrides are ISO dates in the
|
|
2302
|
+
schedule's zone. A `BusinessCalendar` can be passed as `holidays`; its `holidays` are read, and
|
|
2303
|
+
its `weekend` is not, because `weekly` already says which weekdays open. A date the zone
|
|
2304
|
+
deleted (`Pacific/Apia`, 2011-12-30) has no windows. An override wins over a holiday on the
|
|
2305
|
+
same date, and `windows: []` closes a date.
|
|
2306
|
+
- **Open time is elapsed time.** `operatingTimeBetween` is the length of the open intervals
|
|
2307
|
+
inside `[start, end)`, the same number `sumIntervals` gives for their intersections with the
|
|
2308
|
+
range, with hours as the largest unit. `addOperatingTime` is its inverse: the earliest instant
|
|
2309
|
+
at which that much open time has passed, so a deadline that lands on a closing time is that
|
|
2310
|
+
closing time. Its duration is hours and smaller units: `P1D` of open time could mean 24 open
|
|
2311
|
+
hours or one working day, so it returns `""`.
|
|
2312
|
+
- **Searches stop at a horizon.** `nextOpenAt`, `nextCloseAt` and `addOperatingTime` search up to
|
|
2313
|
+
`within` after the input (default `"P1Y"`), added in the schedule's zone so `"P1D"` is one local
|
|
2314
|
+
day. An answer exactly at the horizon counts; one past it returns `""`. A 24/7 schedule never
|
|
2315
|
+
closes, so `nextCloseAt` returns `""` for it.
|
|
2316
|
+
- **Bounded walks.** A range spanning more than 10,000 local dates (about 27 years), counted from
|
|
2317
|
+
its start's date, or a search that would go that far past its input, returns the sentinel
|
|
2318
|
+
rather than a truncated answer. A search answers as soon as the answer is certain, so a long
|
|
2319
|
+
`within` costs nothing when the answer is near. A horizon past Temporal's last instant stops
|
|
2320
|
+
there.
|
|
2321
|
+
- **Not a recurrence engine.** A weekly pattern with dated exceptions covers gate hours, office
|
|
2322
|
+
hours, trading sessions and curfews. Monthly and yearly RFC 5545 rules are out of scope.
|
|
2323
|
+
|
|
2324
|
+
### Transport legs and dwell
|
|
2325
|
+
|
|
2326
|
+
Every transport mode shares three operations: add a leg's duration to a departure, show the
|
|
2327
|
+
arrival in the zone where it lands, and measure how long something sat at a terminal, berth,
|
|
2328
|
+
gate or yard. Every mode also gets the same two things wrong: a leg across a DST transition is a
|
|
2329
|
+
fixed number of *elapsed* hours, and dwell is charged in *local calendar days*, not hours.
|
|
2330
|
+
|
|
2331
|
+
```typescript
|
|
2332
|
+
import { dwellTime, etaAtZone, transitTime } from "@northguild/gmt";
|
|
2333
|
+
|
|
2334
|
+
// A leg is exact time in the departure's own zone. Four elapsed hours across the
|
|
2335
|
+
// spring-forward: the wall clock reads 04:00, not 03:00.
|
|
2336
|
+
transitTime("2024-03-09T23:00:00-05:00[America/New_York]", "PT4H");
|
|
2337
|
+
// "2024-03-10T04:00:00-04:00[America/New_York]"
|
|
2338
|
+
|
|
2339
|
+
// A day is 24 exact hours, not "the same wall time tomorrow".
|
|
2340
|
+
transitTime("2024-03-09T12:00:00-05:00[America/New_York]", "P1D");
|
|
2341
|
+
// "2024-03-10T13:00:00-04:00[America/New_York]"
|
|
2342
|
+
|
|
2343
|
+
// The departure's form is preserved: Z stays Z, an offset stays an offset.
|
|
2344
|
+
transitTime("2024-06-15T10:00:00Z", "PT36H"); // "2024-06-16T22:00:00Z"
|
|
2345
|
+
transitTime("2024-06-15T10:00:00+09:00", "PT1H"); // "2024-06-15T11:00:00+09:00"
|
|
2346
|
+
transitTime("2024-06-15T10:00:00+05:30:15", "PT1H"); // "2024-06-15T11:00:00+05:30:15"
|
|
2347
|
+
|
|
2348
|
+
// Calendar units need a reference point; a zone that contradicts its offset is rejected.
|
|
2349
|
+
transitTime("2024-06-15T10:00:00Z", "P1M"); // ""
|
|
2350
|
+
transitTime("2024-06-15T10:00:00-05:00[America/New_York]", "PT2H"); // "" (June is -04:00)
|
|
2351
|
+
|
|
2352
|
+
// Render a moment where it will be read. No disambiguation arises: on a fall-back night
|
|
2353
|
+
// two arrivals an hour apart print the same wall time, and the offset tells them apart.
|
|
2354
|
+
etaAtZone("2024-06-15T12:30:00Z", "Asia/Tokyo"); // "2024-06-15T21:30:00+09:00[Asia/Tokyo]"
|
|
2355
|
+
etaAtZone("2024-11-03T05:30:00Z", "America/New_York"); // "2024-11-03T01:30:00-04:00[America/New_York]"
|
|
2356
|
+
etaAtZone("2024-11-03T06:30:00Z", "America/New_York"); // "2024-11-03T01:30:00-05:00[America/New_York]"
|
|
2357
|
+
|
|
2358
|
+
// Dwell: exact duration, zone-local entry and exit, and the local calendar days touched.
|
|
2359
|
+
dwellTime("2024-06-15T22:30:00Z", "2024-06-16T01:00:00Z", "Europe/London");
|
|
2360
|
+
// { duration: "PT2H30M",
|
|
2361
|
+
// enter: "2024-06-15T23:30:00+01:00[Europe/London]",
|
|
2362
|
+
// exit: "2024-06-16T02:00:00+01:00[Europe/London]",
|
|
2363
|
+
// calendarDays: 2 }
|
|
2364
|
+
|
|
2365
|
+
// The same two instants, one fewer day: the zone decides.
|
|
2366
|
+
dwellTime("2024-06-15T22:30:00Z", "2024-06-16T01:00:00Z", "Europe/Amsterdam");
|
|
2367
|
+
// { duration: "PT2H30M",
|
|
2368
|
+
// enter: "2024-06-16T00:30:00+02:00[Europe/Amsterdam]",
|
|
2369
|
+
// exit: "2024-06-16T03:00:00+02:00[Europe/Amsterdam]",
|
|
2370
|
+
// calendarDays: 1 }
|
|
2371
|
+
|
|
2372
|
+
// The zone comes from targetZone or from the entry's bracketed zone. Bare instants with
|
|
2373
|
+
// neither return null: an offset is not a place, and a day count needs one.
|
|
2374
|
+
dwellTime("2024-06-15T22:30:00+01:00[Europe/London]", "2024-06-16T02:00:00+01:00[Europe/London]");
|
|
2375
|
+
// { duration: "PT3H30M",
|
|
2376
|
+
// enter: "2024-06-15T22:30:00+01:00[Europe/London]",
|
|
2377
|
+
// exit: "2024-06-16T02:00:00+01:00[Europe/London]",
|
|
2378
|
+
// calendarDays: 2 }
|
|
2379
|
+
dwellTime("2024-06-15T22:30:00Z", "2024-06-16T01:00:00Z"); // null
|
|
2380
|
+
|
|
2381
|
+
// Inverted intervals return null.
|
|
2382
|
+
dwellTime("2024-06-16T01:00:00Z", "2024-06-15T22:30:00Z", "Europe/London"); // null
|
|
2383
|
+
```
|
|
2384
|
+
|
|
2385
|
+
- **`transitTime` adds exact time.** Hours, minutes, seconds and fractions are elapsed time and a
|
|
2386
|
+
`D` component is 24 hours exactly, so the arrival wall clock reflects any DST shift in between.
|
|
2387
|
+
Years, months and weeks return `""`: no leg takes "a month" without a reference point. A
|
|
2388
|
+
negative duration moves backwards, which is how a departure is recovered from an arrival.
|
|
2389
|
+
- **`etaAtZone` renders a moment.** The input is an instant (`Z`, an offset, or an offset with a
|
|
2390
|
+
bracketed zone), and only the instant is read; `targetZone` is where it is rendered. The zone
|
|
2391
|
+
is the caller's fact: GMT does not resolve a port, airport or station code to a timezone. A
|
|
2392
|
+
fixed offset such as `"+09:00"` is accepted as a zone, as `isValidTimeZone` accepts it.
|
|
2393
|
+
`dwellTime` accepts one as `targetZone` too, and counts days in that offset, which observes
|
|
2394
|
+
no DST.
|
|
2395
|
+
- **`dwellTime.calendarDays` counts local dates, not hours.** It is the number of distinct
|
|
2396
|
+
local dates the half-open interval `[entry, exit)` touches: same date is `1`, across one
|
|
2397
|
+
local midnight is `2`, and an exit exactly at local midnight does not touch the new day. It
|
|
2398
|
+
is counted by walking the zone's real transitions, so a 23- or 25-hour local day is one day,
|
|
2399
|
+
a date the zone deleted (`Pacific/Apia`, 2011-12-30) is never touched, and a date the clock
|
|
2400
|
+
falls back into (`America/Goose_Bay`, 2010-11-07) is counted once. `duration` is the exact elapsed time with hours as the largest unit, as
|
|
2401
|
+
`sumIntervals` reports it.
|
|
2402
|
+
|
|
2403
|
+
### Crossings and multi-leg schedules
|
|
2404
|
+
|
|
2405
|
+
A crossing (a canal transit, a strait passage, a border queue) is logged as two instants and
|
|
2406
|
+
read on the clock of whoever administers it. A multi-leg move (truck, ship, rail) is one
|
|
2407
|
+
departure, a duration per leg and a handling time at each handoff, and each handoff is where a
|
|
2408
|
+
naive implementation loses an hour. Every leg boundary here is an exact instant; local time is
|
|
2409
|
+
rendered only at the edges, with `etaAtZone`.
|
|
2410
|
+
|
|
2411
|
+
```typescript
|
|
2412
|
+
import { crossingTime, scheduleDelivery } from "@northguild/gmt";
|
|
2413
|
+
|
|
2414
|
+
// A crossing: exact elapsed time, both ends read in the zone that runs the crossing.
|
|
2415
|
+
crossingTime("2024-03-10T05:00:00Z", "2024-03-10T12:00:00Z", "America/New_York");
|
|
2416
|
+
// { duration: "PT7H",
|
|
2417
|
+
// enter: "2024-03-10T00:00:00-05:00[America/New_York]",
|
|
2418
|
+
// exit: "2024-03-10T08:00:00-04:00[America/New_York]" } (seven hours across the spring-forward)
|
|
2419
|
+
|
|
2420
|
+
// targetZone is always the rendering zone; a bracketed zone on the input never is.
|
|
2421
|
+
crossingTime("2024-06-15T10:00:00+02:00[Europe/Berlin]", "2024-06-15T12:00:00+02:00[Europe/Berlin]", "Africa/Cairo");
|
|
2422
|
+
// { duration: "PT2H",
|
|
2423
|
+
// enter: "2024-06-15T11:00:00+03:00[Africa/Cairo]",
|
|
2424
|
+
// exit: "2024-06-15T13:00:00+03:00[Africa/Cairo]" }
|
|
2425
|
+
crossingTime("2024-06-16T01:00:00Z", "2024-06-15T22:30:00Z", "Europe/London"); // null (exit before entry)
|
|
2426
|
+
|
|
2427
|
+
// Truck, ship, rail. The first departure is a published local wall time, read in startTimeZone.
|
|
2428
|
+
// The ship waits for its scheduled sailing; the rail leg leaves when the 48-hour dwell ends.
|
|
2429
|
+
const legs = [
|
|
2430
|
+
{ departure: "2024-06-14T08:00:00", duration: "PT6H", timeZone: "Asia/Shanghai",
|
|
2431
|
+
dwellAfter: "PT12H", mode: "truck", origin: "CNSZX-DC", destination: "CNYTN" },
|
|
2432
|
+
{ departure: "2024-06-15T10:00:00+08:00[Asia/Shanghai]", duration: "PT336H",
|
|
2433
|
+
timeZone: "America/Los_Angeles", dwellAfter: "PT48H", mode: "ship", origin: "CNYTN", destination: "USLAX" },
|
|
2434
|
+
{ duration: "PT70H", timeZone: "America/Chicago", mode: "rail", origin: "USLAX", destination: "USCHI" },
|
|
2435
|
+
];
|
|
2436
|
+
scheduleDelivery(legs, { startTimeZone: "Asia/Shanghai" });
|
|
2437
|
+
// { eta: "2024-07-03T19:00:00-05:00[America/Chicago]",
|
|
2438
|
+
// legTimes: [
|
|
2439
|
+
// { arrival: "2024-06-14T06:00:00Z", localArrival: "2024-06-14T14:00:00+08:00[Asia/Shanghai]",
|
|
2440
|
+
// dwellAfter: "PT12H", mode: "truck", origin: "CNSZX-DC", destination: "CNYTN" },
|
|
2441
|
+
// { arrival: "2024-06-29T02:00:00Z", localArrival: "2024-06-28T19:00:00-07:00[America/Los_Angeles]",
|
|
2442
|
+
// dwellAfter: "PT48H", mode: "ship", origin: "CNYTN", destination: "USLAX" },
|
|
2443
|
+
// { arrival: "2024-07-04T00:00:00Z", localArrival: "2024-07-03T19:00:00-05:00[America/Chicago]",
|
|
2444
|
+
// dwellAfter: "PT0S", mode: "rail", origin: "USLAX", destination: "USCHI" } ] }
|
|
2445
|
+
|
|
2446
|
+
// A sailing at 20:00 local on the 14th leaves inside the truck leg's 12-hour dwell: missed.
|
|
2447
|
+
scheduleDelivery([legs[0], { ...legs[1], departure: "2024-06-14T20:00:00+08:00[Asia/Shanghai]" }],
|
|
2448
|
+
{ startTimeZone: "Asia/Shanghai" }); // null
|
|
2449
|
+
|
|
2450
|
+
scheduleDelivery(legs); // null (a zoneless first departure needs startTimeZone)
|
|
2451
|
+
scheduleDelivery([]); // { eta: "", legTimes: [] }
|
|
2452
|
+
```
|
|
2453
|
+
|
|
2454
|
+
- **`crossingTime` counts no days.** It returns the exact duration, with hours as the largest
|
|
2455
|
+
unit, and the entry and exit rendered in `targetZone`, an IANA identifier or a fixed offset.
|
|
2456
|
+
A crossing that needs a day count is a dwell: use `dwellTime`. An exit before the entry
|
|
2457
|
+
returns `null`; a zero-length crossing is `PT0S`.
|
|
2458
|
+
- **`dwellAfter` is the minimum connect time.** Each leg leaves at its own `departure`, or, when
|
|
2459
|
+
it has none, at the previous arrival plus the previous leg's `dwellAfter`. A scheduled
|
|
2460
|
+
departure earlier than that (inside the dwell, or before the arrival) is a missed connection
|
|
2461
|
+
and returns `null`. Equal passes: a zero-slack connection is feasible. `dwellAfter` is echoed
|
|
2462
|
+
as written (`"PT90M"` stays `"PT90M"`), `"PT0S"` when omitted. The last leg's `dwellAfter` is
|
|
2463
|
+
validated and echoed but never added to an instant, because no leg follows it.
|
|
2464
|
+
- **Departures must be exact, with two wall-time exceptions.** A departure is an instant (`Z` or
|
|
2465
|
+
an offset) or a zoned string, whose bracketed zone must be real and agree with its offset.
|
|
2466
|
+
Schedules are published as local wall times, so a zoneless first departure is read in
|
|
2467
|
+
`startTimeZone`, and a zoneless later-leg departure is read in the _previous_ leg's own
|
|
2468
|
+
`timeZone` (leg N−1's destination is exactly leg N's departure zone). Zoneless without one of
|
|
2469
|
+
these returns `null`.
|
|
2470
|
+
- **Wall times resolve as `"compatible"`.** A zoneless first departure, a zoneless later-leg
|
|
2471
|
+
departure, or a zoned departure written without an offset, is a wall time. An ambiguous one (a
|
|
2472
|
+
fall-back hour the clock ran through twice) resolves to the earlier instant; a skipped one (a
|
|
2473
|
+
spring-forward hour) resolves to the later instant. Write the offset to pick the other pass of
|
|
2474
|
+
a repeated hour. At a hub, a printed timetable never says which pass it means, so on any leg
|
|
2475
|
+
but the first a repeated hour takes the earliest pass at or after the previous arrival plus its
|
|
2476
|
+
dwell; when neither is that late, the connection is missed.
|
|
2477
|
+
- **Durations follow `transitTime`.** Time units are elapsed time and a day is 24 hours;
|
|
2478
|
+
years, months and weeks return `null`. A negative leg returns `null`, because a leg cannot
|
|
2479
|
+
arrive before it departs. A single-leg schedule is exactly `transitTime` composed with
|
|
2480
|
+
`etaAtZone`.
|
|
2481
|
+
- **Tags are opaque.** `mode`, `origin` and `destination` are echoed onto each `LegTime` and are
|
|
2482
|
+
absent when not supplied. GMT never resolves them to a zone: `timeZone` is the caller's fact.
|
|
2483
|
+
- Also exported: the `Crossing`, `Leg`, `LegTime`, `ScheduleDeliveryOptions` and
|
|
2484
|
+
`DeliverySchedule` types.
|
|
2485
|
+
|
|
2486
|
+
### Cut-offs and deadlines
|
|
2487
|
+
|
|
2488
|
+
A logistics deadline is not a fixed timestamp. It counts back from an event (a vessel's
|
|
2489
|
+
departure, a container's loading, an arrival) in the local time of the place that enforces it,
|
|
2490
|
+
and it moves when that event moves. An ocean booking carries a stack of them: documentation,
|
|
2491
|
+
VGM, gate-in and customs. `cutoffAt` finds one, `cutoffSchedule` finds the whole stack, and
|
|
2492
|
+
`isPastCutoff` and `timeToCutoff` read the clock against one.
|
|
2493
|
+
|
|
2494
|
+
```typescript
|
|
2495
|
+
import { cutoffAt, cutoffSchedule, isPastCutoff, timeToCutoff } from "@northguild/gmt";
|
|
2496
|
+
|
|
2497
|
+
// A vessel sails Friday 14 June 2024 at 18:00 in Amsterdam (16:00Z).
|
|
2498
|
+
// "Two days before, 17:00" is 17:00 local on Wednesday: not 48 hours, which keeps 18:00.
|
|
2499
|
+
cutoffAt("2024-06-14T16:00:00Z", "P2D", { timeZone: "Europe/Amsterdam", atLocalTime: "17:00" });
|
|
2500
|
+
// "2024-06-12T17:00:00+02:00[Europe/Amsterdam]"
|
|
2501
|
+
cutoffAt("2024-06-14T16:00:00Z", "PT48H", { timeZone: "Europe/Amsterdam" });
|
|
2502
|
+
// "2024-06-12T18:00:00+02:00[Europe/Amsterdam]"
|
|
2503
|
+
|
|
2504
|
+
// The whole stack against one departure, earliest first. Reschedule the vessel, call it again.
|
|
2505
|
+
cutoffSchedule("2024-06-14T16:00:00Z", [
|
|
2506
|
+
{ name: "gate-in", offset: "P1D" },
|
|
2507
|
+
{ name: "document", offset: "P2D", atLocalTime: "17:00" },
|
|
2508
|
+
{ name: "customs", offset: "PT30H" },
|
|
2509
|
+
{ name: "VGM", offset: "P1D", atLocalTime: "10:00" },
|
|
2510
|
+
], { timeZone: "Europe/Amsterdam" });
|
|
2511
|
+
// [ { name: "document", at: "2024-06-12T17:00:00+02:00[Europe/Amsterdam]" },
|
|
2512
|
+
// { name: "VGM", at: "2024-06-13T10:00:00+02:00[Europe/Amsterdam]" },
|
|
2513
|
+
// { name: "customs", at: "2024-06-13T12:00:00+02:00[Europe/Amsterdam]" },
|
|
2514
|
+
// { name: "gate-in", at: "2024-06-13T18:00:00+02:00[Europe/Amsterdam]" } ]
|
|
2515
|
+
|
|
2516
|
+
// A cut-off on a weekend or holiday moves by the roll convention you name. There is no default.
|
|
2517
|
+
cutoffAt("2024-06-18T16:00:00Z", "P2D", {
|
|
2518
|
+
timeZone: "Europe/Amsterdam",
|
|
2519
|
+
atLocalTime: "17:00",
|
|
2520
|
+
calendar: { weekend: [6, 7], holidays: [], timeZone: "Europe/Amsterdam" },
|
|
2521
|
+
roll: "preceding",
|
|
2522
|
+
}); // "2024-06-14T17:00:00+02:00[Europe/Amsterdam]" (Sunday rolls back to Friday)
|
|
2523
|
+
|
|
2524
|
+
// The anchor is the caller's event. The same 24 hours from loading and from departure:
|
|
2525
|
+
cutoffAt("2024-06-10T08:00:00Z", "PT24H", { timeZone: "Asia/Shanghai" });
|
|
2526
|
+
// "2024-06-09T16:00:00+08:00[Asia/Shanghai]"
|
|
2527
|
+
cutoffAt("2024-06-12T20:00:00Z", "PT24H", { timeZone: "Asia/Shanghai" });
|
|
2528
|
+
// "2024-06-12T04:00:00+08:00[Asia/Shanghai]"
|
|
2529
|
+
|
|
2530
|
+
// 96 hours before an arrival is exact elapsed time, across New York's fall-back.
|
|
2531
|
+
cutoffAt("2024-11-05T17:00:00Z", "PT96H", { timeZone: "America/New_York" });
|
|
2532
|
+
// "2024-11-01T13:00:00-04:00[America/New_York]"
|
|
2533
|
+
|
|
2534
|
+
// A cut-off in an hour the clock skipped is refused, not moved.
|
|
2535
|
+
cutoffAt("2024-03-11T22:00:00Z", "P1D", { timeZone: "America/New_York", atLocalTime: "02:30" }); // ""
|
|
2536
|
+
|
|
2537
|
+
isPastCutoff("2024-06-12T16:00:00Z", "2024-06-12T17:00:00+02:00[Europe/Amsterdam]"); // true
|
|
2538
|
+
timeToCutoff("2024-06-12T16:00:00Z", "2024-06-12T17:00:00+02:00[Europe/Amsterdam]"); // "-PT1H"
|
|
2539
|
+
timeToCutoff("2024-06-10T15:00:00Z", "2024-06-12T17:00:00+02:00[Europe/Amsterdam]"); // "PT48H"
|
|
2540
|
+
```
|
|
2541
|
+
|
|
2542
|
+
- **`atLocalTime` pins the time of day.** With it, only the day the offset lands on matters: the
|
|
2543
|
+
offset's exact part (hours and smaller) comes off the anchor's instant, its calendar part
|
|
2544
|
+
(years, months, weeks, days) off that local date, and the cut-off is `atLocalTime` on that day.
|
|
2545
|
+
The exact part comes off first, so it can change the day: from midnight, `P2DT0.000000001S`
|
|
2546
|
+
lands a day before `P2D`. Without it, the offset is taken off as Temporal's `ZonedDateTime#subtract` takes it: `P2D`
|
|
2547
|
+
keeps the anchor's wall clock, and `PT96H` is 96 elapsed hours.
|
|
2548
|
+
- **The anchor is exact and is the caller's event.** An instant (`Z` or an offset) or a zoned
|
|
2549
|
+
string; its bracket never supplies the zone, and `timeZone` (an IANA identifier or a fixed
|
|
2550
|
+
offset) is the local frame. `cutoffAt` never guesses whether a rule counts from loading, departure or arrival.
|
|
2551
|
+
The functions are pure, so a rescheduled departure is a new call, not a stored cut-off to fix.
|
|
2552
|
+
- **Non-business days roll as you say.** With `calendar` (a `BusinessCalendar`), a cut-off on a
|
|
2553
|
+
weekend or holiday moves by `roll`, as `rollDate` moves it, and keeps its local time of day.
|
|
2554
|
+
Every industry answers "what if it lands on a non-working day" differently, so the convention
|
|
2555
|
+
is always explicit and there is no default: `calendar` without `roll`, or `roll` without
|
|
2556
|
+
`calendar`, returns `""`. `calendar.timeZone` is not read. `"endOfMonth"` is `rollDate`'s
|
|
2557
|
+
schedule tool, not a cut-off convention.
|
|
2558
|
+
- **The order is not checked.** `cutoffAt` does not check that the cut-off is before the anchor.
|
|
2559
|
+
A negative offset, `P0D` with a late `atLocalTime`, `"following"` or `"endOfMonth"` can each
|
|
2560
|
+
put it after.
|
|
2561
|
+
- **Local wall times resolve for a deadline.** `atLocalTime`, the calendar part of an unpinned
|
|
2562
|
+
offset and a rolled day are wall times. An ambiguous one (a fall-back hour the clock ran
|
|
2563
|
+
through twice) takes the earlier instant, the first occurrence, as RFC 5545 §3.3.5 reads a
|
|
2564
|
+
repeated local time. A nonexistent one (a spring-forward hour, a deleted day) returns `""`
|
|
2565
|
+
rather than shifting the deadline.
|
|
2566
|
+
- **`cutoffSchedule` is all or nothing.** Each entry is `cutoffAt` with its own `atLocalTime`;
|
|
2567
|
+
the result is sorted by instant, and entries on the same instant keep their order. One invalid
|
|
2568
|
+
entry returns `[]`, never a stack with a deadline missing.
|
|
2569
|
+
- **The window closes at the cut-off.** `isPastCutoff` is `true` from the cut-off instant on, and
|
|
2570
|
+
`timeToCutoff` is `PT0S` there, exact hours before it and negative after it. Both compare
|
|
2571
|
+
instants, and `now` is always the caller's. Both return their sentinel (`false`, `""`) for
|
|
2572
|
+
`cutoffAt`'s `""`, so check the cut-off first.
|
|
2573
|
+
- Also exported: the `CutoffOptions`, `Cutoff`, `CutoffTime` and `CutoffScheduleOptions` types.
|
|
2574
|
+
|
|
2575
|
+
### Punctuality, timestamp classes and the next departure
|
|
2576
|
+
|
|
2577
|
+
Every mode publishes a plan and records what happened, and measures the gap the same way. Only
|
|
2578
|
+
the tolerance differs, and it is the caller's. `scheduleDeviation` measures the gap,
|
|
2579
|
+
`classifyPunctuality` judges it against a tolerance, and `punctualityRate` counts a list of
|
|
2580
|
+
arrivals under one. One field often holds four kinds of time (planned, estimated, requested and
|
|
2581
|
+
actual), so `bestAvailable` returns the class with the value, and `estimateDrift` measures how far
|
|
2582
|
+
an estimate moved. `nextDeparture` finds the departure a connection can make.
|
|
2583
|
+
|
|
2584
|
+
```typescript
|
|
2585
|
+
import {
|
|
2586
|
+
bestAvailable,
|
|
2587
|
+
classifyPunctuality,
|
|
2588
|
+
estimateDrift,
|
|
2589
|
+
nextDeparture,
|
|
2590
|
+
punctualityRate,
|
|
2591
|
+
scheduleDeviation,
|
|
2592
|
+
type TimestampEvent,
|
|
2593
|
+
} from "@northguild/gmt";
|
|
2594
|
+
|
|
2595
|
+
scheduleDeviation("2024-06-15T10:00:00Z", "2024-06-15T10:14:00Z"); // "PT14M"
|
|
2596
|
+
scheduleDeviation("2024-06-15T10:00:00Z", "2024-06-15T09:55:00Z"); // "-PT5M" (early)
|
|
2597
|
+
scheduleDeviation("2024-06-15T10:00:00Z", "2024-06-17T11:30:00Z"); // "PT49H30M" (hours, never days)
|
|
2598
|
+
// The same wall time, an hour later across New York's fall-back: 60 minutes, not 0.
|
|
2599
|
+
scheduleDeviation("2024-11-03T01:30:00-04:00[America/New_York]", "2024-11-03T01:30:00-05:00[America/New_York]");
|
|
2600
|
+
// "PT1H"
|
|
2601
|
+
|
|
2602
|
+
// A 15-minute tolerance. Exactly 15 minutes is late: on time is strictly inside.
|
|
2603
|
+
classifyPunctuality("2024-06-15T10:00:00Z", "2024-06-15T10:14:59Z", { late: "PT15M" }); // "onTime"
|
|
2604
|
+
classifyPunctuality("2024-06-15T10:00:00Z", "2024-06-15T10:15:00Z", { late: "PT15M" }); // "late"
|
|
2605
|
+
// Without `early`, an early arrival is on time. With it, 11 minutes early is early.
|
|
2606
|
+
classifyPunctuality("2024-06-15T10:00:00Z", "2024-06-15T09:49:00Z", { late: "PT15M" }); // "onTime"
|
|
2607
|
+
classifyPunctuality("2024-06-15T10:00:00Z", "2024-06-15T09:49:00Z", { late: "PT15M", early: "PT10M" }); // "early"
|
|
2608
|
+
// The same 90-minute delay under a 60-minute and a 120-minute tolerance.
|
|
2609
|
+
classifyPunctuality("2024-06-15T10:00:00Z", "2024-06-15T11:30:00Z", { late: "PT60M" }); // "late"
|
|
2610
|
+
classifyPunctuality("2024-06-15T10:00:00Z", "2024-06-15T11:30:00Z", { late: "PT120M" }); // "onTime"
|
|
2611
|
+
classifyPunctuality("2024-06-15T10:00:00Z", "2024-06-15T10:14:00Z", { late: "P1W" }); // null (weeks have no exact length)
|
|
2612
|
+
|
|
2613
|
+
punctualityRate([
|
|
2614
|
+
{ planned: "2024-06-15T10:00:00Z", actual: "2024-06-15T10:05:00Z" },
|
|
2615
|
+
{ planned: "2024-06-15T10:00:00Z", actual: "2024-06-15T10:14:00Z" },
|
|
2616
|
+
{ planned: "2024-06-15T10:00:00Z", actual: "2024-06-15T10:16:00Z" },
|
|
2617
|
+
{ planned: "2024-06-15T10:00:00Z", actual: "2024-06-15T09:57:00Z" },
|
|
2618
|
+
], { late: "PT15M" }); // { onTime: 3, total: 4, rate: 0.75 }
|
|
2619
|
+
punctualityRate([], { late: "PT15M" }); // null
|
|
2620
|
+
|
|
2621
|
+
// An estimate recorded after the actual is still only an estimate.
|
|
2622
|
+
bestAvailable([
|
|
2623
|
+
{ classifier: "ACT", at: "2024-06-15T12:52:00Z", recordedAt: "2024-06-15T12:53:00Z" },
|
|
2624
|
+
{ classifier: "EST", at: "2024-06-15T13:05:00Z", recordedAt: "2024-06-15T14:00:00Z" },
|
|
2625
|
+
]); // { at: "2024-06-15T12:52:00Z", classifier: "ACT" }
|
|
2626
|
+
// Never an estimate when a request exists.
|
|
2627
|
+
bestAvailable([
|
|
2628
|
+
{ classifier: "EST", at: "2024-06-15T12:40:00Z", recordedAt: "2024-06-14T00:00:00Z" },
|
|
2629
|
+
{ classifier: "REQ", at: "2024-06-15T12:30:00Z", recordedAt: "2024-06-12T00:00:00Z" },
|
|
2630
|
+
]); // { at: "2024-06-15T12:30:00Z", classifier: "REQ" }
|
|
2631
|
+
|
|
2632
|
+
// Three revisions of one arrival estimate: it slid nine hours.
|
|
2633
|
+
const estimates: TimestampEvent[] = [
|
|
2634
|
+
{ classifier: "EST", at: "2024-06-20T08:00:00Z", recordedAt: "2024-06-01T00:00:00Z" },
|
|
2635
|
+
{ classifier: "EST", at: "2024-06-20T12:00:00Z", recordedAt: "2024-06-05T00:00:00Z" },
|
|
2636
|
+
{ classifier: "EST", at: "2024-06-20T17:00:00Z", recordedAt: "2024-06-10T00:00:00Z" },
|
|
2637
|
+
];
|
|
2638
|
+
estimateDrift(estimates, { tolerance: "PT8H" });
|
|
2639
|
+
// { first: "2024-06-20T08:00:00Z", last: "2024-06-20T17:00:00Z", drift: "PT9H", revisions: 3, exceedsTolerance: true }
|
|
2640
|
+
estimateDrift(estimates.slice(0, 1)); // null (no drift from one estimate)
|
|
2641
|
+
|
|
2642
|
+
const timetable = ["2024-06-15T08:00:00Z", "2024-06-15T09:30:00Z", "2024-06-15T11:00:00Z"];
|
|
2643
|
+
nextDeparture("2024-06-15T09:00:00Z", timetable); // "2024-06-15T09:30:00Z"
|
|
2644
|
+
nextDeparture("2024-06-15T09:00:00Z", timetable, { minimumConnection: "PT45M" }); // "2024-06-15T11:00:00Z"
|
|
2645
|
+
nextDeparture("2024-06-15T09:00:00Z", ["2024-06-15T09:30:00"]); // "" (a timetable entry needs an offset)
|
|
2646
|
+
|
|
2647
|
+
// Every 20 minutes from 06:00, up to but never at 09:00.
|
|
2648
|
+
const shuttle = { headway: "PT20M", from: "2024-06-15T06:00:00+02:00", to: "2024-06-15T09:00:00+02:00" };
|
|
2649
|
+
nextDeparture("2024-06-15T06:05:00+02:00", shuttle); // "2024-06-15T06:20:00+02:00"
|
|
2650
|
+
nextDeparture("2024-06-15T09:00:00+02:00", shuttle); // ""
|
|
2651
|
+
```
|
|
2652
|
+
|
|
2653
|
+
- **A deviation is exact elapsed time.** `scheduleDeviation` is `actual` minus `planned` with
|
|
2654
|
+
hours as the largest unit: positive when late, negative when early, `"PT0S"` on the plan. A
|
|
2655
|
+
delay across a DST transition is measured in real minutes, never wall-clock ones. Both times
|
|
2656
|
+
are an instant (`Z` or an offset) or a zoned string; the zone each is written in does not
|
|
2657
|
+
matter. The offset fixes the instant, and a bracketed zone resolves only a rounded one: an
|
|
2658
|
+
offset written to the minute that is the zone's sub-minute offset rounded
|
|
2659
|
+
(`-00:45[Africa/Monrovia]`, for −00:44:30) names the instant the zone gives, so a zoned string
|
|
2660
|
+
GMT wrote is read back as the same moment, except a wall time repeated inside a sub-minute
|
|
2661
|
+
offset change, which reads as its first pass. To show a schedule's local time, use `etaAtZone`.
|
|
2662
|
+
- **"On time" needs a stated tolerance.** GMT holds no default. A `PunctualityTolerance` is
|
|
2663
|
+
`{ late, early? }` as ISO 8601 durations of exact time: a day is 24 hours, and years, months,
|
|
2664
|
+
weeks or a negative value return `null`. Tolerances differ by mode (15 minutes, a 60/120-minute
|
|
2665
|
+
pair, a day), and each is the same call with different numbers.
|
|
2666
|
+
- **Both edges belong to the outside.** A deviation of `late` or more is late; with `early`, one
|
|
2667
|
+
of `-early` or less is early. Without `early`, every early arrival is on time. `late: "PT0S"`
|
|
2668
|
+
makes an arrival exactly on the plan late. This half-open convention is GMT's own, stated here.
|
|
2669
|
+
`classifyPunctuality` returns `null` on invalid input.
|
|
2670
|
+
- **A rate is a count under one tolerance.** `punctualityRate` reads the tolerance once and
|
|
2671
|
+
judges every pair under it. Only `"onTime"` counts, so with an `early` tolerance an early
|
|
2672
|
+
arrival is not on time. `rate` is `onTime / total`, not rounded. An empty list, or any invalid
|
|
2673
|
+
pair, returns `null`, never a rate over the rest. Percentiles and causes are the consumer's.
|
|
2674
|
+
- **The class travels with the value.** A `TimestampEvent` is `{ classifier, at, recordedAt }`,
|
|
2675
|
+
with `classifier` one of DCSA's `PLN`, `EST`, `REQ` and `ACT`. `bestAvailable` picks an `ACT`
|
|
2676
|
+
whenever one exists, else `PLN`, else `REQ`, else `EST`, and within that class the latest
|
|
2677
|
+
`recordedAt`. This order is GMT's and is not configurable. It reads, from its most settled
|
|
2678
|
+
end, the estimated, requested, planned, then actual pattern that
|
|
2679
|
+
[DCSA's Port Call standard](https://reference.dcsa.org/content/standards/releases/port-call/v2-0-0/port-call-v2-0-0-use-cases)
|
|
2680
|
+
describes for some port call services; DCSA defines the classes and that pattern, not a
|
|
2681
|
+
selection rule. `at` is echoed exactly as written. An empty list or any invalid event returns
|
|
2682
|
+
`null`.
|
|
2683
|
+
- **Drift needs two estimates.** `estimateDrift` reads only the `EST` records, in `recordedAt`
|
|
2684
|
+
order, and reports the last `at` minus the first, negative when the estimate moved earlier. It
|
|
2685
|
+
returns `null` with fewer than two, since one estimate has no drift. With `tolerance`,
|
|
2686
|
+
`exceedsTolerance` is `true` when the drift in either direction is greater than it; exactly the
|
|
2687
|
+
tolerance is `false`. Without it, `null`.
|
|
2688
|
+
- **`nextDeparture` is timetable lookup, not routing.** It returns the first departure at or
|
|
2689
|
+
after `after` plus `minimumConnection` (default `"PT0S"`, exact time, not negative); one exactly
|
|
2690
|
+
at that threshold is made. A list entry is returned exactly as written, so it can go straight
|
|
2691
|
+
into `scheduleDelivery` as a leg's `departure`. Every moment (`after`, each entry, `from` and
|
|
2692
|
+
`to`) must carry its offset, as `nextOpenAt` requires: a wall time without one returns `""`,
|
|
2693
|
+
because in a repeated fall-back hour it names two departures. One invalid entry returns `""`.
|
|
2694
|
+
- **A headway window is half-open.** `{ headway, from, to }` has the shape of a GTFS
|
|
2695
|
+
[`frequencies.txt`](https://github.com/google/transit/blob/master/gtfs/spec/en/reference.md#frequenciestxt) row. Departures are `from` plus whole headways in exact time, up to but never
|
|
2696
|
+
at `to`, and are computed from `from`, not stepped, so a long window costs nothing. Across a DST
|
|
2697
|
+
transition they keep their spacing and the wall clock shifts. The result is written the way
|
|
2698
|
+
`from` was written.
|
|
2699
|
+
- Also exported: the `TimestampClass`, `TimestampEvent`, `PunctualityTolerance`, `Punctuality`,
|
|
2700
|
+
`PlannedActual`, `OnTimeRate`, `ClassifiedTimestamp`, `EstimateDriftOptions`, `DriftReport`,
|
|
2701
|
+
`Headway` and `NextDepartureOptions` types.
|
|
2702
|
+
|
|
2703
|
+
### Free time and demurrage
|
|
2704
|
+
|
|
2705
|
+
Free time is the money calculation in container logistics, and none of its terms is a fact about
|
|
2706
|
+
the port or fixed by a world standard. How many days are free, whether the day of discharge is
|
|
2707
|
+
free day one, how free days and charged days are counted, and which clock a charge runs on are set
|
|
2708
|
+
by the carrier's tariff and the service contract. DCSA defines what demurrage, detention and
|
|
2709
|
+
storage are, but not how their days are counted. So every term is a parameter, no counting term has a default, and what comes back is the free-time window and the specific dates charged.
|
|
2710
|
+
|
|
2711
|
+
```typescript
|
|
2712
|
+
import { chargeableDays, demurrageClock, freeTimeExpiry } from "@northguild/gmt";
|
|
2713
|
+
|
|
2714
|
+
const tariff = { basis: "calendar", chargeBasis: "calendar", timeZone: "America/New_York", firstDay: "eventDay" };
|
|
2715
|
+
|
|
2716
|
+
// A Friday afternoon discharge in New York with three free calendar days: the weekend is
|
|
2717
|
+
// consumed, and free time ends at Monday 00:00 local.
|
|
2718
|
+
freeTimeExpiry("2024-06-14T19:00:00Z", 3, tariff);
|
|
2719
|
+
// { freeTimeStart: "2024-06-14", lastFreeDay: "2024-06-16", expiresAt: "2024-06-17T04:00:00Z" }
|
|
2720
|
+
|
|
2721
|
+
// The same tariff read the other way: one more day. firstDay has no default.
|
|
2722
|
+
freeTimeExpiry("2024-06-14T19:00:00Z", 3, { ...tariff, firstDay: "nextDay" });
|
|
2723
|
+
// { freeTimeStart: "2024-06-15", lastFreeDay: "2024-06-17", expiresAt: "2024-06-18T04:00:00Z" }
|
|
2724
|
+
freeTimeExpiry("2024-06-14T19:00:00Z", 3, { basis: "calendar", timeZone: "America/New_York" }); // null
|
|
2725
|
+
|
|
2726
|
+
// Working days skip the weekend, and a terminal holiday moves expiry a day.
|
|
2727
|
+
const terminal = { weekend: [6, 7], holidays: [], timeZone: "America/New_York" };
|
|
2728
|
+
freeTimeExpiry("2024-06-14T19:00:00Z", 3, { ...tariff, basis: "working", calendar: terminal });
|
|
2729
|
+
// { freeTimeStart: "2024-06-14", lastFreeDay: "2024-06-18", expiresAt: "2024-06-19T04:00:00Z" }
|
|
2730
|
+
freeTimeExpiry("2024-06-14T19:00:00Z", 3, { ...tariff, basis: "working", calendar: { ...terminal, holidays: ["2024-06-17"] } });
|
|
2731
|
+
// { freeTimeStart: "2024-06-14", lastFreeDay: "2024-06-19", expiresAt: "2024-06-20T04:00:00Z" }
|
|
2732
|
+
|
|
2733
|
+
// Expiry is half-open: out exactly as free time ends is free, one second later is a charged day,
|
|
2734
|
+
// and the date is listed.
|
|
2735
|
+
chargeableDays("2024-06-14T19:00:00Z", "2024-06-17T04:00:00Z", 3, tariff);
|
|
2736
|
+
// { freeDaysUsed: 3, chargeableDays: 0, expiresAt: "2024-06-17T04:00:00Z",
|
|
2737
|
+
// chargedDates: [], byTier: [{ from: 1, to: null, days: 0 }] }
|
|
2738
|
+
chargeableDays("2024-06-14T19:00:00Z", "2024-06-17T04:00:01Z", 3, tariff);
|
|
2739
|
+
// { freeDaysUsed: 3, chargeableDays: 1, expiresAt: "2024-06-17T04:00:00Z",
|
|
2740
|
+
// chargedDates: ["2024-06-17"], byTier: [{ from: 1, to: null, days: 1 }] }
|
|
2741
|
+
|
|
2742
|
+
// Charged days are counted on their own basis. Where a tariff grants free time in working days,
|
|
2743
|
+
// the days after it are mostly charged as calendar days; some tariffs charge working days only.
|
|
2744
|
+
const juneteenth = { ...terminal, holidays: ["2024-06-19"] };
|
|
2745
|
+
chargeableDays("2024-06-14T19:00:00Z", "2024-06-24T15:00:00Z", 3, { ...tariff, basis: "working", calendar: juneteenth });
|
|
2746
|
+
// { freeDaysUsed: 3, chargeableDays: 6, expiresAt: "2024-06-19T04:00:00Z",
|
|
2747
|
+
// chargedDates: ["2024-06-19", "2024-06-20", "2024-06-21", "2024-06-22", "2024-06-23", "2024-06-24"],
|
|
2748
|
+
// byTier: [{ from: 1, to: null, days: 6 }] }
|
|
2749
|
+
chargeableDays("2024-06-14T19:00:00Z", "2024-06-24T15:00:00Z", 3, { ...tariff, basis: "working", chargeBasis: "working", calendar: juneteenth });
|
|
2750
|
+
// { freeDaysUsed: 3, chargeableDays: 3, expiresAt: "2024-06-19T04:00:00Z",
|
|
2751
|
+
// chargedDates: ["2024-06-20", "2024-06-21", "2024-06-24"],
|
|
2752
|
+
// byTier: [{ from: 1, to: null, days: 3 }] }
|
|
2753
|
+
|
|
2754
|
+
// Tiers are day bands, not rates: twelve charged days in bands of 5, 5 and the rest.
|
|
2755
|
+
chargeableDays("2024-06-14T19:00:00Z", "2024-06-28T15:00:00Z", 3, { ...tariff, tiers: [5, 10] });
|
|
2756
|
+
// { freeDaysUsed: 3, chargeableDays: 12, expiresAt: "2024-06-17T04:00:00Z",
|
|
2757
|
+
// chargedDates: ["2024-06-17", "2024-06-18", "2024-06-19", "2024-06-20", "2024-06-21",
|
|
2758
|
+
// "2024-06-22", "2024-06-23", "2024-06-24", "2024-06-25", "2024-06-26",
|
|
2759
|
+
// "2024-06-27", "2024-06-28"],
|
|
2760
|
+
// byTier: [{ from: 1, to: 5, days: 5 }, { from: 6, to: 10, days: 5 }, { from: 11, to: null, days: 2 }] }
|
|
2761
|
+
|
|
2762
|
+
// Which two events a charge runs between, on the import or the export leg.
|
|
2763
|
+
const imported = [
|
|
2764
|
+
{ type: "discharged", at: "2024-06-14T19:00:00Z" },
|
|
2765
|
+
{ type: "available", at: "2024-06-15T12:00:00Z" },
|
|
2766
|
+
{ type: "gatedOut", at: "2024-06-20T14:30:00Z" },
|
|
2767
|
+
{ type: "emptyReturned", at: "2024-06-27T09:00:00Z" },
|
|
2768
|
+
];
|
|
2769
|
+
demurrageClock(imported, "demurrage", { direction: "import" }); // { start: "2024-06-14T19:00:00Z", end: "2024-06-20T14:30:00Z" }
|
|
2770
|
+
demurrageClock(imported, "detention", { direction: "import" }); // { start: "2024-06-20T14:30:00Z", end: "2024-06-27T09:00:00Z" }
|
|
2771
|
+
demurrageClock(imported, "combined", { direction: "import" }); // { start: "2024-06-14T19:00:00Z", end: "2024-06-27T09:00:00Z" }
|
|
2772
|
+
demurrageClock(imported, "demurrage", { direction: "import", startEvent: "available" }); // { start: "2024-06-15T12:00:00Z", end: "2024-06-20T14:30:00Z" }
|
|
2773
|
+
const exported = [
|
|
2774
|
+
{ type: "emptyReleased", at: "2024-07-01T08:00:00Z" },
|
|
2775
|
+
{ type: "gatedIn", at: "2024-07-05T16:00:00Z" },
|
|
2776
|
+
{ type: "loaded", at: "2024-07-09T03:00:00Z" },
|
|
2777
|
+
];
|
|
2778
|
+
demurrageClock(exported, "demurrage", { direction: "export" }); // { start: "2024-07-05T16:00:00Z", end: "2024-07-09T03:00:00Z" }
|
|
2779
|
+
demurrageClock(exported, "detention", { direction: "export" }); // { start: "2024-07-01T08:00:00Z", end: "2024-07-05T16:00:00Z" }
|
|
2780
|
+
demurrageClock([{ type: "discharged", at: "2024-06-14T19:00:00Z" }], "demurrage", { direction: "import" }); // null (no gate-out yet)
|
|
2781
|
+
```
|
|
2782
|
+
|
|
2783
|
+
- **Days are the terminal's local days.** `clockStart` and `clockEnd` are instants; their local
|
|
2784
|
+
dates in `options.timeZone` are what is counted, over the zone's real day boundaries, the same
|
|
2785
|
+
ones `dwellTime` and `floorToZone` find. A 23- or 25-hour day is one day, a date the zone deleted
|
|
2786
|
+
is never a free or charged day, and a date the clock falls back into counts once. On the calendar
|
|
2787
|
+
basis with `firstDay: "eventDay"`, `freeDaysUsed + chargeableDays` is exactly
|
|
2788
|
+
`dwellTime(...).calendarDays` for the same dwell, except that a fall-back re-entering the day
|
|
2789
|
+
before the event day (Goose Bay, 7 November 2010) is a date `dwellTime` counts and a tariff
|
|
2790
|
+
never does.
|
|
2791
|
+
- **`firstDay` has no default.** `"eventDay"` makes the event day free day one; `"nextDay"` starts
|
|
2792
|
+
free time the following counted day. The two differ by a full day of charges, so omitting it
|
|
2793
|
+
returns `null`.
|
|
2794
|
+
- **`basis` counts free days; `chargeBasis` counts charged days.** `"calendar"` counts every local
|
|
2795
|
+
day; `"working"` counts only the working days of `options.calendar`, a `BusinessCalendar` (its
|
|
2796
|
+
weekend and holidays; its `timeZone` is not read), and returns `null` without one. Many tariffs
|
|
2797
|
+
count both in calendar days. Where a tariff grants free time in working days, the days after it
|
|
2798
|
+
are mostly charged as calendar days; some tariffs charge working days only. Neither has a default.
|
|
2799
|
+
"Calendar days excluding bank holidays" is `"working"` with an empty `weekend`.
|
|
2800
|
+
- **Expiry is half-open.** `expiresAt` is the first instant of the local day after `lastFreeDay`, as
|
|
2801
|
+
a UTC instant. A gate-out at exactly `expiresAt` is not a chargeable day; one nanosecond later is.
|
|
2802
|
+
- **`chargedDates` makes the count auditable.** It is the list a carrier's day-numbered tariff grid
|
|
2803
|
+
is applied to, and the dates an itemised invoice can list.
|
|
2804
|
+
- **Tiers are day bands, not rates.** `tiers: [5, 10]` names days 1–5, 6–10 and 11 onward; `byTier`
|
|
2805
|
+
says how many charged days fell in each band, empty bands included, so a rate table applies by
|
|
2806
|
+
index. Omitted, it is the single open band. GMT computes days, never money.
|
|
2807
|
+
- **`freeDays: 0`** is a tariff with no free time: `chargeableDays` charges every counted day from day
|
|
2808
|
+
one, and `freeTimeExpiry` returns `null` because there is no last free day to name.
|
|
2809
|
+
- **`demurrageClock` selects the pair, per leg.** DCSA's glossary puts demurrage inside the terminal
|
|
2810
|
+
or depot and detention outside it; the carriers' published tariffs agree on the events. Import: demurrage and
|
|
2811
|
+
storage run from `startEvent` (`"discharged"` by default, or `"available"`) to `gatedOut`, detention
|
|
2812
|
+
from `gatedOut` to `emptyReturned`, and the combined clock from `startEvent` to `emptyReturned`.
|
|
2813
|
+
Export: demurrage and storage run from `gatedIn` to `loaded`, detention from `emptyReleased` to
|
|
2814
|
+
`gatedIn`, and the combined clock from `emptyReleased` to `loaded`. `direction` has no default. The
|
|
2815
|
+
event names follow DCSA's Track & Trace equipment events (DISC, GTOT, GTIN, LOAD). A required event
|
|
2816
|
+
that is missing or present twice, or an end before its start, returns `null`, and the result is an
|
|
2817
|
+
`Interval` in the caller's own strings, ready for `chargeableDays(start, end, …)`.
|
|
2818
|
+
|
|
2819
|
+
### Billing deadlines
|
|
2820
|
+
|
|
2821
|
+
A billing regime or service contract can set up to three windows around a demurrage or
|
|
2822
|
+
detention invoice: one to issue it, counted from an anchor date; one to dispute it, counted from
|
|
2823
|
+
issuance; and one to resolve the dispute, counted from the request, unless the parties
|
|
2824
|
+
agree a date instead. The number of days in each is the caller's fact; GMT carries none of them. The arithmetic
|
|
2825
|
+
is the function's: each deadline is a date, counted in calendar days from a date, with the anchor
|
|
2826
|
+
as day zero and the deadline day itself inside the window.
|
|
2827
|
+
|
|
2828
|
+
```typescript
|
|
2829
|
+
import { billingTimeline, convertUtcToPlainDate } from "@northguild/gmt";
|
|
2830
|
+
|
|
2831
|
+
const windows = { issueDays: 30, disputeDays: 30, resolutionDays: 30 };
|
|
2832
|
+
|
|
2833
|
+
// Charges last accrued on 1 March. Day 30 is the last day by the deadline; day 31 is not.
|
|
2834
|
+
billingTimeline({ anchorOn: "2026-03-01", invoiceIssuedOn: "2026-03-31" }, windows);
|
|
2835
|
+
// { invoiceDeadline: "2026-03-31", issuedByDeadline: true, disputeDeadline: "2026-04-30",
|
|
2836
|
+
// requestedByDeadline: null, resolutionDeadline: null }
|
|
2837
|
+
billingTimeline({ anchorOn: "2026-03-01", invoiceIssuedOn: "2026-04-01" }, windows);
|
|
2838
|
+
// { invoiceDeadline: "2026-03-31", issuedByDeadline: false, disputeDeadline: "2026-05-01",
|
|
2839
|
+
// requestedByDeadline: null, resolutionDeadline: null }
|
|
2840
|
+
|
|
2841
|
+
// No invoice yet: a forecast. Only the invoice deadline is known.
|
|
2842
|
+
billingTimeline({ anchorOn: "2026-03-01" }, windows);
|
|
2843
|
+
// { invoiceDeadline: "2026-03-31", issuedByDeadline: null, disputeDeadline: null,
|
|
2844
|
+
// requestedByDeadline: null, resolutionDeadline: null }
|
|
2845
|
+
|
|
2846
|
+
// A dispute received on the dispute deadline, and one received a day later with a resolution
|
|
2847
|
+
// date the parties agreed, which replaces the computed one.
|
|
2848
|
+
billingTimeline({ anchorOn: "2026-03-01", invoiceIssuedOn: "2026-03-20", requestReceivedOn: "2026-04-19" }, windows);
|
|
2849
|
+
// { invoiceDeadline: "2026-03-31", issuedByDeadline: true, disputeDeadline: "2026-04-19",
|
|
2850
|
+
// requestedByDeadline: true, resolutionDeadline: "2026-05-19" }
|
|
2851
|
+
billingTimeline({ anchorOn: "2026-03-01", invoiceIssuedOn: "2026-03-20", requestReceivedOn: "2026-04-20" }, { ...windows, agreedResolutionOn: "2026-06-01" });
|
|
2852
|
+
// { invoiceDeadline: "2026-03-31", issuedByDeadline: true, disputeDeadline: "2026-04-19",
|
|
2853
|
+
// requestedByDeadline: false, resolutionDeadline: "2026-06-01" }
|
|
2854
|
+
|
|
2855
|
+
// A re-bill is anchored on the invoice it received (issued 10 March), not on the charge.
|
|
2856
|
+
billingTimeline({ anchorOn: "2026-03-10", invoiceIssuedOn: "2026-04-05" }, windows);
|
|
2857
|
+
// { invoiceDeadline: "2026-04-09", issuedByDeadline: true, disputeDeadline: "2026-05-05",
|
|
2858
|
+
// requestedByDeadline: null, resolutionDeadline: null }
|
|
2859
|
+
|
|
2860
|
+
// A 14/14/45 service contract through the same chain: the numbers are the caller's.
|
|
2861
|
+
billingTimeline({ anchorOn: "2026-03-01", invoiceIssuedOn: "2026-03-05", requestReceivedOn: "2026-03-18" }, { issueDays: 14, disputeDays: 14, resolutionDays: 45 });
|
|
2862
|
+
// { invoiceDeadline: "2026-03-15", issuedByDeadline: true, disputeDeadline: "2026-03-19",
|
|
2863
|
+
// requestedByDeadline: true, resolutionDeadline: "2026-05-02" }
|
|
2864
|
+
|
|
2865
|
+
// Windows have no defaults.
|
|
2866
|
+
billingTimeline({ anchorOn: "2026-03-01", invoiceIssuedOn: "2026-03-31" }, { disputeDays: 30, resolutionDays: 30 }); // null
|
|
2867
|
+
|
|
2868
|
+
// Deadlines are dates. Reduce an instant to the billing party's local date first.
|
|
2869
|
+
convertUtcToPlainDate("2024-06-17T04:00:01Z", { timeZone: "America/New_York" }); // "2024-06-17"
|
|
2870
|
+
```
|
|
2871
|
+
|
|
2872
|
+
- **Day zero is the anchor and the deadline is `anchor + days`** on the ISO calendar
|
|
2873
|
+
(`Temporal.PlainDate.add`); a date is by the deadline when it is on or before it
|
|
2874
|
+
(`Temporal.PlainDate.compare(date, deadline) <= 0`). That is the function's stated contract, not
|
|
2875
|
+
a reading of any rule; a regime that counts differently passes a different number. Thirty days
|
|
2876
|
+
across a leap day, a month end, a year end or a DST change is thirty dates, never thirty times
|
|
2877
|
+
24 hours: `2028-01-30` plus 30 is `2028-02-29`.
|
|
2878
|
+
- **Deadlines are dates, never instants.** Reduce an instant to the billing party's local date
|
|
2879
|
+
first with `convertUtcToPlainDate(instant, { timeZone })`; the function does not guess a zone,
|
|
2880
|
+
and a date-time or an instant passed as a date returns `null`.
|
|
2881
|
+
- **Windows have no defaults.** Each is a safe integer of at least `0`; a missing, negative or
|
|
2882
|
+
non-integer window returns `null`, because a silently defaulted window is a wrong deadline. The
|
|
2883
|
+
same rule as `firstDay`.
|
|
2884
|
+
- **The anchor is whatever date the caller counts from.** The last date a charge accrued is
|
|
2885
|
+
`chargeableDays(...).chargedDates.at(-1)`. A party re-billing a charge it was itself billed
|
|
2886
|
+
passes the issuance date of the invoice it received; the chain is the same.
|
|
2887
|
+
- **The chain fills in as its dates exist.** With only `anchorOn` the result is a forecast:
|
|
2888
|
+
`invoiceDeadline` is set and every other field is `null`. An invoice date sets
|
|
2889
|
+
`issuedByDeadline` and `disputeDeadline`; a request date sets `requestedByDeadline` and
|
|
2890
|
+
`resolutionDeadline`. A request without an invoice, or dated before it, is a data error and
|
|
2891
|
+
returns `null`; an invoice dated before the anchor is allowed, because an invoice may be issued
|
|
2892
|
+
while charges still accrue.
|
|
2893
|
+
- **An agreed date replaces the computed resolution deadline.** `agreedResolutionOn` must be a
|
|
2894
|
+
valid date on or after `requestReceivedOn`, else `null`; it is validated even when no request
|
|
2895
|
+
exists, but cannot act without one. Every emitted date is bare ISO, so an input annotation such
|
|
2896
|
+
as `[u-ca=iso8601]` does not reach the output.
|
|
2897
|
+
- **GMT computes dates, not liability.** `issuedByDeadline` and `requestedByDeadline` compare
|
|
2898
|
+
dates and say nothing else. Whether a charge is payable, whether a dispute must be heard, and
|
|
2899
|
+
any consequence are the consumer's, the sibling of "GMT computes days, never money".
|
|
2900
|
+
|
|
2182
2901
|
## API Surface
|
|
2183
2902
|
|
|
2184
2903
|
For the complete API listing, see the namespace documentation on GitHub:
|
|
@@ -2189,8 +2908,10 @@ For the complete API listing, see the namespace documentation on GitHub:
|
|
|
2189
2908
|
- [Unix API](https://github.com/northguild/gmt/tree/main/packages/gmt/src/unix) — Unix epoch utilities
|
|
2190
2909
|
- [Precision API](https://github.com/northguild/gmt/tree/main/packages/gmt/src/precision) — nanosecond instants, JSON transport, storage truncation, NTP / FILETIME / .NET ticks / Excel / PostgreSQL epoch bridges
|
|
2191
2910
|
- [Span API](https://github.com/northguild/gmt/tree/main/packages/gmt/src/span) — elapsed and wall-clock durations as raw numbers
|
|
2192
|
-
- [Calendar API](https://github.com/northguild/gmt/tree/main/packages/gmt/src/calendar) — ISO week and ordinal dates, quarter and fiscal periods, zone-aware bucketing,
|
|
2911
|
+
- [Calendar API](https://github.com/northguild/gmt/tree/main/packages/gmt/src/calendar) — ISO week and ordinal dates, quarter and fiscal periods, zone-aware bucketing, business calendars with holiday sets and roll conventions, and operating hours: open intervals, open or closed now, open time elapsed and SLA deadlines
|
|
2193
2912
|
- [Interval API](https://github.com/northguild/gmt/tree/main/packages/gmt/src/interval) — half-open interval algebra over instants: overlap, intersect, clamp, merge, subtract, split, sum
|
|
2913
|
+
- [Transport API](https://github.com/northguild/gmt/tree/main/packages/gmt/src/transport) — transit legs as exact elapsed time, arrivals rendered in the zone where they land, dwell in local calendar days, crossings, multi-leg delivery schedules, cut-offs, schedule deviation and punctuality, PLN/EST/REQ/ACT timestamp classes, and timetable and headway departures
|
|
2914
|
+
- [Intermodal API](https://github.com/northguild/gmt/tree/main/packages/gmt/src/intermodal) — free time, demurrage and detention: which clock, which start day, calendar or working days, and the charged dates an itemised invoice can list, plus the deadline chain around the invoice with every window a caller parameter
|
|
2194
2915
|
- [Instant API](https://github.com/northguild/gmt/tree/main/packages/gmt/src/instant) — the instant-plus-offset pair, and explicit local-time resolution
|
|
2195
2916
|
- [UTC API](https://github.com/northguild/gmt/tree/main/packages/gmt/src/utc) — UTC instant utilities
|
|
2196
2917
|
- [Regex API](https://github.com/northguild/gmt/tree/main/packages/gmt/src/regex) — composable regex patterns
|