Compare commits

..

No commits in common. "master" and "jiff-icu-0.2.2" have entirely different histories.

108 changed files with 5769 additions and 10812 deletions

View file

@ -229,23 +229,6 @@ jobs:
run: |
cargo bench --manifest-path bench/Cargo.toml -- --test
# Test that we can build the fuzzer targets.
#
# It's not necessary to check the fuzzers on all platforms, as this is pretty
# strictly a testing utility.
test-fuzz:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
- name: Build fuzzer targets
working-directory: ./fuzz
run: cargo build --verbose
# Runs miri on a subset of Jiff's test suite. This doesn't quite cover
# everything. In particular, `miri` and `insta` cannot play nice together,
# and `insta` is used a lot among Jiff's tests. However, the primary reason

View file

@ -2,7 +2,6 @@
"rust-analyzer.linkedProjects": [
"bench/Cargo.toml",
"crates/jiff-icu/Cargo.toml",
"fuzz/Cargo.toml",
"Cargo.toml"
]
}

View file

@ -1,18 +1,5 @@
# CHANGELOG
0.2.17 (TBD)
============
TODO
Enhancements:
* [#412](https://github.com/BurntSushi/jiff/issues/412):
Add `Display`, `FromStr`, `Serialize` and `Deserialize` trait implementations
for `jiff::civil::ISOWeekDate`. These all use the ISO 8601 week date format.
* [#418](https://github.com/BurntSushi/jiff/issues/418):
Add some basic predicates to `jiff::Error` for basic error introspection.
0.2.16 (2025-11-07)
===================
This release contains a number of enhancements and bug fixes that have accrued

View file

@ -79,7 +79,7 @@ fn copy(srcdir: &Path, dstdir: &Path, dir: &Path) -> anyhow::Result<()> {
fn copy_rust_source_file(src: &Path, dst: &Path) -> anyhow::Result<()> {
let code = fs::read_to_string(src)
.with_context(|| format!("failed to read {}", src.display()))?;
let code = remove_only_jiffs(&remove_cfg_alloc_or_std(&code));
let code = remove_only_jiffs(&remove_cfg_alloc(&code));
let mut out = String::new();
writeln!(out, "// auto-generated by: jiff-cli generate shared")?;
@ -105,13 +105,13 @@ fn remove_only_jiffs(code: &str) -> String {
RE.replace_all(code, "").into_owned()
}
/// Removes all `#[cfg(feature = "alloc|std")]` gates.
/// Removes all `#[cfg(feature = "alloc")]` gates.
///
/// This is because the proc-macro always runs in a context where `alloc`
/// (and `std`) are enabled.
fn remove_cfg_alloc_or_std(code: &str) -> String {
fn remove_cfg_alloc(code: &str) -> String {
static RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r###"#\[cfg\(feature = "(alloc|std)"\)\]\n"###).unwrap()
Regex::new(r###"#\[cfg\(feature = "alloc"\)\]\n"###).unwrap()
});
RE.replace_all(code, "").into_owned()
}

View file

@ -1,100 +0,0 @@
// auto-generated by: jiff-cli generate shared
use crate::shared::{
error,
util::itime::{days_in_month, days_in_year, IEpochDay},
};
// N.B. Every variant in this error type is a range error.
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum Error {
DateInvalidDayOfYear { year: i16 },
DateInvalidDayOfYearNoLeap,
DateInvalidDays { year: i16, month: i8 },
DateTimeSeconds,
// TODO: I believe this can never happen.
DayOfYear,
EpochDayDays,
EpochDayI32,
NthWeekdayOfMonth,
Tomorrow,
YearNext,
YearPrevious,
Yesterday,
}
impl From<Error> for error::Error {
#[cold]
#[inline(never)]
fn from(err: Error) -> error::Error {
error::ErrorKind::Time(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 {
DateInvalidDayOfYear { year } => write!(
f,
"number of days for `{year:04}` is invalid, \
must be in range `1..={max_day}`",
max_day = days_in_year(year),
),
DateInvalidDayOfYearNoLeap => f.write_str(
"number of days is invalid, must be in range `1..=365`",
),
DateInvalidDays { year, month } => write!(
f,
"number of days for `{year:04}-{month:02}` is invalid, \
must be in range `1..={max_day}`",
max_day = days_in_month(year, month),
),
DateTimeSeconds => {
f.write_str("adding seconds to datetime overflowed")
}
DayOfYear => f.write_str("day of year is invalid"),
EpochDayDays => write!(
f,
"adding to epoch day resulted in a value outside \
the allowed range of `{min}..={max}`",
min = IEpochDay::MIN.epoch_day,
max = IEpochDay::MAX.epoch_day,
),
EpochDayI32 => f.write_str(
"adding to epoch day overflowed 32-bit signed integer",
),
NthWeekdayOfMonth => f.write_str(
"invalid nth weekday of month, \
must be non-zero and in range `-5..=5`",
),
Tomorrow => f.write_str(
"returning tomorrow for `9999-12-31` is not \
possible because it is greater than Jiff's supported
maximum date",
),
YearNext => f.write_str(
"creating a date for a year following `9999` is \
not possible because it is greater than Jiff's supported \
maximum date",
),
YearPrevious => f.write_str(
"creating a date for a year preceding `-9999` is \
not possible because it is less than Jiff's supported \
minimum date",
),
Yesterday => f.write_str(
"returning yesterday for `-9999-01-01` is not \
possible because it is less than Jiff's supported
minimum date",
),
}
}
}

View file

@ -1,57 +0,0 @@
// auto-generated by: jiff-cli generate shared
pub(crate) mod itime;
/// An error scoped to Jiff's `shared` module.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Error {
kind: ErrorKind,
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
self.kind.fmt(f)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
enum ErrorKind {
Time(self::itime::Error),
}
impl From<ErrorKind> for Error {
fn from(kind: ErrorKind) -> Error {
Error { kind }
}
}
impl core::fmt::Display for ErrorKind {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
match *self {
ErrorKind::Time(ref err) => err.fmt(f),
}
}
}
/*
/// A slim error that occurs when an input value is out of bounds.
#[derive(Clone, Debug)]
struct SlimRangeError {
what: &'static str,
}
impl SlimRangeError {
fn new(what: &'static str) -> SlimRangeError {
SlimRangeError { what }
}
}
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")
}
}
*/

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -17,7 +17,6 @@ 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, " ");
@ -38,7 +37,6 @@ 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)?;
@ -56,16 +54,15 @@ 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(err) => {
Err(errant_bytes) => {
// The decode API guarantees `errant_bytes` is non-empty.
write!(f, r"\x{:02x}", err.as_slice()[0])?;
write!(f, r"\x{:02x}", errant_bytes[0])?;
bytes = &bytes[1..];
continue;
}
@ -84,7 +81,6 @@ 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)?;
@ -92,34 +88,3 @@ impl<'a> core::fmt::Debug for Bytes<'a> {
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)?;
write!(f, "\"")?;
Ok(())
}
}

View file

