jpholiday/lib.rs
1//! # jpholiday
2//!
3//! 日本の祝日を判定する依存ゼロの Rust ライブラリです。
4//! [内閣府が公表しているデータ](https://www8.cao.go.jp/chosei/shukujitsu/gaiyou.html)
5//! に基づき、振替休日・国民の休日・春分/秋分の日(天文計算)まで再現します。
6//!
7//! **2027 年**までの祝日は公式発表に基づいて動作確認済みです。それ以降も取得できますが、
8//! 正式な公表がないため正確性は保証されません。
9//!
10//! ## 関数 API
11//!
12//! ```
13//! use jpholiday::Date;
14//!
15//! // 指定日が祝日か
16//! assert!(jpholiday::is_holiday(Date::new(2017, 1, 1).unwrap()));
17//! assert!(!jpholiday::is_holiday(Date::new(2017, 1, 3).unwrap()));
18//!
19//! // 祝日名(振替休日も含む)
20//! assert_eq!(
21//! jpholiday::is_holiday_name(Date::new(2017, 1, 2).unwrap()).as_deref(),
22//! Some("元日 振替休日")
23//! );
24//! assert_eq!(jpholiday::is_holiday_name(Date::new(2017, 1, 3).unwrap()), None);
25//!
26//! // その年・月・範囲の祝日一覧
27//! let year = jpholiday::year_holidays(2017);
28//! assert_eq!(year[0], (Date::new(2017, 1, 1).unwrap(), "元日".to_string()));
29//! ```
30//!
31//! ## クラス API
32//!
33//! ```
34//! use jpholiday::{Date, JPHoliday};
35//!
36//! let jp = JPHoliday::new();
37//! let holidays = jp.holidays(Date::new(2020, 1, 1).unwrap());
38//! assert_eq!(holidays[0].name, "元日");
39//! ```
40//!
41//! ## 独自の祝日を追加する
42//!
43//! ```
44//! use jpholiday::{Date, JPHoliday, OriginalHolidayChecker};
45//!
46//! struct CompanyHoliday;
47//! impl OriginalHolidayChecker for CompanyHoliday {
48//! fn is_holiday(&self, date: Date) -> bool {
49//! date == Date::new(2020, 2, 9).unwrap()
50//! }
51//! fn holiday_name(&self, _date: Date) -> String {
52//! "特別休暇".to_string()
53//! }
54//! }
55//!
56//! let mut jp = JPHoliday::new();
57//! jp.register(CompanyHoliday);
58//! assert!(jp.is_holiday(Date::new(2020, 2, 9).unwrap()));
59//! jp.unregister::<CompanyHoliday>();
60//! assert!(!jp.is_holiday(Date::new(2020, 2, 9).unwrap()));
61//! ```
62
63#![forbid(unsafe_code)]
64#![warn(missing_docs)]
65
66pub mod astronomy;
67mod checker;
68mod date;
69mod error;
70mod jpholiday;
71mod model;
72mod registry;
73
74pub use checker::OriginalHolidayChecker;
75pub use date::{Date, days_in_month, is_leap_year};
76pub use error::DateError;
77pub use jpholiday::JPHoliday;
78pub use model::Holiday;
79
80use crate::checker::{Checker, compute_holidays};
81use crate::registry::HolidayCheckerRegistry;
82use std::sync::{Mutex, MutexGuard, OnceLock, PoisonError};
83
84/// 関数 API が共有するグローバルなチェッカーレジストリ。
85///
86/// スレッド安全性のため [`Mutex`] で保護しており、[`register`]/[`unregister`] による
87/// 登録状態は全関数で共有されます。
88///
89/// グローバル状態として保持するのは「レジストリ(チェッカーの並び)」のみで、祝日計算の結果は
90/// キャッシュしません。これにより、範囲問い合わせ([`between`] 等)でプロセス寿命のキャッシュが
91/// 際限なく増えること(メモリ枯渇)を防ぎます。日付単位のキャッシュが必要なら [`JPHoliday`]
92/// インスタンスを生成して利用してください(インスタンスを drop すればキャッシュも解放されます)。
93fn registry() -> &'static Mutex<HolidayCheckerRegistry> {
94 static INSTANCE: OnceLock<Mutex<HolidayCheckerRegistry>> = OnceLock::new();
95 INSTANCE.get_or_init(|| Mutex::new(HolidayCheckerRegistry::new()))
96}
97
98/// レジストリのロックを取得します。
99///
100/// 独自チェッカー([`OriginalHolidayChecker`])が panic してもロックは保持していない
101/// ([`snapshot`] が即座に解放する)ため、poison は通常発生しませんが、万一に備えて回復します。
102fn locked() -> MutexGuard<'static, HolidayCheckerRegistry> {
103 registry().lock().unwrap_or_else(PoisonError::into_inner)
104}
105
106/// レジストリのスナップショット(チェッカー一覧の複製)を取得し、直ちにロックを解放します。
107///
108/// ユーザー実装([`OriginalHolidayChecker`])はこのロックの**外**で実行されるため、独自
109/// チェッカーがグローバル API([`is_holiday`] 等)を再入呼び出ししても、同じ非再入 Mutex を
110/// 二重取得することはなく、デッドロックしません。
111fn snapshot() -> Vec<Checker> {
112 locked().snapshot()
113}
114
115/// その日に該当するすべての祝日を返します。
116pub fn holidays(date: Date) -> Vec<Holiday> {
117 compute_holidays(&snapshot(), date)
118}
119
120/// その日が祝日かどうかを返します。
121pub fn is_holiday(date: Date) -> bool {
122 !holidays(date).is_empty()
123}
124
125/// その日の祝日名を返します(該当しなければ `None`)。
126pub fn is_holiday_name(date: Date) -> Option<String> {
127 holidays(date).into_iter().next().map(|h| h.name)
128}
129
130/// その年のすべての祝日を `(日付, 名称)` のタプルで返します。
131pub fn year_holidays(year: i32) -> Vec<(Date, String)> {
132 let checkers = snapshot();
133 let mut out = Vec::new();
134 let mut date = Date::new(year, 1, 1).expect("January 1st is always a valid date");
135 while date.year() == year {
136 for holiday in compute_holidays(&checkers, date) {
137 out.push(holiday.into_tuple());
138 }
139 date = date.succ();
140 }
141 out
142}
143
144/// その月のすべての祝日を `(日付, 名称)` のタプルで返します。`month` が範囲外なら空を返します。
145pub fn month_holidays(year: i32, month: u32) -> Vec<(Date, String)> {
146 let checkers = snapshot();
147 let mut out = Vec::new();
148 let mut date = match Date::new(year, month, 1) {
149 Ok(d) => d,
150 Err(_) => return out,
151 };
152 while date.month() == month {
153 for holiday in compute_holidays(&checkers, date) {
154 out.push(holiday.into_tuple());
155 }
156 date = date.succ();
157 }
158 out
159}
160
161/// 指定範囲(両端を含む)のすべての祝日を `(日付, 名称)` のタプルで返します。
162pub fn between(start: Date, end: Date) -> Vec<(Date, String)> {
163 let checkers = snapshot();
164 let mut out = Vec::new();
165 let mut current = start;
166 while current <= end {
167 for holiday in compute_holidays(&checkers, current) {
168 out.push(holiday.into_tuple());
169 }
170 current = current.succ();
171 }
172 out
173}
174
175/// 独自の祝日チェッカーをグローバル API に登録します。
176///
177/// 同一型が既に登録済みなら何もしません。
178pub fn register<C: OriginalHolidayChecker + 'static>(checker: C) {
179 locked().register(checker);
180}
181
182/// 指定型の独自祝日チェッカーをグローバル API から登録解除します。
183///
184/// 登録解除は型パラメータで指定します(例: `jpholiday::unregister::<MyHoliday>()`)。
185pub fn unregister<C: OriginalHolidayChecker + 'static>() {
186 locked().unregister::<C>();
187}