Coverage Report

Created: 2026-01-16 06:52

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/jiff-0.2.18/src/fmt/offset.rs
Line
Count
Source
1
/*!
2
This module provides facilities for parsing time zone offsets.
3
4
The parsing here follows primarily from [RFC 3339] and [ISO 8601], but also
5
from [Temporal's hybrid grammar].
6
7
[RFC 3339]: https://www.rfc-editor.org/rfc/rfc3339
8
[ISO 8601]: https://www.iso.org/iso-8601-date-and-time-format.html
9
[Temporal's hybrid grammar]: https://tc39.es/proposal-temporal/#sec-temporal-iso8601grammar
10
*/
11
12
// Here's the specific part of Temporal's grammar that is implemented below:
13
//
14
// DateTimeUTCOffset :::
15
//   UTCDesignator
16
//   UTCOffsetSubMinutePrecision
17
//
18
// TimeZoneUTCOffsetName :::
19
//   UTCOffsetMinutePrecision
20
//
21
// UTCDesignator ::: one of
22
//   Z z
23
//
24
// UTCOffsetSubMinutePrecision :::
25
//   UTCOffsetMinutePrecision
26
//   UTCOffsetWithSubMinuteComponents[+Extended]
27
//   UTCOffsetWithSubMinuteComponents[~Extended]
28
//
29
// UTCOffsetMinutePrecision :::
30
//   TemporalSign Hour
31
//   TemporalSign Hour TimeSeparator[+Extended] MinuteSecond
32
//   TemporalSign Hour TimeSeparator[~Extended] MinuteSecond
33
//
34
// UTCOffsetWithSubMinuteComponents[Extended] :::
35
//   TemporalSign Hour
36
//     TimeSeparator[?Extended] MinuteSecond
37
//     TimeSeparator[?Extended] MinuteSecond
38
//     TemporalDecimalFraction[opt]
39
//
40
// TimeSeparator[Extended] :::
41
//   [+Extended] :
42
//   [~Extended] [empty]
43
//
44
// TemporalSign :::
45
//   ASCIISign
46
//   <MINUS>
47
//
48
// ASCIISign ::: one of
49
//   + -
50
//
51
// Hour :::
52
//   0 DecimalDigit
53
//   1 DecimalDigit
54
//   20
55
//   21
56
//   22
57
//   23
58
//
59
// MinuteSecond :::
60
//   0 DecimalDigit
61
//   1 DecimalDigit
62
//   2 DecimalDigit
63
//   3 DecimalDigit
64
//   4 DecimalDigit
65
//   5 DecimalDigit
66
//
67
// DecimalDigit :: one of
68
//   0 1 2 3 4 5 6 7 8 9
69
//
70
// TemporalDecimalFraction :::
71
//   TemporalDecimalSeparator DecimalDigit
72
//   TemporalDecimalSeparator DecimalDigit DecimalDigit
73
//   TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit
74
//   TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit
75
//                            DecimalDigit
76
//   TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit
77
//                            DecimalDigit DecimalDigit
78
//   TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit
79
//                            DecimalDigit DecimalDigit DecimalDigit
80
//   TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit
81
//                            DecimalDigit DecimalDigit DecimalDigit
82
//                            DecimalDigit
83
//   TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit
84
//                            DecimalDigit DecimalDigit DecimalDigit
85
//                            DecimalDigit DecimalDigit
86
//   TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit
87
//                            DecimalDigit DecimalDigit DecimalDigit
88
//                            DecimalDigit DecimalDigit DecimalDigit
89
//   TemporalDecimalSeparator ::: one of
90
//   . ,
91
//
92
// The quick summary of the above is that offsets up to nanosecond precision
93
// are supported. The general format is `{+,-}HH[:MM[:SS[.NNNNNNNNN]]]`. But
94
// ISO 8601 extended or basic formats are also supported. For example, the
95
// basic format `-0530` is equivalent to the extended format `-05:30`.
96
//
97
// Note that even though we support parsing up to nanosecond precision, Jiff
98
// currently only supports offsets up to second precision. I don't think there
99
// is any real practical need for any greater precision, but I don't think it
100
// would be too hard to switch an `Offset` from an `i32` representation in
101
// seconds to a `i64` representation in nanoseconds. (Since it only needs to
102
// support a span of time of about 52 hours or so.)
103
104
use crate::{
105
    error::{fmt::offset::Error as E, Error, ErrorContext},
106
    fmt::{
107
        buffer::ArrayBuffer,
108
        temporal::{PiecesNumericOffset, PiecesOffset},
109
        util::parse_temporal_fraction,
110
        Parsed,
111
    },
112
    tz::Offset,
113
    util::{
114
        parse,
115
        rangeint::{ri8, RFrom},
116
        t::{self, C},
117
    },
118
};
119
120
// We define our own ranged types because we want them to only be positive. We
121
// represent the sign explicitly as a separate field. But the range supported
122
// is the same as the component fields of `Offset`.
123
type ParsedOffsetHours = ri8<0, { t::SpanZoneOffsetHours::MAX }>;
124
type ParsedOffsetMinutes = ri8<0, { t::SpanZoneOffsetMinutes::MAX }>;
125
type ParsedOffsetSeconds = ri8<0, { t::SpanZoneOffsetSeconds::MAX }>;
126
127
/// An offset that has been parsed from a datetime string.
128
///
129
/// This represents either a Zulu offset (corresponding to UTC with an unknown
130
/// time zone offset), or a specific numeric offset given in hours, minutes,
131
/// seconds and nanoseconds (with everything except hours being optional).
132
#[derive(Debug)]
133
pub(crate) struct ParsedOffset {
134
    /// The kind of offset parsed.
135
    kind: ParsedOffsetKind,
136
}
137
138
impl ParsedOffset {
139
    /// Convert a parsed offset into a Jiff offset.
140
    ///
141
    /// If the offset was parsed from a Zulu designator, then the offset
142
    /// returned is indistinguishable from `+00` or `-00`.
143
    ///
144
    /// # Errors
145
    ///
146
    /// A variety of parsing errors are possible.
147
    ///
148
    /// Also, beyond normal range checks on the allowed components of a UTC
149
    /// offset, this does rounding based on the fractional nanosecond part. As
150
    /// a result, if the parsed value would be rounded to a value not in bounds
151
    /// for a Jiff offset, this returns an error.
152
761
    pub(crate) fn to_offset(&self) -> Result<Offset, Error> {
153
761
        match self.kind {
154
0
            ParsedOffsetKind::Zulu => Ok(Offset::UTC),
155
761
            ParsedOffsetKind::Numeric(ref numeric) => numeric.to_offset(),
156
        }
157
761
    }
158
159
    /// Convert a parsed offset to a more structured representation.
160
    ///
161
    /// This is like `to_offset`, but preserves `Z` and `-00:00` versus
162
    /// `+00:00`. This does still attempt to create an `Offset`, and that
163
    /// construction can fail.
164
0
    pub(crate) fn to_pieces_offset(&self) -> Result<PiecesOffset, Error> {
165
0
        match self.kind {
166
0
            ParsedOffsetKind::Zulu => Ok(PiecesOffset::Zulu),
167
0
            ParsedOffsetKind::Numeric(ref numeric) => {
168
0
                let mut off = PiecesNumericOffset::from(numeric.to_offset()?);
169
0
                if numeric.sign < C(0) {
170
0
                    off = off.with_negative_zero();
171
0
                }
172
0
                Ok(PiecesOffset::from(off))
173
            }
174
        }
175
0
    }
176
177
    /// Whether this parsed offset corresponds to Zulu time or not.
178
    ///
179
    /// This is useful in error reporting for parsing civil times. Namely, we
180
    /// report an error when parsing a civil time with a Zulu offset since it
181
    /// is almost always the wrong thing to do.
182
0
    pub(crate) fn is_zulu(&self) -> bool {
183
0
        matches!(self.kind, ParsedOffsetKind::Zulu)
184
0
    }
185
186
    /// Whether the parsed offset had an explicit sub-minute component or not.
187
0
    pub(crate) fn has_subminute(&self) -> bool {
188
0
        let ParsedOffsetKind::Numeric(ref numeric) = self.kind else {
189
0
            return false;
190
        };
191
0
        numeric.seconds.is_some()
192
0
    }
193
}
194
195
/// The kind of a parsed offset.
196
#[derive(Debug)]
197
enum ParsedOffsetKind {
198
    /// The zulu offset, corresponding to UTC in a context where the offset for
199
    /// civil time is unknown or unavailable.
200
    Zulu,
201
    /// The specific numeric offset.
202
    Numeric(Numeric),
203
}
204
205
/// A numeric representation of a UTC offset.
206
struct Numeric {
207
    /// The sign that was parsed from the numeric UTC offset. This is always
208
    /// either `1` or `-1`, never `0`.
209
    sign: t::Sign,
210
    /// The hours component. This is non-optional because every UTC offset must
211
    /// have at least hours.
212
    hours: ParsedOffsetHours,
213
    /// The minutes component.
214
    minutes: Option<ParsedOffsetMinutes>,
215
    /// The seconds component. This is only possible when subminute resolution
216
    /// is enabled.
217
    seconds: Option<ParsedOffsetSeconds>,
218
    /// The nanoseconds fractional component. This is only possible when
219
    /// subminute resolution is enabled.
220
    nanoseconds: Option<t::SubsecNanosecond>,
221
}
222
223
impl Numeric {
224
    /// Convert a parsed numeric offset into a Jiff offset.
225
    ///
226
    /// This does rounding based on the fractional nanosecond part. As a
227
    /// result, if the parsed value would be rounded to a value not in bounds
228
    /// for a Jiff offset, this returns an error.
229
761
    fn to_offset(&self) -> Result<Offset, Error> {
230
761
        let mut seconds = t::SpanZoneOffset::rfrom(C(3_600) * self.hours);
231
761
        if let Some(part_minutes) = self.minutes {
232
761
            seconds += C(60) * part_minutes;
233
761
        }
234
761
        if let Some(part_seconds) = self.seconds {
235
134
            seconds += part_seconds;
236
627
        }
237
761
        if let Some(part_nanoseconds) = self.nanoseconds {
238
0
            if part_nanoseconds >= C(500_000_000) {
239
0
                seconds = seconds
240
0
                    .try_checked_add("offset-seconds", C(1))
241
0
                    .context(E::PrecisionLoss)?;
242
0
            }
243
761
        }
244
761
        Ok(Offset::from_seconds_ranged(seconds * self.sign))
245
761
    }
246
}
247
248
// This impl is just used for error messages when converting a `Numeric` to an
249
// `Offset` fails.
250
impl core::fmt::Display for Numeric {
251
0
    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
252
0
        let mut buf = ArrayBuffer::<19>::default();
253
0
        let mut bbuf = buf.as_borrowed();
254
255
0
        bbuf.write_ascii_char(if self.sign == C(-1) { b'-' } else { b'+' });
256
0
        bbuf.write_int_pad2(self.hours.get().unsigned_abs());
257
0
        if let Some(minutes) = self.minutes {
258
0
            bbuf.write_ascii_char(b':');
259
0
            bbuf.write_int_pad2(minutes.get().unsigned_abs());
260
0
        }
261
0
        if let Some(seconds) = self.seconds {
262
0
            if self.minutes.is_none() {
263
0
                bbuf.write_str(":00");
264
0
            }
265
0
            bbuf.write_ascii_char(b':');
266
0
            bbuf.write_int_pad2(seconds.get().unsigned_abs());
267
0
        }
268
0
        if let Some(nanos) = self.nanoseconds {
269
0
            if nanos != C(0) {
270
0
                if self.minutes.is_none() {
271
0
                    bbuf.write_str(":00");
272
0
                }
273
0
                if self.seconds.is_none() {
274
0
                    bbuf.write_str(":00");
275
0
                }
276
0
                bbuf.write_ascii_char(b'.');
277
0
                bbuf.write_fraction(None, nanos.get().unsigned_abs());
278
0
            }
279
0
        }
280
0
        f.write_str(bbuf.filled())
281
0
    }