@ -24,6 +24,8 @@ they are internal types. Specifically, to distinguish them from Jiff's public
types. For example, `Date` versus `IDate`.
*/
use super::error::{err, Error};
#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
pub(crate) struct ITimestamp {
pub(crate) second: i64,
@ -141,13 +143,11 @@ impl IDateTime {
pub(crate) fn checked_add_seconds(
&self,
seconds: i32,
) -> Result<IDateTime, RangeError> {
let day_second = self
.time
.to_second()
.second
.checked_add(seconds)
.ok_or_else(|| RangeError::DateTimeSeconds)?;
) -> Result<IDateTime, Error> {
let day_second =
self.time.to_second().second.checked_add(seconds).ok_or_else(
|| err!("adding `{seconds}s` to datetime overflowed"),
)?;
let days = day_second.div_euclid(86400);
let second = day_second.rem_euclid(86400);
let date = self.date.checked_add_days(days)?;
@ -162,8 +162,8 @@ pub(crate) struct IEpochDay {
}
impl IEpochDay {
pub(crate) const MIN: IEpochDay = IEpochDay { epoch_day: -4371587 };
pub(crate) const MAX: IEpochDay = IEpochDay { epoch_day: 2932896 };
const MIN: IEpochDay = IEpochDay { epoch_day: -4371587 };
const MAX: IEpochDay = IEpochDay { epoch_day: 2932896 };
/// Converts days since the Unix epoch to a Gregorian date.
///
@ -219,17 +219,20 @@ impl IEpochDay {
/// If this would overflow an `i32` or result in an out-of-bounds epoch
/// day, then this returns an error.
#[inline]
pub(crate) fn checked_add(
&self,
amount: i32,
) -> Result<IEpochDay, RangeError> {
pub(crate) fn checked_add(&self, amount: i32) -> Result<IEpochDay, Error> {
let epoch_day = self.epoch_day;
let sum = epoch_day
.checked_add(amount)
.ok_or_else(|| RangeError::EpochDayI32)?;
let sum = epoch_day.checked_add(amount).ok_or_else(|| {
err!("adding `{amount}` to epoch day `{epoch_day}` overflowed i32")
})?;
let ret = IEpochDay { epoch_day: sum };
if !(IEpochDay::MIN <= ret && ret <= IEpochDay::MAX) {
return Err(RangeError::EpochDayDays);
return Err(err!(
"adding `{amount}` to epoch day `{epoch_day}` \
resulted in `{sum}`, which is not in the required \
epoch day range of `{min}..={max}`",
min = IEpochDay::MIN.epoch_day,
max = IEpochDay::MAX.epoch_day,
));
}
Ok(ret)
}
@ -257,11 +260,14 @@ impl IDate {
year: i16,
month: i8,
day: i8,
) -> Result<IDate, RangeError> {
) -> Result<IDate, Error> {
if day > 28 {
let max_day = days_in_month(year, month);
if day > max_day {
return Err(RangeError::DateInvalidDays { year, month });
return Err(err!(
"day={day} is out of range for year={year} \
and month={month}, must be in range 1..={max_day}",
));
}
}
Ok(IDate { year, month, day })
@ -277,22 +283,37 @@ impl IDate {
pub(crate) fn from_day_of_year(
year: i16,
day: i16,
) -> Result<IDate, RangeError> {
) -> Result<IDate, Error> {
if !(1 <= day && day <= 366) {
return Err(RangeError::DateInvalidDayOfYear { year });
return Err(err!(
"day-of-year={day} is out of range for year={year}, \
must be in range 1..={max_day}",
max_day = days_in_year(year),
));
}
let start = IDate { year, month: 1, day: 1 }.to_epoch_day();
let end = start
.checked_add(i32::from(day) - 1)
// This can only happen when `year=9999` and `day=366`.
.map_err(|_| RangeError::DayOfYear)?
.map_err(|_| {
err!(
"failed to find date for \
year={year} and day-of-year={day}: \
adding `{day}` to `{start}` overflows \
Jiff's range",
start = start.epoch_day,
)
})?
.to_date();
// If we overflowed into the next year, then `day` is too big.
if year != end.year {
// Can only happen given day=366 and this is a leap year.
debug_assert_eq!(day, 366);
debug_assert!(!is_leap_year(year));
return Err(RangeError::DateInvalidDayOfYear { year });
return Err(err!(
"day-of-year={day} is out of range for year={year}, \
must be in range 1..={max_day}",
max_day = days_in_year(year),
));
}
Ok(end)
}
@ -308,9 +329,12 @@ impl IDate {
pub(crate) fn from_day_of_year_no_leap(
year: i16,
mut day: i16,
) -> Result<IDate, RangeError> {
) -> Result<IDate, Error> {
if !(1 <= day && day <= 365) {
return Err(RangeError::DateInvalidDayOfYearNoLeap);
return Err(err!(
"day-of-year={day} is out of range for year={year}, \
must be in range 1..=365",
));
}
if day >= 60 && is_leap_year(year) {
day += 1;
@ -368,9 +392,12 @@ impl IDate {
&self,
nth: i8,
weekday: IWeekday,
) -> Result<IDate, RangeError> {
) -> Result<IDate, Error> {
if nth == 0 || !(-5 <= nth && nth <= 5) {
return Err(RangeError::NthWeekdayOfMonth);
return Err(err!(
"got nth weekday of `{nth}`, but \
must be non-zero and in range `-5..=5`",
));
}
if nth > 0 {
let first_weekday = self.first_of_month().weekday();
@ -387,10 +414,13 @@ impl IDate {
// of `Day`, we can't let this boundary condition escape. So we
// check it here.
if day < 1 {
return Err(RangeError::DateInvalidDays {
year: self.year,
month: self.month,
});
return Err(err!(
"day={day} is out of range for year={year} \
and month={month}, must be in range 1..={max_day}",
year = self.year,
month = self.month,
max_day = days_in_month(self.year, self.month),
));
}
IDate::try_new(self.year, self.month, day)
}
@ -398,12 +428,16 @@ impl IDate {
/// Returns the day before this date.
#[inline]
pub(crate) fn yesterday(self) -> Result<IDate, RangeError> {
pub(crate) fn yesterday(self) -> Result<IDate, Error> {
if self.day == 1 {
if self.month == 1 {
let year = self.year - 1;
if year <= -10000 {
return Err(RangeError::Yesterday);
return Err(err!(
"returning yesterday for -9999-01-01 is not \
possible because it is less than Jiff's supported
minimum date",
));
}
return Ok(IDate { year, month: 12, day: 31 });
}
@ -416,12 +450,16 @@ impl IDate {
/// Returns the day after this date.
#[inline]
pub(crate) fn tomorrow(self) -> Result<IDate, RangeError> {
pub(crate) fn tomorrow(self) -> Result<IDate, Error> {
if self.day >= 28 && self.day == days_in_month(self.year, self.month) {
if self.month == 12 {
let year = self.year + 1;
if year >= 10000 {
return Err(RangeError::Tomorrow);
return Err(err!(
"returning tomorrow for 9999-12-31 is not \
possible because it is greater than Jiff's supported
maximum date",
));
}
return Ok(IDate { year, month: 1, day: 1 });
}
@ -433,20 +471,34 @@ impl IDate {
/// Returns the year one year before this date.
#[inline]
pub(crate) fn prev_year(self) -> Result<i16, RangeError> {
pub(crate) fn prev_year(self) -> Result<i16, Error> {
let year = self.year - 1;
if year <= -10_000 {
return Err(RangeError::YearPrevious);
return Err(err!(
"returning previous year for {year:04}-{month:02}-{day:02} is \
not possible because it is less than Jiff's supported \
minimum date",
year = self.year,
month = self.month,
day = self.day,
));
}
Ok(year)
}
/// Returns the year one year from this date.
#[inline]
pub(crate) fn next_year(self) -> Result<i16, RangeError> {
pub(crate) fn next_year(self) -> Result<i16, Error> {
let year = self.year + 1;
if year >= 10_000 {
return Err(RangeError::YearNext);
return Err(err!(
"returning next year for {year:04}-{month:02}-{day:02} is \
not possible because it is greater than Jiff's supported \
maximum date",
year = self.year,
month = self.month,
day = self.day,
));
}
Ok(year)
}
@ -456,7 +508,7 @@ impl IDate {
pub(crate) fn checked_add_days(
&self,
amount: i32,
) -> Result<IDate, RangeError> {
) -> Result<IDate, Error> {
match amount {
0 => Ok(*self),
-1 => self.yesterday(),
@ -668,84 +720,6 @@ pub(crate) enum IAmbiguousOffset {
Fold { before: IOffset, after: IOffset },
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum RangeError {
DateInvalidDayOfYear { year: i16 },
DateInvalidDayOfYearNoLeap,
DateInvalidDays { year: i16, month: i8 },
DateTimeSeconds,
DayOfYear,
EpochDayDays,
EpochDayI32,
NthWeekdayOfMonth,
Tomorrow,
YearNext,
YearPrevious,
Yesterday,
}
impl core::fmt::Display for RangeError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::RangeError::*;
match *self {
DateInvalidDayOfYear { year } => write!(
f,
"number of days for `{year:04}` is invalid, \
must be in range `1..={max_day}`",
max_day = days_in_year(year),
),
DateInvalidDayOfYearNoLeap => f.write_str(
"number of days is invalid, must be in range `1..=365`",
),
DateInvalidDays { year, month } => write!(
f,
"number of days for `{year:04}-{month:02}` is invalid, \
must be in range `1..={max_day}`",
max_day = days_in_month(year, month),
),
DateTimeSeconds => {
f.write_str("adding seconds to datetime overflowed")
}
DayOfYear => f.write_str("day of year is invalid"),
EpochDayDays => write!(
f,
"adding to epoch day resulted in a value outside \
the allowed range of `{min}..={max}`",
min = IEpochDay::MIN.epoch_day,
max = IEpochDay::MAX.epoch_day,
),
EpochDayI32 => f.write_str(
"adding to epoch day overflowed 32-bit signed integer",
),
NthWeekdayOfMonth => f.write_str(
"invalid nth weekday of month, \
must be non-zero and in range `-5..=5`",
),
Tomorrow => f.write_str(
"returning tomorrow for `9999-12-31` is not \
possible because it is greater than Jiff's supported
maximum date",
),
YearNext => f.write_str(
"creating a date for a year following `9999` is \
not possible because it is greater than Jiff's supported \
maximum date",
),
YearPrevious => f.write_str(
"creating a date for a year preceding `-9999` is \
not possible because it is less than Jiff's supported \
minimum date",
),
Yesterday => f.write_str(
"returning yesterday for `-9999-01-01` is not \
possible because it is less than Jiff's supported
minimum date",
),
}
}
}
/// Returns true if and only if the given year is a leap year.
///
/// A leap year is a year with 366 days. Typical years have 365 days.
@ -948,20 +922,4 @@ mod tests {
let d1 = IDate { year: 9999, month: 12, day: 31 };
assert_eq!(d1.tomorrow().ok(), None);
}
#[test]
fn from_day_of_year() {
assert_eq!(
IDate::from_day_of_year(9999, 365),
Ok(IDate { year: 9999, month: 12, day: 31 }),
);
assert_eq!(
IDate::from_day_of_year(9998, 366),
Err(RangeError::DateInvalidDayOfYear { year: 9998 }),
);
assert_eq!(
IDate::from_day_of_year(9999, 366),
Err(RangeError::DayOfYear),
);
}
}

View file

@ -1,4 +1,7 @@
// auto-generated by: jiff-cli generate shared
pub(crate) mod array_str;
pub(crate) mod error;
pub(crate) mod escape;
pub(crate) mod itime;
pub(crate) mod utf8;

View file

@ -1,59 +1,5 @@
// 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
@ -69,24 +15,25 @@ impl core::fmt::Display for Utf8Error {
/// *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, Utf8Error>> {
pub(crate) fn decode(bytes: &[u8]) -> Option<Result<char, &[u8]>> {
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(Utf8Error::new(bytes, err))),
Err(err) => {
return Some(Err(
&bytes[..err.error_len().unwrap_or_else(|| bytes.len())]
))
}
};
// OK because we guaranteed above that `string`
// must be non-empty. And thus, `str::chars` must
// yield at least one Unicode scalar value.
Some(Ok(string.chars().next().unwrap()))
}

View file

@ -1,6 +1,6 @@
[package]
name = "jiff-tzdb"
version = "0.1.5" #:version
version = "0.1.4" #:version
authors = ["Andrew Gallant <jamslam@gmail.com>"]
license = "Unlicense OR MIT"
homepage = "https://github.com/BurntSushi/jiff/tree/master/crates/jiff-tzdb"

View file

@ -1,4 +1,4 @@
pub(super) static VERSION: Option<&str> = Some(r"2025c");
pub(super) static VERSION: Option<&str> = Some(r"2025b");
pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range<usize>)] = &[
(r"Africa/Abidjan", 3982..4112),
@ -14,15 +14,15 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range<usize>)] = &[
(r"Africa/Blantyre", 3851..3982),
(r"Africa/Brazzaville", 10414..10594),
(r"Africa/Bujumbura", 3851..3982),
(r"Africa/Cairo", 162382..163691),
(r"Africa/Casablanca", 190185..192106),
(r"Africa/Cairo", 163461..164770),
(r"Africa/Casablanca", 189897..191818),
(r"Africa/Ceuta", 41827..42389),
(r"Africa/Conakry", 3982..4112),
(r"Africa/Dakar", 3982..4112),
(r"Africa/Dar_es_Salaam", 12063..12254),
(r"Africa/Djibouti", 12063..12254),
(r"Africa/Douala", 10414..10594),
(r"Africa/El_Aaiun", 186481..188307),
(r"Africa/El_Aaiun", 186193..188019),
(r"Africa/Freetown", 3982..4112),
(r"Africa/Gaborone", 3851..3982),
(r"Africa/Harare", 3851..3982),
@ -74,7 +74,7 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range<usize>)] = &[
(r"America/Argentina/Tucuman", 77548..78274),
(r"America/Argentina/Ushuaia", 71856..72564),
(r"America/Aruba", 9872..10049),
(r"America/Asuncion", 151478..152563),
(r"America/Asuncion", 152557..153642),
(r"America/Atikokan", 5647..5796),
(r"America/Atka", 122687..123656),
(r"America/Bahia", 65501..66183),
@ -94,13 +94,13 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range<usize>)] = &[
(r"America/Catamarca", 71148..71856),
(r"America/Cayenne", 6101..6252),
(r"America/Cayman", 5647..5796),
(r"America/Chicago", 184727..186481),
(r"America/Chicago", 184439..186193),
(r"America/Chihuahua", 64055..64746),
(r"America/Ciudad_Juarez", 66873..67591),
(r"America/Coral_Harbour", 5647..5796),
(r"America/Cordoba", 70440..71148),
(r"America/Costa_Rica", 16497..16729),
(r"America/Coyhaique", 167760..169122),
(r"America/Coyhaique", 167472..168834),
(r"America/Creston", 16966..17206),
(r"America/Cuiaba", 132233..133167),
(r"America/Curacao", 9872..10049),
@ -113,21 +113,21 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range<usize>)] = &[
(r"America/Edmonton", 133167..134137),
(r"America/Eirunepe", 30431..30867),
(r"America/El_Salvador", 11514..11690),
(r"America/Ensenada", 166393..167760),
(r"America/Fort_Nelson", 171986..173434),
(r"America/Ensenada", 149270..150349),
(r"America/Fort_Nelson", 171698..173146),
(r"America/Fort_Wayne", 36060..36591),
(r"America/Fortaleza", 37559..38043),
(r"America/Glace_Bay", 110783..111663),
(r"America/Godthab", 192106..193071),
(r"America/Goose_Bay", 176415..177995),
(r"America/Godthab", 191818..192783),
(r"America/Goose_Bay", 176127..177707),
(r"America/Grand_Turk", 108139..108992),
(r"America/Grenada", 9872..10049),
(r"America/Guadeloupe", 9872..10049),
(r"America/Guatemala", 15573..15785),
(r"America/Guayaquil", 9693..9872),
(r"America/Guyana", 10594..10775),
(r"America/Halifax", 179594..181266),
(r"America/Havana", 152563..153680),
(r"America/Halifax", 179306..180978),
(r"America/Havana", 153642..154759),
(r"America/Hermosillo", 17472..17730),
(r"America/Indiana/Indianapolis", 36060..36591),
(r"America/Indiana/Knox", 139996..141012),
@ -143,14 +143,14 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range<usize>)] = &[
(r"America/Jamaica", 21737..22076),
(r"America/Jujuy", 61970..62660),
(r"America/Juneau", 120800..121766),
(r"America/Kentucky/Louisville", 157318..158560),
(r"America/Kentucky/Louisville", 158397..159639),
(r"America/Kentucky/Monticello", 131261..132233),
(r"America/Knox_IN", 139996..141012),
(r"America/Kralendijk", 9872..10049),
(r"America/La_Paz", 7964..8134),
(r"America/Lima", 18282..18565),
(r"America/Los_Angeles", 159794..161088),
(r"America/Louisville", 157318..158560),
(r"America/Los_Angeles", 160873..162167),
(r"America/Louisville", 158397..159639),
(r"America/Lower_Princes", 9872..10049),
(r"America/Maceio", 38601..39103),
(r"America/Managua", 18565..18860),
@ -165,20 +165,20 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range<usize>)] = &[
(r"America/Metlakatla", 42389..42975),
(r"America/Mexico_City", 98432..99205),
(r"America/Miquelon", 40739..41289),
(r"America/Moncton", 174922..176415),
(r"America/Moncton", 174634..176127),
(r"America/Monterrey", 74688..75397),
(r"America/Montevideo", 128417..129386),
(r"America/Montreal", 181266..182983),
(r"America/Montreal", 180978..182695),
(r"America/Montserrat", 9872..10049),
(r"America/Nassau", 181266..182983),
(r"America/New_York", 182983..184727),
(r"America/Nipigon", 181266..182983),
(r"America/Nassau", 180978..182695),
(r"America/New_York", 182695..184439),
(r"America/Nipigon", 180978..182695),
(r"America/Nome", 123656..124631),
(r"America/Noronha", 36591..37075),
(r"America/North_Dakota/Beulah", 146140..147183),
(r"America/North_Dakota/Center", 134137..135127),
(r"America/North_Dakota/New_Salem", 135127..136117),
(r"America/Nuuk", 192106..193071),
(r"America/Nuuk", 191818..192783),
(r"America/Ojinaga", 67591..68309),
(r"America/Panama", 5647..5796),
(r"America/Pangnirtung", 106419..107274),
@ -189,24 +189,24 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range<usize>)] = &[
(r"America/Porto_Acre", 28274..28692),
(r"America/Porto_Velho", 26248..26642),
(r"America/Puerto_Rico", 9872..10049),
(r"America/Punta_Arenas", 156100..157318),
(r"America/Rainy_River", 161088..162382),
(r"America/Punta_Arenas", 157179..158397),
(r"America/Rainy_River", 162167..163461),
(r"America/Rankin_Inlet", 104795..105602),
(r"America/Recife", 37075..37559),
(r"America/Regina", 54642..55280),
(r"America/Resolute", 103988..104795),
(r"America/Rio_Branco", 28274..28692),
(r"America/Rosario", 70440..71148),
(r"America/Santa_Isabel", 166393..167760),
(r"America/Santa_Isabel", 149270..150349),
(r"America/Santarem", 27453..27862),
(r"America/Santiago", 196303..197657),
(r"America/Santiago", 196015..197369),
(r"America/Santo_Domingo", 19436..19753),
(r"America/Sao_Paulo", 137116..138068),
(r"America/Scoresbysund", 193071..194055),
(r"America/Scoresbysund", 192783..193767),
(r"America/Shiprock", 147183..148225),
(r"America/Sitka", 119844..120800),
(r"America/St_Barthelemy", 9872..10049),
(r"America/St_Johns", 188307..190185),
(r"America/St_Johns", 188019..189897),
(r"America/St_Kitts", 9872..10049),
(r"America/St_Lucia", 9872..10049),
(r"America/St_Thomas", 9872..10049),
@ -214,14 +214,14 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range<usize>)] = &[
(r"America/Swift_Current", 22418..22786),
(r"America/Tegucigalpa", 13299..13493),
(r"America/Thule", 30867..31322),
(r"America/Thunder_Bay", 181266..182983),
(r"America/Tijuana", 166393..167760),
(r"America/Toronto", 181266..182983),
(r"America/Thunder_Bay", 180978..182695),
(r"America/Tijuana", 149270..150349),
(r"America/Toronto", 180978..182695),
(r"America/Tortola", 9872..10049),
(r"America/Vancouver", 163691..165021),
(r"America/Vancouver", 164770..166100),
(r"America/Virgin", 9872..10049),
(r"America/Whitehorse", 141012..142041),
(r"America/Winnipeg", 161088..162382),
(r"America/Winnipeg", 162167..163461),
(r"America/Yakutat", 118898..119844),
(r"America/Yellowknife", 133167..134137),
(r"Antarctica/Casey", 18860..19147),
@ -261,23 +261,23 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range<usize>)] = &[
(r"Asia/Chungking", 25461..25854),
(r"Asia/Colombo", 14362..14609),
(r"Asia/Dacca", 14131..14362),
(r"Asia/Damascus", 158560..159794),
(r"Asia/Damascus", 159639..160873),
(r"Asia/Dhaka", 14131..14362),
(r"Asia/Dili", 8651..8821),
(r"Asia/Dubai", 4511..4644),
(r"Asia/Dushanbe", 23152..23518),
(r"Asia/Famagusta", 127477..128417),
(r"Asia/Gaza", 197657..200607),
(r"Asia/Gaza", 197369..200319),
(r"Asia/Harbin", 25461..25854),
(r"Asia/Hebron", 200607..203575),
(r"Asia/Hebron", 200319..203287),
(r"Asia/Ho_Chi_Minh", 15785..16021),
(r"Asia/Hong_Kong", 100821..101596),
(r"Asia/Hovd", 46530..47124),
(r"Asia/Irkutsk", 91581..92341),
(r"Asia/Istanbul", 153680..154880),
(r"Asia/Istanbul", 154759..155959),
(r"Asia/Jakarta", 14856..15104),
(r"Asia/Jayapura", 8480..8651),
(r"Asia/Jerusalem", 194055..195129),
(r"Asia/Jerusalem", 193767..194841),
(r"Asia/Kabul", 6859..7018),
(r"Asia/Kamchatka", 80467..81194),
(r"Asia/Karachi", 17206..17472),
@ -320,7 +320,7 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range<usize>)] = &[
(r"Asia/Tashkent", 22786..23152),
(r"Asia/Tbilisi", 53338..53967),
(r"Asia/Tehran", 103176..103988),
(r"Asia/Tel_Aviv", 194055..195129),
(r"Asia/Tel_Aviv", 193767..194841),
(r"Asia/Thimbu", 7171..7325),
(r"Asia/Thimphu", 7171..7325),
(r"Asia/Tokyo", 15360..15573),
@ -336,14 +336,14 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range<usize>)] = &[
(r"Asia/Yangon", 10227..10414),
(r"Asia/Yekaterinburg", 92341..93101),
(r"Asia/Yerevan", 73980..74688),
(r"Atlantic/Azores", 169122..170523),
(r"Atlantic/Azores", 168834..170235),
(r"Atlantic/Bermuda", 144073..145097),
(r"Atlantic/Canary", 33169..33647),
(r"Atlantic/Cape_Verde", 9164..9339),
(r"Atlantic/Faeroe", 29129..29570),
(r"Atlantic/Faroe", 29129..29570),
(r"Atlantic/Jan_Mayen", 63350..64055),
(r"Atlantic/Madeira", 165021..166393),
(r"Atlantic/Madeira", 166100..167472),
(r"Atlantic/Reykjavik", 3982..4112),
(r"Atlantic/South_Georgia", 3585..3717),
(r"Atlantic/St_Helena", 3982..4112),
@ -375,24 +375,24 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range<usize>)] = &[
(r"Brazil/DeNoronha", 36591..37075),
(r"Brazil/East", 137116..138068),
(r"Brazil/West", 27862..28274),
(r"Canada/Atlantic", 179594..181266),
(r"Canada/Central", 161088..162382),
(r"Canada/Eastern", 181266..182983),
(r"Canada/Atlantic", 179306..180978),
(r"Canada/Central", 162167..163461),
(r"Canada/Eastern", 180978..182695),
(r"Canada/Mountain", 133167..134137),
(r"Canada/Newfoundland", 188307..190185),
(r"Canada/Pacific", 163691..165021),
(r"Canada/Newfoundland", 188019..189897),
(r"Canada/Pacific", 164770..166100),
(r"Canada/Saskatchewan", 54642..55280),
(r"Canada/Yukon", 141012..142041),
(r"CET", 150375..151478),
(r"Chile/Continental", 196303..197657),
(r"Chile/EasterIsland", 195129..196303),
(r"CST6CDT", 184727..186481),
(r"Cuba", 152563..153680),
(r"CET", 151454..152557),
(r"Chile/Continental", 196015..197369),
(r"Chile/EasterIsland", 194841..196015),
(r"CST6CDT", 184439..186193),
(r"Cuba", 153642..154759),
(r"EET", 57918..58600),
(r"Egypt", 162382..163691),
(r"Eire", 173434..174922),
(r"Egypt", 163461..164770),
(r"Eire", 173146..174634),
(r"EST", 5647..5796),
(r"EST5EDT", 182983..184727),
(r"EST5EDT", 182695..184439),
(r"Etc/GMT", 113..224),
(r"Etc/GMT+0", 113..224),
(r"Etc/GMT+1", 3182..3295),
@ -428,44 +428,44 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range<usize>)] = &[
(r"Etc/Universal", 224..335),
(r"Etc/UTC", 224..335),
(r"Etc/Zulu", 224..335),
(r"Europe/Amsterdam", 150375..151478),
(r"Europe/Amsterdam", 151454..152557),
(r"Europe/Andorra", 23884..24273),
(r"Europe/Astrakhan", 81920..82646),
(r"Europe/Athens", 57918..58600),
(r"Europe/Belfast", 177995..179594),
(r"Europe/Belfast", 177707..179306),
(r"Europe/Belgrade", 34563..35041),
(r"Europe/Berlin", 63350..64055),
(r"Europe/Bratislava", 69009..69732),
(r"Europe/Brussels", 150375..151478),
(r"Europe/Brussels", 151454..152557),
(r"Europe/Bucharest", 57257..57918),
(r"Europe/Budapest", 97666..98432),
(r"Europe/Busingen", 35041..35538),
(r"Europe/Chisinau", 64746..65501),
(r"Europe/Copenhagen", 63350..64055),
(r"Europe/Dublin", 173434..174922),
(r"Europe/Gibraltar", 154880..156100),
(r"Europe/Guernsey", 177995..179594),
(r"Europe/Dublin", 173146..174634),
(r"Europe/Gibraltar", 155959..157179),
(r"Europe/Guernsey", 177707..179306),
(r"Europe/Helsinki", 32688..33169),
(r"Europe/Isle_of_Man", 177995..179594),
(r"Europe/Istanbul", 153680..154880),
(r"Europe/Jersey", 177995..179594),
(r"Europe/Isle_of_Man", 177707..179306),
(r"Europe/Istanbul", 154759..155959),
(r"Europe/Jersey", 177707..179306),
(r"Europe/Kaliningrad", 113459..114363),
(r"Europe/Kiev", 38043..38601),
(r"Europe/Kirov", 78274..79009),
(r"Europe/Kyiv", 38043..38601),
(r"Europe/Lisbon", 170523..171986),
(r"Europe/Lisbon", 170235..171698),
(r"Europe/Ljubljana", 34563..35041),
(r"Europe/London", 177995..179594),
(r"Europe/Luxembourg", 150375..151478),
(r"Europe/London", 177707..179306),
(r"Europe/Luxembourg", 151454..152557),
(r"Europe/Madrid", 111663..112560),
(r"Europe/Malta", 126549..127477),
(r"Europe/Mariehamn", 32688..33169),
(r"Europe/Minsk", 99205..100013),
(r"Europe/Monaco", 149270..150375),
(r"Europe/Monaco", 150349..151454),
(r"Europe/Moscow", 109875..110783),
(r"Europe/Nicosia", 44735..45332),
(r"Europe/Oslo", 63350..64055),
(r"Europe/Paris", 149270..150375),
(r"Europe/Paris", 150349..151454),
(r"Europe/Podgorica", 34563..35041),
(r"Europe/Prague", 69009..69732),
(r"Europe/Riga", 55280..55974),
@ -493,8 +493,8 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range<usize>)] = &[
(r"Europe/Zaporozhye", 38043..38601),
(r"Europe/Zurich", 35041..35538),
(r"Factory", 0..113),
(r"GB", 177995..179594),
(r"GB-Eire", 177995..179594),
(r"GB", 177707..179306),
(r"GB-Eire", 177707..179306),
(r"GMT", 113..224),
(r"GMT+0", 113..224),
(r"GMT-0", 113..224),
@ -515,13 +515,13 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range<usize>)] = &[
(r"Indian/Mayotte", 12063..12254),
(r"Indian/Reunion", 4511..4644),
(r"Iran", 103176..103988),
(r"Israel", 194055..195129),
(r"Israel", 193767..194841),
(r"Jamaica", 21737..22076),
(r"Japan", 15360..15573),
(r"Kwajalein", 12882..13101),
(r"Libya", 29570..30001),
(r"MET", 150375..151478),
(r"Mexico/BajaNorte", 166393..167760),
(r"MET", 151454..152557),
(r"Mexico/BajaNorte", 149270..150349),
(r"Mexico/BajaSur", 66183..66873),
(r"Mexico/General", 98432..99205),
(r"MST", 16966..17206),
@ -534,7 +534,7 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range<usize>)] = &[
(r"Pacific/Bougainville", 12254..12455),
(r"Pacific/Chatham", 100013..100821),
(r"Pacific/Chuuk", 6705..6859),
(r"Pacific/Easter", 195129..196303),
(r"Pacific/Easter", 194841..196015),
(r"Pacific/Efate", 22076..22418),
(r"Pacific/Enderbury", 8134..8306),
(r"Pacific/Fakaofo", 5796..5949),
@ -574,29 +574,29 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range<usize>)] = &[
(r"Pacific/Wallis", 3717..3851),
(r"Pacific/Yap", 6705..6859),
(r"Poland", 116167..117090),
(r"Portugal", 170523..171986),
(r"Portugal", 170235..171698),
(r"PRC", 25461..25854),
(r"PST8PDT", 159794..161088),
(r"PST8PDT", 160873..162167),
(r"ROC", 39103..39614),
(r"ROK", 27038..27453),
(r"Singapore", 15104..15360),
(r"Turkey", 153680..154880),
(r"Turkey", 154759..155959),
(r"UCT", 224..335),
(r"Universal", 224..335),
(r"US/Alaska", 124631..125608),
(r"US/Aleutian", 122687..123656),
(r"US/Arizona", 16966..17206),
(r"US/Central", 184727..186481),
(r"US/Central", 184439..186193),
(r"US/East-Indiana", 36060..36591),
(r"US/Eastern", 182983..184727),
(r"US/Eastern", 182695..184439),
(r"US/Hawaii", 13910..14131),
(r"US/Indiana-Starke", 139996..141012),
(r"US/Michigan", 112560..113459),
(r"US/Mountain", 147183..148225),
(r"US/Pacific", 159794..161088),
(r"US/Pacific", 160873..162167),
(r"US/Samoa", 5197..5343),
(r"UTC", 224..335),
(r"W-SU", 109875..110783),
(r"WET", 170523..171986),
(r"WET", 170235..171698),
(r"Zulu", 224..335),
];

4
fuzz/.gitignore vendored
View file

@ -1,4 +0,0 @@
target
corpus
artifacts
coverage

244
fuzz/Cargo.lock generated
View file

@ -1,244 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
dependencies = [
"derive_arbitrary",
]
[[package]]
name = "cc"
version = "1.2.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215"
dependencies = [
"find-msvc-tools",
"jobserver",
"libc",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "derive_arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
[[package]]
name = "getrandom"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasip2",
]
[[package]]
name = "jiff"
version = "0.2.16"
dependencies = [
"jiff-static",
"jiff-tzdb-platform",
"log",
"portable-atomic",
"portable-atomic-util",
"serde_core",
"windows-sys",
]
[[package]]
name = "jiff-fuzz"
version = "0.0.0"
dependencies = [
"jiff",
"libfuzzer-sys",
]
[[package]]
name = "jiff-static"
version = "0.2.16"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "jiff-tzdb"
version = "0.1.5"
[[package]]
name = "jiff-tzdb-platform"
version = "0.1.3"
dependencies = [
"jiff-tzdb",
]
[[package]]
name = "jobserver"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
"getrandom",
"libc",
]
[[package]]
name = "libc"
version = "0.2.178"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
[[package]]
name = "libfuzzer-sys"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404"
dependencies = [
"arbitrary",
"cc",
]
[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "portable-atomic"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
[[package]]
name = "portable-atomic-util"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
dependencies = [
"portable-atomic",
]
[[package]]
name = "proc-macro2"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "syn"
version = "2.0.111"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "wasip2"
version = "1.0.1+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "wit-bindgen"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"

View file

@ -1,44 +0,0 @@
[package]
name = "jiff-fuzz"
version = "0.0.0"
publish = false
edition = "2021"
[features]
relaxed = []
[package.metadata]
cargo-fuzz = true
[dependencies]
libfuzzer-sys = { version = "0.4", features = ["arbitrary-derive"] }
[dependencies.jiff]
path = ".."
[workspace]
members = ["."]
[[bin]]
name = "rfc2822_parse"
path = "fuzz_targets/rfc2822_parse.rs"
test = false
doc = false
bench = false
[[bin]]
name = "strtime_parse"
path = "fuzz_targets/strtime_parse.rs"
test = false
doc = false
bench = false
[[bin]]
name = "temporal_parse"
path = "fuzz_targets/temporal_parse.rs"
test = false
doc = false
bench = false
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }

View file

@ -1,50 +0,0 @@
#![cfg_attr(fuzzing, no_main)]
use std::borrow::Cow;
use libfuzzer_sys::fuzz_target;
use jiff::fmt::rfc2822;
mod shim;
fn do_fuzz(data: &[u8]) {
const RFC2822_PARSER: rfc2822::DateTimeParser =
rfc2822::DateTimeParser::new();
const RFC2822_PRINTER: rfc2822::DateTimePrinter =
rfc2822::DateTimePrinter::new();
let Ok(first) = RFC2822_PARSER.parse_zoned(data) else { return };
let mut unparsed = Vec::with_capacity(data.len());
RFC2822_PRINTER
.print_zoned(&first, &mut unparsed)
.expect("We parsed it, so we should be able to print it");
match RFC2822_PARSER.parse_zoned(&unparsed) {
Ok(second) => {
assert_eq!(
first, second,
"expected the initially parsed value \
to be equal to the value after printing and re-parsing",
);
}
Err(e) if cfg!(not(feature = "relaxed")) => {
let unparsed_str = String::from_utf8_lossy(&unparsed);
panic!(
"should be able to parse a printed value; \
failed with `{e}` at: `{unparsed_str}`{}, \
corresponding to {first:?}",
if matches!(unparsed_str, Cow::Owned(_)) {
Cow::from(format!(" (lossy; actual bytes: {unparsed:?})"))
} else {
Cow::from("")
}
);
}
Err(_) => {}
}
}
fuzz_target!(|data: &[u8]| do_fuzz(data));
maybe_define_main!();

View file

@ -1,48 +0,0 @@
use std::{
error::Error,
ffi::c_int,
{env, fs, ptr},
};
extern "C" {
// Initializer provided by libfuzzer-sys for creating an
// appropriate panic hook.
fn LLVMFuzzerInitialize(
argc: *const isize,
argv: *const *const *const u8,
) -> c_int;
// This is a magic function defined by libfuzzer-sys; use for replay.
#[allow(improper_ctypes)]
fn rust_fuzzer_test_input(input: &[u8]) -> i32;
}
#[allow(unused)]
pub fn main() -> Result<(), Box<dyn Error>> {
let mut count = 0usize;
unsafe {
let _ = LLVMFuzzerInitialize(ptr::null(), ptr::null());
}
for testcase in env::args_os().skip(1) {
let content = fs::read(testcase)?;
unsafe {
let _ = rust_fuzzer_test_input(&content);
}
count += 1;
}
println!("Executed {count} testcases successfully!");
if count == 0 {
println!("Did you mean to specify a testcase?");
}
Ok(())
}
#[macro_export]
macro_rules! maybe_define_main {
() => {
#[cfg(not(fuzzing))]
fn main() {
let _ = $crate::shim::main();
}
};
}

View file

@ -1,103 +0,0 @@
#![cfg_attr(fuzzing, no_main)]
use std::borrow::Cow;
use libfuzzer_sys::{
arbitrary,
arbitrary::{Arbitrary, Unstructured},
fuzz_target,
};
use jiff::fmt::strtime::parse;
mod shim;
#[derive(Debug)]
struct Input<'a> {
format: &'a str,
input: &'a str,
}
impl<'a> Arbitrary<'a> for Input<'a> {
fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
let fmt_len: u8 = u.arbitrary()?;
let in_len: u16 = u.arbitrary()?;
let format = u.bytes(fmt_len as usize)?;
let format = core::str::from_utf8(format)
.map_err(|_| arbitrary::Error::IncorrectFormat)?;
let input = u.bytes(in_len as usize)?;
let input = core::str::from_utf8(input)
.map_err(|_| arbitrary::Error::IncorrectFormat)?;
Ok(Input { format, input })
}
fn arbitrary_take_rest(
mut u: Unstructured<'a>,
) -> arbitrary::Result<Self> {
let len: u8 = u.arbitrary()?;
// ignored in take rest, but keep it consistent
let _in_len: u16 = u.arbitrary()?;
let format = u.bytes(len as usize)?;
let format = core::str::from_utf8(format)
.map_err(|_| arbitrary::Error::IncorrectFormat)?;
let input = u.take_rest();
let input = core::str::from_utf8(input)
.map_err(|_| arbitrary::Error::IncorrectFormat)?;
Ok(Input { format, input })
}
}
fn do_fuzz(src: Input) {
if let Ok(first) = parse(src.format, src.input) {
let mut unparsed = Vec::with_capacity(src.input.len());
if first.format(src.format, &mut unparsed).is_err() {
// There are a number of reasons why this may fail. the formatter
// is simply not as strong as the parser, so we accept failure
// here.
return;
}
match parse(src.format, &unparsed) {
Ok(second) => {
// There's not a direct equality here. To get around this, we
// compare unparsed with doubly-unparsed.
let mut unparsed_again = Vec::with_capacity(unparsed.len());
second.format(src.format, &mut unparsed_again).expect(
"We parsed it (twice!), so we should be able to print it",
);
assert_eq!(
unparsed,
unparsed_again,
"expected the initially parsed value \
to be equal to the value after \
printing and re-parsing; \
found `{}', expected `{}'",
String::from_utf8_lossy(&unparsed_again),
String::from_utf8_lossy(&unparsed),
);
}
Err(e) if cfg!(not(feature = "relaxed")) => {
let unparsed_str = String::from_utf8_lossy(&unparsed);
panic!(
"should be able to parse a printed value; \
failed with `{e}` at: `{unparsed_str}`{}, \
corresponding to {first:?}",
if matches!(unparsed_str, Cow::Owned(_)) {
Cow::from(format!(
" (lossy; actual bytes: {unparsed:?})"
))
} else {
Cow::from("")
}
);
}
Err(_) => {}
}
}
}
fuzz_target!(|data: Input<'_>| do_fuzz(data));
maybe_define_main!();

View file

@ -1,51 +0,0 @@
#![cfg_attr(fuzzing, no_main)]
use std::borrow::Cow;
use libfuzzer_sys::fuzz_target;
use jiff::fmt::temporal;
mod shim;
fn do_fuzz(data: &[u8]) {
const TEMPORAL_PARSER: temporal::SpanParser = temporal::SpanParser::new();
const TEMPORAL_PRINTER: temporal::SpanPrinter =
temporal::SpanPrinter::new();
let Ok(first) = TEMPORAL_PARSER.parse_span(data) else { return };
// get a good start at least
let mut unparsed = Vec::with_capacity(data.len());
TEMPORAL_PRINTER
.print_span(&first, &mut unparsed)
.expect("we parsed it, so we should be able to print it");
match TEMPORAL_PARSER.parse_span(&unparsed) {
Ok(second) => {
assert_eq!(
first,
second.fieldwise(),
"expected the initially parsed value \
to be equal to the value after printing and re-parsing",
);
}
Err(e) if cfg!(not(feature = "relaxed")) => {
let unparsed_str = String::from_utf8_lossy(&unparsed);
panic!(
"should be able to parse a printed value; \
failed with `{e}` at: `{unparsed_str}`{}, \
corresponding to {first:?}",
if matches!(unparsed_str, Cow::Owned(_)) {
Cow::from(format!(" (lossy; actual bytes: {unparsed:?})"))
} else {
Cow::from("")
}
);
}
Err(_) => {}
}
}
fuzz_target!(|data: &[u8]| do_fuzz(data));
maybe_define_main!();

View file

@ -3,7 +3,7 @@ use core::time::Duration as UnsignedDuration;
use crate::{
civil::{DateTime, Era, ISOWeekDate, Time, Weekday},
duration::{Duration, SDuration},
error::{civil::Error as E, Error, ErrorContext},
error::{err, Error, ErrorContext},
fmt::{
self,
temporal::{DEFAULT_DATETIME_PARSER, DEFAULT_DATETIME_PRINTER},
@ -903,9 +903,7 @@ impl Date {
let weekday = weekday.to_iweekday();
let idate = self.to_idate_const();
Ok(Date::from_idate_const(
idate
.nth_weekday_of_month(nth, weekday)
.map_err(Error::itime_range)?,
idate.nth_weekday_of_month(nth, weekday).map_err(Error::shared)?,
))
}
@ -1059,7 +1057,7 @@ impl Date {
let nth = t::SpanWeeks::try_new("nth weekday", nth)?;
if nth == C(0) {
Err(Error::slim_range("nth weekday"))
Err(err!("nth weekday cannot be `0`"))
} else if nth > C(0) {
let nth = nth.max(C(1));
let weekday_diff = weekday.since_ranged(self.weekday().next());
@ -1517,8 +1515,14 @@ impl Date {
-1 => self.yesterday(),
1 => self.tomorrow(),
days => {
let days = UnixEpochDay::try_new("days", days)
.context(E::OverflowDaysDuration)?;
let days = UnixEpochDay::try_new("days", days).with_context(
|| {
err!(
"{days} computed from duration {duration:?} \
overflows Jiff's datetime limits",
)
},
)?;
let days =
self.to_unix_epoch_day().try_checked_add("days", days)?;
Ok(Date::from_unix_epoch_day(days))
@ -2937,9 +2941,11 @@ impl DateDifference {
//
// NOTE: I take the above back. It's actually possible for the
// months component to overflow when largest=month.
return Err(Error::from(E::RoundMustUseDaysOrBigger {
unit: largest,
}));
return Err(err!(
"rounding the span between two dates must use days \
or bigger for its units, but found {units}",
units = largest.plural(),
));
}
if largest <= Unit::Week {
let mut weeks = t::SpanWeeks::rfrom(C(0));
@ -3191,13 +3197,13 @@ impl DateWith {
Some(DateWithDay::OfYear(day)) => {
let year = year.get_unchecked();
let idate = IDate::from_day_of_year(year, day)
.map_err(Error::itime_range)?;
.map_err(Error::shared)?;
return Ok(Date::from_idate_const(idate));
}
Some(DateWithDay::OfYearNoLeap(day)) => {
let year = year.get_unchecked();
let idate = IDate::from_day_of_year_no_leap(year, day)
.map_err(Error::itime_range)?;
.map_err(Error::shared)?;
return Ok(Date::from_idate_const(idate));
}
};

View file

@ -5,7 +5,7 @@ use crate::{
datetime, Date, DateWith, Era, ISOWeekDate, Time, TimeWith, Weekday,
},
duration::{Duration, SDuration},
error::{civil::Error as E, Error, ErrorContext},
error::{err, Error, ErrorContext},
fmt::{
self,
temporal::{self, DEFAULT_DATETIME_PARSER},
@ -1695,18 +1695,25 @@ impl DateTime {
{
(true, true) => Ok(self),
(false, true) => {
let new_date = old_date
.checked_add(span)
.context(E::FailedAddSpanDate)?;
let new_date =
old_date.checked_add(span).with_context(|| {
err!("failed to add {span} to {old_date}")
})?;
Ok(DateTime::from_parts(new_date, old_time))
}
(true, false) => {
let (new_time, leftovers) = old_time
.overflowing_add(span)
.context(E::FailedAddSpanTime)?;
let new_date = old_date
.checked_add(leftovers)
.context(E::FailedAddSpanOverflowing)?;
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}",
)
})?;
Ok(DateTime::from_parts(new_date, new_time))
}
(false, false) => self.checked_add_span_general(&span),
@ -1720,14 +1727,20 @@ 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)
.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)?;
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}",
)
})?;
Ok(DateTime::from_parts(new_date, new_time))
}
@ -1738,9 +1751,13 @@ 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)
.context(E::FailedAddDurationOverflowing)?;
let new_date = date.checked_add(leftovers).with_context(|| {
err!(
"failed to add overflowing signed duration, {leftovers:?}, \
from adding {duration:?} to {time},
to {date}",
)
})?;
Ok(DateTime::from_parts(new_date, new_time))
}
@ -3535,10 +3552,9 @@ impl DateTimeRound {
// it for good reasons.
match self.smallest {
Unit::Year | Unit::Month | Unit::Week => {
return Err(Error::from(
crate::error::util::RoundingIncrementError::Unsupported {
unit: self.smallest,
},
return Err(err!(
"rounding datetimes does not support {unit}",
unit = self.smallest.plural()
));
}
// We don't do any rounding in this case, so just bail now.
@ -3576,7 +3592,9 @@ impl DateTimeRound {
// supported datetimes.
let end = start
.checked_add(Span::new().days_ranged(days_len))
.context(E::FailedAddDays)?;
.with_context(|| {
err!("adding {days_len} days to {start} failed")
})?;
Ok(DateTime::from_parts(end, time))
}

View file

@ -1,7 +1,6 @@
use crate::{
civil::{Date, DateTime, Weekday},
error::{civil::Error as E, Error},
fmt::temporal::{DEFAULT_DATETIME_PARSER, DEFAULT_DATETIME_PRINTER},
error::{err, Error},
util::{
rangeint::RInto,
t::{self, ISOWeek, ISOYear, C},
@ -36,51 +35,6 @@ use crate::{
/// specifically want a week oriented calendar, it's likely that you'll never
/// need to care about this type.
///
/// # Parsing and printing
///
/// The `ISOWeekDate` type provides convenient trait implementations of
/// [`std::str::FromStr`] and [`std::fmt::Display`]. These use the format
/// specified by ISO 8601 for week dates:
///
/// ```
/// use jiff::civil::ISOWeekDate;
///
/// let week_date: ISOWeekDate = "2024-W24-7".parse()?;
/// assert_eq!(week_date.to_string(), "2024-W24-7");
/// assert_eq!(week_date.date().to_string(), "2024-06-16");
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
///
/// ISO 8601 allows the `-` separator to be absent:
///
/// ```
/// use jiff::civil::ISOWeekDate;
///
/// let week_date: ISOWeekDate = "2024W241".parse()?;
/// assert_eq!(week_date.to_string(), "2024-W24-1");
/// assert_eq!(week_date.date().to_string(), "2024-06-10");
///
/// // But you cannot mix and match. Either `-` separates
/// // both the year and week, or neither.
/// assert!("2024W24-1".parse::<ISOWeekDate>().is_err());
/// assert!("2024-W241".parse::<ISOWeekDate>().is_err());
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
///
/// And the `W` may also be lowercase:
///
/// ```
/// use jiff::civil::ISOWeekDate;
///
/// let week_date: ISOWeekDate = "2024-w24-2".parse()?;
/// assert_eq!(week_date.to_string(), "2024-W24-2");
/// assert_eq!(week_date.date().to_string(), "2024-06-11");
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
///
/// # Default value
///
/// For convenience, this type implements the `Default` trait. Its default
@ -711,7 +665,9 @@ 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(Error::from(E::InvalidISOWeekNumber));
return Err(err!(
"ISO week number `{week}` is invalid for year `{year}`"
));
}
// And also, the maximum Date constrains what we can utter with
// ISOWeekDate so that we can preserve infallible conversions between
@ -791,24 +747,6 @@ impl core::fmt::Debug for ISOWeekDate {
}
}
impl core::fmt::Display for ISOWeekDate {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use crate::fmt::StdFmtWrite;
DEFAULT_DATETIME_PRINTER
.print_iso_week_date(self, StdFmtWrite(f))
.map_err(|_| core::fmt::Error)
}
}
impl core::str::FromStr for ISOWeekDate {
type Err = Error;
fn from_str(string: &str) -> Result<ISOWeekDate, Error> {
DEFAULT_DATETIME_PARSER.parse_iso_week_date(string)
}
}
impl Eq for ISOWeekDate {}
impl PartialEq for ISOWeekDate {
@ -870,60 +808,6 @@ impl<'a> From<&'a Zoned> for ISOWeekDate {
}
}
#[cfg(feature = "serde")]
impl serde_core::Serialize for ISOWeekDate {
#[inline]
fn serialize<S: serde_core::Serializer>(
&self,
serializer: S,
) -> Result<S::Ok, S::Error> {
serializer.collect_str(self)
}
}
#[cfg(feature = "serde")]
impl<'de> serde_core::Deserialize<'de> for ISOWeekDate {
#[inline]
fn deserialize<D: serde_core::Deserializer<'de>>(
deserializer: D,
) -> Result<ISOWeekDate, D::Error> {
use serde_core::de;
struct ISOWeekDateVisitor;
impl<'de> de::Visitor<'de> for ISOWeekDateVisitor {
type Value = ISOWeekDate;
fn expecting(
&self,
f: &mut core::fmt::Formatter,
) -> core::fmt::Result {
f.write_str("an ISO 8601 week date string")
}
#[inline]
fn visit_bytes<E: de::Error>(
self,
value: &[u8],
) -> Result<ISOWeekDate, E> {
DEFAULT_DATETIME_PARSER
.parse_iso_week_date(value)
.map_err(de::Error::custom)
}
#[inline]
fn visit_str<E: de::Error>(
self,
value: &str,
) -> Result<ISOWeekDate, E> {
self.visit_bytes(value.as_bytes())
}
}
deserializer.deserialize_str(ISOWeekDateVisitor)
}
}
#[cfg(test)]
impl quickcheck::Arbitrary for ISOWeekDate {
fn arbitrary(g: &mut quickcheck::Gen) -> ISOWeekDate {

View file

@ -3,7 +3,7 @@ use core::time::Duration as UnsignedDuration;
use crate::{
civil::{Date, DateTime},
duration::{Duration, SDuration},
error::{civil::Error as E, Error, ErrorContext},
error::{err, Error, ErrorContext},
fmt::{
self,
temporal::{self, DEFAULT_DATETIME_PARSER},
@ -950,6 +950,7 @@ 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
@ -957,7 +958,15 @@ 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)
.context(E::OverflowTimeNanoseconds)?;
.with_context(|| {
err!(
"adding signed duration {duration:?}, equal to
{nanos} nanoseconds, to {time} overflowed",
duration = original,
nanos = original.as_nanos(),
time = self,
)
})?;
Ok(Time::from_nanosecond(end))
}
@ -2594,9 +2603,11 @@ impl TimeDifference {
}
let largest = self.round.get_largest().unwrap_or(Unit::Hour);
if largest > Unit::Hour {
return Err(Error::from(E::RoundMustUseHoursOrSmaller {
unit: largest,
}));
return Err(err!(
"rounding the span between two times must use hours \
or smaller for its units, but found {units}",
units = largest.plural(),
));
}
let start = t1.to_nanosecond();
let end = t2.to_nanosecond();
@ -3001,13 +3012,22 @@ impl TimeWith {
None => self.original.subsec_nanosecond_ranged(),
Some(subsec_nanosecond) => {
if self.millisecond.is_some() {
return Err(Error::from(E::IllegalTimeWithMillisecond));
return Err(err!(
"cannot set both TimeWith::millisecond \
and TimeWith::subsec_nanosecond",
));
}
if self.microsecond.is_some() {
return Err(Error::from(E::IllegalTimeWithMicrosecond));
return Err(err!(
"cannot set both TimeWith::microsecond \
and TimeWith::subsec_nanosecond",
));
}
if self.nanosecond.is_some() {
return Err(Error::from(E::IllegalTimeWithNanosecond));
return Err(err!(
"cannot set both TimeWith::nanosecond \
and TimeWith::subsec_nanosecond",
));
}
SubsecNanosecond::try_new(
"subsec_nanosecond",

View file

@ -1,7 +1,7 @@
use core::time::Duration as UnsignedDuration;
use crate::{
error::{duration::Error as E, ErrorContext},
error::{err, ErrorContext},
Error, SignedDuration, Span,
};
@ -24,8 +24,12 @@ 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)
.context(E::RangeUnsignedDuration)?;
let sdur =
SignedDuration::try_from(udur).with_context(|| {
err!(
"unsigned duration {udur:?} exceeds Jiff's limits"
)
})?;
Ok(SDuration::Absolute(sdur))
}
}
@ -87,8 +91,9 @@ 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)
.context(E::FailedNegateUnsignedDuration)?
-SignedDuration::try_from(udur).with_context(|| {
err!("failed to negate unsigned duration {udur:?}")
})?
};
Ok(Duration::Signed(sdur))
}

View file

@ -1,14 +1,16 @@
use crate::util::sync::Arc;
use crate::{shared::util::error::Error as SharedError, util::sync::Arc};
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;
/// 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;
/// An error that can occur in this crate.
///
@ -27,11 +29,8 @@ pub(crate) mod zoned;
///
/// Other than implementing the [`std::error::Error`] trait when the
/// `std` feature is enabled, the [`core::fmt::Debug`] trait and the
/// [`core::fmt::Display`] trait, this error type currently provides
/// very limited introspection capabilities. Simple predicates like
/// `Error::is_range` are provided, but the predicates are not
/// exhaustive. That is, there exist some errors that do not return
/// `true` for any of the `Error::is_*` predicates.
/// [`core::fmt::Display`] trait, this error type currently provides no
/// introspection capabilities.
///
/// # Design
///
@ -66,6 +65,50 @@ 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`.
///
@ -77,14 +120,6 @@ impl Error {
/// circumstances, it can be convenient to manufacture a Jiff error value
/// specifically.
///
/// # Core-only environments
///
/// In core-only environments without a dynamic memory allocator, error
/// messages may be degraded in some cases. For example, if the given
/// `core::fmt::Arguments` could not be converted to a simple borrowed
/// `&str`, then this will ignore the input given and return an "unknown"
/// Jiff error.
///
/// # Example
///
/// ```
@ -97,114 +132,79 @@ impl Error {
Error::from(ErrorKind::Adhoc(AdhocError::from_args(message)))
}
/// Returns true when this error originated as a result of a value being
/// out of Jiff's supported range.
///
/// # Example
///
/// ```
/// use jiff::civil::Date;
///
/// assert!(Date::new(2025, 2, 29).unwrap_err().is_range());
/// assert!("2025-02-29".parse::<Date>().unwrap_err().is_range());
/// assert!(Date::strptime("%Y-%m-%d", "2025-02-29").unwrap_err().is_range());
/// ```
pub fn is_range(&self) -> bool {
use self::ErrorKind::*;
matches!(*self.root().kind(), Range(_) | SlimRange(_) | ITimeRange(_))
}
/// Returns true when this error originated as a result of an invalid
/// configuration of parameters to a function call.
///
/// This particular error category is somewhat nebulous, but it's generally
/// meant to cover errors that _could_ have been statically prevented by
/// Jiff with more types in its API. Instead, a smaller API is preferred.
///
/// # Example: invalid rounding options
///
/// ```
/// use jiff::{SpanRound, ToSpan, Unit};
///
/// let span = 44.seconds();
/// let err = span.round(
/// SpanRound::new().smallest(Unit::Second).increment(45),
/// ).unwrap_err();
/// // Rounding increments for seconds must divide evenly into `60`.
/// // But `45` does not. Thus, this is a "configuration" error.
/// assert!(err.is_invalid_parameter());
/// ```
///
/// # Example: invalid units
///
/// One cannot round a span between dates to units less than days:
///
/// ```
/// use jiff::{civil::date, Unit};
///
/// let date1 = date(2025, 3, 18);
/// let date2 = date(2025, 12, 21);
/// let err = date1.until((Unit::Hour, date2)).unwrap_err();
/// assert!(err.is_invalid_parameter());
/// ```
///
/// Similarly, one cannot round a span between times to units greater than
/// hours:
///
/// ```
/// use jiff::{civil::time, Unit};
///
/// let time1 = time(9, 39, 0, 0);
/// let time2 = time(17, 0, 0, 0);
/// let err = time1.until((Unit::Day, time2)).unwrap_err();
/// assert!(err.is_invalid_parameter());
/// ```
pub fn is_invalid_parameter(&self) -> bool {
use self::ErrorKind::*;
use self::{
civil::Error as CivilError, span::Error as SpanError,
tz::offset::Error as OffsetError, util::RoundingIncrementError,
};
matches!(
*self.root().kind(),
RoundingIncrement(
RoundingIncrementError::GreaterThanZero { .. }
| RoundingIncrementError::InvalidDivide { .. }
| RoundingIncrementError::Unsupported { .. }
) | Span(
SpanError::NotAllowedCalendarUnits { .. }
| SpanError::NotAllowedLargestSmallerThanSmallest { .. }
| SpanError::RequiresRelativeWeekOrDay { .. }
| SpanError::RequiresRelativeYearOrMonth { .. }
| SpanError::RequiresRelativeYearOrMonthGivenDaysAre24Hours { .. }
) | Civil(
CivilError::IllegalTimeWithMicrosecond
| CivilError::IllegalTimeWithMillisecond
| CivilError::IllegalTimeWithNanosecond
| CivilError::RoundMustUseDaysOrBigger { .. }
| CivilError::RoundMustUseHoursOrSmaller { .. }
) | TzOffset(OffsetError::RoundInvalidUnit { .. })
)
}
/// Returns true when this error originated as a result of an operation
/// failing because an appropriate Jiff crate feature was not enabled.
///
/// # Example
///
/// ```ignore
/// use jiff::tz::TimeZone;
///
/// // This passes when the `tz-system` crate feature is NOT enabled.
/// assert!(TimeZone::try_system().unwrap_err().is_crate_feature());
/// ```
pub fn is_crate_feature(&self) -> bool {
matches!(*self.root().kind(), ErrorKind::CrateFeature(_))
#[inline(never)]
#[cold]
fn context_impl(self, consequent: Error) -> Error {
#[cfg(feature = "alloc")]
{
let mut err = consequent;
if err.inner.is_none() {
err = err!("unknown jiff error");
}
let inner = err.inner.as_mut().unwrap();
assert!(
inner.cause.is_none(),
"cause of consequence must be `None`"
);
// OK because we just created this error so the Arc
// has one reference.
Arc::get_mut(inner).unwrap().cause = Some(self);
err
}
#[cfg(not(feature = "alloc"))]
{
// We just completely drop `self`. :-(
consequent
}
}
}
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,36 +220,9 @@ 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 itime_range(
err: crate::shared::util::itime::RangeError,
) -> Error {
Error::from(ErrorKind::ITimeRange(err))
}
/// Creates a new error from the special TZif error type.
#[cfg(feature = "alloc")]
pub(crate) fn tzif(err: crate::shared::tzif::TzifError) -> Error {
Error::from(ErrorKind::Tzif(err))
}
/// Creates a new error from the special `PosixTimeZoneError` type.
pub(crate) fn posix_tz(
err: crate::shared::posix::PosixTimeZoneError,
) -> Error {
Error::from(ErrorKind::PosixTz(err))
pub(crate) fn shared(err: SharedError) -> Error {
Error::from(ErrorKind::Shared(err))
}
/// A convenience constructor for building an I/O error.
@ -285,7 +258,7 @@ impl Error {
///
/// The benefit of this API is that it permits creating an `Error` in a
/// `const` context. But the error message quality is currently pretty
/// bad: it's just a generic "unknown Jiff error" message.
/// bad: it's just a generic "unknown jiff error" message.
///
/// This could be improved to take a `&'static str`, but I believe this
/// will require pointer tagging in order to avoid increasing the size of
@ -295,83 +268,6 @@ impl Error {
Error { inner: None }
}
*/
#[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 {
#[cfg(feature = "alloc")]
{
let mut err = _consequent;
if err.inner.is_none() {
err = Error::from(ErrorKind::Unknown);
}
let inner = err.inner.as_mut().unwrap();
assert!(
inner.cause.is_none(),
"cause of consequence must be `None`"
);
// OK because we just created this error so the Arc
// has one reference.
Arc::get_mut(inner).unwrap().cause = Some(self);
err
}
#[cfg(not(feature = "alloc"))]
{
// We just completely drop `self`. :-(
//
// 2025-12-21: ... actually, we used to drop self, but this
// ends up dropping the root cause. And the root cause
// is how the predicates on `Error` work. So we drop the
// consequent instead.
self
}
}
/// Returns the root error in this chain.
fn root(&self) -> &Error {
// OK because `Error::chain` is guaranteed to return a non-empty
// iterator.
self.chain().last().unwrap()
}
/// Returns a chain of error values.
///
/// This starts with the most recent error added to the chain. That is,
/// the highest level context. The last error in the chain is always the
/// "root" cause. That is, the error closest to the point where something
/// has gone wrong.
///
/// The iterator returned is guaranteed to yield at least one error.
fn chain(&self) -> impl Iterator<Item = &Error> {
#[cfg(feature = "alloc")]
{
let mut err = self;
core::iter::once(err).chain(core::iter::from_fn(move || {
err = err
.inner
.as_ref()
.and_then(|inner| inner.cause.as_ref())?;
Some(err)
}))
}
#[cfg(not(feature = "alloc"))]
{
core::iter::once(self)
}
}
/// Returns the kind of this error.
fn kind(&self) -> &ErrorKind {
self.inner
.as_ref()
.map(|inner| &inner.kind)
.unwrap_or(&ErrorKind::Unknown)
}
}
#[cfg(feature = "std")]
@ -379,14 +275,30 @@ impl std::error::Error for Error {}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
let mut it = self.chain().peekable();
while let Some(err) = it.next() {
core::fmt::Display::fmt(err.kind(), f)?;
if it.peek().is_some() {
f.write_str(": ")?;
#[cfg(feature = "alloc")]
{
let mut err = self;
loop {
let Some(ref inner) = err.inner else {
write!(f, "unknown jiff error")?;
break;
};
write!(f, "{}", inner.kind)?;
err = match inner.cause.as_ref() {
None => break,
Some(err) => err,
};
write!(f, ": ")?;
}
Ok(())
}
#[cfg(not(feature = "alloc"))]
{
match self.inner {
None => write!(f, "unknown jiff error"),
Some(ref inner) => write!(f, "{}", inner.kind),
}
}
Ok(())
}
}
@ -416,98 +328,14 @@ 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),
CrateFeature(CrateFeatureError),
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),
ITimeRange(crate::shared::util::itime::RangeError),
OsStrUtf8(self::util::OsStrUtf8Error),
ParseInt(self::util::ParseIntError),
ParseFraction(self::util::ParseFractionError),
PosixTz(crate::shared::posix::PosixTimeZoneError),
Range(RangeError),
RoundingIncrement(self::util::RoundingIncrementError),
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),
#[cfg(feature = "alloc")]
Tzif(crate::shared::tzif::TzifError),
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 {
Adhoc(ref msg) => msg.fmt(f),
Civil(ref err) => err.fmt(f),
CrateFeature(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),
ITimeRange(ref err) => err.fmt(f),
OsStrUtf8(ref err) => err.fmt(f),
ParseInt(ref err) => err.fmt(f),
ParseFraction(ref err) => err.fmt(f),
PosixTz(ref err) => err.fmt(f),
Range(ref err) => err.fmt(f),
RoundingIncrement(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),
#[cfg(feature = "alloc")]
Tzif(ref err) => err.fmt(f),
Unknown => f.write_str("unknown Jiff error"),
Zoned(ref err) => err.fmt(f),
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),
}
}
}
@ -527,10 +355,10 @@ impl From<ErrorKind> for Error {
/// A generic error message.
///
/// This used to be used to represent most errors in Jiff. But then I switched
/// to more structured error types (internally). We still keep this around to
/// support the `Error::from_args` public API, which permits users of Jiff to
/// manifest their own `Error` values from an arbitrary message.
/// This somewhat unfortunately represents most of the errors in Jiff. When I
/// first started building Jiff, I had a goal of making every error structured.
/// But this ended up being a ton of work, and I find it much easier and nicer
/// for error messages to be embedded where they occur.
#[cfg_attr(not(feature = "alloc"), derive(Clone))]
struct AdhocError {
#[cfg(feature = "alloc")]
@ -540,13 +368,18 @@ 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")]
{
use alloc::string::ToString;
let message = message.to_string().into_boxed_str();
AdhocError { message }
AdhocError::from_display(message)
}
#[cfg(not(feature = "alloc"))]
{
@ -554,6 +387,17 @@ 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 }
}
}
@ -632,75 +476,6 @@ 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")
}
}
/// An error used whenever a failure is caused by a missing crate feature.
///
/// This enum doesn't necessarily contain every Jiff crate feature. It only
/// contains the features whose absence can result in an error.
#[derive(Clone, Debug)]
pub(crate) enum CrateFeatureError {
#[cfg(not(feature = "tz-system"))]
TzSystem,
#[cfg(not(feature = "tzdb-concatenated"))]
TzdbConcatenated,
#[cfg(not(feature = "tzdb-zoneinfo"))]
TzdbZoneInfo,
}
impl From<CrateFeatureError> for Error {
#[cold]
#[inline(never)]
fn from(err: CrateFeatureError) -> Error {
ErrorKind::CrateFeature(err).into()
}
}
impl core::fmt::Display for CrateFeatureError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
#[allow(unused_imports)]
use self::CrateFeatureError::*;
f.write_str("operation failed because Jiff crate feature `")?;
#[allow(unused_variables)]
let name: &str = match *self {
#[cfg(not(feature = "tz-system"))]
TzSystem => "tz-system",
#[cfg(not(feature = "tzdb-concatenated"))]
TzdbConcatenated => "tzdb-concatenated",
#[cfg(not(feature = "tzdb-zoneinfo"))]
TzdbZoneInfo => "tzdb-zoneinfo",
};
#[allow(unreachable_code)]
core::fmt::Display::fmt(name, f)?;
f.write_str("` is not enabled")
}
}
/// A `std::io::Error`.
///
/// This type is itself always available, even when the `std` feature is not
@ -806,6 +581,21 @@ 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>`.
@ -813,7 +603,7 @@ impl IntoError for Error {
/// `map_err` everywhere one wants to add context to an error.
///
/// This trick was borrowed from `anyhow`.
pub(crate) trait ErrorContext<T, E> {
pub(crate) trait ErrorContext {
/// Contextualize the given consequent error with this (`self`) error as
/// the cause.
///
@ -822,7 +612,7 @@ pub(crate) trait ErrorContext<T, E> {
/// 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) -> Result<T, Error>;
fn context(self, consequent: impl IntoError) -> Self;
/// Like `context`, but hides error construction within a closure.
///
@ -833,31 +623,39 @@ pub(crate) trait ErrorContext<T, E> {
///
/// Usually this only makes sense to use on a `Result<T, Error>`, otherwise
/// the closure is just executed immediately anyway.
fn with_context<C: IntoError>(
fn with_context<E: IntoError>(
self,
consequent: impl FnOnce() -> C,
) -> Result<T, Error>;
consequent: impl FnOnce() -> E,
) -> Self;
}
impl<T, E> ErrorContext<T, E> for Result<T, E>
where
E: IntoError,
{
impl ErrorContext for Error {
#[cfg_attr(feature = "perf-inline", inline(always))]
fn context(self, consequent: impl IntoError) -> Result<T, Error> {
self.map_err(|err| {
err.into_error().context_impl(consequent.into_error())
})
fn context(self, consequent: impl IntoError) -> Error {
self.context_impl(consequent.into_error())
}
#[cfg_attr(feature = "perf-inline", inline(always))]
fn with_context<C: IntoError>(
fn with_context<E: IntoError>(
self,
consequent: impl FnOnce() -> C,
consequent: impl FnOnce() -> E,
) -> Error {
self.context_impl(consequent().into_error())
}
}
impl<T> ErrorContext for Result<T, Error> {
#[cfg_attr(feature = "perf-inline", inline(always))]
fn context(self, consequent: impl IntoError) -> Result<T, Error> {
self.map_err(|err| err.context_impl(consequent.into_error()))
}
#[cfg_attr(feature = "perf-inline", inline(always))]
fn with_context<E: IntoError>(
self,
consequent: impl FnOnce() -> E,
) -> Result<T, Error> {
self.map_err(|err| {
err.into_error().context_impl(consequent().into_error())
})
self.map_err(|err| err.context_impl(consequent().into_error()))
}
}
@ -892,12 +690,7 @@ 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.
//
// 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;
expected_size *= 3;
}
assert_eq!(expected_size, core::mem::size_of::<Error>());
}

View file

@ -1,84 +0,0 @@
use crate::{error, Unit};
#[derive(Clone, Debug)]
pub(crate) enum Error {
FailedAddDays,
FailedAddDurationOverflowing,
FailedAddSpanDate,
FailedAddSpanOverflowing,
FailedAddSpanTime,
IllegalTimeWithMicrosecond,
IllegalTimeWithMillisecond,
IllegalTimeWithNanosecond,
InvalidISOWeekNumber,
OverflowDaysDuration,
OverflowTimeNanoseconds,
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")
}
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(),
),
}
}
}

View file

@ -1,36 +0,0 @@
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")
}
}
}
}

View file

@ -1,82 +0,0 @@
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",
),
}
}
}

View file

@ -1,87 +0,0 @@
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")
}
}
}
}

View file

@ -1,153 +0,0 @@
use crate::{error, 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),
),
}
}
}

View file

@ -1,231 +0,0 @@
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")
}
}
}
}

View file

@ -1,114 +0,0 @@
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",
),
}
}
}

View file

@ -1,517 +0,0 @@
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")
}
}
}
}

View file

@ -1,354 +0,0 @@
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`)",
),
}
}
}

View file

@ -1,115 +0,0 @@
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(),
),
}
}
}

View file

@ -1,55 +0,0 @@
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(),
),
}
}
}

View file

@ -1,139 +0,0 @@
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",
),
}
}
}

View file

@ -1,38 +0,0 @@
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",
),
}
}
}

View file

@ -1,49 +0,0 @@
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(),
),
}
}
}

View file

@ -1,119 +0,0 @@
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")
}
}
}
}

View file

@ -1,141 +0,0 @@
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")
}
#[cfg(all(feature = "std", not(feature = "tzdb-zoneinfo")))]
DisabledZoneInfo => {
f.write_str("system zoneinfo tzdb unavailable")
}
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",
),
}
}
}

View file

@ -1,8 +0,0 @@
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;

View file

@ -1,110 +0,0 @@
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",
),
}
}
}

View file

@ -1,35 +0,0 @@
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"),
}
}
}

View file

@ -1,104 +0,0 @@
#[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)",
),
}
}
}
}

View file

@ -1,40 +0,0 @@
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"),
}
}
}

View file

@ -1,323 +0,0 @@
#[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"),
}
}
}
}

View file

@ -1,194 +0,0 @@
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>")
}
}
}

View file

@ -1,71 +0,0 @@
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(),
),
}
}
}

View file

