mirror of
https://github.com/BurntSushi/jiff.git
synced 2025-12-23 08:47:45 +00:00
error: switch everything over to structured errors
Some checks failed
ci / test-default (beta, ubuntu-latest, beta) (push) Has been cancelled
ci / test-default (macos, macos-latest, stable) (push) Has been cancelled
ci / test-default (nightly, ubuntu-latest, nightly) (push) Has been cancelled
ci / test-default (stable, ubuntu-latest, stable) (push) Has been cancelled
ci / test-all (macos, macos-latest, nightly) (push) Has been cancelled
ci / test-all (nightly, ubuntu-latest, nightly) (push) Has been cancelled
ci / test-only-bundle (macos, macos-latest, nightly) (push) Has been cancelled
ci / test-only-bundle (nightly, ubuntu-latest, nightly) (push) Has been cancelled
ci / test-core (macos, macos-latest, nightly) (push) Has been cancelled
ci / test-core (nightly, ubuntu-latest, nightly) (push) Has been cancelled
ci / test-various-feature-combos (macos, macos-latest, nightly) (push) Has been cancelled
ci / test-various-feature-combos (nightly, ubuntu-latest, nightly) (push) Has been cancelled
ci / test-release (push) Has been cancelled
ci / test-doc (push) Has been cancelled
ci / test-bench (push) Has been cancelled
ci / test-fuzz (push) Has been cancelled
ci / test-miri (push) Has been cancelled
ci / win-msvc (push) Has been cancelled
ci / win-gnu (push) Has been cancelled
ci / msrv (push) Has been cancelled
ci / examples (push) Has been cancelled
ci / integrations (push) Has been cancelled
ci / time-zone-init (macos-latest) (push) Has been cancelled
ci / time-zone-init (ubuntu-24.04-arm) (push) Has been cancelled
ci / time-zone-init (ubuntu-latest) (push) Has been cancelled
ci / time-zone-init (windows-latest) (push) Has been cancelled
ci / cross (aarch64-linux-android) (push) Has been cancelled
ci / cross (aarch64-unknown-linux-gnu) (push) Has been cancelled
ci / cross (i686-unknown-linux-gnu) (push) Has been cancelled
ci / cross (powerpc-unknown-linux-gnu) (push) Has been cancelled
ci / cross (powerpc64-unknown-linux-gnu) (push) Has been cancelled
ci / cross (s390x-unknown-linux-gnu) (push) Has been cancelled
ci / cross (x86_64-linux-android) (push) Has been cancelled
ci / riscv32imc-unknown-none-elf (push) Has been cancelled
ci / wasm32-wasip1 (push) Has been cancelled
ci / wasm32-unknown-emscripten (push) Has been cancelled
ci / wasm32-unknown-uknown (push) Has been cancelled
ci / docsrs (push) Has been cancelled
ci / rustfmt (push) Has been cancelled
ci / generated (push) Has been cancelled
Some checks failed
ci / test-default (beta, ubuntu-latest, beta) (push) Has been cancelled
ci / test-default (macos, macos-latest, stable) (push) Has been cancelled
ci / test-default (nightly, ubuntu-latest, nightly) (push) Has been cancelled
ci / test-default (stable, ubuntu-latest, stable) (push) Has been cancelled
ci / test-all (macos, macos-latest, nightly) (push) Has been cancelled
ci / test-all (nightly, ubuntu-latest, nightly) (push) Has been cancelled
ci / test-only-bundle (macos, macos-latest, nightly) (push) Has been cancelled
ci / test-only-bundle (nightly, ubuntu-latest, nightly) (push) Has been cancelled
ci / test-core (macos, macos-latest, nightly) (push) Has been cancelled
ci / test-core (nightly, ubuntu-latest, nightly) (push) Has been cancelled
ci / test-various-feature-combos (macos, macos-latest, nightly) (push) Has been cancelled
ci / test-various-feature-combos (nightly, ubuntu-latest, nightly) (push) Has been cancelled
ci / test-release (push) Has been cancelled
ci / test-doc (push) Has been cancelled
ci / test-bench (push) Has been cancelled
ci / test-fuzz (push) Has been cancelled
ci / test-miri (push) Has been cancelled
ci / win-msvc (push) Has been cancelled
ci / win-gnu (push) Has been cancelled
ci / msrv (push) Has been cancelled
ci / examples (push) Has been cancelled
ci / integrations (push) Has been cancelled
ci / time-zone-init (macos-latest) (push) Has been cancelled
ci / time-zone-init (ubuntu-24.04-arm) (push) Has been cancelled
ci / time-zone-init (ubuntu-latest) (push) Has been cancelled
ci / time-zone-init (windows-latest) (push) Has been cancelled
ci / cross (aarch64-linux-android) (push) Has been cancelled
ci / cross (aarch64-unknown-linux-gnu) (push) Has been cancelled
ci / cross (i686-unknown-linux-gnu) (push) Has been cancelled
ci / cross (powerpc-unknown-linux-gnu) (push) Has been cancelled
ci / cross (powerpc64-unknown-linux-gnu) (push) Has been cancelled
ci / cross (s390x-unknown-linux-gnu) (push) Has been cancelled
ci / cross (x86_64-linux-android) (push) Has been cancelled
ci / riscv32imc-unknown-none-elf (push) Has been cancelled
ci / wasm32-wasip1 (push) Has been cancelled
ci / wasm32-unknown-emscripten (push) Has been cancelled
ci / wasm32-unknown-uknown (push) Has been cancelled
ci / docsrs (push) Has been cancelled
ci / rustfmt (push) Has been cancelled
ci / generated (push) Has been cancelled
This was an incredibly tedious and tortuous refactor. But this removes almost all of the "create ad hoc stringly-typed errors everywhere." This partially makes progress toward #418, but my initial impetus for doing this was to see if I could reduce binary size and improve compilation times. My general target was to see if I could reduce total LLVM lines. I tested this with [Biff] using this command in the root of the Biff repo: ``` cargo llvm-lines --profile release-lto ``` Before this change, Biff had 768,596 LLVM lines. With this change, it has 757,331 lines. So... an improvement, but a very modest one. What about compilation times? This does seem to translate to---also a modest---improvement. For compiling release builds of Biff. Before: ``` $ hyperfine -w1 --prepare 'cargo clean' 'cargo b -r' Benchmark 1: cargo b -r Time (mean ± σ): 7.776 s ± 0.052 s [User: 65.876 s, System: 2.621 s] Range (min … max): 7.690 s … 7.862 s 10 runs ``` After: ``` $ hyperfine -w1 --prepare 'cargo clean' 'cargo b -r' Benchmark 1: cargo b -r Time (mean ± σ): 7.591 s ± 0.067 s [User: 65.686 s, System: 2.564 s] Range (min … max): 7.504 s … 7.689 s 10 runs ``` What about dev builds? Before: ``` $ hyperfine -w1 --prepare 'cargo clean' 'cargo b' Benchmark 1: cargo b Time (mean ± σ): 4.074 s ± 0.022 s [User: 14.493 s, System: 1.818 s] Range (min … max): 4.037 s … 4.099 s 10 runs ``` After: ``` $ hyperfine -w1 --prepare 'cargo clean' 'cargo b' Benchmark 1: cargo b Time (mean ± σ): 4.541 s ± 0.027 s [User: 15.385 s, System: 2.081 s] Range (min … max): 4.503 s … 4.591 s 10 runs ``` Well... that's disappointing. A modest improvement to release builds, but a fairly large regression in dev builds. Maybe it's because of the additional hand-written impls for new structured error types? Bah. And binary size? Normal release builds (not LTO) of Biff that were stripped were 4,431,456 bytes before this change and 4,392,064 after. Hopefully this will unlock other improvements to justify doing this. Note also that this slims down a number of error messages. [Biff]: https://github.com/BurntSushi/biff
This commit is contained in:
parent
3765a52b8d
commit
b8757deba8
79 changed files with 5517 additions and 3858 deletions
|
|
@ -17,6 +17,7 @@ use super::utf8;
|
|||
pub(crate) struct Byte(pub u8);
|
||||
|
||||
impl core::fmt::Display for Byte {
|
||||
#[inline(never)]
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
if self.0 == b' ' {
|
||||
return write!(f, " ");
|
||||
|
|
@ -37,6 +38,7 @@ impl core::fmt::Display for Byte {
|
|||
}
|
||||
|
||||
impl core::fmt::Debug for Byte {
|
||||
#[inline(never)]
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
write!(f, "\"")?;
|
||||
core::fmt::Display::fmt(self, f)?;
|
||||
|
|
@ -54,15 +56,16 @@ impl core::fmt::Debug for Byte {
|
|||
pub(crate) struct Bytes<'a>(pub &'a [u8]);
|
||||
|
||||
impl<'a> core::fmt::Display for Bytes<'a> {
|
||||
#[inline(never)]
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
// This is a sad re-implementation of a similar impl found in bstr.
|
||||
let mut bytes = self.0;
|
||||
while let Some(result) = utf8::decode(bytes) {
|
||||
let ch = match result {
|
||||
Ok(ch) => ch,
|
||||
Err(errant_bytes) => {
|
||||
Err(err) => {
|
||||
// The decode API guarantees `errant_bytes` is non-empty.
|
||||
write!(f, r"\x{:02x}", errant_bytes[0])?;
|
||||
write!(f, r"\x{:02x}", err.as_slice()[0])?;
|
||||
bytes = &bytes[1..];
|
||||
continue;
|
||||
}
|
||||
|
|
@ -81,6 +84,37 @@ impl<'a> core::fmt::Display for Bytes<'a> {
|
|||
}
|
||||
|
||||
impl<'a> core::fmt::Debug for Bytes<'a> {
|
||||
#[inline(never)]
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
write!(f, "\"")?;
|
||||
core::fmt::Display::fmt(self, f)?;
|
||||
write!(f, "\"")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper for repeating a single byte utilizing `Byte`.
|
||||
///
|
||||
/// This is limited to repeating a byte up to `u8::MAX` times in order
|
||||
/// to reduce its size overhead. And in practice, Jiff just doesn't
|
||||
/// need more than this (at time of writing, 2025-11-29).
|
||||
pub(crate) struct RepeatByte {
|
||||
pub(crate) byte: u8,
|
||||
pub(crate) count: u8,
|
||||
}
|
||||
|
||||
impl core::fmt::Display for RepeatByte {
|
||||
#[inline(never)]
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
for _ in 0..self.count {
|
||||
write!(f, "{}", Byte(self.byte))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for RepeatByte {
|
||||
#[inline(never)]
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
write!(f, "\"")?;
|
||||
core::fmt::Display::fmt(self, f)?;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,59 @@
|
|||
// auto-generated by: jiff-cli generate shared
|
||||
|
||||
/// Represents an invalid UTF-8 sequence.
|
||||
///
|
||||
/// This is an error returned by `decode`. It is guaranteed to
|
||||
/// contain 1, 2 or 3 bytes.
|
||||
pub(crate) struct Utf8Error {
|
||||
bytes: [u8; 3],
|
||||
len: u8,
|
||||
}
|
||||
|
||||
impl Utf8Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn new(original_bytes: &[u8], err: core::str::Utf8Error) -> Utf8Error {
|
||||
let len = err.error_len().unwrap_or_else(|| original_bytes.len());
|
||||
// OK because the biggest invalid UTF-8
|
||||
// sequence possible is 3.
|
||||
debug_assert!(1 <= len && len <= 3);
|
||||
let mut bytes = [0; 3];
|
||||
bytes[..len].copy_from_slice(&original_bytes[..len]);
|
||||
Utf8Error {
|
||||
bytes,
|
||||
// OK because the biggest invalid UTF-8
|
||||
// sequence possible is 3.
|
||||
len: u8::try_from(len).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the slice of invalid UTF-8 bytes.
|
||||
///
|
||||
/// The slice returned is guaranteed to have length equivalent
|
||||
/// to `Utf8Error::len`.
|
||||
pub(crate) fn as_slice(&self) -> &[u8] {
|
||||
&self.bytes[..self.len()]
|
||||
}
|
||||
|
||||
/// Returns the length of the invalid UTF-8 sequence found.
|
||||
///
|
||||
/// This is guaranteed to be 1, 2 or 3.
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
usize::from(self.len)
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Utf8Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"found invalid UTF-8 byte {errant_bytes:?} in format \
|
||||
string (format strings must be valid UTF-8)",
|
||||
errant_bytes = crate::shared::util::escape::Bytes(self.as_slice()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Decodes the next UTF-8 encoded codepoint from the given byte slice.
|
||||
///
|
||||
/// If no valid encoding of a codepoint exists at the beginning of the
|
||||
|
|
@ -15,22 +69,20 @@
|
|||
/// *WARNING*: This is not designed for performance. If you're looking for
|
||||
/// a fast UTF-8 decoder, this is not it. If you feel like you need one in
|
||||
/// this crate, then please file an issue and discuss your use case.
|
||||
pub(crate) fn decode(bytes: &[u8]) -> Option<Result<char, &[u8]>> {
|
||||
pub(crate) fn decode(bytes: &[u8]) -> Option<Result<char, Utf8Error>> {
|
||||
if bytes.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let string = match core::str::from_utf8(&bytes[..bytes.len().min(4)]) {
|
||||
Ok(s) => s,
|
||||
Err(ref err) if err.valid_up_to() > 0 => {
|
||||
// OK because we just verified we have at least some
|
||||
// valid UTF-8.
|
||||
core::str::from_utf8(&bytes[..err.valid_up_to()]).unwrap()
|
||||
}
|
||||
// In this case, we want to return 1-3 bytes that make up a prefix of
|
||||
// a potentially valid codepoint.
|
||||
Err(err) => {
|
||||
return Some(Err(
|
||||
&bytes[..err.error_len().unwrap_or_else(|| bytes.len())]
|
||||
))
|
||||
}
|
||||
Err(err) => return Some(Err(Utf8Error::new(bytes, err))),
|
||||
};
|
||||
// OK because we guaranteed above that `string`
|
||||
// must be non-empty. And thus, `str::chars` must
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use core::time::Duration as UnsignedDuration;
|
|||
use crate::{
|
||||
civil::{DateTime, Era, ISOWeekDate, Time, Weekday},
|
||||
duration::{Duration, SDuration},
|
||||
error::{err, Error, ErrorContext},
|
||||
error::{civil::Error as E, Error, ErrorContext},
|
||||
fmt::{
|
||||
self,
|
||||
temporal::{DEFAULT_DATETIME_PARSER, DEFAULT_DATETIME_PRINTER},
|
||||
|
|
@ -1057,7 +1057,7 @@ impl Date {
|
|||
|
||||
let nth = t::SpanWeeks::try_new("nth weekday", nth)?;
|
||||
if nth == C(0) {
|
||||
Err(err!("nth weekday cannot be `0`"))
|
||||
Err(Error::from(E::NthWeekdayNonZero))
|
||||
} else if nth > C(0) {
|
||||
let nth = nth.max(C(1));
|
||||
let weekday_diff = weekday.since_ranged(self.weekday().next());
|
||||
|
|
@ -1515,14 +1515,8 @@ impl Date {
|
|||
-1 => self.yesterday(),
|
||||
1 => self.tomorrow(),
|
||||
days => {
|
||||
let days = UnixEpochDay::try_new("days", days).with_context(
|
||||
|| {
|
||||
err!(
|
||||
"{days} computed from duration {duration:?} \
|
||||
overflows Jiff's datetime limits",
|
||||
)
|
||||
},
|
||||
)?;
|
||||
let days = UnixEpochDay::try_new("days", days)
|
||||
.context(E::OverflowDaysDuration)?;
|
||||
let days =
|
||||
self.to_unix_epoch_day().try_checked_add("days", days)?;
|
||||
Ok(Date::from_unix_epoch_day(days))
|
||||
|
|
@ -2941,11 +2935,9 @@ impl DateDifference {
|
|||
//
|
||||
// NOTE: I take the above back. It's actually possible for the
|
||||
// months component to overflow when largest=month.
|
||||
return Err(err!(
|
||||
"rounding the span between two dates must use days \
|
||||
or bigger for its units, but found {units}",
|
||||
units = largest.plural(),
|
||||
));
|
||||
return Err(Error::from(E::RoundMustUseDaysOrBigger {
|
||||
unit: largest,
|
||||
}));
|
||||
}
|
||||
if largest <= Unit::Week {
|
||||
let mut weeks = t::SpanWeeks::rfrom(C(0));
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use crate::{
|
|||
datetime, Date, DateWith, Era, ISOWeekDate, Time, TimeWith, Weekday,
|
||||
},
|
||||
duration::{Duration, SDuration},
|
||||
error::{err, Error, ErrorContext},
|
||||
error::{civil::Error as E, Error, ErrorContext},
|
||||
fmt::{
|
||||
self,
|
||||
temporal::{self, DEFAULT_DATETIME_PARSER},
|
||||
|
|
@ -1695,25 +1695,18 @@ impl DateTime {
|
|||
{
|
||||
(true, true) => Ok(self),
|
||||
(false, true) => {
|
||||
let new_date =
|
||||
old_date.checked_add(span).with_context(|| {
|
||||
err!("failed to add {span} to {old_date}")
|
||||
})?;
|
||||
let new_date = old_date
|
||||
.checked_add(span)
|
||||
.context(E::FailedAddSpanDate)?;
|
||||
Ok(DateTime::from_parts(new_date, old_time))
|
||||
}
|
||||
(true, false) => {
|
||||
let (new_time, leftovers) =
|
||||
old_time.overflowing_add(span).with_context(|| {
|
||||
err!("failed to add {span} to {old_time}")
|
||||
})?;
|
||||
let new_date =
|
||||
old_date.checked_add(leftovers).with_context(|| {
|
||||
err!(
|
||||
"failed to add overflowing span, {leftovers}, \
|
||||
from adding {span} to {old_time}, \
|
||||
to {old_date}",
|
||||
)
|
||||
})?;
|
||||
let (new_time, leftovers) = old_time
|
||||
.overflowing_add(span)
|
||||
.context(E::FailedAddSpanTime)?;
|
||||
let new_date = old_date
|
||||
.checked_add(leftovers)
|
||||
.context(E::FailedAddSpanOverflowing)?;
|
||||
Ok(DateTime::from_parts(new_date, new_time))
|
||||
}
|
||||
(false, false) => self.checked_add_span_general(&span),
|
||||
|
|
@ -1727,20 +1720,14 @@ impl DateTime {
|
|||
let span_date = span.without_lower(Unit::Day);
|
||||
let span_time = span.only_lower(Unit::Day);
|
||||
|
||||
let (new_time, leftovers) =
|
||||
old_time.overflowing_add(span_time).with_context(|| {
|
||||
err!("failed to add {span_time} to {old_time}")
|
||||
})?;
|
||||
let new_date = old_date.checked_add(span_date).with_context(|| {
|
||||
err!("failed to add {span_date} to {old_date}")
|
||||
})?;
|
||||
let new_date = new_date.checked_add(leftovers).with_context(|| {
|
||||
err!(
|
||||
"failed to add overflowing span, {leftovers}, \
|
||||
from adding {span_time} to {old_time}, \
|
||||
to {new_date}",
|
||||
)
|
||||
})?;
|
||||
let (new_time, leftovers) = old_time
|
||||
.overflowing_add(span_time)
|
||||
.context(E::FailedAddSpanTime)?;
|
||||
let new_date =
|
||||
old_date.checked_add(span_date).context(E::FailedAddSpanDate)?;
|
||||
let new_date = new_date
|
||||
.checked_add(leftovers)
|
||||
.context(E::FailedAddSpanOverflowing)?;
|
||||
Ok(DateTime::from_parts(new_date, new_time))
|
||||
}
|
||||
|
||||
|
|
@ -1751,13 +1738,9 @@ impl DateTime {
|
|||
) -> Result<DateTime, Error> {
|
||||
let (date, time) = (self.date(), self.time());
|
||||
let (new_time, leftovers) = time.overflowing_add_duration(duration)?;
|
||||
let new_date = date.checked_add(leftovers).with_context(|| {
|
||||
err!(
|
||||
"failed to add overflowing signed duration, {leftovers:?}, \
|
||||
from adding {duration:?} to {time},
|
||||
to {date}",
|
||||
)
|
||||
})?;
|
||||
let new_date = date
|
||||
.checked_add(leftovers)
|
||||
.context(E::FailedAddDurationOverflowing)?;
|
||||
Ok(DateTime::from_parts(new_date, new_time))
|
||||
}
|
||||
|
||||
|
|
@ -3552,9 +3535,10 @@ impl DateTimeRound {
|
|||
// it for good reasons.
|
||||
match self.smallest {
|
||||
Unit::Year | Unit::Month | Unit::Week => {
|
||||
return Err(err!(
|
||||
"rounding datetimes does not support {unit}",
|
||||
unit = self.smallest.plural()
|
||||
return Err(Error::from(
|
||||
crate::error::util::RoundingIncrementError::Unsupported {
|
||||
unit: self.smallest,
|
||||
},
|
||||
));
|
||||
}
|
||||
// We don't do any rounding in this case, so just bail now.
|
||||
|
|
@ -3592,9 +3576,7 @@ impl DateTimeRound {
|
|||
// supported datetimes.
|
||||
let end = start
|
||||
.checked_add(Span::new().days_ranged(days_len))
|
||||
.with_context(|| {
|
||||
err!("adding {days_len} days to {start} failed")
|
||||
})?;
|
||||
.context(E::FailedAddDays)?;
|
||||
Ok(DateTime::from_parts(end, time))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
civil::{Date, DateTime, Weekday},
|
||||
error::{err, Error},
|
||||
error::{civil::Error as E, Error},
|
||||
fmt::temporal::{DEFAULT_DATETIME_PARSER, DEFAULT_DATETIME_PRINTER},
|
||||
util::{
|
||||
rangeint::RInto,
|
||||
|
|
@ -711,9 +711,7 @@ impl ISOWeekDate {
|
|||
debug_assert_eq!(t::Year::MIN, ISOYear::MIN);
|
||||
debug_assert_eq!(t::Year::MAX, ISOYear::MAX);
|
||||
if week == C(53) && !is_long_year(year) {
|
||||
return Err(err!(
|
||||
"ISO week number `{week}` is invalid for year `{year}`"
|
||||
));
|
||||
return Err(Error::from(E::InvalidISOWeekNumber));
|
||||
}
|
||||
// And also, the maximum Date constrains what we can utter with
|
||||
// ISOWeekDate so that we can preserve infallible conversions between
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use core::time::Duration as UnsignedDuration;
|
|||
use crate::{
|
||||
civil::{Date, DateTime},
|
||||
duration::{Duration, SDuration},
|
||||
error::{err, Error, ErrorContext},
|
||||
error::{civil::Error as E, Error, ErrorContext},
|
||||
fmt::{
|
||||
self,
|
||||
temporal::{self, DEFAULT_DATETIME_PARSER},
|
||||
|
|
@ -950,7 +950,6 @@ impl Time {
|
|||
self,
|
||||
duration: SignedDuration,
|
||||
) -> Result<Time, Error> {
|
||||
let original = duration;
|
||||
let start = t::NoUnits128::rfrom(self.to_nanosecond());
|
||||
let duration = t::NoUnits128::new_unchecked(duration.as_nanos());
|
||||
// This can never fail because the maximum duration fits into a
|
||||
|
|
@ -958,15 +957,7 @@ impl Time {
|
|||
// integer can never overflow a 128-bit integer.
|
||||
let end = start.try_checked_add("nanoseconds", duration).unwrap();
|
||||
let end = CivilDayNanosecond::try_rfrom("nanoseconds", end)
|
||||
.with_context(|| {
|
||||
err!(
|
||||
"adding signed duration {duration:?}, equal to
|
||||
{nanos} nanoseconds, to {time} overflowed",
|
||||
duration = original,
|
||||
nanos = original.as_nanos(),
|
||||
time = self,
|
||||
)
|
||||
})?;
|
||||
.context(E::OverflowTimeNanoseconds)?;
|
||||
Ok(Time::from_nanosecond(end))
|
||||
}
|
||||
|
||||
|
|
@ -2603,11 +2594,9 @@ impl TimeDifference {
|
|||
}
|
||||
let largest = self.round.get_largest().unwrap_or(Unit::Hour);
|
||||
if largest > Unit::Hour {
|
||||
return Err(err!(
|
||||
"rounding the span between two times must use hours \
|
||||
or smaller for its units, but found {units}",
|
||||
units = largest.plural(),
|
||||
));
|
||||
return Err(Error::from(E::RoundMustUseHoursOrSmaller {
|
||||
unit: largest,
|
||||
}));
|
||||
}
|
||||
let start = t1.to_nanosecond();
|
||||
let end = t2.to_nanosecond();
|
||||
|
|
@ -3012,22 +3001,13 @@ impl TimeWith {
|
|||
None => self.original.subsec_nanosecond_ranged(),
|
||||
Some(subsec_nanosecond) => {
|
||||
if self.millisecond.is_some() {
|
||||
return Err(err!(
|
||||
"cannot set both TimeWith::millisecond \
|
||||
and TimeWith::subsec_nanosecond",
|
||||
));
|
||||
return Err(Error::from(E::IllegalTimeWithMillisecond));
|
||||
}
|
||||
if self.microsecond.is_some() {
|
||||
return Err(err!(
|
||||
"cannot set both TimeWith::microsecond \
|
||||
and TimeWith::subsec_nanosecond",
|
||||
));
|
||||
return Err(Error::from(E::IllegalTimeWithMicrosecond));
|
||||
}
|
||||
if self.nanosecond.is_some() {
|
||||
return Err(err!(
|
||||
"cannot set both TimeWith::nanosecond \
|
||||
and TimeWith::subsec_nanosecond",
|
||||
));
|
||||
return Err(Error::from(E::IllegalTimeWithNanosecond));
|
||||
}
|
||||
SubsecNanosecond::try_new(
|
||||
"subsec_nanosecond",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use core::time::Duration as UnsignedDuration;
|
||||
|
||||
use crate::{
|
||||
error::{err, ErrorContext},
|
||||
error::{duration::Error as E, ErrorContext},
|
||||
Error, SignedDuration, Span,
|
||||
};
|
||||
|
||||
|
|
@ -24,12 +24,8 @@ impl Duration {
|
|||
Duration::Span(span) => Ok(SDuration::Span(span)),
|
||||
Duration::Signed(sdur) => Ok(SDuration::Absolute(sdur)),
|
||||
Duration::Unsigned(udur) => {
|
||||
let sdur =
|
||||
SignedDuration::try_from(udur).with_context(|| {
|
||||
err!(
|
||||
"unsigned duration {udur:?} exceeds Jiff's limits"
|
||||
)
|
||||
})?;
|
||||
let sdur = SignedDuration::try_from(udur)
|
||||
.context(E::RangeUnsignedDuration)?;
|
||||
Ok(SDuration::Absolute(sdur))
|
||||
}
|
||||
}
|
||||
|
|
@ -91,9 +87,8 @@ impl Duration {
|
|||
// Otherwise, this is the only failure point in this entire
|
||||
// routine. And specifically, we fail here in precisely
|
||||
// the cases where `udur.as_secs() > |i64::MIN|`.
|
||||
-SignedDuration::try_from(udur).with_context(|| {
|
||||
err!("failed to negate unsigned duration {udur:?}")
|
||||
})?
|
||||
-SignedDuration::try_from(udur)
|
||||
.context(E::FailedNegateUnsignedDuration)?
|
||||
};
|
||||
Ok(Duration::Signed(sdur))
|
||||
}
|
||||
|
|
|
|||
86
src/error/civil.rs
Normal file
86
src/error/civil.rs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
use crate::{error, Unit};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
FailedAddDays,
|
||||
FailedAddDurationOverflowing,
|
||||
FailedAddSpanDate,
|
||||
FailedAddSpanOverflowing,
|
||||
FailedAddSpanTime,
|
||||
IllegalTimeWithMicrosecond,
|
||||
IllegalTimeWithMillisecond,
|
||||
IllegalTimeWithNanosecond,
|
||||
InvalidISOWeekNumber,
|
||||
OverflowDaysDuration,
|
||||
OverflowTimeNanoseconds,
|
||||
NthWeekdayNonZero,
|
||||
RoundMustUseDaysOrBigger { unit: Unit },
|
||||
RoundMustUseHoursOrSmaller { unit: Unit },
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::Civil(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
FailedAddDays => f.write_str("failed to add days to date"),
|
||||
FailedAddDurationOverflowing => {
|
||||
f.write_str("failed to add overflowing duration")
|
||||
}
|
||||
FailedAddSpanDate => f.write_str("failed to add span to date"),
|
||||
FailedAddSpanOverflowing => {
|
||||
f.write_str("failed to add overflowing span")
|
||||
}
|
||||
FailedAddSpanTime => f.write_str("failed to add span to time"),
|
||||
IllegalTimeWithMicrosecond => f.write_str(
|
||||
"cannot set both `TimeWith::microsecond` \
|
||||
and `TimeWith::subsec_nanosecond`",
|
||||
),
|
||||
IllegalTimeWithMillisecond => f.write_str(
|
||||
"cannot set both `TimeWith::millisecond` \
|
||||
and `TimeWith::subsec_nanosecond`",
|
||||
),
|
||||
IllegalTimeWithNanosecond => f.write_str(
|
||||
"cannot set both `TimeWith::nanosecond` \
|
||||
and `TimeWith::subsec_nanosecond`",
|
||||
),
|
||||
InvalidISOWeekNumber => {
|
||||
f.write_str("ISO week number is invalid for given year")
|
||||
}
|
||||
OverflowDaysDuration => f.write_str(
|
||||
"number of days derived from duration exceed's \
|
||||
Jiff's datetime limits",
|
||||
),
|
||||
OverflowTimeNanoseconds => {
|
||||
f.write_str("adding duration to time overflowed")
|
||||
}
|
||||
NthWeekdayNonZero => f.write_str("nth weekday cannot be `0`"),
|
||||
RoundMustUseDaysOrBigger { unit } => write!(
|
||||
f,
|
||||
"rounding the span between two dates must use days \
|
||||
or bigger for its units, but found {unit}",
|
||||
unit = unit.plural(),
|
||||
),
|
||||
RoundMustUseHoursOrSmaller { unit } => write!(
|
||||
f,
|
||||
"rounding the span between two times must use hours \
|
||||
or smaller for its units, but found {unit}",
|
||||
unit = unit.plural(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/error/duration.rs
Normal file
36
src/error/duration.rs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
use crate::error;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
FailedNegateUnsignedDuration,
|
||||
RangeUnsignedDuration,
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::Duration(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
FailedNegateUnsignedDuration => {
|
||||
f.write_str("failed to negate unsigned duration")
|
||||
}
|
||||
RangeUnsignedDuration => {
|
||||
f.write_str("unsigned duration exceeds Jiff's limits")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
82
src/error/fmt/friendly.rs
Normal file
82
src/error/fmt/friendly.rs
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
use crate::{error, util::escape};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
Empty,
|
||||
ExpectedColonAfterMinute,
|
||||
ExpectedIntegerAfterSign,
|
||||
ExpectedMinuteAfterHour,
|
||||
ExpectedOneMoreUnitAfterComma,
|
||||
ExpectedOneSign,
|
||||
ExpectedSecondAfterMinute,
|
||||
ExpectedUnitSuffix,
|
||||
ExpectedWhitespaceAfterComma { byte: u8 },
|
||||
ExpectedWhitespaceAfterCommaEndOfInput,
|
||||
Failed,
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::FmtFriendly(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
Empty => f.write_str("an empty string is not valid"),
|
||||
ExpectedColonAfterMinute => f.write_str(
|
||||
"when parsing the `HH:MM:SS` format, \
|
||||
expected to parse `:` following minute",
|
||||
),
|
||||
ExpectedIntegerAfterSign => f.write_str(
|
||||
"expected duration to start \
|
||||
with a unit value (a decimal integer) after an \
|
||||
optional sign, but no integer was found",
|
||||
),
|
||||
ExpectedMinuteAfterHour => f.write_str(
|
||||
"when parsing the `HH:MM:SS` format, \
|
||||
expected to parse minute following hour",
|
||||
),
|
||||
ExpectedOneMoreUnitAfterComma => f.write_str(
|
||||
"found comma at the end of duration, \
|
||||
but a comma indicates at least one more \
|
||||
unit follows",
|
||||
),
|
||||
ExpectedOneSign => f.write_str(
|
||||
"expected to find either a prefix sign (+/-) or \
|
||||
a suffix sign (`ago`), but found both",
|
||||
),
|
||||
ExpectedSecondAfterMinute => f.write_str(
|
||||
"when parsing the `HH:MM:SS` format, \
|
||||
expected to parse second following minute",
|
||||
),
|
||||
ExpectedUnitSuffix => f.write_str(
|
||||
"expected to find unit designator suffix \
|
||||
(e.g., `years` or `secs`) after parsing \
|
||||
integer",
|
||||
),
|
||||
ExpectedWhitespaceAfterComma { byte } => write!(
|
||||
f,
|
||||
"expected whitespace after comma, but found `{byte}`",
|
||||
byte = escape::Byte(byte),
|
||||
),
|
||||
ExpectedWhitespaceAfterCommaEndOfInput => f.write_str(
|
||||
"expected whitespace after comma, but found end of input",
|
||||
),
|
||||
Failed => f.write_str(
|
||||
"failed to parse input in the \"friendly\" duration format",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
87
src/error/fmt/mod.rs
Normal file
87
src/error/fmt/mod.rs
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
use crate::{error, util::escape};
|
||||
|
||||
pub(crate) mod friendly;
|
||||
pub(crate) mod offset;
|
||||
pub(crate) mod rfc2822;
|
||||
pub(crate) mod rfc9557;
|
||||
pub(crate) mod strtime;
|
||||
pub(crate) mod temporal;
|
||||
pub(crate) mod util;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
HybridDurationEmpty,
|
||||
HybridDurationPrefix {
|
||||
sign: u8,
|
||||
},
|
||||
IntoFull {
|
||||
#[cfg(feature = "alloc")]
|
||||
value: alloc::boxed::Box<str>,
|
||||
#[cfg(feature = "alloc")]
|
||||
unparsed: alloc::boxed::Box<[u8]>,
|
||||
},
|
||||
StdFmtWriteAdapter,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub(crate) fn into_full_error(
|
||||
_value: &dyn core::fmt::Display,
|
||||
_unparsed: &[u8],
|
||||
) -> Error {
|
||||
Error::IntoFull {
|
||||
#[cfg(feature = "alloc")]
|
||||
value: alloc::string::ToString::to_string(_value).into(),
|
||||
#[cfg(feature = "alloc")]
|
||||
unparsed: _unparsed.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::Fmt(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
HybridDurationEmpty => f.write_str(
|
||||
"an empty string is not a valid duration in either \
|
||||
the ISO 8601 format or Jiff's \"friendly\" format",
|
||||
),
|
||||
HybridDurationPrefix { sign } => write!(
|
||||
f,
|
||||
"found nothing after sign `{sign}`, \
|
||||
which is not a valid duration in either \
|
||||
the ISO 8601 format or Jiff's \"friendly\" format",
|
||||
sign = escape::Byte(sign),
|
||||
),
|
||||
#[cfg(not(feature = "alloc"))]
|
||||
IntoFull { .. } => f.write_str(
|
||||
"parsed value, but unparsed input remains \
|
||||
(expected no unparsed input)",
|
||||
),
|
||||
#[cfg(feature = "alloc")]
|
||||
IntoFull { ref value, ref unparsed } => write!(
|
||||
f,
|
||||
"parsed value '{value}', but unparsed input {unparsed:?} \
|
||||
remains (expected no unparsed input)",
|
||||
unparsed = escape::Bytes(unparsed),
|
||||
),
|
||||
StdFmtWriteAdapter => {
|
||||
f.write_str("an error occurred when formatting an argument")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
153
src/error/fmt/offset.rs
Normal file
153
src/error/fmt/offset.rs
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
use crate::{error, shared::util::escape};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
ColonAfterHours,
|
||||
EndOfInput,
|
||||
EndOfInputHour,
|
||||
EndOfInputMinute,
|
||||
EndOfInputNumeric,
|
||||
EndOfInputSecond,
|
||||
InvalidHours,
|
||||
InvalidMinutes,
|
||||
InvalidSeconds,
|
||||
InvalidSecondsFractional,
|
||||
InvalidSign,
|
||||
InvalidSignPlusOrMinus,
|
||||
MissingMinuteAfterHour,
|
||||
MissingSecondAfterMinute,
|
||||
NoColonAfterHours,
|
||||
ParseHours,
|
||||
ParseMinutes,
|
||||
ParseSeconds,
|
||||
PrecisionLoss,
|
||||
RangeHours,
|
||||
RangeMinutes,
|
||||
RangeSeconds,
|
||||
SeparatorAfterHours,
|
||||
SeparatorAfterMinutes,
|
||||
SubminutePrecisionNotEnabled,
|
||||
SubsecondPrecisionNotEnabled,
|
||||
UnexpectedLetterOffsetNoZulu(u8),
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::FmtOffset(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
ColonAfterHours => f.write_str(
|
||||
"parsed hour component of time zone offset, \
|
||||
but found colon after hours which is not allowed",
|
||||
),
|
||||
EndOfInput => {
|
||||
f.write_str("expected UTC offset, but found end of input")
|
||||
}
|
||||
EndOfInputHour => f.write_str(
|
||||
"expected two digit hour after sign, but found end of input",
|
||||
),
|
||||
EndOfInputMinute => f.write_str(
|
||||
"expected two digit minute after hours, \
|
||||
but found end of input",
|
||||
),
|
||||
EndOfInputNumeric => f.write_str(
|
||||
"expected UTC numeric offset, but found end of input",
|
||||
),
|
||||
EndOfInputSecond => f.write_str(
|
||||
"expected two digit second after minutes, \
|
||||
but found end of input",
|
||||
),
|
||||
InvalidHours => {
|
||||
f.write_str("failed to parse hours in UTC numeric offset")
|
||||
}
|
||||
InvalidMinutes => {
|
||||
f.write_str("failed to parse minutes in UTC numeric offset")
|
||||
}
|
||||
InvalidSeconds => {
|
||||
f.write_str("failed to parse seconds in UTC numeric offset")
|
||||
}
|
||||
InvalidSecondsFractional => f.write_str(
|
||||
"failed to parse fractional seconds in UTC numeric offset",
|
||||
),
|
||||
InvalidSign => {
|
||||
f.write_str("failed to parse sign in UTC numeric offset")
|
||||
}
|
||||
InvalidSignPlusOrMinus => f.write_str(
|
||||
"expected `+` or `-` sign at start of UTC numeric offset",
|
||||
),
|
||||
MissingMinuteAfterHour => f.write_str(
|
||||
"parsed hour component of time zone offset, \
|
||||
but could not find required minute component",
|
||||
),
|
||||
MissingSecondAfterMinute => f.write_str(
|
||||
"parsed hour and minute components of time zone offset, \
|
||||
but could not find required second component",
|
||||
),
|
||||
NoColonAfterHours => f.write_str(
|
||||
"parsed hour component of time zone offset, \
|
||||
but could not find required colon separator",
|
||||
),
|
||||
ParseHours => f.write_str(
|
||||
"failed to parse hours (requires a two digit integer)",
|
||||
),
|
||||
ParseMinutes => f.write_str(
|
||||
"failed to parse minutes (requires a two digit integer)",
|
||||
),
|
||||
ParseSeconds => f.write_str(
|
||||
"failed to parse seconds (requires a two digit integer)",
|
||||
),
|
||||
PrecisionLoss => f.write_str(
|
||||
"due to precision loss, offset is \
|
||||
rounded to a value that is out of bounds",
|
||||
),
|
||||
RangeHours => {
|
||||
f.write_str("hour in time zone offset is out of range")
|
||||
}
|
||||
RangeMinutes => {
|
||||
f.write_str("minute in time zone offset is out of range")
|
||||
}
|
||||
RangeSeconds => {
|
||||
f.write_str("second in time zone offset is out of range")
|
||||
}
|
||||
SeparatorAfterHours => f.write_str(
|
||||
"failed to parse separator after hours in \
|
||||
UTC numeric offset",
|
||||
),
|
||||
SeparatorAfterMinutes => f.write_str(
|
||||
"failed to parse separator after minutes in \
|
||||
UTC numeric offset",
|
||||
),
|
||||
SubminutePrecisionNotEnabled => f.write_str(
|
||||
"subminute precision for UTC numeric offset \
|
||||
is not enabled in this context (must provide only \
|
||||
integral minutes)",
|
||||
),
|
||||
SubsecondPrecisionNotEnabled => f.write_str(
|
||||
"subsecond precision for UTC numeric offset \
|
||||
is not enabled in this context (must provide only \
|
||||
integral minutes or seconds)",
|
||||
),
|
||||
UnexpectedLetterOffsetNoZulu(byte) => write!(
|
||||
f,
|
||||
"found `{z}` where a numeric UTC offset \
|
||||
was expected (this context does not permit \
|
||||
the Zulu offset)",
|
||||
z = escape::Byte(byte),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
231
src/error/fmt/rfc2822.rs
Normal file
231
src/error/fmt/rfc2822.rs
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
use crate::{civil::Weekday, error, util::escape};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
CommentClosingParenWithoutOpen,
|
||||
CommentOpeningParenWithoutClose,
|
||||
CommentTooManyNestedParens,
|
||||
EndOfInputDay,
|
||||
Empty,
|
||||
EmptyAfterWhitespace,
|
||||
EndOfInputComma,
|
||||
EndOfInputHour,
|
||||
EndOfInputMinute,
|
||||
EndOfInputMonth,
|
||||
EndOfInputOffset,
|
||||
EndOfInputSecond,
|
||||
EndOfInputTimeSeparator,
|
||||
FailedTimestamp,
|
||||
FailedZoned,
|
||||
InconsistentWeekday { parsed: Weekday, from_date: Weekday },
|
||||
InvalidDate,
|
||||
InvalidHour,
|
||||
InvalidMinute,
|
||||
InvalidMonth,
|
||||
InvalidObsoleteOffset,
|
||||
InvalidOffsetHour,
|
||||
InvalidOffsetMinute,
|
||||
InvalidSecond,
|
||||
InvalidWeekday { got_non_digit: u8 },
|
||||
InvalidYear,
|
||||
NegativeYear,
|
||||
ParseDay,
|
||||
ParseHour,
|
||||
ParseMinute,
|
||||
ParseOffsetHour,
|
||||
ParseOffsetMinute,
|
||||
ParseSecond,
|
||||
ParseYear,
|
||||
TooShortMonth { len: u8 },
|
||||
TooShortOffset,
|
||||
TooShortWeekday { got_non_digit: u8, len: u8 },
|
||||
TooShortYear { len: u8 },
|
||||
UnexpectedByteComma { byte: u8 },
|
||||
UnexpectedByteTimeSeparator { byte: u8 },
|
||||
WhitespaceAfterDay,
|
||||
WhitespaceAfterMonth,
|
||||
WhitespaceAfterTime,
|
||||
WhitespaceAfterTimeForObsoleteOffset,
|
||||
WhitespaceAfterYear,
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::FmtRfc2822(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
CommentClosingParenWithoutOpen => f.write_str(
|
||||
"found closing parenthesis in comment with \
|
||||
no matching opening parenthesis",
|
||||
),
|
||||
CommentOpeningParenWithoutClose => f.write_str(
|
||||
"found opening parenthesis in comment with \
|
||||
no matching closing parenthesis",
|
||||
),
|
||||
CommentTooManyNestedParens => {
|
||||
f.write_str("found too many nested parenthesis in comment")
|
||||
}
|
||||
Empty => {
|
||||
f.write_str("expected RFC 2822 datetime, but got empty string")
|
||||
}
|
||||
EmptyAfterWhitespace => f.write_str(
|
||||
"expected RFC 2822 datetime, but got empty string \
|
||||
after trimming leading whitespace",
|
||||
),
|
||||
EndOfInputComma => f.write_str(
|
||||
"expected comma after parsed weekday in \
|
||||
RFC 2822 datetime, but found end of input instead",
|
||||
),
|
||||
EndOfInputDay => {
|
||||
f.write_str("expected numeric day, but found end of input")
|
||||
}
|
||||
EndOfInputHour => {
|
||||
f.write_str("expected two digit hour, but found end of input")
|
||||
}
|
||||
EndOfInputMinute => f.write_str(
|
||||
"expected two digit minute, but found end of input",
|
||||
),
|
||||
EndOfInputMonth => f.write_str(
|
||||
"expected abbreviated month name, but found end of input",
|
||||
),
|
||||
EndOfInputOffset => f.write_str(
|
||||
"expected sign for time zone offset, \
|
||||
(or a legacy time zone name abbreviation), \
|
||||
but found end of input",
|
||||
),
|
||||
EndOfInputSecond => f.write_str(
|
||||
"expected two digit second, but found end of input",
|
||||
),
|
||||
EndOfInputTimeSeparator => f.write_str(
|
||||
"expected time separator of `:`, but found end of input",
|
||||
),
|
||||
FailedTimestamp => f.write_str(
|
||||
"failed to parse RFC 2822 datetime into Jiff timestamp",
|
||||
),
|
||||
FailedZoned => f.write_str(
|
||||
"failed to parse RFC 2822 datetime into Jiff zoned datetime",
|
||||
),
|
||||
InconsistentWeekday { parsed, from_date } => write!(
|
||||
f,
|
||||
"found parsed weekday of `{parsed:?}`, \
|
||||
but parsed datetime has weekday `{from_date:?}`",
|
||||
),
|
||||
InvalidDate => f.write_str("invalid date"),
|
||||
InvalidHour => f.write_str("invalid hour"),
|
||||
InvalidMinute => f.write_str("invalid minute"),
|
||||
InvalidMonth => f.write_str(
|
||||
"expected abbreviated month name, \
|
||||
but did not recognize a valid abbreviated month name",
|
||||
),
|
||||
InvalidObsoleteOffset => f.write_str(
|
||||
"expected obsolete RFC 2822 time zone abbreviation, \
|
||||
but did not recognize a valid abbreviation",
|
||||
),
|
||||
InvalidOffsetHour => f.write_str("invalid time zone offset hour"),
|
||||
InvalidOffsetMinute => {
|
||||
f.write_str("invalid time zone offset minute")
|
||||
}
|
||||
InvalidSecond => f.write_str("invalid second"),
|
||||
InvalidWeekday { got_non_digit } => write!(
|
||||
f,
|
||||
"expected day at beginning of RFC 2822 datetime \
|
||||
since first non-whitespace byte, `{first}`, \
|
||||
is not a digit, but did not recognize a valid \
|
||||
weekday abbreviation",
|
||||
first = escape::Byte(got_non_digit),
|
||||
),
|
||||
InvalidYear => f.write_str("invalid year"),
|
||||
NegativeYear => f.write_str(
|
||||
"datetime has negative year, \
|
||||
which cannot be formatted with RFC 2822",
|
||||
),
|
||||
ParseDay => f.write_str("failed to parse day"),
|
||||
ParseHour => f.write_str(
|
||||
"failed to parse hour (expects a two digit integer)",
|
||||
),
|
||||
ParseMinute => f.write_str(
|
||||
"failed to parse minute (expects a two digit integer)",
|
||||
),
|
||||
ParseOffsetHour => {
|
||||
f.write_str("failed to parse hours from time zone offset")
|
||||
}
|
||||
ParseOffsetMinute => {
|
||||
f.write_str("failed to parse minutes from time zone offset")
|
||||
}
|
||||
ParseSecond => f.write_str(
|
||||
"failed to parse second (expects a two digit integer)",
|
||||
),
|
||||
ParseYear => f.write_str(
|
||||
"failed to parse year \
|
||||
(expects a two, three or four digit integer)",
|
||||
),
|
||||
TooShortMonth { len } => write!(
|
||||
f,
|
||||
"expected abbreviated month name, but remaining input \
|
||||
is too short (remaining bytes is {len})",
|
||||
),
|
||||
TooShortOffset => write!(
|
||||
f,
|
||||
"expected at least four digits for time zone offset \
|
||||
after sign, but found fewer than four bytes remaining",
|
||||
),
|
||||
TooShortWeekday { got_non_digit, len } => write!(
|
||||
f,
|
||||
"expected day at beginning of RFC 2822 datetime \
|
||||
since first non-whitespace byte, `{first}`, \
|
||||
is not a digit, but given string is too short \
|
||||
(length is {len})",
|
||||
first = escape::Byte(got_non_digit),
|
||||
),
|
||||
TooShortYear { len } => write!(
|
||||
f,
|
||||
"expected at least two ASCII digits for parsing \
|
||||
a year, but only found {len}",
|
||||
),
|
||||
UnexpectedByteComma { byte } => write!(
|
||||
f,
|
||||
"expected comma after parsed weekday in \
|
||||
RFC 2822 datetime, but found `{got}` instead",
|
||||
got = escape::Byte(byte),
|
||||
),
|
||||
UnexpectedByteTimeSeparator { byte } => write!(
|
||||
f,
|
||||
"expected time separator of `:`, but found `{got}`",
|
||||
got = escape::Byte(byte),
|
||||
),
|
||||
WhitespaceAfterDay => {
|
||||
f.write_str("expected whitespace after parsing day")
|
||||
}
|
||||
WhitespaceAfterMonth => f.write_str(
|
||||
"expected whitespace after parsing abbreviated month name",
|
||||
),
|
||||
WhitespaceAfterTime => f.write_str(
|
||||
"expected whitespace after parsing time: \
|
||||
expected at least one whitespace character \
|
||||
(space or tab), but found none",
|
||||
),
|
||||
WhitespaceAfterTimeForObsoleteOffset => f.write_str(
|
||||
"expected obsolete RFC 2822 time zone abbreviation, \
|
||||
but found no remaining non-whitespace characters \
|
||||
after time",
|
||||
),
|
||||
WhitespaceAfterYear => {
|
||||
f.write_str("expected whitespace after parsing year")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
114
src/error/fmt/rfc9557.rs
Normal file
114
src/error/fmt/rfc9557.rs
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
use crate::{error, util::escape};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
EndOfInputAnnotation,
|
||||
EndOfInputAnnotationClose,
|
||||
EndOfInputAnnotationKey,
|
||||
EndOfInputAnnotationSeparator,
|
||||
EndOfInputAnnotationValue,
|
||||
EndOfInputTzAnnotationClose,
|
||||
UnexpectedByteAnnotation { byte: u8 },
|
||||
UnexpectedByteAnnotationClose { byte: u8 },
|
||||
UnexpectedByteAnnotationKey { byte: u8 },
|
||||
UnexpectedByteAnnotationValue { byte: u8 },
|
||||
UnexpectedByteAnnotationSeparator { byte: u8 },
|
||||
UnexpectedByteTzAnnotationClose { byte: u8 },
|
||||
UnexpectedSlashAnnotationSeparator,
|
||||
UnsupportedAnnotationCritical,
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::FmtRfc9557(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
EndOfInputAnnotation => f.write_str(
|
||||
"expected the start of an RFC 9557 annotation or IANA \
|
||||
time zone component name, but found end of input instead",
|
||||
),
|
||||
EndOfInputAnnotationClose => f.write_str(
|
||||
"expected an `]` after parsing an RFC 9557 annotation key \
|
||||
and value, but found end of input instead",
|
||||
),
|
||||
EndOfInputAnnotationKey => f.write_str(
|
||||
"expected the start of an RFC 9557 annotation key, \
|
||||
but found end of input instead",
|
||||
),
|
||||
EndOfInputAnnotationSeparator => f.write_str(
|
||||
"expected an `=` after parsing an RFC 9557 annotation key, \
|
||||
but found end of input instead",
|
||||
),
|
||||
EndOfInputAnnotationValue => f.write_str(
|
||||
"expected the start of an RFC 9557 annotation value, \
|
||||
but found end of input instead",
|
||||
),
|
||||
EndOfInputTzAnnotationClose => f.write_str(
|
||||
"expected an `]` after parsing an RFC 9557 time zone \
|
||||
annotation, but found end of input instead",
|
||||
),
|
||||
UnexpectedByteAnnotation { byte } => write!(
|
||||
f,
|
||||
"expected ASCII alphabetic byte (or underscore or period) \
|
||||
at the start of an RFC 9557 annotation or time zone \
|
||||
component name, but found `{byte}` instead",
|
||||
byte = escape::Byte(byte),
|
||||
),
|
||||
UnexpectedByteAnnotationClose { byte } => write!(
|
||||
f,
|
||||
"expected an `]` after parsing an RFC 9557 annotation key \
|
||||
and value, but found `{byte}` instead",
|
||||
byte = escape::Byte(byte),
|
||||
),
|
||||
UnexpectedByteAnnotationKey { byte } => write!(
|
||||
f,
|
||||
"expected lowercase alphabetic byte (or underscore) \
|
||||
at the start of an RFC 9557 annotation key, \
|
||||
but found `{byte}` instead",
|
||||
byte = escape::Byte(byte),
|
||||
),
|
||||
UnexpectedByteAnnotationValue { byte } => write!(
|
||||
f,
|
||||
"expected alphanumeric ASCII byte \
|
||||
at the start of an RFC 9557 annotation value, \
|
||||
but found `{byte}` instead",
|
||||
byte = escape::Byte(byte),
|
||||
),
|
||||
UnexpectedByteAnnotationSeparator { byte } => write!(
|
||||
f,
|
||||
"expected an `=` after parsing an RFC 9557 annotation \
|
||||
key, but found `{byte}` instead",
|
||||
byte = escape::Byte(byte),
|
||||
),
|
||||
UnexpectedByteTzAnnotationClose { byte } => write!(
|
||||
f,
|
||||
"expected an `]` after parsing an RFC 9557 time zone \
|
||||
annotation, but found `{byte}` instead",
|
||||
byte = escape::Byte(byte),
|
||||
),
|
||||
UnexpectedSlashAnnotationSeparator => f.write_str(
|
||||
"expected an `=` after parsing an RFC 9557 annotation \
|
||||
key, but found `/` instead (time zone annotations must \
|
||||
come first)",
|
||||
),
|
||||
UnsupportedAnnotationCritical => f.write_str(
|
||||
"found unsupported RFC 9557 annotation \
|
||||
with the critical flag (`!`) set",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
517
src/error/fmt/strtime.rs
Normal file
517
src/error/fmt/strtime.rs
Normal file
|
|
@ -0,0 +1,517 @@
|
|||
use crate::{civil::Weekday, error, tz::Offset, util::escape};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
ColonCount {
|
||||
directive: u8,
|
||||
},
|
||||
DirectiveFailure {
|
||||
directive: u8,
|
||||
colons: u8,
|
||||
},
|
||||
DirectiveFailureDot {
|
||||
directive: u8,
|
||||
},
|
||||
ExpectedDirectiveAfterColons,
|
||||
ExpectedDirectiveAfterFlag {
|
||||
flag: u8,
|
||||
},
|
||||
ExpectedDirectiveAfterWidth,
|
||||
FailedStrftime,
|
||||
FailedStrptime,
|
||||
FailedWidth,
|
||||
InvalidDate,
|
||||
InvalidISOWeekDate,
|
||||
InvalidWeekdayMonday {
|
||||
got: Weekday,
|
||||
},
|
||||
InvalidWeekdaySunday {
|
||||
got: Weekday,
|
||||
},
|
||||
MismatchOffset {
|
||||
parsed: Offset,
|
||||
got: Offset,
|
||||
},
|
||||
MismatchWeekday {
|
||||
parsed: Weekday,
|
||||
got: Weekday,
|
||||
},
|
||||
MissingTimeHourForFractional,
|
||||
MissingTimeHourForMinute,
|
||||
MissingTimeHourForSecond,
|
||||
MissingTimeMinuteForFractional,
|
||||
MissingTimeMinuteForSecond,
|
||||
MissingTimeSecondForFractional,
|
||||
RangeTimestamp,
|
||||
RangeWidth,
|
||||
RequiredDateForDateTime,
|
||||
RequiredDateTimeForTimestamp,
|
||||
RequiredDateTimeForZoned,
|
||||
RequiredOffsetForTimestamp,
|
||||
RequiredSomeDayForDate,
|
||||
RequiredTimeForDateTime,
|
||||
RequiredYearForDate,
|
||||
UnconsumedStrptime {
|
||||
#[cfg(feature = "alloc")]
|
||||
remaining: alloc::boxed::Box<[u8]>,
|
||||
},
|
||||
UnexpectedEndAfterDot,
|
||||
UnexpectedEndAfterPercent,
|
||||
UnknownDirectiveAfterDot {
|
||||
directive: u8,
|
||||
},
|
||||
UnknownDirective {
|
||||
directive: u8,
|
||||
},
|
||||
ZonedOffsetOrTz,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub(crate) fn unconsumed(_remaining: &[u8]) -> Error {
|
||||
Error::UnconsumedStrptime {
|
||||
#[cfg(feature = "alloc")]
|
||||
remaining: _remaining.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::FmtStrtime(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
ColonCount { directive } => write!(
|
||||
f,
|
||||
"invalid number of `:` in `%{directive}` directive",
|
||||
directive = escape::Byte(directive),
|
||||
),
|
||||
DirectiveFailure { directive, colons } => write!(
|
||||
f,
|
||||
"%{colons}{directive} failed",
|
||||
colons = escape::RepeatByte { byte: b':', count: colons },
|
||||
directive = escape::Byte(directive),
|
||||
),
|
||||
DirectiveFailureDot { directive } => write!(
|
||||
f,
|
||||
"%.{directive} failed",
|
||||
directive = escape::Byte(directive),
|
||||
),
|
||||
ExpectedDirectiveAfterColons => f.write_str(
|
||||
"expected to find specifier directive after colons, \
|
||||
but found end of format string",
|
||||
),
|
||||
ExpectedDirectiveAfterFlag { flag } => write!(
|
||||
f,
|
||||
"expected to find specifier directive after flag \
|
||||
`{flag}`, but found end of format string",
|
||||
flag = escape::Byte(flag),
|
||||
),
|
||||
ExpectedDirectiveAfterWidth => f.write_str(
|
||||
"expected to find specifier directive after parsed width, \
|
||||
but found end of format string",
|
||||
),
|
||||
FailedStrftime => f.write_str("strftime formatting failed"),
|
||||
FailedStrptime => f.write_str("strptime parsing failed"),
|
||||
FailedWidth => {
|
||||
f.write_str("failed to parse conversion specifier width")
|
||||
}
|
||||
InvalidDate => f.write_str("invalid date"),
|
||||
InvalidISOWeekDate => f.write_str("invalid ISO 8601 week date"),
|
||||
InvalidWeekdayMonday { got } => write!(
|
||||
f,
|
||||
"weekday `{got:?}` is not valid for \
|
||||
Monday based week number",
|
||||
),
|
||||
InvalidWeekdaySunday { got } => write!(
|
||||
f,
|
||||
"weekday `{got:?}` is not valid for \
|
||||
Sunday based week number",
|
||||
),
|
||||
MissingTimeHourForFractional => f.write_str(
|
||||
"parsing format did not include hour directive, \
|
||||
but did include fractional second directive (cannot have \
|
||||
smaller time units with bigger time units missing)",
|
||||
),
|
||||
MissingTimeHourForMinute => f.write_str(
|
||||
"parsing format did not include hour directive, \
|
||||
but did include minute directive (cannot have \
|
||||
smaller time units with bigger time units missing)",
|
||||
),
|
||||
MissingTimeHourForSecond => f.write_str(
|
||||
"parsing format did not include hour directive, \
|
||||
but did include second directive (cannot have \
|
||||
smaller time units with bigger time units missing)",
|
||||
),
|
||||
MissingTimeMinuteForFractional => f.write_str(
|
||||
"parsing format did not include minute directive, \
|
||||
but did include fractional second directive (cannot have \
|
||||
smaller time units with bigger time units missing)",
|
||||
),
|
||||
MissingTimeMinuteForSecond => f.write_str(
|
||||
"parsing format did not include minute directive, \
|
||||
but did include second directive (cannot have \
|
||||
smaller time units with bigger time units missing)",
|
||||
),
|
||||
MissingTimeSecondForFractional => f.write_str(
|
||||
"parsing format did not include second directive, \
|
||||
but did include fractional second directive (cannot have \
|
||||
smaller time units with bigger time units missing)",
|
||||
),
|
||||
MismatchOffset { parsed, got } => write!(
|
||||
f,
|
||||
"parsed time zone offset `{parsed}`, but \
|
||||
offset from timestamp and time zone is `{got}`",
|
||||
),
|
||||
MismatchWeekday { parsed, got } => write!(
|
||||
f,
|
||||
"parsed weekday `{parsed:?}` does not match \
|
||||
weekday `{got:?}` from parsed date",
|
||||
),
|
||||
RangeTimestamp => f.write_str(
|
||||
"parsed datetime and offset, \
|
||||
but combining them into a zoned datetime \
|
||||
is outside Jiff's supported timestamp range",
|
||||
),
|
||||
RangeWidth => write!(
|
||||
f,
|
||||
"parsed width is too big, max is {max}",
|
||||
max = u8::MAX
|
||||
),
|
||||
RequiredDateForDateTime => {
|
||||
f.write_str("date required to parse datetime")
|
||||
}
|
||||
RequiredDateTimeForTimestamp => {
|
||||
f.write_str("datetime required to parse timestamp")
|
||||
}
|
||||
RequiredDateTimeForZoned => {
|
||||
f.write_str("datetime required to parse zoned datetime")
|
||||
}
|
||||
RequiredOffsetForTimestamp => {
|
||||
f.write_str("offset required to parse timestamp")
|
||||
}
|
||||
RequiredSomeDayForDate => f.write_str(
|
||||
"a month/day, day-of-year or week date must be \
|
||||
present to create a date, but none were found",
|
||||
),
|
||||
RequiredTimeForDateTime => {
|
||||
f.write_str("time required to parse datetime")
|
||||
}
|
||||
RequiredYearForDate => f.write_str("year required to parse date"),
|
||||
#[cfg(feature = "alloc")]
|
||||
UnconsumedStrptime { ref remaining } => write!(
|
||||
f,
|
||||
"strptime expects to consume the entire input, but \
|
||||
`{remaining}` remains unparsed",
|
||||
remaining = escape::Bytes(remaining),
|
||||
),
|
||||
#[cfg(not(feature = "alloc"))]
|
||||
UnconsumedStrptime {} => f.write_str(
|
||||
"strptime expects to consume the entire input, but \
|
||||
there is unparsed input remaining",
|
||||
),
|
||||
UnexpectedEndAfterDot => f.write_str(
|
||||
"invalid format string, expected directive after `%.`",
|
||||
),
|
||||
UnexpectedEndAfterPercent => f.write_str(
|
||||
"invalid format string, expected byte after `%`, \
|
||||
but found end of format string",
|
||||
),
|
||||
UnknownDirective { directive } => write!(
|
||||
f,
|
||||
"found unrecognized specifier directive `{directive}`",
|
||||
directive = escape::Byte(directive),
|
||||
),
|
||||
UnknownDirectiveAfterDot { directive } => write!(
|
||||
f,
|
||||
"found unrecognized specifier directive `{directive}` \
|
||||
following `%.`",
|
||||
directive = escape::Byte(directive),
|
||||
),
|
||||
ZonedOffsetOrTz => f.write_str(
|
||||
"either offset (from `%z`) or IANA time zone identifier \
|
||||
(from `%Q`) is required for parsing zoned datetime",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum ParseError {
|
||||
ExpectedAmPm,
|
||||
ExpectedAmPmTooShort,
|
||||
ExpectedIanaTz,
|
||||
ExpectedIanaTzEndOfInput,
|
||||
ExpectedMonthAbbreviation,
|
||||
ExpectedMonthAbbreviationTooShort,
|
||||
ExpectedWeekdayAbbreviation,
|
||||
ExpectedWeekdayAbbreviationTooShort,
|
||||
ExpectedChoice {
|
||||
available: &'static [&'static [u8]],
|
||||
},
|
||||
ExpectedFractionalDigit,
|
||||
ExpectedMatchLiteralByte {
|
||||
expected: u8,
|
||||
got: u8,
|
||||
},
|
||||
ExpectedMatchLiteralEndOfInput {
|
||||
expected: u8,
|
||||
},
|
||||
ExpectedNonEmpty {
|
||||
directive: u8,
|
||||
},
|
||||
#[cfg(not(feature = "alloc"))]
|
||||
NotAllowedAlloc {
|
||||
directive: u8,
|
||||
colons: u8,
|
||||
},
|
||||
NotAllowedLocaleClockTime,
|
||||
NotAllowedLocaleDate,
|
||||
NotAllowedLocaleDateAndTime,
|
||||
NotAllowedLocaleTwelveHourClockTime,
|
||||
NotAllowedTimeZoneAbbreviation,
|
||||
ParseDay,
|
||||
ParseDayOfYear,
|
||||
ParseCentury,
|
||||
ParseFractionalSeconds,
|
||||
ParseHour,
|
||||
ParseIsoWeekNumber,
|
||||
ParseIsoWeekYear,
|
||||
ParseIsoWeekYearTwoDigit,
|
||||
ParseMinute,
|
||||
ParseMondayWeekNumber,
|
||||
ParseMonth,
|
||||
ParseSecond,
|
||||
ParseSundayWeekNumber,
|
||||
ParseTimestamp,
|
||||
ParseWeekdayNumber,
|
||||
ParseYear,
|
||||
ParseYearTwoDigit,
|
||||
UnknownMonthName,
|
||||
UnknownWeekdayAbbreviation,
|
||||
}
|
||||
|
||||
impl error::IntoError for ParseError {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ParseError> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: ParseError) -> error::Error {
|
||||
error::ErrorKind::FmtStrtimeParse(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for ParseError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::ParseError::*;
|
||||
|
||||
match *self {
|
||||
ExpectedAmPm => f.write_str("expected to find `AM` or `PM`"),
|
||||
ExpectedAmPmTooShort => f.write_str(
|
||||
"expected to find `AM` or `PM`, \
|
||||
but the remaining input is too short \
|
||||
to contain one",
|
||||
),
|
||||
ExpectedIanaTz => f.write_str(
|
||||
"expected to find the start of an IANA time zone \
|
||||
identifier name or component",
|
||||
),
|
||||
ExpectedIanaTzEndOfInput => f.write_str(
|
||||
"expected to find the start of an IANA time zone \
|
||||
identifier name or component, \
|
||||
but found end of input instead",
|
||||
),
|
||||
ExpectedMonthAbbreviation => {
|
||||
f.write_str("expected to find month name abbreviation")
|
||||
}
|
||||
ExpectedMonthAbbreviationTooShort => f.write_str(
|
||||
"expected to find month name abbreviation, \
|
||||
but the remaining input is too short \
|
||||
to contain one",
|
||||
),
|
||||
ExpectedWeekdayAbbreviation => {
|
||||
f.write_str("expected to find weekday abbreviation")
|
||||
}
|
||||
ExpectedWeekdayAbbreviationTooShort => f.write_str(
|
||||
"expected to find weekday abbreviation, \
|
||||
but the remaining input is too short \
|
||||
to contain one",
|
||||
),
|
||||
ExpectedChoice { available } => {
|
||||
f.write_str(
|
||||
"failed to find expected value, available choices are: ",
|
||||
)?;
|
||||
for (i, choice) in available.iter().enumerate() {
|
||||
if i > 0 {
|
||||
f.write_str(", ")?;
|
||||
}
|
||||
write!(f, "{}", escape::Bytes(choice))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
ExpectedFractionalDigit => f.write_str(
|
||||
"expected at least one fractional decimal digit, \
|
||||
but did not find any",
|
||||
),
|
||||
ExpectedMatchLiteralByte { expected, got } => write!(
|
||||
f,
|
||||
"expected to match literal byte `{expected}` from \
|
||||
format string, but found byte `{got}` in input",
|
||||
expected = escape::Byte(expected),
|
||||
got = escape::Byte(got),
|
||||
),
|
||||
ExpectedMatchLiteralEndOfInput { expected } => write!(
|
||||
f,
|
||||
"expected to match literal byte `{expected}` from \
|
||||
format string, but found end of input",
|
||||
expected = escape::Byte(expected),
|
||||
),
|
||||
ExpectedNonEmpty { directive } => write!(
|
||||
f,
|
||||
"expected non-empty input for directive `%{directive}`, \
|
||||
but found end of input",
|
||||
directive = escape::Byte(directive),
|
||||
),
|
||||
#[cfg(not(feature = "alloc"))]
|
||||
NotAllowedAlloc { directive, colons } => write!(
|
||||
f,
|
||||
"cannot parse `%{colons}{directive}` \
|
||||
without Jiff's `alloc` feature enabled",
|
||||
colons = escape::RepeatByte { byte: b':', count: colons },
|
||||
directive = escape::Byte(directive),
|
||||
),
|
||||
NotAllowedLocaleClockTime => {
|
||||
f.write_str("parsing locale clock time is not allowed")
|
||||
}
|
||||
NotAllowedLocaleDate => {
|
||||
f.write_str("parsing locale date is not allowed")
|
||||
}
|
||||
NotAllowedLocaleDateAndTime => {
|
||||
f.write_str("parsing locale date and time is not allowed")
|
||||
}
|
||||
NotAllowedLocaleTwelveHourClockTime => {
|
||||
f.write_str("parsing locale 12-hour clock time is not allowed")
|
||||
}
|
||||
NotAllowedTimeZoneAbbreviation => {
|
||||
f.write_str("parsing time zone abbreviation is not allowed")
|
||||
}
|
||||
ParseCentury => {
|
||||
f.write_str("failed to parse year number for century")
|
||||
}
|
||||
ParseDay => f.write_str("failed to parse day number"),
|
||||
ParseDayOfYear => {
|
||||
f.write_str("failed to parse day of year number")
|
||||
}
|
||||
ParseFractionalSeconds => f.write_str(
|
||||
"failed to parse fractional second component \
|
||||
(up to 9 digits, nanosecond precision)",
|
||||
),
|
||||
ParseHour => f.write_str("failed to parse hour number"),
|
||||
ParseMinute => f.write_str("failed to parse minute number"),
|
||||
ParseWeekdayNumber => {
|
||||
f.write_str("failed to parse weekday number")
|
||||
}
|
||||
ParseIsoWeekNumber => {
|
||||
f.write_str("failed to parse ISO 8601 week number")
|
||||
}
|
||||
ParseIsoWeekYear => {
|
||||
f.write_str("failed to parse ISO 8601 week year")
|
||||
}
|
||||
ParseIsoWeekYearTwoDigit => {
|
||||
f.write_str("failed to parse 2-digit ISO 8601 week year")
|
||||
}
|
||||
ParseMondayWeekNumber => {
|
||||
f.write_str("failed to parse Monday-based week number")
|
||||
}
|
||||
ParseMonth => f.write_str("failed to parse month number"),
|
||||
ParseSecond => f.write_str("failed to parse second number"),
|
||||
ParseSundayWeekNumber => {
|
||||
f.write_str("failed to parse Sunday-based week number")
|
||||
}
|
||||
ParseTimestamp => {
|
||||
f.write_str("failed to parse Unix timestamp (in seconds)")
|
||||
}
|
||||
ParseYear => f.write_str("failed to parse year"),
|
||||
ParseYearTwoDigit => f.write_str("failed to parse 2-digit year"),
|
||||
UnknownMonthName => f.write_str("unrecognized month name"),
|
||||
UnknownWeekdayAbbreviation => {
|
||||
f.write_str("unrecognized weekday abbreviation")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum FormatError {
|
||||
RequiresDate,
|
||||
RequiresInstant,
|
||||
RequiresOffset,
|
||||
RequiresTime,
|
||||
RequiresTimeZone,
|
||||
RequiresTimeZoneOrOffset,
|
||||
InvalidUtf8,
|
||||
ZeroPrecisionFloat,
|
||||
ZeroPrecisionNano,
|
||||
}
|
||||
|
||||
impl error::IntoError for FormatError {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FormatError> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: FormatError) -> error::Error {
|
||||
error::ErrorKind::FmtStrtimeFormat(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for FormatError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::FormatError::*;
|
||||
|
||||
match *self {
|
||||
RequiresDate => f.write_str("requires date to format"),
|
||||
RequiresInstant => f.write_str(
|
||||
"requires instant (a timestamp or a date, time and offset)",
|
||||
),
|
||||
RequiresTime => f.write_str("requires time to format"),
|
||||
RequiresOffset => f.write_str("requires time zone offset"),
|
||||
RequiresTimeZone => {
|
||||
f.write_str("requires IANA time zone identifier")
|
||||
}
|
||||
RequiresTimeZoneOrOffset => f.write_str(
|
||||
"requires IANA time zone identifier or \
|
||||
time zone offset, but neither were present",
|
||||
),
|
||||
InvalidUtf8 => {
|
||||
f.write_str("invalid format string, it must be valid UTF-8")
|
||||
}
|
||||
ZeroPrecisionFloat => {
|
||||
f.write_str("zero precision with %f is not allowed")
|
||||
}
|
||||
ZeroPrecisionNano => {
|
||||
f.write_str("zero precision with %N is not allowed")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
354
src/error/fmt/temporal.rs
Normal file
354
src/error/fmt/temporal.rs
Normal file
|
|
@ -0,0 +1,354 @@
|
|||
use crate::{error, tz::Offset, util::escape};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
#[cfg(not(feature = "alloc"))]
|
||||
AllocPosixTimeZone,
|
||||
AmbiguousTimeMonthDay,
|
||||
AmbiguousTimeYearMonth,
|
||||
CivilDateTimeZulu,
|
||||
ConvertDateTimeToTimestamp {
|
||||
offset: Offset,
|
||||
},
|
||||
EmptyTimeZone,
|
||||
ExpectedDateDesignatorFoundByte {
|
||||
byte: u8,
|
||||
},
|
||||
ExpectedDateDesignatorFoundEndOfInput,
|
||||
ExpectedDurationDesignatorFoundByte {
|
||||
byte: u8,
|
||||
},
|
||||
ExpectedDurationDesignatorFoundEndOfInput,
|
||||
ExpectedFourDigitYear,
|
||||
ExpectedNoSeparator,
|
||||
ExpectedOneDigitWeekday,
|
||||
ExpectedSeparatorFoundByte {
|
||||
byte: u8,
|
||||
},
|
||||
ExpectedSeparatorFoundEndOfInput,
|
||||
ExpectedSixDigitYear,
|
||||
ExpectedTimeDesignator,
|
||||
ExpectedTimeDesignatorFoundByte {
|
||||
byte: u8,
|
||||
},
|
||||
ExpectedTimeDesignatorFoundEndOfInput,
|
||||
ExpectedTimeUnits,
|
||||
ExpectedTwoDigitDay,
|
||||
ExpectedTwoDigitHour,
|
||||
ExpectedTwoDigitMinute,
|
||||
ExpectedTwoDigitMonth,
|
||||
ExpectedTwoDigitSecond,
|
||||
ExpectedTwoDigitWeekNumber,
|
||||
ExpectedWeekPrefixFoundByte {
|
||||
byte: u8,
|
||||
},
|
||||
ExpectedWeekPrefixFoundEndOfInput,
|
||||
FailedDayInDate,
|
||||
FailedDayInMonthDay,
|
||||
FailedFractionalSecondInTime,
|
||||
FailedHourInTime,
|
||||
FailedMinuteInTime,
|
||||
FailedMonthInDate,
|
||||
FailedMonthInMonthDay,
|
||||
FailedMonthInYearMonth,
|
||||
FailedOffsetNumeric,
|
||||
FailedSecondInTime,
|
||||
FailedSeparatorAfterMonth,
|
||||
FailedSeparatorAfterWeekNumber,
|
||||
FailedSeparatorAfterYear,
|
||||
FailedTzdbLookup,
|
||||
FailedWeekNumberInDate,
|
||||
FailedWeekNumberPrefixInDate,
|
||||
FailedWeekdayInDate,
|
||||
FailedYearInDate,
|
||||
FailedYearInYearMonth,
|
||||
InvalidDate,
|
||||
InvalidDay,
|
||||
InvalidHour,
|
||||
InvalidMinute,
|
||||
InvalidMonth,
|
||||
InvalidMonthDay,
|
||||
InvalidSecond,
|
||||
InvalidTimeZoneUtf8,
|
||||
InvalidWeekDate,
|
||||
InvalidWeekNumber,
|
||||
InvalidWeekday,
|
||||
InvalidYear,
|
||||
InvalidYearMonth,
|
||||
InvalidYearZero,
|
||||
MissingOffsetInTimestamp,
|
||||
MissingTimeInDate,
|
||||
MissingTimeInTimestamp,
|
||||
MissingTimeZoneAnnotation,
|
||||
ParseDayTwoDigit,
|
||||
ParseHourTwoDigit,
|
||||
ParseMinuteTwoDigit,
|
||||
ParseMonthTwoDigit,
|
||||
ParseSecondTwoDigit,
|
||||
ParseWeekNumberTwoDigit,
|
||||
ParseWeekdayOneDigit,
|
||||
ParseYearFourDigit,
|
||||
ParseYearSixDigit,
|
||||
// This is the only error for formatting a Temporal value. And
|
||||
// actually, it's not even part of Temporal, but just lives in that
|
||||
// module (for convenience reasons).
|
||||
PrintTimeZoneFailure,
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::FmtTemporal(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
#[cfg(not(feature = "alloc"))]
|
||||
AllocPosixTimeZone => f.write_str(
|
||||
"cannot parsed time zones other than IANA time zone \
|
||||
identifiers or fixed offsets \
|
||||
without the `alloc` crate feature enabled for `jiff`",
|
||||
),
|
||||
AmbiguousTimeMonthDay => {
|
||||
f.write_str("parsed time is ambiguous with a month-day date")
|
||||
}
|
||||
AmbiguousTimeYearMonth => {
|
||||
f.write_str("parsed time is ambiguous with a year-month date")
|
||||
}
|
||||
CivilDateTimeZulu => f.write_str(
|
||||
"cannot parse civil date/time from string with a Zulu \
|
||||
offset, parse as a `jiff::Timestamp` first \
|
||||
and convert to a civil date/time instead",
|
||||
),
|
||||
ConvertDateTimeToTimestamp { offset } => write!(
|
||||
f,
|
||||
"failed to convert civil datetime to timestamp \
|
||||
with offset {offset}",
|
||||
),
|
||||
EmptyTimeZone => {
|
||||
f.write_str("an empty string is not a valid time zone")
|
||||
}
|
||||
ExpectedDateDesignatorFoundByte { byte } => write!(
|
||||
f,
|
||||
"expected to find date unit designator suffix \
|
||||
(`Y`, `M`, `W` or `D`), but found `{byte}` instead",
|
||||
byte = escape::Byte(byte),
|
||||
),
|
||||
ExpectedDateDesignatorFoundEndOfInput => f.write_str(
|
||||
"expected to find date unit designator suffix \
|
||||
(`Y`, `M`, `W` or `D`), but found end of input",
|
||||
),
|
||||
ExpectedDurationDesignatorFoundByte { byte } => write!(
|
||||
f,
|
||||
"expected to find duration beginning with `P` or `p`, \
|
||||
but found `{byte}` instead",
|
||||
byte = escape::Byte(byte),
|
||||
),
|
||||
ExpectedDurationDesignatorFoundEndOfInput => f.write_str(
|
||||
"expected to find duration beginning with `P` or `p`, \
|
||||
but found end of input",
|
||||
),
|
||||
ExpectedFourDigitYear => f.write_str(
|
||||
"expected four digit year (or leading sign for \
|
||||
six digit year), but found end of input",
|
||||
),
|
||||
ExpectedNoSeparator => f.write_str(
|
||||
"expected no separator since none was \
|
||||
found after the year, but found a `-` separator",
|
||||
),
|
||||
ExpectedOneDigitWeekday => f.write_str(
|
||||
"expected one digit weekday, but found end of input",
|
||||
),
|
||||
ExpectedSeparatorFoundByte { byte } => write!(
|
||||
f,
|
||||
"expected `-` separator, but found `{byte}`",
|
||||
byte = escape::Byte(byte),
|
||||
),
|
||||
ExpectedSeparatorFoundEndOfInput => {
|
||||
f.write_str("expected `-` separator, but found end of input")
|
||||
}
|
||||
ExpectedSixDigitYear => f.write_str(
|
||||
"expected six digit year (because of a leading sign), \
|
||||
but found end of input",
|
||||
),
|
||||
ExpectedTimeDesignator => f.write_str(
|
||||
"parsing ISO 8601 duration in this context requires \
|
||||
that the duration contain a time component and no \
|
||||
components of days or greater",
|
||||
),
|
||||
ExpectedTimeDesignatorFoundByte { byte } => write!(
|
||||
f,
|
||||
"expected to find time unit designator suffix \
|
||||
(`H`, `M` or `S`), but found `{byte}` instead",
|
||||
byte = escape::Byte(byte),
|
||||
),
|
||||
ExpectedTimeDesignatorFoundEndOfInput => f.write_str(
|
||||
"expected to find time unit designator suffix \
|
||||
(`H`, `M` or `S`), but found end of input",
|
||||
),
|
||||
ExpectedTimeUnits => f.write_str(
|
||||
"found a time designator (`T` or `t`) in an ISO 8601 \
|
||||
duration string, but did not find any time units",
|
||||
),
|
||||
ExpectedTwoDigitDay => {
|
||||
f.write_str("expected two digit day, but found end of input")
|
||||
}
|
||||
ExpectedTwoDigitHour => {
|
||||
f.write_str("expected two digit hour, but found end of input")
|
||||
}
|
||||
ExpectedTwoDigitMinute => f.write_str(
|
||||
"expected two digit minute, but found end of input",
|
||||
),
|
||||
ExpectedTwoDigitMonth => {
|
||||
f.write_str("expected two digit month, but found end of input")
|
||||
}
|
||||
ExpectedTwoDigitSecond => f.write_str(
|
||||
"expected two digit second, but found end of input",
|
||||
),
|
||||
ExpectedTwoDigitWeekNumber => f.write_str(
|
||||
"expected two digit week number, but found end of input",
|
||||
),
|
||||
ExpectedWeekPrefixFoundByte { byte } => write!(
|
||||
f,
|
||||
"expected `W` or `w`, but found `{byte}` instead",
|
||||
byte = escape::Byte(byte),
|
||||
),
|
||||
ExpectedWeekPrefixFoundEndOfInput => {
|
||||
f.write_str("expected `W` or `w`, but found end of input")
|
||||
}
|
||||
FailedDayInDate => f.write_str("failed to parse day in date"),
|
||||
FailedDayInMonthDay => {
|
||||
f.write_str("failed to payse day in month-day")
|
||||
}
|
||||
FailedFractionalSecondInTime => {
|
||||
f.write_str("failed to parse fractional seconds in time")
|
||||
}
|
||||
FailedHourInTime => f.write_str("failed to parse hour in time"),
|
||||
FailedMinuteInTime => {
|
||||
f.write_str("failed to parse minute in time")
|
||||
}
|
||||
FailedMonthInDate => f.write_str("failed to parse month in date"),
|
||||
FailedMonthInMonthDay => {
|
||||
f.write_str("failed to parse month in month-day")
|
||||
}
|
||||
FailedMonthInYearMonth => {
|
||||
f.write_str("failed to parse month in year-month")
|
||||
}
|
||||
FailedOffsetNumeric => f.write_str(
|
||||
"offset successfully parsed, \
|
||||
but failed to convert to numeric `jiff::tz::Offset`",
|
||||
),
|
||||
FailedSecondInTime => {
|
||||
f.write_str("failed to parse second in time")
|
||||
}
|
||||
FailedSeparatorAfterMonth => {
|
||||
f.write_str("failed to parse separator after month")
|
||||
}
|
||||
FailedSeparatorAfterWeekNumber => {
|
||||
f.write_str("failed to parse separator after week number")
|
||||
}
|
||||
FailedSeparatorAfterYear => {
|
||||
f.write_str("failed to parse separator after year")
|
||||
}
|
||||
FailedTzdbLookup => f.write_str(
|
||||
"parsed apparent IANA time zone identifier, \
|
||||
but the tzdb lookup failed",
|
||||
),
|
||||
FailedWeekNumberInDate => {
|
||||
f.write_str("failed to parse week number in date")
|
||||
}
|
||||
FailedWeekNumberPrefixInDate => {
|
||||
f.write_str("failed to parse week number prefix in date")
|
||||
}
|
||||
FailedWeekdayInDate => {
|
||||
f.write_str("failed to parse weekday in date")
|
||||
}
|
||||
FailedYearInDate => f.write_str("failed to parse year in date"),
|
||||
FailedYearInYearMonth => {
|
||||
f.write_str("failed to parse year in year-month")
|
||||
}
|
||||
InvalidDate => f.write_str("parsed date is not valid"),
|
||||
InvalidDay => f.write_str("parsed day is not valid"),
|
||||
InvalidHour => f.write_str("parsed hour is not valid"),
|
||||
InvalidMinute => f.write_str("parsed minute is not valid"),
|
||||
InvalidMonth => f.write_str("parsed month is not valid"),
|
||||
InvalidMonthDay => f.write_str("parsed month-day is not valid"),
|
||||
InvalidSecond => f.write_str("parsed second is not valid"),
|
||||
InvalidTimeZoneUtf8 => f.write_str(
|
||||
"found plausible IANA time zone identifier, \
|
||||
but it is not valid UTF-8",
|
||||
),
|
||||
InvalidWeekDate => f.write_str("parsed week date is not valid"),
|
||||
InvalidWeekNumber => {
|
||||
f.write_str("parsed week number is not valid")
|
||||
}
|
||||
InvalidWeekday => f.write_str("parsed weekday is not valid"),
|
||||
InvalidYear => f.write_str("parsed year is not valid"),
|
||||
InvalidYearMonth => f.write_str("parsed year-month is not valid"),
|
||||
InvalidYearZero => f.write_str(
|
||||
"year zero must be written without a sign or a \
|
||||
positive sign, but not a negative sign",
|
||||
),
|
||||
MissingOffsetInTimestamp => f.write_str(
|
||||
"failed to find offset component, \
|
||||
which is required for parsing a timestamp",
|
||||
),
|
||||
MissingTimeInDate => f.write_str(
|
||||
"successfully parsed date, but no time component was found",
|
||||
),
|
||||
MissingTimeInTimestamp => f.write_str(
|
||||
"failed to find time component, \
|
||||
which is required for parsing a timestamp",
|
||||
),
|
||||
MissingTimeZoneAnnotation => f.write_str(
|
||||
"failed to find time zone annotation in square brackets, \
|
||||
which is required for parsing a zoned datetime",
|
||||
),
|
||||
ParseDayTwoDigit => {
|
||||
f.write_str("failed to parse two digit integer as day")
|
||||
}
|
||||
ParseHourTwoDigit => {
|
||||
f.write_str("failed to parse two digit integer as hour")
|
||||
}
|
||||
ParseMinuteTwoDigit => {
|
||||
f.write_str("failed to parse two digit integer as minute")
|
||||
}
|
||||
ParseMonthTwoDigit => {
|
||||
f.write_str("failed to parse two digit integer as month")
|
||||
}
|
||||
ParseSecondTwoDigit => {
|
||||
f.write_str("failed to parse two digit integer as second")
|
||||
}
|
||||
ParseWeekNumberTwoDigit => {
|
||||
f.write_str("failed to parse two digit integer as week number")
|
||||
}
|
||||
ParseWeekdayOneDigit => {
|
||||
f.write_str("failed to parse one digit integer as weekday")
|
||||
}
|
||||
ParseYearFourDigit => {
|
||||
f.write_str("failed to parse four digit integer as year")
|
||||
}
|
||||
ParseYearSixDigit => {
|
||||
f.write_str("failed to parse six digit integer as year")
|
||||
}
|
||||
PrintTimeZoneFailure => f.write_str(
|
||||
"time zones without IANA identifiers that aren't either \
|
||||
fixed offsets or a POSIX time zone can't be serialized \
|
||||
(this typically occurs when this is a system time zone \
|
||||
derived from `/etc/localtime` on Unix systems that \
|
||||
isn't symlinked to an entry in `/usr/share/zoneinfo`)",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
115
src/error/fmt/util.rs
Normal file
115
src/error/fmt/util.rs
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
use crate::{error, Unit};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
ConversionToSecondsFailed { unit: Unit },
|
||||
EmptyDuration,
|
||||
FailedValueSet { unit: Unit },
|
||||
InvalidFraction,
|
||||
InvalidFractionNanos,
|
||||
MissingFractionalDigits,
|
||||
NotAllowedCalendarUnit { unit: Unit },
|
||||
NotAllowedFractionalUnit { found: Unit },
|
||||
NotAllowedNegative,
|
||||
OutOfOrderHMS { found: Unit },
|
||||
OutOfOrderUnits { found: Unit, previous: Unit },
|
||||
OverflowForUnit { unit: Unit },
|
||||
OverflowForUnitFractional { unit: Unit },
|
||||
SignedOverflowForUnit { unit: Unit },
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::FmtUtil(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
ConversionToSecondsFailed { unit } => write!(
|
||||
f,
|
||||
"converting {unit} to seconds overflows \
|
||||
a signed 64-bit integer",
|
||||
unit = unit.plural(),
|
||||
),
|
||||
EmptyDuration => f.write_str("no parsed duration components"),
|
||||
FailedValueSet { unit } => write!(
|
||||
f,
|
||||
"failed to set value for {unit} unit on span",
|
||||
unit = unit.singular(),
|
||||
),
|
||||
InvalidFraction => f.write_str(
|
||||
"failed to parse fractional component \
|
||||
(up to 9 digits, nanosecond precision is allowed)",
|
||||
),
|
||||
InvalidFractionNanos => f.write_str(
|
||||
"failed to set nanosecond value from fractional component",
|
||||
),
|
||||
MissingFractionalDigits => f.write_str(
|
||||
"found decimal after seconds component, \
|
||||
but did not find any digits after decimal",
|
||||
),
|
||||
NotAllowedCalendarUnit { unit } => write!(
|
||||
f,
|
||||
"parsing calendar units ({unit} in this case) \
|
||||
in this context is not supported \
|
||||
(perhaps try parsing into a `jiff::Span` instead)",
|
||||
unit = unit.plural(),
|
||||
),
|
||||
NotAllowedFractionalUnit { found } => write!(
|
||||
f,
|
||||
"fractional {found} are not supported",
|
||||
found = found.plural(),
|
||||
),
|
||||
NotAllowedNegative => f.write_str(
|
||||
"cannot parse negative duration into unsigned \
|
||||
`std::time::Duration`",
|
||||
),
|
||||
OutOfOrderHMS { found } => write!(
|
||||
f,
|
||||
"found `HH:MM:SS` after unit {found}, \
|
||||
but `HH:MM:SS` can only appear after \
|
||||
years, months, weeks or days",
|
||||
found = found.singular(),
|
||||
),
|
||||
OutOfOrderUnits { found, previous } => write!(
|
||||
f,
|
||||
"found value with unit {found} \
|
||||
after unit {previous}, but units must be \
|
||||
written from largest to smallest \
|
||||
(and they can't be repeated)",
|
||||
found = found.singular(),
|
||||
previous = previous.singular(),
|
||||
),
|
||||
OverflowForUnit { unit } => write!(
|
||||
f,
|
||||
"accumulated duration \
|
||||
overflowed when adding value to unit {unit}",
|
||||
unit = unit.singular(),
|
||||
),
|
||||
OverflowForUnitFractional { unit } => write!(
|
||||
f,
|
||||
"accumulated duration \
|
||||
overflowed when adding fractional value to unit {unit}",
|
||||
unit = unit.singular(),
|
||||
),
|
||||
SignedOverflowForUnit { unit } => write!(
|
||||
f,
|
||||
"value for {unit} is too big (or small) to fit into \
|
||||
a signed 64-bit integer",
|
||||
unit = unit.plural(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,14 @@
|
|||
use crate::{shared::util::error::Error as SharedError, util::sync::Arc};
|
||||
|
||||
/// Creates a new ad hoc error with no causal chain.
|
||||
///
|
||||
/// This accepts the same arguments as the `format!` macro. The error it
|
||||
/// creates is just a wrapper around the string created by `format!`.
|
||||
macro_rules! err {
|
||||
($($tt:tt)*) => {{
|
||||
crate::error::Error::adhoc_from_args(format_args!($($tt)*))
|
||||
}}
|
||||
}
|
||||
|
||||
pub(crate) use err;
|
||||
pub(crate) mod civil;
|
||||
pub(crate) mod duration;
|
||||
pub(crate) mod fmt;
|
||||
pub(crate) mod signed_duration;
|
||||
pub(crate) mod span;
|
||||
pub(crate) mod timestamp;
|
||||
pub(crate) mod tz;
|
||||
pub(crate) mod util;
|
||||
pub(crate) mod zoned;
|
||||
|
||||
/// An error that can occur in this crate.
|
||||
///
|
||||
|
|
@ -65,50 +63,6 @@ struct ErrorInner {
|
|||
cause: Option<Error>,
|
||||
}
|
||||
|
||||
/// The underlying kind of a [`Error`].
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(not(feature = "alloc"), derive(Clone))]
|
||||
enum ErrorKind {
|
||||
/// An ad hoc error that is constructed from anything that implements
|
||||
/// the `core::fmt::Display` trait.
|
||||
///
|
||||
/// In theory we try to avoid these, but they tend to be awfully
|
||||
/// convenient. In practice, we use them a lot, and only use a structured
|
||||
/// representation when a lot of different error cases fit neatly into a
|
||||
/// structure (like range errors).
|
||||
Adhoc(AdhocError),
|
||||
/// An error that occurs when a number is not within its allowed range.
|
||||
///
|
||||
/// This can occur directly as a result of a number provided by the caller
|
||||
/// of a public API, or as a result of an operation on a number that
|
||||
/// results in it being out of range.
|
||||
Range(RangeError),
|
||||
/// An error that occurs within `jiff::shared`.
|
||||
///
|
||||
/// It has its own error type to avoid bringing in this much bigger error
|
||||
/// type.
|
||||
Shared(SharedError),
|
||||
/// An error associated with a file path.
|
||||
///
|
||||
/// This is generally expected to always have a cause attached to it
|
||||
/// explaining what went wrong. The error variant is just a path to make
|
||||
/// it composable with other error types.
|
||||
///
|
||||
/// The cause is typically `Adhoc` or `IO`.
|
||||
///
|
||||
/// When `std` is not enabled, this variant can never be constructed.
|
||||
#[allow(dead_code)] // not used in some feature configs
|
||||
FilePath(FilePathError),
|
||||
/// An error that occurs when interacting with the file system.
|
||||
///
|
||||
/// This is effectively a wrapper around `std::io::Error` coupled with a
|
||||
/// `std::path::PathBuf`.
|
||||
///
|
||||
/// When `std` is not enabled, this variant can never be constructed.
|
||||
#[allow(dead_code)] // not used in some feature configs
|
||||
IO(IOError),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
/// Creates a new error value from `core::fmt::Arguments`.
|
||||
///
|
||||
|
|
@ -132,6 +86,11 @@ impl Error {
|
|||
Error::from(ErrorKind::Adhoc(AdhocError::from_args(message)))
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "perf-inline", inline(always))]
|
||||
pub(crate) fn context(self, consequent: impl IntoError) -> Error {
|
||||
self.context_impl(consequent.into_error())
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
#[cold]
|
||||
fn context_impl(self, consequent: Error) -> Error {
|
||||
|
|
@ -139,7 +98,7 @@ impl Error {
|
|||
{
|
||||
let mut err = consequent;
|
||||
if err.inner.is_none() {
|
||||
err = err!("unknown jiff error");
|
||||
err = Error::from(ErrorKind::Unknown);
|
||||
}
|
||||
let inner = err.inner.as_mut().unwrap();
|
||||
assert!(
|
||||
|
|
@ -160,51 +119,6 @@ impl Error {
|
|||
}
|
||||
|
||||
impl Error {
|
||||
/// Creates a new "ad hoc" error value.
|
||||
///
|
||||
/// An ad hoc error value is just an opaque string.
|
||||
#[cfg(feature = "alloc")]
|
||||
#[inline(never)]
|
||||
#[cold]
|
||||
pub(crate) fn adhoc<'a>(message: impl core::fmt::Display + 'a) -> Error {
|
||||
Error::from(ErrorKind::Adhoc(AdhocError::from_display(message)))
|
||||
}
|
||||
|
||||
/// Like `Error::adhoc`, but accepts a `core::fmt::Arguments`.
|
||||
///
|
||||
/// This is used with the `err!` macro so that we can thread a
|
||||
/// `core::fmt::Arguments` down. This lets us extract a `&'static str`
|
||||
/// from some messages in core-only mode and provide somewhat decent error
|
||||
/// messages in some cases.
|
||||
#[inline(never)]
|
||||
#[cold]
|
||||
pub(crate) fn adhoc_from_args<'a>(
|
||||
message: core::fmt::Arguments<'a>,
|
||||
) -> Error {
|
||||
Error::from(ErrorKind::Adhoc(AdhocError::from_args(message)))
|
||||
}
|
||||
|
||||
/// Like `Error::adhoc`, but creates an error from a `String` directly.
|
||||
///
|
||||
/// This exists to explicitly monomorphize a very common case.
|
||||
#[cfg(feature = "alloc")]
|
||||
#[inline(never)]
|
||||
#[cold]
|
||||
fn adhoc_from_string(message: alloc::string::String) -> Error {
|
||||
Error::adhoc(message)
|
||||
}
|
||||
|
||||
/// Like `Error::adhoc`, but creates an error from a `&'static str`
|
||||
/// directly.
|
||||
///
|
||||
/// This is useful in contexts where you know you have a `&'static str`,
|
||||
/// and avoids relying on `alloc`-only routines like `Error::adhoc`.
|
||||
#[inline(never)]
|
||||
#[cold]
|
||||
pub(crate) fn adhoc_from_static_str(message: &'static str) -> Error {
|
||||
Error::from(ErrorKind::Adhoc(AdhocError::from_static_str(message)))
|
||||
}
|
||||
|
||||
/// Creates a new error indicating that a `given` value is out of the
|
||||
/// specified `min..=max` range. The given `what` label is used in the
|
||||
/// error message as a human readable description of what exactly is out
|
||||
|
|
@ -220,6 +134,18 @@ impl Error {
|
|||
Error::from(ErrorKind::Range(RangeError::new(what, given, min, max)))
|
||||
}
|
||||
|
||||
/// Creates a new error indicating that a `given` value is out of the
|
||||
/// allowed range.
|
||||
///
|
||||
/// This is similar to `Error::range`, but the error message doesn't
|
||||
/// include the illegal value or the allowed range. This is useful for
|
||||
/// ad hoc range errors but should generally be used sparingly.
|
||||
#[inline(never)]
|
||||
#[cold]
|
||||
pub(crate) fn slim_range(what: &'static str) -> Error {
|
||||
Error::from(ErrorKind::SlimRange(SlimRangeError::new(what)))
|
||||
}
|
||||
|
||||
/// Creates a new error from the special "shared" error type.
|
||||
pub(crate) fn shared(err: SharedError) -> Error {
|
||||
Error::from(ErrorKind::Shared(err))
|
||||
|
|
@ -328,14 +254,90 @@ impl core::fmt::Debug for Error {
|
|||
}
|
||||
}
|
||||
|
||||
/// The underlying kind of a [`Error`].
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(not(feature = "alloc"), derive(Clone))]
|
||||
enum ErrorKind {
|
||||
Adhoc(AdhocError),
|
||||
Civil(self::civil::Error),
|
||||
Duration(self::duration::Error),
|
||||
#[allow(dead_code)] // not used in some feature configs
|
||||
FilePath(FilePathError),
|
||||
Fmt(self::fmt::Error),
|
||||
FmtFriendly(self::fmt::friendly::Error),
|
||||
FmtOffset(self::fmt::offset::Error),
|
||||
FmtRfc2822(self::fmt::rfc2822::Error),
|
||||
FmtRfc9557(self::fmt::rfc9557::Error),
|
||||
FmtTemporal(self::fmt::temporal::Error),
|
||||
FmtUtil(self::fmt::util::Error),
|
||||
FmtStrtime(self::fmt::strtime::Error),
|
||||
FmtStrtimeFormat(self::fmt::strtime::FormatError),
|
||||
FmtStrtimeParse(self::fmt::strtime::ParseError),
|
||||
#[allow(dead_code)] // not used in some feature configs
|
||||
IO(IOError),
|
||||
OsStrUtf8(self::util::OsStrUtf8Error),
|
||||
ParseInt(self::util::ParseIntError),
|
||||
ParseFraction(self::util::ParseFractionError),
|
||||
Range(RangeError),
|
||||
RoundingIncrement(self::util::RoundingIncrementError),
|
||||
Shared(SharedError),
|
||||
SignedDuration(self::signed_duration::Error),
|
||||
SlimRange(SlimRangeError),
|
||||
Span(self::span::Error),
|
||||
Timestamp(self::timestamp::Error),
|
||||
TzAmbiguous(self::tz::ambiguous::Error),
|
||||
TzDb(self::tz::db::Error),
|
||||
TzConcatenated(self::tz::concatenated::Error),
|
||||
TzOffset(self::tz::offset::Error),
|
||||
TzPosix(self::tz::posix::Error),
|
||||
TzSystem(self::tz::system::Error),
|
||||
TzTimeZone(self::tz::timezone::Error),
|
||||
#[allow(dead_code)]
|
||||
TzZic(self::tz::zic::Error),
|
||||
Unknown,
|
||||
Zoned(self::zoned::Error),
|
||||
}
|
||||
|
||||
impl core::fmt::Display for ErrorKind {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::ErrorKind::*;
|
||||
|
||||
match *self {
|
||||
ErrorKind::Adhoc(ref msg) => msg.fmt(f),
|
||||
ErrorKind::Range(ref err) => err.fmt(f),
|
||||
ErrorKind::Shared(ref err) => err.fmt(f),
|
||||
ErrorKind::FilePath(ref err) => err.fmt(f),
|
||||
ErrorKind::IO(ref err) => err.fmt(f),
|
||||
Adhoc(ref msg) => msg.fmt(f),
|
||||
Civil(ref err) => err.fmt(f),
|
||||
Duration(ref err) => err.fmt(f),
|
||||
FilePath(ref err) => err.fmt(f),
|
||||
Fmt(ref err) => err.fmt(f),
|
||||
FmtFriendly(ref err) => err.fmt(f),
|
||||
FmtOffset(ref err) => err.fmt(f),
|
||||
FmtRfc2822(ref err) => err.fmt(f),
|
||||
FmtRfc9557(ref err) => err.fmt(f),
|
||||
FmtUtil(ref err) => err.fmt(f),
|
||||
FmtStrtime(ref err) => err.fmt(f),
|
||||
FmtStrtimeFormat(ref err) => err.fmt(f),
|
||||
FmtStrtimeParse(ref err) => err.fmt(f),
|
||||
FmtTemporal(ref err) => err.fmt(f),
|
||||
IO(ref err) => err.fmt(f),
|
||||
OsStrUtf8(ref err) => err.fmt(f),
|
||||
ParseInt(ref err) => err.fmt(f),
|
||||
ParseFraction(ref err) => err.fmt(f),
|
||||
Range(ref err) => err.fmt(f),
|
||||
RoundingIncrement(ref err) => err.fmt(f),
|
||||
Shared(ref err) => err.fmt(f),
|
||||
SignedDuration(ref err) => err.fmt(f),
|
||||
SlimRange(ref err) => err.fmt(f),
|
||||
Span(ref err) => err.fmt(f),
|
||||
Timestamp(ref err) => err.fmt(f),
|
||||
TzAmbiguous(ref err) => err.fmt(f),
|
||||
TzDb(ref err) => err.fmt(f),
|
||||
TzConcatenated(ref err) => err.fmt(f),
|
||||
TzOffset(ref err) => err.fmt(f),
|
||||
TzPosix(ref err) => err.fmt(f),
|
||||
TzSystem(ref err) => err.fmt(f),
|
||||
TzTimeZone(ref err) => err.fmt(f),
|
||||
TzZic(ref err) => err.fmt(f),
|
||||
Unknown => f.write_str("unknown jiff error"),
|
||||
Zoned(ref err) => err.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -368,18 +370,13 @@ struct AdhocError {
|
|||
}
|
||||
|
||||
impl AdhocError {
|
||||
#[cfg(feature = "alloc")]
|
||||
fn from_display<'a>(message: impl core::fmt::Display + 'a) -> AdhocError {
|
||||
use alloc::string::ToString;
|
||||
|
||||
let message = message.to_string().into_boxed_str();
|
||||
AdhocError { message }
|
||||
}
|
||||
|
||||
fn from_args<'a>(message: core::fmt::Arguments<'a>) -> AdhocError {
|
||||
#[cfg(feature = "alloc")]
|
||||
{
|
||||
AdhocError::from_display(message)
|
||||
use alloc::string::ToString;
|
||||
|
||||
let message = message.to_string().into_boxed_str();
|
||||
AdhocError { message }
|
||||
}
|
||||
#[cfg(not(feature = "alloc"))]
|
||||
{
|
||||
|
|
@ -387,17 +384,6 @@ impl AdhocError {
|
|||
"unknown Jiff error (better error messages require \
|
||||
enabling the `alloc` feature for the `jiff` crate)",
|
||||
);
|
||||
AdhocError::from_static_str(message)
|
||||
}
|
||||
}
|
||||
|
||||
fn from_static_str(message: &'static str) -> AdhocError {
|
||||
#[cfg(feature = "alloc")]
|
||||
{
|
||||
AdhocError::from_display(message)
|
||||
}
|
||||
#[cfg(not(feature = "alloc"))]
|
||||
{
|
||||
AdhocError { message }
|
||||
}
|
||||
}
|
||||
|
|
@ -476,6 +462,32 @@ impl core::fmt::Display for RangeError {
|
|||
}
|
||||
}
|
||||
|
||||
/// A slim error that occurs when an input value is out of bounds.
|
||||
///
|
||||
/// Unlike `RangeError`, this only includes a static description of the
|
||||
/// value that is out of bounds. It doesn't include the out-of-range value
|
||||
/// or the min/max values.
|
||||
#[derive(Clone, Debug)]
|
||||
struct SlimRangeError {
|
||||
what: &'static str,
|
||||
}
|
||||
|
||||
impl SlimRangeError {
|
||||
fn new(what: &'static str) -> SlimRangeError {
|
||||
SlimRangeError { what }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for SlimRangeError {}
|
||||
|
||||
impl core::fmt::Display for SlimRangeError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
let SlimRangeError { what } = *self;
|
||||
write!(f, "parameter '{what}' is not in the required range")
|
||||
}
|
||||
}
|
||||
|
||||
/// A `std::io::Error`.
|
||||
///
|
||||
/// This type is itself always available, even when the `std` feature is not
|
||||
|
|
@ -581,21 +593,6 @@ impl IntoError for Error {
|
|||
}
|
||||
}
|
||||
|
||||
impl IntoError for &'static str {
|
||||
#[inline(always)]
|
||||
fn into_error(self) -> Error {
|
||||
Error::adhoc_from_static_str(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
impl IntoError for alloc::string::String {
|
||||
#[inline(always)]
|
||||
fn into_error(self) -> Error {
|
||||
Error::adhoc_from_string(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for contextualizing error values.
|
||||
///
|
||||
/// This makes it easy to contextualize either `Error` or `Result<T, Error>`.
|
||||
|
|
@ -603,7 +600,7 @@ impl IntoError for alloc::string::String {
|
|||
/// `map_err` everywhere one wants to add context to an error.
|
||||
///
|
||||
/// This trick was borrowed from `anyhow`.
|
||||
pub(crate) trait ErrorContext {
|
||||
pub(crate) trait ErrorContext<T, E> {
|
||||
/// Contextualize the given consequent error with this (`self`) error as
|
||||
/// the cause.
|
||||
///
|
||||
|
|
@ -612,7 +609,7 @@ pub(crate) trait ErrorContext {
|
|||
/// Note that if an `Error` is given for `kind`, then this panics if it has
|
||||
/// a cause. (Because the cause would otherwise be dropped. An error causal
|
||||
/// chain is just a linked list, not a tree.)
|
||||
fn context(self, consequent: impl IntoError) -> Self;
|
||||
fn context(self, consequent: impl IntoError) -> Result<T, Error>;
|
||||
|
||||
/// Like `context`, but hides error construction within a closure.
|
||||
///
|
||||
|
|
@ -623,47 +620,31 @@ pub(crate) trait ErrorContext {
|
|||
///
|
||||
/// Usually this only makes sense to use on a `Result<T, Error>`, otherwise
|
||||
/// the closure is just executed immediately anyway.
|
||||
fn with_context<E: IntoError>(
|
||||
fn with_context<C: IntoError>(
|
||||
self,
|
||||
consequent: impl FnOnce() -> E,
|
||||
) -> Self;
|
||||
consequent: impl FnOnce() -> C,
|
||||
) -> Result<T, Error>;
|
||||
}
|
||||
|
||||
impl ErrorContext for Error {
|
||||
#[cfg_attr(feature = "perf-inline", inline(always))]
|
||||
fn context(self, consequent: impl IntoError) -> Error {
|
||||
self.context_impl(consequent.into_error())
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "perf-inline", inline(always))]
|
||||
fn with_context<E: IntoError>(
|
||||
self,
|
||||
consequent: impl FnOnce() -> E,
|
||||
) -> Error {
|
||||
self.context_impl(consequent().into_error())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ErrorContext for Result<T, Error> {
|
||||
impl<T, E> ErrorContext<T, E> for Result<T, E>
|
||||
where
|
||||
E: IntoError,
|
||||
{
|
||||
#[cfg_attr(feature = "perf-inline", inline(always))]
|
||||
fn context(self, consequent: impl IntoError) -> Result<T, Error> {
|
||||
self.map_err(
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
|err| err.context_impl(consequent.into_error()),
|
||||
)
|
||||
self.map_err(|err| {
|
||||
err.into_error().context_impl(consequent.into_error())
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "perf-inline", inline(always))]
|
||||
fn with_context<E: IntoError>(
|
||||
fn with_context<C: IntoError>(
|
||||
self,
|
||||
consequent: impl FnOnce() -> E,
|
||||
consequent: impl FnOnce() -> C,
|
||||
) -> Result<T, Error> {
|
||||
self.map_err(
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
|err| err.context_impl(consequent().into_error()),
|
||||
)
|
||||
self.map_err(|err| {
|
||||
err.into_error().context_impl(consequent().into_error())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -698,7 +679,12 @@ mod tests {
|
|||
// then we could make `Error` a zero sized type. Which might
|
||||
// actually be the right trade-off for core-only, but I'll hold off
|
||||
// until we have some real world use cases.
|
||||
expected_size *= 3;
|
||||
//
|
||||
// OK... after switching to structured errors, this jumped
|
||||
// back up to `expected_size *= 6`. And that was with me being
|
||||
// conscientious about what data we store inside of error types.
|
||||
// Blech.
|
||||
expected_size *= 6;
|
||||
}
|
||||
assert_eq!(expected_size, core::mem::size_of::<Error>());
|
||||
}
|
||||
55
src/error/signed_duration.rs
Normal file
55
src/error/signed_duration.rs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
use crate::{error, Unit};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
ConvertNonFinite,
|
||||
ConvertSystemTime,
|
||||
RoundCalendarUnit { unit: Unit },
|
||||
RoundOverflowed { unit: Unit },
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::SignedDuration(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
ConvertNonFinite => f.write_str(
|
||||
"could not convert non-finite \
|
||||
floating point seconds to signed duration \
|
||||
(floating point seconds must be finite)",
|
||||
),
|
||||
ConvertSystemTime => f.write_str(
|
||||
"failed to get duration between \
|
||||
`std::time::SystemTime` values",
|
||||
),
|
||||
RoundCalendarUnit { unit } => write!(
|
||||
f,
|
||||
"rounding `jiff::SignedDuration` failed because \
|
||||
a calendar unit of '{plural}' was provided \
|
||||
(to round by calendar units, you must use a `jiff::Span`)",
|
||||
plural = unit.plural(),
|
||||
),
|
||||
RoundOverflowed { unit } => write!(
|
||||
f,
|
||||
"rounding signed duration to nearest {singular} \
|
||||
resulted in a value outside the supported range \
|
||||
of a `jiff::SignedDuration`",
|
||||
singular = unit.singular(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
139
src/error/span.rs
Normal file
139
src/error/span.rs
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
use crate::{error, Unit};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
ConvertDateTimeToTimestamp,
|
||||
ConvertNanoseconds { unit: Unit },
|
||||
ConvertNegative,
|
||||
ConvertSpanToSignedDuration,
|
||||
FailedSpanBetweenDateTimes { unit: Unit },
|
||||
FailedSpanBetweenZonedDateTimes { unit: Unit },
|
||||
NotAllowedCalendarUnits { unit: Unit },
|
||||
NotAllowedLargestSmallerThanSmallest { smallest: Unit, largest: Unit },
|
||||
OptionLargest,
|
||||
OptionLargestInSpan,
|
||||
OptionSmallest,
|
||||
RequiresRelativeWeekOrDay { unit: Unit },
|
||||
RequiresRelativeYearOrMonth { unit: Unit },
|
||||
RequiresRelativeYearOrMonthGivenDaysAre24Hours { unit: Unit },
|
||||
ToDurationCivil,
|
||||
ToDurationDaysAre24Hours,
|
||||
ToDurationZoned,
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::Span(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
ConvertDateTimeToTimestamp => f.write_str(
|
||||
"failed to interpret datetime in UTC \
|
||||
in order to convert it to a timestamp",
|
||||
),
|
||||
ConvertNanoseconds { unit } => write!(
|
||||
f,
|
||||
"failed to convert rounded nanoseconds \
|
||||
to span for largest unit set to '{unit}'",
|
||||
unit = unit.plural(),
|
||||
),
|
||||
ConvertNegative => f.write_str(
|
||||
"cannot convert negative span \
|
||||
to unsigned `std::time::Duration`",
|
||||
),
|
||||
ConvertSpanToSignedDuration => f.write_str(
|
||||
"failed to convert span to duration without relative datetime \
|
||||
(must use `jiff::Span::to_duration` instead)",
|
||||
),
|
||||
FailedSpanBetweenDateTimes { unit } => write!(
|
||||
f,
|
||||
"failed to get span between datetimes \
|
||||
with largest unit set to '{unit}'",
|
||||
unit = unit.plural(),
|
||||
),
|
||||
FailedSpanBetweenZonedDateTimes { unit } => write!(
|
||||
f,
|
||||
"failed to get span between zoned datetimes \
|
||||
with largest unit set to '{unit}'",
|
||||
unit = unit.plural(),
|
||||
),
|
||||
OptionLargest => {
|
||||
f.write_str("error with `largest` rounding option")
|
||||
}
|
||||
OptionLargestInSpan => {
|
||||
f.write_str("error with largest unit in span to be rounded")
|
||||
}
|
||||
OptionSmallest => {
|
||||
f.write_str("error with `smallest` rounding option")
|
||||
}
|
||||
NotAllowedCalendarUnits { unit } => write!(
|
||||
f,
|
||||
"operation can only be performed with units of hours \
|
||||
or smaller, but found non-zero '{unit}' units \
|
||||
(operations on `jiff::Timestamp`, `jiff::tz::Offset` \
|
||||
and `jiff::civil::Time` don't support calendar \
|
||||
units in a `jiff::Span`)",
|
||||
unit = unit.singular(),
|
||||
),
|
||||
NotAllowedLargestSmallerThanSmallest { smallest, largest } => {
|
||||
write!(
|
||||
f,
|
||||
"largest unit ('{largest}') cannot be smaller than \
|
||||
smallest unit ('{smallest}')",
|
||||
largest = largest.singular(),
|
||||
smallest = smallest.singular(),
|
||||
)
|
||||
}
|
||||
RequiresRelativeWeekOrDay { unit } => write!(
|
||||
f,
|
||||
"using unit '{unit}' in a span or configuration \
|
||||
requires that either a relative reference time be given \
|
||||
or `jiff::SpanRelativeTo::days_are_24_hours()` is used to \
|
||||
indicate invariant 24-hour days, \
|
||||
but neither were provided",
|
||||
unit = unit.singular(),
|
||||
),
|
||||
RequiresRelativeYearOrMonth { unit } => write!(
|
||||
f,
|
||||
"using unit '{unit}' in a span or configuration \
|
||||
requires that a relative reference time be given, \
|
||||
but none was provided",
|
||||
unit = unit.singular(),
|
||||
),
|
||||
RequiresRelativeYearOrMonthGivenDaysAre24Hours { unit } => write!(
|
||||
f,
|
||||
"using unit '{unit}' in span or configuration \
|
||||
requires that a relative reference time be given \
|
||||
(`jiff::SpanRelativeTo::days_are_24_hours()` was given \
|
||||
but this only permits using days and weeks \
|
||||
without a relative reference time)",
|
||||
unit = unit.singular(),
|
||||
),
|
||||
ToDurationCivil => f.write_str(
|
||||
"could not compute normalized relative span \
|
||||
from civil datetime",
|
||||
),
|
||||
ToDurationDaysAre24Hours => f.write_str(
|
||||
"could not compute normalized relative span \
|
||||
when all days are assumed to be 24 hours",
|
||||
),
|
||||
ToDurationZoned => f.write_str(
|
||||
"could not compute normalized relative span \
|
||||
from zoned datetime",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
38
src/error/timestamp.rs
Normal file
38
src/error/timestamp.rs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
use crate::error;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
OverflowAddDuration,
|
||||
OverflowAddSpan,
|
||||
RequiresSaturatingTimeUnits,
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::Timestamp(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
OverflowAddDuration => {
|
||||
f.write_str("adding duration overflowed timestamp")
|
||||
}
|
||||
OverflowAddSpan => f.write_str("adding span overflowed timestamp"),
|
||||
RequiresSaturatingTimeUnits => f.write_str(
|
||||
"saturating timestamp arithmetic requires only time units",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
49
src/error/tz/ambiguous.rs
Normal file
49
src/error/tz/ambiguous.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
use crate::{
|
||||
error,
|
||||
tz::{Offset, TimeZone},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
BecauseFold { before: Offset, after: Offset },
|
||||
BecauseGap { before: Offset, after: Offset },
|
||||
InTimeZone { tz: TimeZone },
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::TzAmbiguous(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
BecauseFold { before, after } => write!(
|
||||
f,
|
||||
"datetime is ambiguous since it falls into a \
|
||||
fold between offsets {before} and {after}",
|
||||
),
|
||||
BecauseGap { before, after } => write!(
|
||||
f,
|
||||
"datetime is ambiguous since it falls into a \
|
||||
gap between offsets {before} and {after}",
|
||||
),
|
||||
InTimeZone { ref tz } => write!(
|
||||
f,
|
||||
"error converting datetime to instant in time zone {tz}",
|
||||
tz = tz.diagnostic_name(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
119
src/error/tz/concatenated.rs
Normal file
119
src/error/tz/concatenated.rs
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
use crate::error;
|
||||
|
||||
// At time of writing, the biggest TZif data file is a few KB. And the
|
||||
// index block is tens of KB. So impose a limit that is a couple of orders
|
||||
// of magnitude bigger, but still overall pretty small for... some systems.
|
||||
// Anyway, I welcome improvements to this heuristic!
|
||||
pub(crate) const ALLOC_LIMIT: usize = 10 * 1 << 20;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
AllocRequestOverLimit,
|
||||
AllocFailed,
|
||||
AllocOverflow,
|
||||
ExpectedFirstSixBytes,
|
||||
ExpectedIanaName,
|
||||
ExpectedLastByte,
|
||||
#[cfg(test)]
|
||||
ExpectedMoreData,
|
||||
ExpectedVersion,
|
||||
FailedReadData,
|
||||
FailedReadHeader,
|
||||
FailedReadIndex,
|
||||
#[cfg(all(feature = "std", all(not(unix), not(windows))))]
|
||||
FailedSeek,
|
||||
InvalidIndexDataOffsets,
|
||||
InvalidLengthIndexBlock,
|
||||
#[cfg(all(feature = "std", windows))]
|
||||
InvalidOffsetOverflowFile,
|
||||
#[cfg(test)]
|
||||
InvalidOffsetOverflowSlice,
|
||||
#[cfg(test)]
|
||||
InvalidOffsetTooBig,
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::TzConcatenated(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
AllocRequestOverLimit => write!(
|
||||
f,
|
||||
"attempted to allocate more than {ALLOC_LIMIT} bytes \
|
||||
while reading concatenated TZif data, which \
|
||||
exceeds a heuristic limit to prevent huge allocations \
|
||||
(please file a bug if this error is inappropriate)",
|
||||
),
|
||||
AllocFailed => f.write_str(
|
||||
"failed to allocate additional room \
|
||||
for reading concatenated TZif data",
|
||||
),
|
||||
AllocOverflow => {
|
||||
f.write_str("total allocation length overflowed `usize`")
|
||||
}
|
||||
ExpectedFirstSixBytes => f.write_str(
|
||||
"expected first 6 bytes of concatenated TZif header \
|
||||
to be `tzdata`",
|
||||
),
|
||||
ExpectedIanaName => f.write_str(
|
||||
"expected IANA time zone identifier to be valid UTF-8",
|
||||
),
|
||||
ExpectedLastByte => f.write_str(
|
||||
"expected last byte of concatenated TZif header \
|
||||
to be `NUL`",
|
||||
),
|
||||
#[cfg(test)]
|
||||
ExpectedMoreData => f.write_str(
|
||||
"unexpected EOF, expected more bytes based on size \
|
||||
of caller provided buffer",
|
||||
),
|
||||
ExpectedVersion => f.write_str(
|
||||
"expected version in concatenated TZif header to \
|
||||
be valid UTF-8",
|
||||
),
|
||||
FailedReadData => f.write_str("failed to read TZif data block"),
|
||||
FailedReadHeader => {
|
||||
f.write_str("failed to read concatenated TZif header")
|
||||
}
|
||||
FailedReadIndex => f.write_str("failed to read index block"),
|
||||
#[cfg(all(feature = "std", all(not(unix), not(windows))))]
|
||||
FailedSeek => {
|
||||
f.write_str("failed to seek to offset in `std::fs::File`")
|
||||
}
|
||||
InvalidIndexDataOffsets => f.write_str(
|
||||
"invalid index and data offsets, \
|
||||
expected index offset to be less than or equal \
|
||||
to data offset",
|
||||
),
|
||||
InvalidLengthIndexBlock => {
|
||||
f.write_str("length of index block is not a valid multiple")
|
||||
}
|
||||
#[cfg(all(feature = "std", windows))]
|
||||
InvalidOffsetOverflowFile => f.write_str(
|
||||
"offset overflow when reading from `std::fs::File`",
|
||||
),
|
||||
#[cfg(test)]
|
||||
InvalidOffsetOverflowSlice => {
|
||||
f.write_str("offset overflowed `usize`")
|
||||
}
|
||||
#[cfg(test)]
|
||||
InvalidOffsetTooBig => {
|
||||
f.write_str("offset too big for given slice of data")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
141
src/error/tz/db.rs
Normal file
141
src/error/tz/db.rs
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
use crate::error;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
#[cfg(feature = "tzdb-concatenated")]
|
||||
ConcatenatedMissingIanaIdentifiers,
|
||||
#[cfg(all(feature = "std", not(feature = "tzdb-concatenated")))]
|
||||
DisabledConcatenated,
|
||||
#[cfg(all(feature = "std", not(feature = "tzdb-zoneinfo")))]
|
||||
DisabledZoneInfo,
|
||||
FailedTimeZone {
|
||||
#[cfg(feature = "alloc")]
|
||||
name: alloc::boxed::Box<str>,
|
||||
},
|
||||
FailedTimeZoneNoDatabaseConfigured {
|
||||
#[cfg(feature = "alloc")]
|
||||
name: alloc::boxed::Box<str>,
|
||||
},
|
||||
#[cfg(feature = "tzdb-zoneinfo")]
|
||||
ZoneInfoNoTzifFiles,
|
||||
#[cfg(feature = "tzdb-zoneinfo")]
|
||||
ZoneInfoStripPrefix,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub(crate) fn failed_time_zone(_time_zone_name: &str) -> Error {
|
||||
Error::FailedTimeZone {
|
||||
#[cfg(feature = "alloc")]
|
||||
name: _time_zone_name.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn failed_time_zone_no_database_configured(
|
||||
_time_zone_name: &str,
|
||||
) -> Error {
|
||||
Error::FailedTimeZoneNoDatabaseConfigured {
|
||||
#[cfg(feature = "alloc")]
|
||||
name: _time_zone_name.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::TzDb(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
#[cfg(feature = "tzdb-concatenated")]
|
||||
ConcatenatedMissingIanaIdentifiers => f.write_str(
|
||||
"found no IANA time zone identifiers in \
|
||||
concatenated tzdata file",
|
||||
),
|
||||
#[cfg(all(feature = "std", not(feature = "tzdb-concatenated")))]
|
||||
DisabledConcatenated => f.write_str(
|
||||
"system concatenated tzdb unavailable: \
|
||||
Jiff crate feature `tzdb-concatenated` is disabled, \
|
||||
opening tzdb at given path has therefore failed",
|
||||
),
|
||||
#[cfg(all(feature = "std", not(feature = "tzdb-zoneinfo")))]
|
||||
DisabledZoneInfo => f.write_str(
|
||||
"system zoneinfo tzdb unavailable: \
|
||||
Jiff crate feature `tzdb-zoneinfo` is disabled, \
|
||||
opening tzdb at given path has therefore failed",
|
||||
),
|
||||
FailedTimeZone {
|
||||
#[cfg(feature = "alloc")]
|
||||
ref name,
|
||||
} => {
|
||||
#[cfg(feature = "alloc")]
|
||||
{
|
||||
write!(f, "failed to find time zone `{name}` in time zone database")
|
||||
}
|
||||
#[cfg(not(feature = "alloc"))]
|
||||
{
|
||||
f.write_str(
|
||||
"failed to find time zone in time zone database",
|
||||
)
|
||||
}
|
||||
}
|
||||
FailedTimeZoneNoDatabaseConfigured {
|
||||
#[cfg(feature = "alloc")]
|
||||
ref name,
|
||||
} => {
|
||||
#[cfg(feature = "std")]
|
||||
{
|
||||
write!(
|
||||
f,
|
||||
"failed to find time zone `{name}` since there is no \
|
||||
time zone database configured",
|
||||
)
|
||||
}
|
||||
#[cfg(all(not(feature = "std"), feature = "alloc"))]
|
||||
{
|
||||
write!(
|
||||
f,
|
||||
"failed to find time zone `{name}`, since there is no \
|
||||
global time zone database configured (and is \
|
||||
currently impossible to do so without Jiff's `std` \
|
||||
feature enabled, if you need this functionality, \
|
||||
please file an issue on Jiff's tracker with your \
|
||||
use case)",
|
||||
)
|
||||
}
|
||||
#[cfg(all(not(feature = "std"), not(feature = "alloc")))]
|
||||
{
|
||||
f.write_str(
|
||||
"failed to find time zone, since there is no \
|
||||
global time zone database configured (and is \
|
||||
currently impossible to do so without Jiff's `std` \
|
||||
feature enabled, if you need this functionality, \
|
||||
please file an issue on Jiff's tracker with your \
|
||||
use case)",
|
||||
)
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "tzdb-zoneinfo")]
|
||||
ZoneInfoNoTzifFiles => f.write_str(
|
||||
"did not find any TZif files in zoneinfo time zone database",
|
||||
),
|
||||
#[cfg(feature = "tzdb-zoneinfo")]
|
||||
ZoneInfoStripPrefix => f.write_str(
|
||||
"failed to strip zoneinfo time zone database directory \
|
||||
path from path to TZif file",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
8
src/error/tz/mod.rs
Normal file
8
src/error/tz/mod.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
pub(crate) mod ambiguous;
|
||||
pub(crate) mod concatenated;
|
||||
pub(crate) mod db;
|
||||
pub(crate) mod offset;
|
||||
pub(crate) mod posix;
|
||||
pub(crate) mod system;
|
||||
pub(crate) mod timezone;
|
||||
pub(crate) mod zic;
|
||||
110
src/error/tz/offset.rs
Normal file
110
src/error/tz/offset.rs
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
use crate::{
|
||||
error,
|
||||
tz::{Offset, TimeZone},
|
||||
Unit,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
ConvertDateTimeToTimestamp {
|
||||
offset: Offset,
|
||||
},
|
||||
OverflowAddSignedDuration,
|
||||
OverflowSignedDuration,
|
||||
ResolveRejectFold {
|
||||
given: Offset,
|
||||
before: Offset,
|
||||
after: Offset,
|
||||
tz: TimeZone,
|
||||
},
|
||||
ResolveRejectGap {
|
||||
given: Offset,
|
||||
before: Offset,
|
||||
after: Offset,
|
||||
tz: TimeZone,
|
||||
},
|
||||
ResolveRejectUnambiguous {
|
||||
given: Offset,
|
||||
offset: Offset,
|
||||
tz: TimeZone,
|
||||
},
|
||||
RoundInvalidUnit {
|
||||
unit: Unit,
|
||||
},
|
||||
RoundOverflow,
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::TzOffset(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
ConvertDateTimeToTimestamp { offset } => write!(
|
||||
f,
|
||||
"converting datetime with time zone offset `{offset}` \
|
||||
to timestamp overflowed",
|
||||
),
|
||||
OverflowAddSignedDuration => f.write_str(
|
||||
"adding signed duration to time zone offset overflowed",
|
||||
),
|
||||
OverflowSignedDuration => {
|
||||
f.write_str("signed duration overflows time zone offset")
|
||||
}
|
||||
ResolveRejectFold { given, before, after, ref tz } => write!(
|
||||
f,
|
||||
"datetime could not resolve to timestamp \
|
||||
since `reject` conflict resolution was chosen, and \
|
||||
because datetime has offset `{given}`, but the time \
|
||||
zone `{tzname}` for the given datetime falls in a fold \
|
||||
between offsets `{before}` and `{after}`, neither of which \
|
||||
match the offset",
|
||||
tzname = tz.diagnostic_name(),
|
||||
),
|
||||
ResolveRejectGap { given, before, after, ref tz } => write!(
|
||||
f,
|
||||
"datetime could not resolve to timestamp \
|
||||
since `reject` conflict resolution was chosen, and \
|
||||
because datetime has offset `{given}`, but the time \
|
||||
zone `{tzname}` for the given datetime falls in a gap \
|
||||
(between offsets `{before}` and `{after}`), and all \
|
||||
offsets for a gap are regarded as invalid",
|
||||
tzname = tz.diagnostic_name(),
|
||||
),
|
||||
ResolveRejectUnambiguous { given, offset, ref tz } => write!(
|
||||
f,
|
||||
"datetime could not resolve to a timestamp since \
|
||||
`reject` conflict resolution was chosen, and because \
|
||||
datetime has offset `{given}`, but the time \
|
||||
zone `{tzname}` for the given datetime \
|
||||
unambiguously has offset `{offset}`",
|
||||
tzname = tz.diagnostic_name(),
|
||||
),
|
||||
RoundInvalidUnit { unit } => write!(
|
||||
f,
|
||||
"rounding time zone offset failed because \
|
||||
a unit of {unit} was provided, \
|
||||
but time zone offset rounding \
|
||||
can only use hours, minutes or seconds",
|
||||
unit = unit.plural(),
|
||||
),
|
||||
RoundOverflow => f.write_str(
|
||||
"rounding time zone offset resulted in a duration \
|
||||
that overflows",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/error/tz/posix.rs
Normal file
35
src/error/tz/posix.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
use crate::error;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
ColonPrefixInvalidUtf8,
|
||||
InvalidPosixTz,
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::TzPosix(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
ColonPrefixInvalidUtf8 => f.write_str(
|
||||
"POSIX time zone string with a `:` prefix \
|
||||
contains invalid UTF-8",
|
||||
),
|
||||
InvalidPosixTz => f.write_str("invalid POSIX time zone string"),
|
||||
}
|
||||
}
|
||||
}
|
||||
104
src/error/tz/system.rs
Normal file
104
src/error/tz/system.rs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
#[cfg(not(feature = "tz-system"))]
|
||||
pub(crate) use self::disabled::*;
|
||||
#[cfg(feature = "tz-system")]
|
||||
pub(crate) use self::enabled::*;
|
||||
|
||||
#[cfg(not(feature = "tz-system"))]
|
||||
mod disabled {
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, _: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tz-system")]
|
||||
mod enabled {
|
||||
use crate::error;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
FailedEnvTz,
|
||||
FailedEnvTzAsTzif,
|
||||
FailedPosixTzAndUtf8,
|
||||
FailedSystemTimeZone,
|
||||
FailedUnnamedTzifInvalid,
|
||||
FailedUnnamedTzifRead,
|
||||
#[cfg(windows)]
|
||||
WindowsMissingIanaMapping,
|
||||
#[cfg(windows)]
|
||||
WindowsTimeZoneKeyName,
|
||||
#[cfg(windows)]
|
||||
WindowsUtf16DecodeInvalid,
|
||||
#[cfg(windows)]
|
||||
WindowsUtf16DecodeNul,
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::TzSystem(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
FailedEnvTz => f.write_str(
|
||||
"`TZ` environment variable set, but failed to read value",
|
||||
),
|
||||
FailedEnvTzAsTzif => f.write_str(
|
||||
"failed to read `TZ` environment variable value \
|
||||
as a TZif file after attempting (and failing) a tzdb \
|
||||
lookup for that same value",
|
||||
),
|
||||
FailedPosixTzAndUtf8 => f.write_str(
|
||||
"failed to parse `TZ` environment variable as either \
|
||||
a POSIX time zone transition string or as valid UTF-8",
|
||||
),
|
||||
FailedSystemTimeZone => {
|
||||
f.write_str("failed to find system time zone")
|
||||
}
|
||||
FailedUnnamedTzifInvalid => f.write_str(
|
||||
"found invalid TZif data in unnamed time zone file",
|
||||
),
|
||||
FailedUnnamedTzifRead => f.write_str(
|
||||
"failed to read TZif data from unnamed time zone file",
|
||||
),
|
||||
#[cfg(windows)]
|
||||
WindowsMissingIanaMapping => f.write_str(
|
||||
"found Windows time zone name, \
|
||||
but could not find a mapping for it to an \
|
||||
IANA time zone name",
|
||||
),
|
||||
#[cfg(windows)]
|
||||
WindowsTimeZoneKeyName => f.write_str(
|
||||
"could not get `TimeZoneKeyName` from \
|
||||
winapi `DYNAMIC_TIME_ZONE_INFORMATION`",
|
||||
),
|
||||
#[cfg(windows)]
|
||||
WindowsUtf16DecodeInvalid => f.write_str(
|
||||
"failed to convert `u16` slice to UTF-8 \
|
||||
(invalid UTF-16)",
|
||||
),
|
||||
#[cfg(windows)]
|
||||
WindowsUtf16DecodeNul => f.write_str(
|
||||
"failed to convert `u16` slice to UTF-8 \
|
||||
(no NUL terminator found)",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
43
src/error/tz/timezone.rs
Normal file
43
src/error/tz/timezone.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
use crate::error;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
ConvertNonFixed {
|
||||
kind: &'static str,
|
||||
},
|
||||
#[cfg(not(feature = "tz-system"))]
|
||||
FailedSystem,
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::TzTimeZone(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
ConvertNonFixed { kind } => write!(
|
||||
f,
|
||||
"cannot convert non-fixed {kind} time zone to offset \
|
||||
without a timestamp or civil datetime",
|
||||
),
|
||||
#[cfg(not(feature = "tz-system"))]
|
||||
FailedSystem => f.write_str(
|
||||
"failed to get system time zone since Jiff's \
|
||||
`tz-system` crate feature is not enabled",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
323
src/error/tz/zic.rs
Normal file
323
src/error/tz/zic.rs
Normal file
|
|
@ -0,0 +1,323 @@
|
|||
#[cfg(not(test))]
|
||||
pub(crate) use self::disabled::*;
|
||||
#[cfg(test)]
|
||||
pub(crate) use self::enabled::*;
|
||||
|
||||
#[cfg(not(test))]
|
||||
mod disabled {
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, _: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod enabled {
|
||||
use alloc::boxed::Box;
|
||||
|
||||
use crate::error;
|
||||
|
||||
// `man zic` says that the max line length including the line
|
||||
// terminator is 2048. The `core::str::Lines` iterator doesn't include
|
||||
// the terminator, so we subtract 1 to account for that. Note that this
|
||||
// could potentially allow one extra byte in the case of a \r\n line
|
||||
// terminator, but this seems fine.
|
||||
pub(crate) const MAX_LINE_LEN: usize = 2047;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
DuplicateLink { name: Box<str> },
|
||||
DuplicateLinkZone { name: Box<str> },
|
||||
DuplicateZone { name: Box<str> },
|
||||
DuplicateZoneLink { name: Box<str> },
|
||||
ExpectedCloseQuote,
|
||||
ExpectedColonAfterHour,
|
||||
ExpectedColonAfterMinute,
|
||||
ExpectedContinuationZoneThreeFields,
|
||||
ExpectedContinuationZoneLine { name: Box<str> },
|
||||
ExpectedDotAfterSeconds,
|
||||
ExpectedFirstZoneFourFields,
|
||||
ExpectedLinkTwoFields,
|
||||
ExpectedMinuteAfterHours,
|
||||
ExpectedNameBegin,
|
||||
ExpectedNanosecondDigits,
|
||||
ExpectedNonEmptyAbbreviation,
|
||||
ExpectedNonEmptyAt,
|
||||
ExpectedNonEmptyName,
|
||||
ExpectedNonEmptySave,
|
||||
ExpectedNonEmptyZoneName,
|
||||
ExpectedNothingAfterTime,
|
||||
ExpectedRuleNineFields { got: usize },
|
||||
ExpectedSecondAfterMinutes,
|
||||
ExpectedTimeOneHour,
|
||||
ExpectedUntilYear,
|
||||
ExpectedWhitespaceAfterQuotedField,
|
||||
ExpectedZoneNameComponentNoDots { component: Box<str> },
|
||||
FailedContinuationZone,
|
||||
FailedLinkLine,
|
||||
FailedParseDay,
|
||||
FailedParseFieldAt,
|
||||
FailedParseFieldFormat,
|
||||
FailedParseFieldFrom,
|
||||
FailedParseFieldIn,
|
||||
FailedParseFieldLetters,
|
||||
FailedParseFieldLinkName,
|
||||
FailedParseFieldLinkTarget,
|
||||
FailedParseFieldName,
|
||||
FailedParseFieldOn,
|
||||
FailedParseFieldRules,
|
||||
FailedParseFieldSave,
|
||||
FailedParseFieldStdOff,
|
||||
FailedParseFieldTo,
|
||||
FailedParseFieldUntil,
|
||||
FailedParseHour,
|
||||
FailedParseMinute,
|
||||
FailedParseMonth,
|
||||
FailedParseNanosecond,
|
||||
FailedParseSecond,
|
||||
FailedParseTimeDuration,
|
||||
FailedParseYear,
|
||||
FailedRule { name: Box<str> },
|
||||
FailedRuleLine,
|
||||
FailedZoneFirst,
|
||||
Line { number: usize },
|
||||
LineMaxLength,
|
||||
LineNul,
|
||||
LineOverflow,
|
||||
InvalidAbbreviation,
|
||||
InvalidRuleYear { start: i16, end: i16 },
|
||||
InvalidUtf8,
|
||||
UnrecognizedAtTimeSuffix,
|
||||
UnrecognizedDayOfMonthFormat,
|
||||
UnrecognizedDayOfWeek,
|
||||
UnrecognizedMonthName,
|
||||
UnrecognizedSaveTimeSuffix,
|
||||
UnrecognizedTrailingTimeDuration,
|
||||
UnrecognizedZicLine,
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::TzZic(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
DuplicateLink { ref name } => {
|
||||
write!(f, "found duplicate link with name `{name}`")
|
||||
}
|
||||
DuplicateLinkZone { ref name } => write!(
|
||||
f,
|
||||
"found link with name `{name}` that conflicts \
|
||||
with a zone of the same name",
|
||||
),
|
||||
DuplicateZone { ref name } => {
|
||||
write!(f, "found duplicate zone with name `{name}`")
|
||||
}
|
||||
DuplicateZoneLink { ref name } => write!(
|
||||
f,
|
||||
"found zone with name `{name}` that conflicts \
|
||||
with a link of the same name",
|
||||
),
|
||||
ExpectedCloseQuote => {
|
||||
f.write_str("found unclosed quote for field")
|
||||
}
|
||||
ExpectedColonAfterHour => {
|
||||
f.write_str("expected `:` after hours")
|
||||
}
|
||||
ExpectedColonAfterMinute => {
|
||||
f.write_str("expected `:` after minutes")
|
||||
}
|
||||
ExpectedContinuationZoneLine { ref name } => write!(
|
||||
f,
|
||||
"expected continuation zone line for `{name}`, \
|
||||
but found end of data instead",
|
||||
),
|
||||
ExpectedContinuationZoneThreeFields => f.write_str(
|
||||
"expected continuation `ZONE` line \
|
||||
to have at least 3 fields",
|
||||
),
|
||||
ExpectedDotAfterSeconds => {
|
||||
f.write_str("expected `.` after seconds")
|
||||
}
|
||||
ExpectedFirstZoneFourFields => f.write_str(
|
||||
"expected first `ZONE` line to have at least 4 fields",
|
||||
),
|
||||
ExpectedLinkTwoFields => {
|
||||
f.write_str("expected exactly 2 fields after `LINK`")
|
||||
}
|
||||
ExpectedMinuteAfterHours => {
|
||||
f.write_str("expected minute digits after `HH:`")
|
||||
}
|
||||
ExpectedNameBegin => f.write_str(
|
||||
"`NAME` field cannot begin with a digit, `+` or `-`, \
|
||||
but found `NAME` that begins with one of those",
|
||||
),
|
||||
ExpectedNanosecondDigits => {
|
||||
f.write_str("expected nanosecond digits after `HH:MM:SS.`")
|
||||
}
|
||||
ExpectedNonEmptyAbbreviation => f.write_str(
|
||||
"empty time zone abbreviations are not allowed",
|
||||
),
|
||||
ExpectedNonEmptyAt => {
|
||||
f.write_str("`AT` field for rule cannot be empty")
|
||||
}
|
||||
ExpectedNonEmptyName => {
|
||||
f.write_str("`NAME` field for rule cannot be empty")
|
||||
}
|
||||
ExpectedNonEmptySave => {
|
||||
f.write_str("`SAVE` field for rule cannot be empty")
|
||||
}
|
||||
ExpectedNonEmptyZoneName => {
|
||||
f.write_str("zone names cannot be empty")
|
||||
}
|
||||
ExpectedNothingAfterTime => f.write_str(
|
||||
"expected no more fields after time of day, \
|
||||
but found at least one",
|
||||
),
|
||||
ExpectedRuleNineFields { got } => write!(
|
||||
f,
|
||||
"expected exactly 9 fields for rule, \
|
||||
but found {got} fields",
|
||||
),
|
||||
ExpectedSecondAfterMinutes => {
|
||||
f.write_str("expected second digits after `HH:MM:`")
|
||||
}
|
||||
ExpectedTimeOneHour => f.write_str(
|
||||
"expected time duration to contain \
|
||||
at least one hour digit",
|
||||
),
|
||||
ExpectedUntilYear => f.write_str("expected at least a year"),
|
||||
ExpectedWhitespaceAfterQuotedField => {
|
||||
f.write_str("expected whitespace after quoted field")
|
||||
}
|
||||
ExpectedZoneNameComponentNoDots { ref component } => write!(
|
||||
f,
|
||||
"component `{component}` in zone name cannot \
|
||||
be \".\" or \"..\"",
|
||||
),
|
||||
FailedContinuationZone => {
|
||||
f.write_str("failed to parse continuation `Zone` line")
|
||||
}
|
||||
FailedLinkLine => f.write_str("failed to parse `Link` line"),
|
||||
FailedParseDay => f.write_str("failed to parse day"),
|
||||
FailedParseFieldAt => {
|
||||
f.write_str("failed to parse `NAME` field")
|
||||
}
|
||||
FailedParseFieldFormat => {
|
||||
f.write_str("failed to parse `FORMAT` field")
|
||||
}
|
||||
FailedParseFieldFrom => {
|
||||
f.write_str("failed to parse `FROM` field")
|
||||
}
|
||||
FailedParseFieldIn => {
|
||||
f.write_str("failed to parse `IN` field")
|
||||
}
|
||||
FailedParseFieldLetters => {
|
||||
f.write_str("failed to parse `LETTERS` field")
|
||||
}
|
||||
FailedParseFieldLinkName => {
|
||||
f.write_str("failed to parse `LINK` name field")
|
||||
}
|
||||
FailedParseFieldLinkTarget => {
|
||||
f.write_str("failed to parse `LINK` target field")
|
||||
}
|
||||
FailedParseFieldName => {
|
||||
f.write_str("failed to parse `NAME` field")
|
||||
}
|
||||
FailedParseFieldOn => {
|
||||
f.write_str("failed to parse `ON` field")
|
||||
}
|
||||
FailedParseFieldRules => {
|
||||
f.write_str("failed to parse `RULES` field")
|
||||
}
|
||||
FailedParseFieldSave => {
|
||||
f.write_str("failed to parse `SAVE` field")
|
||||
}
|
||||
FailedParseFieldStdOff => {
|
||||
f.write_str("failed to parse `STDOFF` field")
|
||||
}
|
||||
FailedParseFieldTo => {
|
||||
f.write_str("failed to parse `TO` field")
|
||||
}
|
||||
FailedParseFieldUntil => {
|
||||
f.write_str("failed to parse `UNTIL` field")
|
||||
}
|
||||
FailedParseHour => f.write_str("failed to parse hour"),
|
||||
FailedParseMinute => f.write_str("failed to parse minute"),
|
||||
FailedParseMonth => f.write_str("failed to parse month"),
|
||||
FailedParseNanosecond => {
|
||||
f.write_str("failed to parse nanosecond")
|
||||
}
|
||||
FailedParseSecond => f.write_str("failed to parse second"),
|
||||
FailedParseTimeDuration => {
|
||||
f.write_str("failed to parse time duration")
|
||||
}
|
||||
FailedParseYear => f.write_str("failed to parse year"),
|
||||
FailedRule { name: ref rule } => {
|
||||
write!(f, "failed to parse rule `{rule}`")
|
||||
}
|
||||
FailedRuleLine => f.write_str("failed to parse `Rule` line"),
|
||||
FailedZoneFirst => {
|
||||
f.write_str("failed to parse first `Zone` line")
|
||||
}
|
||||
InvalidAbbreviation => f.write_str(
|
||||
"time zone abbreviation \
|
||||
contains invalid character; only \"+\", \"-\" and \
|
||||
ASCII alpha-numeric characters are allowed",
|
||||
),
|
||||
InvalidRuleYear { start, end } => write!(
|
||||
f,
|
||||
"found start year={start} \
|
||||
to be greater than end year={end}"
|
||||
),
|
||||
InvalidUtf8 => f.write_str("invalid UTF-8"),
|
||||
Line { number } => write!(f, "line {number}"),
|
||||
LineMaxLength => write!(
|
||||
f,
|
||||
"found line with length that exceeds \
|
||||
max length of {MAX_LINE_LEN}",
|
||||
),
|
||||
LineNul => f.write_str(
|
||||
"found line with NUL byte, which isn't allowed",
|
||||
),
|
||||
LineOverflow => f.write_str("line count overflowed"),
|
||||
UnrecognizedAtTimeSuffix => {
|
||||
f.write_str("unrecognized `AT` time suffix")
|
||||
}
|
||||
UnrecognizedDayOfMonthFormat => {
|
||||
f.write_str("unrecognized format for day-of-month")
|
||||
}
|
||||
UnrecognizedDayOfWeek => {
|
||||
f.write_str("unrecognized day of the week")
|
||||
}
|
||||
UnrecognizedMonthName => {
|
||||
f.write_str("unrecognized month name")
|
||||
}
|
||||
UnrecognizedSaveTimeSuffix => {
|
||||
f.write_str("unrecognized `SAVE` time suffix")
|
||||
}
|
||||
UnrecognizedTrailingTimeDuration => {
|
||||
f.write_str("found unrecognized suffix in time duration")
|
||||
}
|
||||
UnrecognizedZicLine => f.write_str("unrecognized zic line"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
194
src/error/util.rs
Normal file
194
src/error/util.rs
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
use crate::{error, util::escape::Byte, Unit};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum RoundingIncrementError {
|
||||
ForDateTime,
|
||||
ForSpan,
|
||||
ForTime,
|
||||
ForTimestamp,
|
||||
GreaterThanZero { unit: Unit },
|
||||
InvalidDivide { unit: Unit, must_divide: i64 },
|
||||
Unsupported { unit: Unit },
|
||||
}
|
||||
|
||||
impl From<RoundingIncrementError> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: RoundingIncrementError) -> error::Error {
|
||||
error::ErrorKind::RoundingIncrement(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl error::IntoError for RoundingIncrementError {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for RoundingIncrementError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::RoundingIncrementError::*;
|
||||
|
||||
match *self {
|
||||
ForDateTime => f.write_str("failed rounding datetime"),
|
||||
ForSpan => f.write_str("failed rounding span"),
|
||||
ForTime => f.write_str("failed rounding time"),
|
||||
ForTimestamp => f.write_str("failed rounding timestamp"),
|
||||
GreaterThanZero { unit } => write!(
|
||||
f,
|
||||
"rounding increment for {unit} must be greater than zero",
|
||||
unit = unit.plural(),
|
||||
),
|
||||
InvalidDivide { unit, must_divide } => write!(
|
||||
f,
|
||||
"increment for rounding to {unit} \
|
||||
must be 1) less than {must_divide}, 2) divide into \
|
||||
it evenly and 3) greater than zero",
|
||||
unit = unit.plural(),
|
||||
),
|
||||
Unsupported { unit } => write!(
|
||||
f,
|
||||
"rounding to {unit} is not supported",
|
||||
unit = unit.plural(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum ParseIntError {
|
||||
NoDigitsFound,
|
||||
InvalidDigit(u8),
|
||||
TooBig,
|
||||
}
|
||||
|
||||
impl From<ParseIntError> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: ParseIntError) -> error::Error {
|
||||
error::ErrorKind::ParseInt(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl error::IntoError for ParseIntError {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for ParseIntError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::ParseIntError::*;
|
||||
|
||||
match *self {
|
||||
NoDigitsFound => write!(f, "invalid number, no digits found"),
|
||||
InvalidDigit(got) => {
|
||||
write!(f, "invalid digit, expected 0-9 but got {}", Byte(got))
|
||||
}
|
||||
TooBig => {
|
||||
write!(f, "number too big to parse into 64-bit integer")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum ParseFractionError {
|
||||
NoDigitsFound,
|
||||
TooManyDigits,
|
||||
InvalidDigit(u8),
|
||||
TooBig,
|
||||
}
|
||||
|
||||
impl ParseFractionError {
|
||||
pub(crate) const MAX_PRECISION: usize = 9;
|
||||
}
|
||||
|
||||
impl From<ParseFractionError> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: ParseFractionError) -> error::Error {
|
||||
error::ErrorKind::ParseFraction(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl error::IntoError for ParseFractionError {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for ParseFractionError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::ParseFractionError::*;
|
||||
|
||||
match *self {
|
||||
NoDigitsFound => write!(f, "invalid fraction, no digits found"),
|
||||
TooManyDigits => write!(
|
||||
f,
|
||||
"invalid fraction, too many digits \
|
||||
(at most {max} are allowed)",
|
||||
max = ParseFractionError::MAX_PRECISION,
|
||||
),
|
||||
InvalidDigit(got) => {
|
||||
write!(
|
||||
f,
|
||||
"invalid fractional digit, expected 0-9 but got {}",
|
||||
Byte(got)
|
||||
)
|
||||
}
|
||||
TooBig => {
|
||||
write!(
|
||||
f,
|
||||
"fractional number too big to parse into 64-bit integer"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct OsStrUtf8Error {
|
||||
#[cfg(feature = "std")]
|
||||
value: alloc::boxed::Box<std::ffi::OsStr>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl From<&std::ffi::OsStr> for OsStrUtf8Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(value: &std::ffi::OsStr) -> OsStrUtf8Error {
|
||||
OsStrUtf8Error { value: value.into() }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OsStrUtf8Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: OsStrUtf8Error) -> error::Error {
|
||||
error::ErrorKind::OsStrUtf8(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl error::IntoError for OsStrUtf8Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for OsStrUtf8Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
#[cfg(feature = "std")]
|
||||
{
|
||||
write!(
|
||||
f,
|
||||
"environment value `{value:?}` is not valid UTF-8",
|
||||
value = self.value
|
||||
)
|
||||
}
|
||||
#[cfg(not(feature = "std"))]
|
||||
{
|
||||
write!(f, "<BUG: SHOULD NOT EXIST>")
|
||||
}
|
||||
}
|
||||
}
|
||||
71
src/error/zoned.rs
Normal file
71
src/error/zoned.rs
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
use crate::{error, Unit};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
AddDateTime,
|
||||
AddDays,
|
||||
AddTimestamp,
|
||||
ConvertDateTimeToTimestamp,
|
||||
ConvertIntermediateDatetime,
|
||||
FailedLengthOfDay,
|
||||
FailedSpanNanoseconds,
|
||||
FailedStartOfDay,
|
||||
MismatchTimeZoneUntil { largest: Unit },
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::Zoned(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
AddDateTime => f.write_str(
|
||||
"failed to add span to datetime from zoned datetime",
|
||||
),
|
||||
AddDays => {
|
||||
f.write_str("failed to add days to date in zoned datetime")
|
||||
}
|
||||
AddTimestamp => f.write_str(
|
||||
"failed to add span to timestamp from zoned datetime",
|
||||
),
|
||||
ConvertDateTimeToTimestamp => {
|
||||
f.write_str("failed to convert civil datetime to timestamp")
|
||||
}
|
||||
ConvertIntermediateDatetime => f.write_str(
|
||||
"failed to convert intermediate datetime \
|
||||
to zoned timestamp",
|
||||
),
|
||||
FailedLengthOfDay => f.write_str(
|
||||
"failed to add 1 day to zoned datetime to find length of day",
|
||||
),
|
||||
FailedSpanNanoseconds => f.write_str(
|
||||
"failed to compute span in nanoseconds \
|
||||
between zoned datetimes",
|
||||
),
|
||||
FailedStartOfDay => {
|
||||
f.write_str("failed to find start of day for zoned datetime")
|
||||
}
|
||||
MismatchTimeZoneUntil { largest } => write!(
|
||||
f,
|
||||
"computing the span between zoned datetimes, with \
|
||||
{largest} units, requires that the time zones are \
|
||||
equivalent, but the zoned datetimes have distinct \
|
||||
time zones",
|
||||
largest = largest.singular(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -256,7 +256,9 @@ assert_eq!(dur, Duration::new(30 * 24 * 60 * 60 + 38_016, 0));
|
|||
// In contrast, Jiff will reject `1M`:
|
||||
assert_eq!(
|
||||
"1M".parse::<jiff::Span>().unwrap_err().to_string(),
|
||||
"failed to parse \"1M\" in the \"friendly\" format: expected to find unit designator suffix (e.g., 'years' or 'secs'), but found input beginning with \"M\" instead",
|
||||
"failed to parse input in the \"friendly\" duration format: \
|
||||
expected to find unit designator suffix \
|
||||
(e.g., `years` or `secs`) after parsing integer",
|
||||
);
|
||||
|
||||
# Ok::<(), Box<dyn std::error::Error>>(())
|
||||
|
|
@ -335,7 +337,9 @@ assert_eq!(
|
|||
// Jiff is saving you from doing something wrong
|
||||
assert_eq!(
|
||||
"1 day".parse::<SignedDuration>().unwrap_err().to_string(),
|
||||
"failed to parse \"1 day\" in the \"friendly\" format: parsing day units into a `SignedDuration` is not supported (perhaps try parsing into a `Span` instead)",
|
||||
"failed to parse input in the \"friendly\" duration format: \
|
||||
parsing calendar units (days in this case) in this context \
|
||||
is not supported (perhaps try parsing into a `jiff::Span` instead)",
|
||||
);
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
use crate::{
|
||||
error::{err, ErrorContext},
|
||||
error::{fmt::friendly::Error as E, ErrorContext},
|
||||
fmt::{
|
||||
friendly::parser_label,
|
||||
util::{parse_temporal_fraction, DurationUnits},
|
||||
Parsed,
|
||||
},
|
||||
util::{c::Sign, escape, parse},
|
||||
util::{c::Sign, parse},
|
||||
Error, SignedDuration, Span, Unit,
|
||||
};
|
||||
|
||||
|
|
@ -188,12 +188,7 @@ impl SpanParser {
|
|||
}
|
||||
|
||||
let input = input.as_ref();
|
||||
imp(self, input).with_context(|| {
|
||||
err!(
|
||||
"failed to parse {input:?} in the \"friendly\" format",
|
||||
input = escape::Bytes(input)
|
||||
)
|
||||
})
|
||||
imp(self, input).context(E::Failed)
|
||||
}
|
||||
|
||||
/// Run the parser on the given string (which may be plain bytes) and,
|
||||
|
|
@ -248,12 +243,7 @@ impl SpanParser {
|
|||
}
|
||||
|
||||
let input = input.as_ref();
|
||||
imp(self, input).with_context(|| {
|
||||
err!(
|
||||
"failed to parse {input:?} in the \"friendly\" format",
|
||||
input = escape::Bytes(input)
|
||||
)
|
||||
})
|
||||
imp(self, input).context(E::Failed)
|
||||
}
|
||||
|
||||
/// Run the parser on the given string (which may be plain bytes) and,
|
||||
|
|
@ -312,12 +302,7 @@ impl SpanParser {
|
|||
}
|
||||
|
||||
let input = input.as_ref();
|
||||
imp(self, input).with_context(|| {
|
||||
err!(
|
||||
"failed to parse {input:?} in the \"friendly\" format",
|
||||
input = escape::Bytes(input)
|
||||
)
|
||||
})
|
||||
imp(self, input).context(E::Failed)
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "perf-inline", inline(always))]
|
||||
|
|
@ -327,7 +312,7 @@ impl SpanParser {
|
|||
builder: &mut DurationUnits,
|
||||
) -> Result<Parsed<'i, ()>, Error> {
|
||||
if input.is_empty() {
|
||||
return Err(err!("an empty string is not a valid duration"));
|
||||
return Err(Error::from(E::Empty));
|
||||
}
|
||||
// Guard prefix sign parsing to avoid the function call, which is
|
||||
// marked unlineable to keep the fast path tighter.
|
||||
|
|
@ -342,11 +327,7 @@ impl SpanParser {
|
|||
|
||||
let Parsed { value, input } = self.parse_unit_value(input)?;
|
||||
let Some(first_unit_value) = value else {
|
||||
return Err(err!(
|
||||
"parsing a friendly duration requires it to start \
|
||||
with a unit value (a decimal integer) after an \
|
||||
optional sign, but no integer was found",
|
||||
));
|
||||
return Err(Error::from(E::ExpectedIntegerAfterSign));
|
||||
};
|
||||
|
||||
let Parsed { input, .. } =
|
||||
|
|
@ -434,11 +415,7 @@ impl SpanParser {
|
|||
parsed_any_after_comma = true;
|
||||
}
|
||||
if !parsed_any_after_comma {
|
||||
return Err(err!(
|
||||
"found comma at the end of duration, \
|
||||
but a comma indicates at least one more \
|
||||
unit follows",
|
||||
));
|
||||
return Err(Error::from(E::ExpectedOneMoreUnitAfterComma));
|
||||
}
|
||||
Ok(Parsed { value: (), input })
|
||||
}
|
||||
|
|
@ -454,10 +431,13 @@ impl SpanParser {
|
|||
input: &'i [u8],
|
||||
hour: u64,
|
||||
) -> Result<Parsed<'i, Option<HMS>>, Error> {
|
||||
if !input.first().map_or(false, |&b| b == b':') {
|
||||
let Some((&first, tail)) = input.split_first() else {
|
||||
return Ok(Parsed { input, value: None });
|
||||
};
|
||||
if first != b':' {
|
||||
return Ok(Parsed { input, value: None });
|
||||
}
|
||||
let Parsed { input, value } = self.parse_hms(&input[1..], hour)?;
|
||||
let Parsed { input, value } = self.parse_hms(tail, hour)?;
|
||||
Ok(Parsed { input, value: Some(value) })
|
||||
}
|
||||
|
||||
|
|
@ -477,26 +457,16 @@ impl SpanParser {
|
|||
hour: u64,
|
||||
) -> Result<Parsed<'i, HMS>, Error> {
|
||||
let Parsed { input, value } = self.parse_unit_value(input)?;
|
||||
let Some(minute) = value else {
|
||||
return Err(err!(
|
||||
"expected to parse minute in 'HH:MM:SS' format \
|
||||
following parsed hour of {hour}",
|
||||
));
|
||||
};
|
||||
if !input.first().map_or(false, |&b| b == b':') {
|
||||
return Err(err!(
|
||||
"when parsing 'HH:MM:SS' format, expected to \
|
||||
see a ':' after the parsed minute of {minute}",
|
||||
));
|
||||
let minute = value.ok_or(E::ExpectedMinuteAfterHour)?;
|
||||
|
||||
let (&first, input) =
|
||||
input.split_first().ok_or(E::ExpectedColonAfterMinute)?;
|
||||
if first != b':' {
|
||||
return Err(Error::from(E::ExpectedColonAfterMinute));
|
||||
}
|
||||
let input = &input[1..];
|
||||
|
||||
let Parsed { input, value } = self.parse_unit_value(input)?;
|
||||
let Some(second) = value else {
|
||||
return Err(err!(
|
||||
"expected to parse second in 'HH:MM:SS' format \
|
||||
following parsed minute of {minute}",
|
||||
));
|
||||
};
|
||||
let second = value.ok_or(E::ExpectedSecondAfterMinute)?;
|
||||
let (fraction, input) =
|
||||
if input.first().map_or(false, |&b| b == b'.' || b == b',') {
|
||||
let parsed = parse_temporal_fraction(input)?;
|
||||
|
|
@ -540,22 +510,8 @@ impl SpanParser {
|
|||
&self,
|
||||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, Unit>, Error> {
|
||||
let Some((unit, len)) = parser_label::find(input) else {
|
||||
if input.is_empty() {
|
||||
return Err(err!(
|
||||
"expected to find unit designator suffix \
|
||||
(e.g., 'years' or 'secs'), \
|
||||
but found end of input",
|
||||
));
|
||||
} else {
|
||||
return Err(err!(
|
||||
"expected to find unit designator suffix \
|
||||
(e.g., 'years' or 'secs'), \
|
||||
but found input beginning with {found:?} instead",
|
||||
found = escape::Bytes(&input[..input.len().min(20)]),
|
||||
));
|
||||
}
|
||||
};
|
||||
let (unit, len) =
|
||||
parser_label::find(input).ok_or(E::ExpectedUnitSuffix)?;
|
||||
Ok(Parsed { value: unit, input: &input[len..] })
|
||||
}
|
||||
|
||||
|
|
@ -606,17 +562,15 @@ impl SpanParser {
|
|||
}
|
||||
// Eat any additional whitespace we find before looking for 'ago'.
|
||||
input = self.parse_optional_whitespace(&input[1..]).input;
|
||||
let (suffix_sign, input) = if input.starts_with(b"ago") {
|
||||
(Some(Sign::Negative), &input[3..])
|
||||
} else {
|
||||
(None, input)
|
||||
};
|
||||
let (suffix_sign, input) =
|
||||
if let Some(tail) = input.strip_prefix(b"ago") {
|
||||
(Some(Sign::Negative), tail)
|
||||
} else {
|
||||
(None, input)
|
||||
};
|
||||
let sign = match (prefix_sign, suffix_sign) {
|
||||
(Some(_), Some(_)) => {
|
||||
return Err(err!(
|
||||
"expected to find either a prefix sign (+/-) or \
|
||||
a suffix sign (ago), but found both",
|
||||
))
|
||||
return Err(Error::from(E::ExpectedOneSign));
|
||||
}
|
||||
(Some(sign), None) => sign,
|
||||
(None, Some(sign)) => sign,
|
||||
|
|
@ -637,24 +591,24 @@ impl SpanParser {
|
|||
#[inline(never)]
|
||||
fn parse_optional_comma<'i>(
|
||||
&self,
|
||||
mut input: &'i [u8],
|
||||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, ()>, Error> {
|
||||
if !input.first().map_or(false, |&b| b == b',') {
|
||||
let Some((&first, tail)) = input.split_first() else {
|
||||
return Ok(Parsed { value: (), input });
|
||||
};
|
||||
if first != b',' {
|
||||
return Ok(Parsed { value: (), input });
|
||||
}
|
||||
input = &input[1..];
|
||||
if input.is_empty() {
|
||||
return Err(err!(
|
||||
"expected whitespace after comma, but found end of input"
|
||||
));
|
||||
|
||||
let (second, input) = tail
|
||||
.split_first()
|
||||
.ok_or(E::ExpectedWhitespaceAfterCommaEndOfInput)?;
|
||||
if !is_whitespace(second) {
|
||||
return Err(Error::from(E::ExpectedWhitespaceAfterComma {
|
||||
byte: *second,
|
||||
}));
|
||||
}
|
||||
if !is_whitespace(&input[0]) {
|
||||
return Err(err!(
|
||||
"expected whitespace after comma, but found {found:?}",
|
||||
found = escape::Byte(input[0]),
|
||||
));
|
||||
}
|
||||
Ok(Parsed { value: (), input: &input[1..] })
|
||||
Ok(Parsed { value: (), input })
|
||||
}
|
||||
|
||||
/// Parses zero or more bytes of ASCII whitespace.
|
||||
|
|
@ -776,35 +730,35 @@ mod tests {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
p(""),
|
||||
@r###"failed to parse "" in the "friendly" format: an empty string is not a valid duration"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: an empty string is not valid"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p(" "),
|
||||
@r###"failed to parse " " in the "friendly" format: parsing a friendly duration requires it to start with a unit value (a decimal integer) after an optional sign, but no integer was found"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: expected duration to start with a unit value (a decimal integer) after an optional sign, but no integer was found"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("a"),
|
||||
@r###"failed to parse "a" in the "friendly" format: parsing a friendly duration requires it to start with a unit value (a decimal integer) after an optional sign, but no integer was found"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: expected duration to start with a unit value (a decimal integer) after an optional sign, but no integer was found"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("2 months 1 year"),
|
||||
@r###"failed to parse "2 months 1 year" in the "friendly" format: found value 1 with unit year after unit month, but units must be written from largest to smallest (and they can't be repeated)"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: found value with unit year after unit month, but units must be written from largest to smallest (and they can't be repeated)"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("1 year 1 mont"),
|
||||
@r###"failed to parse "1 year 1 mont" in the "friendly" format: parsed value 'P1Y1M', but unparsed input "nt" remains (expected no unparsed input)"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: parsed value 'P1Y1M', but unparsed input "nt" remains (expected no unparsed input)"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("2 months,"),
|
||||
@r###"failed to parse "2 months," in the "friendly" format: expected whitespace after comma, but found end of input"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: expected whitespace after comma, but found end of input"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("2 months, "),
|
||||
@r#"failed to parse "2 months, " in the "friendly" format: found comma at the end of duration, but a comma indicates at least one more unit follows"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: found comma at the end of duration, but a comma indicates at least one more unit follows"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("2 months ,"),
|
||||
@r###"failed to parse "2 months ," in the "friendly" format: parsed value 'P2M', but unparsed input "," remains (expected no unparsed input)"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: parsed value 'P2M', but unparsed input "," remains (expected no unparsed input)"#,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -814,19 +768,19 @@ mod tests {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
p("1yago"),
|
||||
@r###"failed to parse "1yago" in the "friendly" format: parsed value 'P1Y', but unparsed input "ago" remains (expected no unparsed input)"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: parsed value 'P1Y', but unparsed input "ago" remains (expected no unparsed input)"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("1 year 1 monthago"),
|
||||
@r###"failed to parse "1 year 1 monthago" in the "friendly" format: parsed value 'P1Y1M', but unparsed input "ago" remains (expected no unparsed input)"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: parsed value 'P1Y1M', but unparsed input "ago" remains (expected no unparsed input)"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("+1 year 1 month ago"),
|
||||
@r###"failed to parse "+1 year 1 month ago" in the "friendly" format: expected to find either a prefix sign (+/-) or a suffix sign (ago), but found both"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: expected to find either a prefix sign (+/-) or a suffix sign (`ago`), but found both"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("-1 year 1 month ago"),
|
||||
@r###"failed to parse "-1 year 1 month ago" in the "friendly" format: expected to find either a prefix sign (+/-) or a suffix sign (ago), but found both"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: expected to find either a prefix sign (+/-) or a suffix sign (`ago`), but found both"#,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -840,7 +794,7 @@ mod tests {
|
|||
// the maximum number of microseconds is subtracted off, and we're
|
||||
// left over with a value that overflows an i64.
|
||||
pe("640330789636854776 micros"),
|
||||
@r#"failed to parse "640330789636854776 micros" in the "friendly" format: failed to set value 640330789636854776 as microsecond unit on span: failed to set nanosecond value 9223372036854776000 (it overflows `i64`) on span determined from 640330789636854776.0"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: failed to set value for microsecond unit on span: failed to set nanosecond value from fractional component"#,
|
||||
);
|
||||
// one fewer is okay
|
||||
insta::assert_snapshot!(
|
||||
|
|
@ -853,7 +807,7 @@ mod tests {
|
|||
// different error path by using an explicit fraction. Here, if
|
||||
// we had x.807 micros, it would parse successfully.
|
||||
pe("640330789636854775.808 micros"),
|
||||
@r#"failed to parse "640330789636854775.808 micros" in the "friendly" format: failed to set nanosecond value 9223372036854775808 (it overflows `i64`) on span determined from 640330789636854775.808000000"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: failed to set nanosecond value from fractional component"#,
|
||||
);
|
||||
// one fewer is okay
|
||||
insta::assert_snapshot!(
|
||||
|
|
@ -868,47 +822,47 @@ mod tests {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
p("19999 years"),
|
||||
@r###"failed to parse "19999 years" in the "friendly" format: failed to set value 19999 as year unit on span: parameter 'years' with value 19999 is not in the required range of -19998..=19998"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: failed to set value for year unit on span: parameter 'years' with value 19999 is not in the required range of -19998..=19998"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("19999 years ago"),
|
||||
@r#"failed to parse "19999 years ago" in the "friendly" format: failed to set value -19999 as year unit on span: parameter 'years' with value -19999 is not in the required range of -19998..=19998"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: failed to set value for year unit on span: parameter 'years' with value -19999 is not in the required range of -19998..=19998"#,
|
||||
);
|
||||
|
||||
insta::assert_snapshot!(
|
||||
p("239977 months"),
|
||||
@r###"failed to parse "239977 months" in the "friendly" format: failed to set value 239977 as month unit on span: parameter 'months' with value 239977 is not in the required range of -239976..=239976"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: failed to set value for month unit on span: parameter 'months' with value 239977 is not in the required range of -239976..=239976"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("239977 months ago"),
|
||||
@r#"failed to parse "239977 months ago" in the "friendly" format: failed to set value -239977 as month unit on span: parameter 'months' with value -239977 is not in the required range of -239976..=239976"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: failed to set value for month unit on span: parameter 'months' with value -239977 is not in the required range of -239976..=239976"#,
|
||||
);
|
||||
|
||||
insta::assert_snapshot!(
|
||||
p("1043498 weeks"),
|
||||
@r###"failed to parse "1043498 weeks" in the "friendly" format: failed to set value 1043498 as week unit on span: parameter 'weeks' with value 1043498 is not in the required range of -1043497..=1043497"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: failed to set value for week unit on span: parameter 'weeks' with value 1043498 is not in the required range of -1043497..=1043497"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("1043498 weeks ago"),
|
||||
@r#"failed to parse "1043498 weeks ago" in the "friendly" format: failed to set value -1043498 as week unit on span: parameter 'weeks' with value -1043498 is not in the required range of -1043497..=1043497"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: failed to set value for week unit on span: parameter 'weeks' with value -1043498 is not in the required range of -1043497..=1043497"#,
|
||||
);
|
||||
|
||||
insta::assert_snapshot!(
|
||||
p("7304485 days"),
|
||||
@r###"failed to parse "7304485 days" in the "friendly" format: failed to set value 7304485 as day unit on span: parameter 'days' with value 7304485 is not in the required range of -7304484..=7304484"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: failed to set value for day unit on span: parameter 'days' with value 7304485 is not in the required range of -7304484..=7304484"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("7304485 days ago"),
|
||||
@r#"failed to parse "7304485 days ago" in the "friendly" format: failed to set value -7304485 as day unit on span: parameter 'days' with value -7304485 is not in the required range of -7304484..=7304484"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: failed to set value for day unit on span: parameter 'days' with value -7304485 is not in the required range of -7304484..=7304484"#,
|
||||
);
|
||||
|
||||
insta::assert_snapshot!(
|
||||
p("9223372036854775808 nanoseconds"),
|
||||
@r#"failed to parse "9223372036854775808 nanoseconds" in the "friendly" format: `9223372036854775808` nanoseconds is too big (or small) to fit into a signed 64-bit integer"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: value for nanoseconds is too big (or small) to fit into a signed 64-bit integer"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("9223372036854775808 nanoseconds ago"),
|
||||
@r#"failed to parse "9223372036854775808 nanoseconds ago" in the "friendly" format: failed to set value -9223372036854775808 as nanosecond unit on span: parameter 'nanoseconds' with value -9223372036854775808 is not in the required range of -9223372036854775807..=9223372036854775807"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: failed to set value for nanosecond unit on span: parameter 'nanoseconds' with value -9223372036854775808 is not in the required range of -9223372036854775807..=9223372036854775807"#,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -918,11 +872,11 @@ mod tests {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
p("1.5 years"),
|
||||
@r#"failed to parse "1.5 years" in the "friendly" format: fractional years are not supported"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: fractional years are not supported"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("1.5 nanos"),
|
||||
@r#"failed to parse "1.5 nanos" in the "friendly" format: fractional nanoseconds are not supported"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: fractional nanoseconds are not supported"#,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -932,19 +886,19 @@ mod tests {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
p("05:"),
|
||||
@r###"failed to parse "05:" in the "friendly" format: expected to parse minute in 'HH:MM:SS' format following parsed hour of 5"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse minute following hour"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("05:06"),
|
||||
@r###"failed to parse "05:06" in the "friendly" format: when parsing 'HH:MM:SS' format, expected to see a ':' after the parsed minute of 6"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse `:` following minute"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("05:06:"),
|
||||
@r###"failed to parse "05:06:" in the "friendly" format: expected to parse second in 'HH:MM:SS' format following parsed minute of 6"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse second following minute"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("2 hours, 05:06:07"),
|
||||
@r#"failed to parse "2 hours, 05:06:07" in the "friendly" format: found `HH:MM:SS` after unit hour, but `HH:MM:SS` can only appear after years, months, weeks or days"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: found `HH:MM:SS` after unit hour, but `HH:MM:SS` can only appear after years, months, weeks or days"#,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -968,7 +922,7 @@ mod tests {
|
|||
);
|
||||
insta::assert_snapshot!(
|
||||
perr("9223372036854775808s"),
|
||||
@r#"failed to parse "9223372036854775808s" in the "friendly" format: `9223372036854775808` seconds is too big (or small) to fit into a signed 64-bit integer"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: value for seconds is too big (or small) to fit into a signed 64-bit integer"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("-9223372036854775808s"),
|
||||
|
|
@ -1032,21 +986,21 @@ mod tests {
|
|||
insta::assert_snapshot!(p("-2562047788015215hours"), @"-PT2562047788015215H");
|
||||
insta::assert_snapshot!(
|
||||
pe("2562047788015216hrs"),
|
||||
@r#"failed to parse "2562047788015216hrs" in the "friendly" format: accumulated `SignedDuration` of `0s` overflowed when adding 2562047788015216 of unit hour"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: accumulated duration overflowed when adding value to unit hour"#,
|
||||
);
|
||||
|
||||
insta::assert_snapshot!(p("153722867280912930minutes"), @"PT2562047788015215H30M");
|
||||
insta::assert_snapshot!(p("153722867280912930minutes ago"), @"-PT2562047788015215H30M");
|
||||
insta::assert_snapshot!(
|
||||
pe("153722867280912931mins"),
|
||||
@r#"failed to parse "153722867280912931mins" in the "friendly" format: accumulated `SignedDuration` of `0s` overflowed when adding 153722867280912931 of unit minute"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: accumulated duration overflowed when adding value to unit minute"#,
|
||||
);
|
||||
|
||||
insta::assert_snapshot!(p("9223372036854775807seconds"), @"PT2562047788015215H30M7S");
|
||||
insta::assert_snapshot!(p("-9223372036854775807seconds"), @"-PT2562047788015215H30M7S");
|
||||
insta::assert_snapshot!(
|
||||
pe("9223372036854775808s"),
|
||||
@r#"failed to parse "9223372036854775808s" in the "friendly" format: `9223372036854775808` seconds is too big (or small) to fit into a signed 64-bit integer"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: value for seconds is too big (or small) to fit into a signed 64-bit integer"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("-9223372036854775808s"),
|
||||
|
|
@ -1060,39 +1014,39 @@ mod tests {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
p(""),
|
||||
@r###"failed to parse "" in the "friendly" format: an empty string is not a valid duration"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: an empty string is not valid"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p(" "),
|
||||
@r###"failed to parse " " in the "friendly" format: parsing a friendly duration requires it to start with a unit value (a decimal integer) after an optional sign, but no integer was found"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: expected duration to start with a unit value (a decimal integer) after an optional sign, but no integer was found"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("5"),
|
||||
@r###"failed to parse "5" in the "friendly" format: expected to find unit designator suffix (e.g., 'years' or 'secs'), but found end of input"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: expected to find unit designator suffix (e.g., `years` or `secs`) after parsing integer"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("a"),
|
||||
@r###"failed to parse "a" in the "friendly" format: parsing a friendly duration requires it to start with a unit value (a decimal integer) after an optional sign, but no integer was found"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: expected duration to start with a unit value (a decimal integer) after an optional sign, but no integer was found"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("2 minutes 1 hour"),
|
||||
@r###"failed to parse "2 minutes 1 hour" in the "friendly" format: found value 1 with unit hour after unit minute, but units must be written from largest to smallest (and they can't be repeated)"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: found value with unit hour after unit minute, but units must be written from largest to smallest (and they can't be repeated)"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("1 hour 1 minut"),
|
||||
@r###"failed to parse "1 hour 1 minut" in the "friendly" format: parsed value 'PT1H1M', but unparsed input "ut" remains (expected no unparsed input)"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: parsed value 'PT1H1M', but unparsed input "ut" remains (expected no unparsed input)"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("2 minutes,"),
|
||||
@r###"failed to parse "2 minutes," in the "friendly" format: expected whitespace after comma, but found end of input"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: expected whitespace after comma, but found end of input"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("2 minutes, "),
|
||||
@r#"failed to parse "2 minutes, " in the "friendly" format: found comma at the end of duration, but a comma indicates at least one more unit follows"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: found comma at the end of duration, but a comma indicates at least one more unit follows"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("2 minutes ,"),
|
||||
@r###"failed to parse "2 minutes ," in the "friendly" format: parsed value 'PT2M', but unparsed input "," remains (expected no unparsed input)"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: parsed value 'PT2M', but unparsed input "," remains (expected no unparsed input)"#,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1102,19 +1056,19 @@ mod tests {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
p("1hago"),
|
||||
@r###"failed to parse "1hago" in the "friendly" format: parsed value 'PT1H', but unparsed input "ago" remains (expected no unparsed input)"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: parsed value 'PT1H', but unparsed input "ago" remains (expected no unparsed input)"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("1 hour 1 minuteago"),
|
||||
@r###"failed to parse "1 hour 1 minuteago" in the "friendly" format: parsed value 'PT1H1M', but unparsed input "ago" remains (expected no unparsed input)"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: parsed value 'PT1H1M', but unparsed input "ago" remains (expected no unparsed input)"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("+1 hour 1 minute ago"),
|
||||
@r###"failed to parse "+1 hour 1 minute ago" in the "friendly" format: expected to find either a prefix sign (+/-) or a suffix sign (ago), but found both"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: expected to find either a prefix sign (+/-) or a suffix sign (`ago`), but found both"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("-1 hour 1 minute ago"),
|
||||
@r###"failed to parse "-1 hour 1 minute ago" in the "friendly" format: expected to find either a prefix sign (+/-) or a suffix sign (ago), but found both"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: expected to find either a prefix sign (+/-) or a suffix sign (`ago`), but found both"#,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1127,7 +1081,7 @@ mod tests {
|
|||
// Unlike `Span`, this just overflows because it can't be parsed
|
||||
// as a 64-bit integer.
|
||||
pe("9223372036854775808 micros"),
|
||||
@r#"failed to parse "9223372036854775808 micros" in the "friendly" format: `9223372036854775808` microseconds is too big (or small) to fit into a signed 64-bit integer"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: value for microseconds is too big (or small) to fit into a signed 64-bit integer"#,
|
||||
);
|
||||
// one fewer is okay
|
||||
insta::assert_snapshot!(
|
||||
|
|
@ -1142,7 +1096,7 @@ mod tests {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
p("1.5 nanos"),
|
||||
@r#"failed to parse "1.5 nanos" in the "friendly" format: fractional nanoseconds are not supported"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: fractional nanoseconds are not supported"#,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1152,19 +1106,19 @@ mod tests {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
p("05:"),
|
||||
@r###"failed to parse "05:" in the "friendly" format: expected to parse minute in 'HH:MM:SS' format following parsed hour of 5"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse minute following hour"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("05:06"),
|
||||
@r###"failed to parse "05:06" in the "friendly" format: when parsing 'HH:MM:SS' format, expected to see a ':' after the parsed minute of 6"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse `:` following minute"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("05:06:"),
|
||||
@r###"failed to parse "05:06:" in the "friendly" format: expected to parse second in 'HH:MM:SS' format following parsed minute of 6"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse second following minute"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("2 hours, 05:06:07"),
|
||||
@r#"failed to parse "2 hours, 05:06:07" in the "friendly" format: found `HH:MM:SS` after unit hour, but `HH:MM:SS` can only appear after years, months, weeks or days"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: found `HH:MM:SS` after unit hour, but `HH:MM:SS` can only appear after years, months, weeks or days"#,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1205,11 +1159,11 @@ mod tests {
|
|||
);
|
||||
insta::assert_snapshot!(
|
||||
perr("18446744073709551616s"),
|
||||
@r#"failed to parse "18446744073709551616s" in the "friendly" format: number `18446744073709551616` too big to parse into 64-bit integer"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: number too big to parse into 64-bit integer"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
perr("-1s"),
|
||||
@r#"failed to parse "-1s" in the "friendly" format: cannot parse negative duration into unsigned `std::time::Duration`"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: cannot parse negative duration into unsigned `std::time::Duration`"#,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1263,19 +1217,19 @@ mod tests {
|
|||
insta::assert_snapshot!(p("5124095576030431hours"), @"PT5124095576030431H");
|
||||
insta::assert_snapshot!(
|
||||
pe("5124095576030432hrs"),
|
||||
@r#"failed to parse "5124095576030432hrs" in the "friendly" format: accumulated `SignedDuration` of `0s` overflowed when adding 5124095576030432 of unit hour"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: accumulated duration overflowed when adding value to unit hour"#,
|
||||
);
|
||||
|
||||
insta::assert_snapshot!(p("307445734561825860minutes"), @"PT5124095576030431H");
|
||||
insta::assert_snapshot!(
|
||||
pe("307445734561825861mins"),
|
||||
@r#"failed to parse "307445734561825861mins" in the "friendly" format: accumulated `SignedDuration` of `0s` overflowed when adding 307445734561825861 of unit minute"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: accumulated duration overflowed when adding value to unit minute"#,
|
||||
);
|
||||
|
||||
insta::assert_snapshot!(p("18446744073709551615seconds"), @"PT5124095576030431H15S");
|
||||
insta::assert_snapshot!(
|
||||
pe("18446744073709551616s"),
|
||||
@r#"failed to parse "18446744073709551616s" in the "friendly" format: number `18446744073709551616` too big to parse into 64-bit integer"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: number too big to parse into 64-bit integer"#,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1287,39 +1241,39 @@ mod tests {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
p(""),
|
||||
@r###"failed to parse "" in the "friendly" format: an empty string is not a valid duration"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: an empty string is not valid"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p(" "),
|
||||
@r###"failed to parse " " in the "friendly" format: parsing a friendly duration requires it to start with a unit value (a decimal integer) after an optional sign, but no integer was found"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: expected duration to start with a unit value (a decimal integer) after an optional sign, but no integer was found"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("5"),
|
||||
@r###"failed to parse "5" in the "friendly" format: expected to find unit designator suffix (e.g., 'years' or 'secs'), but found end of input"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: expected to find unit designator suffix (e.g., `years` or `secs`) after parsing integer"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("a"),
|
||||
@r###"failed to parse "a" in the "friendly" format: parsing a friendly duration requires it to start with a unit value (a decimal integer) after an optional sign, but no integer was found"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: expected duration to start with a unit value (a decimal integer) after an optional sign, but no integer was found"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("2 minutes 1 hour"),
|
||||
@r###"failed to parse "2 minutes 1 hour" in the "friendly" format: found value 1 with unit hour after unit minute, but units must be written from largest to smallest (and they can't be repeated)"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: found value with unit hour after unit minute, but units must be written from largest to smallest (and they can't be repeated)"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("1 hour 1 minut"),
|
||||
@r#"failed to parse "1 hour 1 minut" in the "friendly" format: parsed value '3660s', but unparsed input "ut" remains (expected no unparsed input)"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: parsed value '3660s', but unparsed input "ut" remains (expected no unparsed input)"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("2 minutes,"),
|
||||
@r###"failed to parse "2 minutes," in the "friendly" format: expected whitespace after comma, but found end of input"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: expected whitespace after comma, but found end of input"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("2 minutes, "),
|
||||
@r#"failed to parse "2 minutes, " in the "friendly" format: found comma at the end of duration, but a comma indicates at least one more unit follows"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: found comma at the end of duration, but a comma indicates at least one more unit follows"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("2 minutes ,"),
|
||||
@r#"failed to parse "2 minutes ," in the "friendly" format: parsed value '120s', but unparsed input "," remains (expected no unparsed input)"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: parsed value '120s', but unparsed input "," remains (expected no unparsed input)"#,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1331,19 +1285,19 @@ mod tests {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
p("1hago"),
|
||||
@r#"failed to parse "1hago" in the "friendly" format: parsed value '3600s', but unparsed input "ago" remains (expected no unparsed input)"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: parsed value '3600s', but unparsed input "ago" remains (expected no unparsed input)"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("1 hour 1 minuteago"),
|
||||
@r#"failed to parse "1 hour 1 minuteago" in the "friendly" format: parsed value '3660s', but unparsed input "ago" remains (expected no unparsed input)"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: parsed value '3660s', but unparsed input "ago" remains (expected no unparsed input)"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("+1 hour 1 minute ago"),
|
||||
@r###"failed to parse "+1 hour 1 minute ago" in the "friendly" format: expected to find either a prefix sign (+/-) or a suffix sign (ago), but found both"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: expected to find either a prefix sign (+/-) or a suffix sign (`ago`), but found both"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("-1 hour 1 minute ago"),
|
||||
@r###"failed to parse "-1 hour 1 minute ago" in the "friendly" format: expected to find either a prefix sign (+/-) or a suffix sign (ago), but found both"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: expected to find either a prefix sign (+/-) or a suffix sign (`ago`), but found both"#,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1362,7 +1316,7 @@ mod tests {
|
|||
// Unlike `Span`, this just overflows because it can't be parsed
|
||||
// as a 64-bit integer.
|
||||
pe("18446744073709551616 micros"),
|
||||
@r#"failed to parse "18446744073709551616 micros" in the "friendly" format: number `18446744073709551616` too big to parse into 64-bit integer"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: number too big to parse into 64-bit integer"#,
|
||||
);
|
||||
// one fewer is okay
|
||||
insta::assert_snapshot!(
|
||||
|
|
@ -1379,7 +1333,7 @@ mod tests {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
p("1.5 nanos"),
|
||||
@r#"failed to parse "1.5 nanos" in the "friendly" format: fractional nanoseconds are not supported"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: fractional nanoseconds are not supported"#,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1391,19 +1345,19 @@ mod tests {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
p("05:"),
|
||||
@r###"failed to parse "05:" in the "friendly" format: expected to parse minute in 'HH:MM:SS' format following parsed hour of 5"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse minute following hour"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("05:06"),
|
||||
@r###"failed to parse "05:06" in the "friendly" format: when parsing 'HH:MM:SS' format, expected to see a ':' after the parsed minute of 6"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse `:` following minute"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("05:06:"),
|
||||
@r###"failed to parse "05:06:" in the "friendly" format: expected to parse second in 'HH:MM:SS' format following parsed minute of 6"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse second following minute"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("2 hours, 05:06:07"),
|
||||
@r#"failed to parse "2 hours, 05:06:07" in the "friendly" format: found `HH:MM:SS` after unit hour, but `HH:MM:SS` can only appear after years, months, weeks or days"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: found `HH:MM:SS` after unit hour, but `HH:MM:SS` can only appear after years, months, weeks or days"#,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ and features.)
|
|||
*/
|
||||
|
||||
use crate::{
|
||||
error::{err, Error},
|
||||
error::{fmt::Error as E, Error},
|
||||
util::escape,
|
||||
};
|
||||
|
||||
|
|
@ -218,12 +218,7 @@ impl<'i, V: core::fmt::Display> Parsed<'i, V> {
|
|||
if self.input.is_empty() {
|
||||
return Ok(self.value);
|
||||
}
|
||||
Err(err!(
|
||||
"parsed value '{value}', but unparsed input {unparsed:?} \
|
||||
remains (expected no unparsed input)",
|
||||
value = self.value,
|
||||
unparsed = escape::Bytes(self.input),
|
||||
))
|
||||
Err(Error::from(E::into_full_error(&self.value, self.input)))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -244,12 +239,7 @@ impl<'i, V> Parsed<'i, V> {
|
|||
if self.input.is_empty() {
|
||||
return Ok(self.value);
|
||||
}
|
||||
Err(err!(
|
||||
"parsed value '{value}', but unparsed input {unparsed:?} \
|
||||
remains (expected no unparsed input)",
|
||||
value = display,
|
||||
unparsed = escape::Bytes(self.input),
|
||||
))
|
||||
Err(Error::from(E::into_full_error(&display, self.input)))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -379,7 +369,7 @@ pub struct StdIoWrite<W>(pub W);
|
|||
impl<W: std::io::Write> Write for StdIoWrite<W> {
|
||||
#[inline]
|
||||
fn write_str(&mut self, string: &str) -> Result<(), Error> {
|
||||
self.0.write_all(string.as_bytes()).map_err(Error::adhoc)
|
||||
self.0.write_all(string.as_bytes()).map_err(Error::io)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -422,7 +412,7 @@ impl<W: core::fmt::Write> Write for StdFmtWrite<W> {
|
|||
fn write_str(&mut self, string: &str) -> Result<(), Error> {
|
||||
self.0
|
||||
.write_str(string)
|
||||
.map_err(|_| err!("an error occurred when formatting an argument"))
|
||||
.map_err(|_| Error::from(E::StdFmtWriteAdapter))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ from [Temporal's hybrid grammar].
|
|||
// support a span of time of about 52 hours or so.)
|
||||
|
||||
use crate::{
|
||||
error::{err, Error, ErrorContext},
|
||||
error::{fmt::offset::Error as E, Error, ErrorContext},
|
||||
fmt::{
|
||||
temporal::{PiecesNumericOffset, PiecesOffset},
|
||||
util::{parse_temporal_fraction, FractionalFormatter},
|
||||
|
|
@ -110,7 +110,7 @@ use crate::{
|
|||
},
|
||||
tz::Offset,
|
||||
util::{
|
||||
escape, parse,
|
||||
parse,
|
||||
rangeint::{ri8, RFrom},
|
||||
t::{self, C},
|
||||
},
|
||||
|
|
@ -237,13 +237,7 @@ impl Numeric {
|
|||
if part_nanoseconds >= C(500_000_000) {
|
||||
seconds = seconds
|
||||
.try_checked_add("offset-seconds", C(1))
|
||||
.with_context(|| {
|
||||
err!(
|
||||
"due to precision loss, UTC offset '{}' is \
|
||||
rounded to a value that is out of bounds",
|
||||
self,
|
||||
)
|
||||
})?;
|
||||
.context(E::PrecisionLoss)?;
|
||||
}
|
||||
}
|
||||
Ok(Offset::from_seconds_ranged(seconds * self.sign))
|
||||
|
|
@ -413,18 +407,14 @@ impl Parser {
|
|||
mut input: &'i [u8],
|
||||
) -> Result<Parsed<'i, ParsedOffset>, Error> {
|
||||
if input.is_empty() {
|
||||
return Err(err!("expected UTC offset, but found end of input"));
|
||||
return Err(Error::from(E::EndOfInput));
|
||||
}
|
||||
|
||||
if input[0] == b'Z' || input[0] == b'z' {
|
||||
if !self.zulu {
|
||||
return Err(err!(
|
||||
"found {z:?} in {original:?} where a numeric UTC offset \
|
||||
was expected (this context does not permit \
|
||||
the Zulu offset)",
|
||||
z = escape::Byte(input[0]),
|
||||
original = escape::Bytes(input),
|
||||
));
|
||||
return Err(Error::from(E::UnexpectedLetterOffsetNoZulu(
|
||||
input[0],
|
||||
)));
|
||||
}
|
||||
input = &input[1..];
|
||||
let value = ParsedOffset { kind: ParsedOffsetKind::Zulu };
|
||||
|
|
@ -464,40 +454,24 @@ impl Parser {
|
|||
&self,
|
||||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, Numeric>, Error> {
|
||||
let original = escape::Bytes(input);
|
||||
|
||||
// Parse sign component.
|
||||
let Parsed { value: sign, input } =
|
||||
self.parse_sign(input).with_context(|| {
|
||||
err!("failed to parse sign in UTC numeric offset {original:?}")
|
||||
})?;
|
||||
self.parse_sign(input).context(E::InvalidSign)?;
|
||||
|
||||
// Parse hours component.
|
||||
let Parsed { value: hours, input } =
|
||||
self.parse_hours(input).with_context(|| {
|
||||
err!(
|
||||
"failed to parse hours in UTC numeric offset {original:?}"
|
||||
)
|
||||
})?;
|
||||
self.parse_hours(input).context(E::InvalidHours)?;
|
||||
let extended = match self.colon {
|
||||
Colon::Optional => input.starts_with(b":"),
|
||||
Colon::Required => {
|
||||
if !input.is_empty() && !input.starts_with(b":") {
|
||||
return Err(err!(
|
||||
"parsed hour component of time zone offset from \
|
||||
{original:?}, but could not find required colon \
|
||||
separator",
|
||||
));
|
||||
return Err(Error::from(E::NoColonAfterHours));
|
||||
}
|
||||
true
|
||||
}
|
||||
Colon::Absent => {
|
||||
if !input.is_empty() && input.starts_with(b":") {
|
||||
return Err(err!(
|
||||
"parsed hour component of time zone offset from \
|
||||
{original:?}, but found colon after hours which \
|
||||
is not allowed",
|
||||
));
|
||||
return Err(Error::from(E::ColonAfterHours));
|
||||
}
|
||||
false
|
||||
}
|
||||
|
|
@ -513,32 +487,22 @@ impl Parser {
|
|||
};
|
||||
|
||||
// Parse optional separator after hours.
|
||||
let Parsed { value: has_minutes, input } =
|
||||
self.parse_separator(input, extended).with_context(|| {
|
||||
err!(
|
||||
"failed to parse separator after hours in \
|
||||
UTC numeric offset {original:?}"
|
||||
)
|
||||
})?;
|
||||
let Parsed { value: has_minutes, input } = self
|
||||
.parse_separator(input, extended)
|
||||
.context(E::SeparatorAfterHours)?;
|
||||
if !has_minutes {
|
||||
if self.require_minute || (self.subminute && self.require_second) {
|
||||
return Err(err!(
|
||||
"parsed hour component of time zone offset from \
|
||||
{original:?}, but could not find required minute \
|
||||
component",
|
||||
));
|
||||
}
|
||||
return Ok(Parsed { value: numeric, input });
|
||||
return if self.require_minute
|
||||
|| (self.subminute && self.require_second)
|
||||
{
|
||||
Err(Error::from(E::MissingMinuteAfterHour))
|
||||
} else {
|
||||
Ok(Parsed { value: numeric, input })
|
||||
};
|
||||
}
|
||||
|
||||
// Parse minutes component.
|
||||
let Parsed { value: minutes, input } =
|
||||
self.parse_minutes(input).with_context(|| {
|
||||
err!(
|
||||
"failed to parse minutes in UTC numeric offset \
|
||||
{original:?}"
|
||||
)
|
||||
})?;
|
||||
self.parse_minutes(input).context(E::InvalidMinutes)?;
|
||||
numeric.minutes = Some(minutes);
|
||||
|
||||
// If subminute resolution is not supported, then we're done here.
|
||||
|
|
@ -549,65 +513,42 @@ impl Parser {
|
|||
// more precision than is supported. So we return an error here.
|
||||
// If this winds up being problematic, we can make this error
|
||||
// configurable or remove it altogether (unfortunate).
|
||||
if input.get(0).map_or(false, |&b| b == b':') {
|
||||
return Err(err!(
|
||||
"subminute precision for UTC numeric offset {original:?} \
|
||||
is not enabled in this context (must provide only \
|
||||
integral minutes)",
|
||||
));
|
||||
}
|
||||
return Ok(Parsed { value: numeric, input });
|
||||
return if input.get(0).map_or(false, |&b| b == b':') {
|
||||
Err(Error::from(E::SubminutePrecisionNotEnabled))
|
||||
} else {
|
||||
Ok(Parsed { value: numeric, input })
|
||||
};
|
||||
}
|
||||
|
||||
// Parse optional separator after minutes.
|
||||
let Parsed { value: has_seconds, input } =
|
||||
self.parse_separator(input, extended).with_context(|| {
|
||||
err!(
|
||||
"failed to parse separator after minutes in \
|
||||
UTC numeric offset {original:?}"
|
||||
)
|
||||
})?;
|
||||
let Parsed { value: has_seconds, input } = self
|
||||
.parse_separator(input, extended)
|
||||
.context(E::SeparatorAfterMinutes)?;
|
||||
if !has_seconds {
|
||||
if self.require_second {
|
||||
return Err(err!(
|
||||
"parsed hour and minute components of time zone offset \
|
||||
from {original:?}, but could not find required second \
|
||||
component",
|
||||
));
|
||||
}
|
||||
return Ok(Parsed { value: numeric, input });
|
||||
return if self.require_second {
|
||||
Err(Error::from(E::MissingSecondAfterMinute))
|
||||
} else {
|
||||
Ok(Parsed { value: numeric, input })
|
||||
};
|
||||
}
|
||||
|
||||
// Parse seconds component.
|
||||
let Parsed { value: seconds, input } =
|
||||
self.parse_seconds(input).with_context(|| {
|
||||
err!(
|
||||
"failed to parse seconds in UTC numeric offset \
|
||||
{original:?}"
|
||||
)
|
||||
})?;
|
||||
self.parse_seconds(input).context(E::InvalidSeconds)?;
|
||||
numeric.seconds = Some(seconds);
|
||||
|
||||
// If subsecond resolution is not supported, then we're done here.
|
||||
if !self.subsecond {
|
||||
if input.get(0).map_or(false, |&b| b == b'.' || b == b',') {
|
||||
return Err(err!(
|
||||
"subsecond precision for UTC numeric offset {original:?} \
|
||||
is not enabled in this context (must provide only \
|
||||
integral minutes or seconds)",
|
||||
));
|
||||
return Err(Error::from(E::SubsecondPrecisionNotEnabled));
|
||||
}
|
||||
return Ok(Parsed { value: numeric, input });
|
||||
}
|
||||
|
||||
// Parse an optional fractional component.
|
||||
let Parsed { value: nanoseconds, input } =
|
||||
parse_temporal_fraction(input).with_context(|| {
|
||||
err!(
|
||||
"failed to parse fractional nanoseconds in \
|
||||
UTC numeric offset {original:?}",
|
||||
)
|
||||
})?;
|
||||
parse_temporal_fraction(input)
|
||||
.context(E::InvalidSecondsFractional)?;
|
||||
// OK because `parse_temporal_fraction` guarantees `0..=999_999_999`.
|
||||
numeric.nanoseconds =
|
||||
nanoseconds.map(|n| t::SubsecNanosecond::new(n).unwrap());
|
||||
|
|
@ -619,19 +560,13 @@ impl Parser {
|
|||
&self,
|
||||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, t::Sign>, Error> {
|
||||
let sign = input.get(0).copied().ok_or_else(|| {
|
||||
err!("expected UTC numeric offset, but found end of input")
|
||||
})?;
|
||||
let sign = input.get(0).copied().ok_or(E::EndOfInputNumeric)?;
|
||||
let sign = if sign == b'+' {
|
||||
t::Sign::N::<1>()
|
||||
} else if sign == b'-' {
|
||||
t::Sign::N::<-1>()
|
||||
} else {
|
||||
return Err(err!(
|
||||
"expected '+' or '-' sign at start of UTC numeric offset, \
|
||||
but found {found:?} instead",
|
||||
found = escape::Byte(sign),
|
||||
));
|
||||
return Err(Error::from(E::InvalidSignPlusOrMinus));
|
||||
};
|
||||
Ok(Parsed { value: sign, input: &input[1..] })
|
||||
}
|
||||
|
|
@ -641,22 +576,16 @@ impl Parser {
|
|||
&self,
|
||||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, ParsedOffsetHours>, Error> {
|
||||
let (hours, input) = parse::split(input, 2).ok_or_else(|| {
|
||||
err!("expected two digit hour after sign, but found end of input",)
|
||||
})?;
|
||||
let hours = parse::i64(hours).with_context(|| {
|
||||
err!(
|
||||
"failed to parse {hours:?} as hours (a two digit integer)",
|
||||
hours = escape::Bytes(hours),
|
||||
)
|
||||
})?;
|
||||
let (hours, input) =
|
||||
parse::split(input, 2).ok_or(E::EndOfInputHour)?;
|
||||
let hours = parse::i64(hours).context(E::ParseHours)?;
|
||||
// Note that we support a slightly bigger range of offsets than
|
||||
// Temporal. Temporal seems to support only up to 23 hours, but
|
||||
// we go up to 25 hours. This is done to support POSIX time zone
|
||||
// strings, which also require 25 hours (plus the maximal minute/second
|
||||
// components).
|
||||
let hours = ParsedOffsetHours::try_new("hours", hours)
|
||||
.context("offset hours are not valid")?;
|
||||
.context(E::RangeHours)?;
|
||||
Ok(Parsed { value: hours, input })
|
||||
}
|
||||
|
||||
|
|
@ -665,20 +594,11 @@ impl Parser {
|
|||
&self,
|
||||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, ParsedOffsetMinutes>, Error> {
|
||||
let (minutes, input) = parse::split(input, 2).ok_or_else(|| {
|
||||
err!(
|
||||
"expected two digit minute after hours, \
|
||||
but found end of input",
|
||||
)
|
||||
})?;
|
||||
let minutes = parse::i64(minutes).with_context(|| {
|
||||
err!(
|
||||
"failed to parse {minutes:?} as minutes (a two digit integer)",
|
||||
minutes = escape::Bytes(minutes),
|
||||
)
|
||||
})?;
|
||||
let (minutes, input) =
|
||||
parse::split(input, 2).ok_or(E::EndOfInputMinute)?;
|
||||
let minutes = parse::i64(minutes).context(E::ParseMinutes)?;
|
||||
let minutes = ParsedOffsetMinutes::try_new("minutes", minutes)
|
||||
.context("minutes are not valid")?;
|
||||
.context(E::RangeMinutes)?;
|
||||
Ok(Parsed { value: minutes, input })
|
||||
}
|
||||
|
||||
|
|
@ -687,20 +607,11 @@ impl Parser {
|
|||
&self,
|
||||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, ParsedOffsetSeconds>, Error> {
|
||||
let (seconds, input) = parse::split(input, 2).ok_or_else(|| {
|
||||
err!(
|
||||
"expected two digit second after hours, \
|
||||
but found end of input",
|
||||
)
|
||||
})?;
|
||||
let seconds = parse::i64(seconds).with_context(|| {
|
||||
err!(
|
||||
"failed to parse {seconds:?} as seconds (a two digit integer)",
|
||||
seconds = escape::Bytes(seconds),
|
||||
)
|
||||
})?;
|
||||
let (seconds, input) =
|
||||
parse::split(input, 2).ok_or(E::EndOfInputSecond)?;
|
||||
let seconds = parse::i64(seconds).context(E::ParseSeconds)?;
|
||||
let seconds = ParsedOffsetSeconds::try_new("seconds", seconds)
|
||||
.context("time zone offset seconds are not valid")?;
|
||||
.context(E::RangeSeconds)?;
|
||||
Ok(Parsed { value: seconds, input })
|
||||
}
|
||||
|
||||
|
|
@ -941,7 +852,7 @@ mod tests {
|
|||
fn err_numeric_empty() {
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse_numeric(b"").unwrap_err(),
|
||||
@r###"failed to parse sign in UTC numeric offset "": expected UTC numeric offset, but found end of input"###,
|
||||
@"failed to parse sign in UTC numeric offset: expected UTC numeric offset, but found end of input",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -950,7 +861,7 @@ mod tests {
|
|||
fn err_numeric_notsign() {
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse_numeric(b"*").unwrap_err(),
|
||||
@r###"failed to parse sign in UTC numeric offset "*": expected '+' or '-' sign at start of UTC numeric offset, but found "*" instead"###,
|
||||
@"failed to parse sign in UTC numeric offset: expected `+` or `-` sign at start of UTC numeric offset",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -959,7 +870,7 @@ mod tests {
|
|||
fn err_numeric_hours_too_short() {
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse_numeric(b"+a").unwrap_err(),
|
||||
@r###"failed to parse hours in UTC numeric offset "+a": expected two digit hour after sign, but found end of input"###,
|
||||
@"failed to parse hours in UTC numeric offset: expected two digit hour after sign, but found end of input",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -968,7 +879,7 @@ mod tests {
|
|||
fn err_numeric_hours_invalid_digits() {
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse_numeric(b"+ab").unwrap_err(),
|
||||
@r###"failed to parse hours in UTC numeric offset "+ab": failed to parse "ab" as hours (a two digit integer): invalid digit, expected 0-9 but got a"###,
|
||||
@"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",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -977,7 +888,7 @@ mod tests {
|
|||
fn err_numeric_hours_out_of_range() {
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse_numeric(b"-26").unwrap_err(),
|
||||
@r###"failed to parse hours in UTC numeric offset "-26": offset hours are not valid: parameter 'hours' with value 26 is not in the required range of 0..=25"###,
|
||||
@"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",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -986,7 +897,7 @@ mod tests {
|
|||
fn err_numeric_minutes_too_short() {
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse_numeric(b"+05:a").unwrap_err(),
|
||||
@r###"failed to parse minutes in UTC numeric offset "+05:a": expected two digit minute after hours, but found end of input"###,
|
||||
@"failed to parse minutes in UTC numeric offset: expected two digit minute after hours, but found end of input",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -995,7 +906,7 @@ mod tests {
|
|||
fn err_numeric_minutes_invalid_digits() {
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse_numeric(b"+05:ab").unwrap_err(),
|
||||
@r###"failed to parse minutes in UTC numeric offset "+05:ab": failed to parse "ab" as minutes (a two digit integer): invalid digit, expected 0-9 but got a"###,
|
||||
@"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",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1004,7 +915,7 @@ mod tests {
|
|||
fn err_numeric_minutes_out_of_range() {
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse_numeric(b"-05:60").unwrap_err(),
|
||||
@r###"failed to parse minutes in UTC numeric offset "-05:60": minutes are not valid: parameter 'minutes' with value 60 is not in the required range of 0..=59"###,
|
||||
@"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",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1013,7 +924,7 @@ mod tests {
|
|||
fn err_numeric_seconds_too_short() {
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse_numeric(b"+05:30:a").unwrap_err(),
|
||||
@r###"failed to parse seconds in UTC numeric offset "+05:30:a": expected two digit second after hours, but found end of input"###,
|
||||
@"failed to parse seconds in UTC numeric offset: expected two digit second after minutes, but found end of input",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1022,7 +933,7 @@ mod tests {
|
|||
fn err_numeric_seconds_invalid_digits() {
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse_numeric(b"+05:30:ab").unwrap_err(),
|
||||
@r###"failed to parse seconds in UTC numeric offset "+05:30:ab": failed to parse "ab" as seconds (a two digit integer): invalid digit, expected 0-9 but got a"###,
|
||||
@"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",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1031,7 +942,7 @@ mod tests {
|
|||
fn err_numeric_seconds_out_of_range() {
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse_numeric(b"-05:30:60").unwrap_err(),
|
||||
@r###"failed to parse seconds in UTC numeric offset "-05:30:60": time zone offset seconds are not valid: parameter 'seconds' with value 60 is not in the required range of 0..=59"###,
|
||||
@"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",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1041,31 +952,31 @@ mod tests {
|
|||
fn err_numeric_fraction_non_empty() {
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse_numeric(b"-05:30:44.").unwrap_err(),
|
||||
@r###"failed to parse fractional nanoseconds in UTC numeric offset "-05:30:44.": found decimal after seconds component, but did not find any decimal digits after decimal"###,
|
||||
@"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse_numeric(b"-05:30:44,").unwrap_err(),
|
||||
@r###"failed to parse fractional nanoseconds in UTC numeric offset "-05:30:44,": found decimal after seconds component, but did not find any decimal digits after decimal"###,
|
||||
@"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal",
|
||||
);
|
||||
|
||||
// Instead of end-of-string, add invalid digit.
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse_numeric(b"-05:30:44.a").unwrap_err(),
|
||||
@r###"failed to parse fractional nanoseconds in UTC numeric offset "-05:30:44.a": found decimal after seconds component, but did not find any decimal digits after decimal"###,
|
||||
@"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse_numeric(b"-05:30:44,a").unwrap_err(),
|
||||
@r###"failed to parse fractional nanoseconds in UTC numeric offset "-05:30:44,a": found decimal after seconds component, but did not find any decimal digits after decimal"###,
|
||||
@"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal",
|
||||
);
|
||||
|
||||
// And also test basic format.
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse_numeric(b"-053044.a").unwrap_err(),
|
||||
@r###"failed to parse fractional nanoseconds in UTC numeric offset "-053044.a": found decimal after seconds component, but did not find any decimal digits after decimal"###,
|
||||
@"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse_numeric(b"-053044,a").unwrap_err(),
|
||||
@r###"failed to parse fractional nanoseconds in UTC numeric offset "-053044,a": found decimal after seconds component, but did not find any decimal digits after decimal"###,
|
||||
@"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1076,7 +987,7 @@ mod tests {
|
|||
fn err_numeric_subminute_disabled_but_desired() {
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().subminute(false).parse_numeric(b"-05:59:32").unwrap_err(),
|
||||
@r###"subminute precision for UTC numeric offset "-05:59:32" is not enabled in this context (must provide only integral minutes)"###,
|
||||
@"subminute precision for UTC numeric offset is not enabled in this context (must provide only integral minutes)",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1086,11 +997,11 @@ mod tests {
|
|||
fn err_zulu_disabled_but_desired() {
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().zulu(false).parse(b"Z").unwrap_err(),
|
||||
@r###"found "Z" in "Z" where a numeric UTC offset was expected (this context does not permit the Zulu offset)"###,
|
||||
@"found `Z` where a numeric UTC offset was expected (this context does not permit the Zulu offset)",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().zulu(false).parse(b"z").unwrap_err(),
|
||||
@r###"found "z" in "z" where a numeric UTC offset was expected (this context does not permit the Zulu offset)"###,
|
||||
@"found `z` where a numeric UTC offset was expected (this context does not permit the Zulu offset)",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1118,7 +1029,7 @@ mod tests {
|
|||
};
|
||||
insta::assert_snapshot!(
|
||||
numeric.to_offset().unwrap_err(),
|
||||
@"due to precision loss, UTC offset '+25:59:59.5' 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",
|
||||
@"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",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1143,7 +1054,7 @@ mod tests {
|
|||
};
|
||||
insta::assert_snapshot!(
|
||||
numeric.to_offset().unwrap_err(),
|
||||
@"due to precision loss, UTC offset '-25:59:59.5' 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",
|
||||
@"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",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,11 +43,11 @@ general interchange format for new applications.
|
|||
|
||||
use crate::{
|
||||
civil::{Date, DateTime, Time, Weekday},
|
||||
error::{err, ErrorContext},
|
||||
error::{fmt::rfc2822::Error as E, ErrorContext},
|
||||
fmt::{util::DecimalFormatter, Parsed, Write, WriteExt},
|
||||
tz::{Offset, TimeZone},
|
||||
util::{
|
||||
escape, parse,
|
||||
parse,
|
||||
rangeint::{ri8, RFrom},
|
||||
t::{self, C},
|
||||
},
|
||||
|
|
@ -313,9 +313,7 @@ impl DateTimeParser {
|
|||
let input = input.as_ref();
|
||||
let zdt = self
|
||||
.parse_zoned_internal(input)
|
||||
.context(
|
||||
"failed to parse RFC 2822 datetime into Jiff zoned datetime",
|
||||
)?
|
||||
.context(E::FailedZoned)?
|
||||
.into_full()?;
|
||||
Ok(zdt)
|
||||
}
|
||||
|
|
@ -351,7 +349,7 @@ impl DateTimeParser {
|
|||
let input = input.as_ref();
|
||||
let ts = self
|
||||
.parse_timestamp_internal(input)
|
||||
.context("failed to parse RFC 2822 datetime into Jiff timestamp")?
|
||||
.context(E::FailedTimestamp)?
|
||||
.into_full()?;
|
||||
Ok(ts)
|
||||
}
|
||||
|
|
@ -367,9 +365,7 @@ impl DateTimeParser {
|
|||
) -> Result<Parsed<'i, Zoned>, Error> {
|
||||
let Parsed { value: (dt, offset), input } =
|
||||
self.parse_datetime_offset(input)?;
|
||||
let ts = offset
|
||||
.to_timestamp(dt)
|
||||
.context("RFC 2822 datetime out of Jiff's range")?;
|
||||
let ts = offset.to_timestamp(dt)?;
|
||||
let zdt = ts.to_zoned(TimeZone::fixed(offset));
|
||||
Ok(Parsed { value: zdt, input })
|
||||
}
|
||||
|
|
@ -385,9 +381,7 @@ impl DateTimeParser {
|
|||
) -> Result<Parsed<'i, Timestamp>, Error> {
|
||||
let Parsed { value: (dt, offset), input } =
|
||||
self.parse_datetime_offset(input)?;
|
||||
let ts = offset
|
||||
.to_timestamp(dt)
|
||||
.context("RFC 2822 datetime out of Jiff's range")?;
|
||||
let ts = offset.to_timestamp(dt)?;
|
||||
Ok(Parsed { value: ts, input })
|
||||
}
|
||||
|
||||
|
|
@ -425,16 +419,11 @@ impl DateTimeParser {
|
|||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, DateTime>, Error> {
|
||||
if input.is_empty() {
|
||||
return Err(err!(
|
||||
"expected RFC 2822 datetime, but got empty string"
|
||||
));
|
||||
return Err(Error::from(E::Empty));
|
||||
}
|
||||
let Parsed { input, .. } = self.skip_whitespace(input);
|
||||
if input.is_empty() {
|
||||
return Err(err!(
|
||||
"expected RFC 2822 datetime, but got empty string after \
|
||||
trimming whitespace",
|
||||
));
|
||||
return Err(Error::from(E::EmptyAfterWhitespace));
|
||||
}
|
||||
let Parsed { value: wd, input } = self.parse_weekday(input)?;
|
||||
let Parsed { value: day, input } = self.parse_day(input)?;
|
||||
|
|
@ -451,26 +440,19 @@ impl DateTimeParser {
|
|||
self.skip_whitespace(input);
|
||||
let (second, input) = if !input.starts_with(b":") {
|
||||
if !whitespace_after_minute {
|
||||
return Err(err!(
|
||||
"expected whitespace after parsing time: \
|
||||
expected at least one whitespace character \
|
||||
(space or tab), but found none",
|
||||
));
|
||||
return Err(Error::from(E::WhitespaceAfterTime));
|
||||
}
|
||||
(t::Second::N::<0>(), input)
|
||||
} else {
|
||||
let Parsed { input, .. } = self.parse_time_separator(input)?;
|
||||
let Parsed { input, .. } = self.skip_whitespace(input);
|
||||
let Parsed { value: second, input } = self.parse_second(input)?;
|
||||
let Parsed { input, .. } =
|
||||
self.parse_whitespace(input).with_context(|| {
|
||||
err!("expected whitespace after parsing time")
|
||||
})?;
|
||||
let Parsed { input, .. } = self.parse_whitespace(input)?;
|
||||
(second, input)
|
||||
};
|
||||
|
||||
let date =
|
||||
Date::new_ranged(year, month, day).context("invalid date")?;
|
||||
Date::new_ranged(year, month, day).context(E::InvalidDate)?;
|
||||
let time = Time::new_ranged(
|
||||
hour,
|
||||
minute,
|
||||
|
|
@ -480,13 +462,10 @@ impl DateTimeParser {
|
|||
let dt = DateTime::from_parts(date, time);
|
||||
if let Some(wd) = wd {
|
||||
if !self.relaxed_weekday && wd != dt.weekday() {
|
||||
return Err(err!(
|
||||
"found parsed weekday of {parsed}, \
|
||||
but parsed datetime of {dt} has weekday \
|
||||
{has}",
|
||||
parsed = weekday_abbrev(wd),
|
||||
has = weekday_abbrev(dt.weekday()),
|
||||
));
|
||||
return Err(Error::from(E::InconsistentWeekday {
|
||||
parsed: wd,
|
||||
from_date: dt.weekday(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
Ok(Parsed { value: dt, input })
|
||||
|
|
@ -517,15 +496,13 @@ impl DateTimeParser {
|
|||
if matches!(input[0], b'0'..=b'9') {
|
||||
return Ok(Parsed { value: None, input });
|
||||
}
|
||||
if input.len() < 4 {
|
||||
return Err(err!(
|
||||
"expected day at beginning of RFC 2822 datetime \
|
||||
since first non-whitespace byte, {first:?}, \
|
||||
is not a digit, but given string is too short \
|
||||
(length is {length})",
|
||||
first = escape::Byte(input[0]),
|
||||
length = input.len(),
|
||||
));
|
||||
if let Ok(len) = u8::try_from(input.len()) {
|
||||
if len < 4 {
|
||||
return Err(Error::from(E::TooShortWeekday {
|
||||
got_non_digit: input[0],
|
||||
len,
|
||||
}));
|
||||
}
|
||||
}
|
||||
let b1 = input[0];
|
||||
let b2 = input[1];
|
||||
|
|
@ -543,31 +520,19 @@ impl DateTimeParser {
|
|||
b"fri" => Weekday::Friday,
|
||||
b"sat" => Weekday::Saturday,
|
||||
_ => {
|
||||
return Err(err!(
|
||||
"expected day at beginning of RFC 2822 datetime \
|
||||
since first non-whitespace byte, {first:?}, \
|
||||
is not a digit, but did not recognize {got:?} \
|
||||
as a valid weekday abbreviation",
|
||||
first = escape::Byte(input[0]),
|
||||
got = escape::Bytes(&input[..3]),
|
||||
));
|
||||
return Err(Error::from(E::InvalidWeekday {
|
||||
got_non_digit: input[0],
|
||||
}));
|
||||
}
|
||||
};
|
||||
let Parsed { input, .. } = self.skip_whitespace(&input[3..]);
|
||||
let Some(should_be_comma) = input.get(0).copied() else {
|
||||
return Err(err!(
|
||||
"expected comma after parsed weekday `{weekday}` in \
|
||||
RFC 2822 datetime, but found end of string instead",
|
||||
weekday = escape::Bytes(&[b1, b2, b3]),
|
||||
));
|
||||
return Err(Error::from(E::EndOfInputComma));
|
||||
};
|
||||
if should_be_comma != b',' {
|
||||
return Err(err!(
|
||||
"expected comma after parsed weekday `{weekday}` in \
|
||||
RFC 2822 datetime, but found `{got:?}` instead",
|
||||
weekday = escape::Bytes(&[b1, b2, b3]),
|
||||
got = escape::Byte(should_be_comma),
|
||||
));
|
||||
return Err(Error::from(E::UnexpectedByteComma {
|
||||
byte: should_be_comma,
|
||||
}));
|
||||
}
|
||||
let Parsed { input, .. } = self.skip_whitespace(&input[1..]);
|
||||
Ok(Parsed { value: Some(wd), input })
|
||||
|
|
@ -586,21 +551,17 @@ impl DateTimeParser {
|
|||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, t::Day>, Error> {
|
||||
if input.is_empty() {
|
||||
return Err(err!("expected day, but found end of input"));
|
||||
return Err(Error::from(E::EndOfInputDay));
|
||||
}
|
||||
let mut digits = 1;
|
||||
if input.len() >= 2 && matches!(input[1], b'0'..=b'9') {
|
||||
digits = 2;
|
||||
}
|
||||
let (day, input) = input.split_at(digits);
|
||||
let day = parse::i64(day).with_context(|| {
|
||||
err!("failed to parse {day:?} as day", day = escape::Bytes(day))
|
||||
})?;
|
||||
let day = t::Day::try_new("day", day).context("day is not valid")?;
|
||||
let day = parse::i64(day).context(E::ParseDay)?;
|
||||
let day = t::Day::try_new("day", day).context(E::ParseDay)?;
|
||||
let Parsed { input, .. } =
|
||||
self.parse_whitespace(input).with_context(|| {
|
||||
err!("expected whitespace after parsing day {day}")
|
||||
})?;
|
||||
self.parse_whitespace(input).context(E::WhitespaceAfterDay)?;
|
||||
Ok(Parsed { value: day, input })
|
||||
}
|
||||
|
||||
|
|
@ -617,16 +578,12 @@ impl DateTimeParser {
|
|||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, t::Month>, Error> {
|
||||
if input.is_empty() {
|
||||
return Err(err!(
|
||||
"expected abbreviated month name, but found end of input"
|
||||
));
|
||||
return Err(Error::from(E::EndOfInputMonth));
|
||||
}
|
||||
if input.len() < 3 {
|
||||
return Err(err!(
|
||||
"expected abbreviated month name, but remaining input \
|
||||
is too short (remaining bytes is {length})",
|
||||
length = input.len(),
|
||||
));
|
||||
if let Ok(len) = u8::try_from(input.len()) {
|
||||
if len < 3 {
|
||||
return Err(Error::from(E::TooShortMonth { len }));
|
||||
}
|
||||
}
|
||||
let b1 = input[0].to_ascii_lowercase();
|
||||
let b2 = input[1].to_ascii_lowercase();
|
||||
|
|
@ -644,22 +601,14 @@ impl DateTimeParser {
|
|||
b"oct" => 10,
|
||||
b"nov" => 11,
|
||||
b"dec" => 12,
|
||||
_ => {
|
||||
return Err(err!(
|
||||
"expected abbreviated month name, \
|
||||
but did not recognize {got:?} \
|
||||
as a valid month",
|
||||
got = escape::Bytes(&input[..3]),
|
||||
));
|
||||
}
|
||||
_ => return Err(Error::from(E::InvalidMonth)),
|
||||
};
|
||||
// OK because we just assigned a numeric value ourselves
|
||||
// above, and all values are valid months.
|
||||
let month = t::Month::new(month).unwrap();
|
||||
let Parsed { input, .. } =
|
||||
self.parse_whitespace(&input[3..]).with_context(|| {
|
||||
err!("expected whitespace after parsing month name")
|
||||
})?;
|
||||
let Parsed { input, .. } = self
|
||||
.parse_whitespace(&input[3..])
|
||||
.context(E::WhitespaceAfterMonth)?;
|
||||
Ok(Parsed { value: month, input })
|
||||
}
|
||||
|
||||
|
|
@ -692,31 +641,22 @@ impl DateTimeParser {
|
|||
{
|
||||
digits += 1;
|
||||
}
|
||||
if digits <= 1 {
|
||||
return Err(err!(
|
||||
"expected at least two ASCII digits for parsing \
|
||||
a year, but only found {digits}",
|
||||
));
|
||||
if let Ok(len) = u8::try_from(digits) {
|
||||
if len <= 1 {
|
||||
return Err(Error::from(E::TooShortYear { len }));
|
||||
}
|
||||
}
|
||||
let (year, input) = input.split_at(digits);
|
||||
let year = parse::i64(year).with_context(|| {
|
||||
err!(
|
||||
"failed to parse {year:?} as year \
|
||||
(a two, three or four digit integer)",
|
||||
year = escape::Bytes(year),
|
||||
)
|
||||
})?;
|
||||
let year = parse::i64(year).context(E::ParseYear)?;
|
||||
let year = match digits {
|
||||
2 if year <= 49 => year + 2000,
|
||||
2 | 3 => year + 1900,
|
||||
4 => year,
|
||||
_ => unreachable!("digits={digits} must be 2, 3 or 4"),
|
||||
};
|
||||
let year =
|
||||
t::Year::try_new("year", year).context("year is not valid")?;
|
||||
let Parsed { input, .. } = self
|
||||
.parse_whitespace(input)
|
||||
.with_context(|| err!("expected whitespace after parsing year"))?;
|
||||
let year = t::Year::try_new("year", year).context(E::InvalidYear)?;
|
||||
let Parsed { input, .. } =
|
||||
self.parse_whitespace(input).context(E::WhitespaceAfterYear)?;
|
||||
Ok(Parsed { value: year, input })
|
||||
}
|
||||
|
||||
|
|
@ -730,17 +670,9 @@ impl DateTimeParser {
|
|||
&self,
|
||||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, t::Hour>, Error> {
|
||||
let (hour, input) = parse::split(input, 2).ok_or_else(|| {
|
||||
err!("expected two digit hour, but found end of input")
|
||||
})?;
|
||||
let hour = parse::i64(hour).with_context(|| {
|
||||
err!(
|
||||
"failed to parse {hour:?} as hour (a two digit integer)",
|
||||
hour = escape::Bytes(hour),
|
||||
)
|
||||
})?;
|
||||
let hour =
|
||||
t::Hour::try_new("hour", hour).context("hour is not valid")?;
|
||||
let (hour, input) = parse::split(input, 2).ok_or(E::EndOfInputHour)?;
|
||||
let hour = parse::i64(hour).context(E::ParseHour)?;
|
||||
let hour = t::Hour::try_new("hour", hour).context(E::InvalidHour)?;
|
||||
Ok(Parsed { value: hour, input })
|
||||
}
|
||||
|
||||
|
|
@ -751,17 +683,11 @@ impl DateTimeParser {
|
|||
&self,
|
||||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, t::Minute>, Error> {
|
||||
let (minute, input) = parse::split(input, 2).ok_or_else(|| {
|
||||
err!("expected two digit minute, but found end of input")
|
||||
})?;
|
||||
let minute = parse::i64(minute).with_context(|| {
|
||||
err!(
|
||||
"failed to parse {minute:?} as minute (a two digit integer)",
|
||||
minute = escape::Bytes(minute),
|
||||
)
|
||||
})?;
|
||||
let minute = t::Minute::try_new("minute", minute)
|
||||
.context("minute is not valid")?;
|
||||
let (minute, input) =
|
||||
parse::split(input, 2).ok_or(E::EndOfInputMinute)?;
|
||||
let minute = parse::i64(minute).context(E::ParseMinute)?;
|
||||
let minute =
|
||||
t::Minute::try_new("minute", minute).context(E::InvalidMinute)?;
|
||||
Ok(Parsed { value: minute, input })
|
||||
}
|
||||
|
||||
|
|
@ -772,20 +698,14 @@ impl DateTimeParser {
|
|||
&self,
|
||||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, t::Second>, Error> {
|
||||
let (second, input) = parse::split(input, 2).ok_or_else(|| {
|
||||
err!("expected two digit second, but found end of input")
|
||||
})?;
|
||||
let mut second = parse::i64(second).with_context(|| {
|
||||
err!(
|
||||
"failed to parse {second:?} as second (a two digit integer)",
|
||||
second = escape::Bytes(second),
|
||||
)
|
||||
})?;
|
||||
let (second, input) =
|
||||
parse::split(input, 2).ok_or(E::EndOfInputSecond)?;
|
||||
let mut second = parse::i64(second).context(E::ParseSecond)?;
|
||||
if second == 60 {
|
||||
second = 59;
|
||||
}
|
||||
let second = t::Second::try_new("second", second)
|
||||
.context("second is not valid")?;
|
||||
let second =
|
||||
t::Second::try_new("second", second).context(E::InvalidSecond)?;
|
||||
Ok(Parsed { value: second, input })
|
||||
}
|
||||
|
||||
|
|
@ -801,13 +721,7 @@ impl DateTimeParser {
|
|||
type ParsedOffsetHours = ri8<0, { t::SpanZoneOffsetHours::MAX }>;
|
||||
type ParsedOffsetMinutes = ri8<0, { t::SpanZoneOffsetMinutes::MAX }>;
|
||||
|
||||
let sign = input.get(0).copied().ok_or_else(|| {
|
||||
err!(
|
||||
"expected sign for time zone offset, \
|
||||
(or a legacy time zone name abbreviation), \
|
||||
but found end of input",
|
||||
)
|
||||
})?;
|
||||
let sign = input.get(0).copied().ok_or(E::EndOfInputOffset)?;
|
||||
let sign = if sign == b'+' {
|
||||
t::Sign::N::<1>()
|
||||
} else if sign == b'-' {
|
||||
|
|
@ -816,32 +730,16 @@ impl DateTimeParser {
|
|||
return self.parse_offset_obsolete(input);
|
||||
};
|
||||
let input = &input[1..];
|
||||
let (hhmm, input) = parse::split(input, 4).ok_or_else(|| {
|
||||
err!(
|
||||
"expected at least 4 digits for time zone offset \
|
||||
after sign, but found only {len} bytes remaining",
|
||||
len = input.len(),
|
||||
)
|
||||
})?;
|
||||
let (hhmm, input) = parse::split(input, 4).ok_or(E::TooShortOffset)?;
|
||||
|
||||
let hh = parse::i64(&hhmm[0..2]).with_context(|| {
|
||||
err!(
|
||||
"failed to parse hours from time zone offset {hhmm}",
|
||||
hhmm = escape::Bytes(hhmm)
|
||||
)
|
||||
})?;
|
||||
let hh = parse::i64(&hhmm[0..2]).context(E::ParseOffsetHour)?;
|
||||
let hh = ParsedOffsetHours::try_new("zone-offset-hours", hh)
|
||||
.context("time zone offset hours are not valid")?;
|
||||
.context(E::InvalidOffsetHour)?;
|
||||
let hh = t::SpanZoneOffset::rfrom(hh);
|
||||
|
||||
let mm = parse::i64(&hhmm[2..4]).with_context(|| {
|
||||
err!(
|
||||
"failed to parse minutes from time zone offset {hhmm}",
|
||||
hhmm = escape::Bytes(hhmm)
|
||||
)
|
||||
})?;
|
||||
let mm = parse::i64(&hhmm[2..4]).context(E::ParseOffsetMinute)?;
|
||||
let mm = ParsedOffsetMinutes::try_new("zone-offset-minutes", mm)
|
||||
.context("time zone offset minutes are not valid")?;
|
||||
.context(E::InvalidOffsetMinute)?;
|
||||
let mm = t::SpanZoneOffset::rfrom(mm);
|
||||
|
||||
let seconds = hh * C(3_600) + mm * C(60);
|
||||
|
|
@ -865,11 +763,7 @@ impl DateTimeParser {
|
|||
len += 1;
|
||||
}
|
||||
if len == 0 {
|
||||
return Err(err!(
|
||||
"expected obsolete RFC 2822 time zone abbreviation, \
|
||||
but found no remaining non-whitespace characters \
|
||||
after time",
|
||||
));
|
||||
return Err(Error::from(E::WhitespaceAfterTimeForObsoleteOffset));
|
||||
}
|
||||
let offset = match &letters[..len] {
|
||||
b"ut" | b"gmt" | b"z" => Offset::UTC,
|
||||
|
|
@ -917,11 +811,7 @@ impl DateTimeParser {
|
|||
Offset::UTC
|
||||
} else {
|
||||
// But anything else we throw our hands up I guess.
|
||||
return Err(err!(
|
||||
"expected obsolete RFC 2822 time zone abbreviation, \
|
||||
but found {found:?}",
|
||||
found = escape::Bytes(&input[..len]),
|
||||
));
|
||||
return Err(Error::from(E::InvalidObsoleteOffset));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -936,15 +826,12 @@ impl DateTimeParser {
|
|||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, ()>, Error> {
|
||||
if input.is_empty() {
|
||||
return Err(err!(
|
||||
"expected time separator of ':', but found end of input",
|
||||
));
|
||||
return Err(Error::from(E::EndOfInputTimeSeparator));
|
||||
}
|
||||
if input[0] != b':' {
|
||||
return Err(err!(
|
||||
"expected time separator of ':', but found {got}",
|
||||
got = escape::Byte(input[0]),
|
||||
));
|
||||
return Err(Error::from(E::UnexpectedByteTimeSeparator {
|
||||
byte: input[0],
|
||||
}));
|
||||
}
|
||||
Ok(Parsed { value: (), input: &input[1..] })
|
||||
}
|
||||
|
|
@ -959,10 +846,7 @@ impl DateTimeParser {
|
|||
let Parsed { input, value: had_whitespace } =
|
||||
self.skip_whitespace(input);
|
||||
if !had_whitespace {
|
||||
return Err(err!(
|
||||
"expected at least one whitespace character (space or tab), \
|
||||
but found none",
|
||||
));
|
||||
return Err(Error::from(E::WhitespaceAfterTime));
|
||||
}
|
||||
Ok(Parsed { value: (), input })
|
||||
}
|
||||
|
|
@ -1012,26 +896,20 @@ impl DateTimeParser {
|
|||
// I believe this error case is actually impossible, since as
|
||||
// soon as we hit 0, we break out. If there is more "comment,"
|
||||
// then it will flag an error as unparsed input.
|
||||
depth = depth.checked_sub(1).ok_or_else(|| {
|
||||
err!(
|
||||
"found closing parenthesis in comment with \
|
||||
no matching opening parenthesis"
|
||||
)
|
||||
})?;
|
||||
depth = depth
|
||||
.checked_sub(1)
|
||||
.ok_or(E::CommentClosingParenWithoutOpen)?;
|
||||
if depth == 0 {
|
||||
break;
|
||||
}
|
||||
} else if byte == b'(' {
|
||||
depth = depth.checked_add(1).ok_or_else(|| {
|
||||
err!("found too many nested parenthesis in comment")
|
||||
})?;
|
||||
depth = depth
|
||||
.checked_add(1)
|
||||
.ok_or(E::CommentTooManyNestedParens)?;
|
||||
}
|
||||
}
|
||||
if depth > 0 {
|
||||
return Err(err!(
|
||||
"found opening parenthesis in comment with \
|
||||
no matching closing parenthesis"
|
||||
));
|
||||
return Err(Error::from(E::CommentOpeningParenWithoutClose));
|
||||
}
|
||||
let Parsed { input, .. } = self.skip_whitespace(input);
|
||||
Ok(Parsed { value: (), input })
|
||||
|
|
@ -1423,10 +1301,7 @@ impl DateTimePrinter {
|
|||
// RFC 2822 actually says the year must be at least 1900, but
|
||||
// other implementations (like Chrono) allow any positive 4-digit
|
||||
// year.
|
||||
return Err(err!(
|
||||
"datetime {dt} has negative year, \
|
||||
which cannot be formatted with RFC 2822",
|
||||
));
|
||||
return Err(Error::from(E::NegativeYear));
|
||||
}
|
||||
|
||||
wtr.write_str(weekday_abbrev(dt.weekday()))?;
|
||||
|
|
@ -1484,10 +1359,7 @@ impl DateTimePrinter {
|
|||
// RFC 2822 actually says the year must be at least 1900, but
|
||||
// other implementations (like Chrono) allow any positive 4-digit
|
||||
// year.
|
||||
return Err(err!(
|
||||
"datetime {dt} has negative year, \
|
||||
which cannot be formatted with RFC 2822",
|
||||
));
|
||||
return Err(Error::from(E::NegativeYear));
|
||||
}
|
||||
|
||||
wtr.write_str(weekday_abbrev(dt.weekday()))?;
|
||||
|
|
@ -1743,7 +1615,7 @@ mod tests {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
p("Thu, 10 Jan 2024 05:34:45 -0500"),
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: found parsed weekday of Thu, but parsed datetime of 2024-01-10T05:34:45 has weekday Wed",
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: found parsed weekday of `Thursday`, but parsed datetime has weekday `Wednesday`",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Wed, 29 Feb 2023 05:34:45 -0500"),
|
||||
|
|
@ -1755,11 +1627,11 @@ mod tests {
|
|||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Tue, 32 Jun 2024 05:34:45 -0500"),
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: day is not valid: parameter 'day' with value 32 is not in the required range of 1..=31",
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: failed to parse day: parameter 'day' with value 32 is not in the required range of 1..=31",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Sun, 30 Jun 2024 24:00:00 -0500"),
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: hour is not valid: parameter 'hour' with value 24 is not in the required range of 0..=23",
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: invalid hour: parameter 'hour' with value 24 is not in the required range of 0..=23",
|
||||
);
|
||||
// No whitespace after time
|
||||
insta::assert_snapshot!(
|
||||
|
|
@ -1780,43 +1652,43 @@ mod tests {
|
|||
);
|
||||
insta::assert_snapshot!(
|
||||
p(" "),
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected RFC 2822 datetime, but got empty string after trimming whitespace",
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected RFC 2822 datetime, but got empty string after trimming leading whitespace",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Wat"),
|
||||
@r###"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day at beginning of RFC 2822 datetime since first non-whitespace byte, "W", is not a digit, but given string is too short (length is 3)"###,
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day at beginning of RFC 2822 datetime since first non-whitespace byte, `W`, is not a digit, but given string is too short (length is 3)",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Wed"),
|
||||
@r###"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day at beginning of RFC 2822 datetime since first non-whitespace byte, "W", is not a digit, but given string is too short (length is 3)"###,
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day at beginning of RFC 2822 datetime since first non-whitespace byte, `W`, is not a digit, but given string is too short (length is 3)",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Wed "),
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected comma after parsed weekday `Wed` in RFC 2822 datetime, but found end of string instead",
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected comma after parsed weekday in RFC 2822 datetime, but found end of input instead",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Wed ,"),
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day, but found end of input",
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected numeric day, but found end of input",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Wed , "),
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day, but found end of input",
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected numeric day, but found end of input",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Wat, "),
|
||||
@r###"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day at beginning of RFC 2822 datetime since first non-whitespace byte, "W", is not a digit, but did not recognize "Wat" as a valid weekday abbreviation"###,
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day at beginning of RFC 2822 datetime since first non-whitespace byte, `W`, is not a digit, but did not recognize a valid weekday abbreviation",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Wed, "),
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day, but found end of input",
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected numeric day, but found end of input",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Wed, 1"),
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing day 1: expected at least one whitespace character (space or tab), but found none",
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing day: expected whitespace after parsing time: expected at least one whitespace character (space or tab), but found none",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Wed, 10"),
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing day 10: expected at least one whitespace character (space or tab), but found none",
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing day: expected whitespace after parsing time: expected at least one whitespace character (space or tab), but found none",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Wed, 10 J"),
|
||||
|
|
@ -1824,11 +1696,11 @@ mod tests {
|
|||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Wed, 10 Wat"),
|
||||
@r###"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected abbreviated month name, but did not recognize "Wat" as a valid month"###,
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected abbreviated month name, but did not recognize a valid abbreviated month name",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Wed, 10 Jan"),
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing month name: expected at least one whitespace character (space or tab), but found none",
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing abbreviated month name: expected whitespace after parsing time: expected at least one whitespace character (space or tab), but found none",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Wed, 10 Jan 2"),
|
||||
|
|
@ -1836,15 +1708,15 @@ mod tests {
|
|||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Wed, 10 Jan 2024"),
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing year: expected at least one whitespace character (space or tab), but found none",
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing year: expected whitespace after parsing time: expected at least one whitespace character (space or tab), but found none",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Wed, 10 Jan 2024 05"),
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected time separator of ':', but found end of input",
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected time separator of `:`, but found end of input",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Wed, 10 Jan 2024 053"),
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected time separator of ':', but found 3",
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected time separator of `:`, but found `3`",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Wed, 10 Jan 2024 05:34"),
|
||||
|
|
@ -1860,7 +1732,7 @@ mod tests {
|
|||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Wed, 10 Jan 2024 05:34:45 J"),
|
||||
@r###"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected obsolete RFC 2822 time zone abbreviation, but found "J""###,
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected obsolete RFC 2822 time zone abbreviation, but did not recognize a valid abbreviation",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -2040,7 +1912,7 @@ mod tests {
|
|||
.at(5, 34, 45, 0)
|
||||
.in_tz("America/New_York")
|
||||
.unwrap();
|
||||
insta::assert_snapshot!(p(&zdt), @"datetime -000001-01-10T05:34:45 has negative year, which cannot be formatted with RFC 2822");
|
||||
insta::assert_snapshot!(p(&zdt), @"datetime has negative year, which cannot be formatted with RFC 2822");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -2062,6 +1934,6 @@ mod tests {
|
|||
.in_tz("America/New_York")
|
||||
.unwrap()
|
||||
.timestamp();
|
||||
insta::assert_snapshot!(p(ts), @"datetime -000001-01-10T10:30:47 has negative year, which cannot be formatted with RFC 2822");
|
||||
insta::assert_snapshot!(p(ts), @"datetime has negative year, which cannot be formatted with RFC 2822");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,13 +95,13 @@ including by returning an error if it isn't supported.
|
|||
// UTCOffsetMinutePrecision
|
||||
|
||||
use crate::{
|
||||
error::{err, Error},
|
||||
error::{fmt::rfc9557::Error as E, Error},
|
||||
fmt::{
|
||||
offset::{self, ParsedOffset},
|
||||
temporal::{TimeZoneAnnotation, TimeZoneAnnotationKind},
|
||||
Parsed,
|
||||
},
|
||||
util::{escape, parse},
|
||||
util::parse,
|
||||
};
|
||||
|
||||
/// The result of parsing RFC 9557 annotations.
|
||||
|
|
@ -112,11 +112,6 @@ use crate::{
|
|||
/// only validated at a syntax level.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ParsedAnnotations<'i> {
|
||||
/// The original input that all of the annotations were parsed from.
|
||||
///
|
||||
/// N.B. This is currently unused, but potentially useful, so we leave it.
|
||||
#[allow(dead_code)]
|
||||
input: escape::Bytes<'i>,
|
||||
/// An optional time zone annotation that was extracted from the input.
|
||||
time_zone: Option<ParsedTimeZone<'i>>,
|
||||
// While we parse/validate them, we don't support any other annotations
|
||||
|
|
@ -127,7 +122,7 @@ pub(crate) struct ParsedAnnotations<'i> {
|
|||
impl<'i> ParsedAnnotations<'i> {
|
||||
/// Return an empty parsed annotations.
|
||||
pub(crate) fn none() -> ParsedAnnotations<'static> {
|
||||
ParsedAnnotations { input: escape::Bytes(&[]), time_zone: None }
|
||||
ParsedAnnotations { time_zone: None }
|
||||
}
|
||||
|
||||
/// Turns this parsed time zone into a structured time zone annotation,
|
||||
|
|
@ -212,8 +207,6 @@ impl Parser {
|
|||
&self,
|
||||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, ParsedAnnotations<'i>>, Error> {
|
||||
let mkslice = parse::slicer(input);
|
||||
|
||||
let Parsed { value: time_zone, mut input } =
|
||||
self.parse_time_zone_annotation(input)?;
|
||||
loop {
|
||||
|
|
@ -229,10 +222,7 @@ impl Parser {
|
|||
input = unconsumed;
|
||||
}
|
||||
|
||||
let value = ParsedAnnotations {
|
||||
input: escape::Bytes(mkslice(input)),
|
||||
time_zone,
|
||||
};
|
||||
let value = ParsedAnnotations { time_zone };
|
||||
Ok(Parsed { value, input })
|
||||
}
|
||||
|
||||
|
|
@ -241,14 +231,18 @@ impl Parser {
|
|||
mut input: &'i [u8],
|
||||
) -> Result<Parsed<'i, Option<ParsedTimeZone<'i>>>, Error> {
|
||||
let unconsumed = input;
|
||||
if input.is_empty() || input[0] != b'[' {
|
||||
let Some((&first, tail)) = input.split_first() else {
|
||||
return Ok(Parsed { value: None, input: unconsumed });
|
||||
};
|
||||
if first != b'[' {
|
||||
return Ok(Parsed { value: None, input: unconsumed });
|
||||
}
|
||||
input = &input[1..];
|
||||
input = tail;
|
||||
|
||||
let critical = input.starts_with(b"!");
|
||||
if critical {
|
||||
input = &input[1..];
|
||||
let mut critical = false;
|
||||
if let Some(tail) = input.strip_prefix(b"!") {
|
||||
critical = true;
|
||||
input = tail;
|
||||
}
|
||||
|
||||
// If we're starting with a `+` or `-`, then we know we MUST have a
|
||||
|
|
@ -284,8 +278,8 @@ impl Parser {
|
|||
// a generic key/value annotation.
|
||||
return Ok(Parsed { value: None, input: unconsumed });
|
||||
}
|
||||
while input.starts_with(b"/") {
|
||||
input = &input[1..];
|
||||
while let Some(tail) = input.strip_prefix(b"/") {
|
||||
input = tail;
|
||||
let Parsed { input: unconsumed, .. } =
|
||||
self.parse_tz_annotation_iana_name(input)?;
|
||||
input = unconsumed;
|
||||
|
|
@ -306,17 +300,21 @@ impl Parser {
|
|||
&self,
|
||||
mut input: &'i [u8],
|
||||
) -> Result<Parsed<'i, bool>, Error> {
|
||||
if input.is_empty() || input[0] != b'[' {
|
||||
let Some((&first, tail)) = input.split_first() else {
|
||||
return Ok(Parsed { value: false, input });
|
||||
};
|
||||
if first != b'[' {
|
||||
return Ok(Parsed { value: false, input });
|
||||
}
|
||||
input = &input[1..];
|
||||
input = tail;
|
||||
|
||||
let critical = input.starts_with(b"!");
|
||||
if critical {
|
||||
input = &input[1..];
|
||||
let mut critical = false;
|
||||
if let Some(tail) = input.strip_prefix(b"!") {
|
||||
critical = true;
|
||||
input = tail;
|
||||
}
|
||||
|
||||
let Parsed { value: key, input } = self.parse_annotation_key(input)?;
|
||||
let Parsed { input, .. } = self.parse_annotation_key(input)?;
|
||||
let Parsed { input, .. } = self.parse_annotation_separator(input)?;
|
||||
let Parsed { input, .. } = self.parse_annotation_values(input)?;
|
||||
let Parsed { input, .. } = self.parse_annotation_close(input)?;
|
||||
|
|
@ -326,11 +324,7 @@ impl Parser {
|
|||
// critical flag isn't set, we're "permissive" and just validate that
|
||||
// the syntax is correct (as we've already done at this point).
|
||||
if critical {
|
||||
return Err(err!(
|
||||
"found unsupported RFC 9557 annotation with key {key:?} \
|
||||
with the critical flag ('!') set",
|
||||
key = escape::Bytes(key),
|
||||
));
|
||||
return Err(Error::from(E::UnsupportedAnnotationCritical));
|
||||
}
|
||||
|
||||
Ok(Parsed { value: true, input })
|
||||
|
|
@ -381,8 +375,8 @@ impl Parser {
|
|||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, ()>, Error> {
|
||||
let Parsed { mut input, .. } = self.parse_annotation_value(input)?;
|
||||
while input.starts_with(b"-") {
|
||||
input = &input[1..];
|
||||
while let Some(tail) = input.strip_prefix(b"-") {
|
||||
input = tail;
|
||||
let Parsed { input: unconsumed, .. } =
|
||||
self.parse_annotation_value(input)?;
|
||||
input = unconsumed;
|
||||
|
|
@ -413,173 +407,137 @@ impl Parser {
|
|||
&self,
|
||||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, ()>, Error> {
|
||||
if input.is_empty() {
|
||||
return Err(err!(
|
||||
"expected the start of an RFC 9557 annotation or IANA \
|
||||
time zone component name, but found end of input instead",
|
||||
));
|
||||
let Some((&first, tail)) = input.split_first() else {
|
||||
return Err(Error::from(E::EndOfInputAnnotation));
|
||||
};
|
||||
if !matches!(first, b'_' | b'.' | b'A'..=b'Z' | b'a'..=b'z') {
|
||||
return Err(Error::from(E::UnexpectedByteAnnotation {
|
||||
byte: first,
|
||||
}));
|
||||
}
|
||||
if !matches!(input[0], b'_' | b'.' | b'A'..=b'Z' | b'a'..=b'z') {
|
||||
return Err(err!(
|
||||
"expected ASCII alphabetic byte (or underscore or period) \
|
||||
at the start of an RFC 9557 annotation or time zone \
|
||||
component name, but found {:?} instead",
|
||||
escape::Byte(input[0]),
|
||||
));
|
||||
}
|
||||
Ok(Parsed { value: (), input: &input[1..] })
|
||||
Ok(Parsed { value: (), input: tail })
|
||||
}
|
||||
|
||||
fn parse_tz_annotation_char<'i>(
|
||||
&self,
|
||||
input: &'i [u8],
|
||||
) -> Parsed<'i, bool> {
|
||||
let is_tz_annotation_char = |byte| {
|
||||
matches!(
|
||||
byte,
|
||||
b'_' | b'.' | b'+' | b'-' | b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z',
|
||||
)
|
||||
let Some((&first, tail)) = input.split_first() else {
|
||||
return Parsed { value: false, input };
|
||||
};
|
||||
if input.is_empty() || !is_tz_annotation_char(input[0]) {
|
||||
|
||||
if !matches!(
|
||||
first,
|
||||
b'_' | b'.' | b'+' | b'-' | b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z',
|
||||
) {
|
||||
return Parsed { value: false, input };
|
||||
}
|
||||
Parsed { value: true, input: &input[1..] }
|
||||
Parsed { value: true, input: tail }
|
||||
}
|
||||
|
||||
fn parse_annotation_key_leading_char<'i>(
|
||||
&self,
|
||||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, ()>, Error> {
|
||||
if input.is_empty() {
|
||||
return Err(err!(
|
||||
"expected the start of an RFC 9557 annotation key, \
|
||||
but found end of input instead",
|
||||
));
|
||||
let Some((&first, tail)) = input.split_first() else {
|
||||
return Err(Error::from(E::EndOfInputAnnotationKey));
|
||||
};
|
||||
if !matches!(first, b'_' | b'a'..=b'z') {
|
||||
return Err(Error::from(E::UnexpectedByteAnnotationKey {
|
||||
byte: first,
|
||||
}));
|
||||
}
|
||||
if !matches!(input[0], b'_' | b'a'..=b'z') {
|
||||
return Err(err!(
|
||||
"expected lowercase alphabetic byte (or underscore) \
|
||||
at the start of an RFC 9557 annotation key, \
|
||||
but found {:?} instead",
|
||||
escape::Byte(input[0]),
|
||||
));
|
||||
}
|
||||
Ok(Parsed { value: (), input: &input[1..] })
|
||||
Ok(Parsed { value: (), input: tail })
|
||||
}
|
||||
|
||||
fn parse_annotation_key_char<'i>(
|
||||
&self,
|
||||
input: &'i [u8],
|
||||
) -> Parsed<'i, bool> {
|
||||
let is_annotation_key_char =
|
||||
|byte| matches!(byte, b'_' | b'-' | b'0'..=b'9' | b'a'..=b'z');
|
||||
if input.is_empty() || !is_annotation_key_char(input[0]) {
|
||||
let Some((&first, tail)) = input.split_first() else {
|
||||
return Parsed { value: false, input };
|
||||
};
|
||||
if !matches!(first, b'_' | b'-' | b'0'..=b'9' | b'a'..=b'z') {
|
||||
return Parsed { value: false, input };
|
||||
}
|
||||
Parsed { value: true, input: &input[1..] }
|
||||
Parsed { value: true, input: tail }
|
||||
}
|
||||
|
||||
fn parse_annotation_value_leading_char<'i>(
|
||||
&self,
|
||||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, ()>, Error> {
|
||||
if input.is_empty() {
|
||||
return Err(err!(
|
||||
"expected the start of an RFC 9557 annotation value, \
|
||||
but found end of input instead",
|
||||
));
|
||||
let Some((&first, tail)) = input.split_first() else {
|
||||
return Err(Error::from(E::EndOfInputAnnotationValue));
|
||||
};
|
||||
if !matches!(first, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z') {
|
||||
return Err(Error::from(E::UnexpectedByteAnnotationValue {
|
||||
byte: first,
|
||||
}));
|
||||
}
|
||||
if !matches!(input[0], b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z') {
|
||||
return Err(err!(
|
||||
"expected alphanumeric ASCII byte \
|
||||
at the start of an RFC 9557 annotation value, \
|
||||
but found {:?} instead",
|
||||
escape::Byte(input[0]),
|
||||
));
|
||||
}
|
||||
Ok(Parsed { value: (), input: &input[1..] })
|
||||
Ok(Parsed { value: (), input: tail })
|
||||
}
|
||||
|
||||
fn parse_annotation_value_char<'i>(
|
||||
&self,
|
||||
input: &'i [u8],
|
||||
) -> Parsed<'i, bool> {
|
||||
let is_annotation_value_char =
|
||||
|byte| matches!(byte, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z');
|
||||
if input.is_empty() || !is_annotation_value_char(input[0]) {
|
||||
let Some((&first, tail)) = input.split_first() else {
|
||||
return Parsed { value: false, input };
|
||||
};
|
||||
if !matches!(first, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z') {
|
||||
return Parsed { value: false, input };
|
||||
}
|
||||
Parsed { value: true, input: &input[1..] }
|
||||
Parsed { value: true, input: tail }
|
||||
}
|
||||
|
||||
fn parse_annotation_separator<'i>(
|
||||
&self,
|
||||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, ()>, Error> {
|
||||
if input.is_empty() {
|
||||
return Err(err!(
|
||||
"expected an '=' after parsing an RFC 9557 annotation key, \
|
||||
but found end of input instead",
|
||||
));
|
||||
}
|
||||
if input[0] != b'=' {
|
||||
let Some((&first, tail)) = input.split_first() else {
|
||||
return Err(Error::from(E::EndOfInputAnnotationSeparator));
|
||||
};
|
||||
if first != b'=' {
|
||||
// If we see a /, then it's likely the user was trying to insert a
|
||||
// time zone annotation in the wrong place.
|
||||
return Err(if input[0] == b'/' {
|
||||
err!(
|
||||
"expected an '=' after parsing an RFC 9557 annotation \
|
||||
key, but found / instead (time zone annotations must \
|
||||
come first)",
|
||||
)
|
||||
return Err(Error::from(if first == b'/' {
|
||||
E::UnexpectedSlashAnnotationSeparator
|
||||
} else {
|
||||
err!(
|
||||
"expected an '=' after parsing an RFC 9557 annotation \
|
||||
key, but found {:?} instead",
|
||||
escape::Byte(input[0]),
|
||||
)
|
||||
});
|
||||
E::UnexpectedByteAnnotationSeparator { byte: first }
|
||||
}));
|
||||
}
|
||||
Ok(Parsed { value: (), input: &input[1..] })
|
||||
Ok(Parsed { value: (), input: tail })
|
||||
}
|
||||
|
||||
fn parse_annotation_close<'i>(
|
||||
&self,
|
||||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, ()>, Error> {
|
||||
if input.is_empty() {
|
||||
return Err(err!(
|
||||
"expected an ']' after parsing an RFC 9557 annotation key \
|
||||
and value, but found end of input instead",
|
||||
));
|
||||
let Some((&first, tail)) = input.split_first() else {
|
||||
return Err(Error::from(E::EndOfInputAnnotationClose));
|
||||
};
|
||||
if first != b']' {
|
||||
return Err(Error::from(E::UnexpectedByteAnnotationClose {
|
||||
byte: first,
|
||||
}));
|
||||
}
|
||||
if input[0] != b']' {
|
||||
return Err(err!(
|
||||
"expected an ']' after parsing an RFC 9557 annotation key \
|
||||
and value, but found {:?} instead",
|
||||
escape::Byte(input[0]),
|
||||
));
|
||||
}
|
||||
Ok(Parsed { value: (), input: &input[1..] })
|
||||
Ok(Parsed { value: (), input: tail })
|
||||
}
|
||||
|
||||
fn parse_tz_annotation_close<'i>(
|
||||
&self,
|
||||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, ()>, Error> {
|
||||
if input.is_empty() {
|
||||
return Err(err!(
|
||||
"expected an ']' after parsing an RFC 9557 time zone \
|
||||
annotation, but found end of input instead",
|
||||
));
|
||||
let Some((&first, tail)) = input.split_first() else {
|
||||
return Err(Error::from(E::EndOfInputTzAnnotationClose));
|
||||
};
|
||||
if first != b']' {
|
||||
return Err(Error::from(E::UnexpectedByteTzAnnotationClose {
|
||||
byte: first,
|
||||
}));
|
||||
}
|
||||
if input[0] != b']' {
|
||||
return Err(err!(
|
||||
"expected an ']' after parsing an RFC 9557 time zone \
|
||||
annotation, but found {:?} instead",
|
||||
escape::Byte(input[0]),
|
||||
));
|
||||
}
|
||||
Ok(Parsed { value: (), input: &input[1..] })
|
||||
Ok(Parsed { value: (), input: tail })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -665,24 +623,22 @@ mod tests {
|
|||
fn ok_empty() {
|
||||
let p = |input| Parser::new().parse(input).unwrap();
|
||||
|
||||
insta::assert_debug_snapshot!(p(b""), @r###"
|
||||
insta::assert_debug_snapshot!(p(b""), @r#"
|
||||
Parsed {
|
||||
value: ParsedAnnotations {
|
||||
input: "",
|
||||
time_zone: None,
|
||||
},
|
||||
input: "",
|
||||
}
|
||||
"###);
|
||||
insta::assert_debug_snapshot!(p(b"blah"), @r###"
|
||||
"#);
|
||||
insta::assert_debug_snapshot!(p(b"blah"), @r#"
|
||||
Parsed {
|
||||
value: ParsedAnnotations {
|
||||
input: "",
|
||||
time_zone: None,
|
||||
},
|
||||
input: "blah",
|
||||
}
|
||||
"###);
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -691,39 +647,36 @@ mod tests {
|
|||
|
||||
insta::assert_debug_snapshot!(
|
||||
p(b"[u-ca=chinese]"),
|
||||
@r###"
|
||||
@r#"
|
||||
Parsed {
|
||||
value: ParsedAnnotations {
|
||||
input: "[u-ca=chinese]",
|
||||
time_zone: None,
|
||||
},
|
||||
input: "",
|
||||
}
|
||||
"###,
|
||||
"#,
|
||||
);
|
||||
insta::assert_debug_snapshot!(
|
||||
p(b"[u-ca=chinese-japanese]"),
|
||||
@r###"
|
||||
@r#"
|
||||
Parsed {
|
||||
value: ParsedAnnotations {
|
||||
input: "[u-ca=chinese-japanese]",
|
||||
time_zone: None,
|
||||
},
|
||||
input: "",
|
||||
}
|
||||
"###,
|
||||
"#,
|
||||
);
|
||||
insta::assert_debug_snapshot!(
|
||||
p(b"[u-ca=chinese-japanese-russian]"),
|
||||
@r###"
|
||||
@r#"
|
||||
Parsed {
|
||||
value: ParsedAnnotations {
|
||||
input: "[u-ca=chinese-japanese-russian]",
|
||||
time_zone: None,
|
||||
},
|
||||
input: "",
|
||||
}
|
||||
"###,
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -731,10 +684,9 @@ mod tests {
|
|||
fn ok_iana() {
|
||||
let p = |input| Parser::new().parse(input).unwrap();
|
||||
|
||||
insta::assert_debug_snapshot!(p(b"[America/New_York]"), @r###"
|
||||
insta::assert_debug_snapshot!(p(b"[America/New_York]"), @r#"
|
||||
Parsed {
|
||||
value: ParsedAnnotations {
|
||||
input: "[America/New_York]",
|
||||
time_zone: Some(
|
||||
Named {
|
||||
critical: false,
|
||||
|
|
@ -744,11 +696,10 @@ mod tests {
|
|||
},
|
||||
input: "",
|
||||
}
|
||||
"###);
|
||||
insta::assert_debug_snapshot!(p(b"[!America/New_York]"), @r###"
|
||||
"#);
|
||||
insta::assert_debug_snapshot!(p(b"[!America/New_York]"), @r#"
|
||||
Parsed {
|
||||
value: ParsedAnnotations {
|
||||
input: "[!America/New_York]",
|
||||
time_zone: Some(
|
||||
Named {
|
||||
critical: true,
|
||||
|
|
@ -758,11 +709,10 @@ mod tests {
|
|||
},
|
||||
input: "",
|
||||
}
|
||||
"###);
|
||||
insta::assert_debug_snapshot!(p(b"[UTC]"), @r###"
|
||||
"#);
|
||||
insta::assert_debug_snapshot!(p(b"[UTC]"), @r#"
|
||||
Parsed {
|
||||
value: ParsedAnnotations {
|
||||
input: "[UTC]",
|
||||
time_zone: Some(
|
||||
Named {
|
||||
critical: false,
|
||||
|
|
@ -772,11 +722,10 @@ mod tests {
|
|||
},
|
||||
input: "",
|
||||
}
|
||||
"###);
|
||||
insta::assert_debug_snapshot!(p(b"[.._foo_../.0+-]"), @r###"
|
||||
"#);
|
||||
insta::assert_debug_snapshot!(p(b"[.._foo_../.0+-]"), @r#"
|
||||
Parsed {
|
||||
value: ParsedAnnotations {
|
||||
input: "[.._foo_../.0+-]",
|
||||
time_zone: Some(
|
||||
Named {
|
||||
critical: false,
|
||||
|
|
@ -786,17 +735,16 @@ mod tests {
|
|||
},
|
||||
input: "",
|
||||
}
|
||||
"###);
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ok_offset() {
|
||||
let p = |input| Parser::new().parse(input).unwrap();
|
||||
|
||||
insta::assert_debug_snapshot!(p(b"[-00]"), @r###"
|
||||
insta::assert_debug_snapshot!(p(b"[-00]"), @r#"
|
||||
Parsed {
|
||||
value: ParsedAnnotations {
|
||||
input: "[-00]",
|
||||
time_zone: Some(
|
||||
Offset {
|
||||
critical: false,
|
||||
|
|
@ -810,11 +758,10 @@ mod tests {
|
|||
},
|
||||
input: "",
|
||||
}
|
||||
"###);
|
||||
insta::assert_debug_snapshot!(p(b"[+00]"), @r###"
|
||||
"#);
|
||||
insta::assert_debug_snapshot!(p(b"[+00]"), @r#"
|
||||
Parsed {
|
||||
value: ParsedAnnotations {
|
||||
input: "[+00]",
|
||||
time_zone: Some(
|
||||
Offset {
|
||||
critical: false,
|
||||
|
|
@ -828,11 +775,10 @@ mod tests {
|
|||
},
|
||||
input: "",
|
||||
}
|
||||
"###);
|
||||
insta::assert_debug_snapshot!(p(b"[-05]"), @r###"
|
||||
"#);
|
||||
insta::assert_debug_snapshot!(p(b"[-05]"), @r#"
|
||||
Parsed {
|
||||
value: ParsedAnnotations {
|
||||
input: "[-05]",
|
||||
time_zone: Some(
|
||||
Offset {
|
||||
critical: false,
|
||||
|
|
@ -846,11 +792,10 @@ mod tests {
|
|||
},
|
||||
input: "",
|
||||
}
|
||||
"###);
|
||||
insta::assert_debug_snapshot!(p(b"[!+05:12]"), @r###"
|
||||
"#);
|
||||
insta::assert_debug_snapshot!(p(b"[!+05:12]"), @r#"
|
||||
Parsed {
|
||||
value: ParsedAnnotations {
|
||||
input: "[!+05:12]",
|
||||
time_zone: Some(
|
||||
Offset {
|
||||
critical: true,
|
||||
|
|
@ -864,7 +809,7 @@ mod tests {
|
|||
},
|
||||
input: "",
|
||||
}
|
||||
"###);
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -873,10 +818,9 @@ mod tests {
|
|||
|
||||
insta::assert_debug_snapshot!(
|
||||
p(b"[America/New_York][u-ca=chinese-japanese-russian]"),
|
||||
@r###"
|
||||
@r#"
|
||||
Parsed {
|
||||
value: ParsedAnnotations {
|
||||
input: "[America/New_York][u-ca=chinese-japanese-russian]",
|
||||
time_zone: Some(
|
||||
Named {
|
||||
critical: false,
|
||||
|
|
@ -886,7 +830,7 @@ mod tests {
|
|||
},
|
||||
input: "",
|
||||
}
|
||||
"###,
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -894,11 +838,11 @@ mod tests {
|
|||
fn err_iana() {
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[0/Foo]").unwrap_err(),
|
||||
@r###"expected ASCII alphabetic byte (or underscore or period) at the start of an RFC 9557 annotation or time zone component name, but found "0" instead"###,
|
||||
@"expected ASCII alphabetic byte (or underscore or period) at the start of an RFC 9557 annotation or time zone component name, but found `0` instead",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[Foo/0Bar]").unwrap_err(),
|
||||
@r###"expected ASCII alphabetic byte (or underscore or period) at the start of an RFC 9557 annotation or time zone component name, but found "0" instead"###,
|
||||
@"expected ASCII alphabetic byte (or underscore or period) at the start of an RFC 9557 annotation or time zone component name, but found `0` instead",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -906,23 +850,23 @@ mod tests {
|
|||
fn err_offset() {
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[+").unwrap_err(),
|
||||
@r###"failed to parse hours in UTC numeric offset "+": expected two digit hour after sign, but found end of input"###,
|
||||
@"failed to parse hours in UTC numeric offset: expected two digit hour after sign, but found end of input",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[+26]").unwrap_err(),
|
||||
@r###"failed to parse hours in UTC numeric offset "+26]": offset hours are not valid: parameter 'hours' with value 26 is not in the required range of 0..=25"###,
|
||||
@"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",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[-26]").unwrap_err(),
|
||||
@r###"failed to parse hours in UTC numeric offset "-26]": offset hours are not valid: parameter 'hours' with value 26 is not in the required range of 0..=25"###,
|
||||
@"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",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[+05:12:34]").unwrap_err(),
|
||||
@r###"subminute precision for UTC numeric offset "+05:12:34]" is not enabled in this context (must provide only integral minutes)"###,
|
||||
@"subminute precision for UTC numeric offset is not enabled in this context (must provide only integral minutes)",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[+05:12:34.123456789]").unwrap_err(),
|
||||
@r###"subminute precision for UTC numeric offset "+05:12:34.123456789]" is not enabled in this context (must provide only integral minutes)"###,
|
||||
@"subminute precision for UTC numeric offset is not enabled in this context (must provide only integral minutes)",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -930,7 +874,7 @@ mod tests {
|
|||
fn err_critical_unsupported() {
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[!u-ca=chinese]").unwrap_err(),
|
||||
@r###"found unsupported RFC 9557 annotation with key "u-ca" with the critical flag ('!') set"###,
|
||||
@"found unsupported RFC 9557 annotation with the critical flag (`!`) set",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -942,7 +886,7 @@ mod tests {
|
|||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[&").unwrap_err(),
|
||||
@r###"expected ASCII alphabetic byte (or underscore or period) at the start of an RFC 9557 annotation or time zone component name, but found "&" instead"###,
|
||||
@"expected ASCII alphabetic byte (or underscore or period) at the start of an RFC 9557 annotation or time zone component name, but found `&` instead",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[Foo][").unwrap_err(),
|
||||
|
|
@ -950,7 +894,7 @@ mod tests {
|
|||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[Foo][&").unwrap_err(),
|
||||
@r###"expected lowercase alphabetic byte (or underscore) at the start of an RFC 9557 annotation key, but found "&" instead"###,
|
||||
@"expected lowercase alphabetic byte (or underscore) at the start of an RFC 9557 annotation key, but found `&` instead",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -958,27 +902,27 @@ mod tests {
|
|||
fn err_separator() {
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[abc").unwrap_err(),
|
||||
@"expected an ']' after parsing an RFC 9557 time zone annotation, but found end of input instead",
|
||||
@"expected an `]` after parsing an RFC 9557 time zone annotation, but found end of input instead",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[_abc").unwrap_err(),
|
||||
@"expected an ']' after parsing an RFC 9557 time zone annotation, but found end of input instead",
|
||||
@"expected an `]` after parsing an RFC 9557 time zone annotation, but found end of input instead",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[abc^").unwrap_err(),
|
||||
@r###"expected an ']' after parsing an RFC 9557 time zone annotation, but found "^" instead"###,
|
||||
@"expected an `]` after parsing an RFC 9557 time zone annotation, but found `^` instead",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[Foo][abc").unwrap_err(),
|
||||
@"expected an '=' after parsing an RFC 9557 annotation key, but found end of input instead",
|
||||
@"expected an `=` after parsing an RFC 9557 annotation key, but found end of input instead",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[Foo][_abc").unwrap_err(),
|
||||
@"expected an '=' after parsing an RFC 9557 annotation key, but found end of input instead",
|
||||
@"expected an `=` after parsing an RFC 9557 annotation key, but found end of input instead",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[Foo][abc^").unwrap_err(),
|
||||
@r###"expected an '=' after parsing an RFC 9557 annotation key, but found "^" instead"###,
|
||||
@"expected an `=` after parsing an RFC 9557 annotation key, but found `^` instead",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -994,11 +938,11 @@ mod tests {
|
|||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[abc=^").unwrap_err(),
|
||||
@r###"expected alphanumeric ASCII byte at the start of an RFC 9557 annotation value, but found "^" instead"###,
|
||||
@"expected alphanumeric ASCII byte at the start of an RFC 9557 annotation value, but found `^` instead",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[abc=]").unwrap_err(),
|
||||
@r###"expected alphanumeric ASCII byte at the start of an RFC 9557 annotation value, but found "]" instead"###,
|
||||
@"expected alphanumeric ASCII byte at the start of an RFC 9557 annotation value, but found `]` instead",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1006,11 +950,11 @@ mod tests {
|
|||
fn err_close() {
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[abc=123").unwrap_err(),
|
||||
@"expected an ']' after parsing an RFC 9557 annotation key and value, but found end of input instead",
|
||||
@"expected an `]` after parsing an RFC 9557 annotation key and value, but found end of input instead",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[abc=123*").unwrap_err(),
|
||||
@r###"expected an ']' after parsing an RFC 9557 annotation key and value, but found "*" instead"###,
|
||||
@"expected an `]` after parsing an RFC 9557 annotation key and value, but found `*` instead",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1046,7 +990,7 @@ mod tests {
|
|||
let p = |input| Parser::new().parse(input).unwrap_err();
|
||||
insta::assert_snapshot!(
|
||||
p(b"[america/new_york][america/new_york]"),
|
||||
@"expected an '=' after parsing an RFC 9557 annotation key, but found / instead (time zone annotations must come first)",
|
||||
@"expected an `=` after parsing an RFC 9557 annotation key, but found `/` instead (time zone annotations must come first)",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1778,28 +1778,24 @@ pub mod unsigned_duration {
|
|||
fn parse_iso_or_friendly(
|
||||
bytes: &[u8],
|
||||
) -> Result<core::time::Duration, crate::Error> {
|
||||
if bytes.is_empty() {
|
||||
return Err(crate::error::err!(
|
||||
"an empty string is not a valid `std::time::Duration`, \
|
||||
expected either a ISO 8601 or Jiff's 'friendly' \
|
||||
format",
|
||||
let Some((&byte, tail)) = bytes.split_first() else {
|
||||
return Err(crate::Error::from(
|
||||
crate::error::fmt::Error::HybridDurationEmpty,
|
||||
));
|
||||
}
|
||||
let mut first = bytes[0];
|
||||
};
|
||||
let mut first = byte;
|
||||
// N.B. Unsigned durations don't support negative durations (of
|
||||
// course), but we still check for it here so that we can defer to
|
||||
// the dedicated parsers. They will provide their own error messages.
|
||||
if first == b'+' || first == b'-' {
|
||||
if bytes.len() == 1 {
|
||||
return Err(crate::error::err!(
|
||||
"found nothing after sign `{sign}`, \
|
||||
which is not a valid `std::time::Duration`, \
|
||||
expected either a ISO 8601 or Jiff's 'friendly' \
|
||||
format",
|
||||
sign = crate::util::escape::Byte(first),
|
||||
let Some(&byte) = tail.first() else {
|
||||
return Err(crate::Error::from(
|
||||
crate::error::fmt::Error::HybridDurationPrefix {
|
||||
sign: first,
|
||||
},
|
||||
));
|
||||
}
|
||||
first = bytes[1];
|
||||
};
|
||||
first = byte;
|
||||
}
|
||||
let dur = if first == b'P' || first == b'p' {
|
||||
crate::fmt::temporal::DEFAULT_SPAN_PARSER
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
use crate::{
|
||||
error::{err, ErrorContext},
|
||||
error::{
|
||||
fmt::strtime::{Error as E, FormatError as FE},
|
||||
ErrorContext,
|
||||
},
|
||||
fmt::{
|
||||
strtime::{
|
||||
month_name_abbrev, month_name_full, weekday_name_abbrev,
|
||||
|
|
@ -9,8 +12,8 @@ use crate::{
|
|||
util::{DecimalFormatter, FractionalFormatter},
|
||||
Write, WriteExt,
|
||||
},
|
||||
shared::util::utf8,
|
||||
tz::Offset,
|
||||
util::{escape, utf8},
|
||||
Error,
|
||||
};
|
||||
|
||||
|
|
@ -39,10 +42,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
self.wtr.write_str("%")?;
|
||||
break;
|
||||
}
|
||||
return Err(err!(
|
||||
"invalid format string, expected byte after '%', \
|
||||
but found end of format string",
|
||||
));
|
||||
return Err(E::UnexpectedEndAfterPercent.into());
|
||||
}
|
||||
let orig = self.fmt;
|
||||
if let Err(err) = self.format_one() {
|
||||
|
|
@ -61,100 +61,92 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
}
|
||||
|
||||
fn format_one(&mut self) -> Result<(), Error> {
|
||||
let failc =
|
||||
|directive, colons| E::DirectiveFailure { directive, colons };
|
||||
let fail = |directive| failc(directive, 0);
|
||||
|
||||
// Parse extensions like padding/case options and padding width.
|
||||
let ext = self.parse_extension()?;
|
||||
match self.f() {
|
||||
b'%' => self.wtr.write_str("%").context("%% failed")?,
|
||||
b'A' => self.fmt_weekday_full(&ext).context("%A failed")?,
|
||||
b'a' => self.fmt_weekday_abbrev(&ext).context("%a failed")?,
|
||||
b'B' => self.fmt_month_full(&ext).context("%B failed")?,
|
||||
b'b' => self.fmt_month_abbrev(&ext).context("%b failed")?,
|
||||
b'C' => self.fmt_century(&ext).context("%C failed")?,
|
||||
b'c' => self.fmt_datetime(&ext).context("%c failed")?,
|
||||
b'D' => self.fmt_american_date(&ext).context("%D failed")?,
|
||||
b'd' => self.fmt_day_zero(&ext).context("%d failed")?,
|
||||
b'e' => self.fmt_day_space(&ext).context("%e failed")?,
|
||||
b'F' => self.fmt_iso_date(&ext).context("%F failed")?,
|
||||
b'f' => self.fmt_fractional(&ext).context("%f failed")?,
|
||||
b'G' => self.fmt_iso_week_year(&ext).context("%G failed")?,
|
||||
b'g' => self.fmt_iso_week_year2(&ext).context("%g failed")?,
|
||||
b'H' => self.fmt_hour24_zero(&ext).context("%H failed")?,
|
||||
b'h' => self.fmt_month_abbrev(&ext).context("%b failed")?,
|
||||
b'I' => self.fmt_hour12_zero(&ext).context("%H failed")?,
|
||||
b'j' => self.fmt_day_of_year(&ext).context("%j failed")?,
|
||||
b'k' => self.fmt_hour24_space(&ext).context("%k failed")?,
|
||||
b'l' => self.fmt_hour12_space(&ext).context("%l failed")?,
|
||||
b'M' => self.fmt_minute(&ext).context("%M failed")?,
|
||||
b'm' => self.fmt_month(&ext).context("%m failed")?,
|
||||
b'N' => self.fmt_nanoseconds(&ext).context("%N failed")?,
|
||||
b'n' => self.fmt_literal("\n").context("%n failed")?,
|
||||
b'P' => self.fmt_ampm_lower(&ext).context("%P failed")?,
|
||||
b'p' => self.fmt_ampm_upper(&ext).context("%p failed")?,
|
||||
b'%' => self.wtr.write_str("%").context(fail(b'%')),
|
||||
b'A' => self.fmt_weekday_full(&ext).context(fail(b'A')),
|
||||
b'a' => self.fmt_weekday_abbrev(&ext).context(fail(b'a')),
|
||||
b'B' => self.fmt_month_full(&ext).context(fail(b'B')),
|
||||
b'b' => self.fmt_month_abbrev(&ext).context(fail(b'b')),
|
||||
b'C' => self.fmt_century(&ext).context(fail(b'C')),
|
||||
b'c' => self.fmt_datetime(&ext).context(fail(b'c')),
|
||||
b'D' => self.fmt_american_date(&ext).context(fail(b'D')),
|
||||
b'd' => self.fmt_day_zero(&ext).context(fail(b'd')),
|
||||
b'e' => self.fmt_day_space(&ext).context(fail(b'e')),
|
||||
b'F' => self.fmt_iso_date(&ext).context(fail(b'F')),
|
||||
b'f' => self.fmt_fractional(&ext).context(fail(b'f')),
|
||||
b'G' => self.fmt_iso_week_year(&ext).context(fail(b'G')),
|
||||
b'g' => self.fmt_iso_week_year2(&ext).context(fail(b'g')),
|
||||
b'H' => self.fmt_hour24_zero(&ext).context(fail(b'H')),
|
||||
b'h' => self.fmt_month_abbrev(&ext).context(fail(b'b')),
|
||||
b'I' => self.fmt_hour12_zero(&ext).context(fail(b'H')),
|
||||
b'j' => self.fmt_day_of_year(&ext).context(fail(b'j')),
|
||||
b'k' => self.fmt_hour24_space(&ext).context(fail(b'k')),
|
||||
b'l' => self.fmt_hour12_space(&ext).context(fail(b'l')),
|
||||
b'M' => self.fmt_minute(&ext).context(fail(b'M')),
|
||||
b'm' => self.fmt_month(&ext).context(fail(b'm')),
|
||||
b'N' => self.fmt_nanoseconds(&ext).context(fail(b'N')),
|
||||
b'n' => self.fmt_literal("\n").context(fail(b'n')),
|
||||
b'P' => self.fmt_ampm_lower(&ext).context(fail(b'P')),
|
||||
b'p' => self.fmt_ampm_upper(&ext).context(fail(b'p')),
|
||||
b'Q' => match ext.colons {
|
||||
0 => self.fmt_iana_nocolon().context("%Q failed")?,
|
||||
1 => self.fmt_iana_colon().context("%:Q failed")?,
|
||||
_ => {
|
||||
return Err(err!(
|
||||
"invalid number of `:` in `%Q` directive"
|
||||
))
|
||||
}
|
||||
0 => self.fmt_iana_nocolon().context(fail(b'Q')),
|
||||
1 => self.fmt_iana_colon().context(failc(b'Q', 1)),
|
||||
_ => return Err(E::ColonCount { directive: b'Q' }.into()),
|
||||
},
|
||||
b'q' => self.fmt_quarter(&ext).context("%q failed")?,
|
||||
b'R' => self.fmt_clock_nosecs(&ext).context("%R failed")?,
|
||||
b'r' => self.fmt_12hour_time(&ext).context("%r failed")?,
|
||||
b'S' => self.fmt_second(&ext).context("%S failed")?,
|
||||
b's' => self.fmt_timestamp(&ext).context("%s failed")?,
|
||||
b'T' => self.fmt_clock_secs(&ext).context("%T failed")?,
|
||||
b't' => self.fmt_literal("\t").context("%t failed")?,
|
||||
b'U' => self.fmt_week_sun(&ext).context("%U failed")?,
|
||||
b'u' => self.fmt_weekday_mon(&ext).context("%u failed")?,
|
||||
b'V' => self.fmt_week_iso(&ext).context("%V failed")?,
|
||||
b'W' => self.fmt_week_mon(&ext).context("%W failed")?,
|
||||
b'w' => self.fmt_weekday_sun(&ext).context("%w failed")?,
|
||||
b'X' => self.fmt_time(&ext).context("%X failed")?,
|
||||
b'x' => self.fmt_date(&ext).context("%x failed")?,
|
||||
b'Y' => self.fmt_year(&ext).context("%Y failed")?,
|
||||
b'y' => self.fmt_year2(&ext).context("%y failed")?,
|
||||
b'Z' => self.fmt_tzabbrev(&ext).context("%Z failed")?,
|
||||
b'q' => self.fmt_quarter(&ext).context(fail(b'q')),
|
||||
b'R' => self.fmt_clock_nosecs(&ext).context(fail(b'R')),
|
||||
b'r' => self.fmt_12hour_time(&ext).context(fail(b'r')),
|
||||
b'S' => self.fmt_second(&ext).context(fail(b'S')),
|
||||
b's' => self.fmt_timestamp(&ext).context(fail(b's')),
|
||||
b'T' => self.fmt_clock_secs(&ext).context(fail(b'T')),
|
||||
b't' => self.fmt_literal("\t").context(fail(b't')),
|
||||
b'U' => self.fmt_week_sun(&ext).context(fail(b'U')),
|
||||
b'u' => self.fmt_weekday_mon(&ext).context(fail(b'u')),
|
||||
b'V' => self.fmt_week_iso(&ext).context(fail(b'V')),
|
||||
b'W' => self.fmt_week_mon(&ext).context(fail(b'W')),
|
||||
b'w' => self.fmt_weekday_sun(&ext).context(fail(b'w')),
|
||||
b'X' => self.fmt_time(&ext).context(fail(b'X')),
|
||||
b'x' => self.fmt_date(&ext).context(fail(b'x')),
|
||||
b'Y' => self.fmt_year(&ext).context(fail(b'Y')),
|
||||
b'y' => self.fmt_year2(&ext).context(fail(b'y')),
|
||||
b'Z' => self.fmt_tzabbrev(&ext).context(fail(b'Z')),
|
||||
b'z' => match ext.colons {
|
||||
0 => self.fmt_offset_nocolon().context("%z failed")?,
|
||||
1 => self.fmt_offset_colon().context("%:z failed")?,
|
||||
2 => self.fmt_offset_colon2().context("%::z failed")?,
|
||||
3 => self.fmt_offset_colon3().context("%:::z failed")?,
|
||||
_ => {
|
||||
return Err(err!(
|
||||
"invalid number of `:` in `%z` directive"
|
||||
))
|
||||
}
|
||||
0 => self.fmt_offset_nocolon().context(fail(b'z')),
|
||||
1 => self.fmt_offset_colon().context(failc(b'z', 1)),
|
||||
2 => self.fmt_offset_colon2().context(failc(b'z', 2)),
|
||||
3 => self.fmt_offset_colon3().context(failc(b'z', 3)),
|
||||
_ => return Err(E::ColonCount { directive: b'z' }.into()),
|
||||
},
|
||||
b'.' => {
|
||||
if !self.bump_fmt() {
|
||||
return Err(err!(
|
||||
"invalid format string, expected directive after '%.'",
|
||||
));
|
||||
return Err(E::UnexpectedEndAfterDot.into());
|
||||
}
|
||||
// Parse precision settings after the `.`, effectively
|
||||
// overriding any digits that came before it.
|
||||
let ext = Extension { width: self.parse_width()?, ..ext };
|
||||
match self.f() {
|
||||
b'f' => {
|
||||
self.fmt_dot_fractional(&ext).context("%.f failed")?
|
||||
}
|
||||
b'f' => self
|
||||
.fmt_dot_fractional(&ext)
|
||||
.context(E::DirectiveFailureDot { directive: b'f' }),
|
||||
unk => {
|
||||
return Err(err!(
|
||||
"found unrecognized directive %{unk} following %.",
|
||||
unk = escape::Byte(unk),
|
||||
return Err(Error::from(
|
||||
E::UnknownDirectiveAfterDot { directive: unk },
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
unk => {
|
||||
return Err(err!(
|
||||
"found unrecognized specifier directive %{unk}",
|
||||
unk = escape::Byte(unk),
|
||||
));
|
||||
return Err(Error::from(E::UnknownDirective {
|
||||
directive: unk,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}?;
|
||||
self.bump_fmt();
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -200,21 +192,17 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
/// some remaining bytes to parse.
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn utf8_decode_and_bump(&mut self) -> Result<char, Error> {
|
||||
fn utf8_decode_and_bump(&mut self) -> Result<char, FE> {
|
||||
match utf8::decode(self.fmt).expect("non-empty fmt") {
|
||||
Ok(ch) => {
|
||||
self.fmt = &self.fmt[ch.len_utf8()..];
|
||||
return Ok(ch);
|
||||
}
|
||||
Err(errant_bytes) if self.config.lenient => {
|
||||
self.fmt = &self.fmt[errant_bytes.len()..];
|
||||
Err(err) if self.config.lenient => {
|
||||
self.fmt = &self.fmt[err.len()..];
|
||||
return Ok(char::REPLACEMENT_CHARACTER);
|
||||
}
|
||||
Err(errant_bytes) => Err(err!(
|
||||
"found invalid UTF-8 byte {errant_bytes:?} in format \
|
||||
string (format strings must be valid UTF-8)",
|
||||
errant_bytes = escape::Bytes(errant_bytes),
|
||||
)),
|
||||
Err(_) => Err(FE::InvalidUtf8),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -270,11 +258,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
|
||||
/// %P
|
||||
fn fmt_ampm_lower(&mut self, ext: &Extension) -> Result<(), Error> {
|
||||
let hour = self
|
||||
.tm
|
||||
.hour_ranged()
|
||||
.ok_or_else(|| err!("requires time to format AM/PM"))?
|
||||
.get();
|
||||
let hour = self.tm.hour_ranged().ok_or(FE::RequiresTime)?.get();
|
||||
ext.write_str(
|
||||
Case::AsIs,
|
||||
if hour < 12 { "am" } else { "pm" },
|
||||
|
|
@ -284,11 +268,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
|
||||
/// %p
|
||||
fn fmt_ampm_upper(&mut self, ext: &Extension) -> Result<(), Error> {
|
||||
let hour = self
|
||||
.tm
|
||||
.hour_ranged()
|
||||
.ok_or_else(|| err!("requires time to format AM/PM"))?
|
||||
.get();
|
||||
let hour = self.tm.hour_ranged().ok_or(FE::RequiresTime)?.get();
|
||||
// Manually specialize this case to avoid hitting `write_str_cold`.
|
||||
let s = if matches!(ext.flag, Some(Flag::Swapcase)) {
|
||||
if hour < 12 {
|
||||
|
|
@ -343,7 +323,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
#[inline(never)]
|
||||
|| self.tm.to_date().ok().map(|d| d.day_ranged()),
|
||||
)
|
||||
.ok_or_else(|| err!("requires date to format day"))?
|
||||
.ok_or(FE::RequiresDate)?
|
||||
.get();
|
||||
ext.write_int(b'0', Some(2), day, self.wtr)
|
||||
}
|
||||
|
|
@ -357,18 +337,14 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
#[inline(never)]
|
||||
|| self.tm.to_date().ok().map(|d| d.day_ranged()),
|
||||
)
|
||||
.ok_or_else(|| err!("requires date to format day"))?
|
||||
.ok_or(FE::RequiresDate)?
|
||||
.get();
|
||||
ext.write_int(b' ', Some(2), day, self.wtr)
|
||||
}
|
||||
|
||||
/// %I
|
||||
fn fmt_hour12_zero(&mut self, ext: &Extension) -> Result<(), Error> {
|
||||
let mut hour = self
|
||||
.tm
|
||||
.hour_ranged()
|
||||
.ok_or_else(|| err!("requires time to format hour"))?
|
||||
.get();
|
||||
let mut hour = self.tm.hour_ranged().ok_or(FE::RequiresTime)?.get();
|
||||
if hour == 0 {
|
||||
hour = 12;
|
||||
} else if hour > 12 {
|
||||
|
|
@ -379,21 +355,13 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
|
||||
/// %H
|
||||
fn fmt_hour24_zero(&mut self, ext: &Extension) -> Result<(), Error> {
|
||||
let hour = self
|
||||
.tm
|
||||
.hour_ranged()
|
||||
.ok_or_else(|| err!("requires time to format hour"))?
|
||||
.get();
|
||||
let hour = self.tm.hour_ranged().ok_or(FE::RequiresTime)?.get();
|
||||
ext.write_int(b'0', Some(2), hour, self.wtr)
|
||||
}
|
||||
|
||||
/// %l
|
||||
fn fmt_hour12_space(&mut self, ext: &Extension) -> Result<(), Error> {
|
||||
let mut hour = self
|
||||
.tm
|
||||
.hour_ranged()
|
||||
.ok_or_else(|| err!("requires time to format hour"))?
|
||||
.get();
|
||||
let mut hour = self.tm.hour_ranged().ok_or(FE::RequiresTime)?.get();
|
||||
if hour == 0 {
|
||||
hour = 12;
|
||||
} else if hour > 12 {
|
||||
|
|
@ -404,11 +372,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
|
||||
/// %k
|
||||
fn fmt_hour24_space(&mut self, ext: &Extension) -> Result<(), Error> {
|
||||
let hour = self
|
||||
.tm
|
||||
.hour_ranged()
|
||||
.ok_or_else(|| err!("requires time to format hour"))?
|
||||
.get();
|
||||
let hour = self.tm.hour_ranged().ok_or(FE::RequiresTime)?.get();
|
||||
ext.write_int(b' ', Some(2), hour, self.wtr)
|
||||
}
|
||||
|
||||
|
|
@ -424,11 +388,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
|
||||
/// %M
|
||||
fn fmt_minute(&mut self, ext: &Extension) -> Result<(), Error> {
|
||||
let minute = self
|
||||
.tm
|
||||
.minute
|
||||
.ok_or_else(|| err!("requires time to format minute"))?
|
||||
.get();
|
||||
let minute = self.tm.minute.ok_or(FE::RequiresTime)?.get();
|
||||
ext.write_int(b'0', Some(2), minute, self.wtr)
|
||||
}
|
||||
|
||||
|
|
@ -441,7 +401,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
#[inline(never)]
|
||||
|| self.tm.to_date().ok().map(|d| d.month_ranged()),
|
||||
)
|
||||
.ok_or_else(|| err!("requires date to format month"))?
|
||||
.ok_or(FE::RequiresDate)?
|
||||
.get();
|
||||
ext.write_int(b'0', Some(2), month, self.wtr)
|
||||
}
|
||||
|
|
@ -455,7 +415,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
#[inline(never)]
|
||||
|| self.tm.to_date().ok().map(|d| d.month_ranged()),
|
||||
)
|
||||
.ok_or_else(|| err!("requires date to format month"))?;
|
||||
.ok_or(FE::RequiresDate)?;
|
||||
ext.write_str(Case::AsIs, month_name_full(month), self.wtr)
|
||||
}
|
||||
|
||||
|
|
@ -468,19 +428,14 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
#[inline(never)]
|
||||
|| self.tm.to_date().ok().map(|d| d.month_ranged()),
|
||||
)
|
||||
.ok_or_else(|| err!("requires date to format month"))?;
|
||||
.ok_or(FE::RequiresDate)?;
|
||||
ext.write_str(Case::AsIs, month_name_abbrev(month), self.wtr)
|
||||
}
|
||||
|
||||
/// %Q
|
||||
fn fmt_iana_nocolon(&mut self) -> Result<(), Error> {
|
||||
let Some(iana) = self.tm.iana_time_zone() else {
|
||||
let offset = self.tm.offset.ok_or_else(|| {
|
||||
err!(
|
||||
"requires IANA time zone identifier or time \
|
||||
zone offset, but none were present"
|
||||
)
|
||||
})?;
|
||||
let offset = self.tm.offset.ok_or(FE::RequiresTimeZoneOrOffset)?;
|
||||
return write_offset(offset, false, true, false, &mut self.wtr);
|
||||
};
|
||||
self.wtr.write_str(iana)?;
|
||||
|
|
@ -490,12 +445,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
/// %:Q
|
||||
fn fmt_iana_colon(&mut self) -> Result<(), Error> {
|
||||
let Some(iana) = self.tm.iana_time_zone() else {
|
||||
let offset = self.tm.offset.ok_or_else(|| {
|
||||
err!(
|
||||
"requires IANA time zone identifier or time \
|
||||
zone offset, but none were present"
|
||||
)
|
||||
})?;
|
||||
let offset = self.tm.offset.ok_or(FE::RequiresTimeZoneOrOffset)?;
|
||||
return write_offset(offset, true, true, false, &mut self.wtr);
|
||||
};
|
||||
self.wtr.write_str(iana)?;
|
||||
|
|
@ -504,62 +454,44 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
|
||||
/// %z
|
||||
fn fmt_offset_nocolon(&mut self) -> Result<(), Error> {
|
||||
let offset = self.tm.offset.ok_or_else(|| {
|
||||
err!("requires offset to format time zone offset")
|
||||
})?;
|
||||
let offset = self.tm.offset.ok_or(FE::RequiresOffset)?;
|
||||
write_offset(offset, false, true, false, self.wtr)
|
||||
}
|
||||
|
||||
/// %:z
|
||||
fn fmt_offset_colon(&mut self) -> Result<(), Error> {
|
||||
let offset = self.tm.offset.ok_or_else(|| {
|
||||
err!("requires offset to format time zone offset")
|
||||
})?;
|
||||
let offset = self.tm.offset.ok_or(FE::RequiresOffset)?;
|
||||
write_offset(offset, true, true, false, self.wtr)
|
||||
}
|
||||
|
||||
/// %::z
|
||||
fn fmt_offset_colon2(&mut self) -> Result<(), Error> {
|
||||
let offset = self.tm.offset.ok_or_else(|| {
|
||||
err!("requires offset to format time zone offset")
|
||||
})?;
|
||||
let offset = self.tm.offset.ok_or(FE::RequiresOffset)?;
|
||||
write_offset(offset, true, true, true, self.wtr)
|
||||
}
|
||||
|
||||
/// %:::z
|
||||
fn fmt_offset_colon3(&mut self) -> Result<(), Error> {
|
||||
let offset = self.tm.offset.ok_or_else(|| {
|
||||
err!("requires offset to format time zone offset")
|
||||
})?;
|
||||
let offset = self.tm.offset.ok_or(FE::RequiresOffset)?;
|
||||
write_offset(offset, true, false, false, self.wtr)
|
||||
}
|
||||
|
||||
/// %S
|
||||
fn fmt_second(&mut self, ext: &Extension) -> Result<(), Error> {
|
||||
let second = self
|
||||
.tm
|
||||
.second
|
||||
.ok_or_else(|| err!("requires time to format second"))?
|
||||
.get();
|
||||
let second = self.tm.second.ok_or(FE::RequiresTime)?.get();
|
||||
ext.write_int(b'0', Some(2), second, self.wtr)
|
||||
}
|
||||
|
||||
/// %s
|
||||
fn fmt_timestamp(&mut self, ext: &Extension) -> Result<(), Error> {
|
||||
let timestamp = self.tm.to_timestamp().map_err(|_| {
|
||||
err!(
|
||||
"requires instant (a date, time and offset) \
|
||||
to format Unix timestamp",
|
||||
)
|
||||
})?;
|
||||
let timestamp =
|
||||
self.tm.to_timestamp().map_err(|_| FE::RequiresInstant)?;
|
||||
ext.write_int(b' ', None, timestamp.as_second(), self.wtr)
|
||||
}
|
||||
|
||||
/// %f
|
||||
fn fmt_fractional(&mut self, ext: &Extension) -> Result<(), Error> {
|
||||
let subsec = self.tm.subsec.ok_or_else(|| {
|
||||
err!("requires time to format subsecond nanoseconds")
|
||||
})?;
|
||||
let subsec = self.tm.subsec.ok_or(FE::RequiresTime)?;
|
||||
let subsec = i32::from(subsec).unsigned_abs();
|
||||
// For %f, we always want to emit at least one digit. The only way we
|
||||
// wouldn't is if our fractional component is zero. One exception to
|
||||
|
|
@ -568,7 +500,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
// but this seems very odd. And an empty string cannot be parsed by
|
||||
// `%f`.
|
||||
if ext.width == Some(0) {
|
||||
return Err(err!("zero precision with %f is not allowed"));
|
||||
return Err(Error::from(FE::ZeroPrecisionFloat));
|
||||
}
|
||||
if subsec == 0 && ext.width.is_none() {
|
||||
self.wtr.write_str("0")?;
|
||||
|
|
@ -592,11 +524,9 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
|
||||
/// %N
|
||||
fn fmt_nanoseconds(&mut self, ext: &Extension) -> Result<(), Error> {
|
||||
let subsec = self.tm.subsec.ok_or_else(|| {
|
||||
err!("requires time to format subsecond nanoseconds")
|
||||
})?;
|
||||
let subsec = self.tm.subsec.ok_or(FE::RequiresTime)?;
|
||||
if ext.width == Some(0) {
|
||||
return Err(err!("zero precision with %N is not allowed"));
|
||||
return Err(Error::from(FE::ZeroPrecisionNano));
|
||||
}
|
||||
let subsec = i32::from(subsec).unsigned_abs();
|
||||
// Since `%N` is actually an alias for `%9f`, when the precision
|
||||
|
|
@ -611,14 +541,8 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
|
||||
/// %Z
|
||||
fn fmt_tzabbrev(&mut self, ext: &Extension) -> Result<(), Error> {
|
||||
let tz =
|
||||
self.tm.tz.as_ref().ok_or_else(|| {
|
||||
err!("requires time zone in broken down time")
|
||||
})?;
|
||||
let ts = self
|
||||
.tm
|
||||
.to_timestamp()
|
||||
.context("requires timestamp in broken down time")?;
|
||||
let tz = self.tm.tz.as_ref().ok_or(FE::RequiresTimeZone)?;
|
||||
let ts = self.tm.to_timestamp().map_err(|_| FE::RequiresInstant)?;
|
||||
let oinfo = tz.to_offset_info(ts);
|
||||
ext.write_str(Case::Upper, oinfo.abbreviation(), self.wtr)
|
||||
}
|
||||
|
|
@ -632,7 +556,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
#[inline(never)]
|
||||
|| self.tm.to_date().ok().map(|d| d.weekday()),
|
||||
)
|
||||
.ok_or_else(|| err!("requires date to format weekday"))?;
|
||||
.ok_or(FE::RequiresDate)?;
|
||||
ext.write_str(Case::AsIs, weekday_name_full(weekday), self.wtr)
|
||||
}
|
||||
|
||||
|
|
@ -645,7 +569,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
#[inline(never)]
|
||||
|| self.tm.to_date().ok().map(|d| d.weekday()),
|
||||
)
|
||||
.ok_or_else(|| err!("requires date to format weekday"))?;
|
||||
.ok_or(FE::RequiresDate)?;
|
||||
ext.write_str(Case::AsIs, weekday_name_abbrev(weekday), self.wtr)
|
||||
}
|
||||
|
||||
|
|
@ -658,7 +582,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
#[inline(never)]
|
||||
|| self.tm.to_date().ok().map(|d| d.weekday()),
|
||||
)
|
||||
.ok_or_else(|| err!("requires date to format weekday number"))?;
|
||||
.ok_or(FE::RequiresDate)?;
|
||||
ext.write_int(b' ', None, weekday.to_monday_one_offset(), self.wtr)
|
||||
}
|
||||
|
||||
|
|
@ -671,7 +595,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
#[inline(never)]
|
||||
|| self.tm.to_date().ok().map(|d| d.weekday()),
|
||||
)
|
||||
.ok_or_else(|| err!("requires date to format weekday number"))?;
|
||||
.ok_or(FE::RequiresDate)?;
|
||||
ext.write_int(b' ', None, weekday.to_sunday_zero_offset(), self.wtr)
|
||||
}
|
||||
|
||||
|
|
@ -689,9 +613,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
#[inline(never)]
|
||||
|| self.tm.to_date().ok().map(|d| d.day_of_year()),
|
||||
)
|
||||
.ok_or_else(|| {
|
||||
err!("requires date to format Sunday-based week number")
|
||||
})?;
|
||||
.ok_or(FE::RequiresDate)?;
|
||||
let weekday = self
|
||||
.tm
|
||||
.weekday
|
||||
|
|
@ -699,9 +621,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
#[inline(never)]
|
||||
|| self.tm.to_date().ok().map(|d| d.weekday()),
|
||||
)
|
||||
.ok_or_else(|| {
|
||||
err!("requires date to format Sunday-based week number")
|
||||
})?
|
||||
.ok_or(FE::RequiresDate)?
|
||||
.to_sunday_zero_offset();
|
||||
// Example: 2025-01-05 is the first Sunday in 2025, and thus the start
|
||||
// of week 1. This means that 2025-01-04 (Saturday) is in week 0.
|
||||
|
|
@ -726,9 +646,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
.map(|d| d.iso_week_date().week_ranged())
|
||||
},
|
||||
)
|
||||
.ok_or_else(|| {
|
||||
err!("requires date to format ISO 8601 week number")
|
||||
})?;
|
||||
.ok_or(FE::RequiresDate)?;
|
||||
ext.write_int(b'0', Some(2), weeknum, self.wtr)
|
||||
}
|
||||
|
||||
|
|
@ -746,9 +664,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
#[inline(never)]
|
||||
|| self.tm.to_date().ok().map(|d| d.day_of_year()),
|
||||
)
|
||||
.ok_or_else(|| {
|
||||
err!("requires date to format Monday-based week number")
|
||||
})?;
|
||||
.ok_or(FE::RequiresDate)?;
|
||||
let weekday = self
|
||||
.tm
|
||||
.weekday
|
||||
|
|
@ -756,9 +672,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
#[inline(never)]
|
||||
|| self.tm.to_date().ok().map(|d| d.weekday()),
|
||||
)
|
||||
.ok_or_else(|| {
|
||||
err!("requires date to format Monday-based week number")
|
||||
})?
|
||||
.ok_or(FE::RequiresDate)?
|
||||
.to_sunday_zero_offset();
|
||||
// Example: 2025-01-06 is the first Monday in 2025, and thus the start
|
||||
// of week 1. This means that 2025-01-05 (Sunday) is in week 0.
|
||||
|
|
@ -778,7 +692,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
#[inline(never)]
|
||||
|| self.tm.to_date().ok().map(|d| d.year_ranged()),
|
||||
)
|
||||
.ok_or_else(|| err!("requires date to format year"))?
|
||||
.ok_or(FE::RequiresDate)?
|
||||
.get();
|
||||
ext.write_int(b'0', Some(4), year, self.wtr)
|
||||
}
|
||||
|
|
@ -792,7 +706,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
#[inline(never)]
|
||||
|| self.tm.to_date().ok().map(|d| d.year_ranged()),
|
||||
)
|
||||
.ok_or_else(|| err!("requires date to format year (2-digit)"))?
|
||||
.ok_or(FE::RequiresDate)?
|
||||
.get();
|
||||
let year = year % 100;
|
||||
ext.write_int(b'0', Some(2), year, self.wtr)
|
||||
|
|
@ -807,7 +721,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
#[inline(never)]
|
||||
|| self.tm.to_date().ok().map(|d| d.year_ranged()),
|
||||
)
|
||||
.ok_or_else(|| err!("requires date to format century (2-digit)"))?
|
||||
.ok_or(FE::RequiresDate)?
|
||||
.get();
|
||||
let century = year / 100;
|
||||
ext.write_int(b' ', None, century, self.wtr)
|
||||
|
|
@ -827,9 +741,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
.map(|d| d.iso_week_date().year_ranged())
|
||||
},
|
||||
)
|
||||
.ok_or_else(|| {
|
||||
err!("requires date to format ISO 8601 week-based year")
|
||||
})?
|
||||
.ok_or(FE::RequiresDate)?
|
||||
.get();
|
||||
ext.write_int(b'0', Some(4), year, self.wtr)
|
||||
}
|
||||
|
|
@ -848,12 +760,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
.map(|d| d.iso_week_date().year_ranged())
|
||||
},
|
||||
)
|
||||
.ok_or_else(|| {
|
||||
err!(
|
||||
"requires date to format \
|
||||
ISO 8601 week-based year (2-digit)"
|
||||
)
|
||||
})?
|
||||
.ok_or(FE::RequiresDate)?
|
||||
.get();
|
||||
let year = year % 100;
|
||||
ext.write_int(b'0', Some(2), year, self.wtr)
|
||||
|
|
@ -868,7 +775,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
#[inline(never)]
|
||||
|| self.tm.to_date().ok().map(|d| d.month_ranged()),
|
||||
)
|
||||
.ok_or_else(|| err!("requires date to format quarter"))?
|
||||
.ok_or(FE::RequiresDate)?
|
||||
.get();
|
||||
let quarter = match month {
|
||||
1..=3 => 1,
|
||||
|
|
@ -890,7 +797,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
#[inline(never)]
|
||||
|| self.tm.to_date().ok().map(|d| d.day_of_year()),
|
||||
)
|
||||
.ok_or_else(|| err!("requires date to format day of year"))?;
|
||||
.ok_or(FE::RequiresDate)?;
|
||||
ext.write_int(b'0', Some(3), day, self.wtr)
|
||||
}
|
||||
|
||||
|
|
@ -1580,7 +1487,7 @@ mod tests {
|
|||
let dt = date(2025, 1, 20).at(13, 9, 0, 0);
|
||||
insta::assert_snapshot!(
|
||||
f("%s", dt),
|
||||
@"strftime formatting failed: %s failed: requires instant (a date, time and offset) to format Unix timestamp",
|
||||
@"strftime formatting failed: %s failed: requires instant (a timestamp or a date, time and offset)",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1593,7 +1500,7 @@ mod tests {
|
|||
);
|
||||
insta::assert_snapshot!(
|
||||
format(b"abc %F \xFFxyz", d).unwrap_err(),
|
||||
@r#"strftime formatting failed: found invalid UTF-8 byte "\xff" in format string (format strings must be valid UTF-8)"#,
|
||||
@"strftime formatting failed: invalid format string, it must be valid UTF-8",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ use jiff::{civil::time, fmt::strtime};
|
|||
let t = time(23, 59, 59, 0);
|
||||
assert_eq!(
|
||||
strtime::format("%Y", t).unwrap_err().to_string(),
|
||||
"strftime formatting failed: %Y failed: requires date to format year",
|
||||
"strftime formatting failed: %Y failed: requires date to format",
|
||||
);
|
||||
```
|
||||
|
||||
|
|
@ -275,7 +275,7 @@ The following things are currently unsupported:
|
|||
|
||||
use crate::{
|
||||
civil::{Date, DateTime, ISOWeekDate, Time, Weekday},
|
||||
error::{err, ErrorContext},
|
||||
error::{fmt::strtime::Error as E, ErrorContext},
|
||||
fmt::{
|
||||
strtime::{format::Formatter, parse::Parser},
|
||||
Write,
|
||||
|
|
@ -555,7 +555,7 @@ impl<C> Config<C> {
|
|||
/// assert_eq!(
|
||||
/// tm.to_string("%F %z").unwrap_err().to_string(),
|
||||
/// "strftime formatting failed: %z failed: \
|
||||
/// requires offset to format time zone offset",
|
||||
/// requires time zone offset",
|
||||
/// );
|
||||
///
|
||||
/// // Now enable lenient mode:
|
||||
|
|
@ -946,13 +946,9 @@ impl BrokenDownTime {
|
|||
fn parse_mono(fmt: &[u8], inp: &[u8]) -> Result<BrokenDownTime, Error> {
|
||||
let mut pieces = BrokenDownTime::default();
|
||||
let mut p = Parser { fmt, inp, tm: &mut pieces };
|
||||
p.parse().context("strptime parsing failed")?;
|
||||
p.parse().context(E::FailedStrptime)?;
|
||||
if !p.inp.is_empty() {
|
||||
return Err(err!(
|
||||
"strptime expects to consume the entire input, but \
|
||||
{remaining:?} remains unparsed",
|
||||
remaining = escape::Bytes(p.inp),
|
||||
));
|
||||
return Err(Error::from(E::unconsumed(p.inp)));
|
||||
}
|
||||
Ok(pieces)
|
||||
}
|
||||
|
|
@ -1055,7 +1051,7 @@ impl BrokenDownTime {
|
|||
let mkoffset = util::parse::offseter(inp);
|
||||
let mut pieces = BrokenDownTime::default();
|
||||
let mut p = Parser { fmt, inp, tm: &mut pieces };
|
||||
p.parse().context("strptime parsing failed")?;
|
||||
p.parse().context(E::FailedStrptime)?;
|
||||
let remainder = mkoffset(p.inp);
|
||||
Ok((pieces, remainder))
|
||||
}
|
||||
|
|
@ -1158,7 +1154,7 @@ impl BrokenDownTime {
|
|||
) -> Result<(), Error> {
|
||||
let fmt = format.as_ref();
|
||||
let mut formatter = Formatter { config, fmt, tm: self, wtr };
|
||||
formatter.format().context("strftime formatting failed")?;
|
||||
formatter.format().context(E::FailedStrftime)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -1337,10 +1333,11 @@ impl BrokenDownTime {
|
|||
/// )?.to_zoned();
|
||||
/// assert_eq!(
|
||||
/// result.unwrap_err().to_string(),
|
||||
/// "datetime 2024-07-14T21:14:00 could not resolve to a \
|
||||
/// timestamp since 'reject' conflict resolution was chosen, \
|
||||
/// and because datetime has offset -05, but the time zone \
|
||||
/// US/Eastern for the given datetime unambiguously has offset -04",
|
||||
/// "datetime could not resolve to a timestamp since `reject` \
|
||||
/// conflict resolution was chosen, and because \
|
||||
/// datetime has offset `-05`, \
|
||||
/// but the time zone `US/Eastern` for the given datetime \
|
||||
/// unambiguously has offset `-04`",
|
||||
/// );
|
||||
///
|
||||
/// # Ok::<(), Box<dyn std::error::Error>>(())
|
||||
|
|
@ -1435,26 +1432,18 @@ impl BrokenDownTime {
|
|||
if let Some(ts) = self.timestamp {
|
||||
return Ok(ts.to_zoned(TimeZone::unknown()));
|
||||
}
|
||||
Err(err!(
|
||||
"either offset (from %z) or IANA time zone identifier \
|
||||
(from %Q) is required for parsing zoned datetime",
|
||||
))
|
||||
Err(Error::from(E::ZonedOffsetOrTz))
|
||||
}
|
||||
(Some(offset), None) => {
|
||||
let ts = match self.timestamp {
|
||||
Some(ts) => ts,
|
||||
None => {
|
||||
let dt = self.to_datetime().context(
|
||||
"datetime required to parse zoned datetime",
|
||||
)?;
|
||||
let ts =
|
||||
offset.to_timestamp(dt).with_context(|| {
|
||||
err!(
|
||||
"parsed datetime {dt} and offset {offset}, \
|
||||
but combining them into a zoned datetime \
|
||||
is outside Jiff's supported timestamp range",
|
||||
)
|
||||
})?;
|
||||
let dt = self
|
||||
.to_datetime()
|
||||
.context(E::RequiredDateTimeForZoned)?;
|
||||
let ts = offset
|
||||
.to_timestamp(dt)
|
||||
.context(E::RangeTimestamp)?;
|
||||
ts
|
||||
}
|
||||
};
|
||||
|
|
@ -1465,9 +1454,9 @@ impl BrokenDownTime {
|
|||
match self.timestamp {
|
||||
Some(ts) => Ok(ts.to_zoned(tz)),
|
||||
None => {
|
||||
let dt = self.to_datetime().context(
|
||||
"datetime required to parse zoned datetime",
|
||||
)?;
|
||||
let dt = self
|
||||
.to_datetime()
|
||||
.context(E::RequiredDateTimeForZoned)?;
|
||||
Ok(tz.to_zoned(dt)?)
|
||||
}
|
||||
}
|
||||
|
|
@ -1478,19 +1467,17 @@ impl BrokenDownTime {
|
|||
Some(ts) => {
|
||||
let zdt = ts.to_zoned(tz);
|
||||
if zdt.offset() != offset {
|
||||
return Err(err!(
|
||||
"parsed time zone offset `{offset}`, but \
|
||||
offset from timestamp `{ts}` for time zone \
|
||||
`{iana}` is `{got}`",
|
||||
got = zdt.offset(),
|
||||
));
|
||||
return Err(Error::from(E::MismatchOffset {
|
||||
parsed: offset,
|
||||
got: zdt.offset(),
|
||||
}));
|
||||
}
|
||||
Ok(zdt)
|
||||
}
|
||||
None => {
|
||||
let dt = self.to_datetime().context(
|
||||
"datetime required to parse zoned datetime",
|
||||
)?;
|
||||
let dt = self
|
||||
.to_datetime()
|
||||
.context(E::RequiredDateTimeForZoned)?;
|
||||
let azdt =
|
||||
OffsetConflict::Reject.resolve(dt, offset, tz)?;
|
||||
// Guaranteed that if OffsetConflict::Reject doesn't
|
||||
|
|
@ -1587,19 +1574,10 @@ impl BrokenDownTime {
|
|||
#[cold]
|
||||
#[inline(never)]
|
||||
fn fallback(tm: &BrokenDownTime) -> Result<Timestamp, Error> {
|
||||
let dt = tm
|
||||
.to_datetime()
|
||||
.context("datetime required to parse timestamp")?;
|
||||
let offset = tm
|
||||
.to_offset()
|
||||
.context("offset required to parse timestamp")?;
|
||||
offset.to_timestamp(dt).with_context(|| {
|
||||
err!(
|
||||
"parsed datetime {dt} and offset {offset}, \
|
||||
but combining them into a timestamp is outside \
|
||||
Jiff's supported timestamp range",
|
||||
)
|
||||
})
|
||||
let dt =
|
||||
tm.to_datetime().context(E::RequiredDateTimeForTimestamp)?;
|
||||
let offset = tm.offset.ok_or(E::RequiredOffsetForTimestamp)?;
|
||||
offset.to_timestamp(dt).context(E::RangeTimestamp)
|
||||
}
|
||||
if let Some(timestamp) = self.timestamp() {
|
||||
return Ok(timestamp);
|
||||
|
|
@ -1607,16 +1585,6 @@ impl BrokenDownTime {
|
|||
fallback(self)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn to_offset(&self) -> Result<Offset, Error> {
|
||||
let Some(offset) = self.offset else {
|
||||
return Err(err!(
|
||||
"parsing format did not include time zone offset directive",
|
||||
));
|
||||
};
|
||||
Ok(offset)
|
||||
}
|
||||
|
||||
/// Extracts a civil datetime from this broken down time.
|
||||
///
|
||||
/// # Errors
|
||||
|
|
@ -1644,10 +1612,8 @@ impl BrokenDownTime {
|
|||
/// ```
|
||||
#[inline]
|
||||
pub fn to_datetime(&self) -> Result<DateTime, Error> {
|
||||
let date =
|
||||
self.to_date().context("date required to parse datetime")?;
|
||||
let time =
|
||||
self.to_time().context("time required to parse datetime")?;
|
||||
let date = self.to_date().context(E::RequiredDateForDateTime)?;
|
||||
let time = self.to_time().context(E::RequiredTimeForDateTime)?;
|
||||
Ok(DateTime::from_parts(date, time))
|
||||
}
|
||||
|
||||
|
|
@ -1698,7 +1664,7 @@ impl BrokenDownTime {
|
|||
if let Some(date) = tm.to_date_from_iso()? {
|
||||
return Ok(date);
|
||||
}
|
||||
return Err(err!("missing year, date cannot be created"));
|
||||
return Err(Error::from(E::RequiredYearForDate));
|
||||
};
|
||||
let mut date = tm.to_date_from_gregorian(year)?;
|
||||
if date.is_none() {
|
||||
|
|
@ -1714,19 +1680,14 @@ impl BrokenDownTime {
|
|||
date = tm.to_date_from_week_mon(year)?;
|
||||
}
|
||||
let Some(date) = date else {
|
||||
return Err(err!(
|
||||
"a month/day, day-of-year or week date must be \
|
||||
present to create a date, but none were found",
|
||||
));
|
||||
return Err(Error::from(E::RequiredSomeDayForDate));
|
||||
};
|
||||
if let Some(weekday) = tm.weekday {
|
||||
if weekday != date.weekday() {
|
||||
return Err(err!(
|
||||
"parsed weekday {weekday} does not match \
|
||||
weekday {got} from parsed date {date}",
|
||||
weekday = weekday_name_full(weekday),
|
||||
got = weekday_name_full(date.weekday()),
|
||||
));
|
||||
return Err(Error::from(E::MismatchWeekday {
|
||||
parsed: weekday,
|
||||
got: date.weekday(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
Ok(date)
|
||||
|
|
@ -1735,12 +1696,12 @@ impl BrokenDownTime {
|
|||
// The common case is a simple Gregorian date.
|
||||
// We put the rest behind a non-inlineable function
|
||||
// to avoid code bloat for very uncommon cases.
|
||||
let (Some(year), Some(month), Some(day)) =
|
||||
(self.year, self.month, self.day)
|
||||
let (Some(year), Some(month), Some(day), None) =
|
||||
(self.year, self.month, self.day, self.weekday)
|
||||
else {
|
||||
return to_date(self);
|
||||
};
|
||||
Ok(Date::new_ranged(year, month, day).context("invalid date")?)
|
||||
Ok(Date::new_ranged(year, month, day).context(E::InvalidDate)?)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
@ -1751,7 +1712,7 @@ impl BrokenDownTime {
|
|||
let (Some(month), Some(day)) = (self.month, self.day) else {
|
||||
return Ok(None);
|
||||
};
|
||||
Ok(Some(Date::new_ranged(year, month, day).context("invalid date")?))
|
||||
Ok(Some(Date::new_ranged(year, month, day).context(E::InvalidDate)?))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
@ -1767,7 +1728,7 @@ impl BrokenDownTime {
|
|||
.with()
|
||||
.day_of_year(doy.get())
|
||||
.build()
|
||||
.context("invalid date")?
|
||||
.context(E::InvalidDate)?
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
@ -1778,8 +1739,8 @@ impl BrokenDownTime {
|
|||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let wd = ISOWeekDate::new_ranged(y, w, d)
|
||||
.context("invalid ISO 8601 week date")?;
|
||||
let wd =
|
||||
ISOWeekDate::new_ranged(y, w, d).context(E::InvalidISOWeekDate)?;
|
||||
Ok(Some(wd.date()))
|
||||
}
|
||||
|
||||
|
|
@ -1794,28 +1755,20 @@ impl BrokenDownTime {
|
|||
let week = i16::from(week);
|
||||
let wday = i16::from(weekday.to_sunday_zero_offset());
|
||||
let first_of_year = Date::new_ranged(year, C(1).rinto(), C(1).rinto())
|
||||
.context("invalid date")?;
|
||||
.context(E::InvalidDate)?;
|
||||
let first_sunday = first_of_year
|
||||
.nth_weekday_of_month(1, Weekday::Sunday)
|
||||
.map(|d| d.day_of_year())
|
||||
.context("invalid date")?;
|
||||
.context(E::InvalidDate)?;
|
||||
let doy = if week == 0 {
|
||||
let days_before_first_sunday = 7 - wday;
|
||||
let doy = first_sunday
|
||||
.checked_sub(days_before_first_sunday)
|
||||
.ok_or_else(|| {
|
||||
err!(
|
||||
"weekday `{weekday:?}` is not valid for \
|
||||
Sunday based week number `{week}` \
|
||||
in year `{year}`",
|
||||
)
|
||||
})?;
|
||||
.ok_or(E::InvalidWeekdaySunday { got: weekday })?;
|
||||
if doy == 0 {
|
||||
return Err(err!(
|
||||
"weekday `{weekday:?}` is not valid for \
|
||||
Sunday based week number `{week}` \
|
||||
in year `{year}`",
|
||||
));
|
||||
return Err(Error::from(E::InvalidWeekdaySunday {
|
||||
got: weekday,
|
||||
}));
|
||||
}
|
||||
doy
|
||||
} else {
|
||||
|
|
@ -1827,7 +1780,7 @@ impl BrokenDownTime {
|
|||
.with()
|
||||
.day_of_year(doy)
|
||||
.build()
|
||||
.context("invalid date")?;
|
||||
.context(E::InvalidDate)?;
|
||||
Ok(Some(date))
|
||||
}
|
||||
|
||||
|
|
@ -1842,28 +1795,20 @@ impl BrokenDownTime {
|
|||
let week = i16::from(week);
|
||||
let wday = i16::from(weekday.to_monday_zero_offset());
|
||||
let first_of_year = Date::new_ranged(year, C(1).rinto(), C(1).rinto())
|
||||
.context("invalid date")?;
|
||||
.context(E::InvalidDate)?;
|
||||
let first_monday = first_of_year
|
||||
.nth_weekday_of_month(1, Weekday::Monday)
|
||||
.map(|d| d.day_of_year())
|
||||
.context("invalid date")?;
|
||||
.context(E::InvalidDate)?;
|
||||
let doy = if week == 0 {
|
||||
let days_before_first_monday = 7 - wday;
|
||||
let doy = first_monday
|
||||
.checked_sub(days_before_first_monday)
|
||||
.ok_or_else(|| {
|
||||
err!(
|
||||
"weekday `{weekday:?}` is not valid for \
|
||||
Monday based week number `{week}` \
|
||||
in year `{year}`",
|
||||
)
|
||||
})?;
|
||||
.ok_or(E::InvalidWeekdayMonday { got: weekday })?;
|
||||
if doy == 0 {
|
||||
return Err(err!(
|
||||
"weekday `{weekday:?}` is not valid for \
|
||||
Monday based week number `{week}` \
|
||||
in year `{year}`",
|
||||
));
|
||||
return Err(Error::from(E::InvalidWeekdayMonday {
|
||||
got: weekday,
|
||||
}));
|
||||
}
|
||||
doy
|
||||
} else {
|
||||
|
|
@ -1875,7 +1820,7 @@ impl BrokenDownTime {
|
|||
.with()
|
||||
.day_of_year(doy)
|
||||
.build()
|
||||
.context("invalid date")?;
|
||||
.context(E::InvalidDate)?;
|
||||
Ok(Some(date))
|
||||
}
|
||||
|
||||
|
|
@ -1957,52 +1902,28 @@ impl BrokenDownTime {
|
|||
pub fn to_time(&self) -> Result<Time, Error> {
|
||||
let Some(hour) = self.hour_ranged() else {
|
||||
if self.minute.is_some() {
|
||||
return Err(err!(
|
||||
"parsing format did not include hour directive, \
|
||||
but did include minute directive (cannot have \
|
||||
smaller time units with bigger time units missing)",
|
||||
));
|
||||
return Err(Error::from(E::MissingTimeHourForMinute));
|
||||
}
|
||||
if self.second.is_some() {
|
||||
return Err(err!(
|
||||
"parsing format did not include hour directive, \
|
||||
but did include second directive (cannot have \
|
||||
smaller time units with bigger time units missing)",
|
||||
));
|
||||
return Err(Error::from(E::MissingTimeHourForSecond));
|
||||
}
|
||||
if self.subsec.is_some() {
|
||||
return Err(err!(
|
||||
"parsing format did not include hour directive, \
|
||||
but did include fractional second directive (cannot have \
|
||||
smaller time units with bigger time units missing)",
|
||||
));
|
||||
return Err(Error::from(E::MissingTimeHourForFractional));
|
||||
}
|
||||
return Ok(Time::midnight());
|
||||
};
|
||||
let Some(minute) = self.minute else {
|
||||
if self.second.is_some() {
|
||||
return Err(err!(
|
||||
"parsing format did not include minute directive, \
|
||||
but did include second directive (cannot have \
|
||||
smaller time units with bigger time units missing)",
|
||||
));
|
||||
return Err(Error::from(E::MissingTimeMinuteForSecond));
|
||||
}
|
||||
if self.subsec.is_some() {
|
||||
return Err(err!(
|
||||
"parsing format did not include minute directive, \
|
||||
but did include fractional second directive (cannot have \
|
||||
smaller time units with bigger time units missing)",
|
||||
));
|
||||
return Err(Error::from(E::MissingTimeMinuteForFractional));
|
||||
}
|
||||
return Ok(Time::new_ranged(hour, C(0), C(0), C(0)));
|
||||
};
|
||||
let Some(second) = self.second else {
|
||||
if self.subsec.is_some() {
|
||||
return Err(err!(
|
||||
"parsing format did not include second directive, \
|
||||
but did include fractional second directive (cannot have \
|
||||
smaller time units with bigger time units missing)",
|
||||
));
|
||||
return Err(Error::from(E::MissingTimeSecondForFractional));
|
||||
}
|
||||
return Ok(Time::new_ranged(hour, minute, C(0), C(0)));
|
||||
};
|
||||
|
|
@ -3500,7 +3421,7 @@ impl Extension {
|
|||
fn parse_flag<'i>(
|
||||
fmt: &'i [u8],
|
||||
) -> Result<(Option<Flag>, &'i [u8]), Error> {
|
||||
let byte = fmt[0];
|
||||
let (&byte, tail) = fmt.split_first().unwrap();
|
||||
let flag = match byte {
|
||||
b'_' => Flag::PadSpace,
|
||||
b'0' => Flag::PadZero,
|
||||
|
|
@ -3509,15 +3430,12 @@ impl Extension {
|
|||
b'#' => Flag::Swapcase,
|
||||
_ => return Ok((None, fmt)),
|
||||
};
|
||||
let fmt = &fmt[1..];
|
||||
if fmt.is_empty() {
|
||||
return Err(err!(
|
||||
"expected to find specifier directive after flag \
|
||||
{byte:?}, but found end of format string",
|
||||
byte = escape::Byte(byte),
|
||||
));
|
||||
if tail.is_empty() {
|
||||
return Err(Error::from(E::ExpectedDirectiveAfterFlag {
|
||||
flag: byte,
|
||||
}));
|
||||
}
|
||||
Ok((Some(flag), fmt))
|
||||
Ok((Some(flag), tail))
|
||||
}
|
||||
|
||||
/// Parses an optional width that comes after a (possibly absent) flag and
|
||||
|
|
@ -3541,16 +3459,10 @@ impl Extension {
|
|||
return Ok((None, fmt));
|
||||
}
|
||||
let (digits, fmt) = util::parse::split(fmt, digits).unwrap();
|
||||
let width = util::parse::i64(digits)
|
||||
.context("failed to parse conversion specifier width")?;
|
||||
let width = u8::try_from(width).map_err(|_| {
|
||||
err!("{width} is too big, max is {max}", max = u8::MAX)
|
||||
})?;
|
||||
let width = util::parse::i64(digits).context(E::FailedWidth)?;
|
||||
let width = u8::try_from(width).map_err(|_| E::RangeWidth)?;
|
||||
if fmt.is_empty() {
|
||||
return Err(err!(
|
||||
"expected to find specifier directive after width \
|
||||
{width}, but found end of format string",
|
||||
));
|
||||
return Err(Error::from(E::ExpectedDirectiveAfterWidth));
|
||||
}
|
||||
Ok((Some(width), fmt))
|
||||
}
|
||||
|
|
@ -3570,10 +3482,7 @@ impl Extension {
|
|||
}
|
||||
let fmt = &fmt[usize::from(colons)..];
|
||||
if colons > 0 && fmt.is_empty() {
|
||||
return Err(err!(
|
||||
"expected to find specifier directive after {colons} colons, \
|
||||
but found end of format string",
|
||||
));
|
||||
return Err(Error::from(E::ExpectedDirectiveAfterColons));
|
||||
}
|
||||
Ok((u8::try_from(colons).unwrap(), fmt))
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -320,13 +320,12 @@ impl DateTimeParser {
|
|||
/// );
|
||||
/// assert_eq!(
|
||||
/// result.unwrap_err().to_string(),
|
||||
/// "parsing \"2006-04-02T02:30-05[America/Indiana/Vevay]\" failed: \
|
||||
/// datetime 2006-04-02T02:30:00 could not resolve to timestamp \
|
||||
/// since 'reject' conflict resolution was chosen, and because \
|
||||
/// datetime has offset -05, but the time zone America/Indiana/Vevay \
|
||||
/// for the given datetime falls in a gap \
|
||||
/// (between offsets -05 and -04), \
|
||||
/// and all offsets for a gap are regarded as invalid",
|
||||
/// "datetime could not resolve to timestamp since `reject` \
|
||||
/// conflict resolution was chosen, and because datetime \
|
||||
/// has offset `-05`, but the time zone `America/Indiana/Vevay` \
|
||||
/// for the given datetime falls in a gap (between offsets \
|
||||
/// `-05` and `-04`), and all offsets for a gap are \
|
||||
/// regarded as invalid",
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
|
|
@ -410,11 +409,10 @@ impl DateTimeParser {
|
|||
/// );
|
||||
/// assert_eq!(
|
||||
/// result.unwrap_err().to_string(),
|
||||
/// "parsing \"2025-06-20T17:30+00[America/New_York]\" failed: \
|
||||
/// datetime 2025-06-20T17:30:00 could not resolve to a timestamp \
|
||||
/// since 'reject' conflict resolution was chosen, and because \
|
||||
/// datetime has offset +00, but the time zone America/New_York \
|
||||
/// for the given datetime unambiguously has offset -04",
|
||||
/// "datetime could not resolve to a timestamp since `reject` \
|
||||
/// conflict resolution was chosen, and because datetime has \
|
||||
/// offset `+00`, but the time zone `America/New_York` \
|
||||
/// for the given datetime unambiguously has offset `-04`",
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
|
|
@ -1027,9 +1025,8 @@ impl DateTimeParser {
|
|||
/// // Normally this operation will fail.
|
||||
/// assert_eq!(
|
||||
/// PARSER.parse_zoned(timestamp).unwrap_err().to_string(),
|
||||
/// "failed to find time zone in square brackets in \
|
||||
/// \"2025-01-02T15:13-05\", which is required for \
|
||||
/// parsing a zoned instant",
|
||||
/// "failed to find time zone annotation in square brackets, \
|
||||
/// which is required for parsing a zoned datetime",
|
||||
/// );
|
||||
///
|
||||
/// // But you can work-around this with `Pieces`, which gives you direct
|
||||
|
|
@ -1073,8 +1070,8 @@ impl DateTimeParser {
|
|||
///
|
||||
/// assert_eq!(
|
||||
/// PARSER.parse_date("2024-03-10T00:00:00Z").unwrap_err().to_string(),
|
||||
/// "cannot parse civil date from string with a Zulu offset, \
|
||||
/// parse as a `Timestamp` and convert to a civil date instead",
|
||||
/// "cannot parse civil date/time from string with a Zulu offset, \
|
||||
/// parse as a `jiff::Timestamp` first and convert to a civil date/time instead",
|
||||
/// );
|
||||
///
|
||||
/// # Ok::<(), Box<dyn std::error::Error>>(())
|
||||
|
|
@ -2490,7 +2487,7 @@ mod tests {
|
|||
);
|
||||
insta::assert_snapshot!(
|
||||
DateTimeParser::new().parse_date("-000000-01-01").unwrap_err(),
|
||||
@"failed to parse year in date `-000000-01-01`: year zero must be written without a sign or a positive sign, but not a negative sign",
|
||||
@"failed to parse year in date: year zero must be written without a sign or a positive sign, but not a negative sign",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -146,9 +146,8 @@ use crate::{
|
|||
///
|
||||
/// assert_eq!(
|
||||
/// "2025-01-03T17:28-05".parse::<Zoned>().unwrap_err().to_string(),
|
||||
/// "failed to find time zone in square brackets in \
|
||||
/// \"2025-01-03T17:28-05\", which is required for \
|
||||
/// parsing a zoned instant",
|
||||
/// "failed to find time zone annotation in square brackets, \
|
||||
/// which is required for parsing a zoned datetime",
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
civil::{Date, DateTime, ISOWeekDate, Time},
|
||||
error::{err, Error},
|
||||
error::{fmt::temporal::Error as E, Error},
|
||||
fmt::{
|
||||
temporal::{Pieces, PiecesOffset, TimeZoneAnnotationKind},
|
||||
util::{DecimalFormatter, FractionalFormatter},
|
||||
|
|
@ -197,13 +197,7 @@ impl DateTimePrinter {
|
|||
//
|
||||
// Anyway, if you're seeing this error and think there should be a
|
||||
// different behavior, please file an issue.
|
||||
Err(err!(
|
||||
"time zones without IANA identifiers that aren't either \
|
||||
fixed offsets or a POSIX time zone can't be serialized \
|
||||
(this typically occurs when this is a system time zone \
|
||||
derived from `/etc/localtime` on Unix systems that \
|
||||
isn't symlinked to an entry in `/usr/share/zoneinfo`)",
|
||||
))
|
||||
Err(Error::from(E::PrintTimeZoneFailure))
|
||||
}
|
||||
|
||||
pub(super) fn print_pieces<W: Write>(
|
||||
|
|
|
|||
265
src/fmt/util.rs
265
src/fmt/util.rs
|
|
@ -1,7 +1,7 @@
|
|||
use crate::{
|
||||
error::{err, ErrorContext},
|
||||
error::{fmt::util::Error as E, ErrorContext},
|
||||
fmt::Parsed,
|
||||
util::{c::Sign, escape, parse, t},
|
||||
util::{c::Sign, parse, t},
|
||||
Error, SignedDuration, Span, Unit,
|
||||
};
|
||||
|
||||
|
|
@ -462,14 +462,10 @@ impl DurationUnits {
|
|||
|
||||
if let Some(min) = self.min {
|
||||
if min <= unit {
|
||||
return Err(err!(
|
||||
"found value {value:?} with unit {unit} \
|
||||
after unit {prev_unit}, but units must be \
|
||||
written from largest to smallest \
|
||||
(and they can't be repeated)",
|
||||
unit = unit.singular(),
|
||||
prev_unit = min.singular(),
|
||||
));
|
||||
return Err(Error::from(E::OutOfOrderUnits {
|
||||
found: unit,
|
||||
previous: min,
|
||||
}));
|
||||
}
|
||||
}
|
||||
// Given the above check, the given unit must be smaller than any we
|
||||
|
|
@ -503,12 +499,7 @@ impl DurationUnits {
|
|||
) -> Result<(), Error> {
|
||||
if let Some(min) = self.min {
|
||||
if min <= Unit::Hour {
|
||||
return Err(err!(
|
||||
"found `HH:MM:SS` after unit {min}, \
|
||||
but `HH:MM:SS` can only appear after \
|
||||
years, months, weeks or days",
|
||||
min = min.singular(),
|
||||
));
|
||||
return Err(Error::from(E::OutOfOrderHMS { found: min }));
|
||||
}
|
||||
}
|
||||
self.set_unit_value(Unit::Hour, hours)?;
|
||||
|
|
@ -539,15 +530,11 @@ impl DurationUnits {
|
|||
/// return an error if the minimum unit is bigger than `Unit::Hour`.
|
||||
pub(crate) fn set_fraction(&mut self, fraction: u32) -> Result<(), Error> {
|
||||
assert!(fraction <= 999_999_999);
|
||||
if self.min == Some(Unit::Nanosecond) {
|
||||
return Err(err!("fractional nanoseconds are not supported"));
|
||||
}
|
||||
if let Some(min) = self.min {
|
||||
if min > Unit::Hour {
|
||||
return Err(err!(
|
||||
"fractional {plural} are not supported",
|
||||
plural = min.plural()
|
||||
));
|
||||
if min > Unit::Hour || min == Unit::Nanosecond {
|
||||
return Err(Error::from(E::NotAllowedFractionalUnit {
|
||||
found: min,
|
||||
}));
|
||||
}
|
||||
}
|
||||
self.fraction = Some(fraction);
|
||||
|
|
@ -642,13 +629,6 @@ impl DurationUnits {
|
|||
#[cold]
|
||||
#[inline(never)]
|
||||
fn to_span_general(&self) -> Result<Span, Error> {
|
||||
fn error_context(unit: Unit, value: i64) -> Error {
|
||||
err!(
|
||||
"failed to set value {value:?} as {unit} unit on span",
|
||||
unit = unit.singular(),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "perf-inline", inline(always))]
|
||||
fn set_time_unit(
|
||||
unit: Unit,
|
||||
|
|
@ -682,7 +662,7 @@ impl DurationUnits {
|
|||
|
||||
set(span)
|
||||
.or_else(|err| fractional_fallback(err, unit, value, span))
|
||||
.with_context(|| error_context(unit, value))
|
||||
.context(E::FailedValueSet { unit })
|
||||
}
|
||||
|
||||
let (min, _) = self.get_min_max_units()?;
|
||||
|
|
@ -692,25 +672,25 @@ impl DurationUnits {
|
|||
let value = self.get_unit_value(Unit::Year)?;
|
||||
span = span
|
||||
.try_years(value)
|
||||
.with_context(|| error_context(Unit::Year, value))?;
|
||||
.context(E::FailedValueSet { unit: Unit::Year })?;
|
||||
}
|
||||
if self.values[Unit::Month.as_usize()] != 0 {
|
||||
let value = self.get_unit_value(Unit::Month)?;
|
||||
span = span
|
||||
.try_months(value)
|
||||
.with_context(|| error_context(Unit::Month, value))?;
|
||||
.context(E::FailedValueSet { unit: Unit::Month })?;
|
||||
}
|
||||
if self.values[Unit::Week.as_usize()] != 0 {
|
||||
let value = self.get_unit_value(Unit::Week)?;
|
||||
span = span
|
||||
.try_weeks(value)
|
||||
.with_context(|| error_context(Unit::Week, value))?;
|
||||
.context(E::FailedValueSet { unit: Unit::Week })?;
|
||||
}
|
||||
if self.values[Unit::Day.as_usize()] != 0 {
|
||||
let value = self.get_unit_value(Unit::Day)?;
|
||||
span = span
|
||||
.try_days(value)
|
||||
.with_context(|| error_context(Unit::Day, value))?;
|
||||
.context(E::FailedValueSet { unit: Unit::Day })?;
|
||||
}
|
||||
if self.values[Unit::Hour.as_usize()] != 0 {
|
||||
let value = self.get_unit_value(Unit::Hour)?;
|
||||
|
|
@ -822,11 +802,7 @@ impl DurationUnits {
|
|||
fn to_signed_duration_general(&self) -> Result<SignedDuration, Error> {
|
||||
let (min, max) = self.get_min_max_units()?;
|
||||
if max > Unit::Hour {
|
||||
return Err(err!(
|
||||
"parsing {unit} units into a `SignedDuration` is not supported \
|
||||
(perhaps try parsing into a `Span` instead)",
|
||||
unit = max.singular(),
|
||||
));
|
||||
return Err(Error::from(E::NotAllowedCalendarUnit { unit: max }));
|
||||
}
|
||||
|
||||
let mut sdur = SignedDuration::ZERO;
|
||||
|
|
@ -834,85 +810,43 @@ impl DurationUnits {
|
|||
let value = self.get_unit_value(Unit::Hour)?;
|
||||
sdur = SignedDuration::try_from_hours(value)
|
||||
.and_then(|nanos| sdur.checked_add(nanos))
|
||||
.ok_or_else(|| {
|
||||
err!(
|
||||
"accumulated `SignedDuration` of `{sdur:?}` \
|
||||
overflowed when adding {value} of unit {unit}",
|
||||
unit = Unit::Hour.singular(),
|
||||
)
|
||||
})?;
|
||||
.ok_or(E::OverflowForUnit { unit: Unit::Hour })?;
|
||||
}
|
||||
if self.values[Unit::Minute.as_usize()] != 0 {
|
||||
let value = self.get_unit_value(Unit::Minute)?;
|
||||
sdur = SignedDuration::try_from_mins(value)
|
||||
.and_then(|nanos| sdur.checked_add(nanos))
|
||||
.ok_or_else(|| {
|
||||
err!(
|
||||
"accumulated `SignedDuration` of `{sdur:?}` \
|
||||
overflowed when adding {value} of unit {unit}",
|
||||
unit = Unit::Minute.singular(),
|
||||
)
|
||||
})?;
|
||||
.ok_or(E::OverflowForUnit { unit: Unit::Minute })?;
|
||||
}
|
||||
if self.values[Unit::Second.as_usize()] != 0 {
|
||||
let value = self.get_unit_value(Unit::Second)?;
|
||||
sdur = SignedDuration::from_secs(value)
|
||||
.checked_add(sdur)
|
||||
.ok_or_else(|| {
|
||||
err!(
|
||||
"accumulated `SignedDuration` of `{sdur:?}` \
|
||||
overflowed when adding {value} of unit {unit}",
|
||||
unit = Unit::Second.singular(),
|
||||
)
|
||||
})?;
|
||||
.ok_or(E::OverflowForUnit { unit: Unit::Second })?;
|
||||
}
|
||||
if self.values[Unit::Millisecond.as_usize()] != 0 {
|
||||
let value = self.get_unit_value(Unit::Millisecond)?;
|
||||
sdur = SignedDuration::from_millis(value)
|
||||
.checked_add(sdur)
|
||||
.ok_or_else(|| {
|
||||
err!(
|
||||
"accumulated `SignedDuration` of `{sdur:?}` \
|
||||
overflowed when adding {value} of unit {unit}",
|
||||
unit = Unit::Millisecond.singular(),
|
||||
)
|
||||
})?;
|
||||
.ok_or(E::OverflowForUnit { unit: Unit::Millisecond })?;
|
||||
}
|
||||
if self.values[Unit::Microsecond.as_usize()] != 0 {
|
||||
let value = self.get_unit_value(Unit::Microsecond)?;
|
||||
sdur = SignedDuration::from_micros(value)
|
||||
.checked_add(sdur)
|
||||
.ok_or_else(|| {
|
||||
err!(
|
||||
"accumulated `SignedDuration` of `{sdur:?}` \
|
||||
overflowed when adding {value} of unit {unit}",
|
||||
unit = Unit::Microsecond.singular(),
|
||||
)
|
||||
})?;
|
||||
.ok_or(E::OverflowForUnit { unit: Unit::Microsecond })?;
|
||||
}
|
||||
if self.values[Unit::Nanosecond.as_usize()] != 0 {
|
||||
let value = self.get_unit_value(Unit::Nanosecond)?;
|
||||
sdur = SignedDuration::from_nanos(value)
|
||||
.checked_add(sdur)
|
||||
.ok_or_else(|| {
|
||||
err!(
|
||||
"accumulated `SignedDuration` of `{sdur:?}` \
|
||||
overflowed when adding {value} of unit {unit}",
|
||||
unit = Unit::Nanosecond.singular(),
|
||||
)
|
||||
})?;
|
||||
.ok_or(E::OverflowForUnit { unit: Unit::Nanosecond })?;
|
||||
}
|
||||
|
||||
if let Some(fraction) = self.get_fraction()? {
|
||||
sdur = sdur
|
||||
.checked_add(fractional_duration(min, fraction)?)
|
||||
.ok_or_else(|| {
|
||||
err!(
|
||||
"accumulated `SignedDuration` of `{sdur:?}` \
|
||||
overflowed when adding 0.{fraction} of unit {unit}",
|
||||
unit = min.singular(),
|
||||
)
|
||||
})?;
|
||||
.ok_or(E::OverflowForUnitFractional { unit: min })?;
|
||||
}
|
||||
|
||||
Ok(sdur)
|
||||
|
|
@ -1003,19 +937,12 @@ impl DurationUnits {
|
|||
}
|
||||
|
||||
if self.sign.is_negative() {
|
||||
return Err(err!(
|
||||
"cannot parse negative duration into unsigned \
|
||||
`std::time::Duration`",
|
||||
));
|
||||
return Err(Error::from(E::NotAllowedNegative));
|
||||
}
|
||||
|
||||
let (min, max) = self.get_min_max_units()?;
|
||||
if max > Unit::Hour {
|
||||
return Err(err!(
|
||||
"parsing {unit} units into a `std::time::Duration` \
|
||||
is not supported (perhaps try parsing into a `Span` instead)",
|
||||
unit = max.singular(),
|
||||
));
|
||||
return Err(Error::from(E::NotAllowedCalendarUnit { unit: max }));
|
||||
}
|
||||
|
||||
let mut sdur = core::time::Duration::ZERO;
|
||||
|
|
@ -1023,73 +950,37 @@ impl DurationUnits {
|
|||
let value = self.values[Unit::Hour.as_usize()];
|
||||
sdur = try_from_hours(value)
|
||||
.and_then(|nanos| sdur.checked_add(nanos))
|
||||
.ok_or_else(|| {
|
||||
err!(
|
||||
"accumulated `std::time::Duration` of `{sdur:?}` \
|
||||
overflowed when adding {value} of unit {unit}",
|
||||
unit = Unit::Hour.singular(),
|
||||
)
|
||||
})?;
|
||||
.ok_or(E::OverflowForUnit { unit: Unit::Hour })?;
|
||||
}
|
||||
if self.values[Unit::Minute.as_usize()] != 0 {
|
||||
let value = self.values[Unit::Minute.as_usize()];
|
||||
sdur = try_from_mins(value)
|
||||
.and_then(|nanos| sdur.checked_add(nanos))
|
||||
.ok_or_else(|| {
|
||||
err!(
|
||||
"accumulated `std::time::Duration` of `{sdur:?}` \
|
||||
overflowed when adding {value} of unit {unit}",
|
||||
unit = Unit::Minute.singular(),
|
||||
)
|
||||
})?;
|
||||
.ok_or(E::OverflowForUnit { unit: Unit::Minute })?;
|
||||
}
|
||||
if self.values[Unit::Second.as_usize()] != 0 {
|
||||
let value = self.values[Unit::Second.as_usize()];
|
||||
sdur = core::time::Duration::from_secs(value)
|
||||
.checked_add(sdur)
|
||||
.ok_or_else(|| {
|
||||
err!(
|
||||
"accumulated `std::time::Duration` of `{sdur:?}` \
|
||||
overflowed when adding {value} of unit {unit}",
|
||||
unit = Unit::Second.singular(),
|
||||
)
|
||||
})?;
|
||||
.ok_or(E::OverflowForUnit { unit: Unit::Second })?;
|
||||
}
|
||||
if self.values[Unit::Millisecond.as_usize()] != 0 {
|
||||
let value = self.values[Unit::Millisecond.as_usize()];
|
||||
sdur = core::time::Duration::from_millis(value)
|
||||
.checked_add(sdur)
|
||||
.ok_or_else(|| {
|
||||
err!(
|
||||
"accumulated `std::time::Duration` of `{sdur:?}` \
|
||||
overflowed when adding {value} of unit {unit}",
|
||||
unit = Unit::Millisecond.singular(),
|
||||
)
|
||||
})?;
|
||||
.ok_or(E::OverflowForUnit { unit: Unit::Millisecond })?;
|
||||
}
|
||||
if self.values[Unit::Microsecond.as_usize()] != 0 {
|
||||
let value = self.values[Unit::Microsecond.as_usize()];
|
||||
sdur = core::time::Duration::from_micros(value)
|
||||
.checked_add(sdur)
|
||||
.ok_or_else(|| {
|
||||
err!(
|
||||
"accumulated `std::time::Duration` of `{sdur:?}` \
|
||||
overflowed when adding {value} of unit {unit}",
|
||||
unit = Unit::Microsecond.singular(),
|
||||
)
|
||||
})?;
|
||||
.ok_or(E::OverflowForUnit { unit: Unit::Microsecond })?;
|
||||
}
|
||||
if self.values[Unit::Nanosecond.as_usize()] != 0 {
|
||||
let value = self.values[Unit::Nanosecond.as_usize()];
|
||||
sdur = core::time::Duration::from_nanos(value)
|
||||
.checked_add(sdur)
|
||||
.ok_or_else(|| {
|
||||
err!(
|
||||
"accumulated `std::time::Duration` of `{sdur:?}` \
|
||||
overflowed when adding {value} of unit {unit}",
|
||||
unit = Unit::Nanosecond.singular(),
|
||||
)
|
||||
})?;
|
||||
.ok_or(E::OverflowForUnit { unit: Unit::Nanosecond })?;
|
||||
}
|
||||
|
||||
if let Some(fraction) = self.get_fraction()? {
|
||||
|
|
@ -1097,13 +988,7 @@ impl DurationUnits {
|
|||
.checked_add(
|
||||
fractional_duration(min, fraction)?.unsigned_abs(),
|
||||
)
|
||||
.ok_or_else(|| {
|
||||
err!(
|
||||
"accumulated `std::time::Duration` of `{sdur:?}` \
|
||||
overflowed when adding 0.{fraction} of unit {unit}",
|
||||
unit = min.singular(),
|
||||
)
|
||||
})?;
|
||||
.ok_or(E::OverflowForUnitFractional { unit: Unit::Hour })?;
|
||||
}
|
||||
|
||||
Ok(sdur)
|
||||
|
|
@ -1122,7 +1007,7 @@ impl DurationUnits {
|
|||
/// were no parsed duration components.)
|
||||
fn get_min_max_units(&self) -> Result<(Unit, Unit), Error> {
|
||||
let (Some(min), Some(max)) = (self.min, self.max) else {
|
||||
return Err(err!("no parsed duration components"));
|
||||
return Err(Error::from(E::EmptyDuration));
|
||||
};
|
||||
Ok((min, max))
|
||||
}
|
||||
|
|
@ -1143,21 +1028,12 @@ impl DurationUnits {
|
|||
}
|
||||
// Otherwise, if a conversion to `i64` fails, then that failure
|
||||
// is correct.
|
||||
let mut value = i64::try_from(value).map_err(|_| {
|
||||
err!(
|
||||
"`{sign}{value}` {unit} is too big (or small) \
|
||||
to fit into a signed 64-bit integer",
|
||||
unit = unit.plural()
|
||||
)
|
||||
})?;
|
||||
let mut value = i64::try_from(value)
|
||||
.map_err(|_| E::SignedOverflowForUnit { unit })?;
|
||||
if sign.is_negative() {
|
||||
value = value.checked_neg().ok_or_else(|| {
|
||||
err!(
|
||||
"`{sign}{value}` {unit} is too big (or small) \
|
||||
to fit into a signed 64-bit integer",
|
||||
unit = unit.plural()
|
||||
)
|
||||
})?;
|
||||
value = value
|
||||
.checked_neg()
|
||||
.ok_or(E::SignedOverflowForUnit { unit })?;
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
|
|
@ -1258,21 +1134,13 @@ pub(crate) fn parse_temporal_fraction<'i>(
|
|||
}
|
||||
let digits = mkdigits(input);
|
||||
if digits.is_empty() {
|
||||
return Err(err!(
|
||||
"found decimal after seconds component, \
|
||||
but did not find any decimal digits after decimal",
|
||||
));
|
||||
return Err(Error::from(E::MissingFractionalDigits));
|
||||
}
|
||||
// I believe this error can never happen, since we know we have no more
|
||||
// than 9 ASCII digits. Any sequence of 9 ASCII digits can be parsed
|
||||
// into an `i64`.
|
||||
let nanoseconds = parse::fraction(digits).map_err(|err| {
|
||||
err!(
|
||||
"failed to parse {digits:?} as fractional component \
|
||||
(up to 9 digits, nanosecond precision): {err}",
|
||||
digits = escape::Bytes(digits),
|
||||
)
|
||||
})?;
|
||||
let nanoseconds =
|
||||
parse::fraction(digits).context(E::InvalidFraction)?;
|
||||
// OK because parsing is forcefully limited to 9 digits,
|
||||
// which can never be greater than `999_999_99`,
|
||||
// which is less than `u32::MAX`.
|
||||
|
|
@ -1411,18 +1279,10 @@ fn fractional_time_to_span(
|
|||
}
|
||||
if !sdur.is_zero() {
|
||||
let nanos = sdur.as_nanos();
|
||||
let nanos64 = i64::try_from(nanos).map_err(|_| {
|
||||
err!(
|
||||
"failed to set nanosecond value {nanos} (it overflows \
|
||||
`i64`) on span determined from {value}.{fraction}",
|
||||
)
|
||||
})?;
|
||||
span = span.try_nanoseconds(nanos64).with_context(|| {
|
||||
err!(
|
||||
"failed to set nanosecond value {nanos64} on span \
|
||||
determined from {value}.{fraction}",
|
||||
)
|
||||
})?;
|
||||
let nanos64 =
|
||||
i64::try_from(nanos).map_err(|_| E::InvalidFractionNanos)?;
|
||||
span =
|
||||
span.try_nanoseconds(nanos64).context(E::InvalidFractionNanos)?;
|
||||
}
|
||||
|
||||
Ok(span)
|
||||
|
|
@ -1452,13 +1312,9 @@ fn fractional_time_to_duration(
|
|||
) -> Result<SignedDuration, Error> {
|
||||
let sdur = duration_unit_value(unit, value)?;
|
||||
let fraction_dur = fractional_duration(unit, fraction)?;
|
||||
sdur.checked_add(fraction_dur).ok_or_else(|| {
|
||||
err!(
|
||||
"accumulated `SignedDuration` of `{sdur:?}` overflowed \
|
||||
when adding `{fraction_dur:?}` (from fractional {unit} units)",
|
||||
unit = unit.singular(),
|
||||
)
|
||||
})
|
||||
Ok(sdur
|
||||
.checked_add(fraction_dur)
|
||||
.ok_or(E::OverflowForUnitFractional { unit })?)
|
||||
}
|
||||
|
||||
/// Converts the fraction of the given unit to a signed duration.
|
||||
|
|
@ -1488,10 +1344,9 @@ fn fractional_duration(
|
|||
Unit::Millisecond => fraction / t::NANOS_PER_MICRO.value(),
|
||||
Unit::Microsecond => fraction / t::NANOS_PER_MILLI.value(),
|
||||
unit => {
|
||||
return Err(err!(
|
||||
"fractional {unit} units are not allowed",
|
||||
unit = unit.singular(),
|
||||
))
|
||||
return Err(Error::from(E::NotAllowedFractionalUnit {
|
||||
found: unit,
|
||||
}));
|
||||
}
|
||||
};
|
||||
Ok(SignedDuration::from_nanos(nanos))
|
||||
|
|
@ -1516,17 +1371,13 @@ fn duration_unit_value(
|
|||
Unit::Hour => {
|
||||
let seconds = value
|
||||
.checked_mul(t::SECONDS_PER_HOUR.value())
|
||||
.ok_or_else(|| {
|
||||
err!("converting {value} hours to seconds overflows i64")
|
||||
})?;
|
||||
.ok_or(E::ConversionToSecondsFailed { unit: Unit::Hour })?;
|
||||
SignedDuration::from_secs(seconds)
|
||||
}
|
||||
Unit::Minute => {
|
||||
let seconds = value
|
||||
.checked_mul(t::SECONDS_PER_MINUTE.value())
|
||||
.ok_or_else(|| {
|
||||
err!("converting {value} minutes to seconds overflows i64")
|
||||
})?;
|
||||
.ok_or(E::ConversionToSecondsFailed { unit: Unit::Minute })?;
|
||||
SignedDuration::from_secs(seconds)
|
||||
}
|
||||
Unit::Second => SignedDuration::from_secs(value),
|
||||
|
|
@ -1534,11 +1385,9 @@ fn duration_unit_value(
|
|||
Unit::Microsecond => SignedDuration::from_micros(value),
|
||||
Unit::Nanosecond => SignedDuration::from_nanos(value),
|
||||
unsupported => {
|
||||
return Err(err!(
|
||||
"parsing {unit} units into a `SignedDuration` is not supported \
|
||||
(perhaps try parsing into a `Span` instead)",
|
||||
unit = unsupported.singular(),
|
||||
));
|
||||
return Err(Error::from(E::NotAllowedCalendarUnit {
|
||||
unit: unsupported,
|
||||
}))
|
||||
}
|
||||
};
|
||||
Ok(sdur)
|
||||
|
|
|
|||
|
|
@ -87,10 +87,10 @@ impl Logger {
|
|||
/// Create a new logger that logs to stderr and initialize it as the
|
||||
/// global logger. If there was a problem setting the logger, then an
|
||||
/// error is returned.
|
||||
pub(crate) fn init() -> Result<(), crate::Error> {
|
||||
pub(crate) fn init() -> Result<(), log::SetLoggerError> {
|
||||
#[cfg(all(feature = "std", feature = "logging"))]
|
||||
{
|
||||
log::set_logger(LOGGER).map_err(crate::Error::adhoc)?;
|
||||
log::set_logger(LOGGER)?;
|
||||
log::set_max_level(log::LevelFilter::Trace);
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
15
src/now.rs
15
src/now.rs
|
|
@ -83,15 +83,16 @@ mod sys {
|
|||
} else {
|
||||
SystemTime::UNIX_EPOCH.checked_sub(duration)
|
||||
};
|
||||
// It's a little sad that we have to panic here, but the standard
|
||||
// SystemTime::now() API is infallible, so we kind of have to match it.
|
||||
// With that said, a panic here would be highly unusual. It would imply
|
||||
// that the system time is set to some extreme timestamp very far in the
|
||||
// future or the past.
|
||||
// It's a little sad that we have to panic here, but the
|
||||
// standard SystemTime::now() API is infallible, so we kind
|
||||
// of have to match it. With that said, a panic here would be
|
||||
// highly unusual. It would imply that the system time is set
|
||||
// to some extreme timestamp very far in the future or the
|
||||
// past.
|
||||
let Some(timestamp) = result else {
|
||||
panic!(
|
||||
"failed to get current time: \
|
||||
subtracting {duration:?} from Unix epoch overflowed"
|
||||
"failed to get current time from Javascript date: \
|
||||
arithmetic on Unix epoch overflowed"
|
||||
)
|
||||
};
|
||||
timestamp
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ use super::utf8;
|
|||
pub(crate) struct Byte(pub u8);
|
||||
|
||||
impl core::fmt::Display for Byte {
|
||||
#[inline(never)]
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
if self.0 == b' ' {
|
||||
return write!(f, " ");
|
||||
|
|
@ -35,6 +36,7 @@ impl core::fmt::Display for Byte {
|
|||
}
|
||||
|
||||
impl core::fmt::Debug for Byte {
|
||||
#[inline(never)]
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
write!(f, "\"")?;
|
||||
core::fmt::Display::fmt(self, f)?;
|
||||
|
|
@ -52,15 +54,16 @@ impl core::fmt::Debug for Byte {
|
|||
pub(crate) struct Bytes<'a>(pub &'a [u8]);
|
||||
|
||||
impl<'a> core::fmt::Display for Bytes<'a> {
|
||||
#[inline(never)]
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
// This is a sad re-implementation of a similar impl found in bstr.
|
||||
let mut bytes = self.0;
|
||||
while let Some(result) = utf8::decode(bytes) {
|
||||
let ch = match result {
|
||||
Ok(ch) => ch,
|
||||
Err(errant_bytes) => {
|
||||
Err(err) => {
|
||||
// The decode API guarantees `errant_bytes` is non-empty.
|
||||
write!(f, r"\x{:02x}", errant_bytes[0])?;
|
||||
write!(f, r"\x{:02x}", err.as_slice()[0])?;
|
||||
bytes = &bytes[1..];
|
||||
continue;
|
||||
}
|
||||
|
|
@ -79,6 +82,37 @@ impl<'a> core::fmt::Display for Bytes<'a> {
|
|||
}
|
||||
|
||||
impl<'a> core::fmt::Debug for Bytes<'a> {
|
||||
#[inline(never)]
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
write!(f, "\"")?;
|
||||
core::fmt::Display::fmt(self, f)?;
|
||||
write!(f, "\"")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper for repeating a single byte utilizing `Byte`.
|
||||
///
|
||||
/// This is limited to repeating a byte up to `u8::MAX` times in order
|
||||
/// to reduce its size overhead. And in practice, Jiff just doesn't
|
||||
/// need more than this (at time of writing, 2025-11-29).
|
||||
pub(crate) struct RepeatByte {
|
||||
pub(crate) byte: u8,
|
||||
pub(crate) count: u8,
|
||||
}
|
||||
|
||||
impl core::fmt::Display for RepeatByte {
|
||||
#[inline(never)]
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
for _ in 0..self.count {
|
||||
write!(f, "{}", Byte(self.byte))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for RepeatByte {
|
||||
#[inline(never)]
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
write!(f, "\"")?;
|
||||
core::fmt::Display::fmt(self, f)?;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,57 @@
|
|||
/// Represents an invalid UTF-8 sequence.
|
||||
///
|
||||
/// This is an error returned by `decode`. It is guaranteed to
|
||||
/// contain 1, 2 or 3 bytes.
|
||||
pub(crate) struct Utf8Error {
|
||||
bytes: [u8; 3],
|
||||
len: u8,
|
||||
}
|
||||
|
||||
impl Utf8Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn new(original_bytes: &[u8], err: core::str::Utf8Error) -> Utf8Error {
|
||||
let len = err.error_len().unwrap_or_else(|| original_bytes.len());
|
||||
// OK because the biggest invalid UTF-8
|
||||
// sequence possible is 3.
|
||||
debug_assert!(1 <= len && len <= 3);
|
||||
let mut bytes = [0; 3];
|
||||
bytes[..len].copy_from_slice(&original_bytes[..len]);
|
||||
Utf8Error {
|
||||
bytes,
|
||||
// OK because the biggest invalid UTF-8
|
||||
// sequence possible is 3.
|
||||
len: u8::try_from(len).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the slice of invalid UTF-8 bytes.
|
||||
///
|
||||
/// The slice returned is guaranteed to have length equivalent
|
||||
/// to `Utf8Error::len`.
|
||||
pub(crate) fn as_slice(&self) -> &[u8] {
|
||||
&self.bytes[..self.len()]
|
||||
}
|
||||
|
||||
/// Returns the length of the invalid UTF-8 sequence found.
|
||||
///
|
||||
/// This is guaranteed to be 1, 2 or 3.
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
usize::from(self.len)
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Utf8Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"found invalid UTF-8 byte {errant_bytes:?} in format \
|
||||
string (format strings must be valid UTF-8)",
|
||||
errant_bytes = crate::shared::util::escape::Bytes(self.as_slice()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Decodes the next UTF-8 encoded codepoint from the given byte slice.
|
||||
///
|
||||
/// If no valid encoding of a codepoint exists at the beginning of the
|
||||
|
|
@ -13,22 +67,20 @@
|
|||
/// *WARNING*: This is not designed for performance. If you're looking for
|
||||
/// a fast UTF-8 decoder, this is not it. If you feel like you need one in
|
||||
/// this crate, then please file an issue and discuss your use case.
|
||||
pub(crate) fn decode(bytes: &[u8]) -> Option<Result<char, &[u8]>> {
|
||||
pub(crate) fn decode(bytes: &[u8]) -> Option<Result<char, Utf8Error>> {
|
||||
if bytes.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let string = match core::str::from_utf8(&bytes[..bytes.len().min(4)]) {
|
||||
Ok(s) => s,
|
||||
Err(ref err) if err.valid_up_to() > 0 => {
|
||||
// OK because we just verified we have at least some
|
||||
// valid UTF-8.
|
||||
core::str::from_utf8(&bytes[..err.valid_up_to()]).unwrap()
|
||||
}
|
||||
// In this case, we want to return 1-3 bytes that make up a prefix of
|
||||
// a potentially valid codepoint.
|
||||
Err(err) => {
|
||||
return Some(Err(
|
||||
&bytes[..err.error_len().unwrap_or_else(|| bytes.len())]
|
||||
))
|
||||
}
|
||||
Err(err) => return Some(Err(Utf8Error::new(bytes, err))),
|
||||
};
|
||||
// OK because we guaranteed above that `string`
|
||||
// must be non-empty. And thus, `str::chars` must
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ use core::time::Duration;
|
|||
|
||||
use crate::{
|
||||
civil::{Date, DateTime, Time},
|
||||
error::{err, ErrorContext},
|
||||
error::{signed_duration::Error as E, ErrorContext},
|
||||
fmt::{friendly, temporal},
|
||||
tz::Offset,
|
||||
util::{escape, rangeint::TryRFrom, t},
|
||||
util::{rangeint::TryRFrom, t},
|
||||
Error, RoundMode, Timestamp, Unit, Zoned,
|
||||
};
|
||||
|
||||
|
|
@ -65,8 +65,7 @@ const MINS_PER_HOUR: i64 = 60;
|
|||
///
|
||||
/// assert_eq!(
|
||||
/// "P1d".parse::<SignedDuration>().unwrap_err().to_string(),
|
||||
/// "failed to parse \"P1d\" as an ISO 8601 duration string: \
|
||||
/// parsing ISO 8601 duration into a `SignedDuration` requires that \
|
||||
/// "parsing ISO 8601 duration in this context requires that \
|
||||
/// the duration contain a time component and no components of days or \
|
||||
/// greater",
|
||||
/// );
|
||||
|
|
@ -1456,24 +1455,13 @@ impl SignedDuration {
|
|||
#[inline]
|
||||
pub fn try_from_secs_f64(secs: f64) -> Result<SignedDuration, Error> {
|
||||
if !secs.is_finite() {
|
||||
return Err(err!(
|
||||
"could not convert non-finite seconds \
|
||||
{secs} to signed duration",
|
||||
));
|
||||
return Err(Error::from(E::ConvertNonFinite));
|
||||
}
|
||||
if secs < (i64::MIN as f64) {
|
||||
return Err(err!(
|
||||
"floating point seconds {secs} overflows signed duration \
|
||||
minimum value of {:?}",
|
||||
SignedDuration::MIN,
|
||||
));
|
||||
return Err(Error::slim_range("floating point seconds"));
|
||||
}
|
||||
if secs > (i64::MAX as f64) {
|
||||
return Err(err!(
|
||||
"floating point seconds {secs} overflows signed duration \
|
||||
maximum value of {:?}",
|
||||
SignedDuration::MAX,
|
||||
));
|
||||
return Err(Error::slim_range("floating point seconds"));
|
||||
}
|
||||
|
||||
let mut int_secs = secs.trunc() as i64;
|
||||
|
|
@ -1481,15 +1469,9 @@ impl SignedDuration {
|
|||
(secs.fract() * (NANOS_PER_SEC as f64)).round() as i32;
|
||||
if int_nanos.unsigned_abs() == 1_000_000_000 {
|
||||
let increment = i64::from(int_nanos.signum());
|
||||
int_secs = int_secs.checked_add(increment).ok_or_else(|| {
|
||||
err!(
|
||||
"floating point seconds {secs} overflows signed duration \
|
||||
maximum value of {max:?} after rounding its fractional \
|
||||
component of {fract:?}",
|
||||
max = SignedDuration::MAX,
|
||||
fract = secs.fract(),
|
||||
)
|
||||
})?;
|
||||
int_secs = int_secs
|
||||
.checked_add(increment)
|
||||
.ok_or_else(|| Error::slim_range("floating point seconds"))?;
|
||||
int_nanos = 0;
|
||||
}
|
||||
Ok(SignedDuration::new_unchecked(int_secs, int_nanos))
|
||||
|
|
@ -1528,24 +1510,13 @@ impl SignedDuration {
|
|||
#[inline]
|
||||
pub fn try_from_secs_f32(secs: f32) -> Result<SignedDuration, Error> {
|
||||
if !secs.is_finite() {
|
||||
return Err(err!(
|
||||
"could not convert non-finite seconds \
|
||||
{secs} to signed duration",
|
||||
));
|
||||
return Err(Error::from(E::ConvertNonFinite));
|
||||
}
|
||||
if secs < (i64::MIN as f32) {
|
||||
return Err(err!(
|
||||
"floating point seconds {secs} overflows signed duration \
|
||||
minimum value of {:?}",
|
||||
SignedDuration::MIN,
|
||||
));
|
||||
return Err(Error::slim_range("floating point seconds"));
|
||||
}
|
||||
if secs > (i64::MAX as f32) {
|
||||
return Err(err!(
|
||||
"floating point seconds {secs} overflows signed duration \
|
||||
maximum value of {:?}",
|
||||
SignedDuration::MAX,
|
||||
));
|
||||
return Err(Error::slim_range("floating point seconds"));
|
||||
}
|
||||
let mut int_nanos =
|
||||
(secs.fract() * (NANOS_PER_SEC as f32)).round() as i32;
|
||||
|
|
@ -1553,15 +1524,9 @@ impl SignedDuration {
|
|||
if int_nanos.unsigned_abs() == 1_000_000_000 {
|
||||
let increment = i64::from(int_nanos.signum());
|
||||
// N.B. I haven't found a way to trigger this error path in tests.
|
||||
int_secs = int_secs.checked_add(increment).ok_or_else(|| {
|
||||
err!(
|
||||
"floating point seconds {secs} overflows signed duration \
|
||||
maximum value of {max:?} after rounding its fractional \
|
||||
component of {fract:?}",
|
||||
max = SignedDuration::MAX,
|
||||
fract = secs.fract(),
|
||||
)
|
||||
})?;
|
||||
int_secs = int_secs
|
||||
.checked_add(increment)
|
||||
.ok_or_else(|| Error::slim_range("floating point seconds"))?;
|
||||
int_nanos = 0;
|
||||
}
|
||||
Ok(SignedDuration::new_unchecked(int_secs, int_nanos))
|
||||
|
|
@ -2023,25 +1988,18 @@ impl SignedDuration {
|
|||
time2: std::time::SystemTime,
|
||||
) -> Result<SignedDuration, Error> {
|
||||
match time2.duration_since(time1) {
|
||||
Ok(dur) => SignedDuration::try_from(dur).with_context(|| {
|
||||
err!(
|
||||
"unsigned duration {dur:?} for system time since \
|
||||
Unix epoch overflowed signed duration"
|
||||
)
|
||||
}),
|
||||
Ok(dur) => {
|
||||
SignedDuration::try_from(dur).context(E::ConvertSystemTime)
|
||||
}
|
||||
Err(err) => {
|
||||
let dur = err.duration();
|
||||
let dur =
|
||||
SignedDuration::try_from(dur).with_context(|| {
|
||||
err!(
|
||||
"unsigned duration {dur:?} for system time before \
|
||||
Unix epoch overflowed signed duration"
|
||||
)
|
||||
})?;
|
||||
dur.checked_neg().ok_or_else(|| {
|
||||
err!("negating duration {dur:?} from before the Unix epoch \
|
||||
overflowed signed duration")
|
||||
})
|
||||
let dur = SignedDuration::try_from(dur)
|
||||
.context(E::ConvertSystemTime)?;
|
||||
dur.checked_neg()
|
||||
.ok_or_else(|| {
|
||||
Error::slim_range("signed duration seconds")
|
||||
})
|
||||
.context(E::ConvertSystemTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2155,17 +2113,15 @@ impl SignedDuration {
|
|||
///
|
||||
/// assert_eq!(
|
||||
/// SignedDuration::MAX.round(Unit::Hour).unwrap_err().to_string(),
|
||||
/// "rounding `2562047788015215h 30m 7s 999ms 999µs 999ns` to \
|
||||
/// nearest hour in increments of 1 resulted in \
|
||||
/// 9223372036854777600 seconds, which does not fit into an i64 \
|
||||
/// and thus overflows `SignedDuration`",
|
||||
/// "rounding signed duration to nearest hour \
|
||||
/// resulted in a value outside the supported \
|
||||
/// range of a `jiff::SignedDuration`",
|
||||
/// );
|
||||
/// assert_eq!(
|
||||
/// SignedDuration::MIN.round(Unit::Hour).unwrap_err().to_string(),
|
||||
/// "rounding `2562047788015215h 30m 8s 999ms 999µs 999ns ago` to \
|
||||
/// nearest hour in increments of 1 resulted in \
|
||||
/// -9223372036854777600 seconds, which does not fit into an i64 \
|
||||
/// and thus overflows `SignedDuration`",
|
||||
/// "rounding signed duration to nearest hour \
|
||||
/// resulted in a value outside the supported \
|
||||
/// range of a `jiff::SignedDuration`",
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
|
|
@ -2176,9 +2132,9 @@ impl SignedDuration {
|
|||
///
|
||||
/// assert_eq!(
|
||||
/// SignedDuration::ZERO.round(Unit::Day).unwrap_err().to_string(),
|
||||
/// "rounding `SignedDuration` failed \
|
||||
/// because a calendar unit of days was provided \
|
||||
/// (to round by calendar units, you must use a `Span`)",
|
||||
/// "rounding `jiff::SignedDuration` failed \
|
||||
/// because a calendar unit of 'days' was provided \
|
||||
/// (to round by calendar units, you must use a `jiff::Span`)",
|
||||
/// );
|
||||
/// ```
|
||||
#[inline]
|
||||
|
|
@ -2414,9 +2370,8 @@ impl TryFrom<Duration> for SignedDuration {
|
|||
type Error = Error;
|
||||
|
||||
fn try_from(d: Duration) -> Result<SignedDuration, Error> {
|
||||
let secs = i64::try_from(d.as_secs()).map_err(|_| {
|
||||
err!("seconds in unsigned duration {d:?} overflowed i64")
|
||||
})?;
|
||||
let secs = i64::try_from(d.as_secs())
|
||||
.map_err(|_| Error::slim_range("unsigned duration seconds"))?;
|
||||
// Guaranteed to succeed since 0<=nanos<=999,999,999.
|
||||
let nanos = i32::try_from(d.subsec_nanos()).unwrap();
|
||||
Ok(SignedDuration::new_unchecked(secs, nanos))
|
||||
|
|
@ -2429,14 +2384,10 @@ impl TryFrom<SignedDuration> for Duration {
|
|||
fn try_from(sd: SignedDuration) -> Result<Duration, Error> {
|
||||
// This isn't needed, but improves error messages.
|
||||
if sd.is_negative() {
|
||||
return Err(err!(
|
||||
"cannot convert negative duration `{sd:?}` to \
|
||||
unsigned `std::time::Duration`",
|
||||
));
|
||||
return Err(Error::slim_range("negative duration seconds"));
|
||||
}
|
||||
let secs = u64::try_from(sd.as_secs()).map_err(|_| {
|
||||
err!("seconds in signed duration {sd:?} overflowed u64")
|
||||
})?;
|
||||
let secs = u64::try_from(sd.as_secs())
|
||||
.map_err(|_| Error::slim_range("signed duration seconds"))?;
|
||||
// Guaranteed to succeed because the above only succeeds
|
||||
// when `sd` is non-negative. And when `sd` is non-negative,
|
||||
// we are guaranteed that 0<=nanos<=999,999,999.
|
||||
|
|
@ -2771,12 +2722,9 @@ impl SignedDurationRound {
|
|||
/// Does the actual duration rounding.
|
||||
fn round(&self, dur: SignedDuration) -> Result<SignedDuration, Error> {
|
||||
if self.smallest > Unit::Hour {
|
||||
return Err(err!(
|
||||
"rounding `SignedDuration` failed because \
|
||||
a calendar unit of {plural} was provided \
|
||||
(to round by calendar units, you must use a `Span`)",
|
||||
plural = self.smallest.plural(),
|
||||
));
|
||||
return Err(Error::from(E::RoundCalendarUnit {
|
||||
unit: self.smallest,
|
||||
}));
|
||||
}
|
||||
let nanos = t::NoUnits128::new_unchecked(dur.as_nanos());
|
||||
let increment = t::NoUnits::new_unchecked(self.increment);
|
||||
|
|
@ -2789,12 +2737,7 @@ impl SignedDurationRound {
|
|||
let seconds = rounded / t::NANOS_PER_SECOND;
|
||||
let seconds =
|
||||
t::NoUnits::try_rfrom("seconds", seconds).map_err(|_| {
|
||||
err!(
|
||||
"rounding `{dur:#}` to nearest {singular} in increments \
|
||||
of {increment} resulted in {seconds} seconds, which does \
|
||||
not fit into an i64 and thus overflows `SignedDuration`",
|
||||
singular = self.smallest.singular(),
|
||||
)
|
||||
Error::from(E::RoundOverflowed { unit: self.smallest })
|
||||
})?;
|
||||
let subsec_nanos = rounded % t::NANOS_PER_SECOND;
|
||||
// OK because % 1_000_000_000 above guarantees that the result fits
|
||||
|
|
@ -2838,25 +2781,22 @@ impl From<(Unit, i64)> for SignedDurationRound {
|
|||
/// (We do the same thing for `Span`.)
|
||||
#[cfg_attr(feature = "perf-inline", inline(always))]
|
||||
fn parse_iso_or_friendly(bytes: &[u8]) -> Result<SignedDuration, Error> {
|
||||
if bytes.is_empty() {
|
||||
return Err(err!(
|
||||
"an empty string is not a valid `SignedDuration`, \
|
||||
expected either a ISO 8601 or Jiff's 'friendly' \
|
||||
format",
|
||||
let Some((&byte, tail)) = bytes.split_first() else {
|
||||
return Err(crate::Error::from(
|
||||
crate::error::fmt::Error::HybridDurationEmpty,
|
||||
));
|
||||
}
|
||||
let mut first = bytes[0];
|
||||
};
|
||||
let mut first = byte;
|
||||
// N.B. Unsigned durations don't support negative durations (of
|
||||
// course), but we still check for it here so that we can defer to
|
||||
// the dedicated parsers. They will provide their own error messages.
|
||||
if first == b'+' || first == b'-' {
|
||||
if bytes.len() == 1 {
|
||||
return Err(err!(
|
||||
"found nothing after sign `{sign}`, \
|
||||
which is not a valid `SignedDuration`, \
|
||||
expected either a ISO 8601 or Jiff's 'friendly' \
|
||||
format",
|
||||
sign = escape::Byte(first),
|
||||
let Some(&byte) = tail.first() else {
|
||||
return Err(crate::Error::from(
|
||||
crate::error::fmt::Error::HybridDurationPrefix { sign: first },
|
||||
));
|
||||
}
|
||||
first = bytes[1];
|
||||
};
|
||||
first = byte;
|
||||
}
|
||||
if first == b'P' || first == b'p' {
|
||||
temporal::DEFAULT_SPAN_PARSER.parse_duration(bytes)
|
||||
|
|
@ -3048,15 +2988,15 @@ mod tests {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
p("").unwrap_err(),
|
||||
@"an empty string is not a valid `SignedDuration`, expected either a ISO 8601 or Jiff's 'friendly' format",
|
||||
@r#"an empty string is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("+").unwrap_err(),
|
||||
@"found nothing after sign `+`, which is not a valid `SignedDuration`, expected either a ISO 8601 or Jiff's 'friendly' format",
|
||||
@r#"found nothing after sign `+`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("-").unwrap_err(),
|
||||
@"found nothing after sign `-`, which is not a valid `SignedDuration`, expected either a ISO 8601 or Jiff's 'friendly' format",
|
||||
@r#"found nothing after sign `-`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format"#,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -3093,15 +3033,15 @@ mod tests {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
p("").unwrap_err(),
|
||||
@"an empty string is not a valid `SignedDuration`, expected either a ISO 8601 or Jiff's 'friendly' format at line 1 column 2",
|
||||
@r#"an empty string is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format at line 1 column 2"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("+").unwrap_err(),
|
||||
@"found nothing after sign `+`, which is not a valid `SignedDuration`, expected either a ISO 8601 or Jiff's 'friendly' format at line 1 column 3",
|
||||
@r#"found nothing after sign `+`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format at line 1 column 3"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("-").unwrap_err(),
|
||||
@"found nothing after sign `-`, which is not a valid `SignedDuration`, expected either a ISO 8601 or Jiff's 'friendly' format at line 1 column 3",
|
||||
@r#"found nothing after sign `-`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format at line 1 column 3"#,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
353
src/span.rs
353
src/span.rs
|
|
@ -3,12 +3,11 @@ use core::{cmp::Ordering, time::Duration as UnsignedDuration};
|
|||
use crate::{
|
||||
civil::{Date, DateTime, Time},
|
||||
duration::{Duration, SDuration},
|
||||
error::{err, Error, ErrorContext},
|
||||
error::{span::Error as E, Error, ErrorContext},
|
||||
fmt::{friendly, temporal},
|
||||
tz::TimeZone,
|
||||
util::{
|
||||
borrow::DumbCow,
|
||||
escape,
|
||||
rangeint::{ri64, ri8, RFrom, RInto, TryRFrom, TryRInto},
|
||||
round::increment,
|
||||
t::{self, Constant, NoUnits, NoUnits128, Sign, C},
|
||||
|
|
@ -557,12 +556,13 @@ pub(crate) use span_eq;
|
|||
/// span.total(Unit::Hour).unwrap_err().to_string(),
|
||||
/// "using unit 'day' in a span or configuration requires that either \
|
||||
/// a relative reference time be given or \
|
||||
/// `SpanRelativeTo::days_are_24_hours()` is used to indicate \
|
||||
/// `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate \
|
||||
/// invariant 24-hour days, but neither were provided",
|
||||
/// );
|
||||
/// // Opt into invariant 24 hour days without a relative date:
|
||||
/// let marker = SpanRelativeTo::days_are_24_hours();
|
||||
/// let hours = span.total((Unit::Hour, marker))?;
|
||||
/// assert_eq!(hours, 24.0);
|
||||
/// // Or use a relative civil date, and all days are 24 hours:
|
||||
/// let date = civil::date(2020, 1, 1);
|
||||
/// let hours = span.total((Unit::Hour, date))?;
|
||||
|
|
@ -662,10 +662,11 @@ pub(crate) use span_eq;
|
|||
/// assert_eq!(
|
||||
/// Duration::try_from(span).unwrap_err().to_string(),
|
||||
/// "failed to convert span to duration without relative datetime \
|
||||
/// (must use `Span::to_duration` instead): using unit 'day' in a \
|
||||
/// span or configuration requires that either a relative reference \
|
||||
/// time be given or `SpanRelativeTo::days_are_24_hours()` is used \
|
||||
/// to indicate invariant 24-hour days, but neither were provided",
|
||||
/// (must use `jiff::Span::to_duration` instead): using unit 'day' \
|
||||
/// in a span or configuration requires that either a relative \
|
||||
/// reference time be given or \
|
||||
/// `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate \
|
||||
/// invariant 24-hour days, but neither were provided",
|
||||
/// );
|
||||
///
|
||||
/// # Ok::<(), Box<dyn std::error::Error>>(())
|
||||
|
|
@ -2325,7 +2326,8 @@ impl Span {
|
|||
/// Converts a `Span` to a [`SignedDuration`] relative to the date given.
|
||||
///
|
||||
/// In most cases, it is unlikely that you'll need to use this routine to
|
||||
/// convert a `Span` to a `SignedDuration`. Namely, by default:
|
||||
/// convert a `Span` to a `SignedDuration` and instead will be ably to
|
||||
/// use `SignedDuration::try_from(span)`. Namely, by default:
|
||||
///
|
||||
/// * [`Zoned::until`] guarantees that the biggest non-zero unit is hours.
|
||||
/// * [`Timestamp::until`] guarantees that the biggest non-zero unit is
|
||||
|
|
@ -2336,12 +2338,14 @@ impl Span {
|
|||
/// * [`Time::until`] guarantees that the biggest non-zero unit is hours.
|
||||
///
|
||||
/// In the above, only [`DateTime::until`] and [`Date::until`] return
|
||||
/// calendar units by default. In which case, one may pass
|
||||
/// [`SpanRelativeTo::days_are_24_hours`] or an actual relative date to
|
||||
/// resolve the length of a day.
|
||||
/// calendar units by default, and thus would require this routine. (In
|
||||
/// which case, one may pass [`SpanRelativeTo::days_are_24_hours`] or an
|
||||
/// actual relative date to resolve the length of a day.)
|
||||
///
|
||||
/// Of course, any of the above can be changed by asking, for example,
|
||||
/// `Zoned::until` to return units up to years.
|
||||
/// Of course, one may change the defaults. For example, if one
|
||||
/// uses `Zoned::until` with the largest unit set to `Unit::Year`
|
||||
/// and the resulting `Span` includes non-zero calendar units, then
|
||||
/// `SignedDuration::try_from` will fail because there is no relative date.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
|
|
@ -2398,24 +2402,10 @@ impl Span {
|
|||
let relspan = result
|
||||
.and_then(|r| r.into_relative_span(Unit::Second, *self))
|
||||
.with_context(|| match relative.kind {
|
||||
SpanRelativeToKind::Civil(dt) => {
|
||||
err!(
|
||||
"could not compute normalized relative span \
|
||||
from datetime {dt} and span {self}",
|
||||
)
|
||||
}
|
||||
SpanRelativeToKind::Zoned(ref zdt) => {
|
||||
err!(
|
||||
"could not compute normalized relative span \
|
||||
from datetime {zdt} and span {self}",
|
||||
)
|
||||
}
|
||||
SpanRelativeToKind::Civil(_) => E::ToDurationCivil,
|
||||
SpanRelativeToKind::Zoned(_) => E::ToDurationZoned,
|
||||
SpanRelativeToKind::DaysAre24Hours => {
|
||||
err!(
|
||||
"could not compute normalized relative span \
|
||||
from {self} when all days are assumed to be \
|
||||
24 hours",
|
||||
)
|
||||
E::ToDurationDaysAre24Hours
|
||||
}
|
||||
})?;
|
||||
debug_assert!(relspan.span.largest_unit() <= Unit::Second);
|
||||
|
|
@ -3198,13 +3188,7 @@ impl Span {
|
|||
&self,
|
||||
) -> Option<Error> {
|
||||
let non_time_unit = self.largest_calendar_unit()?;
|
||||
Some(err!(
|
||||
"operation can only be performed with units of hours \
|
||||
or smaller, but found non-zero {unit} units \
|
||||
(operations on `Timestamp`, `tz::Offset` and `civil::Time` \
|
||||
don't support calendar units in a `Span`)",
|
||||
unit = non_time_unit.singular(),
|
||||
))
|
||||
Some(Error::from(E::NotAllowedCalendarUnits { unit: non_time_unit }))
|
||||
}
|
||||
|
||||
/// Returns the largest non-zero calendar unit, or `None` if there are no
|
||||
|
|
@ -3305,8 +3289,8 @@ impl Span {
|
|||
match (span.is_zero(), new_is_zero) {
|
||||
(_, true) => Sign::N::<0>(),
|
||||
(true, false) => units.signum().rinto(),
|
||||
// If the old and new span are both non-zero, and we know our new
|
||||
// units are not negative, then the sign remains unchanged.
|
||||
// If the old and new span are both non-zero, and we know our
|
||||
// new units are not negative, then the sign remains unchanged.
|
||||
(false, false) => new.sign,
|
||||
}
|
||||
}
|
||||
|
|
@ -3473,10 +3457,7 @@ impl TryFrom<Span> for UnsignedDuration {
|
|||
fn try_from(sp: Span) -> Result<UnsignedDuration, Error> {
|
||||
// This isn't needed, but improves error messages.
|
||||
if sp.is_negative() {
|
||||
return Err(err!(
|
||||
"cannot convert negative span {sp:?} \
|
||||
to unsigned std::time::Duration",
|
||||
));
|
||||
return Err(Error::from(E::ConvertNegative));
|
||||
}
|
||||
SignedDuration::try_from(sp).and_then(UnsignedDuration::try_from)
|
||||
}
|
||||
|
|
@ -3543,18 +3524,15 @@ impl TryFrom<UnsignedDuration> for Span {
|
|||
|
||||
#[inline]
|
||||
fn try_from(d: UnsignedDuration) -> Result<Span, Error> {
|
||||
let seconds = i64::try_from(d.as_secs()).map_err(|_| {
|
||||
err!("seconds from {d:?} overflows a 64-bit signed integer")
|
||||
})?;
|
||||
let seconds = i64::try_from(d.as_secs())
|
||||
.map_err(|_| Error::slim_range("unsigned duration seconds"))?;
|
||||
let nanoseconds = i64::from(d.subsec_nanos());
|
||||
let milliseconds = nanoseconds / t::NANOS_PER_MILLI.value();
|
||||
let microseconds = (nanoseconds % t::NANOS_PER_MILLI.value())
|
||||
/ t::NANOS_PER_MICRO.value();
|
||||
let nanoseconds = nanoseconds % t::NANOS_PER_MICRO.value();
|
||||
|
||||
let span = Span::new().try_seconds(seconds).with_context(|| {
|
||||
err!("duration {d:?} overflows limits of a Jiff `Span`")
|
||||
})?;
|
||||
let span = Span::new().try_seconds(seconds)?;
|
||||
// These are all OK because `Duration::subsec_nanos` is guaranteed to
|
||||
// return less than 1_000_000_000 nanoseconds. And splitting that up
|
||||
// into millis, micros and nano components is guaranteed to fit into
|
||||
|
|
@ -3606,10 +3584,8 @@ impl TryFrom<Span> for SignedDuration {
|
|||
|
||||
#[inline]
|
||||
fn try_from(sp: Span) -> Result<SignedDuration, Error> {
|
||||
requires_relative_date_err(sp.largest_unit()).context(
|
||||
"failed to convert span to duration without relative datetime \
|
||||
(must use `Span::to_duration` instead)",
|
||||
)?;
|
||||
requires_relative_date_err(sp.largest_unit())
|
||||
.context(E::ConvertSpanToSignedDuration)?;
|
||||
Ok(sp.to_duration_invariant())
|
||||
}
|
||||
}
|
||||
|
|
@ -3678,9 +3654,7 @@ impl TryFrom<SignedDuration> for Span {
|
|||
/ t::NANOS_PER_MICRO.value();
|
||||
let nanoseconds = nanoseconds % t::NANOS_PER_MICRO.value();
|
||||
|
||||
let span = Span::new().try_seconds(seconds).with_context(|| {
|
||||
err!("signed duration {d:?} overflows limits of a Jiff `Span`")
|
||||
})?;
|
||||
let span = Span::new().try_seconds(seconds)?;
|
||||
// These are all OK because `|SignedDuration::subsec_nanos|` is
|
||||
// guaranteed to return less than 1_000_000_000 nanoseconds. And
|
||||
// splitting that up into millis, micros and nano components is
|
||||
|
|
@ -4454,7 +4428,7 @@ impl<'a> SpanArithmetic<'a> {
|
|||
/// span1.checked_add(span2).unwrap_err().to_string(),
|
||||
/// "using unit 'day' in a span or configuration requires that \
|
||||
/// either a relative reference time be given or \
|
||||
/// `SpanRelativeTo::days_are_24_hours()` is used to indicate \
|
||||
/// `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate \
|
||||
/// invariant 24-hour days, but neither were provided",
|
||||
/// );
|
||||
/// let sum = span1.checked_add(
|
||||
|
|
@ -4684,7 +4658,7 @@ impl<'a> SpanCompare<'a> {
|
|||
/// required. Otherwise, you get an error.
|
||||
///
|
||||
/// ```
|
||||
/// use jiff::{SpanCompare, ToSpan, Unit};
|
||||
/// use jiff::{SpanCompare, ToSpan};
|
||||
///
|
||||
/// let span1 = 2.days().hours(12);
|
||||
/// let span2 = 60.hours();
|
||||
|
|
@ -4693,7 +4667,7 @@ impl<'a> SpanCompare<'a> {
|
|||
/// span1.compare(span2).unwrap_err().to_string(),
|
||||
/// "using unit 'day' in a span or configuration requires that \
|
||||
/// either a relative reference time be given or \
|
||||
/// `SpanRelativeTo::days_are_24_hours()` is used to indicate \
|
||||
/// `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate \
|
||||
/// invariant 24-hour days, but neither were provided",
|
||||
/// );
|
||||
/// let ordering = span1.compare(
|
||||
|
|
@ -4924,7 +4898,7 @@ impl<'a> SpanTotal<'a> {
|
|||
/// span.total(Unit::Hour).unwrap_err().to_string(),
|
||||
/// "using unit 'day' in a span or configuration requires that either \
|
||||
/// a relative reference time be given or \
|
||||
/// `SpanRelativeTo::days_are_24_hours()` is used to indicate \
|
||||
/// `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate \
|
||||
/// invariant 24-hour days, but neither were provided",
|
||||
/// );
|
||||
///
|
||||
|
|
@ -5432,8 +5406,9 @@ impl<'a> SpanRound<'a> {
|
|||
/// span.round(Unit::Day).unwrap_err().to_string(),
|
||||
/// "error with `smallest` rounding option: using unit 'day' in a \
|
||||
/// span or configuration requires that either a relative reference \
|
||||
/// time be given or `SpanRelativeTo::days_are_24_hours()` is used \
|
||||
/// to indicate invariant 24-hour days, but neither were provided",
|
||||
/// time be given or `jiff::SpanRelativeTo::days_are_24_hours()` is \
|
||||
/// used to indicate invariant 24-hour days, but neither were \
|
||||
/// provided",
|
||||
/// );
|
||||
/// let rounded = span.round(
|
||||
/// SpanRound::new().smallest(Unit::Day).days_are_24_hours(),
|
||||
|
|
@ -5486,11 +5461,8 @@ impl<'a> SpanRound<'a> {
|
|||
let max = existing_largest.max(largest);
|
||||
let increment = increment::for_span(smallest, self.increment)?;
|
||||
if largest < smallest {
|
||||
return Err(err!(
|
||||
"largest unit ('{largest}') cannot be smaller than \
|
||||
smallest unit ('{smallest}')",
|
||||
largest = largest.singular(),
|
||||
smallest = smallest.singular(),
|
||||
return Err(Error::from(
|
||||
E::NotAllowedLargestSmallerThanSmallest { smallest, largest },
|
||||
));
|
||||
}
|
||||
let relative = match self.relative {
|
||||
|
|
@ -5516,14 +5488,13 @@ impl<'a> SpanRound<'a> {
|
|||
// no reasonable invariant interpretation of the span. And this
|
||||
// is only true when everything is less than 'day'.
|
||||
requires_relative_date_err(smallest)
|
||||
.context("error with `smallest` rounding option")?;
|
||||
.context(E::OptionSmallest)?;
|
||||
if let Some(largest) = self.largest {
|
||||
requires_relative_date_err(largest)
|
||||
.context("error with `largest` rounding option")?;
|
||||
.context(E::OptionLargest)?;
|
||||
}
|
||||
requires_relative_date_err(existing_largest).context(
|
||||
"error with largest unit in span to be rounded",
|
||||
)?;
|
||||
requires_relative_date_err(existing_largest)
|
||||
.context(E::OptionLargestInSpan)?;
|
||||
assert!(max <= Unit::Week);
|
||||
return Ok(round_span_invariant(
|
||||
span, smallest, largest, increment, mode,
|
||||
|
|
@ -5673,7 +5644,7 @@ impl<'a> SpanRelativeTo<'a> {
|
|||
/// span.total(Unit::Hour).unwrap_err().to_string(),
|
||||
/// "using unit 'day' in a span or configuration requires that either \
|
||||
/// a relative reference time be given or \
|
||||
/// `SpanRelativeTo::days_are_24_hours()` is used to indicate \
|
||||
/// `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate \
|
||||
/// invariant 24-hour days, but neither were provided",
|
||||
/// );
|
||||
/// // Opt into invariant 24 hour days without a relative date:
|
||||
|
|
@ -5709,7 +5680,7 @@ impl<'a> SpanRelativeTo<'a> {
|
|||
/// span.total(Unit::Hour).unwrap_err().to_string(),
|
||||
/// "using unit 'week' in a span or configuration requires that either \
|
||||
/// a relative reference time be given or \
|
||||
/// `SpanRelativeTo::days_are_24_hours()` is used to indicate \
|
||||
/// `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate \
|
||||
/// invariant 24-hour days, but neither were provided",
|
||||
/// );
|
||||
/// // Opt into invariant 24 hour days without a relative date:
|
||||
|
|
@ -5814,13 +5785,10 @@ impl<'a> SpanRelativeTo<'a> {
|
|||
}
|
||||
SpanRelativeToKind::DaysAre24Hours => {
|
||||
if matches!(unit, Unit::Year | Unit::Month) {
|
||||
return Err(err!(
|
||||
"using unit '{unit}' in span or configuration \
|
||||
requires that a relative reference time be given \
|
||||
(`SpanRelativeTo::days_are_24_hours()` was given \
|
||||
but this only permits using days and weeks \
|
||||
without a relative reference time)",
|
||||
unit = unit.singular(),
|
||||
return Err(Error::from(
|
||||
E::RequiresRelativeYearOrMonthGivenDaysAre24Hours {
|
||||
unit,
|
||||
},
|
||||
));
|
||||
}
|
||||
Ok(None)
|
||||
|
|
@ -6229,27 +6197,12 @@ impl<'a> RelativeSpanKind<'a> {
|
|||
RelativeSpanKind::Civil { ref start, ref end } => start
|
||||
.datetime
|
||||
.until((largest, end.datetime))
|
||||
.with_context(|| {
|
||||
err!(
|
||||
"failed to get span between {start} and {end} \
|
||||
with largest unit as {unit}",
|
||||
start = start.datetime,
|
||||
end = end.datetime,
|
||||
unit = largest.plural(),
|
||||
)
|
||||
})?,
|
||||
RelativeSpanKind::Zoned { ref start, ref end } => start
|
||||
.zoned
|
||||
.until((largest, &*end.zoned))
|
||||
.with_context(|| {
|
||||
err!(
|
||||
"failed to get span between {start} and {end} \
|
||||
with largest unit as {unit}",
|
||||
start = start.zoned,
|
||||
end = end.zoned,
|
||||
unit = largest.plural(),
|
||||
)
|
||||
})?,
|
||||
.context(E::FailedSpanBetweenDateTimes { unit: largest })?,
|
||||
RelativeSpanKind::Zoned { ref start, ref end } => {
|
||||
start.zoned.until((largest, &*end.zoned)).context(
|
||||
E::FailedSpanBetweenZonedDateTimes { unit: largest },
|
||||
)?
|
||||
}
|
||||
};
|
||||
Ok(RelativeSpan { span, kind: self })
|
||||
}
|
||||
|
|
@ -6290,9 +6243,7 @@ impl RelativeCivil {
|
|||
fn new(datetime: DateTime) -> Result<RelativeCivil, Error> {
|
||||
let timestamp = datetime
|
||||
.to_zoned(TimeZone::UTC)
|
||||
.with_context(|| {
|
||||
err!("failed to convert {datetime} to timestamp")
|
||||
})?
|
||||
.context(E::ConvertDateTimeToTimestamp)?
|
||||
.timestamp();
|
||||
Ok(RelativeCivil { datetime, timestamp })
|
||||
}
|
||||
|
|
@ -6308,14 +6259,10 @@ impl RelativeCivil {
|
|||
/// converted to a timestamp in UTC. This only occurs near the minimum and
|
||||
/// maximum datetime values.
|
||||
fn checked_add(&self, span: Span) -> Result<RelativeCivil, Error> {
|
||||
let datetime = self.datetime.checked_add(span).with_context(|| {
|
||||
err!("failed to add {span} to {dt}", dt = self.datetime)
|
||||
})?;
|
||||
let datetime = self.datetime.checked_add(span)?;
|
||||
let timestamp = datetime
|
||||
.to_zoned(TimeZone::UTC)
|
||||
.with_context(|| {
|
||||
err!("failed to convert {datetime} to timestamp")
|
||||
})?
|
||||
.context(E::ConvertDateTimeToTimestamp)?
|
||||
.timestamp();
|
||||
Ok(RelativeCivil { datetime, timestamp })
|
||||
}
|
||||
|
|
@ -6335,15 +6282,10 @@ impl RelativeCivil {
|
|||
&self,
|
||||
duration: SignedDuration,
|
||||
) -> Result<RelativeCivil, Error> {
|
||||
let datetime =
|
||||
self.datetime.checked_add(duration).with_context(|| {
|
||||
err!("failed to add {duration:?} to {dt}", dt = self.datetime)
|
||||
})?;
|
||||
let datetime = self.datetime.checked_add(duration)?;
|
||||
let timestamp = datetime
|
||||
.to_zoned(TimeZone::UTC)
|
||||
.with_context(|| {
|
||||
err!("failed to convert {datetime} to timestamp")
|
||||
})?
|
||||
.context(E::ConvertDateTimeToTimestamp)?
|
||||
.timestamp();
|
||||
Ok(RelativeCivil { datetime, timestamp })
|
||||
}
|
||||
|
|
@ -6361,15 +6303,9 @@ impl RelativeCivil {
|
|||
largest: Unit,
|
||||
other: &RelativeCivil,
|
||||
) -> Result<Span, Error> {
|
||||
self.datetime.until((largest, other.datetime)).with_context(|| {
|
||||
err!(
|
||||
"failed to get span between {dt1} and {dt2} \
|
||||
with largest unit as {unit}",
|
||||
unit = largest.plural(),
|
||||
dt1 = self.datetime,
|
||||
dt2 = other.datetime,
|
||||
)
|
||||
})
|
||||
self.datetime
|
||||
.until((largest, other.datetime))
|
||||
.context(E::FailedSpanBetweenDateTimes { unit: largest })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -6390,9 +6326,7 @@ impl<'a> RelativeZoned<'a> {
|
|||
&self,
|
||||
span: Span,
|
||||
) -> Result<RelativeZoned<'static>, Error> {
|
||||
let zoned = self.zoned.checked_add(span).with_context(|| {
|
||||
err!("failed to add {span} to {zoned}", zoned = self.zoned)
|
||||
})?;
|
||||
let zoned = self.zoned.checked_add(span)?;
|
||||
Ok(RelativeZoned { zoned: DumbCow::Owned(zoned) })
|
||||
}
|
||||
|
||||
|
|
@ -6406,9 +6340,7 @@ impl<'a> RelativeZoned<'a> {
|
|||
&self,
|
||||
duration: SignedDuration,
|
||||
) -> Result<RelativeZoned<'static>, Error> {
|
||||
let zoned = self.zoned.checked_add(duration).with_context(|| {
|
||||
err!("failed to add {duration:?} to {zoned}", zoned = self.zoned)
|
||||
})?;
|
||||
let zoned = self.zoned.checked_add(duration)?;
|
||||
Ok(RelativeZoned { zoned: DumbCow::Owned(zoned) })
|
||||
}
|
||||
|
||||
|
|
@ -6425,15 +6357,9 @@ impl<'a> RelativeZoned<'a> {
|
|||
largest: Unit,
|
||||
other: &RelativeZoned<'a>,
|
||||
) -> Result<Span, Error> {
|
||||
self.zoned.until((largest, &*other.zoned)).with_context(|| {
|
||||
err!(
|
||||
"failed to get span between {zdt1} and {zdt2} \
|
||||
with largest unit as {unit}",
|
||||
unit = largest.plural(),
|
||||
zdt1 = self.zoned,
|
||||
zdt2 = other.zoned,
|
||||
)
|
||||
})
|
||||
self.zoned
|
||||
.until((largest, &*other.zoned))
|
||||
.context(E::FailedSpanBetweenZonedDateTimes { unit: largest })
|
||||
}
|
||||
|
||||
/// Returns the borrowed version of self; useful when you need to convert
|
||||
|
|
@ -6512,13 +6438,7 @@ impl Nudge {
|
|||
increment,
|
||||
);
|
||||
let span = Span::from_invariant_nanoseconds(largest, rounded_nanos)
|
||||
.with_context(|| {
|
||||
err!(
|
||||
"failed to convert rounded nanoseconds {rounded_nanos} \
|
||||
to span for largest unit as {unit}",
|
||||
unit = largest.plural(),
|
||||
)
|
||||
})?
|
||||
.context(E::ConvertNanoseconds { unit: largest })?
|
||||
.years_ranged(balanced.get_years_ranged())
|
||||
.months_ranged(balanced.get_months_ranged())
|
||||
.weeks_ranged(balanced.get_weeks_ranged());
|
||||
|
|
@ -6551,13 +6471,7 @@ impl Nudge {
|
|||
* balanced.get_units_ranged(smallest).div_ceil(increment);
|
||||
let span = balanced
|
||||
.without_lower(smallest)
|
||||
.try_units_ranged(smallest, truncated.rinto())
|
||||
.with_context(|| {
|
||||
err!(
|
||||
"failed to set {unit} to {truncated} on span {balanced}",
|
||||
unit = smallest.singular()
|
||||
)
|
||||
})?;
|
||||
.try_units_ranged(smallest, truncated.rinto())?;
|
||||
let (relative0, relative1) = clamp_relative_span(
|
||||
relative_start,
|
||||
span,
|
||||
|
|
@ -6578,14 +6492,7 @@ impl Nudge {
|
|||
let grew_big_unit =
|
||||
((rounded.get() as f64) - exact).signum() == (sign.get() as f64);
|
||||
|
||||
let span = span
|
||||
.try_units_ranged(smallest, rounded.rinto())
|
||||
.with_context(|| {
|
||||
err!(
|
||||
"failed to set {unit} to {truncated} on span {span}",
|
||||
unit = smallest.singular()
|
||||
)
|
||||
})?;
|
||||
let span = span.try_units_ranged(smallest, rounded.rinto())?;
|
||||
let rounded_relative_end =
|
||||
if grew_big_unit { relative1 } else { relative0 };
|
||||
Ok(Nudge { span, rounded_relative_end, grew_big_unit })
|
||||
|
|
@ -6631,13 +6538,7 @@ impl Nudge {
|
|||
|
||||
let span =
|
||||
Span::from_invariant_nanoseconds(Unit::Hour, rounded_time_nanos)
|
||||
.with_context(|| {
|
||||
err!(
|
||||
"failed to convert rounded nanoseconds \
|
||||
{rounded_time_nanos} to span for largest unit as {unit}",
|
||||
unit = Unit::Hour.plural(),
|
||||
)
|
||||
})?
|
||||
.context(E::ConvertNanoseconds { unit: Unit::Hour })?
|
||||
.years_ranged(balanced.get_years_ranged())
|
||||
.months_ranged(balanced.get_months_ranged())
|
||||
.weeks_ranged(balanced.get_weeks_ranged())
|
||||
|
|
@ -6682,23 +6583,8 @@ impl Nudge {
|
|||
let span_start = balanced.without_lower(unit);
|
||||
let new_units = span_start
|
||||
.get_units_ranged(unit)
|
||||
.try_checked_add("bubble-units", sign)
|
||||
.with_context(|| {
|
||||
err!(
|
||||
"failed to add sign {sign} to {unit} value {value}",
|
||||
unit = unit.plural(),
|
||||
value = span_start.get_units_ranged(unit),
|
||||
)
|
||||
})?;
|
||||
let span_end = span_start
|
||||
.try_units_ranged(unit, new_units)
|
||||
.with_context(|| {
|
||||
err!(
|
||||
"failed to set {unit} to value \
|
||||
{new_units} on span {span_start}",
|
||||
unit = unit.plural(),
|
||||
)
|
||||
})?;
|
||||
.try_checked_add("bubble-units", sign)?;
|
||||
let span_end = span_start.try_units_ranged(unit, new_units)?;
|
||||
let threshold = match relative.kind {
|
||||
RelativeSpanKind::Civil { ref start, .. } => {
|
||||
start.checked_add(span_end)?.timestamp
|
||||
|
|
@ -6742,13 +6628,8 @@ fn round_span_invariant(
|
|||
let nanos = span.to_invariant_nanoseconds();
|
||||
let rounded =
|
||||
mode.round_by_unit_in_nanoseconds(nanos, smallest, increment);
|
||||
Span::from_invariant_nanoseconds(largest, rounded).with_context(|| {
|
||||
err!(
|
||||
"failed to convert rounded nanoseconds {rounded} \
|
||||
to span for largest unit as {unit}",
|
||||
unit = largest.plural(),
|
||||
)
|
||||
})
|
||||
Span::from_invariant_nanoseconds(largest, rounded)
|
||||
.context(E::ConvertNanoseconds { unit: largest })
|
||||
}
|
||||
|
||||
/// Returns the nanosecond timestamps of `relative + span` and `relative +
|
||||
|
|
@ -6772,24 +6653,9 @@ fn clamp_relative_span(
|
|||
unit: Unit,
|
||||
amount: NoUnits,
|
||||
) -> Result<(NoUnits128, NoUnits128), Error> {
|
||||
let amount = span
|
||||
.get_units_ranged(unit)
|
||||
.try_checked_add("clamp-units", amount)
|
||||
.with_context(|| {
|
||||
err!(
|
||||
"failed to add {amount} to {unit} \
|
||||
value {value} on span {span}",
|
||||
unit = unit.plural(),
|
||||
value = span.get_units_ranged(unit),
|
||||
)
|
||||
})?;
|
||||
let span_amount =
|
||||
span.try_units_ranged(unit, amount).with_context(|| {
|
||||
err!(
|
||||
"failed to set {unit} unit to {amount} on span {span}",
|
||||
unit = unit.plural(),
|
||||
)
|
||||
})?;
|
||||
let amount =
|
||||
span.get_units_ranged(unit).try_checked_add("clamp-units", amount)?;
|
||||
let span_amount = span.try_units_ranged(unit, amount)?;
|
||||
let relative0 = relative.checked_add(span)?.to_nanosecond();
|
||||
let relative1 = relative.checked_add(span_amount)?.to_nanosecond();
|
||||
Ok((relative0, relative1))
|
||||
|
|
@ -6811,25 +6677,22 @@ fn clamp_relative_span(
|
|||
/// (We do the same thing for `SignedDuration`.)
|
||||
#[cfg_attr(feature = "perf-inline", inline(always))]
|
||||
fn parse_iso_or_friendly(bytes: &[u8]) -> Result<Span, Error> {
|
||||
if bytes.is_empty() {
|
||||
return Err(err!(
|
||||
"an empty string is not a valid `Span`, \
|
||||
expected either a ISO 8601 or Jiff's 'friendly' \
|
||||
format",
|
||||
let Some((&byte, tail)) = bytes.split_first() else {
|
||||
return Err(crate::Error::from(
|
||||
crate::error::fmt::Error::HybridDurationEmpty,
|
||||
));
|
||||
}
|
||||
let mut first = bytes[0];
|
||||
};
|
||||
let mut first = byte;
|
||||
// N.B. Unsigned durations don't support negative durations (of
|
||||
// course), but we still check for it here so that we can defer to
|
||||
// the dedicated parsers. They will provide their own error messages.
|
||||
if first == b'+' || first == b'-' {
|
||||
if bytes.len() == 1 {
|
||||
return Err(err!(
|
||||
"found nothing after sign `{sign}`, \
|
||||
which is not a valid `Span`, \
|
||||
expected either a ISO 8601 or Jiff's 'friendly' \
|
||||
format",
|
||||
sign = escape::Byte(first),
|
||||
let Some(&byte) = tail.first() else {
|
||||
return Err(crate::Error::from(
|
||||
crate::error::fmt::Error::HybridDurationPrefix { sign: first },
|
||||
));
|
||||
}
|
||||
first = bytes[1];
|
||||
};
|
||||
first = byte;
|
||||
}
|
||||
if first == b'P' || first == b'p' {
|
||||
temporal::DEFAULT_SPAN_PARSER.parse_span(bytes)
|
||||
|
|
@ -6840,23 +6703,11 @@ fn parse_iso_or_friendly(bytes: &[u8]) -> Result<Span, Error> {
|
|||
|
||||
fn requires_relative_date_err(unit: Unit) -> Result<(), Error> {
|
||||
if unit.is_variable() {
|
||||
return Err(if matches!(unit, Unit::Week | Unit::Day) {
|
||||
err!(
|
||||
"using unit '{unit}' in a span or configuration \
|
||||
requires that either a relative reference time be given \
|
||||
or `SpanRelativeTo::days_are_24_hours()` is used to \
|
||||
indicate invariant 24-hour days, \
|
||||
but neither were provided",
|
||||
unit = unit.singular(),
|
||||
)
|
||||
return Err(Error::from(if matches!(unit, Unit::Week | Unit::Day) {
|
||||
E::RequiresRelativeWeekOrDay { unit }
|
||||
} else {
|
||||
err!(
|
||||
"using unit '{unit}' in a span or configuration \
|
||||
requires that a relative reference time be given, \
|
||||
but none was provided",
|
||||
unit = unit.singular(),
|
||||
)
|
||||
});
|
||||
E::RequiresRelativeYearOrMonth { unit }
|
||||
}));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -7390,15 +7241,15 @@ mod tests {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
p("").unwrap_err(),
|
||||
@"an empty string is not a valid `Span`, expected either a ISO 8601 or Jiff's 'friendly' format",
|
||||
@r#"an empty string is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("+").unwrap_err(),
|
||||
@"found nothing after sign `+`, which is not a valid `Span`, expected either a ISO 8601 or Jiff's 'friendly' format",
|
||||
@r#"found nothing after sign `+`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("-").unwrap_err(),
|
||||
@"found nothing after sign `-`, which is not a valid `Span`, expected either a ISO 8601 or Jiff's 'friendly' format",
|
||||
@r#"found nothing after sign `-`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format"#,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -7435,15 +7286,15 @@ mod tests {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
p("").unwrap_err(),
|
||||
@"an empty string is not a valid `Span`, expected either a ISO 8601 or Jiff's 'friendly' format at line 1 column 2",
|
||||
@r#"an empty string is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format at line 1 column 2"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("+").unwrap_err(),
|
||||
@"found nothing after sign `+`, which is not a valid `Span`, expected either a ISO 8601 or Jiff's 'friendly' format at line 1 column 3",
|
||||
@r#"found nothing after sign `+`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format at line 1 column 3"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("-").unwrap_err(),
|
||||
@"found nothing after sign `-`, which is not a valid `Span`, expected either a ISO 8601 or Jiff's 'friendly' format at line 1 column 3",
|
||||
@r#"found nothing after sign `-`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format at line 1 column 3"#,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use core::time::Duration as UnsignedDuration;
|
|||
|
||||
use crate::{
|
||||
duration::{Duration, SDuration},
|
||||
error::{err, Error, ErrorContext},
|
||||
error::{timestamp::Error as E, Error, ErrorContext},
|
||||
fmt::{
|
||||
self,
|
||||
temporal::{self, DEFAULT_DATETIME_PARSER},
|
||||
|
|
@ -279,8 +279,7 @@ use crate::{
|
|||
/// let result = "2024-06-30 08:30[America/New_York]".parse::<Timestamp>();
|
||||
/// assert_eq!(
|
||||
/// result.unwrap_err().to_string(),
|
||||
/// "failed to find offset component in \
|
||||
/// \"2024-06-30 08:30[America/New_York]\", \
|
||||
/// "failed to find offset component, \
|
||||
/// which is required for parsing a timestamp",
|
||||
/// );
|
||||
/// ```
|
||||
|
|
@ -1520,9 +1519,7 @@ impl Timestamp {
|
|||
let time_seconds = self.as_second_ranged();
|
||||
let sum = time_seconds
|
||||
.try_checked_add("span", span_seconds)
|
||||
.with_context(|| {
|
||||
err!("adding {span} to {self} overflowed")
|
||||
})?;
|
||||
.context(E::OverflowAddSpan)?;
|
||||
return Ok(Timestamp::from_second_ranged(sum));
|
||||
}
|
||||
}
|
||||
|
|
@ -1530,7 +1527,7 @@ impl Timestamp {
|
|||
let span_nanos = span.to_invariant_nanoseconds();
|
||||
let sum = time_nanos
|
||||
.try_checked_add("span", span_nanos)
|
||||
.with_context(|| err!("adding {span} to {self} overflowed"))?;
|
||||
.context(E::OverflowAddSpan)?;
|
||||
Ok(Timestamp::from_nanosecond_ranged(sum))
|
||||
}
|
||||
|
||||
|
|
@ -1540,9 +1537,7 @@ impl Timestamp {
|
|||
duration: SignedDuration,
|
||||
) -> Result<Timestamp, Error> {
|
||||
let start = self.as_duration();
|
||||
let end = start.checked_add(duration).ok_or_else(|| {
|
||||
err!("overflow when adding {duration:?} to {self}")
|
||||
})?;
|
||||
let end = start.checked_add(duration).ok_or(E::OverflowAddDuration)?;
|
||||
Timestamp::from_duration(end)
|
||||
}
|
||||
|
||||
|
|
@ -1648,9 +1643,7 @@ impl Timestamp {
|
|||
duration: A,
|
||||
) -> Result<Timestamp, Error> {
|
||||
let duration: TimestampArithmetic = duration.into();
|
||||
duration.saturating_add(self).context(
|
||||
"saturating `Timestamp` arithmetic requires only time units",
|
||||
)
|
||||
duration.saturating_add(self).context(E::RequiresSaturatingTimeUnits)
|
||||
}
|
||||
|
||||
/// This routine is identical to [`Timestamp::saturating_add`] with the
|
||||
|
|
@ -3429,11 +3422,10 @@ impl TimestampDifference {
|
|||
.get_largest()
|
||||
.unwrap_or_else(|| self.round.get_smallest().max(Unit::Second));
|
||||
if largest >= Unit::Day {
|
||||
return Err(err!(
|
||||
"unit {largest} is not supported when computing the \
|
||||
difference between timestamps (must use units smaller \
|
||||
than 'day')",
|
||||
largest = largest.singular(),
|
||||
return Err(Error::from(
|
||||
crate::error::util::RoundingIncrementError::Unsupported {
|
||||
unit: largest,
|
||||
},
|
||||
));
|
||||
}
|
||||
let nano1 = t1.as_nanosecond_ranged().without_bounds();
|
||||
|
|
@ -3856,7 +3848,7 @@ mod tests {
|
|||
fn timestamp_saturating_add() {
|
||||
insta::assert_snapshot!(
|
||||
Timestamp::MIN.saturating_add(Span::new().days(1)).unwrap_err(),
|
||||
@"saturating `Timestamp` arithmetic requires only time units: operation can only be performed with units of hours or smaller, but found non-zero day units (operations on `Timestamp`, `tz::Offset` and `civil::Time` don't support calendar units in a `Span`)",
|
||||
@"saturating timestamp arithmetic requires only time units: operation can only be performed with units of hours or smaller, but found non-zero 'day' units (operations on `jiff::Timestamp`, `jiff::tz::Offset` and `jiff::civil::Time` don't support calendar units in a `jiff::Span`)",
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -3864,7 +3856,7 @@ mod tests {
|
|||
fn timestamp_saturating_sub() {
|
||||
insta::assert_snapshot!(
|
||||
Timestamp::MAX.saturating_sub(Span::new().days(1)).unwrap_err(),
|
||||
@"saturating `Timestamp` arithmetic requires only time units: operation can only be performed with units of hours or smaller, but found non-zero day units (operations on `Timestamp`, `tz::Offset` and `civil::Time` don't support calendar units in a `Span`)",
|
||||
@"saturating timestamp arithmetic requires only time units: operation can only be performed with units of hours or smaller, but found non-zero 'day' units (operations on `jiff::Timestamp`, `jiff::tz::Offset` and `jiff::civil::Time` don't support calendar units in a `jiff::Span`)",
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
civil::DateTime,
|
||||
error::{err, Error, ErrorContext},
|
||||
error::{tz::ambiguous::Error as E, Error, ErrorContext},
|
||||
shared::util::itime::IAmbiguousOffset,
|
||||
tz::{Offset, TimeZone},
|
||||
Timestamp, Zoned,
|
||||
|
|
@ -655,18 +655,10 @@ impl AmbiguousTimestamp {
|
|||
let offset = match self.offset() {
|
||||
AmbiguousOffset::Unambiguous { offset } => offset,
|
||||
AmbiguousOffset::Gap { before, after } => {
|
||||
return Err(err!(
|
||||
"the datetime {dt} is ambiguous since it falls into \
|
||||
a gap between offsets {before} and {after}",
|
||||
dt = self.dt,
|
||||
));
|
||||
return Err(Error::from(E::BecauseGap { before, after }));
|
||||
}
|
||||
AmbiguousOffset::Fold { before, after } => {
|
||||
return Err(err!(
|
||||
"the datetime {dt} is ambiguous since it falls into \
|
||||
a fold between offsets {before} and {after}",
|
||||
dt = self.dt,
|
||||
));
|
||||
return Err(Error::from(E::BecauseFold { before, after }));
|
||||
}
|
||||
};
|
||||
offset.to_timestamp(self.dt)
|
||||
|
|
@ -1039,13 +1031,10 @@ impl AmbiguousZoned {
|
|||
/// ```
|
||||
#[inline]
|
||||
pub fn compatible(self) -> Result<Zoned, Error> {
|
||||
let ts = self.ts.compatible().with_context(|| {
|
||||
err!(
|
||||
"error converting datetime {dt} to instant in time zone {tz}",
|
||||
dt = self.datetime(),
|
||||
tz = self.time_zone().diagnostic_name(),
|
||||
)
|
||||
})?;
|
||||
let ts = self
|
||||
.ts
|
||||
.compatible()
|
||||
.with_context(|| E::InTimeZone { tz: self.time_zone().clone() })?;
|
||||
Ok(ts.to_zoned(self.tz))
|
||||
}
|
||||
|
||||
|
|
@ -1101,13 +1090,10 @@ impl AmbiguousZoned {
|
|||
/// ```
|
||||
#[inline]
|
||||
pub fn earlier(self) -> Result<Zoned, Error> {
|
||||
let ts = self.ts.earlier().with_context(|| {
|
||||
err!(
|
||||
"error converting datetime {dt} to instant in time zone {tz}",
|
||||
dt = self.datetime(),
|
||||
tz = self.time_zone().diagnostic_name(),
|
||||
)
|
||||
})?;
|
||||
let ts = self
|
||||
.ts
|
||||
.earlier()
|
||||
.with_context(|| E::InTimeZone { tz: self.time_zone().clone() })?;
|
||||
Ok(ts.to_zoned(self.tz))
|
||||
}
|
||||
|
||||
|
|
@ -1163,13 +1149,10 @@ impl AmbiguousZoned {
|
|||
/// ```
|
||||
#[inline]
|
||||
pub fn later(self) -> Result<Zoned, Error> {
|
||||
let ts = self.ts.later().with_context(|| {
|
||||
err!(
|
||||
"error converting datetime {dt} to instant in time zone {tz}",
|
||||
dt = self.datetime(),
|
||||
tz = self.time_zone().diagnostic_name(),
|
||||
)
|
||||
})?;
|
||||
let ts = self
|
||||
.ts
|
||||
.later()
|
||||
.with_context(|| E::InTimeZone { tz: self.time_zone().clone() })?;
|
||||
Ok(ts.to_zoned(self.tz))
|
||||
}
|
||||
|
||||
|
|
@ -1220,13 +1203,10 @@ impl AmbiguousZoned {
|
|||
/// ```
|
||||
#[inline]
|
||||
pub fn unambiguous(self) -> Result<Zoned, Error> {
|
||||
let ts = self.ts.unambiguous().with_context(|| {
|
||||
err!(
|
||||
"error converting datetime {dt} to instant in time zone {tz}",
|
||||
dt = self.datetime(),
|
||||
tz = self.time_zone().diagnostic_name(),
|
||||
)
|
||||
})?;
|
||||
let ts = self
|
||||
.ts
|
||||
.unambiguous()
|
||||
.with_context(|| E::InTimeZone { tz: self.time_zone().clone() })?;
|
||||
Ok(ts.to_zoned(self.tz))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@ use alloc::{
|
|||
};
|
||||
|
||||
use crate::{
|
||||
error::{err, Error, ErrorContext},
|
||||
error::{
|
||||
tz::concatenated::{Error as E, ALLOC_LIMIT},
|
||||
Error, ErrorContext,
|
||||
},
|
||||
tz::TimeZone,
|
||||
util::{array_str::ArrayStr, escape, utf8},
|
||||
};
|
||||
|
|
@ -71,7 +74,7 @@ impl<R: Read> ConcatenatedTzif<R> {
|
|||
alloc(scratch1, self.header.index_len())?;
|
||||
self.rdr
|
||||
.read_exact_at(scratch1, self.header.index_offset)
|
||||
.context("failed to read index block")?;
|
||||
.context(E::FailedReadIndex)?;
|
||||
|
||||
let mut index = &**scratch1;
|
||||
while !index.is_empty() {
|
||||
|
|
@ -94,7 +97,7 @@ impl<R: Read> ConcatenatedTzif<R> {
|
|||
let start = self.header.data_offset.saturating_add(entry.start());
|
||||
self.rdr
|
||||
.read_exact_at(scratch2, start)
|
||||
.context("failed to read TZif data block")?;
|
||||
.context(E::FailedReadData)?;
|
||||
return TimeZone::tzif(name, scratch2).map(Some);
|
||||
}
|
||||
Ok(None)
|
||||
|
|
@ -114,7 +117,7 @@ impl<R: Read> ConcatenatedTzif<R> {
|
|||
alloc(scratch, self.header.index_len())?;
|
||||
self.rdr
|
||||
.read_exact_at(scratch, self.header.index_offset)
|
||||
.context("failed to read index block")?;
|
||||
.context(E::FailedReadIndex)?;
|
||||
|
||||
let names_len = self.header.index_len() / IndexEntry::LEN;
|
||||
// Why are we careless with this alloc? Well, its size is proportional
|
||||
|
|
@ -154,31 +157,17 @@ impl Header {
|
|||
fn read<R: Read + ?Sized>(rdr: &R) -> Result<Header, Error> {
|
||||
// 12 bytes plus 3 4-byte big endian integers.
|
||||
let mut buf = [0; 12 + 3 * 4];
|
||||
rdr.read_exact_at(&mut buf, 0)
|
||||
.context("failed to read concatenated TZif header")?;
|
||||
rdr.read_exact_at(&mut buf, 0).context(E::FailedReadHeader)?;
|
||||
if &buf[..6] != b"tzdata" {
|
||||
return Err(err!(
|
||||
"expected first 6 bytes of concatenated TZif header \
|
||||
to be `tzdata`, but found `{found}`",
|
||||
found = escape::Bytes(&buf[..6]),
|
||||
));
|
||||
return Err(Error::from(E::ExpectedFirstSixBytes));
|
||||
}
|
||||
if buf[11] != 0 {
|
||||
return Err(err!(
|
||||
"expected last byte of concatenated TZif header \
|
||||
to be NUL, but found `{found}`",
|
||||
found = escape::Bytes(&buf[..12]),
|
||||
));
|
||||
return Err(Error::from(E::ExpectedLastByte));
|
||||
}
|
||||
|
||||
let version = {
|
||||
let version = core::str::from_utf8(&buf[6..11]).map_err(|_| {
|
||||
err!(
|
||||
"expected version in concatenated TZif header to \
|
||||
be valid UTF-8, but found `{found}`",
|
||||
found = escape::Bytes(&buf[6..11]),
|
||||
)
|
||||
})?;
|
||||
let version = core::str::from_utf8(&buf[6..11])
|
||||
.map_err(|_| E::ExpectedVersion)?;
|
||||
// OK because `version` is exactly 5 bytes, by construction.
|
||||
ArrayStr::new(version).unwrap()
|
||||
};
|
||||
|
|
@ -187,19 +176,12 @@ impl Header {
|
|||
// OK because the sub-slice is sized to exactly 4 bytes.
|
||||
let data_offset = u64::from(read_be32(&buf[16..20]));
|
||||
if index_offset > data_offset {
|
||||
return Err(err!(
|
||||
"invalid index ({index_offset}) and data ({data_offset}) \
|
||||
offsets, expected index offset to be less than or equal \
|
||||
to data offset",
|
||||
));
|
||||
return Err(Error::from(E::InvalidIndexDataOffsets));
|
||||
}
|
||||
// we don't read 20..24 since we don't care about zonetab (yet)
|
||||
let header = Header { version, index_offset, data_offset };
|
||||
if header.index_len() % IndexEntry::LEN != 0 {
|
||||
return Err(err!(
|
||||
"length of index block is not a multiple {len}",
|
||||
len = IndexEntry::LEN,
|
||||
));
|
||||
return Err(Error::from(E::InvalidLengthIndexBlock));
|
||||
}
|
||||
Ok(header)
|
||||
}
|
||||
|
|
@ -268,12 +250,8 @@ impl<'a> IndexEntry<'a> {
|
|||
///
|
||||
/// This returns an error if the name isn't valid UTF-8.
|
||||
fn name(&self) -> Result<&str, Error> {
|
||||
core::str::from_utf8(self.name_bytes()).map_err(|_| {
|
||||
err!(
|
||||
"IANA time zone identifier `{name}` is not valid UTF-8",
|
||||
name = escape::Bytes(self.name_bytes()),
|
||||
)
|
||||
})
|
||||
core::str::from_utf8(self.name_bytes())
|
||||
.map_err(|_| Error::from(E::ExpectedIanaName))
|
||||
}
|
||||
|
||||
/// Returns the IANA time zone identifier as a byte slice.
|
||||
|
|
@ -350,20 +328,12 @@ fn read_be32(bytes: &[u8]) -> u32 {
|
|||
impl Read for [u8] {
|
||||
fn read_exact_at(&self, buf: &mut [u8], offset: u64) -> Result<(), Error> {
|
||||
let offset = usize::try_from(offset)
|
||||
.map_err(|_| err!("offset `{offset}` overflowed `usize`"))?;
|
||||
.map_err(|_| E::InvalidOffsetOverflowSlice)?;
|
||||
let Some(slice) = self.get(offset..) else {
|
||||
return Err(err!(
|
||||
"given offset `{offset}` is not valid \
|
||||
(only {len} bytes are available)",
|
||||
len = self.len(),
|
||||
));
|
||||
return Err(Error::from(E::InvalidOffsetTooBig));
|
||||
};
|
||||
if buf.len() > slice.len() {
|
||||
return Err(err!(
|
||||
"unexpected EOF, expected {len} bytes but only have {have}",
|
||||
len = buf.len(),
|
||||
have = slice.len()
|
||||
));
|
||||
return Err(Error::from(E::ExpectedMoreData));
|
||||
}
|
||||
buf.copy_from_slice(&slice[..buf.len()]);
|
||||
Ok(())
|
||||
|
|
@ -395,9 +365,7 @@ impl Read for std::fs::File {
|
|||
offset = u64::try_from(n)
|
||||
.ok()
|
||||
.and_then(|n| n.checked_add(offset))
|
||||
.ok_or_else(|| {
|
||||
err!("offset overflow when reading from `File`")
|
||||
})?;
|
||||
.ok_or(E::InvalidOffsetOverflowFile)?;
|
||||
}
|
||||
Err(ref e) if e.kind() == io::ErrorKind::Interrupted => {}
|
||||
Err(e) => return Err(Error::io(e)),
|
||||
|
|
@ -419,9 +387,9 @@ impl Read for std::fs::File {
|
|||
fn read_exact_at(&self, buf: &mut [u8], offset: u64) -> Result<(), Error> {
|
||||
use std::io::{Read as _, Seek as _, SeekFrom};
|
||||
let mut file = self;
|
||||
file.seek(SeekFrom::Start(offset)).map_err(Error::io).with_context(
|
||||
|| err!("failed to seek to offset {offset} in `File`"),
|
||||
)?;
|
||||
file.seek(SeekFrom::Start(offset))
|
||||
.map_err(Error::io)
|
||||
.context(E::FailedSeek)?;
|
||||
file.read_exact(buf).map_err(Error::io)
|
||||
}
|
||||
}
|
||||
|
|
@ -443,31 +411,13 @@ impl Read for std::fs::File {
|
|||
/// enough in that kind of environment by far. The goal is to avoid OOM for
|
||||
/// exorbitantly large allocations through some kind of attack vector.
|
||||
fn alloc(bytes: &mut Vec<u8>, additional: usize) -> Result<(), Error> {
|
||||
// At time of writing, the biggest TZif data file is a few KB. And the
|
||||
// index block is tens of KB. So impose a limit that is a couple of orders
|
||||
// of magnitude bigger, but still overall pretty small for... some systems.
|
||||
// Anyway, I welcome improvements to this heuristic!
|
||||
const LIMIT: usize = 10 * 1 << 20;
|
||||
|
||||
if additional > LIMIT {
|
||||
return Err(err!(
|
||||
"attempted to allocate more than {LIMIT} bytes \
|
||||
while reading concatenated TZif data, which \
|
||||
exceeds a heuristic limit to prevent huge allocations \
|
||||
(please file a bug if this error is inappropriate)",
|
||||
));
|
||||
if additional > ALLOC_LIMIT {
|
||||
return Err(Error::from(E::AllocRequestOverLimit));
|
||||
}
|
||||
bytes.try_reserve_exact(additional).map_err(|_| {
|
||||
err!(
|
||||
"failed to allocation {additional} bytes \
|
||||
for reading concatenated TZif data"
|
||||
)
|
||||
})?;
|
||||
bytes.try_reserve_exact(additional).map_err(|_| E::AllocFailed)?;
|
||||
// This... can't actually happen right?
|
||||
let new_len = bytes
|
||||
.len()
|
||||
.checked_add(additional)
|
||||
.ok_or_else(|| err!("total allocation length overflowed `usize`"))?;
|
||||
let new_len =
|
||||
bytes.len().checked_add(additional).ok_or(E::AllocOverflow)?;
|
||||
bytes.resize(new_len, 0);
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,6 @@ impl Database {
|
|||
|
||||
impl core::fmt::Debug for Database {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
write!(f, "Bundled(unavailable)")
|
||||
f.write_str("Bundled(unavailable)")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ impl Database {
|
|||
|
||||
impl core::fmt::Debug for Database {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
write!(f, "Bundled(available)")
|
||||
f.write_str("Bundled(available)")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,13 +10,10 @@ impl Database {
|
|||
|
||||
#[cfg(feature = "std")]
|
||||
pub(crate) fn from_path(
|
||||
path: &std::path::Path,
|
||||
_path: &std::path::Path,
|
||||
) -> Result<Database, crate::Error> {
|
||||
Err(crate::error::err!(
|
||||
"system concatenated tzdb unavailable: \
|
||||
crate feature `tzdb-concatenated` is disabled, \
|
||||
opening tzdb at {path} has therefore failed",
|
||||
path = path.display(),
|
||||
Err(crate::error::Error::from(
|
||||
crate::error::tz::db::Error::DisabledConcatenated,
|
||||
))
|
||||
}
|
||||
|
||||
|
|
@ -41,6 +38,6 @@ impl Database {
|
|||
|
||||
impl core::fmt::Debug for Database {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
write!(f, "Concatenated(unavailable)")
|
||||
f.write_str("Concatenated(unavailable)")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ use std::{
|
|||
};
|
||||
|
||||
use crate::{
|
||||
error::{err, Error},
|
||||
error::{tz::db::Error as E, Error},
|
||||
timestamp::Timestamp,
|
||||
tz::{
|
||||
concatenated::ConcatenatedTzif, db::special_time_zone, TimeZone,
|
||||
|
|
@ -203,13 +203,13 @@ impl Database {
|
|||
|
||||
impl core::fmt::Debug for Database {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
write!(f, "Concatenated(")?;
|
||||
f.write_str("Concatenated(")?;
|
||||
if let Some(ref path) = self.path {
|
||||
write!(f, "{}", path.display())?;
|
||||
path.display().fmt(f)?;
|
||||
} else {
|
||||
write!(f, "unavailable")?;
|
||||
f.write_str("unavailable")?;
|
||||
}
|
||||
write!(f, ")")
|
||||
f.write_str(")")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -540,11 +540,7 @@ fn read_names_and_version(
|
|||
let names: Vec<Arc<str>> =
|
||||
db.available(scratch)?.into_iter().map(Arc::from).collect();
|
||||
if names.is_empty() {
|
||||
return Err(err!(
|
||||
"found no IANA time zone identifiers in \
|
||||
concatenated tzdata file at {path}",
|
||||
path = path.display(),
|
||||
));
|
||||
return Err(Error::from(E::ConcatenatedMissingIanaIdentifiers));
|
||||
}
|
||||
Ok((names, db.version()))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use crate::{
|
||||
error::{err, Error},
|
||||
error::{tz::db::Error as E, Error},
|
||||
tz::TimeZone,
|
||||
util::{sync::Arc, utf8},
|
||||
};
|
||||
|
|
@ -457,22 +457,10 @@ impl TimeZoneDatabase {
|
|||
/// # Ok::<(), Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub fn get(&self, name: &str) -> Result<TimeZone, Error> {
|
||||
let inner = self.inner.as_deref().ok_or_else(|| {
|
||||
if cfg!(feature = "std") {
|
||||
err!(
|
||||
"failed to find time zone `{name}` since there is no \
|
||||
time zone database configured",
|
||||
)
|
||||
} else {
|
||||
err!(
|
||||
"failed to find time zone `{name}`, there is no \
|
||||
global time zone database configured (and is currently \
|
||||
impossible to do so without Jiff's `std` feature \
|
||||
enabled, if you need this functionality, please file \
|
||||
an issue on Jiff's tracker with your use case)",
|
||||
)
|
||||
}
|
||||
})?;
|
||||
let inner = self
|
||||
.inner
|
||||
.as_deref()
|
||||
.ok_or_else(|| E::failed_time_zone_no_database_configured(name))?;
|
||||
match *inner {
|
||||
Kind::ZoneInfo(ref db) => {
|
||||
if let Some(tz) = db.get(name) {
|
||||
|
|
@ -493,7 +481,7 @@ impl TimeZoneDatabase {
|
|||
}
|
||||
}
|
||||
}
|
||||
Err(err!("failed to find time zone `{name}` in time zone database"))
|
||||
Err(Error::from(E::failed_time_zone(name)))
|
||||
}
|
||||
|
||||
/// Returns a list of all available time zone identifiers from this
|
||||
|
|
|
|||
|
|
@ -10,13 +10,10 @@ impl Database {
|
|||
|
||||
#[cfg(feature = "std")]
|
||||
pub(crate) fn from_dir(
|
||||
dir: &std::path::Path,
|
||||
_dir: &std::path::Path,
|
||||
) -> Result<Database, crate::Error> {
|
||||
Err(crate::error::err!(
|
||||
"system tzdb unavailable: \
|
||||
crate feature `tzdb-zoneinfo` is disabled, \
|
||||
opening tzdb at {dir} has therefore failed",
|
||||
dir = dir.display(),
|
||||
Err(crate::error::Error::from(
|
||||
crate::error::tz::db::Error::DisabledZoneInfo,
|
||||
))
|
||||
}
|
||||
|
||||
|
|
@ -41,6 +38,6 @@ impl Database {
|
|||
|
||||
impl core::fmt::Debug for Database {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
write!(f, "ZoneInfo(unavailable)")
|
||||
f.write_str("ZoneInfo(unavailable)")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ use std::{
|
|||
};
|
||||
|
||||
use crate::{
|
||||
error::{err, Error},
|
||||
error::{tz::db::Error as E, Error},
|
||||
timestamp::Timestamp,
|
||||
tz::{
|
||||
db::special_time_zone, tzif::is_possibly_tzif, TimeZone,
|
||||
|
|
@ -560,7 +560,7 @@ impl ZoneInfoName {
|
|||
fn new(base: &Path, time_zone_name: &Path) -> Result<ZoneInfoName, Error> {
|
||||
let full = base.join(time_zone_name);
|
||||
let original = parse::os_str_utf8(time_zone_name.as_os_str())
|
||||
.map_err(|err| err.path(base))?;
|
||||
.map_err(|err| Error::from(err).path(base))?;
|
||||
let lower = original.to_ascii_lowercase();
|
||||
let inner = ZoneInfoNameInner {
|
||||
full,
|
||||
|
|
@ -792,14 +792,18 @@ fn walk(start: &Path) -> Result<Vec<ZoneInfoName>, Error> {
|
|||
|
||||
let time_zone_name = match path.strip_prefix(start) {
|
||||
Ok(time_zone_name) => time_zone_name,
|
||||
Err(err) => {
|
||||
// I think this error case is actually not possible.
|
||||
// Or if it does, is a legitimate bug. Namely, `start`
|
||||
// should always be a prefix of `path`, since `path`
|
||||
// is itself derived, ultimately, from `start`.
|
||||
Err(_err) => {
|
||||
trace!(
|
||||
"failed to extract time zone name from {} \
|
||||
using {} as a base: {err}",
|
||||
using {} as a base: {_err}",
|
||||
path.display(),
|
||||
start.display(),
|
||||
);
|
||||
seterr(&path, Error::adhoc(err));
|
||||
seterr(&path, Error::from(E::ZoneInfoStripPrefix));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
|
@ -817,7 +821,7 @@ fn walk(start: &Path) -> Result<Vec<ZoneInfoName>, Error> {
|
|||
if names.is_empty() {
|
||||
let err = first_err
|
||||
.take()
|
||||
.unwrap_or_else(|| err!("{}: no TZif files", start.display()));
|
||||
.unwrap_or_else(|| Error::from(E::ZoneInfoNoTzifFiles));
|
||||
Err(err)
|
||||
} else {
|
||||
// If we found at least one valid name, then we declare success and
|
||||
|
|
|
|||
110
src/tz/offset.rs
110
src/tz/offset.rs
|
|
@ -6,7 +6,7 @@ use core::{
|
|||
use crate::{
|
||||
civil,
|
||||
duration::{Duration, SDuration},
|
||||
error::{err, Error, ErrorContext},
|
||||
error::{tz::offset::Error as E, Error, ErrorContext},
|
||||
shared::util::itime::IOffset,
|
||||
span::Span,
|
||||
timestamp::Timestamp,
|
||||
|
|
@ -526,12 +526,8 @@ impl Offset {
|
|||
.to_idatetime()
|
||||
.zip2(self.to_ioffset())
|
||||
.map(|(idt, ioff)| idt.to_timestamp(ioff));
|
||||
Timestamp::from_itimestamp(its).with_context(|| {
|
||||
err!(
|
||||
"converting {dt} with offset {offset} to timestamp overflowed",
|
||||
offset = self,
|
||||
)
|
||||
})
|
||||
Timestamp::from_itimestamp(its)
|
||||
.context(E::ConvertDateTimeToTimestamp { offset: self })
|
||||
}
|
||||
|
||||
/// Adds the given span of time to this offset.
|
||||
|
|
@ -660,21 +656,11 @@ impl Offset {
|
|||
) -> Result<Offset, Error> {
|
||||
let duration =
|
||||
t::SpanZoneOffset::try_new("duration-seconds", duration.as_secs())
|
||||
.with_context(|| {
|
||||
err!(
|
||||
"adding signed duration {duration:?} \
|
||||
to offset {self} overflowed maximum offset seconds"
|
||||
)
|
||||
})?;
|
||||
.context(E::OverflowAddSignedDuration)?;
|
||||
let offset_seconds = self.seconds_ranged();
|
||||
let seconds = offset_seconds
|
||||
.try_checked_add("offset-seconds", duration)
|
||||
.with_context(|| {
|
||||
err!(
|
||||
"adding signed duration {duration:?} \
|
||||
to offset {self} overflowed"
|
||||
)
|
||||
})?;
|
||||
.context(E::OverflowAddSignedDuration)?;
|
||||
Ok(Offset::from_seconds_ranged(seconds))
|
||||
}
|
||||
|
||||
|
|
@ -975,8 +961,7 @@ impl Offset {
|
|||
/// assert_eq!(Offset::MAX.to_string(), "+25:59:59");
|
||||
/// assert_eq!(
|
||||
/// Offset::MAX.round(Unit::Minute).unwrap_err().to_string(),
|
||||
/// "rounding offset `+25:59:59` resulted in a duration of 26h, \
|
||||
/// which overflows `Offset`",
|
||||
/// "rounding time zone offset resulted in a duration that overflows",
|
||||
/// );
|
||||
/// ```
|
||||
#[inline]
|
||||
|
|
@ -1347,11 +1332,10 @@ impl TryFrom<SignedDuration> for Offset {
|
|||
} else if subsec <= -500_000_000 {
|
||||
seconds = seconds.saturating_sub(1);
|
||||
}
|
||||
let seconds = i32::try_from(seconds).map_err(|_| {
|
||||
err!("`SignedDuration` of {sdur} overflows `Offset`")
|
||||
})?;
|
||||
let seconds =
|
||||
i32::try_from(seconds).map_err(|_| E::OverflowSignedDuration)?;
|
||||
Offset::from_seconds(seconds)
|
||||
.map_err(|_| err!("`SignedDuration` of {sdur} overflows `Offset`"))
|
||||
.map_err(|_| Error::from(E::OverflowSignedDuration))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1599,20 +1583,11 @@ impl OffsetRound {
|
|||
fn round(&self, offset: Offset) -> Result<Offset, Error> {
|
||||
let smallest = self.0.get_smallest();
|
||||
if !(Unit::Second <= smallest && smallest <= Unit::Hour) {
|
||||
return Err(err!(
|
||||
"rounding `Offset` failed because \
|
||||
a unit of {plural} was provided, but offset rounding \
|
||||
can only use hours, minutes or seconds",
|
||||
plural = smallest.plural(),
|
||||
));
|
||||
return Err(Error::from(E::RoundInvalidUnit { unit: smallest }));
|
||||
}
|
||||
let rounded_sdur = SignedDuration::from(offset).round(self.0)?;
|
||||
Offset::try_from(rounded_sdur).map_err(|_| {
|
||||
err!(
|
||||
"rounding offset `{offset}` resulted in a duration \
|
||||
of {rounded_sdur:?}, which overflows `Offset`",
|
||||
)
|
||||
})
|
||||
Offset::try_from(rounded_sdur)
|
||||
.map_err(|_| Error::from(E::RoundOverflow))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1888,10 +1863,10 @@ impl OffsetConflict {
|
|||
/// let result = OffsetConflict::Reject.resolve(dt, offset, tz.clone());
|
||||
/// assert_eq!(
|
||||
/// result.unwrap_err().to_string(),
|
||||
/// "datetime 1968-02-01T23:15:00 could not resolve to a timestamp \
|
||||
/// since 'reject' conflict resolution was chosen, and because \
|
||||
/// datetime has offset -00:45, but the time zone Africa/Monrovia \
|
||||
/// for the given datetime unambiguously has offset -00:44:30",
|
||||
/// "datetime could not resolve to a timestamp since `reject` \
|
||||
/// conflict resolution was chosen, and because datetime has offset \
|
||||
/// `-00:45`, but the time zone `Africa/Monrovia` for the given \
|
||||
/// datetime unambiguously has offset `-00:44:30`",
|
||||
/// );
|
||||
/// let is_equal = |parsed: Offset, candidate: Offset| {
|
||||
/// parsed == candidate || candidate.round(Unit::Minute).map_or(
|
||||
|
|
@ -1950,11 +1925,10 @@ impl OffsetConflict {
|
|||
/// let result = "1970-06-01T00-00:45:00[Africa/Monrovia]".parse::<Zoned>();
|
||||
/// assert_eq!(
|
||||
/// result.unwrap_err().to_string(),
|
||||
/// "parsing \"1970-06-01T00-00:45:00[Africa/Monrovia]\" failed: \
|
||||
/// datetime 1970-06-01T00:00:00 could not resolve to a timestamp \
|
||||
/// since 'reject' conflict resolution was chosen, and because \
|
||||
/// datetime has offset -00:45, but the time zone Africa/Monrovia \
|
||||
/// for the given datetime unambiguously has offset -00:44:30",
|
||||
/// "datetime could not resolve to a timestamp since `reject` \
|
||||
/// conflict resolution was chosen, and because datetime has offset \
|
||||
/// `-00:45`, but the time zone `Africa/Monrovia` for the given \
|
||||
/// datetime unambiguously has offset `-00:44:30`",
|
||||
/// );
|
||||
/// ```
|
||||
pub fn resolve_with<F>(
|
||||
|
|
@ -2046,13 +2020,13 @@ impl OffsetConflict {
|
|||
|
||||
let amb = tz.to_ambiguous_timestamp(dt);
|
||||
match amb.offset() {
|
||||
Unambiguous { offset } if !is_equal(given, offset) => Err(err!(
|
||||
"datetime {dt} could not resolve to a timestamp since \
|
||||
'reject' conflict resolution was chosen, and because \
|
||||
datetime has offset {given}, but the time zone {tzname} for \
|
||||
the given datetime unambiguously has offset {offset}",
|
||||
tzname = tz.diagnostic_name(),
|
||||
)),
|
||||
Unambiguous { offset } if !is_equal(given, offset) => {
|
||||
Err(Error::from(E::ResolveRejectUnambiguous {
|
||||
given,
|
||||
offset,
|
||||
tz,
|
||||
}))
|
||||
}
|
||||
Unambiguous { .. } => Ok(amb.into_ambiguous_zoned(tz)),
|
||||
Gap { before, after } => {
|
||||
// In `jiff 0.1`, we reported an error when we found a gap
|
||||
|
|
@ -2065,28 +2039,22 @@ impl OffsetConflict {
|
|||
// changed to treat all offsets in a gap as invalid).
|
||||
//
|
||||
// Ref: https://github.com/tc39/proposal-temporal/issues/2892
|
||||
Err(err!(
|
||||
"datetime {dt} could not resolve to timestamp \
|
||||
since 'reject' conflict resolution was chosen, and \
|
||||
because datetime has offset {given}, but the time \
|
||||
zone {tzname} for the given datetime falls in a gap \
|
||||
(between offsets {before} and {after}), and all \
|
||||
offsets for a gap are regarded as invalid",
|
||||
tzname = tz.diagnostic_name(),
|
||||
))
|
||||
Err(Error::from(E::ResolveRejectGap {
|
||||
given,
|
||||
before,
|
||||
after,
|
||||
tz,
|
||||
}))
|
||||
}
|
||||
Fold { before, after }
|
||||
if !is_equal(given, before) && !is_equal(given, after) =>
|
||||
{
|
||||
Err(err!(
|
||||
"datetime {dt} could not resolve to timestamp \
|
||||
since 'reject' conflict resolution was chosen, and \
|
||||
because datetime has offset {given}, but the time \
|
||||
zone {tzname} for the given datetime falls in a fold \
|
||||
between offsets {before} and {after}, neither of which \
|
||||
match the offset",
|
||||
tzname = tz.diagnostic_name(),
|
||||
))
|
||||
Err(Error::from(E::ResolveRejectFold {
|
||||
given,
|
||||
before,
|
||||
after,
|
||||
tz,
|
||||
}))
|
||||
}
|
||||
Fold { .. } => {
|
||||
let kind = Unambiguous { offset: given };
|
||||
|
|
|
|||
|
|
@ -72,14 +72,14 @@ use core::fmt::Debug;
|
|||
|
||||
use crate::{
|
||||
civil::DateTime,
|
||||
error::{err, Error, ErrorContext},
|
||||
error::{tz::posix::Error as E, Error, ErrorContext},
|
||||
shared,
|
||||
timestamp::Timestamp,
|
||||
tz::{
|
||||
timezone::TimeZoneAbbreviation, AmbiguousOffset, Dst, Offset,
|
||||
TimeZoneOffsetInfo, TimeZoneTransition,
|
||||
},
|
||||
util::{array_str::Abbreviation, escape::Bytes, parse},
|
||||
util::{array_str::Abbreviation, parse},
|
||||
};
|
||||
|
||||
/// The result of parsing the POSIX `TZ` environment variable.
|
||||
|
|
@ -114,11 +114,7 @@ impl PosixTzEnv {
|
|||
let bytes = bytes.as_ref();
|
||||
if bytes.get(0) == Some(&b':') {
|
||||
let Ok(string) = core::str::from_utf8(&bytes[1..]) else {
|
||||
return Err(err!(
|
||||
"POSIX time zone string with a ':' prefix contains \
|
||||
invalid UTF-8: {:?}",
|
||||
Bytes(&bytes[1..]),
|
||||
));
|
||||
return Err(Error::from(E::ColonPrefixInvalidUtf8));
|
||||
};
|
||||
Ok(PosixTzEnv::Implementation(string.into()))
|
||||
} else {
|
||||
|
|
@ -138,8 +134,11 @@ impl PosixTzEnv {
|
|||
impl core::fmt::Display for PosixTzEnv {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
match *self {
|
||||
PosixTzEnv::Rule(ref tz) => write!(f, "{tz}"),
|
||||
PosixTzEnv::Implementation(ref imp) => write!(f, ":{imp}"),
|
||||
PosixTzEnv::Rule(ref tz) => core::fmt::Display::fmt(tz, f),
|
||||
PosixTzEnv::Implementation(ref imp) => {
|
||||
f.write_str(":")?;
|
||||
core::fmt::Display::fmt(imp, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -212,9 +211,7 @@ impl PosixTimeZone<Abbreviation> {
|
|||
let bytes = bytes.as_ref();
|
||||
let inner = shared::PosixTimeZone::parse(bytes.as_ref())
|
||||
.map_err(Error::shared)
|
||||
.map_err(|e| {
|
||||
e.context(err!("invalid POSIX TZ string {:?}", Bytes(bytes)))
|
||||
})?;
|
||||
.context(E::InvalidPosixTz)?;
|
||||
Ok(PosixTimeZone { inner })
|
||||
}
|
||||
|
||||
|
|
@ -228,12 +225,7 @@ impl PosixTimeZone<Abbreviation> {
|
|||
let (inner, remaining) =
|
||||
shared::PosixTimeZone::parse_prefix(bytes.as_ref())
|
||||
.map_err(Error::shared)
|
||||
.map_err(|e| {
|
||||
e.context(err!(
|
||||
"invalid POSIX TZ string {:?}",
|
||||
Bytes(bytes)
|
||||
))
|
||||
})?;
|
||||
.context(E::InvalidPosixTz)?;
|
||||
Ok((PosixTimeZone { inner }, remaining))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::{sync::RwLock, time::Duration};
|
|||
use alloc::string::ToString;
|
||||
|
||||
use crate::{
|
||||
error::{err, Error, ErrorContext},
|
||||
error::{tz::system::Error as E, Error, ErrorContext},
|
||||
tz::{posix::PosixTzEnv, TimeZone, TimeZoneDatabase},
|
||||
util::cache::Expiration,
|
||||
};
|
||||
|
|
@ -141,22 +141,20 @@ pub(crate) fn get(db: &TimeZoneDatabase) -> Result<TimeZone, Error> {
|
|||
pub(crate) fn get_force(db: &TimeZoneDatabase) -> Result<TimeZone, Error> {
|
||||
match get_env_tz(db) {
|
||||
Ok(Some(tz)) => {
|
||||
debug!("checked TZ environment variable and found {tz:?}");
|
||||
debug!("checked `TZ` environment variable and found {tz:?}");
|
||||
return Ok(tz);
|
||||
}
|
||||
Ok(None) => {
|
||||
debug!("TZ environment variable is not set");
|
||||
debug!("`TZ` environment variable is not set");
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(err.context(
|
||||
"TZ environment variable set, but failed to read value",
|
||||
));
|
||||
return Err(err.context(E::FailedEnvTz));
|
||||
}
|
||||
}
|
||||
if let Some(tz) = sys::get(db) {
|
||||
return Ok(tz);
|
||||
}
|
||||
Err(err!("failed to find system time zone"))
|
||||
Err(Error::from(E::FailedSystemTimeZone))
|
||||
}
|
||||
|
||||
/// Materializes a `TimeZone` from a `TZ` environment variable.
|
||||
|
|
@ -184,8 +182,8 @@ fn get_env_tz(db: &TimeZoneDatabase) -> Result<Option<TimeZone>, Error> {
|
|||
// `TZ=UTC`.
|
||||
if tzenv.is_empty() {
|
||||
debug!(
|
||||
"TZ environment variable set to empty value, \
|
||||
assuming TZ=UTC in order to conform to \
|
||||
"`TZ` environment variable set to empty value, \
|
||||
assuming `TZ=UTC` in order to conform to \
|
||||
widespread convention among Unix tooling",
|
||||
);
|
||||
return Ok(Some(TimeZone::UTC));
|
||||
|
|
@ -196,15 +194,7 @@ fn get_env_tz(db: &TimeZoneDatabase) -> Result<Option<TimeZone>, Error> {
|
|||
"failed to parse {tzenv:?} as POSIX TZ rule \
|
||||
(attempting to treat it as an IANA time zone): {_err}",
|
||||
);
|
||||
tzenv
|
||||
.to_str()
|
||||
.ok_or_else(|| {
|
||||
err!(
|
||||
"failed to parse {tzenv:?} as a POSIX TZ transition \
|
||||
string, or as valid UTF-8",
|
||||
)
|
||||
})?
|
||||
.to_string()
|
||||
tzenv.to_str().ok_or(E::FailedPosixTzAndUtf8)?.to_string()
|
||||
}
|
||||
Ok(PosixTzEnv::Implementation(string)) => string.to_string(),
|
||||
Ok(PosixTzEnv::Rule(tz)) => {
|
||||
|
|
@ -231,26 +221,20 @@ fn get_env_tz(db: &TimeZoneDatabase) -> Result<Option<TimeZone>, Error> {
|
|||
// No zoneinfo means this is probably a IANA Time Zone name. But...
|
||||
// it could just be a file path.
|
||||
debug!(
|
||||
"could not find {needle:?} in TZ={tz_name_or_path:?}, \
|
||||
therefore attempting lookup in {db:?}",
|
||||
"could not find {needle:?} in `TZ={tz_name_or_path:?}`, \
|
||||
therefore attempting lookup in `{db:?}`",
|
||||
);
|
||||
return match db.get(&tz_name_or_path) {
|
||||
Ok(tz) => Ok(Some(tz)),
|
||||
Err(_err) => {
|
||||
debug!(
|
||||
"using TZ={tz_name_or_path:?} as time zone name failed, \
|
||||
could not find time zone in zoneinfo database {db:?} \
|
||||
"using `TZ={tz_name_or_path:?}` as time zone name failed, \
|
||||
could not find time zone in zoneinfo database `{db:?}` \
|
||||
(continuing to try and read `{tz_name_or_path}` as \
|
||||
a TZif file)",
|
||||
);
|
||||
sys::read(db, &tz_name_or_path)
|
||||
.ok_or_else(|| {
|
||||
err!(
|
||||
"failed to read TZ={tz_name_or_path:?} \
|
||||
as a TZif file after attempting a tzdb \
|
||||
lookup for `{tz_name_or_path}`",
|
||||
)
|
||||
})
|
||||
.ok_or_else(|| Error::from(E::FailedEnvTzAsTzif))
|
||||
.map(Some)
|
||||
}
|
||||
};
|
||||
|
|
@ -260,16 +244,16 @@ fn get_env_tz(db: &TimeZoneDatabase) -> Result<Option<TimeZone>, Error> {
|
|||
// `zoneinfo/`. Once we have that, we try to look it up in our tzdb.
|
||||
let name = &tz_name_or_path[rpos + needle.len()..];
|
||||
debug!(
|
||||
"extracted {name:?} from TZ={tz_name_or_path:?} \
|
||||
"extracted `{name}` from `TZ={tz_name_or_path}` \
|
||||
and assuming it is an IANA time zone name",
|
||||
);
|
||||
match db.get(&name) {
|
||||
Ok(tz) => return Ok(Some(tz)),
|
||||
Err(_err) => {
|
||||
debug!(
|
||||
"using {name:?} from TZ={tz_name_or_path:?}, \
|
||||
could not find time zone in zoneinfo database {db:?} \
|
||||
(continuing to try and use {tz_name_or_path:?})",
|
||||
"using `{name}` from `TZ={tz_name_or_path}`, \
|
||||
could not find time zone in zoneinfo database `{db:?}` \
|
||||
(continuing to try and use `{tz_name_or_path}`)",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -279,13 +263,7 @@ fn get_env_tz(db: &TimeZoneDatabase) -> Result<Option<TimeZone>, Error> {
|
|||
// and read the data as TZif. This will give us time zone data if it works,
|
||||
// but without a name.
|
||||
sys::read(db, &tz_name_or_path)
|
||||
.ok_or_else(|| {
|
||||
err!(
|
||||
"failed to read TZ={tz_name_or_path:?} \
|
||||
as a TZif file after attempting a tzdb \
|
||||
lookup for `{name}`",
|
||||
)
|
||||
})
|
||||
.ok_or_else(|| Error::from(E::FailedEnvTzAsTzif))
|
||||
.map(Some)
|
||||
}
|
||||
|
||||
|
|
@ -298,8 +276,8 @@ fn get_env_tz(db: &TimeZoneDatabase) -> Result<Option<TimeZone>, Error> {
|
|||
fn read_unnamed_tzif_file(path: &str) -> Result<TimeZone, Error> {
|
||||
let data = std::fs::read(path)
|
||||
.map_err(Error::io)
|
||||
.with_context(|| err!("failed to read {path:?} as TZif file"))?;
|
||||
let tz = TimeZone::tzif_system(&data)
|
||||
.with_context(|| err!("found invalid TZif data at {path:?}"))?;
|
||||
.context(E::FailedUnnamedTzifRead)?;
|
||||
let tz =
|
||||
TimeZone::tzif_system(&data).context(E::FailedUnnamedTzifInvalid)?;
|
||||
Ok(tz)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use windows_sys::Win32::System::Time::{
|
|||
};
|
||||
|
||||
use crate::{
|
||||
error::{err, Error, ErrorContext},
|
||||
error::{tz::system::Error as E, Error, ErrorContext},
|
||||
tz::{TimeZone, TimeZoneDatabase},
|
||||
util::utf8,
|
||||
};
|
||||
|
|
@ -79,16 +79,12 @@ fn windows_to_iana(tz_key_name: &str) -> Result<&'static str, Error> {
|
|||
utf8::cmp_ignore_ascii_case(win_name, &tz_key_name)
|
||||
});
|
||||
let Ok(index) = result else {
|
||||
return Err(err!(
|
||||
"found Windows time zone name {tz_key_name}, \
|
||||
but could not find a mapping for it to an \
|
||||
IANA time zone name",
|
||||
));
|
||||
return Err(Error::from(E::WindowsMissingIanaMapping));
|
||||
};
|
||||
let iana_name = WINDOWS_TO_IANA[index].1;
|
||||
trace!(
|
||||
"found Windows time zone name {tz_key_name}, and \
|
||||
successfully mapped it to IANA time zone {iana_name}",
|
||||
"found Windows time zone name `{tz_key_name}`, and \
|
||||
successfully mapped it to IANA time zone `{iana_name}`",
|
||||
);
|
||||
Ok(iana_name)
|
||||
}
|
||||
|
|
@ -107,24 +103,19 @@ fn get_tz_key_name() -> Result<String, Error> {
|
|||
// when `info` is properly initialized.
|
||||
let info = unsafe { info.assume_init() };
|
||||
let tz_key_name = nul_terminated_utf16_to_string(&info.TimeZoneKeyName)
|
||||
.context(
|
||||
"could not get TimeZoneKeyName from \
|
||||
winapi DYNAMIC_TIME_ZONE_INFORMATION",
|
||||
)?;
|
||||
.context(E::WindowsTimeZoneKeyName)?;
|
||||
Ok(tz_key_name)
|
||||
}
|
||||
|
||||
fn nul_terminated_utf16_to_string(
|
||||
code_units: &[u16],
|
||||
) -> Result<String, Error> {
|
||||
let nul = code_units.iter().position(|&cu| cu == 0).ok_or_else(|| {
|
||||
err!("failed to convert u16 slice to UTF-8 (no NUL terminator found)")
|
||||
})?;
|
||||
let nul = code_units
|
||||
.iter()
|
||||
.position(|&cu| cu == 0)
|
||||
.ok_or(E::WindowsUtf16DecodeNul)?;
|
||||
let string = String::from_utf16(&code_units[..nul])
|
||||
.map_err(Error::adhoc)
|
||||
.with_context(|| {
|
||||
err!("failed to convert u16 slice to UTF-8 (invalid UTF-16)")
|
||||
})?;
|
||||
.map_err(|_| E::WindowsUtf16DecodeInvalid)?;
|
||||
Ok(string)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
civil::DateTime,
|
||||
error::{err, Error},
|
||||
error::{tz::timezone::Error as E, Error},
|
||||
tz::{
|
||||
ambiguous::{AmbiguousOffset, AmbiguousTimestamp, AmbiguousZoned},
|
||||
offset::{Dst, Offset},
|
||||
|
|
@ -392,10 +392,7 @@ impl TimeZone {
|
|||
pub fn try_system() -> Result<TimeZone, Error> {
|
||||
#[cfg(not(feature = "tz-system"))]
|
||||
{
|
||||
Err(err!(
|
||||
"failed to get system time zone since 'tz-system' \
|
||||
crate feature is not enabled",
|
||||
))
|
||||
Err(Error::from(E::FailedSystem))
|
||||
}
|
||||
#[cfg(feature = "tz-system")]
|
||||
{
|
||||
|
|
@ -916,7 +913,7 @@ impl TimeZone {
|
|||
/// assert_eq!(
|
||||
/// tz.to_fixed_offset().unwrap_err().to_string(),
|
||||
/// "cannot convert non-fixed IANA time zone \
|
||||
/// to offset without timestamp or civil datetime",
|
||||
/// to offset without a timestamp or civil datetime",
|
||||
/// );
|
||||
///
|
||||
/// let tz = TimeZone::UTC;
|
||||
|
|
@ -935,11 +932,7 @@ impl TimeZone {
|
|||
#[inline]
|
||||
pub fn to_fixed_offset(&self) -> Result<Offset, Error> {
|
||||
let mkerr = || {
|
||||
err!(
|
||||
"cannot convert non-fixed {kind} time zone to offset \
|
||||
without timestamp or civil datetime",
|
||||
kind = self.kind_description(),
|
||||
)
|
||||
Error::from(E::ConvertNonFixed { kind: self.kind_description() })
|
||||
};
|
||||
repr::each! {
|
||||
&self.repr,
|
||||
|
|
@ -1392,7 +1385,7 @@ impl TimeZone {
|
|||
/// Returns a short description about the kind of this time zone.
|
||||
///
|
||||
/// This is useful in error messages.
|
||||
fn kind_description(&self) -> &str {
|
||||
fn kind_description(&self) -> &'static str {
|
||||
repr::each! {
|
||||
&self.repr,
|
||||
UTC => "UTC",
|
||||
|
|
@ -1887,12 +1880,12 @@ impl<'a> core::fmt::Display for DiagnosticName<'a> {
|
|||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
repr::each! {
|
||||
&self.0.repr,
|
||||
UTC => write!(f, "UTC"),
|
||||
UNKNOWN => write!(f, "Etc/Unknown"),
|
||||
FIXED(offset) => write!(f, "{offset}"),
|
||||
STATIC_TZIF(tzif) => write!(f, "{}", tzif.name().unwrap_or("Local")),
|
||||
ARC_TZIF(tzif) => write!(f, "{}", tzif.name().unwrap_or("Local")),
|
||||
ARC_POSIX(posix) => write!(f, "{posix}"),
|
||||
UTC => f.write_str("UTC"),
|
||||
UNKNOWN => f.write_str("Etc/Unknown"),
|
||||
FIXED(offset) => offset.fmt(f),
|
||||
STATIC_TZIF(tzif) => f.write_str(tzif.name().unwrap_or("Local")),
|
||||
ARC_TZIF(tzif) => f.write_str(tzif.name().unwrap_or("Local")),
|
||||
ARC_POSIX(posix) => posix.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
320
src/tz/zic.rs
320
src/tz/zic.rs
|
|
@ -111,7 +111,10 @@ use alloc::{
|
|||
|
||||
use crate::{
|
||||
civil::{Date, DateTime, Time, Weekday},
|
||||
error::{err, Error, ErrorContext},
|
||||
error::{
|
||||
tz::zic::{Error as E, MAX_LINE_LEN},
|
||||
Error, ErrorContext,
|
||||
},
|
||||
span::{Span, SpanFieldwise, ToSpan},
|
||||
timestamp::Timestamp,
|
||||
tz::{Dst, Offset},
|
||||
|
|
@ -205,12 +208,12 @@ impl Rules {
|
|||
"every name in rule group must be identical"
|
||||
);
|
||||
let dst = Dst::from(r.save.suffix() == RuleSaveSuffixP::Dst);
|
||||
let offset = r.save.to_offset().map_err(|e| {
|
||||
err!("SAVE value in rule {:?} is too big: {e}", inner.name)
|
||||
let offset = r.save.to_offset().with_context(|| {
|
||||
E::FailedRule { name: inner.name.as_str().into() }
|
||||
})?;
|
||||
let years = r.years().with_context(|| E::FailedRule {
|
||||
name: inner.name.as_str().into(),
|
||||
})?;
|
||||
let years = r
|
||||
.years()
|
||||
.map_err(|e| e.context(err!("rule {:?}", inner.name)))?;
|
||||
let month = r.inn.month;
|
||||
let letters = r.letters.part;
|
||||
let day = r.on;
|
||||
|
|
@ -260,13 +263,12 @@ impl ZicP {
|
|||
) -> Result<(), Error> {
|
||||
while parser.read_next_fields()? {
|
||||
self.parse_one(&mut parser)
|
||||
.map_err(|e| e.context(err!("line {}", parser.line_number)))?;
|
||||
.context(E::Line { number: parser.line_number })?;
|
||||
}
|
||||
if let Some(ref name) = parser.continuation_zone_for {
|
||||
return Err(err!(
|
||||
"expected continuation zone line for {name:?}, \
|
||||
but found end of data instead",
|
||||
));
|
||||
return Err(Error::from(E::ExpectedContinuationZoneLine {
|
||||
name: name.as_str().into(),
|
||||
}));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -277,9 +279,8 @@ impl ZicP {
|
|||
assert!(!p.fields.is_empty());
|
||||
|
||||
if let Some(name) = p.continuation_zone_for.take() {
|
||||
let zone = ZoneContinuationP::parse(&p.fields).map_err(|e| {
|
||||
e.context("failed to parse continuation 'Zone' line")
|
||||
})?;
|
||||
let zone = ZoneContinuationP::parse(&p.fields)
|
||||
.context(E::FailedContinuationZone)?;
|
||||
let more_continuations = zone.until.is_some();
|
||||
// OK because `p.continuation_zone_for` is only set when we have
|
||||
// seen a first zone with the corresponding name.
|
||||
|
|
@ -293,51 +294,45 @@ impl ZicP {
|
|||
|
||||
let (first, rest) = (&p.fields[0], &p.fields[1..]);
|
||||
if first.starts_with("R") && "Rule".starts_with(first) {
|
||||
let rule = RuleP::parse(rest)
|
||||
.map_err(|e| e.context("failed to parse 'Rule' line"))?;
|
||||
let rule = RuleP::parse(rest).context(E::FailedRuleLine)?;
|
||||
let name = rule.name.name.clone();
|
||||
self.rules.entry(name).or_default().push(rule);
|
||||
} else if first.starts_with("Z") && "Zone".starts_with(first) {
|
||||
let first = ZoneFirstP::parse(rest)
|
||||
.map_err(|e| e.context("failed to parse first 'Zone' line"))?;
|
||||
let first = ZoneFirstP::parse(rest).context(E::FailedZoneFirst)?;
|
||||
let name = first.name.name.clone();
|
||||
if first.until.is_some() {
|
||||
p.continuation_zone_for = Some(name.clone());
|
||||
}
|
||||
let zone = ZoneP { first, continuations: vec![] };
|
||||
if self.links.contains_key(&name) {
|
||||
return Err(err!(
|
||||
"found zone with name {name:?} that conflicts \
|
||||
with a link of the same name",
|
||||
));
|
||||
return Err(Error::from(E::DuplicateZoneLink {
|
||||
name: name.into(),
|
||||
}));
|
||||
}
|
||||
if let Some(previous_zone) = self.zones.insert(name, zone) {
|
||||
return Err(err!(
|
||||
"found duplicate zone for {:?}",
|
||||
previous_zone.first.name.name,
|
||||
));
|
||||
return Err(Error::from(E::DuplicateZone {
|
||||
name: previous_zone.first.name.name.into(),
|
||||
}));
|
||||
}
|
||||
} else if first.starts_with("L") && "Link".starts_with(first) {
|
||||
let link = LinkP::parse(rest)
|
||||
.map_err(|e| e.context("failed to parse 'Link' line"))?;
|
||||
let link = LinkP::parse(rest).context(E::FailedLinkLine)?;
|
||||
let name = link.name.name.clone();
|
||||
if self.zones.contains_key(&name) {
|
||||
return Err(err!(
|
||||
"found link with name {name:?} that conflicts \
|
||||
with a zone of the same name",
|
||||
));
|
||||
return Err(Error::from(E::DuplicateLinkZone {
|
||||
name: name.into(),
|
||||
}));
|
||||
}
|
||||
if let Some(previous_link) = self.links.insert(name, link) {
|
||||
return Err(err!(
|
||||
"found duplicate link for {:?}",
|
||||
previous_link.name.name,
|
||||
));
|
||||
return Err(Error::from(E::DuplicateLink {
|
||||
name: previous_link.name.name.into(),
|
||||
}));
|
||||
}
|
||||
// N.B. We don't check that the link's target name refers to some
|
||||
// other zone/link here, because the corresponding zone/link might
|
||||
// be defined later.
|
||||
} else {
|
||||
return Err(err!("unrecognized zic line: {first:?}"));
|
||||
return Err(Error::from(E::UnrecognizedZicLine)
|
||||
.context(E::Line { number: p.line_number }));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -369,10 +364,9 @@ struct RuleP {
|
|||
impl RuleP {
|
||||
fn parse(fields: &[&str]) -> Result<RuleP, Error> {
|
||||
if fields.len() != 9 {
|
||||
return Err(err!(
|
||||
"expected exactly 9 fields for rule, but found {} fields",
|
||||
fields.len(),
|
||||
));
|
||||
return Err(Error::from(E::ExpectedRuleNineFields {
|
||||
got: fields.len(),
|
||||
}));
|
||||
}
|
||||
let (name_field, fields) = (fields[0], &fields[1..]);
|
||||
let (from_field, fields) = (fields[0], &fields[1..]);
|
||||
|
|
@ -386,28 +380,21 @@ impl RuleP {
|
|||
|
||||
let name = name_field
|
||||
.parse::<RuleNameP>()
|
||||
.map_err(|e| e.context("failed to parse NAME field"))?;
|
||||
.context(E::FailedParseFieldName)?;
|
||||
let from = from_field
|
||||
.parse::<RuleFromP>()
|
||||
.map_err(|e| e.context("failed to parse FROM field"))?;
|
||||
let to = to_field
|
||||
.parse::<RuleToP>()
|
||||
.map_err(|e| e.context("failed to parse TO field"))?;
|
||||
let inn = in_field
|
||||
.parse::<RuleInP>()
|
||||
.map_err(|e| e.context("failed to parse IN field"))?;
|
||||
let on = on_field
|
||||
.parse::<RuleOnP>()
|
||||
.map_err(|e| e.context("failed to parse ON field"))?;
|
||||
let at = at_field
|
||||
.parse::<RuleAtP>()
|
||||
.map_err(|e| e.context("failed to parse AT field"))?;
|
||||
.context(E::FailedParseFieldFrom)?;
|
||||
let to = to_field.parse::<RuleToP>().context(E::FailedParseFieldTo)?;
|
||||
let inn =
|
||||
in_field.parse::<RuleInP>().context(E::FailedParseFieldIn)?;
|
||||
let on = on_field.parse::<RuleOnP>().context(E::FailedParseFieldOn)?;
|
||||
let at = at_field.parse::<RuleAtP>().context(E::FailedParseFieldAt)?;
|
||||
let save = save_field
|
||||
.parse::<RuleSaveP>()
|
||||
.map_err(|e| e.context("failed to parse SAVE field"))?;
|
||||
.context(E::FailedParseFieldSave)?;
|
||||
let letters = letters_field
|
||||
.parse::<RuleLettersP>()
|
||||
.map_err(|e| e.context("failed to parse LETTERS field"))?;
|
||||
.context(E::FailedParseFieldLetters)?;
|
||||
|
||||
Ok(RuleP { name, from, to, inn, on, at, save, letters })
|
||||
}
|
||||
|
|
@ -420,9 +407,10 @@ impl RuleP {
|
|||
RuleToP::Year { year } => year,
|
||||
};
|
||||
if start > end {
|
||||
return Err(err!(
|
||||
"found start year {start} to be greater than end year {end}"
|
||||
));
|
||||
return Err(Error::from(E::InvalidRuleYear {
|
||||
start: start.get(),
|
||||
end: end.get(),
|
||||
}));
|
||||
}
|
||||
Ok(start..=end)
|
||||
}
|
||||
|
|
@ -462,7 +450,7 @@ struct ZoneFirstP {
|
|||
impl ZoneFirstP {
|
||||
fn parse(fields: &[&str]) -> Result<ZoneFirstP, Error> {
|
||||
if fields.len() < 4 {
|
||||
return Err(err!("first ZONE line must have at least 4 fields"));
|
||||
return Err(Error::from(E::ExpectedFirstZoneFourFields));
|
||||
}
|
||||
let (name_field, fields) = (fields[0], &fields[1..]);
|
||||
let (stdoff_field, fields) = (fields[0], &fields[1..]);
|
||||
|
|
@ -470,23 +458,20 @@ impl ZoneFirstP {
|
|||
let (format_field, fields) = (fields[0], &fields[1..]);
|
||||
let name = name_field
|
||||
.parse::<ZoneNameP>()
|
||||
.map_err(|e| e.context("failed to parse NAME field"))?;
|
||||
.context(E::FailedParseFieldName)?;
|
||||
let stdoff = stdoff_field
|
||||
.parse::<ZoneStdoffP>()
|
||||
.map_err(|e| e.context("failed to parse STDOFF field"))?;
|
||||
.context(E::FailedParseFieldStdOff)?;
|
||||
let rules = rules_field
|
||||
.parse::<ZoneRulesP>()
|
||||
.map_err(|e| e.context("failed to parse RULES field"))?;
|
||||
.context(E::FailedParseFieldRules)?;
|
||||
let format = format_field
|
||||
.parse::<ZoneFormatP>()
|
||||
.map_err(|e| e.context("failed to parse FORMAT field"))?;
|
||||
.context(E::FailedParseFieldFormat)?;
|
||||
let until = if fields.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
ZoneUntilP::parse(fields)
|
||||
.map_err(|e| e.context("failed to parse UNTIL field"))?,
|
||||
)
|
||||
Some(ZoneUntilP::parse(fields).context(E::FailedParseFieldUntil)?)
|
||||
};
|
||||
Ok(ZoneFirstP { name, stdoff, rules, format, until })
|
||||
}
|
||||
|
|
@ -512,29 +497,24 @@ struct ZoneContinuationP {
|
|||
impl ZoneContinuationP {
|
||||
fn parse(fields: &[&str]) -> Result<ZoneContinuationP, Error> {
|
||||
if fields.len() < 3 {
|
||||
return Err(err!(
|
||||
"continuation ZONE line must have at least 3 fields"
|
||||
));
|
||||
return Err(Error::from(E::ExpectedContinuationZoneThreeFields));
|
||||
}
|
||||
let (stdoff_field, fields) = (fields[0], &fields[1..]);
|
||||
let (rules_field, fields) = (fields[0], &fields[1..]);
|
||||
let (format_field, fields) = (fields[0], &fields[1..]);
|
||||
let stdoff = stdoff_field
|
||||
.parse::<ZoneStdoffP>()
|
||||
.map_err(|e| e.context("failed to parse STDOFF field"))?;
|
||||
.context(E::FailedParseFieldStdOff)?;
|
||||
let rules = rules_field
|
||||
.parse::<ZoneRulesP>()
|
||||
.map_err(|e| e.context("failed to parse RULES field"))?;
|
||||
.context(E::FailedParseFieldRules)?;
|
||||
let format = format_field
|
||||
.parse::<ZoneFormatP>()
|
||||
.map_err(|e| e.context("failed to parse FORMAT field"))?;
|
||||
.context(E::FailedParseFieldFormat)?;
|
||||
let until = if fields.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
ZoneUntilP::parse(fields)
|
||||
.map_err(|e| e.context("failed to parse UNTIL field"))?,
|
||||
)
|
||||
Some(ZoneUntilP::parse(fields).context(E::FailedParseFieldUntil)?)
|
||||
};
|
||||
Ok(ZoneContinuationP { stdoff, rules, format, until })
|
||||
}
|
||||
|
|
@ -553,17 +533,14 @@ struct LinkP {
|
|||
impl LinkP {
|
||||
fn parse(fields: &[&str]) -> Result<LinkP, Error> {
|
||||
if fields.len() != 2 {
|
||||
return Err(err!(
|
||||
"expected exactly 2 fields after LINK, but found {}",
|
||||
fields.len()
|
||||
));
|
||||
return Err(Error::from(E::ExpectedLinkTwoFields));
|
||||
}
|
||||
let target = fields[0]
|
||||
.parse::<ZoneNameP>()
|
||||
.map_err(|e| e.context("failed to parse LINK target"))?;
|
||||
.context(E::FailedParseFieldLinkTarget)?;
|
||||
let name = fields[1]
|
||||
.parse::<ZoneNameP>()
|
||||
.map_err(|e| e.context("failed to parse LINK name"))?;
|
||||
.context(E::FailedParseFieldLinkName)?;
|
||||
Ok(LinkP { target, name })
|
||||
}
|
||||
}
|
||||
|
|
@ -592,12 +569,9 @@ impl FromStr for RuleNameP {
|
|||
// or not. We erase that information. We could rejigger things to keep
|
||||
// that information around, but... Meh.
|
||||
if name.is_empty() {
|
||||
Err(err!("NAME field for rule cannot be empty"))
|
||||
Err(Error::from(E::ExpectedNonEmptyName))
|
||||
} else if name.starts_with(|ch| matches!(ch, '0'..='9' | '+' | '-')) {
|
||||
Err(err!(
|
||||
"NAME field cannot begin with a digit, + or -, \
|
||||
but {name:?} begins with one of those",
|
||||
))
|
||||
Err(Error::from(E::ExpectedNameBegin))
|
||||
} else {
|
||||
Ok(RuleNameP { name: name.to_string() })
|
||||
}
|
||||
|
|
@ -614,8 +588,7 @@ impl FromStr for RuleFromP {
|
|||
type Err = Error;
|
||||
|
||||
fn from_str(from: &str) -> Result<RuleFromP, Error> {
|
||||
let year = parse_year(from)
|
||||
.map_err(|e| e.context("failed to parse FROM field"))?;
|
||||
let year = parse_year(from).context(E::FailedParseFieldFrom)?;
|
||||
Ok(RuleFromP { year })
|
||||
}
|
||||
}
|
||||
|
|
@ -642,8 +615,7 @@ impl FromStr for RuleToP {
|
|||
} else if to.starts_with("o") && "only".starts_with(to) {
|
||||
Ok(RuleToP::Only)
|
||||
} else {
|
||||
let year = parse_year(to)
|
||||
.map_err(|e| e.context("failed to parse TO field"))?;
|
||||
let year = parse_year(to).context(E::FailedParseFieldTo)?;
|
||||
Ok(RuleToP::Year { year })
|
||||
}
|
||||
}
|
||||
|
|
@ -679,7 +651,7 @@ impl FromStr for RuleInP {
|
|||
return Ok(RuleInP { month });
|
||||
}
|
||||
}
|
||||
Err(err!("unrecognized month name: {field:?}"))
|
||||
Err(Error::from(E::UnrecognizedMonthName))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -747,7 +719,7 @@ impl FromStr for RuleOnP {
|
|||
// field. That gets checked at a higher level.
|
||||
Ok(RuleOnP::Day { day })
|
||||
} else {
|
||||
Err(err!("unrecognized format for day-of-month: {field:?}"))
|
||||
Err(Error::from(E::UnrecognizedDayOfMonthFormat))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -782,7 +754,7 @@ impl FromStr for RuleAtP {
|
|||
|
||||
fn from_str(at: &str) -> Result<RuleAtP, Error> {
|
||||
if at.is_empty() {
|
||||
return Err(err!("empty field is not a valid AT value"));
|
||||
return Err(Error::from(E::ExpectedNonEmptyAt));
|
||||
}
|
||||
let (span_string, suffix_string) = at.split_at(at.len() - 1);
|
||||
if suffix_string.chars().all(|ch| ch.is_ascii_alphabetic()) {
|
||||
|
|
@ -813,7 +785,7 @@ impl FromStr for RuleAtSuffixP {
|
|||
"w" => Ok(RuleAtSuffixP::Wall),
|
||||
"s" => Ok(RuleAtSuffixP::Standard),
|
||||
"u" | "g" | "z" => Ok(RuleAtSuffixP::Universal),
|
||||
_ => Err(err!("unrecognized AT time suffix {suffix:?}")),
|
||||
_ => Err(Error::from(E::UnrecognizedAtTimeSuffix)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -867,7 +839,7 @@ impl FromStr for RuleSaveP {
|
|||
|
||||
fn from_str(at: &str) -> Result<RuleSaveP, Error> {
|
||||
if at.is_empty() {
|
||||
return Err(err!("empty field is not a valid SAVE value"));
|
||||
return Err(Error::from(E::ExpectedNonEmptySave));
|
||||
}
|
||||
let (span_string, suffix_string) = at.split_at(at.len() - 1);
|
||||
if suffix_string.chars().all(|ch| ch.is_ascii_alphabetic()) {
|
||||
|
|
@ -902,7 +874,7 @@ impl FromStr for RuleSaveSuffixP {
|
|||
match suffix {
|
||||
"s" => Ok(RuleSaveSuffixP::Standard),
|
||||
"d" => Ok(RuleSaveSuffixP::Dst),
|
||||
_ => Err(err!("unrecognized SAVE time suffix {suffix:?}")),
|
||||
_ => Err(Error::from(E::UnrecognizedSaveTimeSuffix)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -939,14 +911,13 @@ impl FromStr for ZoneNameP {
|
|||
|
||||
fn from_str(name: &str) -> Result<ZoneNameP, Error> {
|
||||
if name.is_empty() {
|
||||
return Err(err!("zone names cannot be empty"));
|
||||
return Err(Error::from(E::ExpectedNonEmptyZoneName));
|
||||
}
|
||||
for component in name.split('/') {
|
||||
if component == "." || component == ".." {
|
||||
return Err(err!(
|
||||
"component {component:?} in zone name {name:?} cannot \
|
||||
be \".\" or \"..\"",
|
||||
));
|
||||
return Err(Error::from(E::ExpectedZoneNameComponentNoDots {
|
||||
component: component.into(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
Ok(ZoneNameP { name: name.to_string() })
|
||||
|
|
@ -1035,16 +1006,12 @@ impl FromStr for ZoneFormatP {
|
|||
fn from_str(format: &str) -> Result<ZoneFormatP, Error> {
|
||||
fn check_abbrev(abbrev: &str) -> Result<String, Error> {
|
||||
if abbrev.is_empty() {
|
||||
return Err(err!("empty abbreviations are not allowed"));
|
||||
return Err(Error::from(E::ExpectedNonEmptyAbbreviation));
|
||||
}
|
||||
let is_ok =
|
||||
|ch| matches!(ch, '+'|'-'|'0'..='9'|'A'..='Z'|'a'..='z');
|
||||
if !abbrev.chars().all(is_ok) {
|
||||
return Err(err!(
|
||||
"abbreviation {abbrev:?} \
|
||||
contains invalid character; only \"+\", \"-\" and \
|
||||
ASCII alpha-numeric characters are allowed"
|
||||
));
|
||||
return Err(Error::from(E::InvalidAbbreviation));
|
||||
}
|
||||
Ok(abbrev.to_string())
|
||||
}
|
||||
|
|
@ -1098,28 +1065,24 @@ enum ZoneUntilP {
|
|||
impl ZoneUntilP {
|
||||
fn parse(fields: &[&str]) -> Result<ZoneUntilP, Error> {
|
||||
if fields.is_empty() {
|
||||
return Err(err!("expected at least a year"));
|
||||
return Err(Error::from(E::ExpectedUntilYear));
|
||||
}
|
||||
|
||||
let (year_field, fields) = (fields[0], &fields[1..]);
|
||||
let year = parse_year(year_field)
|
||||
.map_err(|e| e.context("failed to parse year"))?;
|
||||
let year = parse_year(year_field).context(E::FailedParseYear)?;
|
||||
if fields.is_empty() {
|
||||
return Ok(ZoneUntilP::Year { year });
|
||||
}
|
||||
|
||||
let (month_field, fields) = (fields[0], &fields[1..]);
|
||||
let month = month_field
|
||||
.parse::<RuleInP>()
|
||||
.map_err(|e| e.context("failed to parse month"))?;
|
||||
let month =
|
||||
month_field.parse::<RuleInP>().context(E::FailedParseMonth)?;
|
||||
if fields.is_empty() {
|
||||
return Ok(ZoneUntilP::YearMonth { year, month });
|
||||
}
|
||||
|
||||
let (day_field, fields) = (fields[0], &fields[1..]);
|
||||
let day = day_field
|
||||
.parse::<RuleOnP>()
|
||||
.map_err(|e| e.context("failed to parse day"))?;
|
||||
let day = day_field.parse::<RuleOnP>().context(E::FailedParseDay)?;
|
||||
if fields.is_empty() {
|
||||
return Ok(ZoneUntilP::YearMonthDay { year, month, day });
|
||||
}
|
||||
|
|
@ -1127,13 +1090,9 @@ impl ZoneUntilP {
|
|||
let (duration_field, fields) = (fields[0], &fields[1..]);
|
||||
let duration = duration_field
|
||||
.parse::<RuleAtP>()
|
||||
.map_err(|e| e.context("failed to parse time duration"))?;
|
||||
.context(E::FailedParseTimeDuration)?;
|
||||
if !fields.is_empty() {
|
||||
return Err(err!(
|
||||
"expected no more fields after time of day, \
|
||||
but found: {fields:?}",
|
||||
fields = fields.join(" "),
|
||||
));
|
||||
return Err(Error::from(E::ExpectedNothingAfterTime));
|
||||
}
|
||||
Ok(ZoneUntilP::YearMonthDayTime { year, month, day, duration })
|
||||
}
|
||||
|
|
@ -1200,10 +1159,8 @@ fn parse_year(year: &str) -> Result<t::Year, Error> {
|
|||
} else {
|
||||
(t::Sign::N::<1>(), year)
|
||||
};
|
||||
let number = parse::i64(rest.as_bytes())
|
||||
.map_err(|e| e.context("failed to parse year"))?;
|
||||
let year = t::Year::new(number)
|
||||
.ok_or_else(|| err!("year is out of range: {number}"))?;
|
||||
let number = parse::i64(rest.as_bytes()).context(E::FailedParseYear)?;
|
||||
let year = t::Year::try_new("year", number)?;
|
||||
Ok(year * sign)
|
||||
}
|
||||
|
||||
|
|
@ -1239,36 +1196,28 @@ fn parse_span(span: &str) -> Result<Span, Error> {
|
|||
let hour_len = rest.chars().take_while(|c| c.is_ascii_digit()).count();
|
||||
let (hour_digits, rest) = rest.split_at(hour_len);
|
||||
if hour_digits.is_empty() {
|
||||
return Err(err!(
|
||||
"expected time duration to contain at least one hour digit"
|
||||
));
|
||||
return Err(Error::from(E::ExpectedTimeOneHour));
|
||||
}
|
||||
let hours = parse::i64(hour_digits.as_bytes())
|
||||
.map_err(|e| e.context("failed to parse hours in time duration"))?;
|
||||
span = span
|
||||
.try_hours(hours.saturating_mul(i64::from(sign.get())))
|
||||
.map_err(|_| err!("duration hours '{hours:?}' is out of range"))?;
|
||||
let hours =
|
||||
parse::i64(hour_digits.as_bytes()).context(E::FailedParseHour)?;
|
||||
span = span.try_hours(hours.saturating_mul(i64::from(sign.get())))?;
|
||||
if rest.is_empty() {
|
||||
return Ok(span);
|
||||
}
|
||||
|
||||
// Now pluck out the minute component.
|
||||
if !rest.starts_with(":") {
|
||||
return Err(err!("expected ':' after hours, but found {rest:?}"));
|
||||
return Err(Error::from(E::ExpectedColonAfterHour));
|
||||
}
|
||||
let rest = &rest[1..];
|
||||
let minute_len = rest.chars().take_while(|c| c.is_ascii_digit()).count();
|
||||
let (minute_digits, rest) = rest.split_at(minute_len);
|
||||
if minute_digits.is_empty() {
|
||||
return Err(err!(
|
||||
"expected minute digits after 'HH:', but found {rest:?} instead"
|
||||
));
|
||||
return Err(Error::from(E::ExpectedMinuteAfterHours));
|
||||
}
|
||||
let minutes = parse::i64(minute_digits.as_bytes())
|
||||
.map_err(|e| e.context("failed to parse minutes in time duration"))?;
|
||||
let minutes_ranged = t::Minute::new(minutes).ok_or_else(|| {
|
||||
err!("duration minutes '{minutes:?}' is out of range")
|
||||
})?;
|
||||
let minutes =
|
||||
parse::i64(minute_digits.as_bytes()).context(E::FailedParseMinute)?;
|
||||
let minutes_ranged = t::Minute::try_new("minutes", minutes)?;
|
||||
span = span.minutes_ranged((minutes_ranged * sign).rinto());
|
||||
if rest.is_empty() {
|
||||
return Ok(span);
|
||||
|
|
@ -1276,21 +1225,17 @@ fn parse_span(span: &str) -> Result<Span, Error> {
|
|||
|
||||
// Now pluck out the second component.
|
||||
if !rest.starts_with(":") {
|
||||
return Err(err!("expected ':' after minutes, but found {rest:?}"));
|
||||
return Err(Error::from(E::ExpectedColonAfterMinute));
|
||||
}
|
||||
let rest = &rest[1..];
|
||||
let second_len = rest.chars().take_while(|c| c.is_ascii_digit()).count();
|
||||
let (second_digits, rest) = rest.split_at(second_len);
|
||||
if second_digits.is_empty() {
|
||||
return Err(err!(
|
||||
"expected second digits after 'MM:', but found {rest:?} instead"
|
||||
));
|
||||
return Err(Error::from(E::ExpectedSecondAfterMinutes));
|
||||
}
|
||||
let seconds = parse::i64(second_digits.as_bytes())
|
||||
.map_err(|e| e.context("failed to parse seconds in time duration"))?;
|
||||
let seconds_ranged = t::Second::new(seconds).ok_or_else(|| {
|
||||
err!("duration seconds '{seconds:?}' is out of range")
|
||||
})?;
|
||||
let seconds =
|
||||
parse::i64(second_digits.as_bytes()).context(E::FailedParseSecond)?;
|
||||
let seconds_ranged = t::Second::try_new("seconds", seconds)?;
|
||||
span = span.seconds_ranged((seconds_ranged * sign).rinto());
|
||||
if rest.is_empty() {
|
||||
return Ok(span);
|
||||
|
|
@ -1298,33 +1243,24 @@ fn parse_span(span: &str) -> Result<Span, Error> {
|
|||
|
||||
// Now look for the fractional nanosecond component.
|
||||
if !rest.starts_with(".") {
|
||||
return Err(err!("expected '.' after seconds, but found {rest:?}"));
|
||||
return Err(Error::from(E::ExpectedDotAfterSeconds));
|
||||
}
|
||||
let rest = &rest[1..];
|
||||
let nanosecond_len =
|
||||
rest.chars().take_while(|c| c.is_ascii_digit()).count();
|
||||
let (nanosecond_digits, rest) = rest.split_at(nanosecond_len);
|
||||
if nanosecond_digits.is_empty() {
|
||||
return Err(err!(
|
||||
"expected nanosecond digits after 'SS.', \
|
||||
but found {rest:?} instead"
|
||||
));
|
||||
return Err(Error::from(E::ExpectedNanosecondDigits));
|
||||
}
|
||||
let nanoseconds =
|
||||
parse::fraction(nanosecond_digits.as_bytes()).map_err(|e| {
|
||||
e.context("failed to parse nanoseconds in time duration")
|
||||
})?;
|
||||
let nanoseconds_ranged = t::FractionalNanosecond::new(nanoseconds)
|
||||
.ok_or_else(|| {
|
||||
err!("duration nanoseconds '{nanoseconds:?}' is out of range")
|
||||
})?;
|
||||
let nanoseconds = parse::fraction(nanosecond_digits.as_bytes())
|
||||
.context(E::FailedParseNanosecond)?;
|
||||
let nanoseconds_ranged =
|
||||
t::FractionalNanosecond::try_new("nanoseconds", nanoseconds)?;
|
||||
span = span.nanoseconds_ranged((nanoseconds_ranged * sign).rinto());
|
||||
|
||||
// We should have consumed everything at this point.
|
||||
if !rest.is_empty() {
|
||||
return Err(err!(
|
||||
"found unrecognized trailing {rest:?} in time duration"
|
||||
));
|
||||
return Err(Error::from(E::UnrecognizedTrailingTimeDuration));
|
||||
}
|
||||
span.rebalance(Unit::Hour)
|
||||
}
|
||||
|
|
@ -1334,10 +1270,8 @@ fn parse_span(span: &str) -> Result<Span, Error> {
|
|||
/// This checks that the day is in the range 1-31, but otherwise doesn't
|
||||
/// check that it is valid for a particular month.
|
||||
fn parse_day(string: &str) -> Result<t::Day, Error> {
|
||||
let number = parse::i64(string.as_bytes())
|
||||
.map_err(|e| e.context("failed to parse number for day"))?;
|
||||
let day = t::Day::new(number)
|
||||
.ok_or_else(|| err!("{number} is not a valid day"))?;
|
||||
let number = parse::i64(string.as_bytes()).context(E::FailedParseDay)?;
|
||||
let day = t::Day::try_new("day", number)?;
|
||||
Ok(day)
|
||||
}
|
||||
|
||||
|
|
@ -1357,7 +1291,7 @@ fn parse_weekday(string: &str) -> Result<Weekday, Error> {
|
|||
return Ok(weekday);
|
||||
}
|
||||
}
|
||||
Err(err!("unrecognized day of the week: {string:?}"))
|
||||
Err(Error::from(E::UnrecognizedDayOfWeek))
|
||||
}
|
||||
|
||||
/// A parser that emits lines as sequences of fields.
|
||||
|
|
@ -1393,7 +1327,7 @@ impl<'a> FieldParser<'a> {
|
|||
/// This returns an error if the given bytes are not valid UTF-8.
|
||||
fn from_bytes(src: &'a [u8]) -> Result<FieldParser, Error> {
|
||||
let src = core::str::from_utf8(src)
|
||||
.map_err(|e| err!("invalid UTF-8: {e}"))?;
|
||||
.map_err(|_| Error::from(E::InvalidUtf8))?;
|
||||
Ok(FieldParser::new(src))
|
||||
}
|
||||
|
||||
|
|
@ -1409,12 +1343,10 @@ impl<'a> FieldParser<'a> {
|
|||
self.fields.clear();
|
||||
loop {
|
||||
let Some(mut line) = self.lines.next() else { return Ok(false) };
|
||||
self.line_number = self
|
||||
.line_number
|
||||
.checked_add(1)
|
||||
.ok_or_else(|| err!("line count overflowed"))?;
|
||||
self.line_number =
|
||||
self.line_number.checked_add(1).ok_or(E::LineOverflow)?;
|
||||
parse_fields(&line, &mut self.fields)
|
||||
.with_context(|| err!("line {}", self.line_number))?;
|
||||
.context(E::Line { number: self.line_number })?;
|
||||
if self.fields.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -1452,13 +1384,6 @@ fn parse_fields<'a>(
|
|||
matches!(ch, ' ' | '\x0C' | '\n' | '\r' | '\t' | '\x0B')
|
||||
}
|
||||
|
||||
// `man zic` says that the max line length including the line
|
||||
// terminator is 2048. The `core::str::Lines` iterator doesn't include
|
||||
// the terminator, so we subtract 1 to account for that. Note that this
|
||||
// could potentially allow one extra byte in the case of a \r\n line
|
||||
// terminator, but this seems fine.
|
||||
const MAX_LINE_LEN: usize = 2047;
|
||||
|
||||
// The different possible states of the field parser below.
|
||||
enum State {
|
||||
Whitespace,
|
||||
|
|
@ -1469,15 +1394,11 @@ fn parse_fields<'a>(
|
|||
|
||||
fields.clear();
|
||||
if line.len() > MAX_LINE_LEN {
|
||||
return Err(err!(
|
||||
"line with length {} exceeds \
|
||||
max length of {MAX_LINE_LEN}",
|
||||
line.len()
|
||||
));
|
||||
return Err(Error::from(E::LineMaxLength));
|
||||
}
|
||||
// Do a quick scan for a NUL terminator. They are illegal in all cases.
|
||||
if line.contains('\x00') {
|
||||
return Err(err!("found line with NUL byte, which isn't allowed"));
|
||||
return Err(Error::from(E::LineNul));
|
||||
}
|
||||
// The current state of the parser. We start at whitespace, since it also
|
||||
// means "before a field."
|
||||
|
|
@ -1522,9 +1443,8 @@ fn parse_fields<'a>(
|
|||
}
|
||||
State::AfterQuote => {
|
||||
if !is_space(ch) {
|
||||
return Err(err!(
|
||||
"expected whitespace after quoted field, \
|
||||
but found {ch:?} instead",
|
||||
return Err(Error::from(
|
||||
E::ExpectedWhitespaceAfterQuotedField,
|
||||
));
|
||||
}
|
||||
State::Whitespace
|
||||
|
|
@ -1537,7 +1457,7 @@ fn parse_fields<'a>(
|
|||
fields.push(&line[start..]);
|
||||
}
|
||||
State::InQuote => {
|
||||
return Err(err!("found unclosed quote"));
|
||||
return Err(Error::from(E::ExpectedCloseQuote));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -8,4 +8,4 @@ This was copied from `regex-automata` with a few light edits.
|
|||
// shared since they're needed there. We re-export them here
|
||||
// because this is really where they should live, but they're
|
||||
// in shared because `jiff-tzdb-static` needs it.
|
||||
pub(crate) use crate::shared::util::escape::{Byte, Bytes};
|
||||
pub(crate) use crate::shared::util::escape::{Byte, Bytes, RepeatByte};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
use crate::{
|
||||
error::{err, Error},
|
||||
util::escape::{Byte, Bytes},
|
||||
};
|
||||
use crate::error::util::{ParseFractionError, ParseIntError};
|
||||
|
||||
/// Parses an `i64` number from the beginning to the end of the given slice of
|
||||
/// ASCII digit characters.
|
||||
|
|
@ -13,38 +10,20 @@ use crate::{
|
|||
/// integers, and because a higher level routine might want to parse the sign
|
||||
/// and then apply it to the result of this routine.)
|
||||
#[cfg_attr(feature = "perf-inline", inline(always))]
|
||||
pub(crate) fn i64(bytes: &[u8]) -> Result<i64, Error> {
|
||||
pub(crate) fn i64(bytes: &[u8]) -> Result<i64, ParseIntError> {
|
||||
if bytes.is_empty() {
|
||||
return Err(err!("invalid number, no digits found"));
|
||||
return Err(ParseIntError::NoDigitsFound);
|
||||
}
|
||||
let mut n: i64 = 0;
|
||||
for &byte in bytes {
|
||||
let digit = match byte.checked_sub(b'0') {
|
||||
None => {
|
||||
return Err(err!(
|
||||
"invalid digit, expected 0-9 but got {}",
|
||||
Byte(byte),
|
||||
));
|
||||
}
|
||||
Some(digit) if digit > 9 => {
|
||||
return Err(err!(
|
||||
"invalid digit, expected 0-9 but got {}",
|
||||
Byte(byte),
|
||||
))
|
||||
}
|
||||
Some(digit) => {
|
||||
debug_assert!((0..=9).contains(&digit));
|
||||
i64::from(digit)
|
||||
}
|
||||
};
|
||||
n = n.checked_mul(10).and_then(|n| n.checked_add(digit)).ok_or_else(
|
||||
|| {
|
||||
err!(
|
||||
"number '{}' too big to parse into 64-bit integer",
|
||||
Bytes(bytes),
|
||||
)
|
||||
},
|
||||
)?;
|
||||
if !(b'0' <= byte && byte <= b'9') {
|
||||
return Err(ParseIntError::InvalidDigit(byte));
|
||||
}
|
||||
let digit = i64::from(byte - b'0');
|
||||
n = n
|
||||
.checked_mul(10)
|
||||
.and_then(|n| n.checked_add(digit))
|
||||
.ok_or(ParseIntError::TooBig)?;
|
||||
}
|
||||
Ok(n)
|
||||
}
|
||||
|
|
@ -65,7 +44,9 @@ pub(crate) fn i64(bytes: &[u8]) -> Result<i64, Error> {
|
|||
///
|
||||
/// When the parsed integer cannot fit into a `u64`.
|
||||
#[cfg_attr(feature = "perf-inline", inline(always))]
|
||||
pub(crate) fn u64_prefix(bytes: &[u8]) -> Result<(Option<u64>, &[u8]), Error> {
|
||||
pub(crate) fn u64_prefix(
|
||||
bytes: &[u8],
|
||||
) -> Result<(Option<u64>, &[u8]), ParseIntError> {
|
||||
// Discovered via `u64::MAX.to_string().len()`.
|
||||
const MAX_U64_DIGITS: usize = 20;
|
||||
|
||||
|
|
@ -79,15 +60,10 @@ pub(crate) fn u64_prefix(bytes: &[u8]) -> Result<(Option<u64>, &[u8]), Error> {
|
|||
digit_count += 1;
|
||||
// OK because we confirmed `byte` is an ASCII digit.
|
||||
let digit = u64::from(byte - b'0');
|
||||
n = n.checked_mul(10).and_then(|n| n.checked_add(digit)).ok_or_else(
|
||||
#[inline(never)]
|
||||
|| {
|
||||
err!(
|
||||
"number `{}` too big to parse into 64-bit integer",
|
||||
Bytes(&bytes[..digit_count]),
|
||||
)
|
||||
},
|
||||
)?;
|
||||
n = n
|
||||
.checked_mul(10)
|
||||
.and_then(|n| n.checked_add(digit))
|
||||
.ok_or(ParseIntError::TooBig)?;
|
||||
}
|
||||
if digit_count == 0 {
|
||||
return Ok((None, bytes));
|
||||
|
|
@ -105,54 +81,33 @@ pub(crate) fn u64_prefix(bytes: &[u8]) -> Result<(Option<u64>, &[u8]), Error> {
|
|||
///
|
||||
/// If any byte in the given slice is not `[0-9]`, then this returns an error.
|
||||
/// Notably, this routine does not permit parsing a negative integer.
|
||||
pub(crate) fn fraction(bytes: &[u8]) -> Result<u32, Error> {
|
||||
const MAX_PRECISION: usize = 9;
|
||||
|
||||
pub(crate) fn fraction(bytes: &[u8]) -> Result<u32, ParseFractionError> {
|
||||
if bytes.is_empty() {
|
||||
return Err(err!("invalid fraction, no digits found"));
|
||||
} else if bytes.len() > MAX_PRECISION {
|
||||
return Err(err!(
|
||||
"invalid fraction, too many digits \
|
||||
(at most {MAX_PRECISION} are allowed"
|
||||
));
|
||||
return Err(ParseFractionError::NoDigitsFound);
|
||||
} else if bytes.len() > ParseFractionError::MAX_PRECISION {
|
||||
return Err(ParseFractionError::TooManyDigits);
|
||||
}
|
||||
let mut n: u32 = 0;
|
||||
for &byte in bytes {
|
||||
let digit = match byte.checked_sub(b'0') {
|
||||
None => {
|
||||
return Err(err!(
|
||||
"invalid fractional digit, expected 0-9 but got {}",
|
||||
Byte(byte),
|
||||
));
|
||||
return Err(ParseFractionError::InvalidDigit(byte));
|
||||
}
|
||||
Some(digit) if digit > 9 => {
|
||||
return Err(err!(
|
||||
"invalid fractional digit, expected 0-9 but got {}",
|
||||
Byte(byte),
|
||||
))
|
||||
return Err(ParseFractionError::InvalidDigit(byte));
|
||||
}
|
||||
Some(digit) => {
|
||||
debug_assert!((0..=9).contains(&digit));
|
||||
u32::from(digit)
|
||||
}
|
||||
};
|
||||
n = n.checked_mul(10).and_then(|n| n.checked_add(digit)).ok_or_else(
|
||||
|| {
|
||||
err!(
|
||||
"fractional '{}' too big to parse into 64-bit integer",
|
||||
Bytes(bytes),
|
||||
)
|
||||
},
|
||||
)?;
|
||||
n = n
|
||||
.checked_mul(10)
|
||||
.and_then(|n| n.checked_add(digit))
|
||||
.ok_or_else(|| ParseFractionError::TooBig)?;
|
||||
}
|
||||
for _ in bytes.len()..MAX_PRECISION {
|
||||
n = n.checked_mul(10).ok_or_else(|| {
|
||||
err!(
|
||||
"fractional '{}' too big to parse into 64-bit integer \
|
||||
(too much precision supported)",
|
||||
Bytes(bytes)
|
||||
)
|
||||
})?;
|
||||
for _ in bytes.len()..ParseFractionError::MAX_PRECISION {
|
||||
n = n.checked_mul(10).ok_or_else(|| ParseFractionError::TooBig)?;
|
||||
}
|
||||
Ok(n)
|
||||
}
|
||||
|
|
@ -161,15 +116,17 @@ pub(crate) fn fraction(bytes: &[u8]) -> Result<u32, Error> {
|
|||
///
|
||||
/// This is effectively `OsStr::to_str`, but with a slightly better error
|
||||
/// message.
|
||||
#[cfg(feature = "tzdb-zoneinfo")]
|
||||
pub(crate) fn os_str_utf8<'o, O>(os_str: &'o O) -> Result<&'o str, Error>
|
||||
#[cfg(any(feature = "tz-system", feature = "tzdb-zoneinfo"))]
|
||||
pub(crate) fn os_str_utf8<'o, O>(
|
||||
os_str: &'o O,
|
||||
) -> Result<&'o str, crate::error::util::OsStrUtf8Error>
|
||||
where
|
||||
O: ?Sized + AsRef<std::ffi::OsStr>,
|
||||
{
|
||||
let os_str = os_str.as_ref();
|
||||
os_str
|
||||
.to_str()
|
||||
.ok_or_else(|| err!("environment value {os_str:?} is not valid UTF-8"))
|
||||
.ok_or_else(|| crate::error::util::OsStrUtf8Error::from(os_str))
|
||||
}
|
||||
|
||||
/// Parses an `OsStr` into a `&str` when `&[u8]` isn't easily available.
|
||||
|
|
@ -178,7 +135,9 @@ where
|
|||
/// be a zero-cost conversion on Unix platforms to `&[u8]`. On Windows, this
|
||||
/// will do UTF-8 validation and return an error if it's invalid UTF-8.
|
||||
#[cfg(feature = "tz-system")]
|
||||
pub(crate) fn os_str_bytes<'o, O>(os_str: &'o O) -> Result<&'o [u8], Error>
|
||||
pub(crate) fn os_str_bytes<'o, O>(
|
||||
os_str: &'o O,
|
||||
) -> Result<&'o [u8], crate::error::util::OsStrUtf8Error>
|
||||
where
|
||||
O: ?Sized + AsRef<std::ffi::OsStr>,
|
||||
{
|
||||
|
|
@ -190,16 +149,13 @@ where
|
|||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
let string = os_str.to_str().ok_or_else(|| {
|
||||
err!("environment value {os_str:?} is not valid UTF-8")
|
||||
})?;
|
||||
// It is suspect that we're doing UTF-8 validation and then throwing
|
||||
// away the fact that we did UTF-8 validation. So this could lead
|
||||
// to an extra UTF-8 check if the caller ultimately needs UTF-8. If
|
||||
// that's important, we can add a new API that returns a `&str`. But it
|
||||
// probably won't matter because an `OsStr` in this crate is usually
|
||||
// just an environment variable.
|
||||
Ok(string.as_bytes())
|
||||
Ok(os_str_utf8(os_str)?.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -164,16 +164,15 @@ macro_rules! define_ranged {
|
|||
val: impl Into<i64>,
|
||||
) -> Result<Self, Error> {
|
||||
let val = val.into();
|
||||
#[allow(irrefutable_let_patterns)]
|
||||
let Ok(val) = <$repr>::try_from(val) else {
|
||||
return Err(Error::range(
|
||||
what,
|
||||
val,
|
||||
Self::MIN_REPR,
|
||||
Self::MAX_REPR,
|
||||
));
|
||||
};
|
||||
Self::new(val).ok_or_else(|| Self::error(what, val))
|
||||
<$repr>::try_from(val).ok().and_then(Self::new).ok_or_else(
|
||||
|| {
|
||||
Error::range(
|
||||
what,
|
||||
val,
|
||||
Self::MIN_REPR,
|
||||
Self::MAX_REPR,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
@ -182,16 +181,15 @@ macro_rules! define_ranged {
|
|||
val: impl Into<i128>,
|
||||
) -> Result<Self, Error> {
|
||||
let val = val.into();
|
||||
#[allow(irrefutable_let_patterns)]
|
||||
let Ok(val) = <$repr>::try_from(val) else {
|
||||
return Err(Error::range(
|
||||
what,
|
||||
val,
|
||||
Self::MIN_REPR,
|
||||
Self::MAX_REPR,
|
||||
));
|
||||
};
|
||||
Self::new(val).ok_or_else(|| Self::error(what, val))
|
||||
<$repr>::try_from(val).ok().and_then(Self::new).ok_or_else(
|
||||
|| {
|
||||
Error::range(
|
||||
what,
|
||||
val,
|
||||
Self::MIN_REPR,
|
||||
Self::MAX_REPR,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ for time units must divide evenly into 1 unit of the next highest unit.
|
|||
*/
|
||||
|
||||
use crate::{
|
||||
error::{err, Error},
|
||||
error::{util::RoundingIncrementError as E, Error, ErrorContext},
|
||||
util::{
|
||||
rangeint::RFrom,
|
||||
t::{self, Constant, C},
|
||||
|
|
@ -45,7 +45,7 @@ pub(crate) fn for_span(
|
|||
// bounds of i64 and not i128.
|
||||
Ok(t::NoUnits128::rfrom(t::NoUnits::new_unchecked(increment)))
|
||||
} else {
|
||||
get_with_limit(unit, increment, "span", LIMIT)
|
||||
get_with_limit(unit, increment, LIMIT).context(E::ForSpan)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -67,7 +67,7 @@ pub(crate) fn for_datetime(
|
|||
t::HOURS_PER_CIVIL_DAY,
|
||||
Constant(2),
|
||||
];
|
||||
get_with_limit(unit, increment, "datetime", LIMIT)
|
||||
get_with_limit(unit, increment, LIMIT).context(E::ForDateTime)
|
||||
}
|
||||
|
||||
/// Validates the given rounding increment for the given unit.
|
||||
|
|
@ -87,7 +87,7 @@ pub(crate) fn for_time(
|
|||
t::MINUTES_PER_HOUR,
|
||||
t::HOURS_PER_CIVIL_DAY,
|
||||
];
|
||||
get_with_limit(unit, increment, "time", LIMIT)
|
||||
get_with_limit(unit, increment, LIMIT).context(E::ForTime)
|
||||
}
|
||||
|
||||
/// Validates the given rounding increment for the given unit.
|
||||
|
|
@ -107,38 +107,25 @@ pub(crate) fn for_timestamp(
|
|||
t::MINUTES_PER_CIVIL_DAY,
|
||||
t::HOURS_PER_CIVIL_DAY,
|
||||
];
|
||||
get_with_max(unit, increment, "timestamp", MAX)
|
||||
get_with_max(unit, increment, MAX).context(E::ForTimestamp)
|
||||
}
|
||||
|
||||
fn get_with_limit(
|
||||
unit: Unit,
|
||||
increment: i64,
|
||||
what: &'static str,
|
||||
limit: &[t::Constant],
|
||||
) -> Result<t::NoUnits128, Error> {
|
||||
) -> Result<t::NoUnits128, E> {
|
||||
// OK because `NoUnits` specifically allows any `i64` value.
|
||||
let increment = t::NoUnits::new_unchecked(increment);
|
||||
if increment <= C(0) {
|
||||
return Err(err!(
|
||||
"rounding increment {increment} for {unit} must be \
|
||||
greater than zero",
|
||||
unit = unit.plural(),
|
||||
));
|
||||
return Err(E::GreaterThanZero { unit });
|
||||
}
|
||||
let Some(must_divide) = limit.get(unit as usize) else {
|
||||
return Err(err!(
|
||||
"{what} rounding does not support {unit}",
|
||||
unit = unit.plural()
|
||||
));
|
||||
return Err(E::Unsupported { unit });
|
||||
};
|
||||
let must_divide = t::NoUnits::rfrom(*must_divide);
|
||||
if increment >= must_divide || must_divide % increment != C(0) {
|
||||
Err(err!(
|
||||
"increment {increment} for rounding {what} to {unit} \
|
||||
must be 1) less than {must_divide}, 2) divide into \
|
||||
it evenly and 3) greater than zero",
|
||||
unit = unit.plural(),
|
||||
))
|
||||
Err(E::InvalidDivide { unit, must_divide: must_divide.get() })
|
||||
} else {
|
||||
Ok(t::NoUnits128::rfrom(increment))
|
||||
}
|
||||
|
|
@ -147,32 +134,19 @@ fn get_with_limit(
|
|||
fn get_with_max(
|
||||
unit: Unit,
|
||||
increment: i64,
|
||||
what: &'static str,
|
||||
max: &[t::Constant],
|
||||
) -> Result<t::NoUnits128, Error> {
|
||||
) -> Result<t::NoUnits128, E> {
|
||||
// OK because `NoUnits` specifically allows any `i64` value.
|
||||
let increment = t::NoUnits::new_unchecked(increment);
|
||||
if increment <= C(0) {
|
||||
return Err(err!(
|
||||
"rounding increment {increment} for {unit} must be \
|
||||
greater than zero",
|
||||
unit = unit.plural(),
|
||||
));
|
||||
return Err(E::GreaterThanZero { unit });
|
||||
}
|
||||
let Some(must_divide) = max.get(unit as usize) else {
|
||||
return Err(err!(
|
||||
"{what} rounding does not support {unit}",
|
||||
unit = unit.plural()
|
||||
));
|
||||
return Err(E::Unsupported { unit });
|
||||
};
|
||||
let must_divide = t::NoUnits::rfrom(*must_divide);
|
||||
if increment > must_divide || must_divide % increment != C(0) {
|
||||
Err(err!(
|
||||
"increment {increment} for rounding {what} to {unit} \
|
||||
must be 1) less than or equal to {must_divide}, \
|
||||
2) divide into it evenly and 3) greater than zero",
|
||||
unit = unit.plural(),
|
||||
))
|
||||
Err(E::InvalidDivide { unit, must_divide: must_divide.get() })
|
||||
} else {
|
||||
Ok(t::NoUnits128::rfrom(increment))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,5 @@
|
|||
use core::cmp::Ordering;
|
||||
|
||||
/// Decodes the next UTF-8 encoded codepoint from the given byte slice.
|
||||
///
|
||||
/// If no valid encoding of a codepoint exists at the beginning of the
|
||||
/// given byte slice, then a 1-3 byte slice is returned (which is guaranteed
|
||||
/// to be a prefix of `bytes`). That byte slice corresponds either to a single
|
||||
/// invalid byte, or to a prefix of a valid UTF-8 encoding of a Unicode scalar
|
||||
/// value (but which ultimately did not lead to a valid encoding).
|
||||
///
|
||||
/// This returns `None` if and only if `bytes` is empty.
|
||||
///
|
||||
/// This never panics.
|
||||
///
|
||||
/// *WARNING*: This is not designed for performance. If you're looking for a
|
||||
/// fast UTF-8 decoder, this is not it. If you feel like you need one in this
|
||||
/// crate, then please file an issue and discuss your use case.
|
||||
pub(crate) fn decode(bytes: &[u8]) -> Option<Result<char, &[u8]>> {
|
||||
crate::shared::util::utf8::decode(bytes)
|
||||
}
|
||||
|
||||
/// Like std's `eq_ignore_ascii_case`, but returns a full `Ordering`.
|
||||
#[inline]
|
||||
pub(crate) fn cmp_ignore_ascii_case(s1: &str, s2: &str) -> Ordering {
|
||||
|
|
|
|||
121
src/zoned.rs
121
src/zoned.rs
|
|
@ -6,7 +6,7 @@ use crate::{
|
|||
Weekday,
|
||||
},
|
||||
duration::{Duration, SDuration},
|
||||
error::{err, Error, ErrorContext},
|
||||
error::{zoned::Error as E, Error, ErrorContext},
|
||||
fmt::{
|
||||
self,
|
||||
temporal::{self, DEFAULT_DATETIME_PARSER},
|
||||
|
|
@ -2216,41 +2216,20 @@ impl Zoned {
|
|||
.timestamp()
|
||||
.checked_add(span)
|
||||
.map(|ts| ts.to_zoned(self.time_zone().clone()))
|
||||
.with_context(|| {
|
||||
err!(
|
||||
"failed to add span {span} to timestamp {timestamp} \
|
||||
from zoned datetime {zoned}",
|
||||
timestamp = self.timestamp(),
|
||||
zoned = self,
|
||||
)
|
||||
});
|
||||
.context(E::AddTimestamp);
|
||||
}
|
||||
let span_time = span.only_time();
|
||||
let dt =
|
||||
self.datetime().checked_add(span_calendar).with_context(|| {
|
||||
err!(
|
||||
"failed to add span {span_calendar} to datetime {dt} \
|
||||
from zoned datetime {zoned}",
|
||||
dt = self.datetime(),
|
||||
zoned = self,
|
||||
)
|
||||
})?;
|
||||
let dt = self
|
||||
.datetime()
|
||||
.checked_add(span_calendar)
|
||||
.context(E::AddDateTime)?;
|
||||
|
||||
let tz = self.time_zone();
|
||||
let mut ts =
|
||||
tz.to_ambiguous_timestamp(dt).compatible().with_context(|| {
|
||||
err!(
|
||||
"failed to convert civil datetime {dt} to timestamp \
|
||||
with time zone {tz}",
|
||||
tz = self.time_zone().diagnostic_name(),
|
||||
)
|
||||
})?;
|
||||
ts = ts.checked_add(span_time).with_context(|| {
|
||||
err!(
|
||||
"failed to add span {span_time} to timestamp {ts} \
|
||||
(which was created from {dt})"
|
||||
)
|
||||
})?;
|
||||
let mut ts = tz
|
||||
.to_ambiguous_timestamp(dt)
|
||||
.compatible()
|
||||
.context(E::ConvertDateTimeToTimestamp)?;
|
||||
ts = ts.checked_add(span_time).context(E::AddTimestamp)?;
|
||||
Ok(ts.to_zoned(tz.clone()))
|
||||
}
|
||||
|
||||
|
|
@ -4327,13 +4306,7 @@ impl<'a> ZonedDifference<'a> {
|
|||
return zdt1.timestamp().until((largest, zdt2.timestamp()));
|
||||
}
|
||||
if zdt1.time_zone() != zdt2.time_zone() {
|
||||
return Err(err!(
|
||||
"computing the span between zoned datetimes, with \
|
||||
{largest} units, requires that the time zones are \
|
||||
equivalent, but {zdt1} and {zdt2} have distinct \
|
||||
time zones",
|
||||
largest = largest.singular(),
|
||||
));
|
||||
return Err(Error::from(E::MismatchTimeZoneUntil { largest }));
|
||||
}
|
||||
let tz = zdt1.time_zone();
|
||||
|
||||
|
|
@ -4347,43 +4320,27 @@ impl<'a> ZonedDifference<'a> {
|
|||
let mut mid = dt2
|
||||
.date()
|
||||
.checked_add(Span::new().days_ranged(day_correct * -sign))
|
||||
.with_context(|| {
|
||||
err!(
|
||||
"failed to add {days} days to date in {dt2}",
|
||||
days = day_correct * -sign,
|
||||
)
|
||||
})?
|
||||
.context(E::AddDays)?
|
||||
.to_datetime(dt1.time());
|
||||
let mut zmid: Zoned = mid.to_zoned(tz.clone()).with_context(|| {
|
||||
err!(
|
||||
"failed to convert intermediate datetime {mid} \
|
||||
to zoned timestamp in time zone {tz}",
|
||||
tz = tz.diagnostic_name(),
|
||||
)
|
||||
})?;
|
||||
let mut zmid: Zoned = mid
|
||||
.to_zoned(tz.clone())
|
||||
.context(E::ConvertIntermediateDatetime)?;
|
||||
if t::sign(zdt2, &zmid) == -sign {
|
||||
if sign == C(-1) {
|
||||
// FIXME
|
||||
panic!("this should be an error");
|
||||
}
|
||||
day_correct += C(1);
|
||||
mid = dt2
|
||||
.date()
|
||||
.checked_add(Span::new().days_ranged(day_correct * -sign))
|
||||
.with_context(|| {
|
||||
err!(
|
||||
"failed to add {days} days to date in {dt2}",
|
||||
days = day_correct * -sign,
|
||||
)
|
||||
})?
|
||||
.context(E::AddDays)?
|
||||
.to_datetime(dt1.time());
|
||||
zmid = mid.to_zoned(tz.clone()).with_context(|| {
|
||||
err!(
|
||||
"failed to convert intermediate datetime {mid} \
|
||||
to zoned timestamp in time zone {tz}",
|
||||
tz = tz.diagnostic_name(),
|
||||
)
|
||||
})?;
|
||||
zmid = mid
|
||||
.to_zoned(tz.clone())
|
||||
.context(E::ConvertIntermediateDatetime)?;
|
||||
if t::sign(zdt2, &zmid) == -sign {
|
||||
// FIXME
|
||||
panic!("this should be an error too");
|
||||
}
|
||||
}
|
||||
|
|
@ -4635,32 +4592,18 @@ impl ZonedRound {
|
|||
// and a &TimeZone. Fixing just this should just be some minor annoying
|
||||
// work. The grander refactor is something like an `Unzoned` type, but
|
||||
// I'm not sure that's really worth it. ---AG
|
||||
let start = zdt.start_of_day().with_context(move || {
|
||||
err!("failed to find start of day for {zdt}")
|
||||
})?;
|
||||
let start = zdt.start_of_day().context(E::FailedStartOfDay)?;
|
||||
let end = start
|
||||
.checked_add(Span::new().days_ranged(C(1).rinto()))
|
||||
.with_context(|| {
|
||||
err!("failed to add 1 day to {start} to find length of day")
|
||||
})?;
|
||||
.context(E::FailedLengthOfDay)?;
|
||||
let span = start
|
||||
.timestamp()
|
||||
.until((Unit::Nanosecond, end.timestamp()))
|
||||
.with_context(|| {
|
||||
err!(
|
||||
"failed to compute span in nanoseconds \
|
||||
from {start} until {end}"
|
||||
)
|
||||
})?;
|
||||
.context(E::FailedSpanNanoseconds)?;
|
||||
let nanos = span.get_nanoseconds_ranged();
|
||||
let day_length =
|
||||
ZonedDayNanoseconds::try_rfrom("nanoseconds-per-zoned-day", nanos)
|
||||
.with_context(|| {
|
||||
err!(
|
||||
"failed to convert span between {start} until {end} \
|
||||
to nanoseconds",
|
||||
)
|
||||
})?;
|
||||
.context(E::FailedSpanNanoseconds)?;
|
||||
let progress = zdt.timestamp().as_nanosecond_ranged()
|
||||
- start.timestamp().as_nanosecond_ranged();
|
||||
let rounded = self.round.get_mode().round(progress, day_length);
|
||||
|
|
@ -6078,21 +6021,21 @@ mod tests {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
zdt.round(Unit::Year).unwrap_err(),
|
||||
@"datetime rounding does not support years"
|
||||
@"failed rounding datetime: rounding to years is not supported"
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
zdt.round(Unit::Month).unwrap_err(),
|
||||
@"datetime rounding does not support months"
|
||||
@"failed rounding datetime: rounding to months is not supported"
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
zdt.round(Unit::Week).unwrap_err(),
|
||||
@"datetime rounding does not support weeks"
|
||||
@"failed rounding datetime: rounding to weeks is not supported"
|
||||
);
|
||||
|
||||
let options = ZonedRound::new().smallest(Unit::Day).increment(2);
|
||||
insta::assert_snapshot!(
|
||||
zdt.round(options).unwrap_err(),
|
||||
@"increment 2 for rounding datetime to days must be 1) less than 2, 2) divide into it evenly and 3) greater than zero"
|
||||
@"failed rounding datetime: increment for rounding to days must be 1) less than 2, 2) divide into it evenly and 3) greater than zero"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -6124,12 +6067,12 @@ mod tests {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
"1970-06-01T00:00:00-00:44:40[Africa/Monrovia]".parse::<Zoned>().unwrap_err(),
|
||||
@r#"parsing "1970-06-01T00:00:00-00:44:40[Africa/Monrovia]" failed: datetime 1970-06-01T00:00:00 could not resolve to a timestamp since 'reject' conflict resolution was chosen, and because datetime has offset -00:44:40, but the time zone Africa/Monrovia for the given datetime unambiguously has offset -00:44:30"#,
|
||||
@"datetime could not resolve to a timestamp since `reject` conflict resolution was chosen, and because datetime has offset `-00:44:40`, but the time zone `Africa/Monrovia` for the given datetime unambiguously has offset `-00:44:30`",
|
||||
);
|
||||
|
||||
insta::assert_snapshot!(
|
||||
"1970-06-01T00:00:00-00:45:00[Africa/Monrovia]".parse::<Zoned>().unwrap_err(),
|
||||
@r#"parsing "1970-06-01T00:00:00-00:45:00[Africa/Monrovia]" failed: datetime 1970-06-01T00:00:00 could not resolve to a timestamp since 'reject' conflict resolution was chosen, and because datetime has offset -00:45, but the time zone Africa/Monrovia for the given datetime unambiguously has offset -00:44:30"#,
|
||||
@"datetime could not resolve to a timestamp since `reject` conflict resolution was chosen, and because datetime has offset `-00:45`, but the time zone `Africa/Monrovia` for the given datetime unambiguously has offset `-00:44:30`",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ fn main() {
|
|||
}
|
||||
assert_eq!(
|
||||
jiff::tz::TimeZone::try_system().unwrap_err().to_string(),
|
||||
"TZ environment variable set, but failed to read value: \
|
||||
failed to read TZ=\"WAT5HUH\" as a TZif file \
|
||||
after attempting a tzdb lookup for `WAT5HUH`",
|
||||
"`TZ` environment variable set, but failed to read value: \
|
||||
failed to read `TZ` environment variable value as a TZif file \
|
||||
after attempting (and failing) a tzdb lookup for that same value",
|
||||
);
|
||||
|
||||
// SAFETY: This is a single threaded program.
|
||||
|
|
@ -18,9 +18,9 @@ fn main() {
|
|||
}
|
||||
assert_eq!(
|
||||
jiff::tz::TimeZone::try_system().unwrap_err().to_string(),
|
||||
"TZ environment variable set, but failed to read value: \
|
||||
failed to read TZ=\"/usr/share/zoneinfo/WAT5HUH\" as a TZif file \
|
||||
after attempting a tzdb lookup for `WAT5HUH`",
|
||||
"`TZ` environment variable set, but failed to read value: \
|
||||
failed to read `TZ` environment variable value as a TZif file \
|
||||
after attempting (and failing) a tzdb lookup for that same value",
|
||||
);
|
||||
|
||||
unsafe {
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ fn no_calendar_units() -> Result {
|
|||
);
|
||||
insta::assert_snapshot!(
|
||||
1.week().checked_add(blank).unwrap_err(),
|
||||
@"using unit 'week' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
|
||||
@"using unit 'week' in a span or configuration requires that either a relative reference time be given or `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
|
||||
);
|
||||
|
||||
let ok = 1.day();
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ fn calendar_possibly_required() -> Result {
|
|||
let sp = 5.weeks();
|
||||
insta::assert_snapshot!(
|
||||
sp.round(Unit::Hour).unwrap_err(),
|
||||
@"error with largest unit in span to be rounded: using unit 'week' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
|
||||
@"error with largest unit in span to be rounded: using unit 'week' in a span or configuration requires that either a relative reference time be given or `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
|
||||
);
|
||||
span_eq!(sp.round(relative_years)?, 1.month().days(4));
|
||||
span_eq!(sp.round(relative_months)?, 1.month().days(4));
|
||||
|
|
@ -98,7 +98,7 @@ fn calendar_possibly_required() -> Result {
|
|||
// We differ from Temporal in that we require opt-in for 24-hour days.
|
||||
insta::assert_snapshot!(
|
||||
sp.round(Unit::Hour).unwrap_err(),
|
||||
@"error with largest unit in span to be rounded: using unit 'day' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
|
||||
@"error with largest unit in span to be rounded: using unit 'day' in a span or configuration requires that either a relative reference time be given or `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
|
||||
);
|
||||
span_eq!(sp.round(relative_years)?, 1.month().days(11));
|
||||
span_eq!(sp.round(relative_months)?, 1.month().days(11));
|
||||
|
|
@ -218,14 +218,14 @@ fn duration_out_of_range_added_to_relative() -> Result {
|
|||
let relative = SpanRound::new().relative(d);
|
||||
insta::assert_snapshot!(
|
||||
sp.round(relative.smallest(Unit::Year)).unwrap_err(),
|
||||
@"failed to add P2000000DT170000000H to 2000-01-01T00:00:00: failed to add overflowing span, P7083333D, from adding PT170000000H to 00:00:00, to 7475-10-25: parameter 'days' with value 7083333 is not in the required range of -4371587..=2932896",
|
||||
@"failed to add overflowing span: parameter 'days' with value 7083333 is not in the required range of -4371587..=2932896",
|
||||
);
|
||||
|
||||
let sp = -2_000_000.days().hours(170_000_000);
|
||||
let relative = SpanRound::new().relative(d);
|
||||
insta::assert_snapshot!(
|
||||
sp.round(relative.smallest(Unit::Year)).unwrap_err(),
|
||||
@"failed to add -P2000000DT170000000H to 2000-01-01T00:00:00: failed to add overflowing span, -P7083334D, from adding -PT170000000H to 00:00:00, to -003476-03-09: parameter 'days' with value -7083334 is not in the required range of -4371587..=2932896",
|
||||
@"failed to add overflowing span: parameter 'days' with value -7083334 is not in the required range of -4371587..=2932896",
|
||||
);
|
||||
|
||||
Ok(())
|
||||
|
|
@ -713,7 +713,7 @@ fn out_of_range_when_adjusting_rounded_days() -> Result {
|
|||
insta::assert_snapshot!(
|
||||
sp.round(options).unwrap_err(),
|
||||
// Kind of a brutal error message...
|
||||
@"failed to add P1DT631107331200.999999999S to 1970-01-01T00:00:00+00:00[UTC]: failed to add span PT631107331200.999999999S to timestamp 1970-01-02T00:00:00Z (which was created from 1970-01-02T00:00:00): adding PT631107331200.999999999S to 1970-01-02T00:00:00Z overflowed: parameter 'span' with value 631107331200999999999 is not in the required range of -377705023201000000000..=253402207200999999999",
|
||||
@"failed to add span to timestamp from zoned datetime: adding span overflowed timestamp: parameter 'span' with value 631107331200999999999 is not in the required range of -377705023201000000000..=253402207200999999999",
|
||||
);
|
||||
|
||||
Ok(())
|
||||
|
|
@ -728,7 +728,7 @@ fn out_of_range_when_converting_from_normalized_duration() -> Result {
|
|||
insta::assert_snapshot!(
|
||||
sp.round(options).unwrap_err(),
|
||||
// Kind of a brutal error message...
|
||||
@"failed to convert rounded nanoseconds 631107417600999999999 to span for largest unit as nanoseconds: parameter 'nanoseconds' with value 631107417600999999999 is not in the required range of -9223372036854775807..=9223372036854775807",
|
||||
@"failed to convert rounded nanoseconds to span for largest unit set to 'nanoseconds': parameter 'nanoseconds' with value 631107417600999999999 is not in the required range of -9223372036854775807..=9223372036854775807",
|
||||
);
|
||||
|
||||
Ok(())
|
||||
|
|
@ -759,15 +759,15 @@ fn precision_exact_in_round_duration() -> Result {
|
|||
fn relativeto_undefined_throw_on_calendar_units() -> Result {
|
||||
insta::assert_snapshot!(
|
||||
1.day().round(SpanRound::new().largest(Unit::Hour)).unwrap_err(),
|
||||
@"error with largest unit in span to be rounded: using unit 'day' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
|
||||
@"error with largest unit in span to be rounded: using unit 'day' in a span or configuration requires that either a relative reference time be given or `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
1.day().round(SpanRound::new().largest(Unit::Hour)).unwrap_err(),
|
||||
@"error with largest unit in span to be rounded: using unit 'day' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
|
||||
@"error with largest unit in span to be rounded: using unit 'day' in a span or configuration requires that either a relative reference time be given or `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
1.week().round(SpanRound::new().largest(Unit::Hour)).unwrap_err(),
|
||||
@"error with largest unit in span to be rounded: using unit 'week' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
|
||||
@"error with largest unit in span to be rounded: using unit 'week' in a span or configuration requires that either a relative reference time be given or `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
1.month().round(SpanRound::new().largest(Unit::Hour)).unwrap_err(),
|
||||
|
|
@ -780,37 +780,37 @@ fn relativeto_undefined_throw_on_calendar_units() -> Result {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
1.month().round(SpanRound::new().largest(Unit::Hour).days_are_24_hours()).unwrap_err(),
|
||||
@"using unit 'month' in span or configuration requires that a relative reference time be given (`SpanRelativeTo::days_are_24_hours()` was given but this only permits using days and weeks without a relative reference time)",
|
||||
@"using unit 'month' in span or configuration requires that a relative reference time be given (`jiff::SpanRelativeTo::days_are_24_hours()` was given but this only permits using days and weeks without a relative reference time)",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
1.year().round(SpanRound::new().largest(Unit::Hour).days_are_24_hours()).unwrap_err(),
|
||||
@"using unit 'year' in span or configuration requires that a relative reference time be given (`SpanRelativeTo::days_are_24_hours()` was given but this only permits using days and weeks without a relative reference time)",
|
||||
@"using unit 'year' in span or configuration requires that a relative reference time be given (`jiff::SpanRelativeTo::days_are_24_hours()` was given but this only permits using days and weeks without a relative reference time)",
|
||||
);
|
||||
|
||||
insta::assert_snapshot!(
|
||||
1.hour().round(SpanRound::new().largest(Unit::Day)).unwrap_err(),
|
||||
@"error with `largest` rounding option: using unit 'day' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
|
||||
@"error with `largest` rounding option: using unit 'day' in a span or configuration requires that either a relative reference time be given or `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
1.day().round(SpanRound::new().largest(Unit::Day)).unwrap_err(),
|
||||
@"error with `largest` rounding option: using unit 'day' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
|
||||
@"error with `largest` rounding option: using unit 'day' in a span or configuration requires that either a relative reference time be given or `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
1.week().round(SpanRound::new().largest(Unit::Day)).unwrap_err(),
|
||||
@"error with `largest` rounding option: using unit 'day' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
|
||||
@"error with `largest` rounding option: using unit 'day' in a span or configuration requires that either a relative reference time be given or `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
1.month().round(SpanRound::new().largest(Unit::Day)).unwrap_err(),
|
||||
@"error with `largest` rounding option: using unit 'day' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
|
||||
@"error with `largest` rounding option: using unit 'day' in a span or configuration requires that either a relative reference time be given or `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
1.year().round(SpanRound::new().largest(Unit::Day)).unwrap_err(),
|
||||
@"error with `largest` rounding option: using unit 'day' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
|
||||
@"error with `largest` rounding option: using unit 'day' in a span or configuration requires that either a relative reference time be given or `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
|
||||
);
|
||||
|
||||
insta::assert_snapshot!(
|
||||
1.day().round(SpanRound::new().largest(Unit::Week)).unwrap_err(),
|
||||
@"error with `largest` rounding option: using unit 'week' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
|
||||
@"error with `largest` rounding option: using unit 'week' in a span or configuration requires that either a relative reference time be given or `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
1.day().round(SpanRound::new().largest(Unit::Month)).unwrap_err(),
|
||||
|
|
@ -842,7 +842,7 @@ fn result_out_of_range() -> Result {
|
|||
let sp = MAX_SPAN_SECONDS.seconds().nanoseconds(999_999_999);
|
||||
insta::assert_snapshot!(
|
||||
sp.round(Unit::Second).unwrap_err(),
|
||||
@"failed to convert rounded nanoseconds 631107417601000000000 to span for largest unit as seconds: parameter 'seconds' with value 631107417601 is not in the required range of -631107417600..=631107417600",
|
||||
@"failed to convert rounded nanoseconds to span for largest unit set to 'seconds': parameter 'seconds' with value 631107417601 is not in the required range of -631107417600..=631107417600",
|
||||
);
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ fn calendar_possibly_required() -> Result {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
week.total(Unit::Day).unwrap_err(),
|
||||
@"using unit 'week' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
|
||||
@"using unit 'week' in a span or configuration requires that either a relative reference time be given or `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
|
||||
);
|
||||
let result = week.total((Unit::Day, d))?;
|
||||
assert_eq!(result, 7.0);
|
||||
|
|
@ -62,7 +62,7 @@ fn calendar_possibly_required() -> Result {
|
|||
// Differs from Temporal. We require explicit opt-in for 24-hour days.
|
||||
insta::assert_snapshot!(
|
||||
day.total(Unit::Day).unwrap_err(),
|
||||
@"using unit 'day' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
|
||||
@"using unit 'day' in a span or configuration requires that either a relative reference time be given or `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
|
||||
);
|
||||
let result = day.total((Unit::Day, DAY24))?;
|
||||
assert_eq!(result, 42.0);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue