Skip to main content

amlich_core/
holiday_data.rs

1use serde::Deserialize;
2use std::sync::OnceLock;
3
4const SOLAR_HOLIDAYS_JSON: &str = include_str!("../data/holidays/solar-holidays.json");
5const LUNAR_FESTIVALS_JSON: &str = include_str!("../data/holidays/lunar-festivals.json");
6
7#[derive(Debug, Deserialize)]
8struct SolarHolidaysFile {
9    holidays: Vec<SolarHolidayData>,
10}
11
12#[derive(Debug, Deserialize)]
13#[serde(rename_all = "camelCase")]
14pub struct SolarHolidayData {
15    pub solar_day: i32,
16    pub solar_month: i32,
17    pub category: String,
18    pub is_major: bool,
19    pub names: Names,
20    #[serde(default)]
21    pub origin: Option<BilingualText>,
22    #[serde(default)]
23    pub significance: Option<BilingualText>,
24    #[serde(default)]
25    pub activities: Option<BilingualList>,
26    #[serde(default)]
27    pub traditions: Option<BilingualList>,
28    #[serde(default)]
29    pub food: Vec<FoodItem>,
30    #[serde(default)]
31    pub taboos: Vec<TabooItem>,
32    #[serde(default)]
33    pub proverbs: Vec<ProverbItem>,
34    #[serde(default)]
35    pub regions: Option<Regions>,
36}
37
38#[derive(Debug, Deserialize)]
39struct LunarFestivalsFile {
40    festivals: Vec<LunarFestivalData>,
41}
42
43#[derive(Debug, Deserialize)]
44#[serde(rename_all = "camelCase")]
45pub struct LunarFestivalData {
46    pub lunar_day: i32,
47    pub lunar_month: i32,
48    #[serde(default)]
49    pub solar_day: Option<i32>,
50    #[serde(default)]
51    pub solar_month: Option<i32>,
52    pub year_offset: i32,
53    pub category: String,
54    pub is_major: bool,
55    pub names: Names,
56    #[serde(default)]
57    pub origin: Option<BilingualText>,
58    #[serde(default)]
59    pub activities: Option<BilingualList>,
60    #[serde(default)]
61    pub food: Vec<FoodItem>,
62    #[serde(default)]
63    pub taboos: Vec<TabooItem>,
64    #[serde(default)]
65    pub proverbs: Vec<ProverbItem>,
66    #[serde(default)]
67    pub regions: Option<Regions>,
68    #[serde(default)]
69    pub is_solar: bool,
70}
71
72#[derive(Debug, Deserialize)]
73pub struct Names {
74    pub vi: Vec<String>,
75    pub en: Vec<String>,
76}
77
78#[derive(Debug, Deserialize)]
79pub struct BilingualText {
80    pub vi: String,
81    pub en: String,
82}
83
84#[derive(Debug, Deserialize)]
85pub struct BilingualList {
86    pub vi: Vec<String>,
87    pub en: Vec<String>,
88}
89
90#[derive(Debug, Deserialize)]
91pub struct FoodItem {
92    pub name: BilingualText,
93    pub description: BilingualText,
94}
95
96#[derive(Debug, Deserialize)]
97pub struct TabooItem {
98    pub action: BilingualText,
99    pub reason: BilingualText,
100}
101
102#[derive(Debug, Deserialize)]
103pub struct ProverbItem {
104    pub text: String,
105    pub meaning: BilingualText,
106}
107
108#[derive(Debug, Deserialize)]
109pub struct Regions {
110    pub north: BilingualText,
111    pub central: BilingualText,
112    pub south: BilingualText,
113}
114
115static SOLAR_HOLIDAYS: OnceLock<Vec<SolarHolidayData>> = OnceLock::new();
116static LUNAR_FESTIVALS: OnceLock<Vec<LunarFestivalData>> = OnceLock::new();
117
118pub fn solar_holidays() -> &'static [SolarHolidayData] {
119    SOLAR_HOLIDAYS
120        .get_or_init(|| {
121            let parsed: SolarHolidaysFile = serde_json::from_str(SOLAR_HOLIDAYS_JSON)
122                .expect("Failed to parse data/holidays/solar-holidays.json");
123            parsed.holidays
124        })
125        .as_slice()
126}
127
128pub fn lunar_festivals() -> &'static [LunarFestivalData] {
129    LUNAR_FESTIVALS
130        .get_or_init(|| {
131            let parsed: LunarFestivalsFile = serde_json::from_str(LUNAR_FESTIVALS_JSON)
132                .expect("Failed to parse data/holidays/lunar-festivals.json");
133            parsed.festivals
134        })
135        .as_slice()
136}
137
138#[cfg(test)]
139mod tests {
140    use super::{lunar_festivals, solar_holidays};
141
142    #[test]
143    fn parses_lunar_festival_cultural_fields() {
144        let tet = lunar_festivals()
145            .iter()
146            .find(|f| f.names.vi.iter().any(|name| name == "Tết Nguyên Đán"))
147            .expect("Expected Tết Nguyên Đán in lunar-festivals.json");
148
149        assert!(tet.origin.is_some(), "origin should be parsed");
150        assert!(tet.activities.is_some(), "activities should be parsed");
151        assert!(!tet.food.is_empty(), "food should be parsed");
152        assert!(!tet.taboos.is_empty(), "taboos should be parsed");
153        assert!(!tet.proverbs.is_empty(), "proverbs should be parsed");
154        assert!(tet.regions.is_some(), "regions should be parsed");
155    }
156
157    #[test]
158    fn parses_solar_holiday_cultural_fields() {
159        let teachers_day = solar_holidays()
160            .iter()
161            .find(|h| {
162                h.names
163                    .vi
164                    .iter()
165                    .any(|name| name == "Ngày Nhà Giáo Việt Nam")
166            })
167            .expect("Expected Ngày Nhà Giáo Việt Nam in solar-holidays.json");
168
169        assert!(teachers_day.origin.is_some(), "origin should be parsed");
170        assert!(
171            teachers_day.significance.is_some(),
172            "significance should be parsed"
173        );
174        assert!(
175            teachers_day.activities.is_some(),
176            "activities should be parsed"
177        );
178        assert!(
179            teachers_day.traditions.is_some(),
180            "traditions should be parsed"
181        );
182        assert!(!teachers_day.food.is_empty(), "food should be parsed");
183        assert!(!teachers_day.taboos.is_empty(), "taboos should be parsed");
184        assert!(
185            !teachers_day.proverbs.is_empty(),
186            "proverbs should be parsed"
187        );
188        assert!(teachers_day.regions.is_some(), "regions should be parsed");
189    }
190}