@ -256,9 +256,7 @@ 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 input in the \"friendly\" duration format: \
expected to find unit designator suffix \
(e.g., `years` or `secs`) after parsing integer",
"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",
);
# Ok::<(), Box<dyn std::error::Error>>(())
@ -337,9 +335,7 @@ assert_eq!(
// Jiff is saving you from doing something wrong
assert_eq!(
"1 day".parse::<SignedDuration>().unwrap_err().to_string(),
"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)",
"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)",
);
```

View file

@ -1,11 +1,11 @@
use crate::{
error::{fmt::friendly::Error as E, ErrorContext},
error::{err, ErrorContext},
fmt::{
friendly::parser_label,
util::{parse_temporal_fraction, DurationUnits},
Parsed,
},
util::{c::Sign, parse},
util::{c::Sign, escape, parse},
Error, SignedDuration, Span, Unit,
};
@ -188,7 +188,12 @@ impl SpanParser {
}
let input = input.as_ref();
imp(self, input).context(E::Failed)
imp(self, input).with_context(|| {
err!(
"failed to parse {input:?} in the \"friendly\" format",
input = escape::Bytes(input)
)
})
}
/// Run the parser on the given string (which may be plain bytes) and,
@ -243,7 +248,12 @@ impl SpanParser {
}
let input = input.as_ref();
imp(self, input).context(E::Failed)
imp(self, input).with_context(|| {
err!(
"failed to parse {input:?} in the \"friendly\" format",
input = escape::Bytes(input)
)
})
}
/// Run the parser on the given string (which may be plain bytes) and,
@ -302,7 +312,12 @@ impl SpanParser {
}
let input = input.as_ref();
imp(self, input).context(E::Failed)
imp(self, input).with_context(|| {
err!(
"failed to parse {input:?} in the \"friendly\" format",
input = escape::Bytes(input)
)
})
}
#[cfg_attr(feature = "perf-inline", inline(always))]
@ -312,7 +327,7 @@ impl SpanParser {
builder: &mut DurationUnits,
) -> Result<Parsed<'i, ()>, Error> {
if input.is_empty() {
return Err(Error::from(E::Empty));
return Err(err!("an empty string is not a valid duration"));
}
// Guard prefix sign parsing to avoid the function call, which is
// marked unlineable to keep the fast path tighter.
@ -327,7 +342,11 @@ impl SpanParser {
let Parsed { value, input } = self.parse_unit_value(input)?;
let Some(first_unit_value) = value else {
return Err(Error::from(E::ExpectedIntegerAfterSign));
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",
));
};
let Parsed { input, .. } =
@ -415,7 +434,11 @@ impl SpanParser {
parsed_any_after_comma = true;
}
if !parsed_any_after_comma {
return Err(Error::from(E::ExpectedOneMoreUnitAfterComma));
return Err(err!(
"found comma at the end of duration, \
but a comma indicates at least one more \
unit follows",
));
}
Ok(Parsed { value: (), input })
}
@ -431,13 +454,10 @@ impl SpanParser {
input: &'i [u8],
hour: u64,
) -> Result<Parsed<'i, Option<HMS>>, Error> {
let Some((&first, tail)) = input.split_first() else {
return Ok(Parsed { input, value: None });
};
if first != b':' {
if !input.first().map_or(false, |&b| b == b':') {
return Ok(Parsed { input, value: None });
}
let Parsed { input, value } = self.parse_hms(tail, hour)?;
let Parsed { input, value } = self.parse_hms(&input[1..], hour)?;
Ok(Parsed { input, value: Some(value) })
}
@ -457,16 +477,26 @@ impl SpanParser {
hour: u64,
) -> Result<Parsed<'i, HMS>, Error> {
let Parsed { input, value } = self.parse_unit_value(input)?;
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 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 input = &input[1..];
let Parsed { input, value } = self.parse_unit_value(input)?;
let second = value.ok_or(E::ExpectedSecondAfterMinute)?;
let Some(second) = value else {
return Err(err!(
"expected to parse second in 'HH:MM:SS' format \
following parsed minute of {minute}",
));
};
let (fraction, input) =
if input.first().map_or(false, |&b| b == b'.' || b == b',') {
let parsed = parse_temporal_fraction(input)?;
@ -510,8 +540,22 @@ impl SpanParser {
&self,
input: &'i [u8],
) -> Result<Parsed<'i, Unit>, Error> {
let (unit, len) =
parser_label::find(input).ok_or(E::ExpectedUnitSuffix)?;
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)]),
));
}
};
Ok(Parsed { value: unit, input: &input[len..] })
}
@ -562,15 +606,17 @@ 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 let Some(tail) = input.strip_prefix(b"ago") {
(Some(Sign::Negative), tail)
} else {
(None, input)
};
let (suffix_sign, input) = if input.starts_with(b"ago") {
(Some(Sign::Negative), &input[3..])
} else {
(None, input)
};
let sign = match (prefix_sign, suffix_sign) {
(Some(_), Some(_)) => {
return Err(Error::from(E::ExpectedOneSign));
return Err(err!(
"expected to find either a prefix sign (+/-) or \
a suffix sign (ago), but found both",
))
}
(Some(sign), None) => sign,
(None, Some(sign)) => sign,
@ -591,24 +637,24 @@ impl SpanParser {
#[inline(never)]
fn parse_optional_comma<'i>(
&self,
input: &'i [u8],
mut input: &'i [u8],
) -> Result<Parsed<'i, ()>, Error> {
let Some((&first, tail)) = input.split_first() else {
return Ok(Parsed { value: (), input });
};
if first != b',' {
if !input.first().map_or(false, |&b| b == b',') {
return Ok(Parsed { value: (), input });
}
let (second, input) = tail
.split_first()
.ok_or(E::ExpectedWhitespaceAfterCommaEndOfInput)?;
if !is_whitespace(second) {
return Err(Error::from(E::ExpectedWhitespaceAfterComma {
byte: *second,
}));
input = &input[1..];
if input.is_empty() {
return Err(err!(
"expected whitespace after comma, but found end of input"
));
}
Ok(Parsed { value: (), input })
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..] })
}
/// Parses zero or more bytes of ASCII whitespace.
@ -730,35 +776,35 @@ mod tests {
insta::assert_snapshot!(
p(""),
@r#"failed to parse input in the "friendly" duration format: an empty string is not valid"#,
@r###"failed to parse "" in the "friendly" format: an empty string is not a valid duration"###,
);
insta::assert_snapshot!(
p(" "),
@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"#,
@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"###,
);
insta::assert_snapshot!(
p("a"),
@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"#,
@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"###,
);
insta::assert_snapshot!(
p("2 months 1 year"),
@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)"#,
@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)"###,
);
insta::assert_snapshot!(
p("1 year 1 mont"),
@r#"failed to parse input in the "friendly" duration format: parsed value 'P1Y1M', but unparsed input "nt" remains (expected no unparsed input)"#,
@r###"failed to parse "1 year 1 mont" in the "friendly" format: parsed value 'P1Y1M', but unparsed input "nt" remains (expected no unparsed input)"###,
);
insta::assert_snapshot!(
p("2 months,"),
@r#"failed to parse input in the "friendly" duration format: expected whitespace after comma, but found end of input"#,
@r###"failed to parse "2 months," in the "friendly" format: expected whitespace after comma, but found end of input"###,
);
insta::assert_snapshot!(
p("2 months, "),
@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"#,
@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"#,
);
insta::assert_snapshot!(
p("2 months ,"),
@r#"failed to parse input in the "friendly" duration format: parsed value 'P2M', but unparsed input "," remains (expected no unparsed input)"#,
@r###"failed to parse "2 months ," in the "friendly" format: parsed value 'P2M', but unparsed input "," remains (expected no unparsed input)"###,
);
}
@ -768,19 +814,19 @@ mod tests {
insta::assert_snapshot!(
p("1yago"),
@r#"failed to parse input in the "friendly" duration format: parsed value 'P1Y', but unparsed input "ago" remains (expected no unparsed input)"#,
@r###"failed to parse "1yago" in the "friendly" 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 input in the "friendly" duration format: parsed value 'P1Y1M', but unparsed input "ago" remains (expected no unparsed input)"#,
@r###"failed to parse "1 year 1 monthago" in the "friendly" 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 input in the "friendly" duration format: expected to find either a prefix sign (+/-) or a suffix sign (`ago`), but found both"#,
@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"###,
);
insta::assert_snapshot!(
p("-1 year 1 month ago"),
@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"#,
@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"###,
);
}
@ -794,7 +840,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 input in the "friendly" duration format: failed to set value for microsecond unit on span: failed to set nanosecond value from fractional component"#,
@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"#,
);
// one fewer is okay
insta::assert_snapshot!(
@ -807,7 +853,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 input in the "friendly" duration format: failed to set nanosecond value from fractional component"#,
@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"#,
);
// one fewer is okay
insta::assert_snapshot!(
@ -822,47 +868,47 @@ mod tests {
insta::assert_snapshot!(
p("19999 years"),
@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"#,
@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"###,
);
insta::assert_snapshot!(
p("19999 years ago"),
@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"#,
@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"#,
);
insta::assert_snapshot!(
p("239977 months"),
@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"#,
@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"###,
);
insta::assert_snapshot!(
p("239977 months ago"),
@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"#,
@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"#,
);
insta::assert_snapshot!(
p("1043498 weeks"),
@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"#,
@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"###,
);
insta::assert_snapshot!(
p("1043498 weeks ago"),
@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"#,
@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"#,
);
insta::assert_snapshot!(
p("7304485 days"),
@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"#,
@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"###,
);
insta::assert_snapshot!(
p("7304485 days ago"),
@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"#,
@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"#,
);
insta::assert_snapshot!(
p("9223372036854775808 nanoseconds"),
@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"#,
@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"#,
);
insta::assert_snapshot!(
p("9223372036854775808 nanoseconds ago"),
@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"#,
@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"#,
);
}
@ -872,11 +918,11 @@ mod tests {
insta::assert_snapshot!(
p("1.5 years"),
@r#"failed to parse input in the "friendly" duration format: fractional years are not supported"#,
@r#"failed to parse "1.5 years" in the "friendly" format: fractional years are not supported"#,
);
insta::assert_snapshot!(
p("1.5 nanos"),
@r#"failed to parse input in the "friendly" duration format: fractional nanoseconds are not supported"#,
@r#"failed to parse "1.5 nanos" in the "friendly" format: fractional nanoseconds are not supported"#,
);
}
@ -886,19 +932,19 @@ mod tests {
insta::assert_snapshot!(
p("05:"),
@r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse minute following hour"#,
@r###"failed to parse "05:" in the "friendly" format: expected to parse minute in 'HH:MM:SS' format following parsed hour of 5"###,
);
insta::assert_snapshot!(
p("05:06"),
@r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse `:` following minute"#,
@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"###,
);
insta::assert_snapshot!(
p("05:06:"),
@r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse second following minute"#,
@r###"failed to parse "05:06:" in the "friendly" format: expected to parse second in 'HH:MM:SS' format following parsed minute of 6"###,
);
insta::assert_snapshot!(
p("2 hours, 05:06:07"),
@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"#,
@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"#,
);
}
@ -922,7 +968,7 @@ mod tests {
);
insta::assert_snapshot!(
perr("9223372036854775808s"),
@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"#,
@r#"failed to parse "9223372036854775808s" in the "friendly" format: `9223372036854775808` seconds is too big (or small) to fit into a signed 64-bit integer"#,
);
insta::assert_snapshot!(
p("-9223372036854775808s"),
@ -986,21 +1032,21 @@ mod tests {
insta::assert_snapshot!(p("-2562047788015215hours"), @"-PT2562047788015215H");
insta::assert_snapshot!(
pe("2562047788015216hrs"),
@r#"failed to parse input in the "friendly" duration format: accumulated duration overflowed when adding value to unit hour"#,
@r#"failed to parse "2562047788015216hrs" in the "friendly" format: accumulated `SignedDuration` of `0s` overflowed when adding 2562047788015216 of unit hour"#,
);
insta::assert_snapshot!(p("153722867280912930minutes"), @"PT2562047788015215H30M");
insta::assert_snapshot!(p("153722867280912930minutes ago"), @"-PT2562047788015215H30M");
insta::assert_snapshot!(
pe("153722867280912931mins"),
@r#"failed to parse input in the "friendly" duration format: accumulated duration overflowed when adding value to unit minute"#,
@r#"failed to parse "153722867280912931mins" in the "friendly" format: accumulated `SignedDuration` of `0s` overflowed when adding 153722867280912931 of unit minute"#,
);
insta::assert_snapshot!(p("9223372036854775807seconds"), @"PT2562047788015215H30M7S");
insta::assert_snapshot!(p("-9223372036854775807seconds"), @"-PT2562047788015215H30M7S");
insta::assert_snapshot!(
pe("9223372036854775808s"),
@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"#,
@r#"failed to parse "9223372036854775808s" in the "friendly" format: `9223372036854775808` seconds is too big (or small) to fit into a signed 64-bit integer"#,
);
insta::assert_snapshot!(
p("-9223372036854775808s"),
@ -1014,39 +1060,39 @@ mod tests {
insta::assert_snapshot!(
p(""),
@r#"failed to parse input in the "friendly" duration format: an empty string is not valid"#,
@r###"failed to parse "" in the "friendly" format: an empty string is not a valid duration"###,
);
insta::assert_snapshot!(
p(" "),
@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"#,
@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"###,
);
insta::assert_snapshot!(
p("5"),
@r#"failed to parse input in the "friendly" duration format: expected to find unit designator suffix (e.g., `years` or `secs`) after parsing integer"#,
@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"###,
);
insta::assert_snapshot!(
p("a"),
@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"#,
@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"###,
);
insta::assert_snapshot!(
p("2 minutes 1 hour"),
@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)"#,
@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)"###,
);
insta::assert_snapshot!(
p("1 hour 1 minut"),
@r#"failed to parse input in the "friendly" duration format: parsed value 'PT1H1M', but unparsed input "ut" remains (expected no unparsed input)"#,
@r###"failed to parse "1 hour 1 minut" in the "friendly" format: parsed value 'PT1H1M', but unparsed input "ut" remains (expected no unparsed input)"###,
);
insta::assert_snapshot!(
p("2 minutes,"),
@r#"failed to parse input in the "friendly" duration format: expected whitespace after comma, but found end of input"#,
@r###"failed to parse "2 minutes," in the "friendly" format: expected whitespace after comma, but found end of input"###,
);
insta::assert_snapshot!(
p("2 minutes, "),
@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"#,
@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"#,
);
insta::assert_snapshot!(
p("2 minutes ,"),
@r#"failed to parse input in the "friendly" duration format: parsed value 'PT2M', but unparsed input "," remains (expected no unparsed input)"#,
@r###"failed to parse "2 minutes ," in the "friendly" format: parsed value 'PT2M', but unparsed input "," remains (expected no unparsed input)"###,
);
}
@ -1056,19 +1102,19 @@ mod tests {
insta::assert_snapshot!(
p("1hago"),
@r#"failed to parse input in the "friendly" duration format: parsed value 'PT1H', but unparsed input "ago" remains (expected no unparsed input)"#,
@r###"failed to parse "1hago" in the "friendly" 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 input in the "friendly" duration format: parsed value 'PT1H1M', but unparsed input "ago" remains (expected no unparsed input)"#,
@r###"failed to parse "1 hour 1 minuteago" in the "friendly" 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 input in the "friendly" duration format: expected to find either a prefix sign (+/-) or a suffix sign (`ago`), but found both"#,
@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"###,
);
insta::assert_snapshot!(
p("-1 hour 1 minute ago"),
@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"#,
@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"###,
);
}
@ -1081,7 +1127,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 input in the "friendly" duration format: value for microseconds is too big (or small) to fit into a signed 64-bit integer"#,
@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"#,
);
// one fewer is okay
insta::assert_snapshot!(
@ -1096,7 +1142,7 @@ mod tests {
insta::assert_snapshot!(
p("1.5 nanos"),
@r#"failed to parse input in the "friendly" duration format: fractional nanoseconds are not supported"#,
@r#"failed to parse "1.5 nanos" in the "friendly" format: fractional nanoseconds are not supported"#,
);
}
@ -1106,19 +1152,19 @@ mod tests {
insta::assert_snapshot!(
p("05:"),
@r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse minute following hour"#,
@r###"failed to parse "05:" in the "friendly" format: expected to parse minute in 'HH:MM:SS' format following parsed hour of 5"###,
);
insta::assert_snapshot!(
p("05:06"),
@r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse `:` following minute"#,
@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"###,
);
insta::assert_snapshot!(
p("05:06:"),
@r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse second following minute"#,
@r###"failed to parse "05:06:" in the "friendly" format: expected to parse second in 'HH:MM:SS' format following parsed minute of 6"###,
);
insta::assert_snapshot!(
p("2 hours, 05:06:07"),
@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"#,
@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"#,
);
}
@ -1159,11 +1205,11 @@ mod tests {
);
insta::assert_snapshot!(
perr("18446744073709551616s"),
@r#"failed to parse input in the "friendly" duration format: number too big to parse into 64-bit integer"#,
@r#"failed to parse "18446744073709551616s" in the "friendly" format: number `18446744073709551616` too big to parse into 64-bit integer"#,
);
insta::assert_snapshot!(
perr("-1s"),
@r#"failed to parse input in the "friendly" duration format: cannot parse negative duration into unsigned `std::time::Duration`"#,
@r#"failed to parse "-1s" in the "friendly" format: cannot parse negative duration into unsigned `std::time::Duration`"#,
);
}
@ -1217,19 +1263,19 @@ mod tests {
insta::assert_snapshot!(p("5124095576030431hours"), @"PT5124095576030431H");
insta::assert_snapshot!(
pe("5124095576030432hrs"),
@r#"failed to parse input in the "friendly" duration format: accumulated duration overflowed when adding value to unit hour"#,
@r#"failed to parse "5124095576030432hrs" in the "friendly" format: accumulated `SignedDuration` of `0s` overflowed when adding 5124095576030432 of unit hour"#,
);
insta::assert_snapshot!(p("307445734561825860minutes"), @"PT5124095576030431H");
insta::assert_snapshot!(
pe("307445734561825861mins"),
@r#"failed to parse input in the "friendly" duration format: accumulated duration overflowed when adding value to unit minute"#,
@r#"failed to parse "307445734561825861mins" in the "friendly" format: accumulated `SignedDuration` of `0s` overflowed when adding 307445734561825861 of unit minute"#,
);
insta::assert_snapshot!(p("18446744073709551615seconds"), @"PT5124095576030431H15S");
insta::assert_snapshot!(
pe("18446744073709551616s"),
@r#"failed to parse input in the "friendly" duration format: number too big to parse into 64-bit integer"#,
@r#"failed to parse "18446744073709551616s" in the "friendly" format: number `18446744073709551616` too big to parse into 64-bit integer"#,
);
}
@ -1241,39 +1287,39 @@ mod tests {
insta::assert_snapshot!(
p(""),
@r#"failed to parse input in the "friendly" duration format: an empty string is not valid"#,
@r###"failed to parse "" in the "friendly" format: an empty string is not a valid duration"###,
);
insta::assert_snapshot!(
p(" "),
@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"#,
@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"###,
);
insta::assert_snapshot!(
p("5"),
@r#"failed to parse input in the "friendly" duration format: expected to find unit designator suffix (e.g., `years` or `secs`) after parsing integer"#,
@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"###,
);
insta::assert_snapshot!(
p("a"),
@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"#,
@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"###,
);
insta::assert_snapshot!(
p("2 minutes 1 hour"),
@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)"#,
@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)"###,
);
insta::assert_snapshot!(
p("1 hour 1 minut"),
@r#"failed to parse input in the "friendly" duration format: parsed value '3660s', but unparsed input "ut" remains (expected no unparsed input)"#,
@r#"failed to parse "1 hour 1 minut" in the "friendly" format: parsed value '3660s', but unparsed input "ut" remains (expected no unparsed input)"#,
);
insta::assert_snapshot!(
p("2 minutes,"),
@r#"failed to parse input in the "friendly" duration format: expected whitespace after comma, but found end of input"#,
@r###"failed to parse "2 minutes," in the "friendly" format: expected whitespace after comma, but found end of input"###,
);
insta::assert_snapshot!(
p("2 minutes, "),
@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"#,
@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"#,
);
insta::assert_snapshot!(
p("2 minutes ,"),
@r#"failed to parse input in the "friendly" duration format: parsed value '120s', but unparsed input "," remains (expected no unparsed input)"#,
@r#"failed to parse "2 minutes ," in the "friendly" format: parsed value '120s', but unparsed input "," remains (expected no unparsed input)"#,
);
}
@ -1285,19 +1331,19 @@ mod tests {
insta::assert_snapshot!(
p("1hago"),
@r#"failed to parse input in the "friendly" duration format: parsed value '3600s', but unparsed input "ago" remains (expected no unparsed input)"#,
@r#"failed to parse "1hago" in the "friendly" 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 input in the "friendly" duration format: parsed value '3660s', but unparsed input "ago" remains (expected no unparsed input)"#,
@r#"failed to parse "1 hour 1 minuteago" in the "friendly" 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 input in the "friendly" duration format: expected to find either a prefix sign (+/-) or a suffix sign (`ago`), but found both"#,
@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"###,
);
insta::assert_snapshot!(
p("-1 hour 1 minute ago"),
@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"#,
@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"###,
);
}
@ -1316,7 +1362,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 input in the "friendly" duration format: number too big to parse into 64-bit integer"#,
@r#"failed to parse "18446744073709551616 micros" in the "friendly" format: number `18446744073709551616` too big to parse into 64-bit integer"#,
);
// one fewer is okay
insta::assert_snapshot!(
@ -1333,7 +1379,7 @@ mod tests {
insta::assert_snapshot!(
p("1.5 nanos"),
@r#"failed to parse input in the "friendly" duration format: fractional nanoseconds are not supported"#,
@r#"failed to parse "1.5 nanos" in the "friendly" format: fractional nanoseconds are not supported"#,
);
}
@ -1345,19 +1391,19 @@ mod tests {
insta::assert_snapshot!(
p("05:"),
@r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse minute following hour"#,
@r###"failed to parse "05:" in the "friendly" format: expected to parse minute in 'HH:MM:SS' format following parsed hour of 5"###,
);
insta::assert_snapshot!(
p("05:06"),
@r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse `:` following minute"#,
@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"###,
);
insta::assert_snapshot!(
p("05:06:"),
@r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse second following minute"#,
@r###"failed to parse "05:06:" in the "friendly" format: expected to parse second in 'HH:MM:SS' format following parsed minute of 6"###,
);
insta::assert_snapshot!(
p("2 hours, 05:06:07"),
@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"#,
@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"#,
);
}
}

View file

@ -1,6 +1,6 @@
use crate::{
fmt::{
util::{FractionalFormatter, IntegerFormatter},
util::{DecimalFormatter, FractionalFormatter},
Write, WriteExt,
},
Error, SignedDuration, Span, Unit,
@ -1223,19 +1223,19 @@ impl SpanPrinter {
) -> Result<(), Error> {
let span = span.abs();
if span.get_years() != 0 {
wtr.write(Unit::Year, span.get_years().unsigned_abs().into())?;
wtr.write(Unit::Year, span.get_years().unsigned_abs())?;
}
if span.get_months() != 0 {
wtr.write(Unit::Month, span.get_months().unsigned_abs().into())?;
wtr.write(Unit::Month, span.get_months().unsigned_abs())?;
}
if span.get_weeks() != 0 {
wtr.write(Unit::Week, span.get_weeks().unsigned_abs().into())?;
wtr.write(Unit::Week, span.get_weeks().unsigned_abs())?;
}
if span.get_days() != 0 {
wtr.write(Unit::Day, span.get_days().unsigned_abs().into())?;
wtr.write(Unit::Day, span.get_days().unsigned_abs())?;
}
if span.get_hours() != 0 {
wtr.write(Unit::Hour, span.get_hours().unsigned_abs().into())?;
wtr.write(Unit::Hour, span.get_hours().unsigned_abs())?;
}
if span.get_minutes() != 0 {
wtr.write(Unit::Minute, span.get_minutes().unsigned_abs())?;
@ -1310,7 +1310,7 @@ impl SpanPrinter {
span_time = span_time.abs();
let fmtint =
IntegerFormatter::new().padding(self.padding.unwrap_or(2));
DecimalFormatter::new().padding(self.padding.unwrap_or(2));
let fmtfraction = FractionalFormatter::new().precision(self.precision);
wtr.wtr.write_int(&fmtint, span_time.get_hours_ranged().get())?;
wtr.wtr.write_str(":")?;
@ -1366,16 +1366,10 @@ impl SpanPrinter {
wtr.write(Unit::Minute, secs / SECS_PER_MIN)?;
wtr.write(Unit::Second, secs % SECS_PER_MIN)?;
let mut nanos = dur.subsec_nanos();
wtr.write(
Unit::Millisecond,
(nanos / NANOS_PER_MILLI).into(),
)?;
wtr.write(Unit::Millisecond, nanos / NANOS_PER_MILLI)?;
nanos %= NANOS_PER_MILLI;
wtr.write(
Unit::Microsecond,
(nanos / NANOS_PER_MICRO).into(),
)?;
wtr.write(Unit::Nanosecond, (nanos % NANOS_PER_MICRO).into())?;
wtr.write(Unit::Microsecond, nanos / NANOS_PER_MICRO)?;
wtr.write(Unit::Nanosecond, nanos % NANOS_PER_MICRO)?;
}
Some(FractionalUnit::Hour) => {
wtr.write_fractional_duration(FractionalUnit::Hour, &dur)?;
@ -1427,10 +1421,7 @@ impl SpanPrinter {
wtr.write(Unit::Minute, secs / SECS_PER_MIN)?;
wtr.write(Unit::Second, secs % SECS_PER_MIN)?;
let mut nanos = dur.subsec_nanos();
wtr.write(
Unit::Millisecond,
(nanos / NANOS_PER_MILLI).into(),
)?;
wtr.write(Unit::Millisecond, nanos / NANOS_PER_MILLI)?;
nanos %= NANOS_PER_MILLI;
let leftovers = core::time::Duration::new(0, nanos);
@ -1488,7 +1479,7 @@ impl SpanPrinter {
// bigger.
let fmtint =
IntegerFormatter::new().padding(self.padding.unwrap_or(2));
DecimalFormatter::new().padding(self.padding.unwrap_or(2));
let fmtfraction = FractionalFormatter::new().precision(self.precision);
let mut secs = udur.as_secs();
@ -1618,7 +1609,7 @@ struct DesignatorWriter<'p, 'w, W> {
wtr: &'w mut W,
desig: Designators,
sign: Option<DirectionSign>,
fmtint: IntegerFormatter,
fmtint: DecimalFormatter,
fmtfraction: FractionalFormatter,
written_non_zero_unit: bool,
}
@ -1633,7 +1624,7 @@ impl<'p, 'w, W: Write> DesignatorWriter<'p, 'w, W> {
let desig = Designators::new(printer.designator);
let sign = printer.direction.sign(printer, has_calendar, signum);
let fmtint =
IntegerFormatter::new().padding(printer.padding.unwrap_or(0));
DecimalFormatter::new().padding(printer.padding.unwrap_or(0));
let fmtfraction =
FractionalFormatter::new().precision(printer.precision);
DesignatorWriter {
@ -1679,7 +1670,12 @@ impl<'p, 'w, W: Write> DesignatorWriter<'p, 'w, W> {
Ok(())
}
fn write(&mut self, unit: Unit, value: u64) -> Result<(), Error> {
fn write(
&mut self,
unit: Unit,
value: impl Into<u64>,
) -> Result<(), Error> {
let value = value.into();
if value == 0 {
return Ok(());
}
@ -1733,7 +1729,7 @@ impl<'p, 'w, W: Write> DesignatorWriter<'p, 'w, W> {
struct FractionalPrinter {
integer: u64,
fraction: u32,
fmtint: IntegerFormatter,
fmtint: DecimalFormatter,
fmtfraction: FractionalFormatter,
}
@ -1750,7 +1746,7 @@ impl FractionalPrinter {
fn from_span(
span: &Span,
unit: FractionalUnit,
fmtint: IntegerFormatter,
fmtint: DecimalFormatter,
fmtfraction: FractionalFormatter,
) -> FractionalPrinter {
debug_assert!(span.largest_unit() <= Unit::from(unit));
@ -1762,7 +1758,7 @@ impl FractionalPrinter {
fn from_duration(
dur: &core::time::Duration,
unit: FractionalUnit,
fmtint: IntegerFormatter,
fmtint: DecimalFormatter,
fmtfraction: FractionalFormatter,
) -> FractionalPrinter {
match unit {

View file

@ -166,11 +166,11 @@ and features.)
*/
use crate::{
error::{fmt::Error as E, Error},
error::{err, Error},
util::escape,
};
use self::util::{Fractional, FractionalFormatter, Integer, IntegerFormatter};
use self::util::{Decimal, DecimalFormatter, Fractional, FractionalFormatter};
pub mod friendly;
mod offset;
@ -218,7 +218,12 @@ impl<'i, V: core::fmt::Display> Parsed<'i, V> {
if self.input.is_empty() {
return Ok(self.value);
}
Err(Error::from(E::into_full_error(&self.value, self.input)))
Err(err!(
"parsed value '{value}', but unparsed input {unparsed:?} \
remains (expected no unparsed input)",
value = self.value,
unparsed = escape::Bytes(self.input),
))
}
}
@ -239,7 +244,12 @@ impl<'i, V> Parsed<'i, V> {
if self.input.is_empty() {
return Ok(self.value);
}
Err(Error::from(E::into_full_error(&display, self.input)))
Err(err!(
"parsed value '{value}', but unparsed input {unparsed:?} \
remains (expected no unparsed input)",
value = display,
unparsed = escape::Bytes(self.input),
))
}
}
@ -324,17 +334,6 @@ impl<W: Write> Write for &mut W {
}
}
impl Write for &mut dyn Write {
fn write_str(&mut self, string: &str) -> Result<(), Error> {
(**self).write_str(string)
}
#[inline]
fn write_char(&mut self, char: char) -> Result<(), Error> {
(**self).write_char(char)
}
}
/// An adapter for using `std::io::Write` implementations with `fmt::Write`.
///
/// This is useful when one wants to format a datetime or span directly
@ -369,7 +368,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::io)
self.0.write_all(string.as_bytes()).map_err(Error::adhoc)
}
}
@ -412,7 +411,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(|_| Error::from(E::StdFmtWriteAdapter))
.map_err(|_| err!("an error occurred when formatting an argument"))
}
}
@ -434,7 +433,7 @@ trait WriteExt: Write {
#[inline]
fn write_int(
&mut self,
formatter: &IntegerFormatter,
formatter: &DecimalFormatter,
n: impl Into<i64>,
) -> Result<(), Error> {
self.write_decimal(&formatter.format_signed(n.into()))
@ -445,7 +444,7 @@ trait WriteExt: Write {
#[inline]
fn write_uint(
&mut self,
formatter: &IntegerFormatter,
formatter: &DecimalFormatter,
n: impl Into<u64>,
) -> Result<(), Error> {
self.write_decimal(&formatter.format_unsigned(n.into()))
@ -464,7 +463,7 @@ trait WriteExt: Write {
/// Write the given decimal number to this buffer.
#[inline]
fn write_decimal(&mut self, decimal: &Integer) -> Result<(), Error> {
fn write_decimal(&mut self, decimal: &Decimal) -> Result<(), Error> {
self.write_str(decimal.as_str())
}

View file

@ -102,7 +102,7 @@ from [Temporal's hybrid grammar].
// support a span of time of about 52 hours or so.)
use crate::{
error::{fmt::offset::Error as E, Error, ErrorContext},
error::{err, Error, ErrorContext},
fmt::{
temporal::{PiecesNumericOffset, PiecesOffset},
util::{parse_temporal_fraction, FractionalFormatter},
@ -110,7 +110,7 @@ use crate::{
},
tz::Offset,
util::{
parse,
escape, parse,
rangeint::{ri8, RFrom},
t::{self, C},
},
@ -237,7 +237,13 @@ impl Numeric {
if part_nanoseconds >= C(500_000_000) {
seconds = seconds
.try_checked_add("offset-seconds", C(1))
.context(E::PrecisionLoss)?;
.with_context(|| {
err!(
"due to precision loss, UTC offset '{}' is \
rounded to a value that is out of bounds",
self,
)
})?;
}
}
Ok(Offset::from_seconds_ranged(seconds * self.sign))
@ -248,7 +254,11 @@ impl Numeric {
// `Offset` fails.
impl core::fmt::Display for Numeric {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.write_str(if self.sign == C(-1) { "-" } else { "+" })?;
if self.sign == C(-1) {
write!(f, "-")?;
} else {
write!(f, "+")?;
}
write!(f, "{:02}", self.hours)?;
if let Some(minutes) = self.minutes {
write!(f, ":{:02}", minutes)?;
@ -258,8 +268,11 @@ impl core::fmt::Display for Numeric {
}
if let Some(nanos) = self.nanoseconds {
static FMT: FractionalFormatter = FractionalFormatter::new();
f.write_str(".")?;
f.write_str(FMT.format(i32::from(nanos).unsigned_abs()).as_str())?;
write!(
f,
".{}",
FMT.format(i32::from(nanos).unsigned_abs()).as_str()
)?;
}
Ok(())
}
@ -400,14 +413,18 @@ impl Parser {
mut input: &'i [u8],
) -> Result<Parsed<'i, ParsedOffset>, Error> {
if input.is_empty() {
return Err(Error::from(E::EndOfInput));
return Err(err!("expected UTC offset, but found end of input"));
}
if input[0] == b'Z' || input[0] == b'z' {
if !self.zulu {
return Err(Error::from(E::UnexpectedLetterOffsetNoZulu(
input[0],
)));
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),
));
}
input = &input[1..];
let value = ParsedOffset { kind: ParsedOffsetKind::Zulu };
@ -447,24 +464,40 @@ 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).context(E::InvalidSign)?;
self.parse_sign(input).with_context(|| {
err!("failed to parse sign in UTC numeric offset {original:?}")
})?;
// Parse hours component.
let Parsed { value: hours, input } =
self.parse_hours(input).context(E::InvalidHours)?;
self.parse_hours(input).with_context(|| {
err!(
"failed to parse hours in UTC numeric offset {original:?}"
)
})?;
let extended = match self.colon {
Colon::Optional => input.starts_with(b":"),
Colon::Required => {
if !input.is_empty() && !input.starts_with(b":") {
return Err(Error::from(E::NoColonAfterHours));
return Err(err!(
"parsed hour component of time zone offset from \
{original:?}, but could not find required colon \
separator",
));
}
true
}
Colon::Absent => {
if !input.is_empty() && input.starts_with(b":") {
return Err(Error::from(E::ColonAfterHours));
return Err(err!(
"parsed hour component of time zone offset from \
{original:?}, but found colon after hours which \
is not allowed",
));
}
false
}
@ -480,22 +513,32 @@ impl Parser {
};
// Parse optional separator after hours.
let Parsed { value: has_minutes, input } = self
.parse_separator(input, extended)
.context(E::SeparatorAfterHours)?;
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:?}"
)
})?;
if !has_minutes {
return if self.require_minute
|| (self.subminute && self.require_second)
{
Err(Error::from(E::MissingMinuteAfterHour))
} else {
Ok(Parsed { value: numeric, input })
};
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 });
}
// Parse minutes component.
let Parsed { value: minutes, input } =
self.parse_minutes(input).context(E::InvalidMinutes)?;
self.parse_minutes(input).with_context(|| {
err!(
"failed to parse minutes in UTC numeric offset \
{original:?}"
)
})?;
numeric.minutes = Some(minutes);
// If subminute resolution is not supported, then we're done here.
@ -506,42 +549,65 @@ 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).
return if input.get(0).map_or(false, |&b| b == b':') {
Err(Error::from(E::SubminutePrecisionNotEnabled))
} else {
Ok(Parsed { value: numeric, input })
};
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 });
}
// Parse optional separator after minutes.
let Parsed { value: has_seconds, input } = self
.parse_separator(input, extended)
.context(E::SeparatorAfterMinutes)?;
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:?}"
)
})?;
if !has_seconds {
return if self.require_second {
Err(Error::from(E::MissingSecondAfterMinute))
} else {
Ok(Parsed { value: numeric, input })
};
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 });
}
// Parse seconds component.
let Parsed { value: seconds, input } =
self.parse_seconds(input).context(E::InvalidSeconds)?;
self.parse_seconds(input).with_context(|| {
err!(
"failed to parse seconds in UTC numeric offset \
{original:?}"
)
})?;
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(Error::from(E::SubsecondPrecisionNotEnabled));
return Err(err!(
"subsecond precision for UTC numeric offset {original:?} \
is not enabled in this context (must provide only \
integral minutes or seconds)",
));
}
return Ok(Parsed { value: numeric, input });
}
// Parse an optional fractional component.
let Parsed { value: nanoseconds, input } =
parse_temporal_fraction(input)
.context(E::InvalidSecondsFractional)?;
parse_temporal_fraction(input).with_context(|| {
err!(
"failed to parse fractional nanoseconds in \
UTC numeric offset {original:?}",
)
})?;
// OK because `parse_temporal_fraction` guarantees `0..=999_999_999`.
numeric.nanoseconds =
nanoseconds.map(|n| t::SubsecNanosecond::new(n).unwrap());
@ -553,13 +619,19 @@ impl Parser {
&self,
input: &'i [u8],
) -> Result<Parsed<'i, t::Sign>, Error> {
let sign = input.get(0).copied().ok_or(E::EndOfInputNumeric)?;
let sign = input.get(0).copied().ok_or_else(|| {
err!("expected UTC numeric offset, but found end of input")
})?;
let sign = if sign == b'+' {
t::Sign::N::<1>()
} else if sign == b'-' {
t::Sign::N::<-1>()
} else {
return Err(Error::from(E::InvalidSignPlusOrMinus));
return Err(err!(
"expected '+' or '-' sign at start of UTC numeric offset, \
but found {found:?} instead",
found = escape::Byte(sign),
));
};
Ok(Parsed { value: sign, input: &input[1..] })
}
@ -569,16 +641,22 @@ impl Parser {
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ParsedOffsetHours>, Error> {
let (hours, input) =
parse::split(input, 2).ok_or(E::EndOfInputHour)?;
let hours = parse::i64(hours).context(E::ParseHours)?;
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),
)
})?;
// 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(E::RangeHours)?;
.context("offset hours are not valid")?;
Ok(Parsed { value: hours, input })
}
@ -587,11 +665,20 @@ impl Parser {
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ParsedOffsetMinutes>, Error> {
let (minutes, input) =
parse::split(input, 2).ok_or(E::EndOfInputMinute)?;
let minutes = parse::i64(minutes).context(E::ParseMinutes)?;
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 = ParsedOffsetMinutes::try_new("minutes", minutes)
.context(E::RangeMinutes)?;
.context("minutes are not valid")?;
Ok(Parsed { value: minutes, input })
}
@ -600,11 +687,20 @@ impl Parser {
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ParsedOffsetSeconds>, Error> {
let (seconds, input) =
parse::split(input, 2).ok_or(E::EndOfInputSecond)?;
let seconds = parse::i64(seconds).context(E::ParseSeconds)?;
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 = ParsedOffsetSeconds::try_new("seconds", seconds)
.context(E::RangeSeconds)?;
.context("time zone offset seconds are not valid")?;
Ok(Parsed { value: seconds, input })
}
@ -845,7 +941,7 @@ mod tests {
fn err_numeric_empty() {
insta::assert_snapshot!(
Parser::new().parse_numeric(b"").unwrap_err(),
@"failed to parse sign in UTC numeric offset: expected UTC numeric offset, but found end of input",
@r###"failed to parse sign in UTC numeric offset "": expected UTC numeric offset, but found end of input"###,
);
}
@ -854,7 +950,7 @@ mod tests {
fn err_numeric_notsign() {
insta::assert_snapshot!(
Parser::new().parse_numeric(b"*").unwrap_err(),
@"failed to parse sign in UTC numeric offset: expected `+` or `-` sign at start of UTC numeric offset",
@r###"failed to parse sign in UTC numeric offset "*": expected '+' or '-' sign at start of UTC numeric offset, but found "*" instead"###,
);
}
@ -863,7 +959,7 @@ mod tests {
fn err_numeric_hours_too_short() {
insta::assert_snapshot!(
Parser::new().parse_numeric(b"+a").unwrap_err(),
@"failed to parse hours in UTC numeric offset: expected two digit hour after sign, but found end of input",
@r###"failed to parse hours in UTC numeric offset "+a": expected two digit hour after sign, but found end of input"###,
);
}
@ -872,7 +968,7 @@ mod tests {
fn err_numeric_hours_invalid_digits() {
insta::assert_snapshot!(
Parser::new().parse_numeric(b"+ab").unwrap_err(),
@"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",
@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"###,
);
}
@ -881,7 +977,7 @@ mod tests {
fn err_numeric_hours_out_of_range() {
insta::assert_snapshot!(
Parser::new().parse_numeric(b"-26").unwrap_err(),
@"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",
@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"###,
);
}
@ -890,7 +986,7 @@ mod tests {
fn err_numeric_minutes_too_short() {
insta::assert_snapshot!(
Parser::new().parse_numeric(b"+05:a").unwrap_err(),
@"failed to parse minutes in UTC numeric offset: expected two digit minute after hours, but found end of input",
@r###"failed to parse minutes in UTC numeric offset "+05:a": expected two digit minute after hours, but found end of input"###,
);
}
@ -899,7 +995,7 @@ mod tests {
fn err_numeric_minutes_invalid_digits() {
insta::assert_snapshot!(
Parser::new().parse_numeric(b"+05:ab").unwrap_err(),
@"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",
@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"###,
);
}
@ -908,7 +1004,7 @@ mod tests {
fn err_numeric_minutes_out_of_range() {
insta::assert_snapshot!(
Parser::new().parse_numeric(b"-05:60").unwrap_err(),
@"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",
@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"###,
);
}
@ -917,7 +1013,7 @@ mod tests {
fn err_numeric_seconds_too_short() {
insta::assert_snapshot!(
Parser::new().parse_numeric(b"+05:30:a").unwrap_err(),
@"failed to parse seconds in UTC numeric offset: expected two digit second after minutes, but found end of input",
@r###"failed to parse seconds in UTC numeric offset "+05:30:a": expected two digit second after hours, but found end of input"###,
);
}
@ -926,7 +1022,7 @@ mod tests {
fn err_numeric_seconds_invalid_digits() {
insta::assert_snapshot!(
Parser::new().parse_numeric(b"+05:30:ab").unwrap_err(),
@"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",
@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"###,
);
}
@ -935,7 +1031,7 @@ mod tests {
fn err_numeric_seconds_out_of_range() {
insta::assert_snapshot!(
Parser::new().parse_numeric(b"-05:30:60").unwrap_err(),
@"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",
@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"###,
);
}
@ -945,31 +1041,31 @@ mod tests {
fn err_numeric_fraction_non_empty() {
insta::assert_snapshot!(
Parser::new().parse_numeric(b"-05:30:44.").unwrap_err(),
@"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal",
@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"###,
);
insta::assert_snapshot!(
Parser::new().parse_numeric(b"-05:30:44,").unwrap_err(),
@"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal",
@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"###,
);
// Instead of end-of-string, add invalid digit.
insta::assert_snapshot!(
Parser::new().parse_numeric(b"-05:30:44.a").unwrap_err(),
@"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal",
@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"###,
);
insta::assert_snapshot!(
Parser::new().parse_numeric(b"-05:30:44,a").unwrap_err(),
@"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal",
@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"###,
);
// And also test basic format.
insta::assert_snapshot!(
Parser::new().parse_numeric(b"-053044.a").unwrap_err(),
@"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal",
@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"###,
);
insta::assert_snapshot!(
Parser::new().parse_numeric(b"-053044,a").unwrap_err(),
@"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal",
@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"###,
);
}
@ -980,7 +1076,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(),
@"subminute precision for UTC numeric offset is not enabled in this context (must provide only integral minutes)",
@r###"subminute precision for UTC numeric offset "-05:59:32" is not enabled in this context (must provide only integral minutes)"###,
);
}
@ -990,11 +1086,11 @@ mod tests {
fn err_zulu_disabled_but_desired() {
insta::assert_snapshot!(
Parser::new().zulu(false).parse(b"Z").unwrap_err(),
@"found `Z` where a numeric UTC offset was expected (this context does not permit the Zulu offset)",
@r###"found "Z" in "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(),
@"found `z` where a numeric UTC offset was expected (this context does not permit the Zulu offset)",
@r###"found "z" in "z" where a numeric UTC offset was expected (this context does not permit the Zulu offset)"###,
);
}
@ -1022,7 +1118,7 @@ mod tests {
};
insta::assert_snapshot!(
numeric.to_offset().unwrap_err(),
@"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",
@"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",
);
}
@ -1047,7 +1143,7 @@ mod tests {
};
insta::assert_snapshot!(
numeric.to_offset().unwrap_err(),
@"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",
@"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",
);
}
}

View file

@ -43,11 +43,11 @@ general interchange format for new applications.
use crate::{
civil::{Date, DateTime, Time, Weekday},
error::{fmt::rfc2822::Error as E, ErrorContext},
fmt::{util::IntegerFormatter, Parsed, Write, WriteExt},
error::{err, ErrorContext},
fmt::{util::DecimalFormatter, Parsed, Write, WriteExt},
tz::{Offset, TimeZone},
util::{
parse,
escape, parse,
rangeint::{ri8, RFrom},
t::{self, C},
},
@ -313,7 +313,9 @@ impl DateTimeParser {
let input = input.as_ref();
let zdt = self
.parse_zoned_internal(input)
.context(E::FailedZoned)?
.context(
"failed to parse RFC 2822 datetime into Jiff zoned datetime",
)?
.into_full()?;
Ok(zdt)
}
@ -349,7 +351,7 @@ impl DateTimeParser {
let input = input.as_ref();
let ts = self
.parse_timestamp_internal(input)
.context(E::FailedTimestamp)?
.context("failed to parse RFC 2822 datetime into Jiff timestamp")?
.into_full()?;
Ok(ts)
}
@ -365,7 +367,9 @@ impl DateTimeParser {
) -> Result<Parsed<'i, Zoned>, Error> {
let Parsed { value: (dt, offset), input } =
self.parse_datetime_offset(input)?;
let ts = offset.to_timestamp(dt)?;
let ts = offset
.to_timestamp(dt)
.context("RFC 2822 datetime out of Jiff's range")?;
let zdt = ts.to_zoned(TimeZone::fixed(offset));
Ok(Parsed { value: zdt, input })
}
@ -381,7 +385,9 @@ impl DateTimeParser {
) -> Result<Parsed<'i, Timestamp>, Error> {
let Parsed { value: (dt, offset), input } =
self.parse_datetime_offset(input)?;
let ts = offset.to_timestamp(dt)?;
let ts = offset
.to_timestamp(dt)
.context("RFC 2822 datetime out of Jiff's range")?;
Ok(Parsed { value: ts, input })
}
@ -419,11 +425,16 @@ impl DateTimeParser {
input: &'i [u8],
) -> Result<Parsed<'i, DateTime>, Error> {
if input.is_empty() {
return Err(Error::from(E::Empty));
return Err(err!(
"expected RFC 2822 datetime, but got empty string"
));
}
let Parsed { input, .. } = self.skip_whitespace(input);
if input.is_empty() {
return Err(Error::from(E::EmptyAfterWhitespace));
return Err(err!(
"expected RFC 2822 datetime, but got empty string after \
trimming whitespace",
));
}
let Parsed { value: wd, input } = self.parse_weekday(input)?;
let Parsed { value: day, input } = self.parse_day(input)?;
@ -440,19 +451,26 @@ impl DateTimeParser {
self.skip_whitespace(input);
let (second, input) = if !input.starts_with(b":") {
if !whitespace_after_minute {
return Err(Error::from(E::WhitespaceAfterTime));
return Err(err!(
"expected whitespace after parsing time: \
expected at least one whitespace character \
(space or tab), but found none",
));
}
(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)?;
let Parsed { input, .. } =
self.parse_whitespace(input).with_context(|| {
err!("expected whitespace after parsing time")
})?;
(second, input)
};
let date =
Date::new_ranged(year, month, day).context(E::InvalidDate)?;
Date::new_ranged(year, month, day).context("invalid date")?;
let time = Time::new_ranged(
hour,
minute,
@ -462,10 +480,13 @@ impl DateTimeParser {
let dt = DateTime::from_parts(date, time);
if let Some(wd) = wd {
if !self.relaxed_weekday && wd != dt.weekday() {
return Err(Error::from(E::InconsistentWeekday {
parsed: wd,
from_date: 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()),
));
}
}
Ok(Parsed { value: dt, input })
@ -496,13 +517,15 @@ impl DateTimeParser {
if matches!(input[0], b'0'..=b'9') {
return Ok(Parsed { value: None, input });
}
if let Ok(len) = u8::try_from(input.len()) {
if len < 4 {
return Err(Error::from(E::TooShortWeekday {
got_non_digit: input[0],
len,
}));
}
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(),
));
}
let b1 = input[0];
let b2 = input[1];
@ -520,19 +543,31 @@ impl DateTimeParser {
b"fri" => Weekday::Friday,
b"sat" => Weekday::Saturday,
_ => {
return Err(Error::from(E::InvalidWeekday {
got_non_digit: input[0],
}));
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]),
));
}
};
let Parsed { input, .. } = self.skip_whitespace(&input[3..]);
let Some(should_be_comma) = input.get(0).copied() else {
return Err(Error::from(E::EndOfInputComma));
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]),
));
};
if should_be_comma != b',' {
return Err(Error::from(E::UnexpectedByteComma {
byte: should_be_comma,
}));
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),
));
}
let Parsed { input, .. } = self.skip_whitespace(&input[1..]);
Ok(Parsed { value: Some(wd), input })
@ -551,17 +586,21 @@ impl DateTimeParser {
input: &'i [u8],
) -> Result<Parsed<'i, t::Day>, Error> {
if input.is_empty() {
return Err(Error::from(E::EndOfInputDay));
return Err(err!("expected day, but found end of input"));
}
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).context(E::ParseDay)?;
let day = t::Day::try_new("day", day).context(E::ParseDay)?;
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 Parsed { input, .. } =
self.parse_whitespace(input).context(E::WhitespaceAfterDay)?;
self.parse_whitespace(input).with_context(|| {
err!("expected whitespace after parsing day {day}")
})?;
Ok(Parsed { value: day, input })
}
@ -578,12 +617,16 @@ impl DateTimeParser {
input: &'i [u8],
) -> Result<Parsed<'i, t::Month>, Error> {
if input.is_empty() {
return Err(Error::from(E::EndOfInputMonth));
return Err(err!(
"expected abbreviated month name, but found end of input"
));
}
if let Ok(len) = u8::try_from(input.len()) {
if len < 3 {
return Err(Error::from(E::TooShortMonth { len }));
}
if input.len() < 3 {
return Err(err!(
"expected abbreviated month name, but remaining input \
is too short (remaining bytes is {length})",
length = input.len(),
));
}
let b1 = input[0].to_ascii_lowercase();
let b2 = input[1].to_ascii_lowercase();
@ -601,14 +644,22 @@ impl DateTimeParser {
b"oct" => 10,
b"nov" => 11,
b"dec" => 12,
_ => return Err(Error::from(E::InvalidMonth)),
_ => {
return Err(err!(
"expected abbreviated month name, \
but did not recognize {got:?} \
as a valid month",
got = escape::Bytes(&input[..3]),
));
}
};
// 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..])
.context(E::WhitespaceAfterMonth)?;
let Parsed { input, .. } =
self.parse_whitespace(&input[3..]).with_context(|| {
err!("expected whitespace after parsing month name")
})?;
Ok(Parsed { value: month, input })
}
@ -641,22 +692,31 @@ impl DateTimeParser {
{
digits += 1;
}
if let Ok(len) = u8::try_from(digits) {
if len <= 1 {
return Err(Error::from(E::TooShortYear { len }));
}
if digits <= 1 {
return Err(err!(
"expected at least two ASCII digits for parsing \
a year, but only found {digits}",
));
}
let (year, input) = input.split_at(digits);
let year = parse::i64(year).context(E::ParseYear)?;
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 = 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(E::InvalidYear)?;
let Parsed { input, .. } =
self.parse_whitespace(input).context(E::WhitespaceAfterYear)?;
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"))?;
Ok(Parsed { value: year, input })
}
@ -670,9 +730,17 @@ impl DateTimeParser {
&self,
input: &'i [u8],
) -> Result<Parsed<'i, t::Hour>, Error> {
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)?;
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")?;
Ok(Parsed { value: hour, input })
}
@ -683,11 +751,17 @@ impl DateTimeParser {
&self,
input: &'i [u8],
) -> Result<Parsed<'i, t::Minute>, Error> {
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)?;
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")?;
Ok(Parsed { value: minute, input })
}
@ -698,14 +772,20 @@ impl DateTimeParser {
&self,
input: &'i [u8],
) -> Result<Parsed<'i, t::Second>, Error> {
let (second, input) =
parse::split(input, 2).ok_or(E::EndOfInputSecond)?;
let mut second = parse::i64(second).context(E::ParseSecond)?;
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),
)
})?;
if second == 60 {
second = 59;
}
let second =
t::Second::try_new("second", second).context(E::InvalidSecond)?;
let second = t::Second::try_new("second", second)
.context("second is not valid")?;
Ok(Parsed { value: second, input })
}
@ -721,7 +801,13 @@ impl DateTimeParser {
type ParsedOffsetHours = ri8<0, { t::SpanZoneOffsetHours::MAX }>;
type ParsedOffsetMinutes = ri8<0, { t::SpanZoneOffsetMinutes::MAX }>;
let sign = input.get(0).copied().ok_or(E::EndOfInputOffset)?;
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 = if sign == b'+' {
t::Sign::N::<1>()
} else if sign == b'-' {
@ -730,16 +816,32 @@ impl DateTimeParser {
return self.parse_offset_obsolete(input);
};
let input = &input[1..];
let (hhmm, input) = parse::split(input, 4).ok_or(E::TooShortOffset)?;
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 hh = parse::i64(&hhmm[0..2]).context(E::ParseOffsetHour)?;
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 = ParsedOffsetHours::try_new("zone-offset-hours", hh)
.context(E::InvalidOffsetHour)?;
.context("time zone offset hours are not valid")?;
let hh = t::SpanZoneOffset::rfrom(hh);
let mm = parse::i64(&hhmm[2..4]).context(E::ParseOffsetMinute)?;
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 = ParsedOffsetMinutes::try_new("zone-offset-minutes", mm)
.context(E::InvalidOffsetMinute)?;
.context("time zone offset minutes are not valid")?;
let mm = t::SpanZoneOffset::rfrom(mm);
let seconds = hh * C(3_600) + mm * C(60);
@ -763,7 +865,11 @@ impl DateTimeParser {
len += 1;
}
if len == 0 {
return Err(Error::from(E::WhitespaceAfterTimeForObsoleteOffset));
return Err(err!(
"expected obsolete RFC 2822 time zone abbreviation, \
but found no remaining non-whitespace characters \
after time",
));
}
let offset = match &letters[..len] {
b"ut" | b"gmt" | b"z" => Offset::UTC,
@ -811,7 +917,11 @@ impl DateTimeParser {
Offset::UTC
} else {
// But anything else we throw our hands up I guess.
return Err(Error::from(E::InvalidObsoleteOffset));
return Err(err!(
"expected obsolete RFC 2822 time zone abbreviation, \
but found {found:?}",
found = escape::Bytes(&input[..len]),
));
}
}
};
@ -826,12 +936,15 @@ impl DateTimeParser {
input: &'i [u8],
) -> Result<Parsed<'i, ()>, Error> {
if input.is_empty() {
return Err(Error::from(E::EndOfInputTimeSeparator));
return Err(err!(
"expected time separator of ':', but found end of input",
));
}
if input[0] != b':' {
return Err(Error::from(E::UnexpectedByteTimeSeparator {
byte: input[0],
}));
return Err(err!(
"expected time separator of ':', but found {got}",
got = escape::Byte(input[0]),
));
}
Ok(Parsed { value: (), input: &input[1..] })
}
@ -846,7 +959,10 @@ impl DateTimeParser {
let Parsed { input, value: had_whitespace } =
self.skip_whitespace(input);
if !had_whitespace {
return Err(Error::from(E::WhitespaceAfterTime));
return Err(err!(
"expected at least one whitespace character (space or tab), \
but found none",
));
}
Ok(Parsed { value: (), input })
}
@ -896,20 +1012,26 @@ 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(E::CommentClosingParenWithoutOpen)?;
depth = depth.checked_sub(1).ok_or_else(|| {
err!(
"found closing parenthesis in comment with \
no matching opening parenthesis"
)
})?;
if depth == 0 {
break;
}
} else if byte == b'(' {
depth = depth
.checked_add(1)
.ok_or(E::CommentTooManyNestedParens)?;
depth = depth.checked_add(1).ok_or_else(|| {
err!("found too many nested parenthesis in comment")
})?;
}
}
if depth > 0 {
return Err(Error::from(E::CommentOpeningParenWithoutClose));
return Err(err!(
"found opening parenthesis in comment with \
no matching closing parenthesis"
));
}
let Parsed { input, .. } = self.skip_whitespace(input);
Ok(Parsed { value: (), input })
@ -1292,16 +1414,19 @@ impl DateTimePrinter {
offset: Option<Offset>,
mut wtr: W,
) -> Result<(), Error> {
static FMT_DAY: IntegerFormatter = IntegerFormatter::new();
static FMT_YEAR: IntegerFormatter = IntegerFormatter::new().padding(4);
static FMT_TIME_UNIT: IntegerFormatter =
IntegerFormatter::new().padding(2);
static FMT_DAY: DecimalFormatter = DecimalFormatter::new();
static FMT_YEAR: DecimalFormatter = DecimalFormatter::new().padding(4);
static FMT_TIME_UNIT: DecimalFormatter =
DecimalFormatter::new().padding(2);
if dt.year() < 0 {
// RFC 2822 actually says the year must be at least 1900, but
// other implementations (like Chrono) allow any positive 4-digit
// year.
return Err(Error::from(E::NegativeYear));
return Err(err!(
"datetime {dt} has negative year, \
which cannot be formatted with RFC 2822",
));
}
wtr.write_str(weekday_abbrev(dt.weekday()))?;
@ -1349,17 +1474,20 @@ impl DateTimePrinter {
timestamp: &Timestamp,
mut wtr: W,
) -> Result<(), Error> {
static FMT_DAY: IntegerFormatter = IntegerFormatter::new().padding(2);
static FMT_YEAR: IntegerFormatter = IntegerFormatter::new().padding(4);
static FMT_TIME_UNIT: IntegerFormatter =
IntegerFormatter::new().padding(2);
static FMT_DAY: DecimalFormatter = DecimalFormatter::new().padding(2);
static FMT_YEAR: DecimalFormatter = DecimalFormatter::new().padding(4);
static FMT_TIME_UNIT: DecimalFormatter =
DecimalFormatter::new().padding(2);
let dt = TimeZone::UTC.to_datetime(*timestamp);
if dt.year() < 0 {
// RFC 2822 actually says the year must be at least 1900, but
// other implementations (like Chrono) allow any positive 4-digit
// year.
return Err(Error::from(E::NegativeYear));
return Err(err!(
"datetime {dt} has negative year, \
which cannot be formatted with RFC 2822",
));
}
wtr.write_str(weekday_abbrev(dt.weekday()))?;
@ -1615,7 +1743,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 `Thursday`, but parsed datetime has weekday `Wednesday`",
@"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",
);
insta::assert_snapshot!(
p("Wed, 29 Feb 2023 05:34:45 -0500"),
@ -1627,11 +1755,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: failed to parse day: parameter 'day' with value 32 is not in the required range of 1..=31",
@"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",
);
insta::assert_snapshot!(
p("Sun, 30 Jun 2024 24:00:00 -0500"),
@"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",
@"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",
);
// No whitespace after time
insta::assert_snapshot!(
@ -1652,43 +1780,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 leading whitespace",
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected RFC 2822 datetime, but got empty string after trimming whitespace",
);
insta::assert_snapshot!(
p("Wat"),
@"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)",
@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)"###,
);
insta::assert_snapshot!(
p("Wed"),
@"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)",
@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)"###,
);
insta::assert_snapshot!(
p("Wed "),
@"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",
@"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",
);
insta::assert_snapshot!(
p("Wed ,"),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected numeric day, but found end of input",
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day, but found end of input",
);
insta::assert_snapshot!(
p("Wed , "),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected numeric day, but found end of input",
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day, but found end of input",
);
insta::assert_snapshot!(
p("Wat, "),
@"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",
@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"###,
);
insta::assert_snapshot!(
p("Wed, "),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected numeric day, but found end of input",
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected 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: expected whitespace after parsing time: 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 1: 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: expected whitespace after parsing time: 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 10: expected at least one whitespace character (space or tab), but found none",
);
insta::assert_snapshot!(
p("Wed, 10 J"),
@ -1696,11 +1824,11 @@ mod tests {
);
insta::assert_snapshot!(
p("Wed, 10 Wat"),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected abbreviated month name, but did not recognize a valid abbreviated month name",
@r###"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected abbreviated month name, but did not recognize "Wat" as a valid month"###,
);
insta::assert_snapshot!(
p("Wed, 10 Jan"),
@"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",
@"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",
);
insta::assert_snapshot!(
p("Wed, 10 Jan 2"),
@ -1708,15 +1836,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 whitespace after parsing time: 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 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"),
@ -1732,7 +1860,7 @@ mod tests {
);
insta::assert_snapshot!(
p("Wed, 10 Jan 2024 05:34:45 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",
@r###"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected obsolete RFC 2822 time zone abbreviation, but found "J""###,
);
}
@ -1912,7 +2040,7 @@ mod tests {
.at(5, 34, 45, 0)
.in_tz("America/New_York")
.unwrap();
insta::assert_snapshot!(p(&zdt), @"datetime has negative year, which cannot be formatted with RFC 2822");
insta::assert_snapshot!(p(&zdt), @"datetime -000001-01-10T05:34:45 has negative year, which cannot be formatted with RFC 2822");
}
#[test]
@ -1934,6 +2062,6 @@ mod tests {
.in_tz("America/New_York")
.unwrap()
.timestamp();
insta::assert_snapshot!(p(ts), @"datetime has negative year, which cannot be formatted with RFC 2822");
insta::assert_snapshot!(p(ts), @"datetime -000001-01-10T10:30:47 has negative year, which cannot be formatted with RFC 2822");
}
}

View file

@ -95,13 +95,13 @@ including by returning an error if it isn't supported.
// UTCOffsetMinutePrecision
use crate::{
error::{fmt::rfc9557::Error as E, Error},
error::{err, Error},
fmt::{
offset::{self, ParsedOffset},
temporal::{TimeZoneAnnotation, TimeZoneAnnotationKind},
Parsed,
},
util::parse,
util::{escape, parse},
};
/// The result of parsing RFC 9557 annotations.
@ -112,6 +112,11 @@ 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
@ -122,7 +127,7 @@ pub(crate) struct ParsedAnnotations<'i> {
impl<'i> ParsedAnnotations<'i> {
/// Return an empty parsed annotations.
pub(crate) fn none() -> ParsedAnnotations<'static> {
ParsedAnnotations { time_zone: None }
ParsedAnnotations { input: escape::Bytes(&[]), time_zone: None }
}
/// Turns this parsed time zone into a structured time zone annotation,
@ -207,6 +212,8 @@ 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 {
@ -222,7 +229,10 @@ impl Parser {
input = unconsumed;
}
let value = ParsedAnnotations { time_zone };
let value = ParsedAnnotations {
input: escape::Bytes(mkslice(input)),
time_zone,
};
Ok(Parsed { value, input })
}
@ -231,18 +241,14 @@ impl Parser {
mut input: &'i [u8],
) -> Result<Parsed<'i, Option<ParsedTimeZone<'i>>>, Error> {
let unconsumed = input;
let Some((&first, tail)) = input.split_first() else {
return Ok(Parsed { value: None, input: unconsumed });
};
if first != b'[' {
if input.is_empty() || input[0] != b'[' {
return Ok(Parsed { value: None, input: unconsumed });
}
input = tail;
input = &input[1..];
let mut critical = false;
if let Some(tail) = input.strip_prefix(b"!") {
critical = true;
input = tail;
let critical = input.starts_with(b"!");
if critical {
input = &input[1..];
}
// If we're starting with a `+` or `-`, then we know we MUST have a
@ -278,8 +284,8 @@ impl Parser {
// a generic key/value annotation.
return Ok(Parsed { value: None, input: unconsumed });
}
while let Some(tail) = input.strip_prefix(b"/") {
input = tail;
while input.starts_with(b"/") {
input = &input[1..];
let Parsed { input: unconsumed, .. } =
self.parse_tz_annotation_iana_name(input)?;
input = unconsumed;
@ -300,21 +306,17 @@ impl Parser {
&self,
mut input: &'i [u8],
) -> Result<Parsed<'i, bool>, Error> {
let Some((&first, tail)) = input.split_first() else {
return Ok(Parsed { value: false, input });
};
if first != b'[' {
if input.is_empty() || input[0] != b'[' {
return Ok(Parsed { value: false, input });
}
input = tail;
input = &input[1..];
let mut critical = false;
if let Some(tail) = input.strip_prefix(b"!") {
critical = true;
input = tail;
let critical = input.starts_with(b"!");
if critical {
input = &input[1..];
}
let Parsed { input, .. } = self.parse_annotation_key(input)?;
let Parsed { value: key, 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)?;
@ -324,7 +326,11 @@ 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(Error::from(E::UnsupportedAnnotationCritical));
return Err(err!(
"found unsupported RFC 9557 annotation with key {key:?} \
with the critical flag ('!') set",
key = escape::Bytes(key),
));
}
Ok(Parsed { value: true, input })
@ -375,8 +381,8 @@ impl Parser {
input: &'i [u8],
) -> Result<Parsed<'i, ()>, Error> {
let Parsed { mut input, .. } = self.parse_annotation_value(input)?;
while let Some(tail) = input.strip_prefix(b"-") {
input = tail;
while input.starts_with(b"-") {
input = &input[1..];
let Parsed { input: unconsumed, .. } =
self.parse_annotation_value(input)?;
input = unconsumed;
@ -407,137 +413,173 @@ impl Parser {
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ()>, Error> {
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 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",
));
}
Ok(Parsed { value: (), input: tail })
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..] })
}
fn parse_tz_annotation_char<'i>(
&self,
input: &'i [u8],
) -> Parsed<'i, bool> {
let Some((&first, tail)) = input.split_first() else {
return Parsed { value: false, input };
let is_tz_annotation_char = |byte| {
matches!(
byte,
b'_' | b'.' | b'+' | b'-' | b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z',
)
};
if !matches!(
first,
b'_' | b'.' | b'+' | b'-' | b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z',
) {
if input.is_empty() || !is_tz_annotation_char(input[0]) {
return Parsed { value: false, input };
}
Parsed { value: true, input: tail }
Parsed { value: true, input: &input[1..] }
}
fn parse_annotation_key_leading_char<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ()>, Error> {
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 input.is_empty() {
return Err(err!(
"expected the start of an RFC 9557 annotation key, \
but found end of input instead",
));
}
Ok(Parsed { value: (), input: tail })
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..] })
}
fn parse_annotation_key_char<'i>(
&self,
input: &'i [u8],
) -> Parsed<'i, bool> {
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') {
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]) {
return Parsed { value: false, input };
}
Parsed { value: true, input: tail }
Parsed { value: true, input: &input[1..] }
}
fn parse_annotation_value_leading_char<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ()>, Error> {
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 input.is_empty() {
return Err(err!(
"expected the start of an RFC 9557 annotation value, \
but found end of input instead",
));
}
Ok(Parsed { value: (), input: tail })
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..] })
}
fn parse_annotation_value_char<'i>(
&self,
input: &'i [u8],
) -> Parsed<'i, bool> {
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') {
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]) {
return Parsed { value: false, input };
}
Parsed { value: true, input: tail }
Parsed { value: true, input: &input[1..] }
}
fn parse_annotation_separator<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ()>, Error> {
let Some((&first, tail)) = input.split_first() else {
return Err(Error::from(E::EndOfInputAnnotationSeparator));
};
if first != b'=' {
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'=' {
// If we see a /, then it's likely the user was trying to insert a
// time zone annotation in the wrong place.
return Err(Error::from(if first == b'/' {
E::UnexpectedSlashAnnotationSeparator
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)",
)
} else {
E::UnexpectedByteAnnotationSeparator { byte: first }
}));
err!(
"expected an '=' after parsing an RFC 9557 annotation \
key, but found {:?} instead",
escape::Byte(input[0]),
)
});
}
Ok(Parsed { value: (), input: tail })
Ok(Parsed { value: (), input: &input[1..] })
}
fn parse_annotation_close<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ()>, Error> {
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.is_empty() {
return Err(err!(
"expected an ']' after parsing an RFC 9557 annotation key \
and value, but found end of input instead",
));
}
Ok(Parsed { value: (), input: tail })
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..] })
}
fn parse_tz_annotation_close<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ()>, Error> {
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.is_empty() {
return Err(err!(
"expected an ']' after parsing an RFC 9557 time zone \
annotation, but found end of input instead",
));
}
Ok(Parsed { value: (), input: tail })
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..] })
}
}
@ -623,22 +665,24 @@ 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]
@ -647,36 +691,39 @@ 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: "",
}
"#,
"###,
);
}
@ -684,9 +731,10 @@ 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,
@ -696,10 +744,11 @@ 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,
@ -709,10 +758,11 @@ 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,
@ -722,10 +772,11 @@ 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,
@ -735,16 +786,17 @@ 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,
@ -758,10 +810,11 @@ 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,
@ -775,10 +828,11 @@ 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,
@ -792,10 +846,11 @@ 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,
@ -809,7 +864,7 @@ mod tests {
},
input: "",
}
"#);
"###);
}
#[test]
@ -818,9 +873,10 @@ 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,
@ -830,7 +886,7 @@ mod tests {
},
input: "",
}
"#,
"###,
);
}
@ -838,11 +894,11 @@ mod tests {
fn err_iana() {
insta::assert_snapshot!(
Parser::new().parse(b"[0/Foo]").unwrap_err(),
@"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",
@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"###,
);
insta::assert_snapshot!(
Parser::new().parse(b"[Foo/0Bar]").unwrap_err(),
@"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",
@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"###,
);
}
@ -850,23 +906,23 @@ mod tests {
fn err_offset() {
insta::assert_snapshot!(
Parser::new().parse(b"[+").unwrap_err(),
@"failed to parse hours in UTC numeric offset: expected two digit hour after sign, but found end of input",
@r###"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(),
@"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",
@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"###,
);
insta::assert_snapshot!(
Parser::new().parse(b"[-26]").unwrap_err(),
@"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",
@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"###,
);
insta::assert_snapshot!(
Parser::new().parse(b"[+05:12:34]").unwrap_err(),
@"subminute precision for UTC numeric offset is not enabled in this context (must provide only integral minutes)",
@r###"subminute precision for UTC numeric offset "+05:12:34]" is not enabled in this context (must provide only integral minutes)"###,
);
insta::assert_snapshot!(
Parser::new().parse(b"[+05:12:34.123456789]").unwrap_err(),
@"subminute precision for UTC numeric offset is not enabled in this context (must provide only integral minutes)",
@r###"subminute precision for UTC numeric offset "+05:12:34.123456789]" is not enabled in this context (must provide only integral minutes)"###,
);
}
@ -874,7 +930,7 @@ mod tests {
fn err_critical_unsupported() {
insta::assert_snapshot!(
Parser::new().parse(b"[!u-ca=chinese]").unwrap_err(),
@"found unsupported RFC 9557 annotation with the critical flag (`!`) set",
@r###"found unsupported RFC 9557 annotation with key "u-ca" with the critical flag ('!') set"###,
);
}
@ -886,7 +942,7 @@ mod tests {
);
insta::assert_snapshot!(
Parser::new().parse(b"[&").unwrap_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",
@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"###,
);
insta::assert_snapshot!(
Parser::new().parse(b"[Foo][").unwrap_err(),
@ -894,7 +950,7 @@ mod tests {
);
insta::assert_snapshot!(
Parser::new().parse(b"[Foo][&").unwrap_err(),
@"expected lowercase alphabetic byte (or underscore) at the start of an RFC 9557 annotation key, but found `&` instead",
@r###"expected lowercase alphabetic byte (or underscore) at the start of an RFC 9557 annotation key, but found "&" instead"###,
);
}
@ -902,27 +958,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(),
@"expected an `]` after parsing an RFC 9557 time zone annotation, but found `^` instead",
@r###"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(),
@"expected an `=` after parsing an RFC 9557 annotation key, but found `^` instead",
@r###"expected an '=' after parsing an RFC 9557 annotation key, but found "^" instead"###,
);
}
@ -938,11 +994,11 @@ mod tests {
);
insta::assert_snapshot!(
Parser::new().parse(b"[abc=^").unwrap_err(),
@"expected alphanumeric ASCII byte at the start of an RFC 9557 annotation value, but found `^` instead",
@r###"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(),
@"expected alphanumeric ASCII byte at the start of an RFC 9557 annotation value, but found `]` instead",
@r###"expected alphanumeric ASCII byte at the start of an RFC 9557 annotation value, but found "]" instead"###,
);
}
@ -950,11 +1006,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(),
@"expected an `]` after parsing an RFC 9557 annotation key and value, but found `*` instead",
@r###"expected an ']' after parsing an RFC 9557 annotation key and value, but found "*" instead"###,
);
}
@ -990,7 +1046,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)",
);
}
}

View file

@ -1778,24 +1778,28 @@ pub mod unsigned_duration {
fn parse_iso_or_friendly(
bytes: &[u8],
) -> Result<core::time::Duration, crate::Error> {
let Some((&byte, tail)) = bytes.split_first() else {
return Err(crate::Error::from(
crate::error::fmt::Error::HybridDurationEmpty,
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 mut first = byte;
}
let mut first = bytes[0];
// 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'-' {
let Some(&byte) = tail.first() else {
return Err(crate::Error::from(
crate::error::fmt::Error::HybridDurationPrefix {
sign: first,
},
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),
));
};
first = byte;
}
first = bytes[1];
}
let dur = if first == b'P' || first == b'p' {
crate::fmt::temporal::DEFAULT_SPAN_PARSER

View file

@ -1,19 +1,16 @@
use crate::{
error::{
fmt::strtime::{Error as E, FormatError as FE},
ErrorContext,
},
error::{err, ErrorContext},
fmt::{
strtime::{
month_name_abbrev, month_name_full, weekday_name_abbrev,
weekday_name_full, BrokenDownTime, Config, Custom, Extension,
Flag,
},
util::{FractionalFormatter, IntegerFormatter},
util::{DecimalFormatter, FractionalFormatter},
Write, WriteExt,
},
tz::Offset,
util::utf8,
util::{escape, utf8},
Error,
};
@ -42,7 +39,10 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
self.wtr.write_str("%")?;
break;
}
return Err(E::UnexpectedEndAfterPercent.into());
return Err(err!(
"invalid format string, expected byte after '%', \
but found end of format string",
));
}
let orig = self.fmt;
if let Err(err) = self.format_one() {
@ -61,92 +61,100 @@ 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(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'%' => 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'Q' => match ext.colons {
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()),
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"
))
}
},
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'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'z' => match ext.colons {
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()),
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"
))
}
},
b'.' => {
if !self.bump_fmt() {
return Err(E::UnexpectedEndAfterDot.into());
return Err(err!(
"invalid format string, expected directive after '%.'",
));
}
// 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(E::DirectiveFailureDot { directive: b'f' }),
b'f' => {
self.fmt_dot_fractional(&ext).context("%.f failed")?
}
unk => {
return Err(Error::from(
E::UnknownDirectiveAfterDot { directive: unk },
return Err(err!(
"found unrecognized directive %{unk} following %.",
unk = escape::Byte(unk),
));
}
}
}
unk => {
return Err(Error::from(E::UnknownDirective {
directive: unk,
}))
return Err(err!(
"found unrecognized specifier directive %{unk}",
unk = escape::Byte(unk),
));
}
}?;
}
self.bump_fmt();
Ok(())
}
@ -192,17 +200,21 @@ 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, FE> {
fn utf8_decode_and_bump(&mut self) -> Result<char, Error> {
match utf8::decode(self.fmt).expect("non-empty fmt") {
Ok(ch) => {
self.fmt = &self.fmt[ch.len_utf8()..];
return Ok(ch);
}
Err(err) if self.config.lenient => {
self.fmt = &self.fmt[err.len()..];
Err(errant_bytes) if self.config.lenient => {
self.fmt = &self.fmt[errant_bytes.len()..];
return Ok(char::REPLACEMENT_CHARACTER);
}
Err(_) => Err(FE::InvalidUtf8),
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),
)),
}
}
@ -258,7 +270,11 @@ 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(FE::RequiresTime)?.get();
let hour = self
.tm
.hour_ranged()
.ok_or_else(|| err!("requires time to format AM/PM"))?
.get();
ext.write_str(
Case::AsIs,
if hour < 12 { "am" } else { "pm" },
@ -268,7 +284,11 @@ 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(FE::RequiresTime)?.get();
let hour = self
.tm
.hour_ranged()
.ok_or_else(|| err!("requires time to format AM/PM"))?
.get();
// Manually specialize this case to avoid hitting `write_str_cold`.
let s = if matches!(ext.flag, Some(Flag::Swapcase)) {
if hour < 12 {
@ -319,11 +339,8 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
let day = self
.tm
.day
.or_else(
#[inline(never)]
|| self.tm.to_date().ok().map(|d| d.day_ranged()),
)
.ok_or(FE::RequiresDate)?
.or_else(|| self.tm.to_date().ok().map(|d| d.day_ranged()))
.ok_or_else(|| err!("requires date to format day"))?
.get();
ext.write_int(b'0', Some(2), day, self.wtr)
}
@ -333,18 +350,19 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
let day = self
.tm
.day
.or_else(
#[inline(never)]
|| self.tm.to_date().ok().map(|d| d.day_ranged()),
)
.ok_or(FE::RequiresDate)?
.or_else(|| self.tm.to_date().ok().map(|d| d.day_ranged()))
.ok_or_else(|| err!("requires date to format day"))?
.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(FE::RequiresTime)?.get();
let mut hour = self
.tm
.hour_ranged()
.ok_or_else(|| err!("requires time to format hour"))?
.get();
if hour == 0 {
hour = 12;
} else if hour > 12 {
@ -355,13 +373,21 @@ 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(FE::RequiresTime)?.get();
let hour = self
.tm
.hour_ranged()
.ok_or_else(|| err!("requires time to format hour"))?
.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(FE::RequiresTime)?.get();
let mut hour = self
.tm
.hour_ranged()
.ok_or_else(|| err!("requires time to format hour"))?
.get();
if hour == 0 {
hour = 12;
} else if hour > 12 {
@ -372,7 +398,11 @@ 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(FE::RequiresTime)?.get();
let hour = self
.tm
.hour_ranged()
.ok_or_else(|| err!("requires time to format hour"))?
.get();
ext.write_int(b' ', Some(2), hour, self.wtr)
}
@ -388,7 +418,11 @@ 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(FE::RequiresTime)?.get();
let minute = self
.tm
.minute
.ok_or_else(|| err!("requires time to format minute"))?
.get();
ext.write_int(b'0', Some(2), minute, self.wtr)
}
@ -397,11 +431,8 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
let month = self
.tm
.month
.or_else(
#[inline(never)]
|| self.tm.to_date().ok().map(|d| d.month_ranged()),
)
.ok_or(FE::RequiresDate)?
.or_else(|| self.tm.to_date().ok().map(|d| d.month_ranged()))
.ok_or_else(|| err!("requires date to format month"))?
.get();
ext.write_int(b'0', Some(2), month, self.wtr)
}
@ -411,11 +442,8 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
let month = self
.tm
.month
.or_else(
#[inline(never)]
|| self.tm.to_date().ok().map(|d| d.month_ranged()),
)
.ok_or(FE::RequiresDate)?;
.or_else(|| self.tm.to_date().ok().map(|d| d.month_ranged()))
.ok_or_else(|| err!("requires date to format month"))?;
ext.write_str(Case::AsIs, month_name_full(month), self.wtr)
}
@ -424,18 +452,20 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
let month = self
.tm
.month
.or_else(
#[inline(never)]
|| self.tm.to_date().ok().map(|d| d.month_ranged()),
)
.ok_or(FE::RequiresDate)?;
.or_else(|| self.tm.to_date().ok().map(|d| d.month_ranged()))
.ok_or_else(|| err!("requires date to format month"))?;
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(FE::RequiresTimeZoneOrOffset)?;
let offset = self.tm.offset.ok_or_else(|| {
err!(
"requires IANA time zone identifier or time \
zone offset, but none were present"
)
})?;
return write_offset(offset, false, true, false, &mut self.wtr);
};
self.wtr.write_str(iana)?;
@ -445,7 +475,12 @@ 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(FE::RequiresTimeZoneOrOffset)?;
let offset = self.tm.offset.ok_or_else(|| {
err!(
"requires IANA time zone identifier or time \
zone offset, but none were present"
)
})?;
return write_offset(offset, true, true, false, &mut self.wtr);
};
self.wtr.write_str(iana)?;
@ -454,44 +489,62 @@ 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(FE::RequiresOffset)?;
let offset = self.tm.offset.ok_or_else(|| {
err!("requires offset to format time zone offset")
})?;
write_offset(offset, false, true, false, self.wtr)
}
/// %:z
fn fmt_offset_colon(&mut self) -> Result<(), Error> {
let offset = self.tm.offset.ok_or(FE::RequiresOffset)?;
let offset = self.tm.offset.ok_or_else(|| {
err!("requires offset to format time zone offset")
})?;
write_offset(offset, true, true, false, self.wtr)
}
/// %::z
fn fmt_offset_colon2(&mut self) -> Result<(), Error> {
let offset = self.tm.offset.ok_or(FE::RequiresOffset)?;
let offset = self.tm.offset.ok_or_else(|| {
err!("requires offset to format time zone offset")
})?;
write_offset(offset, true, true, true, self.wtr)
}
/// %:::z
fn fmt_offset_colon3(&mut self) -> Result<(), Error> {
let offset = self.tm.offset.ok_or(FE::RequiresOffset)?;
let offset = self.tm.offset.ok_or_else(|| {
err!("requires offset to format time zone offset")
})?;
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(FE::RequiresTime)?.get();
let second = self
.tm
.second
.ok_or_else(|| err!("requires time to format second"))?
.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(|_| FE::RequiresInstant)?;
let timestamp = self.tm.to_timestamp().map_err(|_| {
err!(
"requires instant (a date, time and offset) \
to format Unix timestamp",
)
})?;
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(FE::RequiresTime)?;
let subsec = self.tm.subsec.ok_or_else(|| {
err!("requires time to format subsecond nanoseconds")
})?;
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
@ -500,7 +553,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(Error::from(FE::ZeroPrecisionFloat));
return Err(err!("zero precision with %f is not allowed"));
}
if subsec == 0 && ext.width.is_none() {
self.wtr.write_str("0")?;
@ -524,9 +577,11 @@ 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(FE::RequiresTime)?;
let subsec = self.tm.subsec.ok_or_else(|| {
err!("requires time to format subsecond nanoseconds")
})?;
if ext.width == Some(0) {
return Err(Error::from(FE::ZeroPrecisionNano));
return Err(err!("zero precision with %N is not allowed"));
}
let subsec = i32::from(subsec).unsigned_abs();
// Since `%N` is actually an alias for `%9f`, when the precision
@ -541,8 +596,14 @@ 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(FE::RequiresTimeZone)?;
let ts = self.tm.to_timestamp().map_err(|_| FE::RequiresInstant)?;
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 oinfo = tz.to_offset_info(ts);
ext.write_str(Case::Upper, oinfo.abbreviation(), self.wtr)
}
@ -552,11 +613,8 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
let weekday = self
.tm
.weekday
.or_else(
#[inline(never)]
|| self.tm.to_date().ok().map(|d| d.weekday()),
)
.ok_or(FE::RequiresDate)?;
.or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
.ok_or_else(|| err!("requires date to format weekday"))?;
ext.write_str(Case::AsIs, weekday_name_full(weekday), self.wtr)
}
@ -565,11 +623,8 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
let weekday = self
.tm
.weekday
.or_else(
#[inline(never)]
|| self.tm.to_date().ok().map(|d| d.weekday()),
)
.ok_or(FE::RequiresDate)?;
.or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
.ok_or_else(|| err!("requires date to format weekday"))?;
ext.write_str(Case::AsIs, weekday_name_abbrev(weekday), self.wtr)
}
@ -578,11 +633,8 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
let weekday = self
.tm
.weekday
.or_else(
#[inline(never)]
|| self.tm.to_date().ok().map(|d| d.weekday()),
)
.ok_or(FE::RequiresDate)?;
.or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
.ok_or_else(|| err!("requires date to format weekday number"))?;
ext.write_int(b' ', None, weekday.to_monday_one_offset(), self.wtr)
}
@ -591,11 +643,8 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
let weekday = self
.tm
.weekday
.or_else(
#[inline(never)]
|| self.tm.to_date().ok().map(|d| d.weekday()),
)
.ok_or(FE::RequiresDate)?;
.or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
.ok_or_else(|| err!("requires date to format weekday number"))?;
ext.write_int(b' ', None, weekday.to_sunday_zero_offset(), self.wtr)
}
@ -609,19 +658,17 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
.tm
.day_of_year
.map(|day| day.get())
.or_else(
#[inline(never)]
|| self.tm.to_date().ok().map(|d| d.day_of_year()),
)
.ok_or(FE::RequiresDate)?;
.or_else(|| self.tm.to_date().ok().map(|d| d.day_of_year()))
.ok_or_else(|| {
err!("requires date to format Sunday-based week number")
})?;
let weekday = self
.tm
.weekday
.or_else(
#[inline(never)]
|| self.tm.to_date().ok().map(|d| d.weekday()),
)
.ok_or(FE::RequiresDate)?
.or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
.ok_or_else(|| {
err!("requires date to format Sunday-based week number")
})?
.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.
@ -637,16 +684,12 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
let weeknum = self
.tm
.iso_week
.or_else(
#[inline(never)]
|| {
self.tm
.to_date()
.ok()
.map(|d| d.iso_week_date().week_ranged())
},
)
.ok_or(FE::RequiresDate)?;
.or_else(|| {
self.tm.to_date().ok().map(|d| d.iso_week_date().week_ranged())
})
.ok_or_else(|| {
err!("requires date to format ISO 8601 week number")
})?;
ext.write_int(b'0', Some(2), weeknum, self.wtr)
}
@ -660,19 +703,17 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
.tm
.day_of_year
.map(|day| day.get())
.or_else(
#[inline(never)]
|| self.tm.to_date().ok().map(|d| d.day_of_year()),
)
.ok_or(FE::RequiresDate)?;
.or_else(|| self.tm.to_date().ok().map(|d| d.day_of_year()))
.ok_or_else(|| {
err!("requires date to format Monday-based week number")
})?;
let weekday = self
.tm
.weekday
.or_else(
#[inline(never)]
|| self.tm.to_date().ok().map(|d| d.weekday()),
)
.ok_or(FE::RequiresDate)?
.or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
.ok_or_else(|| {
err!("requires date to format Monday-based week number")
})?
.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.
@ -688,11 +729,8 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
let year = self
.tm
.year
.or_else(
#[inline(never)]
|| self.tm.to_date().ok().map(|d| d.year_ranged()),
)
.ok_or(FE::RequiresDate)?
.or_else(|| self.tm.to_date().ok().map(|d| d.year_ranged()))
.ok_or_else(|| err!("requires date to format year"))?
.get();
ext.write_int(b'0', Some(4), year, self.wtr)
}
@ -702,11 +740,8 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
let year = self
.tm
.year
.or_else(
#[inline(never)]
|| self.tm.to_date().ok().map(|d| d.year_ranged()),
)
.ok_or(FE::RequiresDate)?
.or_else(|| self.tm.to_date().ok().map(|d| d.year_ranged()))
.ok_or_else(|| err!("requires date to format year (2-digit)"))?
.get();
let year = year % 100;
ext.write_int(b'0', Some(2), year, self.wtr)
@ -717,11 +752,8 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
let year = self
.tm
.year
.or_else(
#[inline(never)]
|| self.tm.to_date().ok().map(|d| d.year_ranged()),
)
.ok_or(FE::RequiresDate)?
.or_else(|| self.tm.to_date().ok().map(|d| d.year_ranged()))
.ok_or_else(|| err!("requires date to format century (2-digit)"))?
.get();
let century = year / 100;
ext.write_int(b' ', None, century, self.wtr)
@ -732,16 +764,12 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
let year = self
.tm
.iso_week_year
.or_else(
#[inline(never)]
|| {
self.tm
.to_date()
.ok()
.map(|d| d.iso_week_date().year_ranged())
},
)
.ok_or(FE::RequiresDate)?
.or_else(|| {
self.tm.to_date().ok().map(|d| d.iso_week_date().year_ranged())
})
.ok_or_else(|| {
err!("requires date to format ISO 8601 week-based year")
})?
.get();
ext.write_int(b'0', Some(4), year, self.wtr)
}
@ -751,16 +779,15 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
let year = self
.tm
.iso_week_year
.or_else(
#[inline(never)]
|| {
self.tm
.to_date()
.ok()
.map(|d| d.iso_week_date().year_ranged())
},
)
.ok_or(FE::RequiresDate)?
.or_else(|| {
self.tm.to_date().ok().map(|d| d.iso_week_date().year_ranged())
})
.ok_or_else(|| {
err!(
"requires date to format \
ISO 8601 week-based year (2-digit)"
)
})?
.get();
let year = year % 100;
ext.write_int(b'0', Some(2), year, self.wtr)
@ -771,11 +798,8 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
let month = self
.tm
.month
.or_else(
#[inline(never)]
|| self.tm.to_date().ok().map(|d| d.month_ranged()),
)
.ok_or(FE::RequiresDate)?
.or_else(|| self.tm.to_date().ok().map(|d| d.month_ranged()))
.ok_or_else(|| err!("requires date to format quarter"))?
.get();
let quarter = match month {
1..=3 => 1,
@ -793,11 +817,8 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
.tm
.day_of_year
.map(|day| day.get())
.or_else(
#[inline(never)]
|| self.tm.to_date().ok().map(|d| d.day_of_year()),
)
.ok_or(FE::RequiresDate)?;
.or_else(|| self.tm.to_date().ok().map(|d| d.day_of_year()))
.ok_or_else(|| err!("requires date to format day of year"))?;
ext.write_int(b'0', Some(3), day, self.wtr)
}
@ -850,7 +871,7 @@ fn write_offset<W: Write>(
second: bool,
wtr: &mut W,
) -> Result<(), Error> {
static FMT_TWO: IntegerFormatter = IntegerFormatter::new().padding(2);
static FMT_TWO: DecimalFormatter = DecimalFormatter::new().padding(2);
let hours = offset.part_hours_ranged().abs().get();
let minutes = offset.part_minutes_ranged().abs().get();
@ -946,7 +967,7 @@ impl Extension {
self.width.or(pad_width)
};
let mut formatter = IntegerFormatter::new().padding_byte(pad_byte);
let mut formatter = DecimalFormatter::new().padding_byte(pad_byte);
if let Some(width) = pad_width {
formatter = formatter.padding(width);
}
@ -1487,7 +1508,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 timestamp or a date, time and offset)",
@"strftime formatting failed: %s failed: requires instant (a date, time and offset) to format Unix timestamp",
);
}
@ -1500,7 +1521,7 @@ mod tests {
);
insta::assert_snapshot!(
format(b"abc %F \xFFxyz", d).unwrap_err(),
@"strftime formatting failed: invalid format string, it must be valid UTF-8",
@r#"strftime formatting failed: found invalid UTF-8 byte "\xff" in format string (format strings must be valid UTF-8)"#,
);
}

View file

@ -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",
"strftime formatting failed: %Y failed: requires date to format year",
);
```
@ -275,7 +275,7 @@ The following things are currently unsupported:
use crate::{
civil::{Date, DateTime, ISOWeekDate, Time, Weekday},
error::{fmt::strtime::Error as E, ErrorContext},
error::{err, 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 time zone offset",
/// requires offset to format time zone offset",
/// );
///
/// // Now enable lenient mode:
@ -946,9 +946,13 @@ 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(E::FailedStrptime)?;
p.parse().context("strptime parsing failed")?;
if !p.inp.is_empty() {
return Err(Error::from(E::unconsumed(p.inp)));
return Err(err!(
"strptime expects to consume the entire input, but \
{remaining:?} remains unparsed",
remaining = escape::Bytes(p.inp),
));
}
Ok(pieces)
}
@ -1051,7 +1055,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(E::FailedStrptime)?;
p.parse().context("strptime parsing failed")?;
let remainder = mkoffset(p.inp);
Ok((pieces, remainder))
}
@ -1154,7 +1158,7 @@ impl BrokenDownTime {
) -> Result<(), Error> {
let fmt = format.as_ref();
let mut formatter = Formatter { config, fmt, tm: self, wtr };
formatter.format().context(E::FailedStrftime)?;
formatter.format().context("strftime formatting failed")?;
Ok(())
}
@ -1333,11 +1337,10 @@ impl BrokenDownTime {
/// )?.to_zoned();
/// assert_eq!(
/// result.unwrap_err().to_string(),
/// "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`",
/// "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",
/// );
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
@ -1432,18 +1435,26 @@ impl BrokenDownTime {
if let Some(ts) = self.timestamp {
return Ok(ts.to_zoned(TimeZone::unknown()));
}
Err(Error::from(E::ZonedOffsetOrTz))
Err(err!(
"either offset (from %z) or IANA time zone identifier \
(from %Q) is required for parsing zoned datetime",
))
}
(Some(offset), None) => {
let ts = match self.timestamp {
Some(ts) => ts,
None => {
let dt = self
.to_datetime()
.context(E::RequiredDateTimeForZoned)?;
let ts = offset
.to_timestamp(dt)
.context(E::RangeTimestamp)?;
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",
)
})?;
ts
}
};
@ -1454,9 +1465,9 @@ impl BrokenDownTime {
match self.timestamp {
Some(ts) => Ok(ts.to_zoned(tz)),
None => {
let dt = self
.to_datetime()
.context(E::RequiredDateTimeForZoned)?;
let dt = self.to_datetime().context(
"datetime required to parse zoned datetime",
)?;
Ok(tz.to_zoned(dt)?)
}
}
@ -1467,17 +1478,19 @@ impl BrokenDownTime {
Some(ts) => {
let zdt = ts.to_zoned(tz);
if zdt.offset() != offset {
return Err(Error::from(E::MismatchOffset {
parsed: offset,
got: zdt.offset(),
}));
return Err(err!(
"parsed time zone offset `{offset}`, but \
offset from timestamp `{ts}` for time zone \
`{iana}` is `{got}`",
got = zdt.offset(),
));
}
Ok(zdt)
}
None => {
let dt = self
.to_datetime()
.context(E::RequiredDateTimeForZoned)?;
let dt = self.to_datetime().context(
"datetime required to parse zoned datetime",
)?;
let azdt =
OffsetConflict::Reject.resolve(dt, offset, tz)?;
// Guaranteed that if OffsetConflict::Reject doesn't
@ -1571,20 +1584,31 @@ impl BrokenDownTime {
/// ```
#[inline]
pub fn to_timestamp(&self) -> Result<Timestamp, Error> {
// Previously, I had used this as the "fast path" and
// put the conversion code below into a cold unlineable
// function. But this "fast path" is actually the unusual
// case. It's rare to parse a timestamp (as an integer
// number of seconds since the Unix epoch) directly.
// So the code below, while bigger, is the common case.
// So it probably makes sense to keep it inlined.
if let Some(timestamp) = self.timestamp() {
return Ok(timestamp);
}
let dt =
self.to_datetime().context(E::RequiredDateTimeForTimestamp)?;
let offset = self.offset.ok_or(E::RequiredOffsetForTimestamp)?;
offset.to_timestamp(dt).context(E::RangeTimestamp)
let dt = self
.to_datetime()
.context("datetime required to parse timestamp")?;
let offset =
self.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",
)
})
}
#[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.
@ -1614,8 +1638,10 @@ impl BrokenDownTime {
/// ```
#[inline]
pub fn to_datetime(&self) -> Result<DateTime, Error> {
let date = self.to_date().context(E::RequiredDateForDateTime)?;
let time = self.to_time().context(E::RequiredTimeForDateTime)?;
let date =
self.to_date().context("date required to parse datetime")?;
let time =
self.to_time().context("time required to parse datetime")?;
Ok(DateTime::from_parts(date, time))
}
@ -1634,12 +1660,8 @@ impl BrokenDownTime {
/// # Errors
///
/// This returns an error if there weren't enough components to construct
/// a civil date, or if the components don't form into a valid date. This
/// means there must be at least a year and a way to determine the day of
/// the year.
///
/// This will also return an error when there is a weekday component
/// set to a value inconsistent with the date returned.
/// a civil date. This means there must be at least a year and a way to
/// determine the day of the year.
///
/// It's okay if there are more units than are needed to construct a civil
/// datetime. For example, if this broken down time contains a civil time,
@ -1659,62 +1681,42 @@ impl BrokenDownTime {
/// ```
#[inline]
pub fn to_date(&self) -> Result<Date, Error> {
#[cold]
#[inline(never)]
fn to_date(tm: &BrokenDownTime) -> Result<Date, Error> {
let Some(year) = tm.year else {
// The Gregorian year and ISO week year may be parsed
// separately. That is, they are two different fields. So if
// the Gregorian year is absent, we might still have an ISO
// 8601 week date.
if let Some(date) = tm.to_date_from_iso()? {
return Ok(date);
}
return Err(Error::from(E::RequiredYearForDate));
};
let mut date = tm.to_date_from_gregorian(year)?;
if date.is_none() {
date = tm.to_date_from_iso()?;
let Some(year) = self.year else {
// The Gregorian year and ISO week year may be parsed separately.
// That is, they are two different fields. So if the Gregorian year
// is absent, we might still have an ISO 8601 week date.
if let Some(date) = self.to_date_from_iso()? {
return Ok(date);
}
if date.is_none() {
date = tm.to_date_from_day_of_year(year)?;
}
if date.is_none() {
date = tm.to_date_from_week_sun(year)?;
}
if date.is_none() {
date = tm.to_date_from_week_mon(year)?;
}
let Some(date) = date else {
return Err(Error::from(E::RequiredSomeDayForDate));
};
if let Some(weekday) = tm.weekday {
if weekday != date.weekday() {
return Err(Error::from(E::MismatchWeekday {
parsed: weekday,
got: date.weekday(),
}));
}
}
Ok(date)
}
// 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)
else {
return to_date(self);
return Err(err!("missing year, date cannot be created"));
};
let mut date = self.to_date_from_gregorian(year)?;
if date.is_none() {
date = self.to_date_from_iso()?;
}
if date.is_none() {
date = self.to_date_from_day_of_year(year)?;
}
if date.is_none() {
date = self.to_date_from_week_sun(year)?;
}
if date.is_none() {
date = self.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",
));
};
let date =
Date::new_ranged(year, month, day).context(E::InvalidDate)?;
if let Some(weekday) = self.weekday {
if weekday != date.weekday() {
return Err(Error::from(E::MismatchWeekday {
parsed: weekday,
got: 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()),
));
}
}
Ok(date)
@ -1728,7 +1730,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(E::InvalidDate)?))
Ok(Some(Date::new_ranged(year, month, day).context("invalid date")?))
}
#[inline]
@ -1744,7 +1746,7 @@ impl BrokenDownTime {
.with()
.day_of_year(doy.get())
.build()
.context(E::InvalidDate)?
.context("invalid date")?
}))
}
@ -1755,8 +1757,8 @@ impl BrokenDownTime {
else {
return Ok(None);
};
let wd =
ISOWeekDate::new_ranged(y, w, d).context(E::InvalidISOWeekDate)?;
let wd = ISOWeekDate::new_ranged(y, w, d)
.context("invalid ISO 8601 week date")?;
Ok(Some(wd.date()))
}
@ -1771,20 +1773,28 @@ 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(E::InvalidDate)?;
.context("invalid date")?;
let first_sunday = first_of_year
.nth_weekday_of_month(1, Weekday::Sunday)
.map(|d| d.day_of_year())
.context(E::InvalidDate)?;
.context("invalid date")?;
let doy = if week == 0 {
let days_before_first_sunday = 7 - wday;
let doy = first_sunday
.checked_sub(days_before_first_sunday)
.ok_or(E::InvalidWeekdaySunday { got: weekday })?;
.ok_or_else(|| {
err!(
"weekday `{weekday:?}` is not valid for \
Sunday based week number `{week}` \
in year `{year}`",
)
})?;
if doy == 0 {
return Err(Error::from(E::InvalidWeekdaySunday {
got: weekday,
}));
return Err(err!(
"weekday `{weekday:?}` is not valid for \
Sunday based week number `{week}` \
in year `{year}`",
));
}
doy
} else {
@ -1796,7 +1806,7 @@ impl BrokenDownTime {
.with()
.day_of_year(doy)
.build()
.context(E::InvalidDate)?;
.context("invalid date")?;
Ok(Some(date))
}
@ -1811,20 +1821,28 @@ 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(E::InvalidDate)?;
.context("invalid date")?;
let first_monday = first_of_year
.nth_weekday_of_month(1, Weekday::Monday)
.map(|d| d.day_of_year())
.context(E::InvalidDate)?;
.context("invalid date")?;
let doy = if week == 0 {
let days_before_first_monday = 7 - wday;
let doy = first_monday
.checked_sub(days_before_first_monday)
.ok_or(E::InvalidWeekdayMonday { got: weekday })?;
.ok_or_else(|| {
err!(
"weekday `{weekday:?}` is not valid for \
Monday based week number `{week}` \
in year `{year}`",
)
})?;
if doy == 0 {
return Err(Error::from(E::InvalidWeekdayMonday {
got: weekday,
}));
return Err(err!(
"weekday `{weekday:?}` is not valid for \
Monday based week number `{week}` \
in year `{year}`",
));
}
doy
} else {
@ -1836,7 +1854,7 @@ impl BrokenDownTime {
.with()
.day_of_year(doy)
.build()
.context(E::InvalidDate)?;
.context("invalid date")?;
Ok(Some(date))
}
@ -1918,28 +1936,52 @@ impl BrokenDownTime {
pub fn to_time(&self) -> Result<Time, Error> {
let Some(hour) = self.hour_ranged() else {
if self.minute.is_some() {
return Err(Error::from(E::MissingTimeHourForMinute));
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)",
));
}
if self.second.is_some() {
return Err(Error::from(E::MissingTimeHourForSecond));
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)",
));
}
if self.subsec.is_some() {
return Err(Error::from(E::MissingTimeHourForFractional));
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 Ok(Time::midnight());
};
let Some(minute) = self.minute else {
if self.second.is_some() {
return Err(Error::from(E::MissingTimeMinuteForSecond));
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)",
));
}
if self.subsec.is_some() {
return Err(Error::from(E::MissingTimeMinuteForFractional));
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 Ok(Time::new_ranged(hour, C(0), C(0), C(0)));
};
let Some(second) = self.second else {
if self.subsec.is_some() {
return Err(Error::from(E::MissingTimeSecondForFractional));
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 Ok(Time::new_ranged(hour, minute, C(0), C(0)));
};
@ -2079,8 +2121,8 @@ impl BrokenDownTime {
/// // An error only occurs when you try to extract a date:
/// assert_eq!(
/// tm.to_date().unwrap_err().to_string(),
/// "invalid date: number of days for `2023` is invalid, \
/// must be in range `1..=365`",
/// "invalid date: day-of-year=366 is out of range \
/// for year=2023, must be in range 1..=365",
/// );
/// // But parsing a value that is always illegal will
/// // result in an error:
@ -3437,7 +3479,7 @@ impl Extension {
fn parse_flag<'i>(
fmt: &'i [u8],
) -> Result<(Option<Flag>, &'i [u8]), Error> {
let (&byte, tail) = fmt.split_first().unwrap();
let byte = fmt[0];
let flag = match byte {
b'_' => Flag::PadSpace,
b'0' => Flag::PadZero,
@ -3446,12 +3488,15 @@ impl Extension {
b'#' => Flag::Swapcase,
_ => return Ok((None, fmt)),
};
if tail.is_empty() {
return Err(Error::from(E::ExpectedDirectiveAfterFlag {
flag: byte,
}));
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),
));
}
Ok((Some(flag), tail))
Ok((Some(flag), fmt))
}
/// Parses an optional width that comes after a (possibly absent) flag and
@ -3475,10 +3520,16 @@ impl Extension {
return Ok((None, fmt));
}
let (digits, fmt) = util::parse::split(fmt, digits).unwrap();
let width = util::parse::i64(digits).context(E::FailedWidth)?;
let width = u8::try_from(width).map_err(|_| E::RangeWidth)?;
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)
})?;
if fmt.is_empty() {
return Err(Error::from(E::ExpectedDirectiveAfterWidth));
return Err(err!(
"expected to find specifier directive after width \
{width}, but found end of format string",
));
}
Ok((Some(width), fmt))
}
@ -3498,7 +3549,10 @@ impl Extension {
}
let fmt = &fmt[usize::from(colons)..];
if colons > 0 && fmt.is_empty() {
return Err(Error::from(E::ExpectedDirectiveAfterColons));
return Err(err!(
"expected to find specifier directive after {colons} colons, \
but found end of format string",
));
}
Ok((u8::try_from(colons).unwrap(), fmt))
}