282
}
283
284
// We give a succinct Debug impl (identical to Display) to make snapshot
285
// testing a bit nicer.
286
impl core::fmt::Debug for Numeric {
287
0
    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
288
0
        core::fmt::Display::fmt(self, f)
289
0
    }
290
}
291
292
/// A parser for UTC offsets.
293
///
294
/// At time of writing, the typical configuration for offset parsing is to
295
/// enable Zulu support and subminute precision. But when parsing zoned
296
/// datetimes, and specifically, offsets within time zone annotations (the RFC
297
/// 9557 extension to RFC 3339), then neither zulu nor subminute support are
298
/// enabled.
299
///
300
/// N.B. I'm not actually totally clear on why zulu/subminute aren't allowed in
301
/// time zone annotations, but that's what Temporal's grammar seems to dictate.
302
/// One might argue that this is what RFCs 3339 and 9557 require, but the
303
/// Temporal grammar is already recognizing a superset anyway.
304
#[derive(Debug)]
305
pub(crate) struct Parser {
306
    zulu: bool,
307
    require_minute: bool,
308
    require_second: bool,
309
    subminute: bool,
310
    subsecond: bool,
311
    colon: Colon,
312
}
313
314
impl Parser {
315
    /// Create a new UTC offset parser with the default configuration.
316
0
    pub(crate) const fn new() -> Parser {
317
0
        Parser {
318
0
            zulu: true,
319
0
            require_minute: false,
320
0
            require_second: false,
321
0
            subminute: true,
322
0
            subsecond: true,
323
0
            colon: Colon::Optional,
324
0
        }
325
0
    }
326
327
    /// When enabled, the `z` and `Z` designators are recognized as a "zulu"
328
    /// indicator for UTC when the civil time offset is unknown or unavailable.
329
    ///
330
    /// When disabled, neither `z` nor `Z` will be recognized and a parser
331
    /// error will occur if one is found.
332
    ///
333
    /// This is enabled by default.
334
0
    pub(crate) const fn zulu(self, yes: bool) -> Parser {
335
0
        Parser { zulu: yes, ..self }
336
0
    }
337
338
    /// When enabled, the minute component of a time zone offset is required.
339
    /// If no minutes are found, then an error is returned.
340
    ///
341
    /// This is disabled by default.
342
0
    pub(crate) const fn require_minute(self, yes: bool) -> Parser {
343
0
        Parser { require_minute: yes, ..self }
344
0
    }
345
346
    /// When enabled, the second component of a time zone offset is required.
347
    /// If no seconds (or minutes) are found, then an error is returned.
348
    ///
349
    /// When `subminute` is disabled, this setting has no effect.
350
    ///
351
    /// This is disabled by default.
352
0
    pub(crate) const fn require_second(self, yes: bool) -> Parser {
353
0
        Parser { require_second: yes, ..self }
354
0
    }
355
356
    /// When enabled, offsets with precision greater than integral minutes
357
    /// are supported. Specifically, when enabled, nanosecond precision is
358
    /// supported.
359
    ///
360
    /// When disabled, offsets must be integral minutes. And the `subsecond`
361
    /// option is ignored.
362
0
    pub(crate) const fn subminute(self, yes: bool) -> Parser {
363
0
        Parser { subminute: yes, ..self }
364
0
    }
365
366
    /// When enabled, offsets with precision greater than integral seconds
367
    /// are supported. Specifically, when enabled, nanosecond precision is
368
    /// supported. Note though that when a fractional second is found, it is
369
    /// used to round to the nearest second. (Jiff's `Offset` type only has
370
    /// second resolution.)
371
    ///
372
    /// When disabled, offsets must be integral seconds (or integrate minutes
373
    /// if the `subminute` option is disabled as well).
374
    ///
375
    /// This is ignored if `subminute` is disabled.
376
0
    pub(crate) const fn subsecond(self, yes: bool) -> Parser {
377
0
        Parser { subsecond: yes, ..self }
378
0
    }
379
380
    /// Sets how to handle parsing of colons in a time zone offset.
381
    ///
382
    /// This is set to `Colon::Optional` by default.
383
0
    pub(crate) const fn colon(self, colon: Colon) -> Parser {
384
0
        Parser { colon, ..self }
385
0
    }
386
387
    /// Parse an offset from the beginning of `input`.
388
    ///
389
    /// If no offset could be found or it was otherwise invalid, then an error
390
    /// is returned.
391
    ///
392
    /// In general, parsing stops when, after all required components are seen,
393
    /// an optional component is not present (either because of the end of the
394
    /// input or because of a character that cannot possibly begin said optional
395
    /// component). This does mean that there are some corner cases where error
396
    /// messages will not be as good as they possibly can be. But there are
397
    /// two exceptions here:
398
    ///
399
    /// 1. When Zulu support is disabled and a `Z` or `z` are found, then an
400
    /// error is returned indicating that `Z` was recognized but specifically
401
    /// not allowed.
402
    /// 2. When subminute precision is disabled and a `:` is found after the
403
    /// minutes component, then an error is returned indicating that the
404
    /// seconds component was recognized but specifically not allowed.
405
    ///
406
    /// Otherwise, for example, if `input` is `-0512:34`, then the `-0512`
407
    /// will be parsed as `-5 hours, 12 minutes` with an offset of `5`.
408
    /// Presumably, whatever higher level parser is invoking this routine will
409
    /// then see an unexpected `:`. But it's likely that a better error message
410
    /// would call out the fact that mixed basic and extended formats (from
411
    /// ISO 8601) aren't allowed, and that the offset needs to be written as
412
    /// either `-05:12:34` or `-051234`. But... these are odd corner cases, so
413
    /// we abide them.
414
1.62k
    pub(crate) fn parse<'i>(
415
1.62k
        &self,
416
1.62k
        mut input: &'i [u8],
417
1.62k
    ) -> Result<Parsed<'i, ParsedOffset>, Error> {
