bizdays-toolkit 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -1
- package/README.md +41 -2
- package/dist/core/calendar.d.ts +6 -28
- package/dist/core/calendar.js +75 -160
- package/dist/core/input.d.ts +5 -0
- package/dist/core/input.js +8 -0
- package/dist/core/schedule.d.ts +130 -0
- package/dist/core/schedule.js +203 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -2
- package/dist/jp/index.d.ts +5 -0
- package/dist/jp/index.js +4 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.2.0] - 2026-10-14
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `createSchedule()`: 曜日・祝日・毎年の月日・個別の日付の規則から、日ごとに任意の値(営業時間など)を返すスケジュール。祝日の値は `"weekday"`(曜日どおり)・`{ sameAs: 曜日 }`(その曜日の値)・`{ value }`(指定の値)から選べる。`valueOn`・`dayInfo`(値を決めた規則と祝日の情報付き)
|
|
12
|
+
- `civilOf()`(営業日カレンダー・スケジュール)、`toDateTimeParts()`、`toJapanDateTime()`(`bizdays-toolkit/jp`): 入力を、タイムゾーンでの暦日と時刻(`"HH:mm"`)に変換する
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- 営業日カレンダー(`createCalendar`)を、スケジュールの真偽値版として作り直した。判定の優先順位と結果は変わらない
|
|
17
|
+
|
|
7
18
|
## [0.1.2] - 2026-10-12
|
|
8
19
|
|
|
9
20
|
ライブラリのコードと祝日データに変更はありません。
|
|
@@ -39,7 +50,8 @@
|
|
|
39
50
|
|
|
40
51
|
- 日本: 2026 年 2 月の官報(2027 年の春分・秋分の日)まで反映(`dataVersion: "2026-02"`、`confirmedThrough: 2027`)
|
|
41
52
|
|
|
42
|
-
[Unreleased]: https://github.com/inohan77/bizdays-toolkit/compare/v0.
|
|
53
|
+
[Unreleased]: https://github.com/inohan77/bizdays-toolkit/compare/v0.2.0...HEAD
|
|
54
|
+
[0.2.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.1.2...v0.2.0
|
|
43
55
|
[0.1.2]: https://github.com/inohan77/bizdays-toolkit/compare/v0.1.1...v0.1.2
|
|
44
56
|
[0.1.1]: https://github.com/inohan77/bizdays-toolkit/compare/v0.1.0...v0.1.1
|
|
45
57
|
[0.1.0]: https://github.com/inohan77/bizdays-toolkit/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -17,9 +17,10 @@
|
|
|
17
17
|
祝日と、会社・店舗の休業日を組み合わせた営業日カレンダーです。
|
|
18
18
|
|
|
19
19
|
- **日本の祝日を内閣府のデータで全日照合**: 1955 年〜官報公表済みの年まで、内閣府「国民の祝日」CSV のすべての日付と一致することをテストで確かめています(振替休日・国民の休日・皇室行事による休日・五輪特措法による移動を含む)
|
|
20
|
+
- **日ごとの値(営業時間など)**: 「火曜定休、祝日は日曜の営業時間、元日は休業、大晦日は短縮営業」のように、曜日・祝日・特定日で変わる値を同じ規則で引けます
|
|
20
21
|
- **営業日の計算**: 定休日の曜日・毎年の休業日(年末年始のような年をまたぐ期間も可)・臨時休業日・臨時営業日と祝日を組み合わせ、「N 営業日後」「締め時刻を過ぎたら翌営業日」などを計算します
|
|
21
22
|
- **タイムゾーンを間違えない**: `Date` などの瞬間は、カレンダーのタイムゾーン(日本は `Asia/Tokyo`)で暦日に変換してから判定します。UTC で動くサーバーでも、日本時間 0〜9 時が前日扱いになりません
|
|
22
|
-
- **軽量・依存ゼロ**: minify + gzip で約
|
|
23
|
+
- **軽量・依存ゼロ**: minify + gzip で約 4KB(日本の祝日込みで約 5.5KB)。ESM・型定義同梱・`sideEffects: false` で、Node.js・ブラウザー・Edge ランタイムで動作します
|
|
23
24
|
- **他の国・地域に拡張できる**: 祝日の提供元はインターフェース(`HolidayProvider`)で差し替えられます。現在は日本のみ同梱しています
|
|
24
25
|
|
|
25
26
|
[English](#english)
|
|
@@ -87,6 +88,43 @@ shipping.dayInfo("2027-01-01");
|
|
|
87
88
|
5. `closedWeekdays`(定休日の曜日)
|
|
88
89
|
6. それ以外は営業日
|
|
89
90
|
|
|
91
|
+
### 日ごとの値(営業時間など)
|
|
92
|
+
|
|
93
|
+
`createSchedule` は、曜日・祝日・特定日の規則から、日ごとに任意の値を返します。値の型は自由なので、営業時間のほか、受付枠や配送料の区分などにも使えます。
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { createSchedule } from "bizdays-toolkit";
|
|
97
|
+
import { jp } from "bizdays-toolkit/jp";
|
|
98
|
+
|
|
99
|
+
type Hours = { open: string; close: string } | null; // null は休業
|
|
100
|
+
const weekday = { open: "10:00", close: "21:00" };
|
|
101
|
+
const sunday = { open: "10:00", close: "19:00" };
|
|
102
|
+
|
|
103
|
+
const storeHours = createSchedule<Hours>({
|
|
104
|
+
holidays: jp,
|
|
105
|
+
weekly: [sunday, weekday, null, weekday, weekday, weekday, weekday], // [日, 月, …, 土]。火曜定休
|
|
106
|
+
holiday: { sameAs: 0 }, // 祝日は日曜の値("weekday": 曜日どおり / { value: … }: 指定の値)
|
|
107
|
+
annual: [{ date: "01-01", value: null, name: "元日休業" }], // 毎年の月日(期間も可)
|
|
108
|
+
dates: [{ date: "2026-12-31", value: { open: "10:00", close: "17:00" }, name: "大晦日" }],
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
storeHours.valueOn("2026-11-03"); // { open: "10:00", close: "19:00" }(文化の日は日曜の営業時間)
|
|
112
|
+
storeHours.dayInfo("2027-01-01");
|
|
113
|
+
// { date: "2027-01-01", weekday: 5, value: null, reason: "annual", name: "元日休業", holiday: {…} }
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
値は `dates`(個別の日付・期間)→ `annual`(毎年の月日・期間)→ 祝日 → `weekly`(曜日)の順に決まります。同じ種類の規則が重なる場合は、配列で先に書いたものが優先です。
|
|
117
|
+
|
|
118
|
+
「今、営業時間内か」のような時刻の範囲の判定は、業種ごとの決まり(深夜をまたぐ営業、休憩時間など)があるためライブラリでは行いません。`civilOf` でタイムゾーンでの暦日と時刻を取り出せば、アプリ側で簡単に書けます。
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
const { date, time } = storeHours.civilOf(new Date()); // { date: "2026-10-14", time: "16:30" }(日本時間)
|
|
122
|
+
const hours = storeHours.valueOn(date);
|
|
123
|
+
const isOpen = hours !== null && time !== null && hours.open <= time && time < hours.close;
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`civilOf` は営業日カレンダーにもあり、`bizdays-toolkit/jp` の `toJapanDateTime`、ルートの `toDateTimeParts(input, timeZone)` でも同じ変換ができます。
|
|
127
|
+
|
|
90
128
|
### 日付・日時の入力
|
|
91
129
|
|
|
92
130
|
すべてのメソッドは次の入力を受け付けます。戻り値の日付は常に `"YYYY-MM-DD"` 文字列です。
|
|
@@ -155,9 +193,10 @@ MIT
|
|
|
155
193
|
A business-day calendar that combines public holidays with company or store closures.
|
|
156
194
|
|
|
157
195
|
- **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)
|
|
196
|
+
- **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
|
|
158
197
|
- **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
|
|
159
198
|
- **Time-zone safe**: instants (`Date`, ISO strings with offsets, Temporal) are converted to a civil date in the calendar's time zone before evaluation
|
|
160
|
-
- **Lightweight, zero dependencies**: about
|
|
199
|
+
- **Lightweight, zero dependencies**: about 4 KB min+gzip (about 5.5 KB with Japanese holidays), ESM, bundled types, `sideEffects: false`
|
|
161
200
|
- **Extensible** through the `HolidayProvider` interface (only Japan is bundled for now)
|
|
162
201
|
|
|
163
202
|
```ts
|
package/dist/core/calendar.d.ts
CHANGED
|
@@ -8,10 +8,13 @@
|
|
|
8
8
|
* 4. 祝日 … holidayPolicy に従う(closed: 休業 / open: 営業 / ignore: 判定に使わない)
|
|
9
9
|
* 5. closedWeekdays … 定休日の曜日
|
|
10
10
|
* 6. それ以外は営業日
|
|
11
|
+
*
|
|
12
|
+
* 内部では、営業日かどうか(真偽値)を返すスケジュール(createSchedule)として作っている。
|
|
11
13
|
*/
|
|
12
14
|
import { type DateString, type Weekday } from "./date.ts";
|
|
13
15
|
import { type DateInput } from "./input.ts";
|
|
14
|
-
import type
|
|
16
|
+
import { type HolidayLookup, type HolidaySourceOptions } from "./schedule.ts";
|
|
17
|
+
import type { Holiday } from "./types.ts";
|
|
15
18
|
/** 祝日の扱い */
|
|
16
19
|
export type HolidayPolicy = "closed" | "open" | "ignore";
|
|
17
20
|
/** 毎年の月日 "MM-DD"(名前付きも可) */
|
|
@@ -36,14 +39,7 @@ export interface DateRange {
|
|
|
36
39
|
readonly to: DateString;
|
|
37
40
|
readonly name?: string;
|
|
38
41
|
}
|
|
39
|
-
export interface CalendarOptions<Kind extends string = string> {
|
|
40
|
-
/** 祝日の提供元(例: jp)。複数渡すと和集合になる */
|
|
41
|
-
readonly holidays?: HolidayProvider<Kind> | readonly HolidayProvider<Kind>[];
|
|
42
|
-
/**
|
|
43
|
-
* 日時の入力(Date・オフセット付き文字列など)を暦日に変換するタイムゾーン。
|
|
44
|
-
* 省略時は祝日の提供元のタイムゾーン、提供元もなければ "UTC"
|
|
45
|
-
*/
|
|
46
|
-
readonly timeZone?: string;
|
|
42
|
+
export interface CalendarOptions<Kind extends string = string> extends HolidaySourceOptions<Kind> {
|
|
47
43
|
/** 定休日の曜日(0 = 日曜日) */
|
|
48
44
|
readonly closedWeekdays?: readonly Weekday[];
|
|
49
45
|
/** 毎年の休業日 */
|
|
@@ -54,8 +50,6 @@ export interface CalendarOptions<Kind extends string = string> {
|
|
|
54
50
|
readonly openDates?: readonly (SpecificDate | DateRange)[];
|
|
55
51
|
/** 祝日の扱い(既定: "closed") */
|
|
56
52
|
readonly holidayPolicy?: HolidayPolicy;
|
|
57
|
-
/** 祝日の提供元の対応年を外れた日付の扱い(既定: "throw") */
|
|
58
|
-
readonly outOfRange?: "throw" | "ignore";
|
|
59
53
|
/** 営業日を探すときに調べる最大日数(既定: 3660)。超えたら Error */
|
|
60
54
|
readonly maxSearchDays?: number;
|
|
61
55
|
}
|
|
@@ -80,23 +74,7 @@ export interface BusinessDayOptions {
|
|
|
80
74
|
*/
|
|
81
75
|
readonly cutoff?: string;
|
|
82
76
|
}
|
|
83
|
-
export interface Calendar<Kind extends string = string> {
|
|
84
|
-
/** 日時の入力を暦日に変換するタイムゾーン */
|
|
85
|
-
readonly timeZone: string;
|
|
86
|
-
/** 祝日の提供元 */
|
|
87
|
-
readonly providers: readonly HolidayProvider<Kind>[];
|
|
88
|
-
/** 入力をカレンダーのタイムゾーンでの暦日に変換する */
|
|
89
|
-
dateOf(input: DateInput): DateString;
|
|
90
|
-
/** 今日の日付(カレンダーのタイムゾーン) */
|
|
91
|
-
today(now?: Date): DateString;
|
|
92
|
-
/** 祝日か */
|
|
93
|
-
isHoliday(input: DateInput): boolean;
|
|
94
|
-
/** 祝日なら祝日を返す */
|
|
95
|
-
getHoliday(input: DateInput): Holiday<Kind> | undefined;
|
|
96
|
-
/** その年の祝日 */
|
|
97
|
-
holidaysInYear(year: number): readonly Holiday<Kind>[];
|
|
98
|
-
/** 期間内の祝日(両端を含む) */
|
|
99
|
-
holidaysBetween(from: DateInput, to: DateInput): readonly Holiday<Kind>[];
|
|
77
|
+
export interface Calendar<Kind extends string = string> extends HolidayLookup<Kind> {
|
|
100
78
|
/** 1 日分の判定結果(営業日か・理由・祝日) */
|
|
101
79
|
dayInfo(input: DateInput): DayInfo<Kind>;
|
|
102
80
|
/** 営業日か */
|
package/dist/core/calendar.js
CHANGED
|
@@ -8,160 +8,91 @@
|
|
|
8
8
|
* 4. 祝日 … holidayPolicy に従う(closed: 休業 / open: 営業 / ignore: 判定に使わない)
|
|
9
9
|
* 5. closedWeekdays … 定休日の曜日
|
|
10
10
|
* 6. それ以外は営業日
|
|
11
|
+
*
|
|
12
|
+
* 内部では、営業日かどうか(真偽値)を返すスケジュール(createSchedule)として作っている。
|
|
11
13
|
*/
|
|
12
|
-
import { addDays, fromDayNumber,
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
function parseMonthDay(value) {
|
|
17
|
-
const m = MONTH_DAY_RE.exec(value);
|
|
18
|
-
const month = Number(m?.[1]);
|
|
19
|
-
const day = Number(m?.[2]);
|
|
20
|
-
if (!m || month < 1 || month > 12 || day < 1 || day > MAX_DAYS_IN_MONTH[month - 1]) {
|
|
21
|
-
throw new RangeError(`Invalid month-day: ${JSON.stringify(value)} (expected MM-DD)`);
|
|
22
|
-
}
|
|
23
|
-
return value;
|
|
24
|
-
}
|
|
25
|
-
function compileDates(entries, label) {
|
|
26
|
-
return (entries ?? []).map((e) => {
|
|
27
|
-
if (typeof e === "string") {
|
|
28
|
-
const n = toDayNumber(e);
|
|
29
|
-
return { from: n, to: n, name: undefined };
|
|
30
|
-
}
|
|
31
|
-
if ("date" in e) {
|
|
32
|
-
const n = toDayNumber(e.date);
|
|
33
|
-
return { from: n, to: n, name: e.name };
|
|
34
|
-
}
|
|
35
|
-
const from = toDayNumber(e.from);
|
|
36
|
-
const to = toDayNumber(e.to);
|
|
37
|
-
if (from > to)
|
|
38
|
-
throw new RangeError(`${label}: "from" must not be after "to" (${e.from} > ${e.to})`);
|
|
39
|
-
return { from, to, name: e.name };
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
function compileAnnual(entries) {
|
|
14
|
+
import { addDays, fromDayNumber, toDayNumber } from "./date.js";
|
|
15
|
+
import { parseTimeOfDay, toCivil } from "./input.js";
|
|
16
|
+
import { createSchedule, parseMonthDay, } from "./schedule.js";
|
|
17
|
+
function toDateValues(entries, value) {
|
|
43
18
|
return (entries ?? []).map((e) => {
|
|
44
|
-
if (typeof e === "string")
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const md = parseMonthDay(e.date);
|
|
50
|
-
return { from: md, to: md, name: e.name };
|
|
51
|
-
}
|
|
52
|
-
return { from: parseMonthDay(e.from), to: parseMonthDay(e.to), name: e.name };
|
|
19
|
+
if (typeof e === "string")
|
|
20
|
+
return { date: e, value };
|
|
21
|
+
if ("date" in e)
|
|
22
|
+
return e.name === undefined ? { date: e.date, value } : { ...e, value };
|
|
23
|
+
return e.name === undefined ? { from: e.from, to: e.to, value } : { ...e, value };
|
|
53
24
|
});
|
|
54
25
|
}
|
|
55
|
-
function matchAnnual(rule, monthDay) {
|
|
56
|
-
return rule.from <= rule.to
|
|
57
|
-
? monthDay >= rule.from && monthDay <= rule.to
|
|
58
|
-
: monthDay >= rule.from || monthDay <= rule.to;
|
|
59
|
-
}
|
|
60
26
|
/** 祝日と休業日の規則から営業日カレンダーを作る */
|
|
61
27
|
export function createCalendar(options = {}) {
|
|
62
|
-
const providers = Object.freeze(options.holidays === undefined
|
|
63
|
-
? []
|
|
64
|
-
: Array.isArray(options.holidays)
|
|
65
|
-
? [...options.holidays]
|
|
66
|
-
: [options.holidays]);
|
|
67
|
-
let timeZone;
|
|
68
|
-
if (options.timeZone !== undefined) {
|
|
69
|
-
timeZone = validateTimeZone(options.timeZone);
|
|
70
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
const zones = [...new Set(providers.map((p) => validateTimeZone(p.timeZone)))];
|
|
73
|
-
if (zones.length > 1) {
|
|
74
|
-
throw new RangeError(`Holiday providers use different time zones (${zones.join(", ")}); specify options.timeZone`);
|
|
75
|
-
}
|
|
76
|
-
timeZone = zones[0] ?? "UTC";
|
|
77
|
-
}
|
|
78
28
|
const closedWeekdays = new Set();
|
|
79
29
|
for (const w of options.closedWeekdays ?? []) {
|
|
80
30
|
if (!Number.isInteger(w) || w < 0 || w > 6)
|
|
81
31
|
throw new RangeError(`Invalid weekday: ${w}`);
|
|
82
32
|
closedWeekdays.add(w);
|
|
83
33
|
}
|
|
84
|
-
const closedAnnual = compileAnnual(options.closedAnnual);
|
|
85
|
-
const closedDates = compileDates(options.closedDates, "closedDates");
|
|
86
|
-
const openDates = compileDates(options.openDates, "openDates");
|
|
87
34
|
const holidayPolicy = options.holidayPolicy ?? "closed";
|
|
88
35
|
if (!["closed", "open", "ignore"].includes(holidayPolicy)) {
|
|
89
36
|
throw new RangeError(`Invalid holidayPolicy: ${String(holidayPolicy)}`);
|
|
90
37
|
}
|
|
91
|
-
const outOfRange = options.outOfRange ?? "throw";
|
|
92
38
|
const maxSearchDays = options.maxSearchDays ?? 3660;
|
|
93
39
|
if (!Number.isInteger(maxSearchDays) || maxSearchDays < 1) {
|
|
94
40
|
throw new RangeError(`Invalid maxSearchDays: ${maxSearchDays}`);
|
|
95
41
|
}
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
return map;
|
|
120
|
-
}
|
|
121
|
-
const dateOf = (input) => toCivil(input, timeZone).date;
|
|
122
|
-
function holidayOn(date) {
|
|
123
|
-
if (providers.length === 0)
|
|
124
|
-
return undefined;
|
|
125
|
-
return holidayMap(parseDate(date).year).get(date);
|
|
126
|
-
}
|
|
127
|
-
function findRule(rules, dayNumber) {
|
|
128
|
-
return rules.find((r) => dayNumber >= r.from && dayNumber <= r.to);
|
|
129
|
-
}
|
|
42
|
+
const weekly = [0, 1, 2, 3, 4, 5, 6].map((w) => !closedWeekdays.has(w));
|
|
43
|
+
const sourceOptions = {
|
|
44
|
+
...(options.holidays !== undefined && { holidays: options.holidays }),
|
|
45
|
+
...(options.timeZone !== undefined && { timeZone: options.timeZone }),
|
|
46
|
+
...(options.outOfRange !== undefined && { outOfRange: options.outOfRange }),
|
|
47
|
+
};
|
|
48
|
+
const schedule = createSchedule({
|
|
49
|
+
...sourceOptions,
|
|
50
|
+
weekly,
|
|
51
|
+
holiday: holidayPolicy === "ignore" ? "weekday" : { value: holidayPolicy === "open" },
|
|
52
|
+
annual: (options.closedAnnual ?? []).map((e) => {
|
|
53
|
+
if (typeof e === "string")
|
|
54
|
+
return { date: parseMonthDay(e), value: false };
|
|
55
|
+
if ("date" in e)
|
|
56
|
+
return e.name === undefined ? { date: e.date, value: false } : { ...e, value: false };
|
|
57
|
+
return e.name === undefined
|
|
58
|
+
? { from: e.from, to: e.to, value: false }
|
|
59
|
+
: { ...e, value: false };
|
|
60
|
+
}),
|
|
61
|
+
// 臨時営業日を先に並べて、臨時休業日より優先する
|
|
62
|
+
dates: [...toDateValues(options.openDates, true), ...toDateValues(options.closedDates, false)],
|
|
63
|
+
});
|
|
130
64
|
function info(date) {
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
};
|
|
147
|
-
const open = findRule(openDates, dayNumber);
|
|
148
|
-
if (open)
|
|
149
|
-
return make(true, "open-date", open.name);
|
|
150
|
-
const closed = findRule(closedDates, dayNumber);
|
|
151
|
-
if (closed)
|
|
152
|
-
return make(false, "closed-date", closed.name);
|
|
153
|
-
const monthDay = date.slice(5);
|
|
154
|
-
const annual = closedAnnual.find((r) => matchAnnual(r, monthDay));
|
|
155
|
-
if (annual)
|
|
156
|
-
return make(false, "closed-annual", annual.name);
|
|
157
|
-
if (holiday && holidayPolicy !== "ignore") {
|
|
158
|
-
return make(holidayPolicy === "open", "holiday", holiday.name);
|
|
65
|
+
const day = schedule.dayInfo(date);
|
|
66
|
+
let reason;
|
|
67
|
+
switch (day.reason) {
|
|
68
|
+
case "date":
|
|
69
|
+
reason = day.value ? "open-date" : "closed-date";
|
|
70
|
+
break;
|
|
71
|
+
case "annual":
|
|
72
|
+
reason = "closed-annual";
|
|
73
|
+
break;
|
|
74
|
+
case "holiday":
|
|
75
|
+
reason = "holiday";
|
|
76
|
+
break;
|
|
77
|
+
case "weekday":
|
|
78
|
+
reason = day.value ? "regular" : "closed-weekday";
|
|
79
|
+
break;
|
|
159
80
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
81
|
+
const result = {
|
|
82
|
+
date: day.date,
|
|
83
|
+
weekday: day.weekday,
|
|
84
|
+
businessDay: day.value,
|
|
85
|
+
reason,
|
|
86
|
+
};
|
|
87
|
+
// 曜日で決まった日には名前を付けない(祝日名は holiday で分かる)
|
|
88
|
+
if (day.name !== undefined && day.reason !== "weekday")
|
|
89
|
+
result.name = day.name;
|
|
90
|
+
if (day.holiday !== undefined)
|
|
91
|
+
result.holiday = day.holiday;
|
|
92
|
+
return Object.freeze(result);
|
|
163
93
|
}
|
|
164
|
-
const isOpen = (date) =>
|
|
94
|
+
const isOpen = (date) => schedule.valueOn(date);
|
|
95
|
+
const dateOf = schedule.dateOf;
|
|
165
96
|
function search(start, step) {
|
|
166
97
|
let n = toDayNumber(start);
|
|
167
98
|
for (let i = 0; i < maxSearchDays; i++, n += step) {
|
|
@@ -171,13 +102,6 @@ export function createCalendar(options = {}) {
|
|
|
171
102
|
}
|
|
172
103
|
throw new Error(`No business day found within ${maxSearchDays} days from ${start}`);
|
|
173
104
|
}
|
|
174
|
-
function ordered(from, to) {
|
|
175
|
-
const a = dateOf(from);
|
|
176
|
-
const b = dateOf(to);
|
|
177
|
-
if (a > b)
|
|
178
|
-
throw new RangeError(`"from" must not be after "to" (${a} > ${b})`);
|
|
179
|
-
return [a, b];
|
|
180
|
-
}
|
|
181
105
|
function countOpen(from, toExclusive) {
|
|
182
106
|
let count = 0;
|
|
183
107
|
for (let n = from; n < toExclusive; n++)
|
|
@@ -186,33 +110,21 @@ export function createCalendar(options = {}) {
|
|
|
186
110
|
return count;
|
|
187
111
|
}
|
|
188
112
|
const calendar = {
|
|
189
|
-
timeZone,
|
|
190
|
-
providers,
|
|
113
|
+
timeZone: schedule.timeZone,
|
|
114
|
+
providers: schedule.providers,
|
|
191
115
|
dateOf,
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
return Object.freeze([...holidayMap(year).values()].sort((a, b) => a.date < b.date ? -1 : a.date > b.date ? 1 : 0));
|
|
199
|
-
},
|
|
200
|
-
holidaysBetween(from, to) {
|
|
201
|
-
const [a, b] = ordered(from, to);
|
|
202
|
-
const result = [];
|
|
203
|
-
for (let y = parseDate(a).year; y <= parseDate(b).year; y++) {
|
|
204
|
-
for (const h of calendar.holidaysInYear(y))
|
|
205
|
-
if (h.date >= a && h.date <= b)
|
|
206
|
-
result.push(h);
|
|
207
|
-
}
|
|
208
|
-
return Object.freeze(result);
|
|
209
|
-
},
|
|
116
|
+
civilOf: schedule.civilOf,
|
|
117
|
+
today: schedule.today,
|
|
118
|
+
isHoliday: schedule.isHoliday,
|
|
119
|
+
getHoliday: schedule.getHoliday,
|
|
120
|
+
holidaysInYear: schedule.holidaysInYear,
|
|
121
|
+
holidaysBetween: schedule.holidaysBetween,
|
|
210
122
|
dayInfo: (input) => info(dateOf(input)),
|
|
211
123
|
isBusinessDay: (input) => isOpen(dateOf(input)),
|
|
212
124
|
nextBusinessDay: (input) => search(addDays(dateOf(input), 1), 1),
|
|
213
125
|
previousBusinessDay: (input) => search(addDays(dateOf(input), -1), -1),
|
|
214
126
|
businessDayOnOrAfter(input, opts = {}) {
|
|
215
|
-
const civil = toCivil(input, timeZone);
|
|
127
|
+
const civil = toCivil(input, schedule.timeZone);
|
|
216
128
|
let start = civil.date;
|
|
217
129
|
if (opts.cutoff !== undefined) {
|
|
218
130
|
const cutoff = parseTimeOfDay(opts.cutoff);
|
|
@@ -240,7 +152,10 @@ export function createCalendar(options = {}) {
|
|
|
240
152
|
return a <= b ? countOpen(a, b) : -countOpen(b, a);
|
|
241
153
|
},
|
|
242
154
|
listBusinessDays(from, to) {
|
|
243
|
-
const
|
|
155
|
+
const a = dateOf(from);
|
|
156
|
+
const b = dateOf(to);
|
|
157
|
+
if (a > b)
|
|
158
|
+
throw new RangeError(`"from" must not be after "to" (${a} > ${b})`);
|
|
244
159
|
const result = [];
|
|
245
160
|
for (let n = toDayNumber(a); n <= toDayNumber(b); n++) {
|
|
246
161
|
const date = fromDayNumber(n);
|
package/dist/core/input.d.ts
CHANGED
|
@@ -38,5 +38,10 @@ export declare function instantToCivil(epochMilliseconds: number, timeZone: stri
|
|
|
38
38
|
export declare function toCivil(input: DateInput, timeZone: string): CivilDateTime;
|
|
39
39
|
/** 入力を、タイムゾーンでの暦日 "YYYY-MM-DD" に変換する */
|
|
40
40
|
export declare function toDateString(input: DateInput, timeZone: string): DateString;
|
|
41
|
+
/** 入力を、タイムゾーンでの暦日と時刻("HH:mm"。暦日だけの入力では null)に変換する */
|
|
42
|
+
export declare function toDateTimeParts(input: DateInput, timeZone: string): {
|
|
43
|
+
readonly date: DateString;
|
|
44
|
+
readonly time: string | null;
|
|
45
|
+
};
|
|
41
46
|
/** "HH:mm" を 0 時からの経過分に変換する(00:00〜23:59) */
|
|
42
47
|
export declare function parseTimeOfDay(value: string): number;
|
package/dist/core/input.js
CHANGED
|
@@ -120,6 +120,14 @@ export function toCivil(input, timeZone) {
|
|
|
120
120
|
export function toDateString(input, timeZone) {
|
|
121
121
|
return toCivil(input, validateTimeZone(timeZone)).date;
|
|
122
122
|
}
|
|
123
|
+
/** 入力を、タイムゾーンでの暦日と時刻("HH:mm"。暦日だけの入力では null)に変換する */
|
|
124
|
+
export function toDateTimeParts(input, timeZone) {
|
|
125
|
+
const civil = toCivil(input, validateTimeZone(timeZone));
|
|
126
|
+
const time = civil.minutes === null
|
|
127
|
+
? null
|
|
128
|
+
: `${String(Math.floor(civil.minutes / 60)).padStart(2, "0")}:${String(civil.minutes % 60).padStart(2, "0")}`;
|
|
129
|
+
return Object.freeze({ date: civil.date, time });
|
|
130
|
+
}
|
|
123
131
|
/** "HH:mm" を 0 時からの経過分に変換する(00:00〜23:59) */
|
|
124
132
|
export function parseTimeOfDay(value) {
|
|
125
133
|
const m = /^(\d{2}):(\d{2})$/.exec(value);
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 日ごとに値を返すスケジュール(営業時間・受付枠など、曜日・祝日・特定日で変わるもの)。
|
|
3
|
+
*
|
|
4
|
+
* 1 日の値は次の優先順位で決まる(上が優先):
|
|
5
|
+
* 1. dates … 個別の日付・期間(配列で先に書いたものが優先)
|
|
6
|
+
* 2. annual … 毎年の月日・月日の期間(年をまたぐ期間も可。配列で先に書いたものが優先)
|
|
7
|
+
* 3. 祝日 … holiday の規則("weekday": 曜日どおり / { sameAs: 曜日 }: その曜日の値 / { value }: 指定の値)
|
|
8
|
+
* 4. weekly … 曜日ごとの値
|
|
9
|
+
*
|
|
10
|
+
* 営業日カレンダー(createCalendar)は、このスケジュールの真偽値版として作られている。
|
|
11
|
+
*/
|
|
12
|
+
import { type DateString, type Weekday } from "./date.ts";
|
|
13
|
+
import { type DateInput } from "./input.ts";
|
|
14
|
+
import type { Holiday, HolidayProvider } from "./types.ts";
|
|
15
|
+
/** 祝日の値の決め方 */
|
|
16
|
+
export type HolidayRule<T> =
|
|
17
|
+
/** 祝日を考えず、曜日の値を使う */
|
|
18
|
+
"weekday"
|
|
19
|
+
/** 指定した曜日の値を使う(例: 祝日は日曜日の営業時間) */
|
|
20
|
+
| {
|
|
21
|
+
readonly sameAs: Weekday;
|
|
22
|
+
}
|
|
23
|
+
/** 指定した値を使う */
|
|
24
|
+
| {
|
|
25
|
+
readonly value: T;
|
|
26
|
+
};
|
|
27
|
+
/** 毎年の月日 "MM-DD" の値 */
|
|
28
|
+
export interface AnnualValue<T> {
|
|
29
|
+
readonly date: string;
|
|
30
|
+
readonly value: T;
|
|
31
|
+
readonly name?: string;
|
|
32
|
+
}
|
|
33
|
+
/** 毎年の月日の期間の値(from > to なら年をまたぐ。例: 12-31 〜 01-03) */
|
|
34
|
+
export interface AnnualRangeValue<T> {
|
|
35
|
+
readonly from: string;
|
|
36
|
+
readonly to: string;
|
|
37
|
+
readonly value: T;
|
|
38
|
+
readonly name?: string;
|
|
39
|
+
}
|
|
40
|
+
/** 個別の日付 "YYYY-MM-DD" の値 */
|
|
41
|
+
export interface DateValue<T> {
|
|
42
|
+
readonly date: DateString;
|
|
43
|
+
readonly value: T;
|
|
44
|
+
readonly name?: string;
|
|
45
|
+
}
|
|
46
|
+
/** 個別の日付の期間(両端を含む)の値 */
|
|
47
|
+
export interface DateRangeValue<T> {
|
|
48
|
+
readonly from: DateString;
|
|
49
|
+
readonly to: DateString;
|
|
50
|
+
readonly value: T;
|
|
51
|
+
readonly name?: string;
|
|
52
|
+
}
|
|
53
|
+
/** 祝日の提供元とタイムゾーンの設定(スケジュールと営業日カレンダーで共通) */
|
|
54
|
+
export interface HolidaySourceOptions<Kind extends string = string> {
|
|
55
|
+
/** 祝日の提供元(例: jp)。複数渡すと和集合になる */
|
|
56
|
+
readonly holidays?: HolidayProvider<Kind> | readonly HolidayProvider<Kind>[];
|
|
57
|
+
/**
|
|
58
|
+
* 日時の入力(Date・オフセット付き文字列など)を暦日に変換するタイムゾーン。
|
|
59
|
+
* 省略時は祝日の提供元のタイムゾーン、提供元もなければ "UTC"
|
|
60
|
+
*/
|
|
61
|
+
readonly timeZone?: string;
|
|
62
|
+
/** 祝日の提供元の対応年を外れた日付の扱い(既定: "throw") */
|
|
63
|
+
readonly outOfRange?: "throw" | "ignore";
|
|
64
|
+
}
|
|
65
|
+
export interface ScheduleOptions<T, Kind extends string = string> extends HolidaySourceOptions<Kind> {
|
|
66
|
+
/** 曜日ごとの値([日, 月, 火, 水, 木, 金, 土]) */
|
|
67
|
+
readonly weekly: readonly [T, T, T, T, T, T, T];
|
|
68
|
+
/** 祝日の値の決め方(既定: "weekday") */
|
|
69
|
+
readonly holiday?: HolidayRule<T>;
|
|
70
|
+
/** 毎年の月日の値 */
|
|
71
|
+
readonly annual?: readonly (AnnualValue<T> | AnnualRangeValue<T>)[];
|
|
72
|
+
/** 個別の日付の値 */
|
|
73
|
+
readonly dates?: readonly (DateValue<T> | DateRangeValue<T>)[];
|
|
74
|
+
}
|
|
75
|
+
/** 値を決めた規則 */
|
|
76
|
+
export type ScheduleReason = "date" | "annual" | "holiday" | "weekday";
|
|
77
|
+
/** 1 日分の値と、その値を決めた規則 */
|
|
78
|
+
export interface ScheduleDay<T, Kind extends string = string> {
|
|
79
|
+
readonly date: DateString;
|
|
80
|
+
readonly weekday: Weekday;
|
|
81
|
+
readonly value: T;
|
|
82
|
+
/** 値を決めた規則 */
|
|
83
|
+
readonly reason: ScheduleReason;
|
|
84
|
+
/** 値を決めた規則の名前(dates・annual の name、祝日なら祝日名) */
|
|
85
|
+
readonly name?: string;
|
|
86
|
+
/** その日が祝日なら祝日(holiday の規則に関係なく付く) */
|
|
87
|
+
readonly holiday?: Holiday<Kind>;
|
|
88
|
+
}
|
|
89
|
+
/** 暦日と時刻(カレンダーのタイムゾーン) */
|
|
90
|
+
export interface CivilDateTimeParts {
|
|
91
|
+
/** 暦日 "YYYY-MM-DD" */
|
|
92
|
+
readonly date: DateString;
|
|
93
|
+
/** 時刻 "HH:mm"。暦日だけの入力では null */
|
|
94
|
+
readonly time: string | null;
|
|
95
|
+
}
|
|
96
|
+
/** 祝日を調べる機能(スケジュールと営業日カレンダーで共通) */
|
|
97
|
+
export interface HolidayLookup<Kind extends string = string> {
|
|
98
|
+
/** 日時の入力を暦日に変換するタイムゾーン */
|
|
99
|
+
readonly timeZone: string;
|
|
100
|
+
/** 祝日の提供元 */
|
|
101
|
+
readonly providers: readonly HolidayProvider<Kind>[];
|
|
102
|
+
/** 入力をタイムゾーンでの暦日に変換する */
|
|
103
|
+
dateOf(input: DateInput): DateString;
|
|
104
|
+
/** 入力をタイムゾーンでの暦日と時刻に変換する */
|
|
105
|
+
civilOf(input: DateInput): CivilDateTimeParts;
|
|
106
|
+
/** 今日の日付(タイムゾーン) */
|
|
107
|
+
today(now?: Date): DateString;
|
|
108
|
+
/** 祝日か */
|
|
109
|
+
isHoliday(input: DateInput): boolean;
|
|
110
|
+
/** 祝日なら祝日を返す */
|
|
111
|
+
getHoliday(input: DateInput): Holiday<Kind> | undefined;
|
|
112
|
+
/** その年の祝日 */
|
|
113
|
+
holidaysInYear(year: number): readonly Holiday<Kind>[];
|
|
114
|
+
/** 期間内の祝日(両端を含む) */
|
|
115
|
+
holidaysBetween(from: DateInput, to: DateInput): readonly Holiday<Kind>[];
|
|
116
|
+
}
|
|
117
|
+
export interface Schedule<T, Kind extends string = string> extends HolidayLookup<Kind> {
|
|
118
|
+
/** その日の値 */
|
|
119
|
+
valueOn(input: DateInput): T;
|
|
120
|
+
/** その日の値と、値を決めた規則 */
|
|
121
|
+
dayInfo(input: DateInput): ScheduleDay<T, Kind>;
|
|
122
|
+
}
|
|
123
|
+
/** "MM-DD" を検証する(2 月 29 日は可) */
|
|
124
|
+
export declare function parseMonthDay(value: string): string;
|
|
125
|
+
/** 祝日を調べる機能を作る(スケジュールと営業日カレンダーで共通) */
|
|
126
|
+
export declare function createHolidayLookup<Kind extends string>(options: HolidaySourceOptions<Kind>): HolidayLookup<Kind> & {
|
|
127
|
+
holidayOn(date: DateString): Holiday<Kind> | undefined;
|
|
128
|
+
};
|
|
129
|
+
/** 曜日・祝日・特定日の規則から、日ごとに値を返すスケジュールを作る */
|
|
130
|
+
export declare function createSchedule<T, Kind extends string = string>(options: ScheduleOptions<T, Kind>): Schedule<T, Kind>;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 日ごとに値を返すスケジュール(営業時間・受付枠など、曜日・祝日・特定日で変わるもの)。
|
|
3
|
+
*
|
|
4
|
+
* 1 日の値は次の優先順位で決まる(上が優先):
|
|
5
|
+
* 1. dates … 個別の日付・期間(配列で先に書いたものが優先)
|
|
6
|
+
* 2. annual … 毎年の月日・月日の期間(年をまたぐ期間も可。配列で先に書いたものが優先)
|
|
7
|
+
* 3. 祝日 … holiday の規則("weekday": 曜日どおり / { sameAs: 曜日 }: その曜日の値 / { value }: 指定の値)
|
|
8
|
+
* 4. weekly … 曜日ごとの値
|
|
9
|
+
*
|
|
10
|
+
* 営業日カレンダー(createCalendar)は、このスケジュールの真偽値版として作られている。
|
|
11
|
+
*/
|
|
12
|
+
import { parseDate, toDayNumber, weekdayOf } from "./date.js";
|
|
13
|
+
import { instantToCivil, toCivil, toDateTimeParts, validateTimeZone, } from "./input.js";
|
|
14
|
+
// ------------------------------------------------------------
|
|
15
|
+
// 内部の共通処理
|
|
16
|
+
// ------------------------------------------------------------
|
|
17
|
+
const MONTH_DAY_RE = /^(\d{2})-(\d{2})$/;
|
|
18
|
+
const MAX_DAYS_IN_MONTH = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
19
|
+
/** "MM-DD" を検証する(2 月 29 日は可) */
|
|
20
|
+
export function parseMonthDay(value) {
|
|
21
|
+
const m = MONTH_DAY_RE.exec(value);
|
|
22
|
+
const month = Number(m?.[1]);
|
|
23
|
+
const day = Number(m?.[2]);
|
|
24
|
+
if (!m || month < 1 || month > 12 || day < 1 || day > MAX_DAYS_IN_MONTH[month - 1]) {
|
|
25
|
+
throw new RangeError(`Invalid month-day: ${JSON.stringify(value)} (expected MM-DD)`);
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
function compileDates(entries, label) {
|
|
30
|
+
return (entries ?? []).map((e) => {
|
|
31
|
+
if ("date" in e) {
|
|
32
|
+
const n = toDayNumber(e.date);
|
|
33
|
+
return { from: n, to: n, value: e.value, name: e.name };
|
|
34
|
+
}
|
|
35
|
+
const from = toDayNumber(e.from);
|
|
36
|
+
const to = toDayNumber(e.to);
|
|
37
|
+
if (from > to) {
|
|
38
|
+
throw new RangeError(`${label}: "from" must not be after "to" (${e.from} > ${e.to})`);
|
|
39
|
+
}
|
|
40
|
+
return { from, to, value: e.value, name: e.name };
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function compileAnnual(entries) {
|
|
44
|
+
return (entries ?? []).map((e) => {
|
|
45
|
+
if ("date" in e) {
|
|
46
|
+
const md = parseMonthDay(e.date);
|
|
47
|
+
return { from: md, to: md, value: e.value, name: e.name };
|
|
48
|
+
}
|
|
49
|
+
return { from: parseMonthDay(e.from), to: parseMonthDay(e.to), value: e.value, name: e.name };
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function matchAnnual(rule, monthDay) {
|
|
53
|
+
return rule.from <= rule.to
|
|
54
|
+
? monthDay >= rule.from && monthDay <= rule.to
|
|
55
|
+
: monthDay >= rule.from || monthDay <= rule.to;
|
|
56
|
+
}
|
|
57
|
+
/** 祝日を調べる機能を作る(スケジュールと営業日カレンダーで共通) */
|
|
58
|
+
export function createHolidayLookup(options) {
|
|
59
|
+
const providers = Object.freeze(options.holidays === undefined
|
|
60
|
+
? []
|
|
61
|
+
: Array.isArray(options.holidays)
|
|
62
|
+
? [...options.holidays]
|
|
63
|
+
: [options.holidays]);
|
|
64
|
+
let timeZone;
|
|
65
|
+
if (options.timeZone !== undefined) {
|
|
66
|
+
timeZone = validateTimeZone(options.timeZone);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
const zones = [...new Set(providers.map((p) => validateTimeZone(p.timeZone)))];
|
|
70
|
+
if (zones.length > 1) {
|
|
71
|
+
throw new RangeError(`Holiday providers use different time zones (${zones.join(", ")}); specify options.timeZone`);
|
|
72
|
+
}
|
|
73
|
+
timeZone = zones[0] ?? "UTC";
|
|
74
|
+
}
|
|
75
|
+
const outOfRange = options.outOfRange ?? "throw";
|
|
76
|
+
const yearCache = new Map();
|
|
77
|
+
function providerYears(provider, year) {
|
|
78
|
+
const { from, to } = provider.supportedYears;
|
|
79
|
+
if (year < from || year > to) {
|
|
80
|
+
if (outOfRange === "ignore")
|
|
81
|
+
return [];
|
|
82
|
+
throw new RangeError(`Holidays for ${provider.id} are supported for ${from}-${to}; requested ${year}`);
|
|
83
|
+
}
|
|
84
|
+
return provider.holidaysInYear(year);
|
|
85
|
+
}
|
|
86
|
+
function holidayMap(year) {
|
|
87
|
+
let map = yearCache.get(year);
|
|
88
|
+
if (!map) {
|
|
89
|
+
map = new Map();
|
|
90
|
+
for (const provider of providers) {
|
|
91
|
+
for (const h of providerYears(provider, year)) {
|
|
92
|
+
// 同じ日に複数の提供元の祝日がある場合は、先に渡した提供元を優先する
|
|
93
|
+
if (!map.has(h.date))
|
|
94
|
+
map.set(h.date, h);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
yearCache.set(year, map);
|
|
98
|
+
}
|
|
99
|
+
return map;
|
|
100
|
+
}
|
|
101
|
+
function holidayOn(date) {
|
|
102
|
+
if (providers.length === 0)
|
|
103
|
+
return undefined;
|
|
104
|
+
return holidayMap(parseDate(date).year).get(date);
|
|
105
|
+
}
|
|
106
|
+
const dateOf = (input) => toCivil(input, timeZone).date;
|
|
107
|
+
function holidaysInYear(year) {
|
|
108
|
+
if (!Number.isInteger(year))
|
|
109
|
+
throw new RangeError(`Invalid year: ${year}`);
|
|
110
|
+
return Object.freeze([...holidayMap(year).values()].sort((a, b) => a.date < b.date ? -1 : a.date > b.date ? 1 : 0));
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
timeZone,
|
|
114
|
+
providers,
|
|
115
|
+
holidayOn,
|
|
116
|
+
dateOf,
|
|
117
|
+
civilOf: (input) => toDateTimeParts(input, timeZone),
|
|
118
|
+
today: (now = new Date()) => instantToCivil(now.getTime(), timeZone).date,
|
|
119
|
+
isHoliday: (input) => holidayOn(dateOf(input)) !== undefined,
|
|
120
|
+
getHoliday: (input) => holidayOn(dateOf(input)),
|
|
121
|
+
holidaysInYear,
|
|
122
|
+
holidaysBetween(from, to) {
|
|
123
|
+
const a = dateOf(from);
|
|
124
|
+
const b = dateOf(to);
|
|
125
|
+
if (a > b)
|
|
126
|
+
throw new RangeError(`"from" must not be after "to" (${a} > ${b})`);
|
|
127
|
+
const result = [];
|
|
128
|
+
for (let y = parseDate(a).year; y <= parseDate(b).year; y++) {
|
|
129
|
+
for (const h of holidaysInYear(y))
|
|
130
|
+
if (h.date >= a && h.date <= b)
|
|
131
|
+
result.push(h);
|
|
132
|
+
}
|
|
133
|
+
return Object.freeze(result);
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
// ------------------------------------------------------------
|
|
138
|
+
// createSchedule
|
|
139
|
+
// ------------------------------------------------------------
|
|
140
|
+
/** 曜日・祝日・特定日の規則から、日ごとに値を返すスケジュールを作る */
|
|
141
|
+
export function createSchedule(options) {
|
|
142
|
+
const weekly = options.weekly;
|
|
143
|
+
if (!Array.isArray(weekly) || weekly.length !== 7) {
|
|
144
|
+
throw new RangeError("weekly must have 7 values ([Sunday, Monday, ..., Saturday])");
|
|
145
|
+
}
|
|
146
|
+
const holidayRule = options.holiday ?? "weekday";
|
|
147
|
+
if (typeof holidayRule === "object" && "sameAs" in holidayRule) {
|
|
148
|
+
const w = holidayRule.sameAs;
|
|
149
|
+
if (!Number.isInteger(w) || w < 0 || w > 6)
|
|
150
|
+
throw new RangeError(`Invalid weekday: ${w}`);
|
|
151
|
+
}
|
|
152
|
+
else if (holidayRule !== "weekday" &&
|
|
153
|
+
!(typeof holidayRule === "object" && "value" in holidayRule)) {
|
|
154
|
+
throw new RangeError(`Invalid holiday rule: ${JSON.stringify(holidayRule)}`);
|
|
155
|
+
}
|
|
156
|
+
const dates = compileDates(options.dates, "dates");
|
|
157
|
+
const annual = compileAnnual(options.annual);
|
|
158
|
+
const lookup = createHolidayLookup(options);
|
|
159
|
+
function info(date) {
|
|
160
|
+
const dayNumber = toDayNumber(date);
|
|
161
|
+
const weekday = weekdayOf(date);
|
|
162
|
+
const holiday = lookup.holidayOn(date);
|
|
163
|
+
const make = (value, reason, name) => {
|
|
164
|
+
const result = {
|
|
165
|
+
date,
|
|
166
|
+
weekday,
|
|
167
|
+
value,
|
|
168
|
+
reason,
|
|
169
|
+
};
|
|
170
|
+
if (name !== undefined)
|
|
171
|
+
result.name = name;
|
|
172
|
+
if (holiday !== undefined)
|
|
173
|
+
result.holiday = holiday;
|
|
174
|
+
return Object.freeze(result);
|
|
175
|
+
};
|
|
176
|
+
const byDate = dates.find((r) => dayNumber >= r.from && dayNumber <= r.to);
|
|
177
|
+
if (byDate)
|
|
178
|
+
return make(byDate.value, "date", byDate.name);
|
|
179
|
+
const monthDay = date.slice(5);
|
|
180
|
+
const byAnnual = annual.find((r) => matchAnnual(r, monthDay));
|
|
181
|
+
if (byAnnual)
|
|
182
|
+
return make(byAnnual.value, "annual", byAnnual.name);
|
|
183
|
+
if (holiday && holidayRule !== "weekday") {
|
|
184
|
+
const value = "sameAs" in holidayRule ? weekly[holidayRule.sameAs] : holidayRule.value;
|
|
185
|
+
return make(value, "holiday", holiday.name);
|
|
186
|
+
}
|
|
187
|
+
return make(weekly[weekday], "weekday", undefined);
|
|
188
|
+
}
|
|
189
|
+
const schedule = {
|
|
190
|
+
timeZone: lookup.timeZone,
|
|
191
|
+
providers: lookup.providers,
|
|
192
|
+
dateOf: lookup.dateOf,
|
|
193
|
+
civilOf: lookup.civilOf,
|
|
194
|
+
today: lookup.today,
|
|
195
|
+
isHoliday: lookup.isHoliday,
|
|
196
|
+
getHoliday: lookup.getHoliday,
|
|
197
|
+
holidaysInYear: lookup.holidaysInYear,
|
|
198
|
+
holidaysBetween: lookup.holidaysBetween,
|
|
199
|
+
valueOn: (input) => info(lookup.dateOf(input)).value,
|
|
200
|
+
dayInfo: (input) => info(lookup.dateOf(input)),
|
|
201
|
+
};
|
|
202
|
+
return Object.freeze(schedule);
|
|
203
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 祝日と、会社・店舗の休業日を組み合わせた営業日カレンダー(createCalendar)と、
|
|
3
|
+
* 曜日・祝日・特定日で変わる値(営業時間など)のスケジュール(createSchedule)。
|
|
3
4
|
*
|
|
4
5
|
* 国ごとの祝日はサブパスから読み込む(例: "bizdays-toolkit/jp")。
|
|
5
6
|
*/
|
|
6
7
|
export { createCalendar, type AnnualDate, type AnnualRange, type BusinessDayOptions, type Calendar, type CalendarOptions, type DateRange, type DayInfo, type DayReason, type HolidayPolicy, type SpecificDate, } from "./core/calendar.ts";
|
|
8
|
+
export { createSchedule, type AnnualRangeValue, type AnnualValue, type CivilDateTimeParts, type DateRangeValue, type DateValue, type HolidayLookup, type HolidayRule, type HolidaySourceOptions, type Schedule, type ScheduleDay, type ScheduleOptions, type ScheduleReason, } from "./core/schedule.ts";
|
|
7
9
|
export { addDays, diffDays, isValidDate, nthWeekdayOfMonth, weekdayOf, type DateString, type Weekday, } from "./core/date.ts";
|
|
8
|
-
export { toDateString, type DateInput, type TemporalDateLike, type TemporalInstantLike, } from "./core/input.ts";
|
|
10
|
+
export { toDateString, toDateTimeParts, type DateInput, type TemporalDateLike, type TemporalInstantLike, } from "./core/input.ts";
|
|
9
11
|
export type { Holiday, HolidayProvider } from "./core/types.ts";
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 祝日と、会社・店舗の休業日を組み合わせた営業日カレンダー(createCalendar)と、
|
|
3
|
+
* 曜日・祝日・特定日で変わる値(営業時間など)のスケジュール(createSchedule)。
|
|
3
4
|
*
|
|
4
5
|
* 国ごとの祝日はサブパスから読み込む(例: "bizdays-toolkit/jp")。
|
|
5
6
|
*/
|
|
6
7
|
export { createCalendar, } from "./core/calendar.js";
|
|
8
|
+
export { createSchedule, } from "./core/schedule.js";
|
|
7
9
|
export { addDays, diffDays, isValidDate, nthWeekdayOfMonth, weekdayOf, } from "./core/date.js";
|
|
8
|
-
export { toDateString, } from "./core/input.js";
|
|
10
|
+
export { toDateString, toDateTimeParts, } from "./core/input.js";
|
package/dist/jp/index.d.ts
CHANGED
|
@@ -30,6 +30,11 @@ export declare function holidaysInYear(year: number): readonly JapaneseHoliday[]
|
|
|
30
30
|
export declare function holidaysBetween(from: DateInput, to: DateInput): readonly JapaneseHoliday[];
|
|
31
31
|
/** 日本時間での暦日 */
|
|
32
32
|
export declare function toJapanDate(input: DateInput): DateString;
|
|
33
|
+
/** 日本時間での暦日と時刻("HH:mm"。暦日だけの入力では null) */
|
|
34
|
+
export declare function toJapanDateTime(input: DateInput): {
|
|
35
|
+
readonly date: DateString;
|
|
36
|
+
readonly time: string | null;
|
|
37
|
+
};
|
|
33
38
|
/** 反映済みの官報の年月 */
|
|
34
39
|
export declare const dataVersion: string;
|
|
35
40
|
/** 春分・秋分の日が官報で確定している最後の年 */
|
package/dist/jp/index.js
CHANGED
|
@@ -52,6 +52,10 @@ export function holidaysBetween(from, to) {
|
|
|
52
52
|
export function toJapanDate(input) {
|
|
53
53
|
return calendar().dateOf(input);
|
|
54
54
|
}
|
|
55
|
+
/** 日本時間での暦日と時刻("HH:mm"。暦日だけの入力では null) */
|
|
56
|
+
export function toJapanDateTime(input) {
|
|
57
|
+
return calendar().civilOf(input);
|
|
58
|
+
}
|
|
55
59
|
/** 反映済みの官報の年月 */
|
|
56
60
|
export const dataVersion = DATA_VERSION;
|
|
57
61
|
/** 春分・秋分の日が官報で確定している最後の年 */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bizdays-toolkit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Holidays
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|
|
7
7
|
"business-day",
|