ferio/
lib.rs

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={}&section={}", 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}