418
1.62k
        if input.is_empty() {
419
0
            return Err(Error::from(E::EndOfInput));
420
1.62k
        }
421
422
1.62k
        if input[0] == b'Z' || input[0] == b'z' {
423
10
            if !self.zulu {
424
10
                return Err(Error::from(E::UnexpectedLetterOffsetNoZulu(
425
10
                    input[0],
426
10
                )));
427
0
            }
428
0
            input = &input[1..];
429
0
            let value = ParsedOffset { kind: ParsedOffsetKind::Zulu };
430
0
            return Ok(Parsed { value, input });
431
1.61k
        }
432
1.61k
        let Parsed { value: numeric, input } = self.parse_numeric(input)?;
433
761
        let value = ParsedOffset { kind: ParsedOffsetKind::Numeric(numeric) };
434
761
        Ok(Parsed { value, input })
435
1.62k
    }
436
437
    /// Like `parse`, but will return `None` if `input` cannot possibly start
438
    /// with an offset.
439
    ///
440
    /// Basically, if `input` is empty, or is not one of `z`, `Z`, `+` or `-`
441
    /// then this returns `None`.
442
    #[cfg_attr(feature = "perf-inline", inline(always))]
443
0
    pub(crate) fn parse_optional<'i>(
444
0
        &self,
