amlich_core/
holiday_data.rs1use 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}