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) -> Option<NaiveTime> {
24        let dt = 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        Some(dt)
32    }
33}
34
35// No location info.
36#[derive(Clone, Debug, Default, Hash, PartialEq, Eq)]
37pub struct NoLocation;
38
39impl Localize for NoLocation {
40    type DateTime = NaiveDateTime;
41
42    fn naive(&self, dt: Self::DateTime) -> NaiveDateTime {
43        dt
44    }
45
46    fn datetime(&self, naive: NaiveDateTime) -> Self::DateTime {
47        naive
48    }
49}
50
51/// Time zone is specified and coordinates can optionally be specified for
52/// accurate sun events.
53#[derive(Clone, Debug, PartialEq)]
54pub struct TzLocation<Tz>
55where
56    Tz: TimeZone + Send + Sync,
57{
58    tz: Tz,
59    coords: Option<Coordinates>,
60}
61
62impl<Tz> TzLocation<Tz>
63where
64    Tz: TimeZone + Send + Sync,
65{
66    /// Create a new location context which only contains timezone information.
67    pub fn new(tz: Tz) -> Self {
68        Self { tz, coords: None }
69    }
70
71    /// Extract the coordinates for this location.
72    pub fn get_coords(&self) -> Option<Coordinates> {
73        self.coords
74    }
75
76    /// Extract the timezone for this location.
77    pub fn get_timezone(&self) -> &Tz {
78        &self.tz
79    }
80
81    /// Attach coordinates to the location context.
82    ///
83    /// If coordinates where already specified, they will be replaced with the
84    /// new ones.
85    pub fn with_coords(self, coords: Coordinates) -> Self {
86        Self { tz: self.tz, coords: Some(coords) }
87    }
88}
89
90#[cfg(feature = "auto-timezone")]
91impl TzLocation<chrono_tz::Tz> {
92    /// Create a new location context from a set of coordinates and with timezone
93    /// information inferred from this localization.
94    ///
95    /// Returns `None` if latitude or longitude is invalid.
96    ///
97    /// ```
98    /// use chrono_tz::Europe;
99    /// use opening_hours::localization::{Coordinates, TzLocation};
100    ///
101    /// let coords = Coordinates::new(48.8535, 2.34839).unwrap();
102    ///
103    /// assert_eq!(
104    ///     TzLocation::from_coords(coords),
105    ///     TzLocation::new(Europe::Paris).with_coords(coords),
106    /// );
107    /// ```
108    pub fn from_coords(coords: Coordinates) -> Self {
109        use std::collections::HashMap;
110        use std::sync::LazyLock;
111
112        static TZ_NAME_FINDER: LazyLock<tzf_rs::DefaultFinder> =
113            LazyLock::new(tzf_rs::DefaultFinder::new);
114
115        static TZ_BY_NAME: LazyLock<HashMap<&str, chrono_tz::Tz>> = LazyLock::new(|| {
116            chrono_tz::TZ_VARIANTS
117                .iter()
118                .copied()
119                .map(|tz| (tz.name(), tz))
120                .collect()
121        });
122
123        let tz_name = TZ_NAME_FINDER.get_tz_name(coords.lon(), coords.lat());
124
125        #[allow(clippy::unnecessary_lazy_evaluations)]
126        let tz = TZ_BY_NAME.get(tz_name).copied().unwrap_or_else(|| {
127            #[cfg(feature = "log")]
128            log::warn!("Could not find time zone `{tz_name}` at {coords}");
129            chrono_tz::UTC
130        });
131
132        Self::new(tz).with_coords(coords)
133    }
134}
135
136impl<Tz> Localize for TzLocation<Tz>
137where
138    Tz: TimeZone + Send + Sync,
139    Tz::Offset: Send + Sync,
140{
141    type DateTime = chrono::DateTime<Tz>;
142
143    fn naive(&self, dt: Self::DateTime) -> NaiveDateTime {
144        dt.with_timezone(&self.tz).naive_local()
145    }
146
147    fn datetime(&self, mut naive: NaiveDateTime) -> Self::DateTime {
148        loop {
149            if let Some(dt) = self.tz.from_local_datetime(&naive).latest() {
150                return dt;
151            }
152
153            naive = naive
154                .checked_add_signed(TimeDelta::minutes(1))
155                .expect("no valid datetime for time zone");
156        }
157    }
158
159    fn event_time(&self, date: NaiveDate, event: TimeEvent) -> Option<NaiveTime> {
160        let Some(coords) = self.coords else {
161            return NoLocation.event_time(date, event);
162        };
163
164        let dt = coords.event_time(date, event)?;
165        Some(self.naive(dt.with_timezone(&self.tz)).time())
166    }
167}