445
0
        input: &'i [u8],
446
0
    ) -> Result<Parsed<'i, Option<ParsedOffset>>, Error> {
447
0
        let Some(first) = input.first().copied() else {
448
0
            return Ok(Parsed { value: None, input });
449
        };
450
0
        if !matches!(first, b'z' | b'Z' | b'+' | b'-') {
451
0
            return Ok(Parsed { value: None, input });
452
0
        }
453
0
        let Parsed { value, input } = self.parse(input)?;
454
0
        Ok(Parsed { value: Some(value), input })
455
0
    }
456
457
    /// Parses a numeric offset from the beginning of `input`.
458
    ///
459
    /// The beginning of the input is expected to start with a `+` or a `-`.
460
    /// Any other case (including an empty string) will result in an error.
461
    #[cfg_attr(feature = "perf-inline", inline(always))]
462
1.61k
    fn parse_numeric<'i>(
463
1.61k
        &self,
464
1.61k
        input: &'i [u8],
465
1.61k
    ) -> Result<Parsed<'i, Numeric>, Error> {
466
        // Parse sign component.
467
1.36k
        let Parsed { value: sign, input } =
468
1.61k
            self.parse_sign(input).context(E::InvalidSign)?;
469
470
        // Parse hours component.
471
1.10k
        let Parsed { value: hours, input } =
472
1.36k
            self.parse_hours(input).context(E::InvalidHours)?;
473
1.10k
        let extended = match self.colon {
474
0
            Colon::Optional => input.starts_with(b":"),
475
            Colon::Required => {
476
478
                if !input.is_empty() && !input.starts_with(b":") {
477
136
                    return Err(Error::from(E::NoColonAfterHours));
478
342
                }
479
342
                true
480
            }
481
            Colon::Absent => {
482
623
                if !input.is_empty() && input.starts_with(b":") {
483
4
                    return Err(Error::from(E::ColonAfterHours));
484
619
                }
485
619
                false
486
            }
487
        };
488
489
        // Start building up our numeric offset value.
490
961
        let mut numeric = Numeric {
491
961
            sign,
492
961
            hours,
493
961
            minutes: None,
494
961
            seconds: None,
495
961
            nanoseconds: None,
496
961
        };
497
498
        // Parse optional separator after hours.
499
961
        let Parsed { value: has_minutes, input } = self
500
961
            .parse_separator(input, extended)
501
961
            .context(E::SeparatorAfterHours)?;
502
961
        if !has_minutes {
503
40
            return if self.require_minute
504
0
                || (self.subminute && self.require_second)
505
            {
506
40
                Err(Error::from(E::MissingMinuteAfterHour))
507
            } else {
508
0
                Ok(Parsed { value: numeric, input })
509
            };
510
921
        }
511
512
        // Parse minutes component.
513
840
        let Parsed { value: minutes, input } =
514
921
            self.parse_minutes(input).context(E::InvalidMinutes)?;
515
840
        numeric.minutes = Some(minutes);
516
517
        // If subminute resolution is not supported, then we're done here.
