@northguild/gmt 1.18.0 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (593) hide show
  1. package/README.md +55 -2920
  2. package/dist/calendar/calculate/bucketRange.d.ts +10 -2
  3. package/dist/calendar/calculate/bucketRange.js +18 -61
  4. package/dist/calendar/calculate/floorToZone.d.ts +10 -2
  5. package/dist/calendar/calculate/floorToZone.js +15 -7
  6. package/dist/calendar/hours/addOperatingTime.d.ts +6 -0
  7. package/dist/calendar/hours/addOperatingTime.js +7 -1
  8. package/dist/calendar/hours/isOpenAt.d.ts +6 -0
  9. package/dist/calendar/hours/isOpenAt.js +6 -0
  10. package/dist/calendar/hours/nextCloseAt.d.ts +6 -0
  11. package/dist/calendar/hours/nextCloseAt.js +7 -1
  12. package/dist/calendar/hours/nextOpenAt.d.ts +6 -0
  13. package/dist/calendar/hours/nextOpenAt.js +7 -1
  14. package/dist/calendar/hours/operatingIntervals.d.ts +5 -0
  15. package/dist/calendar/hours/operatingIntervals.js +5 -0
  16. package/dist/calendar/hours/operatingTimeBetween.d.ts +5 -0
  17. package/dist/calendar/hours/operatingTimeBetween.js +5 -0
  18. package/dist/calendar/hours/recurringWindows.d.ts +8 -1
  19. package/dist/calendar/hours/recurringWindows.js +12 -4
  20. package/dist/duration/calculate/durationFromUnits.d.ts +47 -0
  21. package/dist/duration/calculate/durationFromUnits.js +67 -0
  22. package/dist/duration/calculate/getDurationUnit.d.ts +2 -2
  23. package/dist/duration/calculate/getDurationUnit.js +2 -2
  24. package/dist/duration/calculate/index.d.ts +3 -1
  25. package/dist/duration/calculate/index.js +3 -1
  26. package/dist/duration/calculate/negateDuration.d.ts +1 -1
  27. package/dist/duration/calculate/negateDuration.js +1 -1
  28. package/dist/duration/calculate/{durationAs.d.ts → totalDuration.d.ts} +25 -19
  29. package/dist/duration/calculate/{durationAs.js → totalDuration.js} +25 -19
  30. package/dist/duration/calculate/totalUnits.d.ts +49 -0
  31. package/dist/duration/calculate/totalUnits.js +73 -0
  32. package/dist/duration/compare/compareDurations.d.ts +1 -1
  33. package/dist/duration/compare/compareDurations.js +1 -1
  34. package/dist/duty/calculate/dutyDayFor.d.ts +38 -0
  35. package/dist/duty/calculate/dutyDayFor.js +71 -0
  36. package/dist/duty/calculate/dutyTotals.d.ts +24 -0
  37. package/dist/duty/calculate/dutyTotals.js +41 -0
  38. package/dist/duty/calculate/dutyViolations.d.ts +74 -0
  39. package/dist/duty/calculate/dutyViolations.js +65 -0
  40. package/dist/duty/calculate/evaluateDutyLog.d.ts +77 -0
  41. package/dist/duty/calculate/evaluateDutyLog.js +75 -0
  42. package/dist/duty/calculate/index.d.ts +7 -0
  43. package/dist/duty/calculate/index.js +7 -0
  44. package/dist/duty/calculate/longestRun.d.ts +28 -0
  45. package/dist/duty/calculate/longestRun.js +54 -0
  46. package/dist/duty/calculate/qualifyingRestPairs.d.ts +37 -0
  47. package/dist/duty/calculate/qualifyingRestPairs.js +58 -0
  48. package/dist/duty/calculate/qualifyingRests.d.ts +27 -0
  49. package/dist/duty/calculate/qualifyingRests.js +49 -0
  50. package/dist/duty/index.d.ts +3 -0
  51. package/dist/duty/index.js +4 -0
  52. package/dist/duty/validate/index.d.ts +1 -0
  53. package/dist/duty/validate/index.js +1 -0
  54. package/dist/duty/validate/isValidDutyLog.d.ts +24 -0
  55. package/dist/duty/validate/isValidDutyLog.js +34 -0
  56. package/dist/index.d.ts +1 -0
  57. package/dist/index.js +1 -0
  58. package/dist/instant/convert/classifyLocal.d.ts +5 -1
  59. package/dist/instant/convert/classifyLocal.js +16 -6
  60. package/dist/instant/convert/fromOffsetInstant.d.ts +4 -0
  61. package/dist/instant/convert/fromOffsetInstant.js +6 -1
  62. package/dist/instant/convert/resolveLocal.d.ts +7 -2
  63. package/dist/instant/convert/resolveLocal.js +16 -8
  64. package/dist/instant/convert/toOffsetInstant.d.ts +12 -8
  65. package/dist/instant/convert/toOffsetInstant.js +19 -11
  66. package/dist/instant/index.d.ts +1 -0
  67. package/dist/instant/index.js +1 -0
  68. package/dist/instant/validate/index.d.ts +1 -0
  69. package/dist/instant/validate/index.js +1 -0
  70. package/dist/instant/validate/isValidUtcOffset.d.ts +32 -0
  71. package/dist/instant/validate/isValidUtcOffset.js +35 -0
  72. package/dist/intermodal/calculate/chargeableDays.d.ts +6 -0
  73. package/dist/intermodal/calculate/chargeableDays.js +12 -4
  74. package/dist/intermodal/calculate/freeTimeExpiry.d.ts +7 -0
  75. package/dist/intermodal/calculate/freeTimeExpiry.js +15 -4
  76. package/dist/intermodal/calculate/index.d.ts +1 -0
  77. package/dist/intermodal/calculate/index.js +1 -0
  78. package/dist/intermodal/calculate/multimodalETA.d.ts +76 -0
  79. package/dist/intermodal/calculate/multimodalETA.js +145 -0
  80. package/dist/intermodal/format/bolTimestamp.d.ts +76 -0
  81. package/dist/intermodal/format/bolTimestamp.js +78 -0
  82. package/dist/intermodal/format/formatEdifactDate.d.ts +34 -0
  83. package/dist/intermodal/format/formatEdifactDate.js +42 -0
  84. package/dist/intermodal/format/formatEdifactDatePeriod.d.ts +35 -0
  85. package/dist/intermodal/format/formatEdifactDatePeriod.js +43 -0
  86. package/dist/intermodal/format/formatEdifactDateTime.d.ts +37 -0
  87. package/dist/intermodal/format/formatEdifactDateTime.js +45 -0
  88. package/dist/intermodal/format/formatEdifactDateTimePeriod.d.ts +40 -0
  89. package/dist/intermodal/format/formatEdifactDateTimePeriod.js +48 -0
  90. package/dist/intermodal/format/formatEdifactOffsetDateTime.d.ts +62 -0
  91. package/dist/intermodal/format/formatEdifactOffsetDateTime.js +70 -0
  92. package/dist/intermodal/format/formatEdifactTime.d.ts +35 -0
  93. package/dist/intermodal/format/formatEdifactTime.js +43 -0
  94. package/dist/intermodal/format/formatEpcisEvent.d.ts +47 -0
  95. package/dist/intermodal/format/formatEpcisEvent.js +79 -0
  96. package/dist/intermodal/format/formatX12Date.d.ts +35 -0
  97. package/dist/intermodal/format/formatX12Date.js +43 -0
  98. package/dist/intermodal/format/formatX12DateRange.d.ts +37 -0
  99. package/dist/intermodal/format/formatX12DateRange.js +45 -0
  100. package/dist/intermodal/format/formatX12DateTime.d.ts +39 -0
  101. package/dist/intermodal/format/formatX12DateTime.js +47 -0
  102. package/dist/intermodal/format/formatX12DateTimeRange.d.ts +40 -0
  103. package/dist/intermodal/format/formatX12DateTimeRange.js +48 -0
  104. package/dist/intermodal/format/formatX12Time.d.ts +42 -0
  105. package/dist/intermodal/format/formatX12Time.js +50 -0
  106. package/dist/intermodal/format/formatX12TimeElement.d.ts +46 -0
  107. package/dist/intermodal/format/formatX12TimeElement.js +54 -0
  108. package/dist/intermodal/format/index.d.ts +14 -0
  109. package/dist/intermodal/format/index.js +14 -0
  110. package/dist/intermodal/index.d.ts +3 -0
  111. package/dist/intermodal/index.js +3 -0
  112. package/dist/intermodal/parse/classifyEdifactDtmFormat.d.ts +48 -0
  113. package/dist/intermodal/parse/classifyEdifactDtmFormat.js +55 -0
  114. package/dist/intermodal/parse/classifyX12DateTimePeriodFormat.d.ts +50 -0
  115. package/dist/intermodal/parse/classifyX12DateTimePeriodFormat.js +57 -0
  116. package/dist/intermodal/parse/classifyX12TimeCode.d.ts +46 -0
  117. package/dist/intermodal/parse/classifyX12TimeCode.js +57 -0
  118. package/dist/intermodal/parse/index.d.ts +18 -0
  119. package/dist/intermodal/parse/index.js +18 -0
  120. package/dist/intermodal/parse/parseEdifactDate.d.ts +37 -0
  121. package/dist/intermodal/parse/parseEdifactDate.js +45 -0
  122. package/dist/intermodal/parse/parseEdifactDatePeriod.d.ts +43 -0
  123. package/dist/intermodal/parse/parseEdifactDatePeriod.js +51 -0
  124. package/dist/intermodal/parse/parseEdifactDateTime.d.ts +43 -0
  125. package/dist/intermodal/parse/parseEdifactDateTime.js +51 -0
  126. package/dist/intermodal/parse/parseEdifactDateTimePeriod.d.ts +45 -0
  127. package/dist/intermodal/parse/parseEdifactDateTimePeriod.js +53 -0
  128. package/dist/intermodal/parse/parseEdifactOffsetDateTime.d.ts +60 -0
  129. package/dist/intermodal/parse/parseEdifactOffsetDateTime.js +68 -0
  130. package/dist/intermodal/parse/parseEdifactTime.d.ts +37 -0
  131. package/dist/intermodal/parse/parseEdifactTime.js +45 -0
  132. package/dist/intermodal/parse/parseEpcisEvent.d.ts +95 -0
  133. package/dist/intermodal/parse/parseEpcisEvent.js +102 -0
  134. package/dist/intermodal/parse/parseX12Date.d.ts +39 -0
  135. package/dist/intermodal/parse/parseX12Date.js +47 -0
  136. package/dist/intermodal/parse/parseX12DateAndTime.d.ts +52 -0
  137. package/dist/intermodal/parse/parseX12DateAndTime.js +61 -0
  138. package/dist/intermodal/parse/parseX12DateRange.d.ts +44 -0
  139. package/dist/intermodal/parse/parseX12DateRange.js +52 -0
  140. package/dist/intermodal/parse/parseX12DateTime.d.ts +45 -0
  141. package/dist/intermodal/parse/parseX12DateTime.js +53 -0
  142. package/dist/intermodal/parse/parseX12DateTimeRange.d.ts +46 -0
  143. package/dist/intermodal/parse/parseX12DateTimeRange.js +54 -0
  144. package/dist/intermodal/parse/parseX12Time.d.ts +65 -0
  145. package/dist/intermodal/parse/parseX12Time.js +77 -0
  146. package/dist/intermodal/parse/x12TimeCodeOffset.d.ts +57 -0
  147. package/dist/intermodal/parse/x12TimeCodeOffset.js +110 -0
  148. package/dist/intermodal/parse/x12TimeCodeZone.d.ts +57 -0
  149. package/dist/intermodal/parse/x12TimeCodeZone.js +105 -0
  150. package/dist/intermodal/validate/index.d.ts +18 -0
  151. package/dist/intermodal/validate/index.js +18 -0
  152. package/dist/intermodal/validate/isValidEdifactDate.d.ts +26 -0
  153. package/dist/intermodal/validate/isValidEdifactDate.js +28 -0
  154. package/dist/intermodal/validate/isValidEdifactDatePeriod.d.ts +28 -0
  155. package/dist/intermodal/validate/isValidEdifactDatePeriod.js +30 -0
  156. package/dist/intermodal/validate/isValidEdifactDateTime.d.ts +30 -0
  157. package/dist/intermodal/validate/isValidEdifactDateTime.js +32 -0
  158. package/dist/intermodal/validate/isValidEdifactDateTimePeriod.d.ts +28 -0
  159. package/dist/intermodal/validate/isValidEdifactDateTimePeriod.js +30 -0
  160. package/dist/intermodal/validate/isValidEdifactDtmFormat.d.ts +39 -0
  161. package/dist/intermodal/validate/isValidEdifactDtmFormat.js +42 -0
  162. package/dist/intermodal/validate/isValidEdifactOffsetDateTime.d.ts +36 -0
  163. package/dist/intermodal/validate/isValidEdifactOffsetDateTime.js +38 -0
  164. package/dist/intermodal/validate/isValidEdifactTime.d.ts +28 -0
  165. package/dist/intermodal/validate/isValidEdifactTime.js +30 -0
  166. package/dist/intermodal/validate/isValidEpcisEvent.d.ts +41 -0
  167. package/dist/intermodal/validate/isValidEpcisEvent.js +58 -0
  168. package/dist/intermodal/validate/isValidEpcisEventTime.d.ts +33 -0
  169. package/dist/intermodal/validate/isValidEpcisEventTime.js +48 -0
  170. package/dist/intermodal/validate/isValidEpcisTimeZoneOffset.d.ts +30 -0
  171. package/dist/intermodal/validate/isValidEpcisTimeZoneOffset.js +38 -0
  172. package/dist/intermodal/validate/isValidX12Date.d.ts +28 -0
  173. package/dist/intermodal/validate/isValidX12Date.js +30 -0
  174. package/dist/intermodal/validate/isValidX12DateAndTime.d.ts +32 -0
  175. package/dist/intermodal/validate/isValidX12DateAndTime.js +35 -0
  176. package/dist/intermodal/validate/isValidX12DateRange.d.ts +29 -0
  177. package/dist/intermodal/validate/isValidX12DateRange.js +31 -0
  178. package/dist/intermodal/validate/isValidX12DateTime.d.ts +30 -0
  179. package/dist/intermodal/validate/isValidX12DateTime.js +32 -0
  180. package/dist/intermodal/validate/isValidX12DateTimePeriodFormat.d.ts +43 -0
  181. package/dist/intermodal/validate/isValidX12DateTimePeriodFormat.js +46 -0
  182. package/dist/intermodal/validate/isValidX12DateTimeRange.d.ts +31 -0
  183. package/dist/intermodal/validate/isValidX12DateTimeRange.js +33 -0
  184. package/dist/intermodal/validate/isValidX12Time.d.ts +38 -0
  185. package/dist/intermodal/validate/isValidX12Time.js +40 -0
  186. package/dist/intermodal/validate/isValidX12TimeCode.d.ts +38 -0
  187. package/dist/intermodal/validate/isValidX12TimeCode.js +41 -0
  188. package/dist/internal/businessCalendar.js +4 -9
  189. package/dist/internal/dutyEngine.d.ts +125 -0
  190. package/dist/internal/dutyEngine.js +1746 -0
  191. package/dist/internal/dutyLog.d.ts +27 -0
  192. package/dist/internal/dutyLog.js +47 -0
  193. package/dist/internal/dutyPeriods.d.ts +135 -0
  194. package/dist/internal/dutyPeriods.js +369 -0
  195. package/dist/internal/dutyRules.d.ts +197 -0
  196. package/dist/internal/dutyRules.js +484 -0
  197. package/dist/internal/ediDateTimeFields.d.ts +97 -0
  198. package/dist/internal/ediDateTimeFields.js +288 -0
  199. package/dist/internal/ediDateTimeWriter.d.ts +92 -0
  200. package/dist/internal/ediDateTimeWriter.js +298 -0
  201. package/dist/internal/ediGrammar.d.ts +158 -0
  202. package/dist/internal/ediGrammar.js +384 -0
  203. package/dist/internal/englishCalendarNames.d.ts +3 -1
  204. package/dist/internal/englishCalendarNames.js +3 -1
  205. package/dist/internal/freeTimeLedger.d.ts +5 -0
  206. package/dist/internal/freeTimeLedger.js +11 -2
  207. package/dist/internal/index.d.ts +5 -2
  208. package/dist/internal/index.js +9 -2
  209. package/dist/internal/intervalNanoseconds.js +2 -1
  210. package/dist/internal/isObject.d.ts +13 -6
  211. package/dist/internal/isObject.js +14 -7
  212. package/dist/internal/isoYearString.d.ts +21 -0
  213. package/dist/internal/isoYearString.js +27 -0
  214. package/dist/internal/maxPieces.js +2 -1
  215. package/dist/internal/normalizeTimeZone.d.ts +4 -1
  216. package/dist/internal/normalizeTimeZone.js +4 -1
  217. package/dist/internal/operatingSchedule.d.ts +13 -4
  218. package/dist/internal/operatingSchedule.js +146 -58
  219. package/dist/internal/patternToken.d.ts +10 -3
  220. package/dist/internal/patternToken.js +31 -12
  221. package/dist/internal/readDurationUnits.d.ts +34 -0
  222. package/dist/internal/readDurationUnits.js +78 -0
  223. package/dist/internal/readSetFields.d.ts +50 -0
  224. package/dist/internal/readSetFields.js +179 -0
  225. package/dist/internal/resolveRelativeUnit.d.ts +1 -1
  226. package/dist/internal/resolveRelativeUnit.js +1 -1
  227. package/dist/internal/resolveUnixIntervalPair.d.ts +8 -2
  228. package/dist/internal/resolveUnixIntervalPair.js +11 -7
  229. package/dist/internal/startOrEndOfUnix.d.ts +2 -1
  230. package/dist/internal/startOrEndOfUnix.js +9 -6
  231. package/dist/internal/twoDigitYear.d.ts +19 -0
  232. package/dist/internal/twoDigitYear.js +22 -0
  233. package/dist/internal/unixEpochValue.js +3 -2
  234. package/dist/internal/unixWallClock.d.ts +24 -0
  235. package/dist/internal/unixWallClock.js +35 -0
  236. package/dist/internal/utcWallClock.d.ts +20 -0
  237. package/dist/internal/utcWallClock.js +32 -0
  238. package/dist/internal/yearWindowStart.d.ts +28 -0
  239. package/dist/internal/yearWindowStart.js +52 -0
  240. package/dist/internal/zoneBucketCap.d.ts +18 -0
  241. package/dist/internal/zoneBucketCap.js +64 -0
  242. package/dist/internal/zoneFrame.d.ts +180 -0
  243. package/dist/internal/zoneFrame.js +221 -0
  244. package/dist/internal/zonedNowUnitValue.d.ts +7 -4
  245. package/dist/internal/zonedNowUnitValue.js +8 -4
  246. package/dist/internal/zonedWallClockDifference.js +4 -4
  247. package/dist/internal/zonedWallClockOperations.d.ts +5 -2
  248. package/dist/internal/zonedWallClockOperations.js +7 -10
  249. package/dist/plain/calculate/addDate.js +4 -7
  250. package/dist/plain/calculate/addDateTime.js +4 -7
  251. package/dist/plain/calculate/addTime.js +4 -7
  252. package/dist/plain/calculate/endOfTime.d.ts +1 -1
  253. package/dist/plain/calculate/setDate.d.ts +20 -3
  254. package/dist/plain/calculate/setDate.js +27 -7
  255. package/dist/plain/calculate/setDateTime.d.ts +15 -3
  256. package/dist/plain/calculate/setDateTime.js +22 -7
  257. package/dist/plain/calculate/setTime.d.ts +13 -1
  258. package/dist/plain/calculate/setTime.js +21 -6
  259. package/dist/plain/calculate/startOfTime.d.ts +1 -1
  260. package/dist/plain/calculate/subtractDate.js +4 -7
  261. package/dist/plain/calculate/subtractDateTime.js +4 -7
  262. package/dist/plain/calculate/subtractTime.js +4 -7
  263. package/dist/plain/format/formatCalendar.js +2 -1
  264. package/dist/plain/format/formatRelativeDate.js +2 -1
  265. package/dist/plain/format/formatRelativeDateTime.js +2 -1
  266. package/dist/plain/format/formatRelativeTime.js +2 -1
  267. package/dist/plain/format/{formatSql.d.ts → formatSqlDateTime.d.ts} +7 -7
  268. package/dist/plain/format/{formatSql.js → formatSqlDateTime.js} +7 -7
  269. package/dist/plain/format/index.d.ts +1 -1
  270. package/dist/plain/format/index.js +1 -1
  271. package/dist/plain/get/getNowUnit.d.ts +2 -0
  272. package/dist/plain/get/getNowUnit.js +2 -0
  273. package/dist/plain/get/getYear.d.ts +2 -0
  274. package/dist/plain/get/getYear.js +4 -3
  275. package/dist/plain/interval/intervalXorAllDate.js +2 -2
  276. package/dist/plain/interval/intervalXorAllDateTime.js +2 -2
  277. package/dist/plain/interval/intervalXorAllTime.js +2 -2
  278. package/dist/plain/interval/mergeIntervalsDate.js +2 -2
  279. package/dist/plain/interval/mergeIntervalsDateTime.js +2 -2
  280. package/dist/plain/interval/mergeIntervalsTime.js +2 -2
  281. package/dist/plain/parse/index.d.ts +1 -1
  282. package/dist/plain/parse/index.js +1 -1
  283. package/dist/plain/parse/parseDateTimeWithPattern.d.ts +16 -3
  284. package/dist/plain/parse/parseDateTimeWithPattern.js +20 -5
  285. package/dist/plain/parse/parseDateWithPattern.d.ts +17 -3
  286. package/dist/plain/parse/parseDateWithPattern.js +21 -5
  287. package/dist/plain/parse/{parseSql.d.ts → parseSqlDateTime.d.ts} +8 -8
  288. package/dist/plain/parse/{parseSql.js → parseSqlDateTime.js} +8 -8
  289. package/dist/plain/parse/parseTimeWithPattern.js +1 -0
  290. package/dist/plain/parse/parseUnitFromDate.d.ts +3 -0
  291. package/dist/plain/parse/parseUnitFromDate.js +5 -1
  292. package/dist/plain/parse/parseUnitFromDateTime.d.ts +3 -0
  293. package/dist/plain/parse/parseUnitFromDateTime.js +5 -1
  294. package/dist/plain/parse/parseYearFromDate.d.ts +5 -2
  295. package/dist/plain/parse/parseYearFromDate.js +7 -3
  296. package/dist/plain/parse/parseYearFromDateTime.d.ts +4 -2
  297. package/dist/plain/parse/parseYearFromDateTime.js +6 -3
  298. package/dist/plain/validate/index.d.ts +1 -0
  299. package/dist/plain/validate/index.js +1 -0
  300. package/dist/plain/validate/isValidSqlDateTime.d.ts +33 -0
  301. package/dist/plain/validate/isValidSqlDateTime.js +36 -0
  302. package/dist/precision/convert/fromNanoseconds.d.ts +9 -3
  303. package/dist/precision/convert/fromNanoseconds.js +9 -3
  304. package/dist/regex/epcis.d.ts +60 -0
  305. package/dist/regex/epcis.js +60 -0
  306. package/dist/regex/http-date.d.ts +6 -4
  307. package/dist/regex/http-date.js +6 -4
  308. package/dist/regex/index.d.ts +1 -0
  309. package/dist/regex/index.js +1 -0
  310. package/dist/regex/rfc-2822.d.ts +2 -0
  311. package/dist/regex/rfc-2822.js +2 -0
  312. package/dist/regex/rfc-3339.d.ts +3 -1
  313. package/dist/regex/rfc-3339.js +3 -1
  314. package/dist/regex/sql-date-time.d.ts +2 -1
  315. package/dist/regex/sql-date-time.js +2 -1
  316. package/dist/regex/time-zone-like.d.ts +3 -2
  317. package/dist/regex/time-zone-like.js +3 -2
  318. package/dist/regex/utc-offset.d.ts +2 -1
  319. package/dist/regex/utc-offset.js +2 -1
  320. package/dist/transport/calculate/crossingTime.d.ts +6 -1
  321. package/dist/transport/calculate/crossingTime.js +6 -1
  322. package/dist/transport/calculate/cutoffAt.d.ts +6 -1
  323. package/dist/transport/calculate/cutoffAt.js +2 -0
  324. package/dist/transport/calculate/cutoffSchedule.d.ts +6 -1
  325. package/dist/transport/calculate/cutoffSchedule.js +2 -0
  326. package/dist/transport/calculate/dwellTime.d.ts +6 -1
  327. package/dist/transport/calculate/dwellTime.js +6 -1
  328. package/dist/transport/calculate/scheduleDelivery.d.ts +13 -5
  329. package/dist/transport/calculate/scheduleDelivery.js +6 -1
  330. package/dist/transport/convert/etaAtZone.d.ts +6 -1
  331. package/dist/transport/convert/etaAtZone.js +6 -1
  332. package/dist/types/business-calendar.d.ts +5 -4
  333. package/dist/types/duty.d.ts +777 -0
  334. package/dist/types/duty.js +0 -0
  335. package/dist/types/edi.d.ts +407 -0
  336. package/dist/types/edi.js +0 -0
  337. package/dist/types/epcis.d.ts +25 -0
  338. package/dist/types/epcis.js +0 -0
  339. package/dist/types/exact-duration-unit.d.ts +8 -0
  340. package/dist/types/exact-duration-unit.js +0 -0
  341. package/dist/types/free-time-options.d.ts +4 -3
  342. package/dist/types/index.d.ts +5 -0
  343. package/dist/types/index.js +5 -0
  344. package/dist/types/operating-schedule.d.ts +3 -2
  345. package/dist/types/two-digit-year.d.ts +23 -0
  346. package/dist/types/two-digit-year.js +0 -0
  347. package/dist/types/zone-bucket-unit.d.ts +1 -1
  348. package/dist/unix/calculate/addUnix.d.ts +11 -2
  349. package/dist/unix/calculate/addUnix.js +14 -11
  350. package/dist/unix/calculate/diffUnix.d.ts +10 -2
  351. package/dist/unix/calculate/diffUnix.js +11 -6
  352. package/dist/unix/calculate/diffUnixAsDuration.d.ts +10 -2
  353. package/dist/unix/calculate/diffUnixAsDuration.js +10 -5
  354. package/dist/unix/calculate/endOfQuarterForUnix.d.ts +10 -2
  355. package/dist/unix/calculate/endOfQuarterForUnix.js +6 -0
  356. package/dist/unix/calculate/endOfUnix.d.ts +11 -2
  357. package/dist/unix/calculate/endOfUnix.js +7 -0
  358. package/dist/unix/calculate/isBetweenUnix.d.ts +3 -2
  359. package/dist/unix/calculate/isBetweenUnix.js +3 -3
  360. package/dist/unix/calculate/roundUnix.d.ts +10 -2
  361. package/dist/unix/calculate/roundUnix.js +13 -6
  362. package/dist/unix/calculate/setUnix.d.ts +25 -5
  363. package/dist/unix/calculate/setUnix.js +33 -12
  364. package/dist/unix/calculate/startOfQuarterForUnix.d.ts +10 -2
  365. package/dist/unix/calculate/startOfQuarterForUnix.js +6 -0
  366. package/dist/unix/calculate/startOfUnix.d.ts +11 -2
  367. package/dist/unix/calculate/startOfUnix.js +7 -0
  368. package/dist/unix/calculate/subtractUnix.d.ts +10 -2
  369. package/dist/unix/calculate/subtractUnix.js +13 -11
  370. package/dist/unix/compare/areUnixEqualBy.d.ts +11 -2
  371. package/dist/unix/compare/areUnixEqualBy.js +7 -0
  372. package/dist/unix/convert/convertUnixToPlainDate.d.ts +5 -2
  373. package/dist/unix/convert/convertUnixToPlainDate.js +5 -4
  374. package/dist/unix/convert/convertUnixToPlainDateTime.d.ts +5 -2
  375. package/dist/unix/convert/convertUnixToPlainDateTime.js +5 -4
  376. package/dist/unix/convert/convertUnixToPlainTime.d.ts +5 -2
  377. package/dist/unix/convert/convertUnixToPlainTime.js +5 -4
  378. package/dist/unix/convert/convertUnixToZoned.d.ts +5 -1
  379. package/dist/unix/convert/convertUnixToZoned.js +5 -1
  380. package/dist/unix/format/formatCalendarUnix.d.ts +6 -2
  381. package/dist/unix/format/formatCalendarUnix.js +6 -1
  382. package/dist/unix/format/formatRelativeUnix.d.ts +11 -3
  383. package/dist/unix/format/formatRelativeUnix.js +13 -5
  384. package/dist/unix/format/formatUnix.d.ts +7 -2
  385. package/dist/unix/format/formatUnix.js +4 -0
  386. package/dist/unix/get/getUnixDay.d.ts +1 -3
  387. package/dist/unix/get/getUnixDay.js +5 -12
  388. package/dist/unix/get/getUnixHour.d.ts +1 -3
  389. package/dist/unix/get/getUnixHour.js +5 -12
  390. package/dist/unix/get/getUnixMicrosecond.d.ts +1 -3
  391. package/dist/unix/get/getUnixMicrosecond.js +4 -11
  392. package/dist/unix/get/getUnixMillisecond.d.ts +1 -3
  393. package/dist/unix/get/getUnixMillisecond.js +5 -12
  394. package/dist/unix/get/getUnixMinute.d.ts +1 -3
  395. package/dist/unix/get/getUnixMinute.js +5 -12
  396. package/dist/unix/get/getUnixMonth.d.ts +1 -3
  397. package/dist/unix/get/getUnixMonth.js +5 -12
  398. package/dist/unix/get/getUnixNanosecond.d.ts +1 -3
  399. package/dist/unix/get/getUnixNanosecond.js +4 -11
  400. package/dist/unix/get/getUnixNowUnit.d.ts +2 -0
  401. package/dist/unix/get/getUnixNowUnit.js +2 -0
  402. package/dist/unix/get/getUnixSecond.d.ts +1 -3
  403. package/dist/unix/get/getUnixSecond.js +5 -12
  404. package/dist/unix/get/getUnixYear.d.ts +3 -3
  405. package/dist/unix/get/getUnixYear.js +5 -12
  406. package/dist/unix/interval/intervalCountUnix.d.ts +11 -2
  407. package/dist/unix/interval/intervalCountUnix.js +7 -0
  408. package/dist/unix/interval/intervalFromDurationUnix.d.ts +10 -2
  409. package/dist/unix/interval/intervalFromDurationUnix.js +12 -7
  410. package/dist/unix/interval/intervalLengthUnix.d.ts +10 -2
  411. package/dist/unix/interval/intervalLengthUnix.js +6 -0
  412. package/dist/unix/interval/intervalOverlappingDaysUnix.d.ts +10 -2
  413. package/dist/unix/interval/intervalOverlappingDaysUnix.js +10 -5
  414. package/dist/unix/interval/splitIntervalByUnitUnix.d.ts +10 -2
  415. package/dist/unix/interval/splitIntervalByUnitUnix.js +12 -6
  416. package/dist/unix/parse/parseDateFromUnix.d.ts +4 -2
  417. package/dist/unix/parse/parseDateFromUnix.js +3 -3
  418. package/dist/unix/parse/parseDayFromUnix.d.ts +4 -2
  419. package/dist/unix/parse/parseDayOfWeekFromUnix.d.ts +4 -2
  420. package/dist/unix/parse/parseDayOfWeekFromUnix.js +2 -2
  421. package/dist/unix/parse/parseHourFromUnix.d.ts +4 -2
  422. package/dist/unix/parse/parseMicrosecondFromUnix.d.ts +4 -2
  423. package/dist/unix/parse/parseMillisecondFromUnix.d.ts +4 -2
  424. package/dist/unix/parse/parseMinuteFromUnix.d.ts +4 -2
  425. package/dist/unix/parse/parseMonthFromUnix.d.ts +4 -2
  426. package/dist/unix/parse/parseNanosecondFromUnix.d.ts +4 -2
  427. package/dist/unix/parse/parseSecondFromUnix.d.ts +4 -2
  428. package/dist/unix/parse/parseTimeFromUnix.d.ts +5 -2
  429. package/dist/unix/parse/parseTimeFromUnix.js +4 -3
  430. package/dist/unix/parse/parseUnitFromUnix.d.ts +9 -3
  431. package/dist/unix/parse/parseUnitFromUnix.js +21 -14
  432. package/dist/unix/parse/parseWeekFromUnix.d.ts +3 -2
  433. package/dist/unix/parse/parseWeekFromUnix.js +4 -4
  434. package/dist/unix/parse/parseYearFromUnix.d.ts +8 -3
  435. package/dist/unix/parse/parseYearFromUnix.js +4 -1
  436. package/dist/unix/validate/isValidUnixUnit.d.ts +1 -1
  437. package/dist/utc/calculate/addUtc.js +4 -7
  438. package/dist/utc/calculate/setUtc.d.ts +16 -3
  439. package/dist/utc/calculate/setUtc.js +22 -8
  440. package/dist/utc/calculate/subtractUtc.js +4 -7
  441. package/dist/utc/convert/convertUtcToPlainDate.d.ts +8 -2
  442. package/dist/utc/convert/convertUtcToPlainDate.js +10 -7
  443. package/dist/utc/convert/convertUtcToPlainDateTime.d.ts +9 -2
  444. package/dist/utc/convert/convertUtcToPlainDateTime.js +12 -13
  445. package/dist/utc/convert/convertUtcToPlainTime.d.ts +5 -2
  446. package/dist/utc/convert/convertUtcToPlainTime.js +7 -6
  447. package/dist/utc/convert/convertUtcToZoned.d.ts +5 -1
  448. package/dist/utc/convert/convertUtcToZoned.js +5 -1
  449. package/dist/utc/format/formatCalendarUtc.d.ts +6 -2
  450. package/dist/utc/format/formatCalendarUtc.js +6 -1
  451. package/dist/utc/format/{formatHttp.d.ts → formatHttpDate.d.ts} +7 -7
  452. package/dist/utc/format/{formatHttp.js → formatHttpDate.js} +7 -7
  453. package/dist/utc/format/formatRelativeUtc.d.ts +11 -3
  454. package/dist/utc/format/formatRelativeUtc.js +13 -5
  455. package/dist/utc/format/formatUtc.d.ts +7 -2
  456. package/dist/utc/format/formatUtc.js +4 -0
  457. package/dist/utc/format/index.d.ts +1 -1
  458. package/dist/utc/format/index.js +1 -1
  459. package/dist/utc/get/getUtcDay.d.ts +1 -3
  460. package/dist/utc/get/getUtcDay.js +5 -12
  461. package/dist/utc/get/getUtcHour.d.ts +1 -3
  462. package/dist/utc/get/getUtcHour.js +5 -12
  463. package/dist/utc/get/getUtcMicrosecond.d.ts +1 -3
  464. package/dist/utc/get/getUtcMicrosecond.js +4 -11
  465. package/dist/utc/get/getUtcMillisecond.d.ts +1 -3
  466. package/dist/utc/get/getUtcMillisecond.js +5 -12
  467. package/dist/utc/get/getUtcMinute.d.ts +1 -3
  468. package/dist/utc/get/getUtcMinute.js +5 -12
  469. package/dist/utc/get/getUtcMonth.d.ts +1 -3
  470. package/dist/utc/get/getUtcMonth.js +5 -12
  471. package/dist/utc/get/getUtcNanosecond.d.ts +1 -3
  472. package/dist/utc/get/getUtcNanosecond.js +4 -11
  473. package/dist/utc/get/getUtcNow.d.ts +1 -3
  474. package/dist/utc/get/getUtcNow.js +2 -9
  475. package/dist/utc/get/getUtcNowUnit.d.ts +2 -0
  476. package/dist/utc/get/getUtcNowUnit.js +2 -0
  477. package/dist/utc/get/getUtcSecond.d.ts +1 -3
  478. package/dist/utc/get/getUtcSecond.js +5 -12
  479. package/dist/utc/get/getUtcYear.d.ts +3 -3
  480. package/dist/utc/get/getUtcYear.js +5 -12
  481. package/dist/utc/interval/intervalXorAllUtc.js +2 -2
  482. package/dist/utc/interval/mergeIntervalsUtc.js +2 -2
  483. package/dist/utc/parse/index.d.ts +1 -1
  484. package/dist/utc/parse/index.js +1 -1
  485. package/dist/utc/parse/parseDateFromUtc.d.ts +4 -2
  486. package/dist/utc/parse/parseDateFromUtc.js +2 -2
  487. package/dist/utc/parse/parseDayFromUtc.d.ts +4 -2
  488. package/dist/utc/parse/parseDayFromUtc.js +2 -2
  489. package/dist/utc/parse/parseDayOfWeekFromUtc.d.ts +4 -2
  490. package/dist/utc/parse/parseDayOfWeekFromUtc.js +4 -4
  491. package/dist/utc/parse/parseHourFromUtc.d.ts +4 -2
  492. package/dist/utc/parse/parseHourFromUtc.js +2 -2
  493. package/dist/utc/parse/{parseHttp.d.ts → parseHttpDate.d.ts} +12 -12
  494. package/dist/utc/parse/{parseHttp.js → parseHttpDate.js} +12 -12
  495. package/dist/utc/parse/parseMicrosecondFromUtc.d.ts +4 -2
  496. package/dist/utc/parse/parseMicrosecondFromUtc.js +2 -2
  497. package/dist/utc/parse/parseMillisecondFromUtc.d.ts +4 -2
  498. package/dist/utc/parse/parseMillisecondFromUtc.js +2 -2
  499. package/dist/utc/parse/parseMinuteFromUtc.d.ts +4 -2
  500. package/dist/utc/parse/parseMinuteFromUtc.js +2 -2
  501. package/dist/utc/parse/parseMonthFromUtc.d.ts +4 -2
  502. package/dist/utc/parse/parseMonthFromUtc.js +2 -2
  503. package/dist/utc/parse/parseNanosecondFromUtc.d.ts +4 -2
  504. package/dist/utc/parse/parseNanosecondFromUtc.js +2 -2
  505. package/dist/utc/parse/parseSecondFromUtc.d.ts +4 -2
  506. package/dist/utc/parse/parseSecondFromUtc.js +2 -2
  507. package/dist/utc/parse/parseTimeFromUtc.d.ts +5 -2
  508. package/dist/utc/parse/parseTimeFromUtc.js +5 -4
  509. package/dist/utc/parse/parseUnitFromUtc.d.ts +10 -3
  510. package/dist/utc/parse/parseUnitFromUtc.js +11 -5
  511. package/dist/utc/parse/parseWeekFromUtc.d.ts +4 -2
  512. package/dist/utc/parse/parseWeekFromUtc.js +4 -4
  513. package/dist/utc/parse/parseYearFromUtc.d.ts +8 -4
  514. package/dist/utc/parse/parseYearFromUtc.js +8 -5
  515. package/dist/utc/validate/index.d.ts +2 -0
  516. package/dist/utc/validate/index.js +2 -0
  517. package/dist/utc/validate/isValidHttpDate.d.ts +35 -0
  518. package/dist/utc/validate/isValidHttpDate.js +38 -0
  519. package/dist/utc/validate/isValidUtc.d.ts +4 -0
  520. package/dist/utc/validate/isValidUtc.js +4 -0
  521. package/dist/utc/validate/isValidUtcTimestamp.d.ts +43 -0
  522. package/dist/utc/validate/isValidUtcTimestamp.js +48 -0
  523. package/dist/zoned/calculate/addZoned.js +4 -7
  524. package/dist/zoned/calculate/setZoned.d.ts +19 -5
  525. package/dist/zoned/calculate/setZoned.js +26 -9
  526. package/dist/zoned/calculate/subtractZoned.js +4 -7
  527. package/dist/zoned/convert/convertPlainDateTimeToZoned.d.ts +4 -1
  528. package/dist/zoned/convert/convertPlainDateTimeToZoned.js +4 -1
  529. package/dist/zoned/convert/convertZonedToZoned.d.ts +5 -1
  530. package/dist/zoned/convert/convertZonedToZoned.js +5 -1
  531. package/dist/zoned/format/formatCalendarZoned.js +2 -1
  532. package/dist/zoned/format/formatRelativeZoned.js +2 -1
  533. package/dist/zoned/format/formatTimeZoneName.d.ts +5 -1
  534. package/dist/zoned/format/formatTimeZoneName.js +5 -1
  535. package/dist/zoned/get/getDstTransitions.d.ts +5 -3
  536. package/dist/zoned/get/getDstTransitions.js +12 -7
  537. package/dist/zoned/get/getSystemTimeZone.d.ts +3 -1
  538. package/dist/zoned/get/getSystemTimeZone.js +5 -2
  539. package/dist/zoned/get/getTimeZoneOffset.d.ts +10 -2
  540. package/dist/zoned/get/getTimeZoneOffset.js +18 -5
  541. package/dist/zoned/get/getZonedDay.d.ts +2 -1
  542. package/dist/zoned/get/getZonedDay.js +6 -7
  543. package/dist/zoned/get/getZonedDayOfWeek.d.ts +2 -1
  544. package/dist/zoned/get/getZonedDayOfWeek.js +6 -5
  545. package/dist/zoned/get/getZonedHour.d.ts +2 -1
  546. package/dist/zoned/get/getZonedHour.js +6 -7
  547. package/dist/zoned/get/getZonedMicrosecond.d.ts +2 -1
  548. package/dist/zoned/get/getZonedMicrosecond.js +6 -5
  549. package/dist/zoned/get/getZonedMillisecond.d.ts +2 -1
  550. package/dist/zoned/get/getZonedMillisecond.js +6 -7
  551. package/dist/zoned/get/getZonedMinute.d.ts +2 -1
  552. package/dist/zoned/get/getZonedMinute.js +6 -7
  553. package/dist/zoned/get/getZonedMonth.d.ts +2 -1
  554. package/dist/zoned/get/getZonedMonth.js +6 -7
  555. package/dist/zoned/get/getZonedNanosecond.d.ts +2 -1
  556. package/dist/zoned/get/getZonedNanosecond.js +6 -5
  557. package/dist/zoned/get/getZonedNow.d.ts +5 -1
  558. package/dist/zoned/get/getZonedNow.js +5 -1
  559. package/dist/zoned/get/getZonedNowUnit.d.ts +6 -3
  560. package/dist/zoned/get/getZonedNowUnit.js +10 -7
  561. package/dist/zoned/get/getZonedSecond.d.ts +2 -1
  562. package/dist/zoned/get/getZonedSecond.js +6 -7
  563. package/dist/zoned/get/getZonedToday.d.ts +2 -1
  564. package/dist/zoned/get/getZonedToday.js +6 -5
  565. package/dist/zoned/get/getZonedWeekOfYear.d.ts +2 -1
  566. package/dist/zoned/get/getZonedWeekOfYear.js +6 -5
  567. package/dist/zoned/get/getZonedYear.d.ts +4 -1
  568. package/dist/zoned/get/getZonedYear.js +9 -7
  569. package/dist/zoned/interval/intervalXorAllZoned.js +2 -2
  570. package/dist/zoned/interval/mergeIntervalsZoned.js +2 -2
  571. package/dist/zoned/parse/parseUnitFromZoned.d.ts +4 -1
  572. package/dist/zoned/parse/parseUnitFromZoned.js +5 -1
  573. package/dist/zoned/parse/parseYearFromZoned.d.ts +4 -2
  574. package/dist/zoned/parse/parseYearFromZoned.js +6 -3
  575. package/dist/zoned/validate/hasDaylightSaving.d.ts +4 -2
  576. package/dist/zoned/validate/hasDaylightSaving.js +13 -7
  577. package/dist/zoned/validate/index.d.ts +2 -0
  578. package/dist/zoned/validate/index.js +2 -0
  579. package/dist/zoned/validate/isValidRfc2822DateTime.d.ts +32 -0
  580. package/dist/zoned/validate/isValidRfc2822DateTime.js +35 -0
  581. package/dist/zoned/validate/isValidRfc3339DateTime.d.ts +31 -0
  582. package/dist/zoned/validate/isValidRfc3339DateTime.js +34 -0
  583. package/dist/zoned/validate/isValidTimeZone.d.ts +5 -1
  584. package/dist/zoned/validate/isValidTimeZone.js +6 -2
  585. package/package.json +50 -1
  586. package/skills/gmt-arithmetic/SKILL.md +99 -12
  587. package/skills/gmt-basics/SKILL.md +57 -15
  588. package/skills/gmt-integration/SKILL.md +24 -9
  589. package/skills/gmt-timezone/SKILL.md +134 -29
  590. package/dist/internal/unixZonedDateTime.d.ts +0 -22
  591. package/dist/internal/unixZonedDateTime.js +0 -32
  592. package/dist/internal/utcZonedDateTime.d.ts +0 -19
  593. package/dist/internal/utcZonedDateTime.js +0 -31