File diff suppressed because it is too large Load diff

View file

@ -171,7 +171,7 @@ There is some more [background on Temporal's format] available.
*/
use crate::{
civil::{self, ISOWeekDate},
civil,
error::Error,
fmt::Write,
span::Span,
@ -320,12 +320,13 @@ impl DateTimeParser {
/// );
/// assert_eq!(
/// result.unwrap_err().to_string(),
/// "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",
/// "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",
/// );
/// ```
///
@ -409,10 +410,11 @@ impl DateTimeParser {
/// );
/// assert_eq!(
/// result.unwrap_err().to_string(),
/// "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`",
/// "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",
/// );
/// ```
///
@ -1025,8 +1027,9 @@ impl DateTimeParser {
/// // Normally this operation will fail.
/// assert_eq!(
/// PARSER.parse_zoned(timestamp).unwrap_err().to_string(),
/// "failed to find time zone annotation in square brackets, \
/// which is required for parsing a zoned datetime",
/// "failed to find time zone in square brackets in \
/// \"2025-01-02T15:13-05\", which is required for \
/// parsing a zoned instant",
/// );
///
/// // But you can work-around this with `Pieces`, which gives you direct
@ -1070,8 +1073,8 @@ impl DateTimeParser {
///
/// assert_eq!(
/// PARSER.parse_date("2024-03-10T00:00:00Z").unwrap_err().to_string(),
/// "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",
/// "cannot parse civil date from string with a Zulu offset, \
/// parse as a `Timestamp` and convert to a civil date instead",
/// );
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
@ -1107,22 +1110,6 @@ impl DateTimeParser {
let pieces = parsed.to_pieces()?;
Ok(pieces)
}
/// Parses an ISO 8601 week date.
///
/// This isn't exported because it's not clear that it's worth it.
/// Moreover, this isn't part of the Temporal spec, so it's a little odd
/// to have it here. If this really needs to be exported, we probably need
/// a new module that wraps and re-uses this module's internal parser to
/// avoid too much code duplication.
pub(crate) fn parse_iso_week_date<I: AsRef<[u8]>>(
&self,
input: I,
) -> Result<ISOWeekDate, Error> {
let input = input.as_ref();
let wd = self.p.parse_iso_week_date(input)?.into_full()?;
Ok(wd)
}
}
/// A printer for Temporal datetimes.
@ -1977,25 +1964,6 @@ impl DateTimePrinter {
) -> Result<(), Error> {
self.p.print_pieces(pieces, wtr)
}
/// Prints an ISO 8601 week date.
///
/// This isn't exported because it's not clear that it's worth it.
/// Moreover, this isn't part of the Temporal spec, so it's a little odd
/// to have it here. But it's very convenient to have the ISO 8601 week
/// date parser in this module, and so we stick the printer here along
/// with it.
///
/// Note that this printer will use `w` when `lowercase` is enabled. (It
/// isn't possible to enable this using the current Jiff public API. But
/// it's probably fine.)
pub(crate) fn print_iso_week_date<W: Write>(
&self,
iso_week_date: &ISOWeekDate,
wtr: W,
) -> Result<(), Error> {
self.p.print_iso_week_date(iso_week_date, wtr)
}
}
/// A parser for Temporal durations.
@ -2487,7 +2455,7 @@ mod tests {
);
insta::assert_snapshot!(
DateTimeParser::new().parse_date("-000000-01-01").unwrap_err(),
@"failed to parse year in date: year zero must be written without a sign or a positive sign, but not a negative sign",
@r###"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"###,
);
}

File diff suppressed because it is too large Load diff

View file

@ -146,8 +146,9 @@ use crate::{
///
/// assert_eq!(
/// "2025-01-03T17:28-05".parse::<Zoned>().unwrap_err().to_string(),
/// "failed to find time zone annotation in square brackets, \
/// which is required for parsing a zoned datetime",
/// "failed to find time zone in square brackets in \
/// \"2025-01-03T17:28-05\", which is required for \
/// parsing a zoned instant",
/// );
/// ```
///

View file

@ -1,9 +1,9 @@
use crate::{
civil::{Date, DateTime, ISOWeekDate, Time},
error::{fmt::temporal::Error as E, Error},
civil::{Date, DateTime, Time},
error::{err, Error},
fmt::{
temporal::{Pieces, PiecesOffset, TimeZoneAnnotationKind},
util::{FractionalFormatter, IntegerFormatter},
util::{DecimalFormatter, FractionalFormatter},
Write, WriteExt,
},
span::Span,
@ -108,11 +108,11 @@ impl DateTimePrinter {
date: &Date,
mut wtr: W,
) -> Result<(), Error> {
static FMT_YEAR_POSITIVE: IntegerFormatter =
IntegerFormatter::new().padding(4);
static FMT_YEAR_NEGATIVE: IntegerFormatter =
IntegerFormatter::new().padding(6);
static FMT_TWO: IntegerFormatter = IntegerFormatter::new().padding(2);
static FMT_YEAR_POSITIVE: DecimalFormatter =
DecimalFormatter::new().padding(4);
static FMT_YEAR_NEGATIVE: DecimalFormatter =
DecimalFormatter::new().padding(6);
static FMT_TWO: DecimalFormatter = DecimalFormatter::new().padding(2);
if date.year() >= 0 {
wtr.write_int(&FMT_YEAR_POSITIVE, date.year())?;
@ -132,7 +132,7 @@ impl DateTimePrinter {
time: &Time,
mut wtr: W,
) -> Result<(), Error> {
static FMT_TWO: IntegerFormatter = IntegerFormatter::new().padding(2);
static FMT_TWO: DecimalFormatter = DecimalFormatter::new().padding(2);
static FMT_FRACTION: FractionalFormatter = FractionalFormatter::new();
wtr.write_int(&FMT_TWO, time.hour())?;
@ -197,7 +197,13 @@ impl DateTimePrinter {
//
// Anyway, if you're seeing this error and think there should be a
// different behavior, please file an issue.
Err(Error::from(E::PrintTimeZoneFailure))
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`)",
))
}
pub(super) fn print_pieces<W: Write>(
@ -249,34 +255,6 @@ impl DateTimePrinter {
Ok(())
}
pub(super) fn print_iso_week_date<W: Write>(
&self,
iso_week_date: &ISOWeekDate,
mut wtr: W,
) -> Result<(), Error> {
static FMT_YEAR_POSITIVE: IntegerFormatter =
IntegerFormatter::new().padding(4);
static FMT_YEAR_NEGATIVE: IntegerFormatter =
IntegerFormatter::new().padding(6);
static FMT_TWO: IntegerFormatter = IntegerFormatter::new().padding(2);
static FMT_ONE: IntegerFormatter = IntegerFormatter::new().padding(1);
if iso_week_date.year() >= 0 {
wtr.write_int(&FMT_YEAR_POSITIVE, iso_week_date.year())?;
} else {
wtr.write_int(&FMT_YEAR_NEGATIVE, iso_week_date.year())?;
}
wtr.write_str("-")?;
wtr.write_char(if self.lowercase { 'w' } else { 'W' })?;
wtr.write_int(&FMT_TWO, iso_week_date.week())?;
wtr.write_str("-")?;
wtr.write_int(
&FMT_ONE,
iso_week_date.weekday().to_monday_one_offset(),
)?;
Ok(())
}
/// Formats the given "pieces" offset into the writer given.
fn print_pieces_offset<W: Write>(
&self,
@ -304,7 +282,7 @@ impl DateTimePrinter {
offset: &Offset,
mut wtr: W,
) -> Result<(), Error> {
static FMT_TWO: IntegerFormatter = IntegerFormatter::new().padding(2);
static FMT_TWO: DecimalFormatter = DecimalFormatter::new().padding(2);
wtr.write_str(if offset.is_negative() { "-" } else { "+" })?;
let mut hours = offset.part_hours_ranged().abs().get();
@ -339,7 +317,7 @@ impl DateTimePrinter {
offset: &Offset,
mut wtr: W,
) -> Result<(), Error> {
static FMT_TWO: IntegerFormatter = IntegerFormatter::new().padding(2);
static FMT_TWO: DecimalFormatter = DecimalFormatter::new().padding(2);
wtr.write_str(if offset.is_negative() { "-" } else { "+" })?;
let hours = offset.part_hours_ranged().abs().get();
@ -427,7 +405,7 @@ impl SpanPrinter {
span: &Span,
mut wtr: W,
) -> Result<(), Error> {
static FMT_INT: IntegerFormatter = IntegerFormatter::new();
static FMT_INT: DecimalFormatter = DecimalFormatter::new();
static FMT_FRACTION: FractionalFormatter = FractionalFormatter::new();
if span.is_negative() {
@ -541,7 +519,7 @@ impl SpanPrinter {
dur: &SignedDuration,
mut wtr: W,
) -> Result<(), Error> {
static FMT_INT: IntegerFormatter = IntegerFormatter::new();
static FMT_INT: DecimalFormatter = DecimalFormatter::new();
static FMT_FRACTION: FractionalFormatter = FractionalFormatter::new();
let mut non_zero_greater_than_second = false;
@ -590,7 +568,7 @@ impl SpanPrinter {
dur: &core::time::Duration,
mut wtr: W,
) -> Result<(), Error> {
static FMT_INT: IntegerFormatter = IntegerFormatter::new();
static FMT_INT: DecimalFormatter = DecimalFormatter::new();
static FMT_FRACTION: FractionalFormatter = FractionalFormatter::new();
let mut non_zero_greater_than_second = false;
@ -642,10 +620,7 @@ impl SpanPrinter {
mod tests {
use alloc::string::String;
use crate::{
civil::{date, Weekday},
span::ToSpan,
};
use crate::{civil::date, span::ToSpan};
use super::*;
@ -947,22 +922,4 @@ mod tests {
@"PT5124095576030431H15.999999999S",
);
}
#[test]
fn print_iso_week_date() {
let p = |d: ISOWeekDate| -> String {
let mut buf = String::new();
DateTimePrinter::new().print_iso_week_date(&d, &mut buf).unwrap();
buf
};
insta::assert_snapshot!(
p(ISOWeekDate::new(2024, 52, Weekday::Monday).unwrap()),
@"2024-W52-1",
);
insta::assert_snapshot!(
p(ISOWeekDate::new(2004, 1, Weekday::Sunday).unwrap()),
@"2004-W01-7",
);
}
}

View file

@ -1,7 +1,7 @@
use crate::{
error::{fmt::util::Error as E, ErrorContext},
error::{err, ErrorContext},
fmt::Parsed,
util::{c::Sign, parse, t},
util::{c::Sign, escape, parse, t},
Error, SignedDuration, Span, Unit,
};
@ -14,31 +14,52 @@ use crate::{
/// faster. We roll our own which is a bit slower, but gets us enough of a win
/// to be satisfied with and with (almost) pure safe code.
///
/// This only includes the sign when formatting a negative signed integer.
/// By default, this only includes the sign if it's negative. To always include
/// the sign, set `force_sign` to `true`.
#[derive(Clone, Copy, Debug)]
pub(crate) struct IntegerFormatter {
pub(crate) struct DecimalFormatter {
force_sign: Option<bool>,
minimum_digits: u8,
padding_byte: u8,
}
impl IntegerFormatter {
/// Creates a new integer formatter using the default configuration.
pub(crate) const fn new() -> IntegerFormatter {
IntegerFormatter { minimum_digits: 0, padding_byte: b'0' }
impl DecimalFormatter {
/// Creates a new decimal formatter using the default configuration.
pub(crate) const fn new() -> DecimalFormatter {
DecimalFormatter {
force_sign: None,
minimum_digits: 0,
padding_byte: b'0',
}
}
/// Format the given value using this configuration as a signed integer
/// Format the given value using this configuration as a signed decimal
/// ASCII number.
#[cfg_attr(feature = "perf-inline", inline(always))]
pub(crate) const fn format_signed(&self, value: i64) -> Integer {
Integer::signed(self, value)
pub(crate) const fn format_signed(&self, value: i64) -> Decimal {
Decimal::signed(self, value)
}
/// Format the given value using this configuration as an unsigned integer
/// Format the given value using this configuration as an unsigned decimal
/// ASCII number.
#[cfg_attr(feature = "perf-inline", inline(always))]
pub(crate) const fn format_unsigned(&self, value: u64) -> Integer {
Integer::unsigned(self, value)
pub(crate) const fn format_unsigned(&self, value: u64) -> Decimal {
Decimal::unsigned(self, value)
}
/// Forces the sign to be rendered, even if it's positive.
///
/// When `zero_is_positive` is true, then a zero value is formatted with a
/// positive sign. Otherwise, it is formatted with a negative sign.
///
/// Regardless of this setting, a sign is never emitted when formatting an
/// unsigned integer.
#[cfg(test)]
pub(crate) const fn force_sign(
self,
zero_is_positive: bool,
) -> DecimalFormatter {
DecimalFormatter { force_sign: Some(zero_is_positive), ..self }
}
/// The minimum number of digits/padding that this number should be
@ -48,113 +69,181 @@ impl IntegerFormatter {
///
/// The minimum number of digits is capped at the maximum number of digits
/// for an i64 value (19) or a u64 value (20).
pub(crate) const fn padding(self, mut digits: u8) -> IntegerFormatter {
if digits > Integer::MAX_LEN {
digits = Integer::MAX_LEN;
pub(crate) const fn padding(self, mut digits: u8) -> DecimalFormatter {
if digits > Decimal::MAX_I64_DIGITS {
digits = Decimal::MAX_I64_DIGITS;
}
IntegerFormatter { minimum_digits: digits, ..self }
DecimalFormatter { minimum_digits: digits, ..self }
}
/// The padding byte to use when `padding` is set.
///
/// The default is `0`.
pub(crate) const fn padding_byte(self, byte: u8) -> IntegerFormatter {
IntegerFormatter { padding_byte: byte, ..self }
pub(crate) const fn padding_byte(self, byte: u8) -> DecimalFormatter {
DecimalFormatter { padding_byte: byte, ..self }
}
/// Returns the minimum number of digits for an integer value.
const fn get_minimum_digits(&self) -> u8 {
if self.minimum_digits <= Integer::MAX_LEN {
/// Returns the minimum number of digits for a signed value.
const fn get_signed_minimum_digits(&self) -> u8 {
if self.minimum_digits <= Decimal::MAX_I64_DIGITS {
self.minimum_digits
} else {
Integer::MAX_LEN
Decimal::MAX_I64_DIGITS
}
}
/// Returns the minimum number of digits for an unsigned value.
const fn get_unsigned_minimum_digits(&self) -> u8 {
if self.minimum_digits <= Decimal::MAX_U64_DIGITS {
self.minimum_digits
} else {
Decimal::MAX_U64_DIGITS
}
}
}
impl Default for IntegerFormatter {
fn default() -> IntegerFormatter {
IntegerFormatter::new()
impl Default for DecimalFormatter {
fn default() -> DecimalFormatter {
DecimalFormatter::new()
}
}
/// A formatted integer number that can be converted to a sequence of bytes.
/// A formatted decimal number that can be converted to a sequence of bytes.
#[derive(Debug)]
pub(crate) struct Integer {
pub(crate) struct Decimal {
buf: [u8; Self::MAX_LEN as usize],
start: u8,
end: u8,
}
impl Integer {
impl Decimal {
/// Discovered via
/// `i64::MIN.to_string().len().max(u64::MAX.to_string().len())`.
const MAX_LEN: u8 = 20;
/// Discovered via `i64::MAX.to_string().len()`.
const MAX_I64_DIGITS: u8 = 19;
/// Discovered via `u64::MAX.to_string().len()`.
const MAX_U64_DIGITS: u8 = 20;
/// Using the given formatter, turn the value given into an unsigned
/// integer representation using ASCII bytes.
/// decimal representation using ASCII bytes.
#[cfg_attr(feature = "perf-inline", inline(always))]
const fn unsigned(
formatter: &IntegerFormatter,
formatter: &DecimalFormatter,
mut value: u64,
) -> Integer {
let mut integer =
Integer { buf: [0; Self::MAX_LEN as usize], start: Self::MAX_LEN };
) -> Decimal {
let mut decimal = Decimal {
buf: [0; Self::MAX_LEN as usize],
start: Self::MAX_LEN,
end: Self::MAX_LEN,
};
loop {
integer.start -= 1;
decimal.start -= 1;
let digit = (value % 10) as u8;
value /= 10;
integer.buf[integer.start as usize] = b'0' + digit;
decimal.buf[decimal.start as usize] = b'0' + digit;
if value == 0 {
break;
}
}
while integer.len() < formatter.get_minimum_digits() {
integer.start -= 1;
integer.buf[integer.start as usize] = formatter.padding_byte;
while decimal.len() < formatter.get_unsigned_minimum_digits() {
decimal.start -= 1;
decimal.buf[decimal.start as usize] = formatter.padding_byte;
}
integer
decimal
}
/// Using the given formatter, turn the value given into a signed integer
/// Using the given formatter, turn the value given into a signed decimal
/// representation using ASCII bytes.
#[cfg_attr(feature = "perf-inline", inline(always))]
const fn signed(formatter: &IntegerFormatter, value: i64) -> Integer {
const fn signed(formatter: &DecimalFormatter, mut value: i64) -> Decimal {
// Specialize the common case to generate tighter codegen.
if value >= 0 {
return Integer::unsigned(formatter, value.unsigned_abs());
if value >= 0 && formatter.force_sign.is_none() {
let mut decimal = Decimal {
buf: [0; Self::MAX_LEN as usize],
start: Self::MAX_LEN,
end: Self::MAX_LEN,
};
loop {
decimal.start -= 1;
let digit = (value % 10) as u8;
value /= 10;
decimal.buf[decimal.start as usize] = b'0' + digit;
if value == 0 {
break;
}
}
while decimal.len() < formatter.get_signed_minimum_digits() {
decimal.start -= 1;
decimal.buf[decimal.start as usize] = formatter.padding_byte;
}
return decimal;
}
Integer::signed_cold(formatter, value)
Decimal::signed_cold(formatter, value)
}
#[cold]
#[inline(never)]
const fn signed_cold(formatter: &IntegerFormatter, value: i64) -> Integer {
let mut integer = Integer::unsigned(formatter, value.unsigned_abs());
if value < 0 {
integer.start -= 1;
integer.buf[integer.start as usize] = b'-';
const fn signed_cold(formatter: &DecimalFormatter, value: i64) -> Decimal {
let sign = value.signum();
let Some(mut value) = value.checked_abs() else {
let buf = [
b'-', b'9', b'2', b'2', b'3', b'3', b'7', b'2', b'0', b'3',
b'6', b'8', b'5', b'4', b'7', b'7', b'5', b'8', b'0', b'8',
];
return Decimal { buf, start: 0, end: Self::MAX_LEN };
};
let mut decimal = Decimal {
buf: [0; Self::MAX_LEN as usize],
start: Self::MAX_LEN,
end: Self::MAX_LEN,
};
loop {
decimal.start -= 1;
let digit = (value % 10) as u8;
value /= 10;
decimal.buf[decimal.start as usize] = b'0' + digit;
if value == 0 {
break;
}
}
integer
while decimal.len() < formatter.get_signed_minimum_digits() {
decimal.start -= 1;
decimal.buf[decimal.start as usize] = formatter.padding_byte;
}
if sign < 0 {
decimal.start -= 1;
decimal.buf[decimal.start as usize] = b'-';
} else if let Some(zero_is_positive) = formatter.force_sign {
let ascii_sign =
if sign > 0 || zero_is_positive { b'+' } else { b'-' };
decimal.start -= 1;
decimal.buf[decimal.start as usize] = ascii_sign;
}
decimal
}
/// Returns the total number of ASCII bytes (including the sign) that are
/// used to represent this integer number.
/// used to represent this decimal number.
#[inline]
const fn len(&self) -> u8 {
Self::MAX_LEN - self.start
self.end - self.start
}
/// Returns the ASCII representation of this integer as a byte slice.
/// Returns the ASCII representation of this decimal as a byte slice.
///
/// The slice returned is guaranteed to be valid ASCII.
#[inline]
fn as_bytes(&self) -> &[u8] {
&self.buf[usize::from(self.start)..]
&self.buf[usize::from(self.start)..usize::from(self.end)]
}
/// Returns the ASCII representation of this integer as a string slice.
/// Returns the ASCII representation of this decimal as a string slice.
#[inline]
pub(crate) fn as_str(&self) -> &str {
// SAFETY: This is safe because all bytes written to `self.buf` are
@ -373,10 +462,14 @@ impl DurationUnits {
if let Some(min) = self.min {
if min <= unit {
return Err(Error::from(E::OutOfOrderUnits {
found: unit,
previous: min,
}));
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(),
));
}
}
// Given the above check, the given unit must be smaller than any we
@ -410,7 +503,12 @@ impl DurationUnits {
) -> Result<(), Error> {
if let Some(min) = self.min {
if min <= Unit::Hour {
return Err(Error::from(E::OutOfOrderHMS { found: min }));
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(),
));
}
}
self.set_unit_value(Unit::Hour, hours)?;
@ -441,11 +539,15 @@ 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 || min == Unit::Nanosecond {
return Err(Error::from(E::NotAllowedFractionalUnit {
found: min,
}));
if min > Unit::Hour {
return Err(err!(
"fractional {plural} are not supported",
plural = min.plural()
));
}
}
self.fraction = Some(fraction);
@ -540,6 +642,13 @@ 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,
@ -573,7 +682,7 @@ impl DurationUnits {
set(span)
.or_else(|err| fractional_fallback(err, unit, value, span))
.context(E::FailedValueSet { unit })
.with_context(|| error_context(unit, value))
}
let (min, _) = self.get_min_max_units()?;
@ -583,25 +692,25 @@ impl DurationUnits {
let value = self.get_unit_value(Unit::Year)?;
span = span
.try_years(value)
.context(E::FailedValueSet { unit: Unit::Year })?;
.with_context(|| error_context(Unit::Year, value))?;
}
if self.values[Unit::Month.as_usize()] != 0 {
let value = self.get_unit_value(Unit::Month)?;
span = span
.try_months(value)
.context(E::FailedValueSet { unit: Unit::Month })?;
.with_context(|| error_context(Unit::Month, value))?;
}
if self.values[Unit::Week.as_usize()] != 0 {
let value = self.get_unit_value(Unit::Week)?;
span = span
.try_weeks(value)
.context(E::FailedValueSet { unit: Unit::Week })?;
.with_context(|| error_context(Unit::Week, value))?;
}
if self.values[Unit::Day.as_usize()] != 0 {
let value = self.get_unit_value(Unit::Day)?;
span = span
.try_days(value)
.context(E::FailedValueSet { unit: Unit::Day })?;
.with_context(|| error_context(Unit::Day, value))?;
}
if self.values[Unit::Hour.as_usize()] != 0 {
let value = self.get_unit_value(Unit::Hour)?;
@ -713,7 +822,11 @@ 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(Error::from(E::NotAllowedCalendarUnit { unit: max }));
return Err(err!(
"parsing {unit} units into a `SignedDuration` is not supported \
(perhaps try parsing into a `Span` instead)",
unit = max.singular(),
));
}
let mut sdur = SignedDuration::ZERO;
@ -721,43 +834,85 @@ 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(E::OverflowForUnit { unit: Unit::Hour })?;
.ok_or_else(|| {
err!(
"accumulated `SignedDuration` of `{sdur:?}` \
overflowed when adding {value} of unit {unit}",
unit = Unit::Hour.singular(),
)
})?;
}
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(E::OverflowForUnit { unit: Unit::Minute })?;
.ok_or_else(|| {
err!(
"accumulated `SignedDuration` of `{sdur:?}` \
overflowed when adding {value} of unit {unit}",
unit = Unit::Minute.singular(),
)
})?;
}
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(E::OverflowForUnit { unit: Unit::Second })?;
.ok_or_else(|| {
err!(
"accumulated `SignedDuration` of `{sdur:?}` \
overflowed when adding {value} of unit {unit}",
unit = Unit::Second.singular(),
)
})?;
}
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(E::OverflowForUnit { unit: Unit::Millisecond })?;
.ok_or_else(|| {
err!(
"accumulated `SignedDuration` of `{sdur:?}` \
overflowed when adding {value} of unit {unit}",
unit = Unit::Millisecond.singular(),
)
})?;
}
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(E::OverflowForUnit { unit: Unit::Microsecond })?;
.ok_or_else(|| {
err!(
"accumulated `SignedDuration` of `{sdur:?}` \
overflowed when adding {value} of unit {unit}",
unit = Unit::Microsecond.singular(),
)
})?;
}
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(E::OverflowForUnit { unit: Unit::Nanosecond })?;
.ok_or_else(|| {
err!(
"accumulated `SignedDuration` of `{sdur:?}` \
overflowed when adding {value} of unit {unit}",
unit = Unit::Nanosecond.singular(),
)
})?;
}
if let Some(fraction) = self.get_fraction()? {
sdur = sdur
.checked_add(fractional_duration(min, fraction)?)
.ok_or(E::OverflowForUnitFractional { unit: min })?;
.ok_or_else(|| {
err!(
"accumulated `SignedDuration` of `{sdur:?}` \
overflowed when adding 0.{fraction} of unit {unit}",
unit = min.singular(),
)
})?;
}
Ok(sdur)
@ -848,12 +1003,19 @@ impl DurationUnits {
}
if self.sign.is_negative() {
return Err(Error::from(E::NotAllowedNegative));
return Err(err!(
"cannot parse negative duration into unsigned \
`std::time::Duration`",
));
}
let (min, max) = self.get_min_max_units()?;
if max > Unit::Hour {
return Err(Error::from(E::NotAllowedCalendarUnit { unit: max }));
return Err(err!(
"parsing {unit} units into a `std::time::Duration` \
is not supported (perhaps try parsing into a `Span` instead)",
unit = max.singular(),
));
}
let mut sdur = core::time::Duration::ZERO;
@ -861,37 +1023,73 @@ impl DurationUnits {
let value = self.values[Unit::Hour.as_usize()];
sdur = try_from_hours(value)
.and_then(|nanos| sdur.checked_add(nanos))
.ok_or(E::OverflowForUnit { unit: Unit::Hour })?;
.ok_or_else(|| {
err!(
"accumulated `std::time::Duration` of `{sdur:?}` \
overflowed when adding {value} of unit {unit}",
unit = Unit::Hour.singular(),
)
})?;
}
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(E::OverflowForUnit { unit: Unit::Minute })?;
.ok_or_else(|| {
err!(
"accumulated `std::time::Duration` of `{sdur:?}` \
overflowed when adding {value} of unit {unit}",
unit = Unit::Minute.singular(),
)
})?;
}
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(E::OverflowForUnit { unit: Unit::Second })?;
.ok_or_else(|| {
err!(
"accumulated `std::time::Duration` of `{sdur:?}` \
overflowed when adding {value} of unit {unit}",
unit = Unit::Second.singular(),
)
})?;
}
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(E::OverflowForUnit { unit: Unit::Millisecond })?;
.ok_or_else(|| {
err!(
"accumulated `std::time::Duration` of `{sdur:?}` \
overflowed when adding {value} of unit {unit}",
unit = Unit::Millisecond.singular(),
)
})?;
}
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(E::OverflowForUnit { unit: Unit::Microsecond })?;
.ok_or_else(|| {
err!(
"accumulated `std::time::Duration` of `{sdur:?}` \
overflowed when adding {value} of unit {unit}",
unit = Unit::Microsecond.singular(),
)
})?;
}
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(E::OverflowForUnit { unit: Unit::Nanosecond })?;
.ok_or_else(|| {
err!(
"accumulated `std::time::Duration` of `{sdur:?}` \
overflowed when adding {value} of unit {unit}",
unit = Unit::Nanosecond.singular(),
)
})?;
}
if let Some(fraction) = self.get_fraction()? {
@ -899,7 +1097,13 @@ impl DurationUnits {
.checked_add(
fractional_duration(min, fraction)?.unsigned_abs(),
)
.ok_or(E::OverflowForUnitFractional { unit: Unit::Hour })?;
.ok_or_else(|| {
err!(
"accumulated `std::time::Duration` of `{sdur:?}` \
overflowed when adding 0.{fraction} of unit {unit}",
unit = min.singular(),
)
})?;
}
Ok(sdur)
@ -918,7 +1122,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(Error::from(E::EmptyDuration));
return Err(err!("no parsed duration components"));
};
Ok((min, max))
}
@ -939,12 +1143,21 @@ impl DurationUnits {
}
// Otherwise, if a conversion to `i64` fails, then that failure
// is correct.
let mut value = i64::try_from(value)
.map_err(|_| E::SignedOverflowForUnit { unit })?;
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()
)
})?;
if sign.is_negative() {
value = value
.checked_neg()
.ok_or(E::SignedOverflowForUnit { unit })?;
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()
)
})?;
}
Ok(value)
}
@ -1045,13 +1258,21 @@ pub(crate) fn parse_temporal_fraction<'i>(
}
let digits = mkdigits(input);
if digits.is_empty() {
return Err(Error::from(E::MissingFractionalDigits));
return Err(err!(
"found decimal after seconds component, \
but did not find any decimal digits after decimal",
));
}
// 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).context(E::InvalidFraction)?;
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),
)
})?;
// OK because parsing is forcefully limited to 9 digits,
// which can never be greater than `999_999_99`,
// which is less than `u32::MAX`.
@ -1190,10 +1411,18 @@ fn fractional_time_to_span(
}
if !sdur.is_zero() {
let nanos = sdur.as_nanos();
let nanos64 =
i64::try_from(nanos).map_err(|_| E::InvalidFractionNanos)?;
span =
span.try_nanoseconds(nanos64).context(E::InvalidFractionNanos)?;
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}",
)
})?;
}
Ok(span)
@ -1223,9 +1452,13 @@ fn fractional_time_to_duration(
) -> Result<SignedDuration, Error> {
let sdur = duration_unit_value(unit, value)?;
let fraction_dur = fractional_duration(unit, fraction)?;
Ok(sdur
.checked_add(fraction_dur)
.ok_or(E::OverflowForUnitFractional { unit })?)
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(),
)
})
}
/// Converts the fraction of the given unit to a signed duration.
@ -1255,9 +1488,10 @@ fn fractional_duration(
Unit::Millisecond => fraction / t::NANOS_PER_MICRO.value(),
Unit::Microsecond => fraction / t::NANOS_PER_MILLI.value(),
unit => {
return Err(Error::from(E::NotAllowedFractionalUnit {
found: unit,
}));
return Err(err!(
"fractional {unit} units are not allowed",
unit = unit.singular(),
))
}
};
Ok(SignedDuration::from_nanos(nanos))
@ -1282,13 +1516,17 @@ fn duration_unit_value(
Unit::Hour => {
let seconds = value
.checked_mul(t::SECONDS_PER_HOUR.value())
.ok_or(E::ConversionToSecondsFailed { unit: Unit::Hour })?;
.ok_or_else(|| {
err!("converting {value} hours to seconds overflows i64")
})?;
SignedDuration::from_secs(seconds)
}
Unit::Minute => {
let seconds = value
.checked_mul(t::SECONDS_PER_MINUTE.value())
.ok_or(E::ConversionToSecondsFailed { unit: Unit::Minute })?;
.ok_or_else(|| {
err!("converting {value} minutes to seconds overflows i64")
})?;
SignedDuration::from_secs(seconds)
}
Unit::Second => SignedDuration::from_secs(value),
@ -1296,9 +1534,11 @@ fn duration_unit_value(
Unit::Microsecond => SignedDuration::from_micros(value),
Unit::Nanosecond => SignedDuration::from_nanos(value),
unsupported => {
return Err(Error::from(E::NotAllowedCalendarUnit {
unit: unsupported,
}))
return Err(err!(
"parsing {unit} units into a `SignedDuration` is not supported \
(perhaps try parsing into a `Span` instead)",
unit = unsupported.singular(),
));
}
};
Ok(sdur)
@ -1311,30 +1551,43 @@ mod tests {
use super::*;
#[test]
fn integer() {
let x = IntegerFormatter::new().format_signed(i64::MIN);
fn decimal() {
let x = DecimalFormatter::new().format_signed(i64::MIN);
assert_eq!(x.as_str(), "-9223372036854775808");
let x = IntegerFormatter::new().format_signed(i64::MIN + 1);
let x = DecimalFormatter::new().format_signed(i64::MIN + 1);
assert_eq!(x.as_str(), "-9223372036854775807");
let x = IntegerFormatter::new().format_signed(i64::MAX);
let x = DecimalFormatter::new().format_signed(i64::MAX);
assert_eq!(x.as_str(), "9223372036854775807");
let x = IntegerFormatter::new().format_signed(0);
let x =
DecimalFormatter::new().force_sign(true).format_signed(i64::MAX);
assert_eq!(x.as_str(), "+9223372036854775807");
let x = DecimalFormatter::new().format_signed(0);
assert_eq!(x.as_str(), "0");
let x = IntegerFormatter::new().padding(4).format_signed(0);
let x = DecimalFormatter::new().force_sign(true).format_signed(0);
assert_eq!(x.as_str(), "+0");
let x = DecimalFormatter::new().force_sign(false).format_signed(0);
assert_eq!(x.as_str(), "-0");
let x = DecimalFormatter::new().padding(4).format_signed(0);
assert_eq!(x.as_str(), "0000");
let x = IntegerFormatter::new().padding(4).format_signed(789);
let x = DecimalFormatter::new().padding(4).format_signed(789);
assert_eq!(x.as_str(), "0789");
let x = IntegerFormatter::new().padding(4).format_signed(-789);
let x = DecimalFormatter::new().padding(4).format_signed(-789);
assert_eq!(x.as_str(), "-0789");
let x = IntegerFormatter::new().padding(4).format_signed(789);
assert_eq!(x.as_str(), "0789");
let x = DecimalFormatter::new()
.force_sign(true)
.padding(4)
.format_signed(789);
assert_eq!(x.as_str(), "+0789");
}
#[test]

View file

@ -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<(), log::SetLoggerError> {
pub(crate) fn init() -> Result<(), crate::Error> {
#[cfg(all(feature = "std", feature = "logging"))]
{
log::set_logger(LOGGER)?;
log::set_logger(LOGGER).map_err(crate::Error::adhoc)?;
log::set_max_level(log::LevelFilter::Trace);
Ok(())
}

View file

@ -83,16 +83,15 @@ 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 from Javascript date: \
arithmetic on Unix epoch overflowed"
"failed to get current time: \
subtracting {duration:?} from Unix epoch overflowed"
)
};
timestamp

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

46
src/shared/util/error.rs Normal file
View file

@ -0,0 +1,46 @@
macro_rules! err {
($($tt:tt)*) => {{
crate::shared::util::error::Error::from_args(format_args!($($tt)*))
}}
}
pub(crate) use err;
/// An error that can be returned when parsing.
#[derive(Clone, Debug)]
pub struct Error {
#[cfg(feature = "alloc")]
message: alloc::boxed::Box<str>,
// only-jiff-start
#[cfg(not(feature = "alloc"))]
message: &'static str,
// only-jiff-end
}
impl Error {
pub(crate) fn from_args<'a>(message: core::fmt::Arguments<'a>) -> Error {
#[cfg(feature = "alloc")]
{
use alloc::string::ToString;
let message = message.to_string().into_boxed_str();
Error { message }
}
// only-jiff-start
#[cfg(not(feature = "alloc"))]
{
let message = message.as_str().unwrap_or(
"unknown Jiff error (better error messages require \
enabling the `alloc` feature for the `jiff` crate)",
);
Error { message }
}
// only-jiff-end
}
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
core::fmt::Display::fmt(&self.message, f)
}
}

88
src/shared/util/escape.rs Normal file
View file

@ -0,0 +1,88 @@
/*!
Provides convenience routines for escaping raw bytes.
This was copied from `regex-automata` with a few light edits.
*/
use super::utf8;
/// Provides a convenient `Debug` implementation for a `u8`.
///
/// The `Debug` impl treats the byte as an ASCII, and emits a human
/// readable representation of it. If the byte isn't ASCII, then it's
/// emitted as a hex escape sequence.
#[derive(Clone, Copy)]
pub(crate) struct Byte(pub u8);
impl core::fmt::Display for Byte {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
if self.0 == b' ' {
return write!(f, " ");
}
// 10 bytes is enough for any output from ascii::escape_default.
let mut bytes = [0u8; 10];
let mut len = 0;
for (i, mut b) in core::ascii::escape_default(self.0).enumerate() {
// capitalize \xab to \xAB
if i >= 2 && b'a' <= b && b <= b'f' {
b -= 32;
}
bytes[len] = b;
len += 1;
}
write!(f, "{}", core::str::from_utf8(&bytes[..len]).unwrap())
}
}
impl core::fmt::Debug for Byte {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
write!(f, "\"")?;
core::fmt::Display::fmt(self, f)?;
write!(f, "\"")?;
Ok(())
}
}
/// Provides a convenient `Debug` implementation for `&[u8]`.
///
/// This generally works best when the bytes are presumed to be mostly
/// UTF-8, but will work for anything. For any bytes that aren't UTF-8,
/// they are emitted as hex escape sequences.
#[derive(Clone, Copy)]
pub(crate) struct Bytes<'a>(pub &'a [u8]);
impl<'a> core::fmt::Display for Bytes<'a> {
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) => {
// The decode API guarantees `errant_bytes` is non-empty.
write!(f, r"\x{:02x}", errant_bytes[0])?;
bytes = &bytes[1..];
continue;
}
};
bytes = &bytes[ch.len_utf8()..];
match ch {
'\0' => write!(f, "\\0")?,
'\x01'..='\x7f' => {
write!(f, "{}", (ch as u8).escape_ascii())?;
}
_ => write!(f, "{}", ch.escape_debug())?,
}
}
Ok(())
}
}
impl<'a> core::fmt::Debug for Bytes<'a> {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
write!(f, "\"")?;
core::fmt::Display::fmt(self, f)?;
write!(f, "\"")?;
Ok(())
}
}

View file

@ -22,6 +22,8 @@ they are internal types. Specifically, to distinguish them from Jiff's public
types. For example, `Date` versus `IDate`.
*/
use super::error::{err, Error};
#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
pub(crate) struct ITimestamp {
pub(crate) second: i64,
@ -139,13 +141,11 @@ impl IDateTime {
pub(crate) fn checked_add_seconds(
&self,
seconds: i32,
) -> Result<IDateTime, RangeError> {
let day_second = self
.time
.to_second()
.second
.checked_add(seconds)
.ok_or_else(|| RangeError::DateTimeSeconds)?;
) -> Result<IDateTime, Error> {
let day_second =
self.time.to_second().second.checked_add(seconds).ok_or_else(
|| err!("adding `{seconds}s` to datetime overflowed"),
)?;
let days = day_second.div_euclid(86400);
let second = day_second.rem_euclid(86400);
let date = self.date.checked_add_days(days)?;
@ -160,8 +160,8 @@ pub(crate) struct IEpochDay {
}
impl IEpochDay {
pub(crate) const MIN: IEpochDay = IEpochDay { epoch_day: -4371587 };
pub(crate) const MAX: IEpochDay = IEpochDay { epoch_day: 2932896 };
const MIN: IEpochDay = IEpochDay { epoch_day: -4371587 };
const MAX: IEpochDay = IEpochDay { epoch_day: 2932896 };
/// Converts days since the Unix epoch to a Gregorian date.
///
@ -217,17 +217,20 @@ impl IEpochDay {
/// If this would overflow an `i32` or result in an out-of-bounds epoch
/// day, then this returns an error.
#[inline]
pub(crate) fn checked_add(
&self,
amount: i32,
) -> Result<IEpochDay, RangeError> {
pub(crate) fn checked_add(&self, amount: i32) -> Result<IEpochDay, Error> {
let epoch_day = self.epoch_day;
let sum = epoch_day
.checked_add(amount)
.ok_or_else(|| RangeError::EpochDayI32)?;
let sum = epoch_day.checked_add(amount).ok_or_else(|| {
err!("adding `{amount}` to epoch day `{epoch_day}` overflowed i32")
})?;
let ret = IEpochDay { epoch_day: sum };
if !(IEpochDay::MIN <= ret && ret <= IEpochDay::MAX) {
return Err(RangeError::EpochDayDays);
return Err(err!(
"adding `{amount}` to epoch day `{epoch_day}` \
resulted in `{sum}`, which is not in the required \
epoch day range of `{min}..={max}`",
min = IEpochDay::MIN.epoch_day,
max = IEpochDay::MAX.epoch_day,
));
}
Ok(ret)
}
@ -255,11 +258,14 @@ impl IDate {
year: i16,
month: i8,
day: i8,
) -> Result<IDate, RangeError> {
) -> Result<IDate, Error> {
if day > 28 {
let max_day = days_in_month(year, month);
if day > max_day {
return Err(RangeError::DateInvalidDays { year, month });
return Err(err!(
"day={day} is out of range for year={year} \
and month={month}, must be in range 1..={max_day}",
));
}
}
Ok(IDate { year, month, day })
@ -275,22 +281,37 @@ impl IDate {
pub(crate) fn from_day_of_year(
year: i16,
day: i16,
) -> Result<IDate, RangeError> {
) -> Result<IDate, Error> {
if !(1 <= day && day <= 366) {
return Err(RangeError::DateInvalidDayOfYear { year });
return Err(err!(
"day-of-year={day} is out of range for year={year}, \
must be in range 1..={max_day}",
max_day = days_in_year(year),
));
}
let start = IDate { year, month: 1, day: 1 }.to_epoch_day();
let end = start
.checked_add(i32::from(day) - 1)
// This can only happen when `year=9999` and `day=366`.
.map_err(|_| RangeError::DayOfYear)?
.map_err(|_| {
err!(
"failed to find date for \
year={year} and day-of-year={day}: \
adding `{day}` to `{start}` overflows \
Jiff's range",
start = start.epoch_day,
)
})?
.to_date();
// If we overflowed into the next year, then `day` is too big.
if year != end.year {
// Can only happen given day=366 and this is a leap year.
debug_assert_eq!(day, 366);
debug_assert!(!is_leap_year(year));
return Err(RangeError::DateInvalidDayOfYear { year });
return Err(err!(
"day-of-year={day} is out of range for year={year}, \
must be in range 1..={max_day}",
max_day = days_in_year(year),
));
}
Ok(end)
}
@ -306,9 +327,12 @@ impl IDate {
pub(crate) fn from_day_of_year_no_leap(
year: i16,
mut day: i16,
) -> Result<IDate, RangeError> {
) -> Result<IDate, Error> {
if !(1 <= day && day <= 365) {
return Err(RangeError::DateInvalidDayOfYearNoLeap);
return Err(err!(
"day-of-year={day} is out of range for year={year}, \
must be in range 1..=365",
));
}
if day >= 60 && is_leap_year(year) {
day += 1;
@ -366,9 +390,12 @@ impl IDate {
&self,
nth: i8,
weekday: IWeekday,
) -> Result<IDate, RangeError> {
) -> Result<IDate, Error> {
if nth == 0 || !(-5 <= nth && nth <= 5) {
return Err(RangeError::NthWeekdayOfMonth);
return Err(err!(
"got nth weekday of `{nth}`, but \
must be non-zero and in range `-5..=5`",
));
}
if nth > 0 {
let first_weekday = self.first_of_month().weekday();
@ -385,10 +412,13 @@ impl IDate {
// of `Day`, we can't let this boundary condition escape. So we
// check it here.
if day < 1 {
return Err(RangeError::DateInvalidDays {
year: self.year,
month: self.month,
});
return Err(err!(
"day={day} is out of range for year={year} \
and month={month}, must be in range 1..={max_day}",
year = self.year,
month = self.month,
max_day = days_in_month(self.year, self.month),
));
}
IDate::try_new(self.year, self.month, day)
}
@ -396,12 +426,16 @@ impl IDate {
/// Returns the day before this date.
#[inline]
pub(crate) fn yesterday(self) -> Result<IDate, RangeError> {
pub(crate) fn yesterday(self) -> Result<IDate, Error> {
if self.day == 1 {
if self.month == 1 {
let year = self.year - 1;
if year <= -10000 {
return Err(RangeError::Yesterday);
return Err(err!(
"returning yesterday for -9999-01-01 is not \
possible because it is less than Jiff's supported
minimum date",
));
}
return Ok(IDate { year, month: 12, day: 31 });
}
@ -414,12 +448,16 @@ impl IDate {
/// Returns the day after this date.
#[inline]
pub(crate) fn tomorrow(self) -> Result<IDate, RangeError> {
pub(crate) fn tomorrow(self) -> Result<IDate, Error> {
if self.day >= 28 && self.day == days_in_month(self.year, self.month) {
if self.month == 12 {
let year = self.year + 1;
if year >= 10000 {
return Err(RangeError::Tomorrow);
return Err(err!(
"returning tomorrow for 9999-12-31 is not \
possible because it is greater than Jiff's supported
maximum date",
));
}
return Ok(IDate { year, month: 1, day: 1 });
}
@ -431,20 +469,34 @@ impl IDate {
/// Returns the year one year before this date.
#[inline]
pub(crate) fn prev_year(self) -> Result<i16, RangeError> {
pub(crate) fn prev_year(self) -> Result<i16, Error> {
let year = self.year - 1;
if year <= -10_000 {
return Err(RangeError::YearPrevious);
return Err(err!(
"returning previous year for {year:04}-{month:02}-{day:02} is \
not possible because it is less than Jiff's supported \
minimum date",
year = self.year,
month = self.month,
day = self.day,
));
}
Ok(year)
}
/// Returns the year one year from this date.
#[inline]
pub(crate) fn next_year(self) -> Result<i16, RangeError> {
pub(crate) fn next_year(self) -> Result<i16, Error> {
let year = self.year + 1;
if year >= 10_000 {
return Err(RangeError::YearNext);
return Err(err!(
"returning next year for {year:04}-{month:02}-{day:02} is \
not possible because it is greater than Jiff's supported \
maximum date",
year = self.year,
month = self.month,
day = self.day,
));
}
Ok(year)
}
@ -454,7 +506,7 @@ impl IDate {
pub(crate) fn checked_add_days(
&self,
amount: i32,
) -> Result<IDate, RangeError> {
) -> Result<IDate, Error> {
match amount {
0 => Ok(*self),
-1 => self.yesterday(),
@ -666,84 +718,6 @@ pub(crate) enum IAmbiguousOffset {
Fold { before: IOffset, after: IOffset },
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum RangeError {
DateInvalidDayOfYear { year: i16 },
DateInvalidDayOfYearNoLeap,
DateInvalidDays { year: i16, month: i8 },
DateTimeSeconds,
DayOfYear,
EpochDayDays,
EpochDayI32,
NthWeekdayOfMonth,
Tomorrow,
YearNext,
YearPrevious,
Yesterday,
}
impl core::fmt::Display for RangeError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::RangeError::*;
match *self {
DateInvalidDayOfYear { year } => write!(
f,
"number of days for `{year:04}` is invalid, \
must be in range `1..={max_day}`",
max_day = days_in_year(year),
),
DateInvalidDayOfYearNoLeap => f.write_str(
"number of days is invalid, must be in range `1..=365`",
),
DateInvalidDays { year, month } => write!(
f,
"number of days for `{year:04}-{month:02}` is invalid, \
must be in range `1..={max_day}`",
max_day = days_in_month(year, month),
),
DateTimeSeconds => {
f.write_str("adding seconds to datetime overflowed")
}
DayOfYear => f.write_str("day of year is invalid"),
EpochDayDays => write!(
f,
"adding to epoch day resulted in a value outside \
the allowed range of `{min}..={max}`",
min = IEpochDay::MIN.epoch_day,
max = IEpochDay::MAX.epoch_day,
),
EpochDayI32 => f.write_str(
"adding to epoch day overflowed 32-bit signed integer",
),
NthWeekdayOfMonth => f.write_str(
"invalid nth weekday of month, \
must be non-zero and in range `-5..=5`",
),
Tomorrow => f.write_str(
"returning tomorrow for `9999-12-31` is not \
possible because it is greater than Jiff's supported
maximum date",
),
YearNext => f.write_str(
"creating a date for a year following `9999` is \
not possible because it is greater than Jiff's supported \
maximum date",
),
YearPrevious => f.write_str(
"creating a date for a year preceding `-9999` is \
not possible because it is less than Jiff's supported \
minimum date",
),
Yesterday => f.write_str(
"returning yesterday for `-9999-01-01` is not \
possible because it is less than Jiff's supported
minimum date",
),
}
}
}
/// Returns true if and only if the given year is a leap year.
///
/// A leap year is a year with 366 days. Typical years have 365 days.
@ -946,20 +920,4 @@ mod tests {
let d1 = IDate { year: 9999, month: 12, day: 31 };
assert_eq!(d1.tomorrow().ok(), None);
}
#[test]
fn from_day_of_year() {
assert_eq!(
IDate::from_day_of_year(9999, 365),
Ok(IDate { year: 9999, month: 12, day: 31 }),
);
assert_eq!(
IDate::from_day_of_year(9998, 366),
Err(RangeError::DateInvalidDayOfYear { year: 9998 }),
);
assert_eq!(
IDate::from_day_of_year(9999, 366),
Err(RangeError::DayOfYear),
);
}
}

View file

@ -1,2 +1,5 @@
pub(crate) mod array_str;
pub(crate) mod error;
pub(crate) mod escape;
pub(crate) mod itime;
pub(crate) mod utf8;

37
src/shared/util/utf8.rs Normal file
View file

@ -0,0 +1,37 @@
/// 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]>> {
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 => {
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())]
))
}
};
// OK because we guaranteed above that `string`
// must be non-empty. And thus, `str::chars` must
// yield at least one Unicode scalar value.
Some(Ok(string.chars().next().unwrap()))
}

View file

@ -2,10 +2,10 @@ use core::time::Duration;
use crate::{
civil::{Date, DateTime, Time},
error::{signed_duration::Error as E, ErrorContext},
error::{err, ErrorContext},
fmt::{friendly, temporal},
tz::Offset,
util::{rangeint::TryRFrom, t},
util::{escape, rangeint::TryRFrom, t},
Error, RoundMode, Timestamp, Unit, Zoned,
};
@ -65,7 +65,8 @@ const MINS_PER_HOUR: i64 = 60;
///
/// assert_eq!(
/// "P1d".parse::<SignedDuration>().unwrap_err().to_string(),
/// "parsing ISO 8601 duration in this context requires that \
/// "failed to parse \"P1d\" as an ISO 8601 duration string: \
/// parsing ISO 8601 duration into a `SignedDuration` requires that \
/// the duration contain a time component and no components of days or \
/// greater",
/// );
@ -1455,13 +1456,24 @@ impl SignedDuration {
#[inline]
pub fn try_from_secs_f64(secs: f64) -> Result<SignedDuration, Error> {
if !secs.is_finite() {
return Err(Error::from(E::ConvertNonFinite));
return Err(err!(
"could not convert non-finite seconds \
{secs} to signed duration",
));
}
if secs < (i64::MIN as f64) {
return Err(Error::slim_range("floating point seconds"));
return Err(err!(
"floating point seconds {secs} overflows signed duration \
minimum value of {:?}",
SignedDuration::MIN,
));
}
if secs > (i64::MAX as f64) {
return Err(Error::slim_range("floating point seconds"));
return Err(err!(
"floating point seconds {secs} overflows signed duration \
maximum value of {:?}",
SignedDuration::MAX,
));
}
let mut int_secs = secs.trunc() as i64;
@ -1469,9 +1481,15 @@ 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(|| Error::slim_range("floating point seconds"))?;
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_nanos = 0;
}
Ok(SignedDuration::new_unchecked(int_secs, int_nanos))
@ -1510,13 +1528,24 @@ impl SignedDuration {
#[inline]
pub fn try_from_secs_f32(secs: f32) -> Result<SignedDuration, Error> {
if !secs.is_finite() {
return Err(Error::from(E::ConvertNonFinite));
return Err(err!(
"could not convert non-finite seconds \
{secs} to signed duration",
));
}
if secs < (i64::MIN as f32) {
return Err(Error::slim_range("floating point seconds"));
return Err(err!(
"floating point seconds {secs} overflows signed duration \
minimum value of {:?}",
SignedDuration::MIN,
));
}
if secs > (i64::MAX as f32) {
return Err(Error::slim_range("floating point seconds"));
return Err(err!(
"floating point seconds {secs} overflows signed duration \
maximum value of {:?}",
SignedDuration::MAX,
));
}
let mut int_nanos =
(secs.fract() * (NANOS_PER_SEC as f32)).round() as i32;
@ -1524,9 +1553,15 @@ 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(|| Error::slim_range("floating point seconds"))?;
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_nanos = 0;
}
Ok(SignedDuration::new_unchecked(int_secs, int_nanos))
@ -1988,18 +2023,25 @@ impl SignedDuration {
time2: std::time::SystemTime,
) -> Result<SignedDuration, Error> {
match time2.duration_since(time1) {
Ok(dur) => {
SignedDuration::try_from(dur).context(E::ConvertSystemTime)
}
Ok(dur) => SignedDuration::try_from(dur).with_context(|| {
err!(
"unsigned duration {dur:?} for system time since \
Unix epoch overflowed signed duration"
)
}),
Err(err) => {
let dur = err.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)
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")
})
}
}
}
@ -2113,15 +2155,17 @@ impl SignedDuration {
///
/// assert_eq!(
/// SignedDuration::MAX.round(Unit::Hour).unwrap_err().to_string(),
/// "rounding signed duration to nearest hour \
/// resulted in a value outside the supported \
/// range of a `jiff::SignedDuration`",
/// "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`",
/// );
/// assert_eq!(
/// SignedDuration::MIN.round(Unit::Hour).unwrap_err().to_string(),
/// "rounding signed duration to nearest hour \
/// resulted in a value outside the supported \
/// range of a `jiff::SignedDuration`",
/// "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`",
/// );
/// ```
///
@ -2132,9 +2176,9 @@ impl SignedDuration {
///
/// assert_eq!(
/// SignedDuration::ZERO.round(Unit::Day).unwrap_err().to_string(),
/// "rounding `jiff::SignedDuration` failed \
/// because a calendar unit of 'days' was provided \
/// (to round by calendar units, you must use a `jiff::Span`)",
/// "rounding `SignedDuration` failed \
/// because a calendar unit of days was provided \
/// (to round by calendar units, you must use a `Span`)",
/// );
/// ```
#[inline]
@ -2347,19 +2391,16 @@ impl core::fmt::Debug for SignedDuration {
if f.alternate() {
if self.subsec_nanos() == 0 {
core::fmt::Display::fmt(&self.as_secs(), f)?;
f.write_str("s")
write!(f, "{}s", self.as_secs())
} else if self.as_secs() == 0 {
core::fmt::Display::fmt(&self.subsec_nanos(), f)?;
f.write_str("ns")
write!(f, "{}ns", self.subsec_nanos())
} else {
core::fmt::Display::fmt(&self.as_secs(), f)?;
f.write_str("s ")?;
core::fmt::Display::fmt(
&self.subsec_nanos().unsigned_abs(),
write!(
f,
)?;
f.write_str("ns")
"{}s {}ns",
self.as_secs(),
self.subsec_nanos().unsigned_abs()
)
}
} else {
friendly::DEFAULT_SPAN_PRINTER
@ -2373,8 +2414,9 @@ 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(|_| Error::slim_range("unsigned duration seconds"))?;
let secs = i64::try_from(d.as_secs()).map_err(|_| {
err!("seconds in unsigned duration {d:?} overflowed i64")
})?;
// Guaranteed to succeed since 0<=nanos<=999,999,999.
let nanos = i32::try_from(d.subsec_nanos()).unwrap();
Ok(SignedDuration::new_unchecked(secs, nanos))
@ -2387,10 +2429,14 @@ 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(Error::slim_range("negative duration seconds"));
return Err(err!(
"cannot convert negative duration `{sd:?}` to \
unsigned `std::time::Duration`",
));
}
let secs = u64::try_from(sd.as_secs())
.map_err(|_| Error::slim_range("signed duration seconds"))?;
let secs = u64::try_from(sd.as_secs()).map_err(|_| {
err!("seconds in signed duration {sd:?} overflowed u64")
})?;
// 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.
@ -2725,9 +2771,12 @@ impl SignedDurationRound {
/// Does the actual duration rounding.
fn round(&self, dur: SignedDuration) -> Result<SignedDuration, Error> {
if self.smallest > Unit::Hour {
return Err(Error::from(E::RoundCalendarUnit {
unit: self.smallest,
}));
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(),
));
}
let nanos = t::NoUnits128::new_unchecked(dur.as_nanos());
let increment = t::NoUnits::new_unchecked(self.increment);
@ -2740,7 +2789,12 @@ impl SignedDurationRound {
let seconds = rounded / t::NANOS_PER_SECOND;
let seconds =
t::NoUnits::try_rfrom("seconds", seconds).map_err(|_| {
Error::from(E::RoundOverflowed { unit: self.smallest })
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(),
)
})?;
let subsec_nanos = rounded % t::NANOS_PER_SECOND;
// OK because % 1_000_000_000 above guarantees that the result fits
@ -2784,22 +2838,25 @@ 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> {
let Some((&byte, tail)) = bytes.split_first() else {
return Err(crate::Error::from(
crate::error::fmt::Error::HybridDurationEmpty,
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 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.
}
let mut first = bytes[0];
if first == b'+' || first == b'-' {
let Some(&byte) = tail.first() else {
return Err(crate::Error::from(
crate::error::fmt::Error::HybridDurationPrefix { sign: first },
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),
));
};
first = byte;
}
first = bytes[1];
}
if first == b'P' || first == b'p' {
temporal::DEFAULT_SPAN_PARSER.parse_duration(bytes)
@ -2991,15 +3048,15 @@ mod tests {
insta::assert_snapshot!(
p("").unwrap_err(),
@r#"an empty string is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format"#,
@"an empty string is not a valid `SignedDuration`, expected either a ISO 8601 or Jiff's 'friendly' format",
);
insta::assert_snapshot!(
p("+").unwrap_err(),
@r#"found nothing after sign `+`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format"#,
@"found nothing after sign `+`, which is not a valid `SignedDuration`, expected either a ISO 8601 or Jiff's 'friendly' format",
);
insta::assert_snapshot!(
p("-").unwrap_err(),
@r#"found nothing after sign `-`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format"#,
@"found nothing after sign `-`, which is not a valid `SignedDuration`, expected either a ISO 8601 or Jiff's 'friendly' format",
);
}
@ -3036,15 +3093,15 @@ mod tests {
insta::assert_snapshot!(
p("").unwrap_err(),
@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"#,
@"an empty string is not a valid `SignedDuration`, expected either a ISO 8601 or Jiff's 'friendly' format at line 1 column 2",
);
insta::assert_snapshot!(
p("+").unwrap_err(),
@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"#,
@"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",
);
insta::assert_snapshot!(
p("-").unwrap_err(),
@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"#,
@"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",
);
}

View file

@ -3,11 +3,12 @@ use core::{cmp::Ordering, time::Duration as UnsignedDuration};
use crate::{
civil::{Date, DateTime, Time},
duration::{Duration, SDuration},
error::{span::Error as E, Error, ErrorContext},
error::{err, 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},
@ -556,13 +557,12 @@ 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 \
/// `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate \
/// `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,11 +662,10 @@ 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 `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",
/// (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",
/// );
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
@ -2326,8 +2325,7 @@ 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` and instead will be ably to
/// use `SignedDuration::try_from(span)`. Namely, by default:
/// convert a `Span` to a `SignedDuration`. Namely, by default:
///
/// * [`Zoned::until`] guarantees that the biggest non-zero unit is hours.
/// * [`Timestamp::until`] guarantees that the biggest non-zero unit is
@ -2338,14 +2336,12 @@ 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, 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.)
/// 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.
///
/// 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.
/// Of course, any of the above can be changed by asking, for example,
/// `Zoned::until` to return units up to years.
///
/// # Errors
///
@ -2402,10 +2398,24 @@ impl Span {
let relspan = result
.and_then(|r| r.into_relative_span(Unit::Second, *self))
.with_context(|| match relative.kind {
SpanRelativeToKind::Civil(_) => E::ToDurationCivil,
SpanRelativeToKind::Zoned(_) => E::ToDurationZoned,
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::DaysAre24Hours => {
E::ToDurationDaysAre24Hours
err!(
"could not compute normalized relative span \
from {self} when all days are assumed to be \
24 hours",
)
}
})?;
debug_assert!(relspan.span.largest_unit() <= Unit::Second);
@ -3188,7 +3198,13 @@ impl Span {
&self,
) -> Option<Error> {
let non_time_unit = self.largest_calendar_unit()?;
Some(Error::from(E::NotAllowedCalendarUnits { unit: non_time_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(),
))
}
/// Returns the largest non-zero calendar unit, or `None` if there are no
@ -3256,7 +3272,7 @@ impl Span {
if self.nanoseconds != C(0) {
write!(buf, ", nanoseconds: {:?}", self.nanoseconds).unwrap();
}
buf.push_str(" }}");
write!(buf, " }}").unwrap();
buf
}
@ -3289,8 +3305,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,
}
}
@ -3457,7 +3473,10 @@ 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(Error::from(E::ConvertNegative));
return Err(err!(
"cannot convert negative span {sp:?} \
to unsigned std::time::Duration",
));
}
SignedDuration::try_from(sp).and_then(UnsignedDuration::try_from)
}
@ -3524,15 +3543,18 @@ impl TryFrom<UnsignedDuration> for Span {
#[inline]
fn try_from(d: UnsignedDuration) -> Result<Span, Error> {
let seconds = i64::try_from(d.as_secs())
.map_err(|_| Error::slim_range("unsigned duration seconds"))?;
let seconds = i64::try_from(d.as_secs()).map_err(|_| {
err!("seconds from {d:?} overflows a 64-bit signed integer")
})?;
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)?;
let span = Span::new().try_seconds(seconds).with_context(|| {
err!("duration {d:?} overflows limits of a Jiff `Span`")
})?;
// 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
@ -3584,8 +3606,10 @@ impl TryFrom<Span> for SignedDuration {
#[inline]
fn try_from(sp: Span) -> Result<SignedDuration, Error> {
requires_relative_date_err(sp.largest_unit())
.context(E::ConvertSpanToSignedDuration)?;
requires_relative_date_err(sp.largest_unit()).context(
"failed to convert span to duration without relative datetime \
(must use `Span::to_duration` instead)",
)?;
Ok(sp.to_duration_invariant())
}
}
@ -3654,7 +3678,9 @@ 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)?;
let span = Span::new().try_seconds(seconds).with_context(|| {
err!("signed duration {d:?} overflows limits of a Jiff `Span`")
})?;
// 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
@ -4428,7 +4454,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 \
/// `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate \
/// `SpanRelativeTo::days_are_24_hours()` is used to indicate \
/// invariant 24-hour days, but neither were provided",
/// );
/// let sum = span1.checked_add(
@ -4658,7 +4684,7 @@ impl<'a> SpanCompare<'a> {
/// required. Otherwise, you get an error.
///
/// ```
/// use jiff::{SpanCompare, ToSpan};
/// use jiff::{SpanCompare, ToSpan, Unit};
///
/// let span1 = 2.days().hours(12);
/// let span2 = 60.hours();
@ -4667,7 +4693,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 \
/// `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate \
/// `SpanRelativeTo::days_are_24_hours()` is used to indicate \
/// invariant 24-hour days, but neither were provided",
/// );
/// let ordering = span1.compare(
@ -4898,7 +4924,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 \
/// `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate \
/// `SpanRelativeTo::days_are_24_hours()` is used to indicate \
/// invariant 24-hour days, but neither were provided",
/// );
///
@ -5406,9 +5432,8 @@ 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 `jiff::SpanRelativeTo::days_are_24_hours()` is \
/// used to indicate invariant 24-hour days, but neither were \
/// provided",
/// time be given or `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(),
@ -5461,8 +5486,11 @@ impl<'a> SpanRound<'a> {
let max = existing_largest.max(largest);
let increment = increment::for_span(smallest, self.increment)?;
if largest < smallest {
return Err(Error::from(
E::NotAllowedLargestSmallerThanSmallest { smallest, largest },
return Err(err!(
"largest unit ('{largest}') cannot be smaller than \
smallest unit ('{smallest}')",
largest = largest.singular(),
smallest = smallest.singular(),
));
}
let relative = match self.relative {
@ -5488,13 +5516,14 @@ 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(E::OptionSmallest)?;
.context("error with `smallest` rounding option")?;
if let Some(largest) = self.largest {
requires_relative_date_err(largest)
.context(E::OptionLargest)?;
.context("error with `largest` rounding option")?;
}
requires_relative_date_err(existing_largest)
.context(E::OptionLargestInSpan)?;
requires_relative_date_err(existing_largest).context(
"error with largest unit in span to be rounded",
)?;
assert!(max <= Unit::Week);
return Ok(round_span_invariant(
span, smallest, largest, increment, mode,
@ -5644,7 +5673,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 \
/// `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate \
/// `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:
@ -5680,7 +5709,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 \
/// `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate \
/// `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:
@ -5785,10 +5814,13 @@ impl<'a> SpanRelativeTo<'a> {
}
SpanRelativeToKind::DaysAre24Hours => {
if matches!(unit, Unit::Year | Unit::Month) {
return Err(Error::from(
E::RequiresRelativeYearOrMonthGivenDaysAre24Hours {
unit,
},
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(),
));
}
Ok(None)
@ -6197,12 +6229,27 @@ impl<'a> RelativeSpanKind<'a> {
RelativeSpanKind::Civil { ref start, ref end } => start
.datetime
.until((largest, end.datetime))
.context(E::FailedSpanBetweenDateTimes { unit: largest })?,
RelativeSpanKind::Zoned { ref start, ref end } => {
start.zoned.until((largest, &*end.zoned)).context(
E::FailedSpanBetweenZonedDateTimes { unit: largest },
)?
}
.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(),
)
})?,
};
Ok(RelativeSpan { span, kind: self })
}
@ -6243,7 +6290,9 @@ impl RelativeCivil {
fn new(datetime: DateTime) -> Result<RelativeCivil, Error> {
let timestamp = datetime
.to_zoned(TimeZone::UTC)
.context(E::ConvertDateTimeToTimestamp)?
.with_context(|| {
err!("failed to convert {datetime} to timestamp")
})?
.timestamp();
Ok(RelativeCivil { datetime, timestamp })
}
@ -6259,10 +6308,14 @@ 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)?;
let datetime = self.datetime.checked_add(span).with_context(|| {
err!("failed to add {span} to {dt}", dt = self.datetime)
})?;
let timestamp = datetime
.to_zoned(TimeZone::UTC)
.context(E::ConvertDateTimeToTimestamp)?
.with_context(|| {
err!("failed to convert {datetime} to timestamp")
})?
.timestamp();
Ok(RelativeCivil { datetime, timestamp })
}
@ -6282,10 +6335,15 @@ impl RelativeCivil {
&self,
duration: SignedDuration,
) -> Result<RelativeCivil, Error> {
let datetime = self.datetime.checked_add(duration)?;
let datetime =
self.datetime.checked_add(duration).with_context(|| {
err!("failed to add {duration:?} to {dt}", dt = self.datetime)
})?;
let timestamp = datetime
.to_zoned(TimeZone::UTC)
.context(E::ConvertDateTimeToTimestamp)?
.with_context(|| {
err!("failed to convert {datetime} to timestamp")
})?
.timestamp();
Ok(RelativeCivil { datetime, timestamp })
}
@ -6303,9 +6361,15 @@ impl RelativeCivil {
largest: Unit,
other: &RelativeCivil,
) -> Result<Span, Error> {
self.datetime
.until((largest, other.datetime))
.context(E::FailedSpanBetweenDateTimes { unit: largest })
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,
)
})
}
}
@ -6326,7 +6390,9 @@ impl<'a> RelativeZoned<'a> {
&self,
span: Span,
) -> Result<RelativeZoned<'static>, Error> {
let zoned = self.zoned.checked_add(span)?;
let zoned = self.zoned.checked_add(span).with_context(|| {
err!("failed to add {span} to {zoned}", zoned = self.zoned)
})?;
Ok(RelativeZoned { zoned: DumbCow::Owned(zoned) })
}
@ -6340,7 +6406,9 @@ impl<'a> RelativeZoned<'a> {
&self,
duration: SignedDuration,
) -> Result<RelativeZoned<'static>, Error> {
let zoned = self.zoned.checked_add(duration)?;
let zoned = self.zoned.checked_add(duration).with_context(|| {
err!("failed to add {duration:?} to {zoned}", zoned = self.zoned)
})?;
Ok(RelativeZoned { zoned: DumbCow::Owned(zoned) })
}
@ -6357,9 +6425,15 @@ impl<'a> RelativeZoned<'a> {
largest: Unit,
other: &RelativeZoned<'a>,
) -> Result<Span, Error> {
self.zoned
.until((largest, &*other.zoned))
.context(E::FailedSpanBetweenZonedDateTimes { unit: largest })
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,
)
})
}
/// Returns the borrowed version of self; useful when you need to convert
@ -6438,7 +6512,13 @@ impl Nudge {
increment,
);
let span = Span::from_invariant_nanoseconds(largest, rounded_nanos)
.context(E::ConvertNanoseconds { unit: largest })?
.with_context(|| {
err!(
"failed to convert rounded nanoseconds {rounded_nanos} \
to span for largest unit as {unit}",
unit = largest.plural(),
)
})?
.years_ranged(balanced.get_years_ranged())
.months_ranged(balanced.get_months_ranged())
.weeks_ranged(balanced.get_weeks_ranged());
@ -6471,7 +6551,13 @@ impl Nudge {
* balanced.get_units_ranged(smallest).div_ceil(increment);
let span = balanced
.without_lower(smallest)
.try_units_ranged(smallest, truncated.rinto())?;
.try_units_ranged(smallest, truncated.rinto())
.with_context(|| {
err!(
"failed to set {unit} to {truncated} on span {balanced}",
unit = smallest.singular()
)
})?;
let (relative0, relative1) = clamp_relative_span(
relative_start,
span,
@ -6492,7 +6578,14 @@ 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())?;
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 rounded_relative_end =
if grew_big_unit { relative1 } else { relative0 };
Ok(Nudge { span, rounded_relative_end, grew_big_unit })
@ -6538,7 +6631,13 @@ impl Nudge {
let span =
Span::from_invariant_nanoseconds(Unit::Hour, rounded_time_nanos)
.context(E::ConvertNanoseconds { unit: Unit::Hour })?
.with_context(|| {
err!(
"failed to convert rounded nanoseconds \
{rounded_time_nanos} to span for largest unit as {unit}",
unit = Unit::Hour.plural(),
)
})?
.years_ranged(balanced.get_years_ranged())
.months_ranged(balanced.get_months_ranged())
.weeks_ranged(balanced.get_weeks_ranged())
@ -6583,8 +6682,23 @@ impl Nudge {
let span_start = balanced.without_lower(unit);
let new_units = span_start
.get_units_ranged(unit)
.try_checked_add("bubble-units", sign)?;
let span_end = span_start.try_units_ranged(unit, new_units)?;
.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(),
)
})?;
let threshold = match relative.kind {
RelativeSpanKind::Civil { ref start, .. } => {
start.checked_add(span_end)?.timestamp
@ -6628,8 +6742,13 @@ 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)
.context(E::ConvertNanoseconds { unit: largest })
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(),
)
})
}
/// Returns the nanosecond timestamps of `relative + span` and `relative +
@ -6653,9 +6772,24 @@ 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)?;
let span_amount = span.try_units_ranged(unit, amount)?;
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 relative0 = relative.checked_add(span)?.to_nanosecond();
let relative1 = relative.checked_add(span_amount)?.to_nanosecond();
Ok((relative0, relative1))
@ -6677,22 +6811,25 @@ 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> {
let Some((&byte, tail)) = bytes.split_first() else {
return Err(crate::Error::from(
crate::error::fmt::Error::HybridDurationEmpty,
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 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.
}
let mut first = bytes[0];
if first == b'+' || first == b'-' {
let Some(&byte) = tail.first() else {
return Err(crate::Error::from(
crate::error::fmt::Error::HybridDurationPrefix { sign: first },
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),
));
};
first = byte;
}
first = bytes[1];
}
if first == b'P' || first == b'p' {
temporal::DEFAULT_SPAN_PARSER.parse_span(bytes)
@ -6703,11 +6840,23 @@ 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(Error::from(if matches!(unit, Unit::Week | Unit::Day) {
E::RequiresRelativeWeekOrDay { unit }
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(),
)
} else {
E::RequiresRelativeYearOrMonth { unit }
}));
err!(
"using unit '{unit}' in a span or configuration \
requires that a relative reference time be given, \
but none was provided",
unit = unit.singular(),
)
});
}
Ok(())
}
@ -7241,15 +7390,15 @@ mod tests {
insta::assert_snapshot!(
p("").unwrap_err(),
@r#"an empty string is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format"#,
@"an empty string is not a valid `Span`, expected either a ISO 8601 or Jiff's 'friendly' format",
);
insta::assert_snapshot!(
p("+").unwrap_err(),
@r#"found nothing after sign `+`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format"#,
@"found nothing after sign `+`, which is not a valid `Span`, expected either a ISO 8601 or Jiff's 'friendly' format",
);
insta::assert_snapshot!(
p("-").unwrap_err(),
@r#"found nothing after sign `-`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format"#,
@"found nothing after sign `-`, which is not a valid `Span`, expected either a ISO 8601 or Jiff's 'friendly' format",
);
}
@ -7286,15 +7435,15 @@ mod tests {
insta::assert_snapshot!(
p("").unwrap_err(),
@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"#,
@"an empty string is not a valid `Span`, expected either a ISO 8601 or Jiff's 'friendly' format at line 1 column 2",
);
insta::assert_snapshot!(
p("+").unwrap_err(),
@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"#,
@"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",
);
insta::assert_snapshot!(
p("-").unwrap_err(),
@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"#,
@"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",
);
}
}

