/rust/registry/src/index.crates.io-1949cf8c6b5b557f/jiff-0.2.17/src/shared/posix.rs
Line | Count | Source |
1 | | use core::fmt::Debug; |
2 | | |
3 | | use super::{ |
4 | | util::{ |
5 | | array_str::Abbreviation, |
6 | | itime::{ |
7 | | IAmbiguousOffset, IDate, IDateTime, IOffset, ITime, ITimeSecond, |
8 | | ITimestamp, IWeekday, |
9 | | }, |
10 | | }, |
11 | | PosixDay, PosixDayTime, PosixDst, PosixOffset, PosixRule, PosixTime, |
12 | | PosixTimeZone, |
13 | | }; |
14 | | |
15 | | impl PosixTimeZone<Abbreviation> { |
16 | | /// Parse a POSIX `TZ` environment variable, assuming it's a rule and not |
17 | | /// an implementation defined value, from the given bytes. |
18 | 0 | pub fn parse( |
19 | 0 | bytes: &[u8], |
20 | 0 | ) -> Result<PosixTimeZone<Abbreviation>, PosixTimeZoneError> { |
21 | | // We enable the IANA v3+ extensions here. (Namely, that the time |
22 | | // specification hour value has the range `-167..=167` instead of |
23 | | // `0..=24`.) Requiring strict POSIX rules doesn't seem necessary |
24 | | // since the extension is a strict superset. Plus, GNU tooling |
25 | | // seems to accept the extension. |
26 | 0 | let parser = Parser { ianav3plus: true, ..Parser::new(bytes) }; |
27 | 0 | parser.parse() |
28 | 0 | } |
29 | | |
30 | | // only-jiff-start |
31 | | /// Like parse, but parses a prefix of the input given and returns whatever |
32 | | /// is remaining. |
33 | 0 | pub fn parse_prefix<'b>( |
34 | 0 | bytes: &'b [u8], |
35 | 0 | ) -> Result<(PosixTimeZone<Abbreviation>, &'b [u8]), PosixTimeZoneError> |
36 | | { |
37 | 0 | let parser = Parser { ianav3plus: true, ..Parser::new(bytes) }; |
38 | 0 | parser.parse_prefix() |
39 | 0 | } |
40 | | // only-jiff-end |
41 | | } |
42 | | |
43 | | impl<ABBREV: AsRef<str> + Debug> PosixTimeZone<ABBREV> { |
44 | | /// Returns the appropriate time zone offset to use for the given |
45 | | /// timestamp. |
46 | | /// |
47 | | /// If you need information like whether the offset is in DST or not, or |
48 | | /// the time zone abbreviation, then use `PosixTimeZone::to_offset_info`. |
49 | | /// But that API may be more expensive to use, so only use it if you need |
50 | | /// the additional data. |
51 | 0 | pub(crate) fn to_offset(&self, timestamp: ITimestamp) -> IOffset { |
52 | 0 | let std_offset = self.std_offset.to_ioffset(); |
53 | 0 | if self.dst.is_none() { |
54 | 0 | return std_offset; |
55 | 0 | } |
56 | | |
57 | 0 | let dt = timestamp.to_datetime(IOffset::UTC); |
58 | 0 | self.dst_info_utc(dt.date.year) |
59 | 0 | .filter(|dst_info| dst_info.in_dst(dt)) Unexecuted instantiation: <jiff::shared::PosixTimeZone<jiff::shared::util::array_str::ArrayStr<30>>>::to_offset::{closure#0}Unexecuted instantiation: <jiff::shared::PosixTimeZone<&str>>::to_offset::{closure#0} |
60 | 0 | .map(|dst_info| dst_info.offset().to_ioffset()) Unexecuted instantiation: <jiff::shared::PosixTimeZone<jiff::shared::util::array_str::ArrayStr<30>>>::to_offset::{closure#1}Unexecuted instantiation: <jiff::shared::PosixTimeZone<&str>>::to_offset::{closure#1} |
61 | 0 | .unwrap_or_else(|| std_offset) |
62 | 0 | } Unexecuted instantiation: <jiff::shared::PosixTimeZone<jiff::shared::util::array_str::ArrayStr<30>>>::to_offset Unexecuted instantiation: <jiff::shared::PosixTimeZone<&str>>::to_offset |
63 | | |
64 | | /// Returns the appropriate time zone offset to use for the given |
65 | | /// timestamp. |
66 | | /// |
67 | | /// This also includes whether the offset returned should be considered |
68 | | /// to be "DST" or not, along with the time zone abbreviation (e.g., EST |
69 | | /// for standard time in New York, and EDT for DST in New York). |
70 | 0 | pub(crate) fn to_offset_info( |
71 | 0 | &self, |
72 | 0 | timestamp: ITimestamp, |
73 | 0 | ) -> (IOffset, &'_ str, bool) { |
74 | 0 | let std_offset = self.std_offset.to_ioffset(); |
75 | 0 | if self.dst.is_none() { |
76 | 0 | return (std_offset, self.std_abbrev.as_ref(), false); |
77 | 0 | } |
78 | | |
79 | 0 | let dt = timestamp.to_datetime(IOffset::UTC); |
80 | 0 | self.dst_info_utc(dt.date.year) |
81 | 0 | .filter(|dst_info| dst_info.in_dst(dt)) Unexecuted instantiation: <jiff::shared::PosixTimeZone<jiff::shared::util::array_str::ArrayStr<30>>>::to_offset_info::{closure#0}Unexecuted instantiation: <jiff::shared::PosixTimeZone<&str>>::to_offset_info::{closure#0} |
82 | 0 | .map(|dst_info| { |
83 | 0 | ( |
84 | 0 | dst_info.offset().to_ioffset(), |
85 | 0 | dst_info.dst.abbrev.as_ref(), |
86 | 0 | true, |
87 | 0 | ) |
88 | 0 | }) Unexecuted instantiation: <jiff::shared::PosixTimeZone<jiff::shared::util::array_str::ArrayStr<30>>>::to_offset_info::{closure#1}Unexecuted instantiation: <jiff::shared::PosixTimeZone<&str>>::to_offset_info::{closure#1} |
89 | 0 | .unwrap_or_else(|| (std_offset, self.std_abbrev.as_ref(), false)) Unexecuted instantiation: <jiff::shared::PosixTimeZone<jiff::shared::util::array_str::ArrayStr<30>>>::to_offset_info::{closure#2}Unexecuted instantiation: <jiff::shared::PosixTimeZone<&str>>::to_offset_info::{closure#2} |
90 | 0 | } Unexecuted instantiation: <jiff::shared::PosixTimeZone<jiff::shared::util::array_str::ArrayStr<30>>>::to_offset_info Unexecuted instantiation: <jiff::shared::PosixTimeZone<&str>>::to_offset_info |
91 | | |
92 | | /// Returns a possibly ambiguous timestamp for the given civil datetime. |
93 | | /// |
94 | | /// The given datetime should correspond to the "wall" clock time of what |
95 | | /// humans use to tell time for this time zone. |
96 | | /// |
97 | | /// Note that "ambiguous timestamp" is represented by the possible |
98 | | /// selection of offsets that could be applied to the given datetime. In |
99 | | /// general, it is only ambiguous around transitions to-and-from DST. The |
100 | | /// ambiguity can arise as a "fold" (when a particular wall clock time is |
101 | | /// repeated) or as a "gap" (when a particular wall clock time is skipped |
102 | | /// entirely). |
103 | 0 | pub(crate) fn to_ambiguous_kind(&self, dt: IDateTime) -> IAmbiguousOffset { |
104 | 0 | let year = dt.date.year; |
105 | 0 | let std_offset = self.std_offset.to_ioffset(); |
106 | 0 | let Some(dst_info) = self.dst_info_wall(year) else { |
107 | 0 | return IAmbiguousOffset::Unambiguous { offset: std_offset }; |
108 | | }; |
109 | 0 | let dst_offset = dst_info.offset().to_ioffset(); |
110 | 0 | let diff = dst_offset.second - std_offset.second; |
111 | | // When the difference between DST and standard is positive, that means |
112 | | // STD->DST results in a gap while DST->STD results in a fold. However, |
113 | | // when the difference is negative, that means STD->DST results in a |
114 | | // fold while DST->STD results in a gap. The former is by far the most |
115 | | // common. The latter is a bit weird, but real cases do exist. For |
116 | | // example, Dublin has DST in winter (UTC+01) and STD in the summer |
117 | | // (UTC+00). |
118 | | // |
119 | | // When the difference is zero, then we have a weird POSIX time zone |
120 | | // where a DST transition rule was specified, but was set to explicitly |
121 | | // be the same as STD. In this case, there can be no ambiguity. (The |
122 | | // zero case is strictly redundant. Both the diff < 0 and diff > 0 |
123 | | // cases handle the zero case correctly. But we write it out for |
124 | | // clarity.) |
125 | 0 | if diff == 0 { |
126 | 0 | debug_assert_eq!(std_offset, dst_offset); |
127 | 0 | IAmbiguousOffset::Unambiguous { offset: std_offset } |
128 | 0 | } else if diff.is_negative() { |
129 | | // For DST transitions that always move behind one hour, ambiguous |
130 | | // timestamps only occur when the given civil datetime falls in the |
131 | | // standard time range. |
132 | 0 | if dst_info.in_dst(dt) { |
133 | 0 | IAmbiguousOffset::Unambiguous { offset: dst_offset } |
134 | | } else { |
135 | 0 | let fold_start = dst_info.start.saturating_add_seconds(diff); |
136 | 0 | let gap_end = |
137 | 0 | dst_info.end.saturating_add_seconds(diff.saturating_neg()); |
138 | 0 | if fold_start <= dt && dt < dst_info.start { |
139 | 0 | IAmbiguousOffset::Fold { |
140 | 0 | before: std_offset, |
141 | 0 | after: dst_offset, |
142 | 0 | } |
143 | 0 | } else if dst_info.end <= dt && dt < gap_end { |
144 | 0 | IAmbiguousOffset::Gap { |
145 | 0 | before: dst_offset, |
146 | 0 | after: std_offset, |
147 | 0 | } |
148 | | } else { |
149 | 0 | IAmbiguousOffset::Unambiguous { offset: std_offset } |
150 | | } |
151 | | } |
152 | | } else { |
153 | | // For DST transitions that always move ahead one hour, ambiguous |
154 | | // timestamps only occur when the given civil datetime falls in the |
155 | | // DST range. |
156 | 0 | if !dst_info.in_dst(dt) { |
157 | 0 | IAmbiguousOffset::Unambiguous { offset: std_offset } |
158 | | } else { |
159 | | // PERF: I wonder if it makes sense to pre-compute these? |
160 | | // Probably not, because we have to do it based on year of |
161 | | // datetime given. But if we ever add a "caching" layer for |
162 | | // POSIX time zones, then it might be worth adding these to it. |
163 | 0 | let gap_end = dst_info.start.saturating_add_seconds(diff); |
164 | 0 | let fold_start = |
165 | 0 | dst_info.end.saturating_add_seconds(diff.saturating_neg()); |
166 | 0 | if dst_info.start <= dt && dt < gap_end { |
167 | 0 | IAmbiguousOffset::Gap { |
168 | 0 | before: std_offset, |
169 | 0 | after: dst_offset, |
170 | 0 | } |
171 | 0 | } else if fold_start <= dt && dt < dst_info.end { |
172 | 0 | IAmbiguousOffset::Fold { |
173 | 0 | before: dst_offset, |
174 | 0 | after: std_offset, |
175 | 0 | } |
176 | | } else { |
177 | 0 | IAmbiguousOffset::Unambiguous { offset: dst_offset } |
178 | | } |
179 | | } |
180 | | } |
181 | 0 | } Unexecuted instantiation: <jiff::shared::PosixTimeZone<jiff::shared::util::array_str::ArrayStr<30>>>::to_ambiguous_kind Unexecuted instantiation: <jiff::shared::PosixTimeZone<&str>>::to_ambiguous_kind |
182 | | |
183 | | /// Returns the timestamp of the most recent time zone transition prior |
184 | | /// to the timestamp given. If one doesn't exist, `None` is returned. |
185 | 0 | pub(crate) fn previous_transition( |
186 | 0 | &self, |
187 | 0 | timestamp: ITimestamp, |
188 | 0 | ) -> Option<(ITimestamp, IOffset, &'_ str, bool)> { |
189 | 0 | let dt = timestamp.to_datetime(IOffset::UTC); |
190 | 0 | let dst_info = self.dst_info_utc(dt.date.year)?; |
191 | 0 | let (earlier, later) = dst_info.ordered(); |
192 | 0 | let (prev, dst_info) = if dt > later { |
193 | 0 | (later, dst_info) |
194 | 0 | } else if dt > earlier { |
195 | 0 | (earlier, dst_info) |
196 | | } else { |
197 | 0 | let prev_year = dt.date.prev_year().ok()?; |
198 | 0 | let dst_info = self.dst_info_utc(prev_year)?; |
199 | 0 | let (_, later) = dst_info.ordered(); |
200 | 0 | (later, dst_info) |
201 | | }; |
202 | | |
203 | 0 | let timestamp = prev.to_timestamp_checked(IOffset::UTC)?; |
204 | 0 | let dt = timestamp.to_datetime(IOffset::UTC); |
205 | 0 | let (offset, abbrev, dst) = if dst_info.in_dst(dt) { |
206 | 0 | (dst_info.offset(), dst_info.dst.abbrev.as_ref(), true) |
207 | | } else { |
208 | 0 | (&self.std_offset, self.std_abbrev.as_ref(), false) |
209 | | }; |
210 | 0 | Some((timestamp, offset.to_ioffset(), abbrev, dst)) |
211 | 0 | } Unexecuted instantiation: <jiff::shared::PosixTimeZone<jiff::shared::util::array_str::ArrayStr<30>>>::previous_transition Unexecuted instantiation: <jiff::shared::PosixTimeZone<&str>>::previous_transition |
212 | | |
213 | | /// Returns the timestamp of the soonest time zone transition after the |
214 | | /// timestamp given. If one doesn't exist, `None` is returned. |
215 | 0 | pub(crate) fn next_transition( |
216 | 0 | &self, |
217 | 0 | timestamp: ITimestamp, |
218 | 0 | ) -> Option<(ITimestamp, IOffset, &'_ str, bool)> { |
219 | 0 | let dt = timestamp.to_datetime(IOffset::UTC); |
220 | 0 | let dst_info = self.dst_info_utc(dt.date.year)?; |
221 | 0 | let (earlier, later) = dst_info.ordered(); |
222 | 0 | let (next, dst_info) = if dt < earlier { |
223 | 0 | (earlier, dst_info) |
224 | 0 | } else if dt < later { |
225 | 0 | (later, dst_info) |
226 | | } else { |
227 | 0 | let next_year = dt.date.next_year().ok()?; |
228 | 0 | let dst_info = self.dst_info_utc(next_year)?; |
229 | 0 | let (earlier, _) = dst_info.ordered(); |
230 | 0 | (earlier, dst_info) |
231 | | }; |
232 | | |
233 | 0 | let timestamp = next.to_timestamp_checked(IOffset::UTC)?; |
234 | 0 | let dt = timestamp.to_datetime(IOffset::UTC); |
235 | 0 | let (offset, abbrev, dst) = if dst_info.in_dst(dt) { |
236 | 0 | (dst_info.offset(), dst_info.dst.abbrev.as_ref(), true) |
237 | | } else { |
238 | 0 | (&self.std_offset, self.std_abbrev.as_ref(), false) |
239 | | }; |
240 | 0 | Some((timestamp, offset.to_ioffset(), abbrev, dst)) |
241 | 0 | } Unexecuted instantiation: <jiff::shared::PosixTimeZone<jiff::shared::util::array_str::ArrayStr<30>>>::next_transition Unexecuted instantiation: <jiff::shared::PosixTimeZone<&str>>::next_transition |
242 | | |
243 | | /// Returns the range in which DST occurs. |
244 | | /// |
245 | | /// The civil datetimes returned are in UTC. This is useful for determining |
246 | | /// whether a timestamp is in DST or not. |
247 | 0 | fn dst_info_utc(&self, year: i16) -> Option<DstInfo<'_, ABBREV>> { |
248 | 0 | let dst = self.dst.as_ref()?; |
249 | | // DST time starts with respect to standard time, so offset it by the |
250 | | // standard offset. |
251 | 0 | let start = |
252 | 0 | dst.rule.start.to_datetime(year, self.std_offset.to_ioffset()); |
253 | | // DST time ends with respect to DST time, so offset it by the DST |
254 | | // offset. |
255 | 0 | let mut end = dst.rule.end.to_datetime(year, dst.offset.to_ioffset()); |
256 | | // This is a whacky special case when DST is permanent, but the math |
257 | | // using to calculate the start/end datetimes ends up leaving a gap |
258 | | // for standard time to appear. In which case, it's possible for a |
259 | | // timestamp at the end of a calendar year to get standard time when |
260 | | // it really should be DST. |
261 | | // |
262 | | // We detect this case by re-interpreting the end of the boundary using |
263 | | // the standard offset. If we get a datetime that is in a different |
264 | | // year, then it follows that standard time is actually impossible to |
265 | | // occur. |
266 | | // |
267 | | // These weird POSIX time zones can occur as the TZ strings in |
268 | | // a TZif file compiled using rearguard semantics. For example, |
269 | | // `Africa/Casablanca` has: |
270 | | // |
271 | | // XXX-2<+01>-1,0/0,J365/23 |
272 | | // |
273 | | // Notice here that DST is actually one hour *behind* (it is usually |
274 | | // one hour *ahead*) _and_ it ends at 23:00:00 on the last day of the |
275 | | // year. But if it ends at 23:00, then jumping to standard time moves |
276 | | // the clocks *forward*. Which would bring us to 00:00:00 on the first |
277 | | // of the next year... but that is when DST begins! Hence, DST is |
278 | | // permanent. |
279 | | // |
280 | | // Ideally, this could just be handled by our math automatically. But |
281 | | // I couldn't figure out how to make it work. In particular, in the |
282 | | // above example for year 2087, we get |
283 | | // |
284 | | // start == 2087-01-01T00:00:00Z |
285 | | // end == 2087-12-31T22:00:00Z |
286 | | // |
287 | | // Which leaves a two hour gap for a timestamp to get erroneously |
288 | | // categorized as standard time. |
289 | | // |
290 | | // ... so we special case this. We could pre-compute whether a POSIX |
291 | | // time zone is in permanent DST at construction time, but it's not |
292 | | // obvious to me that it's worth it. Especially since this is an |
293 | | // exceptionally rare case. |
294 | | // |
295 | | // Note that I did try to consult tzcode's (incredibly inscrutable) |
296 | | // `localtime` implementation to figure out how they deal with it. At |
297 | | // first, it looks like they don't have any special handling for this |
298 | | // case. But looking more closely, they skip any time zone transitions |
299 | | // generated by POSIX time zones whose rule spans more than 1 year: |
300 | | // |
301 | | // https://github.com/eggert/tz/blob/8d65db9786753f3b263087e31c59d191561d63e3/localtime.c#L1717-L1735 |
302 | | // |
303 | | // By just ignoring them, I think it achieves the desired effect of |
304 | | // permanent DST. But I'm not 100% confident in my understanding of |
305 | | // the code. |
306 | 0 | if start.date.month == 1 |
307 | 0 | && start.date.day == 1 |
308 | 0 | && start.time == ITime::MIN |
309 | | // NOTE: This should come last because it is potentially expensive. |
310 | 0 | && year |
311 | 0 | != end.saturating_add_seconds(self.std_offset.second).date.year |
312 | 0 | { |
313 | 0 | end = IDateTime { |
314 | 0 | date: IDate { year, month: 12, day: 31 }, |
315 | 0 | time: ITime::MAX, |
316 | 0 | }; |
317 | 0 | } |
318 | 0 | Some(DstInfo { dst, start, end }) |
319 | 0 | } Unexecuted instantiation: <jiff::shared::PosixTimeZone<jiff::shared::util::array_str::ArrayStr<30>>>::dst_info_utc Unexecuted instantiation: <jiff::shared::PosixTimeZone<&str>>::dst_info_utc |
320 | | |
321 | | /// Returns the range in which DST occurs. |
322 | | /// |
323 | | /// The civil datetimes returned are in "wall clock time." That is, they |
324 | | /// represent the transitions as they are seen from humans reading a clock |
325 | | /// within the geographic location of that time zone. |
326 | 0 | fn dst_info_wall(&self, year: i16) -> Option<DstInfo<'_, ABBREV>> { |
327 | 0 | let dst = self.dst.as_ref()?; |
328 | | // POSIX time zones express their DST transitions in terms of wall |
329 | | // clock time. Since this method specifically is returning wall |
330 | | // clock times, we don't want to offset our datetimes at all. |
331 | 0 | let start = dst.rule.start.to_datetime(year, IOffset::UTC); |
332 | 0 | let end = dst.rule.end.to_datetime(year, IOffset::UTC); |
333 | 0 | Some(DstInfo { dst, start, end }) |
334 | 0 | } Unexecuted instantiation: <jiff::shared::PosixTimeZone<jiff::shared::util::array_str::ArrayStr<30>>>::dst_info_wall Unexecuted instantiation: <jiff::shared::PosixTimeZone<&str>>::dst_info_wall |
335 | | |
336 | | /// Returns the DST transition rule. This panics if this time zone doesn't |
337 | | /// have DST. |
338 | | #[cfg(test)] |
339 | | fn rule(&self) -> &PosixRule { |
340 | | &self.dst.as_ref().unwrap().rule |
341 | | } |
342 | | } |
343 | | |
344 | | impl<ABBREV: AsRef<str>> core::fmt::Display for PosixTimeZone<ABBREV> { |
345 | 0 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { |
346 | 0 | core::fmt::Display::fmt( |
347 | 0 | &AbbreviationDisplay(self.std_abbrev.as_ref()), |
348 | 0 | f, |
349 | 0 | )?; |
350 | 0 | core::fmt::Display::fmt(&self.std_offset, f)?; |
351 | 0 | if let Some(ref dst) = self.dst { |
352 | 0 | dst.display(&self.std_offset, f)?; |
353 | 0 | } |
354 | 0 | Ok(()) |
355 | 0 | } |
356 | | } |
357 | | |
358 | | impl<ABBREV: AsRef<str>> PosixDst<ABBREV> { |
359 | 0 | fn display( |
360 | 0 | &self, |
361 | 0 | std_offset: &PosixOffset, |
362 | 0 | f: &mut core::fmt::Formatter, |
363 | 0 | ) -> core::fmt::Result { |
364 | 0 | core::fmt::Display::fmt( |
365 | 0 | &AbbreviationDisplay(self.abbrev.as_ref()), |
366 | 0 | f, |
367 | 0 | )?; |
368 | | // The overwhelming common case is that DST is exactly one hour ahead |
369 | | // of standard time. So common that this is the default. So don't write |
370 | | // the offset if we don't need to. |
371 | 0 | let default = PosixOffset { second: std_offset.second + 3600 }; |
372 | 0 | if self.offset != default { |
373 | 0 | core::fmt::Display::fmt(&self.offset, f)?; |
374 | 0 | } |
375 | 0 | f.write_str(",")?; |
376 | 0 | core::fmt::Display::fmt(&self.rule, f)?; |
377 | 0 | Ok(()) |
378 | 0 | } |
379 | | } |
380 | | |
381 | | impl core::fmt::Display for PosixRule { |
382 | 0 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { |
383 | 0 | core::fmt::Display::fmt(&self.start, f)?; |
384 | 0 | f.write_str(",")?; |
385 | 0 | core::fmt::Display::fmt(&self.end, f)?; |
386 | 0 | Ok(()) |
387 | 0 | } |
388 | | } |
389 | | |
390 | | impl PosixDayTime { |
391 | | /// Turns this POSIX datetime spec into a civil datetime in the year given |
392 | | /// with the given offset. The datetimes returned are offset by the given |
393 | | /// offset. For wall clock time, an offset of `0` should be given. For |
394 | | /// UTC time, the offset (standard or DST) corresponding to this time |
395 | | /// spec should be given. |
396 | | /// |
397 | | /// The datetime returned is guaranteed to have a year component equal |
398 | | /// to the year given. This guarantee is upheld even when the datetime |
399 | | /// specification (combined with the offset) would extend past the end of |
400 | | /// the year (or before the start of the year). In this case, the maximal |
401 | | /// (or minimal) datetime for the given year is returned. |
402 | 0 | pub(crate) fn to_datetime(&self, year: i16, offset: IOffset) -> IDateTime { |
403 | 0 | let mkmin = || IDateTime { |
404 | 0 | date: IDate { year, month: 1, day: 1 }, |
405 | | time: ITime::MIN, |
406 | 0 | }; |
407 | 0 | let mkmax = || IDateTime { |
408 | 0 | date: IDate { year, month: 12, day: 31 }, |
409 | | time: ITime::MAX, |
410 | 0 | }; |
411 | 0 | let Some(date) = self.date.to_date(year) else { return mkmax() }; |
412 | | // The range on `self.time` is `-604799..=604799`, and the range |
413 | | // on `offset.second` is `-93599..=93599`. Therefore, subtracting |
414 | | // them can never overflow an `i32`. |
415 | 0 | let offset = self.time.second - offset.second; |
416 | | // If the time goes negative or above 86400, then we might have |
417 | | // to adjust our date. |
418 | 0 | let days = offset.div_euclid(86400); |
419 | 0 | let second = offset.rem_euclid(86400); |
420 | | |
421 | 0 | let Ok(date) = date.checked_add_days(days) else { |
422 | 0 | return if offset < 0 { mkmin() } else { mkmax() }; |
423 | | }; |
424 | 0 | if date.year < year { |
425 | 0 | mkmin() |
426 | 0 | } else if date.year > year { |
427 | 0 | mkmax() |
428 | | } else { |
429 | 0 | let time = ITimeSecond { second }.to_time(); |
430 | 0 | IDateTime { date, time } |
431 | | } |
432 | 0 | } |
433 | | } |
434 | | |
435 | | impl core::fmt::Display for PosixDayTime { |
436 | 0 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { |
437 | 0 | core::fmt::Display::fmt(&self.date, f)?; |
438 | | // This is the default time, so don't write it if we |
439 | | // don't need to. |
440 | 0 | if self.time != PosixTime::DEFAULT { |
441 | 0 | f.write_str("/")?; |
442 | 0 | core::fmt::Display::fmt(&self.time, f)?; |
443 | 0 | } |
444 | 0 | Ok(()) |
445 | 0 | } |
446 | | } |
447 | | |
448 | | impl PosixDay { |
449 | | /// Convert this date specification to a civil date in the year given. |
450 | | /// |
451 | | /// If this date specification couldn't be turned into a date in the year |
452 | | /// given, then `None` is returned. This happens when `366` is given as |
453 | | /// a day, but the year given is not a leap year. In this case, callers may |
454 | | /// want to assume a datetime that is maximal for the year given. |
455 | 0 | fn to_date(&self, year: i16) -> Option<IDate> { |
456 | 0 | match *self { |
457 | 0 | PosixDay::JulianOne(day) => { |
458 | | // Parsing validates that our day is 1-365 which will always |
459 | | // succeed for all possible year values. That is, every valid |
460 | | // year has a December 31. |
461 | 0 | Some( |
462 | 0 | IDate::from_day_of_year_no_leap(year, day) |
463 | 0 | .expect("Julian `J day` should be in bounds"), |
464 | 0 | ) |
465 | | } |
466 | 0 | PosixDay::JulianZero(day) => { |
467 | | // OK because our value for `day` is validated to be `0..=365`, |
468 | | // and since it is an `i16`, it is always valid to add 1. |
469 | | // |
470 | | // Also, while `day+1` is guaranteed to be in `1..=366`, it is |
471 | | // possible that `366` is invalid, for when `year` is not a |
472 | | // leap year. In this case, we throw our hands up, and ask the |
473 | | // caller to make a decision for how to deal with it. Why does |
474 | | // POSIX go out of its way to specifically not specify behavior |
475 | | // in error cases? |
476 | 0 | IDate::from_day_of_year(year, day + 1).ok() |
477 | | } |
478 | 0 | PosixDay::WeekdayOfMonth { month, week, weekday } => { |
479 | 0 | let weekday = IWeekday::from_sunday_zero_offset(weekday); |
480 | 0 | let first = IDate { year, month, day: 1 }; |
481 | 0 | let week = if week == 5 { -1 } else { week }; |
482 | 0 | debug_assert!(week == -1 || (1..=4).contains(&week)); |
483 | | // This is maybe non-obvious, but this will always succeed |
484 | | // because it can only fail when the week number is one of |
485 | | // {-5, 0, 5}. Since we've validated that 'week' is in 1..=5, |
486 | | // we know it can't be 0. Moreover, because of the conditional |
487 | | // above and since `5` actually means "last weekday of month," |
488 | | // that case will always translate to `-1`. |
489 | | // |
490 | | // Also, I looked at how other libraries deal with this case, |
491 | | // and almost all of them just do a bunch of inline hairy |
492 | | // arithmetic here. I suppose I could be reduced to such |
493 | | // things if perf called for it, but we have a nice civil date |
494 | | // abstraction. So use it, god damn it. (Well, we did, and now |
495 | | // we have a lower level IDate abstraction. But it's still |
496 | | // an abstraction!) |
497 | 0 | Some( |
498 | 0 | first |
499 | 0 | .nth_weekday_of_month(week, weekday) |
500 | 0 | .expect("nth weekday always exists"), |
501 | 0 | ) |
502 | | } |
503 | | } |
504 | 0 | } |
505 | | } |
506 | | |
507 | | impl core::fmt::Display for PosixDay { |
508 | 0 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { |
509 | 0 | match *self { |
510 | 0 | PosixDay::JulianOne(n) => { |
511 | 0 | f.write_str("J")?; |
512 | 0 | core::fmt::Display::fmt(&n, f) |
513 | | } |
514 | 0 | PosixDay::JulianZero(n) => core::fmt::Display::fmt(&n, f), |
515 | 0 | PosixDay::WeekdayOfMonth { month, week, weekday } => { |
516 | 0 | f.write_str("M")?; |
517 | 0 | core::fmt::Display::fmt(&month, f)?; |
518 | 0 | f.write_str(".")?; |
519 | 0 | core::fmt::Display::fmt(&week, f)?; |
520 | 0 | f.write_str(".")?; |
521 | 0 | core::fmt::Display::fmt(&weekday, f)?; |
522 | 0 | Ok(()) |
523 | | } |
524 | | } |
525 | 0 | } |
526 | | } |
527 | | |
528 | | impl PosixTime { |
529 | | const DEFAULT: PosixTime = PosixTime { second: 2 * 60 * 60 }; |
530 | | } |
531 | | |
532 | | impl core::fmt::Display for PosixTime { |
533 | 0 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { |
534 | 0 | if self.second.is_negative() { |
535 | 0 | f.write_str("-")?; |
536 | | // The default is positive, so when |
537 | | // positive, we write nothing. |
538 | 0 | } |
539 | 0 | let second = self.second.unsigned_abs(); |
540 | 0 | let h = second / 3600; |
541 | 0 | let m = (second / 60) % 60; |
542 | 0 | let s = second % 60; |
543 | 0 | core::fmt::Display::fmt(&h, f)?; |
544 | 0 | if m != 0 || s != 0 { |
545 | 0 | write!(f, ":{m:02}")?; |
546 | 0 | if s != 0 { |
547 | 0 | write!(f, ":{s:02}")?; |
548 | 0 | } |
549 | 0 | } |
550 | 0 | Ok(()) |
551 | 0 | } |
552 | | } |
553 | | |
554 | | impl PosixOffset { |
555 | 0 | fn to_ioffset(&self) -> IOffset { |
556 | 0 | IOffset { second: self.second } |
557 | 0 | } |
558 | | } |
559 | | |
560 | | impl core::fmt::Display for PosixOffset { |
561 | 0 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { |
562 | | // Yes, this is backwards. Blame POSIX. |
563 | | // N.B. `+` is the default, so we don't |
564 | | // need to write that out. |
565 | 0 | if self.second > 0 { |
566 | 0 | f.write_str("-")?; |
567 | 0 | } |
568 | 0 | let second = self.second.unsigned_abs(); |
569 | 0 | let h = second / 3600; |
570 | 0 | let m = (second / 60) % 60; |
571 | 0 | let s = second % 60; |
572 | 0 | core::fmt::Display::fmt(&h, f)?; |
573 | 0 | if m != 0 || s != 0 { |
574 | 0 | write!(f, ":{m:02}")?; |
575 | 0 | if s != 0 { |
576 | 0 | write!(f, ":{s:02}")?; |
577 | 0 | } |
578 | 0 | } |
579 | 0 | Ok(()) |
580 | 0 | } |
581 | | } |
582 | | |
583 | | /// A helper type for formatting a time zone abbreviation. |
584 | | /// |
585 | | /// Basically, this will write the `<` and `>` quotes if necessary, and |
586 | | /// otherwise write out the abbreviation in its unquoted form. |
587 | | #[derive(Debug)] |
588 | | struct AbbreviationDisplay<S>(S); |
589 | | |
590 | | impl<S: AsRef<str>> core::fmt::Display for AbbreviationDisplay<S> { |
591 | 0 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { |
592 | 0 | let s = self.0.as_ref(); |
593 | 0 | if s.chars().any(|ch| ch == '+' || ch == '-') { |
594 | 0 | f.write_str("<")?; |
595 | 0 | core::fmt::Display::fmt(&s, f)?; |
596 | 0 | f.write_str(">") |
597 | | } else { |
598 | 0 | core::fmt::Display::fmt(&s, f) |
599 | | } |
600 | 0 | } |
601 | | } |
602 | | |
603 | | /// The daylight saving time (DST) info for a POSIX time zone in a particular |
604 | | /// year. |
605 | | #[derive(Debug, Eq, PartialEq)] |
606 | | struct DstInfo<'a, ABBREV> { |
607 | | /// The DST transition rule that generated this info. |
608 | | dst: &'a PosixDst<ABBREV>, |
609 | | /// The start time (inclusive) that DST begins. |
610 | | /// |
611 | | /// Note that this may be greater than `end`. This tends to happen in the |
612 | | /// southern hemisphere. |
613 | | /// |
614 | | /// Note also that this may be in UTC or in wall clock civil |
615 | | /// time. It depends on whether `PosixTimeZone::dst_info_utc` or |
616 | | /// `PosixTimeZone::dst_info_wall` was used. |
617 | | start: IDateTime, |
618 | | /// The end time (exclusive) that DST ends. |
619 | | /// |
620 | | /// Note that this may be less than `start`. This tends to happen in the |
621 | | /// southern hemisphere. |
622 | | /// |
623 | | /// Note also that this may be in UTC or in wall clock civil |
624 | | /// time. It depends on whether `PosixTimeZone::dst_info_utc` or |
625 | | /// `PosixTimeZone::dst_info_wall` was used. |
626 | | end: IDateTime, |
627 | | } |
628 | | |
629 | | impl<'a, ABBREV> DstInfo<'a, ABBREV> { |
630 | | /// Returns true if and only if the given civil datetime ought to be |
631 | | /// considered in DST. |
632 | 0 | fn in_dst(&self, utc_dt: IDateTime) -> bool { |
633 | 0 | if self.start <= self.end { |
634 | 0 | self.start <= utc_dt && utc_dt < self.end |
635 | | } else { |
636 | 0 | !(self.end <= utc_dt && utc_dt < self.start) |
637 | | } |
638 | 0 | } Unexecuted instantiation: <jiff::shared::posix::DstInfo<jiff::shared::util::array_str::ArrayStr<30>>>::in_dst Unexecuted instantiation: <jiff::shared::posix::DstInfo<&str>>::in_dst |
639 | | |
640 | | /// Returns the earlier and later times for this DST info. |
641 | 0 | fn ordered(&self) -> (IDateTime, IDateTime) { |
642 | 0 | if self.start <= self.end { |
643 | 0 | (self.start, self.end) |
644 | | } else { |
645 | 0 | (self.end, self.start) |
646 | | } |
647 | 0 | } Unexecuted instantiation: <jiff::shared::posix::DstInfo<jiff::shared::util::array_str::ArrayStr<30>>>::ordered Unexecuted instantiation: <jiff::shared::posix::DstInfo<&str>>::ordered |
648 | | |
649 | | /// Returns the DST offset. |
650 | 0 | fn offset(&self) -> &PosixOffset { |
651 | 0 | &self.dst.offset |
652 | 0 | } Unexecuted instantiation: <jiff::shared::posix::DstInfo<jiff::shared::util::array_str::ArrayStr<30>>>::offset Unexecuted instantiation: <jiff::shared::posix::DstInfo<&str>>::offset |
653 | | } |
654 | | |
655 | | /// A parser for POSIX time zones. |
656 | | #[derive(Debug)] |
657 | | struct Parser<'s> { |
658 | | /// The `TZ` string that we're parsing. |
659 | | tz: &'s [u8], |
660 | | /// The parser's current position in `tz`. |
661 | | pos: core::cell::Cell<usize>, |
662 | | /// Whether to use IANA rules, i.e., when parsing a TZ string in a TZif |
663 | | /// file of version 3 or greater. From `tzfile(5)`: |
664 | | /// |
665 | | /// > First, the hours part of its transition times may be signed and range |
666 | | /// > from `-167` through `167` instead of the POSIX-required unsigned |
667 | | /// > values from `0` through `24`. Second, DST is in effect all year if |
668 | | /// > it starts January 1 at 00:00 and ends December 31 at 24:00 plus the |
669 | | /// > difference between daylight saving and standard time. |
670 | | /// |
671 | | /// At time of writing, I don't think I understand the significance of |
672 | | /// the second part above. (RFC 8536 elaborates that it is meant to be an |
673 | | /// explicit clarification of something that POSIX itself implies.) But the |
674 | | /// first part is clear: it permits the hours to be a bigger range. |
675 | | ianav3plus: bool, |
676 | | } |
677 | | |
678 | | impl<'s> Parser<'s> { |
679 | | /// Create a new parser for extracting a POSIX time zone from the given |
680 | | /// bytes. |
681 | 0 | fn new<B: ?Sized + AsRef<[u8]>>(tz: &'s B) -> Parser<'s> { |
682 | 0 | Parser { |
683 | 0 | tz: tz.as_ref(), |
684 | 0 | pos: core::cell::Cell::new(0), |
685 | 0 | ianav3plus: false, |
686 | 0 | } |
687 | 0 | } |
688 | | |
689 | | /// Parses a POSIX time zone from the current position of the parser and |
690 | | /// ensures that the entire TZ string corresponds to a single valid POSIX |
691 | | /// time zone. |
692 | 0 | fn parse( |
693 | 0 | &self, |
694 | 0 | ) -> Result<PosixTimeZone<Abbreviation>, PosixTimeZoneError> { |
695 | 0 | let (time_zone, remaining) = self.parse_prefix()?; |
696 | 0 | if !remaining.is_empty() { |
697 | 0 | return Err(ErrorKind::FoundRemaining.into()); |
698 | 0 | } |
699 | 0 | Ok(time_zone) |
700 | 0 | } |
701 | | |
702 | | /// Parses a POSIX time zone from the current position of the parser and |
703 | | /// returns the remaining input. |
704 | 0 | fn parse_prefix( |
705 | 0 | &self, |
706 | 0 | ) -> Result<(PosixTimeZone<Abbreviation>, &'s [u8]), PosixTimeZoneError> |
707 | | { |
708 | 0 | let time_zone = self.parse_posix_time_zone()?; |
709 | 0 | Ok((time_zone, self.remaining())) |
710 | 0 | } |
711 | | |
712 | | /// Parse a POSIX time zone from the current position of the parser. |
713 | | /// |
714 | | /// Upon success, the parser will be positioned immediately following the |
715 | | /// TZ string. |
716 | 0 | fn parse_posix_time_zone( |
717 | 0 | &self, |
718 | 0 | ) -> Result<PosixTimeZone<Abbreviation>, PosixTimeZoneError> { |
719 | 0 | if self.is_done() { |
720 | 0 | return Err(ErrorKind::Empty.into()); |
721 | 0 | } |
722 | 0 | let std_abbrev = |
723 | 0 | self.parse_abbreviation().map_err(ErrorKind::AbbreviationStd)?; |
724 | 0 | let std_offset = |
725 | 0 | self.parse_posix_offset().map_err(ErrorKind::OffsetStd)?; |
726 | 0 | let mut dst = None; |
727 | 0 | if !self.is_done() |
728 | 0 | && (self.byte().is_ascii_alphabetic() || self.byte() == b'<') |
729 | | { |
730 | 0 | dst = Some(self.parse_posix_dst(&std_offset)?); |
731 | 0 | } |
732 | 0 | Ok(PosixTimeZone { std_abbrev, std_offset, dst }) |
733 | 0 | } |
734 | | |
735 | | /// Parse a DST zone with an optional explicit transition rule. |
736 | | /// |
737 | | /// This assumes the parser is positioned at the first byte of the DST |
738 | | /// abbreviation. |
739 | | /// |
740 | | /// Upon success, the parser will be positioned immediately after the end |
741 | | /// of the DST transition rule (which might just be the abbreviation, but |
742 | | /// might also include explicit start/end datetime specifications). |
743 | 0 | fn parse_posix_dst( |
744 | 0 | &self, |
745 | 0 | std_offset: &PosixOffset, |
746 | 0 | ) -> Result<PosixDst<Abbreviation>, PosixTimeZoneError> { |
747 | 0 | let abbrev = |
748 | 0 | self.parse_abbreviation().map_err(ErrorKind::AbbreviationDst)?; |
749 | 0 | if self.is_done() { |
750 | 0 | return Err(ErrorKind::FoundDstNoRule.into()); |
751 | 0 | } |
752 | | // This is the default: one hour ahead of standard time. We may |
753 | | // override this if the DST portion specifies an offset. (But it |
754 | | // usually doesn't.) |
755 | 0 | let mut offset = PosixOffset { second: std_offset.second + 3600 }; |
756 | 0 | if self.byte() != b',' { |
757 | | offset = |
758 | 0 | self.parse_posix_offset().map_err(ErrorKind::OffsetDst)?; |
759 | 0 | if self.is_done() { |
760 | 0 | return Err(ErrorKind::FoundDstNoRuleWithOffset.into()); |
761 | 0 | } |
762 | 0 | } |
763 | 0 | if self.byte() != b',' { |
764 | 0 | return Err(ErrorKind::ExpectedCommaAfterDst.into()); |
765 | 0 | } |
766 | 0 | if !self.bump() { |
767 | 0 | return Err(ErrorKind::FoundEndAfterComma.into()); |
768 | 0 | } |
769 | 0 | let rule = self.parse_rule().map_err(ErrorKind::Rule)?; |
770 | 0 | Ok(PosixDst { abbrev, offset, rule }) |
771 | 0 | } |
772 | | |
773 | | /// Parse a time zone abbreviation. |
774 | | /// |
775 | | /// This assumes the parser is positioned at the first byte of |
776 | | /// the abbreviation. This is either the first character in the |
777 | | /// abbreviation, or the opening quote of a quoted abbreviation. |
778 | | /// |
779 | | /// Upon success, the parser will be positioned immediately following |
780 | | /// the abbreviation name. |
781 | | /// |
782 | | /// The string returned is guaranteed to be no more than 30 bytes. |
783 | | /// (This restriction is somewhat arbitrary, but it's so we can put |
784 | | /// the abbreviation in a fixed capacity array.) |
785 | 0 | fn parse_abbreviation(&self) -> Result<Abbreviation, AbbreviationError> { |
786 | 0 | if self.byte() == b'<' { |
787 | 0 | if !self.bump() { |
788 | 0 | return Err(AbbreviationError::Quoted( |
789 | 0 | QuotedAbbreviationError::UnexpectedEndAfterOpening, |
790 | 0 | )); |
791 | 0 | } |
792 | 0 | self.parse_quoted_abbreviation().map_err(AbbreviationError::Quoted) |
793 | | } else { |
794 | 0 | self.parse_unquoted_abbreviation() |
795 | 0 | .map_err(AbbreviationError::Unquoted) |
796 | | } |
797 | 0 | } |
798 | | |
799 | | /// Parses an unquoted time zone abbreviation. |
800 | | /// |
801 | | /// This assumes the parser is position at the first byte in the |
802 | | /// abbreviation. |
803 | | /// |
804 | | /// Upon success, the parser will be positioned immediately after the |
805 | | /// last byte in the abbreviation. |
806 | | /// |
807 | | /// The string returned is guaranteed to be no more than 30 bytes. |
808 | | /// (This restriction is somewhat arbitrary, but it's so we can put |
809 | | /// the abbreviation in a fixed capacity array.) |
810 | 0 | fn parse_unquoted_abbreviation( |
811 | 0 | &self, |
812 | 0 | ) -> Result<Abbreviation, UnquotedAbbreviationError> { |
813 | 0 | let start = self.pos(); |
814 | 0 | for i in 0.. { |
815 | 0 | if !self.byte().is_ascii_alphabetic() { |
816 | 0 | break; |
817 | 0 | } |
818 | 0 | if i >= Abbreviation::capacity() { |
819 | 0 | return Err(UnquotedAbbreviationError::TooLong); |
820 | 0 | } |
821 | 0 | if !self.bump() { |
822 | 0 | break; |
823 | 0 | } |
824 | | } |
825 | 0 | let end = self.pos(); |
826 | 0 | let abbrev = |
827 | 0 | core::str::from_utf8(&self.tz[start..end]).map_err(|_| { |
828 | | // NOTE: I believe this error is technically impossible |
829 | | // since the loop above restricts letters in an |
830 | | // abbreviation to ASCII. So everything from `start` to |
831 | | // `end` is ASCII and thus should be UTF-8. But it doesn't |
832 | | // cost us anything to report an error here in case the |
833 | | // code above evolves somehow. |
834 | 0 | UnquotedAbbreviationError::InvalidUtf8 |
835 | 0 | })?; |
836 | 0 | if abbrev.len() < 3 { |
837 | 0 | return Err(UnquotedAbbreviationError::TooShort); |
838 | 0 | } |
839 | | // OK because we verified above that the abbreviation |
840 | | // does not exceed `Abbreviation::capacity`. |
841 | 0 | Ok(Abbreviation::new(abbrev).unwrap()) |
842 | 0 | } |
843 | | |
844 | | /// Parses a quoted time zone abbreviation. |
845 | | /// |
846 | | /// This assumes the parser is positioned immediately after the opening |
847 | | /// `<` quote. That is, at the first byte in the abbreviation. |
848 | | /// |
849 | | /// Upon success, the parser will be positioned immediately after the |
850 | | /// closing `>` quote. |
851 | | /// |
852 | | /// The string returned is guaranteed to be no more than 30 bytes. |
853 | | /// (This restriction is somewhat arbitrary, but it's so we can put |
854 | | /// the abbreviation in a fixed capacity array.) |
855 | 0 | fn parse_quoted_abbreviation( |
856 | 0 | &self, |
857 | 0 | ) -> Result<Abbreviation, QuotedAbbreviationError> { |
858 | 0 | let start = self.pos(); |
859 | 0 | for i in 0.. { |
860 | 0 | if !self.byte().is_ascii_alphanumeric() |
861 | 0 | && self.byte() != b'+' |
862 | 0 | && self.byte() != b'-' |
863 | | { |
864 | 0 | break; |
865 | 0 | } |
866 | 0 | if i >= Abbreviation::capacity() { |
867 | 0 | return Err(QuotedAbbreviationError::TooLong); |
868 | 0 | } |
869 | 0 | if !self.bump() { |
870 | 0 | break; |
871 | 0 | } |
872 | | } |
873 | 0 | let end = self.pos(); |
874 | 0 | let abbrev = |
875 | 0 | core::str::from_utf8(&self.tz[start..end]).map_err(|_| { |
876 | | // NOTE: I believe this error is technically impossible |
877 | | // since the loop above restricts letters in an |
878 | | // abbreviation to ASCII. So everything from `start` to |
879 | | // `end` is ASCII and thus should be UTF-8. But it doesn't |
880 | | // cost us anything to report an error here in case the |
881 | | // code above evolves somehow. |
882 | 0 | QuotedAbbreviationError::InvalidUtf8 |
883 | 0 | })?; |
884 | 0 | if self.is_done() { |
885 | 0 | return Err(QuotedAbbreviationError::UnexpectedEnd); |
886 | 0 | } |
887 | 0 | if self.byte() != b'>' { |
888 | 0 | return Err(QuotedAbbreviationError::UnexpectedLastByte); |
889 | 0 | } |
890 | 0 | self.bump(); |
891 | 0 | if abbrev.len() < 3 { |
892 | 0 | return Err(QuotedAbbreviationError::TooShort); |
893 | 0 | } |
894 | | // OK because we verified above that the abbreviation |
895 | | // does not exceed `Abbreviation::capacity`. |
896 | 0 | Ok(Abbreviation::new(abbrev).unwrap()) |
897 | 0 | } |
898 | | |
899 | | /// Parse a POSIX time offset. |
900 | | /// |
901 | | /// This assumes the parser is positioned at the first byte of the |
902 | | /// offset. This can either be a digit (for a positive offset) or the |
903 | | /// sign of the offset (which must be either `-` or `+`). |
904 | | /// |
905 | | /// Upon success, the parser will be positioned immediately after the |
906 | | /// end of the offset. |
907 | 0 | fn parse_posix_offset(&self) -> Result<PosixOffset, PosixOffsetError> { |
908 | 0 | let sign = self.parse_optional_sign()?.unwrap_or(1); |
909 | 0 | let hour = self.parse_hour_posix()?; |
910 | 0 | let (mut minute, mut second) = (0, 0); |
911 | 0 | if self.maybe_byte() == Some(b':') { |
912 | 0 | if !self.bump() { |
913 | 0 | return Err(PosixOffsetError::IncompleteMinutes); |
914 | 0 | } |
915 | 0 | minute = self.parse_minute()?; |
916 | 0 | if self.maybe_byte() == Some(b':') { |
917 | 0 | if !self.bump() { |
918 | 0 | return Err(PosixOffsetError::IncompleteSeconds); |
919 | 0 | } |
920 | 0 | second = self.parse_second()?; |
921 | 0 | } |
922 | 0 | } |
923 | 0 | let mut offset = PosixOffset { second: i32::from(hour) * 3600 }; |
924 | 0 | offset.second += i32::from(minute) * 60; |
925 | 0 | offset.second += i32::from(second); |
926 | | // Yes, we flip the sign, because POSIX is backwards. |
927 | | // For example, `EST5` corresponds to `-05:00`. |
928 | 0 | offset.second *= i32::from(-sign); |
929 | | // Must be true because the parsing routines for hours, minutes |
930 | | // and seconds enforce they are in the ranges -24..=24, 0..=59 |
931 | | // and 0..=59, respectively. |
932 | 0 | assert!( |
933 | 0 | -89999 <= offset.second && offset.second <= 89999, |
934 | 0 | "POSIX offset seconds {} is out of range", |
935 | | offset.second |
936 | | ); |
937 | 0 | Ok(offset) |
938 | 0 | } |
939 | | |
940 | | /// Parses a POSIX DST transition rule. |
941 | | /// |
942 | | /// This assumes the parser is positioned at the first byte in the |
943 | | /// rule. That is, it comes immediately after the DST abbreviation or |
944 | | /// its optional offset. |
945 | | /// |
946 | | /// Upon success, the parser will be positioned immediately after the |
947 | | /// DST transition rule. In typical cases, this corresponds to the end |
948 | | /// of the TZ string. |
949 | 0 | fn parse_rule(&self) -> Result<PosixRule, PosixRuleError> { |
950 | 0 | let start = self |
951 | 0 | .parse_posix_datetime() |
952 | 0 | .map_err(PosixRuleError::DateTimeStart)?; |
953 | 0 | if self.maybe_byte() != Some(b',') || !self.bump() { |
954 | 0 | return Err(PosixRuleError::ExpectedEnd); |
955 | 0 | } |
956 | 0 | let end = self |
957 | 0 | .parse_posix_datetime() |
958 | 0 | .map_err(PosixRuleError::DateTimeEnd)?; |
959 | 0 | Ok(PosixRule { start, end }) |
960 | 0 | } |
961 | | |
962 | | /// Parses a POSIX datetime specification. |
963 | | /// |
964 | | /// This assumes the parser is position at the first byte where a |
965 | | /// datetime specification is expected to occur. |
966 | | /// |
967 | | /// Upon success, the parser will be positioned after the datetime |
968 | | /// specification. This will either be immediately after the date, or |
969 | | /// if it's present, the time part of the specification. |
970 | 0 | fn parse_posix_datetime( |
971 | 0 | &self, |
972 | 0 | ) -> Result<PosixDayTime, PosixDateTimeError> { |
973 | 0 | let mut daytime = PosixDayTime { |
974 | 0 | date: self.parse_posix_date()?, |
975 | | time: PosixTime::DEFAULT, |
976 | | }; |
977 | 0 | if self.maybe_byte() != Some(b'/') { |
978 | 0 | return Ok(daytime); |
979 | 0 | } |
980 | 0 | if !self.bump() { |
981 | 0 | return Err(PosixDateTimeError::ExpectedTime); |
982 | 0 | } |
983 | 0 | daytime.time = self.parse_posix_time()?; |
984 | 0 | Ok(daytime) |
985 | 0 | } |
986 | | |
987 | | /// Parses a POSIX date specification. |
988 | | /// |
989 | | /// This assumes the parser is positioned at the first byte of the date |
990 | | /// specification. This can be `J` (for one based Julian day without |
991 | | /// leap days), `M` (for "weekday of month") or a digit starting the |
992 | | /// zero based Julian day with leap days. This routine will validate |
993 | | /// that the position points to one of these possible values. That is, |
994 | | /// the caller doesn't need to parse the `M` or the `J` or the leading |
995 | | /// digit. The caller should just call this routine when it *expect* a |
996 | | /// date specification to follow. |
997 | | /// |
998 | | /// Upon success, the parser will be positioned immediately after the |
999 | | /// date specification. |
1000 | 0 | fn parse_posix_date(&self) -> Result<PosixDay, PosixDateError> { |
1001 | 0 | match self.byte() { |
1002 | | b'J' => { |
1003 | 0 | if !self.bump() { |
1004 | 0 | return Err(PosixDateError::ExpectedJulianNoLeap); |
1005 | 0 | } |
1006 | 0 | Ok(PosixDay::JulianOne(self.parse_posix_julian_day_no_leap()?)) |
1007 | | } |
1008 | 0 | b'0'..=b'9' => Ok(PosixDay::JulianZero( |
1009 | 0 | self.parse_posix_julian_day_with_leap()?, |
1010 | | )), |
1011 | | b'M' => { |
1012 | 0 | if !self.bump() { |
1013 | 0 | return Err(PosixDateError::ExpectedMonthWeekWeekday); |
1014 | 0 | } |
1015 | 0 | let (month, week, weekday) = self.parse_weekday_of_month()?; |
1016 | 0 | Ok(PosixDay::WeekdayOfMonth { month, week, weekday }) |
1017 | | } |
1018 | 0 | _ => Err(PosixDateError::UnexpectedByte), |
1019 | | } |
1020 | 0 | } |
1021 | | |
1022 | | /// Parses a POSIX Julian day that does not include leap days |
1023 | | /// (`1 <= n <= 365`). |
1024 | | /// |
1025 | | /// This assumes the parser is positioned just after the `J` and at the |
1026 | | /// first digit of the Julian day. Upon success, the parser will be |
1027 | | /// positioned immediately following the day number. |
1028 | 0 | fn parse_posix_julian_day_no_leap( |
1029 | 0 | &self, |
1030 | 0 | ) -> Result<i16, PosixJulianNoLeapError> { |
1031 | 0 | let number = self |
1032 | 0 | .parse_number_with_upto_n_digits(3) |
1033 | 0 | .map_err(PosixJulianNoLeapError::Parse)?; |
1034 | 0 | let number = i16::try_from(number) |
1035 | 0 | .map_err(|_| PosixJulianNoLeapError::Range)?; |
1036 | 0 | if !(1 <= number && number <= 365) { |
1037 | 0 | return Err(PosixJulianNoLeapError::Range); |
1038 | 0 | } |
1039 | 0 | Ok(number) |
1040 | 0 | } |
1041 | | |
1042 | | /// Parses a POSIX Julian day that includes leap days (`0 <= n <= |
1043 | | /// 365`). |
1044 | | /// |
1045 | | /// This assumes the parser is positioned at the first digit of the |
1046 | | /// Julian day. Upon success, the parser will be positioned immediately |
1047 | | /// following the day number. |
1048 | 0 | fn parse_posix_julian_day_with_leap( |
1049 | 0 | &self, |
1050 | 0 | ) -> Result<i16, PosixJulianLeapError> { |
1051 | 0 | let number = self |
1052 | 0 | .parse_number_with_upto_n_digits(3) |
1053 | 0 | .map_err(PosixJulianLeapError::Parse)?; |
1054 | 0 | let number = |
1055 | 0 | i16::try_from(number).map_err(|_| PosixJulianLeapError::Range)?; |
1056 | 0 | if !(0 <= number && number <= 365) { |
1057 | 0 | return Err(PosixJulianLeapError::Range); |
1058 | 0 | } |
1059 | 0 | Ok(number) |
1060 | 0 | } |
1061 | | |
1062 | | /// Parses a POSIX "weekday of month" specification. |
1063 | | /// |
1064 | | /// This assumes the parser is positioned just after the `M` byte and |
1065 | | /// at the first digit of the month. Upon success, the parser will be |
1066 | | /// positioned immediately following the "weekday of the month" that |
1067 | | /// was parsed. |
1068 | | /// |
1069 | | /// The tuple returned is month (1..=12), week (1..=5) and weekday |
1070 | | /// (0..=6 with 0=Sunday). |
1071 | 0 | fn parse_weekday_of_month( |
1072 | 0 | &self, |
1073 | 0 | ) -> Result<(i8, i8, i8), WeekdayOfMonthError> { |
1074 | 0 | let month = self.parse_month()?; |
1075 | 0 | if self.maybe_byte() != Some(b'.') { |
1076 | 0 | return Err(WeekdayOfMonthError::ExpectedDotAfterMonth); |
1077 | 0 | } |
1078 | 0 | if !self.bump() { |
1079 | 0 | return Err(WeekdayOfMonthError::ExpectedWeekAfterMonth); |
1080 | 0 | } |
1081 | 0 | let week = self.parse_week()?; |
1082 | 0 | if self.maybe_byte() != Some(b'.') { |
1083 | 0 | return Err(WeekdayOfMonthError::ExpectedDotAfterWeek); |
1084 | 0 | } |
1085 | 0 | if !self.bump() { |
1086 | 0 | return Err(WeekdayOfMonthError::ExpectedDayOfWeekAfterWeek); |
1087 | 0 | } |
1088 | 0 | let weekday = self.parse_weekday()?; |
1089 | 0 | Ok((month, week, weekday)) |
1090 | 0 | } |
1091 | | |
1092 | | /// This parses a POSIX time specification in the format |
1093 | | /// `[+/-]hh?[:mm[:ss]]`. |
1094 | | /// |
1095 | | /// This assumes the parser is positioned at the first `h` (or the |
1096 | | /// sign, if present). Upon success, the parser will be positioned |
1097 | | /// immediately following the end of the time specification. |
1098 | 0 | fn parse_posix_time(&self) -> Result<PosixTime, PosixTimeError> { |
1099 | 0 | let (sign, hour) = if self.ianav3plus { |
1100 | 0 | let sign = self.parse_optional_sign()?.unwrap_or(1); |
1101 | 0 | let hour = self.parse_hour_ianav3plus()?; |
1102 | 0 | (sign, hour) |
1103 | | } else { |
1104 | 0 | (1, i16::from(self.parse_hour_posix()?)) |
1105 | | }; |
1106 | 0 | let (mut minute, mut second) = (0, 0); |
1107 | 0 | if self.maybe_byte() == Some(b':') { |
1108 | 0 | if !self.bump() { |
1109 | 0 | return Err(PosixTimeError::IncompleteMinutes); |
1110 | 0 | } |
1111 | 0 | minute = self.parse_minute()?; |
1112 | 0 | if self.maybe_byte() == Some(b':') { |
1113 | 0 | if !self.bump() { |
1114 | 0 | return Err(PosixTimeError::IncompleteSeconds); |
1115 | 0 | } |
1116 | 0 | second = self.parse_second()?; |
1117 | 0 | } |
1118 | 0 | } |
1119 | 0 | let mut time = PosixTime { second: i32::from(hour) * 3600 }; |
1120 | 0 | time.second += i32::from(minute) * 60; |
1121 | 0 | time.second += i32::from(second); |
1122 | 0 | time.second *= i32::from(sign); |
1123 | | // Must be true because the parsing routines for hours, minutes |
1124 | | // and seconds enforce they are in the ranges -167..=167, 0..=59 |
1125 | | // and 0..=59, respectively. |
1126 | 0 | assert!( |
1127 | 0 | -604799 <= time.second && time.second <= 604799, |
1128 | 0 | "POSIX time seconds {} is out of range", |
1129 | | time.second |
1130 | | ); |
1131 | 0 | Ok(time) |
1132 | 0 | } |
1133 | | |
1134 | | /// Parses a month. |
1135 | | /// |
1136 | | /// This is expected to be positioned at the first digit. Upon success, |
1137 | | /// the parser will be positioned after the month (which may contain |
1138 | | /// two digits). |
1139 | 0 | fn parse_month(&self) -> Result<i8, MonthError> { |
1140 | 0 | let number = self |
1141 | 0 | .parse_number_with_upto_n_digits(2) |
1142 | 0 | .map_err(MonthError::Parse)?; |
1143 | 0 | let number = i8::try_from(number).map_err(|_| MonthError::Range)?; |
1144 | 0 | if !(1 <= number && number <= 12) { |
1145 | 0 | return Err(MonthError::Range); |
1146 | 0 | } |
1147 | 0 | Ok(number) |
1148 | 0 | } |
1149 | | |
1150 | | /// Parses a week-of-month number. |
1151 | | /// |
1152 | | /// This is expected to be positioned at the first digit. Upon success, |
1153 | | /// the parser will be positioned after the week digit. |
1154 | 0 | fn parse_week(&self) -> Result<i8, WeekOfMonthError> { |
1155 | 0 | let number = self |
1156 | 0 | .parse_number_with_exactly_n_digits(1) |
1157 | 0 | .map_err(WeekOfMonthError::Parse)?; |
1158 | 0 | let number = |
1159 | 0 | i8::try_from(number).map_err(|_| WeekOfMonthError::Range)?; |
1160 | 0 | if !(1 <= number && number <= 5) { |
1161 | 0 | return Err(WeekOfMonthError::Range); |
1162 | 0 | } |
1163 | 0 | Ok(number) |
1164 | 0 | } |
1165 | | |
1166 | | /// Parses a weekday number. |
1167 | | /// |
1168 | | /// This is expected to be positioned at the first digit. Upon success, |
1169 | | /// the parser will be positioned after the week digit. |
1170 | | /// |
1171 | | /// The weekday returned is guaranteed to be in the range `0..=6`, with |
1172 | | /// `0` corresponding to Sunday. |
1173 | 0 | fn parse_weekday(&self) -> Result<i8, WeekdayError> { |
1174 | 0 | let number = self |
1175 | 0 | .parse_number_with_exactly_n_digits(1) |
1176 | 0 | .map_err(WeekdayError::Parse)?; |
1177 | 0 | let number = i8::try_from(number).map_err(|_| WeekdayError::Range)?; |
1178 | 0 | if !(0 <= number && number <= 6) { |
1179 | 0 | return Err(WeekdayError::Range); |
1180 | 0 | } |
1181 | 0 | Ok(number) |
1182 | 0 | } |
1183 | | |
1184 | | /// Parses an hour from a POSIX time specification with the IANA |
1185 | | /// v3+ extension. That is, the hour may be in the range `0..=167`. |
1186 | | /// (Callers should parse an optional sign preceding the hour digits |
1187 | | /// when IANA V3+ parsing is enabled.) |
1188 | | /// |
1189 | | /// The hour is allowed to be a single digit (unlike minutes or |
1190 | | /// seconds). |
1191 | | /// |
1192 | | /// This assumes the parser is positioned at the position where the |
1193 | | /// first hour digit should occur. Upon success, the parser will be |
1194 | | /// positioned immediately after the last hour digit. |
1195 | 0 | fn parse_hour_ianav3plus(&self) -> Result<i16, HourIanaError> { |
1196 | | // Callers should only be using this method when IANA v3+ parsing |
1197 | | // is enabled. |
1198 | 0 | assert!(self.ianav3plus); |
1199 | 0 | let number = self |
1200 | 0 | .parse_number_with_upto_n_digits(3) |
1201 | 0 | .map_err(HourIanaError::Parse)?; |
1202 | 0 | let number = |
1203 | 0 | i16::try_from(number).map_err(|_| HourIanaError::Range)?; |
1204 | 0 | if !(0 <= number && number <= 167) { |
1205 | | // The error message says -167 but the check above uses 0. |
1206 | | // This is because the caller is responsible for parsing |
1207 | | // the sign. |
1208 | 0 | return Err(HourIanaError::Range); |
1209 | 0 | } |
1210 | 0 | Ok(number) |
1211 | 0 | } |
1212 | | |
1213 | | /// Parses an hour from a POSIX time specification, with the allowed |
1214 | | /// range being `0..=24`. |
1215 | | /// |
1216 | | /// The hour is allowed to be a single digit (unlike minutes or |
1217 | | /// seconds). |
1218 | | /// |
1219 | | /// This assumes the parser is positioned at the position where the |
1220 | | /// first hour digit should occur. Upon success, the parser will be |
1221 | | /// positioned immediately after the last hour digit. |
1222 | 0 | fn parse_hour_posix(&self) -> Result<i8, HourPosixError> { |
1223 | 0 | let number = self |
1224 | 0 | .parse_number_with_upto_n_digits(2) |
1225 | 0 | .map_err(HourPosixError::Parse)?; |
1226 | 0 | let number = |
1227 | 0 | i8::try_from(number).map_err(|_| HourPosixError::Range)?; |
1228 | 0 | if !(0 <= number && number <= 24) { |
1229 | 0 | return Err(HourPosixError::Range); |
1230 | 0 | } |
1231 | 0 | Ok(number) |
1232 | 0 | } |
1233 | | |
1234 | | /// Parses a minute from a POSIX time specification. |
1235 | | /// |
1236 | | /// The minute must be exactly two digits. |
1237 | | /// |
1238 | | /// This assumes the parser is positioned at the position where the |
1239 | | /// first minute digit should occur. Upon success, the parser will be |
1240 | | /// positioned immediately after the second minute digit. |
1241 | 0 | fn parse_minute(&self) -> Result<i8, MinuteError> { |
1242 | 0 | let number = self |
1243 | 0 | .parse_number_with_exactly_n_digits(2) |
1244 | 0 | .map_err(MinuteError::Parse)?; |
1245 | 0 | let number = i8::try_from(number).map_err(|_| MinuteError::Range)?; |
1246 | 0 | if !(0 <= number && number <= 59) { |
1247 | 0 | return Err(MinuteError::Range); |
1248 | 0 | } |
1249 | 0 | Ok(number) |
1250 | 0 | } |
1251 | | |
1252 | | /// Parses a second from a POSIX time specification. |
1253 | | /// |
1254 | | /// The second must be exactly two digits. |
1255 | | /// |
1256 | | /// This assumes the parser is positioned at the position where the |
1257 | | /// first second digit should occur. Upon success, the parser will be |
1258 | | /// positioned immediately after the second second digit. |
1259 | 0 | fn parse_second(&self) -> Result<i8, SecondError> { |
1260 | 0 | let number = self |
1261 | 0 | .parse_number_with_exactly_n_digits(2) |
1262 | 0 | .map_err(SecondError::Parse)?; |
1263 | 0 | let number = i8::try_from(number).map_err(|_| SecondError::Range)?; |
1264 | 0 | if !(0 <= number && number <= 59) { |
1265 | 0 | return Err(SecondError::Range); |
1266 | 0 | } |
1267 | 0 | Ok(number) |
1268 | 0 | } |
1269 | | |
1270 | | /// Parses a signed 64-bit integer expressed in exactly `n` digits. |
1271 | | /// |
1272 | | /// If `n` digits could not be found (or if the `TZ` string ends before |
1273 | | /// `n` digits could be found), then this returns an error. |
1274 | | /// |
1275 | | /// This assumes that `n >= 1` and that the parser is positioned at the |
1276 | | /// first digit. Upon success, the parser is positioned immediately |
1277 | | /// after the `n`th digit. |
1278 | 0 | fn parse_number_with_exactly_n_digits( |
1279 | 0 | &self, |
1280 | 0 | n: usize, |
1281 | 0 | ) -> Result<i32, NumberError> { |
1282 | 0 | assert!(n >= 1, "numbers must have at least 1 digit"); |
1283 | 0 | let mut number: i32 = 0; |
1284 | 0 | for _ in 0..n { |
1285 | 0 | if self.is_done() { |
1286 | 0 | return Err(NumberError::ExpectedLength); |
1287 | 0 | } |
1288 | 0 | let byte = self.byte(); |
1289 | 0 | let digit = match byte.checked_sub(b'0') { |
1290 | | None => { |
1291 | 0 | return Err(NumberError::InvalidDigit); |
1292 | | } |
1293 | 0 | Some(digit) if digit > 9 => { |
1294 | 0 | return Err(NumberError::InvalidDigit); |
1295 | | } |
1296 | 0 | Some(digit) => { |
1297 | 0 | debug_assert!((0..=9).contains(&digit)); |
1298 | 0 | i32::from(digit) |
1299 | | } |
1300 | | }; |
1301 | 0 | number = number |
1302 | 0 | .checked_mul(10) |
1303 | 0 | .and_then(|n| n.checked_add(digit)) |
1304 | 0 | .ok_or(NumberError::TooBig)?; |
1305 | 0 | self.bump(); |
1306 | | } |
1307 | 0 | Ok(number) |
1308 | 0 | } |
1309 | | |
1310 | | /// Parses a signed 64-bit integer expressed with up to `n` digits and |
1311 | | /// at least 1 digit. |
1312 | | /// |
1313 | | /// This assumes that `n >= 1` and that the parser is positioned at the |
1314 | | /// first digit. Upon success, the parser is position immediately after |
1315 | | /// the last digit (which can be at most `n`). |
1316 | 0 | fn parse_number_with_upto_n_digits( |
1317 | 0 | &self, |
1318 | 0 | n: usize, |
1319 | 0 | ) -> Result<i32, NumberError> { |
1320 | 0 | assert!(n >= 1, "numbers must have at least 1 digit"); |
1321 | 0 | let mut number: i32 = 0; |
1322 | 0 | for i in 0..n { |
1323 | 0 | if self.is_done() || !self.byte().is_ascii_digit() { |
1324 | 0 | if i == 0 { |
1325 | 0 | return Err(NumberError::Empty); |
1326 | 0 | } |
1327 | 0 | break; |
1328 | 0 | } |
1329 | 0 | let digit = i32::from(self.byte() - b'0'); |
1330 | 0 | number = number |
1331 | 0 | .checked_mul(10) |
1332 | 0 | .and_then(|n| n.checked_add(digit)) |
1333 | 0 | .ok_or(NumberError::TooBig)?; |
1334 | 0 | self.bump(); |
1335 | | } |
1336 | 0 | Ok(number) |
1337 | 0 | } |
1338 | | |
1339 | | /// Parses an optional sign. |
1340 | | /// |
1341 | | /// This assumes the parser is positioned at the position where a |
1342 | | /// positive or negative sign is permitted. If one exists, then it |
1343 | | /// is consumed and returned. Moreover, if one exists, then this |
1344 | | /// guarantees that it is not the last byte in the input. That is, upon |
1345 | | /// success, it is valid to call `self.byte()`. |
1346 | 0 | fn parse_optional_sign(&self) -> Result<Option<i8>, OptionalSignError> { |
1347 | 0 | if self.is_done() { |
1348 | 0 | return Ok(None); |
1349 | 0 | } |
1350 | 0 | Ok(match self.byte() { |
1351 | | b'-' => { |
1352 | 0 | if !self.bump() { |
1353 | 0 | return Err(OptionalSignError::ExpectedDigitAfterMinus); |
1354 | 0 | } |
1355 | 0 | Some(-1) |
1356 | | } |
1357 | | b'+' => { |
1358 | 0 | if !self.bump() { |
1359 | 0 | return Err(OptionalSignError::ExpectedDigitAfterPlus); |
1360 | 0 | } |
1361 | 0 | Some(1) |
1362 | | } |
1363 | 0 | _ => None, |
1364 | | }) |
1365 | 0 | } |
1366 | | } |
1367 | | |
1368 | | /// Helper routines for parsing a POSIX `TZ` string. |
1369 | | impl<'s> Parser<'s> { |
1370 | | /// Bump the parser to the next byte. |
1371 | | /// |
1372 | | /// If the end of the input has been reached, then `false` is returned. |
1373 | 0 | fn bump(&self) -> bool { |
1374 | 0 | if self.is_done() { |
1375 | 0 | return false; |
1376 | 0 | } |
1377 | 0 | self.pos.set( |
1378 | 0 | self.pos().checked_add(1).expect("pos cannot overflow usize"), |
1379 | | ); |
1380 | 0 | !self.is_done() |
1381 | 0 | } |
1382 | | |
1383 | | /// Returns true if the next call to `bump` would return false. |
1384 | 0 | fn is_done(&self) -> bool { |
1385 | 0 | self.pos() == self.tz.len() |
1386 | 0 | } |
1387 | | |
1388 | | /// Return the byte at the current position of the parser. |
1389 | | /// |
1390 | | /// This panics if the parser is positioned at the end of the TZ |
1391 | | /// string. |
1392 | 0 | fn byte(&self) -> u8 { |
1393 | 0 | self.tz[self.pos()] |
1394 | 0 | } |
1395 | | |
1396 | | /// Return the byte at the current position of the parser. If the TZ |
1397 | | /// string has been exhausted, then this returns `None`. |
1398 | 0 | fn maybe_byte(&self) -> Option<u8> { |
1399 | 0 | self.tz.get(self.pos()).copied() |
1400 | 0 | } |
1401 | | |
1402 | | /// Return the current byte offset of the parser. |
1403 | | /// |
1404 | | /// The offset starts at `0` from the beginning of the TZ string. |
1405 | 0 | fn pos(&self) -> usize { |
1406 | 0 | self.pos.get() |
1407 | 0 | } |
1408 | | |
1409 | | /// Returns the remaining bytes of the TZ string. |
1410 | | /// |
1411 | | /// This includes `self.byte()`. It may be empty. |
1412 | 0 | fn remaining(&self) -> &'s [u8] { |
1413 | 0 | &self.tz[self.pos()..] |
1414 | 0 | } |
1415 | | } |
1416 | | |
1417 | | #[derive(Clone, Debug, Eq, PartialEq)] |
1418 | | pub struct PosixTimeZoneError { |
1419 | | kind: ErrorKind, |
1420 | | } |
1421 | | |
1422 | | #[derive(Clone, Debug, Eq, PartialEq)] |
1423 | | enum ErrorKind { |
1424 | | AbbreviationDst(AbbreviationError), |
1425 | | AbbreviationStd(AbbreviationError), |
1426 | | Empty, |
1427 | | ExpectedCommaAfterDst, |
1428 | | FoundDstNoRule, |
1429 | | FoundDstNoRuleWithOffset, |
1430 | | FoundEndAfterComma, |
1431 | | FoundRemaining, |
1432 | | OffsetDst(PosixOffsetError), |
1433 | | OffsetStd(PosixOffsetError), |
1434 | | Rule(PosixRuleError), |
1435 | | } |
1436 | | |
1437 | | impl core::fmt::Display for PosixTimeZoneError { |
1438 | 0 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { |
1439 | | use self::ErrorKind::*; |
1440 | 0 | match self.kind { |
1441 | 0 | AbbreviationDst(ref err) => { |
1442 | 0 | f.write_str("failed to parse DST time zone abbreviation: ")?; |
1443 | 0 | core::fmt::Display::fmt(err, f) |
1444 | | } |
1445 | 0 | AbbreviationStd(ref err) => { |
1446 | 0 | f.write_str( |
1447 | 0 | "failed to parse standard time zone abbreviation: ", |
1448 | 0 | )?; |
1449 | 0 | core::fmt::Display::fmt(err, f) |
1450 | | } |
1451 | 0 | Empty => f.write_str( |
1452 | 0 | "an empty string is not a valid POSIX time zone \ |
1453 | 0 | transition rule", |
1454 | | ), |
1455 | 0 | ExpectedCommaAfterDst => f.write_str( |
1456 | 0 | "expected `,` after parsing DST offset \ |
1457 | 0 | in POSIX time zone string", |
1458 | | ), |
1459 | 0 | FoundDstNoRule => f.write_str( |
1460 | 0 | "found DST abbreviation in POSIX time zone string, \ |
1461 | 0 | but no transition rule \ |
1462 | 0 | (this is technically allowed by POSIX, but has \ |
1463 | 0 | unspecified behavior)", |
1464 | | ), |
1465 | 0 | FoundDstNoRuleWithOffset => f.write_str( |
1466 | 0 | "found DST abbreviation and offset in POSIX time zone string, \ |
1467 | 0 | but no transition rule \ |
1468 | 0 | (this is technically allowed by POSIX, but has \ |
1469 | 0 | unspecified behavior)", |
1470 | | ), |
1471 | 0 | FoundEndAfterComma => f.write_str( |
1472 | 0 | "after parsing DST offset in POSIX time zone string, \ |
1473 | 0 | found end of string after a trailing `,`", |
1474 | | ), |
1475 | 0 | FoundRemaining => f.write_str( |
1476 | 0 | "expected entire POSIX TZ string to be a valid \ |
1477 | 0 | time zone transition rule, but found data after \ |
1478 | 0 | parsing a valid time zone transition rule", |
1479 | | ), |
1480 | 0 | OffsetDst(ref err) => { |
1481 | 0 | f.write_str("failed to parse DST offset: ")?; |
1482 | 0 | core::fmt::Display::fmt(err, f) |
1483 | | } |
1484 | 0 | OffsetStd(ref err) => { |
1485 | 0 | f.write_str("failed to parse standard offset: ")?; |
1486 | 0 | core::fmt::Display::fmt(err, f) |
1487 | | } |
1488 | 0 | Rule(ref err) => core::fmt::Display::fmt(err, f), |
1489 | | } |
1490 | 0 | } |
1491 | | } |
1492 | | |
1493 | | impl From<ErrorKind> for PosixTimeZoneError { |
1494 | 0 | fn from(kind: ErrorKind) -> PosixTimeZoneError { |
1495 | 0 | PosixTimeZoneError { kind } |
1496 | 0 | } |
1497 | | } |
1498 | | |
1499 | | #[derive(Clone, Debug, Eq, PartialEq)] |
1500 | | enum PosixOffsetError { |
1501 | | HourPosix(HourPosixError), |
1502 | | IncompleteMinutes, |
1503 | | IncompleteSeconds, |
1504 | | Minute(MinuteError), |
1505 | | OptionalSign(OptionalSignError), |
1506 | | Second(SecondError), |
1507 | | } |
1508 | | |
1509 | | impl core::fmt::Display for PosixOffsetError { |
1510 | 0 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { |
1511 | | use self::PosixOffsetError::*; |
1512 | 0 | match *self { |
1513 | 0 | HourPosix(ref err) => core::fmt::Display::fmt(err, f), |
1514 | 0 | IncompleteMinutes => f.write_str( |
1515 | 0 | "incomplete time in \ |
1516 | 0 | POSIX time zone string (missing minutes)", |
1517 | | ), |
1518 | 0 | IncompleteSeconds => f.write_str( |
1519 | 0 | "incomplete time in \ |
1520 | 0 | POSIX time zone string (missing seconds)", |
1521 | | ), |
1522 | 0 | Minute(ref err) => core::fmt::Display::fmt(err, f), |
1523 | 0 | Second(ref err) => core::fmt::Display::fmt(err, f), |
1524 | 0 | OptionalSign(ref err) => { |
1525 | 0 | f.write_str( |
1526 | 0 | "failed to parse sign for time offset \ |
1527 | 0 | POSIX time zone string", |
1528 | 0 | )?; |
1529 | 0 | core::fmt::Display::fmt(err, f) |
1530 | | } |
1531 | | } |
1532 | 0 | } |
1533 | | } |
1534 | | |
1535 | | impl From<HourPosixError> for PosixOffsetError { |
1536 | 0 | fn from(err: HourPosixError) -> PosixOffsetError { |
1537 | 0 | PosixOffsetError::HourPosix(err) |
1538 | 0 | } |
1539 | | } |
1540 | | |
1541 | | impl From<MinuteError> for PosixOffsetError { |
1542 | 0 | fn from(err: MinuteError) -> PosixOffsetError { |
1543 | 0 | PosixOffsetError::Minute(err) |
1544 | 0 | } |
1545 | | } |
1546 | | |
1547 | | impl From<OptionalSignError> for PosixOffsetError { |
1548 | 0 | fn from(err: OptionalSignError) -> PosixOffsetError { |
1549 | 0 | PosixOffsetError::OptionalSign(err) |
1550 | 0 | } |
1551 | | } |
1552 | | |
1553 | | impl From<SecondError> for PosixOffsetError { |
1554 | 0 | fn from(err: SecondError) -> PosixOffsetError { |
1555 | 0 | PosixOffsetError::Second(err) |
1556 | 0 | } |
1557 | | } |
1558 | | |
1559 | | #[derive(Clone, Debug, Eq, PartialEq)] |
1560 | | enum PosixRuleError { |
1561 | | DateTimeEnd(PosixDateTimeError), |
1562 | | DateTimeStart(PosixDateTimeError), |
1563 | | ExpectedEnd, |
1564 | | } |
1565 | | |
1566 | | impl core::fmt::Display for PosixRuleError { |
1567 | 0 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { |
1568 | | use self::PosixRuleError::*; |
1569 | 0 | match *self { |
1570 | 0 | DateTimeEnd(ref err) => { |
1571 | 0 | f.write_str("failed to parse end of DST transition rule: ")?; |
1572 | 0 | core::fmt::Display::fmt(err, f) |
1573 | | } |
1574 | 0 | DateTimeStart(ref err) => { |
1575 | 0 | f.write_str("failed to parse start of DST transition rule: ")?; |
1576 | 0 | core::fmt::Display::fmt(err, f) |
1577 | | } |
1578 | 0 | ExpectedEnd => f.write_str( |
1579 | 0 | "expected end of DST rule after parsing the start \ |
1580 | 0 | of the DST rule", |
1581 | | ), |
1582 | | } |
1583 | 0 | } |
1584 | | } |
1585 | | |
1586 | | #[derive(Clone, Debug, Eq, PartialEq)] |
1587 | | enum PosixDateTimeError { |
1588 | | Date(PosixDateError), |
1589 | | ExpectedTime, |
1590 | | Time(PosixTimeError), |
1591 | | } |
1592 | | |
1593 | | impl core::fmt::Display for PosixDateTimeError { |
1594 | 0 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { |
1595 | | use self::PosixDateTimeError::*; |
1596 | 0 | match *self { |
1597 | 0 | Date(ref err) => core::fmt::Display::fmt(err, f), |
1598 | 0 | ExpectedTime => f.write_str( |
1599 | 0 | "expected time specification after `/` following a date |
1600 | 0 | specification in a POSIX time zone DST transition rule", |
1601 | | ), |
1602 | 0 | Time(ref err) => core::fmt::Display::fmt(err, f), |
1603 | | } |
1604 | 0 | } |
1605 | | } |
1606 | | |
1607 | | impl From<PosixDateError> for PosixDateTimeError { |
1608 | 0 | fn from(err: PosixDateError) -> PosixDateTimeError { |
1609 | 0 | PosixDateTimeError::Date(err) |
1610 | 0 | } |
1611 | | } |
1612 | | |
1613 | | impl From<PosixTimeError> for PosixDateTimeError { |
1614 | 0 | fn from(err: PosixTimeError) -> PosixDateTimeError { |
1615 | 0 | PosixDateTimeError::Time(err) |
1616 | 0 | } |
1617 | | } |
1618 | | |
1619 | | #[derive(Clone, Debug, Eq, PartialEq)] |
1620 | | enum PosixDateError { |
1621 | | ExpectedJulianNoLeap, |
1622 | | ExpectedMonthWeekWeekday, |
1623 | | JulianLeap(PosixJulianLeapError), |
1624 | | JulianNoLeap(PosixJulianNoLeapError), |
1625 | | UnexpectedByte, |
1626 | | WeekdayOfMonth(WeekdayOfMonthError), |
1627 | | } |
1628 | | |
1629 | | impl core::fmt::Display for PosixDateError { |
1630 | 0 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { |
1631 | | use self::PosixDateError::*; |
1632 | 0 | match *self { |
1633 | 0 | ExpectedJulianNoLeap => f.write_str( |
1634 | 0 | "expected one-based Julian day after `J` in date \ |
1635 | 0 | specification of a POSIX time zone DST \ |
1636 | 0 | transition rule, but found the end of input", |
1637 | | ), |
1638 | 0 | ExpectedMonthWeekWeekday => f.write_str( |
1639 | 0 | "expected month-week-weekday after `M` in date \ |
1640 | 0 | specification of a POSIX time zone DST \ |
1641 | 0 | transition rule, but found the end of input", |
1642 | | ), |
1643 | 0 | JulianLeap(ref err) => core::fmt::Display::fmt(err, f), |
1644 | 0 | JulianNoLeap(ref err) => core::fmt::Display::fmt(err, f), |
1645 | 0 | UnexpectedByte => f.write_str( |
1646 | 0 | "expected `J`, a digit or `M` at the beginning of a date \ |
1647 | 0 | specification of a POSIX time zone DST transition rule", |
1648 | | ), |
1649 | 0 | WeekdayOfMonth(ref err) => core::fmt::Display::fmt(err, f), |
1650 | | } |
1651 | 0 | } |
1652 | | } |
1653 | | |
1654 | | impl From<PosixJulianLeapError> for PosixDateError { |
1655 | 0 | fn from(err: PosixJulianLeapError) -> PosixDateError { |
1656 | 0 | PosixDateError::JulianLeap(err) |
1657 | 0 | } |
1658 | | } |
1659 | | |
1660 | | impl From<PosixJulianNoLeapError> for PosixDateError { |
1661 | 0 | fn from(err: PosixJulianNoLeapError) -> PosixDateError { |
1662 | 0 | PosixDateError::JulianNoLeap(err) |
1663 | 0 | } |
1664 | | } |
1665 | | |
1666 | | impl From<WeekdayOfMonthError> for PosixDateError { |
1667 | 0 | fn from(err: WeekdayOfMonthError) -> PosixDateError { |
1668 | 0 | PosixDateError::WeekdayOfMonth(err) |
1669 | 0 | } |
1670 | | } |
1671 | | |
1672 | | #[derive(Clone, Debug, Eq, PartialEq)] |
1673 | | enum PosixJulianNoLeapError { |
1674 | | Parse(NumberError), |
1675 | | Range, |
1676 | | } |
1677 | | |
1678 | | impl core::fmt::Display for PosixJulianNoLeapError { |
1679 | 0 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { |
1680 | | use self::PosixJulianNoLeapError::*; |
1681 | 0 | match *self { |
1682 | 0 | Parse(ref err) => { |
1683 | 0 | f.write_str("invalid one-based Julian day digits: ")?; |
1684 | 0 | core::fmt::Display::fmt(err, f) |
1685 | | } |
1686 | 0 | Range => f.write_str( |
1687 | 0 | "parsed one-based Julian day, but it's not in supported \ |
1688 | 0 | range of `1..=365`", |
1689 | | ), |
1690 | | } |
1691 | 0 | } |
1692 | | } |
1693 | | |
1694 | | #[derive(Clone, Debug, Eq, PartialEq)] |
1695 | | enum PosixJulianLeapError { |
1696 | | Parse(NumberError), |
1697 | | Range, |
1698 | | } |
1699 | | |
1700 | | impl core::fmt::Display for PosixJulianLeapError { |
1701 | 0 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { |
1702 | | use self::PosixJulianLeapError::*; |
1703 | 0 | match *self { |
1704 | 0 | Parse(ref err) => { |
1705 | 0 | f.write_str("invalid zero-based Julian day digits: ")?; |
1706 | 0 | core::fmt::Display::fmt(err, f) |
1707 | | } |
1708 | 0 | Range => f.write_str( |
1709 | 0 | "parsed zero-based Julian day, but it's not in supported \ |
1710 | 0 | range of `0..=365`", |
1711 | | ), |
1712 | | } |
1713 | 0 | } |
1714 | | } |
1715 | | |
1716 | | #[derive(Clone, Debug, Eq, PartialEq)] |
1717 | | enum AbbreviationError { |
1718 | | Quoted(QuotedAbbreviationError), |
1719 | | Unquoted(UnquotedAbbreviationError), |
1720 | | } |
1721 | | |
1722 | | impl core::fmt::Display for AbbreviationError { |
1723 | 0 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { |
1724 | | use self::AbbreviationError::*; |
1725 | 0 | match *self { |
1726 | 0 | Quoted(ref err) => core::fmt::Display::fmt(err, f), |
1727 | 0 | Unquoted(ref err) => core::fmt::Display::fmt(err, f), |
1728 | | } |
1729 | 0 | } |
1730 | | } |
1731 | | |
1732 | | #[derive(Clone, Debug, Eq, PartialEq)] |
1733 | | enum UnquotedAbbreviationError { |
1734 | | InvalidUtf8, |
1735 | | TooLong, |
1736 | | TooShort, |
1737 | | } |
1738 | | |
1739 | | impl core::fmt::Display for UnquotedAbbreviationError { |
1740 | 0 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { |
1741 | | use self::UnquotedAbbreviationError::*; |
1742 | 0 | match *self { |
1743 | 0 | InvalidUtf8 => f.write_str( |
1744 | 0 | "unquoted time zone abbreviation must be valid UTF-8", |
1745 | | ), |
1746 | 0 | TooLong => write!( |
1747 | 0 | f, |
1748 | 0 | "expected unquoted time zone abbreviation with at most \ |
1749 | 0 | {} bytes, but found an abbreviation that is longer", |
1750 | 0 | Abbreviation::capacity(), |
1751 | | ), |
1752 | 0 | TooShort => f.write_str( |
1753 | 0 | "expected unquoted time zone abbreviation to have length of \ |
1754 | 0 | 3 or more bytes, but an abbreviation that is shorter", |
1755 | | ), |
1756 | | } |
1757 | 0 | } |
1758 | | } |
1759 | | |
1760 | | #[derive(Clone, Debug, Eq, PartialEq)] |
1761 | | enum QuotedAbbreviationError { |
1762 | | InvalidUtf8, |
1763 | | TooLong, |
1764 | | TooShort, |
1765 | | UnexpectedEnd, |
1766 | | UnexpectedEndAfterOpening, |
1767 | | UnexpectedLastByte, |
1768 | | } |
1769 | | |
1770 | | impl core::fmt::Display for QuotedAbbreviationError { |
1771 | 0 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { |
1772 | | use self::QuotedAbbreviationError::*; |
1773 | 0 | match *self { |
1774 | 0 | InvalidUtf8 => f.write_str( |
1775 | 0 | "quoted time zone abbreviation must be valid UTF-8", |
1776 | | ), |
1777 | 0 | TooLong => write!( |
1778 | 0 | f, |
1779 | 0 | "expected quoted time zone abbreviation with at most \ |
1780 | 0 | {} bytes, but found an abbreviation that is longer", |
1781 | 0 | Abbreviation::capacity(), |
1782 | | ), |
1783 | 0 | TooShort => f.write_str( |
1784 | 0 | "expected quoted time zone abbreviation to have length of \ |
1785 | 0 | 3 or more bytes, but an abbreviation that is shorter", |
1786 | | ), |
1787 | 0 | UnexpectedEnd => f.write_str( |
1788 | 0 | "found non-empty quoted time zone abbreviation, but \ |
1789 | 0 | found end of input before an end-of-quoted abbreviation \ |
1790 | 0 | `>` character", |
1791 | | ), |
1792 | 0 | UnexpectedEndAfterOpening => f.write_str( |
1793 | 0 | "found opening `<` quote for time zone abbreviation in \ |
1794 | 0 | POSIX time zone transition rule, and expected a name \ |
1795 | 0 | following it, but found the end of input instead", |
1796 | | ), |
1797 | 0 | UnexpectedLastByte => f.write_str( |
1798 | 0 | "found non-empty quoted time zone abbreviation, but \ |
1799 | 0 | found did not find end-of-quoted abbreviation `>` \ |
1800 | 0 | character", |
1801 | | ), |
1802 | | } |
1803 | 0 | } |
1804 | | } |
1805 | | |
1806 | | #[derive(Clone, Debug, Eq, PartialEq)] |
1807 | | enum WeekdayOfMonthError { |
1808 | | ExpectedDayOfWeekAfterWeek, |
1809 | | ExpectedDotAfterMonth, |
1810 | | ExpectedDotAfterWeek, |
1811 | | ExpectedWeekAfterMonth, |
1812 | | Month(MonthError), |
1813 | | WeekOfMonth(WeekOfMonthError), |
1814 | | Weekday(WeekdayError), |
1815 | | } |
1816 | | |
1817 | | impl core::fmt::Display for WeekdayOfMonthError { |
1818 | 0 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { |
1819 | | use self::WeekdayOfMonthError::*; |
1820 | 0 | match *self { |
1821 | 0 | ExpectedDayOfWeekAfterWeek => f.write_str( |
1822 | 0 | "expected day-of-week after week in POSIX time zone rule", |
1823 | | ), |
1824 | | ExpectedDotAfterMonth => { |
1825 | 0 | f.write_str("expected `.` after month in POSIX time zone rule") |
1826 | | } |
1827 | 0 | ExpectedWeekAfterMonth => f.write_str( |
1828 | 0 | "expected week after month in POSIX time zone rule", |
1829 | | ), |
1830 | | ExpectedDotAfterWeek => { |
1831 | 0 | f.write_str("expected `.` after week in POSIX time zone rule") |
1832 | | } |
1833 | 0 | Month(ref err) => core::fmt::Display::fmt(err, f), |
1834 | 0 | WeekOfMonth(ref err) => core::fmt::Display::fmt(err, f), |
1835 | 0 | Weekday(ref err) => core::fmt::Display::fmt(err, f), |
1836 | | } |
1837 | 0 | } |
1838 | | } |
1839 | | |
1840 | | impl From<MonthError> for WeekdayOfMonthError { |
1841 | 0 | fn from(err: MonthError) -> WeekdayOfMonthError { |
1842 | 0 | WeekdayOfMonthError::Month(err) |
1843 | 0 | } |
1844 | | } |
1845 | | |
1846 | | impl From<WeekOfMonthError> for WeekdayOfMonthError { |
1847 | 0 | fn from(err: WeekOfMonthError) -> WeekdayOfMonthError { |
1848 | 0 | WeekdayOfMonthError::WeekOfMonth(err) |
1849 | 0 | } |
1850 | | } |
1851 | | |
1852 | | impl From<WeekdayError> for WeekdayOfMonthError { |
1853 | 0 | fn from(err: WeekdayError) -> WeekdayOfMonthError { |
1854 | 0 | WeekdayOfMonthError::Weekday(err) |
1855 | 0 | } |
1856 | | } |
1857 | | |
1858 | | #[derive(Clone, Debug, Eq, PartialEq)] |
1859 | | enum PosixTimeError { |
1860 | | HourIana(HourIanaError), |
1861 | | HourPosix(HourPosixError), |
1862 | | IncompleteMinutes, |
1863 | | IncompleteSeconds, |
1864 | | Minute(MinuteError), |
1865 | | OptionalSign(OptionalSignError), |
1866 | | Second(SecondError), |
1867 | | } |
1868 | | |
1869 | | impl core::fmt::Display for PosixTimeError { |
1870 | 0 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { |
1871 | | use self::PosixTimeError::*; |
1872 | 0 | match *self { |
1873 | 0 | HourIana(ref err) => core::fmt::Display::fmt(err, f), |
1874 | 0 | HourPosix(ref err) => core::fmt::Display::fmt(err, f), |
1875 | 0 | IncompleteMinutes => f.write_str( |
1876 | 0 | "incomplete time zone transition time in \ |
1877 | 0 | POSIX time zone string (missing minutes)", |
1878 | | ), |
1879 | 0 | IncompleteSeconds => f.write_str( |
1880 | 0 | "incomplete time zone transition time in \ |
1881 | 0 | POSIX time zone string (missing seconds)", |
1882 | | ), |
1883 | 0 | Minute(ref err) => core::fmt::Display::fmt(err, f), |
1884 | 0 | Second(ref err) => core::fmt::Display::fmt(err, f), |
1885 | 0 | OptionalSign(ref err) => { |
1886 | 0 | f.write_str( |
1887 | 0 | "failed to parse sign for time zone transition time", |
1888 | 0 | )?; |
1889 | 0 | core::fmt::Display::fmt(err, f) |
1890 | | } |
1891 | | } |
1892 | 0 | } |
1893 | | } |
1894 | | |
1895 | | impl From<HourIanaError> for PosixTimeError { |
1896 | 0 | fn from(err: HourIanaError) -> PosixTimeError { |
1897 | 0 | PosixTimeError::HourIana(err) |
1898 | 0 | } |
1899 | | } |
1900 | | |
1901 | | impl From<HourPosixError> for PosixTimeError { |
1902 | 0 | fn from(err: HourPosixError) -> PosixTimeError { |
1903 | 0 | PosixTimeError::HourPosix(err) |
1904 | 0 | } |
1905 | | } |
1906 | | |
1907 | | impl From<MinuteError> for PosixTimeError { |
1908 | 0 | fn from(err: MinuteError) -> PosixTimeError { |
1909 | 0 | PosixTimeError::Minute(err) |
1910 | 0 | } |
1911 | | } |
1912 | | |
1913 | | impl From<OptionalSignError> for PosixTimeError { |
1914 | 0 | fn from(err: OptionalSignError) -> PosixTimeError { |
1915 | 0 | PosixTimeError::OptionalSign(err) |
1916 | 0 | } |
1917 | | } |
1918 | | |
1919 | | impl From<SecondError> for PosixTimeError { |
1920 | 0 | fn from(err: SecondError) -> PosixTimeError { |
1921 | 0 | PosixTimeError::Second(err) |
1922 | 0 | } |
1923 | | } |
1924 | | |
1925 | | #[derive(Clone, Debug, Eq, PartialEq)] |
1926 | | enum MonthError { |
1927 | | Parse(NumberError), |
1928 | | Range, |
1929 | | } |
1930 | | |
1931 | | impl core::fmt::Display for MonthError { |
1932 | 0 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { |
1933 | | use self::MonthError::*; |
1934 | 0 | match *self { |
1935 | 0 | Parse(ref err) => { |
1936 | 0 | f.write_str("invalid month digits: ")?; |
1937 | 0 | core::fmt::Display::fmt(err, f) |
1938 | | } |
1939 | 0 | Range => f.write_str( |
1940 | 0 | "parsed month, but it's not in supported \ |
1941 | 0 | range of `1..=12`", |
1942 | | ), |
1943 | | } |
1944 | 0 | } |
1945 | | } |
1946 | | |
1947 | | #[derive(Clone, Debug, Eq, PartialEq)] |
1948 | | enum WeekOfMonthError { |
1949 | | Parse(NumberError), |
1950 | | Range, |
1951 | | } |
1952 | | |
1953 | | impl core::fmt::Display for WeekOfMonthError { |
1954 | 0 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { |
1955 | | use self::WeekOfMonthError::*; |
1956 | 0 | match *self { |
1957 | 0 | Parse(ref err) => { |
1958 | 0 | f.write_str("invalid week-of-month digits: ")?; |
1959 | 0 | core::fmt::Display::fmt(err, f) |
1960 | | } |
1961 | 0 | Range => f.write_str( |
1962 | 0 | "parsed week-of-month, but it's not in supported \ |
1963 | 0 | range of `1..=5`", |
1964 | | ), |
1965 | | } |
1966 | 0 | } |
1967 | | } |
1968 | | |
1969 | | #[derive(Clone, Debug, Eq, PartialEq)] |
1970 | | enum WeekdayError { |
1971 | | Parse(NumberError), |
1972 | | Range, |
1973 | | } |
1974 | | |
1975 | | impl core::fmt::Display for WeekdayError { |
1976 | 0 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { |
1977 | | use self::WeekdayError::*; |
1978 | 0 | match *self { |
1979 | 0 | Parse(ref err) => { |
1980 | 0 | f.write_str("invalid weekday digits: ")?; |
1981 | 0 | core::fmt::Display::fmt(err, f) |
1982 | | } |
1983 | 0 | Range => f.write_str( |
1984 | 0 | "parsed weekday, but it's not in supported \ |
1985 | 0 | range of `0..=6` (with `0` corresponding to Sunday)", |
1986 | | ), |
1987 | | } |
1988 | 0 | } |
1989 | | } |
1990 | | |
1991 | | #[derive(Clone, Debug, Eq, PartialEq)] |
1992 | | enum HourIanaError { |
1993 | | Parse(NumberError), |
1994 | | Range, |
1995 | | } |
1996 | | |
1997 | | impl core::fmt::Display for HourIanaError { |
1998 | 0 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { |
1999 | | use self::HourIanaError::*; |
2000 | 0 | match *self { |
2001 | 0 | Parse(ref err) => { |
2002 | 0 | f.write_str("invalid hour digits: ")?; |
2003 | 0 | core::fmt::Display::fmt(err, f) |
2004 | | } |
2005 | 0 | Range => f.write_str( |
2006 | 0 | "parsed hours, but it's not in supported \ |
2007 | 0 | range of `-167..=167`", |
2008 | | ), |
2009 | | } |
2010 | 0 | } |
2011 | | } |
2012 | | |
2013 | | #[derive(Clone, Debug, Eq, PartialEq)] |
2014 | | enum HourPosixError { |
2015 | | Parse(NumberError), |
2016 | | Range, |
2017 | | } |
2018 | | |
2019 | | impl core::fmt::Display for HourPosixError { |
2020 | 0 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { |
2021 | | use self::HourPosixError::*; |
2022 | 0 | match *self { |
2023 | 0 | Parse(ref err) => { |
2024 | 0 | f.write_str("invalid hour digits: ")?; |
2025 | 0 | core::fmt::Display::fmt(err, f) |
2026 | | } |
2027 | 0 | Range => f.write_str( |
2028 | 0 | "parsed hours, but it's not in supported \ |
2029 | 0 | range of `0..=24`", |
2030 | | ), |
2031 | | } |
2032 | 0 | } |
2033 | | } |
2034 | | |
2035 | | #[derive(Clone, Debug, Eq, PartialEq)] |
2036 | | enum MinuteError { |
2037 | | Parse(NumberError), |
2038 | | Range, |
2039 | | } |
2040 | | |
2041 | | impl core::fmt::Display for MinuteError { |
2042 | 0 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { |
2043 | | use self::MinuteError::*; |
2044 | 0 | match *self { |
2045 | 0 | Parse(ref err) => { |
2046 | 0 | f.write_str("invalid minute digits: ")?; |
2047 | 0 | core::fmt::Display::fmt(err, f) |
2048 | | } |
2049 | 0 | Range => f.write_str( |
2050 | 0 | "parsed minutes, but it's not in supported \ |
2051 | 0 | range of `0..=59`", |
2052 | | ), |
2053 | | } |
2054 | 0 | } |
2055 | | } |
2056 | | |
2057 | | #[derive(Clone, Debug, Eq, PartialEq)] |
2058 | | enum SecondError { |
2059 | | Parse(NumberError), |
2060 | | Range, |
2061 | | } |
2062 | | |
2063 | | impl core::fmt::Display for SecondError { |
2064 | 0 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { |
2065 | | use self::SecondError::*; |
2066 | 0 | match *self { |
2067 | 0 | Parse(ref err) => { |
2068 | 0 | f.write_str("invalid second digits: ")?; |
2069 | 0 | core::fmt::Display::fmt(err, f) |
2070 | | } |
2071 | 0 | Range => f.write_str( |
2072 | 0 | "parsed seconds, but it's not in supported \ |
2073 | 0 | range of `0..=59`", |
2074 | | ), |
2075 | | } |
2076 | 0 | } |
2077 | | } |
2078 | | |
2079 | | #[derive(Clone, Debug, Eq, PartialEq)] |
2080 | | enum NumberError { |
2081 | | Empty, |
2082 | | ExpectedLength, |
2083 | | InvalidDigit, |
2084 | | TooBig, |
2085 | | } |
2086 | | |
2087 | | impl core::fmt::Display for NumberError { |
2088 | 0 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { |
2089 | | use self::NumberError::*; |
2090 | 0 | match *self { |
2091 | 0 | Empty => f.write_str("invalid number, no digits found"), |
2092 | 0 | ExpectedLength => f.write_str( |
2093 | 0 | "expected a fixed number of digits, \ |
2094 | 0 | but found incorrect number", |
2095 | | ), |
2096 | 0 | InvalidDigit => f.write_str("expected digit in range `0..=9`"), |
2097 | 0 | TooBig => f.write_str( |
2098 | 0 | "parsed number too big to fit into a 32-bit signed integer", |
2099 | | ), |
2100 | | } |
2101 | 0 | } |
2102 | | } |
2103 | | |
2104 | | #[derive(Clone, Debug, Eq, PartialEq)] |
2105 | | enum OptionalSignError { |
2106 | | ExpectedDigitAfterMinus, |
2107 | | ExpectedDigitAfterPlus, |
2108 | | } |
2109 | | |
2110 | | impl core::fmt::Display for OptionalSignError { |
2111 | 0 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { |
2112 | | use self::OptionalSignError::*; |
2113 | 0 | match *self { |
2114 | 0 | ExpectedDigitAfterMinus => f.write_str( |
2115 | 0 | "expected digit after `-` sign, \ |
2116 | 0 | but got end of input", |
2117 | | ), |
2118 | 0 | ExpectedDigitAfterPlus => f.write_str( |
2119 | 0 | "expected digit after `+` sign, \ |
2120 | 0 | but got end of input", |
2121 | | ), |
2122 | | } |
2123 | 0 | } |
2124 | | } |
2125 | | |
2126 | | #[cfg(test)] |
2127 | | mod tests { |
2128 | | use super::*; |
2129 | | |
2130 | | fn posix_time_zone( |
2131 | | input: impl AsRef<[u8]>, |
2132 | | ) -> PosixTimeZone<Abbreviation> { |
2133 | | let input = input.as_ref(); |
2134 | | let tz = PosixTimeZone::parse(input).unwrap(); |
2135 | | #[cfg(feature = "alloc")] |
2136 | | { |
2137 | | use alloc::string::ToString; |
2138 | | |
2139 | | // While we're here, assert that converting the TZ back |
2140 | | // to a string matches what we got. In the original version |
2141 | | // of the POSIX TZ parser, we were very meticulous about |
2142 | | // capturing the exact AST of the time zone. But I've |
2143 | | // since simplified the data structure considerably such |
2144 | | // that it is lossy in terms of what was actually parsed |
2145 | | // (but of course, not lossy in terms of the semantic |
2146 | | // meaning of the time zone). |
2147 | | // |
2148 | | // So to account for this, we serialize to a string and |
2149 | | // then parse it back. We should get what we started with. |
2150 | | let reparsed = |
2151 | | PosixTimeZone::parse(tz.to_string().as_bytes()).unwrap(); |
2152 | | assert_eq!(tz, reparsed); |
2153 | | assert_eq!(tz.to_string(), reparsed.to_string()); |
2154 | | } |
2155 | | tz |
2156 | | } |
2157 | | |
2158 | | fn parser(s: &str) -> Parser<'_> { |
2159 | | Parser::new(s.as_bytes()) |
2160 | | } |
2161 | | |
2162 | | fn date(year: i16, month: i8, day: i8) -> IDate { |
2163 | | IDate { year, month, day } |
2164 | | } |
2165 | | |
2166 | | #[test] |
2167 | | fn parse() { |
2168 | | let p = parser("NZST-12NZDT,J60,J300"); |
2169 | | assert_eq!( |
2170 | | p.parse().unwrap(), |
2171 | | PosixTimeZone { |
2172 | | std_abbrev: "NZST".into(), |
2173 | | std_offset: PosixOffset { second: 12 * 60 * 60 }, |
2174 | | dst: Some(PosixDst { |
2175 | | abbrev: "NZDT".into(), |
2176 | | offset: PosixOffset { second: 13 * 60 * 60 }, |
2177 | | rule: PosixRule { |
2178 | | start: PosixDayTime { |
2179 | | date: PosixDay::JulianOne(60), |
2180 | | time: PosixTime { second: 2 * 60 * 60 }, |
2181 | | }, |
2182 | | end: PosixDayTime { |
2183 | | date: PosixDay::JulianOne(300), |
2184 | | time: PosixTime { second: 2 * 60 * 60 }, |
2185 | | }, |
2186 | | }, |
2187 | | }), |
2188 | | }, |
2189 | | ); |
2190 | | |
2191 | | let p = Parser::new("NZST-12NZDT,J60,J300WAT"); |
2192 | | assert!(p.parse().is_err()); |
2193 | | } |
2194 | | |
2195 | | #[test] |
2196 | | fn parse_posix_time_zone() { |
2197 | | let p = Parser::new("NZST-12NZDT,M9.5.0,M4.1.0/3"); |
2198 | | assert_eq!( |
2199 | | p.parse_posix_time_zone().unwrap(), |
2200 | | PosixTimeZone { |
2201 | | std_abbrev: "NZST".into(), |
2202 | | std_offset: PosixOffset { second: 12 * 60 * 60 }, |
2203 | | dst: Some(PosixDst { |
2204 | | abbrev: "NZDT".into(), |
2205 | | offset: PosixOffset { second: 13 * 60 * 60 }, |
2206 | | rule: PosixRule { |
2207 | | start: PosixDayTime { |
2208 | | date: PosixDay::WeekdayOfMonth { |
2209 | | month: 9, |
2210 | | week: 5, |
2211 | | weekday: 0, |
2212 | | }, |
2213 | | time: PosixTime { second: 2 * 60 * 60 }, |
2214 | | }, |
2215 | | end: PosixDayTime { |
2216 | | date: PosixDay::WeekdayOfMonth { |
2217 | | month: 4, |
2218 | | week: 1, |
2219 | | weekday: 0, |
2220 | | }, |
2221 | | time: PosixTime { second: 3 * 60 * 60 }, |
2222 | | }, |
2223 | | }, |
2224 | | }), |
2225 | | }, |
2226 | | ); |
2227 | | |
2228 | | let p = Parser::new("NZST-12NZDT,M9.5.0,M4.1.0/3WAT"); |
2229 | | assert_eq!( |
2230 | | p.parse_posix_time_zone().unwrap(), |
2231 | | PosixTimeZone { |
2232 | | std_abbrev: "NZST".into(), |
2233 | | std_offset: PosixOffset { second: 12 * 60 * 60 }, |
2234 | | dst: Some(PosixDst { |
2235 | | abbrev: "NZDT".into(), |
2236 | | offset: PosixOffset { second: 13 * 60 * 60 }, |
2237 | | rule: PosixRule { |
2238 | | start: PosixDayTime { |
2239 | | date: PosixDay::WeekdayOfMonth { |
2240 | | month: 9, |
2241 | | week: 5, |
2242 | | weekday: 0, |
2243 | | }, |
2244 | | time: PosixTime { second: 2 * 60 * 60 }, |
2245 | | }, |
2246 | | end: PosixDayTime { |
2247 | | date: PosixDay::WeekdayOfMonth { |
2248 | | month: 4, |
2249 | | week: 1, |
2250 | | weekday: 0, |
2251 | | }, |
2252 | | time: PosixTime { second: 3 * 60 * 60 }, |
2253 | | }, |
2254 | | }, |
2255 | | }), |
2256 | | }, |
2257 | | ); |
2258 | | |
2259 | | let p = Parser::new("NZST-12NZDT,J60,J300"); |
2260 | | assert_eq!( |
2261 | | p.parse_posix_time_zone().unwrap(), |
2262 | | PosixTimeZone { |
2263 | | std_abbrev: "NZST".into(), |
2264 | | std_offset: PosixOffset { second: 12 * 60 * 60 }, |
2265 | | dst: Some(PosixDst { |
2266 | | abbrev: "NZDT".into(), |
2267 | | offset: PosixOffset { second: 13 * 60 * 60 }, |
2268 | | rule: PosixRule { |
2269 | | start: PosixDayTime { |
2270 | | date: PosixDay::JulianOne(60), |
2271 | | time: PosixTime { second: 2 * 60 * 60 }, |
2272 | | }, |
2273 | | end: PosixDayTime { |
2274 | | date: PosixDay::JulianOne(300), |
2275 | | time: PosixTime { second: 2 * 60 * 60 }, |
2276 | | }, |
2277 | | }, |
2278 | | }), |
2279 | | }, |
2280 | | ); |
2281 | | |
2282 | | let p = Parser::new("NZST-12NZDT,J60,J300WAT"); |
2283 | | assert_eq!( |
2284 | | p.parse_posix_time_zone().unwrap(), |
2285 | | PosixTimeZone { |
2286 | | std_abbrev: "NZST".into(), |
2287 | | std_offset: PosixOffset { second: 12 * 60 * 60 }, |
2288 | | dst: Some(PosixDst { |
2289 | | abbrev: "NZDT".into(), |
2290 | | offset: PosixOffset { second: 13 * 60 * 60 }, |
2291 | | rule: PosixRule { |
2292 | | start: PosixDayTime { |
2293 | | date: PosixDay::JulianOne(60), |
2294 | | time: PosixTime { second: 2 * 60 * 60 }, |
2295 | | }, |
2296 | | end: PosixDayTime { |
2297 | | date: PosixDay::JulianOne(300), |
2298 | | time: PosixTime { second: 2 * 60 * 60 }, |
2299 | | }, |
2300 | | }, |
2301 | | }), |
2302 | | }, |
2303 | | ); |
2304 | | } |
2305 | | |
2306 | | #[test] |
2307 | | fn parse_posix_dst() { |
2308 | | let p = Parser::new("NZDT,M9.5.0,M4.1.0/3"); |
2309 | | assert_eq!( |
2310 | | p.parse_posix_dst(&PosixOffset { second: 12 * 60 * 60 }).unwrap(), |
2311 | | PosixDst { |
2312 | | abbrev: "NZDT".into(), |
2313 | | offset: PosixOffset { second: 13 * 60 * 60 }, |
2314 | | rule: PosixRule { |
2315 | | start: PosixDayTime { |
2316 | | date: PosixDay::WeekdayOfMonth { |
2317 | | month: 9, |
2318 | | week: 5, |
2319 | | weekday: 0, |
2320 | | }, |
2321 | | time: PosixTime { second: 2 * 60 * 60 }, |
2322 | | }, |
2323 | | end: PosixDayTime { |
2324 | | date: PosixDay::WeekdayOfMonth { |
2325 | | month: 4, |
2326 | | week: 1, |
2327 | | weekday: 0, |
2328 | | }, |
2329 | | time: PosixTime { second: 3 * 60 * 60 }, |
2330 | | }, |
2331 | | }, |
2332 | | }, |
2333 | | ); |
2334 | | |
2335 | | let p = Parser::new("NZDT,J60,J300"); |
2336 | | assert_eq!( |
2337 | | p.parse_posix_dst(&PosixOffset { second: 12 * 60 * 60 }).unwrap(), |
2338 | | PosixDst { |
2339 | | abbrev: "NZDT".into(), |
2340 | | offset: PosixOffset { second: 13 * 60 * 60 }, |
2341 | | rule: PosixRule { |
2342 | | start: PosixDayTime { |
2343 | | date: PosixDay::JulianOne(60), |
2344 | | time: PosixTime { second: 2 * 60 * 60 }, |
2345 | | }, |
2346 | | end: PosixDayTime { |
2347 | | date: PosixDay::JulianOne(300), |
2348 | | time: PosixTime { second: 2 * 60 * 60 }, |
2349 | | }, |
2350 | | }, |
2351 | | }, |
2352 | | ); |
2353 | | |
2354 | | let p = Parser::new("NZDT-7,J60,J300"); |
2355 | | assert_eq!( |
2356 | | p.parse_posix_dst(&PosixOffset { second: 12 * 60 * 60 }).unwrap(), |
2357 | | PosixDst { |
2358 | | abbrev: "NZDT".into(), |
2359 | | offset: PosixOffset { second: 7 * 60 * 60 }, |
2360 | | rule: PosixRule { |
2361 | | start: PosixDayTime { |
2362 | | date: PosixDay::JulianOne(60), |
2363 | | time: PosixTime { second: 2 * 60 * 60 }, |
2364 | | }, |
2365 | | end: PosixDayTime { |
2366 | | date: PosixDay::JulianOne(300), |
2367 | | time: PosixTime { second: 2 * 60 * 60 }, |
2368 | | }, |
2369 | | }, |
2370 | | }, |
2371 | | ); |
2372 | | |
2373 | | let p = Parser::new("NZDT+7,J60,J300"); |
2374 | | assert_eq!( |
2375 | | p.parse_posix_dst(&PosixOffset { second: 12 * 60 * 60 }).unwrap(), |
2376 | | PosixDst { |
2377 | | abbrev: "NZDT".into(), |
2378 | | offset: PosixOffset { second: -7 * 60 * 60 }, |
2379 | | rule: PosixRule { |
2380 | | start: PosixDayTime { |
2381 | | date: PosixDay::JulianOne(60), |
2382 | | time: PosixTime { second: 2 * 60 * 60 }, |
2383 | | }, |
2384 | | end: PosixDayTime { |
2385 | | date: PosixDay::JulianOne(300), |
2386 | | time: PosixTime { second: 2 * 60 * 60 }, |
2387 | | }, |
2388 | | }, |
2389 | | }, |
2390 | | ); |
2391 | | |
2392 | | let p = Parser::new("NZDT7,J60,J300"); |
2393 | | assert_eq!( |
2394 | | p.parse_posix_dst(&PosixOffset { second: 12 * 60 * 60 }).unwrap(), |
2395 | | PosixDst { |
2396 | | abbrev: "NZDT".into(), |
2397 | | offset: PosixOffset { second: -7 * 60 * 60 }, |
2398 | | rule: PosixRule { |
2399 | | start: PosixDayTime { |
2400 | | date: PosixDay::JulianOne(60), |
2401 | | time: PosixTime { second: 2 * 60 * 60 }, |
2402 | | }, |
2403 | | end: PosixDayTime { |
2404 | | date: PosixDay::JulianOne(300), |
2405 | | time: PosixTime { second: 2 * 60 * 60 }, |
2406 | | }, |
2407 | | }, |
2408 | | }, |
2409 | | ); |
2410 | | |
2411 | | let p = Parser::new("NZDT7,"); |
2412 | | assert!(p |
2413 | | .parse_posix_dst(&PosixOffset { second: 12 * 60 * 60 }) |
2414 | | .is_err()); |
2415 | | |
2416 | | let p = Parser::new("NZDT7!"); |
2417 | | assert!(p |
2418 | | .parse_posix_dst(&PosixOffset { second: 12 * 60 * 60 }) |
2419 | | .is_err()); |
2420 | | } |
2421 | | |
2422 | | #[test] |
2423 | | fn parse_abbreviation() { |
2424 | | let p = Parser::new("ABC"); |
2425 | | assert_eq!(p.parse_abbreviation().unwrap(), "ABC"); |
2426 | | |
2427 | | let p = Parser::new("<ABC>"); |
2428 | | assert_eq!(p.parse_abbreviation().unwrap(), "ABC"); |
2429 | | |
2430 | | let p = Parser::new("<+09>"); |
2431 | | assert_eq!(p.parse_abbreviation().unwrap(), "+09"); |
2432 | | |
2433 | | let p = Parser::new("+09"); |
2434 | | assert!(p.parse_abbreviation().is_err()); |
2435 | | } |
2436 | | |
2437 | | #[test] |
2438 | | fn parse_unquoted_abbreviation() { |
2439 | | let p = Parser::new("ABC"); |
2440 | | assert_eq!(p.parse_unquoted_abbreviation().unwrap(), "ABC"); |
2441 | | |
2442 | | let p = Parser::new("ABCXYZ"); |
2443 | | assert_eq!(p.parse_unquoted_abbreviation().unwrap(), "ABCXYZ"); |
2444 | | |
2445 | | let p = Parser::new("ABC123"); |
2446 | | assert_eq!(p.parse_unquoted_abbreviation().unwrap(), "ABC"); |
2447 | | |
2448 | | let tz = "a".repeat(30); |
2449 | | let p = Parser::new(&tz); |
2450 | | assert_eq!(p.parse_unquoted_abbreviation().unwrap(), &*tz); |
2451 | | |
2452 | | let p = Parser::new("a"); |
2453 | | assert!(p.parse_unquoted_abbreviation().is_err()); |
2454 | | |
2455 | | let p = Parser::new("ab"); |
2456 | | assert!(p.parse_unquoted_abbreviation().is_err()); |
2457 | | |
2458 | | let p = Parser::new("ab1"); |
2459 | | assert!(p.parse_unquoted_abbreviation().is_err()); |
2460 | | |
2461 | | let tz = "a".repeat(31); |
2462 | | let p = Parser::new(&tz); |
2463 | | assert!(p.parse_unquoted_abbreviation().is_err()); |
2464 | | |
2465 | | let p = Parser::new(b"ab\xFFcd"); |
2466 | | assert!(p.parse_unquoted_abbreviation().is_err()); |
2467 | | } |
2468 | | |
2469 | | #[test] |
2470 | | fn parse_quoted_abbreviation() { |
2471 | | // The inputs look a little funny here, but that's because |
2472 | | // 'parse_quoted_abbreviation' starts after the opening quote |
2473 | | // has been parsed. |
2474 | | |
2475 | | let p = Parser::new("ABC>"); |
2476 | | assert_eq!(p.parse_quoted_abbreviation().unwrap(), "ABC"); |
2477 | | |
2478 | | let p = Parser::new("ABCXYZ>"); |
2479 | | assert_eq!(p.parse_quoted_abbreviation().unwrap(), "ABCXYZ"); |
2480 | | |
2481 | | let p = Parser::new("ABC>123"); |
2482 | | assert_eq!(p.parse_quoted_abbreviation().unwrap(), "ABC"); |
2483 | | |
2484 | | let p = Parser::new("ABC123>"); |
2485 | | assert_eq!(p.parse_quoted_abbreviation().unwrap(), "ABC123"); |
2486 | | |
2487 | | let p = Parser::new("ab1>"); |
2488 | | assert_eq!(p.parse_quoted_abbreviation().unwrap(), "ab1"); |
2489 | | |
2490 | | let p = Parser::new("+09>"); |
2491 | | assert_eq!(p.parse_quoted_abbreviation().unwrap(), "+09"); |
2492 | | |
2493 | | let p = Parser::new("-09>"); |
2494 | | assert_eq!(p.parse_quoted_abbreviation().unwrap(), "-09"); |
2495 | | |
2496 | | let tz = alloc::format!("{}>", "a".repeat(30)); |
2497 | | let p = Parser::new(&tz); |
2498 | | assert_eq!( |
2499 | | p.parse_quoted_abbreviation().unwrap(), |
2500 | | tz.trim_end_matches(">") |
2501 | | ); |
2502 | | |
2503 | | let p = Parser::new("a>"); |
2504 | | assert!(p.parse_quoted_abbreviation().is_err()); |
2505 | | |
2506 | | let p = Parser::new("ab>"); |
2507 | | assert!(p.parse_quoted_abbreviation().is_err()); |
2508 | | |
2509 | | let tz = alloc::format!("{}>", "a".repeat(31)); |
2510 | | let p = Parser::new(&tz); |
2511 | | assert!(p.parse_quoted_abbreviation().is_err()); |
2512 | | |
2513 | | let p = Parser::new(b"ab\xFFcd>"); |
2514 | | assert!(p.parse_quoted_abbreviation().is_err()); |
2515 | | |
2516 | | let p = Parser::new("ABC"); |
2517 | | assert!(p.parse_quoted_abbreviation().is_err()); |
2518 | | |
2519 | | let p = Parser::new("ABC!>"); |
2520 | | assert!(p.parse_quoted_abbreviation().is_err()); |
2521 | | } |
2522 | | |
2523 | | #[test] |
2524 | | fn parse_posix_offset() { |
2525 | | let p = Parser::new("5"); |
2526 | | assert_eq!(p.parse_posix_offset().unwrap().second, -5 * 60 * 60); |
2527 | | |
2528 | | let p = Parser::new("+5"); |
2529 | | assert_eq!(p.parse_posix_offset().unwrap().second, -5 * 60 * 60); |
2530 | | |
2531 | | let p = Parser::new("-5"); |
2532 | | assert_eq!(p.parse_posix_offset().unwrap().second, 5 * 60 * 60); |
2533 | | |
2534 | | let p = Parser::new("-12:34:56"); |
2535 | | assert_eq!( |
2536 | | p.parse_posix_offset().unwrap().second, |
2537 | | 12 * 60 * 60 + 34 * 60 + 56, |
2538 | | ); |
2539 | | |
2540 | | let p = Parser::new("a"); |
2541 | | assert!(p.parse_posix_offset().is_err()); |
2542 | | |
2543 | | let p = Parser::new("-"); |
2544 | | assert!(p.parse_posix_offset().is_err()); |
2545 | | |
2546 | | let p = Parser::new("+"); |
2547 | | assert!(p.parse_posix_offset().is_err()); |
2548 | | |
2549 | | let p = Parser::new("-a"); |
2550 | | assert!(p.parse_posix_offset().is_err()); |
2551 | | |
2552 | | let p = Parser::new("+a"); |
2553 | | assert!(p.parse_posix_offset().is_err()); |
2554 | | |
2555 | | let p = Parser::new("-25"); |
2556 | | assert!(p.parse_posix_offset().is_err()); |
2557 | | |
2558 | | let p = Parser::new("+25"); |
2559 | | assert!(p.parse_posix_offset().is_err()); |
2560 | | |
2561 | | // This checks that we don't accidentally permit IANA rules for |
2562 | | // offset parsing. Namely, the IANA tzfile v3+ extension only applies |
2563 | | // to transition times. But since POSIX says that the "time" for the |
2564 | | // offset and transition is the same format, it would be an easy |
2565 | | // implementation mistake to implement the more flexible rule for |
2566 | | // IANA and have it accidentally also apply to the offset. So we check |
2567 | | // that it doesn't here. |
2568 | | let p = Parser { ianav3plus: true, ..Parser::new("25") }; |
2569 | | assert!(p.parse_posix_offset().is_err()); |
2570 | | let p = Parser { ianav3plus: true, ..Parser::new("+25") }; |
2571 | | assert!(p.parse_posix_offset().is_err()); |
2572 | | let p = Parser { ianav3plus: true, ..Parser::new("-25") }; |
2573 | | assert!(p.parse_posix_offset().is_err()); |
2574 | | } |
2575 | | |
2576 | | #[test] |
2577 | | fn parse_rule() { |
2578 | | let p = Parser::new("M9.5.0,M4.1.0/3"); |
2579 | | assert_eq!( |
2580 | | p.parse_rule().unwrap(), |
2581 | | PosixRule { |
2582 | | start: PosixDayTime { |
2583 | | date: PosixDay::WeekdayOfMonth { |
2584 | | month: 9, |
2585 | | week: 5, |
2586 | | weekday: 0, |
2587 | | }, |
2588 | | time: PosixTime { second: 2 * 60 * 60 }, |
2589 | | }, |
2590 | | end: PosixDayTime { |
2591 | | date: PosixDay::WeekdayOfMonth { |
2592 | | month: 4, |
2593 | | week: 1, |
2594 | | weekday: 0, |
2595 | | }, |
2596 | | time: PosixTime { second: 3 * 60 * 60 }, |
2597 | | }, |
2598 | | }, |
2599 | | ); |
2600 | | |
2601 | | let p = Parser::new("M9.5.0"); |
2602 | | assert!(p.parse_rule().is_err()); |
2603 | | |
2604 | | let p = Parser::new(",M9.5.0,M4.1.0/3"); |
2605 | | assert!(p.parse_rule().is_err()); |
2606 | | |
2607 | | let p = Parser::new("M9.5.0/"); |
2608 | | assert!(p.parse_rule().is_err()); |
2609 | | |
2610 | | let p = Parser::new("M9.5.0,M4.1.0/"); |
2611 | | assert!(p.parse_rule().is_err()); |
2612 | | } |
2613 | | |
2614 | | #[test] |
2615 | | fn parse_posix_datetime() { |
2616 | | let p = Parser::new("J1"); |
2617 | | assert_eq!( |
2618 | | p.parse_posix_datetime().unwrap(), |
2619 | | PosixDayTime { |
2620 | | date: PosixDay::JulianOne(1), |
2621 | | time: PosixTime { second: 2 * 60 * 60 } |
2622 | | }, |
2623 | | ); |
2624 | | |
2625 | | let p = Parser::new("J1/3"); |
2626 | | assert_eq!( |
2627 | | p.parse_posix_datetime().unwrap(), |
2628 | | PosixDayTime { |
2629 | | date: PosixDay::JulianOne(1), |
2630 | | time: PosixTime { second: 3 * 60 * 60 } |
2631 | | }, |
2632 | | ); |
2633 | | |
2634 | | let p = Parser::new("M4.1.0/3"); |
2635 | | assert_eq!( |
2636 | | p.parse_posix_datetime().unwrap(), |
2637 | | PosixDayTime { |
2638 | | date: PosixDay::WeekdayOfMonth { |
2639 | | month: 4, |
2640 | | week: 1, |
2641 | | weekday: 0, |
2642 | | }, |
2643 | | time: PosixTime { second: 3 * 60 * 60 }, |
2644 | | }, |
2645 | | ); |
2646 | | |
2647 | | let p = Parser::new("1/3:45:05"); |
2648 | | assert_eq!( |
2649 | | p.parse_posix_datetime().unwrap(), |
2650 | | PosixDayTime { |
2651 | | date: PosixDay::JulianZero(1), |
2652 | | time: PosixTime { second: 3 * 60 * 60 + 45 * 60 + 5 }, |
2653 | | }, |
2654 | | ); |
2655 | | |
2656 | | let p = Parser::new("a"); |
2657 | | assert!(p.parse_posix_datetime().is_err()); |
2658 | | |
2659 | | let p = Parser::new("J1/"); |
2660 | | assert!(p.parse_posix_datetime().is_err()); |
2661 | | |
2662 | | let p = Parser::new("1/"); |
2663 | | assert!(p.parse_posix_datetime().is_err()); |
2664 | | |
2665 | | let p = Parser::new("M4.1.0/"); |
2666 | | assert!(p.parse_posix_datetime().is_err()); |
2667 | | } |
2668 | | |
2669 | | #[test] |
2670 | | fn parse_posix_date() { |
2671 | | let p = Parser::new("J1"); |
2672 | | assert_eq!(p.parse_posix_date().unwrap(), PosixDay::JulianOne(1)); |
2673 | | let p = Parser::new("J365"); |
2674 | | assert_eq!(p.parse_posix_date().unwrap(), PosixDay::JulianOne(365)); |
2675 | | |
2676 | | let p = Parser::new("0"); |
2677 | | assert_eq!(p.parse_posix_date().unwrap(), PosixDay::JulianZero(0)); |
2678 | | let p = Parser::new("1"); |
2679 | | assert_eq!(p.parse_posix_date().unwrap(), PosixDay::JulianZero(1)); |
2680 | | let p = Parser::new("365"); |
2681 | | assert_eq!(p.parse_posix_date().unwrap(), PosixDay::JulianZero(365)); |
2682 | | |
2683 | | let p = Parser::new("M9.5.0"); |
2684 | | assert_eq!( |
2685 | | p.parse_posix_date().unwrap(), |
2686 | | PosixDay::WeekdayOfMonth { month: 9, week: 5, weekday: 0 }, |
2687 | | ); |
2688 | | let p = Parser::new("M9.5.6"); |
2689 | | assert_eq!( |
2690 | | p.parse_posix_date().unwrap(), |
2691 | | PosixDay::WeekdayOfMonth { month: 9, week: 5, weekday: 6 }, |
2692 | | ); |
2693 | | let p = Parser::new("M09.5.6"); |
2694 | | assert_eq!( |
2695 | | p.parse_posix_date().unwrap(), |
2696 | | PosixDay::WeekdayOfMonth { month: 9, week: 5, weekday: 6 }, |
2697 | | ); |
2698 | | let p = Parser::new("M12.1.1"); |
2699 | | assert_eq!( |
2700 | | p.parse_posix_date().unwrap(), |
2701 | | PosixDay::WeekdayOfMonth { month: 12, week: 1, weekday: 1 }, |
2702 | | ); |
2703 | | |
2704 | | let p = Parser::new("a"); |
2705 | | assert!(p.parse_posix_date().is_err()); |
2706 | | |
2707 | | let p = Parser::new("j"); |
2708 | | assert!(p.parse_posix_date().is_err()); |
2709 | | |
2710 | | let p = Parser::new("m"); |
2711 | | assert!(p.parse_posix_date().is_err()); |
2712 | | |
2713 | | let p = Parser::new("n"); |
2714 | | assert!(p.parse_posix_date().is_err()); |
2715 | | |
2716 | | let p = Parser::new("J366"); |
2717 | | assert!(p.parse_posix_date().is_err()); |
2718 | | |
2719 | | let p = Parser::new("366"); |
2720 | | assert!(p.parse_posix_date().is_err()); |
2721 | | } |
2722 | | |
2723 | | #[test] |
2724 | | fn parse_posix_julian_day_no_leap() { |
2725 | | let p = Parser::new("1"); |
2726 | | assert_eq!(p.parse_posix_julian_day_no_leap().unwrap(), 1); |
2727 | | |
2728 | | let p = Parser::new("001"); |
2729 | | assert_eq!(p.parse_posix_julian_day_no_leap().unwrap(), 1); |
2730 | | |
2731 | | let p = Parser::new("365"); |
2732 | | assert_eq!(p.parse_posix_julian_day_no_leap().unwrap(), 365); |
2733 | | |
2734 | | let p = Parser::new("3655"); |
2735 | | assert_eq!(p.parse_posix_julian_day_no_leap().unwrap(), 365); |
2736 | | |
2737 | | let p = Parser::new("0"); |
2738 | | assert!(p.parse_posix_julian_day_no_leap().is_err()); |
2739 | | |
2740 | | let p = Parser::new("366"); |
2741 | | assert!(p.parse_posix_julian_day_no_leap().is_err()); |
2742 | | } |
2743 | | |
2744 | | #[test] |
2745 | | fn parse_posix_julian_day_with_leap() { |
2746 | | let p = Parser::new("0"); |
2747 | | assert_eq!(p.parse_posix_julian_day_with_leap().unwrap(), 0); |
2748 | | |
2749 | | let p = Parser::new("1"); |
2750 | | assert_eq!(p.parse_posix_julian_day_with_leap().unwrap(), 1); |
2751 | | |
2752 | | let p = Parser::new("001"); |
2753 | | assert_eq!(p.parse_posix_julian_day_with_leap().unwrap(), 1); |
2754 | | |
2755 | | let p = Parser::new("365"); |
2756 | | assert_eq!(p.parse_posix_julian_day_with_leap().unwrap(), 365); |
2757 | | |
2758 | | let p = Parser::new("3655"); |
2759 | | assert_eq!(p.parse_posix_julian_day_with_leap().unwrap(), 365); |
2760 | | |
2761 | | let p = Parser::new("366"); |
2762 | | assert!(p.parse_posix_julian_day_with_leap().is_err()); |
2763 | | } |
2764 | | |
2765 | | #[test] |
2766 | | fn parse_weekday_of_month() { |
2767 | | let p = Parser::new("9.5.0"); |
2768 | | assert_eq!(p.parse_weekday_of_month().unwrap(), (9, 5, 0)); |
2769 | | |
2770 | | let p = Parser::new("9.1.6"); |
2771 | | assert_eq!(p.parse_weekday_of_month().unwrap(), (9, 1, 6)); |
2772 | | |
2773 | | let p = Parser::new("09.1.6"); |
2774 | | assert_eq!(p.parse_weekday_of_month().unwrap(), (9, 1, 6)); |
2775 | | |
2776 | | let p = Parser::new("9"); |
2777 | | assert!(p.parse_weekday_of_month().is_err()); |
2778 | | |
2779 | | let p = Parser::new("9."); |
2780 | | assert!(p.parse_weekday_of_month().is_err()); |
2781 | | |
2782 | | let p = Parser::new("9.5"); |
2783 | | assert!(p.parse_weekday_of_month().is_err()); |
2784 | | |
2785 | | let p = Parser::new("9.5."); |
2786 | | assert!(p.parse_weekday_of_month().is_err()); |
2787 | | |
2788 | | let p = Parser::new("0.5.0"); |
2789 | | assert!(p.parse_weekday_of_month().is_err()); |
2790 | | |
2791 | | let p = Parser::new("13.5.0"); |
2792 | | assert!(p.parse_weekday_of_month().is_err()); |
2793 | | |
2794 | | let p = Parser::new("9.0.0"); |
2795 | | assert!(p.parse_weekday_of_month().is_err()); |
2796 | | |
2797 | | let p = Parser::new("9.6.0"); |
2798 | | assert!(p.parse_weekday_of_month().is_err()); |
2799 | | |
2800 | | let p = Parser::new("9.5.7"); |
2801 | | assert!(p.parse_weekday_of_month().is_err()); |
2802 | | } |
2803 | | |
2804 | | #[test] |
2805 | | fn parse_posix_time() { |
2806 | | let p = Parser::new("5"); |
2807 | | assert_eq!(p.parse_posix_time().unwrap().second, 5 * 60 * 60); |
2808 | | |
2809 | | let p = Parser::new("22"); |
2810 | | assert_eq!(p.parse_posix_time().unwrap().second, 22 * 60 * 60); |
2811 | | |
2812 | | let p = Parser::new("02"); |
2813 | | assert_eq!(p.parse_posix_time().unwrap().second, 2 * 60 * 60); |
2814 | | |
2815 | | let p = Parser::new("5:45"); |
2816 | | assert_eq!( |
2817 | | p.parse_posix_time().unwrap().second, |
2818 | | 5 * 60 * 60 + 45 * 60 |
2819 | | ); |
2820 | | |
2821 | | let p = Parser::new("5:45:12"); |
2822 | | assert_eq!( |
2823 | | p.parse_posix_time().unwrap().second, |
2824 | | 5 * 60 * 60 + 45 * 60 + 12 |
2825 | | ); |
2826 | | |
2827 | | let p = Parser::new("5:45:129"); |
2828 | | assert_eq!( |
2829 | | p.parse_posix_time().unwrap().second, |
2830 | | 5 * 60 * 60 + 45 * 60 + 12 |
2831 | | ); |
2832 | | |
2833 | | let p = Parser::new("5:45:12:"); |
2834 | | assert_eq!( |
2835 | | p.parse_posix_time().unwrap().second, |
2836 | | 5 * 60 * 60 + 45 * 60 + 12 |
2837 | | ); |
2838 | | |
2839 | | let p = Parser { ianav3plus: true, ..Parser::new("+5:45:12") }; |
2840 | | assert_eq!( |
2841 | | p.parse_posix_time().unwrap().second, |
2842 | | 5 * 60 * 60 + 45 * 60 + 12 |
2843 | | ); |
2844 | | |
2845 | | let p = Parser { ianav3plus: true, ..Parser::new("-5:45:12") }; |
2846 | | assert_eq!( |
2847 | | p.parse_posix_time().unwrap().second, |
2848 | | -(5 * 60 * 60 + 45 * 60 + 12) |
2849 | | ); |
2850 | | |
2851 | | let p = Parser { ianav3plus: true, ..Parser::new("-167:45:12") }; |
2852 | | assert_eq!( |
2853 | | p.parse_posix_time().unwrap().second, |
2854 | | -(167 * 60 * 60 + 45 * 60 + 12), |
2855 | | ); |
2856 | | |
2857 | | let p = Parser::new("25"); |
2858 | | assert!(p.parse_posix_time().is_err()); |
2859 | | |
2860 | | let p = Parser::new("12:2"); |
2861 | | assert!(p.parse_posix_time().is_err()); |
2862 | | |
2863 | | let p = Parser::new("12:"); |
2864 | | assert!(p.parse_posix_time().is_err()); |
2865 | | |
2866 | | let p = Parser::new("12:23:5"); |
2867 | | assert!(p.parse_posix_time().is_err()); |
2868 | | |
2869 | | let p = Parser::new("12:23:"); |
2870 | | assert!(p.parse_posix_time().is_err()); |
2871 | | |
2872 | | let p = Parser { ianav3plus: true, ..Parser::new("168") }; |
2873 | | assert!(p.parse_posix_time().is_err()); |
2874 | | |
2875 | | let p = Parser { ianav3plus: true, ..Parser::new("-168") }; |
2876 | | assert!(p.parse_posix_time().is_err()); |
2877 | | |
2878 | | let p = Parser { ianav3plus: true, ..Parser::new("+168") }; |
2879 | | assert!(p.parse_posix_time().is_err()); |
2880 | | } |
2881 | | |
2882 | | #[test] |
2883 | | fn parse_month() { |
2884 | | let p = Parser::new("1"); |
2885 | | assert_eq!(p.parse_month().unwrap(), 1); |
2886 | | |
2887 | | // Should this be allowed? POSIX spec is unclear. |
2888 | | // We allow it because our parse does stop at 2 |
2889 | | // digits, so this seems harmless. Namely, '001' |
2890 | | // results in an error. |
2891 | | let p = Parser::new("01"); |
2892 | | assert_eq!(p.parse_month().unwrap(), 1); |
2893 | | |
2894 | | let p = Parser::new("12"); |
2895 | | assert_eq!(p.parse_month().unwrap(), 12); |
2896 | | |
2897 | | let p = Parser::new("0"); |
2898 | | assert!(p.parse_month().is_err()); |
2899 | | |
2900 | | let p = Parser::new("00"); |
2901 | | assert!(p.parse_month().is_err()); |
2902 | | |
2903 | | let p = Parser::new("001"); |
2904 | | assert!(p.parse_month().is_err()); |
2905 | | |
2906 | | let p = Parser::new("13"); |
2907 | | assert!(p.parse_month().is_err()); |
2908 | | } |
2909 | | |
2910 | | #[test] |
2911 | | fn parse_week() { |
2912 | | let p = Parser::new("1"); |
2913 | | assert_eq!(p.parse_week().unwrap(), 1); |
2914 | | |
2915 | | let p = Parser::new("5"); |
2916 | | assert_eq!(p.parse_week().unwrap(), 5); |
2917 | | |
2918 | | let p = Parser::new("55"); |
2919 | | assert_eq!(p.parse_week().unwrap(), 5); |
2920 | | |
2921 | | let p = Parser::new("0"); |
2922 | | assert!(p.parse_week().is_err()); |
2923 | | |
2924 | | let p = Parser::new("6"); |
2925 | | assert!(p.parse_week().is_err()); |
2926 | | |
2927 | | let p = Parser::new("00"); |
2928 | | assert!(p.parse_week().is_err()); |
2929 | | |
2930 | | let p = Parser::new("01"); |
2931 | | assert!(p.parse_week().is_err()); |
2932 | | |
2933 | | let p = Parser::new("05"); |
2934 | | assert!(p.parse_week().is_err()); |
2935 | | } |
2936 | | |
2937 | | #[test] |
2938 | | fn parse_weekday() { |
2939 | | let p = Parser::new("0"); |
2940 | | assert_eq!(p.parse_weekday().unwrap(), 0); |
2941 | | |
2942 | | let p = Parser::new("1"); |
2943 | | assert_eq!(p.parse_weekday().unwrap(), 1); |
2944 | | |
2945 | | let p = Parser::new("6"); |
2946 | | assert_eq!(p.parse_weekday().unwrap(), 6); |
2947 | | |
2948 | | let p = Parser::new("00"); |
2949 | | assert_eq!(p.parse_weekday().unwrap(), 0); |
2950 | | |
2951 | | let p = Parser::new("06"); |
2952 | | assert_eq!(p.parse_weekday().unwrap(), 0); |
2953 | | |
2954 | | let p = Parser::new("60"); |
2955 | | assert_eq!(p.parse_weekday().unwrap(), 6); |
2956 | | |
2957 | | let p = Parser::new("7"); |
2958 | | assert!(p.parse_weekday().is_err()); |
2959 | | } |
2960 | | |
2961 | | #[test] |
2962 | | fn parse_hour_posix() { |
2963 | | let p = Parser::new("5"); |
2964 | | assert_eq!(p.parse_hour_posix().unwrap(), 5); |
2965 | | |
2966 | | let p = Parser::new("0"); |
2967 | | assert_eq!(p.parse_hour_posix().unwrap(), 0); |
2968 | | |
2969 | | let p = Parser::new("00"); |
2970 | | assert_eq!(p.parse_hour_posix().unwrap(), 0); |
2971 | | |
2972 | | let p = Parser::new("24"); |
2973 | | assert_eq!(p.parse_hour_posix().unwrap(), 24); |
2974 | | |
2975 | | let p = Parser::new("100"); |
2976 | | assert_eq!(p.parse_hour_posix().unwrap(), 10); |
2977 | | |
2978 | | let p = Parser::new("25"); |
2979 | | assert!(p.parse_hour_posix().is_err()); |
2980 | | |
2981 | | let p = Parser::new("99"); |
2982 | | assert!(p.parse_hour_posix().is_err()); |
2983 | | } |
2984 | | |
2985 | | #[test] |
2986 | | fn parse_hour_ianav3plus() { |
2987 | | let new = |input| Parser { ianav3plus: true, ..Parser::new(input) }; |
2988 | | |
2989 | | let p = new("5"); |
2990 | | assert_eq!(p.parse_hour_ianav3plus().unwrap(), 5); |
2991 | | |
2992 | | let p = new("0"); |
2993 | | assert_eq!(p.parse_hour_ianav3plus().unwrap(), 0); |
2994 | | |
2995 | | let p = new("00"); |
2996 | | assert_eq!(p.parse_hour_ianav3plus().unwrap(), 0); |
2997 | | |
2998 | | let p = new("000"); |
2999 | | assert_eq!(p.parse_hour_ianav3plus().unwrap(), 0); |
3000 | | |
3001 | | let p = new("24"); |
3002 | | assert_eq!(p.parse_hour_ianav3plus().unwrap(), 24); |
3003 | | |
3004 | | let p = new("100"); |
3005 | | assert_eq!(p.parse_hour_ianav3plus().unwrap(), 100); |
3006 | | |
3007 | | let p = new("1000"); |
3008 | | assert_eq!(p.parse_hour_ianav3plus().unwrap(), 100); |
3009 | | |
3010 | | let p = new("167"); |
3011 | | assert_eq!(p.parse_hour_ianav3plus().unwrap(), 167); |
3012 | | |
3013 | | let p = new("168"); |
3014 | | assert!(p.parse_hour_ianav3plus().is_err()); |
3015 | | |
3016 | | let p = new("999"); |
3017 | | assert!(p.parse_hour_ianav3plus().is_err()); |
3018 | | } |
3019 | | |
3020 | | #[test] |
3021 | | fn parse_minute() { |
3022 | | let p = Parser::new("00"); |
3023 | | assert_eq!(p.parse_minute().unwrap(), 0); |
3024 | | |
3025 | | let p = Parser::new("24"); |
3026 | | assert_eq!(p.parse_minute().unwrap(), 24); |
3027 | | |
3028 | | let p = Parser::new("59"); |
3029 | | assert_eq!(p.parse_minute().unwrap(), 59); |
3030 | | |
3031 | | let p = Parser::new("599"); |
3032 | | assert_eq!(p.parse_minute().unwrap(), 59); |
3033 | | |
3034 | | let p = Parser::new("0"); |
3035 | | assert!(p.parse_minute().is_err()); |
3036 | | |
3037 | | let p = Parser::new("1"); |
3038 | | assert!(p.parse_minute().is_err()); |
3039 | | |
3040 | | let p = Parser::new("9"); |
3041 | | assert!(p.parse_minute().is_err()); |
3042 | | |
3043 | | let p = Parser::new("60"); |
3044 | | assert!(p.parse_minute().is_err()); |
3045 | | } |
3046 | | |
3047 | | #[test] |
3048 | | fn parse_second() { |
3049 | | let p = Parser::new("00"); |
3050 | | assert_eq!(p.parse_second().unwrap(), 0); |
3051 | | |
3052 | | let p = Parser::new("24"); |
3053 | | assert_eq!(p.parse_second().unwrap(), 24); |
3054 | | |
3055 | | let p = Parser::new("59"); |
3056 | | assert_eq!(p.parse_second().unwrap(), 59); |
3057 | | |
3058 | | let p = Parser::new("599"); |
3059 | | assert_eq!(p.parse_second().unwrap(), 59); |
3060 | | |
3061 | | let p = Parser::new("0"); |
3062 | | assert!(p.parse_second().is_err()); |
3063 | | |
3064 | | let p = Parser::new("1"); |
3065 | | assert!(p.parse_second().is_err()); |
3066 | | |
3067 | | let p = Parser::new("9"); |
3068 | | assert!(p.parse_second().is_err()); |
3069 | | |
3070 | | let p = Parser::new("60"); |
3071 | | assert!(p.parse_second().is_err()); |
3072 | | } |
3073 | | |
3074 | | #[test] |
3075 | | fn parse_number_with_exactly_n_digits() { |
3076 | | let p = Parser::new("1"); |
3077 | | assert_eq!(p.parse_number_with_exactly_n_digits(1).unwrap(), 1); |
3078 | | |
3079 | | let p = Parser::new("12"); |
3080 | | assert_eq!(p.parse_number_with_exactly_n_digits(2).unwrap(), 12); |
3081 | | |
3082 | | let p = Parser::new("123"); |
3083 | | assert_eq!(p.parse_number_with_exactly_n_digits(2).unwrap(), 12); |
3084 | | |
3085 | | let p = Parser::new(""); |
3086 | | assert!(p.parse_number_with_exactly_n_digits(1).is_err()); |
3087 | | |
3088 | | let p = Parser::new("1"); |
3089 | | assert!(p.parse_number_with_exactly_n_digits(2).is_err()); |
3090 | | |
3091 | | let p = Parser::new("12"); |
3092 | | assert!(p.parse_number_with_exactly_n_digits(3).is_err()); |
3093 | | } |
3094 | | |
3095 | | #[test] |
3096 | | fn parse_number_with_upto_n_digits() { |
3097 | | let p = Parser::new("1"); |
3098 | | assert_eq!(p.parse_number_with_upto_n_digits(1).unwrap(), 1); |
3099 | | |
3100 | | let p = Parser::new("1"); |
3101 | | assert_eq!(p.parse_number_with_upto_n_digits(2).unwrap(), 1); |
3102 | | |
3103 | | let p = Parser::new("12"); |
3104 | | assert_eq!(p.parse_number_with_upto_n_digits(2).unwrap(), 12); |
3105 | | |
3106 | | let p = Parser::new("12"); |
3107 | | assert_eq!(p.parse_number_with_upto_n_digits(3).unwrap(), 12); |
3108 | | |
3109 | | let p = Parser::new("123"); |
3110 | | assert_eq!(p.parse_number_with_upto_n_digits(2).unwrap(), 12); |
3111 | | |
3112 | | let p = Parser::new(""); |
3113 | | assert!(p.parse_number_with_upto_n_digits(1).is_err()); |
3114 | | |
3115 | | let p = Parser::new("a"); |
3116 | | assert!(p.parse_number_with_upto_n_digits(1).is_err()); |
3117 | | } |
3118 | | |
3119 | | #[test] |
3120 | | fn to_dst_civil_datetime_utc_range() { |
3121 | | let tz = posix_time_zone("WART4WARST,J1/-3,J365/20"); |
3122 | | let dst_info = DstInfo { |
3123 | | // We test this in other places. It's too annoying to write this |
3124 | | // out here, and I didn't adopt snapshot testing until I had |
3125 | | // written out these tests by hand. ¯\_(ツ)_/¯ |
3126 | | dst: tz.dst.as_ref().unwrap(), |
3127 | | start: date(2024, 1, 1).at(1, 0, 0, 0), |
3128 | | end: date(2024, 12, 31).at(23, 0, 0, 0), |
3129 | | }; |
3130 | | assert_eq!(tz.dst_info_utc(2024), Some(dst_info)); |
3131 | | |
3132 | | let tz = posix_time_zone("WART4WARST,J1/-4,J365/21"); |
3133 | | let dst_info = DstInfo { |
3134 | | dst: tz.dst.as_ref().unwrap(), |
3135 | | start: date(2024, 1, 1).at(0, 0, 0, 0), |
3136 | | end: date(2024, 12, 31).at(23, 59, 59, 999_999_999), |
3137 | | }; |
3138 | | assert_eq!(tz.dst_info_utc(2024), Some(dst_info)); |
3139 | | |
3140 | | let tz = posix_time_zone("EST5EDT,M3.2.0,M11.1.0"); |
3141 | | let dst_info = DstInfo { |
3142 | | dst: tz.dst.as_ref().unwrap(), |
3143 | | start: date(2024, 3, 10).at(7, 0, 0, 0), |
3144 | | end: date(2024, 11, 3).at(6, 0, 0, 0), |
3145 | | }; |
3146 | | assert_eq!(tz.dst_info_utc(2024), Some(dst_info)); |
3147 | | } |
3148 | | |
3149 | | // See: https://github.com/BurntSushi/jiff/issues/386 |
3150 | | #[test] |
3151 | | fn regression_permanent_dst() { |
3152 | | let tz = posix_time_zone("XXX-2<+01>-1,0/0,J365/23"); |
3153 | | let dst_info = DstInfo { |
3154 | | dst: tz.dst.as_ref().unwrap(), |
3155 | | start: date(2087, 1, 1).at(0, 0, 0, 0), |
3156 | | end: date(2087, 12, 31).at(23, 59, 59, 999_999_999), |
3157 | | }; |
3158 | | assert_eq!(tz.dst_info_utc(2087), Some(dst_info)); |
3159 | | } |
3160 | | |
3161 | | #[test] |
3162 | | fn reasonable() { |
3163 | | assert!(PosixTimeZone::parse(b"EST5").is_ok()); |
3164 | | assert!(PosixTimeZone::parse(b"EST5EDT").is_err()); |
3165 | | assert!(PosixTimeZone::parse(b"EST5EDT,J1,J365").is_ok()); |
3166 | | |
3167 | | let tz = posix_time_zone("EST24EDT,J1,J365"); |
3168 | | assert_eq!( |
3169 | | tz, |
3170 | | PosixTimeZone { |
3171 | | std_abbrev: "EST".into(), |
3172 | | std_offset: PosixOffset { second: -24 * 60 * 60 }, |
3173 | | dst: Some(PosixDst { |
3174 | | abbrev: "EDT".into(), |
3175 | | offset: PosixOffset { second: -23 * 60 * 60 }, |
3176 | | rule: PosixRule { |
3177 | | start: PosixDayTime { |
3178 | | date: PosixDay::JulianOne(1), |
3179 | | time: PosixTime::DEFAULT, |
3180 | | }, |
3181 | | end: PosixDayTime { |
3182 | | date: PosixDay::JulianOne(365), |
3183 | | time: PosixTime::DEFAULT, |
3184 | | }, |
3185 | | }, |
3186 | | }), |
3187 | | }, |
3188 | | ); |
3189 | | |
3190 | | let tz = posix_time_zone("EST-24EDT,J1,J365"); |
3191 | | assert_eq!( |
3192 | | tz, |
3193 | | PosixTimeZone { |
3194 | | std_abbrev: "EST".into(), |
3195 | | std_offset: PosixOffset { second: 24 * 60 * 60 }, |
3196 | | dst: Some(PosixDst { |
3197 | | abbrev: "EDT".into(), |
3198 | | offset: PosixOffset { second: 25 * 60 * 60 }, |
3199 | | rule: PosixRule { |
3200 | | start: PosixDayTime { |
3201 | | date: PosixDay::JulianOne(1), |
3202 | | time: PosixTime::DEFAULT, |
3203 | | }, |
3204 | | end: PosixDayTime { |
3205 | | date: PosixDay::JulianOne(365), |
3206 | | time: PosixTime::DEFAULT, |
3207 | | }, |
3208 | | }, |
3209 | | }), |
3210 | | }, |
3211 | | ); |
3212 | | } |
3213 | | |
3214 | | #[test] |
3215 | | fn posix_date_time_spec_to_datetime() { |
3216 | | // For this test, we just keep the offset to zero to simplify things |
3217 | | // a bit. We get coverage for non-zero offsets in higher level tests. |
3218 | | let to_datetime = |daytime: &PosixDayTime, year: i16| { |
3219 | | daytime.to_datetime(year, IOffset::UTC) |
3220 | | }; |
3221 | | |
3222 | | let tz = posix_time_zone("EST5EDT,J1,J365/5:12:34"); |
3223 | | assert_eq!( |
3224 | | to_datetime(&tz.rule().start, 2023), |
3225 | | date(2023, 1, 1).at(2, 0, 0, 0), |
3226 | | ); |
3227 | | assert_eq!( |
3228 | | to_datetime(&tz.rule().end, 2023), |
3229 | | date(2023, 12, 31).at(5, 12, 34, 0), |
3230 | | ); |
3231 | | |
3232 | | let tz = posix_time_zone("EST+5EDT,M3.2.0/2,M11.1.0/2"); |
3233 | | assert_eq!( |
3234 | | to_datetime(&tz.rule().start, 2024), |
3235 | | date(2024, 3, 10).at(2, 0, 0, 0), |
3236 | | ); |
3237 | | assert_eq!( |
3238 | | to_datetime(&tz.rule().end, 2024), |
3239 | | date(2024, 11, 3).at(2, 0, 0, 0), |
3240 | | ); |
3241 | | |
3242 | | let tz = posix_time_zone("EST+5EDT,M1.1.1,M12.5.2"); |
3243 | | assert_eq!( |
3244 | | to_datetime(&tz.rule().start, 2024), |
3245 | | date(2024, 1, 1).at(2, 0, 0, 0), |
3246 | | ); |
3247 | | assert_eq!( |
3248 | | to_datetime(&tz.rule().end, 2024), |
3249 | | date(2024, 12, 31).at(2, 0, 0, 0), |
3250 | | ); |
3251 | | |
3252 | | let tz = posix_time_zone("EST5EDT,0/0,J365/25"); |
3253 | | assert_eq!( |
3254 | | to_datetime(&tz.rule().start, 2024), |
3255 | | date(2024, 1, 1).at(0, 0, 0, 0), |
3256 | | ); |
3257 | | assert_eq!( |
3258 | | to_datetime(&tz.rule().end, 2024), |
3259 | | date(2024, 12, 31).at(23, 59, 59, 999_999_999), |
3260 | | ); |
3261 | | |
3262 | | let tz = posix_time_zone("XXX3EDT4,0/0,J365/23"); |
3263 | | assert_eq!( |
3264 | | to_datetime(&tz.rule().start, 2024), |
3265 | | date(2024, 1, 1).at(0, 0, 0, 0), |
3266 | | ); |
3267 | | assert_eq!( |
3268 | | to_datetime(&tz.rule().end, 2024), |
3269 | | date(2024, 12, 31).at(23, 0, 0, 0), |
3270 | | ); |
3271 | | |
3272 | | let tz = posix_time_zone("XXX3EDT4,0/0,365"); |
3273 | | assert_eq!( |
3274 | | to_datetime(&tz.rule().end, 2023), |
3275 | | date(2023, 12, 31).at(23, 59, 59, 999_999_999), |
3276 | | ); |
3277 | | assert_eq!( |
3278 | | to_datetime(&tz.rule().end, 2024), |
3279 | | date(2024, 12, 31).at(2, 0, 0, 0), |
3280 | | ); |
3281 | | |
3282 | | let tz = posix_time_zone("XXX3EDT4,J1/-167:59:59,J365/167:59:59"); |
3283 | | assert_eq!( |
3284 | | to_datetime(&tz.rule().start, 2024), |
3285 | | date(2024, 1, 1).at(0, 0, 0, 0), |
3286 | | ); |
3287 | | assert_eq!( |
3288 | | to_datetime(&tz.rule().end, 2024), |
3289 | | date(2024, 12, 31).at(23, 59, 59, 999_999_999), |
3290 | | ); |
3291 | | } |
3292 | | |
3293 | | #[test] |
3294 | | fn posix_date_time_spec_time() { |
3295 | | let tz = posix_time_zone("EST5EDT,J1,J365/5:12:34"); |
3296 | | assert_eq!(tz.rule().start.time, PosixTime::DEFAULT); |
3297 | | assert_eq!( |
3298 | | tz.rule().end.time, |
3299 | | PosixTime { second: 5 * 60 * 60 + 12 * 60 + 34 }, |
3300 | | ); |
3301 | | } |
3302 | | |
3303 | | #[test] |
3304 | | fn posix_date_spec_to_date() { |
3305 | | let tz = posix_time_zone("EST+5EDT,M3.2.0/2,M11.1.0/2"); |
3306 | | let start = tz.rule().start.date.to_date(2023); |
3307 | | assert_eq!(start, Some(date(2023, 3, 12))); |
3308 | | let end = tz.rule().end.date.to_date(2023); |
3309 | | assert_eq!(end, Some(date(2023, 11, 5))); |
3310 | | let start = tz.rule().start.date.to_date(2024); |
3311 | | assert_eq!(start, Some(date(2024, 3, 10))); |
3312 | | let end = tz.rule().end.date.to_date(2024); |
3313 | | assert_eq!(end, Some(date(2024, 11, 3))); |
3314 | | |
3315 | | let tz = posix_time_zone("EST+5EDT,J60,J365"); |
3316 | | let start = tz.rule().start.date.to_date(2023); |
3317 | | assert_eq!(start, Some(date(2023, 3, 1))); |
3318 | | let end = tz.rule().end.date.to_date(2023); |
3319 | | assert_eq!(end, Some(date(2023, 12, 31))); |
3320 | | let start = tz.rule().start.date.to_date(2024); |
3321 | | assert_eq!(start, Some(date(2024, 3, 1))); |
3322 | | let end = tz.rule().end.date.to_date(2024); |
3323 | | assert_eq!(end, Some(date(2024, 12, 31))); |
3324 | | |
3325 | | let tz = posix_time_zone("EST+5EDT,59,365"); |
3326 | | let start = tz.rule().start.date.to_date(2023); |
3327 | | assert_eq!(start, Some(date(2023, 3, 1))); |
3328 | | let end = tz.rule().end.date.to_date(2023); |
3329 | | assert_eq!(end, None); |
3330 | | let start = tz.rule().start.date.to_date(2024); |
3331 | | assert_eq!(start, Some(date(2024, 2, 29))); |
3332 | | let end = tz.rule().end.date.to_date(2024); |
3333 | | assert_eq!(end, Some(date(2024, 12, 31))); |
3334 | | |
3335 | | let tz = posix_time_zone("EST+5EDT,M1.1.1,M12.5.2"); |
3336 | | let start = tz.rule().start.date.to_date(2024); |
3337 | | assert_eq!(start, Some(date(2024, 1, 1))); |
3338 | | let end = tz.rule().end.date.to_date(2024); |
3339 | | assert_eq!(end, Some(date(2024, 12, 31))); |
3340 | | } |
3341 | | |
3342 | | #[test] |
3343 | | fn posix_time_spec_to_civil_time() { |
3344 | | let tz = posix_time_zone("EST5EDT,J1,J365/5:12:34"); |
3345 | | assert_eq!( |
3346 | | tz.dst.as_ref().unwrap().rule.start.time.second, |
3347 | | 2 * 60 * 60, |
3348 | | ); |
3349 | | assert_eq!( |
3350 | | tz.dst.as_ref().unwrap().rule.end.time.second, |
3351 | | 5 * 60 * 60 + 12 * 60 + 34, |
3352 | | ); |
3353 | | |
3354 | | let tz = posix_time_zone("EST5EDT,J1/23:59:59,J365/24:00:00"); |
3355 | | assert_eq!( |
3356 | | tz.dst.as_ref().unwrap().rule.start.time.second, |
3357 | | 23 * 60 * 60 + 59 * 60 + 59, |
3358 | | ); |
3359 | | assert_eq!( |
3360 | | tz.dst.as_ref().unwrap().rule.end.time.second, |
3361 | | 24 * 60 * 60, |
3362 | | ); |
3363 | | |
3364 | | let tz = posix_time_zone("EST5EDT,J1/-1,J365/167:00:00"); |
3365 | | assert_eq!( |
3366 | | tz.dst.as_ref().unwrap().rule.start.time.second, |
3367 | | -1 * 60 * 60, |
3368 | | ); |
3369 | | assert_eq!( |
3370 | | tz.dst.as_ref().unwrap().rule.end.time.second, |
3371 | | 167 * 60 * 60, |
3372 | | ); |
3373 | | } |
3374 | | |
3375 | | #[test] |
3376 | | fn parse_iana() { |
3377 | | // Ref: https://github.com/chronotope/chrono/issues/1153 |
3378 | | let p = PosixTimeZone::parse(b"CRAZY5SHORT,M12.5.0/50,0/2").unwrap(); |
3379 | | assert_eq!( |
3380 | | p, |
3381 | | PosixTimeZone { |
3382 | | std_abbrev: "CRAZY".into(), |
3383 | | std_offset: PosixOffset { second: -5 * 60 * 60 }, |
3384 | | dst: Some(PosixDst { |
3385 | | abbrev: "SHORT".into(), |
3386 | | offset: PosixOffset { second: -4 * 60 * 60 }, |
3387 | | rule: PosixRule { |
3388 | | start: PosixDayTime { |
3389 | | date: PosixDay::WeekdayOfMonth { |
3390 | | month: 12, |
3391 | | week: 5, |
3392 | | weekday: 0, |
3393 | | }, |
3394 | | time: PosixTime { second: 50 * 60 * 60 }, |
3395 | | }, |
3396 | | end: PosixDayTime { |
3397 | | date: PosixDay::JulianZero(0), |
3398 | | time: PosixTime { second: 2 * 60 * 60 }, |
3399 | | }, |
3400 | | }, |
3401 | | }), |
3402 | | }, |
3403 | | ); |
3404 | | |
3405 | | assert!(PosixTimeZone::parse(b"America/New_York").is_err()); |
3406 | | assert!(PosixTimeZone::parse(b":America/New_York").is_err()); |
3407 | | } |
3408 | | |
3409 | | // See: https://github.com/BurntSushi/jiff/issues/407 |
3410 | | #[test] |
3411 | | fn parse_empty_is_err() { |
3412 | | assert!(PosixTimeZone::parse(b"").is_err()); |
3413 | | } |
3414 | | |
3415 | | // See: https://github.com/BurntSushi/jiff/issues/407 |
3416 | | #[test] |
3417 | | fn parse_weird_is_err() { |
3418 | | let s = |
3419 | | b"AAAAAAAAAAAAAAACAAAAAAAAAAAAQA8AACAAAAAAAAAAAAAAAAACAAAAAAAAAAA"; |
3420 | | assert!(PosixTimeZone::parse(s).is_err()); |
3421 | | |
3422 | | let s = |
3423 | | b"<AAAAAAAAAAAAAAACAAAAAAAAAAAAQA>8<AACAAAAAAAAAAAAAAAAACAAAAAAAAAAA>"; |
3424 | | assert!(PosixTimeZone::parse(s).is_err()); |
3425 | | |
3426 | | let s = b"PPPPPPPPPPPPPPPPPPPPnoofPPPAAA6DaPPPPPPPPPPPPPPPPPPPPPnoofPPPPP,n"; |
3427 | | assert!(PosixTimeZone::parse(s).is_err()); |
3428 | | |
3429 | | let s = b"oooooooooovooooooooooooooooool9<ooooo2o-o-oooooookoorooooooooroo8"; |
3430 | | assert!(PosixTimeZone::parse(s).is_err()); |
3431 | | } |
3432 | | } |