Skip to main content

jiff_core/tz/
posix.rs

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