View file

@ -2,7 +2,7 @@ use core::time::Duration as UnsignedDuration;
use crate::{
duration::{Duration, SDuration},
error::{timestamp::Error as E, Error, ErrorContext},
error::{err, Error, ErrorContext},
fmt::{
self,
temporal::{self, DEFAULT_DATETIME_PARSER},
@ -279,7 +279,8 @@ 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, \
/// "failed to find offset component in \
/// \"2024-06-30 08:30[America/New_York]\", \
/// which is required for parsing a timestamp",
/// );
/// ```
@ -1519,7 +1520,9 @@ impl Timestamp {
let time_seconds = self.as_second_ranged();
let sum = time_seconds
.try_checked_add("span", span_seconds)
.context(E::OverflowAddSpan)?;
.with_context(|| {
err!("adding {span} to {self} overflowed")
})?;
return Ok(Timestamp::from_second_ranged(sum));
}
}
@ -1527,7 +1530,7 @@ impl Timestamp {
let span_nanos = span.to_invariant_nanoseconds();
let sum = time_nanos
.try_checked_add("span", span_nanos)
.context(E::OverflowAddSpan)?;
.with_context(|| err!("adding {span} to {self} overflowed"))?;
Ok(Timestamp::from_nanosecond_ranged(sum))
}
@ -1537,7 +1540,9 @@ impl Timestamp {
duration: SignedDuration,
) -> Result<Timestamp, Error> {
let start = self.as_duration();
let end = start.checked_add(duration).ok_or(E::OverflowAddDuration)?;
let end = start.checked_add(duration).ok_or_else(|| {
err!("overflow when adding {duration:?} to {self}")
})?;
Timestamp::from_duration(end)
}
@ -1643,7 +1648,9 @@ impl Timestamp {
duration: A,
) -> Result<Timestamp, Error> {
let duration: TimestampArithmetic = duration.into();
duration.saturating_add(self).context(E::RequiresSaturatingTimeUnits)
duration.saturating_add(self).context(
"saturating `Timestamp` arithmetic requires only time units",
)
}
/// This routine is identical to [`Timestamp::saturating_add`] with the
@ -3422,10 +3429,11 @@ impl TimestampDifference {
.get_largest()
.unwrap_or_else(|| self.round.get_smallest().max(Unit::Second));
if largest >= Unit::Day {
return Err(Error::from(
crate::error::util::RoundingIncrementError::Unsupported {
unit: largest,
},
return Err(err!(
"unit {largest} is not supported when computing the \
difference between timestamps (must use units smaller \
than 'day')",
largest = largest.singular(),
));
}
let nano1 = t1.as_nanosecond_ranged().without_bounds();
@ -3848,7 +3856,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 `jiff::Timestamp`, `jiff::tz::Offset` and `jiff::civil::Time` don't support calendar units in a `jiff::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 `Timestamp`, `tz::Offset` and `civil::Time` don't support calendar units in a `Span`)",
)
}
@ -3856,7 +3864,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 `jiff::Timestamp`, `jiff::tz::Offset` and `jiff::civil::Time` don't support calendar units in a `jiff::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 `Timestamp`, `tz::Offset` and `civil::Time` don't support calendar units in a `Span`)",
)
}

