Skip to main content

opening_hours/localization/
localize.rs

1use std::fmt::Debug;
2use std::ops::Add;
3
4use chrono::{Duration, NaiveDate, NaiveDateTime, NaiveTime, TimeDelta, TimeZone};
5use opening_hours_syntax::rules::time::TimeEvent;
6
7use crate::localization::Coordinates;
8
9/// Specifies how dates should be localized while evaluating opening hours. No
10/// localisation is available by default but this can be used to specify a
11/// timezone and coordinates (which affect sun events).
12pub trait Localize: Clone + Send + Sync {
13    /// The type for localized date & time.
14    type DateTime: Clone + Add<Duration, Output = Self::DateTime>;
15
16    /// Get naive local time.
17    fn naive(&self, dt: Self::DateTime) -> NaiveDateTime;
18
19    /// Localize a naive datetime.
20    fn datetime(&self, naive: NaiveDateTime) -> Self::DateTime;
21
22    /// Get the localized time for a sun event at a given date.
23    fn event_time(&self, _date: NaiveDate, event: TimeEvent) -> NaiveTime {
24        match event {
25            TimeEvent::Dawn => const { NaiveTime::from_hms_opt(6, 0, 0).unwrap() },
26            TimeEvent::Sunrise => const { NaiveTime::from_hms_opt(7, 0, 0).unwrap() },
27            TimeEvent::Sunset => const { NaiveTime::from_hms_opt(19, 0, 0).unwrap() },
28            TimeEvent::Dusk => const { NaiveTime::from_hms_opt(20, 0, 0).unwrap() },
29        }
30    }
31}
32
33// No location info.
34#[derive(Clone, Debug, Default, Hash, PartialEq, Eq)]
35pub struct NoLocation;
36
37impl Localize for NoLocation {
38    type DateTime = NaiveDateTime;
39
40    fn naive(&self, dt: Self::DateTime) -> NaiveDateTime {
41        dt
42    }
43
44    fn datetime(&self, naive: NaiveDateTime) -> Self::DateTime {
45        naive
46    }
47}
48
49/// Time zone is specified and coordinates can optionally be specified for
50/// accurate sun events.
51#[derive(Clone, Debug, PartialEq)]
52pub struct TzLocation<Tz>
53where
54    Tz: TimeZone + Send + Sync,
55{
56    tz: Tz,
57    coords: Option<Coordinates>,
58}
59
60impl<Tz> TzLocation<Tz>
61where
62    Tz: TimeZone + Send + Sync,
63{
64    /// Create a new location context which only contains timezone information.
65    pub fn new(tz: Tz) -> Self {
66        Self { tz, coords: None }
67    }
68
69    /// Extract the coordinates for this location.
70    pub fn get_coords(&self) -> Option<Coordinates> {
71        self.coords
72    }
73
74    /// Extract the timezone for this location.
75    pub fn get_timezone(&self) -> &Tz {
76        &self.tz
77    }
78
79    /// Attach coordinates to the location context.
80    ///
81    /// If coordinates where already specified, they will be replaced with the
82    /// new ones.
83    pub fn with_coords(self, coords: Coordinates) -> Self {
84        Self { tz: self.tz, coords: Some(coords) }
85    }
86}
87
88#[cfg(feature = "auto-timezone")]
89impl TzLocation<chrono_tz::Tz> {
90    /// Create a new location context from a set of coordinates and with timezone
91    /// information inferred from this localization.
92    ///
93    /// Returns `None` if latitude or longitude is invalid.
94    ///
95    /// ```
96    /// use chrono_tz::Europe;
97    /// use opening_hours::localization::{Coordinates, TzLocation};
98    ///
99    /// let coords = Coordinates::new(48.8535, 2.34839).unwrap();
100    ///
101    /// assert_eq!(
102    ///     TzLocation::from_coords(coords),
103    ///     TzLocation::new(Europe::Paris).with_coords(coords),
104    /// );
105    /// ```
106    pub fn from_coords(coords: Coordinates) -> Self {
107        use std::collections::HashMap;
108        use std::sync::LazyLock;
109
110        static TZ_NAME_FINDER: LazyLock<tzf_rs::DefaultFinder> =
111            LazyLock::new(tzf_rs::DefaultFinder::new);
112
113        static TZ_BY_NAME: LazyLock<HashMap<&str, chrono_tz::Tz>> = LazyLock::new(|| {
114            chrono_tz::TZ_VARIANTS
115                .iter()
116                .copied()
117                .map(|tz| (tz.name(), tz))
118                .collect()
119        });
120
121        let tz_name = TZ_NAME_FINDER.get_tz_name(coords.lon(), coords.lat());
122
123        #[allow(clippy::unnecessary_lazy_evaluations)]
124        let tz = TZ_BY_NAME.get(tz_name).copied().unwrap_or_else(|| {
125            #[cfg(feature = "log")]
126            log::warn!("Could not find time zone `{tz_name}` at {coords}");
127            chrono_tz::UTC
128        });
129
130        Self::new(tz).with_coords(coords)
131    }
132}
133
134impl<Tz> Localize for TzLocation<Tz>
135where
136    Tz: TimeZone + Send + Sync,
137    Tz::Offset: Send + Sync,
138{
139    type DateTime = chrono::DateTime<Tz>;
140
141    fn naive(&self, dt: Self::DateTime) -> NaiveDateTime {
142        dt.with_timezone(&self.tz).naive_local()
143    }
144
145    fn datetime(&self, mut naive: NaiveDateTime) -> Self::DateTime {
146        loop {
147            if let Some(dt) = self.tz.from_local_datetime(&naive).latest() {
148                return dt;
149            }
150
151            naive = naive
152                .checked_add_signed(TimeDelta::minutes(1))
153                .expect("no valid datetime for time zone");
154        }
155    }
156
157    fn event_time(&self, date: NaiveDate, event: TimeEvent) -> NaiveTime {
158        let Some(coords) = self.coords else {
159            return NoLocation.event_time(date, event);
160        };
161
162        let Some(dt) = coords.event_time(date, event) else {
163            // If the event never happens (eg. at the poles), fallback to
164            // naïve algorithm.
165            return NoLocation.event_time(date, event);
166        };
167
168        self.naive(dt.with_timezone(&self.tz)).time()
169    }
170}