bizdays-toolkit 0.18.0 → 0.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.
- package/CHANGELOG.md +26 -1
- package/README.md +11 -9
- package/dist/core/calendar.js +2 -2
- package/dist/core/diagnose.d.ts +5 -1
- package/dist/core/diagnose.js +68 -10
- package/dist/core/schedule.d.ts +96 -6
- package/dist/core/schedule.js +137 -13
- package/dist/index.d.ts +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,30 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.19.0] - 2026-10-25
|
|
8
|
+
|
|
9
|
+
### 移行
|
|
10
|
+
|
|
11
|
+
動作が変わる既存の API はない(`"after-holiday"` の規則に `skipWeekdays` を付けなければ振替先はこれまでどおり)。繰り返しの規則の形 `{ businessDayOfMonth }` と項目 `skipWeekdays`・`calendar`、診断コード `counting-basis` が増える。版ごとの作業は [docs/migration.md](./docs/migration.md) にまとめた。
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- 繰り返しの規則に月の第 n 営業日の形 `{ businessDayOfMonth: n }` を追加した(型 `BusinessDayOfMonthPattern`・`BusinessDayOfMonth`。n は 1〜31 と -1(最後)〜-31、配列可、`months` で絞れる)。「月末最終営業日は棚卸しで休業」「四半期初の第 1・第 5 営業日は予約を止める」を規則として書ける。数える基準はそのカレンダーの繰り返しの規則をすべて除いた判定(個別の日付・毎年の休業日・祝日・定休日の曜日)で、`nthBusinessDayOfMonth`・`lastBusinessDayOfMonth` は規則を適用した後の実際の営業日を返す。`onHolidayRule` は `"under-holiday"`(既定)と `"over-holiday"`(`holidayPolicy: "open"` で第 n 営業日が祝日に当たるときも適用する)を受け、`"after-holiday"` は `invalid-option`。スケジュールでは数える基準のカレンダーを `calendar` で指定する(必須。タイムゾーンが違えば `time-zone-conflict`。カレンダーの側に書くと `invalid-option`)
|
|
16
|
+
- `"after-holiday"` の曜日・日の規則に `skipWeekdays` を追加した。本来の日が祝日かその曜日なら、以降で最初の「祝日でもその曜日でもない日」に振り替える(「6 月 15 日は創立記念日。土日祝なら翌営業日」は `{ day: 15, months: [6], onHolidayRule: "after-holiday", skipWeekdays: [0, 6] }`、「月曜定休。祝日なら翌平日(土曜日には落とさない)」は `{ weekday: 1, onHolidayRule: "after-holiday", skipWeekdays: [6] }`)。`"after-holiday"` 以外の規則や規則自身の曜日に付けると `invalid-option`
|
|
17
|
+
- 診断コード `counting-basis`(info)を追加した。`businessDayOfMonth` の規則と、同じ設定の毎週の曜日の規則(`{ weekday }`)が同居する設定で、その曜日が営業日として数えられることを知らせる。`businessDayOfMonth` の規則は `never-matches`(その月にない回)・`shadowed`(n・months・基準のカレンダーの包含)・`redundant` の対象。`holidayPolicy: "closed"` の `businessDayOfMonth` の規則の `"over-holiday"` は `redundant`(info)。`skipWeekdays` が違う規則どうしは `shadowed` にしない
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- `docs/guide.md`・`docs/guide.ja.md`: 繰り返しの規則の表と本文に第 n 営業日と `skipWeekdays` を足し、コード例(銀行の支店とスケジュール)を足した
|
|
22
|
+
- README: 規則の表に `businessDayOfMonth`・`skipWeekdays` を足した
|
|
23
|
+
- `docs/decisions.md`: 47 番(第 n 営業日の規則・`skipWeekdays`・監視の報告書の日付)を追加し、14 番に `skipWeekdays` の追記をした
|
|
24
|
+
|
|
25
|
+
### 開発
|
|
26
|
+
|
|
27
|
+
- 監視の照合スクリプト(`scripts/verify-govuk.ts`・`verify-datagovsg.ts`・`verify-cao.ts`)の「確認した日付」を UTC から日本時間に揃えた(issue のコメントの日付と同じ基準)
|
|
28
|
+
- テスト: `test/business-day-of-month.test.ts`・`test/skip-weekdays.test.ts` を追加し、`test/types.check.ts`・利用者の型チェックに新しい規則を足した
|
|
29
|
+
- サイズの上限(`size-limits.json`)を上げた。ルート 25,000 → 26,500(測定 25,100)、`./jp` 28,000 → 29,500(測定 28,128)、`./diagnose` 33,000 → 35,500(測定 34,038)(gzip)。第 n 営業日の規則の判定・基準のキャッシュ・検証と、`skipWeekdays` の振替、診断の追加のため
|
|
30
|
+
|
|
7
31
|
## [0.18.0] - 2026-10-25
|
|
8
32
|
|
|
9
33
|
### 移行
|
|
@@ -494,7 +518,8 @@
|
|
|
494
518
|
|
|
495
519
|
- 日本: 2026 年 2 月の官報(2027 年の春分・秋分の日)まで反映(`dataVersion: "2026-02"`、`confirmedThrough: 2027`)
|
|
496
520
|
|
|
497
|
-
[Unreleased]: https://github.com/inohan77/bizdays-toolkit/compare/v0.
|
|
521
|
+
[Unreleased]: https://github.com/inohan77/bizdays-toolkit/compare/v0.19.0...HEAD
|
|
522
|
+
[0.19.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.18.0...v0.19.0
|
|
498
523
|
[0.18.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.17.1...v0.18.0
|
|
499
524
|
[0.17.1]: https://github.com/inohan77/bizdays-toolkit/compare/v0.17.0...v0.17.1
|
|
500
525
|
[0.17.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.16.0...v0.17.0
|
package/README.md
CHANGED
|
@@ -74,15 +74,17 @@ Each holiday carries its date, name (and English name), `kind` (national, substi
|
|
|
74
74
|
|
|
75
75
|
A calendar is built from the holiday provider and your closures: `closedWeekdays`, `closedAnnual` (month-day ranges, across the new year too), `closedDates` and `openDates`, and recurring rules in `closedRecurring` / `openRecurring`:
|
|
76
76
|
|
|
77
|
-
| Rule | Meaning
|
|
78
|
-
| --------------------------------------------------------- |
|
|
79
|
-
| `{ weekday: 3, nth: 3 }` | The 3rd Wednesday (`nth` is 1–5 or -1 for the last; an array for several)
|
|
80
|
-
| `{ weekday: 3, interval: 2, anchor: "2026-01-07" }` | Every other Wednesday, counted from the anchor
|
|
81
|
-
| `{ day: 20 }`, `{ day: -1 }` | The 20th, the last day of the month
|
|
82
|
-
| `{
|
|
83
|
-
| `{
|
|
84
|
-
| `
|
|
85
|
-
| `
|
|
77
|
+
| Rule | Meaning |
|
|
78
|
+
| --------------------------------------------------------- | -------------------------------------------------------------------------------- |
|
|
79
|
+
| `{ weekday: 3, nth: 3 }` | The 3rd Wednesday (`nth` is 1–5 or -1 for the last; an array for several) |
|
|
80
|
+
| `{ weekday: 3, interval: 2, anchor: "2026-01-07" }` | Every other Wednesday, counted from the anchor |
|
|
81
|
+
| `{ day: 20 }`, `{ day: -1 }` | The 20th, the last day of the month |
|
|
82
|
+
| `{ businessDayOfMonth: -1 }`, `{ businessDayOfMonth: 5 }` | The last business day of the month, the 5th business day |
|
|
83
|
+
| `{ holiday: true, kinds: ["substitute"] }` | Holidays, optionally narrowed by kind or by key (`keys: ["vernal-equinox"]`) |
|
|
84
|
+
| `{ dayAfterHoliday: true }`, `{ dayBeforeHoliday: true }` | The day after (or before) a holiday or a run of holidays |
|
|
85
|
+
| `onHolidayRule: "after-holiday"` | "Closed on Mondays, or the next weekday when Monday is a holiday" (museum rule) |
|
|
86
|
+
| `skipWeekdays: [0, 6]` | With `"after-holiday"`: also skip weekends ("June 15, or the next business day") |
|
|
87
|
+
| `effectiveFrom`, `effectiveTo` | The period the rule is in effect |
|
|
86
88
|
|
|
87
89
|
`dayInfo` tells you why a day is open or closed (`reason`, `name`, `rulePath`). Every bundled holiday that recurs each year has a stable `key` (`vernal-equinox`, `good-friday`, `hari-raya-puasa`, …), so a rule can select individual holidays that no date pattern can express. Details: [Business-day calendars](https://github.com/inohan77/bizdays-toolkit/blob/main/docs/guide.md#business-day-calendars), [Recurring rules](https://github.com/inohan77/bizdays-toolkit/blob/main/docs/guide.md#recurring-rules), [Holiday keys](https://github.com/inohan77/bizdays-toolkit/blob/main/docs/guide.md#holiday-keys), [Common recipes](https://github.com/inohan77/bizdays-toolkit/blob/main/docs/guide.md#common-recipes).
|
|
88
90
|
|
package/dist/core/calendar.js
CHANGED
|
@@ -72,8 +72,8 @@ export function compileCalendarOptions(c, options) {
|
|
|
72
72
|
const annual = compileAnnualRules(c, "closedAnnual", o["closedAnnual"], () => false, true);
|
|
73
73
|
const open = compileDateRules(c, "openDates", o["openDates"], () => true, true);
|
|
74
74
|
const closed = compileDateRules(c, "closedDates", o["closedDates"], () => false, true);
|
|
75
|
-
const openRecurring = compileRecurringRules(c, "openRecurring", o["openRecurring"], () => true, RECURRING_RULE_KEYS);
|
|
76
|
-
const closedRecurring = compileRecurringRules(c, "closedRecurring", o["closedRecurring"], () => false, RECURRING_RULE_KEYS);
|
|
75
|
+
const openRecurring = compileRecurringRules(c, "openRecurring", o["openRecurring"], () => true, RECURRING_RULE_KEYS, "calendar");
|
|
76
|
+
const closedRecurring = compileRecurringRules(c, "closedRecurring", o["closedRecurring"], () => false, RECURRING_RULE_KEYS, "calendar");
|
|
77
77
|
return {
|
|
78
78
|
schedule: {
|
|
79
79
|
source,
|
package/dist/core/diagnose.d.ts
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
* no-business-day … 営業日が 1 日もない設定(営業日カレンダーのみ)
|
|
14
14
|
* - info … redundant: 取り除いても結果が変わらない規則・設定(判定に祝日を使わない設定の unconfirmedHolidays: "throw" を含む)
|
|
15
15
|
* expired: 第 2 引数の asOf より前に終わった規則・日付(asOf を渡したときだけ)
|
|
16
|
+
* counting-basis: 第 n 営業日の規則(businessDayOfMonth)の数える基準は繰り返しの規則を除くので、
|
|
17
|
+
* 同じ設定の毎週の曜日の規則({ weekday })の曜日は営業日として数えられる(relatedPath にその規則)
|
|
16
18
|
*
|
|
17
19
|
* 未確定の祝日の扱い(unconfirmedHolidays)は診断では常に "accept" で評価する。診断は設定の形を調べるもので、
|
|
18
20
|
* 祝日が確定しているかどうかは関係がなく、"throw" のまま評価すると例外を握りつぶして「当てはまらない」と誤判定するため
|
|
@@ -51,7 +53,9 @@ export type KnownDiagnosticCode = KnownErrorCode
|
|
|
51
53
|
/** 取り除いても結果が変わらない */
|
|
52
54
|
| "redundant"
|
|
53
55
|
/** 基準日(asOf)より前に終わった規則・日付(整理の候補) */
|
|
54
|
-
| "expired"
|
|
56
|
+
| "expired"
|
|
57
|
+
/** 第 n 営業日の規則の数える基準に、同じ設定の曜日の規則が入らない(その曜日を営業日として数える) */
|
|
58
|
+
| "counting-basis";
|
|
55
59
|
/**
|
|
56
60
|
* 診断コード。将来の版で値が増えることがあるので、分岐では「その他」も扱うこと。
|
|
57
61
|
* @public
|
package/dist/core/diagnose.js
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
* no-business-day … 営業日が 1 日もない設定(営業日カレンダーのみ)
|
|
14
14
|
* - info … redundant: 取り除いても結果が変わらない規則・設定(判定に祝日を使わない設定の unconfirmedHolidays: "throw" を含む)
|
|
15
15
|
* expired: 第 2 引数の asOf より前に終わった規則・日付(asOf を渡したときだけ)
|
|
16
|
+
* counting-basis: 第 n 営業日の規則(businessDayOfMonth)の数える基準は繰り返しの規則を除くので、
|
|
17
|
+
* 同じ設定の毎週の曜日の規則({ weekday })の曜日は営業日として数えられる(relatedPath にその規則)
|
|
16
18
|
*
|
|
17
19
|
* 未確定の祝日の扱い(unconfirmedHolidays)は診断では常に "accept" で評価する。診断は設定の形を調べるもので、
|
|
18
20
|
* 祝日が確定しているかどうかは関係がなく、"throw" のまま評価すると例外を握りつぶして「当てはまらない」と誤判定するため
|
|
@@ -26,7 +28,7 @@
|
|
|
26
28
|
import { daysInMonth, fromDayNumber, isValidDate, parseDate, toDayNumber, } from "./date.js";
|
|
27
29
|
import { compileCalendarOptions } from "./calendar.js";
|
|
28
30
|
import { IssueCollector, rangeError } from "./errors.js";
|
|
29
|
-
import { compileScheduleOptions, createHolidayLookup, createResolver, inPeriod, matchMonthDay, matchPattern, } from "./schedule.js";
|
|
31
|
+
import { compileScheduleOptions, createBasis, createHolidayLookup, createResolver, inPeriod, matchMonthDay, matchPattern, } from "./schedule.js";
|
|
30
32
|
// ------------------------------------------------------------
|
|
31
33
|
// 知らないキーの候補
|
|
32
34
|
// ------------------------------------------------------------
|
|
@@ -311,6 +313,7 @@ function analyze(c, compiled, isCalendar) {
|
|
|
311
313
|
if (!checkProviderData(c, source))
|
|
312
314
|
return;
|
|
313
315
|
const lookup = createHolidayLookup({ ...source, unconfirmedHolidays: "accept" });
|
|
316
|
+
const basis = createBasis(compiled, lookup);
|
|
314
317
|
const cycle = patternCycle(recurring);
|
|
315
318
|
const window = scanWindow(source, [...annual, ...recurring], cycle);
|
|
316
319
|
/** 使われないことが分かった規則(redundant を重ねて出さない) */
|
|
@@ -381,12 +384,41 @@ function analyze(c, compiled, isCalendar) {
|
|
|
381
384
|
eachDay(window, (n) => (found = inPeriod(rule, n) && matchPattern(rule, fromDayNumber(n), holidayOfSafe)));
|
|
382
385
|
return found;
|
|
383
386
|
};
|
|
387
|
+
/** 第 n 営業日の規則が、調べる期間と有効期間の中で 1 日でも当てはまるか(基準は祝日次第で年ごとに違うので、全日を調べる。対応年の外は当てはまらないとみなす) */
|
|
388
|
+
const businessDayRuleMatches = (rule) => {
|
|
389
|
+
if (!window)
|
|
390
|
+
return false;
|
|
391
|
+
let found = false;
|
|
392
|
+
eachDay(window, (n) => {
|
|
393
|
+
if (!inPeriod(rule, n))
|
|
394
|
+
return false;
|
|
395
|
+
try {
|
|
396
|
+
found = matchPattern(rule, fromDayNumber(n), holidayOfSafe, basis);
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
found = false;
|
|
400
|
+
}
|
|
401
|
+
return found;
|
|
402
|
+
});
|
|
403
|
+
return found;
|
|
404
|
+
};
|
|
384
405
|
/** 集合 a(null はすべて)が集合 b(null はすべて)を含むか */
|
|
385
406
|
const setCovers = (a, b) => a === null || (b !== null && [...b].every((v) => a.has(v)));
|
|
407
|
+
/** 振替で飛ばす曜日が同じか(違えば当てはまる日が違うので覆わない) */
|
|
408
|
+
const sameSkip = (a, b) => a.skipWeekdays === null
|
|
409
|
+
? b.skipWeekdays === null
|
|
410
|
+
: b.skipWeekdays !== null &&
|
|
411
|
+
a.skipWeekdays.size === b.skipWeekdays.size &&
|
|
412
|
+
[...a.skipWeekdays].every((w) => b.skipWeekdays.has(w));
|
|
386
413
|
recurring.forEach((rule, j) => {
|
|
387
414
|
const bits = recurringBits[j];
|
|
388
415
|
const holidayBased = rule.holiday || rule.dayAfterHoliday || rule.dayBeforeHoliday;
|
|
389
|
-
const
|
|
416
|
+
const businessDayBased = rule.businessDays !== null;
|
|
417
|
+
const matches = holidayBased
|
|
418
|
+
? holidayRuleMatches(rule)
|
|
419
|
+
: businessDayBased
|
|
420
|
+
? businessDayRuleMatches(rule)
|
|
421
|
+
: matchesAnyDay(rule, cycle);
|
|
390
422
|
if (!matches) {
|
|
391
423
|
unused.add(rule);
|
|
392
424
|
c.warning("never-matches", rule.path, holidayBased
|
|
@@ -397,15 +429,35 @@ function analyze(c, compiled, isCalendar) {
|
|
|
397
429
|
: rule.dayAfterHoliday
|
|
398
430
|
? "This rule matches no day: no day after a holiday on the selected weekdays and months within its effective period"
|
|
399
431
|
: "This rule matches no day: no day before a holiday on the selected weekdays and months within its effective period"
|
|
400
|
-
:
|
|
401
|
-
? "This rule matches no day
|
|
402
|
-
:
|
|
432
|
+
: businessDayBased
|
|
433
|
+
? "This rule matches no day: no month has that many business days of the counting basis in the selected months within its effective period"
|
|
434
|
+
: rule.effectiveFrom === null && rule.effectiveTo === null
|
|
435
|
+
? "This rule matches no day (for example, a day that does not exist in the selected months)"
|
|
436
|
+
: "This rule matches no day within its effective period");
|
|
403
437
|
return;
|
|
404
438
|
}
|
|
439
|
+
// 第 n 営業日の規則の "over-holiday" は、祝日が基準の営業日にならない設定(holidayPolicy: "closed")では結果を変えない
|
|
440
|
+
if (businessDayBased &&
|
|
441
|
+
rule.basis === null &&
|
|
442
|
+
rule.onHolidayRule === "over-holiday" &&
|
|
443
|
+
typeof compiled.holiday === "object" &&
|
|
444
|
+
"value" in compiled.holiday &&
|
|
445
|
+
compiled.holiday.value === false) {
|
|
446
|
+
c.info("redundant", `${rule.path}.onHolidayRule`, 'This option does not change the result: holidays are closed (holidayPolicy: "closed"), so an nth business day is never a holiday');
|
|
447
|
+
}
|
|
448
|
+
// 第 n 営業日の規則の数える基準は繰り返しの規則を除くので、同じ設定の毎週の曜日の規則があれば知らせる(定休日を曜日の規則で書いた設定)
|
|
449
|
+
if (businessDayBased && rule.basis === null) {
|
|
450
|
+
const weekly = recurring.find((r) => r.weekday !== null && r.nth === null && r.interval === null && r.months === null);
|
|
451
|
+
if (weekly) {
|
|
452
|
+
c.info("counting-basis", rule.path, `The business days are counted without recurring rules, so the weekday rule ${weekly.path} does not reduce the count; ` +
|
|
453
|
+
"write regular closed weekdays as closedWeekdays if they should not be counted", weekly.path);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
405
456
|
// 祝日・祝日の翌日・祝前日の規則は祝日次第で年ごとに変わるので、形の包含は同じ形の規則どうし(種別・識別子・曜日・月の絞り込み)だけで調べる
|
|
406
457
|
const k = recurring.findIndex((r, i) => i < j &&
|
|
407
458
|
canCover(r, rule) &&
|
|
408
459
|
periodCovers(r, rule) &&
|
|
460
|
+
sameSkip(r, rule) &&
|
|
409
461
|
(rule.holiday
|
|
410
462
|
? r.holiday &&
|
|
411
463
|
setCovers(r.kinds, rule.kinds) &&
|
|
@@ -417,11 +469,17 @@ function analyze(c, compiled, isCalendar) {
|
|
|
417
469
|
r.dayBeforeHoliday === rule.dayBeforeHoliday &&
|
|
418
470
|
setCovers(r.weekdays, rule.weekdays) &&
|
|
419
471
|
setCovers(r.months, rule.months)
|
|
420
|
-
:
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
472
|
+
: rule.businessDays !== null
|
|
473
|
+
? r.businessDays !== null &&
|
|
474
|
+
r.calendar === rule.calendar &&
|
|
475
|
+
setCovers(r.businessDays, rule.businessDays) &&
|
|
476
|
+
setCovers(r.months, rule.months)
|
|
477
|
+
: !r.holiday &&
|
|
478
|
+
!r.dayAfterHoliday &&
|
|
479
|
+
!r.dayBeforeHoliday &&
|
|
480
|
+
r.businessDays === null &&
|
|
481
|
+
intervalCovers(r, rule) &&
|
|
482
|
+
isSubset(bits, recurringBits[i])));
|
|
425
483
|
if (k < 0)
|
|
426
484
|
return;
|
|
427
485
|
unused.add(rule);
|
package/dist/core/schedule.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { IssueCollector } from "./errors.ts";
|
|
|
23
23
|
import { type CivilDateTimeParts, type DateInput, type DateTimeInput } from "./input.ts";
|
|
24
24
|
export type { CivilDateTimeParts } from "./input.ts";
|
|
25
25
|
import { type Holiday, type HolidayProvider } from "./types.ts";
|
|
26
|
+
import type { Calendar } from "./calendar.ts";
|
|
26
27
|
/**
|
|
27
28
|
* 祝日の値の決め方(スケジュールの holiday の設定)。
|
|
28
29
|
* @public
|
|
@@ -48,6 +49,11 @@ export type Nth = 1 | 2 | 3 | 4 | 5 | -1;
|
|
|
48
49
|
* @public
|
|
49
50
|
*/
|
|
50
51
|
export type DayOfMonth = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -1;
|
|
52
|
+
/**
|
|
53
|
+
* 月の第 n 営業日(1〜31。-1 は最後、-2 はその前、… -31 まで。0 は不可)。
|
|
54
|
+
* @public
|
|
55
|
+
*/
|
|
56
|
+
export type BusinessDayOfMonth = number;
|
|
51
57
|
/**
|
|
52
58
|
* 月(1〜12)。
|
|
53
59
|
* @public
|
|
@@ -89,7 +95,16 @@ export interface WeekdayPattern {
|
|
|
89
95
|
readonly anchor?: DateString;
|
|
90
96
|
/** 当てはめる月(省略時はすべての月) */
|
|
91
97
|
readonly months?: readonly Month[];
|
|
98
|
+
/**
|
|
99
|
+
* onHolidayRule: "after-holiday" と組で、祝日に加えて飛ばす曜日(0 = 日曜日〜6 = 土曜日)。
|
|
100
|
+
* 本来の日が祝日かこの曜日なら、その日以降で最初の、祝日でもこの曜日でもない日に振り替える
|
|
101
|
+
* (例: 月曜定休で、月曜が祝日なら翌平日(土曜日には落とさない)は { weekday: 1, onHolidayRule: "after-holiday", skipWeekdays: [6] })。
|
|
102
|
+
* weekday 自身は含められない。"after-holiday" 以外と組にすると invalid-option
|
|
103
|
+
*/
|
|
104
|
+
readonly skipWeekdays?: readonly Weekday[];
|
|
92
105
|
readonly day?: never;
|
|
106
|
+
readonly businessDayOfMonth?: never;
|
|
107
|
+
readonly calendar?: never;
|
|
93
108
|
readonly dayAfterHoliday?: never;
|
|
94
109
|
readonly dayBeforeHoliday?: never;
|
|
95
110
|
readonly holiday?: never;
|
|
@@ -106,16 +121,61 @@ export interface DayOfMonthPattern {
|
|
|
106
121
|
readonly day: DayOfMonth | readonly DayOfMonth[];
|
|
107
122
|
/** 当てはめる月(省略時はすべての月) */
|
|
108
123
|
readonly months?: readonly Month[];
|
|
124
|
+
/**
|
|
125
|
+
* onHolidayRule: "after-holiday" と組で、祝日に加えて飛ばす曜日。本来の日が祝日かこの曜日なら、その日以降で最初の、
|
|
126
|
+
* 祝日でもこの曜日でもない日に振り替える(例: 「6 月 15 日は創立記念日。土日祝なら翌営業日」は
|
|
127
|
+
* { day: 15, months: [6], onHolidayRule: "after-holiday", skipWeekdays: [0, 6] })。"after-holiday" 以外と組にすると invalid-option
|
|
128
|
+
*/
|
|
129
|
+
readonly skipWeekdays?: readonly Weekday[];
|
|
130
|
+
readonly weekday?: never;
|
|
131
|
+
readonly nth?: never;
|
|
132
|
+
readonly interval?: never;
|
|
133
|
+
readonly anchor?: never;
|
|
134
|
+
readonly businessDayOfMonth?: never;
|
|
135
|
+
readonly calendar?: never;
|
|
136
|
+
readonly dayAfterHoliday?: never;
|
|
137
|
+
readonly dayBeforeHoliday?: never;
|
|
138
|
+
readonly holiday?: never;
|
|
139
|
+
readonly weekdays?: never;
|
|
140
|
+
readonly kinds?: never;
|
|
141
|
+
readonly keys?: never;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* 月の第 n 営業日(例: 月末最終営業日、第 5 営業日)。n は 1〜31、-1 は最後、-2 はその前。その月にない回は当てはまらない。
|
|
145
|
+
*
|
|
146
|
+
* 数える基準はこの規則を書いたカレンダーの「繰り返しの規則をすべて除いた判定」(個別の日付 → 毎年の月日 → 祝日 → 定休日の曜日)で、
|
|
147
|
+
* 定休日の曜日・祝日・年末年始休業(closedAnnual)・臨時休業日は数えに入らない。繰り返しの規則(定休日を closedRecurring の
|
|
148
|
+
* 曜日の規則で書いたものを含む)は基準に入らない。nthBusinessDayOfMonth・lastBusinessDayOfMonth はこの規則を適用した後の
|
|
149
|
+
* 実際の営業日を返すので、{ businessDayOfMonth: -1 } を休業にすると lastBusinessDayOfMonth はその前日になる。
|
|
150
|
+
* onHolidayRule は "under-holiday"(既定)と "over-holiday" を受け、"after-holiday" は invalid-option(第 n 営業日をずらす意味がない)。
|
|
151
|
+
* holidayPolicy: "open" のカレンダーでは祝日も営業日として数えるので第 n 営業日が祝日に当たることがあり、既定ではその日は
|
|
152
|
+
* 祝日の段階が先に決めてこの規則は当てはまらない(曜日の規則と同じ優先順位)。「第 1 営業日は祝日でも休業」は "over-holiday" で書く。
|
|
153
|
+
* holidayPolicy: "closed" では祝日が基準の営業日にならないので "over-holiday" は結果を変えない(診断が info で知らせる)。
|
|
154
|
+
*
|
|
155
|
+
* スケジュール(createSchedule の recurring)では、スケジュール自身に営業・休業の概念がないので、数える基準にする作成済みの
|
|
156
|
+
* カレンダーを calendar で指定する(必須。タイムゾーンはスケジュールと同じであること)。カレンダー(closedRecurring・openRecurring)
|
|
157
|
+
* では calendar は指定できない(invalid-option。基準は自分の設定で決まる)。
|
|
158
|
+
* @public
|
|
159
|
+
*/
|
|
160
|
+
export interface BusinessDayOfMonthPattern {
|
|
161
|
+
/** 月の第 n 営業日(1〜31、-1 は最後、-2 はその前、… -31 まで)。配列で複数指定できる */
|
|
162
|
+
readonly businessDayOfMonth: BusinessDayOfMonth | readonly BusinessDayOfMonth[];
|
|
163
|
+
/** 当てはめる月(省略時はすべての月) */
|
|
164
|
+
readonly months?: readonly Month[];
|
|
165
|
+
/** 営業日を数える基準のカレンダー(スケジュールでは必須。カレンダーでは指定できない) */
|
|
166
|
+
readonly calendar?: Calendar;
|
|
109
167
|
readonly weekday?: never;
|
|
110
168
|
readonly nth?: never;
|
|
111
169
|
readonly interval?: never;
|
|
112
170
|
readonly anchor?: never;
|
|
171
|
+
readonly day?: never;
|
|
113
172
|
readonly dayAfterHoliday?: never;
|
|
114
173
|
readonly dayBeforeHoliday?: never;
|
|
115
174
|
readonly holiday?: never;
|
|
116
175
|
readonly weekdays?: never;
|
|
117
176
|
readonly kinds?: never;
|
|
118
177
|
readonly keys?: never;
|
|
178
|
+
readonly skipWeekdays?: never;
|
|
119
179
|
}
|
|
120
180
|
/**
|
|
121
181
|
* 祝日の翌日(祝日が続く場合は連休が明けた日。その日自身は祝日でない)。「祝日」は提供元の祝日だけ。
|
|
@@ -133,10 +193,13 @@ export interface DayAfterHolidayPattern {
|
|
|
133
193
|
readonly interval?: never;
|
|
134
194
|
readonly anchor?: never;
|
|
135
195
|
readonly day?: never;
|
|
196
|
+
readonly businessDayOfMonth?: never;
|
|
197
|
+
readonly calendar?: never;
|
|
136
198
|
readonly dayBeforeHoliday?: never;
|
|
137
199
|
readonly holiday?: never;
|
|
138
200
|
readonly kinds?: never;
|
|
139
201
|
readonly keys?: never;
|
|
202
|
+
readonly skipWeekdays?: never;
|
|
140
203
|
}
|
|
141
204
|
/**
|
|
142
205
|
* 祝前日(翌日が祝日で、その日自身は祝日でない日。連休なら連休の初日の前日)。「祝日」は提供元の祝日だけ。
|
|
@@ -155,10 +218,13 @@ export interface DayBeforeHolidayPattern {
|
|
|
155
218
|
readonly interval?: never;
|
|
156
219
|
readonly anchor?: never;
|
|
157
220
|
readonly day?: never;
|
|
221
|
+
readonly businessDayOfMonth?: never;
|
|
222
|
+
readonly calendar?: never;
|
|
158
223
|
readonly dayAfterHoliday?: never;
|
|
159
224
|
readonly holiday?: never;
|
|
160
225
|
readonly kinds?: never;
|
|
161
226
|
readonly keys?: never;
|
|
227
|
+
readonly skipWeekdays?: never;
|
|
162
228
|
}
|
|
163
229
|
/**
|
|
164
230
|
* 祝日(提供元の祝日である日)。kinds で祝日の種別を、keys で個別の祝日を絞れる(省略時はすべての祝日)。
|
|
@@ -188,14 +254,17 @@ export interface HolidayPattern<Kind extends string = string> {
|
|
|
188
254
|
readonly interval?: never;
|
|
189
255
|
readonly anchor?: never;
|
|
190
256
|
readonly day?: never;
|
|
257
|
+
readonly businessDayOfMonth?: never;
|
|
258
|
+
readonly calendar?: never;
|
|
191
259
|
readonly dayAfterHoliday?: never;
|
|
192
260
|
readonly dayBeforeHoliday?: never;
|
|
261
|
+
readonly skipWeekdays?: never;
|
|
193
262
|
}
|
|
194
263
|
/**
|
|
195
264
|
* 繰り返しのパターン。
|
|
196
265
|
* @public
|
|
197
266
|
*/
|
|
198
|
-
export type RecurringPattern<Kind extends string = string> = WeekdayPattern | DayOfMonthPattern | DayAfterHolidayPattern | DayBeforeHolidayPattern | HolidayPattern<Kind>;
|
|
267
|
+
export type RecurringPattern<Kind extends string = string> = WeekdayPattern | DayOfMonthPattern | BusinessDayOfMonthPattern | DayAfterHolidayPattern | DayBeforeHolidayPattern | HolidayPattern<Kind>;
|
|
199
268
|
/**
|
|
200
269
|
* 繰り返しの規則(営業日カレンダーの closedRecurring・openRecurring)。
|
|
201
270
|
* @public
|
|
@@ -421,6 +490,14 @@ export interface RecurringRuleCompiled<T> extends CompiledPeriod {
|
|
|
421
490
|
readonly anchor: number | null;
|
|
422
491
|
/** 日の集合(曜日の規則では null) */
|
|
423
492
|
readonly days: ReadonlySet<number> | null;
|
|
493
|
+
/** 第 n 営業日の集合(第 n 営業日の規則以外では null) */
|
|
494
|
+
readonly businessDays: ReadonlySet<number> | null;
|
|
495
|
+
/** 営業日を数える基準(スケジュールの第 n 営業日の規則の calendar から。null なら自分の設定の「繰り返しの規則を除いた判定」) */
|
|
496
|
+
readonly basis: BasisOfMonth | null;
|
|
497
|
+
/** basis の元のカレンダー(診断で同じ基準かを比べるため。basis が null なら null) */
|
|
498
|
+
readonly calendar: Calendar | null;
|
|
499
|
+
/** "after-holiday" の振替で、祝日に加えて飛ばす曜日(null はなし) */
|
|
500
|
+
readonly skipWeekdays: ReadonlySet<number> | null;
|
|
424
501
|
/** 祝日の翌日の規則か */
|
|
425
502
|
readonly dayAfterHoliday: boolean;
|
|
426
503
|
/** 祝前日の規則か */
|
|
@@ -439,18 +516,31 @@ export interface RecurringRuleCompiled<T> extends CompiledPeriod {
|
|
|
439
516
|
readonly value: T;
|
|
440
517
|
readonly name: string | undefined;
|
|
441
518
|
}
|
|
519
|
+
/**
|
|
520
|
+
* @internal 営業日を数える基準。年と月を受け取り、その月の基準で営業日である日(1〜31)を日付順に返す。
|
|
521
|
+
* 第 n 営業日の規則(businessDayOfMonth)の判定に使う
|
|
522
|
+
*/
|
|
523
|
+
export type BasisOfMonth = (year: number, month: number) => readonly number[];
|
|
524
|
+
/** @internal カレンダーの判定関数から、月ごとに結果をキャッシュする基準を作る */
|
|
525
|
+
export declare function basisFromPredicate(isOpen: (date: DateString) => boolean): BasisOfMonth;
|
|
442
526
|
/** @internal 繰り返しの規則を検証して取り込む */
|
|
443
|
-
export declare function compileRecurringRules<T>(c: IssueCollector, path: string, entries: unknown, valueOf: (entry: Record<string, unknown>, path: string) => T | undefined, keys: readonly string[]): RecurringRuleCompiled<T>[];
|
|
527
|
+
export declare function compileRecurringRules<T>(c: IssueCollector, path: string, entries: unknown, valueOf: (entry: Record<string, unknown>, path: string) => T | undefined, keys: readonly string[], context: "calendar" | "schedule"): RecurringRuleCompiled<T>[];
|
|
444
528
|
/** @internal その日の祝日を返す関数(祝日・祝日の翌日・祝前日の規則の判定に使う。提供元の対応年の外は祝日でないとみなすこと) */
|
|
445
529
|
export type HolidayOf = (date: DateString) => Holiday<string> | undefined;
|
|
446
530
|
/**
|
|
447
|
-
* @internal 繰り返しの規則の形(曜日・第 n
|
|
531
|
+
* @internal 繰り返しの規則の形(曜日・第 n 回目・周期・日・第 n 営業日・祝日・祝日の翌日・祝前日・月)がその日に当てはまるか。
|
|
448
532
|
* 有効期間は考えない(規則の診断で、形どうしの包含を調べるために分けている)。
|
|
449
|
-
* 祝日に従属する規則は holidayOf で判定し、holidayOf
|
|
533
|
+
* 祝日に従属する規則は holidayOf で判定し、holidayOf を渡さなければ当てはまらない。
|
|
534
|
+
* 第 n 営業日の規則は、規則自身の基準(スケジュールの calendar)か basis で数え、どちらもなければ当てはまらない
|
|
450
535
|
*/
|
|
451
|
-
export declare function matchPattern(rule: RecurringRuleCompiled<unknown>, date: DateString, holidayOf?: HolidayOf): boolean;
|
|
536
|
+
export declare function matchPattern(rule: RecurringRuleCompiled<unknown>, date: DateString, holidayOf?: HolidayOf, basis?: BasisOfMonth): boolean;
|
|
452
537
|
/** @internal 繰り返しの規則がその日に当てはまるか(有効期間を含む) */
|
|
453
|
-
export declare function matchRecurring(rule: RecurringRuleCompiled<unknown>, date: DateString, holidayOf: HolidayOf): boolean;
|
|
538
|
+
export declare function matchRecurring(rule: RecurringRuleCompiled<unknown>, date: DateString, holidayOf: HolidayOf, basis?: BasisOfMonth): boolean;
|
|
539
|
+
/**
|
|
540
|
+
* @internal 第 n 営業日の規則が自分の設定を基準にする場合の基準を作る(繰り返しの規則をすべて除いた判定で営業日か)。
|
|
541
|
+
* 基準が要らない設定(第 n 営業日の規則がない、またはすべて calendar を持つ)では undefined
|
|
542
|
+
*/
|
|
543
|
+
export declare function createBasis<T, Kind extends string>(compiled: CompiledSchedule<T, Kind>, lookup: HolidayLookupInternal<Kind>): BasisOfMonth | undefined;
|
|
454
544
|
/** @internal 祝日の提供元とタイムゾーンの設定を検証した結果 */
|
|
455
545
|
export interface HolidaySource<Kind extends string> {
|
|
456
546
|
readonly providers: readonly HolidayProvider<Kind>[];
|
package/dist/core/schedule.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*
|
|
19
19
|
* 営業日カレンダー(createCalendar)は、このスケジュールの真偽値版として作られている。
|
|
20
20
|
*/
|
|
21
|
-
import { addDays, daysInMonth, fromDayNumber, isValidDate, parseDate, toDayNumber, weekdayOf, } from "./date.js";
|
|
21
|
+
import { addDays, daysInMonth, formatDate, fromDayNumber, isValidDate, parseDate, toDayNumber, weekdayOf, } from "./date.js";
|
|
22
22
|
import { checkUnknownKeys, IssueCollector, rangeError } from "./errors.js";
|
|
23
23
|
import { instantToCivil, toCivil, toDateTimeParts, toInstant, validateTimeZone, } from "./input.js";
|
|
24
24
|
import { HOLIDAY_KEY_PATTERN } from "./types.js";
|
|
@@ -176,6 +176,29 @@ export function compileAnnualRules(c, path, entries, valueOf, allowString) {
|
|
|
176
176
|
return rules;
|
|
177
177
|
}
|
|
178
178
|
const NTH_VALUES = [1, 2, 3, 4, 5, -1];
|
|
179
|
+
const isBusinessDayOfMonth = (v) => Number.isInteger(v) && v !== 0 && v >= -31 && v <= 31;
|
|
180
|
+
/** @internal カレンダーの判定関数から、月ごとに結果をキャッシュする基準を作る */
|
|
181
|
+
export function basisFromPredicate(isOpen) {
|
|
182
|
+
const cache = new Map();
|
|
183
|
+
return (year, month) => {
|
|
184
|
+
const key = year * 12 + month;
|
|
185
|
+
let days = cache.get(key);
|
|
186
|
+
if (days === undefined) {
|
|
187
|
+
const list = [];
|
|
188
|
+
const dim = daysInMonth(year, month);
|
|
189
|
+
for (let d = 1; d <= dim; d++)
|
|
190
|
+
if (isOpen(formatDate(year, month, d)))
|
|
191
|
+
list.push(d);
|
|
192
|
+
days = Object.freeze(list);
|
|
193
|
+
if (cache.size >= 240)
|
|
194
|
+
cache.clear();
|
|
195
|
+
cache.set(key, days);
|
|
196
|
+
}
|
|
197
|
+
return days;
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
/** 作成済みのカレンダーか(createCalendar の結果の形。isBusinessDay と timeZone を持つ) */
|
|
201
|
+
const isCalendarLike = (v) => isObject(v) && typeof v["isBusinessDay"] === "function" && typeof v["timeZone"] === "string";
|
|
179
202
|
const ON_HOLIDAY_RULES = [
|
|
180
203
|
"under-holiday",
|
|
181
204
|
"over-holiday",
|
|
@@ -193,7 +216,7 @@ function checkSet(c, path, raw, valid, expected) {
|
|
|
193
216
|
return new Set(list);
|
|
194
217
|
}
|
|
195
218
|
/** @internal 繰り返しの規則を検証して取り込む */
|
|
196
|
-
export function compileRecurringRules(c, path, entries, valueOf, keys) {
|
|
219
|
+
export function compileRecurringRules(c, path, entries, valueOf, keys, context) {
|
|
197
220
|
if (entries === undefined)
|
|
198
221
|
return [];
|
|
199
222
|
if (!Array.isArray(entries)) {
|
|
@@ -215,13 +238,15 @@ export function compileRecurringRules(c, path, entries, valueOf, keys) {
|
|
|
215
238
|
const hasAfter = "dayAfterHoliday" in entry;
|
|
216
239
|
const hasBefore = "dayBeforeHoliday" in entry;
|
|
217
240
|
const hasHoliday = "holiday" in entry;
|
|
241
|
+
const hasBusinessDay = "businessDayOfMonth" in entry;
|
|
218
242
|
if (Number(hasWeekday) +
|
|
219
243
|
Number(hasDay) +
|
|
244
|
+
Number(hasBusinessDay) +
|
|
220
245
|
Number(hasAfter) +
|
|
221
246
|
Number(hasBefore) +
|
|
222
247
|
Number(hasHoliday) !==
|
|
223
248
|
1) {
|
|
224
|
-
c.error("invalid-option", p, 'must have exactly one of "weekday", "day", "dayAfterHoliday", "dayBeforeHoliday" or "holiday"');
|
|
249
|
+
c.error("invalid-option", p, 'must have exactly one of "weekday", "day", "businessDayOfMonth", "dayAfterHoliday", "dayBeforeHoliday" or "holiday"');
|
|
225
250
|
return;
|
|
226
251
|
}
|
|
227
252
|
let kinds = null;
|
|
@@ -229,6 +254,9 @@ export function compileRecurringRules(c, path, entries, valueOf, keys) {
|
|
|
229
254
|
let weekday = null;
|
|
230
255
|
let nth = null;
|
|
231
256
|
let days = null;
|
|
257
|
+
let businessDays = null;
|
|
258
|
+
let basis = null;
|
|
259
|
+
let basisCalendar = null;
|
|
232
260
|
let interval = null;
|
|
233
261
|
let anchor = null;
|
|
234
262
|
const hasInterval = "interval" in entry;
|
|
@@ -288,6 +316,32 @@ export function compileRecurringRules(c, path, entries, valueOf, keys) {
|
|
|
288
316
|
else
|
|
289
317
|
ok = false;
|
|
290
318
|
}
|
|
319
|
+
else if (hasBusinessDay) {
|
|
320
|
+
const set = checkSet(c, `${p}.businessDayOfMonth`, entry["businessDayOfMonth"], isBusinessDayOfMonth, "1-31 or -1 (last business day) to -31");
|
|
321
|
+
if (set)
|
|
322
|
+
businessDays = set;
|
|
323
|
+
else
|
|
324
|
+
ok = false;
|
|
325
|
+
if (entry["onHolidayRule"] === "after-holiday") {
|
|
326
|
+
c.error("invalid-option", `${p}.onHolidayRule`, '"after-holiday" cannot be used with "businessDayOfMonth" (shifting the nth business day has no meaning; use "under-holiday" or "over-holiday")');
|
|
327
|
+
ok = false;
|
|
328
|
+
}
|
|
329
|
+
if (context === "calendar") {
|
|
330
|
+
if ("calendar" in entry) {
|
|
331
|
+
c.error("invalid-option", `${p}.calendar`, '"calendar" cannot be used in a calendar (the counting basis is the calendar\'s own options without recurring rules)');
|
|
332
|
+
ok = false;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
else if (!isCalendarLike(entry["calendar"])) {
|
|
336
|
+
c.error("invalid-option", `${p}.calendar`, "must be a calendar made with createCalendar (the basis for counting business days; required in a schedule)");
|
|
337
|
+
ok = false;
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
const calendar = entry["calendar"];
|
|
341
|
+
basisCalendar = calendar;
|
|
342
|
+
basis = basisFromPredicate((date) => calendar.isBusinessDay(date));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
291
345
|
else {
|
|
292
346
|
const key = hasAfter ? "dayAfterHoliday" : hasBefore ? "dayBeforeHoliday" : "holiday";
|
|
293
347
|
if (entry[key] !== true) {
|
|
@@ -315,6 +369,10 @@ export function compileRecurringRules(c, path, entries, valueOf, keys) {
|
|
|
315
369
|
}
|
|
316
370
|
}
|
|
317
371
|
}
|
|
372
|
+
if (!hasBusinessDay && "calendar" in entry) {
|
|
373
|
+
c.error("invalid-option", `${p}.calendar`, '"calendar" can be used only with "businessDayOfMonth"');
|
|
374
|
+
ok = false;
|
|
375
|
+
}
|
|
318
376
|
if (!hasHoliday && "kinds" in entry) {
|
|
319
377
|
c.error("invalid-option", `${p}.kinds`, '"kinds" can be used only with "holiday"');
|
|
320
378
|
ok = false;
|
|
@@ -375,6 +433,28 @@ export function compileRecurringRules(c, path, entries, valueOf, keys) {
|
|
|
375
433
|
c.error("invalid-option", `${p}.onHolidayRule`, `must be one of ${ON_HOLIDAY_RULES.map((v) => `"${v}"`).join(", ")}`);
|
|
376
434
|
ok = false;
|
|
377
435
|
}
|
|
436
|
+
let skipWeekdays = null;
|
|
437
|
+
if ("skipWeekdays" in entry) {
|
|
438
|
+
if (entry["onHolidayRule"] !== "after-holiday" || !(hasWeekday || hasDay)) {
|
|
439
|
+
c.error("invalid-option", `${p}.skipWeekdays`, '"skipWeekdays" can be used only with onHolidayRule: "after-holiday" on a "weekday" or "day" rule');
|
|
440
|
+
ok = false;
|
|
441
|
+
}
|
|
442
|
+
else if (!Array.isArray(entry["skipWeekdays"])) {
|
|
443
|
+
c.error("invalid-option", `${p}.skipWeekdays`, "must be a non-empty array of weekdays (0 = Sunday to 6 = Saturday)");
|
|
444
|
+
ok = false;
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
const set = checkSet(c, `${p}.skipWeekdays`, entry["skipWeekdays"], isWeekday, "a weekday (0 = Sunday to 6 = Saturday)");
|
|
448
|
+
if (set === undefined)
|
|
449
|
+
ok = false;
|
|
450
|
+
else if (weekday !== null && set.has(weekday)) {
|
|
451
|
+
c.error("invalid-option", `${p}.skipWeekdays`, `cannot include the rule's own weekday ${weekday} (the rule would be shifted every time)`);
|
|
452
|
+
ok = false;
|
|
453
|
+
}
|
|
454
|
+
else
|
|
455
|
+
skipWeekdays = set;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
378
458
|
const period = checkPeriod(c, p, entry);
|
|
379
459
|
if (ok && period && value !== undefined) {
|
|
380
460
|
rules.push(Object.freeze({
|
|
@@ -384,6 +464,10 @@ export function compileRecurringRules(c, path, entries, valueOf, keys) {
|
|
|
384
464
|
interval,
|
|
385
465
|
anchor,
|
|
386
466
|
days,
|
|
467
|
+
businessDays,
|
|
468
|
+
basis,
|
|
469
|
+
calendar: basisCalendar,
|
|
470
|
+
skipWeekdays,
|
|
387
471
|
dayAfterHoliday: hasAfter,
|
|
388
472
|
dayBeforeHoliday: hasBefore,
|
|
389
473
|
holiday: hasHoliday,
|
|
@@ -401,14 +485,26 @@ export function compileRecurringRules(c, path, entries, valueOf, keys) {
|
|
|
401
485
|
return rules;
|
|
402
486
|
}
|
|
403
487
|
/**
|
|
404
|
-
* @internal 繰り返しの規則の形(曜日・第 n
|
|
488
|
+
* @internal 繰り返しの規則の形(曜日・第 n 回目・周期・日・第 n 営業日・祝日・祝日の翌日・祝前日・月)がその日に当てはまるか。
|
|
405
489
|
* 有効期間は考えない(規則の診断で、形どうしの包含を調べるために分けている)。
|
|
406
|
-
* 祝日に従属する規則は holidayOf で判定し、holidayOf
|
|
490
|
+
* 祝日に従属する規則は holidayOf で判定し、holidayOf を渡さなければ当てはまらない。
|
|
491
|
+
* 第 n 営業日の規則は、規則自身の基準(スケジュールの calendar)か basis で数え、どちらもなければ当てはまらない
|
|
407
492
|
*/
|
|
408
|
-
export function matchPattern(rule, date, holidayOf) {
|
|
493
|
+
export function matchPattern(rule, date, holidayOf, basis) {
|
|
409
494
|
const { year, month, day } = parseDate(date);
|
|
410
495
|
if (rule.months && !rule.months.has(month))
|
|
411
496
|
return false;
|
|
497
|
+
if (rule.businessDays) {
|
|
498
|
+
// 第 n 営業日: 規則自身の基準(スケジュールの calendar)か、呼び出し側の基準(カレンダーの「繰り返しの規則を除いた判定」)で数える
|
|
499
|
+
const b = rule.basis ?? basis;
|
|
500
|
+
if (b === undefined)
|
|
501
|
+
return false;
|
|
502
|
+
const days = b(year, month);
|
|
503
|
+
const index = days.indexOf(day);
|
|
504
|
+
if (index < 0)
|
|
505
|
+
return false;
|
|
506
|
+
return rule.businessDays.has(index + 1) || rule.businessDays.has(index - days.length);
|
|
507
|
+
}
|
|
412
508
|
if (rule.holiday || rule.dayAfterHoliday || rule.dayBeforeHoliday) {
|
|
413
509
|
if (holidayOf === undefined)
|
|
414
510
|
return false;
|
|
@@ -438,8 +534,18 @@ export function matchPattern(rule, date, holidayOf) {
|
|
|
438
534
|
return rule.days.has(day) || (rule.days.has(-1) && day === dim);
|
|
439
535
|
}
|
|
440
536
|
/** @internal 繰り返しの規則がその日に当てはまるか(有効期間を含む) */
|
|
441
|
-
export function matchRecurring(rule, date, holidayOf) {
|
|
442
|
-
return inPeriod(rule, toDayNumber(date)) && matchPattern(rule, date, holidayOf);
|
|
537
|
+
export function matchRecurring(rule, date, holidayOf, basis) {
|
|
538
|
+
return inPeriod(rule, toDayNumber(date)) && matchPattern(rule, date, holidayOf, basis);
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* @internal 第 n 営業日の規則が自分の設定を基準にする場合の基準を作る(繰り返しの規則をすべて除いた判定で営業日か)。
|
|
542
|
+
* 基準が要らない設定(第 n 営業日の規則がない、またはすべて calendar を持つ)では undefined
|
|
543
|
+
*/
|
|
544
|
+
export function createBasis(compiled, lookup) {
|
|
545
|
+
if (!compiled.recurring.some((r) => r.businessDays !== null && r.basis === null))
|
|
546
|
+
return undefined;
|
|
547
|
+
const resolve = createResolver({ ...compiled, recurring: [] }, lookup);
|
|
548
|
+
return basisFromPredicate((date) => resolve(date).day.value === true);
|
|
443
549
|
}
|
|
444
550
|
/** 規則の value を取り出す(value が必須の規則用) */
|
|
445
551
|
function requiredValue(c) {
|
|
@@ -705,6 +811,9 @@ export const RECURRING_RULE_KEYS = [
|
|
|
705
811
|
"keys",
|
|
706
812
|
"weekdays",
|
|
707
813
|
"months",
|
|
814
|
+
"businessDayOfMonth",
|
|
815
|
+
"calendar",
|
|
816
|
+
"skipWeekdays",
|
|
708
817
|
"name",
|
|
709
818
|
"onHolidayRule",
|
|
710
819
|
...PERIOD_KEYS,
|
|
@@ -744,7 +853,17 @@ export function compileScheduleOptions(c, options) {
|
|
|
744
853
|
}
|
|
745
854
|
const annual = compileAnnualRules(c, "annual", options["annual"], requiredValue(c), false);
|
|
746
855
|
const dates = compileDateRules(c, "dates", options["dates"], requiredValue(c), false);
|
|
747
|
-
const recurring = compileRecurringRules(c, "recurring", options["recurring"], requiredValue(c), [...RECURRING_RULE_KEYS, "value"]);
|
|
856
|
+
const recurring = compileRecurringRules(c, "recurring", options["recurring"], requiredValue(c), [...RECURRING_RULE_KEYS, "value"], "schedule");
|
|
857
|
+
// 第 n 営業日の規則の基準のカレンダーは、スケジュールと同じタイムゾーンであること(合成と同じ理由。瞬間の暦日が食い違うため)
|
|
858
|
+
for (const rule of recurring) {
|
|
859
|
+
if (rule.businessDays === null)
|
|
860
|
+
continue;
|
|
861
|
+
const entry = options["recurring"][Number(/\[(\d+)\]$/.exec(rule.path)[1])];
|
|
862
|
+
const tz = entry["calendar"].timeZone;
|
|
863
|
+
if (tz !== source.timeZone) {
|
|
864
|
+
c.error("time-zone-conflict", `${rule.path}.calendar`, `the calendar's time zone ${tz} differs from the schedule's ${source.timeZone}`);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
748
867
|
return { source, weekly, holiday, annual, dates, recurring };
|
|
749
868
|
}
|
|
750
869
|
/** 振替元をさかのぼる最大日数(祝日が続く日数の上限。2019 年の大型連休でも 10 日) */
|
|
@@ -766,6 +885,7 @@ export function matchAnnual(rule, date) {
|
|
|
766
885
|
export function createResolver(compiled, lookup) {
|
|
767
886
|
const { weekly, holiday: holidayRule, annual, dates, recurring } = compiled;
|
|
768
887
|
const hasShift = recurring.some((r) => r.onHolidayRule === "after-holiday");
|
|
888
|
+
const basis = createBasis(compiled, lookup);
|
|
769
889
|
/** 振替・祝日の規則の判断に使う祝日。提供元の対応年の外は祝日でないとみなす(振替元をさかのぼる途中で範囲を出る場合のため) */
|
|
770
890
|
function holidayOfSafe(date) {
|
|
771
891
|
try {
|
|
@@ -786,7 +906,8 @@ export function createResolver(compiled, lookup) {
|
|
|
786
906
|
function shiftedOrigin(rule, date) {
|
|
787
907
|
for (let k = 1; k <= MAX_HOLIDAY_RUN; k++) {
|
|
788
908
|
const prev = addDays(date, -k);
|
|
789
|
-
|
|
909
|
+
// 飛ばす日は祝日と、skipWeekdays の曜日。それ以外の日に達したら、振替元はない
|
|
910
|
+
if (!isHolidaySafe(prev) && !rule.skipWeekdays?.has(weekdayOf(prev)))
|
|
790
911
|
return undefined;
|
|
791
912
|
if (matchRecurring(rule, prev, holidayOfSafe))
|
|
792
913
|
return prev;
|
|
@@ -823,7 +944,7 @@ export function createResolver(compiled, lookup) {
|
|
|
823
944
|
return make(byAnnual.value, "annual", byAnnual, byAnnual.name);
|
|
824
945
|
if (holiday) {
|
|
825
946
|
// 3. 祝日の上("over-holiday"。祝日の規則 { holiday: true } もここ)
|
|
826
|
-
const over = recurring.find((r) => r.onHolidayRule === "over-holiday" && matchRecurring(r, date, holidayOfSafe));
|
|
947
|
+
const over = recurring.find((r) => r.onHolidayRule === "over-holiday" && matchRecurring(r, date, holidayOfSafe, basis));
|
|
827
948
|
if (over)
|
|
828
949
|
return make(over.value, "recurring", over, over.name);
|
|
829
950
|
// 4. 祝日
|
|
@@ -833,14 +954,17 @@ export function createResolver(compiled, lookup) {
|
|
|
833
954
|
}
|
|
834
955
|
// 祝日の扱いが「曜日どおり」なら、祝日の下("under-holiday")の規則も当てはまる。
|
|
835
956
|
// "after-holiday" の規則は祝日には当てはめない(振り替える)
|
|
836
|
-
const under = recurring.find((r) => r.onHolidayRule === "under-holiday" && matchRecurring(r, date, holidayOfSafe));
|
|
957
|
+
const under = recurring.find((r) => r.onHolidayRule === "under-holiday" && matchRecurring(r, date, holidayOfSafe, basis));
|
|
837
958
|
if (under)
|
|
838
959
|
return make(under.value, "recurring", under, under.name);
|
|
839
960
|
return make(weekly[weekday], "weekday", null, undefined);
|
|
840
961
|
}
|
|
841
962
|
// 5. 繰り返しの規則(祝日でない日。配列で先に書いたものが優先)。振替先も同じ段階で判定する
|
|
842
963
|
for (const rule of recurring) {
|
|
843
|
-
|
|
964
|
+
// skipWeekdays の曜日には当てはめない(祝日と同じく振り替える)
|
|
965
|
+
if (rule.skipWeekdays?.has(weekday))
|
|
966
|
+
continue;
|
|
967
|
+
if (matchRecurring(rule, date, holidayOfSafe, basis))
|
|
844
968
|
return make(rule.value, "recurring", rule, rule.name);
|
|
845
969
|
if (hasShift && rule.onHolidayRule === "after-holiday") {
|
|
846
970
|
const origin = shiftedOrigin(rule, date);
|
package/dist/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* 公開する名前と型は etc/api.md に記録し、pnpm check で意図しない変化を検出する。
|
|
9
9
|
*/
|
|
10
10
|
export { combineCalendars, createCalendar, createCalendarFromSchedule, type AdjustOptions, type AnnualDate, type AnnualRange, type BusinessDayConvention, type BusinessDayOptions, type Calendar, type CalendarOptions, type CombineOptions, type DateRange, type DayInfo, type DayReason, type DueDateOptions, type HolidayPolicy, type KnownDayReason, type SpecificDate, } from "./core/calendar.ts";
|
|
11
|
-
export { createSchedule, type AnnualRangeValue, type AnnualValue, type CivilDateTimeParts, type DateRangeValue, type DateValue, type DayAfterHolidayPattern, type DayBeforeHolidayPattern, type DayOfMonth, type DayOfMonthPattern, type EffectivePeriod, type HolidayLookup, type HolidayPattern, type HolidaySourceOptions, type HolidayValueRule, type KnownScheduleReason, type Month, type Nth, type OnHolidayRule, type RecurringPattern, type RecurringRule, type RecurringValue, type Schedule, type ScheduleDay, type ScheduleOptions, type ScheduleReason, type WeekdayPattern, } from "./core/schedule.ts";
|
|
11
|
+
export { createSchedule, type AnnualRangeValue, type AnnualValue, type BusinessDayOfMonth, type BusinessDayOfMonthPattern, type CivilDateTimeParts, type DateRangeValue, type DateValue, type DayAfterHolidayPattern, type DayBeforeHolidayPattern, type DayOfMonth, type DayOfMonthPattern, type EffectivePeriod, type HolidayLookup, type HolidayPattern, type HolidaySourceOptions, type HolidayValueRule, type KnownScheduleReason, type Month, type Nth, type OnHolidayRule, type RecurringPattern, type RecurringRule, type RecurringValue, type Schedule, type ScheduleDay, type ScheduleOptions, type ScheduleReason, type WeekdayPattern, } from "./core/schedule.ts";
|
|
12
12
|
export { addDays, diffDays, isValidDate, weekdayOf, type DateString, type Weekday, } from "./core/date.ts";
|
|
13
13
|
export { closingMonth, closingPeriod, monthOf, type MonthString } from "./core/month.ts";
|
|
14
14
|
export { isBizdaysError, type ErrorCode, type KnownErrorCode } from "./core/errors.ts";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bizdays-toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Holidays, business-day calendar and day-by-day schedules (e.g. store hours) with time zone support. Japanese holidays verified against the Cabinet Office data; UK bank holidays (England, Wales, Scotland, Northern Ireland) verified against GOV.UK; Singapore public holidays verified against data.gov.sg. / 祝日と営業日のカレンダー(タイムゾーン対応・日本の祝日は内閣府、英国の bank holidays は GOV.UK、シンガポールの祝日は data.gov.sg のデータで全日照合)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bank-holidays",
|