bizdays-toolkit 0.4.0 → 0.6.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 +43 -2
- package/README.md +116 -4
- package/dist/core/calendar.d.ts +50 -1
- package/dist/core/calendar.js +81 -1
- package/dist/core/diagnose.d.ts +79 -0
- package/dist/core/diagnose.js +386 -0
- package/dist/core/errors.d.ts +16 -3
- package/dist/core/errors.js +23 -4
- package/dist/core/month.d.ts +50 -0
- package/dist/core/month.js +76 -0
- package/dist/core/schedule.d.ts +22 -6
- package/dist/core/schedule.js +33 -22
- package/dist/diagnose/index.d.ts +7 -0
- package/dist/diagnose/index.js +7 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -0
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,46 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
-
## [0.
|
|
7
|
+
## [0.6.0] - 2026-10-16
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- 日付の調整 `adjust(input, { convention })`(営業日カレンダー): 休業日を `"following"`(翌営業日)・`"preceding"`(前営業日)・`"modified-following"`(翌営業日。月が変わるなら前営業日)・`"modified-preceding"`(前営業日。月が変わるなら翌営業日)で調整する。月が変わるかは調整する前の日付の月と比べる
|
|
12
|
+
- 月の営業日(営業日カレンダー): `nthBusinessDayOfMonth(month, n)`(負なら月末から数える。-1 は最終営業日)・`firstBusinessDayOfMonth(month)`・`lastBusinessDayOfMonth(month)`
|
|
13
|
+
- 月の期日 `dueDate(month, { day, convention })`(営業日カレンダー): その月の `day` 日(-1 は末日。その月にない日は末日に読み替える)を `convention` で調整する
|
|
14
|
+
- 月の計算(ルート): `monthOf(dateOrMonth, offset?)`(n か月後の月 `"YYYY-MM"`)・`closingMonth(date, closingDay)`(締め日で区切ったときの月)
|
|
15
|
+
- 例外のコード `invalid-month`(月 `"YYYY-MM"` の誤り)
|
|
16
|
+
- 型: `BusinessDayConvention`・`AdjustOptions`・`DueDateOptions`・`MonthString`
|
|
17
|
+
|
|
18
|
+
### 開発
|
|
19
|
+
|
|
20
|
+
- `pnpm check`(`scripts/check-package.ts`)で、CHANGELOG の版の節の並び・日付と、末尾のリンク定義が節とそろっていることを確かめるようにした(0.5.0 でリンク定義の更新漏れがあったため)
|
|
21
|
+
- リリースのワークフローで、CHANGELOG の日付が `YYYY-MM-DD` のまま・見出しがないときの表示を分かりやすくした(止まる条件は変わらない)
|
|
22
|
+
- 月の計算と日付の調整の性質のテストを追加
|
|
23
|
+
- サイズの上限を上げた(`.` 16500・`./jp` 19500・`./diagnose` 22000。月の計算の追加。minify + gzip ではルートが約 7.2KB、日本の祝日込みで約 8.3KB)
|
|
24
|
+
- `docs/decisions.md` に 16 番目(日付の調整と月の期日の設計)を追加
|
|
25
|
+
|
|
26
|
+
## [0.5.0] - 2026-10-16
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- 規則の診断(サブパス `bizdays-toolkit/diagnose`): `diagnoseCalendar(options)`・`diagnoseSchedule(options)`。例外を投げず、`{ severity, code, path, message, relatedPath? }` の配列を返す
|
|
31
|
+
- `error`: 作成で例外になる誤り(`code` は例外と同じ)。作成時は最初の 1 件で止まるが、診断ではすべて返す。祝日の提供元が返すデータは、対応年のすべての年を調べる(`invalid-provider-data`)
|
|
32
|
+
- `warning`: `unknown-option`(知らないキー。近い既知のキーを候補に挙げる)・`shadowed`(同じ種類の先の規則に完全に含まれる。`relatedPath` に先の規則)・`never-matches`(当てはまる日がない繰り返しの規則)・`out-of-range`(個別の日付が祝日の提供元の対応年の外)・`no-business-day`(営業日が 1 日もない設定)
|
|
33
|
+
- `info`: `redundant`(取り除いても結果が変わらない規則)
|
|
34
|
+
- 型: `Diagnostic`・`DiagnosticSeverity`・`DiagnosticCode`・`KnownDiagnosticCode`
|
|
35
|
+
|
|
36
|
+
### Removed
|
|
37
|
+
|
|
38
|
+
- `HolidayRule` 型(0.4.0 で非推奨): `HolidayValueRule` を使う
|
|
39
|
+
|
|
40
|
+
### 開発
|
|
41
|
+
|
|
42
|
+
- 診断の性質のテスト(`test/diagnose-properties.test.ts`): 使われない・取り除いても変わらないと診断した規則を実際に取り除き、結果が変わらないことを乱数の設定で確かめる
|
|
43
|
+
- サイズの上限に `./diagnose` を追加
|
|
44
|
+
- `docs/decisions.md` に 15 番目(規則の診断の設計)を追加
|
|
45
|
+
|
|
46
|
+
## [0.4.0] - 2026-10-15
|
|
8
47
|
|
|
9
48
|
### Added
|
|
10
49
|
|
|
@@ -109,7 +148,9 @@
|
|
|
109
148
|
|
|
110
149
|
- 日本: 2026 年 2 月の官報(2027 年の春分・秋分の日)まで反映(`dataVersion: "2026-02"`、`confirmedThrough: 2027`)
|
|
111
150
|
|
|
112
|
-
[Unreleased]: https://github.com/inohan77/bizdays-toolkit/compare/v0.
|
|
151
|
+
[Unreleased]: https://github.com/inohan77/bizdays-toolkit/compare/v0.6.0...HEAD
|
|
152
|
+
[0.6.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.5.0...v0.6.0
|
|
153
|
+
[0.5.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.4.0...v0.5.0
|
|
113
154
|
[0.4.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.3.1...v0.4.0
|
|
114
155
|
[0.3.1]: https://github.com/inohan77/bizdays-toolkit/compare/v0.3.0...v0.3.1
|
|
115
156
|
[0.3.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.2.0...v0.3.0
|
package/README.md
CHANGED
|
@@ -19,9 +19,11 @@
|
|
|
19
19
|
- **日本の祝日を内閣府のデータで全日照合**: 1955 年〜官報公表済みの年まで、内閣府「国民の祝日」CSV のすべての日付と一致することをテストで確かめています(振替休日・国民の休日・皇室行事による休日・五輪特措法による移動を含む)
|
|
20
20
|
- **繰り返しの規則**: 「第 3 水曜定休」「毎月末日は棚卸」「月曜休館(祝日の場合は翌平日)」のような規則を、祝日に当たったときの扱い(祝日に従う・祝日でも当てはめる・翌日に振り替える)とあわせて指定できます
|
|
21
21
|
- **日ごとの値(営業時間など)**: 「火曜定休、祝日は日曜の営業時間、元日は休業、大晦日は短縮営業」のように、曜日・祝日・特定日で変わる値を同じ規則で引けます
|
|
22
|
+
- **設定の診断**: `bizdays-toolkit/diagnose` の `diagnoseCalendar`・`diagnoseSchedule` が、決して使われない規則・取り除いても結果が変わらない規則・キーの打ち間違い・営業日が 1 日もない設定などを、例外を投げずに一覧で返します。テストに 1 行足せば、設定の変更で規則が死んでいないかを確かめられます
|
|
23
|
+
- **月の期日**: 「20 日締め翌月 10 日払い(休業日なら翌営業日)」「月末の最終営業日」のような期日を、金融で使われる 4 つの調整方法(following・preceding・modified-following・modified-preceding)で計算します
|
|
22
24
|
- **営業日の計算**: 定休日の曜日・毎年の休業日(年末年始のような年をまたぐ期間も可)・臨時休業日・臨時営業日と祝日を組み合わせ、「N 営業日後」「締め時刻を過ぎたら翌営業日」などを計算します
|
|
23
25
|
- **タイムゾーンを間違えない**: `Date` などの瞬間は、カレンダーのタイムゾーン(日本は `Asia/Tokyo`)で暦日に変換してから判定します。UTC で動くサーバーでも、日本時間 0〜9 時が前日扱いになりません
|
|
24
|
-
- **軽量・依存ゼロ**: minify + gzip で約
|
|
26
|
+
- **軽量・依存ゼロ**: minify + gzip で約 7KB(日本の祝日込みで約 8.3KB)。ESM・型定義同梱・`sideEffects: false` で、Node.js・ブラウザー・Edge ランタイムで動作します
|
|
25
27
|
- **他の国・地域に拡張できる**: 祝日の提供元はインターフェース(`HolidayProvider`)で差し替えられます。現在は日本のみ同梱しています
|
|
26
28
|
|
|
27
29
|
[English](#english)
|
|
@@ -176,6 +178,56 @@ const shibuya = createCalendar({ ...base, openDates: ["2026-12-27"] });
|
|
|
176
178
|
const yokohama = createCalendar({ ...base, closedRecurring: [{ weekday: 3, nth: 3 }] });
|
|
177
179
|
```
|
|
178
180
|
|
|
181
|
+
### 月の期日と日付の調整
|
|
182
|
+
|
|
183
|
+
休業日に当たった日付を、指定した方法で営業日に調整します。月の第 n 営業日や、「毎月 n 日(休業日なら翌営業日)」のような期日も求められます。
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
import { closingMonth, createCalendar, monthOf } from "bizdays-toolkit";
|
|
187
|
+
import { jp } from "bizdays-toolkit/jp";
|
|
188
|
+
|
|
189
|
+
const calendar = createCalendar({
|
|
190
|
+
holidays: jp,
|
|
191
|
+
closedWeekdays: [0, 6],
|
|
192
|
+
closedAnnual: [{ from: "12-31", to: "01-03", name: "年末年始" }],
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
calendar.adjust("2026-10-31", { convention: "following" }); // "2026-11-02"(10/31 は土曜日)
|
|
196
|
+
calendar.adjust("2026-10-31", { convention: "modified-following" }); // "2026-10-30"(月が変わるので前へ)
|
|
197
|
+
|
|
198
|
+
calendar.firstBusinessDayOfMonth("2027-01"); // "2027-01-04"
|
|
199
|
+
calendar.lastBusinessDayOfMonth("2026-12"); // "2026-12-30"
|
|
200
|
+
calendar.nthBusinessDayOfMonth("2026-10", 5); // "2026-10-07"(-1 は最終、-2 は最後から 2 番目)
|
|
201
|
+
|
|
202
|
+
// 毎月 10 日払い(休業日なら翌営業日)
|
|
203
|
+
calendar.dueDate("2026-10", { day: 10, convention: "following" }); // "2026-10-13"(12 日はスポーツの日)
|
|
204
|
+
|
|
205
|
+
// 20 日締め・翌月末払い(休業日なら前営業日)
|
|
206
|
+
const billing = closingMonth("2026-10-21", 20); // "2026-11"(10/21〜11/20 が 11 月分)
|
|
207
|
+
calendar.dueDate(monthOf(billing, 1), { day: -1, convention: "preceding" }); // "2026-12-30"
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**調整の方法**(`convention`、必須)
|
|
211
|
+
|
|
212
|
+
| 方法 | 休業日のとき |
|
|
213
|
+
| ---------------------- | -------------------------------------- |
|
|
214
|
+
| `"following"` | 翌営業日 |
|
|
215
|
+
| `"preceding"` | 前営業日 |
|
|
216
|
+
| `"modified-following"` | 翌営業日。ただし月が変わるなら前営業日 |
|
|
217
|
+
| `"modified-preceding"` | 前営業日。ただし月が変わるなら翌営業日 |
|
|
218
|
+
|
|
219
|
+
- 営業日ならどの方法でもその日のままです
|
|
220
|
+
- 月が変わるかは、**調整する前の日付の月**と比べます。`dueDate` では、末日に読み替えた後の日付の月です(例: 2 月の 31 日払いは 2 月末日を基準にし、`"modified-following"` なら 2 月の中で調整します)
|
|
221
|
+
- `modified-…` で、同じ月に反対向きの営業日もない(その月が全部休業日)ときは、月が変わっても元の向きに調整します
|
|
222
|
+
- 「最も近い営業日」(nearest)はありません。前後が同じ距離のときの決め方が業種ごとに違うためです
|
|
223
|
+
|
|
224
|
+
**月の計算**(ルートの関数。営業日は考えません)
|
|
225
|
+
|
|
226
|
+
- `monthOf(dateOrMonth, offset?)`: 日付または月 `"YYYY-MM"` の、`offset` か月後(負なら前)の月
|
|
227
|
+
- `closingMonth(date, closingDay)`: 締め日(1〜31、`-1` は末日)で区切ったときに日付が属する月。締め日の翌日から次の締め日までを、次の締め日の月とします。締め日がその月にない日(31 日締めの 2 月など)は末日締めです
|
|
228
|
+
|
|
229
|
+
`dueDate` の `day` も、その月にない日(2 月 30 日など)は末日に読み替えます。繰り返しの規則(`{ day: 31 }`)はその月にない日に当てはまらないので、扱いが異なります。期日は「その月のどこか」に必ず決まる必要があるためです。
|
|
230
|
+
|
|
179
231
|
### 日ごとの値(営業時間など)
|
|
180
232
|
|
|
181
233
|
`createSchedule` は、曜日・祝日・特定日の規則から、日ごとに任意の値を返します。値の型は自由なので、営業時間のほか、受付枠や配送料の区分などにも使えます。
|
|
@@ -273,7 +325,65 @@ try {
|
|
|
273
325
|
}
|
|
274
326
|
```
|
|
275
327
|
|
|
276
|
-
主なコード: `invalid-date`・`invalid-month-day`・`invalid-time`・`invalid-range`・`invalid-weekday`・`invalid-time-zone`・`invalid-option`・`out-of-range`・`invalid-provider`・`invalid-provider-data`・`no-business-day`。将来の版でコードが増えることがあります。
|
|
328
|
+
主なコード: `invalid-date`・`invalid-month`・`invalid-month-day`・`invalid-time`・`invalid-range`・`invalid-weekday`・`invalid-time-zone`・`invalid-option`・`out-of-range`・`invalid-provider`・`invalid-provider-data`・`no-business-day`。将来の版でコードが増えることがあります。
|
|
329
|
+
|
|
330
|
+
### 設定の診断
|
|
331
|
+
|
|
332
|
+
`bizdays-toolkit/diagnose` の `diagnoseCalendar`・`diagnoseSchedule` は、設定を調べて問題を一覧で返します。例外は投げません(問題がなければ空の配列)。
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
import { diagnoseCalendar } from "bizdays-toolkit/diagnose";
|
|
336
|
+
import { jp } from "bizdays-toolkit/jp";
|
|
337
|
+
|
|
338
|
+
diagnoseCalendar({
|
|
339
|
+
holidays: jp,
|
|
340
|
+
closedWeekdays: [0],
|
|
341
|
+
closedRecurring: [
|
|
342
|
+
{ weekday: 3, nth: 3, name: "第 3 水曜定休" },
|
|
343
|
+
{ weekday: 3, nth: 3, months: [8], name: "8 月の第 3 水曜" }, // 先の規則に含まれる
|
|
344
|
+
{ weekday: 0, name: "日曜休み" }, // 定休日の曜日と同じ
|
|
345
|
+
],
|
|
346
|
+
closedDates: ["2100-01-05"], // 日本の祝日の対応年(1955〜2099 年)の外
|
|
347
|
+
closeAnnual: ["12-31"], // 打ち間違い
|
|
348
|
+
});
|
|
349
|
+
// [
|
|
350
|
+
// { severity: "warning", code: "unknown-option", path: "closeAnnual", message: '… (did you mean "closedAnnual"?)' },
|
|
351
|
+
// { severity: "warning", code: "out-of-range", path: "closedDates[0]", relatedPath: "holidays", message: "…" },
|
|
352
|
+
// { severity: "warning", code: "shadowed", path: "closedRecurring[1]", relatedPath: "closedRecurring[0]", message: "…" },
|
|
353
|
+
// { severity: "info", code: "redundant", path: "closedRecurring[2]", message: "…" },
|
|
354
|
+
// ]
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
| 重さ(`severity`) | `code` | 内容 |
|
|
358
|
+
| ------------------ | ----------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
359
|
+
| `error` | 例外と同じコード | `createCalendar`・`createSchedule` で例外になる誤り。作成時は最初の 1 件で止まりますが、診断ではすべて返します |
|
|
360
|
+
| `error` | `invalid-provider-data` | 祝日の提供元が返したデータの誤り。作成時は使った年だけを調べますが、診断では対応年のすべての年を調べます |
|
|
361
|
+
| `warning` | `unknown-option` | 知らないキー(作成時は無視されます)。近い既知のキーがあれば `message` に候補を挙げます |
|
|
362
|
+
| `warning` | `shadowed` | 同じ種類の先の規則(`relatedPath`)に完全に含まれ、決して使われない規則 |
|
|
363
|
+
| `warning` | `never-matches` | 当てはまる日がない繰り返しの規則(`{ day: 30, months: [2] }` など) |
|
|
364
|
+
| `warning` | `out-of-range` | 個別の日付が祝日の提供元(`relatedPath`)の対応年の外 |
|
|
365
|
+
| `warning` | `no-business-day` | 営業日が 1 日もない設定(営業日カレンダーのみ。営業日を探すと例外になります) |
|
|
366
|
+
| `info` | `redundant` | 取り除いても結果が変わらない規則(先の規則の組み合わせに覆われる、定休日の曜日と同じ、スケジュールで曜日の値と同じ値など) |
|
|
367
|
+
|
|
368
|
+
- 判定には `code` を使ってください。`message` は英語で、文言は変わることがあります。`code` は将来の版で増えることがあります
|
|
369
|
+
- `error` があるときは、規則どうしの診断(`shadowed`・`redundant` など)は行いません
|
|
370
|
+
- `shadowed` は同じ種類の規則どうし(個別の日付どうし、毎年の月日どうし、繰り返しの規則どうし)だけを調べます。判定の順番は固定なので、種類をまたいで使われない規則は `redundant` として見つかります。臨時営業日(`openDates`)と臨時休業日(`closedDates`)は同じ種類で、先の臨時営業日に含まれる臨時休業日は `shadowed` です
|
|
371
|
+
- 繰り返しの規則の `shadowed` は、祝日の扱い(`onHolidayRule`)も見ます。たとえば `"under-holiday"` の規則は、同じ日に当たる `"over-holiday"` の規則を覆いません(祝日には後の規則が当たるため)
|
|
372
|
+
- `redundant` は、規則を取り除いた結果と 1 日ずつ比べます。個別の日付はその期間を、毎年の月日と繰り返しの規則は個別の日付を除いた設定で、祝日の提供元の対応年(日本は 1955〜2099 年)を、提供元がなければ 2001〜2028 年(曜日と月日の組み合わせが一巡する 28 年)を調べます。スケジュールの値は、配列と普通のオブジェクトなら中身で、それ以外(`Date` など)は同じものかで比べます
|
|
373
|
+
- 日本の祝日を使う設定では、全日を調べるため数百ミリ秒かかります。リクエストごとではなく、テストや起動時の確認で使ってください。開発・テスト用の道具なので、アプリの本体のバンドルに入らないようサブパスに分けています
|
|
374
|
+
|
|
375
|
+
テストで設定を守る例:
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
import assert from "node:assert/strict";
|
|
379
|
+
import { test } from "node:test";
|
|
380
|
+
import { diagnoseCalendar } from "bizdays-toolkit/diagnose";
|
|
381
|
+
import { shippingOptions } from "../src/calendar.ts";
|
|
382
|
+
|
|
383
|
+
test("出荷カレンダーの設定に問題がない", () => {
|
|
384
|
+
assert.deepEqual(diagnoseCalendar(shippingOptions), []);
|
|
385
|
+
});
|
|
386
|
+
```
|
|
277
387
|
|
|
278
388
|
## バージョン番号とデータの更新
|
|
279
389
|
|
|
@@ -283,7 +393,7 @@ try {
|
|
|
283
393
|
- 祝日の判定結果を固定したい場合は、版を完全一致で指定してください(例: `"bizdays-toolkit": "0.1.0"`)
|
|
284
394
|
- 反映済みのデータは `dataVersion`(日本は反映済みの官報の年月)と `confirmedThrough` で確認できます
|
|
285
395
|
- 1.0.0 までは、マイナー版で破壊的変更を行うことがあります
|
|
286
|
-
- 判定の理由(`reason`)・祝日の種別(`kind
|
|
396
|
+
- 判定の理由(`reason`)・祝日の種別(`kind`)・エラーコード・診断のコードは、将来の版で値が増えることがあります。値を列挙して分岐するときは「その他」も扱ってください([RELEASING.md](./RELEASING.md) の「互換性の約束」)
|
|
287
397
|
|
|
288
398
|
## 出典
|
|
289
399
|
|
|
@@ -302,9 +412,11 @@ A business-day calendar that combines public holidays with company or store clos
|
|
|
302
412
|
- **Japanese holidays verified day by day** against the Cabinet Office CSV, from 1955 through the latest year officially announced (substitute holidays, citizens' holidays, imperial ceremonies, and the Olympic moves included)
|
|
303
413
|
- **Day-by-day schedules**: `createSchedule` returns any value per day (e.g. store hours) from weekly values, a holiday rule (`"weekday"`, `{ sameAs: 0 }`, or `{ value }`), annual dates and specific dates
|
|
304
414
|
- **Recurring rules**: weekly, nth-weekday-of-month (`{ weekday: 3, nth: 3 }`) and day-of-month (`{ day: -1 }`) rules, with per-rule holiday handling (`onHolidayRule`: `"under-holiday"`, `"over-holiday"`, or `"after-holiday"` to move the rule to the next non-holiday, e.g. "closed on Mondays; if Monday is a holiday, closed on the next weekday")
|
|
415
|
+
- **Option diagnostics**: `diagnoseCalendar` / `diagnoseSchedule` from `bizdays-toolkit/diagnose` return a list of problems without throwing: rules that can never apply (`shadowed`, `never-matches`), rules whose removal changes nothing (`redundant`), typos in option keys with a suggestion (`unknown-option`), dates outside the holiday provider's years (`out-of-range`), and calendars with no business day. Use `code` to branch; `message` is for humans
|
|
416
|
+
- **Month-based due dates**: `adjust` with `following` / `preceding` / `modified-following` / `modified-preceding`, the nth / first / last business day of a month, `dueDate(month, { day, convention })`, and the calendar helpers `closingMonth` and `monthOf`
|
|
305
417
|
- **Business-day arithmetic**: closed weekdays, annual closures (including ranges across the new year), specific closed/open dates, holiday policy, `addBusinessDays`, cut-off times, and more
|
|
306
418
|
- **Time-zone safe**: instants (`Date`, ISO strings with offsets, Temporal) are converted to a civil date in the calendar's time zone before evaluation
|
|
307
|
-
- **Lightweight, zero dependencies**: about
|
|
419
|
+
- **Lightweight, zero dependencies**: about 7 KB min+gzip (about 8.3 KB with Japanese holidays), ESM, bundled types, `sideEffects: false`
|
|
308
420
|
- **Extensible** through the `HolidayProvider` interface (only Japan is bundled for now)
|
|
309
421
|
|
|
310
422
|
```ts
|
package/dist/core/calendar.d.ts
CHANGED
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
import { type DateString, type Weekday } from "./date.ts";
|
|
20
20
|
import { IssueCollector } from "./errors.ts";
|
|
21
21
|
import { type DateInput } from "./input.ts";
|
|
22
|
-
import { type
|
|
22
|
+
import { type MonthString } from "./month.ts";
|
|
23
|
+
import { type CompiledSchedule, type DayOfMonth, type RecurringRule, type HolidayLookup, type HolidaySourceOptions } from "./schedule.ts";
|
|
23
24
|
import type { Holiday } from "./types.ts";
|
|
24
25
|
/**
|
|
25
26
|
* 祝日の扱い。
|
|
@@ -124,6 +125,35 @@ export interface BusinessDayOptions {
|
|
|
124
125
|
*/
|
|
125
126
|
readonly cutoff?: string;
|
|
126
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* 休業日に当たった日付の調整の方法(営業日の慣行)。
|
|
130
|
+
* - "following": 翌営業日
|
|
131
|
+
* - "preceding": 前営業日
|
|
132
|
+
* - "modified-following": 翌営業日。ただし月が変わるなら前営業日
|
|
133
|
+
* - "modified-preceding": 前営業日。ただし月が変わるなら翌営業日
|
|
134
|
+
*
|
|
135
|
+
* 月が変わるかは、調整する前の日付の月と比べる。
|
|
136
|
+
* @public
|
|
137
|
+
*/
|
|
138
|
+
export type BusinessDayConvention = "following" | "preceding" | "modified-following" | "modified-preceding";
|
|
139
|
+
/**
|
|
140
|
+
* 日付の調整のオプション。
|
|
141
|
+
* @public
|
|
142
|
+
*/
|
|
143
|
+
export interface AdjustOptions {
|
|
144
|
+
/** 調整の方法 */
|
|
145
|
+
readonly convention: BusinessDayConvention;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* 月の期日のオプション。
|
|
149
|
+
* @public
|
|
150
|
+
*/
|
|
151
|
+
export interface DueDateOptions {
|
|
152
|
+
/** 期日の日(1〜31、-1 は末日)。その月にない日(2 月 30 日など)は末日に読み替える */
|
|
153
|
+
readonly day: DayOfMonth;
|
|
154
|
+
/** 期日が休業日のときの調整の方法 */
|
|
155
|
+
readonly convention: BusinessDayConvention;
|
|
156
|
+
}
|
|
127
157
|
/**
|
|
128
158
|
* 営業日カレンダー。
|
|
129
159
|
* @public
|
|
@@ -150,6 +180,25 @@ export interface Calendar<Kind extends string = string> extends HolidayLookup<Ki
|
|
|
150
180
|
businessDaysBetween(from: DateInput, to: DateInput): number;
|
|
151
181
|
/** 期間内の営業日(両端を含む) */
|
|
152
182
|
listBusinessDays(from: DateInput, to: DateInput): readonly DateString[];
|
|
183
|
+
/**
|
|
184
|
+
* 営業日ならその日、休業日なら convention に従って調整した営業日。
|
|
185
|
+
* 月が変わるか(modified-…)は、調整する前の日付の月と比べる
|
|
186
|
+
*/
|
|
187
|
+
adjust(input: DateInput, options: AdjustOptions): DateString;
|
|
188
|
+
/**
|
|
189
|
+
* 月 "YYYY-MM" の第 n 営業日。n が負なら月末から数える(-1 は最終営業日)。
|
|
190
|
+
* その月の営業日が足りなければ code: "no-business-day" の Error
|
|
191
|
+
*/
|
|
192
|
+
nthBusinessDayOfMonth(month: MonthString, n: number): DateString;
|
|
193
|
+
/** 月 "YYYY-MM" の最初の営業日(nthBusinessDayOfMonth(month, 1)) */
|
|
194
|
+
firstBusinessDayOfMonth(month: MonthString): DateString;
|
|
195
|
+
/** 月 "YYYY-MM" の最後の営業日(nthBusinessDayOfMonth(month, -1)) */
|
|
196
|
+
lastBusinessDayOfMonth(month: MonthString): DateString;
|
|
197
|
+
/**
|
|
198
|
+
* 月 "YYYY-MM" の day 日を期日とし、休業日なら convention で調整した日。
|
|
199
|
+
* その月にない日は末日に読み替えてから調整する(例: 2 月の 30 日払いは 2 月末日)
|
|
200
|
+
*/
|
|
201
|
+
dueDate(month: MonthString, options: DueDateOptions): DateString;
|
|
153
202
|
}
|
|
154
203
|
interface CompiledCalendar<Kind extends string> {
|
|
155
204
|
readonly schedule: CompiledSchedule<boolean, Kind>;
|
package/dist/core/calendar.js
CHANGED
|
@@ -16,9 +16,10 @@
|
|
|
16
16
|
*
|
|
17
17
|
* 内部では、営業日かどうか(真偽値)を返すスケジュール(createSchedule)として作っている。
|
|
18
18
|
*/
|
|
19
|
-
import { addDays, fromDayNumber, toDayNumber } from "./date.js";
|
|
19
|
+
import { addDays, daysInMonth, formatDate, fromDayNumber, toDayNumber, } from "./date.js";
|
|
20
20
|
import { checkUnknownKeys, IssueCollector, plainError, rangeError } from "./errors.js";
|
|
21
21
|
import { parseTimeOfDay, toCivil } from "./input.js";
|
|
22
|
+
import { dayOfMonth, isDayOfMonthValue, parseMonth } from "./month.js";
|
|
22
23
|
import { buildSchedule, compileAnnualRules, compileDateRules, compileHolidaySource, compileRecurringRules, isWeekday, RECURRING_RULE_KEYS, } from "./schedule.js";
|
|
23
24
|
const CALENDAR_OPTION_KEYS = [
|
|
24
25
|
"holidays",
|
|
@@ -156,6 +157,74 @@ export function createCalendar(options = {}) {
|
|
|
156
157
|
count++;
|
|
157
158
|
return count;
|
|
158
159
|
}
|
|
160
|
+
const CONVENTIONS = [
|
|
161
|
+
"following",
|
|
162
|
+
"preceding",
|
|
163
|
+
"modified-following",
|
|
164
|
+
"modified-preceding",
|
|
165
|
+
];
|
|
166
|
+
function adjustDate(date, options) {
|
|
167
|
+
const convention = options?.convention;
|
|
168
|
+
if (!CONVENTIONS.includes(convention)) {
|
|
169
|
+
throw rangeError("invalid-argument", `convention must be one of ${CONVENTIONS.map((v) => `"${v}"`).join(", ")}`);
|
|
170
|
+
}
|
|
171
|
+
if (isOpen(date))
|
|
172
|
+
return date;
|
|
173
|
+
const month = date.slice(0, 7);
|
|
174
|
+
switch (convention) {
|
|
175
|
+
case "following":
|
|
176
|
+
return search(date, 1);
|
|
177
|
+
case "preceding":
|
|
178
|
+
return search(date, -1);
|
|
179
|
+
case "modified-following": {
|
|
180
|
+
const next = search(date, 1);
|
|
181
|
+
if (next.startsWith(month))
|
|
182
|
+
return next;
|
|
183
|
+
// 同じ月に前営業日がなければ、月が変わっても翌営業日にする
|
|
184
|
+
const previous = searchWithin(date, -1, month);
|
|
185
|
+
return previous ?? next;
|
|
186
|
+
}
|
|
187
|
+
case "modified-preceding": {
|
|
188
|
+
const previous = search(date, -1);
|
|
189
|
+
if (previous.startsWith(month))
|
|
190
|
+
return previous;
|
|
191
|
+
const next = searchWithin(date, 1, month);
|
|
192
|
+
return next ?? previous;
|
|
193
|
+
}
|
|
194
|
+
default:
|
|
195
|
+
throw rangeError("invalid-argument", `Unknown convention: ${String(convention)}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/** 同じ月の中だけで営業日を探す(なければ undefined) */
|
|
199
|
+
function searchWithin(start, step, month) {
|
|
200
|
+
for (let date = start; date.startsWith(month); date = addDays(date, step)) {
|
|
201
|
+
if (isOpen(date))
|
|
202
|
+
return date;
|
|
203
|
+
}
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
/** 月の営業日(月初から順) */
|
|
207
|
+
function businessDaysOfMonth(month) {
|
|
208
|
+
const { year, month: m } = parseMonth(month);
|
|
209
|
+
const result = [];
|
|
210
|
+
for (let d = 1; d <= daysInMonth(year, m); d++) {
|
|
211
|
+
const date = formatDate(year, m, d);
|
|
212
|
+
if (isOpen(date))
|
|
213
|
+
result.push(date);
|
|
214
|
+
}
|
|
215
|
+
return result;
|
|
216
|
+
}
|
|
217
|
+
function nthOfMonth(month, n) {
|
|
218
|
+
if (!Number.isInteger(n) || n === 0) {
|
|
219
|
+
throw rangeError("invalid-argument", `n must be a non-zero integer: ${n}`);
|
|
220
|
+
}
|
|
221
|
+
const days = businessDaysOfMonth(month);
|
|
222
|
+
const found = n > 0 ? days[n - 1] : days[days.length + n];
|
|
223
|
+
if (found === undefined) {
|
|
224
|
+
throw plainError("no-business-day", `${month} has ${days.length} business day(s); requested ${n > 0 ? "" : "last "}#${Math.abs(n)}`);
|
|
225
|
+
}
|
|
226
|
+
return found;
|
|
227
|
+
}
|
|
159
228
|
const calendar = {
|
|
160
229
|
timeZone: schedule.timeZone,
|
|
161
230
|
providers: schedule.providers,
|
|
@@ -211,6 +280,17 @@ export function createCalendar(options = {}) {
|
|
|
211
280
|
}
|
|
212
281
|
return Object.freeze(result);
|
|
213
282
|
},
|
|
283
|
+
adjust: (input, options) => adjustDate(dateOf(input), options),
|
|
284
|
+
nthBusinessDayOfMonth: nthOfMonth,
|
|
285
|
+
firstBusinessDayOfMonth: (month) => nthOfMonth(month, 1),
|
|
286
|
+
lastBusinessDayOfMonth: (month) => nthOfMonth(month, -1),
|
|
287
|
+
dueDate(month, options) {
|
|
288
|
+
const day = options?.day;
|
|
289
|
+
if (!isDayOfMonthValue(day)) {
|
|
290
|
+
throw rangeError("invalid-argument", `day must be an integer from 1 to 31, or -1 (last day): ${String(day)}`);
|
|
291
|
+
}
|
|
292
|
+
return adjustDate(dayOfMonth(parseMonth(month), day), options);
|
|
293
|
+
},
|
|
214
294
|
};
|
|
215
295
|
return Object.freeze(calendar);
|
|
216
296
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 規則の診断(diagnoseCalendar・diagnoseSchedule)。
|
|
3
|
+
*
|
|
4
|
+
* 設定を作るときと同じ検証(IssueCollector)で誤りと知らないキーを集め、誤りがなければ
|
|
5
|
+
* 規則どうしの関係を調べる。例外は投げず、見つけた問題を配列で返す。
|
|
6
|
+
*
|
|
7
|
+
* - error … createCalendar・createSchedule で例外になる誤り(code は例外と同じ)
|
|
8
|
+
* - warning … 作れるが、意図と違う可能性が高いもの
|
|
9
|
+
* unknown-option … 知らないキー(作成時は無視される)。近い既知のキーがあれば message に候補を挙げる
|
|
10
|
+
* shadowed … 同じ種類の先の規則に完全に覆われ、決して使われない(relatedPath に先の規則)
|
|
11
|
+
* never-matches … 当てはまる日がない繰り返しの規則(2 月 30 日など)
|
|
12
|
+
* out-of-range … 個別の日付が祝日の提供元の対応年の外にある(relatedPath に提供元)
|
|
13
|
+
* no-business-day … 営業日が 1 日もない設定(営業日カレンダーのみ)
|
|
14
|
+
* - info … redundant: 取り除いても結果が変わらない規則
|
|
15
|
+
*
|
|
16
|
+
* 判定の順番(個別の日付 → 毎年の月日 → 繰り返し(over)→ 祝日 → 繰り返し(under・振替先)→ 曜日)は
|
|
17
|
+
* 固定なので、shadowed は同じ種類の規則どうしに限って調べる。種類をまたいで使われない規則は
|
|
18
|
+
* redundant(取り除いても結果が変わらない)として見つかる。
|
|
19
|
+
*/
|
|
20
|
+
import { type CalendarOptions } from "./calendar.ts";
|
|
21
|
+
import { type KnownErrorCode } from "./errors.ts";
|
|
22
|
+
import { type ScheduleOptions } from "./schedule.ts";
|
|
23
|
+
/**
|
|
24
|
+
* 診断の重さ。
|
|
25
|
+
* - "error": 作成すると例外になる誤り
|
|
26
|
+
* - "warning": 作れるが、意図と違う可能性が高い
|
|
27
|
+
* - "info": 結果には影響しないが、整理できる
|
|
28
|
+
*
|
|
29
|
+
* 出力の列挙だが、重さの段階は増やさないため閉じた型にしている(docs/decisions.md の 15)。
|
|
30
|
+
* @public
|
|
31
|
+
*/
|
|
32
|
+
export type DiagnosticSeverity = "error" | "warning" | "info";
|
|
33
|
+
/**
|
|
34
|
+
* 既知の診断コード(増えることがある)。error の code は例外の code(KnownErrorCode)と同じ。
|
|
35
|
+
* @public
|
|
36
|
+
*/
|
|
37
|
+
export type KnownDiagnosticCode = KnownErrorCode
|
|
38
|
+
/** 知らないキー(作成時は無視される) */
|
|
39
|
+
| "unknown-option"
|
|
40
|
+
/** 同じ種類の先の規則に完全に覆われ、決して使われない */
|
|
41
|
+
| "shadowed"
|
|
42
|
+
/** 当てはまる日がない繰り返しの規則 */
|
|
43
|
+
| "never-matches"
|
|
44
|
+
/** 取り除いても結果が変わらない */
|
|
45
|
+
| "redundant";
|
|
46
|
+
/**
|
|
47
|
+
* 診断コード。将来の版で値が増えることがあるので、分岐では「その他」も扱うこと。
|
|
48
|
+
* @public
|
|
49
|
+
*/
|
|
50
|
+
export type DiagnosticCode = KnownDiagnosticCode | (string & {});
|
|
51
|
+
/**
|
|
52
|
+
* 診断の結果 1 件。message は英語で、文言は API に含まない(判定には code を使う)。
|
|
53
|
+
* @public
|
|
54
|
+
*/
|
|
55
|
+
export interface Diagnostic {
|
|
56
|
+
readonly severity: DiagnosticSeverity;
|
|
57
|
+
readonly code: DiagnosticCode;
|
|
58
|
+
/** 設定の中の位置(例: "closedRecurring[1]"・"closedDates[0].date")。設定全体なら "" */
|
|
59
|
+
readonly path: string;
|
|
60
|
+
readonly message: string;
|
|
61
|
+
/** 関係する別の位置(shadowed では覆っている先の規則、out-of-range では祝日の提供元) */
|
|
62
|
+
readonly relatedPath?: string;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 営業日カレンダーの設定を診断する。例外は投げず、見つけた問題を返す(問題がなければ空の配列)。
|
|
66
|
+
*
|
|
67
|
+
* error があるときは、規則どうしの診断(shadowed・redundant など)は行わない。
|
|
68
|
+
* JavaScript から渡された型の合わない設定も受け付ける。
|
|
69
|
+
* @public
|
|
70
|
+
*/
|
|
71
|
+
export declare function diagnoseCalendar(options?: CalendarOptions | object): readonly Diagnostic[];
|
|
72
|
+
/**
|
|
73
|
+
* スケジュールの設定を診断する。例外は投げず、見つけた問題を返す(問題がなければ空の配列)。
|
|
74
|
+
*
|
|
75
|
+
* 値が同じかは、配列と普通のオブジェクトは中身で、それ以外は Object.is で比べる。
|
|
76
|
+
* error があるときは、規則どうしの診断(shadowed・redundant など)は行わない。
|
|
77
|
+
* @public
|
|
78
|
+
*/
|
|
79
|
+
export declare function diagnoseSchedule(options: ScheduleOptions<unknown> | object): readonly Diagnostic[];
|