use chrono::{Datelike, Duration, NaiveDate};
pub fn resolve_lunar_holiday(
algorithm: &str,
year: i32,
duration_days: u32,
) -> Option<Vec<NaiveDate>> {
let base = match algorithm {
"chinese_new_year" => approximate_chinese_new_year(year),
"diwali" => approximate_diwali(year),
"vesak" => approximate_vesak(year),
"hari_raya_puasa" => approximate_hari_raya_puasa(year),
"hari_raya_haji" => approximate_hari_raya_haji(year),
"deepavali" => approximate_deepavali(year),
"korean_new_year" => approximate_korean_new_year(year),
"korean_buddha_birthday" => approximate_korean_buddha_birthday(year),
"chuseok" => approximate_chuseok(year),
_ => return None,
}?;
let mut dates = Vec::with_capacity(duration_days as usize);
for offset in 0..duration_days {
dates.push(base + Duration::days(offset as i64));
}
Some(dates)
}
pub fn approximate_chinese_new_year(year: i32) -> Option<NaiveDate> {
let base_year = 2000;
let cny_2000 = NaiveDate::from_ymd_opt(2000, 2, 5)?;
let years_diff = year - base_year;
let lunar_cycle = 29.5306_f64;
let days_offset = (years_diff as f64 * 12.0 * lunar_cycle) % 365.25;
let mut result = cny_2000 + Duration::days(days_offset as i64);
while result.month() > 2 || (result.month() == 2 && result.day() > 20) {
result -= Duration::days(29);
}
while result.month() == 1 && result.day() < 21 {
result += Duration::days(29);
}
if result.year() != year {
let day = result.day().min(28);
result = NaiveDate::from_ymd_opt(year, result.month(), day)
.or_else(|| NaiveDate::from_ymd_opt(year, result.month(), 28))?;
}
Some(result)
}
pub fn approximate_diwali(year: i32) -> Option<NaiveDate> {
match year % 4 {
0 => NaiveDate::from_ymd_opt(year, 11, 1),
1 => NaiveDate::from_ymd_opt(year, 10, 24),
2 => NaiveDate::from_ymd_opt(year, 11, 12),
_ => NaiveDate::from_ymd_opt(year, 11, 4),
}
}
pub fn approximate_vesak(year: i32) -> Option<NaiveDate> {
let base = match year % 19 {
0 => 18,
1 => 7,
2 => 26,
3 => 15,
4 => 5,
5 => 24,
6 => 13,
7 => 2,
8 => 22,
9 => 11,
10 => 30,
11 => 19,
12 => 8,
13 => 27,
14 => 17,
15 => 6,
16 => 25,
17 => 14,
_ => 3,
};
let month = if base > 20 { 4 } else { 5 };
let day = if base > 20 { base - 10 } else { base };
NaiveDate::from_ymd_opt(year, month, (day as u32).clamp(1, 28))
}
pub fn approximate_hari_raya_puasa(year: i32) -> Option<NaiveDate> {
let base_year = 2024;
let base_date = NaiveDate::from_ymd_opt(2024, 4, 10)?;
let years_diff = year - base_year;
let days_shift = (years_diff as f64 * -10.63) as i64;
let mut result = base_date + Duration::days(days_shift);
while result.year() != year {
if result.year() > year {
result -= Duration::days(354);
} else {
result += Duration::days(354);
}
}
Some(result)
}
pub fn approximate_hari_raya_haji(year: i32) -> Option<NaiveDate> {
approximate_hari_raya_puasa(year).map(|d| d + Duration::days(70))
}
pub fn approximate_deepavali(year: i32) -> Option<NaiveDate> {
approximate_diwali(year)
}
pub fn approximate_korean_new_year(year: i32) -> Option<NaiveDate> {
approximate_chinese_new_year(year)
}
pub fn approximate_korean_buddha_birthday(year: i32) -> Option<NaiveDate> {
match year % 19 {
0 => NaiveDate::from_ymd_opt(year, 5, 15),
1 => NaiveDate::from_ymd_opt(year, 5, 4),
2 => NaiveDate::from_ymd_opt(year, 5, 23),
3 => NaiveDate::from_ymd_opt(year, 5, 12),
4 => NaiveDate::from_ymd_opt(year, 5, 1),
5 => NaiveDate::from_ymd_opt(year, 5, 20),
6 => NaiveDate::from_ymd_opt(year, 5, 10),
7 => NaiveDate::from_ymd_opt(year, 4, 29),
8 => NaiveDate::from_ymd_opt(year, 5, 18),
9 => NaiveDate::from_ymd_opt(year, 5, 7),
10 => NaiveDate::from_ymd_opt(year, 5, 26),
11 => NaiveDate::from_ymd_opt(year, 5, 15),
12 => NaiveDate::from_ymd_opt(year, 5, 4),
13 => NaiveDate::from_ymd_opt(year, 5, 24),
14 => NaiveDate::from_ymd_opt(year, 5, 13),
15 => NaiveDate::from_ymd_opt(year, 5, 2),
16 => NaiveDate::from_ymd_opt(year, 5, 21),
17 => NaiveDate::from_ymd_opt(year, 5, 10),
_ => NaiveDate::from_ymd_opt(year, 4, 30),
}
}
pub fn approximate_chuseok(year: i32) -> Option<NaiveDate> {
match year % 19 {
0 => NaiveDate::from_ymd_opt(year, 9, 17),
1 => NaiveDate::from_ymd_opt(year, 10, 6),
2 => NaiveDate::from_ymd_opt(year, 9, 25),
3 => NaiveDate::from_ymd_opt(year, 9, 14),
4 => NaiveDate::from_ymd_opt(year, 10, 3),
5 => NaiveDate::from_ymd_opt(year, 9, 22),
6 => NaiveDate::from_ymd_opt(year, 9, 11),
7 => NaiveDate::from_ymd_opt(year, 9, 30),
8 => NaiveDate::from_ymd_opt(year, 9, 19),
9 => NaiveDate::from_ymd_opt(year, 10, 9),
10 => NaiveDate::from_ymd_opt(year, 9, 28),
11 => NaiveDate::from_ymd_opt(year, 9, 17),
12 => NaiveDate::from_ymd_opt(year, 10, 6),
13 => NaiveDate::from_ymd_opt(year, 9, 25),
14 => NaiveDate::from_ymd_opt(year, 9, 14),
15 => NaiveDate::from_ymd_opt(year, 10, 4),
16 => NaiveDate::from_ymd_opt(year, 9, 22),
17 => NaiveDate::from_ymd_opt(year, 9, 12),
_ => NaiveDate::from_ymd_opt(year, 10, 1),
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_resolve_chinese_new_year() {
let dates = resolve_lunar_holiday("chinese_new_year", 2024, 1);
assert!(dates.is_some());
let dates = dates.unwrap();
assert_eq!(dates.len(), 1);
let d = dates[0];
assert_eq!(d.year(), 2024);
assert!(d.month() <= 2);
}
#[test]
fn test_resolve_multi_day() {
let dates = resolve_lunar_holiday("chinese_new_year", 2024, 7);
assert!(dates.is_some());
assert_eq!(dates.unwrap().len(), 7);
}
#[test]
fn test_resolve_unknown_algorithm() {
assert!(resolve_lunar_holiday("unknown_algo", 2024, 1).is_none());
}
#[test]
fn test_diwali_range() {
for year in 2020..2030 {
let d = approximate_diwali(year).expect("valid date");
assert!(d.month() == 10 || d.month() == 11);
}
}
#[test]
fn test_vesak_range() {
for year in 2020..2030 {
let d = approximate_vesak(year).expect("valid date");
assert!(d.month() == 4 || d.month() == 5);
}
}
#[test]
fn test_hari_raya_haji_after_puasa() {
let puasa = approximate_hari_raya_puasa(2024).expect("valid");
let haji = approximate_hari_raya_haji(2024).expect("valid");
assert_eq!(haji - puasa, Duration::days(70));
}
#[test]
fn test_chuseok_range() {
for year in 2020..2030 {
let d = approximate_chuseok(year).expect("valid date");
assert!(d.month() == 9 || d.month() == 10);
}
}
}