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        static DB_PUBLIC_GLOBAL: LazyLock<HashMap<Country, Arc<CompactCalendar>>> =
82            LazyLock::new(|| {
83                decode_holidays_db(
84                    env!("HOLIDAYS_PUBLIC_GLOBAL_REGIONS"),
85                    include_bytes!(env!("HOLIDAYS_PUBLIC_GLOBAL_FILE")),
86                )
87            });
88
89        static DB_SCHOOL_GLOBAL: LazyLock<HashMap<Country, Arc<CompactCalendar>>> =
90            LazyLock::new(|| {
91                decode_holidays_db(
92                    env!("HOLIDAYS_SCHOOL_GLOBAL_REGIONS"),
93                    include_bytes!(env!("HOLIDAYS_SCHOOL_GLOBAL_FILE")),
94                )
95            });
96
97        ContextHolidays::new(
98            DB_PUBLIC_GLOBAL.get(&self).cloned().unwrap_or_default(),
99            DB_SCHOOL_GLOBAL.get(&self).cloned().unwrap_or_default(),
100        )
101    }
102
103    /// Load regional holidays for this country from a compact embedded
104    /// database.
105    ///
106    /// ```
107    /// use chrono::NaiveDate;
108    /// use opening_hours::localization::Country;
109    ///
110    /// let holidays_de = Country::DE.holidays_regional();
111    /// let date = NaiveDate::from_ymd_opt(2025, 3, 8).unwrap(); // International Women's Day
112    /// assert!(holidays_de.get_public().contains(date));
113    /// ```
114    pub fn holidays_regional(self) -> ContextHolidays {
115        static DB_PUBLIC_REGIONAL: LazyLock<HashMap<Country, Arc<CompactCalendar>>> =
116            LazyLock::new(|| {
117                decode_holidays_db(
118                    env!("HOLIDAYS_PUBLIC_REGIONAL_REGIONS"),
119                    include_bytes!(env!("HOLIDAYS_PUBLIC_REGIONAL_FILE")),
120                )
121            });
122
123        static DB_SCHOOL_REGIONAL: LazyLock<HashMap<Country, Arc<CompactCalendar>>> =
124            LazyLock::new(|| {
125                decode_holidays_db(
126                    env!("HOLIDAYS_SCHOOL_REGIONAL_REGIONS"),
127                    include_bytes!(env!("HOLIDAYS_SCHOOL_REGIONAL_FILE")),
128                )
129            });
130
131        ContextHolidays::new(
132            DB_PUBLIC_REGIONAL.get(&self).cloned().unwrap_or_default(),
133            DB_SCHOOL_REGIONAL.get(&self).cloned().unwrap_or_default(),
134        )
135    }
136}
137
138/// Helper function to parse the compact calendars generated at build time
139fn decode_holidays_db(
140    countries: &'static str,
141    encoded_data: &'static [u8],
142) -> HashMap<Country, Arc<CompactCalendar>> {
143    let mut reader = DeflateDecoder::new(encoded_data);
144
145    countries
146        .split(',')
147        .filter_map(|region| {
148            let calendar =
149                CompactCalendar::deserialize(&mut reader).expect("unable to parse holiday data");
150
151            let Ok(country) = region.parse() else {
152                #[cfg(feature = "log")]
153                log::warn!("Unknown initialized country code {region}");
154                return None;
155            };
156
157            Some((country, Arc::new(calendar)))
158        })
159        .collect()
160}