View file

@ -1,6 +1,6 @@
use crate::{
civil::DateTime,
error::{tz::ambiguous::Error as E, Error, ErrorContext},
error::{err, Error, ErrorContext},
shared::util::itime::IAmbiguousOffset,
tz::{Offset, TimeZone},
Timestamp, Zoned,
@ -655,10 +655,18 @@ impl AmbiguousTimestamp {
let offset = match self.offset() {
AmbiguousOffset::Unambiguous { offset } => offset,
AmbiguousOffset::Gap { before, after } => {
return Err(Error::from(E::BecauseGap { before, after }));
return Err(err!(
"the datetime {dt} is ambiguous since it falls into \
a gap between offsets {before} and {after}",
dt = self.dt,
));
}
AmbiguousOffset::Fold { before, after } => {
return Err(Error::from(E::BecauseFold { before, after }));
return Err(err!(
"the datetime {dt} is ambiguous since it falls into \
a fold between offsets {before} and {after}",
dt = self.dt,
));
}
};
offset.to_timestamp(self.dt)
@ -1031,10 +1039,13 @@ impl AmbiguousZoned {
/// ```
#[inline]
pub fn compatible(self) -> Result<Zoned, Error> {
let ts = self
.ts
.compatible()
.with_context(|| E::InTimeZone { tz: self.time_zone().clone() })?;
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(),
)
})?;
Ok(ts.to_zoned(self.tz))
}
@ -1090,10 +1101,13 @@ impl AmbiguousZoned {
/// ```
#[inline]
pub fn earlier(self) -> Result<Zoned, Error> {
let ts = self
.ts
.earlier()
.with_context(|| E::InTimeZone { tz: self.time_zone().clone() })?;
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(),
)
})?;
Ok(ts.to_zoned(self.tz))
}
@ -1149,10 +1163,13 @@ impl AmbiguousZoned {
/// ```
#[inline]
pub fn later(self) -> Result<Zoned, Error> {
let ts = self
.ts
.later()
.with_context(|| E::InTimeZone { tz: self.time_zone().clone() })?;
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(),
)
})?;
Ok(ts.to_zoned(self.tz))
}
@ -1203,10 +1220,13 @@ impl AmbiguousZoned {
/// ```
#[inline]
pub fn unambiguous(self) -> Result<Zoned, Error> {
let ts = self
.ts
.unambiguous()
.with_context(|| E::InTimeZone { tz: self.time_zone().clone() })?;
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(),
)
})?;
Ok(ts.to_zoned(self.tz))
}

