1#![allow(clippy::zero_prefixed_literal)]
12
13mod data;
14pub use data::ZONES;
15#[cfg(test)]
16mod tests;
17
18use std::slice::Iter;
19use std::ops::RangeBounds;
20
21use greg::{
22 Point,
23 Frame,
24 utc
25};
26use greg::calendar::{
27 Utc,
28 DateTime,
29};
30use greg::calendar::zone::{
31 Shift,
32 Steady,
33 Unsteady
34};
35
36#[derive(Copy, Clone, Eq, PartialEq)]
40pub struct Zone {
41 name: &'static str,
42 lmt: Offset,
43 offsets: &'static [(Point, Offset)]
44}
45
46#[derive(Copy, Clone, Debug, Eq, PartialEq)]
54pub struct Offset {
55 name: &'static str,
56 seconds: i64
57}
58
59#[derive(Debug, PartialEq, Eq)]
67pub enum Ambiguity {
68 Skipped(Point),
70 Repeated([(Point, Offset); 2])
72}
73
74pub struct OffsetFrames {
76 offsets: Iter<'static, (Point, Offset)>,
77 current: Option<(Point, Offset)>
78}
79
80impl Iterator for OffsetFrames {
81 type Item = (Frame, Offset);
82 fn next(&mut self) -> Option<Self::Item> {
83 match (self.current.take(), self.offsets.next()) {
84 (Some((curr_p, curr_off)), Some(&(next_p, next_off))) => {
85 self.current = Some((next_p, next_off));
86 let frame = Frame {
87 start: curr_p,
88 stop: next_p
89 };
90 Some((frame, curr_off))
91 },
92 (Some((last_p, last_off)), None) => Some((
93 Frame {start: last_p, stop: Point::from_epoch(i64::MAX)},
94 last_off
95 )),
96 (None, _) => None
97 }
98 }
99}
100impl ExactSizeIterator for OffsetFrames {
101 fn len(&self) -> usize {
102 self.offsets.len() + self.current.iter().count()
103 }
104}
105
106impl Offset {
111 const fn new(name: &'static str, seconds: i64) -> Self {
112 Self {name, seconds}
113 }
114 pub const fn name(self) -> &'static str {self.name}
116}
117
118impl Shift for Offset {
119 fn apply(&self, point: Point) -> Point {
120 let timestamp = point.timestamp + self.seconds;
121 Point {timestamp}
122 }
123}
124
125impl Steady for Offset {
126 fn revert(&self, point: Point) -> Point {
127 let timestamp = point.timestamp - self.seconds;
128 Point {timestamp}
129 }
130}
131
132impl Zone {
138 const fn define(
139 name: &'static str,
140 lmt: Offset,
141 offsets: &'static [(Point, Offset)])
142 -> Self
143 {
144 Self {name, lmt, offsets}
145 }
146 const fn alias(name: &'static str, alias: Self) -> Self {
147 Self {name, ..alias}
148 }
149
150 pub const fn name(self) -> &'static str {self.name}
152
153 fn contains(point: Point) -> bool {
154 (utc!(1800-01-01)..utc!(2100-01-01)).contains(&point)
155 }
156
157 pub fn format_with_name(&self, point: Point) -> String {
161 let offset = self.offset_at(point);
162 let DateTime(date, time) = Utc::lookup(offset.apply(point));
163 format!("{date} {time} {}", offset.name())
164 }
165
166 pub fn offset_at(&self, point: Point) -> Offset {
168 assert!(
169 Self::contains(point),
170 "Point out of valid time zone range 1800..2100"
171 );
172 self.offsets.iter()
173 .rev()
174 .find(|&&(p, _offset)| p <= point)
175 .map(|&(_p, offset)| offset)
176 .unwrap_or(self.lmt)
177 }
178
179 pub fn offsets(&self) -> &'static [(Point, Offset)] {self.offsets}
187
188 pub fn offset_frames(&self) -> OffsetFrames {
190 let offsets = self.offsets.iter();
191 let current = Some((Point::from_epoch(i64::MIN), self.lmt));
192 OffsetFrames {offsets, current}
193 }
194}
195
196impl Shift for Zone {
197 fn apply(&self, point: Point) -> Point {
198 self.offset_at(point).apply(point)
199 }
200}
201
202
203impl Unsteady for Zone {
204 type Ambiguity = Ambiguity;
205
206 fn revert_lossy(&self, utc_point: Point) -> Point {
207 assert!(
208 Self::contains(utc_point),
209 "Point out of valid time zone range 1800..2100"
210 );
211
212 for (frame, offset) in self.offset_frames() {
213 let reverted = offset.revert(utc_point);
214 if frame.contains(&reverted) {
215 return reverted;
216 }
217 else if frame.start > reverted {
218 return frame.start;
220 }
221 }
222 unreachable!()
223 }
224
225 fn try_revert(&self, utc_point: Point) -> Result<Point, Ambiguity> {
226 assert!(
227 Self::contains(utc_point),
228 "Point out of valid time zone range 1800..2100"
229 );
230 let mut found = None;
231 for (frame, offset) in self.offset_frames() {
232 let reverted = offset.revert(utc_point);
233 match frame.contains(&reverted) {
234 true => match found {
235 Some(previous) => return Err(Ambiguity::Repeated(
236 [previous, (reverted, offset)]
237 )),
238 None => found = Some((reverted, offset))
239 },
240 false => match found {
241 Some((point, _)) => return Ok(point),
242 None if frame.start > reverted => return Err(
243 Ambiguity::Skipped(frame.start)
244 ),
245 None => continue
246 }
247 }
248 }
249 unreachable!()
250 }
251}
252