jiff/shared/
posix.rs

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