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 => 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#[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_timezone(&self) -> &Tz {
71 &self.tz
72 }
73
74 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 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 return NoLocation.event_time(date, event);
161 };
162
163 self.naive(dt.with_timezone(&self.tz)).time()
164 }
165}