bizdays-toolkit 0.3.1 → 0.4.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 +21 -1
- package/README.md +95 -6
- package/dist/core/calendar.d.ts +19 -5
- package/dist/core/calendar.js +20 -4
- package/dist/core/schedule.d.ts +112 -8
- package/dist/core/schedule.js +190 -9
- package/dist/index.d.ts +1 -1
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,25 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.4.0] - 2026-10-16
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- 繰り返しの規則: 毎週(`{ weekday }`)・毎月の第 n 曜日(`{ weekday, nth }`。`nth` は 1〜5 と -1(最終))・毎月の日(`{ day }`。`day` は 1〜31 と -1(末日))。どれにも月の絞り込み(`months`)を付けられる。営業日カレンダーの `closedRecurring`・`openRecurring`、スケジュールの `recurring`
|
|
12
|
+
- 祝日に当たったときの扱いを規則ごとに選ぶ `onHolidayRule`: `"under-holiday"`(祝日の扱いに従う。既定)・`"over-holiday"`(祝日でも当てはめる)・`"after-holiday"`(祝日でない翌日に振り替える。「月曜休館(祝日の場合は翌平日)」など)
|
|
13
|
+
- `dayInfo()` の `shiftedFrom`: 振替で決まった日の、振替の元の日付
|
|
14
|
+
- 判定の理由に `"recurring"`(スケジュール)・`"open-recurring"`・`"closed-recurring"`(営業日カレンダー)を追加
|
|
15
|
+
- 型: `RecurringRule`・`RecurringValue`・`RecurringPattern`・`WeekdayPattern`・`DayOfMonthPattern`・`OnHolidayRule`・`Nth`・`DayOfMonth`・`Month`・`HolidayValueRule`
|
|
16
|
+
|
|
17
|
+
### Deprecated
|
|
18
|
+
|
|
19
|
+
- `HolidayRule` 型: `HolidayValueRule` に改名した(繰り返しの規則の `onHolidayRule` と紛れないため)。旧名は同じ型の別名として残し、次以降のマイナー版で削除する
|
|
20
|
+
|
|
21
|
+
### 開発
|
|
22
|
+
|
|
23
|
+
- 性能の計測(`pnpm bench`、依存なし)を追加
|
|
24
|
+
- README に繰り返しの規則の節と、よくある設定の例(国民の休日を営業日として扱う、共通の設定から店舗ごとのカレンダーを作る)を追加。`docs/decisions.md` に 12〜14 番目(判定の順番を固定する理由、「第 n 何曜日」の定義、振替先の定義)を追加
|
|
25
|
+
|
|
7
26
|
## [0.3.1] - 2026-10-15
|
|
8
27
|
|
|
9
28
|
ライブラリのコード・公開する API・祝日データに変更はありません。開発の検査を強めた版です。
|
|
@@ -90,7 +109,8 @@
|
|
|
90
109
|
|
|
91
110
|
- 日本: 2026 年 2 月の官報(2027 年の春分・秋分の日)まで反映(`dataVersion: "2026-02"`、`confirmedThrough: 2027`)
|
|
92
111
|
|
|
93
|
-
[Unreleased]: https://github.com/inohan77/bizdays-toolkit/compare/v0.
|
|
112
|
+
[Unreleased]: https://github.com/inohan77/bizdays-toolkit/compare/v0.4.0...HEAD
|
|
113
|
+
[0.4.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.3.1...v0.4.0
|
|
94
114
|
[0.3.1]: https://github.com/inohan77/bizdays-toolkit/compare/v0.3.0...v0.3.1
|
|
95
115
|
[0.3.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.2.0...v0.3.0
|
|
96
116
|
[0.2.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.1.2...v0.2.0
|
package/README.md
CHANGED
|
@@ -17,10 +17,11 @@
|
|
|
17
17
|
祝日と、会社・店舗の休業日を組み合わせた営業日カレンダーです。
|
|
18
18
|
|
|
19
19
|
- **日本の祝日を内閣府のデータで全日照合**: 1955 年〜官報公表済みの年まで、内閣府「国民の祝日」CSV のすべての日付と一致することをテストで確かめています(振替休日・国民の休日・皇室行事による休日・五輪特措法による移動を含む)
|
|
20
|
+
- **繰り返しの規則**: 「第 3 水曜定休」「毎月末日は棚卸」「月曜休館(祝日の場合は翌平日)」のような規則を、祝日に当たったときの扱い(祝日に従う・祝日でも当てはめる・翌日に振り替える)とあわせて指定できます
|
|
20
21
|
- **日ごとの値(営業時間など)**: 「火曜定休、祝日は日曜の営業時間、元日は休業、大晦日は短縮営業」のように、曜日・祝日・特定日で変わる値を同じ規則で引けます
|
|
21
22
|
- **営業日の計算**: 定休日の曜日・毎年の休業日(年末年始のような年をまたぐ期間も可)・臨時休業日・臨時営業日と祝日を組み合わせ、「N 営業日後」「締め時刻を過ぎたら翌営業日」などを計算します
|
|
22
23
|
- **タイムゾーンを間違えない**: `Date` などの瞬間は、カレンダーのタイムゾーン(日本は `Asia/Tokyo`)で暦日に変換してから判定します。UTC で動くサーバーでも、日本時間 0〜9 時が前日扱いになりません
|
|
23
|
-
- **軽量・依存ゼロ**: minify + gzip で約
|
|
24
|
+
- **軽量・依存ゼロ**: minify + gzip で約 6.5KB(日本の祝日込みで約 7.5KB)。ESM・型定義同梱・`sideEffects: false` で、Node.js・ブラウザー・Edge ランタイムで動作します
|
|
24
25
|
- **他の国・地域に拡張できる**: 祝日の提供元はインターフェース(`HolidayProvider`)で差し替えられます。現在は日本のみ同梱しています
|
|
25
26
|
|
|
26
27
|
[English](#english)
|
|
@@ -84,9 +85,96 @@ shipping.dayInfo("2027-01-01");
|
|
|
84
85
|
1. `openDates`(臨時営業日)
|
|
85
86
|
2. `closedDates`(臨時休業日)
|
|
86
87
|
3. `closedAnnual`(毎年の休業日)
|
|
87
|
-
4.
|
|
88
|
-
5. `
|
|
89
|
-
6.
|
|
88
|
+
4. `openRecurring`・`closedRecurring` のうち `onHolidayRule: "over-holiday"` の規則(祝日でも当てはめる)
|
|
89
|
+
5. 祝日(`holidayPolicy` に従う。`"open"` は定休日の曜日より優先して営業日にする)
|
|
90
|
+
6. `openRecurring`・`closedRecurring`(既定の `"under-holiday"` と、`"after-holiday"` の振替先)
|
|
91
|
+
7. `closedWeekdays`(定休日の曜日)
|
|
92
|
+
8. それ以外は営業日
|
|
93
|
+
|
|
94
|
+
この順番は固定です。繰り返しの規則と祝日の関係だけは、規則ごとに `onHolidayRule` で選べます(次の節)。
|
|
95
|
+
|
|
96
|
+
### 繰り返しの規則(毎週・毎月の第 n 曜日・毎月の日)
|
|
97
|
+
|
|
98
|
+
「第 3 水曜定休」「第 2・第 4 土曜休業」「毎月末日は棚卸」「月曜休館(祝日の場合は翌平日)」のような繰り返しの休業日・営業日は、`closedRecurring`・`openRecurring` で指定します。
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
const museum = createCalendar({
|
|
102
|
+
holidays: jp,
|
|
103
|
+
holidayPolicy: "open", // 祝日は開館
|
|
104
|
+
closedRecurring: [
|
|
105
|
+
{ weekday: 1, name: "月曜休館", onHolidayRule: "after-holiday" }, // 祝日の場合は翌平日に休館
|
|
106
|
+
{ weekday: 3, nth: 3, months: [2, 8], name: "2 月・8 月の第 3 水曜は臨時休館" },
|
|
107
|
+
{ day: -1, name: "月末棚卸", onHolidayRule: "over-holiday" }, // 祝日でも休館
|
|
108
|
+
],
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
museum.isBusinessDay("2026-10-12"); // true(スポーツの日の月曜は開館)
|
|
112
|
+
museum.dayInfo("2026-10-13");
|
|
113
|
+
// { date: "2026-10-13", weekday: 2, businessDay: false, reason: "closed-recurring",
|
|
114
|
+
// name: "月曜休館", shiftedFrom: "2026-10-12" }
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
| 書き方 | 意味 |
|
|
118
|
+
| ------------------------ | ------------------------------------------------------------------------------- |
|
|
119
|
+
| `{ weekday: 1 }` | 毎週月曜日 |
|
|
120
|
+
| `{ weekday: 3, nth: 3 }` | 第 3 水曜日。`nth` は 1〜5 と -1(最終)。`[2, 4]` のように配列で複数指定できる |
|
|
121
|
+
| `{ day: 20 }` | 毎月 20 日。`day` は 1〜31 と -1(末日)。配列で複数指定できる |
|
|
122
|
+
| `months: [8, 12]` | どの書き方にも付けられる、月の絞り込み |
|
|
123
|
+
|
|
124
|
+
- **第 n 何曜日**は「その月で n 回目のその曜日」です(1〜7 日が第 1)。カレンダーの第 n 週の行にある曜日ではありません
|
|
125
|
+
- **その月にない日**(`nth: 5` がない月、`day: 31` がない月)は当てはまりません。末日は `day: -1` で指定します
|
|
126
|
+
- 繰り返しの規則どうしは配列で先に書いたものが優先で、`openRecurring` は `closedRecurring` より優先です
|
|
127
|
+
|
|
128
|
+
**祝日に当たったときの扱い**(`onHolidayRule`)は、規則ごとに選べます。
|
|
129
|
+
|
|
130
|
+
| 値 | 意味 | 店舗の告知での書き方 |
|
|
131
|
+
| ------------------------- | -------------------------------------------------------- | ------------------------------------- |
|
|
132
|
+
| `"under-holiday"`(既定) | 祝日の下。祝日の扱い(`holidayPolicy`)に従う | 「第 3 水曜定休(祝日の場合は営業)」 |
|
|
133
|
+
| `"over-holiday"` | 祝日の上。祝日でも当てはめる | 「第 3 水曜定休(祝日も休み)」 |
|
|
134
|
+
| `"after-holiday"` | 祝日の後。祝日には当てはめず、祝日でない翌日に振り替える | 「月曜休館(祝日の場合は翌平日)」 |
|
|
135
|
+
|
|
136
|
+
- `"under-holiday"` の規則は、`holidayPolicy: "ignore"`(祝日を判定に使わない)なら祝日にも当てはまります
|
|
137
|
+
- `"after-holiday"` の振替先は「その日以降で最初の祝日でない日」です。祝日が続く場合(GW など)は連休が明けた日になります。振替先の日に臨時営業日・臨時休業日・毎年の休業日があれば、そちらが優先です。振替で決まった日の `dayInfo()` には、振替の元の日付 `shiftedFrom` が付きます
|
|
138
|
+
- 振替の判断に使う「祝日」は、祝日の提供元(`jp` など)の祝日だけです(毎年の休業日などは含みません)
|
|
139
|
+
|
|
140
|
+
スケジュールでは、同じ書き方の規則に `value` を付けて `recurring` に指定します。
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
const hours = createSchedule<Hours>({
|
|
144
|
+
holidays: jp,
|
|
145
|
+
weekly: [sunday, weekday, weekday, weekday, weekday, weekday, weekday],
|
|
146
|
+
recurring: [
|
|
147
|
+
{ weekday: 1, value: null, name: "月曜休業", onHolidayRule: "after-holiday" },
|
|
148
|
+
{ day: -1, value: { open: "10:00", close: "17:00" }, name: "月末は短縮営業" },
|
|
149
|
+
],
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### よくある設定の例
|
|
154
|
+
|
|
155
|
+
**国民の休日(祝日に挟まれた日)を営業日として扱う**:祝日の提供元を包んで、種別で除外します。
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
import { jp } from "bizdays-toolkit/jp";
|
|
159
|
+
|
|
160
|
+
const jpWithoutCitizensHoliday = {
|
|
161
|
+
...jp,
|
|
162
|
+
holidaysInYear: (year: number) => jp.holidaysInYear(year).filter((h) => h.kind !== "citizens"),
|
|
163
|
+
};
|
|
164
|
+
const calendar = createCalendar({ holidays: jpWithoutCitizensHoliday, closedWeekdays: [0, 6] });
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**共通の設定から店舗ごとのカレンダーを作る**:設定は普通のオブジェクトなので、展開して足します。
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
const base = {
|
|
171
|
+
holidays: jp,
|
|
172
|
+
closedWeekdays: [0],
|
|
173
|
+
closedAnnual: [{ from: "12-31", to: "01-03" }],
|
|
174
|
+
} as const;
|
|
175
|
+
const shibuya = createCalendar({ ...base, openDates: ["2026-12-27"] });
|
|
176
|
+
const yokohama = createCalendar({ ...base, closedRecurring: [{ weekday: 3, nth: 3 }] });
|
|
177
|
+
```
|
|
90
178
|
|
|
91
179
|
### 日ごとの値(営業時間など)
|
|
92
180
|
|
|
@@ -113,7 +201,7 @@ storeHours.dayInfo("2027-01-01");
|
|
|
113
201
|
// { date: "2027-01-01", weekday: 5, value: null, reason: "annual", name: "元日休業", holiday: {…} }
|
|
114
202
|
```
|
|
115
203
|
|
|
116
|
-
値は `dates`(個別の日付・期間)→ `annual`(毎年の月日・期間)→ 祝日 → `weekly`(曜日)の順に決まります。同じ種類の規則が重なる場合は、配列で先に書いたものが優先です。
|
|
204
|
+
値は `dates`(個別の日付・期間)→ `annual`(毎年の月日・期間)→ `recurring`(`"over-holiday"`)→ 祝日 → `recurring`(既定・振替先)→ `weekly`(曜日)の順に決まります。同じ種類の規則が重なる場合は、配列で先に書いたものが優先です。
|
|
117
205
|
|
|
118
206
|
「今、営業時間内か」のような時刻の範囲の判定は、業種ごとの決まり(深夜をまたぐ営業、休憩時間など)があるためライブラリでは行いません。`civilOf` でタイムゾーンでの暦日と時刻を取り出せば、アプリ側で簡単に書けます。
|
|
119
207
|
|
|
@@ -213,9 +301,10 @@ A business-day calendar that combines public holidays with company or store clos
|
|
|
213
301
|
|
|
214
302
|
- **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)
|
|
215
303
|
- **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
|
+
- **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")
|
|
216
305
|
- **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
|
|
217
306
|
- **Time-zone safe**: instants (`Date`, ISO strings with offsets, Temporal) are converted to a civil date in the calendar's time zone before evaluation
|
|
218
|
-
- **Lightweight, zero dependencies**: about
|
|
307
|
+
- **Lightweight, zero dependencies**: about 6.5 KB min+gzip (about 7.5 KB with Japanese holidays), ESM, bundled types, `sideEffects: false`
|
|
219
308
|
- **Extensible** through the `HolidayProvider` interface (only Japan is bundled for now)
|
|
220
309
|
|
|
221
310
|
```ts
|
package/dist/core/calendar.d.ts
CHANGED
|
@@ -5,16 +5,21 @@
|
|
|
5
5
|
* 1. openDates … 臨時営業日(個別の日付・期間)
|
|
6
6
|
* 2. closedDates … 臨時休業日(個別の日付・期間)
|
|
7
7
|
* 3. closedAnnual … 毎年の休業日(月日・月日の期間。年をまたぐ期間も可)
|
|
8
|
-
* 4.
|
|
9
|
-
* 5.
|
|
10
|
-
* 6.
|
|
8
|
+
* 4. openRecurring・closedRecurring(onHolidayRule: "over-holiday")… 祝日でも当てはめる繰り返しの規則
|
|
9
|
+
* 5. 祝日 … holidayPolicy に従う(closed: 休業 / open: 営業 / ignore: 判定に使わない)
|
|
10
|
+
* 6. openRecurring・closedRecurring("under-holiday"・"after-holiday" の振替先)… 繰り返しの規則
|
|
11
|
+
* 7. closedWeekdays … 定休日の曜日
|
|
12
|
+
* 8. それ以外は営業日
|
|
13
|
+
*
|
|
14
|
+
* 繰り返しの規則は openRecurring が closedRecurring より優先。"after-holiday" の規則は、
|
|
15
|
+
* 祝日には当てはめず、その日以降で最初の祝日でない日に当てはめる(振替)
|
|
11
16
|
*
|
|
12
17
|
* 内部では、営業日かどうか(真偽値)を返すスケジュール(createSchedule)として作っている。
|
|
13
18
|
*/
|
|
14
19
|
import { type DateString, type Weekday } from "./date.ts";
|
|
15
20
|
import { IssueCollector } from "./errors.ts";
|
|
16
21
|
import { type DateInput } from "./input.ts";
|
|
17
|
-
import { type CompiledSchedule, type HolidayLookup, type HolidaySourceOptions } from "./schedule.ts";
|
|
22
|
+
import { type CompiledSchedule, type RecurringRule, type HolidayLookup, type HolidaySourceOptions } from "./schedule.ts";
|
|
18
23
|
import type { Holiday } from "./types.ts";
|
|
19
24
|
/**
|
|
20
25
|
* 祝日の扱い。
|
|
@@ -68,6 +73,13 @@ export interface CalendarOptions<Kind extends string = string> extends HolidaySo
|
|
|
68
73
|
readonly closedDates?: readonly (SpecificDate | DateRange)[];
|
|
69
74
|
/** 臨時営業日(ほかのすべての規則より優先) */
|
|
70
75
|
readonly openDates?: readonly (SpecificDate | DateRange)[];
|
|
76
|
+
/**
|
|
77
|
+
* 繰り返しの休業日(毎週・毎月の第 n 曜日・毎月の日)。
|
|
78
|
+
* 祝日に当たったときの扱いは規則ごとに onHolidayRule で指定する(既定: "under-holiday")
|
|
79
|
+
*/
|
|
80
|
+
readonly closedRecurring?: readonly RecurringRule[];
|
|
81
|
+
/** 繰り返しの営業日。closedRecurring と同じ日に当たる場合はこちらが優先 */
|
|
82
|
+
readonly openRecurring?: readonly RecurringRule[];
|
|
71
83
|
/** 祝日の扱い(既定: "closed") */
|
|
72
84
|
readonly holidayPolicy?: HolidayPolicy;
|
|
73
85
|
/** 営業日を探すときに調べる最大日数(既定: 3660)。超えたら Error */
|
|
@@ -77,7 +89,7 @@ export interface CalendarOptions<Kind extends string = string> extends HolidaySo
|
|
|
77
89
|
* 営業・休業を決めた規則(既知の値)。
|
|
78
90
|
* @public
|
|
79
91
|
*/
|
|
80
|
-
export type KnownDayReason = "open-date" | "closed-date" | "closed-annual" | "holiday" | "closed-weekday" | "regular";
|
|
92
|
+
export type KnownDayReason = "open-date" | "closed-date" | "closed-annual" | "open-recurring" | "closed-recurring" | "holiday" | "closed-weekday" | "regular";
|
|
81
93
|
/**
|
|
82
94
|
* 営業・休業を決めた規則。将来の版で値が増えることがあるので、分岐では「その他」も扱うこと。
|
|
83
95
|
* すべての値を網羅したことを型で確かめたい場合は KnownDayReason を使う。
|
|
@@ -98,6 +110,8 @@ export interface DayInfo<Kind extends string = string> {
|
|
|
98
110
|
readonly name?: string;
|
|
99
111
|
/** その日が祝日なら祝日(holidayPolicy に関係なく付く) */
|
|
100
112
|
readonly holiday?: Holiday<Kind>;
|
|
113
|
+
/** 祝日から振り替えられた繰り返しの規則で決まった場合、振替の元の日付(onHolidayRule: "after-holiday") */
|
|
114
|
+
readonly shiftedFrom?: DateString;
|
|
101
115
|
}
|
|
102
116
|
/**
|
|
103
117
|
* 営業日を探すときのオプション。
|
package/dist/core/calendar.js
CHANGED
|
@@ -5,16 +5,21 @@
|
|
|
5
5
|
* 1. openDates … 臨時営業日(個別の日付・期間)
|
|
6
6
|
* 2. closedDates … 臨時休業日(個別の日付・期間)
|
|
7
7
|
* 3. closedAnnual … 毎年の休業日(月日・月日の期間。年をまたぐ期間も可)
|
|
8
|
-
* 4.
|
|
9
|
-
* 5.
|
|
10
|
-
* 6.
|
|
8
|
+
* 4. openRecurring・closedRecurring(onHolidayRule: "over-holiday")… 祝日でも当てはめる繰り返しの規則
|
|
9
|
+
* 5. 祝日 … holidayPolicy に従う(closed: 休業 / open: 営業 / ignore: 判定に使わない)
|
|
10
|
+
* 6. openRecurring・closedRecurring("under-holiday"・"after-holiday" の振替先)… 繰り返しの規則
|
|
11
|
+
* 7. closedWeekdays … 定休日の曜日
|
|
12
|
+
* 8. それ以外は営業日
|
|
13
|
+
*
|
|
14
|
+
* 繰り返しの規則は openRecurring が closedRecurring より優先。"after-holiday" の規則は、
|
|
15
|
+
* 祝日には当てはめず、その日以降で最初の祝日でない日に当てはめる(振替)
|
|
11
16
|
*
|
|
12
17
|
* 内部では、営業日かどうか(真偽値)を返すスケジュール(createSchedule)として作っている。
|
|
13
18
|
*/
|
|
14
19
|
import { addDays, fromDayNumber, toDayNumber } from "./date.js";
|
|
15
20
|
import { checkUnknownKeys, IssueCollector, plainError, rangeError } from "./errors.js";
|
|
16
21
|
import { parseTimeOfDay, toCivil } from "./input.js";
|
|
17
|
-
import { buildSchedule, compileAnnualRules, compileDateRules, compileHolidaySource, isWeekday, } from "./schedule.js";
|
|
22
|
+
import { buildSchedule, compileAnnualRules, compileDateRules, compileHolidaySource, compileRecurringRules, isWeekday, RECURRING_RULE_KEYS, } from "./schedule.js";
|
|
18
23
|
const CALENDAR_OPTION_KEYS = [
|
|
19
24
|
"holidays",
|
|
20
25
|
"timeZone",
|
|
@@ -23,6 +28,8 @@ const CALENDAR_OPTION_KEYS = [
|
|
|
23
28
|
"closedAnnual",
|
|
24
29
|
"closedDates",
|
|
25
30
|
"openDates",
|
|
31
|
+
"closedRecurring",
|
|
32
|
+
"openRecurring",
|
|
26
33
|
"holidayPolicy",
|
|
27
34
|
"maxSearchDays",
|
|
28
35
|
];
|
|
@@ -61,6 +68,8 @@ export function compileCalendarOptions(c, options) {
|
|
|
61
68
|
const annual = compileAnnualRules(c, "closedAnnual", o["closedAnnual"], () => false, true);
|
|
62
69
|
const open = compileDateRules(c, "openDates", o["openDates"], () => true, true);
|
|
63
70
|
const closed = compileDateRules(c, "closedDates", o["closedDates"], () => false, true);
|
|
71
|
+
const openRecurring = compileRecurringRules(c, "openRecurring", o["openRecurring"], () => true, RECURRING_RULE_KEYS);
|
|
72
|
+
const closedRecurring = compileRecurringRules(c, "closedRecurring", o["closedRecurring"], () => false, RECURRING_RULE_KEYS);
|
|
64
73
|
return {
|
|
65
74
|
schedule: {
|
|
66
75
|
source,
|
|
@@ -69,6 +78,8 @@ export function compileCalendarOptions(c, options) {
|
|
|
69
78
|
annual,
|
|
70
79
|
// 臨時営業日を先に並べて、臨時休業日より優先する
|
|
71
80
|
dates: [...open, ...closed],
|
|
81
|
+
// 繰り返しの営業日を先に並べて、繰り返しの休業日より優先する
|
|
82
|
+
recurring: [...openRecurring, ...closedRecurring],
|
|
72
83
|
},
|
|
73
84
|
maxSearchDays: Number.isInteger(maxSearchDays) ? maxSearchDays : 3660,
|
|
74
85
|
};
|
|
@@ -98,6 +109,9 @@ export function createCalendar(options = {}) {
|
|
|
98
109
|
case "annual":
|
|
99
110
|
reason = "closed-annual";
|
|
100
111
|
break;
|
|
112
|
+
case "recurring":
|
|
113
|
+
reason = day.value ? "open-recurring" : "closed-recurring";
|
|
114
|
+
break;
|
|
101
115
|
case "holiday":
|
|
102
116
|
reason = "holiday";
|
|
103
117
|
break;
|
|
@@ -120,6 +134,8 @@ export function createCalendar(options = {}) {
|
|
|
120
134
|
result.name = day.name;
|
|
121
135
|
if (day.holiday !== undefined)
|
|
122
136
|
result.holiday = day.holiday;
|
|
137
|
+
if (day.shiftedFrom !== undefined)
|
|
138
|
+
result.shiftedFrom = day.shiftedFrom;
|
|
123
139
|
return Object.freeze(result);
|
|
124
140
|
}
|
|
125
141
|
const isOpen = (date) => schedule.valueOn(date);
|
package/dist/core/schedule.d.ts
CHANGED
|
@@ -4,8 +4,13 @@
|
|
|
4
4
|
* 1 日の値は次の優先順位で決まる(上が優先):
|
|
5
5
|
* 1. dates … 個別の日付・期間(配列で先に書いたものが優先)
|
|
6
6
|
* 2. annual … 毎年の月日・月日の期間(年をまたぐ期間も可。配列で先に書いたものが優先)
|
|
7
|
-
* 3.
|
|
8
|
-
* 4.
|
|
7
|
+
* 3. recurring(onHolidayRule: "over-holiday")… 祝日でも当てはめる繰り返しの規則
|
|
8
|
+
* 4. 祝日 … holiday の規則("weekday": 曜日どおり / { sameAs: 曜日 }: その曜日の値 / { value }: 指定の値)
|
|
9
|
+
* 5. recurring("under-holiday"・"after-holiday" の振替先)… 繰り返しの規則(配列で先に書いたものが優先)
|
|
10
|
+
* 6. weekly … 曜日ごとの値
|
|
11
|
+
*
|
|
12
|
+
* "under-holiday" の規則は、祝日の扱いが "weekday"(曜日どおり)なら祝日にも当てはまる。
|
|
13
|
+
* "after-holiday" の規則は、祝日には当てはめず、その日以降で最初の祝日でない日に当てはめる(振替)
|
|
9
14
|
*
|
|
10
15
|
* 営業日カレンダー(createCalendar)は、このスケジュールの真偽値版として作られている。
|
|
11
16
|
*/
|
|
@@ -14,10 +19,10 @@ import { IssueCollector } from "./errors.ts";
|
|
|
14
19
|
import { type DateInput } from "./input.ts";
|
|
15
20
|
import type { Holiday, HolidayProvider } from "./types.ts";
|
|
16
21
|
/**
|
|
17
|
-
*
|
|
22
|
+
* 祝日の値の決め方(スケジュールの holiday の設定)。
|
|
18
23
|
* @public
|
|
19
24
|
*/
|
|
20
|
-
export type
|
|
25
|
+
export type HolidayValueRule<T> =
|
|
21
26
|
/** 祝日を考えず、曜日の値を使う */
|
|
22
27
|
"weekday"
|
|
23
28
|
/** 指定した曜日の値を使う(例: 祝日は日曜日の営業時間) */
|
|
@@ -28,6 +33,80 @@ export type HolidayRule<T> =
|
|
|
28
33
|
| {
|
|
29
34
|
readonly value: T;
|
|
30
35
|
};
|
|
36
|
+
/**
|
|
37
|
+
* HolidayValueRule の旧名。繰り返しの規則の onHolidayRule と紛れないよう改名した。
|
|
38
|
+
* @deprecated HolidayValueRule を使うこと。次以降のマイナー版で削除する
|
|
39
|
+
* @public
|
|
40
|
+
*/
|
|
41
|
+
export type HolidayRule<T> = HolidayValueRule<T>;
|
|
42
|
+
/**
|
|
43
|
+
* 月の第 n 回目(1〜5)。-1 は最終(その月の最後のその曜日)。
|
|
44
|
+
* @public
|
|
45
|
+
*/
|
|
46
|
+
export type Nth = 1 | 2 | 3 | 4 | 5 | -1;
|
|
47
|
+
/**
|
|
48
|
+
* 月の日(1〜31)。-1 は末日。
|
|
49
|
+
* @public
|
|
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
|
+
* 月(1〜12)。
|
|
54
|
+
* @public
|
|
55
|
+
*/
|
|
56
|
+
export type Month = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
|
|
57
|
+
/**
|
|
58
|
+
* 繰り返しの規則が祝日に当たったときの扱い。
|
|
59
|
+
* - "under-holiday": 祝日の下。祝日の扱いに従う(既定)
|
|
60
|
+
* - "over-holiday": 祝日の上。祝日でも当てはめる
|
|
61
|
+
* - "after-holiday": 祝日の後。祝日には当てはめず、祝日でない翌日に振り替える
|
|
62
|
+
* @public
|
|
63
|
+
*/
|
|
64
|
+
export type OnHolidayRule = "under-holiday" | "over-holiday" | "after-holiday";
|
|
65
|
+
/**
|
|
66
|
+
* 曜日の繰り返し。nth を省略すると毎週、指定すると月の第 n 回目(例: 第 3 水曜日)。
|
|
67
|
+
* @public
|
|
68
|
+
*/
|
|
69
|
+
export interface WeekdayPattern {
|
|
70
|
+
readonly weekday: Weekday;
|
|
71
|
+
/** 月の第 n 回目(その月で n 回目のその曜日。1〜7 日が第 1)。配列で複数指定できる */
|
|
72
|
+
readonly nth?: Nth | readonly Nth[];
|
|
73
|
+
/** 当てはめる月(省略時はすべての月) */
|
|
74
|
+
readonly months?: readonly Month[];
|
|
75
|
+
readonly day?: never;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* 毎月の日の繰り返し(例: 毎月 20 日、毎月末日)。その月にない日(2 月 30 日など)は当てはまらない。
|
|
79
|
+
* @public
|
|
80
|
+
*/
|
|
81
|
+
export interface DayOfMonthPattern {
|
|
82
|
+
/** 月の日(1〜31、-1 は末日)。配列で複数指定できる */
|
|
83
|
+
readonly day: DayOfMonth | readonly DayOfMonth[];
|
|
84
|
+
/** 当てはめる月(省略時はすべての月) */
|
|
85
|
+
readonly months?: readonly Month[];
|
|
86
|
+
readonly weekday?: never;
|
|
87
|
+
readonly nth?: never;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* 繰り返しのパターン。
|
|
91
|
+
* @public
|
|
92
|
+
*/
|
|
93
|
+
export type RecurringPattern = WeekdayPattern | DayOfMonthPattern;
|
|
94
|
+
/**
|
|
95
|
+
* 繰り返しの規則(営業日カレンダーの closedRecurring・openRecurring)。
|
|
96
|
+
* @public
|
|
97
|
+
*/
|
|
98
|
+
export type RecurringRule = RecurringPattern & {
|
|
99
|
+
readonly name?: string;
|
|
100
|
+
/** 祝日に当たったときの扱い(既定: "under-holiday") */
|
|
101
|
+
readonly onHolidayRule?: OnHolidayRule;
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* 繰り返しの規則の値(スケジュールの recurring)。
|
|
105
|
+
* @public
|
|
106
|
+
*/
|
|
107
|
+
export type RecurringValue<T> = RecurringRule & {
|
|
108
|
+
readonly value: T;
|
|
109
|
+
};
|
|
31
110
|
/**
|
|
32
111
|
* 毎年の月日 "MM-DD" の値。
|
|
33
112
|
* @public
|
|
@@ -89,7 +168,9 @@ export interface ScheduleOptions<T, Kind extends string = string> extends Holida
|
|
|
89
168
|
/** 曜日ごとの値([日, 月, 火, 水, 木, 金, 土]) */
|
|
90
169
|
readonly weekly: readonly [T, T, T, T, T, T, T];
|
|
91
170
|
/** 祝日の値の決め方(既定: "weekday") */
|
|
92
|
-
readonly holiday?:
|
|
171
|
+
readonly holiday?: HolidayValueRule<T>;
|
|
172
|
+
/** 繰り返しの規則(毎週・毎月の第 n 曜日・毎月の日)の値 */
|
|
173
|
+
readonly recurring?: readonly RecurringValue<T>[];
|
|
93
174
|
/** 毎年の月日の値 */
|
|
94
175
|
readonly annual?: readonly (AnnualValue<T> | AnnualRangeValue<T>)[];
|
|
95
176
|
/** 個別の日付の値 */
|
|
@@ -99,7 +180,7 @@ export interface ScheduleOptions<T, Kind extends string = string> extends Holida
|
|
|
99
180
|
* 値を決めた規則(既知の値)。
|
|
100
181
|
* @public
|
|
101
182
|
*/
|
|
102
|
-
export type KnownScheduleReason = "date" | "annual" | "holiday" | "weekday";
|
|
183
|
+
export type KnownScheduleReason = "date" | "annual" | "recurring" | "holiday" | "weekday";
|
|
103
184
|
/**
|
|
104
185
|
* 値を決めた規則。将来の版で値が増えることがあるので、分岐では「その他」も扱うこと。
|
|
105
186
|
* すべての値を網羅したことを型で確かめたい場合は KnownScheduleReason を使う。
|
|
@@ -116,10 +197,12 @@ export interface ScheduleDay<T, Kind extends string = string> {
|
|
|
116
197
|
readonly value: T;
|
|
117
198
|
/** 値を決めた規則 */
|
|
118
199
|
readonly reason: ScheduleReason;
|
|
119
|
-
/** 値を決めた規則の名前(dates・annual の name、祝日なら祝日名) */
|
|
200
|
+
/** 値を決めた規則の名前(dates・annual・recurring の name、祝日なら祝日名) */
|
|
120
201
|
readonly name?: string;
|
|
121
202
|
/** その日が祝日なら祝日(holiday の規則に関係なく付く) */
|
|
122
203
|
readonly holiday?: Holiday<Kind>;
|
|
204
|
+
/** 祝日から振り替えられた規則で値が決まった場合、振替の元の日付(onHolidayRule: "after-holiday") */
|
|
205
|
+
readonly shiftedFrom?: DateString;
|
|
123
206
|
}
|
|
124
207
|
/**
|
|
125
208
|
* 暦日と時刻(カレンダーのタイムゾーン)。
|
|
@@ -190,6 +273,24 @@ export interface AnnualRule<T> {
|
|
|
190
273
|
export declare function compileDateRules<T>(c: IssueCollector, path: string, entries: unknown, valueOf: (entry: Record<string, unknown>, path: string) => T | undefined, allowString: boolean): DateRule<T>[];
|
|
191
274
|
/** @internal 毎年の月日の規則を検証して取り込む(entry は "MM-DD"・{ date }・{ from, to }) */
|
|
192
275
|
export declare function compileAnnualRules<T>(c: IssueCollector, path: string, entries: unknown, valueOf: (entry: Record<string, unknown>, path: string) => T | undefined, allowString: boolean): AnnualRule<T>[];
|
|
276
|
+
/** @internal 繰り返しの規則(検証済み) */
|
|
277
|
+
export interface RecurringRuleCompiled<T> {
|
|
278
|
+
/** 曜日の規則なら曜日、日の規則なら null */
|
|
279
|
+
readonly weekday: Weekday | null;
|
|
280
|
+
/** 第 n 回目の集合(null は毎週) */
|
|
281
|
+
readonly nth: ReadonlySet<number> | null;
|
|
282
|
+
/** 日の集合(曜日の規則では null) */
|
|
283
|
+
readonly days: ReadonlySet<number> | null;
|
|
284
|
+
/** 月の集合(null はすべての月) */
|
|
285
|
+
readonly months: ReadonlySet<number> | null;
|
|
286
|
+
readonly onHolidayRule: OnHolidayRule;
|
|
287
|
+
readonly value: T;
|
|
288
|
+
readonly name: string | undefined;
|
|
289
|
+
}
|
|
290
|
+
/** @internal 繰り返しの規則を検証して取り込む */
|
|
291
|
+
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>[];
|
|
292
|
+
/** @internal 繰り返しの規則がその日に当てはまるか(祝日かどうかは考えない) */
|
|
293
|
+
export declare function matchRecurring(rule: RecurringRuleCompiled<unknown>, date: DateString): boolean;
|
|
193
294
|
/** @internal 祝日の提供元とタイムゾーンの設定を検証した結果 */
|
|
194
295
|
export interface HolidaySource<Kind extends string> {
|
|
195
296
|
readonly providers: readonly HolidayProvider<Kind>[];
|
|
@@ -206,10 +307,13 @@ export declare function createHolidayLookup<Kind extends string>(source: Holiday
|
|
|
206
307
|
export interface CompiledSchedule<T, Kind extends string> {
|
|
207
308
|
readonly source: HolidaySource<Kind>;
|
|
208
309
|
readonly weekly: readonly T[];
|
|
209
|
-
readonly holiday:
|
|
310
|
+
readonly holiday: HolidayValueRule<T>;
|
|
210
311
|
readonly annual: readonly AnnualRule<T>[];
|
|
211
312
|
readonly dates: readonly DateRule<T>[];
|
|
313
|
+
readonly recurring: readonly RecurringRuleCompiled<T>[];
|
|
212
314
|
}
|
|
315
|
+
/** @internal 繰り返しの規則の項目 */
|
|
316
|
+
export declare const RECURRING_RULE_KEYS: string[];
|
|
213
317
|
/** @internal スケジュールの設定を検証して取り込む(誤りは c に集める) */
|
|
214
318
|
export declare function compileScheduleOptions<T, Kind extends string>(c: IssueCollector, options: unknown): CompiledSchedule<T, Kind> | undefined;
|
|
215
319
|
/** @internal 検証済みの規則からスケジュールを作る */
|
package/dist/core/schedule.js
CHANGED
|
@@ -4,12 +4,17 @@
|
|
|
4
4
|
* 1 日の値は次の優先順位で決まる(上が優先):
|
|
5
5
|
* 1. dates … 個別の日付・期間(配列で先に書いたものが優先)
|
|
6
6
|
* 2. annual … 毎年の月日・月日の期間(年をまたぐ期間も可。配列で先に書いたものが優先)
|
|
7
|
-
* 3.
|
|
8
|
-
* 4.
|
|
7
|
+
* 3. recurring(onHolidayRule: "over-holiday")… 祝日でも当てはめる繰り返しの規則
|
|
8
|
+
* 4. 祝日 … holiday の規則("weekday": 曜日どおり / { sameAs: 曜日 }: その曜日の値 / { value }: 指定の値)
|
|
9
|
+
* 5. recurring("under-holiday"・"after-holiday" の振替先)… 繰り返しの規則(配列で先に書いたものが優先)
|
|
10
|
+
* 6. weekly … 曜日ごとの値
|
|
11
|
+
*
|
|
12
|
+
* "under-holiday" の規則は、祝日の扱いが "weekday"(曜日どおり)なら祝日にも当てはまる。
|
|
13
|
+
* "after-holiday" の規則は、祝日には当てはめず、その日以降で最初の祝日でない日に当てはめる(振替)
|
|
9
14
|
*
|
|
10
15
|
* 営業日カレンダー(createCalendar)は、このスケジュールの真偽値版として作られている。
|
|
11
16
|
*/
|
|
12
|
-
import { isValidDate, parseDate, toDayNumber, weekdayOf, } from "./date.js";
|
|
17
|
+
import { addDays, daysInMonth, isValidDate, parseDate, toDayNumber, weekdayOf, } from "./date.js";
|
|
13
18
|
import { checkUnknownKeys, IssueCollector, rangeError } from "./errors.js";
|
|
14
19
|
import { instantToCivil, toCivil, toDateTimeParts, validateTimeZone, } from "./input.js";
|
|
15
20
|
// ------------------------------------------------------------
|
|
@@ -138,6 +143,125 @@ export function compileAnnualRules(c, path, entries, valueOf, allowString) {
|
|
|
138
143
|
});
|
|
139
144
|
return rules;
|
|
140
145
|
}
|
|
146
|
+
const NTH_VALUES = [1, 2, 3, 4, 5, -1];
|
|
147
|
+
const ON_HOLIDAY_RULES = [
|
|
148
|
+
"under-holiday",
|
|
149
|
+
"over-holiday",
|
|
150
|
+
"after-holiday",
|
|
151
|
+
];
|
|
152
|
+
const isDayOfMonth = (v) => v === -1 || (Number.isInteger(v) && v >= 1 && v <= 31);
|
|
153
|
+
const isMonth = (v) => Number.isInteger(v) && v >= 1 && v <= 12;
|
|
154
|
+
/** 単独の値または空でない配列を検証し、集合にする(誤りは c に記録して undefined) */
|
|
155
|
+
function checkSet(c, path, raw, valid, expected) {
|
|
156
|
+
const list = Array.isArray(raw) ? raw : [raw];
|
|
157
|
+
if (list.length === 0 || !list.every(valid)) {
|
|
158
|
+
c.error("invalid-option", path, `must be ${expected}, or a non-empty array of them`);
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
return new Set(list);
|
|
162
|
+
}
|
|
163
|
+
/** @internal 繰り返しの規則を検証して取り込む */
|
|
164
|
+
export function compileRecurringRules(c, path, entries, valueOf, keys) {
|
|
165
|
+
if (entries === undefined)
|
|
166
|
+
return [];
|
|
167
|
+
if (!Array.isArray(entries)) {
|
|
168
|
+
c.error("invalid-option", path, "must be an array");
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
const rules = [];
|
|
172
|
+
entries.forEach((entry, i) => {
|
|
173
|
+
const p = `${path}[${i}]`;
|
|
174
|
+
if (!isObject(entry)) {
|
|
175
|
+
c.error("invalid-option", p, "must be an object");
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
checkUnknownKeys(c, p, entry, keys);
|
|
179
|
+
const value = valueOf(entry, p);
|
|
180
|
+
const name = checkName(c, p, entry["name"]);
|
|
181
|
+
const hasWeekday = "weekday" in entry;
|
|
182
|
+
const hasDay = "day" in entry;
|
|
183
|
+
if (hasWeekday === hasDay) {
|
|
184
|
+
c.error("invalid-option", p, 'must have either "weekday" or "day" (not both)');
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
let ok = true;
|
|
188
|
+
let weekday = null;
|
|
189
|
+
let nth = null;
|
|
190
|
+
let days = null;
|
|
191
|
+
if (hasWeekday) {
|
|
192
|
+
if (isWeekday(entry["weekday"]))
|
|
193
|
+
weekday = entry["weekday"];
|
|
194
|
+
else {
|
|
195
|
+
c.error("invalid-weekday", `${p}.weekday`, "must be an integer from 0 (Sunday) to 6 (Saturday)");
|
|
196
|
+
ok = false;
|
|
197
|
+
}
|
|
198
|
+
if (entry["nth"] !== undefined) {
|
|
199
|
+
const set = checkSet(c, `${p}.nth`, entry["nth"], (v) => NTH_VALUES.includes(v), "1-5 or -1 (last)");
|
|
200
|
+
if (set)
|
|
201
|
+
nth = set;
|
|
202
|
+
else
|
|
203
|
+
ok = false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
if ("nth" in entry) {
|
|
208
|
+
c.error("invalid-option", `${p}.nth`, '"nth" can be used only with "weekday"');
|
|
209
|
+
ok = false;
|
|
210
|
+
}
|
|
211
|
+
const set = checkSet(c, `${p}.day`, entry["day"], isDayOfMonth, "1-31 or -1 (last day)");
|
|
212
|
+
if (set)
|
|
213
|
+
days = set;
|
|
214
|
+
else
|
|
215
|
+
ok = false;
|
|
216
|
+
}
|
|
217
|
+
let months = null;
|
|
218
|
+
if (entry["months"] !== undefined) {
|
|
219
|
+
if (!Array.isArray(entry["months"])) {
|
|
220
|
+
c.error("invalid-option", `${p}.months`, "must be a non-empty array of months (1-12)");
|
|
221
|
+
ok = false;
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
const set = checkSet(c, `${p}.months`, entry["months"], isMonth, "a month (1-12)");
|
|
225
|
+
if (set)
|
|
226
|
+
months = set;
|
|
227
|
+
else
|
|
228
|
+
ok = false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const onHoliday = entry["onHolidayRule"] ?? "under-holiday";
|
|
232
|
+
if (!ON_HOLIDAY_RULES.includes(onHoliday)) {
|
|
233
|
+
c.error("invalid-option", `${p}.onHolidayRule`, `must be one of ${ON_HOLIDAY_RULES.map((v) => `"${v}"`).join(", ")}`);
|
|
234
|
+
ok = false;
|
|
235
|
+
}
|
|
236
|
+
if (ok && value !== undefined) {
|
|
237
|
+
rules.push(Object.freeze({
|
|
238
|
+
weekday,
|
|
239
|
+
nth,
|
|
240
|
+
days,
|
|
241
|
+
months,
|
|
242
|
+
onHolidayRule: onHoliday,
|
|
243
|
+
value,
|
|
244
|
+
name,
|
|
245
|
+
}));
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
return rules;
|
|
249
|
+
}
|
|
250
|
+
/** @internal 繰り返しの規則がその日に当てはまるか(祝日かどうかは考えない) */
|
|
251
|
+
export function matchRecurring(rule, date) {
|
|
252
|
+
const { year, month, day } = parseDate(date);
|
|
253
|
+
if (rule.months && !rule.months.has(month))
|
|
254
|
+
return false;
|
|
255
|
+
const dim = daysInMonth(year, month);
|
|
256
|
+
if (rule.weekday !== null) {
|
|
257
|
+
if (weekdayOf(date) !== rule.weekday)
|
|
258
|
+
return false;
|
|
259
|
+
if (rule.nth === null)
|
|
260
|
+
return true;
|
|
261
|
+
return rule.nth.has(Math.ceil(day / 7)) || (rule.nth.has(-1) && day + 7 > dim);
|
|
262
|
+
}
|
|
263
|
+
return rule.days.has(day) || (rule.days.has(-1) && day === dim);
|
|
264
|
+
}
|
|
141
265
|
/** 規則の value を取り出す(value が必須の規則用) */
|
|
142
266
|
function requiredValue(c) {
|
|
143
267
|
return (entry, path) => {
|
|
@@ -319,7 +443,10 @@ const SCHEDULE_OPTION_KEYS = [
|
|
|
319
443
|
"holiday",
|
|
320
444
|
"annual",
|
|
321
445
|
"dates",
|
|
446
|
+
"recurring",
|
|
322
447
|
];
|
|
448
|
+
/** @internal 繰り返しの規則の項目 */
|
|
449
|
+
export const RECURRING_RULE_KEYS = ["weekday", "nth", "day", "months", "name", "onHolidayRule"];
|
|
323
450
|
/** @internal スケジュールの設定を検証して取り込む(誤りは c に集める) */
|
|
324
451
|
export function compileScheduleOptions(c, options) {
|
|
325
452
|
if (!isObject(options)) {
|
|
@@ -355,8 +482,11 @@ export function compileScheduleOptions(c, options) {
|
|
|
355
482
|
}
|
|
356
483
|
const annual = compileAnnualRules(c, "annual", options["annual"], requiredValue(c), false);
|
|
357
484
|
const dates = compileDateRules(c, "dates", options["dates"], requiredValue(c), false);
|
|
358
|
-
|
|
485
|
+
const recurring = compileRecurringRules(c, "recurring", options["recurring"], requiredValue(c), [...RECURRING_RULE_KEYS, "value"]);
|
|
486
|
+
return { source, weekly, holiday, annual, dates, recurring };
|
|
359
487
|
}
|
|
488
|
+
/** 振替元をさかのぼる最大日数(祝日が続く日数の上限。2019 年の大型連休でも 10 日) */
|
|
489
|
+
const MAX_HOLIDAY_RUN = 31;
|
|
360
490
|
function matchAnnual(rule, monthDay) {
|
|
361
491
|
return rule.from <= rule.to
|
|
362
492
|
? monthDay >= rule.from && monthDay <= rule.to
|
|
@@ -364,13 +494,37 @@ function matchAnnual(rule, monthDay) {
|
|
|
364
494
|
}
|
|
365
495
|
/** @internal 検証済みの規則からスケジュールを作る */
|
|
366
496
|
export function buildSchedule(compiled) {
|
|
367
|
-
const { weekly, holiday: holidayRule, annual, dates } = compiled;
|
|
497
|
+
const { weekly, holiday: holidayRule, annual, dates, recurring } = compiled;
|
|
368
498
|
const lookup = createHolidayLookup(compiled.source);
|
|
499
|
+
const hasShift = recurring.some((r) => r.onHolidayRule === "after-holiday");
|
|
500
|
+
/** 振替の判断に使う「祝日」か。提供元の対応年の外は祝日でないとみなす(振替元をさかのぼる途中で範囲を出る場合のため) */
|
|
501
|
+
function isHolidaySafe(date) {
|
|
502
|
+
try {
|
|
503
|
+
return lookup.holidayOn(date) !== undefined;
|
|
504
|
+
}
|
|
505
|
+
catch {
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* "after-holiday" の規則が、date(祝日でない日)に振り替えられてきたか。
|
|
511
|
+
* date の前日からさかのぼって祝日が続く間に、規則に当たる日があれば、その日付を返す
|
|
512
|
+
*/
|
|
513
|
+
function shiftedOrigin(rule, date) {
|
|
514
|
+
for (let k = 1; k <= MAX_HOLIDAY_RUN; k++) {
|
|
515
|
+
const prev = addDays(date, -k);
|
|
516
|
+
if (!isHolidaySafe(prev))
|
|
517
|
+
return undefined;
|
|
518
|
+
if (matchRecurring(rule, prev))
|
|
519
|
+
return prev;
|
|
520
|
+
}
|
|
521
|
+
return undefined;
|
|
522
|
+
}
|
|
369
523
|
function info(date) {
|
|
370
524
|
const dayNumber = toDayNumber(date);
|
|
371
525
|
const weekday = weekdayOf(date);
|
|
372
526
|
const holiday = lookup.holidayOn(date);
|
|
373
|
-
const make = (value, reason, name) => {
|
|
527
|
+
const make = (value, reason, name, shiftedFrom) => {
|
|
374
528
|
const result = {
|
|
375
529
|
date,
|
|
376
530
|
weekday,
|
|
@@ -381,8 +535,11 @@ export function buildSchedule(compiled) {
|
|
|
381
535
|
result.name = name;
|
|
382
536
|
if (holiday !== undefined)
|
|
383
537
|
result.holiday = holiday;
|
|
538
|
+
if (shiftedFrom !== undefined)
|
|
539
|
+
result.shiftedFrom = shiftedFrom;
|
|
384
540
|
return Object.freeze(result);
|
|
385
541
|
};
|
|
542
|
+
// 1. 個別の日付 2. 毎年の月日
|
|
386
543
|
const byDate = dates.find((r) => dayNumber >= r.from && dayNumber <= r.to);
|
|
387
544
|
if (byDate)
|
|
388
545
|
return make(byDate.value, "date", byDate.name);
|
|
@@ -390,10 +547,34 @@ export function buildSchedule(compiled) {
|
|
|
390
547
|
const byAnnual = annual.find((r) => matchAnnual(r, monthDay));
|
|
391
548
|
if (byAnnual)
|
|
392
549
|
return make(byAnnual.value, "annual", byAnnual.name);
|
|
393
|
-
if (holiday
|
|
394
|
-
|
|
395
|
-
|
|
550
|
+
if (holiday) {
|
|
551
|
+
// 3. 祝日の上("over-holiday")
|
|
552
|
+
const over = recurring.find((r) => r.onHolidayRule === "over-holiday" && matchRecurring(r, date));
|
|
553
|
+
if (over)
|
|
554
|
+
return make(over.value, "recurring", over.name);
|
|
555
|
+
// 4. 祝日
|
|
556
|
+
if (holidayRule !== "weekday") {
|
|
557
|
+
const value = "sameAs" in holidayRule ? weekly[holidayRule.sameAs] : holidayRule.value;
|
|
558
|
+
return make(value, "holiday", holiday.name);
|
|
559
|
+
}
|
|
560
|
+
// 祝日の扱いが「曜日どおり」なら、祝日の下("under-holiday")の規則も当てはまる。
|
|
561
|
+
// "after-holiday" の規則は祝日には当てはめない(振り替える)
|
|
562
|
+
const under = recurring.find((r) => r.onHolidayRule === "under-holiday" && matchRecurring(r, date));
|
|
563
|
+
if (under)
|
|
564
|
+
return make(under.value, "recurring", under.name);
|
|
565
|
+
return make(weekly[weekday], "weekday", undefined);
|
|
566
|
+
}
|
|
567
|
+
// 5. 繰り返しの規則(祝日でない日。配列で先に書いたものが優先)。振替先も同じ段階で判定する
|
|
568
|
+
for (const rule of recurring) {
|
|
569
|
+
if (matchRecurring(rule, date))
|
|
570
|
+
return make(rule.value, "recurring", rule.name);
|
|
571
|
+
if (hasShift && rule.onHolidayRule === "after-holiday") {
|
|
572
|
+
const origin = shiftedOrigin(rule, date);
|
|
573
|
+
if (origin !== undefined)
|
|
574
|
+
return make(rule.value, "recurring", rule.name, origin);
|
|
575
|
+
}
|
|
396
576
|
}
|
|
577
|
+
// 6. 曜日
|
|
397
578
|
return make(weekly[weekday], "weekday", undefined);
|
|
398
579
|
}
|
|
399
580
|
const schedule = {
|
package/dist/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* 公開する名前と型は etc/api.md に記録し、pnpm check で意図しない変化を検出する。
|
|
9
9
|
*/
|
|
10
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";
|
|
11
|
-
export { createSchedule, type AnnualRangeValue, type AnnualValue, type CivilDateTimeParts, type DateRangeValue, type DateValue, type HolidayLookup, type HolidayRule, type HolidaySourceOptions, type KnownScheduleReason, type Schedule, type ScheduleDay, type ScheduleOptions, type ScheduleReason, } from "./core/schedule.ts";
|
|
11
|
+
export { createSchedule, type AnnualRangeValue, type AnnualValue, type CivilDateTimeParts, type DateRangeValue, type DateValue, type DayOfMonth, type DayOfMonthPattern, type HolidayLookup, type HolidayRule, 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 { isBizdaysError, type ErrorCode, type KnownErrorCode } from "./core/errors.ts";
|
|
14
14
|
export { toDateString, toDateTimeParts, type DateInput, type TemporalDateLike, type TemporalInstantLike, } from "./core/input.ts";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bizdays-toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"lint": "oxlint --deny-warnings src scripts test",
|
|
49
49
|
"test": "node --test \"test/**/*.test.ts\"",
|
|
50
50
|
"test:tz": "node scripts/test-tz.ts",
|
|
51
|
+
"bench": "node scripts/bench.ts",
|
|
51
52
|
"fmt": "oxfmt",
|
|
52
53
|
"fmt:check": "oxfmt --check",
|
|
53
54
|
"check": "pnpm typecheck && pnpm lint && pnpm test && pnpm test:tz && pnpm build && pnpm fmt:check && node scripts/check-package.ts && node scripts/check-size.ts && node scripts/api-report.ts",
|