View file

@ -4,10 +4,7 @@ use alloc::{
};
use crate::{
error::{
tz::concatenated::{Error as E, ALLOC_LIMIT},
Error, ErrorContext,
},
error::{err, Error, ErrorContext},
tz::TimeZone,
util::{array_str::ArrayStr, escape, utf8},
};
@ -74,7 +71,7 @@ impl<R: Read> ConcatenatedTzif<R> {
alloc(scratch1, self.header.index_len())?;
self.rdr
.read_exact_at(scratch1, self.header.index_offset)
.context(E::FailedReadIndex)?;
.context("failed to read index block")?;
let mut index = &**scratch1;
while !index.is_empty() {
@ -97,7 +94,7 @@ impl<R: Read> ConcatenatedTzif<R> {
let start = self.header.data_offset.saturating_add(entry.start());
self.rdr
.read_exact_at(scratch2, start)
.context(E::FailedReadData)?;
.context("failed to read TZif data block")?;
return TimeZone::tzif(name, scratch2).map(Some);
}
Ok(None)
@ -117,7 +114,7 @@ impl<R: Read> ConcatenatedTzif<R> {
alloc(scratch, self.header.index_len())?;
self.rdr
.read_exact_at(scratch, self.header.index_offset)
.context(E::FailedReadIndex)?;
.context("failed to read index block")?;
let names_len = self.header.index_len() / IndexEntry::LEN;
// Why are we careless with this alloc? Well, its size is proportional
@ -157,17 +154,31 @@ 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(E::FailedReadHeader)?;
rdr.read_exact_at(&mut buf, 0)
.context("failed to read concatenated TZif header")?;
if &buf[..6] != b"tzdata" {
return Err(Error::from(E::ExpectedFirstSixBytes));
return Err(err!(
"expected first 6 bytes of concatenated TZif header \
to be `tzdata`, but found `{found}`",
found = escape::Bytes(&buf[..6]),
));
}
if buf[11] != 0 {
return Err(Error::from(E::ExpectedLastByte));
return Err(err!(
"expected last byte of concatenated TZif header \
to be NUL, but found `{found}`",
found = escape::Bytes(&buf[..12]),
));
}
let version = {
let version = core::str::from_utf8(&buf[6..11])
.map_err(|_| E::ExpectedVersion)?;
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]),
)
})?;
// OK because `version` is exactly 5 bytes, by construction.
ArrayStr::new(version).unwrap()
};
@ -176,12 +187,19 @@ 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(Error::from(E::InvalidIndexDataOffsets));
return Err(err!(
"invalid index ({index_offset}) and data ({data_offset}) \
offsets, expected index offset to be less than or equal \
to data offset",
));
}
// 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(Error::from(E::InvalidLengthIndexBlock));
return Err(err!(
"length of index block is not a multiple {len}",
len = IndexEntry::LEN,
));
}
Ok(header)
}
@ -250,8 +268,12 @@ 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(|_| Error::from(E::ExpectedIanaName))
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()),
)
})
}
/// Returns the IANA time zone identifier as a byte slice.
@ -328,12 +350,20 @@ 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(|_| E::InvalidOffsetOverflowSlice)?;
.map_err(|_| err!("offset `{offset}` overflowed `usize`"))?;
let Some(slice) = self.get(offset..) else {
return Err(Error::from(E::InvalidOffsetTooBig));
return Err(err!(
"given offset `{offset}` is not valid \
(only {len} bytes are available)",
len = self.len(),
));
};
if buf.len() > slice.len() {
return Err(Error::from(E::ExpectedMoreData));
return Err(err!(
"unexpected EOF, expected {len} bytes but only have {have}",
len = buf.len(),
have = slice.len()
));
}
buf.copy_from_slice(&slice[..buf.len()]);
Ok(())
@ -365,7 +395,9 @@ impl Read for std::fs::File {
offset = u64::try_from(n)
.ok()
.and_then(|n| n.checked_add(offset))
.ok_or(E::InvalidOffsetOverflowFile)?;
.ok_or_else(|| {
err!("offset overflow when reading from `File`")
})?;
}
Err(ref e) if e.kind() == io::ErrorKind::Interrupted => {}
Err(e) => return Err(Error::io(e)),
@ -387,9 +419,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)
.context(E::FailedSeek)?;
file.seek(SeekFrom::Start(offset)).map_err(Error::io).with_context(
|| err!("failed to seek to offset {offset} in `File`"),
)?;
file.read_exact(buf).map_err(Error::io)
}
}
@ -411,13 +443,31 @@ 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> {
if additional > ALLOC_LIMIT {
return Err(Error::from(E::AllocRequestOverLimit));
// 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)",
));
}
bytes.try_reserve_exact(additional).map_err(|_| E::AllocFailed)?;
bytes.try_reserve_exact(additional).map_err(|_| {
err!(
"failed to allocation {additional} bytes \
for reading concatenated TZif data"
)
})?;
// This... can't actually happen right?
let new_len =
bytes.len().checked_add(additional).ok_or(E::AllocOverflow)?;
let new_len = bytes
.len()
.checked_add(additional)
.ok_or_else(|| err!("total allocation length overflowed `usize`"))?;
bytes.resize(new_len, 0);
Ok(())
}

View file

@ -25,6 +25,6 @@ impl Database {
impl core::fmt::Debug for Database {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.write_str("Bundled(unavailable)")
write!(f, "Bundled(unavailable)")
}
}

View file

@ -26,8 +26,8 @@ impl Database {
Err(_err) => {
warn!(
"failed to parse TZif data from bundled \
tzdb for time zone `{canonical_name}` \
(this is likely a bug, please report it): {_err}"
tzdb for time zone {canonical_name} \
(this is like a bug, please report it): {_err}"
);
return None;
}
@ -48,7 +48,7 @@ impl Database {
impl core::fmt::Debug for Database {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.write_str("Bundled(available)")
write!(f, "Bundled(available)")
}
}

View file

@ -10,12 +10,14 @@ 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::Error::from(
crate::error::CrateFeatureError::TzdbConcatenated,
)
.context(crate::error::tz::db::Error::DisabledConcatenated))
Err(crate::error::err!(
"system concatenated tzdb unavailable: \
crate feature `tzdb-concatenated` is disabled, \
opening tzdb at {path} has therefore failed",
path = path.display(),
))
}
pub(crate) fn none() -> Database {
@ -39,6 +41,6 @@ impl Database {
impl core::fmt::Debug for Database {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.write_str("Concatenated(unavailable)")
write!(f, "Concatenated(unavailable)")
}
}

View file

@ -13,7 +13,7 @@ use std::{
};
use crate::{
error::{tz::db::Error as E, Error},
error::{err, 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 {
f.write_str("Concatenated(")?;
write!(f, "Concatenated(")?;
if let Some(ref path) = self.path {
path.display().fmt(f)?;
write!(f, "{}", path.display())?;
} else {
f.write_str("unavailable")?;
write!(f, "unavailable")?;
}
f.write_str(")")
write!(f, ")")
}
}
@ -540,7 +540,11 @@ 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(Error::from(E::ConcatenatedMissingIanaIdentifiers));
return Err(err!(
"found no IANA time zone identifiers in \
concatenated tzdata file at {path}",
path = path.display(),
));
}
Ok((names, db.version()))
}

View file

@ -1,5 +1,5 @@
use crate::{
error::{tz::db::Error as E, Error},
error::{err, Error},
tz::TimeZone,
util::{sync::Arc, utf8},
};
@ -457,10 +457,22 @@ 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(|| E::failed_time_zone_no_database_configured(name))?;
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)",
)
}
})?;
match *inner {
Kind::ZoneInfo(ref db) => {
if let Some(tz) = db.get(name) {
@ -481,7 +493,7 @@ impl TimeZoneDatabase {
}
}
}
Err(Error::from(E::failed_time_zone(name)))
Err(err!("failed to find time zone `{name}` in time zone database"))
}
/// Returns a list of all available time zone identifiers from this
@ -560,16 +572,16 @@ impl TimeZoneDatabase {
impl core::fmt::Debug for TimeZoneDatabase {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.write_str("TimeZoneDatabase(")?;
write!(f, "TimeZoneDatabase(")?;
let Some(inner) = self.inner.as_deref() else {
return f.write_str("unavailable)");
return write!(f, "unavailable)");
};
match *inner {
Kind::ZoneInfo(ref db) => core::fmt::Debug::fmt(db, f)?,
Kind::Concatenated(ref db) => core::fmt::Debug::fmt(db, f)?,
Kind::Bundled(ref db) => core::fmt::Debug::fmt(db, f)?,
Kind::ZoneInfo(ref db) => write!(f, "{db:?}")?,
Kind::Concatenated(ref db) => write!(f, "{db:?}")?,
Kind::Bundled(ref db) => write!(f, "{db:?}")?,
}
f.write_str(")")
write!(f, ")")
}
}
@ -675,7 +687,7 @@ impl<'d> TimeZoneName<'d> {
impl<'d> core::fmt::Display for TimeZoneName<'d> {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.write_str(self.as_str())
write!(f, "{}", self.as_str())
}
}

View file

