jiff/tz/
posix.rs

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