opening_hours/localization/
localize.rs1use 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
9pub trait Localize: Clone + Send + Sync {
13 type DateTime: Clone + Add<Duration, Output = Self::DateTime>;
15
16 fn naive(&self, dt: Self::DateTime) -> NaiveDateTime;
18
19 fn datetime(&self, naive: NaiveDateTime) -> Self::DateTime;
21
22 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#[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#[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 pub fn new(tz: Tz) -> Self {
68 Self { tz, coords: None }
69 }
70
71 pub fn get_coords(&self) -> Option<Coordinates> {
73 self.coords
74 }
75
76 pub fn get_timezone(&self) -> &Tz {
78 &self.tz
79 }
80
81 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 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}