1mod schema;
2
3use chrono::{Datelike, Local};
4use schema::{holidays_schema::HolidayRoot, sections_schema::SectionsRoot};
5use scraper::{Html, Selector};
6use std::str::FromStr;
7use thiserror::Error;
8
9#[derive(Debug)]
10pub struct Holiday {
11 pub name: String,
12 pub wikipedia_url: String,
13}
14
15impl Holiday {
16 pub fn get_greeting(&self) -> String {
17 let contains_day = self.name.to_lowercase().contains("day");
18 let contains_month = self.name.to_lowercase().contains("month");
19 if contains_day || contains_month {
20 format!("Happy {}", self.name)
21 } else {
22 format!("Happy {} Day", self.name)
23 }
24 }
25}
26
27#[derive(Error, Debug)]
28pub enum HolidayErrors {
29 #[error("Failed to connect to wikipedia")]
30 Reqwest(#[from] reqwest::Error),
31 #[error("Wikipedia page for {0} doesn't have a holidays section")]
32 NoHolidaysFound(String),
33}
34
35#[derive(Debug)]
36pub enum HolidayDate {
37 Today,
38 ManualDate { month: u32, day: u32 },
39}
40
41impl HolidayDate {
42 pub fn get_date(&self) -> String {
43 match self {
44 HolidayDate::Today => {
45 let today = Local::today();
46 let month = get_month(today.month());
47 format!("{}_{}", month, today.day())
48 }
49 HolidayDate::ManualDate { month, day } => {
50 let month = get_month(*month);
51 format!("{}_{}", month, day)
52 }
53 }
54 }
55}
56
57#[derive(Error, Debug)]
58pub enum HolidayDateError {
59 #[error("Invalid date format: {0}")]
60 InvalidDate(String),
61 #[error("Invalid month: {0}")]
62 MonthParseError(String),
63 #[error("Invalid day: {0}")]
64 DayParseError(#[from] std::num::ParseIntError),
65}
66
67impl FromStr for HolidayDate {
68 type Err = HolidayDateError;
69
70 fn from_str(s: &str) -> Result<Self, Self::Err> {
71 let parts: Vec<&str> = s.split('_').collect();
72 if parts.len() != 2 {
73 return Err(Self::Err::InvalidDate(s.to_string()));
74 }
75 let month = parse_month(parts[0])?;
76 let day = parts[1].parse::<u32>()?;
77
78 Ok(HolidayDate::ManualDate { month, day })
79 }
80}
81
82fn get_month(m: u32) -> &'static str {
83 match m {
84 1 => "January",
85 2 => "February",
86 3 => "March",
87 4 => "April",
88 5 => "May",
89 6 => "June",
90 7 => "July",
91 8 => "August",
92 9 => "September",
93 10 => "October",
94 11 => "November",
95 12 => "December",
96 _ => panic!("Invalid month"),
97 }
98}
99
100fn parse_month(s: &str) -> Result<u32, HolidayDateError> {
101 match s {
102 "January" => Ok(1),
103 "February" => Ok(2),
104 "March" => Ok(3),
105 "April" => Ok(4),
106 "May" => Ok(5),
107 "June" => Ok(6),
108 "July" => Ok(7),
109 "August" => Ok(8),
110 "September" => Ok(9),
111 "October" => Ok(10),
112 "November" => Ok(11),
113 "December" => Ok(12),
114 _ => Err(HolidayDateError::MonthParseError(s.to_string())),
115 }
116}
117
118pub async fn get_holidays(date: &HolidayDate) -> Result<Vec<Holiday>, HolidayErrors> {
119 let resp = reqwest::get(format!(
120 "https://en.wikipedia.org/w/api.php/?action=parse&format=json&prop=sections&page={}",
121 date.get_date()
122 ))
123 .await?
124 .json::<SectionsRoot>()
125 .await?;
126
127 let section = resp
128 .parse
129 .sections
130 .iter()
131 .find(|section| section.line == "Holidays and observances")
132 .ok_or_else(|| HolidayErrors::NoHolidaysFound(date.get_date()))?;
133
134 let resp = reqwest::get(format!("https://en.wikipedia.org/w/api.php/?action=parse&format=json&prop=text&disableeditsection=1&page={}§ion={}", date.get_date(), section.index)).await?.json::<HolidayRoot>().await?;
135
136 let document = Html::parse_document(&resp.parse.text.field);
137 let selector = Selector::parse("li a:nth-child(1)").unwrap();
138 let holidays = document
139 .select(&selector)
140 .filter(|e| e.inner_html() != "feast day")
141 .filter(|e| {
142 e.value()
143 .attr("href")
144 .map(|h| h.starts_with("/wiki/") && h != "/wiki/Feast_day")
145 .unwrap_or(false)
146 })
147 .map(|e| {
148 let name = e.text().fold(String::new(), |mut acc, el| {
149 acc.push_str(el);
150 acc
151 });
152 let href = e
153 .value()
154 .attr("href")
155 .map(|url| format!("https://en.wikipedia.org{}", url))
156 .unwrap_or(format!(
157 "https://en.wikipedia.org/w/index.php?search={name}"
158 ));
159
160 Holiday {
161 name,
162 wikipedia_url: href,
163 }
164 })
165 .collect::<Vec<_>>();
166
167 Ok(holidays)
168}