518
840
        if !self.subminute {
519
            // While we generally try to "stop" parsing once we're done
520
            // seeing things we expect, in this case, if we see a colon, it
521
            // almost certainly indicates that someone has tried to provide
522
            // more precision than is supported. So we return an error here.
523
            // If this winds up being problematic, we can make this error
524
            // configurable or remove it altogether (unfortunate).
525
0
            return if input.get(0).map_or(false, |&b| b == b':') {
526
0
                Err(Error::from(E::SubminutePrecisionNotEnabled))
527
            } else {
528
0
                Ok(Parsed { value: numeric, input })
529
            };
530
840
        }
531
532
        // Parse optional separator after minutes.
533
840
        let Parsed { value: has_seconds, input } = self
534
840
            .parse_separator(input, extended)
535
840
            .context(E::SeparatorAfterMinutes)?;
536
840
        if !has_seconds {
537
627
            return if self.require_second {
538
0
                Err(Error::from(E::MissingSecondAfterMinute))
539
            } else {
540
627
                Ok(Parsed { value: numeric, input })
541
            };
542
213
        }
543
544
        // Parse seconds component.
545
136
        let Parsed { value: seconds, input } =
546
213
            self.parse_seconds(input).context(E::InvalidSeconds)?;
547
136
        numeric.seconds = Some(seconds);
548
549
        // If subsecond resolution is not supported, then we're done here.
550
136
        if !self.subsecond {
551
136
            if input.get(0).map_or(false, |&b| b == b'.' || b == b',') {
552
2
                return Err(Error::from(E::SubsecondPrecisionNotEnabled));
553
134
            }
554
134
            return Ok(Parsed { value: numeric, input });
555
0
        }
556
557
        // Parse an optional fractional component.
558
0
        let Parsed { value: nanoseconds, input } =
559
0
            parse_temporal_fraction(input)
560
0
                .context(E::InvalidSecondsFractional)?;
561
        // OK because `parse_temporal_fraction` guarantees `0..=999_999_999`.
562
        numeric.nanoseconds =
563
0
            nanoseconds.map(|n| t::SubsecNanosecond::new(n).unwrap());
564
0
        Ok(Parsed { value: numeric, input })
565
1.61k
    }
566
567
    #[cfg_attr(feature = "perf-inline", inline(always))]
568
1.61k
    fn parse_sign<'i>(
569
1.61k
        &self,
570
1.61k
        input: &'i [u8],
571
1.61k
    ) -> Result<Parsed<'i, t::Sign>, Error> {
572
1.61k
        let sign = input.get(0).copied().ok_or(E::EndOfInputNumeric)?;
573
1.61k
        let sign = if sign == b'+' {
574
573
            t::Sign::N::<1>()
575
1.04k
        } else if sign == b'-' {
576
793
            t::Sign::N::<-1>()
577
        } else {
578
251
            return Err(Error::from(E::InvalidSignPlusOrMinus));
579
        };
580
1.36k
        Ok(Parsed { value: sign, input: &input[1..] })
581
1.61k
    }
582
583
    #[cfg_attr(feature = "perf-inline", inline(always))]
584
1.36k
    fn parse_hours<'i>(
585
1.36k
        &self,
586
1.36k
        input: &'i [u8],
587
1.36k
    ) -> Result<Parsed<'i, ParsedOffsetHours>, Error> {
588
1.34k
        let (hours, input) =
589
1.36k
            parse::split(input, 2).ok_or(E::EndOfInputHour)?;
590
1.34k
        let hours = parse::i64(hours).context(E::ParseHours)?;
591
        // Note that we support a slightly bigger range of offsets than
592
        // Temporal. Temporal seems to support only up to 23 hours, but
593
        // we go up to 25 hours. This is done to support POSIX time zone
594
        // strings, which also require 25 hours (plus the maximal minute/second
595
        // components).
596
1.12k
        let hours = ParsedOffsetHours::try_new("hours", hours)
597
1.12k
            .context(E::RangeHours)?;
598
1.10k
        Ok(Parsed { value: hours, input })
599
1.36k
    }
600
601
    #[cfg_attr(feature = "perf-inline", inline(always))]
602
921
    fn parse_minutes<'i>(
603
921
        &self,
604
921
        input: &'i [u8],
605
921
    ) -> Result<Parsed<'i, ParsedOffsetMinutes>, Error> {
606
912
        let (minutes, input) =
607
921
            parse::split(input, 2).ok_or(E::EndOfInputMinute)?;
608
912
        let minutes = parse::i64(minutes).context(E::ParseMinutes)?;
609
850
        let minutes = ParsedOffsetMinutes::try_new("minutes", minutes)
610
850
            .context(E::RangeMinutes)?;
611
840
        Ok(Parsed { value: minutes, input })
612
921
    }
613
614
    #[cfg_attr(feature = "perf-inline", inline(always))]
615
213
    fn parse_seconds<'i>(
616
213
        &self,
617
213
        input: &'i [u8],
618
213
    ) -> Result<Parsed<'i, ParsedOffsetSeconds>, Error> {
619
209
        let (seconds, input) =
620
213
            parse::split(input, 2).ok_or(E::EndOfInputSecond)?;
621
209
        let seconds = parse::i64(seconds).context(E::ParseSeconds)?;
622
154
        let seconds = ParsedOffsetSeconds::try_new("seconds", seconds)
623
154
            .context(E::RangeSeconds)?;
624
136
        Ok(Parsed { value: seconds, input })
625
213
    }
626
627
    /// Parses a separator between hours/minutes or minutes/seconds. When
628
    /// `true` is returned, we expect to parse the next component. When `false`
629
    /// is returned, then no separator was found and there is no expectation of
630
    /// finding another component.
631
    ///
632
    /// When in extended mode, true is returned if and only if a separator is
