Skip to main content

opening_hours/localization/country/
mod.rs

1mod generated;
2
3use std::collections::HashMap;
4use std::sync::{Arc, LazyLock};
5
6use compact_calendar::CompactCalendar;
7use flate2::bufread::DeflateDecoder;
8
9use crate::ContextHolidays;
10
11// Fetch generated code
12pub use generated::*;
13
14// --
15// -- Errors
16// --
17
18/// Could not find input ISO code.
19#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
20pub struct UnknownCountryCode(pub String);
21
22impl std::fmt::Display for UnknownCountryCode {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        write!(f, "Unknown ISO code `{}`", self.0)
25    }
26}
27
28impl std::error::Error for UnknownCountryCode {}
29
30// --
31// -- Country Implems
32// --
33
34impl Country {
35    /// Attempt to automatically detect a country from coordinates.
36    ///
37    /// ```
38    /// use opening_hours::localization::{Coordinates, Country};
39    ///
40    /// let coords = Coordinates::new(48.86, 2.34).unwrap();
41    /// let country_paris = Country::try_from_coords(coords).unwrap();
42    /// assert_eq!(country_paris, Country::FR);
43    /// ```
44    #[cfg(feature = "auto-country")]
45    pub fn try_from_coords(coords: crate::localization::Coordinates) -> Option<Self> {
46        use country_boundaries::CountryBoundaries;
47        use std::io::Read;
48
49        static BOUNDARIES: LazyLock<CountryBoundaries> = LazyLock::new(|| {
50            let mut buffer = Vec::new();
51
52            DeflateDecoder::new(include_bytes!(env!("COUNTRY_BOUNDS_FILE")).as_slice())
53                .read_to_end(&mut buffer)
54                .expect("unable to parse country bounds data");
55
56            CountryBoundaries::from_reader(buffer.as_slice())
57                .expect("failed to load country boundaries database")
58        });
59
60        for cc in BOUNDARIES.ids(country_boundaries::LatLon::new(coords.lat(), coords.lon()).ok()?)
61        {
62            if let Ok(res) = cc.parse() {
63                return Some(res);
64            }
65        }
66
67        None
68    }
69
70    /// Load holidays for this country from a compact embedded database.
71    ///
72    /// ```
73    /// use chrono::NaiveDate;
74    /// use opening_hours::localization::Country;
75    ///
76    /// let holidays_fr = Country::FR.holidays();
77    /// let date = NaiveDate::from_ymd_opt(2024, 7, 14).unwrap(); // french national day
78    /// assert!(holidays_fr.get_public().contains(date));
79    /// ```
80    pub fn holidays(self) -> ContextHolidays {
81        fn decode_holidays_db(
82            countries: &'static str,
83            encoded_data: &'static [u8],
84        ) -> HashMap<Country, Arc<CompactCalendar>> {
85            let mut reader = DeflateDecoder::new(encoded_data);
86
87            countries
88                .split(',')
89                .filter_map(|region| {
90                    let calendar = CompactCalendar::deserialize(&mut reader)
91                        .expect("unable to parse holiday data");
92
93                    let Ok(country) = region.parse() else {
94                        #[cfg(feature = "log")]
95                        log::warn!("Unknown initialized country code {region}");
96                        return None;
97                    };
98
99                    Some((country, Arc::new(calendar)))
100                })
101                .collect()
102        }
103
104        static DB_PUBLIC: LazyLock<HashMap<Country, Arc<CompactCalendar>>> = LazyLock::new(|| {
105            decode_holidays_db(
106                env!("HOLIDAYS_PUBLIC_REGIONS"),
107                include_bytes!(env!("HOLIDAYS_PUBLIC_FILE")),
108            )
109        });
110
111        static DB_SCHOOL: LazyLock<HashMap<Country, Arc<CompactCalendar>>> = LazyLock::new(|| {
112            decode_holidays_db(
113                env!("HOLIDAYS_SCHOOL_REGIONS"),
114                include_bytes!(env!("HOLIDAYS_SCHOOL_FILE")),
115            )
116        });
117
118        ContextHolidays::new(
119            DB_PUBLIC.get(&self).cloned().unwrap_or_default(),
120            DB_SCHOOL.get(&self).cloned().unwrap_or_default(),
121        )
122    }
123}