bizdays-toolkit 0.5.0 → 0.7.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 +32 -1
- package/README.md +73 -4
- package/dist/core/calendar.d.ts +50 -1
- package/dist/core/calendar.js +81 -1
- package/dist/core/errors.d.ts +2 -0
- package/dist/core/month.d.ts +50 -0
- package/dist/core/month.js +76 -0
- package/dist/core/schedule.js +38 -1
- package/dist/core/types.d.ts +7 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,35 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.7.0] - 2026-10-16
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- 祝日(`Holiday`)に、ほかの言語の名称 `names`(省略可能。キーは BCP 47 の言語タグの正規の形。例: `{ "zh-Hant": "…", ko: "…" }`)を追加した。利用者が実装した提供元が付けた `names` を、提供元のデータの検証で確かめ(誤りは `invalid-provider-data`)、複製・凍結して返す。同梱の日本の祝日は `names` を持たない(公式の出典がある日本語・英語の名称だけを同梱する)
|
|
12
|
+
|
|
13
|
+
### 開発
|
|
14
|
+
|
|
15
|
+
- `size-limits.json` の値を上げるときの理由は、コミットメッセージではなく CHANGELOG の「開発」に書く取り決めにした。`pnpm check`(`scripts/check-size.ts`)が前の版のタグの `size-limits.json` と比べ、値が上がっているのに記載がなければ失敗する(0.6.0 で理由をコミットメッセージに書き漏らしたため)。CI とリリースのワークフローは履歴とタグを取得して(`fetch-depth: 0`)この確認を行い、リリースのワークフローではタグを付けたコミットを 1 つ前の版のタグと比べる
|
|
16
|
+
|
|
17
|
+
## [0.6.0] - 2026-10-16
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- 日付の調整 `adjust(input, { convention })`(営業日カレンダー): 休業日を `"following"`(翌営業日)・`"preceding"`(前営業日)・`"modified-following"`(翌営業日。月が変わるなら前営業日)・`"modified-preceding"`(前営業日。月が変わるなら翌営業日)で調整する。月が変わるかは調整する前の日付の月と比べる
|
|
22
|
+
- 月の営業日(営業日カレンダー): `nthBusinessDayOfMonth(month, n)`(負なら月末から数える。-1 は最終営業日)・`firstBusinessDayOfMonth(month)`・`lastBusinessDayOfMonth(month)`
|
|
23
|
+
- 月の期日 `dueDate(month, { day, convention })`(営業日カレンダー): その月の `day` 日(-1 は末日。その月にない日は末日に読み替える)を `convention` で調整する
|
|
24
|
+
- 月の計算(ルート): `monthOf(dateOrMonth, offset?)`(n か月後の月 `"YYYY-MM"`)・`closingMonth(date, closingDay)`(締め日で区切ったときの月)
|
|
25
|
+
- 例外のコード `invalid-month`(月 `"YYYY-MM"` の誤り)
|
|
26
|
+
- 型: `BusinessDayConvention`・`AdjustOptions`・`DueDateOptions`・`MonthString`
|
|
27
|
+
|
|
28
|
+
### 開発
|
|
29
|
+
|
|
30
|
+
- `pnpm check`(`scripts/check-package.ts`)で、CHANGELOG の版の節の並び・日付と、末尾のリンク定義が節とそろっていることを確かめるようにした(0.5.0 でリンク定義の更新漏れがあったため)
|
|
31
|
+
- リリースのワークフローで、CHANGELOG の日付が `YYYY-MM-DD` のまま・見出しがないときの表示を分かりやすくした(止まる条件は変わらない)
|
|
32
|
+
- 月の計算と日付の調整の性質のテストを追加
|
|
33
|
+
- サイズの上限を上げた(`.` 16500・`./jp` 19500・`./diagnose` 22000。月の計算の追加。minify + gzip ではルートが約 7.2KB、日本の祝日込みで約 8.3KB)
|
|
34
|
+
- `docs/decisions.md` に 16 番目(日付の調整と月の期日の設計)を追加
|
|
35
|
+
|
|
7
36
|
## [0.5.0] - 2026-10-16
|
|
8
37
|
|
|
9
38
|
### Added
|
|
@@ -129,7 +158,9 @@
|
|
|
129
158
|
|
|
130
159
|
- 日本: 2026 年 2 月の官報(2027 年の春分・秋分の日)まで反映(`dataVersion: "2026-02"`、`confirmedThrough: 2027`)
|
|
131
160
|
|
|
132
|
-
[Unreleased]: https://github.com/inohan77/bizdays-toolkit/compare/v0.
|
|
161
|
+
[Unreleased]: https://github.com/inohan77/bizdays-toolkit/compare/v0.7.0...HEAD
|
|
162
|
+
[0.7.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.6.0...v0.7.0
|
|
163
|
+
[0.6.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.5.0...v0.6.0
|
|
133
164
|
[0.5.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.4.0...v0.5.0
|
|
134
165
|
[0.4.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.3.1...v0.4.0
|
|
135
166
|
[0.3.1]: https://github.com/inohan77/bizdays-toolkit/compare/v0.3.0...v0.3.1
|
package/README.md
CHANGED
|
@@ -20,9 +20,10 @@
|
|
|
20
20
|
- **繰り返しの規則**: 「第 3 水曜定休」「毎月末日は棚卸」「月曜休館(祝日の場合は翌平日)」のような規則を、祝日に当たったときの扱い(祝日に従う・祝日でも当てはめる・翌日に振り替える)とあわせて指定できます
|
|
21
21
|
- **日ごとの値(営業時間など)**: 「火曜定休、祝日は日曜の営業時間、元日は休業、大晦日は短縮営業」のように、曜日・祝日・特定日で変わる値を同じ規則で引けます
|
|
22
22
|
- **設定の診断**: `bizdays-toolkit/diagnose` の `diagnoseCalendar`・`diagnoseSchedule` が、決して使われない規則・取り除いても結果が変わらない規則・キーの打ち間違い・営業日が 1 日もない設定などを、例外を投げずに一覧で返します。テストに 1 行足せば、設定の変更で規則が死んでいないかを確かめられます
|
|
23
|
+
- **月の期日**: 「20 日締め翌月 10 日払い(休業日なら翌営業日)」「月末の最終営業日」のような期日を、金融で使われる 4 つの調整方法(following・preceding・modified-following・modified-preceding)で計算します
|
|
23
24
|
- **営業日の計算**: 定休日の曜日・毎年の休業日(年末年始のような年をまたぐ期間も可)・臨時休業日・臨時営業日と祝日を組み合わせ、「N 営業日後」「締め時刻を過ぎたら翌営業日」などを計算します
|
|
24
25
|
- **タイムゾーンを間違えない**: `Date` などの瞬間は、カレンダーのタイムゾーン(日本は `Asia/Tokyo`)で暦日に変換してから判定します。UTC で動くサーバーでも、日本時間 0〜9 時が前日扱いになりません
|
|
25
|
-
- **軽量・依存ゼロ**: minify + gzip で約
|
|
26
|
+
- **軽量・依存ゼロ**: minify + gzip で約 7.4KB(日本の祝日込みで約 8.5KB)。ESM・型定義同梱・`sideEffects: false` で、Node.js・ブラウザー・Edge ランタイムで動作します
|
|
26
27
|
- **他の国・地域に拡張できる**: 祝日の提供元はインターフェース(`HolidayProvider`)で差し替えられます。現在は日本のみ同梱しています
|
|
27
28
|
|
|
28
29
|
[English](#english)
|
|
@@ -177,6 +178,56 @@ const shibuya = createCalendar({ ...base, openDates: ["2026-12-27"] });
|
|
|
177
178
|
const yokohama = createCalendar({ ...base, closedRecurring: [{ weekday: 3, nth: 3 }] });
|
|
178
179
|
```
|
|
179
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
|
+
|
|
180
231
|
### 日ごとの値(営業時間など)
|
|
181
232
|
|
|
182
233
|
`createSchedule` は、曜日・祝日・特定日の規則から、日ごとに任意の値を返します。値の型は自由なので、営業時間のほか、受付枠や配送料の区分などにも使えます。
|
|
@@ -254,6 +305,23 @@ const myCountry: HolidayProvider = {
|
|
|
254
305
|
const calendar = createCalendar({ holidays: myCountry, closedWeekdays: [0, 6] });
|
|
255
306
|
```
|
|
256
307
|
|
|
308
|
+
祝日には、ほかの言語の名称を `names` で付けられます。キーは BCP 47 の言語タグの正規の形(`"zh-Hant"`・`"ko"` など)です。同梱の日本の祝日は `names` を持ちません(公式の出典がある日本語・英語だけを同梱しています)。必要な場合は、提供元を包んで付けます。
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
import { jp } from "bizdays-toolkit/jp";
|
|
312
|
+
|
|
313
|
+
const zhHant: Record<string, string> = { "2026-01-01": "元日" /* … 利用者が用意した訳 */ };
|
|
314
|
+
const jpWithNames = {
|
|
315
|
+
...jp,
|
|
316
|
+
holidaysInYear: (year: number) =>
|
|
317
|
+
jp.holidaysInYear(year).map((h) => {
|
|
318
|
+
const name = zhHant[h.date];
|
|
319
|
+
return name === undefined ? h : { ...h, names: { "zh-Hant": name } };
|
|
320
|
+
}),
|
|
321
|
+
};
|
|
322
|
+
createCalendar({ holidays: jpWithNames }).getHoliday("2026-01-01")?.names; // { "zh-Hant": "元日" }
|
|
323
|
+
```
|
|
324
|
+
|
|
257
325
|
### 対応年の外
|
|
258
326
|
|
|
259
327
|
祝日の提供元の対応年(日本は 1955〜2099 年)を外れた日付を判定すると `RangeError` になります。祝日がない日として扱いたい場合は `outOfRange: "ignore"` を指定してください。
|
|
@@ -274,7 +342,7 @@ try {
|
|
|
274
342
|
}
|
|
275
343
|
```
|
|
276
344
|
|
|
277
|
-
主なコード: `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`。将来の版でコードが増えることがあります。
|
|
345
|
+
主なコード: `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`。将来の版でコードが増えることがあります。
|
|
278
346
|
|
|
279
347
|
### 設定の診断
|
|
280
348
|
|
|
@@ -362,10 +430,11 @@ A business-day calendar that combines public holidays with company or store clos
|
|
|
362
430
|
- **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
|
|
363
431
|
- **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")
|
|
364
432
|
- **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
|
|
433
|
+
- **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`
|
|
365
434
|
- **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
|
|
366
435
|
- **Time-zone safe**: instants (`Date`, ISO strings with offsets, Temporal) are converted to a civil date in the calendar's time zone before evaluation
|
|
367
|
-
- **Lightweight, zero dependencies**: about
|
|
368
|
-
- **Extensible** through the `HolidayProvider` interface (only Japan is bundled for now)
|
|
436
|
+
- **Lightweight, zero dependencies**: about 7.4 KB min+gzip (about 8.5 KB with Japanese holidays), ESM, bundled types, `sideEffects: false`
|
|
437
|
+
- **Extensible** through the `HolidayProvider` interface (only Japan is bundled for now). Holidays can carry names in other languages via the optional `names` field (keys are canonical BCP 47 language tags); the bundled Japanese data includes only the officially sourced Japanese and English names
|
|
369
438
|
|
|
370
439
|
```ts
|
|
371
440
|
import { createCalendar } from "bizdays-toolkit";
|
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
|
}
|
package/dist/core/errors.d.ts
CHANGED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 月("YYYY-MM")の計算。締め日の月(closingMonth)や、n か月後の月(monthOf)。
|
|
3
|
+
*
|
|
4
|
+
* 営業日を考えない暦の計算なので、カレンダーなしで使える。営業日を考えた月の計算
|
|
5
|
+
* (月の第 n 営業日・期日)は営業日カレンダーのメソッドにある。
|
|
6
|
+
*/
|
|
7
|
+
import { type DateString } from "./date.ts";
|
|
8
|
+
/**
|
|
9
|
+
* 月を表す "YYYY-MM" 文字列。
|
|
10
|
+
* @public
|
|
11
|
+
*/
|
|
12
|
+
export type MonthString = string;
|
|
13
|
+
/** @internal 年と月 */
|
|
14
|
+
export interface YearMonth {
|
|
15
|
+
readonly year: number;
|
|
16
|
+
readonly month: number;
|
|
17
|
+
}
|
|
18
|
+
/** @internal "YYYY-MM" を年と月に分解する(誤りは code: "invalid-month") */
|
|
19
|
+
export declare function parseMonth(value: unknown): YearMonth;
|
|
20
|
+
/** @internal 年と月を "YYYY-MM" にする(年が 1000〜9999 の外なら code: "out-of-range") */
|
|
21
|
+
export declare function formatMonth({ year, month }: YearMonth): MonthString;
|
|
22
|
+
/** @internal n か月後(負なら前)の年と月 */
|
|
23
|
+
export declare function shiftMonth({ year, month }: YearMonth, offset: number): YearMonth;
|
|
24
|
+
/** @internal その月の d 日(その月にない日・-1 は末日に読み替える) */
|
|
25
|
+
export declare function dayOfMonth({ year, month }: YearMonth, day: number): DateString;
|
|
26
|
+
/** @internal 月の日(1〜31、-1 は末日)として正しいか */
|
|
27
|
+
export declare const isDayOfMonthValue: (v: unknown) => v is number;
|
|
28
|
+
/**
|
|
29
|
+
* 日付または月の、offset か月後(負なら前)の月 "YYYY-MM"。
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* monthOf("2026-10-16"); // "2026-10"
|
|
33
|
+
* monthOf("2026-10-16", 1); // "2026-11"
|
|
34
|
+
* monthOf("2026-01", -1); // "2025-12"
|
|
35
|
+
* @public
|
|
36
|
+
*/
|
|
37
|
+
export declare function monthOf(input: DateString | MonthString, offset?: number): MonthString;
|
|
38
|
+
/**
|
|
39
|
+
* 締め日で区切ったときに、日付が属する月 "YYYY-MM"。
|
|
40
|
+
*
|
|
41
|
+
* 締め日の翌日から次の締め日までを、次の締め日の月とする(例: 20 日締めなら 10 月 21 日〜11 月 20 日が 11 月分)。
|
|
42
|
+
* closingDay が -1 またはその月の日数以上なら、締め日はその月の末日(例: 31 日締めの 2 月は 28 日か 29 日締め)。
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* closingMonth("2026-10-20", 20); // "2026-10"
|
|
46
|
+
* closingMonth("2026-10-21", 20); // "2026-11"
|
|
47
|
+
* closingMonth("2026-10-31", -1); // "2026-10"(末日締め)
|
|
48
|
+
* @public
|
|
49
|
+
*/
|
|
50
|
+
export declare function closingMonth(date: DateString, closingDay: number): MonthString;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 月("YYYY-MM")の計算。締め日の月(closingMonth)や、n か月後の月(monthOf)。
|
|
3
|
+
*
|
|
4
|
+
* 営業日を考えない暦の計算なので、カレンダーなしで使える。営業日を考えた月の計算
|
|
5
|
+
* (月の第 n 営業日・期日)は営業日カレンダーのメソッドにある。
|
|
6
|
+
*/
|
|
7
|
+
import { daysInMonth, formatDate, parseDate } from "./date.js";
|
|
8
|
+
import { rangeError } from "./errors.js";
|
|
9
|
+
const MONTH_RE = /^(\d{4})-(\d{2})$/;
|
|
10
|
+
/** @internal "YYYY-MM" を年と月に分解する(誤りは code: "invalid-month") */
|
|
11
|
+
export function parseMonth(value) {
|
|
12
|
+
const m = typeof value === "string" ? MONTH_RE.exec(value) : null;
|
|
13
|
+
const year = Number(m?.[1]);
|
|
14
|
+
const month = Number(m?.[2]);
|
|
15
|
+
if (!m || year < 1000 || month < 1 || month > 12) {
|
|
16
|
+
throw rangeError("invalid-month", `Invalid month: ${JSON.stringify(value)} (expected YYYY-MM)`);
|
|
17
|
+
}
|
|
18
|
+
return { year, month };
|
|
19
|
+
}
|
|
20
|
+
/** @internal 年と月を "YYYY-MM" にする(年が 1000〜9999 の外なら code: "out-of-range") */
|
|
21
|
+
export function formatMonth({ year, month }) {
|
|
22
|
+
if (year < 1000 || year > 9999) {
|
|
23
|
+
throw rangeError("out-of-range", `Year out of range (1000-9999): ${year}`);
|
|
24
|
+
}
|
|
25
|
+
return `${year}-${String(month).padStart(2, "0")}`;
|
|
26
|
+
}
|
|
27
|
+
/** @internal n か月後(負なら前)の年と月 */
|
|
28
|
+
export function shiftMonth({ year, month }, offset) {
|
|
29
|
+
const index = year * 12 + (month - 1) + offset;
|
|
30
|
+
return { year: Math.floor(index / 12), month: (index % 12) + 1 };
|
|
31
|
+
}
|
|
32
|
+
/** @internal その月の d 日(その月にない日・-1 は末日に読み替える) */
|
|
33
|
+
export function dayOfMonth({ year, month }, day) {
|
|
34
|
+
const last = daysInMonth(year, month);
|
|
35
|
+
return formatDate(year, month, day === -1 || day > last ? last : day);
|
|
36
|
+
}
|
|
37
|
+
/** @internal 月の日(1〜31、-1 は末日)として正しいか */
|
|
38
|
+
export const isDayOfMonthValue = (v) => v === -1 || (Number.isInteger(v) && v >= 1 && v <= 31);
|
|
39
|
+
/**
|
|
40
|
+
* 日付または月の、offset か月後(負なら前)の月 "YYYY-MM"。
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* monthOf("2026-10-16"); // "2026-10"
|
|
44
|
+
* monthOf("2026-10-16", 1); // "2026-11"
|
|
45
|
+
* monthOf("2026-01", -1); // "2025-12"
|
|
46
|
+
* @public
|
|
47
|
+
*/
|
|
48
|
+
export function monthOf(input, offset = 0) {
|
|
49
|
+
if (!Number.isInteger(offset)) {
|
|
50
|
+
throw rangeError("invalid-argument", `offset must be an integer: ${offset}`);
|
|
51
|
+
}
|
|
52
|
+
const base = typeof input === "string" && input.split("-").length === 3
|
|
53
|
+
? parseDate(input)
|
|
54
|
+
: parseMonth(input);
|
|
55
|
+
return formatMonth(shiftMonth(base, offset));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 締め日で区切ったときに、日付が属する月 "YYYY-MM"。
|
|
59
|
+
*
|
|
60
|
+
* 締め日の翌日から次の締め日までを、次の締め日の月とする(例: 20 日締めなら 10 月 21 日〜11 月 20 日が 11 月分)。
|
|
61
|
+
* closingDay が -1 またはその月の日数以上なら、締め日はその月の末日(例: 31 日締めの 2 月は 28 日か 29 日締め)。
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* closingMonth("2026-10-20", 20); // "2026-10"
|
|
65
|
+
* closingMonth("2026-10-21", 20); // "2026-11"
|
|
66
|
+
* closingMonth("2026-10-31", -1); // "2026-10"(末日締め)
|
|
67
|
+
* @public
|
|
68
|
+
*/
|
|
69
|
+
export function closingMonth(date, closingDay) {
|
|
70
|
+
if (!isDayOfMonthValue(closingDay)) {
|
|
71
|
+
throw rangeError("invalid-argument", `closingDay must be an integer from 1 to 31, or -1 (last day): ${closingDay}`);
|
|
72
|
+
}
|
|
73
|
+
const { year, month, day } = parseDate(date);
|
|
74
|
+
const closing = closingDay === -1 ? daysInMonth(year, month) : closingDay;
|
|
75
|
+
return formatMonth(shiftMonth({ year, month }, day <= closing ? 0 : 1));
|
|
76
|
+
}
|
package/dist/core/schedule.js
CHANGED
|
@@ -373,9 +373,46 @@ function loadProviderYear(provider, year) {
|
|
|
373
373
|
if (typeof confirmed !== "boolean") {
|
|
374
374
|
throw providerDataError(provider, year, `item ${i} (${date}) must have boolean confirmed`);
|
|
375
375
|
}
|
|
376
|
-
|
|
376
|
+
const holiday = {
|
|
377
|
+
date,
|
|
378
|
+
name,
|
|
379
|
+
nameEn,
|
|
380
|
+
kind: kind,
|
|
381
|
+
confirmed,
|
|
382
|
+
};
|
|
383
|
+
if (h["names"] !== undefined) {
|
|
384
|
+
const problem = checkNames(h["names"]);
|
|
385
|
+
if (problem)
|
|
386
|
+
throw providerDataError(provider, year, `item ${i} (${date}) names: ${problem}`);
|
|
387
|
+
holiday.names = Object.freeze(Object.fromEntries(Object.entries(h["names"])));
|
|
388
|
+
}
|
|
389
|
+
return Object.freeze(holiday);
|
|
377
390
|
});
|
|
378
391
|
}
|
|
392
|
+
/** 祝日のほかの言語の名称を検証する(誤りがあれば理由を返す) */
|
|
393
|
+
function checkNames(names) {
|
|
394
|
+
// 普通のオブジェクト(別の実行環境で作られたものも可)。継承した項目を持つものは受け付けない
|
|
395
|
+
const proto = isObject(names) ? Object.getPrototypeOf(names) : undefined;
|
|
396
|
+
if (!isObject(names) || (proto !== null && Object.getPrototypeOf(proto) !== null)) {
|
|
397
|
+
return "must be a plain object of { [language tag]: name }";
|
|
398
|
+
}
|
|
399
|
+
for (const [tag, value] of Object.entries(names)) {
|
|
400
|
+
let canonical;
|
|
401
|
+
try {
|
|
402
|
+
canonical = Intl.getCanonicalLocales(tag)[0];
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
return `${JSON.stringify(tag)} is not a BCP 47 language tag`;
|
|
406
|
+
}
|
|
407
|
+
if (canonical !== tag) {
|
|
408
|
+
return `${JSON.stringify(tag)} must be written in canonical form ${JSON.stringify(canonical)}`;
|
|
409
|
+
}
|
|
410
|
+
if (typeof value !== "string" || value === "") {
|
|
411
|
+
return `${JSON.stringify(tag)} must be a non-empty string`;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return undefined;
|
|
415
|
+
}
|
|
379
416
|
/** @internal 祝日を調べる機能を作る(設定は検証済みであること) */
|
|
380
417
|
export function createHolidayLookup(source) {
|
|
381
418
|
const { providers, timeZone, outOfRange } = source;
|
package/dist/core/types.d.ts
CHANGED
|
@@ -10,6 +10,11 @@ export interface Holiday<Kind extends string = string> {
|
|
|
10
10
|
readonly name: string;
|
|
11
11
|
/** 英語の名称 */
|
|
12
12
|
readonly nameEn: string;
|
|
13
|
+
/**
|
|
14
|
+
* ほかの言語の名称(省略可能)。キーは BCP 47 の言語タグの正規の形(例: "zh-Hant", "ko")。
|
|
15
|
+
* 同梱の提供元(日本)は持たない。利用者が実装した提供元が付けた場合に、検証して複製・凍結して返す
|
|
16
|
+
*/
|
|
17
|
+
readonly names?: Readonly<Record<string, string>>;
|
|
13
18
|
/** 種別(国ごとに定義。日本は JapaneseHolidayKind) */
|
|
14
19
|
readonly kind: Kind;
|
|
15
20
|
/**
|
|
@@ -23,7 +28,8 @@ export interface Holiday<Kind extends string = string> {
|
|
|
23
28
|
*
|
|
24
29
|
* 他の国・地域の祝日はこのインターフェースを実装して createCalendar・createSchedule に渡す。
|
|
25
30
|
* holidaysInYear が返すデータは、年ごとに初めて使うときに検証され、誤りがあれば
|
|
26
|
-
* code: "invalid-provider-data"
|
|
31
|
+
* code: "invalid-provider-data" の例外になる。返した祝日は、契約の項目(date・name・nameEn・names・
|
|
32
|
+
* kind・confirmed)だけを持つ変更できないオブジェクトに複製される。
|
|
27
33
|
*
|
|
28
34
|
* 互換性の約束: このインターフェースに項目を足すときは、メジャー版以外では省略可能な項目に限る。
|
|
29
35
|
* @public
|
package/dist/index.d.ts
CHANGED
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
* 公開する API には TSDoc の @public(安定)または @beta(マイナー版で変わりうる)を付ける。
|
|
8
8
|
* 公開する名前と型は etc/api.md に記録し、pnpm check で意図しない変化を検出する。
|
|
9
9
|
*/
|
|
10
|
-
export { createCalendar, type AnnualDate, type AnnualRange, type BusinessDayOptions, type Calendar, type CalendarOptions, type DateRange, type DayInfo, type DayReason, type HolidayPolicy, type KnownDayReason, type SpecificDate, } from "./core/calendar.ts";
|
|
10
|
+
export { createCalendar, type AdjustOptions, type AnnualDate, type AnnualRange, type BusinessDayConvention, type BusinessDayOptions, type Calendar, type CalendarOptions, type DateRange, type DayInfo, type DayReason, type DueDateOptions, type HolidayPolicy, type KnownDayReason, type SpecificDate, } from "./core/calendar.ts";
|
|
11
11
|
export { createSchedule, type AnnualRangeValue, type AnnualValue, type CivilDateTimeParts, type DateRangeValue, type DateValue, type DayOfMonth, type DayOfMonthPattern, type HolidayLookup, 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
|
+
export { closingMonth, monthOf, type MonthString } from "./core/month.ts";
|
|
13
14
|
export { isBizdaysError, type ErrorCode, type KnownErrorCode } from "./core/errors.ts";
|
|
14
15
|
export { toDateString, toDateTimeParts, type DateInput, type TemporalDateLike, type TemporalInstantLike, } from "./core/input.ts";
|
|
15
16
|
export type { Holiday, HolidayProvider } from "./core/types.ts";
|
package/dist/index.js
CHANGED
|
@@ -10,5 +10,6 @@
|
|
|
10
10
|
export { createCalendar, } from "./core/calendar.js";
|
|
11
11
|
export { createSchedule, } from "./core/schedule.js";
|
|
12
12
|
export { addDays, diffDays, isValidDate, weekdayOf, } from "./core/date.js";
|
|
13
|
+
export { closingMonth, monthOf } from "./core/month.js";
|
|
13
14
|
export { isBizdaysError } from "./core/errors.js";
|
|
14
15
|
export { toDateString, toDateTimeParts, } from "./core/input.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bizdays-toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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. / 祝日と営業日のカレンダー(タイムゾーン対応・日本の祝日は内閣府データで全日照合)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bizdays",
|