633
    /// found.
634
    ///
635
    /// When in basic mode (not extended), then a subsequent component is only
636
    /// expected when `input` begins with two ASCII digits.
637
    #[cfg_attr(feature = "perf-inline", inline(always))]
638
1.80k
    fn parse_separator<'i>(
639
1.80k
        &self,
640
1.80k
        mut input: &'i [u8],
641
1.80k
        extended: bool,
642
1.80k
    ) -> Result<Parsed<'i, bool>, Error> {
643
1.80k
        if !extended {
644
1.20k
            let expected =
645
1.20k
                input.len() >= 2 && input[..2].iter().all(u8::is_ascii_digit);
646
1.20k
            return Ok(Parsed { value: expected, input });
647
599
        }
648
599
        let is_separator = input.get(0).map_or(false, |&b| b == b':');
649
599
        if is_separator {
650
422
            input = &input[1..];
651
422
        }
652
599
        Ok(Parsed { value: is_separator, input })
653
1.80k
    }
654
}
655
656
/// How to handle parsing of colons in a time zone offset.
657
#[derive(Debug)]
658
pub(crate) enum Colon {
659
    /// Colons may be present or not. When present, colons must be used
660
    /// consistently. For example, `+05:3015` and `-0530:15` are not allowed.
661
    Optional,
662
    /// Colons must be present.
663
    Required,
664
    /// Colons must be absent.
665
    Absent,
666
}
667
668
#[cfg(test)]
669
mod tests {
670
    use crate::util::rangeint::RInto;
671
672
    use super::*;
673
674
    #[test]
675
    fn ok_zulu() {
676
        let p = |input| Parser::new().parse(input).unwrap();
677
678
        insta::assert_debug_snapshot!(p(b"Z"), @r###"
679
        Parsed {
680
            value: ParsedOffset {
681
                kind: Zulu,
682
            },
683
            input: "",
684
        }
685
        "###);
686
        insta::assert_debug_snapshot!(p(b"z"), @r###"
687
        Parsed {
688
            value: ParsedOffset {
689
                kind: Zulu,
690
            },
691
            input: "",
692
        }
693
        "###);
694
    }
695
696
    #[test]
697
    fn ok_numeric() {
698
        let p = |input| Parser::new().parse(input).unwrap();
699
700
        insta::assert_debug_snapshot!(p(b"-05"), @r###"
701
        Parsed {
702
            value: ParsedOffset {
703
                kind: Numeric(
704
                    -05,
705
                ),
706
            },
707
            input: "",
708
        }
709
        "###);
710
    }
711
712
    // Successful parse tests where the offset ends at the end of the string.
713
    #[test]
714
    fn ok_numeric_complete() {
715
        let p = |input| Parser::new().parse_numeric(input).unwrap();
716
717
        insta::assert_debug_snapshot!(p(b"-05"), @r###"
718
        Parsed {
719
            value: -05,
720
            input: "",
721
        }
722
        "###);
723
        insta::assert_debug_snapshot!(p(b"+05"), @r###"
724
        Parsed {
725
            value: +05,
726
            input: "",
727
        }
728
        "###);
729
730
        insta::assert_debug_snapshot!(p(b"+25:59"), @r###"
731
        Parsed {
732
            value: +25:59,
733
            input: "",
734
        }
735
        "###);
736
        insta::assert_debug_snapshot!(p(b"+2559"), @r###"
737
        Parsed {
738
            value: +25:59,
739
            input: "",
740
        }
741
        "###);
742
743
        insta::assert_debug_snapshot!(p(b"+25:59:59"), @r###"
744
        Parsed {
745
            value: +25:59:59,
746
            input: "",
747
        }
748
        "###);
749
        insta::assert_debug_snapshot!(p(b"+255959"), @r###"
750
        Parsed {
751
            value: +25:59:59,
752
            input: "",
753
        }
754
        "###);
755
756
        insta::assert_debug_snapshot!(p(b"+25:59:59.999"), @r###"
757
        Parsed {
758
            value: +25:59:59.999,
759
            input: "",
760
        }
761
        "###);
762
        insta::assert_debug_snapshot!(p(b"+25:59:59,999"), @r###"
763
        Parsed {
764
            value: +25:59:59.999,
765
            input: "",
766
        }
767
        "###);
768
        insta::assert_debug_snapshot!(p(b"+255959.999"), @r###"
769
        Parsed {
770
            value: +25:59:59.999,
771
            input: "",
772
        }
773
        "###);
774
        insta::assert_debug_snapshot!(p(b"+255959,999"), @r###"
775
        Parsed {
776
            value: +25:59:59.999,
777
            input: "",
778
        }
779
        "###);
780
781
        insta::assert_debug_snapshot!(p(b"+25:59:59.999999999"), @r###"
782
        Parsed {
783
            value: +25:59:59.999999999,
784
            input: "",
785
        }
786
        "###);
787
    }
788
789
    // Successful parse tests where the offset ends before the end of the
790
    // string.
791
    #[test]
792
    fn ok_numeric_incomplete() {
793
        let p = |input| Parser::new().parse_numeric(input).unwrap();
794
795
        insta::assert_debug_snapshot!(p(b"-05a"), @r###"
796
        Parsed {
797
            value: -05,
798
            input: "a",
799
        }
800
        "###);
801
        insta::assert_debug_snapshot!(p(b"-05:12a"), @r###"
802
        Parsed {
803
            value: -05:12,
804
            input: "a",
805
        }
806
        "###);
807
        insta::assert_debug_snapshot!(p(b"-05:12."), @r###"
808
        Parsed {
809
            value: -05:12,
810
            input: ".",
811
        }
812
        "###);
813
        insta::assert_debug_snapshot!(p(b"-05:12,"), @r###"
814
        Parsed {
815
            value: -05:12,
816
            input: ",",
817
        }
818
        "###);
819
        insta::assert_debug_snapshot!(p(b"-0512a"), @r###"
820
        Parsed {
821
            value: -05:12,
822
            input: "a",
823
        }
824
        "###);
825
        insta::assert_debug_snapshot!(p(b"-0512:"), @r###"
826
        Parsed {
827
            value: -05:12,
828
            input: ":",
829
        }
830
        "###);
831
        insta::assert_debug_snapshot!(p(b"-05:12:34a"), @r###"
832
        Parsed {
833
            value: -05:12:34,
834
            input: "a",
835
        }
836
        "###);
837
        insta::assert_debug_snapshot!(p(b"-05:12:34.9a"), @r###"
838
        Parsed {
839
            value: -05:12:34.9,
840
            input: "a",
841
        }
842
        "###);
843
        insta::assert_debug_snapshot!(p(b"-05:12:34.9."), @r###"
844
        Parsed {
845
            value: -05:12:34.9,
846
            input: ".",
847
        }
848
        "###);
849
        insta::assert_debug_snapshot!(p(b"-05:12:34.9,"), @r###"
850
        Parsed {
851
            value: -05:12:34.9,
852
            input: ",",
853
        }
854
        "###);
855
    }
856
857
    // An empty string is invalid. The parser is written from the perspective
858
    // that if it's called, then the caller expects a numeric UTC offset at
859
    // that position.
860
    #[test]
861
    fn err_numeric_empty() {
862
        insta::assert_snapshot!(
863
            Parser::new().parse_numeric(b"").unwrap_err(),
864
            @"failed to parse sign in UTC numeric offset: expected UTC numeric offset, but found end of input",
865
        );
866
    }
867
868
    // A numeric offset always has to begin with a '+' or a '-'.
869
    #[test]
870
    fn err_numeric_notsign() {
871
        insta::assert_snapshot!(
872
            Parser::new().parse_numeric(b"*").unwrap_err(),
873
            @"failed to parse sign in UTC numeric offset: expected `+` or `-` sign at start of UTC numeric offset",
874
        );
875
    }
876
877
    // The hours component must be at least two bytes.
878
    #[test]
879
    fn err_numeric_hours_too_short() {
880
        insta::assert_snapshot!(
881
            Parser::new().parse_numeric(b"+a").unwrap_err(),
882
            @"failed to parse hours in UTC numeric offset: expected two digit hour after sign, but found end of input",
883
        );
884
    }
885
886
    // The hours component must be at least two ASCII digits.
887
    #[test]
888
    fn err_numeric_hours_invalid_digits() {
889
        insta::assert_snapshot!(
890
            Parser::new().parse_numeric(b"+ab").unwrap_err(),
891
            @"failed to parse hours in UTC numeric offset: failed to parse hours (requires a two digit integer): invalid digit, expected 0-9 but got a",
892
        );
893
    }
894
895
    // The hours component must be in range.
896
    #[test]
897
    fn err_numeric_hours_out_of_range() {
898
        insta::assert_snapshot!(
899
            Parser::new().parse_numeric(b"-26").unwrap_err(),
900
            @"failed to parse hours in UTC numeric offset: hour in time zone offset is out of range: parameter 'hours' with value 26 is not in the required range of 0..=25",
901
        );
902
    }
903
904
    // The minutes component must be at least two bytes.
905
    #[test]
906
    fn err_numeric_minutes_too_short() {
907
        insta::assert_snapshot!(
908
            Parser::new().parse_numeric(b"+05:a").unwrap_err(),
909
            @"failed to parse minutes in UTC numeric offset: expected two digit minute after hours, but found end of input",
910
        );
911
    }
912
913
    // The minutes component must be at least two ASCII digits.
914
    #[test]
915
    fn err_numeric_minutes_invalid_digits() {
916
        insta::assert_snapshot!(
917
            Parser::new().parse_numeric(b"+05:ab").unwrap_err(),
918
            @"failed to parse minutes in UTC numeric offset: failed to parse minutes (requires a two digit integer): invalid digit, expected 0-9 but got a",
919
        );
920
    }
921
922
    // The minutes component must be in range.
923
    #[test]
924
    fn err_numeric_minutes_out_of_range() {
925
        insta::assert_snapshot!(
926
            Parser::new().parse_numeric(b"-05:60").unwrap_err(),
927
            @"failed to parse minutes in UTC numeric offset: minute in time zone offset is out of range: parameter 'minutes' with value 60 is not in the required range of 0..=59",
928
        );
929
    }
930
931
    // The seconds component must be at least two bytes.
932
    #[test]
933
    fn err_numeric_seconds_too_short() {
934
        insta::assert_snapshot!(
935
            Parser::new().parse_numeric(b"+05:30:a").unwrap_err(),
936
            @"failed to parse seconds in UTC numeric offset: expected two digit second after minutes, but found end of input",
937
        );
938
    }
939
940
    // The seconds component must be at least two ASCII digits.
941
    #[test]
942
    fn err_numeric_seconds_invalid_digits() {
943
        insta::assert_snapshot!(
944
            Parser::new().parse_numeric(b"+05:30:ab").unwrap_err(),
945
            @"failed to parse seconds in UTC numeric offset: failed to parse seconds (requires a two digit integer): invalid digit, expected 0-9 but got a",
946
        );
947
    }
948
949
    // The seconds component must be in range.
950
    #[test]
951
    fn err_numeric_seconds_out_of_range() {
952
        insta::assert_snapshot!(
953
            Parser::new().parse_numeric(b"-05:30:60").unwrap_err(),
954
            @"failed to parse seconds in UTC numeric offset: second in time zone offset is out of range: parameter 'seconds' with value 60 is not in the required range of 0..=59",
955
        );
956
    }
957
958
    // The fraction component, if present as indicated by a separator, must be
959
    // non-empty.
960
    #[test]
961
    fn err_numeric_fraction_non_empty() {
962
        insta::assert_snapshot!(
963
            Parser::new().parse_numeric(b"-05:30:44.").unwrap_err(),
964
            @"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal",
965
        );
966
        insta::assert_snapshot!(
967
            Parser::new().parse_numeric(b"-05:30:44,").unwrap_err(),
968
            @"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal",
969
        );
970
971
        // Instead of end-of-string, add invalid digit.
972
        insta::assert_snapshot!(
973
            Parser::new().parse_numeric(b"-05:30:44.a").unwrap_err(),
974
            @"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal",
975
        );
976
        insta::assert_snapshot!(
977
            Parser::new().parse_numeric(b"-05:30:44,a").unwrap_err(),
978
            @"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal",
979
        );
980
981
        // And also test basic format.
982
        insta::assert_snapshot!(
983
            Parser::new().parse_numeric(b"-053044.a").unwrap_err(),
984
            @"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal",
985
        );
986
        insta::assert_snapshot!(
987
            Parser::new().parse_numeric(b"-053044,a").unwrap_err(),
988
            @"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal",
989
        );
990
    }
991
992
    // A special case where it is clear that sub-minute precision has been
993
    // requested, but that it is has been forcefully disabled. This error is
994
    // meant to make what is likely a subtle failure mode more explicit.
995
    #[test]
996
    fn err_numeric_subminute_disabled_but_desired() {
997
        insta::assert_snapshot!(
998
            Parser::new().subminute(false).parse_numeric(b"-05:59:32").unwrap_err(),
999
            @"subminute precision for UTC numeric offset is not enabled in this context (must provide only integral minutes)",
1000
        );
1001
    }
1002
1003
    // Another special case where Zulu parsing has been explicitly disabled,
1004
    // but a Zulu string was found.
1005
    #[test]
1006
    fn err_zulu_disabled_but_desired() {
1007
        insta::assert_snapshot!(
1008
            Parser::new().zulu(false).parse(b"Z").unwrap_err(),
1009
            @"found `Z` where a numeric UTC offset was expected (this context does not permit the Zulu offset)",
1010
        );
1011
        insta::assert_snapshot!(
1012
            Parser::new().zulu(false).parse(b"z").unwrap_err(),
1013
            @"found `z` where a numeric UTC offset was expected (this context does not permit the Zulu offset)",
1014
        );
1015
    }
1016
1017
    // Once a `Numeric` has been parsed, it is almost possible to assume that
1018
    // it can be infallibly converted to an `Offset`. The one case where this
1019
    // isn't true is when there is a fractional nanosecond part along with
1020
    // maximal
1021
    #[test]
1022
    fn err_numeric_too_big_for_offset() {
1023
        let numeric = Numeric {
1024
            sign: t::Sign::MAX_SELF,
1025
            hours: ParsedOffsetHours::MAX_SELF,
1026
            minutes: Some(ParsedOffsetMinutes::MAX_SELF),
1027
            seconds: Some(ParsedOffsetSeconds::MAX_SELF),
1028
            nanoseconds: Some(C(499_999_999).rinto()),
1029
        };
1030
        assert_eq!(numeric.to_offset().unwrap(), Offset::MAX);
1031
1032
        let numeric = Numeric {
1033
            sign: t::Sign::MAX_SELF,
1034
            hours: ParsedOffsetHours::MAX_SELF,
1035
            minutes: Some(ParsedOffsetMinutes::MAX_SELF),
1036
            seconds: Some(ParsedOffsetSeconds::MAX_SELF),
1037
            nanoseconds: Some(C(500_000_000).rinto()),
1038
        };
1039
        insta::assert_snapshot!(
1040
            numeric.to_offset().unwrap_err(),
1041
            @"due to precision loss, offset is rounded to a value that is out of bounds: parameter 'offset-seconds' with value 1 is not in the required range of -93599..=93599",
1042
        );
1043
    }
1044
1045
    // Same as numeric_too_big_for_offset, but at the minimum boundary.
1046
    #[test]
1047
    fn err_numeric_too_small_for_offset() {
1048
        let numeric = Numeric {
1049
            sign: t::Sign::MIN_SELF,
1050
            hours: ParsedOffsetHours::MAX_SELF,
1051
            minutes: Some(ParsedOffsetMinutes::MAX_SELF),
1052
            seconds: Some(ParsedOffsetSeconds::MAX_SELF),
1053
            nanoseconds: Some(C(499_999_999).rinto()),
1054
        };
1055
        assert_eq!(numeric.to_offset().unwrap(), Offset::MIN);
1056
1057
        let numeric = Numeric {
1058
            sign: t::Sign::MIN_SELF,
1059
            hours: ParsedOffsetHours::MAX_SELF,
1060
            minutes: Some(ParsedOffsetMinutes::MAX_SELF),
1061
            seconds: Some(ParsedOffsetSeconds::MAX_SELF),
1062
            nanoseconds: Some(C(500_000_000).rinto()),
1063
        };
1064
        insta::assert_snapshot!(
1065
            numeric.to_offset().unwrap_err(),
1066
            @"due to precision loss, offset is rounded to a value that is out of bounds: parameter 'offset-seconds' with value 1 is not in the required range of -93599..=93599",
1067
        );
1068
    }
1069
}