@@ -0,0 +1,1746 @@
1
+ import { Temporal } from "@js-temporal/polyfill";
2
+ import { parseDutyLog } from "./dutyLog.js";
3
+ import { calendarScale, datePlusDays, localDateOf, operatorDayScale, periodAfter, periodsEndingAt, resolveWallClock, WallClockRejected, } from "./dutyPeriods.js";
4
+ import { MAX_PERIOD_COUNT, parseDutyRules, usesOperatorDays, } from "./dutyRules.js";
5
+ import { isObject } from "./isObject.js";
6
+ import { parseScheduleDisambiguation } from "./operatingSchedule.js";
7
+ import { formatHourDuration } from "./hourDurationString.js";
8
+ import { coalesceIntervalNanoseconds } from "./intervalNanoseconds.js";
9
+ import { zoneFrame } from "./zoneFrame.js";
10
+ import { parseDayStartTime } from "./dutyPeriods.js";
11
+ /** TC39 `nsMinInstant`: a window never reaches back past the first instant. */
12
+ const MIN_INSTANT_NANOSECONDS = -8640000000000000000000n;
13
+ /** A window a call cannot place in Temporal's range. Caught by the public function. */
14
+ export class Unrepresentable extends RangeError {
15
+ }
16
+ /**
17
+ * A count as a number, exactly. Past `Number.MAX_SAFE_INTEGER` a number would round it, so the
18
+ * call is refused instead.
19
+ */
20
+ function exactCount(value) {
21
+ if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
22
+ throw new Unrepresentable("a count past the safe integers");
23
+ }
24
+ return Number(value);
25
+ }
26
+ const duration = (nanoseconds) => ({
27
+ duration: nanoseconds > 0n ? nanoseconds : 0n,
28
+ });
29
+ const tally = (value) => ({ count: value > 0 ? value : 0 });
30
+ const max = (a, b) => (a > b ? a : b);
31
+ const min = (a, b) => (a < b ? a : b);
32
+ const length = (run) => run.end - run.start;
33
+ /**
34
+ * Read the log, rules and options `evaluateDutyLog` and `dutyViolations` take, all-or-nothing.
35
+ *
36
+ * - `options` must be an object with a valid `timeZone`; `dayStartTime` is optional, but a rule
37
+ * that counts operator days needs it; `disambiguation` is `"compatible"` when absent. Each
38
+ * member is read once.
39
+ *
40
+ * @param log candidate duty log
41
+ * @param rules candidate rules
42
+ * @param options candidate `{ timeZone, dayStartTime?, disambiguation? }`
43
+ * @returns the context and resolved rules, or null on invalid input
44
+ */
45
+ export function prepareDutyCall(log, rules, options) {
46
+ const entries = parseDutyLog(log);
47
+ const resolved = parseDutyRules(rules);
48
+ if (entries === null || resolved === null || !isObject(options)) {
49
+ return null;
50
+ }
51
+ const { timeZone, dayStartTime } = options;
52
+ const disambiguation = parseScheduleDisambiguation(options);
53
+ const frame = zoneFrame(timeZone);
54
+ const time = dayStartTime === undefined ? null : parseDayStartTime(dayStartTime);
55
+ if (frame === null ||
56
+ disambiguation === null ||
57
+ (dayStartTime !== undefined && time === null) ||
58
+ (time === null && resolved.some(usesOperatorDays))) {
59
+ return null;
60
+ }
61
+ return {
62
+ context: {
63
+ entries,
64
+ frame,
65
+ dayStartTime: time,
66
+ disambiguation,
67
+ scales: new Map(),
68
+ wallClocks: new Map(),
69
+ windowInstances: new Map(),
70
+ runs: new Map(),
71
+ },
72
+ rules: resolved,
73
+ };
74
+ }
75
+ function scaleFor(context, unit) {
76
+ const known = context.scales.get(unit);
77
+ if (known !== undefined) {
78
+ return known;
79
+ }
80
+ let scale;
81
+ if (unit === "operatorDay") {
82
+ if (context.dayStartTime === null) {
83
+ throw new Unrepresentable("operator days need dayStartTime");
84
+ }
85
+ scale = operatorDayScale(context.frame, context.dayStartTime, context.disambiguation);
86
+ }
87
+ else {
88
+ scale = calendarScale(context.frame, unit);
89
+ }
90
+ context.scales.set(unit, scale);
91
+ return scale;
92
+ }
93
+ /** The log as it stands at `at`: later entries dropped, one straddling `at` cut there. */
94
+ function clip(entries, at) {
95
+ const clipped = [];
96
+ for (const entry of entries) {
97
+ if (entry.start >= at) {
98
+ break;
99
+ }
100
+ clipped.push(entry.end > at ? { ...entry, end: at } : entry);
101
+ }
102
+ return clipped;
103
+ }
104
+ const matches = (test, status) => typeof test === "function" ? test(status) : test.has(status);
105
+ /**
106
+ * Read a status list a helper takes: an array of strings, `[]` allowed. Each element is read once.
107
+ *
108
+ * @param value candidate status list
109
+ * @returns the statuses, or null when `value` is not an array of strings
110
+ */
111
+ export function readStatusList(value) {
112
+ if (!Array.isArray(value)) {
113
+ return null;
114
+ }
115
+ const statuses = new Set();
116
+ const count = value.length;
117
+ for (let index = 0; index < count; index++) {
118
+ const status = value[index];
119
+ if (typeof status !== "string") {
120
+ return null;
121
+ }
122
+ statuses.add(status);
123
+ }
124
+ return statuses;
125
+ }
126
+ /**
127
+ * Maximal runs of touching entries whose status passes `test`, merged as `mergeIntervals` merges
128
+ * them (CORE-6): touching entries join, and a gap or another status ends a run.
129
+ */
130
+ export function runsOf(log, test) {
131
+ return coalesceIntervalNanoseconds(log
132
+ .filter((entry) => matches(test, entry.status))
133
+ .map(({ start, end }) => ({ start, end, startText: "", endText: "" }))).map(({ start, end }) => ({ start, end }));
134
+ }
135
+ /** The runs of the whole log in a set of statuses, found once per call and kept. */
136
+ function logRuns(context, statuses) {
137
+ const known = context.runs.get(statuses);
138
+ if (known !== undefined) {
139
+ return known;
140
+ }
141
+ const found = runsOf(context.entries, statuses);
142
+ context.runs.set(statuses, found);
143
+ return found;
144
+ }
145
+ /**
146
+ * The parts of ordered, separate runs inside `[from, to)`, empty parts dropped: `clipRuns`, with
147
+ * the runs before `from` skipped by a binary search instead of read one by one.
148
+ */
149
+ function runsBetween(runs, from, to) {
150
+ let low = 0;
151
+ let high = runs.length;
152
+ // A binary search over at most 2^32 runs.
153
+ for (let step = 0; step < 33 && low < high; step++) {
154
+ const middle = (low + high) >> 1;
155
+ if (runs[middle].end <= from) {
156
+ low = middle + 1;
157
+ }
158
+ else {
159
+ high = middle;
160
+ }
161
+ }
162
+ const inside = [];
163
+ for (let index = low; index < runs.length; index++) {
164
+ const run = runs[index];
165
+ if (run.start >= to) {
166
+ break;
167
+ }
168
+ inside.push({ start: max(run.start, from), end: min(run.end, to) });
169
+ }
170
+ return inside;
171
+ }
172
+ /** The parts of runs inside `[from, to)`, empty parts dropped. */
173
+ export function clipRuns(runs, from, to) {
174
+ const clipped = [];
175
+ for (const run of runs) {
176
+ const start = max(run.start, from);
177
+ const end = min(run.end, to);
178
+ if (start < end) {
179
+ clipped.push({ start, end });
180
+ }
181
+ }
182
+ return clipped;
183
+ }
184
+ /** Time in `statuses` inside `[from, to)`. */
185
+ export function timeIn(log, statuses, from, to) {
186
+ let total = 0n;
187
+ for (const entry of log) {
188
+ if (matches(statuses, entry.status)) {
189
+ const start = max(entry.start, from);
190
+ const end = min(entry.end, to);
191
+ total += end > start ? end - start : 0n;
192
+ }
193
+ }
194
+ return total;
195
+ }
196
+ /** The latest run at least `minimum` long. */
197
+ function lastAtLeast(runs, minimum) {
198
+ for (let index = runs.length - 1; index >= 0; index--) {
199
+ const run = runs[index];
200
+ if (length(run) >= minimum) {
201
+ return run;
202
+ }
203
+ }
204
+ return undefined;
205
+ }
206
+ /**
207
+ * The paired rests among runs of rest: two runs in a row, each shorter than `fullRest` and at
208
+ * least `pair.each`, the longer at least `pair.one` and both together at least `pair.combined`.
209
+ *
210
+ * - A run of `fullRest` or more is a rest on its own: it pairs with nothing, and no pair spans it.
211
+ * - A run shorter than `pair.each` is neither half of a pair nor a break between two runs.
212
+ * - Pairs may chain: of runs a, b and c, both (a, b) and (b, c) are returned when both qualify.
213
+ *
214
+ * @param runs runs of rest, in order
215
+ * @param fullRest the length at which a run is a full rest
216
+ * @param pair the three floors
217
+ * @returns the qualifying pairs, in order
218
+ */
219
+ export function restPairs(runs, fullRest, pair) {
220
+ const pairs = [];
221
+ let previous;
222
+ for (const run of runs) {
223
+ const taken = length(run);
224
+ if (taken >= fullRest) {
225
+ previous = undefined;
226
+ continue;
227
+ }
228
+ if (taken < pair.each) {
229
+ continue;
230
+ }
231
+ if (previous !== undefined) {
232
+ const before = length(previous);
233
+ if (max(before, taken) >= pair.one && before + taken >= pair.combined) {
234
+ pairs.push({ first: previous, second: run });
235
+ }
236
+ }
237
+ previous = run;
238
+ }
239
+ return pairs;
240
+ }
241
+ /** A period repeated `times` over: `times` × its length, or `times` × its count. */
242
+ function repeated(period, times) {
243
+ return period.kind === "elapsed"
244
+ ? { kind: "elapsed", length: period.length * BigInt(times) }
245
+ : { ...period, count: period.count * times };
246
+ }
247
+ /** The window a period names at `at`: sliding for elapsed time, whole periods otherwise. */
248
+ function windowOf(context, period, at, anchor) {
249
+ if (period.kind === "elapsed") {
250
+ return {
251
+ span: {
252
+ start: max(at - period.length, MIN_INSTANT_NANOSECONDS),
253
+ end: at,
254
+ },
255
+ basis: "elapsed",
256
+ rolling: true,
257
+ };
258
+ }
259
+ const scale = scaleFor(context, period.unit);
260
+ const span = periodsEndingAt(scale, period.count, anchor);
261
+ if (span === null) {
262
+ throw new Unrepresentable("period outside the instant range");
263
+ }
264
+ return { span, basis: scale.basis, rolling: period.count > 1 };
265
+ }
266
+ /** The end of the `periods`-th period, the one `from` falls in counting as the first. */
267
+ function boundaryAfter(context, period, periods, from) {
268
+ if (period.kind === "elapsed") {
269
+ return from + period.length * BigInt(periods);
270
+ }
271
+ const scale = scaleFor(context, period.unit);
272
+ const first = scale.containing(from);
273
+ const last = first === null
274
+ ? null
275
+ : periodAfter(scale, first, period.count * periods - 1);
276
+ if (last === null) {
277
+ throw new Unrepresentable("period outside the instant range");
278
+ }
279
+ return last.end;
280
+ }
281
+ const basisOf = (window) => window.basis;
282
+ const groupingOf = (window) => window.rolling ? "slide" : "period";
283
+ /** A cap: holds while `used` is at most `limit`. */
284
+ function capped(used, limit, rest) {
285
+ const usedValue = "duration" in used ? used.duration : BigInt(used.count);
286
+ const limitValue = "duration" in limit ? limit.duration : BigInt(limit.count);
287
+ const remaining = limitValue - usedValue;
288
+ return {
289
+ satisfied: usedValue <= limitValue,
290
+ used,
291
+ limit,
292
+ remaining: "duration" in limit ? duration(remaining) : tally(Number(remaining)),
293
+ excess: usedValue - limitValue,
294
+ ...rest,
295
+ };
296
+ }
297
+ /** A floor: holds while `used` is at least `limit`, or `met` says otherwise. */
298
+ function floored(used, limit, met, rest) {
299
+ return {
300
+ satisfied: met,
301
+ used: duration(used),
302
+ limit: duration(limit),
303
+ remaining: duration(limit - used),
304
+ excess: limit - used,
305
+ ...rest,
306
+ };
307
+ }
308
+ /** How a list of rest runs measures against a `restRequirement`'s floors. */
309
+ function restChecks(rule, lengths) {
310
+ const sorted = [...lengths].sort((a, b) => (a > b ? -1 : a < b ? 1 : 0));
311
+ const longest = sorted[0] ?? 0n;
312
+ const checks = [];
313
+ let counted = sorted.reduce((sum, value) => sum + value, 0n);
314
+ if (rule.composition !== null) {
315
+ const { maxPeriods, longestAtLeast, othersAtLeast } = rule.composition;
316
+ const others = sorted
317
+ .slice(1)
318
+ .filter((value) => value >= othersAtLeast)
319
+ .slice(0, maxPeriods - 1);
320
+ counted = min(counted, others.reduce((sum, value) => sum + value, longest));
321
+ checks.push({ used: longest, limit: longestAtLeast });
322
+ }
323
+ if (rule.composite !== null) {
324
+ const { consecutive, additional, blockMinimum } = rule.composite;
325
+ const blocks = sorted
326
+ .slice(1)
327
+ .filter((value) => value >= blockMinimum)
328
+ .reduce((sum, value) => sum + value, 0n);
329
+ checks.push({ used: longest, limit: consecutive });
330
+ checks.push({
331
+ used: max(longest - consecutive, 0n) + blocks,
332
+ limit: additional,
333
+ });
334
+ counted = min(counted, longest + blocks);
335
+ }
336
+ checks.push({ used: counted, limit: rule.minimum });
337
+ const failing = checks.find((check) => check.used < check.limit);
338
+ return {
339
+ met: failing === undefined,
340
+ failing,
341
+ // The last check is always the minimum: rest counted against `minimum`.
342
+ total: checks[checks.length - 1],
343
+ };
344
+ }
345
+ /** Rest runs with rest added from `at` to `end`, joined to a run that reaches `at`. */
346
+ function withRestUntil(runs, at, end) {
347
+ const last = runs[runs.length - 1];
348
+ return last !== undefined && last.end === at
349
+ ? [...runs.slice(0, -1), { start: last.start, end }]
350
+ : [...runs, { start: at, end }];
351
+ }
352
+ /** `resolveWallClock` under the call's policy, each local date-time resolved once per call. */
353
+ function wallClockOf(context, date, time) {
354
+ const key = `${date.toString()}T${time.toString()}`;
355
+ const known = context.wallClocks.get(key);
356
+ if (known !== undefined) {
357
+ return known;
358
+ }
359
+ const resolved = resolveWallClock(context.frame, date, time, context.disambiguation);
360
+ context.wallClocks.set(key, resolved);
361
+ return resolved;
362
+ }
363
+ /**
364
+ * The whole local windows inside `[from, to)` in the reference zone, their edges resolved under
365
+ * the call's `disambiguation`.
366
+ *
367
+ * - `strict`: under `"reject"`, a window with a repeated or skipped edge that reaches `[from, to)`
368
+ * throws `WallClockRejected`. Without it, such an edge is read `"compatible"`: for rest that is
369
+ * only supposed (the best case of a window not yet over), or for finding candidate instants.
370
+ */
371
+ function localWindowsIn(context, window, from, to, strict) {
372
+ const found = [];
373
+ const instances = windowInstances(context, window, from, to);
374
+ // Every policy maps wall times to instants without reversing them, so the instances are in
375
+ // order by start and by end: skip to the first that ends after `from`.
376
+ let low = 0;
377
+ let high = instances.length;
378
+ // A binary search over at most 2^32 instances.
379
+ for (let step = 0; step < 33 && low < high; step++) {
380
+ const middle = (low + high) >> 1;
381
+ if (instances[middle].end <= from) {
382
+ low = middle + 1;
383
+ }
384
+ else {
385
+ high = middle;
386
+ }
387
+ }
388
+ for (let index = low; index < instances.length; index++) {
389
+ const instance = instances[index];
390
+ if (instance.start >= to) {
391
+ break;
392
+ }
393
+ // Under "reject", a window with a repeated or skipped edge that reaches the run is refused.
394
+ if (strict && instance.rejected && instance.end > from) {
395
+ throw new WallClockRejected("a local window the zone repeats or skips");
396
+ }
397
+ if (instance.start < instance.end &&
398
+ instance.start >= from &&
399
+ instance.end <= to) {
400
+ found.push({ start: instance.start, end: instance.end });
401
+ }
402
+ }
403
+ return found;
404
+ }
405
+ /** Two days: how far either side of a request the window instances are resolved and kept. */
406
+ const WINDOW_MARGIN = 172800000000000n;
407
+ /** The instances of a local window on the dates `first` to `last`, in date order. */
408
+ function instancesOn(context, window, first, last) {
409
+ const instances = [];
410
+ let date = first;
411
+ for (let step = 0; date !== null && Temporal.PlainDate.compare(date, last) <= 0; step++) {
412
+ if (step > 2 * MAX_PERIOD_COUNT) {
413
+ throw new Unrepresentable("too many local windows");
414
+ }
415
+ const next = datePlusDays(date, 1);
416
+ const start = wallClockOf(context, date, window.from);
417
+ const end = window.wraps && next === null
418
+ ? null
419
+ : wallClockOf(context, window.wraps ? next : date, window.to);
420
+ if (start !== null && end !== null) {
421
+ instances.push({
422
+ start: start.at,
423
+ end: end.at,
424
+ rejected: start.rejected || end.rejected,
425
+ });
426
+ }
427
+ date = next;
428
+ }
429
+ return instances;
430
+ }
431
+ /** How close a request must be to a stretch already resolved to extend it: thirty days. */
432
+ const WINDOW_JOIN = 2592000000000000n;
433
+ /**
434
+ * Every instance of a local window on the local dates around `[from, to)`, resolved once per call
435
+ * and kept, so each later evaluation only filters them.
436
+ *
437
+ * - A request near a stretch of dates already kept resolves only the dates it adds, so a scan
438
+ * that moves along the log resolves each date once. A request far from every stretch starts a
439
+ * stretch of its own: the dates between two far-apart rests are never resolved.
440
+ * - One request may need at most `2 × MAX_PERIOD_COUNT` dates; past that it throws
441
+ * `Unrepresentable`. What is kept from earlier requests does not count against it.
442
+ */
443
+ function windowInstances(context, window, from, to) {
444
+ const key = `${window.from.toString()}-${window.to.toString()}`;
445
+ const stretches = context.windowInstances.get(key) ?? [];
446
+ const covering = stretches.find((stretch) => stretch.from <= from && stretch.to >= to);
447
+ if (covering !== undefined) {
448
+ return covering.instances;
449
+ }
450
+ const wantedFrom = from - WINDOW_MARGIN;
451
+ const wantedTo = to + WINDOW_MARGIN;
452
+ const wanted = datePlusDays(localDateOf(context.frame, wantedFrom), -1);
453
+ const until = localDateOf(context.frame, wantedTo - 1n);
454
+ if (wanted === null ||
455
+ wanted.until(until, { largestUnit: "day" }).days > 2 * MAX_PERIOD_COUNT) {
456
+ throw new Unrepresentable("too many local windows");
457
+ }
458
+ const known = stretches.find((stretch) => stretch.from - WINDOW_JOIN <= wantedTo &&
459
+ stretch.to + WINDOW_JOIN >= wantedFrom);
460
+ const spanFrom = known === undefined ? wantedFrom : min(known.from, wantedFrom);
461
+ const spanTo = known === undefined ? wantedTo : max(known.to, wantedTo);
462
+ // A fall-back can put an earlier instant on a later local date, so the dates kept stay a
463
+ // superset of the dates wanted.
464
+ const first = known !== undefined && Temporal.PlainDate.compare(known.first, wanted) < 0
465
+ ? known.first
466
+ : wanted;
467
+ const last = known !== undefined && Temporal.PlainDate.compare(known.last, until) > 0
468
+ ? known.last
469
+ : until;
470
+ let instances;
471
+ if (known === undefined) {
472
+ instances = instancesOn(context, window, first, last);
473
+ }
474
+ else {
475
+ const before = datePlusDays(known.first, -1);
476
+ const after = datePlusDays(known.last, 1);
477
+ instances = [
478
+ ...(before !== null && Temporal.PlainDate.compare(first, before) <= 0
479
+ ? instancesOn(context, window, first, before)
480
+ : []),
481
+ ...known.instances,
482
+ ...(after !== null && Temporal.PlainDate.compare(after, last) <= 0
483
+ ? instancesOn(context, window, after, last)
484
+ : []),
485
+ ];
486
+ }
487
+ const stretch = { from: spanFrom, to: spanTo, first, last, instances };
488
+ context.windowInstances.set(key, [
489
+ ...stretches.filter((other) => other !== known),
490
+ stretch,
491
+ ]);
492
+ return instances;
493
+ }
494
+ /** How many free periods one run of rest holds. */
495
+ function freePeriodsIn(context, rule, run, strict) {
496
+ if (rule.containing === null) {
497
+ return length(run) / rule.minimum;
498
+ }
499
+ // Greedy, earliest end first: each free period ends as soon as it is long enough and holds
500
+ // enough windows, which leaves the most room for the next.
501
+ const { window, count } = rule.containing;
502
+ const instances = localWindowsIn(context, window, run.start, run.end, strict);
503
+ let cursor = run.start;
504
+ let first = 0;
505
+ let found = 0;
506
+ // Each free period takes at least one window, so there are no more of them than windows. The
507
+ // cursor only moves on, so the first window after it is found by moving on too.
508
+ for (let step = 0; step <= instances.length; step++) {
509
+ for (; first < instances.length; first++) {
510
+ if (instances[first].start >= cursor) {
511
+ break;
512
+ }
513
+ }
514
+ const last = instances[first + count - 1];
515
+ const end = last === undefined ? null : max(cursor + rule.minimum, last.end);
516
+ if (end === null || end > run.end) {
517
+ break;
518
+ }
519
+ found++;
520
+ cursor = end;
521
+ }
522
+ return BigInt(found);
523
+ }
524
+ function evaluateFreePeriod(context, rule, at, anchor) {
525
+ const window = windowOf(context, rule.within, at, anchor);
526
+ // Every count below ends at or before `at`, so the runs of the whole log, cut to the count's
527
+ // span, are the runs of the log as it stands at `at`.
528
+ const rests = logRuns(context, rule.restStatuses);
529
+ const countIn = (from, to, extendTo) => {
530
+ const inside = runsBetween(rests, from, to);
531
+ const runs = extendTo === undefined ? inside : withRestUntil(inside, at, extendTo);
532
+ // Rest supposed from `at` is not the log, so "reject" does not refuse its windows.
533
+ const strict = extendTo === undefined;
534
+ return runs.reduce((sum, run) => sum + freePeriodsIn(context, rule, run, strict), 0n);
535
+ };
536
+ // `atLeast` is a safe integer, so a count capped there compares with it as the count does.
537
+ const enough = (count) => count >= BigInt(rule.atLeast);
538
+ const { span } = window;
539
+ const found = exactCount(countIn(span.start, min(span.end, at)));
540
+ const unfinished = span.end > at;
541
+ const met = found >= rule.atLeast ||
542
+ (unfinished && enough(countIn(span.start, at, span.end)));
543
+ let dueBy = null;
544
+ if (found >= rule.atLeast && rule.atLeast > 0) {
545
+ if (rule.within.kind === "elapsed") {
546
+ // With no rest after `at`, the count of a window ending at t only falls as t grows, and
547
+ // a window starting at `at` holds none: find the last t that still holds enough.
548
+ const windowLength = rule.within.length;
549
+ let low = at;
550
+ let high = at + windowLength;
551
+ // A Temporal duration is under 2^53 seconds, so under 2^83 nanoseconds: 90 halvings
552
+ // always settle it.
553
+ for (let step = 0; step < 90 && high - low > 1n; step++) {
554
+ const middle = (low + high) / 2n;
555
+ if (enough(countIn(middle - windowLength, at))) {
556
+ low = middle;
557
+ }
558
+ else {
559
+ high = middle;
560
+ }
561
+ }
562
+ dueBy = low;
563
+ }
564
+ else {
565
+ // The window k periods later runs from the k-th period of this one, for `count` periods.
566
+ // Its count only falls as k grows, and k = count holds none, so search for the first short.
567
+ const scale = scaleFor(context, rule.within.unit);
568
+ const count = rule.within.count;
569
+ const first = scale.containing(span.start);
570
+ const unitAt = (index) => {
571
+ // The scale keeps the periods it walks, so the search below walks each one once.
572
+ const found = first === null ? null : periodAfter(scale, first, index);
573
+ if (found === null) {
574
+ throw new Unrepresentable("period outside the instant range");
575
+ }
576
+ return found;
577
+ };
578
+ let low = 1;
579
+ let high = count;
580
+ for (let step = 0; step < 32 && low < high; step++) {
581
+ const middle = (low + high) >> 1;
582
+ if (!enough(countIn(unitAt(middle).start, at))) {
583
+ high = middle;
584
+ }
585
+ else {
586
+ low = middle + 1;
587
+ }
588
+ }
589
+ dueBy = unitAt(low + count - 1).end;
590
+ }
591
+ }
592
+ else if (met) {
593
+ dueBy = span.end;
594
+ }
595
+ return {
596
+ satisfied: met,
597
+ used: tally(found),
598
+ limit: tally(rule.atLeast),
599
+ remaining: tally(rule.atLeast - found),
600
+ window: span,
601
+ periodBasis: basisOf(window),
602
+ dueBy,
603
+ resetAt: null,
604
+ excess: BigInt(rule.atLeast - found),
605
+ grouping: groupingOf(window),
606
+ };
607
+ }
608
+ function evaluateRestRequirement(context, rule, log, at, anchor) {
609
+ const window = windowOf(context, rule.per, at, anchor);
610
+ const { span } = window;
611
+ const rests = runsOf(log, rule.restStatuses);
612
+ const inside = clipRuns(rests, span.start, min(span.end, at));
613
+ const actual = restChecks(rule, inside.map(length));
614
+ const met = actual.met ||
615
+ (span.end > at &&
616
+ restChecks(rule, withRestUntil(inside, at, span.end).map(length)).met);
617
+ // A failed rule shows the floor it failed; one that holds, or can still be met before its
618
+ // window ends, shows the rest counted so far against the minimum.
619
+ const shown = (met ? undefined : actual.failing) ?? actual.total;
620
+ const rest = {
621
+ window: span,
622
+ periodBasis: basisOf(window),
623
+ resetAt: null,
624
+ grouping: groupingOf(window),
625
+ };
626
+ if (rule.maxGap !== null) {
627
+ const last = rests[rests.length - 1];
628
+ const resting = last !== undefined && last.end === at;
629
+ // A stretch without rest runs from the end of one rest to the start of the next. The log's
630
+ // first entry starts one too, as a rest ending there would: time before the log is unknown,
631
+ // but the work logged before the first rest is a stretch without rest like any other.
632
+ const logStart = log[0]?.start;
633
+ const gapStarts = logStart === undefined || rests[0]?.start === logStart
634
+ ? rests.map((run) => run.end)
635
+ : [logStart, ...rests.map((run) => run.end)];
636
+ const gapEnds = gapStarts.length > rests.length
637
+ ? rests.map((run) => run.start)
638
+ : rests.slice(1).map((run) => run.start);
639
+ let widest = 0n;
640
+ for (let index = 0; index < gapStarts.length; index++) {
641
+ const gapStart = gapStarts[index];
642
+ const gapEnd = gapEnds[index] ?? (resting ? gapStart : at);
643
+ if (gapEnd > span.start && gapStart < min(span.end, at)) {
644
+ widest = max(widest, gapEnd - gapStart);
645
+ }
646
+ }
647
+ const lastGapStart = gapStarts[gapStarts.length - 1];
648
+ const dueBy = lastGapStart !== undefined && !resting
649
+ ? lastGapStart + rule.maxGap
650
+ : null;
651
+ if (widest > rule.maxGap) {
652
+ return {
653
+ satisfied: false,
654
+ used: duration(widest),
655
+ limit: duration(rule.maxGap),
656
+ remaining: duration(0n),
657
+ excess: widest - rule.maxGap,
658
+ dueBy,
659
+ ...rest,
660
+ };
661
+ }
662
+ return floored(shown.used, shown.limit, met, {
663
+ ...rest,
664
+ dueBy,
665
+ });
666
+ }
667
+ return floored(shown.used, shown.limit, met, {
668
+ ...rest,
669
+ dueBy: null,
670
+ });
671
+ }
672
+ function evaluateRestBeforeDuty(rule, log, at) {
673
+ const isRest = (status) => rule.restStatuses.has(status);
674
+ const rests = runsOf(log, isRest);
675
+ const last = log[log.length - 1];
676
+ const onDuty = last !== undefined && last.end === at && !isRest(last.status);
677
+ // On duty, the duty started where the run outside rest reaching `at` began; resting, a duty
678
+ // starting now is the one to be ready for.
679
+ const dutyStart = onDuty
680
+ ? runsOf(log, (status) => !isRest(status)).at(-1).start
681
+ : at;
682
+ const logStart = log[0]?.start ?? at;
683
+ if (rule.orFactor !== null) {
684
+ // The rest judged is the one right before the duty, measured whole, against the duty it
685
+ // ends: from the end of the last rest of at least `minimum` before it, breaks included, to
686
+ // where it starts. A duty between an earlier rest and this one is never skipped, and more
687
+ // rest before the duty only lengthens the rest or shortens the duty before it.
688
+ const rest = rests.find((run) => run.end === dutyStart);
689
+ const restStart = rest?.start ?? dutyStart;
690
+ const fullRest = rests
691
+ .filter((run) => run.end <= restStart && length(run) >= rule.minimum)
692
+ .at(-1);
693
+ const dutyFrom = fullRest?.end ?? min(logStart, restStart);
694
+ const previousDuty = restStart - dutyFrom;
695
+ const { numerator, denominator } = rule.orFactor.k;
696
+ // k × the duty, rounded up to the nanosecond, exactly.
697
+ const scaled = (previousDuty * numerator + denominator - 1n) / denominator;
698
+ const limit = max(rule.minimum, max(scaled, rule.orFactor.floor));
699
+ const used = rest === undefined ? 0n : length(rest);
700
+ return {
701
+ ...floored(used, limit, !onDuty || used >= limit, {
702
+ window: { start: dutyFrom, end: dutyStart },
703
+ periodBasis: "elapsed",
704
+ dueBy: null,
705
+ resetAt: null,
706
+ grouping: "start",
707
+ }),
708
+ // A rest that starts with the log, or a duty with none logged before it, may have more
709
+ // rest before the log: the record does not judge it.
710
+ beforeLog: restStart <= logStart,
711
+ key: restStart,
712
+ };
713
+ }
714
+ const windowStart = max(dutyStart - rule.within, MIN_INSTANT_NANOSECONDS);
715
+ // The rest before the duty: the longest run of rest inside the window, the latest on a tie.
716
+ let rest;
717
+ for (const run of rests) {
718
+ const inside = min(run.end, dutyStart) - max(run.start, windowStart);
719
+ if (inside > 0n && (rest === undefined || inside >= rest.inside)) {
720
+ rest = { run, inside };
721
+ }
722
+ }
723
+ const used = rest?.inside ?? 0n;
724
+ return {
725
+ ...floored(used, rule.minimum, !onDuty || used >= rule.minimum, {
726
+ window: { start: windowStart, end: dutyStart },
727
+ periodBasis: "elapsed",
728
+ dueBy: null,
729
+ resetAt: null,
730
+ grouping: "start",
731
+ }),
732
+ // Every duty start after one short rest is the same failure: the record lists it once.
733
+ key: rest?.run.start ?? windowStart,
734
+ };
735
+ }
736
+ function evaluateRule(context, rule, at, anchor) {
737
+ // A free period reads only runs of rest, kept for the whole call, not the log cut at `at`.
738
+ if (rule.kind === "freePeriod") {
739
+ return evaluateFreePeriod(context, rule, at, anchor);
740
+ }
741
+ const log = clip(context.entries, at);
742
+ const first = log[0]?.start ?? at;
743
+ switch (rule.kind) {
744
+ case "cumulativeCap": {
745
+ const reset = lastAtLeast(runsOf(log, rule.since.statuses), rule.since.consecutive);
746
+ const from = reset?.end ?? first;
747
+ return capped(duration(timeIn(log, rule.statuses, from, at)), duration(rule.cap), {
748
+ window: { start: from, end: at },
749
+ periodBasis: null,
750
+ dueBy: null,
751
+ resetAt: reset?.end ?? null,
752
+ grouping: "start",
753
+ });
754
+ }
755
+ case "elapsedWindow": {
756
+ const rests = runsOf(log, rule.since.statuses);
757
+ const reset = lastAtLeast(rests, rule.since.consecutive);
758
+ const after = reset?.end ?? null;
759
+ const opener = log.find((entry) => (after === null || entry.start >= after) &&
760
+ rule.startStatuses.has(entry.status));
761
+ if (opener === undefined) {
762
+ return {
763
+ satisfied: true,
764
+ used: duration(0n),
765
+ limit: duration(rule.length),
766
+ remaining: duration(rule.length),
767
+ window: null,
768
+ periodBasis: null,
769
+ dueBy: null,
770
+ resetAt: after,
771
+ excess: 0n,
772
+ grouping: "start",
773
+ };
774
+ }
775
+ let opensAt = opener.start;
776
+ let second;
777
+ if (rule.exclude !== null) {
778
+ const pairs = restPairs(rests.filter((run) => run.start >= opener.start), rule.since.consecutive, rule.exclude);
779
+ const latest = pairs[pairs.length - 1];
780
+ if (latest !== undefined) {
781
+ opensAt = latest.first.end;
782
+ second = latest.second;
783
+ }
784
+ }
785
+ // The window's time stops while the second rest of a pair lasts, so a second rest that
786
+ // starts before the close moves it by its whole length, and one that starts after the close
787
+ // moves nothing. The extension and the second rest each lengthen the window, so either one
788
+ // can bring the other inside it: apply each once, as soon as it falls inside.
789
+ let closesAt = opensAt + rule.length;
790
+ let limit = rule.length;
791
+ let excluded = 0n;
792
+ let extended = false;
793
+ for (let pass = 0; pass < 2; pass++) {
794
+ if (excluded === 0n &&
795
+ second !== undefined &&
796
+ second.start < closesAt) {
797
+ excluded = length(second);
798
+ closesAt += excluded;
799
+ }
800
+ if (!extended &&
801
+ rule.extension !== null &&
802
+ opener.start <= rule.extension.from &&
803
+ rule.extension.from < closesAt) {
804
+ extended = true;
805
+ closesAt += rule.extension.by;
806
+ limit += rule.extension.by;
807
+ }
808
+ }
809
+ const late = timeIn(log, rule.restrictedStatuses, closesAt, at);
810
+ return {
811
+ satisfied: late === 0n,
812
+ used: duration(at - opensAt - excluded),
813
+ limit: duration(limit),
814
+ remaining: duration(closesAt - at),
815
+ window: { start: opensAt, end: closesAt },
816
+ periodBasis: "elapsed",
817
+ dueBy: closesAt,
818
+ resetAt: after,
819
+ excess: late,
820
+ grouping: "start",
821
+ };
822
+ }
823
+ case "requiredInterruption": {
824
+ const outside = rule.outsideStatuses;
825
+ const breaks = runsOf(log, (status) => !rule.countedStatuses.has(status) &&
826
+ (outside === null || outside.has(status)));
827
+ let resetAt = null;
828
+ let part = 0;
829
+ for (const pause of breaks) {
830
+ const taken = length(pause);
831
+ const split = rule.orderedSplit;
832
+ if (taken >= rule.minimum) {
833
+ resetAt = pause.end;
834
+ part = 0;
835
+ }
836
+ else if (split !== null && taken >= split[part]) {
837
+ part++;
838
+ if (part === split.length) {
839
+ resetAt = pause.end;
840
+ part = 0;
841
+ }
842
+ }
843
+ }
844
+ const from = resetAt ?? first;
845
+ return capped(duration(timeIn(log, rule.countedStatuses, from, at)), duration(rule.after), {
846
+ window: { start: from, end: at },
847
+ periodBasis: null,
848
+ dueBy: null,
849
+ resetAt,
850
+ grouping: "start",
851
+ });
852
+ }
853
+ case "rollingCap": {
854
+ const window = windowOf(context, rule.window, at, anchor);
855
+ const reset = rule.reset === null
856
+ ? undefined
857
+ : lastAtLeast(runsOf(log, rule.reset.statuses), rule.reset.consecutive);
858
+ const from = reset !== undefined && reset.end > window.span.start
859
+ ? reset.end
860
+ : window.span.start;
861
+ const to = min(window.span.end, at);
862
+ let used = timeIn(log, rule.statuses, from, to);
863
+ let limit = rule.cap;
864
+ if (rule.precondition !== null) {
865
+ const { restStatuses, minimum, beforeHours } = rule.precondition;
866
+ const rest = clipRuns(runsOf(log, restStatuses), from, to).find((run) => length(run) >= minimum);
867
+ const before = rest === undefined
868
+ ? used
869
+ : timeIn(log, rule.statuses, from, rest.start);
870
+ if (rest === undefined || before > beforeHours) {
871
+ limit = beforeHours;
872
+ used = before;
873
+ }
874
+ }
875
+ return capped(duration(used), duration(limit), {
876
+ window: { start: from, end: window.span.end },
877
+ periodBasis: basisOf(window),
878
+ dueBy: null,
879
+ resetAt: reset?.end ?? null,
880
+ grouping: groupingOf(window),
881
+ });
882
+ }
883
+ case "fixedPeriodCap": {
884
+ const window = windowOf(context, repeated(rule.period, rule.adjacent), at, anchor);
885
+ const total = timeIn(log, rule.statuses, window.span.start, min(window.span.end, at));
886
+ const rest = {
887
+ window: window.span,
888
+ periodBasis: basisOf(window),
889
+ dueBy: null,
890
+ resetAt: null,
891
+ grouping: groupingOf(window),
892
+ };
893
+ if (!rule.average) {
894
+ return capped(duration(total), duration(rule.cap), rest);
895
+ }
896
+ const periods = BigInt(rule.adjacent);
897
+ const allowed = rule.cap * periods;
898
+ return {
899
+ satisfied: total <= allowed,
900
+ used: duration((total + periods - 1n) / periods),
901
+ limit: duration(rule.cap),
902
+ remaining: duration(allowed - total),
903
+ excess: total - allowed,
904
+ ...rest,
905
+ };
906
+ }
907
+ case "restRequirement":
908
+ return evaluateRestRequirement(context, rule, log, at, anchor);
909
+ case "restBeforeDuty":
910
+ return evaluateRestBeforeDuty(rule, log, at);
911
+ case "quota": {
912
+ const continuing = context.entries.some((entry) => rule.statuses.has(entry.status) &&
913
+ entry.start <= at &&
914
+ entry.end > at);
915
+ const events = runsOf(log, rule.statuses).filter((run) => !(continuing && run.end === at) &&
916
+ length(run) >= rule.minimum &&
917
+ length(run) < rule.maximum);
918
+ if ("every" in rule.reset) {
919
+ const window = windowOf(context, rule.reset.every, at, anchor);
920
+ const to = min(window.span.end, at);
921
+ const found = events.filter((run) => run.start >= window.span.start && run.start < to).length;
922
+ return capped(tally(found), tally(rule.count), {
923
+ window: window.span,
924
+ periodBasis: basisOf(window),
925
+ dueBy: null,
926
+ resetAt: null,
927
+ grouping: groupingOf(window),
928
+ });
929
+ }
930
+ const reset = lastAtLeast(runsOf(log, rule.reset.statuses), rule.reset.consecutive);
931
+ const from = reset?.end ?? first;
932
+ return capped(tally(events.filter((run) => run.start >= from).length), tally(rule.count), {
933
+ window: { start: from, end: at },
934
+ periodBasis: null,
935
+ dueBy: null,
936
+ resetAt: reset?.end ?? null,
937
+ grouping: "start",
938
+ });
939
+ }
940
+ case "dueBy": {
941
+ const events = runsOf(log, rule.statuses);
942
+ const event = lastAtLeast(events, rule.minimum);
943
+ const reset = rule.reset === null
944
+ ? undefined
945
+ : lastAtLeast(runsOf(log, rule.reset.statuses), rule.reset.consecutive);
946
+ const clock = event === undefined
947
+ ? (reset?.end ?? null)
948
+ : reset === undefined
949
+ ? event.end
950
+ : max(event.end, reset.end);
951
+ const from = clock ?? first;
952
+ const due = boundaryAfter(context, rule.period, rule.periods, from);
953
+ const latest = events[events.length - 1];
954
+ const pending = latest !== undefined && latest.end === at && latest.start <= due;
955
+ return {
956
+ satisfied: at <= due || pending,
957
+ used: duration(at - from),
958
+ limit: duration(due - from),
959
+ remaining: duration(due - at),
960
+ window: { start: from, end: due },
961
+ periodBasis: rule.period.kind === "elapsed"
962
+ ? "elapsed"
963
+ : scaleFor(context, rule.period.unit).basis,
964
+ dueBy: due,
965
+ resetAt: clock,
966
+ excess: at - due,
967
+ grouping: "start",
968
+ };
969
+ }
970
+ case "pattern": {
971
+ const window = windowOf(context, repeated(rule.period, rule.lookback), at, anchor);
972
+ const { span } = window;
973
+ const to = min(span.end, at);
974
+ const unfinished = span.end > at;
975
+ const counts = new Map();
976
+ for (const named of rule.classes) {
977
+ const runs = runsOf(log, named.statuses);
978
+ const qualifies = (run) => run.start >= span.start &&
979
+ run.start < to &&
980
+ length(run) >= named.minimum;
981
+ const found = runs.filter(qualifies).length;
982
+ const last = runs[runs.length - 1];
983
+ let best = found;
984
+ if (unfinished) {
985
+ // A run counts in the window it starts in, measured whole, so the most the window can
986
+ // still gain is the runs that can start before it ends, each `minimum` long before the
987
+ // next. A run still going at `at` that started inside the window qualifies by going on;
988
+ // one that started before it must end first.
989
+ let firstStart = at;
990
+ if (last !== undefined &&
991
+ last.end === at &&
992
+ !qualifies(last) &&
993
+ last.start >= span.start) {
994
+ best += 1;
995
+ firstStart = last.start + named.minimum;
996
+ }
997
+ if (firstStart < span.end) {
998
+ const room = span.end - firstStart;
999
+ // `best` is only held to `atLeast`, a safe integer, so a count past the safe
1000
+ // integers is capped there without changing an answer.
1001
+ best += Number(min((room + named.minimum - 1n) / named.minimum, BigInt(Number.MAX_SAFE_INTEGER)));
1002
+ }
1003
+ }
1004
+ counts.set(named.label, { found, best });
1005
+ }
1006
+ const slack = rule.require.map((required) => {
1007
+ const tallied = counts.get(required.label);
1008
+ return {
1009
+ ...required,
1010
+ found: tallied.found,
1011
+ actual: tallied.found - required.atLeast,
1012
+ best: tallied.best - required.atLeast,
1013
+ };
1014
+ });
1015
+ const tightest = slack.reduce((worst, item) => item.actual < worst.actual ? item : worst);
1016
+ const met = slack.every((item) => item.actual >= 0) ||
1017
+ (unfinished && slack.every((item) => item.best >= 0));
1018
+ return {
1019
+ satisfied: met,
1020
+ used: tally(tightest.found),
1021
+ limit: tally(tightest.atLeast),
1022
+ remaining: tally(-tightest.actual),
1023
+ window: span,
1024
+ periodBasis: basisOf(window),
1025
+ dueBy: null,
1026
+ resetAt: null,
1027
+ excess: BigInt(-tightest.actual),
1028
+ grouping: groupingOf(window),
1029
+ };
1030
+ }
1031
+ case "occurrenceCap": {
1032
+ const scale = scaleFor(context, rule.per.unit);
1033
+ const runs = runsOf(log, rule.statuses);
1034
+ const blocks = [];
1035
+ let blockAnchor = anchor;
1036
+ for (let index = 0; index < rule.lookback; index++) {
1037
+ const block = periodsEndingAt(scale, rule.per.count, blockAnchor);
1038
+ if (block === null) {
1039
+ throw new Unrepresentable("period outside the instant range");
1040
+ }
1041
+ blocks.push(block);
1042
+ blockAnchor = block.start - 1n;
1043
+ }
1044
+ const occupied = blocks.filter((block) => {
1045
+ const to = min(block.end, at);
1046
+ return runs.some((run) => rule.byInitiationDate
1047
+ ? run.start >= block.start && run.start < to
1048
+ : run.start < to && run.end > block.start);
1049
+ }).length;
1050
+ const earliest = blocks[blocks.length - 1];
1051
+ const latest = blocks[0];
1052
+ return capped(tally(occupied), tally(rule.maximum), {
1053
+ window: { start: earliest.start, end: latest.end },
1054
+ periodBasis: scale.basis,
1055
+ dueBy: null,
1056
+ resetAt: null,
1057
+ grouping: rule.lookback * rule.per.count > 1 ? "slide" : "period",
1058
+ });
1059
+ }
1060
+ case "usageLookback": {
1061
+ const window = windowOf(context, rule.notWithin, at, anchor);
1062
+ const reset = lastAtLeast(runsOf(log, rule.reset.statuses), rule.reset.consecutive);
1063
+ const from = reset !== undefined && reset.end > window.span.start
1064
+ ? reset.end
1065
+ : window.span.start;
1066
+ // A use is an instant, and each window holds it once. An elapsed window runs up to and
1067
+ // including the use being made at `at`, so one exactly its length earlier is outside it; a
1068
+ // window of local periods holds only its own, so one on its end is the next's.
1069
+ const found = rule.uses.filter((use) => use >= from &&
1070
+ use <= at &&
1071
+ (rule.notWithin.kind === "elapsed"
1072
+ ? use > at - rule.notWithin.length
1073
+ : use < window.span.end)).length;
1074
+ return capped(tally(found), tally(1), {
1075
+ window: { start: from, end: window.span.end },
1076
+ periodBasis: basisOf(window),
1077
+ dueBy: null,
1078
+ resetAt: reset?.end ?? null,
1079
+ grouping: groupingOf(window),
1080
+ });
1081
+ }
1082
+ }
1083
+ }
1084
+ const instantText = (nanoseconds) => Temporal.Instant.fromEpochNanoseconds(nanoseconds).toString();
1085
+ function measureText(measure) {
1086
+ if (measure === null) {
1087
+ return null;
1088
+ }
1089
+ if (!("duration" in measure)) {
1090
+ return measure.count;
1091
+ }
1092
+ const text = formatHourDuration(measure.duration);
1093
+ // A requirement `orFactor` scales can pass the longest duration Temporal holds.
1094
+ if (text === "") {
1095
+ throw new Unrepresentable("a duration past Temporal's range");
1096
+ }
1097
+ return text;
1098
+ }
1099
+ function spanText(span) {
1100
+ return { start: instantText(span.start), end: instantText(span.end) };
1101
+ }
1102
+ /**
1103
+ * Evaluate every rule at one instant.
1104
+ *
1105
+ * @param context the call's context
1106
+ * @param rules the resolved rules
1107
+ * @param at the instant, epoch nanoseconds
1108
+ * @returns one outcome per rule, in order
1109
+ */
1110
+ export function evaluateDutyRules(context, rules, at) {
1111
+ return rules.map((rule) => {
1112
+ // The log is read up to `at`, so its local periods are the ones holding the instant before:
1113
+ // at a boundary, the period just ended is judged, and a roster ending there is checked whole.
1114
+ const evaluation = evaluateRule(context, rule, at, at - 1n);
1115
+ return {
1116
+ label: rule.label,
1117
+ kind: rule.kind,
1118
+ severity: rule.severity,
1119
+ satisfied: evaluation.satisfied,
1120
+ used: measureText(evaluation.used),
1121
+ limit: measureText(evaluation.limit),
1122
+ remaining: measureText(evaluation.remaining),
1123
+ window: evaluation.window === null ? null : spanText(evaluation.window),
1124
+ periodBasis: evaluation.periodBasis,
1125
+ dueBy: evaluation.dueBy === null ? null : instantText(evaluation.dueBy),
1126
+ resetAt: evaluation.resetAt === null ? null : instantText(evaluation.resetAt),
1127
+ };
1128
+ });
1129
+ }
1130
+ /** The elapsed lengths a rule's window slides by, for the candidate instants of a range. */
1131
+ function slideLengths(rule) {
1132
+ const periods = [];
1133
+ switch (rule.kind) {
1134
+ case "rollingCap":
1135
+ periods.push(rule.window);
1136
+ break;
1137
+ case "restRequirement":
1138
+ periods.push(rule.per);
1139
+ break;
1140
+ case "freePeriod":
1141
+ periods.push(rule.within);
1142
+ break;
1143
+ case "quota":
1144
+ if ("every" in rule.reset) {
1145
+ periods.push(rule.reset.every);
1146
+ }
1147
+ break;
1148
+ case "pattern":
1149
+ periods.push(repeated(rule.period, rule.lookback));
1150
+ break;
1151
+ case "usageLookback":
1152
+ periods.push(rule.notWithin);
1153
+ break;
1154
+ default:
1155
+ break;
1156
+ }
1157
+ return periods.flatMap((period) => period.kind === "elapsed" ? [period.length] : []);
1158
+ }
1159
+ /** The local units a rule counts in, whose boundaries are candidate instants of a range. */
1160
+ function unitsOf(rule) {
1161
+ const periods = [];
1162
+ switch (rule.kind) {
1163
+ case "rollingCap":
1164
+ periods.push(rule.window);
1165
+ break;
1166
+ case "fixedPeriodCap":
1167
+ periods.push(rule.period);
1168
+ break;
1169
+ case "restRequirement":
1170
+ periods.push(rule.per);
1171
+ break;
1172
+ case "freePeriod":
1173
+ periods.push(rule.within);
1174
+ break;
1175
+ case "quota":
1176
+ if ("every" in rule.reset) {
1177
+ periods.push(rule.reset.every);
1178
+ }
1179
+ break;
1180
+ case "dueBy":
1181
+ case "pattern":
1182
+ periods.push(rule.period);
1183
+ break;
1184
+ case "occurrenceCap":
1185
+ periods.push(rule.per);
1186
+ break;
1187
+ case "usageLookback":
1188
+ periods.push(rule.notWithin);
1189
+ break;
1190
+ default:
1191
+ break;
1192
+ }
1193
+ return periods.flatMap((period) => period.kind === "units" ? [period.unit] : []);
1194
+ }
1195
+ const NANOSECONDS_PER_DAY = 86400000000000n;
1196
+ /**
1197
+ * More than any one local period of a unit can last: its longest calendar length plus a week. A
1198
+ * clock change moves the wall clock by less than 48 hours, since Temporal holds every offset
1199
+ * strictly within ±24 hours, so neither a skipped date nor a repeated stretch can add a week.
1200
+ */
1201
+ const LONGER_THAN_ANY = {
1202
+ operatorDay: 8n * NANOSECONDS_PER_DAY,
1203
+ day: 8n * NANOSECONDS_PER_DAY,
1204
+ week: 14n * NANOSECONDS_PER_DAY,
1205
+ month: 38n * NANOSECONDS_PER_DAY,
1206
+ year: 373n * NANOSECONDS_PER_DAY,
1207
+ };
1208
+ /**
1209
+ * Call `visit` with the end of every period of `scale` that ends inside `(from, until]`, starting
1210
+ * with the one `from` falls in.
1211
+ *
1212
+ * - Throws `Unrepresentable` when more than `MAX_PERIOD_COUNT` periods end there, or for a period
1213
+ * Temporal cannot hold.
1214
+ * - A span of `MAX_PERIOD_COUNT + 1` times `LONGER_THAN_ANY` or more certainly holds more ends
1215
+ * than that, so it is refused without a lookup. A shorter span is walked, and the walk decides.
1216
+ */
1217
+ export function walkBoundaries(scale, unit, from, until, visit) {
1218
+ if (from >= until) {
1219
+ return;
1220
+ }
1221
+ if (until - from >= BigInt(MAX_PERIOD_COUNT + 1) * LONGER_THAN_ANY[unit]) {
1222
+ throw new Unrepresentable("too many periods in the range");
1223
+ }
1224
+ let current = scale.containing(from);
1225
+ for (let step = 0; current !== null && current.end <= until; step++) {
1226
+ if (step >= MAX_PERIOD_COUNT) {
1227
+ throw new Unrepresentable("too many periods in the range");
1228
+ }
1229
+ visit(current.end);
1230
+ current = scale.containing(current.end);
1231
+ }
1232
+ if (current === null) {
1233
+ throw new Unrepresentable("period outside the instant range");
1234
+ }
1235
+ }
1236
+ /**
1237
+ * The runs whose lengths a rule compares with floors, and the floors: where a run reaches a floor
1238
+ * (`start + floor`), or, inside a sliding window, shrinks below it (`end + length − floor`), the
1239
+ * rule's answer can change between entry bounds.
1240
+ */
1241
+ function runFloors(rule) {
1242
+ switch (rule.kind) {
1243
+ case "cumulativeCap":
1244
+ return [
1245
+ {
1246
+ test: rule.since.statuses,
1247
+ floors: [rule.since.consecutive],
1248
+ sliding: false,
1249
+ },
1250
+ ];
1251
+ case "elapsedWindow":
1252
+ return [
1253
+ {
1254
+ test: rule.since.statuses,
1255
+ floors: [
1256
+ rule.since.consecutive,
1257
+ ...(rule.exclude === null
1258
+ ? []
1259
+ : [rule.exclude.each, rule.exclude.one]),
1260
+ ],
1261
+ sliding: false,
1262
+ },
1263
+ ];
1264
+ case "requiredInterruption": {
1265
+ const outside = rule.outsideStatuses;
1266
+ return [
1267
+ {
1268
+ test: (status) => !rule.countedStatuses.has(status) &&
1269
+ (outside === null || outside.has(status)),
1270
+ floors: [rule.minimum, ...(rule.orderedSplit ?? [])],
1271
+ sliding: false,
1272
+ },
1273
+ ];
1274
+ }
1275
+ case "rollingCap":
1276
+ return [
1277
+ ...(rule.reset === null
1278
+ ? []
1279
+ : [
1280
+ {
1281
+ test: rule.reset.statuses,
1282
+ floors: [rule.reset.consecutive],
1283
+ sliding: false,
1284
+ },
1285
+ ]),
1286
+ ...(rule.precondition === null
1287
+ ? []
1288
+ : [
1289
+ {
1290
+ test: rule.precondition.restStatuses,
1291
+ floors: [rule.precondition.minimum],
1292
+ sliding: true,
1293
+ },
1294
+ ]),
1295
+ ];
1296
+ case "restRequirement": {
1297
+ const floors = [rule.minimum];
1298
+ if (rule.composition !== null) {
1299
+ floors.push(rule.composition.longestAtLeast);
1300
+ if (rule.composition.othersAtLeast > 0n) {
1301
+ floors.push(rule.composition.othersAtLeast);
1302
+ }
1303
+ }
1304
+ if (rule.composite !== null) {
1305
+ floors.push(rule.composite.consecutive, rule.composite.blockMinimum, rule.composite.consecutive + rule.composite.additional);
1306
+ }
1307
+ return [{ test: rule.restStatuses, floors, sliding: true }];
1308
+ }
1309
+ case "freePeriod":
1310
+ // Multiples are added per run in `candidateInstants`: a run holds floor(length / minimum).
1311
+ return [];
1312
+ case "quota":
1313
+ return "every" in rule.reset
1314
+ ? []
1315
+ : [
1316
+ {
1317
+ test: rule.reset.statuses,
1318
+ floors: [rule.reset.consecutive],
1319
+ sliding: false,
1320
+ },
1321
+ ];
1322
+ case "dueBy":
1323
+ return [
1324
+ { test: rule.statuses, floors: [rule.minimum], sliding: false },
1325
+ ...(rule.reset === null
1326
+ ? []
1327
+ : [
1328
+ {
1329
+ test: rule.reset.statuses,
1330
+ floors: [rule.reset.consecutive],
1331
+ sliding: false,
1332
+ },
1333
+ ]),
1334
+ ];
1335
+ case "pattern":
1336
+ return rule.classes.map((named) => ({
1337
+ test: named.statuses,
1338
+ floors: [named.minimum],
1339
+ sliding: false,
1340
+ }));
1341
+ case "usageLookback":
1342
+ return [
1343
+ {
1344
+ test: rule.reset.statuses,
1345
+ floors: [rule.reset.consecutive],
1346
+ sliding: false,
1347
+ },
1348
+ ];
1349
+ default:
1350
+ return [];
1351
+ }
1352
+ }
1353
+ /**
1354
+ * The instants at which a rule's answer can change, inside `(start, end]`.
1355
+ *
1356
+ * A rule's measures are piecewise linear in the instant evaluated, so a failed window shows at a
1357
+ * breakpoint or just either side of one:
1358
+ *
1359
+ * - every entry bound and use, those bounds shifted by the rule's sliding lengths, and every
1360
+ * boundary of its local periods;
1361
+ * - where a run reaches or, in a sliding window, shrinks past a floor the rule compares it with;
1362
+ * - for a rest rule that picks its longest runs, where two runs cross as the longest;
1363
+ * - where a gap passes `maxGap`, a rest falls due, and a free period gains or loses a local window;
1364
+ * - the first instant after `start`, so a window failing as the range opens is seen.
1365
+ */
1366
+ function candidateInstants(context, rule, range) {
1367
+ // Nothing after the log's last entry is judged: the log says nothing about that time.
1368
+ const logEnd = context.entries[context.entries.length - 1]?.end;
1369
+ if (logEnd === undefined) {
1370
+ return [];
1371
+ }
1372
+ const until = min(range.end, logEnd);
1373
+ const base = [];
1374
+ for (const entry of context.entries) {
1375
+ base.push(entry.start, entry.end);
1376
+ }
1377
+ if (rule.kind === "usageLookback") {
1378
+ base.push(...rule.uses);
1379
+ }
1380
+ if (rule.kind === "elapsedWindow" && rule.extension !== null) {
1381
+ base.push(rule.extension.from);
1382
+ }
1383
+ // A rule's answer changes only at a breakpoint, so the instants either side of each one also
1384
+ // show where a failure starts and where a pass between two failures starts. Only the instants
1385
+ // inside `(range.start, until]` are kept.
1386
+ const points = new Set([range.start + 1n, until]);
1387
+ const add = (point) => {
1388
+ if (point > range.start && point <= until) {
1389
+ points.add(point);
1390
+ }
1391
+ };
1392
+ const around = (point) => {
1393
+ add(point - 1n);
1394
+ add(point);
1395
+ add(point + 1n);
1396
+ if (points.size > MAX_CANDIDATE_INSTANTS) {
1397
+ throw new Unrepresentable("too many instants to check");
1398
+ }
1399
+ };
1400
+ for (const point of base) {
1401
+ around(point);
1402
+ }
1403
+ const shifts = slideLengths(rule);
1404
+ for (const shift of shifts) {
1405
+ for (const point of base) {
1406
+ // A run leaves a window the instant after its bound plus the window's length.
1407
+ around(point + shift);
1408
+ }
1409
+ }
1410
+ for (const { test, floors, sliding } of runFloors(rule)) {
1411
+ for (const run of runsOf(context.entries, test)) {
1412
+ for (const floor of floors) {
1413
+ around(run.start + floor);
1414
+ if (sliding) {
1415
+ for (const shift of shifts) {
1416
+ around(run.end + shift - floor);
1417
+ }
1418
+ }
1419
+ }
1420
+ }
1421
+ }
1422
+ if (rule.kind === "elapsedWindow" && rule.exclude !== null) {
1423
+ // The second rest of a pair completes the combined floor part-way through. Its partner is
1424
+ // the one `restPairs` gives it: the last earlier run long enough to be half of a pair, with no
1425
+ // full rest between. A run shorter than `each` is skipped, never a partner.
1426
+ const fullRest = rule.since.consecutive;
1427
+ let partner;
1428
+ for (const run of runsOf(context.entries, rule.since.statuses)) {
1429
+ if (partner !== undefined) {
1430
+ around(run.start + rule.exclude.combined - length(partner));
1431
+ }
1432
+ const taken = length(run);
1433
+ if (taken >= fullRest) {
1434
+ partner = undefined;
1435
+ }
1436
+ else if (taken >= rule.exclude.each) {
1437
+ partner = run;
1438
+ }
1439
+ }
1440
+ }
1441
+ if (rule.kind === "restRequirement") {
1442
+ const rests = runsOf(context.entries, rule.restStatuses);
1443
+ const ranks = rule.composition !== null || rule.composite !== null;
1444
+ if (rule.maxGap !== null) {
1445
+ // A stretch without rest starts at each rest's end and at the log's first entry.
1446
+ for (const start of [
1447
+ ...rests.map((run) => run.end),
1448
+ ...context.entries.slice(0, 1).map((entry) => entry.start),
1449
+ ]) {
1450
+ around(start + rule.maxGap);
1451
+ }
1452
+ }
1453
+ for (const shift of ranks ? shifts : []) {
1454
+ // A rest that ends two window lengths before the range, or starts two after it, is out of
1455
+ // every window the range judges, so it crosses nothing there.
1456
+ const near = rests.filter((run) => run.end >= range.start - 2n * shift &&
1457
+ run.start <= until + 2n * shift);
1458
+ for (let index = 0; index < near.length; index++) {
1459
+ const earlier = near[index];
1460
+ // A rest shrinking out of the window crosses one growing at its end, or one held whole.
1461
+ for (let other = 0; other < near.length; other++) {
1462
+ const later = near[other];
1463
+ if (later.start - earlier.end >= 2n * shift ||
1464
+ earlier.start - later.end >= 2n * shift) {
1465
+ continue;
1466
+ }
1467
+ const crossing = (earlier.end + shift + later.start) / 2n;
1468
+ around(crossing);
1469
+ around(crossing + 1n);
1470
+ around(earlier.end + shift - length(later));
1471
+ around(later.start + length(earlier));
1472
+ }
1473
+ }
1474
+ }
1475
+ }
1476
+ if (rule.kind === "freePeriod") {
1477
+ const minimum = rule.minimum;
1478
+ // A run's points lie at most `atLeast + 1` free periods and a window length past its end, so
1479
+ // a run further than that before the range, or after it, adds none inside it.
1480
+ const farthest = (BigInt(rule.atLeast) + 1n) * minimum +
1481
+ shifts.reduce((longest, shift) => max(longest, shift), 0n);
1482
+ const rests = runsOf(context.entries, rule.restStatuses).filter((run) => run.end + farthest >= range.start && run.start <= until + 1n);
1483
+ // A window fails only while it holds fewer than `atLeast` free periods, so a run's count
1484
+ // changing above `atLeast` changes no answer: each run's first `atLeast + 1` multiples are
1485
+ // the ones that matter. Of those, only the ones that land in the range are taken.
1486
+ const reach = BigInt(rule.atLeast) + 1n;
1487
+ const multiplesFrom = (origin, last) => {
1488
+ const first = max(0n, floorDivide(range.start - origin, minimum) - 1n);
1489
+ const final = min(last, floorDivide(until - origin, minimum) + 1n);
1490
+ for (let multiple = first; multiple <= final; multiple++) {
1491
+ around(origin + multiple * minimum);
1492
+ }
1493
+ };
1494
+ const multiplesBefore = (origin, last) => {
1495
+ const first = max(0n, floorDivide(origin - until, minimum) - 1n);
1496
+ const final = min(last, floorDivide(origin - range.start, minimum) + 1n);
1497
+ for (let multiple = first; multiple <= final; multiple++) {
1498
+ around(origin - multiple * minimum);
1499
+ }
1500
+ };
1501
+ for (const run of rests) {
1502
+ const multiples = min(length(run) / minimum + 1n, reach);
1503
+ multiplesFrom(run.start, multiples);
1504
+ for (const shift of shifts) {
1505
+ multiplesBefore(run.end + shift, multiples);
1506
+ }
1507
+ if (rule.containing !== null) {
1508
+ const windows = localWindowsIn(context, rule.containing.window, run.start, run.end, false);
1509
+ for (const window of windows) {
1510
+ multiplesFrom(window.end, multiples);
1511
+ for (const shift of shifts) {
1512
+ around(window.start + shift);
1513
+ }
1514
+ }
1515
+ }
1516
+ }
1517
+ }
1518
+ if (rule.kind === "dueBy") {
1519
+ // A rest falls due the instant after its deadline; each run long enough to start the clock,
1520
+ // and the log's start, set one.
1521
+ const clocks = [
1522
+ context.entries[0]?.start,
1523
+ ...runsOf(context.entries, rule.statuses)
1524
+ .filter((run) => length(run) >= rule.minimum)
1525
+ .map((run) => run.end),
1526
+ ...(rule.reset === null
1527
+ ? []
1528
+ : runsOf(context.entries, rule.reset.statuses)
1529
+ .filter((run) => length(run) >=
1530
+ rule.reset.consecutive)
1531
+ .map((run) => run.end)),
1532
+ ];
1533
+ for (const clock of clocks) {
1534
+ if (clock !== undefined) {
1535
+ around(boundaryAfter(context, rule.period, rule.periods, clock));
1536
+ }
1537
+ }
1538
+ }
1539
+ // Before the log's first entry and the first use, no rule has anything to fail on, so the
1540
+ // period boundaries walked are the ones from there to `until`, however long `range` is.
1541
+ const from = max(range.start, firstEvidence(context, rule));
1542
+ for (const unit of unitsOf(rule)) {
1543
+ walkBoundaries(scaleFor(context, unit), unit, from, until, around);
1544
+ }
1545
+ return [...points]
1546
+ .filter((point) => point > range.start && point <= until)
1547
+ .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
1548
+ }
1549
+ /** `a / b` rounded down, for a positive `b`: bigint division rounds towards zero. */
1550
+ function floorDivide(a, b) {
1551
+ return a >= 0n ? a / b : -((-a + b - 1n) / b);
1552
+ }
1553
+ /**
1554
+ * The worse of two failures, `a` the earlier: the larger excess, and on a tie the earlier, whose
1555
+ * window is the first to show it (a cap's window ends when the time over it was worked, not after
1556
+ * the rest that followed).
1557
+ */
1558
+ function worse(a, b) {
1559
+ return b.evaluation.excess > a.evaluation.excess ? b : a;
1560
+ }
1561
+ const keyOf = (evaluation) => evaluation.key ?? evaluation.window.start;
1562
+ /** Whether a failure continues a violation: no passing check between, or the same key. */
1563
+ function continues(violation, failure) {
1564
+ return violation.grouping === "slide"
1565
+ ? violation.open
1566
+ : keyOf(failure.evaluation) === violation.key;
1567
+ }
1568
+ /** A violation's report after one more failure: the last check of a period, else the worst. */
1569
+ function report(violation, failure) {
1570
+ return violation.grouping === "period"
1571
+ ? failure
1572
+ : worse(violation.worst, failure);
1573
+ }
1574
+ /** The kinds that hold rest to a floor, which the record does not judge before the log starts. */
1575
+ const FLOOR_KINDS = new Set([
1576
+ "restRequirement",
1577
+ "freePeriod",
1578
+ "pattern",
1579
+ "restBeforeDuty",
1580
+ ]);
1581
+ /**
1582
+ * The most instants one rule may be checked at in one range. A log of a year has a few tens of
1583
+ * thousands; more than this is refused rather than checked one by one.
1584
+ */
1585
+ const MAX_CANDIDATE_INSTANTS = 1_000_000;
1586
+ /** Thirty days: how far past a range's end the scan reads at a time, so a period walk stays short. */
1587
+ const FORWARD_CHUNK = 2592000000000000n;
1588
+ /**
1589
+ * The earliest instant a rule has anything to fail on: the log's first entry, or for
1590
+ * `usageLookback` its first use when that is earlier. Undefined for an empty log.
1591
+ */
1592
+ function firstEvidence(context, rule) {
1593
+ const first = context.entries[0]?.start;
1594
+ return first === undefined
1595
+ ? undefined
1596
+ : [first, ...(rule.kind === "usageLookback" ? rule.uses : [])].reduce(min);
1597
+ }
1598
+ /**
1599
+ * The violations of one rule, each listed in the range it began in and reported by its worst
1600
+ * window, which may end after the range.
1601
+ *
1602
+ * - Failures inside `range` are grouped into violations as `scanDutyViolations` describes.
1603
+ * - The first is dropped when it was already failing at `range.start`: it belongs to the range
1604
+ * before. So two adjacent ranges list every violation once.
1605
+ * - The last is followed past `range.end` to its worst window, up to the log's last entry, never
1606
+ * into time the log does not cover.
1607
+ */
1608
+ function ruleViolations(context, rule, asked) {
1609
+ const evidence = firstEvidence(context, rule);
1610
+ if (evidence === undefined) {
1611
+ return [];
1612
+ }
1613
+ // Nothing fails before the rule's first evidence, so a range opening earlier opens just
1614
+ // before it: no check is made, and no local window resolved, decades before the log.
1615
+ const range = { start: max(asked.start, evidence - 1n), end: asked.end };
1616
+ // A floor on rest is not judged on a window that starts before the log: the log cannot say
1617
+ // what happened then. A cap is, since time no entry covers never counts towards one.
1618
+ const logStart = context.entries[0]?.start;
1619
+ const judgesRest = FLOOR_KINDS.has(rule.kind);
1620
+ const check = (at) => {
1621
+ const evaluation = evaluateRule(context, rule, at, at - 1n);
1622
+ const judged = evaluation.window !== null &&
1623
+ !(judgesRest &&
1624
+ logStart !== undefined &&
1625
+ (evaluation.window.start < logStart || evaluation.beforeLog === true));
1626
+ const failure = judged && !evaluation.satisfied ? { evaluation, at } : null;
1627
+ return { evaluation, failure };
1628
+ };
1629
+ const violations = [];
1630
+ const candidates = candidateInstants(context, rule, range);
1631
+ let firstChecked;
1632
+ for (const at of candidates) {
1633
+ const { failure } = check(at);
1634
+ const last = violations[violations.length - 1];
1635
+ if (firstChecked === undefined) {
1636
+ firstChecked = failure;
1637
+ }
1638
+ if (failure === null) {
1639
+ if (last !== undefined) {
1640
+ last.open = false;
1641
+ }
1642
+ }
1643
+ else if (last !== undefined && continues(last, failure)) {
1644
+ last.worst = report(last, failure);
1645
+ }
1646
+ else {
1647
+ violations.push({
1648
+ worst: failure,
1649
+ grouping: failure.evaluation.grouping,
1650
+ key: keyOf(failure.evaluation),
1651
+ open: true,
1652
+ });
1653
+ }
1654
+ }
1655
+ const first = violations[0];
1656
+ if (first !== undefined) {
1657
+ let before;
1658
+ if (first.grouping === "slide") {
1659
+ // Failing from the range's first instant, and already at its start.
1660
+ before =
1661
+ firstChecked !== undefined &&
1662
+ firstChecked !== null &&
1663
+ check(range.start).failure !== null;
1664
+ }
1665
+ else {
1666
+ const key = first.key;
1667
+ before =
1668
+ key < range.start &&
1669
+ candidateInstants(context, rule, { start: key, end: range.start }).some((at) => {
1670
+ const { failure } = check(at);
1671
+ return failure !== null && continues(first, failure);
1672
+ });
1673
+ }
1674
+ if (before) {
1675
+ violations.shift();
1676
+ }
1677
+ }
1678
+ const last = violations[violations.length - 1];
1679
+ const logEnd = context.entries[context.entries.length - 1]?.end;
1680
+ if (last !== undefined &&
1681
+ logEnd !== undefined &&
1682
+ logEnd > range.end &&
1683
+ (last.grouping !== "slide" || last.open)) {
1684
+ let cursor = range.end;
1685
+ let going = true;
1686
+ for (let chunk = 0; going && cursor < logEnd && chunk <= MAX_PERIOD_COUNT; chunk++) {
1687
+ const until = min(cursor + FORWARD_CHUNK, logEnd);
1688
+ for (const at of candidateInstants(context, rule, {
1689
+ start: cursor,
1690
+ end: until,
1691
+ })) {
1692
+ const { evaluation, failure } = check(at);
1693
+ if (last.grouping === "slide") {
1694
+ if (failure === null) {
1695
+ going = false;
1696
+ break;
1697
+ }
1698
+ last.worst = report(last, failure);
1699
+ }
1700
+ else if (evaluation.window === null ||
1701
+ keyOf(evaluation) !== last.key) {
1702
+ going = false;
1703
+ break;
1704
+ }
1705
+ else if (failure !== null) {
1706
+ last.worst = report(last, failure);
1707
+ }
1708
+ }
1709
+ cursor = until;
1710
+ }
1711
+ }
1712
+ return violations.map((violation) => violation.worst);
1713
+ }
1714
+ /**
1715
+ * Every violation of the rules whose severity is `"violation"` inside a range. A moving window's
1716
+ * failures are one violation while no check passes between them; a count since a reset, or one
1717
+ * fixed period, is one violation however often it is checked. Each is reported by its worst
1718
+ * window and listed in the range it began in.
1719
+ *
1720
+ * @param context the call's context
1721
+ * @param rules the resolved rules
1722
+ * @param range the range, epoch nanoseconds
1723
+ * @returns the violations, ordered by window start and then by rule
1724
+ */
1725
+ export function scanDutyViolations(context, rules, range) {
1726
+ const found = [];
1727
+ rules.forEach((rule, order) => {
1728
+ if (rule.severity === "violation") {
1729
+ for (const failure of ruleViolations(context, rule, range)) {
1730
+ found.push({ order, label: rule.label, failure });
1731
+ }
1732
+ }
1733
+ });
1734
+ return found
1735
+ .sort((a, b) => {
1736
+ const aStart = a.failure.evaluation.window.start;
1737
+ const bStart = b.failure.evaluation.window.start;
1738
+ return aStart < bStart ? -1 : aStart > bStart ? 1 : a.order - b.order;
1739
+ })
1740
+ .map(({ label, failure }) => ({
1741
+ label,
1742
+ window: spanText(failure.evaluation.window),
1743
+ used: measureText(failure.evaluation.used),
1744
+ limit: measureText(failure.evaluation.limit),
1745
+ }));
1746
+ }