1use chrono::{Datelike, Duration, NaiveDate, Weekday};
2use serde::{Deserialize, Serialize};
3use inflector::Inflector;
4use number_names::{ordinal};
5use std::collections::HashMap;
6use std::env;
7use std::fs;
8use std::path::Path;
9use once_cell::sync::Lazy;
10use regex::Regex;
11
12use crate::config::get_storage_path;
13
14#[derive(Deserialize, Debug, Clone, Serialize)]
15pub struct RangeEnd {
16 chapter: u32,
17 verse: u32,
18}
19
20#[derive(Deserialize, Debug, Clone, Serialize)]
21pub struct Verse {
22 pub chapter: u32,
23 pub verse: u32,
24 pub translation: Option<String>,
25 range_end: Option<RangeEnd>,
26}
27
28#[derive(Deserialize, Debug, Clone, Serialize)]
29pub struct Verses {
30 pub book: String,
31 pub verses: Vec<Verse>,
32}
33
34#[derive(Deserialize, Debug, Clone, Serialize)]
35pub struct Reading {
36 #[serde(rename = "rawReading")]
37 pub raw_reading: String,
38 pub reading: Vec<Verses>,
39}
40
41#[derive(Deserialize, Debug, Clone, Serialize)]
42pub struct Readings {
43 pub title: String,
44 pub first: Option<Reading>,
45 pub responsal: Option<Reading>,
46 pub second: Option<Reading>,
47 pub gospel: Option<Reading>,
48 pub rank: Option<String>,
49}
50
51#[derive(Debug, Clone)]
52struct Calendar {
53 weekday_cycle: u8,
54 sunday_cycle: u8,
55 holy_family: NaiveDate,
56 epiphany: NaiveDate,
57 christmas_end: NaiveDate,
58 ash_wednesday: NaiveDate,
59 easter: NaiveDate,
60 pentecost: NaiveDate,
61 advent_start: NaiveDate,
62 weeks_before_lent: u8,
63 pentecost_start_ot: u8,
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
67pub enum LiturgicalSeason {
68 Christmas,
69 Ordinary,
70 Lent,
71 Easter,
72 Advent,
73}
74
75impl LiturgicalSeason {
76 fn as_str(&self) -> &'static str {
77 match self {
78 Self::Christmas => "CHRISTMAS",
79 Self::Ordinary => "ORDINARY",
80 Self::Lent => "LENT",
81 Self::Easter => "EASTER",
82 Self::Advent => "ADVENT",
83 }
84 }
85}
86
87type LectionaryTemplate = HashMap<String, HashMap<String, Readings>>;
88
89static LECTIONARYTEMPLATE: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/lectionaryTemplate.json"));
90
91pub struct LiturgyGenerator {
92 lectionary: LectionaryTemplate,
93 calendar: Calendar,
94 year: i32,
95}
96
97impl LiturgyGenerator {
98 pub fn new(year: i32) -> Result<Self, Box<dyn std::error::Error>> {
99 let lectionary_data = LECTIONARYTEMPLATE;
102 let lectionary: LectionaryTemplate = serde_json::from_str(&lectionary_data)?;
103
104 let calendar = Self::get_calendar(year);
106
107 Ok(Self {
108 lectionary,
109 calendar,
110 year,
111 })
112 }
113
114 fn get_calendar(year: i32) -> Calendar {
115 let easter = calendar_utils::calculate_easter(year);
116 let calendar = Calendar {
117 weekday_cycle: calendar_utils::calculate_weekday_cycle(year),
118 sunday_cycle: calendar_utils::calculate_sunday_cycle(year),
119 holy_family: calendar_utils::calculate_holy_family(year),
120 epiphany: calendar_utils::calculate_epiphany(year),
121 christmas_end: calendar_utils::calculate_christmas_end(year),
122 ash_wednesday: calendar_utils::calculate_ash_wednesday(easter),
123 easter: easter,
124 pentecost: calendar_utils::calculate_pentecost(easter),
125 advent_start: calendar_utils::calculate_advent_start(year),
126 weeks_before_lent: calendar_utils::calculate_weeks_before_lent(year),
127 pentecost_start_ot: calendar_utils::calculate_pentecost_start_ot(year),
128 };
129 calendar
142 }
143
144 fn sunday_cycle_to_char(cycle: u8) -> char {
145 match cycle {
146 1 => 'A',
147 2 => 'B',
148 3 => 'C',
149 _ => 'A',
150 }
151 }
152
153 fn get_prev_saturday(mut date: NaiveDate, delta: u32) -> NaiveDate {
154 for _ in 0..delta {
155 date = date.pred_opt().unwrap();
156 while date.weekday() != Weekday::Sat {
157 date = date.pred_opt().unwrap();
158 }
159 }
160 date
161 }
162
163 fn get_next_thursday(mut date: NaiveDate, delta: u32) -> NaiveDate {
164 for _ in 0..delta {
165 date = date.succ_opt().unwrap();
166 while date.weekday() != Weekday::Thu {
167 date = date.succ_opt().unwrap();
168 }
169 }
170 date
171 }
172
173 fn get_next_sunday(mut date: NaiveDate) -> NaiveDate {
174 date = date.succ_opt().unwrap();
175 while date.weekday() != Weekday::Sun {
176 date = date.succ_opt().unwrap();
177 }
178 date
179 }
180
181 fn date_to_lectionary_date(date: NaiveDate) -> String {
182 let month = match date.month() {
183 1 => "Jan",
184 2 => "Feb",
185 3 => "Mar",
186 4 => "Apr",
187 5 => "May",
188 6 => "Jun",
189 7 => "Jul",
190 8 => "Aug",
191 9 => "Sep",
192 10 => "Oct",
193 11 => "Nov",
194 12 => "Dec",
195 _ => unreachable!(),
196 };
197 format!("{}{}", month, date.day())
198 }
199
200 fn get_ordinal_number(n: u32) -> String {
201 let o = ordinal(n as u64).to_title_case();
210 o
211 }
212
213 fn get_day_of_week_string(day: u32) -> &'static str {
214 match day {
215 0 => "Monday",
216 1 => "Tuesday",
217 2 => "Wednesday",
218 3 => "Thursday",
219 4 => "Friday",
220 5 => "Saturday",
221 6 => "Sunday",
222 _ => "Monday",
223 }
224 }
225
226 fn determine_seasons(&self) -> HashMap<NaiveDate, LiturgicalSeason> {
227 let mut seasons = HashMap::new();
228 let start_date = NaiveDate::from_ymd_opt(self.year, 1, 1).unwrap();
229 let end_date = NaiveDate::from_ymd_opt(self.year + 1, 1, 1).unwrap();
230
231 let mut current_date = start_date;
232 while current_date < end_date {
233 let season = if current_date <= self.calendar.christmas_end {
234 LiturgicalSeason::Christmas
235 } else if current_date < self.calendar.ash_wednesday {
236 LiturgicalSeason::Ordinary
237 } else if current_date < self.calendar.easter - Duration::days(2) {
238 LiturgicalSeason::Lent
239 } else if current_date <= self.calendar.pentecost {
240 LiturgicalSeason::Easter
241 } else if current_date < self.calendar.advent_start {
242 LiturgicalSeason::Ordinary
243 } else if current_date < NaiveDate::from_ymd_opt(self.year, 12, 25).unwrap() {
244 LiturgicalSeason::Advent
245 } else {
246 LiturgicalSeason::Christmas
247 };
248
249 seasons.insert(current_date, season);
250 current_date = current_date.succ_opt().unwrap();
251 }
252
253 seasons
254 }
255
256 fn process_christmas_readings(
257 &self,
258 date: NaiveDate,
259 liturgy: &mut HashMap<NaiveDate, Vec<Readings>>,
260 ) {
261 let sunday_cycle_char = Self::sunday_cycle_to_char(self.calendar.sunday_cycle);
262 let christmas_lectionary = &self.lectionary["CHRISTMAS"];
263 let date_search = Self::date_to_lectionary_date(date);
264
265 liturgy.entry(date).or_insert_with(Vec::new);
266
267 if date == NaiveDate::from_ymd_opt(self.year, 12, 25).unwrap() {
268 let masses = ["CHRISTMAS-VIGIL", "CHRISTMAS-NIGHT", "CHRISTMAS-DAWN", "CHRISTMAS-DAY"];
270 for mass in &masses {
271 if let Some(reading) = christmas_lectionary.get(*mass) {
272 liturgy.get_mut(&date).unwrap().push(reading.clone());
273 }
274 }
275 } else if date == self.calendar.holy_family {
276 let next_sunday_cycle = self.calendar.sunday_cycle % 3 + 1;
278 let key_search = format!("HOLYFAMILY-{}", Self::sunday_cycle_to_char(next_sunday_cycle));
279
280 if next_sunday_cycle == 1 {
281 if let Some(reading) = christmas_lectionary.get(&key_search) {
282 liturgy.get_mut(&date).unwrap().push(reading.clone());
283 }
284 } else {
285 if let Some(reading) = christmas_lectionary.get("HOLYFAMILY-A") {
286 liturgy.get_mut(&date).unwrap().push(reading.clone());
287 }
288 if let Some(reading) = christmas_lectionary.get(&key_search) {
289 liturgy.get_mut(&date).unwrap().push(reading.clone());
290 }
291 }
292 } else if christmas_lectionary.contains_key(&date_search) &&
293 (date < self.calendar.epiphany ||
294 date > NaiveDate::from_ymd_opt(self.year, 12, 25).unwrap()) {
295 if let Some(reading) = christmas_lectionary.get(&date_search) {
297 liturgy.get_mut(&date).unwrap().push(reading.clone());
298 }
299 } else if date == self.calendar.christmas_end {
300 let key_search = format!("BAPTISM-{}", sunday_cycle_char);
302 if self.calendar.sunday_cycle == 1 {
303 if let Some(reading) = christmas_lectionary.get("BAPTISM-A") {
304 liturgy.get_mut(&date).unwrap().push(reading.clone());
305 }
306 } else {
307 if let Some(reading) = christmas_lectionary.get("BAPTISM-A") {
308 liturgy.get_mut(&date).unwrap().push(reading.clone());
309 }
310 if let Some(reading) = christmas_lectionary.get(&key_search) {
311 liturgy.get_mut(&date).unwrap().push(reading.clone());
312 }
313 }
314 } else if date == self.calendar.epiphany {
315 if let Some(reading) = christmas_lectionary.get("EPIPHANY") {
317 liturgy.get_mut(&date).unwrap().push(reading.clone());
318 }
319 } else {
320 let days_from_epiphany = (date - self.calendar.epiphany).num_days();
322 let key = format!("EPIPHANY-{}", days_from_epiphany);
323 if let Some(reading) = christmas_lectionary.get(&key) {
324 liturgy.get_mut(&date).unwrap().push(reading.clone());
325 }
326 }
327 }
328
329 fn process_advent_readings(
330 &self,
331 date: NaiveDate,
332 liturgy: &mut HashMap<NaiveDate, Vec<Readings>>,
333 ) {
334 let sunday_cycle_char = Self::sunday_cycle_to_char(self.calendar.sunday_cycle);
335 let advent_lectionary = &self.lectionary["ADVENT"];
336 let day_of_week = date.weekday().num_days_from_monday() + 1;
337 let weeks_since_start = ((date - self.calendar.advent_start).num_days() / 7 + 1) as u32;
338
339 liturgy.entry(date).or_insert_with(Vec::new);
340
341 if date.day() >= 17 && date.day() <= 24 {
342 let key_search = format!("Dec{}", date.day());
344 if let Some(mut reading) = advent_lectionary.get(&key_search).cloned() {
345 let title = format!(
346 "{} {} of Advent",
347 Self::get_ordinal_number(weeks_since_start),
348 Self::get_day_of_week_string((day_of_week - 1) as u32)
349 );
350 reading.title = title;
351 liturgy.get_mut(&date).unwrap().push(reading);
352 }
353 } else if date.weekday() == Weekday::Sun {
354 let key_search = format!("{}-7-{}", weeks_since_start, sunday_cycle_char);
356 if let Some(reading) = advent_lectionary.get(&key_search) {
357 liturgy.get_mut(&date).unwrap().push(reading.clone());
358 }
359 } else {
360 let key_search = format!("{}-{}", weeks_since_start, day_of_week);
362 if let Some(reading) = advent_lectionary.get(&key_search) {
363 liturgy.get_mut(&date).unwrap().push(reading.clone());
364 }
365 }
366 }
367
368 fn process_ordinary_readings(
369 &self,
370 date: NaiveDate,
371 liturgy: &mut HashMap<NaiveDate, Vec<Readings>>,
372 ) {
373 let sunday_cycle_char = Self::sunday_cycle_to_char(self.calendar.sunday_cycle);
374 let ordinary_lectionary = &self.lectionary["ORDINARY"];
375 let day_of_week = date.weekday().num_days_from_monday() + 1;
376
377 let weeks_since_start = if date < self.calendar.ash_wednesday {
378 ((date - self.calendar.christmas_end).num_days() / 7 + 1) as u32
379 } else {
380 ((date - self.calendar.pentecost).num_days() / 7 + self.calendar.pentecost_start_ot as i64) as u32
381 };
382
383 let week_str = if weeks_since_start < 10 {
384 format!("0{}", weeks_since_start)
385 } else {
386 weeks_since_start.to_string()
387 };
388
389 liturgy.entry(date).or_insert_with(Vec::new);
390
391 if date.weekday() == Weekday::Sun {
392 let key_search = format!("{}-7-{}", week_str, sunday_cycle_char);
393 if let Some(mut reading) = ordinary_lectionary.get(&key_search).cloned() {
394 reading.title = format!("{} Sunday of Ordinary Time", Self::get_ordinal_number(weeks_since_start));
395 liturgy.get_mut(&date).unwrap().push(reading);
396 }
397 } else {
398 let key_search = format!("{}-{}-{}", week_str, day_of_week, self.calendar.weekday_cycle);
399 if let Some(reading) = ordinary_lectionary.get(&key_search) {
400 liturgy.get_mut(&date).unwrap().push(reading.clone());
401 }
402 }
403 }
404
405 fn process_lent_readings(
406 &self,
407 date: NaiveDate,
408 liturgy: &mut HashMap<NaiveDate, Vec<Readings>>,
409 ) {
410 let sunday_cycle_char = Self::sunday_cycle_to_char(self.calendar.sunday_cycle);
411 let lent_lectionary = &self.lectionary["LENT"];
412 let easter_lectionary = &self.lectionary["EASTER"];
413 let day_of_week = date.weekday().num_days_from_monday() + 1;
414 let weeks_since_ash = ((date - self.calendar.ash_wednesday + Duration::days(2)).num_days() / 7) as u32;
415
416 liturgy.entry(date).or_insert_with(Vec::new);
417
418 if date + Duration::days(3) == self.calendar.easter {
419 if let Some(reading) = lent_lectionary.get("CHRISM") {
421 liturgy.get_mut(&date).unwrap().push(reading.clone());
422 }
423 if let Some(reading) = easter_lectionary.get("0-4") {
424 liturgy.get_mut(&date).unwrap().push(reading.clone());
425 }
426 } else if date.weekday() == Weekday::Sun {
427 if weeks_since_ash + 1 == 6 {
428 let key_search = format!("6-7-{}", sunday_cycle_char);
430 if let Some(reading) = lent_lectionary.get(&format!("{}-MASS", key_search)) {
431 liturgy.get_mut(&date).unwrap().push(reading.clone());
432 }
433 if let Some(reading) = lent_lectionary.get(&format!("{}-PROC", key_search)) {
434 liturgy.get_mut(&date).unwrap().push(reading.clone());
435 }
436 } else {
437 let key_search = format!("{}-7-{}", weeks_since_ash + 1, sunday_cycle_char);
438 if let Some(reading) = lent_lectionary.get(&key_search) {
439 liturgy.get_mut(&date).unwrap().push(reading.clone());
440 }
441 }
442 } else {
443 let key_search = format!("{}-{}", weeks_since_ash, day_of_week);
444 if let Some(reading) = lent_lectionary.get(&key_search) {
445 liturgy.get_mut(&date).unwrap().push(reading.clone());
446 }
447 }
448 }
449
450 fn process_easter_readings(
451 &self,
452 date: NaiveDate,
453 liturgy: &mut HashMap<NaiveDate, Vec<Readings>>,
454 ) {
455 let sunday_cycle_char = Self::sunday_cycle_to_char(self.calendar.sunday_cycle);
456 let easter_lectionary = &self.lectionary["EASTER"];
457 let day_of_week = date.weekday().num_days_from_monday() + 1;
458 let weeks_since_easter = ((date - self.calendar.easter).num_days() / 7 + 1) as u32;
459
460 liturgy.entry(date).or_insert_with(Vec::new);
461
462 if date == self.calendar.easter - Duration::days(1) {
463 liturgy.get_mut(&date).unwrap().push(Readings {
465 title: "No Mass".to_string(),
466 first: None,
467 responsal: None,
468 second: None,
469 gospel: None,
470 rank: None,
471 });
472 } else if date == self.calendar.easter - Duration::days(2) {
473 if let Some(reading) = easter_lectionary.get("0-5") {
475 liturgy.get_mut(&date).unwrap().push(reading.clone());
476 }
477 } else if date == self.calendar.easter {
478 if let Some(reading) = easter_lectionary.get("0-7-VIGIL") {
480 liturgy.get_mut(&date).unwrap().push(reading.clone());
481 }
482 if let Some(reading) = easter_lectionary.get("0-7") {
483 liturgy.get_mut(&date).unwrap().push(reading.clone());
484 }
485 } else if date == self.calendar.pentecost {
486 if let Some(reading) = easter_lectionary.get("PENTECOST-VIGIL") {
488 liturgy.get_mut(&date).unwrap().push(reading.clone());
489 }
490 if let Some(reading) = easter_lectionary.get("PENTECOST-A") {
491 liturgy.get_mut(&date).unwrap().push(reading.clone());
492 }
493 if self.calendar.sunday_cycle != 1 {
494 let key = format!("PENTECOST-{}", sunday_cycle_char);
495 if let Some(reading) = easter_lectionary.get(&key) {
496 liturgy.get_mut(&date).unwrap().push(reading.clone());
497 }
498 }
499 } else if date.weekday() == Weekday::Sun {
500 let key_search = format!("{}-7-{}", weeks_since_easter - 1, sunday_cycle_char);
502 if let Some(reading) = easter_lectionary.get(&key_search) {
503 liturgy.get_mut(&date).unwrap().push(reading.clone());
504 }
505 if weeks_since_easter == 6 {
506 let ascension_key = format!("ASCENSION-{}", sunday_cycle_char);
508 if let Some(reading) = easter_lectionary.get(&ascension_key) {
509 liturgy.get_mut(&date).unwrap().insert(0, reading.clone());
510 }
511 }
512 } else {
513 let key_search = format!("{}-{}", weeks_since_easter, day_of_week);
515 if let Some(reading) = easter_lectionary.get(&key_search) {
516 liturgy.get_mut(&date).unwrap().push(reading.clone());
517 }
518 }
519 }
520
521 fn process_solemnities(&self, liturgy: &mut HashMap<NaiveDate, Vec<Readings>>) {
522 let sunday_cycle_char = Self::sunday_cycle_to_char(self.calendar.sunday_cycle);
523 let solemnity_lectionary = &self.lectionary["SOLEMNITY"];
524
525 let trinity_sunday = Self::get_next_sunday(self.calendar.pentecost);
526 let body_blood = Self::get_next_sunday(trinity_sunday);
527 let sacred_heart = body_blood + Duration::days(5);
528 let start_holy_week = self.calendar.easter - Duration::days(6);
529
530 if let Some(reading) = solemnity_lectionary.get(&format!("TrinSun-{}", sunday_cycle_char)) {
532 liturgy.entry(trinity_sunday).or_insert_with(Vec::new).insert(0, reading.clone());
533 }
534 if let Some(reading) = solemnity_lectionary.get(&format!("BodyBlood-{}", sunday_cycle_char)) {
535 liturgy.entry(body_blood).or_insert_with(Vec::new).insert(0, reading.clone());
536 }
537 if let Some(reading) = solemnity_lectionary.get(&format!("SacHeart-{}", sunday_cycle_char)) {
538 liturgy.entry(sacred_heart).or_insert_with(Vec::new).insert(0, reading.clone());
539 }
540
541 let dates_to_check = [
543 (3, 19, "Mar19"), (3, 25, "Mar25"), (6, 24, "Jun24"), (12, 8, "Dec8"), ];
548
549 for (month, day, key) in &dates_to_check {
550 if let Some(solemnity_date) = NaiveDate::from_ymd_opt(self.year, *month, *day) {
551 let mut actual_date = solemnity_date;
552
553 match key {
554 &"Mar19" => {
555 if start_holy_week < solemnity_date && solemnity_date < self.calendar.easter {
557 actual_date = Self::get_prev_saturday(solemnity_date, 1);
558 } else if solemnity_date.weekday() == Weekday::Sun &&
559 matches!(liturgy.get(&solemnity_date).map(|v| v.is_empty()).unwrap_or(true), false) {
560 actual_date = solemnity_date + Duration::days(1);
561 }
562 },
563 &"Mar25" => {
564 if solemnity_date.weekday() == Weekday::Sun {
566 actual_date = NaiveDate::from_ymd_opt(self.year, 3, 26).unwrap();
567 }
568 if start_holy_week <= actual_date && actual_date < Self::get_next_sunday(self.calendar.easter) {
569 actual_date = self.calendar.easter + Duration::days(8);
570 }
571 },
572 &"Jun24" => {
573 if solemnity_date == sacred_heart {
575 actual_date = NaiveDate::from_ymd_opt(self.year, 6, 23).unwrap();
576 }
577 if let Some(vigil_reading) = solemnity_lectionary.get("Jun24-VIGIL") {
579 liturgy.entry(actual_date).or_insert_with(Vec::new).insert(0, vigil_reading.clone());
580 }
581 },
582 &"Dec8" => {
583 if solemnity_date.weekday() == Weekday::Sun {
585 actual_date = NaiveDate::from_ymd_opt(self.year, 12, 9).unwrap();
586 }
587 },
588 _ => {}
589 }
590
591 if let Some(reading) = solemnity_lectionary.get(*key) {
592 liturgy.entry(actual_date).or_insert_with(Vec::new).insert(0, reading.clone());
593 }
594 }
595 }
596
597 for (date_str, reading) in solemnity_lectionary {
599 if let Ok(parsed_date) = Self::parse_lectionary_date_to_date(date_str, self.year) {
600 if ["Mar19", "Mar25", "Jun24", "Dec8"].contains(&date_str.as_str()) ||
602 date_str.contains("TrinSun") || date_str.contains("BodyBlood") || date_str.contains("SacHeart") {
603 continue;
604 }
605
606 if date_str.ends_with("-VIGIL") {
607 if let Some(base_reading) = solemnity_lectionary.get(&date_str.replace("-VIGIL", "")) {
608 let readings = liturgy.entry(parsed_date).or_insert_with(Vec::new);
609 readings.insert(0, base_reading.clone());
610 readings.insert(0, reading.clone());
611 }
612 } else if !solemnity_lectionary.contains_key(&format!("{}-VIGIL", date_str)) {
613 liturgy.entry(parsed_date).or_insert_with(Vec::new).insert(0, reading.clone());
614 }
615 }
616 }
617 }
618
619 fn process_saints(&self, liturgy: &mut HashMap<NaiveDate, Vec<Readings>>) {
620 let saint_lectionary = &self.lectionary["SAINTPROPER"];
621
622 let thanksgiving = Self::get_next_thursday(
624 NaiveDate::from_ymd_opt(self.year, 10, 31).unwrap(),
625 4
626 );
627 if let Some(reading) = saint_lectionary.get("THANKSGIVING") {
628 liturgy.entry(thanksgiving).or_insert_with(Vec::new).push(reading.clone());
629 }
630
631 for (date_str, reading) in saint_lectionary {
633 if date_str == "THANKSGIVING" {
634 continue; }
636
637 if let Ok(saint_date) = Self::parse_lectionary_date_to_date(date_str, self.year) {
638 let day_of_week = saint_date.weekday();
639 let rank = reading.rank.clone().unwrap_or(String::new());
640
641 if day_of_week == Weekday::Sun || rank.contains("Opt") || rank.contains("Mem") {
642 liturgy.entry(saint_date).or_insert_with(Vec::new).push(reading.clone());
643 } else {
644 liturgy.entry(saint_date).or_insert_with(Vec::new).insert(0, reading.clone());
645 }
646 }
647 }
648 }
649
650 fn parse_lectionary_date_to_date(date_str: &str, year: i32) -> Result<NaiveDate, Box<dyn std::error::Error>> {
651 if date_str.len() < 4 {
652 return Err("Invalid date string".into());
653 }
654
655 let month_str = &date_str[..3];
656 let day_str = &date_str[3..].chars().take_while(|c| c.is_ascii_digit()).collect::<String>();
657
658 let month = match month_str {
659 "Jan" => 1, "Feb" => 2, "Mar" => 3, "Apr" => 4,
660 "May" => 5, "Jun" => 6, "Jul" => 7, "Aug" => 8,
661 "Sep" => 9, "Oct" => 10, "Nov" => 11, "Dec" => 12,
662 _ => return Err("Invalid month".into()),
663 };
664
665 let day: u32 = day_str.parse()?;
666 NaiveDate::from_ymd_opt(year, month, day)
667 .ok_or_else(|| "Invalid date".into())
668 }
669
670 pub fn generate(&self) -> Result<(HashMap<String, Vec<Readings>>, HashMap<String, LiturgicalSeason>), Box<dyn std::error::Error>> {
671 let seasons = self.determine_seasons();
672 let mut liturgy: HashMap<NaiveDate, Vec<Readings>> = HashMap::new();
673
674 for (&date, &season) in &seasons {
676 match season {
677 LiturgicalSeason::Christmas => self.process_christmas_readings(date, &mut liturgy),
678 LiturgicalSeason::Advent => self.process_advent_readings(date, &mut liturgy),
679 LiturgicalSeason::Ordinary => self.process_ordinary_readings(date, &mut liturgy),
680 LiturgicalSeason::Lent => self.process_lent_readings(date, &mut liturgy),
681 LiturgicalSeason::Easter => self.process_easter_readings(date, &mut liturgy),
682 }
683 }
684
685 self.process_solemnities(&mut liturgy);
687 self.process_saints(&mut liturgy);
688
689 let parsed_liturgy: HashMap<String, Vec<Readings>> = liturgy
691 .into_iter()
692 .map(|(date, readings)| (date.format("%Y-%m-%d").to_string(), readings))
693 .collect();
694
695 let parsed_seasons: HashMap<String, LiturgicalSeason> = seasons
696 .into_iter()
697 .map(|(date, season)| (date.format("%Y-%m-%d").to_string(), season))
698 .collect();
699
700 Ok((parsed_liturgy, parsed_seasons))
701 }
702
703 pub fn save_to_files(&self) -> Result<(), Box<dyn std::error::Error>> {
704 let (liturgy, seasons) = self.generate()?;
705
706 let liturgy_folder = get_storage_path().join("liturgies");
708 std::fs::create_dir_all(&liturgy_folder)?;
709
710 let liturgy_json = serde_json::to_string_pretty(&liturgy)?;
712 std::fs::write(&liturgy_folder.join(format!("liturgy{}.json", self.year)), &liturgy_json)?;
713 println!("{}", liturgy_json);
714
715 let seasons_json = serde_json::to_string_pretty(&seasons)?;
717 std::fs::write(format!("./liturgies/liturgy{}season.json", self.year), &seasons_json)?;
718
719 Ok(())
720 }
721}
722
723#[cfg(test)]
738mod tests {
739 use super::*;
740
741 #[test]
742 fn test_sunday_cycle_conversion() {
743 assert_eq!(LiturgyGenerator::sunday_cycle_to_char(1), 'A');
744 assert_eq!(LiturgyGenerator::sunday_cycle_to_char(2), 'B');
745 assert_eq!(LiturgyGenerator::sunday_cycle_to_char(3), 'C');
746 }
747
748 #[test]
749 fn test_date_to_lectionary_date() {
750 let date = NaiveDate::from_ymd_opt(2024, 3, 15).unwrap();
751 assert_eq!(LiturgyGenerator::date_to_lectionary_date(date), "Mar15");
752 }
753
754 #[test]
755 fn test_ordinal_numbers() {
756 assert_eq!(LiturgyGenerator::get_ordinal_number(1), "First");
757 assert_eq!(LiturgyGenerator::get_ordinal_number(2), "Second");
758 assert_eq!(LiturgyGenerator::get_ordinal_number(21), "Twenty First");
759 }
760
761 #[test]
762 fn test_parse_lectionary_date() {
763 let result = LiturgyGenerator::parse_lectionary_date_to_date("Mar15", 2024);
764 assert!(result.is_ok());
765 assert_eq!(result.unwrap(), NaiveDate::from_ymd_opt(2024, 3, 15).unwrap());
766 }
767}
768
769pub mod calendar_utils {
771 use chrono::{NaiveDate, Datelike};
772
773 pub fn calculate_easter(year: i32) -> NaiveDate {
775 let a = year % 19;
776 let b = year / 100;
777 let c = year % 100;
778 let d = b / 4;
779 let e = b % 4;
780 let f = (b + 8) / 25;
781 let g = (b - f + 1) / 3;
782 let h = (19 * a + b - d - g + 15) % 30;
783 let i = c / 4;
784 let k = c % 4;
785 let l = (32 + 2 * e + 2 * i - h - k) % 7;
786 let m = (a + 11 * h + 22 * l) / 451;
787 let n = (h + l - 7 * m + 114) / 31;
788 let p = (h + l - 7 * m + 114) % 31;
789
790 NaiveDate::from_ymd_opt(year, n as u32, (p + 1) as u32).unwrap()
791 }
792
793 pub fn calculate_ash_wednesday(easter: NaiveDate) -> NaiveDate {
795 easter - chrono::Duration::days(46)
796 }
797
798 pub fn calculate_pentecost(easter: NaiveDate) -> NaiveDate {
800 easter + chrono::Duration::days(49)
801 }
802
803 pub fn calculate_advent_start(year: i32) -> NaiveDate {
805 let christmas = NaiveDate::from_ymd_opt(year, 12, 25).unwrap();
806 let mut advent_start = christmas;
807
808 for _ in 0..4 {
810 advent_start = advent_start - chrono::Duration::days(1);
811 while advent_start.weekday() != chrono::Weekday::Sun {
812 advent_start = advent_start - chrono::Duration::days(1);
813 }
814 }
815
816 advent_start
817 }
818
819 pub fn calculate_sunday_cycle(year: i32) -> u8 {
821 ((year - 1) % 3 + 1) as u8
825 }
826
827 pub fn calculate_weekday_cycle(year: i32) -> u8 {
828 ((year + 1) % 2 + 1) as u8
829 }
830
831 pub fn calculate_epiphany(year: i32) -> NaiveDate {
832 let jan1 = NaiveDate::from_ymd_opt(year, 1, 1).unwrap();
833 let mut epiphany = jan1;
834
835 loop {
836 epiphany = epiphany + chrono::Duration::days(1);
837 if epiphany.weekday() == chrono::Weekday::Sun {
838 break;
839 }
840 }
841
842 epiphany
843 }
844
845 pub fn calculate_holy_family(year: i32) -> NaiveDate {
846 let epiphany = calculate_epiphany(year + 1);
847 let mut holy_family = epiphany;
848 if NaiveDate::from_ymd_opt(year, 12, 25).unwrap().weekday() == chrono::Weekday::Sun {
849 return NaiveDate::from_ymd_opt(year, 12, 30).unwrap();
850 } else {
851 loop {
852 holy_family = holy_family - chrono::Duration::days(1);
853 if holy_family.weekday() == chrono::Weekday::Sun {
854 return holy_family;
855 }
856 }
857 }
858 }
859
860 pub fn calculate_christmas_end(year: i32) -> NaiveDate {
861 let epiphany = calculate_epiphany(year);
862 let mut christmas_end = epiphany;
863 if christmas_end.day() == 6 || christmas_end.day() == 7 {
864 return christmas_end + chrono::Duration::days(1);
865 } else {
866 loop {
867 christmas_end = christmas_end + chrono::Duration::days(1);
868 if christmas_end.weekday() == chrono::Weekday::Sun {
869 return christmas_end;
870 }
871 }
872 }
873
874 }
875
876 pub fn calculate_weeks_before_lent(year: i32) -> u8 {
877 let mut weeks: u8 = 0;
878 let easter = calculate_easter(year);
879 let mut sunday_index = calculate_ash_wednesday(easter);
880 let christmas_end = calculate_christmas_end(year);
881
882 while sunday_index > christmas_end {
883 loop {
884 sunday_index = sunday_index - chrono::Duration::days(1);
885 if sunday_index.weekday() == chrono::Weekday::Sun {
886 break;
887 }
888 }
889 weeks = weeks + 1;
890 }
891
892 return weeks;
893 }
894
895 pub fn calculate_pentecost_start_ot(year: i32) -> u8 {
896 let mut weeks: u8 = 0;
897 let mut sunday_index = calculate_advent_start(year);
898 let easter = calculate_easter(year);
899 let pentecost = calculate_pentecost(easter);
900
901 while sunday_index > pentecost {
902 loop {
903 sunday_index = sunday_index - chrono::Duration::days(1);
904 if sunday_index.weekday() == chrono::Weekday::Sun {
905 break;
906 }
907 }
908 weeks = weeks + 1;
909 }
910
911 let weeks_before_lent = calculate_weeks_before_lent(year);
912 if weeks + calculate_weeks_before_lent(year) == 34 {
913 return weeks_before_lent + 1;
914 } else {
915 return weeks_before_lent + 2;
916 }
917 }
918}
919
920
921pub fn search_bible(bible:&str, liturgy:&mut Vec<Readings>) {
923 for readings in liturgy {
924 if let Some(reading) = &mut readings.first {
925 search_reading(bible, reading);
927 }
928 if let Some(reading) = &mut readings.responsal {
929 search_reading(bible, reading);
930 }
931 if let Some(reading) = &mut readings.second {
932 search_reading(bible, reading);
933 }
934 if let Some(reading) = &mut readings.gospel {
935 search_reading(bible, reading);
936 }
937 }
938}
939
940fn search_reading(bible:&str, reading:&mut Reading) {
942 for verses in &mut reading.reading {
943 search_verses(bible, verses);
945 }
946}
947
948fn search_verses<'a>(bible:&'a str, verses:&mut Verses) -> Vec<String> {
949 let book = &verses.book;
950 let bible_lines: Vec<&str> = bible.lines().collect();
951 let book_start = match bible_lines.clone().into_iter().position(|line| line == book) {
952 Some(content) => content,
953 None => {return Vec::new();},
954 };
955 let mut result: Vec<String> = Vec::new();
956 let mut new_verses_vector: Vec<Verse> = Vec::new();
957
958 for verse in &mut verses.verses {
959 let chapter = verse.chapter;
960 let verse_number = verse.verse;
961 let chapter_number = verse.chapter;
962 let mut book_index = bible_lines.iter().skip(book_start);
963 let end_asterisk_regex = Regex::new(r"\s*\*[^*]*\*$").unwrap();
964 if let Some(range_end) = &verse.range_end {
967 let end_chapter = range_end.chapter;
969 let end_verse = range_end.verse;
970
971 let mut start_reading = false;
972 loop {
973 let line = book_index.clone().collect::<Vec<&&str>>()[1];
974 let line_split: Vec<&str> = line.split(':').collect();
976 let current_chapter = line_split[0].parse::<u32>().unwrap_or(0);
977 if current_chapter == 0 {break;}
978 let (unparsed_current_verse, result) = line_split[1].split_once(' ').unwrap();
979
980 let result = end_asterisk_regex.replace_all(&result.to_string(), "").trim().to_string();
981
982 let current_verse = unparsed_current_verse.parse::<u32>().unwrap();
984
985 if current_chapter == chapter_number && current_verse == verse_number {
986 start_reading = true;
987 }
988 if start_reading {
989 let new_verse = Verse {
993 chapter: current_chapter,
994 verse: current_verse,
995 translation: Some(result.to_string()),
996 range_end: None,
997 };
998 new_verses_vector.push(new_verse);
999 }
1000 if current_chapter >= end_chapter && current_verse >= end_verse {
1002 break;
1003 }
1004 book_index.next();
1005 }
1006
1007 } else {
1009 for line in book_index {
1010 if line.starts_with(&(chapter.to_string())) {
1011 let split_text: Vec<&str> = line.split(':').collect();
1012 if let Some(verse_text) = split_text.get(1) {
1013 if verse_text.starts_with(&(verse_number.to_string())) {
1014 let space_split = line.split_once(' ');
1015 let (_, verse_text) = space_split.unwrap_or(("", ""));
1016 let formatted_text = end_asterisk_regex.replace_all(verse_text, "").trim().to_string();
1017 result.push(formatted_text.clone());
1018
1019 verse.translation = Some(formatted_text.to_string());
1020 break;
1021 }
1022 }
1023 }
1024 }
1025 }
1026
1027 }
1028
1029 if !new_verses_vector.is_empty() {
1030 verses.verses = new_verses_vector;
1031 }
1032 return result;
1035}