bizdays-toolkit 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,25 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.6.0] - 2026-10-16
8
+
9
+ ### Added
10
+
11
+ - 日付の調整 `adjust(input, { convention })`(営業日カレンダー): 休業日を `"following"`(翌営業日)・`"preceding"`(前営業日)・`"modified-following"`(翌営業日。月が変わるなら前営業日)・`"modified-preceding"`(前営業日。月が変わるなら翌営業日)で調整する。月が変わるかは調整する前の日付の月と比べる
12
+ - 月の営業日(営業日カレンダー): `nthBusinessDayOfMonth(month, n)`(負なら月末から数える。-1 は最終営業日)・`firstBusinessDayOfMonth(month)`・`lastBusinessDayOfMonth(month)`
13
+ - 月の期日 `dueDate(month, { day, convention })`(営業日カレンダー): その月の `day` 日(-1 は末日。その月にない日は末日に読み替える)を `convention` で調整する
14
+ - 月の計算(ルート): `monthOf(dateOrMonth, offset?)`(n か月後の月 `"YYYY-MM"`)・`closingMonth(date, closingDay)`(締め日で区切ったときの月)
15
+ - 例外のコード `invalid-month`(月 `"YYYY-MM"` の誤り)
16
+ - 型: `BusinessDayConvention`・`AdjustOptions`・`DueDateOptions`・`MonthString`
17
+
18
+ ### 開発
19
+
20
+ - `pnpm check`(`scripts/check-package.ts`)で、CHANGELOG の版の節の並び・日付と、末尾のリンク定義が節とそろっていることを確かめるようにした(0.5.0 でリンク定義の更新漏れがあったため)
21
+ - リリースのワークフローで、CHANGELOG の日付が `YYYY-MM-DD` のまま・見出しがないときの表示を分かりやすくした(止まる条件は変わらない)
22
+ - 月の計算と日付の調整の性質のテストを追加
23
+ - サイズの上限を上げた(`.` 16500・`./jp` 19500・`./diagnose` 22000。月の計算の追加。minify + gzip ではルートが約 7.2KB、日本の祝日込みで約 8.3KB)
24
+ - `docs/decisions.md` に 16 番目(日付の調整と月の期日の設計)を追加
25
+
7
26
  ## [0.5.0] - 2026-10-16
8
27
 
9
28
  ### Added
@@ -129,7 +148,8 @@
129
148
 
130
149
  - 日本: 2026 年 2 月の官報(2027 年の春分・秋分の日)まで反映(`dataVersion: "2026-02"`、`confirmedThrough: 2027`)
131
150
 
132
- [Unreleased]: https://github.com/inohan77/bizdays-toolkit/compare/v0.5.0...HEAD
151
+ [Unreleased]: https://github.com/inohan77/bizdays-toolkit/compare/v0.6.0...HEAD
152
+ [0.6.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.5.0...v0.6.0
133
153
  [0.5.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.4.0...v0.5.0
134
154
  [0.4.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.3.1...v0.4.0
135
155
  [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 で約 6.5KB(日本の祝日込みで約 7.5KB)。ESM・型定義同梱・`sideEffects: false` で、Node.js・ブラウザー・Edge ランタイムで動作します
26
+ - **軽量・依存ゼロ**: minify + gzip で約 7KB(日本の祝日込みで約 8.3KB)。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` は、曜日・祝日・特定日の規則から、日ごとに任意の値を返します。値の型は自由なので、営業時間のほか、受付枠や配送料の区分などにも使えます。
@@ -274,7 +325,7 @@ try {
274
325
  }
275
326
  ```
276
327
 
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`。将来の版でコードが増えることがあります。
328
+ 主なコード: `invalid-date`・`invalid-month`・`invalid-month-day`・`invalid-time`・`invalid-range`・`invalid-weekday`・`invalid-time-zone`・`invalid-option`・`out-of-range`・`invalid-provider`・`invalid-provider-data`・`no-business-day`。将来の版でコードが増えることがあります。
278
329
 
279
330
  ### 設定の診断
280
331
 
@@ -362,9 +413,10 @@ A business-day calendar that combines public holidays with company or store clos
362
413
  - **Day-by-day schedules**: `createSchedule` returns any value per day (e.g. store hours) from weekly values, a holiday rule (`"weekday"`, `{ sameAs: 0 }`, or `{ value }`), annual dates and specific dates
363
414
  - **Recurring rules**: weekly, nth-weekday-of-month (`{ weekday: 3, nth: 3 }`) and day-of-month (`{ day: -1 }`) rules, with per-rule holiday handling (`onHolidayRule`: `"under-holiday"`, `"over-holiday"`, or `"after-holiday"` to move the rule to the next non-holiday, e.g. "closed on Mondays; if Monday is a holiday, closed on the next weekday")
364
415
  - **Option diagnostics**: `diagnoseCalendar` / `diagnoseSchedule` from `bizdays-toolkit/diagnose` return a list of problems without throwing: rules that can never apply (`shadowed`, `never-matches`), rules whose removal changes nothing (`redundant`), typos in option keys with a suggestion (`unknown-option`), dates outside the holiday provider's years (`out-of-range`), and calendars with no business day. Use `code` to branch; `message` is for humans
416
+ - **Month-based due dates**: `adjust` with `following` / `preceding` / `modified-following` / `modified-preceding`, the nth / first / last business day of a month, `dueDate(month, { day, convention })`, and the calendar helpers `closingMonth` and `monthOf`
365
417
  - **Business-day arithmetic**: closed weekdays, annual closures (including ranges across the new year), specific closed/open dates, holiday policy, `addBusinessDays`, cut-off times, and more
366
418
  - **Time-zone safe**: instants (`Date`, ISO strings with offsets, Temporal) are converted to a civil date in the calendar's time zone before evaluation
367
- - **Lightweight, zero dependencies**: about 6.5 KB min+gzip (about 7.5 KB with Japanese holidays), ESM, bundled types, `sideEffects: false`
419
+ - **Lightweight, zero dependencies**: about 7 KB min+gzip (about 8.3 KB with Japanese holidays), ESM, bundled types, `sideEffects: false`
368
420
  - **Extensible** through the `HolidayProvider` interface (only Japan is bundled for now)
369
421
 
370
422
  ```ts
@@ -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 CompiledSchedule, type RecurringRule, type HolidayLookup, type HolidaySourceOptions } from "./schedule.ts";
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>;
@@ -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
  }
@@ -12,6 +12,8 @@
12
12
  export type KnownErrorCode =
13
13
  /** 日付 "YYYY-MM-DD" の形式・値の誤り */
14
14
  "invalid-date"
15
+ /** 月 "YYYY-MM" の形式・値の誤り */
16
+ | "invalid-month"
15
17
  /** 毎年の月日 "MM-DD" の形式・値の誤り */
16
18
  | "invalid-month-day"
17
19
  /** 時刻・日時の形式・値の誤り */
@@ -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/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.5.0",
3
+ "version": "0.6.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",