temporal-workdays 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -3
- package/README.md +144 -82
- package/dist/index.cjs +816 -142
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +229 -28
- package/dist/index.d.ts +229 -28
- package/dist/index.js +809 -143
- package/dist/index.js.map +1 -1
- package/package.json +31 -7
package/dist/index.js
CHANGED
|
@@ -30,221 +30,887 @@ function daysInMonth(year, month) {
|
|
|
30
30
|
if (month === 2) return isLeapYear(year) ? 29 : 28;
|
|
31
31
|
return month === 4 || month === 6 || month === 9 || month === 11 ? 30 : 31;
|
|
32
32
|
}
|
|
33
|
+
function floorDiv(a, b) {
|
|
34
|
+
const q = a / b;
|
|
35
|
+
return a % b < 0n ? q - 1n : q;
|
|
36
|
+
}
|
|
33
37
|
|
|
34
|
-
// src/
|
|
35
|
-
var
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
// src/errors.ts
|
|
39
|
+
var PREFIX = "temporal-workdays: ";
|
|
40
|
+
function rangeError(message) {
|
|
41
|
+
return new RangeError(PREFIX + message);
|
|
42
|
+
}
|
|
43
|
+
function typeError(message) {
|
|
44
|
+
return new TypeError(PREFIX + message);
|
|
45
|
+
}
|
|
46
|
+
function describe(value) {
|
|
47
|
+
switch (typeof value) {
|
|
48
|
+
case "string":
|
|
49
|
+
return JSON.stringify(value);
|
|
50
|
+
case "number":
|
|
51
|
+
case "boolean":
|
|
52
|
+
case "bigint":
|
|
53
|
+
case "undefined":
|
|
54
|
+
return String(value);
|
|
55
|
+
case "symbol":
|
|
56
|
+
return value.toString();
|
|
57
|
+
case "function":
|
|
58
|
+
return "a function";
|
|
59
|
+
case "object":
|
|
60
|
+
return value === null ? "null" : Object.prototype.toString.call(value);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/temporal.ts
|
|
65
|
+
function hasBrand(value, brand) {
|
|
66
|
+
return typeof value === "object" && value !== null && Object.prototype.toString.call(value) === `[object ${brand}]`;
|
|
67
|
+
}
|
|
68
|
+
function isPlainDate(value) {
|
|
69
|
+
return hasBrand(value, "Temporal.PlainDate");
|
|
70
|
+
}
|
|
71
|
+
function isPlainDateTime(value) {
|
|
72
|
+
return hasBrand(value, "Temporal.PlainDateTime");
|
|
73
|
+
}
|
|
74
|
+
function isZonedDateTime(value) {
|
|
75
|
+
return hasBrand(value, "Temporal.ZonedDateTime");
|
|
76
|
+
}
|
|
77
|
+
function toPlainDate(date) {
|
|
78
|
+
if (isPlainDate(date)) return date;
|
|
79
|
+
if (isPlainDateTime(date) || isZonedDateTime(date)) {
|
|
80
|
+
return date.toPlainDate();
|
|
41
81
|
}
|
|
82
|
+
throw typeError(`expected a Temporal.PlainDate, PlainDateTime or ZonedDateTime, got ${describe(date)}`);
|
|
83
|
+
}
|
|
84
|
+
function toIso(value) {
|
|
85
|
+
return value.calendarId === "iso8601" ? value : value.withCalendar("iso8601");
|
|
86
|
+
}
|
|
87
|
+
function withCalendarId(value, calendarId) {
|
|
88
|
+
return calendarId === "iso8601" ? value : value.withCalendar(calendarId);
|
|
89
|
+
}
|
|
90
|
+
function toIsoDate(date) {
|
|
91
|
+
return toIso(date);
|
|
42
92
|
}
|
|
43
|
-
function
|
|
44
|
-
const iso = date
|
|
93
|
+
function epochDayOf(date) {
|
|
94
|
+
const iso = toIsoDate(date);
|
|
45
95
|
return epochDayFromIso(iso.year, iso.month, iso.day);
|
|
46
96
|
}
|
|
47
|
-
|
|
48
|
-
|
|
97
|
+
var DateFactory = class {
|
|
98
|
+
#iso;
|
|
99
|
+
#epochDay;
|
|
100
|
+
constructor(reference) {
|
|
101
|
+
this.#iso = toIsoDate(reference);
|
|
102
|
+
this.#epochDay = epochDayFromIso(this.#iso.year, this.#iso.month, this.#iso.day);
|
|
103
|
+
}
|
|
104
|
+
get epochDay() {
|
|
105
|
+
return this.#epochDay;
|
|
106
|
+
}
|
|
107
|
+
at(epochDay) {
|
|
108
|
+
return epochDay === this.#epochDay ? this.#iso : this.#iso.add({ days: epochDay - this.#epochDay });
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// src/day-calendar.ts
|
|
113
|
+
var MAX_SCAN_DAYS = 366 * 20;
|
|
114
|
+
var DATE_RE = /^([+-]\d{6}|\d{4})-(\d{2})-(\d{2})$/;
|
|
115
|
+
var MONTH_DAY_RE = /^(?:--)?(\d{2})-(\d{2})$/;
|
|
116
|
+
var OPTION_KEYS = /* @__PURE__ */ new Set([
|
|
117
|
+
"weekend",
|
|
118
|
+
"holidays",
|
|
119
|
+
"extraWorkdays",
|
|
120
|
+
"hours",
|
|
121
|
+
"specialHours"
|
|
122
|
+
]);
|
|
123
|
+
function checkDate(year, month, day, source) {
|
|
124
|
+
if (month < 1 || month > 12 || day < 1 || day > daysInMonth(year, month)) {
|
|
125
|
+
throw rangeError(`invalid date ${describe(source)}`);
|
|
126
|
+
}
|
|
49
127
|
}
|
|
50
|
-
function
|
|
128
|
+
function parseDate(value) {
|
|
51
129
|
if (typeof value === "string") {
|
|
52
130
|
const m = DATE_RE.exec(value);
|
|
53
|
-
if (!m) throw
|
|
54
|
-
const [
|
|
55
|
-
|
|
56
|
-
return epochDayFromIso(
|
|
131
|
+
if (!m) throw rangeError(`expected a "YYYY-MM-DD" date, got ${describe(value)}`);
|
|
132
|
+
const [year, month, day] = [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
133
|
+
checkDate(year, month, day, value);
|
|
134
|
+
return epochDayFromIso(year, month, day);
|
|
135
|
+
}
|
|
136
|
+
if (isPlainDate(value)) return epochDayOf(value);
|
|
137
|
+
throw typeError(`expected a "YYYY-MM-DD" string or Temporal.PlainDate, got ${describe(value)}`);
|
|
138
|
+
}
|
|
139
|
+
function addHoliday(entry, dates, annual) {
|
|
140
|
+
if (typeof entry === "string") {
|
|
141
|
+
const md = MONTH_DAY_RE.exec(entry);
|
|
142
|
+
if (md) {
|
|
143
|
+
const [month, day] = [Number(md[1]), Number(md[2])];
|
|
144
|
+
checkDate(2e3, month, day, entry);
|
|
145
|
+
annual.add(month * 100 + day);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
} else if (hasBrand(entry, "Temporal.PlainMonthDay")) {
|
|
149
|
+
const pmd = entry;
|
|
150
|
+
if (pmd.calendarId !== "iso8601") {
|
|
151
|
+
throw rangeError("Temporal.PlainMonthDay holidays must use the iso8601 calendar");
|
|
152
|
+
}
|
|
153
|
+
annual.add(Number(pmd.monthCode.slice(1)) * 100 + pmd.day);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
dates.add(parseDate(entry));
|
|
157
|
+
}
|
|
158
|
+
function isIterable(value) {
|
|
159
|
+
return typeof value === "object" && value !== null && typeof value[Symbol.iterator] === "function";
|
|
160
|
+
}
|
|
161
|
+
function checkOptionKeys(options, allowed) {
|
|
162
|
+
for (const key of Object.keys(options)) {
|
|
163
|
+
if (!allowed.has(key)) throw typeError(`unknown option ${JSON.stringify(key)}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function checkOptionsObject(options) {
|
|
167
|
+
if (options !== void 0 && (typeof options !== "object" || options === null)) {
|
|
168
|
+
throw typeError(`options must be an object, got ${describe(options)}`);
|
|
57
169
|
}
|
|
58
|
-
if (isTemporalLike(value, "Temporal.PlainDate")) return epochDayOfPlainDate(value);
|
|
59
|
-
throw new TypeError(`temporal-workdays: expected a "YYYY-MM-DD" string or Temporal.PlainDate, got ${String(value)}`);
|
|
60
170
|
}
|
|
61
|
-
function
|
|
62
|
-
|
|
171
|
+
function compileDayCalendar(options = {}) {
|
|
172
|
+
checkOptionsObject(options);
|
|
173
|
+
checkOptionKeys(options, OPTION_KEYS);
|
|
63
174
|
const weekend = new Array(8).fill(false);
|
|
175
|
+
const weekendDays = options.weekend ?? [6, 7];
|
|
176
|
+
if (!isIterable(weekendDays)) throw typeError(`weekend must be an array of days, got ${describe(weekendDays)}`);
|
|
64
177
|
for (const d of weekendDays) {
|
|
65
178
|
if (!Number.isInteger(d) || d < 1 || d > 7) {
|
|
66
|
-
throw
|
|
179
|
+
throw rangeError(`weekend days must be integers 1 (Mon) \u2013 7 (Sun), got ${describe(d)}`);
|
|
67
180
|
}
|
|
68
181
|
weekend[d] = true;
|
|
69
182
|
}
|
|
70
183
|
const dates = /* @__PURE__ */ new Set();
|
|
71
184
|
const annual = /* @__PURE__ */ new Set();
|
|
72
185
|
let predicate;
|
|
73
|
-
const holidays = options
|
|
186
|
+
const holidays = options.holidays;
|
|
74
187
|
if (typeof holidays === "function") {
|
|
75
188
|
predicate = holidays;
|
|
76
|
-
} else if (holidays
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const md = MONTH_DAY_RE.exec(h);
|
|
80
|
-
if (md) {
|
|
81
|
-
const [mo, d] = [Number(md[1]), Number(md[2])];
|
|
82
|
-
checkDay(2e3, mo, d, h);
|
|
83
|
-
annual.add(mo * 100 + d);
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
dates.add(parseSingleDate(h));
|
|
87
|
-
} else if (isTemporalLike(h, "Temporal.PlainMonthDay")) {
|
|
88
|
-
const pmd = h;
|
|
89
|
-
if (pmd.calendarId !== "iso8601") {
|
|
90
|
-
throw new RangeError("temporal-workdays: Temporal.PlainMonthDay holidays must use the iso8601 calendar");
|
|
91
|
-
}
|
|
92
|
-
annual.add(Number(pmd.monthCode.slice(1)) * 100 + pmd.day);
|
|
93
|
-
} else {
|
|
94
|
-
dates.add(parseSingleDate(h));
|
|
95
|
-
}
|
|
96
|
-
}
|
|
189
|
+
} else if (holidays !== void 0) {
|
|
190
|
+
if (!isIterable(holidays)) throw typeError(`holidays must be an array or a function, got ${describe(holidays)}`);
|
|
191
|
+
for (const entry of holidays) addHoliday(entry, dates, annual);
|
|
97
192
|
}
|
|
98
193
|
const extra = /* @__PURE__ */ new Set();
|
|
99
|
-
|
|
100
|
-
|
|
194
|
+
const extraWorkdays = options.extraWorkdays;
|
|
195
|
+
if (extraWorkdays !== void 0) {
|
|
196
|
+
if (!isIterable(extraWorkdays)) throw typeError(`extraWorkdays must be an array, got ${describe(extraWorkdays)}`);
|
|
197
|
+
for (const entry of extraWorkdays) extra.add(parseDate(entry));
|
|
101
198
|
}
|
|
102
199
|
return { weekend, dates, annual, predicate, extra };
|
|
103
200
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
function getCompiled(options) {
|
|
107
|
-
if (options === void 0) return DEFAULT;
|
|
108
|
-
let c = cache.get(options);
|
|
109
|
-
if (!c) {
|
|
110
|
-
c = compile(options);
|
|
111
|
-
cache.set(options, c);
|
|
112
|
-
}
|
|
113
|
-
return c;
|
|
201
|
+
function isWeekendDay(cal, epochDay) {
|
|
202
|
+
return cal.weekend[dayOfWeekFromEpochDay(epochDay)] === true;
|
|
114
203
|
}
|
|
115
|
-
function
|
|
116
|
-
if (
|
|
117
|
-
|
|
204
|
+
function isListedHoliday(cal, epochDay, dates) {
|
|
205
|
+
if (cal.dates.has(epochDay)) return true;
|
|
206
|
+
if (cal.annual.size > 0) {
|
|
207
|
+
const [, month, day] = isoFromEpochDay(epochDay);
|
|
208
|
+
if (cal.annual.has(month * 100 + day)) return true;
|
|
118
209
|
}
|
|
119
|
-
return
|
|
210
|
+
return Boolean(cal.predicate?.(dates.at(epochDay)));
|
|
120
211
|
}
|
|
121
|
-
function isHolidayDay(
|
|
122
|
-
|
|
123
|
-
if (c.annual.size > 0) {
|
|
124
|
-
const [, mo, d] = isoFromEpochDay(epochDay);
|
|
125
|
-
if (c.annual.has(mo * 100 + d)) return true;
|
|
126
|
-
}
|
|
127
|
-
if (c.predicate) {
|
|
128
|
-
const iso = base.calendarId === "iso8601" ? base : base.withCalendar("iso8601");
|
|
129
|
-
return c.predicate(epochDay === baseDay ? iso : iso.add({ days: epochDay - baseDay }));
|
|
130
|
-
}
|
|
131
|
-
return false;
|
|
132
|
-
}
|
|
133
|
-
function isWorkdayDay(c, epochDay, base, baseDay) {
|
|
134
|
-
if (c.extra.has(epochDay)) return true;
|
|
135
|
-
if (c.weekend[dayOfWeekFromEpochDay(epochDay)]) return false;
|
|
136
|
-
return !isHolidayDay(c, epochDay, base, baseDay);
|
|
212
|
+
function isHolidayDay(cal, epochDay, dates) {
|
|
213
|
+
return !cal.extra.has(epochDay) && isListedHoliday(cal, epochDay, dates);
|
|
137
214
|
}
|
|
138
|
-
function
|
|
139
|
-
if (
|
|
215
|
+
function isWorkdayDay(cal, epochDay, dates) {
|
|
216
|
+
if (cal.extra.has(epochDay)) return true;
|
|
217
|
+
if (isWeekendDay(cal, epochDay)) return false;
|
|
218
|
+
return !isListedHoliday(cal, epochDay, dates);
|
|
140
219
|
}
|
|
141
220
|
function noWorkdayError() {
|
|
142
|
-
return
|
|
143
|
-
`temporal-workdays: no workday found within ${MAX_SCAN_DAYS} days \u2014 check your weekend/holiday configuration`
|
|
144
|
-
);
|
|
221
|
+
return rangeError(`no workday found within ${MAX_SCAN_DAYS} days \u2014 check the weekend/holiday configuration`);
|
|
145
222
|
}
|
|
146
|
-
function
|
|
147
|
-
|
|
148
|
-
let day = baseDay;
|
|
223
|
+
function stepWorkdays(cal, from, count, dir, dates) {
|
|
224
|
+
let day = from;
|
|
149
225
|
let remaining = count;
|
|
150
226
|
let gap = 0;
|
|
151
227
|
while (remaining > 0) {
|
|
152
228
|
day += dir;
|
|
153
|
-
if (isWorkdayDay(
|
|
229
|
+
if (isWorkdayDay(cal, day, dates)) {
|
|
154
230
|
remaining--;
|
|
155
231
|
gap = 0;
|
|
156
232
|
} else if (++gap > MAX_SCAN_DAYS) {
|
|
157
233
|
throw noWorkdayError();
|
|
158
234
|
}
|
|
159
235
|
}
|
|
160
|
-
return day
|
|
236
|
+
return day;
|
|
237
|
+
}
|
|
238
|
+
function countWorkdays(cal, from, to, dates) {
|
|
239
|
+
const [lo, hi, sign] = from <= to ? [from, to, 1] : [to, from, -1];
|
|
240
|
+
let count = 0;
|
|
241
|
+
for (let day = lo + 1; day <= hi; day++) {
|
|
242
|
+
if (isWorkdayDay(cal, day, dates)) count++;
|
|
243
|
+
}
|
|
244
|
+
return count === 0 ? 0 : sign * count;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/time.ts
|
|
248
|
+
var NS_PER_SECOND = 1e9;
|
|
249
|
+
var NS_PER_MINUTE = 60 * NS_PER_SECOND;
|
|
250
|
+
var NS_PER_HOUR = 60 * NS_PER_MINUTE;
|
|
251
|
+
var NS_PER_DAY = 24 * NS_PER_HOUR;
|
|
252
|
+
var TIME_RE = /^(\d{2}):(\d{2})(?::(\d{2})(?:[.,](\d{1,9}))?)?$/;
|
|
253
|
+
function nsOfDay(hour, minute, second = 0, subsecondNs = 0) {
|
|
254
|
+
return hour * NS_PER_HOUR + minute * NS_PER_MINUTE + second * NS_PER_SECOND + subsecondNs;
|
|
255
|
+
}
|
|
256
|
+
function nsOfTime(t) {
|
|
257
|
+
return nsOfDay(t.hour, t.minute, t.second, t.millisecond * 1e6 + t.microsecond * 1e3 + t.nanosecond);
|
|
258
|
+
}
|
|
259
|
+
function parseTime(value) {
|
|
260
|
+
if (typeof value === "string") {
|
|
261
|
+
const m = TIME_RE.exec(value);
|
|
262
|
+
if (m) {
|
|
263
|
+
const [hour, minute, second] = [Number(m[1]), Number(m[2]), Number(m[3] ?? 0)];
|
|
264
|
+
const subsecond = Number((m[4] ?? "").padEnd(9, "0"));
|
|
265
|
+
const ns = nsOfDay(hour, minute, second, subsecond);
|
|
266
|
+
if (minute < 60 && second < 60 && ns <= NS_PER_DAY) return ns;
|
|
267
|
+
}
|
|
268
|
+
throw rangeError(`expected a time like "09:30" (00:00 \u2013 24:00), got ${describe(value)}`);
|
|
269
|
+
}
|
|
270
|
+
if (hasBrand(value, "Temporal.PlainTime")) return nsOfTime(value);
|
|
271
|
+
throw typeError(`expected a "HH:MM" string or Temporal.PlainTime, got ${describe(value)}`);
|
|
272
|
+
}
|
|
273
|
+
var pad = (n, width = 2) => String(n).padStart(width, "0");
|
|
274
|
+
function formatTime(ns) {
|
|
275
|
+
const hour = Math.floor(ns / NS_PER_HOUR);
|
|
276
|
+
const minute = Math.floor(ns % NS_PER_HOUR / NS_PER_MINUTE);
|
|
277
|
+
const second = Math.floor(ns % NS_PER_MINUTE / NS_PER_SECOND);
|
|
278
|
+
const fraction = ns % NS_PER_SECOND;
|
|
279
|
+
let text = `${pad(hour)}:${pad(minute)}`;
|
|
280
|
+
if (second !== 0 || fraction !== 0) text += `:${pad(second)}`;
|
|
281
|
+
if (fraction !== 0) text += `.${pad(fraction, 9).replace(/0+$/, "")}`;
|
|
282
|
+
return text;
|
|
283
|
+
}
|
|
284
|
+
function parseDayHours(value, where) {
|
|
285
|
+
if (!Array.isArray(value))
|
|
286
|
+
throw typeError(`${where} must be an array of [start, end] ranges, got ${describe(value)}`);
|
|
287
|
+
const ranges = value.map((range) => {
|
|
288
|
+
if (!Array.isArray(range) || range.length !== 2) {
|
|
289
|
+
throw typeError(`${where}: each range must be a [start, end] pair, got ${describe(range)}`);
|
|
290
|
+
}
|
|
291
|
+
const [start, end] = range;
|
|
292
|
+
const interval = [parseTime(start), parseTime(end)];
|
|
293
|
+
if (interval[0] >= interval[1]) {
|
|
294
|
+
throw rangeError(`${where}: range start must be before its end, got ${describe(start)}\u2013${describe(end)}`);
|
|
295
|
+
}
|
|
296
|
+
return interval;
|
|
297
|
+
});
|
|
298
|
+
ranges.sort((a, b) => a[0] - b[0]);
|
|
299
|
+
const merged = [];
|
|
300
|
+
for (const [start, end] of ranges) {
|
|
301
|
+
const last = merged.at(-1);
|
|
302
|
+
if (last && start < last[1]) {
|
|
303
|
+
throw rangeError(
|
|
304
|
+
`${where}: ranges overlap (${formatTime(last[0])}\u2013${formatTime(last[1])} and ${formatTime(start)}\u2013${formatTime(end)})`
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
if (last?.[1] === start) last[1] = end;
|
|
308
|
+
else merged.push([start, end]);
|
|
309
|
+
}
|
|
310
|
+
return merged;
|
|
311
|
+
}
|
|
312
|
+
function formatDayHours(intervals) {
|
|
313
|
+
return intervals.map(([start, end]) => [formatTime(start), formatTime(end)]);
|
|
314
|
+
}
|
|
315
|
+
function subtractInterval(intervals, cutStart, cutEnd) {
|
|
316
|
+
const out = [];
|
|
317
|
+
for (const [start, end] of intervals) {
|
|
318
|
+
if (end <= cutStart || start >= cutEnd) {
|
|
319
|
+
out.push([start, end]);
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
if (start < cutStart) out.push([start, cutStart]);
|
|
323
|
+
if (end > cutEnd) out.push([cutEnd, end]);
|
|
324
|
+
}
|
|
325
|
+
return out;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// src/hours.ts
|
|
329
|
+
function byDayOfWeek(f) {
|
|
330
|
+
return { 1: f(1), 2: f(2), 3: f(3), 4: f(4), 5: f(5), 6: f(6), 7: f(7) };
|
|
331
|
+
}
|
|
332
|
+
var WEEKLY_KEYS = /* @__PURE__ */ new Set(["1", "2", "3", "4", "5", "6", "7", "default"]);
|
|
333
|
+
var EMPTY = [];
|
|
334
|
+
function compileWeekly(hours) {
|
|
335
|
+
if (hours === void 0) throw typeError("working-time functions need an `hours` option");
|
|
336
|
+
if (Array.isArray(hours)) {
|
|
337
|
+
const uniform = parseDayHours(hours, "hours");
|
|
338
|
+
return byDayOfWeek(() => uniform);
|
|
339
|
+
}
|
|
340
|
+
if (typeof hours !== "object" || hours === null) {
|
|
341
|
+
throw typeError(`hours must be an array of ranges or an object by day of week, got ${describe(hours)}`);
|
|
342
|
+
}
|
|
343
|
+
checkOptionKeys(hours, WEEKLY_KEYS);
|
|
344
|
+
const byDay = hours;
|
|
345
|
+
const fallback = byDay["default"] === void 0 ? EMPTY : parseDayHours(byDay["default"], "hours.default");
|
|
346
|
+
return byDayOfWeek((dow) => byDay[dow] === void 0 ? fallback : parseDayHours(byDay[dow], `hours[${dow}]`));
|
|
347
|
+
}
|
|
348
|
+
function compileSpecial(special) {
|
|
349
|
+
if (special === void 0) return { kind: "none" };
|
|
350
|
+
if (typeof special === "function") return { kind: "function", fn: special };
|
|
351
|
+
if (typeof special !== "object" || special === null || Array.isArray(special)) {
|
|
352
|
+
throw typeError(`specialHours must be an object keyed by date or a function, got ${describe(special)}`);
|
|
353
|
+
}
|
|
354
|
+
const byDay = /* @__PURE__ */ new Map();
|
|
355
|
+
for (const [date, hours] of Object.entries(special)) {
|
|
356
|
+
byDay.set(parseDate(date), parseDayHours(hours, `specialHours[${JSON.stringify(date)}]`));
|
|
357
|
+
}
|
|
358
|
+
return { kind: "table", byDay };
|
|
359
|
+
}
|
|
360
|
+
function compileHoursCalendar(options) {
|
|
361
|
+
checkOptionsObject(options);
|
|
362
|
+
const days = compileDayCalendar(options);
|
|
363
|
+
const weekly = compileWeekly(options?.hours);
|
|
364
|
+
const weeklyText = byDayOfWeek((dow) => formatDayHours(weekly[dow]));
|
|
365
|
+
return { days, weekly, weeklyText, special: compileSpecial(options.specialHours) };
|
|
366
|
+
}
|
|
367
|
+
function intervalsOn(cal, epochDay, dates) {
|
|
368
|
+
const dow = dayOfWeekFromEpochDay(epochDay);
|
|
369
|
+
const workday = isWorkdayDay(cal.days, epochDay, dates);
|
|
370
|
+
const { special } = cal;
|
|
371
|
+
if (special.kind === "table") {
|
|
372
|
+
const override = special.byDay.get(epochDay);
|
|
373
|
+
if (override) return override;
|
|
374
|
+
} else if (special.kind === "function") {
|
|
375
|
+
const date = dates.at(epochDay);
|
|
376
|
+
const result = special.fn(date, workday ? cal.weeklyText[dow] : []);
|
|
377
|
+
if (result !== void 0) return parseDayHours(result, `specialHours(${date.toString()})`);
|
|
378
|
+
}
|
|
379
|
+
return workday ? cal.weekly[dow] : EMPTY;
|
|
380
|
+
}
|
|
381
|
+
var BIG_NS_PER_DAY = BigInt(NS_PER_DAY);
|
|
382
|
+
var BIG_NS_PER_SECOND = BigInt(NS_PER_SECOND);
|
|
383
|
+
function wallFields(day, ns) {
|
|
384
|
+
const [year, month, dd] = isoFromEpochDay(day);
|
|
385
|
+
return {
|
|
386
|
+
year,
|
|
387
|
+
month,
|
|
388
|
+
day: dd,
|
|
389
|
+
hour: Math.floor(ns / NS_PER_HOUR),
|
|
390
|
+
minute: Math.floor(ns % NS_PER_HOUR / NS_PER_MINUTE),
|
|
391
|
+
second: Math.floor(ns % NS_PER_MINUTE / NS_PER_SECOND),
|
|
392
|
+
millisecond: Math.floor(ns % NS_PER_SECOND / 1e6),
|
|
393
|
+
microsecond: Math.floor(ns % 1e6 / 1e3),
|
|
394
|
+
nanosecond: ns % 1e3
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
function wallPosition(day, ns) {
|
|
398
|
+
return BigInt(day) * BIG_NS_PER_DAY + BigInt(ns);
|
|
399
|
+
}
|
|
400
|
+
function wallDay(v) {
|
|
401
|
+
return epochDayFromIso(v.year, v.month, v.day);
|
|
402
|
+
}
|
|
403
|
+
function durationFrom(zero, ns) {
|
|
404
|
+
return zero.add({ seconds: Number(ns / BIG_NS_PER_SECOND), nanoseconds: Number(ns % BIG_NS_PER_SECOND) }).round({ largestUnit: "hours" });
|
|
405
|
+
}
|
|
406
|
+
function normalizePeriods(periods) {
|
|
407
|
+
periods.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
|
|
408
|
+
const merged = [];
|
|
409
|
+
for (const [start, end] of periods) {
|
|
410
|
+
if (end <= start) continue;
|
|
411
|
+
const last = merged.at(-1);
|
|
412
|
+
if (last && start <= last[1]) {
|
|
413
|
+
if (end > last[1]) last[1] = end;
|
|
414
|
+
} else {
|
|
415
|
+
merged.push([start, end]);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return merged;
|
|
419
|
+
}
|
|
420
|
+
var PlainTimeline = class {
|
|
421
|
+
origin;
|
|
422
|
+
lookahead = 0n;
|
|
423
|
+
dates;
|
|
424
|
+
#iso;
|
|
425
|
+
#calendarId;
|
|
426
|
+
constructor(value) {
|
|
427
|
+
this.#calendarId = value.calendarId;
|
|
428
|
+
this.#iso = toIso(value);
|
|
429
|
+
this.origin = wallPosition(wallDay(this.#iso), nsOfTime(this.#iso));
|
|
430
|
+
this.dates = new DateFactory(this.#iso.toPlainDate());
|
|
431
|
+
}
|
|
432
|
+
positionOf(value) {
|
|
433
|
+
if (!isPlainDateTime(value)) throw mixedTypesError(value);
|
|
434
|
+
const iso = toIso(value);
|
|
435
|
+
return wallPosition(wallDay(iso), nsOfTime(iso));
|
|
436
|
+
}
|
|
437
|
+
periods(day, intervals) {
|
|
438
|
+
return intervals.map(([start, end]) => [wallPosition(day, start), wallPosition(day, end)]);
|
|
439
|
+
}
|
|
440
|
+
at(position) {
|
|
441
|
+
const day = floorDiv(position, BIG_NS_PER_DAY);
|
|
442
|
+
const fields = wallFields(Number(day), Number(position - day * BIG_NS_PER_DAY));
|
|
443
|
+
return withCalendarId(this.#iso.with(fields), this.#calendarId);
|
|
444
|
+
}
|
|
445
|
+
duration(ns) {
|
|
446
|
+
return durationFrom(this.#iso.until(this.#iso), ns);
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
var SEGMENT_MARGIN = 48n * BigInt(NS_PER_HOUR);
|
|
450
|
+
var FOREVER = 2n ** 100n;
|
|
451
|
+
var ZONED_LOOKAHEAD = 48n * BigInt(NS_PER_HOUR);
|
|
452
|
+
var ZonedTimeline = class {
|
|
453
|
+
origin;
|
|
454
|
+
lookahead = ZONED_LOOKAHEAD;
|
|
455
|
+
dates;
|
|
456
|
+
#iso;
|
|
457
|
+
#calendarId;
|
|
458
|
+
#segment;
|
|
459
|
+
constructor(value) {
|
|
460
|
+
this.#calendarId = value.calendarId;
|
|
461
|
+
this.#iso = toIso(value);
|
|
462
|
+
this.origin = this.#iso.epochNanoseconds;
|
|
463
|
+
this.dates = new DateFactory(this.#iso.toPlainDate());
|
|
464
|
+
}
|
|
465
|
+
positionOf(value) {
|
|
466
|
+
if (!isZonedDateTime(value)) throw mixedTypesError(value);
|
|
467
|
+
return value.epochNanoseconds;
|
|
468
|
+
}
|
|
469
|
+
periods(day, intervals) {
|
|
470
|
+
return normalizePeriods(intervals.map(([start, end]) => [this.#exact(day, start), this.#exact(day, end)]));
|
|
471
|
+
}
|
|
472
|
+
/** Exact time of a wall-clock time; skipped times move forward, repeated times use the earlier. */
|
|
473
|
+
#exact(day, ns) {
|
|
474
|
+
const wall = wallPosition(day, ns);
|
|
475
|
+
const segment = this.#segment;
|
|
476
|
+
if (segment) {
|
|
477
|
+
const candidate = wall - segment.offset;
|
|
478
|
+
if (candidate >= segment.start && candidate < segment.end) return candidate;
|
|
479
|
+
}
|
|
480
|
+
const d = Number(floorDiv(wall, BIG_NS_PER_DAY));
|
|
481
|
+
const fields = wallFields(d, Number(wall - BigInt(d) * BIG_NS_PER_DAY));
|
|
482
|
+
const zdt = this.#iso.with(fields, { disambiguation: "compatible", offset: "ignore" });
|
|
483
|
+
this.#segment = segmentAround(zdt);
|
|
484
|
+
return zdt.epochNanoseconds;
|
|
485
|
+
}
|
|
486
|
+
at(position) {
|
|
487
|
+
const delta = position - this.origin;
|
|
488
|
+
const result = this.#iso.add({
|
|
489
|
+
seconds: Number(delta / BIG_NS_PER_SECOND),
|
|
490
|
+
nanoseconds: Number(delta % BIG_NS_PER_SECOND)
|
|
491
|
+
});
|
|
492
|
+
return withCalendarId(result, this.#calendarId);
|
|
493
|
+
}
|
|
494
|
+
duration(ns) {
|
|
495
|
+
return durationFrom(this.#iso.until(this.#iso), ns);
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
function segmentAround(zdt) {
|
|
499
|
+
const previous = zdt.add({ nanoseconds: 1 }).getTimeZoneTransition("previous");
|
|
500
|
+
const next = zdt.getTimeZoneTransition("next");
|
|
501
|
+
return {
|
|
502
|
+
start: previous ? previous.epochNanoseconds + SEGMENT_MARGIN : -FOREVER,
|
|
503
|
+
end: next ? next.epochNanoseconds - SEGMENT_MARGIN : FOREVER,
|
|
504
|
+
offset: BigInt(zdt.offsetNanoseconds)
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
function addPeriods(pending, periods) {
|
|
508
|
+
const first = periods[0];
|
|
509
|
+
const last = periods.at(-1);
|
|
510
|
+
if (first === void 0 || last === void 0) return pending;
|
|
511
|
+
const pendingFirst = pending[0];
|
|
512
|
+
const pendingLast = pending.at(-1);
|
|
513
|
+
if (pendingFirst === void 0 || pendingLast === void 0) return [...periods];
|
|
514
|
+
if (first[0] > pendingLast[1]) return pending.concat(periods);
|
|
515
|
+
if (last[1] < pendingFirst[0]) return periods.concat(pending);
|
|
516
|
+
return normalizePeriods([...pending, ...periods]);
|
|
517
|
+
}
|
|
518
|
+
function mixedTypesError(value) {
|
|
519
|
+
return typeError(`start and end must be the same Temporal type, got ${describe(value)} for end`);
|
|
520
|
+
}
|
|
521
|
+
function withTimeline(value, run) {
|
|
522
|
+
if (isZonedDateTime(value)) return run(new ZonedTimeline(value));
|
|
523
|
+
if (isPlainDateTime(value)) return run(new PlainTimeline(value));
|
|
524
|
+
throw typeError(
|
|
525
|
+
`expected a Temporal.PlainDateTime or ZonedDateTime, got ${describe(value)}` + (isPlainDate(value) ? " (use the workday functions for dates)" : "")
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
function noWorkingTimeError() {
|
|
529
|
+
return rangeError(`no working time found within ${MAX_SCAN_DAYS} days \u2014 check the hours/holiday configuration`);
|
|
530
|
+
}
|
|
531
|
+
function scan(cal, tl, from, dir, visit) {
|
|
532
|
+
let pending = [];
|
|
533
|
+
let day = Number(floorDiv(from - BigInt(dir) * tl.lookahead, BIG_NS_PER_DAY));
|
|
534
|
+
for (let idle = 0; ; day += dir) {
|
|
535
|
+
const periods = tl.periods(day, intervalsOn(cal, day, tl.dates));
|
|
536
|
+
idle = periods.length === 0 ? idle + 1 : 0;
|
|
537
|
+
if (idle > MAX_SCAN_DAYS) throw noWorkingTimeError();
|
|
538
|
+
pending = addPeriods(pending, periods);
|
|
539
|
+
const frontier = dir === 1 ? wallPosition(day + 1, 0) - tl.lookahead : wallPosition(day, 0) + tl.lookahead;
|
|
540
|
+
for (; ; ) {
|
|
541
|
+
const next = dir === 1 ? pending[0] : pending.at(-1);
|
|
542
|
+
if (next === void 0 || (dir === 1 ? next[0] >= frontier : next[1] <= frontier)) break;
|
|
543
|
+
if (dir === 1) pending.shift();
|
|
544
|
+
else pending.pop();
|
|
545
|
+
const result = visit(next);
|
|
546
|
+
if (result !== void 0) return result;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
var CALENDAR_UNITS = ["years", "months", "weeks", "days"];
|
|
551
|
+
var TIME_UNITS = [
|
|
552
|
+
["hours", BigInt(NS_PER_HOUR)],
|
|
553
|
+
["minutes", BigInt(NS_PER_MINUTE)],
|
|
554
|
+
["seconds", BigInt(NS_PER_SECOND)],
|
|
555
|
+
["milliseconds", 1000000n],
|
|
556
|
+
["microseconds", 1000n],
|
|
557
|
+
["nanoseconds", 1n]
|
|
558
|
+
];
|
|
559
|
+
function durationToNs(duration) {
|
|
560
|
+
if (typeof duration !== "object" || duration === null) {
|
|
561
|
+
throw typeError(`expected a Temporal.Duration or an object like { hours: 4 }, got ${describe(duration)}`);
|
|
562
|
+
}
|
|
563
|
+
let seen = false;
|
|
564
|
+
for (const unit of CALENDAR_UNITS) {
|
|
565
|
+
const value = duration[unit];
|
|
566
|
+
if (value === void 0) continue;
|
|
567
|
+
seen = true;
|
|
568
|
+
if (value !== 0) throw rangeError(`${unit} are not supported in working time; use addWorkdays for whole days`);
|
|
569
|
+
}
|
|
570
|
+
let total = 0n;
|
|
571
|
+
let sign = 0;
|
|
572
|
+
for (const [unit, nsPerUnit] of TIME_UNITS) {
|
|
573
|
+
const value = duration[unit];
|
|
574
|
+
if (value === void 0) continue;
|
|
575
|
+
seen = true;
|
|
576
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
577
|
+
throw rangeError(`duration ${unit} must be an integer, got ${describe(value)}`);
|
|
578
|
+
}
|
|
579
|
+
if (value === 0) continue;
|
|
580
|
+
if (sign !== 0 && Math.sign(value) !== sign) throw rangeError("duration fields must all have the same sign");
|
|
581
|
+
sign = Math.sign(value);
|
|
582
|
+
total += BigInt(value) * nsPerUnit;
|
|
583
|
+
}
|
|
584
|
+
if (!seen) throw typeError("duration needs at least one of hours, minutes, seconds, \u2026 (e.g. { hours: 4 })");
|
|
585
|
+
return total;
|
|
586
|
+
}
|
|
587
|
+
function negate(duration) {
|
|
588
|
+
if (typeof duration !== "object" || duration === null) return duration;
|
|
589
|
+
const negated = {};
|
|
590
|
+
for (const unit of [...CALENDAR_UNITS, ...TIME_UNITS.map(([u]) => u)]) {
|
|
591
|
+
const value = duration[unit];
|
|
592
|
+
negated[unit] = typeof value === "number" ? -value : value;
|
|
593
|
+
}
|
|
594
|
+
return negated;
|
|
595
|
+
}
|
|
596
|
+
function isWorkingPosition(cal, tl, position) {
|
|
597
|
+
const first = Number(floorDiv(position - tl.lookahead, BIG_NS_PER_DAY));
|
|
598
|
+
const last = Number(floorDiv(position + tl.lookahead, BIG_NS_PER_DAY));
|
|
599
|
+
for (let day = first; day <= last; day++) {
|
|
600
|
+
for (const [start, end] of tl.periods(day, intervalsOn(cal, day, tl.dates))) {
|
|
601
|
+
if (position >= start && position < end) return true;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return false;
|
|
605
|
+
}
|
|
606
|
+
function addForward(cal, tl, total) {
|
|
607
|
+
let cursor = tl.origin;
|
|
608
|
+
let remaining = total;
|
|
609
|
+
return scan(cal, tl, tl.origin, 1, ([start, end]) => {
|
|
610
|
+
const from = start > cursor ? start : cursor;
|
|
611
|
+
if (end <= from) return void 0;
|
|
612
|
+
if (remaining <= end - from) return tl.at(from + remaining);
|
|
613
|
+
remaining -= end - from;
|
|
614
|
+
cursor = end;
|
|
615
|
+
return void 0;
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
function addBackward(cal, tl, total) {
|
|
619
|
+
let cursor = tl.origin;
|
|
620
|
+
let remaining = total;
|
|
621
|
+
return scan(cal, tl, tl.origin, -1, ([start, end]) => {
|
|
622
|
+
const to = end < cursor ? end : cursor;
|
|
623
|
+
if (to <= start) return void 0;
|
|
624
|
+
if (remaining <= to - start) return tl.at(to - remaining);
|
|
625
|
+
remaining -= to - start;
|
|
626
|
+
cursor = start;
|
|
627
|
+
return void 0;
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
function workingNsBetween(cal, tl, a, b) {
|
|
631
|
+
let total = 0n;
|
|
632
|
+
let cursor = a;
|
|
633
|
+
scan(cal, tl, a, 1, ([start, end]) => {
|
|
634
|
+
if (start >= b) return true;
|
|
635
|
+
const from = start > cursor ? start : cursor;
|
|
636
|
+
const to = end < b ? end : b;
|
|
637
|
+
if (to > from) {
|
|
638
|
+
total += to - from;
|
|
639
|
+
cursor = to;
|
|
640
|
+
}
|
|
641
|
+
return void 0;
|
|
642
|
+
});
|
|
643
|
+
return total;
|
|
644
|
+
}
|
|
645
|
+
function rollForwardPosition(cal, tl) {
|
|
646
|
+
if (isWorkingPosition(cal, tl, tl.origin)) return void 0;
|
|
647
|
+
return scan(cal, tl, tl.origin, 1, ([start]) => start > tl.origin ? tl.at(start) : void 0);
|
|
648
|
+
}
|
|
649
|
+
function rollBackwardPosition(cal, tl) {
|
|
650
|
+
if (isWorkingPosition(cal, tl, tl.origin)) return void 0;
|
|
651
|
+
return scan(cal, tl, tl.origin, -1, ([, end]) => end <= tl.origin ? tl.at(end) : void 0);
|
|
652
|
+
}
|
|
653
|
+
function isWorkingTimeWith(cal, dateTime) {
|
|
654
|
+
return withTimeline(dateTime, (tl) => isWorkingPosition(cal, tl, tl.origin));
|
|
655
|
+
}
|
|
656
|
+
function addWorkingTimeWith(cal, dateTime, duration) {
|
|
657
|
+
const total = durationToNs(duration);
|
|
658
|
+
return withTimeline(dateTime, (tl) => {
|
|
659
|
+
if (total === 0n) return dateTime;
|
|
660
|
+
return total > 0n ? addForward(cal, tl, total) : addBackward(cal, tl, -total);
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
function subtractWorkingTimeWith(cal, dateTime, duration) {
|
|
664
|
+
return addWorkingTimeWith(cal, dateTime, negate(duration));
|
|
665
|
+
}
|
|
666
|
+
function workingTimeBetweenWith(cal, start, end) {
|
|
667
|
+
return withTimeline(start, (tl) => {
|
|
668
|
+
const a = tl.origin;
|
|
669
|
+
const b = tl.positionOf(end);
|
|
670
|
+
const ns = a <= b ? workingNsBetween(cal, tl, a, b) : -workingNsBetween(cal, tl, b, a);
|
|
671
|
+
return tl.duration(ns);
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
function rollForwardToWorkingTimeWith(cal, dateTime) {
|
|
675
|
+
return withTimeline(dateTime, (tl) => rollForwardPosition(cal, tl)) ?? dateTime;
|
|
676
|
+
}
|
|
677
|
+
function rollBackwardToWorkingTimeWith(cal, dateTime) {
|
|
678
|
+
return withTimeline(dateTime, (tl) => rollBackwardPosition(cal, tl)) ?? dateTime;
|
|
161
679
|
}
|
|
162
|
-
function
|
|
680
|
+
function getWorkingHoursWith(cal, date) {
|
|
681
|
+
const dates = new DateFactory(toPlainDate(date));
|
|
682
|
+
return formatDayHours(intervalsOn(cal, dates.epochDay, dates));
|
|
683
|
+
}
|
|
684
|
+
function isWorkingTime(dateTime, options) {
|
|
685
|
+
return isWorkingTimeWith(compileHoursCalendar(options), dateTime);
|
|
686
|
+
}
|
|
687
|
+
function addWorkingTime(dateTime, duration, options) {
|
|
688
|
+
return addWorkingTimeWith(compileHoursCalendar(options), dateTime, duration);
|
|
689
|
+
}
|
|
690
|
+
function subtractWorkingTime(dateTime, duration, options) {
|
|
691
|
+
return subtractWorkingTimeWith(compileHoursCalendar(options), dateTime, duration);
|
|
692
|
+
}
|
|
693
|
+
function workingTimeBetween(start, end, options) {
|
|
694
|
+
return workingTimeBetweenWith(compileHoursCalendar(options), start, end);
|
|
695
|
+
}
|
|
696
|
+
function rollForwardToWorkingTime(dateTime, options) {
|
|
697
|
+
return rollForwardToWorkingTimeWith(compileHoursCalendar(options), dateTime);
|
|
698
|
+
}
|
|
699
|
+
function rollBackwardToWorkingTime(dateTime, options) {
|
|
700
|
+
return rollBackwardToWorkingTimeWith(compileHoursCalendar(options), dateTime);
|
|
701
|
+
}
|
|
702
|
+
function getWorkingHours(date, options) {
|
|
703
|
+
return getWorkingHoursWith(compileHoursCalendar(options), date);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// src/workdays.ts
|
|
707
|
+
function shiftDays(date, days) {
|
|
163
708
|
return days === 0 ? date : date.add({ days });
|
|
164
709
|
}
|
|
710
|
+
function checkAmount(amount) {
|
|
711
|
+
if (!Number.isSafeInteger(amount)) throw rangeError(`amount must be an integer, got ${describe(amount)}`);
|
|
712
|
+
}
|
|
713
|
+
function isWeekendWith(cal, date) {
|
|
714
|
+
return isWeekendDay(cal, new DateFactory(toPlainDate(date)).epochDay);
|
|
715
|
+
}
|
|
716
|
+
function isHolidayWith(cal, date) {
|
|
717
|
+
const dates = new DateFactory(toPlainDate(date));
|
|
718
|
+
return isHolidayDay(cal, dates.epochDay, dates);
|
|
719
|
+
}
|
|
720
|
+
function isWorkdayWith(cal, date) {
|
|
721
|
+
const dates = new DateFactory(toPlainDate(date));
|
|
722
|
+
return isWorkdayDay(cal, dates.epochDay, dates);
|
|
723
|
+
}
|
|
724
|
+
function addWorkdaysWith(cal, date, amount) {
|
|
725
|
+
checkAmount(amount);
|
|
726
|
+
const dates = new DateFactory(toPlainDate(date));
|
|
727
|
+
if (amount === 0) return date;
|
|
728
|
+
const target = stepWorkdays(cal, dates.epochDay, Math.abs(amount), amount > 0 ? 1 : -1, dates);
|
|
729
|
+
return shiftDays(date, target - dates.epochDay);
|
|
730
|
+
}
|
|
731
|
+
function subtractWorkdaysWith(cal, date, amount) {
|
|
732
|
+
checkAmount(amount);
|
|
733
|
+
return addWorkdaysWith(cal, date, -amount);
|
|
734
|
+
}
|
|
735
|
+
function nextWorkdayWith(cal, date) {
|
|
736
|
+
const dates = new DateFactory(toPlainDate(date));
|
|
737
|
+
return shiftDays(date, stepWorkdays(cal, dates.epochDay, 1, 1, dates) - dates.epochDay);
|
|
738
|
+
}
|
|
739
|
+
function previousWorkdayWith(cal, date) {
|
|
740
|
+
const dates = new DateFactory(toPlainDate(date));
|
|
741
|
+
return shiftDays(date, stepWorkdays(cal, dates.epochDay, 1, -1, dates) - dates.epochDay);
|
|
742
|
+
}
|
|
743
|
+
function rollForwardWith(cal, date) {
|
|
744
|
+
return isWorkdayWith(cal, date) ? date : nextWorkdayWith(cal, date);
|
|
745
|
+
}
|
|
746
|
+
function rollBackwardWith(cal, date) {
|
|
747
|
+
return isWorkdayWith(cal, date) ? date : previousWorkdayWith(cal, date);
|
|
748
|
+
}
|
|
749
|
+
function workdaysBetweenWith(cal, start, end) {
|
|
750
|
+
const a = new DateFactory(toPlainDate(start));
|
|
751
|
+
const endDay = new DateFactory(toPlainDate(end)).epochDay;
|
|
752
|
+
return countWorkdays(cal, a.epochDay, endDay, a);
|
|
753
|
+
}
|
|
754
|
+
function* eachWorkdayWith(cal, start, end) {
|
|
755
|
+
const dates = new DateFactory(toPlainDate(start));
|
|
756
|
+
const endDay = new DateFactory(toPlainDate(end)).epochDay;
|
|
757
|
+
for (let day = dates.epochDay; day <= endDay; day++) {
|
|
758
|
+
if (isWorkdayDay(cal, day, dates)) yield shiftDays(start, day - dates.epochDay);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
165
761
|
function isWeekend(date, options) {
|
|
166
|
-
return
|
|
762
|
+
return isWeekendWith(compileDayCalendar(options), date);
|
|
167
763
|
}
|
|
168
764
|
function isHoliday(date, options) {
|
|
169
|
-
|
|
170
|
-
const base = toPlainDate(date);
|
|
171
|
-
const day = epochDayOfPlainDate(base);
|
|
172
|
-
return !c.extra.has(day) && isHolidayDay(c, day, base, day);
|
|
765
|
+
return isHolidayWith(compileDayCalendar(options), date);
|
|
173
766
|
}
|
|
174
767
|
function isWorkday(date, options) {
|
|
175
|
-
|
|
176
|
-
const base = toPlainDate(date);
|
|
177
|
-
const day = epochDayOfPlainDate(base);
|
|
178
|
-
return isWorkdayDay(c, day, base, day);
|
|
768
|
+
return isWorkdayWith(compileDayCalendar(options), date);
|
|
179
769
|
}
|
|
180
770
|
function addWorkdays(date, amount, options) {
|
|
181
|
-
|
|
182
|
-
if (amount === 0) return date;
|
|
183
|
-
const c = getCompiled(options);
|
|
184
|
-
return shift(date, step(c, toPlainDate(date), Math.abs(amount), amount > 0 ? 1 : -1));
|
|
771
|
+
return addWorkdaysWith(compileDayCalendar(options), date, amount);
|
|
185
772
|
}
|
|
186
773
|
function subtractWorkdays(date, amount, options) {
|
|
187
|
-
|
|
188
|
-
return addWorkdays(date, -amount, options);
|
|
774
|
+
return subtractWorkdaysWith(compileDayCalendar(options), date, amount);
|
|
189
775
|
}
|
|
190
776
|
function nextWorkday(date, options) {
|
|
191
|
-
return
|
|
777
|
+
return nextWorkdayWith(compileDayCalendar(options), date);
|
|
192
778
|
}
|
|
193
779
|
function previousWorkday(date, options) {
|
|
194
|
-
return
|
|
780
|
+
return previousWorkdayWith(compileDayCalendar(options), date);
|
|
195
781
|
}
|
|
196
782
|
function rollForward(date, options) {
|
|
197
|
-
return
|
|
783
|
+
return rollForwardWith(compileDayCalendar(options), date);
|
|
198
784
|
}
|
|
199
785
|
function rollBackward(date, options) {
|
|
200
|
-
return
|
|
786
|
+
return rollBackwardWith(compileDayCalendar(options), date);
|
|
201
787
|
}
|
|
202
788
|
function workdaysBetween(start, end, options) {
|
|
203
|
-
|
|
204
|
-
const a = toPlainDate(start);
|
|
205
|
-
const b = toPlainDate(end);
|
|
206
|
-
const aDay = epochDayOfPlainDate(a);
|
|
207
|
-
const bDay = epochDayOfPlainDate(b);
|
|
208
|
-
if (aDay === bDay) return 0;
|
|
209
|
-
const [lo, hi, base, baseDay, sign] = aDay < bDay ? [aDay, bDay, a, aDay, 1] : [bDay, aDay, b, bDay, -1];
|
|
210
|
-
let count = 0;
|
|
211
|
-
for (let day = lo + 1; day <= hi; day++) {
|
|
212
|
-
if (isWorkdayDay(c, day, base, baseDay)) count++;
|
|
213
|
-
}
|
|
214
|
-
return sign * count;
|
|
789
|
+
return workdaysBetweenWith(compileDayCalendar(options), start, end);
|
|
215
790
|
}
|
|
216
|
-
function
|
|
217
|
-
|
|
218
|
-
const base = toPlainDate(start);
|
|
219
|
-
const baseDay = epochDayOfPlainDate(base);
|
|
220
|
-
const endDay = epochDayOfPlainDate(toPlainDate(end));
|
|
221
|
-
for (let day = baseDay; day <= endDay; day++) {
|
|
222
|
-
if (isWorkdayDay(c, day, base, baseDay)) yield shift(start, day - baseDay);
|
|
223
|
-
}
|
|
791
|
+
function eachWorkday(start, end, options) {
|
|
792
|
+
return eachWorkdayWith(compileDayCalendar(options), start, end);
|
|
224
793
|
}
|
|
794
|
+
|
|
795
|
+
// src/work-calendar.ts
|
|
225
796
|
function createWorkCalendar(options = {}) {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
797
|
+
checkOptionsObject(options);
|
|
798
|
+
const snapshot = snapshotOptions(options);
|
|
799
|
+
const days = compileDayCalendar(snapshot);
|
|
800
|
+
const calendar = {
|
|
801
|
+
options: snapshot,
|
|
802
|
+
isWorkday: (date) => isWorkdayWith(days, date),
|
|
803
|
+
isWeekend: (date) => isWeekendWith(days, date),
|
|
804
|
+
isHoliday: (date) => isHolidayWith(days, date),
|
|
805
|
+
addWorkdays: (date, amount) => addWorkdaysWith(days, date, amount),
|
|
806
|
+
subtractWorkdays: (date, amount) => subtractWorkdaysWith(days, date, amount),
|
|
807
|
+
nextWorkday: (date) => nextWorkdayWith(days, date),
|
|
808
|
+
previousWorkday: (date) => previousWorkdayWith(days, date),
|
|
809
|
+
rollForward: (date) => rollForwardWith(days, date),
|
|
810
|
+
rollBackward: (date) => rollBackwardWith(days, date),
|
|
811
|
+
workdaysBetween: (start, end) => workdaysBetweenWith(days, start, end),
|
|
812
|
+
eachWorkday: (start, end) => eachWorkdayWith(days, start, end)
|
|
813
|
+
};
|
|
814
|
+
if (!hasHours(snapshot)) return Object.freeze(calendar);
|
|
815
|
+
const hours = compileHoursCalendar(snapshot);
|
|
816
|
+
return Object.freeze({
|
|
817
|
+
...calendar,
|
|
818
|
+
options: snapshot,
|
|
819
|
+
isWorkingTime: (dateTime) => isWorkingTimeWith(hours, dateTime),
|
|
820
|
+
addWorkingTime: (dateTime, duration) => addWorkingTimeWith(hours, dateTime, duration),
|
|
821
|
+
subtractWorkingTime: (dateTime, duration) => subtractWorkingTimeWith(hours, dateTime, duration),
|
|
822
|
+
workingTimeBetween: (start, end) => workingTimeBetweenWith(hours, start, end),
|
|
823
|
+
rollForwardToWorkingTime: (dateTime) => rollForwardToWorkingTimeWith(hours, dateTime),
|
|
824
|
+
rollBackwardToWorkingTime: (dateTime) => rollBackwardToWorkingTimeWith(hours, dateTime),
|
|
825
|
+
getWorkingHours: (date) => getWorkingHoursWith(hours, date)
|
|
230
826
|
});
|
|
231
|
-
|
|
827
|
+
}
|
|
828
|
+
function hasHours(options) {
|
|
829
|
+
return options.hours !== void 0;
|
|
830
|
+
}
|
|
831
|
+
function snapshotOptions(options) {
|
|
832
|
+
const copy = (value) => isIterable(value) ? Object.freeze(Array.from(value)) : value;
|
|
232
833
|
return Object.freeze({
|
|
233
|
-
options
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
addWorkdays: (d, n) => addWorkdays(d, n, frozen),
|
|
238
|
-
subtractWorkdays: (d, n) => subtractWorkdays(d, n, frozen),
|
|
239
|
-
nextWorkday: (d) => nextWorkday(d, frozen),
|
|
240
|
-
previousWorkday: (d) => previousWorkday(d, frozen),
|
|
241
|
-
rollForward: (d) => rollForward(d, frozen),
|
|
242
|
-
rollBackward: (d) => rollBackward(d, frozen),
|
|
243
|
-
workdaysBetween: (a, b) => workdaysBetween(a, b, frozen),
|
|
244
|
-
eachWorkday: (a, b) => eachWorkday(a, b, frozen)
|
|
834
|
+
...options,
|
|
835
|
+
weekend: copy(options.weekend),
|
|
836
|
+
holidays: copy(options.holidays),
|
|
837
|
+
extraWorkdays: copy(options.extraWorkdays)
|
|
245
838
|
});
|
|
246
839
|
}
|
|
247
840
|
|
|
248
|
-
|
|
841
|
+
// src/date-holidays.ts
|
|
842
|
+
var ENTRY_DATE_RE = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})(?: ([+-])(\d{2})(\d{2}))?$/;
|
|
843
|
+
var DAY = BigInt(NS_PER_DAY);
|
|
844
|
+
var SNAP = 90n * BigInt(NS_PER_MINUTE);
|
|
845
|
+
function wallSpan(entry) {
|
|
846
|
+
const m = ENTRY_DATE_RE.exec(entry.date);
|
|
847
|
+
if (!m) throw rangeError(`unexpected date-holidays date ${describe(entry.date)}`);
|
|
848
|
+
const n = (i) => Number(m[i]);
|
|
849
|
+
const shiftSign = m[7] === "-" ? -1 : 1;
|
|
850
|
+
const shift = m[7] === void 0 ? 0 : shiftSign * (n(8) * NS_PER_HOUR + n(9) * NS_PER_MINUTE);
|
|
851
|
+
const timeNs = n(4) * NS_PER_HOUR + n(5) * NS_PER_MINUTE + n(6) * 1e9 + shift;
|
|
852
|
+
const start = BigInt(epochDayFromIso(n(1), n(2), n(3))) * DAY + BigInt(timeNs);
|
|
853
|
+
const durationMs = entry.end.getTime() - entry.start.getTime();
|
|
854
|
+
if (!Number.isFinite(durationMs) || durationMs <= 0) {
|
|
855
|
+
throw rangeError(`date-holidays entry ${describe(entry.date)} has no valid start/end`);
|
|
856
|
+
}
|
|
857
|
+
let end = start + BigInt(durationMs) * 1000000n;
|
|
858
|
+
const nearestMidnight = floorDiv(end + DAY / 2n, DAY) * DAY;
|
|
859
|
+
const distance = end - nearestMidnight;
|
|
860
|
+
if (distance !== 0n && distance > -SNAP && distance < SNAP) end = nearestMidnight;
|
|
861
|
+
return [start, end];
|
|
862
|
+
}
|
|
863
|
+
function addSpan(data, [start, end]) {
|
|
864
|
+
for (let day = floorDiv(start, DAY); day * DAY < end; day++) {
|
|
865
|
+
const dayStart = day * DAY;
|
|
866
|
+
const from = start > dayStart ? start - dayStart : 0n;
|
|
867
|
+
const to = end < dayStart + DAY ? end - dayStart : DAY;
|
|
868
|
+
const epochDay = Number(day);
|
|
869
|
+
if (from === 0n && to === DAY) {
|
|
870
|
+
data.full.add(epochDay);
|
|
871
|
+
} else {
|
|
872
|
+
const list = data.partial.get(epochDay) ?? [];
|
|
873
|
+
list.push([Number(from), Number(to)]);
|
|
874
|
+
data.partial.set(epochDay, list);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
function fromDateHolidays(source, options = {}) {
|
|
879
|
+
if (typeof source?.getHolidays !== "function") {
|
|
880
|
+
throw typeError(`expected a date-holidays instance (with getHolidays), got ${describe(source)}`);
|
|
881
|
+
}
|
|
882
|
+
const typesOption = options.types ?? ["public"];
|
|
883
|
+
if (!Array.isArray(typesOption) || !typesOption.every((t) => typeof t === "string")) {
|
|
884
|
+
throw typeError(`types must be an array of strings, got ${describe(typesOption)}`);
|
|
885
|
+
}
|
|
886
|
+
const types = new Set(typesOption);
|
|
887
|
+
const loaded = /* @__PURE__ */ new Set();
|
|
888
|
+
const known = { full: /* @__PURE__ */ new Set(), partial: /* @__PURE__ */ new Map() };
|
|
889
|
+
const ensure = (date) => {
|
|
890
|
+
const iso = toIsoDate(date);
|
|
891
|
+
for (const year of [iso.year - 1, iso.year, iso.year + 1]) {
|
|
892
|
+
if (loaded.has(year)) continue;
|
|
893
|
+
loaded.add(year);
|
|
894
|
+
for (const entry of source.getHolidays(year)) {
|
|
895
|
+
if (types.has(entry.type)) addSpan(known, wallSpan(entry));
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
return epochDayOf(iso);
|
|
899
|
+
};
|
|
900
|
+
return {
|
|
901
|
+
holidays: (date) => known.full.has(ensure(date)),
|
|
902
|
+
specialHours: (date, regular) => {
|
|
903
|
+
const day = ensure(date);
|
|
904
|
+
if (known.full.has(day)) return void 0;
|
|
905
|
+
const closures = known.partial.get(day);
|
|
906
|
+
if (!closures) return void 0;
|
|
907
|
+
let open = parseDayHours(regular, "regular hours");
|
|
908
|
+
for (const [from, to] of closures) open = subtractInterval(open, from, to);
|
|
909
|
+
return formatDayHours(open);
|
|
910
|
+
}
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
export { addWorkdays, addWorkingTime, createWorkCalendar, eachWorkday, fromDateHolidays, getWorkingHours, isHoliday, isWeekend, isWorkday, isWorkingTime, nextWorkday, previousWorkday, rollBackward, rollBackwardToWorkingTime, rollForward, rollForwardToWorkingTime, subtractWorkdays, subtractWorkingTime, workdaysBetween, workingTimeBetween };
|
|
249
915
|
//# sourceMappingURL=index.js.map
|
|
250
916
|
//# sourceMappingURL=index.js.map
|