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 => NaiveTime::from_hms_opt(6, 0, 0).unwrap(),
26            TimeEvent::Sunrise => NaiveTime::from_hms_opt(7, 0, 0).unwrap(),
27            TimeEvent::Sunset => NaiveTime::from_hms_opt(19, 0, 0).unwrap(),
28            TimeEvent::Dusk => 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 timezone for this location.
70    pub fn get_timezone(&self) -> &Tz {
71        &self.tz
72    }
73
74    /// Attach coordinates to the location context.
75    ///
76    /// If coordinates where already specified, they will be replaced with the
77    /// new ones.
78    pub fn with_coords(self, coords: Coordinates) -> Self {
79        Self { tz: self.tz, coords: Some(coords) }
80    }
81}
82
83#[cfg(feature = "auto-timezone")]
84impl TzLocation<chrono_tz::Tz> {
85    /// Create a new location context from a set of coordinates and with timezone
86    /// information inferred from this localization.
87    ///
88    /// Returns `None` if latitude or longitude is invalid.
89    ///
90    /// ```
91    /// use chrono_tz::Europe;
92    /// use opening_hours::localization::{Coordinates, TzLocation};
93    ///
94    /// let coords = Coordinates::new(48.8535, 2.34839).unwrap();
95    ///
96    /// assert_eq!(
97    ///     TzLocation::from_coords(coords),
98    ///     TzLocation::new(Europe::Paris).with_coords(coords),
99    /// );
100    /// ```
101    pub fn from_coords(coords: Coordinates) -> Self {
102        use std::collections::HashMap;
103        use std::sync::LazyLock;
104
105        static TZ_NAME_FINDER: LazyLock<tzf_rs::DefaultFinder> =
106            LazyLock::new(tzf_rs::DefaultFinder::new);
107
108        static TZ_BY_NAME: LazyLock<HashMap<&str, chrono_tz::Tz>> = LazyLock::new(|| {
109            chrono_tz::TZ_VARIANTS
110                .iter()
111                .copied()
112                .map(|tz| (tz.name(), tz))
113                .collect()
114        });
115
116        let tz_name = TZ_NAME_FINDER.get_tz_name(coords.lon(), coords.lat());
117
118        #[allow(clippy::unnecessary_lazy_evaluations)]
119        let tz = TZ_BY_NAME.get(tz_name).copied().unwrap_or_else(|| {
120            #[cfg(feature = "log")]
121            log::warn!("Could not find time zone `{tz_name}` at {coords}");
122            chrono_tz::UTC
123        });
124
125        Self::new(tz).with_coords(coords)
126    }
127}
128
129impl<Tz> Localize for TzLocation<Tz>
130where
131    Tz: TimeZone + Send + Sync,
132    Tz::Offset: Send + Sync,
133{
134    type DateTime = chrono::DateTime<Tz>;
135
136    fn naive(&self, dt: Self::DateTime) -> NaiveDateTime {
137        dt.with_timezone(&self.tz).naive_local()
138    }
139
140    fn datetime(&self, mut naive: NaiveDateTime) -> Self::DateTime {
141        loop {
142            if let Some(dt) = self.tz.from_local_datetime(&naive).latest() {
143                return dt;
144            }
145
146            naive = naive
147                .checked_add_signed(TimeDelta::minutes(1))
148                .expect("no valid datetime for time zone");
149        }
150    }
151
152    fn event_time(&self, date: NaiveDate, event: TimeEvent) -> NaiveTime {
153        let Some(coords) = self.coords else {
154            return NoLocation.event_time(date, event);
155        };
156
157        let Some(dt) = coords.event_time(date, event) else {
158            // If the event never happens (eg. at the poles), fallback to
159            // naïve algorithm.
160            return NoLocation.event_time(date, event);
161        };
162
163        self.naive(dt.with_timezone(&self.tz)).time()
164    }
165}