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) -> 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#[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#[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 pub fn new(tz: Tz) -> Self {
66 Self { tz, coords: None }
67 }
68
69 pub fn get_coords(&self) -> Option<Coordinates> {
71 self.coords
72 }
73
74 pub fn get_timezone(&self) -> &Tz {
76 &self.tz
77 }
78
79 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 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 return NoLocation.event_time(date, event);
166 };
167
168 self.naive(dt.with_timezone(&self.tz)).time()
169 }
170}