Coverage Report

Created: 2025-11-16 07:09

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