@ -10,12 +10,14 @@ 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::Error::from(
crate::error::CrateFeatureError::TzdbZoneInfo,
)
.context(crate::error::tz::db::Error::DisabledZoneInfo))
Err(crate::error::err!(
"system tzdb unavailable: \
crate feature `tzdb-zoneinfo` is disabled, \
opening tzdb at {dir} has therefore failed",
dir = dir.display(),
))
}
pub(crate) fn none() -> Database {
@ -39,6 +41,6 @@ impl Database {
impl core::fmt::Debug for Database {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.write_str("ZoneInfo(unavailable)")
write!(f, "ZoneInfo(unavailable)")
}
}

View file

@ -17,7 +17,7 @@ use std::{
};
use crate::{
error::{tz::db::Error as E, Error},
error::{err, Error},
timestamp::Timestamp,
tz::{
db::special_time_zone, tzif::is_possibly_tzif, TimeZone,
@ -204,13 +204,13 @@ impl Database {
impl core::fmt::Debug for Database {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.write_str("ZoneInfo(")?;
write!(f, "ZoneInfo(")?;
if let Some(ref dir) = self.dir {
core::fmt::Display::fmt(&dir.display(), f)?;
write!(f, "{}", dir.display())?;
} else {
f.write_str("unavailable")?;
write!(f, "unavailable")?;
}
f.write_str(")")
write!(f, ")")
}
}
@ -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| Error::from(err).path(base))?;
.map_err(|err| err.path(base))?;
let lower = original.to_ascii_lowercase();
let inner = ZoneInfoNameInner {
full,
@ -792,18 +792,14 @@ fn walk(start: &Path) -> Result<Vec<ZoneInfoName>, Error> {
let time_zone_name = match path.strip_prefix(start) {
Ok(time_zone_name) => time_zone_name,
// 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) => {
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::from(E::ZoneInfoStripPrefix));
seterr(&path, Error::adhoc(err));
continue;
}
};
@ -821,7 +817,7 @@ fn walk(start: &Path) -> Result<Vec<ZoneInfoName>, Error> {
if names.is_empty() {
let err = first_err
.take()
.unwrap_or_else(|| Error::from(E::ZoneInfoNoTzifFiles));
.unwrap_or_else(|| err!("{}: no TZif files", start.display()));
Err(err)
} else {
// If we found at least one valid name, then we declare success and

View file

@ -6,7 +6,7 @@ use core::{
use crate::{
civil,
duration::{Duration, SDuration},
error::{tz::offset::Error as E, Error, ErrorContext},
error::{err, Error, ErrorContext},
shared::util::itime::IOffset,
span::Span,
timestamp::Timestamp,
@ -526,8 +526,12 @@ impl Offset {
.to_idatetime()
.zip2(self.to_ioffset())
.map(|(idt, ioff)| idt.to_timestamp(ioff));
Timestamp::from_itimestamp(its)
.context(E::ConvertDateTimeToTimestamp { offset: self })
Timestamp::from_itimestamp(its).with_context(|| {
err!(
"converting {dt} with offset {offset} to timestamp overflowed",
offset = self,
)
})
}
/// Adds the given span of time to this offset.
@ -656,11 +660,21 @@ impl Offset {
) -> Result<Offset, Error> {
let duration =
t::SpanZoneOffset::try_new("duration-seconds", duration.as_secs())
.context(E::OverflowAddSignedDuration)?;
.with_context(|| {
err!(
"adding signed duration {duration:?} \
to offset {self} overflowed maximum offset seconds"
)
})?;
let offset_seconds = self.seconds_ranged();
let seconds = offset_seconds
.try_checked_add("offset-seconds", duration)
.context(E::OverflowAddSignedDuration)?;
.with_context(|| {
err!(
"adding signed duration {duration:?} \
to offset {self} overflowed"
)
})?;
Ok(Offset::from_seconds_ranged(seconds))
}
@ -961,7 +975,8 @@ impl Offset {
/// assert_eq!(Offset::MAX.to_string(), "+25:59:59");
/// assert_eq!(
/// Offset::MAX.round(Unit::Minute).unwrap_err().to_string(),
/// "rounding time zone offset resulted in a duration that overflows",
/// "rounding offset `+25:59:59` resulted in a duration of 26h, \
/// which overflows `Offset`",
/// );
/// ```
#[inline]
@ -1110,7 +1125,7 @@ impl core::fmt::Display for Offset {
let minutes = self.part_minutes_ranged().abs().get();
let seconds = self.part_seconds_ranged().abs().get();
if hours == 0 && minutes == 0 && seconds == 0 {
f.write_str("+00")
write!(f, "+00")
} else if hours != 0 && minutes == 0 && seconds == 0 {
write!(f, "{sign}{hours:02}")
} else if minutes != 0 && seconds == 0 {
@ -1332,10 +1347,11 @@ impl TryFrom<SignedDuration> for Offset {
} else if subsec <= -500_000_000 {
seconds = seconds.saturating_sub(1);
}
let seconds =
i32::try_from(seconds).map_err(|_| E::OverflowSignedDuration)?;
let seconds = i32::try_from(seconds).map_err(|_| {
err!("`SignedDuration` of {sdur} overflows `Offset`")
})?;
Offset::from_seconds(seconds)
.map_err(|_| Error::from(E::OverflowSignedDuration))
.map_err(|_| err!("`SignedDuration` of {sdur} overflows `Offset`"))
}
}
@ -1583,11 +1599,20 @@ 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(Error::from(E::RoundInvalidUnit { unit: smallest }));
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(),
));
}
let rounded_sdur = SignedDuration::from(offset).round(self.0)?;
Offset::try_from(rounded_sdur)
.map_err(|_| Error::from(E::RoundOverflow))
Offset::try_from(rounded_sdur).map_err(|_| {
err!(
"rounding offset `{offset}` resulted in a duration \
of {rounded_sdur:?}, which overflows `Offset`",
)
})
}
}
@ -1863,10 +1888,10 @@ impl OffsetConflict {
/// let result = OffsetConflict::Reject.resolve(dt, offset, tz.clone());
/// assert_eq!(
/// result.unwrap_err().to_string(),
/// "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`",
/// "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",
/// );
/// let is_equal = |parsed: Offset, candidate: Offset| {
/// parsed == candidate || candidate.round(Unit::Minute).map_or(
@ -1925,10 +1950,11 @@ impl OffsetConflict {
/// let result = "1970-06-01T00-00:45:00[Africa/Monrovia]".parse::<Zoned>();
/// assert_eq!(
/// result.unwrap_err().to_string(),
/// "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`",
/// "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",
/// );
/// ```
pub fn resolve_with<F>(
@ -2020,13 +2046,13 @@ impl OffsetConflict {
let amb = tz.to_ambiguous_timestamp(dt);
match amb.offset() {
Unambiguous { offset } if !is_equal(given, offset) => {
Err(Error::from(E::ResolveRejectUnambiguous {
given,
offset,
tz,
}))
}
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 { .. } => Ok(amb.into_ambiguous_zoned(tz)),
Gap { before, after } => {
// In `jiff 0.1`, we reported an error when we found a gap
@ -2039,22 +2065,28 @@ impl OffsetConflict {
// changed to treat all offsets in a gap as invalid).
//
// Ref: https://github.com/tc39/proposal-temporal/issues/2892
Err(Error::from(E::ResolveRejectGap {
given,
before,
after,
tz,
}))
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(),
))
}
Fold { before, after }
if !is_equal(given, before) && !is_equal(given, after) =>
{
Err(Error::from(E::ResolveRejectFold {
given,
before,
after,
tz,
}))
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(),
))
}
Fold { .. } => {
let kind = Unambiguous { offset: given };

View file

@ -72,14 +72,14 @@ use core::fmt::Debug;
use crate::{
civil::DateTime,
error::{tz::posix::Error as E, Error, ErrorContext},
error::{err, Error, ErrorContext},
shared,
timestamp::Timestamp,
tz::{
timezone::TimeZoneAbbreviation, AmbiguousOffset, Dst, Offset,
TimeZoneOffsetInfo, TimeZoneTransition,
},
util::{array_str::Abbreviation, parse},
util::{array_str::Abbreviation, escape::Bytes, parse},
};
/// The result of parsing the POSIX `TZ` environment variable.
@ -114,7 +114,11 @@ 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(Error::from(E::ColonPrefixInvalidUtf8));
return Err(err!(
"POSIX time zone string with a ':' prefix contains \
invalid UTF-8: {:?}",
Bytes(&bytes[1..]),
));
};
Ok(PosixTzEnv::Implementation(string.into()))
} else {
@ -134,11 +138,8 @@ 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) => core::fmt::Display::fmt(tz, f),
PosixTzEnv::Implementation(ref imp) => {
f.write_str(":")?;
core::fmt::Display::fmt(imp, f)
}
PosixTzEnv::Rule(ref tz) => write!(f, "{tz}"),
PosixTzEnv::Implementation(ref imp) => write!(f, ":{imp}"),
}
}
}
@ -210,8 +211,10 @@ impl PosixTimeZone<Abbreviation> {
) -> Result<PosixTimeZoneOwned, Error> {
let bytes = bytes.as_ref();
let inner = shared::PosixTimeZone::parse(bytes.as_ref())
.map_err(Error::posix_tz)
.context(E::InvalidPosixTz)?;
.map_err(Error::shared)
.map_err(|e| {
e.context(err!("invalid POSIX TZ string {:?}", Bytes(bytes)))
})?;
Ok(PosixTimeZone { inner })
}
@ -224,8 +227,13 @@ impl PosixTimeZone<Abbreviation> {
let bytes = bytes.as_ref();
let (inner, remaining) =
shared::PosixTimeZone::parse_prefix(bytes.as_ref())
.map_err(Error::posix_tz)
.context(E::InvalidPosixTz)?;
.map_err(Error::shared)
.map_err(|e| {
e.context(err!(
"invalid POSIX TZ string {:?}",
Bytes(bytes)
))
})?;
Ok((PosixTimeZone { inner }, remaining))
}

View file

@ -3,7 +3,7 @@ use std::{sync::RwLock, time::Duration};
use alloc::string::ToString;
use crate::{
error::{tz::system::Error as E, Error, ErrorContext},
error::{err, Error, ErrorContext},
tz::{posix::PosixTzEnv, TimeZone, TimeZoneDatabase},
util::cache::Expiration,
};
@ -141,20 +141,22 @@ 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(E::FailedEnvTz));
return Err(err.context(
"TZ environment variable set, but failed to read value",
));
}
}
if let Some(tz) = sys::get(db) {
return Ok(tz);
}
Err(Error::from(E::FailedSystemTimeZone))
Err(err!("failed to find system time zone"))
}
/// Materializes a `TimeZone` from a `TZ` environment variable.
@ -182,8 +184,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));
@ -194,7 +196,15 @@ 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(E::FailedPosixTzAndUtf8)?.to_string()
tzenv
.to_str()
.ok_or_else(|| {
err!(
"failed to parse {tzenv:?} as a POSIX TZ transition \
string, or as valid UTF-8",
)
})?
.to_string()
}
Ok(PosixTzEnv::Implementation(string)) => string.to_string(),
Ok(PosixTzEnv::Rule(tz)) => {
@ -221,20 +231,26 @@ 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(|| Error::from(E::FailedEnvTzAsTzif))
.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}`",
)
})
.map(Some)
}
};
@ -244,16 +260,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:?})",
);
}
}
@ -263,7 +279,13 @@ 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(|| Error::from(E::FailedEnvTzAsTzif))
.ok_or_else(|| {
err!(
"failed to read TZ={tz_name_or_path:?} \
as a TZif file after attempting a tzdb \
lookup for `{name}`",
)
})
.map(Some)
}
@ -276,8 +298,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)
.context(E::FailedUnnamedTzifRead)?;
let tz =
TimeZone::tzif_system(&data).context(E::FailedUnnamedTzifInvalid)?;
.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:?}"))?;
Ok(tz)
}

View file

@ -8,7 +8,7 @@ use windows_sys::Win32::System::Time::{
};
use crate::{
error::{tz::system::Error as E, Error, ErrorContext},
error::{err, Error, ErrorContext},
tz::{TimeZone, TimeZoneDatabase},
util::utf8,
};
@ -79,12 +79,16 @@ 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(Error::from(E::WindowsMissingIanaMapping));
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",
));
};
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)
}
@ -103,19 +107,24 @@ 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(E::WindowsTimeZoneKeyName)?;
.context(
"could not get TimeZoneKeyName from \
winapi DYNAMIC_TIME_ZONE_INFORMATION",
)?;
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(E::WindowsUtf16DecodeNul)?;
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 string = String::from_utf16(&code_units[..nul])
.map_err(|_| E::WindowsUtf16DecodeInvalid)?;
.map_err(Error::adhoc)
.with_context(|| {
err!("failed to convert u16 slice to UTF-8 (invalid UTF-16)")
})?;
Ok(string)
}

View file

@ -1,6 +1,6 @@
use crate::{
civil::DateTime,
error::{tz::timezone::Error as E, Error},
error::{err, Error},
tz::{
ambiguous::{AmbiguousOffset, AmbiguousTimestamp, AmbiguousZoned},
offset::{Dst, Offset},
@ -392,8 +392,10 @@ impl TimeZone {
pub fn try_system() -> Result<TimeZone, Error> {
#[cfg(not(feature = "tz-system"))]
{
Err(Error::from(crate::error::CrateFeatureError::TzSystem)
.context(E::FailedSystem))
Err(err!(
"failed to get system time zone since 'tz-system' \
crate feature is not enabled",
))
}
#[cfg(feature = "tz-system")]
{
@ -914,7 +916,7 @@ impl TimeZone {
/// assert_eq!(
/// tz.to_fixed_offset().unwrap_err().to_string(),
/// "cannot convert non-fixed IANA time zone \
/// to offset without a timestamp or civil datetime",
/// to offset without timestamp or civil datetime",
/// );
///
/// let tz = TimeZone::UTC;
@ -933,7 +935,11 @@ impl TimeZone {
#[inline]
pub fn to_fixed_offset(&self) -> Result<Offset, Error> {
let mkerr = || {
Error::from(E::ConvertNonFixed { kind: self.kind_description() })
err!(
"cannot convert non-fixed {kind} time zone to offset \
without timestamp or civil datetime",
kind = self.kind_description(),
)
};
repr::each! {
&self.repr,
@ -1386,7 +1392,7 @@ impl TimeZone {
/// Returns a short description about the kind of this time zone.
///
/// This is useful in error messages.
fn kind_description(&self) -> &'static str {
fn kind_description(&self) -> &str {
repr::each! {
&self.repr,
UTC => "UTC",
@ -1881,12 +1887,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 => 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),
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}"),
}
}
}
@ -1933,10 +1939,6 @@ impl<'t> TimeZoneAbbreviation<'t> {
///
/// This module exists to _encapsulate_ the representation rigorously and
/// expose a safe and sound API.
// To squash warnings on older versions of Rust. Our polyfill below should
// match what std does on newer versions of Rust, so the confusability should
// be fine. ---AG
#[allow(unstable_name_collisions)]
mod repr {
use core::mem::ManuallyDrop;
@ -2269,9 +2271,9 @@ mod repr {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
each! {
self,
UTC => f.write_str("UTC"),
UNKNOWN => f.write_str("Etc/Unknown"),
FIXED(offset) => core::fmt::Debug::fmt(&offset, f),
UTC => write!(f, "UTC"),
UNKNOWN => write!(f, "Etc/Unknown"),
FIXED(offset) => write!(f, "{offset:?}"),
STATIC_TZIF(tzif) => {
// The full debug output is a bit much, so constrain it.
let field = tzif.name().unwrap_or("Local");
@ -2282,11 +2284,7 @@ mod repr {
let field = tzif.name().unwrap_or("Local");
f.debug_tuple("TZif").field(&field).finish()
},
ARC_POSIX(posix) => {
f.write_str("Posix(")?;
core::fmt::Display::fmt(&posix, f)?;
f.write_str(")")
},
ARC_POSIX(posix) => write!(f, "Posix({posix})"),
}
}
}

View file

@ -157,7 +157,8 @@ impl TzifOwned {
name: Option<String>,
bytes: &[u8],
) -> Result<Self, Error> {
let sh = shared::TzifOwned::parse(name, bytes).map_err(Error::tzif)?;
let sh =
shared::TzifOwned::parse(name, bytes).map_err(Error::shared)?;
Ok(TzifOwned::from_shared_owned(sh))
}
@ -570,9 +571,9 @@ impl shared::TzifLocalTimeType {
impl core::fmt::Display for shared::TzifIndicator {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
match *self {
shared::TzifIndicator::LocalWall => f.write_str("local/wall"),
shared::TzifIndicator::LocalStandard => f.write_str("local/std"),
shared::TzifIndicator::UTStandard => f.write_str("ut/std"),
shared::TzifIndicator::LocalWall => write!(f, "local/wall"),
shared::TzifIndicator::LocalStandard => write!(f, "local/std"),
shared::TzifIndicator::UTStandard => write!(f, "ut/std"),
}
}
}

View file

@ -111,10 +111,7 @@ use alloc::{
use crate::{
civil::{Date, DateTime, Time, Weekday},
error::{
tz::zic::{Error as E, MAX_LINE_LEN},
Error, ErrorContext,
},
error::{err, Error, ErrorContext},
span::{Span, SpanFieldwise, ToSpan},
timestamp::Timestamp,
tz::{Dst, Offset},
@ -208,12 +205,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().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 offset = r.save.to_offset().map_err(|e| {
err!("SAVE value in rule {:?} is too big: {e}", inner.name)
})?;
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;
@ -263,12 +260,13 @@ impl ZicP {
) -> Result<(), Error> {
while parser.read_next_fields()? {
self.parse_one(&mut parser)
.context(E::Line { number: parser.line_number })?;
.map_err(|e| e.context(err!("line {}", parser.line_number)))?;
}
if let Some(ref name) = parser.continuation_zone_for {
return Err(Error::from(E::ExpectedContinuationZoneLine {
name: name.as_str().into(),
}));
return Err(err!(
"expected continuation zone line for {name:?}, \
but found end of data instead",
));
}
Ok(())
}
@ -279,8 +277,9 @@ impl ZicP {
assert!(!p.fields.is_empty());
if let Some(name) = p.continuation_zone_for.take() {
let zone = ZoneContinuationP::parse(&p.fields)
.context(E::FailedContinuationZone)?;
let zone = ZoneContinuationP::parse(&p.fields).map_err(|e| {
e.context("failed to parse continuation 'Zone' line")
})?;
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.
@ -294,45 +293,51 @@ 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).context(E::FailedRuleLine)?;
let rule = RuleP::parse(rest)
.map_err(|e| e.context("failed to parse 'Rule' line"))?;
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).context(E::FailedZoneFirst)?;
let first = ZoneFirstP::parse(rest)
.map_err(|e| e.context("failed to parse first 'Zone' line"))?;
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(Error::from(E::DuplicateZoneLink {
name: name.into(),
}));
return Err(err!(
"found zone with name {name:?} that conflicts \
with a link of the same name",
));
}
if let Some(previous_zone) = self.zones.insert(name, zone) {
return Err(Error::from(E::DuplicateZone {
name: previous_zone.first.name.name.into(),
}));
return Err(err!(
"found duplicate zone for {:?}",
previous_zone.first.name.name,
));
}
} else if first.starts_with("L") && "Link".starts_with(first) {
let link = LinkP::parse(rest).context(E::FailedLinkLine)?;
let link = LinkP::parse(rest)
.map_err(|e| e.context("failed to parse 'Link' line"))?;
let name = link.name.name.clone();
if self.zones.contains_key(&name) {
return Err(Error::from(E::DuplicateLinkZone {
name: name.into(),
}));
return Err(err!(
"found link with name {name:?} that conflicts \
with a zone of the same name",
));
}
if let Some(previous_link) = self.links.insert(name, link) {
return Err(Error::from(E::DuplicateLink {
name: previous_link.name.name.into(),
}));
return Err(err!(
"found duplicate link for {:?}",
previous_link.name.name,
));
}
// 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(Error::from(E::UnrecognizedZicLine)
.context(E::Line { number: p.line_number }));
return Err(err!("unrecognized zic line: {first:?}"));
}
Ok(())
}
@ -364,9 +369,10 @@ struct RuleP {
impl RuleP {
fn parse(fields: &[&str]) -> Result<RuleP, Error> {
if fields.len() != 9 {
return Err(Error::from(E::ExpectedRuleNineFields {
got: fields.len(),
}));
return Err(err!(
"expected exactly 9 fields for rule, but found {} fields",
fields.len(),
));
}
let (name_field, fields) = (fields[0], &fields[1..]);
let (from_field, fields) = (fields[0], &fields[1..]);
@ -380,21 +386,28 @@ impl RuleP {
let name = name_field
.parse::<RuleNameP>()
.context(E::FailedParseFieldName)?;
.map_err(|e| e.context("failed to parse NAME field"))?;
let from = from_field
.parse::<RuleFromP>()
.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)?;
.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"))?;
let save = save_field
.parse::<RuleSaveP>()
.context(E::FailedParseFieldSave)?;
.map_err(|e| e.context("failed to parse SAVE field"))?;
let letters = letters_field
.parse::<RuleLettersP>()
.context(E::FailedParseFieldLetters)?;
.map_err(|e| e.context("failed to parse LETTERS field"))?;
Ok(RuleP { name, from, to, inn, on, at, save, letters })
}
@ -407,10 +420,9 @@ impl RuleP {
RuleToP::Year { year } => year,
};
if start > end {
return Err(Error::from(E::InvalidRuleYear {
start: start.get(),
end: end.get(),
}));
return Err(err!(
"found start year {start} to be greater than end year {end}"
));
}
Ok(start..=end)
}
@ -450,7 +462,7 @@ struct ZoneFirstP {
impl ZoneFirstP {
fn parse(fields: &[&str]) -> Result<ZoneFirstP, Error> {
if fields.len() < 4 {
return Err(Error::from(E::ExpectedFirstZoneFourFields));
return Err(err!("first ZONE line must have at least 4 fields"));
}
let (name_field, fields) = (fields[0], &fields[1..]);
let (stdoff_field, fields) = (fields[0], &fields[1..]);
@ -458,20 +470,23 @@ impl ZoneFirstP {
let (format_field, fields) = (fields[0], &fields[1..]);
let name = name_field
.parse::<ZoneNameP>()
.context(E::FailedParseFieldName)?;
.map_err(|e| e.context("failed to parse NAME field"))?;
let stdoff = stdoff_field
.parse::<ZoneStdoffP>()
.context(E::FailedParseFieldStdOff)?;
.map_err(|e| e.context("failed to parse STDOFF field"))?;
let rules = rules_field
.parse::<ZoneRulesP>()
.context(E::FailedParseFieldRules)?;
.map_err(|e| e.context("failed to parse RULES field"))?;
let format = format_field
.parse::<ZoneFormatP>()
.context(E::FailedParseFieldFormat)?;
.map_err(|e| e.context("failed to parse FORMAT field"))?;
let until = if fields.is_empty() {
None
} else {
Some(ZoneUntilP::parse(fields).context(E::FailedParseFieldUntil)?)
Some(
ZoneUntilP::parse(fields)
.map_err(|e| e.context("failed to parse UNTIL field"))?,
)
};
Ok(ZoneFirstP { name, stdoff, rules, format, until })
}
@ -497,24 +512,29 @@ struct ZoneContinuationP {
impl ZoneContinuationP {
fn parse(fields: &[&str]) -> Result<ZoneContinuationP, Error> {
if fields.len() < 3 {
return Err(Error::from(E::ExpectedContinuationZoneThreeFields));
return Err(err!(
"continuation ZONE line must have at least 3 fields"
));
}
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>()
.context(E::FailedParseFieldStdOff)?;
.map_err(|e| e.context("failed to parse STDOFF field"))?;
let rules = rules_field
.parse::<ZoneRulesP>()
.context(E::FailedParseFieldRules)?;
.map_err(|e| e.context("failed to parse RULES field"))?;
let format = format_field
.parse::<ZoneFormatP>()
.context(E::FailedParseFieldFormat)?;
.map_err(|e| e.context("failed to parse FORMAT field"))?;
let until = if fields.is_empty() {
None
} else {
Some(ZoneUntilP::parse(fields).context(E::FailedParseFieldUntil)?)
Some(
ZoneUntilP::parse(fields)
.map_err(|e| e.context("failed to parse UNTIL field"))?,
)
};
Ok(ZoneContinuationP { stdoff, rules, format, until })
}
@ -533,14 +553,17 @@ struct LinkP {
impl LinkP {
fn parse(fields: &[&str]) -> Result<LinkP, Error> {
if fields.len() != 2 {
return Err(Error::from(E::ExpectedLinkTwoFields));
return Err(err!(
"expected exactly 2 fields after LINK, but found {}",
fields.len()
));
}
let target = fields[0]
.parse::<ZoneNameP>()
.context(E::FailedParseFieldLinkTarget)?;
.map_err(|e| e.context("failed to parse LINK target"))?;
let name = fields[1]
.parse::<ZoneNameP>()
.context(E::FailedParseFieldLinkName)?;
.map_err(|e| e.context("failed to parse LINK name"))?;
Ok(LinkP { target, name })
}
}
@ -569,9 +592,12 @@ 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(Error::from(E::ExpectedNonEmptyName))
Err(err!("NAME field for rule cannot be empty"))
} else if name.starts_with(|ch| matches!(ch, '0'..='9' | '+' | '-')) {
Err(Error::from(E::ExpectedNameBegin))
Err(err!(
"NAME field cannot begin with a digit, + or -, \
but {name:?} begins with one of those",
))
} else {
Ok(RuleNameP { name: name.to_string() })
}
@ -588,7 +614,8 @@ impl FromStr for RuleFromP {
type Err = Error;
fn from_str(from: &str) -> Result<RuleFromP, Error> {
let year = parse_year(from).context(E::FailedParseFieldFrom)?;
let year = parse_year(from)
.map_err(|e| e.context("failed to parse FROM field"))?;
Ok(RuleFromP { year })
}
}
@ -615,7 +642,8 @@ impl FromStr for RuleToP {
} else if to.starts_with("o") && "only".starts_with(to) {
Ok(RuleToP::Only)
} else {
let year = parse_year(to).context(E::FailedParseFieldTo)?;
let year = parse_year(to)
.map_err(|e| e.context("failed to parse TO field"))?;
Ok(RuleToP::Year { year })
}
}
@ -651,7 +679,7 @@ impl FromStr for RuleInP {
return Ok(RuleInP { month });
}
}
Err(Error::from(E::UnrecognizedMonthName))
Err(err!("unrecognized month name: {field:?}"))
}
}
@ -719,7 +747,7 @@ impl FromStr for RuleOnP {
// field. That gets checked at a higher level.
Ok(RuleOnP::Day { day })
} else {
Err(Error::from(E::UnrecognizedDayOfMonthFormat))
Err(err!("unrecognized format for day-of-month: {field:?}"))
}
}
}
@ -754,7 +782,7 @@ impl FromStr for RuleAtP {
fn from_str(at: &str) -> Result<RuleAtP, Error> {
if at.is_empty() {
return Err(Error::from(E::ExpectedNonEmptyAt));
return Err(err!("empty field is not a valid AT value"));
}
let (span_string, suffix_string) = at.split_at(at.len() - 1);
if suffix_string.chars().all(|ch| ch.is_ascii_alphabetic()) {
@ -785,7 +813,7 @@ impl FromStr for RuleAtSuffixP {
"w" => Ok(RuleAtSuffixP::Wall),
"s" => Ok(RuleAtSuffixP::Standard),
"u" | "g" | "z" => Ok(RuleAtSuffixP::Universal),
_ => Err(Error::from(E::UnrecognizedAtTimeSuffix)),
_ => Err(err!("unrecognized AT time suffix {suffix:?}")),
}
}
}
@ -839,7 +867,7 @@ impl FromStr for RuleSaveP {
fn from_str(at: &str) -> Result<RuleSaveP, Error> {
if at.is_empty() {
return Err(Error::from(E::ExpectedNonEmptySave));
return Err(err!("empty field is not a valid SAVE value"));
}
let (span_string, suffix_string) = at.split_at(at.len() - 1);
if suffix_string.chars().all(|ch| ch.is_ascii_alphabetic()) {
@ -874,7 +902,7 @@ impl FromStr for RuleSaveSuffixP {
match suffix {
"s" => Ok(RuleSaveSuffixP::Standard),
"d" => Ok(RuleSaveSuffixP::Dst),
_ => Err(Error::from(E::UnrecognizedSaveTimeSuffix)),
_ => Err(err!("unrecognized SAVE time suffix {suffix:?}")),
}
}
}
@ -911,13 +939,14 @@ impl FromStr for ZoneNameP {
fn from_str(name: &str) -> Result<ZoneNameP, Error> {
if name.is_empty() {
return Err(Error::from(E::ExpectedNonEmptyZoneName));
return Err(err!("zone names cannot be empty"));
}
for component in name.split('/') {
if component == "." || component == ".." {
return Err(Error::from(E::ExpectedZoneNameComponentNoDots {
component: component.into(),
}));
return Err(err!(
"component {component:?} in zone name {name:?} cannot \
be \".\" or \"..\"",
));
}
}
Ok(ZoneNameP { name: name.to_string() })
@ -1006,12 +1035,16 @@ 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(Error::from(E::ExpectedNonEmptyAbbreviation));
return Err(err!("empty abbreviations are not allowed"));
}
let is_ok =
|ch| matches!(ch, '+'|'-'|'0'..='9'|'A'..='Z'|'a'..='z');
if !abbrev.chars().all(is_ok) {
return Err(Error::from(E::InvalidAbbreviation));
return Err(err!(
"abbreviation {abbrev:?} \
contains invalid character; only \"+\", \"-\" and \
ASCII alpha-numeric characters are allowed"
));
}
Ok(abbrev.to_string())
}
@ -1065,24 +1098,28 @@ enum ZoneUntilP {
impl ZoneUntilP {
fn parse(fields: &[&str]) -> Result<ZoneUntilP, Error> {
if fields.is_empty() {
return Err(Error::from(E::ExpectedUntilYear));
return Err(err!("expected at least a year"));
}
let (year_field, fields) = (fields[0], &fields[1..]);
let year = parse_year(year_field).context(E::FailedParseYear)?;
let year = parse_year(year_field)
.map_err(|e| e.context("failed to parse year"))?;
if fields.is_empty() {
return Ok(ZoneUntilP::Year { year });
}
let (month_field, fields) = (fields[0], &fields[1..]);
let month =
month_field.parse::<RuleInP>().context(E::FailedParseMonth)?;
let month = month_field
.parse::<RuleInP>()
.map_err(|e| e.context("failed to parse month"))?;
if fields.is_empty() {
return Ok(ZoneUntilP::YearMonth { year, month });
}
let (day_field, fields) = (fields[0], &fields[1..]);
let day = day_field.parse::<RuleOnP>().context(E::FailedParseDay)?;
let day = day_field
.parse::<RuleOnP>()
.map_err(|e| e.context("failed to parse day"))?;
if fields.is_empty() {
return Ok(ZoneUntilP::YearMonthDay { year, month, day });
}
@ -1090,9 +1127,13 @@ impl ZoneUntilP {
let (duration_field, fields) = (fields[0], &fields[1..]);
let duration = duration_field
.parse::<RuleAtP>()
.context(E::FailedParseTimeDuration)?;
.map_err(|e| e.context("failed to parse time duration"))?;
if !fields.is_empty() {
return Err(Error::from(E::ExpectedNothingAfterTime));
return Err(err!(
"expected no more fields after time of day, \
but found: {fields:?}",
fields = fields.join(" "),
));
}
Ok(ZoneUntilP::YearMonthDayTime { year, month, day, duration })
}
@ -1159,8 +1200,10 @@ fn parse_year(year: &str) -> Result<t::Year, Error> {
} else {
(t::Sign::N::<1>(), year)
};
let number = parse::i64(rest.as_bytes()).context(E::FailedParseYear)?;
let year = t::Year::try_new("year", number)?;
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}"))?;
Ok(year * sign)
}
@ -1196,28 +1239,36 @@ 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(Error::from(E::ExpectedTimeOneHour));
return Err(err!(
"expected time duration to contain at least one hour digit"
));
}
let hours =
parse::i64(hour_digits.as_bytes()).context(E::FailedParseHour)?;
span = span.try_hours(hours.saturating_mul(i64::from(sign.get())))?;
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"))?;
if rest.is_empty() {
return Ok(span);
}
// Now pluck out the minute component.
if !rest.starts_with(":") {
return Err(Error::from(E::ExpectedColonAfterHour));
return Err(err!("expected ':' after hours, but found {rest:?}"));
}
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(Error::from(E::ExpectedMinuteAfterHours));
return Err(err!(
"expected minute digits after 'HH:', but found {rest:?} instead"
));
}
let minutes =
parse::i64(minute_digits.as_bytes()).context(E::FailedParseMinute)?;
let minutes_ranged = t::Minute::try_new("minutes", minutes)?;
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")
})?;
span = span.minutes_ranged((minutes_ranged * sign).rinto());
if rest.is_empty() {
return Ok(span);
@ -1225,17 +1276,21 @@ fn parse_span(span: &str) -> Result<Span, Error> {
// Now pluck out the second component.
if !rest.starts_with(":") {
return Err(Error::from(E::ExpectedColonAfterMinute));
return Err(err!("expected ':' after minutes, but found {rest:?}"));
}
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(Error::from(E::ExpectedSecondAfterMinutes));
return Err(err!(
"expected second digits after 'MM:', but found {rest:?} instead"
));
}
let seconds =
parse::i64(second_digits.as_bytes()).context(E::FailedParseSecond)?;
let seconds_ranged = t::Second::try_new("seconds", seconds)?;
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")
})?;
span = span.seconds_ranged((seconds_ranged * sign).rinto());
if rest.is_empty() {
return Ok(span);
@ -1243,24 +1298,33 @@ fn parse_span(span: &str) -> Result<Span, Error> {
// Now look for the fractional nanosecond component.
if !rest.starts_with(".") {
return Err(Error::from(E::ExpectedDotAfterSeconds));
return Err(err!("expected '.' after seconds, but found {rest:?}"));
}
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(Error::from(E::ExpectedNanosecondDigits));
return Err(err!(
"expected nanosecond digits after 'SS.', \
but found {rest:?} instead"
));
}
let nanoseconds = parse::fraction(nanosecond_digits.as_bytes())
.context(E::FailedParseNanosecond)?;
let nanoseconds_ranged =
t::FractionalNanosecond::try_new("nanoseconds", nanoseconds)?;
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")
})?;
span = span.nanoseconds_ranged((nanoseconds_ranged * sign).rinto());
// We should have consumed everything at this point.
if !rest.is_empty() {
return Err(Error::from(E::UnrecognizedTrailingTimeDuration));
return Err(err!(
"found unrecognized trailing {rest:?} in time duration"
));
}
span.rebalance(Unit::Hour)
}
@ -1270,8 +1334,10 @@ 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()).context(E::FailedParseDay)?;
let day = t::Day::try_new("day", number)?;
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"))?;
Ok(day)
}
@ -1291,7 +1357,7 @@ fn parse_weekday(string: &str) -> Result<Weekday, Error> {
return Ok(weekday);
}
}
Err(Error::from(E::UnrecognizedDayOfWeek))
Err(err!("unrecognized day of the week: {string:?}"))
}
/// A parser that emits lines as sequences of fields.
@ -1327,7 +1393,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(|_| Error::from(E::InvalidUtf8))?;
.map_err(|e| err!("invalid UTF-8: {e}"))?;
Ok(FieldParser::new(src))
}
@ -1343,10 +1409,12 @@ 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(E::LineOverflow)?;
self.line_number = self
.line_number
.checked_add(1)
.ok_or_else(|| err!("line count overflowed"))?;
parse_fields(&line, &mut self.fields)
.context(E::Line { number: self.line_number })?;
.with_context(|| err!("line {}", self.line_number))?;
if self.fields.is_empty() {
continue;
}
@ -1384,6 +1452,13 @@ 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,
@ -1394,11 +1469,15 @@ fn parse_fields<'a>(
fields.clear();
if line.len() > MAX_LINE_LEN {
return Err(Error::from(E::LineMaxLength));
return Err(err!(
"line with length {} exceeds \
max length of {MAX_LINE_LEN}",
line.len()
));
}
// Do a quick scan for a NUL terminator. They are illegal in all cases.
if line.contains('\x00') {
return Err(Error::from(E::LineNul));
return Err(err!("found line with NUL byte, which isn't allowed"));
}
// The current state of the parser. We start at whitespace, since it also
// means "before a field."
@ -1443,8 +1522,9 @@ fn parse_fields<'a>(
}
State::AfterQuote => {
if !is_space(ch) {
return Err(Error::from(
E::ExpectedWhitespaceAfterQuotedField,
return Err(err!(
"expected whitespace after quoted field, \
but found {ch:?} instead",
));
}
State::Whitespace
@ -1457,7 +1537,7 @@ fn parse_fields<'a>(
fields.push(&line[start..]);
}
State::InQuote => {
return Err(Error::from(E::ExpectedCloseQuote));
return Err(err!("found unclosed quote"));
}
}
Ok(())

View file

@ -49,7 +49,7 @@ impl Sign {
impl core::fmt::Display for Sign {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
if self.is_negative() {
f.write_str("-")
write!(f, "-")
} else {
Ok(())
}

View file

@ -36,13 +36,15 @@ impl Expiration {
impl core::fmt::Display for Expiration {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
let maybe_duration = self.0.and_then(|instant| {
crate::now::monotonic_time()
.and_then(|now| instant.checked_duration_since(now))
});
match maybe_duration {
None => f.write_str("expired"),
Some(duration) => core::fmt::Debug::fmt(&duration, f),
}
let Some(instant) = self.0 else {
return write!(f, "expired");
};
let Some(now) = crate::now::monotonic_time() else {
return write!(f, "expired");
};
let Some(duration) = instant.checked_duration_since(now) else {
return write!(f, "expired");
};
write!(f, "{duration:?}")
}
}

View file

@ -4,121 +4,8 @@ Provides convenience routines for escaping raw bytes.
This was copied from `regex-automata` with a few light edits.
*/
use super::utf8;
/// Provides a convenient `Debug` implementation for a `u8`.
///
/// The `Debug` impl treats the byte as an ASCII, and emits a human
/// readable representation of it. If the byte isn't ASCII, then it's
/// emitted as a hex escape sequence.
#[derive(Clone, Copy)]
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 f.write_str(" ");
}
// 10 bytes is enough for any output from ascii::escape_default.
let mut bytes = [0u8; 10];
let mut len = 0;
for (i, mut b) in core::ascii::escape_default(self.0).enumerate() {
// capitalize \xab to \xAB
if i >= 2 && b'a' <= b && b <= b'f' {
b -= 32;
}
bytes[len] = b;
len += 1;
}
f.write_str(core::str::from_utf8(&bytes[..len]).unwrap())
}
}
impl core::fmt::Debug for Byte {
#[inline(never)]
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.write_str("\"")?;
core::fmt::Display::fmt(self, f)?;
f.write_str("\"")?;
Ok(())
}
}
/// Provides a convenient `Debug` implementation for `&[u8]`.
///
/// This generally works best when the bytes are presumed to be mostly
/// UTF-8, but will work for anything. For any bytes that aren't UTF-8,
/// they are emitted as hex escape sequences.
#[derive(Clone, Copy)]
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(err) => {
// The decode API guarantees `errant_bytes` is non-empty.
write!(f, r"\x{:02x}", err.as_slice()[0])?;
bytes = &bytes[1..];
continue;
}
};
bytes = &bytes[ch.len_utf8()..];
match ch {
'\0' => f.write_str(r"\0")?,
'\x01'..='\x7f' => {
core::fmt::Display::fmt(&(ch as u8).escape_ascii(), f)?;
}
_ => {
core::fmt::Display::fmt(&ch.escape_debug(), f)?;
}
}
}
Ok(())
}
}
impl<'a> core::fmt::Debug for Bytes<'a> {
#[inline(never)]
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.write_str("\"")?;
core::fmt::Display::fmt(self, f)?;
f.write_str("\"")?;
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 {
core::fmt::Display::fmt(&Byte(self.byte), f)?;
}
Ok(())
}
}
impl core::fmt::Debug for RepeatByte {
#[inline(never)]
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.write_str("\"")?;
core::fmt::Display::fmt(self, f)?;
f.write_str("\"")?;
Ok(())
}
}
// These were originally defined here, but they got moved to
// 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};

View file

@ -1,4 +1,7 @@
use crate::error::util::{ParseFractionError, ParseIntError};
use crate::{
error::{err, Error},
util::escape::{Byte, Bytes},
};
/// Parses an `i64` number from the beginning to the end of the given slice of
/// ASCII digit characters.
@ -10,20 +13,38 @@ use crate::error::util::{ParseFractionError, ParseIntError};
/// 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, ParseIntError> {
pub(crate) fn i64(bytes: &[u8]) -> Result<i64, Error> {
if bytes.is_empty() {
return Err(ParseIntError::NoDigitsFound);
return Err(err!("invalid number, no digits found"));
}
let mut n: i64 = 0;
for &byte in 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)?;
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),
)
},
)?;
}
Ok(n)
}
@ -44,9 +65,7 @@ pub(crate) fn i64(bytes: &[u8]) -> Result<i64, ParseIntError> {
///
/// 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]), ParseIntError> {
pub(crate) fn u64_prefix(bytes: &[u8]) -> Result<(Option<u64>, &[u8]), Error> {
// Discovered via `u64::MAX.to_string().len()`.
const MAX_U64_DIGITS: usize = 20;
@ -60,10 +79,15 @@ pub(crate) fn u64_prefix(
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(ParseIntError::TooBig)?;
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]),
)
},
)?;
}
if digit_count == 0 {
return Ok((None, bytes));
@ -81,33 +105,54 @@ pub(crate) fn u64_prefix(
///
/// 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, ParseFractionError> {
pub(crate) fn fraction(bytes: &[u8]) -> Result<u32, Error> {
const MAX_PRECISION: usize = 9;
if bytes.is_empty() {
return Err(ParseFractionError::NoDigitsFound);
} else if bytes.len() > ParseFractionError::MAX_PRECISION {
return Err(ParseFractionError::TooManyDigits);
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"
));
}
let mut n: u32 = 0;
for &byte in bytes {
let digit = match byte.checked_sub(b'0') {
None => {
return Err(ParseFractionError::InvalidDigit(byte));
return Err(err!(
"invalid fractional digit, expected 0-9 but got {}",
Byte(byte),
));
}
Some(digit) if digit > 9 => {
return Err(ParseFractionError::InvalidDigit(byte));
return Err(err!(
"invalid fractional digit, expected 0-9 but got {}",
Byte(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(|| ParseFractionError::TooBig)?;
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),
)
},
)?;
}
for _ in bytes.len()..ParseFractionError::MAX_PRECISION {
n = n.checked_mul(10).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)
)
})?;
}
Ok(n)
}
@ -116,17 +161,15 @@ pub(crate) fn fraction(bytes: &[u8]) -> Result<u32, ParseFractionError> {
///
/// This is effectively `OsStr::to_str`, but with a slightly better error
/// message.
#[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>
#[cfg(feature = "tzdb-zoneinfo")]
pub(crate) fn os_str_utf8<'o, O>(os_str: &'o O) -> Result<&'o str, Error>
where
O: ?Sized + AsRef<std::ffi::OsStr>,
{
let os_str = os_str.as_ref();
os_str
.to_str()
.ok_or_else(|| crate::error::util::OsStrUtf8Error::from(os_str))
.ok_or_else(|| err!("environment value {os_str:?} is not valid UTF-8"))
}
/// Parses an `OsStr` into a `&str` when `&[u8]` isn't easily available.
@ -135,9 +178,7 @@ 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], crate::error::util::OsStrUtf8Error>
pub(crate) fn os_str_bytes<'o, O>(os_str: &'o O) -> Result<&'o [u8], Error>
where
O: ?Sized + AsRef<std::ffi::OsStr>,
{
@ -149,13 +190,16 @@ 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(os_str_utf8(os_str)?.as_bytes())
Ok(string.as_bytes())
}
}

Some files were not shown because too many files have changed in this diff Show more