mirror of
https://github.com/BurntSushi/jiff.git
synced 2025-12-23 08:47:45 +00:00
Compare commits
30 commits
jiff-icu-0
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61dc9bd8aa | ||
|
|
1467f47e36 | ||
|
|
5b27b22096 | ||
|
|
4d2041567c | ||
|
|
21f69521cd | ||
|
|
a50f6797ce | ||
|
|
0392d43064 | ||
|
|
523b55bc1a | ||
|
|
3cb0b6aefa | ||
|
|
8e8033a29d | ||
|
|
259e8134ef | ||
|
|
831d3efb4d | ||
|
|
26202e5d4d | ||
|
|
ac0054c72f | ||
|
|
f275164239 | ||
|
|
d024322b27 | ||
|
|
973876a9f1 | ||
|
|
3a832162be | ||
|
|
b8757deba8 | ||
|
|
3765a52b8d | ||
|
|
42080c4e71 | ||
|
|
40941a5188 | ||
|
|
3780351543 | ||
|
|
54c1b2d25b | ||
|
|
aa361bbfc2 | ||
|
|
34359896a4 | ||
|
|
f889e5b40a | ||
|
|
4526cd2663 | ||
|
|
9d7e099a7a | ||
|
|
552b9d1fef |
108 changed files with 10811 additions and 5768 deletions
17
.github/workflows/ci.yml
vendored
17
.github/workflows/ci.yml
vendored
|
|
@ -229,6 +229,23 @@ 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
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"rust-analyzer.linkedProjects": [
|
||||
"bench/Cargo.toml",
|
||||
"crates/jiff-icu/Cargo.toml",
|
||||
"fuzz/Cargo.toml",
|
||||
"Cargo.toml"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
13
CHANGELOG.md
13
CHANGELOG.md
|
|
@ -1,5 +1,18 @@
|
|||
# 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
|
||||
|
|
|
|||
|
|
@ -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(&code));
|
||||
let code = remove_only_jiffs(&remove_cfg_alloc_or_std(&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")]` gates.
|
||||
/// Removes all `#[cfg(feature = "alloc|std")]` gates.
|
||||
///
|
||||
/// This is because the proc-macro always runs in a context where `alloc`
|
||||
/// (and `std`) are enabled.
|
||||
fn remove_cfg_alloc(code: &str) -> String {
|
||||
fn remove_cfg_alloc_or_std(code: &str) -> String {
|
||||
static RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r###"#\[cfg\(feature = "alloc"\)\]\n"###).unwrap()
|
||||
Regex::new(r###"#\[cfg\(feature = "(alloc|std)"\)\]\n"###).unwrap()
|
||||
});
|
||||
RE.replace_all(code, "").into_owned()
|
||||
}
|
||||
|
|
|
|||
100
crates/jiff-static/src/shared/error/itime.rs
Normal file
100
crates/jiff-static/src/shared/error/itime.rs
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
// 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",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
57
crates/jiff-static/src/shared/error/mod.rs
Normal file
57
crates/jiff-static/src/shared/error/mod.rs
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
// 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
|
|
@ -17,6 +17,7 @@ use super::utf8;
|
|||
pub(crate) struct Byte(pub u8);
|
||||
|
||||
impl core::fmt::Display for Byte {
|
||||
#[inline(never)]
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
if self.0 == b' ' {
|
||||
return write!(f, " ");
|
||||
|
|
@ -37,6 +38,7 @@ impl core::fmt::Display for Byte {
|
|||
}
|
||||
|
||||
impl core::fmt::Debug for Byte {
|
||||
#[inline(never)]
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
write!(f, "\"")?;
|
||||
core::fmt::Display::fmt(self, f)?;
|
||||
|
|
@ -54,15 +56,16 @@ impl core::fmt::Debug for Byte {
|
|||
pub(crate) struct Bytes<'a>(pub &'a [u8]);
|
||||
|
||||
impl<'a> core::fmt::Display for Bytes<'a> {
|
||||
#[inline(never)]
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
// This is a sad re-implementation of a similar impl found in bstr.
|
||||
let mut bytes = self.0;
|
||||
while let Some(result) = utf8::decode(bytes) {
|
||||
let ch = match result {
|
||||
Ok(ch) => ch,
|
||||
Err(errant_bytes) => {
|
||||
Err(err) => {
|
||||
// The decode API guarantees `errant_bytes` is non-empty.
|
||||
write!(f, r"\x{:02x}", errant_bytes[0])?;
|
||||
write!(f, r"\x{:02x}", err.as_slice()[0])?;
|
||||
bytes = &bytes[1..];
|
||||
continue;
|
||||
}
|
||||
|
|
@ -81,6 +84,7 @@ 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)?;
|
||||
|
|
@ -88,3 +92,34 @@ 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(())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,8 +24,6 @@ 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,
|
||||
|
|
@ -143,11 +141,13 @@ impl IDateTime {
|
|||
pub(crate) fn checked_add_seconds(
|
||||
&self,
|
||||
seconds: i32,
|
||||
) -> Result<IDateTime, Error> {
|
||||
let day_second =
|
||||
self.time.to_second().second.checked_add(seconds).ok_or_else(
|
||||
|| err!("adding `{seconds}s` to datetime overflowed"),
|
||||
)?;
|
||||
) -> Result<IDateTime, RangeError> {
|
||||
let day_second = self
|
||||
.time
|
||||
.to_second()
|
||||
.second
|
||||
.checked_add(seconds)
|
||||
.ok_or_else(|| RangeError::DateTimeSeconds)?;
|
||||
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 {
|
||||
const MIN: IEpochDay = IEpochDay { epoch_day: -4371587 };
|
||||
const MAX: IEpochDay = IEpochDay { epoch_day: 2932896 };
|
||||
pub(crate) const MIN: IEpochDay = IEpochDay { epoch_day: -4371587 };
|
||||
pub(crate) const MAX: IEpochDay = IEpochDay { epoch_day: 2932896 };
|
||||
|
||||
/// Converts days since the Unix epoch to a Gregorian date.
|
||||
///
|
||||
|
|
@ -219,20 +219,17 @@ 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, Error> {
|
||||
pub(crate) fn checked_add(
|
||||
&self,
|
||||
amount: i32,
|
||||
) -> Result<IEpochDay, RangeError> {
|
||||
let epoch_day = self.epoch_day;
|
||||
let sum = epoch_day.checked_add(amount).ok_or_else(|| {
|
||||
err!("adding `{amount}` to epoch day `{epoch_day}` overflowed i32")
|
||||
})?;
|
||||
let sum = epoch_day
|
||||
.checked_add(amount)
|
||||
.ok_or_else(|| RangeError::EpochDayI32)?;
|
||||
let ret = IEpochDay { epoch_day: sum };
|
||||
if !(IEpochDay::MIN <= ret && ret <= IEpochDay::MAX) {
|
||||
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,
|
||||
));
|
||||
return Err(RangeError::EpochDayDays);
|
||||
}
|
||||
Ok(ret)
|
||||
}
|
||||
|
|
@ -260,14 +257,11 @@ impl IDate {
|
|||
year: i16,
|
||||
month: i8,
|
||||
day: i8,
|
||||
) -> Result<IDate, Error> {
|
||||
) -> Result<IDate, RangeError> {
|
||||
if day > 28 {
|
||||
let max_day = days_in_month(year, month);
|
||||
if day > max_day {
|
||||
return Err(err!(
|
||||
"day={day} is out of range for year={year} \
|
||||
and month={month}, must be in range 1..={max_day}",
|
||||
));
|
||||
return Err(RangeError::DateInvalidDays { year, month });
|
||||
}
|
||||
}
|
||||
Ok(IDate { year, month, day })
|
||||
|
|
@ -283,37 +277,22 @@ impl IDate {
|
|||
pub(crate) fn from_day_of_year(
|
||||
year: i16,
|
||||
day: i16,
|
||||
) -> Result<IDate, Error> {
|
||||
) -> Result<IDate, RangeError> {
|
||||
if !(1 <= day && day <= 366) {
|
||||
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),
|
||||
));
|
||||
return Err(RangeError::DateInvalidDayOfYear { year });
|
||||
}
|
||||
let start = IDate { year, month: 1, day: 1 }.to_epoch_day();
|
||||
let end = start
|
||||
.checked_add(i32::from(day) - 1)
|
||||
.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,
|
||||
)
|
||||
})?
|
||||
// This can only happen when `year=9999` and `day=366`.
|
||||
.map_err(|_| RangeError::DayOfYear)?
|
||||
.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(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),
|
||||
));
|
||||
return Err(RangeError::DateInvalidDayOfYear { year });
|
||||
}
|
||||
Ok(end)
|
||||
}
|
||||
|
|
@ -329,12 +308,9 @@ impl IDate {
|
|||
pub(crate) fn from_day_of_year_no_leap(
|
||||
year: i16,
|
||||
mut day: i16,
|
||||
) -> Result<IDate, Error> {
|
||||
) -> Result<IDate, RangeError> {
|
||||
if !(1 <= day && day <= 365) {
|
||||
return Err(err!(
|
||||
"day-of-year={day} is out of range for year={year}, \
|
||||
must be in range 1..=365",
|
||||
));
|
||||
return Err(RangeError::DateInvalidDayOfYearNoLeap);
|
||||
}
|
||||
if day >= 60 && is_leap_year(year) {
|
||||
day += 1;
|
||||
|
|
@ -392,12 +368,9 @@ impl IDate {
|
|||
&self,
|
||||
nth: i8,
|
||||
weekday: IWeekday,
|
||||
) -> Result<IDate, Error> {
|
||||
) -> Result<IDate, RangeError> {
|
||||
if nth == 0 || !(-5 <= nth && nth <= 5) {
|
||||
return Err(err!(
|
||||
"got nth weekday of `{nth}`, but \
|
||||
must be non-zero and in range `-5..=5`",
|
||||
));
|
||||
return Err(RangeError::NthWeekdayOfMonth);
|
||||
}
|
||||
if nth > 0 {
|
||||
let first_weekday = self.first_of_month().weekday();
|
||||
|
|
@ -414,13 +387,10 @@ impl IDate {
|
|||
// of `Day`, we can't let this boundary condition escape. So we
|
||||
// check it here.
|
||||
if day < 1 {
|
||||
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),
|
||||
));
|
||||
return Err(RangeError::DateInvalidDays {
|
||||
year: self.year,
|
||||
month: self.month,
|
||||
});
|
||||
}
|
||||
IDate::try_new(self.year, self.month, day)
|
||||
}
|
||||
|
|
@ -428,16 +398,12 @@ impl IDate {
|
|||
|
||||
/// Returns the day before this date.
|
||||
#[inline]
|
||||
pub(crate) fn yesterday(self) -> Result<IDate, Error> {
|
||||
pub(crate) fn yesterday(self) -> Result<IDate, RangeError> {
|
||||
if self.day == 1 {
|
||||
if self.month == 1 {
|
||||
let year = self.year - 1;
|
||||
if year <= -10000 {
|
||||
return Err(err!(
|
||||
"returning yesterday for -9999-01-01 is not \
|
||||
possible because it is less than Jiff's supported
|
||||
minimum date",
|
||||
));
|
||||
return Err(RangeError::Yesterday);
|
||||
}
|
||||
return Ok(IDate { year, month: 12, day: 31 });
|
||||
}
|
||||
|
|
@ -450,16 +416,12 @@ impl IDate {
|
|||
|
||||
/// Returns the day after this date.
|
||||
#[inline]
|
||||
pub(crate) fn tomorrow(self) -> Result<IDate, Error> {
|
||||
pub(crate) fn tomorrow(self) -> Result<IDate, RangeError> {
|
||||
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(err!(
|
||||
"returning tomorrow for 9999-12-31 is not \
|
||||
possible because it is greater than Jiff's supported
|
||||
maximum date",
|
||||
));
|
||||
return Err(RangeError::Tomorrow);
|
||||
}
|
||||
return Ok(IDate { year, month: 1, day: 1 });
|
||||
}
|
||||
|
|
@ -471,34 +433,20 @@ impl IDate {
|
|||
|
||||
/// Returns the year one year before this date.
|
||||
#[inline]
|
||||
pub(crate) fn prev_year(self) -> Result<i16, Error> {
|
||||
pub(crate) fn prev_year(self) -> Result<i16, RangeError> {
|
||||
let year = self.year - 1;
|
||||
if year <= -10_000 {
|
||||
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,
|
||||
));
|
||||
return Err(RangeError::YearPrevious);
|
||||
}
|
||||
Ok(year)
|
||||
}
|
||||
|
||||
/// Returns the year one year from this date.
|
||||
#[inline]
|
||||
pub(crate) fn next_year(self) -> Result<i16, Error> {
|
||||
pub(crate) fn next_year(self) -> Result<i16, RangeError> {
|
||||
let year = self.year + 1;
|
||||
if year >= 10_000 {
|
||||
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,
|
||||
));
|
||||
return Err(RangeError::YearNext);
|
||||
}
|
||||
Ok(year)
|
||||
}
|
||||
|
|
@ -508,7 +456,7 @@ impl IDate {
|
|||
pub(crate) fn checked_add_days(
|
||||
&self,
|
||||
amount: i32,
|
||||
) -> Result<IDate, Error> {
|
||||
) -> Result<IDate, RangeError> {
|
||||
match amount {
|
||||
0 => Ok(*self),
|
||||
-1 => self.yesterday(),
|
||||
|
|
@ -720,6 +668,84 @@ 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.
|
||||
|
|
@ -922,4 +948,20 @@ 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
// 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;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,59 @@
|
|||
// auto-generated by: jiff-cli generate shared
|
||||
|
||||
/// Represents an invalid UTF-8 sequence.
|
||||
///
|
||||
/// This is an error returned by `decode`. It is guaranteed to
|
||||
/// contain 1, 2 or 3 bytes.
|
||||
pub(crate) struct Utf8Error {
|
||||
bytes: [u8; 3],
|
||||
len: u8,
|
||||
}
|
||||
|
||||
impl Utf8Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn new(original_bytes: &[u8], err: core::str::Utf8Error) -> Utf8Error {
|
||||
let len = err.error_len().unwrap_or_else(|| original_bytes.len());
|
||||
// OK because the biggest invalid UTF-8
|
||||
// sequence possible is 3.
|
||||
debug_assert!(1 <= len && len <= 3);
|
||||
let mut bytes = [0; 3];
|
||||
bytes[..len].copy_from_slice(&original_bytes[..len]);
|
||||
Utf8Error {
|
||||
bytes,
|
||||
// OK because the biggest invalid UTF-8
|
||||
// sequence possible is 3.
|
||||
len: u8::try_from(len).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the slice of invalid UTF-8 bytes.
|
||||
///
|
||||
/// The slice returned is guaranteed to have length equivalent
|
||||
/// to `Utf8Error::len`.
|
||||
pub(crate) fn as_slice(&self) -> &[u8] {
|
||||
&self.bytes[..self.len()]
|
||||
}
|
||||
|
||||
/// Returns the length of the invalid UTF-8 sequence found.
|
||||
///
|
||||
/// This is guaranteed to be 1, 2 or 3.
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
usize::from(self.len)
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Utf8Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"found invalid UTF-8 byte {errant_bytes:?} in format \
|
||||
string (format strings must be valid UTF-8)",
|
||||
errant_bytes = crate::shared::util::escape::Bytes(self.as_slice()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Decodes the next UTF-8 encoded codepoint from the given byte slice.
|
||||
///
|
||||
/// If no valid encoding of a codepoint exists at the beginning of the
|
||||
|
|
@ -15,25 +69,24 @@
|
|||
/// *WARNING*: This is not designed for performance. If you're looking for
|
||||
/// a fast UTF-8 decoder, this is not it. If you feel like you need one in
|
||||
/// this crate, then please file an issue and discuss your use case.
|
||||
pub(crate) fn decode(bytes: &[u8]) -> Option<Result<char, &[u8]>> {
|
||||
pub(crate) fn decode(bytes: &[u8]) -> Option<Result<char, Utf8Error>> {
|
||||
if bytes.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let string = match core::str::from_utf8(&bytes[..bytes.len().min(4)]) {
|
||||
Ok(s) => s,
|
||||
Err(ref err) if err.valid_up_to() > 0 => {
|
||||
// OK because we just verified we have at least some
|
||||
// valid UTF-8.
|
||||
core::str::from_utf8(&bytes[..err.valid_up_to()]).unwrap()
|
||||
}
|
||||
// In this case, we want to return 1-3 bytes that make up a prefix of
|
||||
// a potentially valid codepoint.
|
||||
Err(err) => {
|
||||
return Some(Err(
|
||||
&bytes[..err.error_len().unwrap_or_else(|| bytes.len())]
|
||||
))
|
||||
}
|
||||
Err(err) => return Some(Err(Utf8Error::new(bytes, err))),
|
||||
};
|
||||
// OK because we guaranteed above that `string`
|
||||
// must be non-empty. And thus, `str::chars` must
|
||||
// yield at least one Unicode scalar value.
|
||||
Some(Ok(string.chars().next().unwrap()))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "jiff-tzdb"
|
||||
version = "0.1.4" #:version
|
||||
version = "0.1.5" #:version
|
||||
authors = ["Andrew Gallant <jamslam@gmail.com>"]
|
||||
license = "Unlicense OR MIT"
|
||||
homepage = "https://github.com/BurntSushi/jiff/tree/master/crates/jiff-tzdb"
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,4 +1,4 @@
|
|||
pub(super) static VERSION: Option<&str> = Some(r"2025b");
|
||||
pub(super) static VERSION: Option<&str> = Some(r"2025c");
|
||||
|
||||
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", 163461..164770),
|
||||
(r"Africa/Casablanca", 189897..191818),
|
||||
(r"Africa/Cairo", 162382..163691),
|
||||
(r"Africa/Casablanca", 190185..192106),
|
||||
(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", 186193..188019),
|
||||
(r"Africa/El_Aaiun", 186481..188307),
|
||||
(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", 152557..153642),
|
||||
(r"America/Asuncion", 151478..152563),
|
||||
(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", 184439..186193),
|
||||
(r"America/Chicago", 184727..186481),
|
||||
(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", 167472..168834),
|
||||
(r"America/Coyhaique", 167760..169122),
|
||||
(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", 149270..150349),
|
||||
(r"America/Fort_Nelson", 171698..173146),
|
||||
(r"America/Ensenada", 166393..167760),
|
||||
(r"America/Fort_Nelson", 171986..173434),
|
||||
(r"America/Fort_Wayne", 36060..36591),
|
||||
(r"America/Fortaleza", 37559..38043),
|
||||
(r"America/Glace_Bay", 110783..111663),
|
||||
(r"America/Godthab", 191818..192783),
|
||||
(r"America/Goose_Bay", 176127..177707),
|
||||
(r"America/Godthab", 192106..193071),
|
||||
(r"America/Goose_Bay", 176415..177995),
|
||||
(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", 179306..180978),
|
||||
(r"America/Havana", 153642..154759),
|
||||
(r"America/Halifax", 179594..181266),
|
||||
(r"America/Havana", 152563..153680),
|
||||
(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", 158397..159639),
|
||||
(r"America/Kentucky/Louisville", 157318..158560),
|
||||
(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", 160873..162167),
|
||||
(r"America/Louisville", 158397..159639),
|
||||
(r"America/Los_Angeles", 159794..161088),
|
||||
(r"America/Louisville", 157318..158560),
|
||||
(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", 174634..176127),
|
||||
(r"America/Moncton", 174922..176415),
|
||||
(r"America/Monterrey", 74688..75397),
|
||||
(r"America/Montevideo", 128417..129386),
|
||||
(r"America/Montreal", 180978..182695),
|
||||
(r"America/Montreal", 181266..182983),
|
||||
(r"America/Montserrat", 9872..10049),
|
||||
(r"America/Nassau", 180978..182695),
|
||||
(r"America/New_York", 182695..184439),
|
||||
(r"America/Nipigon", 180978..182695),
|
||||
(r"America/Nassau", 181266..182983),
|
||||
(r"America/New_York", 182983..184727),
|
||||
(r"America/Nipigon", 181266..182983),
|
||||
(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", 191818..192783),
|
||||
(r"America/Nuuk", 192106..193071),
|
||||
(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", 157179..158397),
|
||||
(r"America/Rainy_River", 162167..163461),
|
||||
(r"America/Punta_Arenas", 156100..157318),
|
||||
(r"America/Rainy_River", 161088..162382),
|
||||
(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", 149270..150349),
|
||||
(r"America/Santa_Isabel", 166393..167760),
|
||||
(r"America/Santarem", 27453..27862),
|
||||
(r"America/Santiago", 196015..197369),
|
||||
(r"America/Santiago", 196303..197657),
|
||||
(r"America/Santo_Domingo", 19436..19753),
|
||||
(r"America/Sao_Paulo", 137116..138068),
|
||||
(r"America/Scoresbysund", 192783..193767),
|
||||
(r"America/Scoresbysund", 193071..194055),
|
||||
(r"America/Shiprock", 147183..148225),
|
||||
(r"America/Sitka", 119844..120800),
|
||||
(r"America/St_Barthelemy", 9872..10049),
|
||||
(r"America/St_Johns", 188019..189897),
|
||||
(r"America/St_Johns", 188307..190185),
|
||||
(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", 180978..182695),
|
||||
(r"America/Tijuana", 149270..150349),
|
||||
(r"America/Toronto", 180978..182695),
|
||||
(r"America/Thunder_Bay", 181266..182983),
|
||||
(r"America/Tijuana", 166393..167760),
|
||||
(r"America/Toronto", 181266..182983),
|
||||
(r"America/Tortola", 9872..10049),
|
||||
(r"America/Vancouver", 164770..166100),
|
||||
(r"America/Vancouver", 163691..165021),
|
||||
(r"America/Virgin", 9872..10049),
|
||||
(r"America/Whitehorse", 141012..142041),
|
||||
(r"America/Winnipeg", 162167..163461),
|
||||
(r"America/Winnipeg", 161088..162382),
|
||||
(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", 159639..160873),
|
||||
(r"Asia/Damascus", 158560..159794),
|
||||
(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", 197369..200319),
|
||||
(r"Asia/Gaza", 197657..200607),
|
||||
(r"Asia/Harbin", 25461..25854),
|
||||
(r"Asia/Hebron", 200319..203287),
|
||||
(r"Asia/Hebron", 200607..203575),
|
||||
(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", 154759..155959),
|
||||
(r"Asia/Istanbul", 153680..154880),
|
||||
(r"Asia/Jakarta", 14856..15104),
|
||||
(r"Asia/Jayapura", 8480..8651),
|
||||
(r"Asia/Jerusalem", 193767..194841),
|
||||
(r"Asia/Jerusalem", 194055..195129),
|
||||
(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", 193767..194841),
|
||||
(r"Asia/Tel_Aviv", 194055..195129),
|
||||
(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", 168834..170235),
|
||||
(r"Atlantic/Azores", 169122..170523),
|
||||
(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", 166100..167472),
|
||||
(r"Atlantic/Madeira", 165021..166393),
|
||||
(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", 179306..180978),
|
||||
(r"Canada/Central", 162167..163461),
|
||||
(r"Canada/Eastern", 180978..182695),
|
||||
(r"Canada/Atlantic", 179594..181266),
|
||||
(r"Canada/Central", 161088..162382),
|
||||
(r"Canada/Eastern", 181266..182983),
|
||||
(r"Canada/Mountain", 133167..134137),
|
||||
(r"Canada/Newfoundland", 188019..189897),
|
||||
(r"Canada/Pacific", 164770..166100),
|
||||
(r"Canada/Newfoundland", 188307..190185),
|
||||
(r"Canada/Pacific", 163691..165021),
|
||||
(r"Canada/Saskatchewan", 54642..55280),
|
||||
(r"Canada/Yukon", 141012..142041),
|
||||
(r"CET", 151454..152557),
|
||||
(r"Chile/Continental", 196015..197369),
|
||||
(r"Chile/EasterIsland", 194841..196015),
|
||||
(r"CST6CDT", 184439..186193),
|
||||
(r"Cuba", 153642..154759),
|
||||
(r"CET", 150375..151478),
|
||||
(r"Chile/Continental", 196303..197657),
|
||||
(r"Chile/EasterIsland", 195129..196303),
|
||||
(r"CST6CDT", 184727..186481),
|
||||
(r"Cuba", 152563..153680),
|
||||
(r"EET", 57918..58600),
|
||||
(r"Egypt", 163461..164770),
|
||||
(r"Eire", 173146..174634),
|
||||
(r"Egypt", 162382..163691),
|
||||
(r"Eire", 173434..174922),
|
||||
(r"EST", 5647..5796),
|
||||
(r"EST5EDT", 182695..184439),
|
||||
(r"EST5EDT", 182983..184727),
|
||||
(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", 151454..152557),
|
||||
(r"Europe/Amsterdam", 150375..151478),
|
||||
(r"Europe/Andorra", 23884..24273),
|
||||
(r"Europe/Astrakhan", 81920..82646),
|
||||
(r"Europe/Athens", 57918..58600),
|
||||
(r"Europe/Belfast", 177707..179306),
|
||||
(r"Europe/Belfast", 177995..179594),
|
||||
(r"Europe/Belgrade", 34563..35041),
|
||||
(r"Europe/Berlin", 63350..64055),
|
||||
(r"Europe/Bratislava", 69009..69732),
|
||||
(r"Europe/Brussels", 151454..152557),
|
||||
(r"Europe/Brussels", 150375..151478),
|
||||
(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", 173146..174634),
|
||||
(r"Europe/Gibraltar", 155959..157179),
|
||||
(r"Europe/Guernsey", 177707..179306),
|
||||
(r"Europe/Dublin", 173434..174922),
|
||||
(r"Europe/Gibraltar", 154880..156100),
|
||||
(r"Europe/Guernsey", 177995..179594),
|
||||
(r"Europe/Helsinki", 32688..33169),
|
||||
(r"Europe/Isle_of_Man", 177707..179306),
|
||||
(r"Europe/Istanbul", 154759..155959),
|
||||
(r"Europe/Jersey", 177707..179306),
|
||||
(r"Europe/Isle_of_Man", 177995..179594),
|
||||
(r"Europe/Istanbul", 153680..154880),
|
||||
(r"Europe/Jersey", 177995..179594),
|
||||
(r"Europe/Kaliningrad", 113459..114363),
|
||||
(r"Europe/Kiev", 38043..38601),
|
||||
(r"Europe/Kirov", 78274..79009),
|
||||
(r"Europe/Kyiv", 38043..38601),
|
||||
(r"Europe/Lisbon", 170235..171698),
|
||||
(r"Europe/Lisbon", 170523..171986),
|
||||
(r"Europe/Ljubljana", 34563..35041),
|
||||
(r"Europe/London", 177707..179306),
|
||||
(r"Europe/Luxembourg", 151454..152557),
|
||||
(r"Europe/London", 177995..179594),
|
||||
(r"Europe/Luxembourg", 150375..151478),
|
||||
(r"Europe/Madrid", 111663..112560),
|
||||
(r"Europe/Malta", 126549..127477),
|
||||
(r"Europe/Mariehamn", 32688..33169),
|
||||
(r"Europe/Minsk", 99205..100013),
|
||||
(r"Europe/Monaco", 150349..151454),
|
||||
(r"Europe/Monaco", 149270..150375),
|
||||
(r"Europe/Moscow", 109875..110783),
|
||||
(r"Europe/Nicosia", 44735..45332),
|
||||
(r"Europe/Oslo", 63350..64055),
|
||||
(r"Europe/Paris", 150349..151454),
|
||||
(r"Europe/Paris", 149270..150375),
|
||||
(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", 177707..179306),
|
||||
(r"GB-Eire", 177707..179306),
|
||||
(r"GB", 177995..179594),
|
||||
(r"GB-Eire", 177995..179594),
|
||||
(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", 193767..194841),
|
||||
(r"Israel", 194055..195129),
|
||||
(r"Jamaica", 21737..22076),
|
||||
(r"Japan", 15360..15573),
|
||||
(r"Kwajalein", 12882..13101),
|
||||
(r"Libya", 29570..30001),
|
||||
(r"MET", 151454..152557),
|
||||
(r"Mexico/BajaNorte", 149270..150349),
|
||||
(r"MET", 150375..151478),
|
||||
(r"Mexico/BajaNorte", 166393..167760),
|
||||
(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", 194841..196015),
|
||||
(r"Pacific/Easter", 195129..196303),
|
||||
(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", 170235..171698),
|
||||
(r"Portugal", 170523..171986),
|
||||
(r"PRC", 25461..25854),
|
||||
(r"PST8PDT", 160873..162167),
|
||||
(r"PST8PDT", 159794..161088),
|
||||
(r"ROC", 39103..39614),
|
||||
(r"ROK", 27038..27453),
|
||||
(r"Singapore", 15104..15360),
|
||||
(r"Turkey", 154759..155959),
|
||||
(r"Turkey", 153680..154880),
|
||||
(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", 184439..186193),
|
||||
(r"US/Central", 184727..186481),
|
||||
(r"US/East-Indiana", 36060..36591),
|
||||
(r"US/Eastern", 182695..184439),
|
||||
(r"US/Eastern", 182983..184727),
|
||||
(r"US/Hawaii", 13910..14131),
|
||||
(r"US/Indiana-Starke", 139996..141012),
|
||||
(r"US/Michigan", 112560..113459),
|
||||
(r"US/Mountain", 147183..148225),
|
||||
(r"US/Pacific", 160873..162167),
|
||||
(r"US/Pacific", 159794..161088),
|
||||
(r"US/Samoa", 5197..5343),
|
||||
(r"UTC", 224..335),
|
||||
(r"W-SU", 109875..110783),
|
||||
(r"WET", 170235..171698),
|
||||
(r"WET", 170523..171986),
|
||||
(r"Zulu", 224..335),
|
||||
];
|
||||
|
|
|
|||
4
fuzz/.gitignore
vendored
Normal file
4
fuzz/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
target
|
||||
corpus
|
||||
artifacts
|
||||
coverage
|
||||
244
fuzz/Cargo.lock
generated
Normal file
244
fuzz/Cargo.lock
generated
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
# 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"
|
||||
44
fuzz/Cargo.toml
Normal file
44
fuzz/Cargo.toml
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
[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)'] }
|
||||
50
fuzz/fuzz_targets/rfc2822_parse.rs
Normal file
50
fuzz/fuzz_targets/rfc2822_parse.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
#![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!();
|
||||
48
fuzz/fuzz_targets/shim.rs
Normal file
48
fuzz/fuzz_targets/shim.rs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
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();
|
||||
}
|
||||
};
|
||||
}
|
||||
103
fuzz/fuzz_targets/strtime_parse.rs
Normal file
103
fuzz/fuzz_targets/strtime_parse.rs
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
#![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!();
|
||||
51
fuzz/fuzz_targets/temporal_parse.rs
Normal file
51
fuzz/fuzz_targets/temporal_parse.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
#![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!();
|
||||
|
|
@ -3,7 +3,7 @@ use core::time::Duration as UnsignedDuration;
|
|||
use crate::{
|
||||
civil::{DateTime, Era, ISOWeekDate, Time, Weekday},
|
||||
duration::{Duration, SDuration},
|
||||
error::{err, Error, ErrorContext},
|
||||
error::{civil::Error as E, Error, ErrorContext},
|
||||
fmt::{
|
||||
self,
|
||||
temporal::{DEFAULT_DATETIME_PARSER, DEFAULT_DATETIME_PRINTER},
|
||||
|
|
@ -903,7 +903,9 @@ 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::shared)?,
|
||||
idate
|
||||
.nth_weekday_of_month(nth, weekday)
|
||||
.map_err(Error::itime_range)?,
|
||||
))
|
||||
}
|
||||
|
||||
|
|
@ -1057,7 +1059,7 @@ impl Date {
|
|||
|
||||
let nth = t::SpanWeeks::try_new("nth weekday", nth)?;
|
||||
if nth == C(0) {
|
||||
Err(err!("nth weekday cannot be `0`"))
|
||||
Err(Error::slim_range("nth weekday"))
|
||||
} else if nth > C(0) {
|
||||
let nth = nth.max(C(1));
|
||||
let weekday_diff = weekday.since_ranged(self.weekday().next());
|
||||
|
|
@ -1515,14 +1517,8 @@ impl Date {
|
|||
-1 => self.yesterday(),
|
||||
1 => self.tomorrow(),
|
||||
days => {
|
||||
let days = UnixEpochDay::try_new("days", days).with_context(
|
||||
|| {
|
||||
err!(
|
||||
"{days} computed from duration {duration:?} \
|
||||
overflows Jiff's datetime limits",
|
||||
)
|
||||
},
|
||||
)?;
|
||||
let days = UnixEpochDay::try_new("days", days)
|
||||
.context(E::OverflowDaysDuration)?;
|
||||
let days =
|
||||
self.to_unix_epoch_day().try_checked_add("days", days)?;
|
||||
Ok(Date::from_unix_epoch_day(days))
|
||||
|
|
@ -2941,11 +2937,9 @@ impl DateDifference {
|
|||
//
|
||||
// NOTE: I take the above back. It's actually possible for the
|
||||
// months component to overflow when largest=month.
|
||||
return Err(err!(
|
||||
"rounding the span between two dates must use days \
|
||||
or bigger for its units, but found {units}",
|
||||
units = largest.plural(),
|
||||
));
|
||||
return Err(Error::from(E::RoundMustUseDaysOrBigger {
|
||||
unit: largest,
|
||||
}));
|
||||
}
|
||||
if largest <= Unit::Week {
|
||||
let mut weeks = t::SpanWeeks::rfrom(C(0));
|
||||
|
|
@ -3197,13 +3191,13 @@ impl DateWith {
|
|||
Some(DateWithDay::OfYear(day)) => {
|
||||
let year = year.get_unchecked();
|
||||
let idate = IDate::from_day_of_year(year, day)
|
||||
.map_err(Error::shared)?;
|
||||
.map_err(Error::itime_range)?;
|
||||
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::shared)?;
|
||||
.map_err(Error::itime_range)?;
|
||||
return Ok(Date::from_idate_const(idate));
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use crate::{
|
|||
datetime, Date, DateWith, Era, ISOWeekDate, Time, TimeWith, Weekday,
|
||||
},
|
||||
duration::{Duration, SDuration},
|
||||
error::{err, Error, ErrorContext},
|
||||
error::{civil::Error as E, Error, ErrorContext},
|
||||
fmt::{
|
||||
self,
|
||||
temporal::{self, DEFAULT_DATETIME_PARSER},
|
||||
|
|
@ -1695,25 +1695,18 @@ impl DateTime {
|
|||
{
|
||||
(true, true) => Ok(self),
|
||||
(false, true) => {
|
||||
let new_date =
|
||||
old_date.checked_add(span).with_context(|| {
|
||||
err!("failed to add {span} to {old_date}")
|
||||
})?;
|
||||
let new_date = old_date
|
||||
.checked_add(span)
|
||||
.context(E::FailedAddSpanDate)?;
|
||||
Ok(DateTime::from_parts(new_date, old_time))
|
||||
}
|
||||
(true, false) => {
|
||||
let (new_time, leftovers) =
|
||||
old_time.overflowing_add(span).with_context(|| {
|
||||
err!("failed to add {span} to {old_time}")
|
||||
})?;
|
||||
let new_date =
|
||||
old_date.checked_add(leftovers).with_context(|| {
|
||||
err!(
|
||||
"failed to add overflowing span, {leftovers}, \
|
||||
from adding {span} to {old_time}, \
|
||||
to {old_date}",
|
||||
)
|
||||
})?;
|
||||
let (new_time, leftovers) = old_time
|
||||
.overflowing_add(span)
|
||||
.context(E::FailedAddSpanTime)?;
|
||||
let new_date = old_date
|
||||
.checked_add(leftovers)
|
||||
.context(E::FailedAddSpanOverflowing)?;
|
||||
Ok(DateTime::from_parts(new_date, new_time))
|
||||
}
|
||||
(false, false) => self.checked_add_span_general(&span),
|
||||
|
|
@ -1727,20 +1720,14 @@ impl DateTime {
|
|||
let span_date = span.without_lower(Unit::Day);
|
||||
let span_time = span.only_lower(Unit::Day);
|
||||
|
||||
let (new_time, leftovers) =
|
||||
old_time.overflowing_add(span_time).with_context(|| {
|
||||
err!("failed to add {span_time} to {old_time}")
|
||||
})?;
|
||||
let new_date = old_date.checked_add(span_date).with_context(|| {
|
||||
err!("failed to add {span_date} to {old_date}")
|
||||
})?;
|
||||
let new_date = new_date.checked_add(leftovers).with_context(|| {
|
||||
err!(
|
||||
"failed to add overflowing span, {leftovers}, \
|
||||
from adding {span_time} to {old_time}, \
|
||||
to {new_date}",
|
||||
)
|
||||
})?;
|
||||
let (new_time, leftovers) = old_time
|
||||
.overflowing_add(span_time)
|
||||
.context(E::FailedAddSpanTime)?;
|
||||
let new_date =
|
||||
old_date.checked_add(span_date).context(E::FailedAddSpanDate)?;
|
||||
let new_date = new_date
|
||||
.checked_add(leftovers)
|
||||
.context(E::FailedAddSpanOverflowing)?;
|
||||
Ok(DateTime::from_parts(new_date, new_time))
|
||||
}
|
||||
|
||||
|
|
@ -1751,13 +1738,9 @@ impl DateTime {
|
|||
) -> Result<DateTime, Error> {
|
||||
let (date, time) = (self.date(), self.time());
|
||||
let (new_time, leftovers) = time.overflowing_add_duration(duration)?;
|
||||
let new_date = date.checked_add(leftovers).with_context(|| {
|
||||
err!(
|
||||
"failed to add overflowing signed duration, {leftovers:?}, \
|
||||
from adding {duration:?} to {time},
|
||||
to {date}",
|
||||
)
|
||||
})?;
|
||||
let new_date = date
|
||||
.checked_add(leftovers)
|
||||
.context(E::FailedAddDurationOverflowing)?;
|
||||
Ok(DateTime::from_parts(new_date, new_time))
|
||||
}
|
||||
|
||||
|
|
@ -3552,9 +3535,10 @@ impl DateTimeRound {
|
|||
// it for good reasons.
|
||||
match self.smallest {
|
||||
Unit::Year | Unit::Month | Unit::Week => {
|
||||
return Err(err!(
|
||||
"rounding datetimes does not support {unit}",
|
||||
unit = self.smallest.plural()
|
||||
return Err(Error::from(
|
||||
crate::error::util::RoundingIncrementError::Unsupported {
|
||||
unit: self.smallest,
|
||||
},
|
||||
));
|
||||
}
|
||||
// We don't do any rounding in this case, so just bail now.
|
||||
|
|
@ -3592,9 +3576,7 @@ impl DateTimeRound {
|
|||
// supported datetimes.
|
||||
let end = start
|
||||
.checked_add(Span::new().days_ranged(days_len))
|
||||
.with_context(|| {
|
||||
err!("adding {days_len} days to {start} failed")
|
||||
})?;
|
||||
.context(E::FailedAddDays)?;
|
||||
Ok(DateTime::from_parts(end, time))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use crate::{
|
||||
civil::{Date, DateTime, Weekday},
|
||||
error::{err, Error},
|
||||
error::{civil::Error as E, Error},
|
||||
fmt::temporal::{DEFAULT_DATETIME_PARSER, DEFAULT_DATETIME_PRINTER},
|
||||
util::{
|
||||
rangeint::RInto,
|
||||
t::{self, ISOWeek, ISOYear, C},
|
||||
|
|
@ -35,6 +36,51 @@ 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
|
||||
|
|
@ -665,9 +711,7 @@ impl ISOWeekDate {
|
|||
debug_assert_eq!(t::Year::MIN, ISOYear::MIN);
|
||||
debug_assert_eq!(t::Year::MAX, ISOYear::MAX);
|
||||
if week == C(53) && !is_long_year(year) {
|
||||
return Err(err!(
|
||||
"ISO week number `{week}` is invalid for year `{year}`"
|
||||
));
|
||||
return Err(Error::from(E::InvalidISOWeekNumber));
|
||||
}
|
||||
// And also, the maximum Date constrains what we can utter with
|
||||
// ISOWeekDate so that we can preserve infallible conversions between
|
||||
|
|
@ -747,6 +791,24 @@ 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 {
|
||||
|
|
@ -808,6 +870,60 @@ 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 {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use core::time::Duration as UnsignedDuration;
|
|||
use crate::{
|
||||
civil::{Date, DateTime},
|
||||
duration::{Duration, SDuration},
|
||||
error::{err, Error, ErrorContext},
|
||||
error::{civil::Error as E, Error, ErrorContext},
|
||||
fmt::{
|
||||
self,
|
||||
temporal::{self, DEFAULT_DATETIME_PARSER},
|
||||
|
|
@ -950,7 +950,6 @@ impl Time {
|
|||
self,
|
||||
duration: SignedDuration,
|
||||
) -> Result<Time, Error> {
|
||||
let original = duration;
|
||||
let start = t::NoUnits128::rfrom(self.to_nanosecond());
|
||||
let duration = t::NoUnits128::new_unchecked(duration.as_nanos());
|
||||
// This can never fail because the maximum duration fits into a
|
||||
|
|
@ -958,15 +957,7 @@ impl Time {
|
|||
// integer can never overflow a 128-bit integer.
|
||||
let end = start.try_checked_add("nanoseconds", duration).unwrap();
|
||||
let end = CivilDayNanosecond::try_rfrom("nanoseconds", end)
|
||||
.with_context(|| {
|
||||
err!(
|
||||
"adding signed duration {duration:?}, equal to
|
||||
{nanos} nanoseconds, to {time} overflowed",
|
||||
duration = original,
|
||||
nanos = original.as_nanos(),
|
||||
time = self,
|
||||
)
|
||||
})?;
|
||||
.context(E::OverflowTimeNanoseconds)?;
|
||||
Ok(Time::from_nanosecond(end))
|
||||
}
|
||||
|
||||
|
|
@ -2603,11 +2594,9 @@ impl TimeDifference {
|
|||
}
|
||||
let largest = self.round.get_largest().unwrap_or(Unit::Hour);
|
||||
if largest > Unit::Hour {
|
||||
return Err(err!(
|
||||
"rounding the span between two times must use hours \
|
||||
or smaller for its units, but found {units}",
|
||||
units = largest.plural(),
|
||||
));
|
||||
return Err(Error::from(E::RoundMustUseHoursOrSmaller {
|
||||
unit: largest,
|
||||
}));
|
||||
}
|
||||
let start = t1.to_nanosecond();
|
||||
let end = t2.to_nanosecond();
|
||||
|
|
@ -3012,22 +3001,13 @@ impl TimeWith {
|
|||
None => self.original.subsec_nanosecond_ranged(),
|
||||
Some(subsec_nanosecond) => {
|
||||
if self.millisecond.is_some() {
|
||||
return Err(err!(
|
||||
"cannot set both TimeWith::millisecond \
|
||||
and TimeWith::subsec_nanosecond",
|
||||
));
|
||||
return Err(Error::from(E::IllegalTimeWithMillisecond));
|
||||
}
|
||||
if self.microsecond.is_some() {
|
||||
return Err(err!(
|
||||
"cannot set both TimeWith::microsecond \
|
||||
and TimeWith::subsec_nanosecond",
|
||||
));
|
||||
return Err(Error::from(E::IllegalTimeWithMicrosecond));
|
||||
}
|
||||
if self.nanosecond.is_some() {
|
||||
return Err(err!(
|
||||
"cannot set both TimeWith::nanosecond \
|
||||
and TimeWith::subsec_nanosecond",
|
||||
));
|
||||
return Err(Error::from(E::IllegalTimeWithNanosecond));
|
||||
}
|
||||
SubsecNanosecond::try_new(
|
||||
"subsec_nanosecond",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use core::time::Duration as UnsignedDuration;
|
||||
|
||||
use crate::{
|
||||
error::{err, ErrorContext},
|
||||
error::{duration::Error as E, ErrorContext},
|
||||
Error, SignedDuration, Span,
|
||||
};
|
||||
|
||||
|
|
@ -24,12 +24,8 @@ impl Duration {
|
|||
Duration::Span(span) => Ok(SDuration::Span(span)),
|
||||
Duration::Signed(sdur) => Ok(SDuration::Absolute(sdur)),
|
||||
Duration::Unsigned(udur) => {
|
||||
let sdur =
|
||||
SignedDuration::try_from(udur).with_context(|| {
|
||||
err!(
|
||||
"unsigned duration {udur:?} exceeds Jiff's limits"
|
||||
)
|
||||
})?;
|
||||
let sdur = SignedDuration::try_from(udur)
|
||||
.context(E::RangeUnsignedDuration)?;
|
||||
Ok(SDuration::Absolute(sdur))
|
||||
}
|
||||
}
|
||||
|
|
@ -91,9 +87,8 @@ impl Duration {
|
|||
// Otherwise, this is the only failure point in this entire
|
||||
// routine. And specifically, we fail here in precisely
|
||||
// the cases where `udur.as_secs() > |i64::MIN|`.
|
||||
-SignedDuration::try_from(udur).with_context(|| {
|
||||
err!("failed to negate unsigned duration {udur:?}")
|
||||
})?
|
||||
-SignedDuration::try_from(udur)
|
||||
.context(E::FailedNegateUnsignedDuration)?
|
||||
};
|
||||
Ok(Duration::Signed(sdur))
|
||||
}
|
||||
|
|
|
|||
84
src/error/civil.rs
Normal file
84
src/error/civil.rs
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
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(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/error/duration.rs
Normal file
36
src/error/duration.rs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
use crate::error;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
FailedNegateUnsignedDuration,
|
||||
RangeUnsignedDuration,
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::Duration(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
FailedNegateUnsignedDuration => {
|
||||
f.write_str("failed to negate unsigned duration")
|
||||
}
|
||||
RangeUnsignedDuration => {
|
||||
f.write_str("unsigned duration exceeds Jiff's limits")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
82
src/error/fmt/friendly.rs
Normal file
82
src/error/fmt/friendly.rs
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
use crate::{error, util::escape};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
Empty,
|
||||
ExpectedColonAfterMinute,
|
||||
ExpectedIntegerAfterSign,
|
||||
ExpectedMinuteAfterHour,
|
||||
ExpectedOneMoreUnitAfterComma,
|
||||
ExpectedOneSign,
|
||||
ExpectedSecondAfterMinute,
|
||||
ExpectedUnitSuffix,
|
||||
ExpectedWhitespaceAfterComma { byte: u8 },
|
||||
ExpectedWhitespaceAfterCommaEndOfInput,
|
||||
Failed,
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::FmtFriendly(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
Empty => f.write_str("an empty string is not valid"),
|
||||
ExpectedColonAfterMinute => f.write_str(
|
||||
"when parsing the `HH:MM:SS` format, \
|
||||
expected to parse `:` following minute",
|
||||
),
|
||||
ExpectedIntegerAfterSign => f.write_str(
|
||||
"expected duration to start \
|
||||
with a unit value (a decimal integer) after an \
|
||||
optional sign, but no integer was found",
|
||||
),
|
||||
ExpectedMinuteAfterHour => f.write_str(
|
||||
"when parsing the `HH:MM:SS` format, \
|
||||
expected to parse minute following hour",
|
||||
),
|
||||
ExpectedOneMoreUnitAfterComma => f.write_str(
|
||||
"found comma at the end of duration, \
|
||||
but a comma indicates at least one more \
|
||||
unit follows",
|
||||
),
|
||||
ExpectedOneSign => f.write_str(
|
||||
"expected to find either a prefix sign (+/-) or \
|
||||
a suffix sign (`ago`), but found both",
|
||||
),
|
||||
ExpectedSecondAfterMinute => f.write_str(
|
||||
"when parsing the `HH:MM:SS` format, \
|
||||
expected to parse second following minute",
|
||||
),
|
||||
ExpectedUnitSuffix => f.write_str(
|
||||
"expected to find unit designator suffix \
|
||||
(e.g., `years` or `secs`) after parsing \
|
||||
integer",
|
||||
),
|
||||
ExpectedWhitespaceAfterComma { byte } => write!(
|
||||
f,
|
||||
"expected whitespace after comma, but found `{byte}`",
|
||||
byte = escape::Byte(byte),
|
||||
),
|
||||
ExpectedWhitespaceAfterCommaEndOfInput => f.write_str(
|
||||
"expected whitespace after comma, but found end of input",
|
||||
),
|
||||
Failed => f.write_str(
|
||||
"failed to parse input in the \"friendly\" duration format",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
87
src/error/fmt/mod.rs
Normal file
87
src/error/fmt/mod.rs
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
use crate::{error, util::escape};
|
||||
|
||||
pub(crate) mod friendly;
|
||||
pub(crate) mod offset;
|
||||
pub(crate) mod rfc2822;
|
||||
pub(crate) mod rfc9557;
|
||||
pub(crate) mod strtime;
|
||||
pub(crate) mod temporal;
|
||||
pub(crate) mod util;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
HybridDurationEmpty,
|
||||
HybridDurationPrefix {
|
||||
sign: u8,
|
||||
},
|
||||
IntoFull {
|
||||
#[cfg(feature = "alloc")]
|
||||
value: alloc::boxed::Box<str>,
|
||||
#[cfg(feature = "alloc")]
|
||||
unparsed: alloc::boxed::Box<[u8]>,
|
||||
},
|
||||
StdFmtWriteAdapter,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub(crate) fn into_full_error(
|
||||
_value: &dyn core::fmt::Display,
|
||||
_unparsed: &[u8],
|
||||
) -> Error {
|
||||
Error::IntoFull {
|
||||
#[cfg(feature = "alloc")]
|
||||
value: alloc::string::ToString::to_string(_value).into(),
|
||||
#[cfg(feature = "alloc")]
|
||||
unparsed: _unparsed.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::Fmt(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
HybridDurationEmpty => f.write_str(
|
||||
"an empty string is not a valid duration in either \
|
||||
the ISO 8601 format or Jiff's \"friendly\" format",
|
||||
),
|
||||
HybridDurationPrefix { sign } => write!(
|
||||
f,
|
||||
"found nothing after sign `{sign}`, \
|
||||
which is not a valid duration in either \
|
||||
the ISO 8601 format or Jiff's \"friendly\" format",
|
||||
sign = escape::Byte(sign),
|
||||
),
|
||||
#[cfg(not(feature = "alloc"))]
|
||||
IntoFull { .. } => f.write_str(
|
||||
"parsed value, but unparsed input remains \
|
||||
(expected no unparsed input)",
|
||||
),
|
||||
#[cfg(feature = "alloc")]
|
||||
IntoFull { ref value, ref unparsed } => write!(
|
||||
f,
|
||||
"parsed value '{value}', but unparsed input {unparsed:?} \
|
||||
remains (expected no unparsed input)",
|
||||
unparsed = escape::Bytes(unparsed),
|
||||
),
|
||||
StdFmtWriteAdapter => {
|
||||
f.write_str("an error occurred when formatting an argument")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
153
src/error/fmt/offset.rs
Normal file
153
src/error/fmt/offset.rs
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
use crate::{error, util::escape};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
ColonAfterHours,
|
||||
EndOfInput,
|
||||
EndOfInputHour,
|
||||
EndOfInputMinute,
|
||||
EndOfInputNumeric,
|
||||
EndOfInputSecond,
|
||||
InvalidHours,
|
||||
InvalidMinutes,
|
||||
InvalidSeconds,
|
||||
InvalidSecondsFractional,
|
||||
InvalidSign,
|
||||
InvalidSignPlusOrMinus,
|
||||
MissingMinuteAfterHour,
|
||||
MissingSecondAfterMinute,
|
||||
NoColonAfterHours,
|
||||
ParseHours,
|
||||
ParseMinutes,
|
||||
ParseSeconds,
|
||||
PrecisionLoss,
|
||||
RangeHours,
|
||||
RangeMinutes,
|
||||
RangeSeconds,
|
||||
SeparatorAfterHours,
|
||||
SeparatorAfterMinutes,
|
||||
SubminutePrecisionNotEnabled,
|
||||
SubsecondPrecisionNotEnabled,
|
||||
UnexpectedLetterOffsetNoZulu(u8),
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::FmtOffset(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
ColonAfterHours => f.write_str(
|
||||
"parsed hour component of time zone offset, \
|
||||
but found colon after hours which is not allowed",
|
||||
),
|
||||
EndOfInput => {
|
||||
f.write_str("expected UTC offset, but found end of input")
|
||||
}
|
||||
EndOfInputHour => f.write_str(
|
||||
"expected two digit hour after sign, but found end of input",
|
||||
),
|
||||
EndOfInputMinute => f.write_str(
|
||||
"expected two digit minute after hours, \
|
||||
but found end of input",
|
||||
),
|
||||
EndOfInputNumeric => f.write_str(
|
||||
"expected UTC numeric offset, but found end of input",
|
||||
),
|
||||
EndOfInputSecond => f.write_str(
|
||||
"expected two digit second after minutes, \
|
||||
but found end of input",
|
||||
),
|
||||
InvalidHours => {
|
||||
f.write_str("failed to parse hours in UTC numeric offset")
|
||||
}
|
||||
InvalidMinutes => {
|
||||
f.write_str("failed to parse minutes in UTC numeric offset")
|
||||
}
|
||||
InvalidSeconds => {
|
||||
f.write_str("failed to parse seconds in UTC numeric offset")
|
||||
}
|
||||
InvalidSecondsFractional => f.write_str(
|
||||
"failed to parse fractional seconds in UTC numeric offset",
|
||||
),
|
||||
InvalidSign => {
|
||||
f.write_str("failed to parse sign in UTC numeric offset")
|
||||
}
|
||||
InvalidSignPlusOrMinus => f.write_str(
|
||||
"expected `+` or `-` sign at start of UTC numeric offset",
|
||||
),
|
||||
MissingMinuteAfterHour => f.write_str(
|
||||
"parsed hour component of time zone offset, \
|
||||
but could not find required minute component",
|
||||
),
|
||||
MissingSecondAfterMinute => f.write_str(
|
||||
"parsed hour and minute components of time zone offset, \
|
||||
but could not find required second component",
|
||||
),
|
||||
NoColonAfterHours => f.write_str(
|
||||
"parsed hour component of time zone offset, \
|
||||
but could not find required colon separator",
|
||||
),
|
||||
ParseHours => f.write_str(
|
||||
"failed to parse hours (requires a two digit integer)",
|
||||
),
|
||||
ParseMinutes => f.write_str(
|
||||
"failed to parse minutes (requires a two digit integer)",
|
||||
),
|
||||
ParseSeconds => f.write_str(
|
||||
"failed to parse seconds (requires a two digit integer)",
|
||||
),
|
||||
PrecisionLoss => f.write_str(
|
||||
"due to precision loss, offset is \
|
||||
rounded to a value that is out of bounds",
|
||||
),
|
||||
RangeHours => {
|
||||
f.write_str("hour in time zone offset is out of range")
|
||||
}
|
||||
RangeMinutes => {
|
||||
f.write_str("minute in time zone offset is out of range")
|
||||
}
|
||||
RangeSeconds => {
|
||||
f.write_str("second in time zone offset is out of range")
|
||||
}
|
||||
SeparatorAfterHours => f.write_str(
|
||||
"failed to parse separator after hours in \
|
||||
UTC numeric offset",
|
||||
),
|
||||
SeparatorAfterMinutes => f.write_str(
|
||||
"failed to parse separator after minutes in \
|
||||
UTC numeric offset",
|
||||
),
|
||||
SubminutePrecisionNotEnabled => f.write_str(
|
||||
"subminute precision for UTC numeric offset \
|
||||
is not enabled in this context (must provide only \
|
||||
integral minutes)",
|
||||
),
|
||||
SubsecondPrecisionNotEnabled => f.write_str(
|
||||
"subsecond precision for UTC numeric offset \
|
||||
is not enabled in this context (must provide only \
|
||||
integral minutes or seconds)",
|
||||
),
|
||||
UnexpectedLetterOffsetNoZulu(byte) => write!(
|
||||
f,
|
||||
"found `{z}` where a numeric UTC offset \
|
||||
was expected (this context does not permit \
|
||||
the Zulu offset)",
|
||||
z = escape::Byte(byte),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
231
src/error/fmt/rfc2822.rs
Normal file
231
src/error/fmt/rfc2822.rs
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
use crate::{civil::Weekday, error, util::escape};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
CommentClosingParenWithoutOpen,
|
||||
CommentOpeningParenWithoutClose,
|
||||
CommentTooManyNestedParens,
|
||||
EndOfInputDay,
|
||||
Empty,
|
||||
EmptyAfterWhitespace,
|
||||
EndOfInputComma,
|
||||
EndOfInputHour,
|
||||
EndOfInputMinute,
|
||||
EndOfInputMonth,
|
||||
EndOfInputOffset,
|
||||
EndOfInputSecond,
|
||||
EndOfInputTimeSeparator,
|
||||
FailedTimestamp,
|
||||
FailedZoned,
|
||||
InconsistentWeekday { parsed: Weekday, from_date: Weekday },
|
||||
InvalidDate,
|
||||
InvalidHour,
|
||||
InvalidMinute,
|
||||
InvalidMonth,
|
||||
InvalidObsoleteOffset,
|
||||
InvalidOffsetHour,
|
||||
InvalidOffsetMinute,
|
||||
InvalidSecond,
|
||||
InvalidWeekday { got_non_digit: u8 },
|
||||
InvalidYear,
|
||||
NegativeYear,
|
||||
ParseDay,
|
||||
ParseHour,
|
||||
ParseMinute,
|
||||
ParseOffsetHour,
|
||||
ParseOffsetMinute,
|
||||
ParseSecond,
|
||||
ParseYear,
|
||||
TooShortMonth { len: u8 },
|
||||
TooShortOffset,
|
||||
TooShortWeekday { got_non_digit: u8, len: u8 },
|
||||
TooShortYear { len: u8 },
|
||||
UnexpectedByteComma { byte: u8 },
|
||||
UnexpectedByteTimeSeparator { byte: u8 },
|
||||
WhitespaceAfterDay,
|
||||
WhitespaceAfterMonth,
|
||||
WhitespaceAfterTime,
|
||||
WhitespaceAfterTimeForObsoleteOffset,
|
||||
WhitespaceAfterYear,
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::FmtRfc2822(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
CommentClosingParenWithoutOpen => f.write_str(
|
||||
"found closing parenthesis in comment with \
|
||||
no matching opening parenthesis",
|
||||
),
|
||||
CommentOpeningParenWithoutClose => f.write_str(
|
||||
"found opening parenthesis in comment with \
|
||||
no matching closing parenthesis",
|
||||
),
|
||||
CommentTooManyNestedParens => {
|
||||
f.write_str("found too many nested parenthesis in comment")
|
||||
}
|
||||
Empty => {
|
||||
f.write_str("expected RFC 2822 datetime, but got empty string")
|
||||
}
|
||||
EmptyAfterWhitespace => f.write_str(
|
||||
"expected RFC 2822 datetime, but got empty string \
|
||||
after trimming leading whitespace",
|
||||
),
|
||||
EndOfInputComma => f.write_str(
|
||||
"expected comma after parsed weekday in \
|
||||
RFC 2822 datetime, but found end of input instead",
|
||||
),
|
||||
EndOfInputDay => {
|
||||
f.write_str("expected numeric day, but found end of input")
|
||||
}
|
||||
EndOfInputHour => {
|
||||
f.write_str("expected two digit hour, but found end of input")
|
||||
}
|
||||
EndOfInputMinute => f.write_str(
|
||||
"expected two digit minute, but found end of input",
|
||||
),
|
||||
EndOfInputMonth => f.write_str(
|
||||
"expected abbreviated month name, but found end of input",
|
||||
),
|
||||
EndOfInputOffset => f.write_str(
|
||||
"expected sign for time zone offset, \
|
||||
(or a legacy time zone name abbreviation), \
|
||||
but found end of input",
|
||||
),
|
||||
EndOfInputSecond => f.write_str(
|
||||
"expected two digit second, but found end of input",
|
||||
),
|
||||
EndOfInputTimeSeparator => f.write_str(
|
||||
"expected time separator of `:`, but found end of input",
|
||||
),
|
||||
FailedTimestamp => f.write_str(
|
||||
"failed to parse RFC 2822 datetime into Jiff timestamp",
|
||||
),
|
||||
FailedZoned => f.write_str(
|
||||
"failed to parse RFC 2822 datetime into Jiff zoned datetime",
|
||||
),
|
||||
InconsistentWeekday { parsed, from_date } => write!(
|
||||
f,
|
||||
"found parsed weekday of `{parsed:?}`, \
|
||||
but parsed datetime has weekday `{from_date:?}`",
|
||||
),
|
||||
InvalidDate => f.write_str("invalid date"),
|
||||
InvalidHour => f.write_str("invalid hour"),
|
||||
InvalidMinute => f.write_str("invalid minute"),
|
||||
InvalidMonth => f.write_str(
|
||||
"expected abbreviated month name, \
|
||||
but did not recognize a valid abbreviated month name",
|
||||
),
|
||||
InvalidObsoleteOffset => f.write_str(
|
||||
"expected obsolete RFC 2822 time zone abbreviation, \
|
||||
but did not recognize a valid abbreviation",
|
||||
),
|
||||
InvalidOffsetHour => f.write_str("invalid time zone offset hour"),
|
||||
InvalidOffsetMinute => {
|
||||
f.write_str("invalid time zone offset minute")
|
||||
}
|
||||
InvalidSecond => f.write_str("invalid second"),
|
||||
InvalidWeekday { got_non_digit } => write!(
|
||||
f,
|
||||
"expected day at beginning of RFC 2822 datetime \
|
||||
since first non-whitespace byte, `{first}`, \
|
||||
is not a digit, but did not recognize a valid \
|
||||
weekday abbreviation",
|
||||
first = escape::Byte(got_non_digit),
|
||||
),
|
||||
InvalidYear => f.write_str("invalid year"),
|
||||
NegativeYear => f.write_str(
|
||||
"datetime has negative year, \
|
||||
which cannot be formatted with RFC 2822",
|
||||
),
|
||||
ParseDay => f.write_str("failed to parse day"),
|
||||
ParseHour => f.write_str(
|
||||
"failed to parse hour (expects a two digit integer)",
|
||||
),
|
||||
ParseMinute => f.write_str(
|
||||
"failed to parse minute (expects a two digit integer)",
|
||||
),
|
||||
ParseOffsetHour => {
|
||||
f.write_str("failed to parse hours from time zone offset")
|
||||
}
|
||||
ParseOffsetMinute => {
|
||||
f.write_str("failed to parse minutes from time zone offset")
|
||||
}
|
||||
ParseSecond => f.write_str(
|
||||
"failed to parse second (expects a two digit integer)",
|
||||
),
|
||||
ParseYear => f.write_str(
|
||||
"failed to parse year \
|
||||
(expects a two, three or four digit integer)",
|
||||
),
|
||||
TooShortMonth { len } => write!(
|
||||
f,
|
||||
"expected abbreviated month name, but remaining input \
|
||||
is too short (remaining bytes is {len})",
|
||||
),
|
||||
TooShortOffset => write!(
|
||||
f,
|
||||
"expected at least four digits for time zone offset \
|
||||
after sign, but found fewer than four bytes remaining",
|
||||
),
|
||||
TooShortWeekday { got_non_digit, len } => write!(
|
||||
f,
|
||||
"expected day at beginning of RFC 2822 datetime \
|
||||
since first non-whitespace byte, `{first}`, \
|
||||
is not a digit, but given string is too short \
|
||||
(length is {len})",
|
||||
first = escape::Byte(got_non_digit),
|
||||
),
|
||||
TooShortYear { len } => write!(
|
||||
f,
|
||||
"expected at least two ASCII digits for parsing \
|
||||
a year, but only found {len}",
|
||||
),
|
||||
UnexpectedByteComma { byte } => write!(
|
||||
f,
|
||||
"expected comma after parsed weekday in \
|
||||
RFC 2822 datetime, but found `{got}` instead",
|
||||
got = escape::Byte(byte),
|
||||
),
|
||||
UnexpectedByteTimeSeparator { byte } => write!(
|
||||
f,
|
||||
"expected time separator of `:`, but found `{got}`",
|
||||
got = escape::Byte(byte),
|
||||
),
|
||||
WhitespaceAfterDay => {
|
||||
f.write_str("expected whitespace after parsing day")
|
||||
}
|
||||
WhitespaceAfterMonth => f.write_str(
|
||||
"expected whitespace after parsing abbreviated month name",
|
||||
),
|
||||
WhitespaceAfterTime => f.write_str(
|
||||
"expected whitespace after parsing time: \
|
||||
expected at least one whitespace character \
|
||||
(space or tab), but found none",
|
||||
),
|
||||
WhitespaceAfterTimeForObsoleteOffset => f.write_str(
|
||||
"expected obsolete RFC 2822 time zone abbreviation, \
|
||||
but found no remaining non-whitespace characters \
|
||||
after time",
|
||||
),
|
||||
WhitespaceAfterYear => {
|
||||
f.write_str("expected whitespace after parsing year")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
114
src/error/fmt/rfc9557.rs
Normal file
114
src/error/fmt/rfc9557.rs
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
use crate::{error, util::escape};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
EndOfInputAnnotation,
|
||||
EndOfInputAnnotationClose,
|
||||
EndOfInputAnnotationKey,
|
||||
EndOfInputAnnotationSeparator,
|
||||
EndOfInputAnnotationValue,
|
||||
EndOfInputTzAnnotationClose,
|
||||
UnexpectedByteAnnotation { byte: u8 },
|
||||
UnexpectedByteAnnotationClose { byte: u8 },
|
||||
UnexpectedByteAnnotationKey { byte: u8 },
|
||||
UnexpectedByteAnnotationValue { byte: u8 },
|
||||
UnexpectedByteAnnotationSeparator { byte: u8 },
|
||||
UnexpectedByteTzAnnotationClose { byte: u8 },
|
||||
UnexpectedSlashAnnotationSeparator,
|
||||
UnsupportedAnnotationCritical,
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::FmtRfc9557(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
EndOfInputAnnotation => f.write_str(
|
||||
"expected the start of an RFC 9557 annotation or IANA \
|
||||
time zone component name, but found end of input instead",
|
||||
),
|
||||
EndOfInputAnnotationClose => f.write_str(
|
||||
"expected an `]` after parsing an RFC 9557 annotation key \
|
||||
and value, but found end of input instead",
|
||||
),
|
||||
EndOfInputAnnotationKey => f.write_str(
|
||||
"expected the start of an RFC 9557 annotation key, \
|
||||
but found end of input instead",
|
||||
),
|
||||
EndOfInputAnnotationSeparator => f.write_str(
|
||||
"expected an `=` after parsing an RFC 9557 annotation key, \
|
||||
but found end of input instead",
|
||||
),
|
||||
EndOfInputAnnotationValue => f.write_str(
|
||||
"expected the start of an RFC 9557 annotation value, \
|
||||
but found end of input instead",
|
||||
),
|
||||
EndOfInputTzAnnotationClose => f.write_str(
|
||||
"expected an `]` after parsing an RFC 9557 time zone \
|
||||
annotation, but found end of input instead",
|
||||
),
|
||||
UnexpectedByteAnnotation { byte } => write!(
|
||||
f,
|
||||
"expected ASCII alphabetic byte (or underscore or period) \
|
||||
at the start of an RFC 9557 annotation or time zone \
|
||||
component name, but found `{byte}` instead",
|
||||
byte = escape::Byte(byte),
|
||||
),
|
||||
UnexpectedByteAnnotationClose { byte } => write!(
|
||||
f,
|
||||
"expected an `]` after parsing an RFC 9557 annotation key \
|
||||
and value, but found `{byte}` instead",
|
||||
byte = escape::Byte(byte),
|
||||
),
|
||||
UnexpectedByteAnnotationKey { byte } => write!(
|
||||
f,
|
||||
"expected lowercase alphabetic byte (or underscore) \
|
||||
at the start of an RFC 9557 annotation key, \
|
||||
but found `{byte}` instead",
|
||||
byte = escape::Byte(byte),
|
||||
),
|
||||
UnexpectedByteAnnotationValue { byte } => write!(
|
||||
f,
|
||||
"expected alphanumeric ASCII byte \
|
||||
at the start of an RFC 9557 annotation value, \
|
||||
but found `{byte}` instead",
|
||||
byte = escape::Byte(byte),
|
||||
),
|
||||
UnexpectedByteAnnotationSeparator { byte } => write!(
|
||||
f,
|
||||
"expected an `=` after parsing an RFC 9557 annotation \
|
||||
key, but found `{byte}` instead",
|
||||
byte = escape::Byte(byte),
|
||||
),
|
||||
UnexpectedByteTzAnnotationClose { byte } => write!(
|
||||
f,
|
||||
"expected an `]` after parsing an RFC 9557 time zone \
|
||||
annotation, but found `{byte}` instead",
|
||||
byte = escape::Byte(byte),
|
||||
),
|
||||
UnexpectedSlashAnnotationSeparator => f.write_str(
|
||||
"expected an `=` after parsing an RFC 9557 annotation \
|
||||
key, but found `/` instead (time zone annotations must \
|
||||
come first)",
|
||||
),
|
||||
UnsupportedAnnotationCritical => f.write_str(
|
||||
"found unsupported RFC 9557 annotation \
|
||||
with the critical flag (`!`) set",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
517
src/error/fmt/strtime.rs
Normal file
517
src/error/fmt/strtime.rs
Normal file
|
|
@ -0,0 +1,517 @@
|
|||
use crate::{civil::Weekday, error, tz::Offset, util::escape};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
ColonCount {
|
||||
directive: u8,
|
||||
},
|
||||
DirectiveFailure {
|
||||
directive: u8,
|
||||
colons: u8,
|
||||
},
|
||||
DirectiveFailureDot {
|
||||
directive: u8,
|
||||
},
|
||||
ExpectedDirectiveAfterColons,
|
||||
ExpectedDirectiveAfterFlag {
|
||||
flag: u8,
|
||||
},
|
||||
ExpectedDirectiveAfterWidth,
|
||||
FailedStrftime,
|
||||
FailedStrptime,
|
||||
FailedWidth,
|
||||
InvalidDate,
|
||||
InvalidISOWeekDate,
|
||||
InvalidWeekdayMonday {
|
||||
got: Weekday,
|
||||
},
|
||||
InvalidWeekdaySunday {
|
||||
got: Weekday,
|
||||
},
|
||||
MismatchOffset {
|
||||
parsed: Offset,
|
||||
got: Offset,
|
||||
},
|
||||
MismatchWeekday {
|
||||
parsed: Weekday,
|
||||
got: Weekday,
|
||||
},
|
||||
MissingTimeHourForFractional,
|
||||
MissingTimeHourForMinute,
|
||||
MissingTimeHourForSecond,
|
||||
MissingTimeMinuteForFractional,
|
||||
MissingTimeMinuteForSecond,
|
||||
MissingTimeSecondForFractional,
|
||||
RangeTimestamp,
|
||||
RangeWidth,
|
||||
RequiredDateForDateTime,
|
||||
RequiredDateTimeForTimestamp,
|
||||
RequiredDateTimeForZoned,
|
||||
RequiredOffsetForTimestamp,
|
||||
RequiredSomeDayForDate,
|
||||
RequiredTimeForDateTime,
|
||||
RequiredYearForDate,
|
||||
UnconsumedStrptime {
|
||||
#[cfg(feature = "alloc")]
|
||||
remaining: alloc::boxed::Box<[u8]>,
|
||||
},
|
||||
UnexpectedEndAfterDot,
|
||||
UnexpectedEndAfterPercent,
|
||||
UnknownDirectiveAfterDot {
|
||||
directive: u8,
|
||||
},
|
||||
UnknownDirective {
|
||||
directive: u8,
|
||||
},
|
||||
ZonedOffsetOrTz,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub(crate) fn unconsumed(_remaining: &[u8]) -> Error {
|
||||
Error::UnconsumedStrptime {
|
||||
#[cfg(feature = "alloc")]
|
||||
remaining: _remaining.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::FmtStrtime(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
ColonCount { directive } => write!(
|
||||
f,
|
||||
"invalid number of `:` in `%{directive}` directive",
|
||||
directive = escape::Byte(directive),
|
||||
),
|
||||
DirectiveFailure { directive, colons } => write!(
|
||||
f,
|
||||
"%{colons}{directive} failed",
|
||||
colons = escape::RepeatByte { byte: b':', count: colons },
|
||||
directive = escape::Byte(directive),
|
||||
),
|
||||
DirectiveFailureDot { directive } => write!(
|
||||
f,
|
||||
"%.{directive} failed",
|
||||
directive = escape::Byte(directive),
|
||||
),
|
||||
ExpectedDirectiveAfterColons => f.write_str(
|
||||
"expected to find specifier directive after colons, \
|
||||
but found end of format string",
|
||||
),
|
||||
ExpectedDirectiveAfterFlag { flag } => write!(
|
||||
f,
|
||||
"expected to find specifier directive after flag \
|
||||
`{flag}`, but found end of format string",
|
||||
flag = escape::Byte(flag),
|
||||
),
|
||||
ExpectedDirectiveAfterWidth => f.write_str(
|
||||
"expected to find specifier directive after parsed width, \
|
||||
but found end of format string",
|
||||
),
|
||||
FailedStrftime => f.write_str("strftime formatting failed"),
|
||||
FailedStrptime => f.write_str("strptime parsing failed"),
|
||||
FailedWidth => {
|
||||
f.write_str("failed to parse conversion specifier width")
|
||||
}
|
||||
InvalidDate => f.write_str("invalid date"),
|
||||
InvalidISOWeekDate => f.write_str("invalid ISO 8601 week date"),
|
||||
InvalidWeekdayMonday { got } => write!(
|
||||
f,
|
||||
"weekday `{got:?}` is not valid for \
|
||||
Monday based week number",
|
||||
),
|
||||
InvalidWeekdaySunday { got } => write!(
|
||||
f,
|
||||
"weekday `{got:?}` is not valid for \
|
||||
Sunday based week number",
|
||||
),
|
||||
MissingTimeHourForFractional => f.write_str(
|
||||
"parsing format did not include hour directive, \
|
||||
but did include fractional second directive (cannot have \
|
||||
smaller time units with bigger time units missing)",
|
||||
),
|
||||
MissingTimeHourForMinute => f.write_str(
|
||||
"parsing format did not include hour directive, \
|
||||
but did include minute directive (cannot have \
|
||||
smaller time units with bigger time units missing)",
|
||||
),
|
||||
MissingTimeHourForSecond => f.write_str(
|
||||
"parsing format did not include hour directive, \
|
||||
but did include second directive (cannot have \
|
||||
smaller time units with bigger time units missing)",
|
||||
),
|
||||
MissingTimeMinuteForFractional => f.write_str(
|
||||
"parsing format did not include minute directive, \
|
||||
but did include fractional second directive (cannot have \
|
||||
smaller time units with bigger time units missing)",
|
||||
),
|
||||
MissingTimeMinuteForSecond => f.write_str(
|
||||
"parsing format did not include minute directive, \
|
||||
but did include second directive (cannot have \
|
||||
smaller time units with bigger time units missing)",
|
||||
),
|
||||
MissingTimeSecondForFractional => f.write_str(
|
||||
"parsing format did not include second directive, \
|
||||
but did include fractional second directive (cannot have \
|
||||
smaller time units with bigger time units missing)",
|
||||
),
|
||||
MismatchOffset { parsed, got } => write!(
|
||||
f,
|
||||
"parsed time zone offset `{parsed}`, but \
|
||||
offset from timestamp and time zone is `{got}`",
|
||||
),
|
||||
MismatchWeekday { parsed, got } => write!(
|
||||
f,
|
||||
"parsed weekday `{parsed:?}` does not match \
|
||||
weekday `{got:?}` from parsed date",
|
||||
),
|
||||
RangeTimestamp => f.write_str(
|
||||
"parsed datetime and offset, \
|
||||
but combining them into a zoned datetime \
|
||||
is outside Jiff's supported timestamp range",
|
||||
),
|
||||
RangeWidth => write!(
|
||||
f,
|
||||
"parsed width is too big, max is {max}",
|
||||
max = u8::MAX
|
||||
),
|
||||
RequiredDateForDateTime => {
|
||||
f.write_str("date required to parse datetime")
|
||||
}
|
||||
RequiredDateTimeForTimestamp => {
|
||||
f.write_str("datetime required to parse timestamp")
|
||||
}
|
||||
RequiredDateTimeForZoned => {
|
||||
f.write_str("datetime required to parse zoned datetime")
|
||||
}
|
||||
RequiredOffsetForTimestamp => {
|
||||
f.write_str("offset required to parse timestamp")
|
||||
}
|
||||
RequiredSomeDayForDate => f.write_str(
|
||||
"a month/day, day-of-year or week date must be \
|
||||
present to create a date, but none were found",
|
||||
),
|
||||
RequiredTimeForDateTime => {
|
||||
f.write_str("time required to parse datetime")
|
||||
}
|
||||
RequiredYearForDate => f.write_str("year required to parse date"),
|
||||
#[cfg(feature = "alloc")]
|
||||
UnconsumedStrptime { ref remaining } => write!(
|
||||
f,
|
||||
"strptime expects to consume the entire input, but \
|
||||
`{remaining}` remains unparsed",
|
||||
remaining = escape::Bytes(remaining),
|
||||
),
|
||||
#[cfg(not(feature = "alloc"))]
|
||||
UnconsumedStrptime {} => f.write_str(
|
||||
"strptime expects to consume the entire input, but \
|
||||
there is unparsed input remaining",
|
||||
),
|
||||
UnexpectedEndAfterDot => f.write_str(
|
||||
"invalid format string, expected directive after `%.`",
|
||||
),
|
||||
UnexpectedEndAfterPercent => f.write_str(
|
||||
"invalid format string, expected byte after `%`, \
|
||||
but found end of format string",
|
||||
),
|
||||
UnknownDirective { directive } => write!(
|
||||
f,
|
||||
"found unrecognized specifier directive `{directive}`",
|
||||
directive = escape::Byte(directive),
|
||||
),
|
||||
UnknownDirectiveAfterDot { directive } => write!(
|
||||
f,
|
||||
"found unrecognized specifier directive `{directive}` \
|
||||
following `%.`",
|
||||
directive = escape::Byte(directive),
|
||||
),
|
||||
ZonedOffsetOrTz => f.write_str(
|
||||
"either offset (from `%z`) or IANA time zone identifier \
|
||||
(from `%Q`) is required for parsing zoned datetime",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum ParseError {
|
||||
ExpectedAmPm,
|
||||
ExpectedAmPmTooShort,
|
||||
ExpectedIanaTz,
|
||||
ExpectedIanaTzEndOfInput,
|
||||
ExpectedMonthAbbreviation,
|
||||
ExpectedMonthAbbreviationTooShort,
|
||||
ExpectedWeekdayAbbreviation,
|
||||
ExpectedWeekdayAbbreviationTooShort,
|
||||
ExpectedChoice {
|
||||
available: &'static [&'static [u8]],
|
||||
},
|
||||
ExpectedFractionalDigit,
|
||||
ExpectedMatchLiteralByte {
|
||||
expected: u8,
|
||||
got: u8,
|
||||
},
|
||||
ExpectedMatchLiteralEndOfInput {
|
||||
expected: u8,
|
||||
},
|
||||
ExpectedNonEmpty {
|
||||
directive: u8,
|
||||
},
|
||||
#[cfg(not(feature = "alloc"))]
|
||||
NotAllowedAlloc {
|
||||
directive: u8,
|
||||
colons: u8,
|
||||
},
|
||||
NotAllowedLocaleClockTime,
|
||||
NotAllowedLocaleDate,
|
||||
NotAllowedLocaleDateAndTime,
|
||||
NotAllowedLocaleTwelveHourClockTime,
|
||||
NotAllowedTimeZoneAbbreviation,
|
||||
ParseDay,
|
||||
ParseDayOfYear,
|
||||
ParseCentury,
|
||||
ParseFractionalSeconds,
|
||||
ParseHour,
|
||||
ParseIsoWeekNumber,
|
||||
ParseIsoWeekYear,
|
||||
ParseIsoWeekYearTwoDigit,
|
||||
ParseMinute,
|
||||
ParseMondayWeekNumber,
|
||||
ParseMonth,
|
||||
ParseSecond,
|
||||
ParseSundayWeekNumber,
|
||||
ParseTimestamp,
|
||||
ParseWeekdayNumber,
|
||||
ParseYear,
|
||||
ParseYearTwoDigit,
|
||||
UnknownMonthName,
|
||||
UnknownWeekdayAbbreviation,
|
||||
}
|
||||
|
||||
impl error::IntoError for ParseError {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ParseError> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: ParseError) -> error::Error {
|
||||
error::ErrorKind::FmtStrtimeParse(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for ParseError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::ParseError::*;
|
||||
|
||||
match *self {
|
||||
ExpectedAmPm => f.write_str("expected to find `AM` or `PM`"),
|
||||
ExpectedAmPmTooShort => f.write_str(
|
||||
"expected to find `AM` or `PM`, \
|
||||
but the remaining input is too short \
|
||||
to contain one",
|
||||
),
|
||||
ExpectedIanaTz => f.write_str(
|
||||
"expected to find the start of an IANA time zone \
|
||||
identifier name or component",
|
||||
),
|
||||
ExpectedIanaTzEndOfInput => f.write_str(
|
||||
"expected to find the start of an IANA time zone \
|
||||
identifier name or component, \
|
||||
but found end of input instead",
|
||||
),
|
||||
ExpectedMonthAbbreviation => {
|
||||
f.write_str("expected to find month name abbreviation")
|
||||
}
|
||||
ExpectedMonthAbbreviationTooShort => f.write_str(
|
||||
"expected to find month name abbreviation, \
|
||||
but the remaining input is too short \
|
||||
to contain one",
|
||||
),
|
||||
ExpectedWeekdayAbbreviation => {
|
||||
f.write_str("expected to find weekday abbreviation")
|
||||
}
|
||||
ExpectedWeekdayAbbreviationTooShort => f.write_str(
|
||||
"expected to find weekday abbreviation, \
|
||||
but the remaining input is too short \
|
||||
to contain one",
|
||||
),
|
||||
ExpectedChoice { available } => {
|
||||
f.write_str(
|
||||
"failed to find expected value, available choices are: ",
|
||||
)?;
|
||||
for (i, choice) in available.iter().enumerate() {
|
||||
if i > 0 {
|
||||
f.write_str(", ")?;
|
||||
}
|
||||
write!(f, "{}", escape::Bytes(choice))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
ExpectedFractionalDigit => f.write_str(
|
||||
"expected at least one fractional decimal digit, \
|
||||
but did not find any",
|
||||
),
|
||||
ExpectedMatchLiteralByte { expected, got } => write!(
|
||||
f,
|
||||
"expected to match literal byte `{expected}` from \
|
||||
format string, but found byte `{got}` in input",
|
||||
expected = escape::Byte(expected),
|
||||
got = escape::Byte(got),
|
||||
),
|
||||
ExpectedMatchLiteralEndOfInput { expected } => write!(
|
||||
f,
|
||||
"expected to match literal byte `{expected}` from \
|
||||
format string, but found end of input",
|
||||
expected = escape::Byte(expected),
|
||||
),
|
||||
ExpectedNonEmpty { directive } => write!(
|
||||
f,
|
||||
"expected non-empty input for directive `%{directive}`, \
|
||||
but found end of input",
|
||||
directive = escape::Byte(directive),
|
||||
),
|
||||
#[cfg(not(feature = "alloc"))]
|
||||
NotAllowedAlloc { directive, colons } => write!(
|
||||
f,
|
||||
"cannot parse `%{colons}{directive}` \
|
||||
without Jiff's `alloc` feature enabled",
|
||||
colons = escape::RepeatByte { byte: b':', count: colons },
|
||||
directive = escape::Byte(directive),
|
||||
),
|
||||
NotAllowedLocaleClockTime => {
|
||||
f.write_str("parsing locale clock time is not allowed")
|
||||
}
|
||||
NotAllowedLocaleDate => {
|
||||
f.write_str("parsing locale date is not allowed")
|
||||
}
|
||||
NotAllowedLocaleDateAndTime => {
|
||||
f.write_str("parsing locale date and time is not allowed")
|
||||
}
|
||||
NotAllowedLocaleTwelveHourClockTime => {
|
||||
f.write_str("parsing locale 12-hour clock time is not allowed")
|
||||
}
|
||||
NotAllowedTimeZoneAbbreviation => {
|
||||
f.write_str("parsing time zone abbreviation is not allowed")
|
||||
}
|
||||
ParseCentury => {
|
||||
f.write_str("failed to parse year number for century")
|
||||
}
|
||||
ParseDay => f.write_str("failed to parse day number"),
|
||||
ParseDayOfYear => {
|
||||
f.write_str("failed to parse day of year number")
|
||||
}
|
||||
ParseFractionalSeconds => f.write_str(
|
||||
"failed to parse fractional second component \
|
||||
(up to 9 digits, nanosecond precision)",
|
||||
),
|
||||
ParseHour => f.write_str("failed to parse hour number"),
|
||||
ParseMinute => f.write_str("failed to parse minute number"),
|
||||
ParseWeekdayNumber => {
|
||||
f.write_str("failed to parse weekday number")
|
||||
}
|
||||
ParseIsoWeekNumber => {
|
||||
f.write_str("failed to parse ISO 8601 week number")
|
||||
}
|
||||
ParseIsoWeekYear => {
|
||||
f.write_str("failed to parse ISO 8601 week year")
|
||||
}
|
||||
ParseIsoWeekYearTwoDigit => {
|
||||
f.write_str("failed to parse 2-digit ISO 8601 week year")
|
||||
}
|
||||
ParseMondayWeekNumber => {
|
||||
f.write_str("failed to parse Monday-based week number")
|
||||
}
|
||||
ParseMonth => f.write_str("failed to parse month number"),
|
||||
ParseSecond => f.write_str("failed to parse second number"),
|
||||
ParseSundayWeekNumber => {
|
||||
f.write_str("failed to parse Sunday-based week number")
|
||||
}
|
||||
ParseTimestamp => {
|
||||
f.write_str("failed to parse Unix timestamp (in seconds)")
|
||||
}
|
||||
ParseYear => f.write_str("failed to parse year"),
|
||||
ParseYearTwoDigit => f.write_str("failed to parse 2-digit year"),
|
||||
UnknownMonthName => f.write_str("unrecognized month name"),
|
||||
UnknownWeekdayAbbreviation => {
|
||||
f.write_str("unrecognized weekday abbreviation")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum FormatError {
|
||||
RequiresDate,
|
||||
RequiresInstant,
|
||||
RequiresOffset,
|
||||
RequiresTime,
|
||||
RequiresTimeZone,
|
||||
RequiresTimeZoneOrOffset,
|
||||
InvalidUtf8,
|
||||
ZeroPrecisionFloat,
|
||||
ZeroPrecisionNano,
|
||||
}
|
||||
|
||||
impl error::IntoError for FormatError {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FormatError> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: FormatError) -> error::Error {
|
||||
error::ErrorKind::FmtStrtimeFormat(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for FormatError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::FormatError::*;
|
||||
|
||||
match *self {
|
||||
RequiresDate => f.write_str("requires date to format"),
|
||||
RequiresInstant => f.write_str(
|
||||
"requires instant (a timestamp or a date, time and offset)",
|
||||
),
|
||||
RequiresTime => f.write_str("requires time to format"),
|
||||
RequiresOffset => f.write_str("requires time zone offset"),
|
||||
RequiresTimeZone => {
|
||||
f.write_str("requires IANA time zone identifier")
|
||||
}
|
||||
RequiresTimeZoneOrOffset => f.write_str(
|
||||
"requires IANA time zone identifier or \
|
||||
time zone offset, but neither were present",
|
||||
),
|
||||
InvalidUtf8 => {
|
||||
f.write_str("invalid format string, it must be valid UTF-8")
|
||||
}
|
||||
ZeroPrecisionFloat => {
|
||||
f.write_str("zero precision with %f is not allowed")
|
||||
}
|
||||
ZeroPrecisionNano => {
|
||||
f.write_str("zero precision with %N is not allowed")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
354
src/error/fmt/temporal.rs
Normal file
354
src/error/fmt/temporal.rs
Normal file
|
|
@ -0,0 +1,354 @@
|
|||
use crate::{error, tz::Offset, util::escape};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
#[cfg(not(feature = "alloc"))]
|
||||
AllocPosixTimeZone,
|
||||
AmbiguousTimeMonthDay,
|
||||
AmbiguousTimeYearMonth,
|
||||
CivilDateTimeZulu,
|
||||
ConvertDateTimeToTimestamp {
|
||||
offset: Offset,
|
||||
},
|
||||
EmptyTimeZone,
|
||||
ExpectedDateDesignatorFoundByte {
|
||||
byte: u8,
|
||||
},
|
||||
ExpectedDateDesignatorFoundEndOfInput,
|
||||
ExpectedDurationDesignatorFoundByte {
|
||||
byte: u8,
|
||||
},
|
||||
ExpectedDurationDesignatorFoundEndOfInput,
|
||||
ExpectedFourDigitYear,
|
||||
ExpectedNoSeparator,
|
||||
ExpectedOneDigitWeekday,
|
||||
ExpectedSeparatorFoundByte {
|
||||
byte: u8,
|
||||
},
|
||||
ExpectedSeparatorFoundEndOfInput,
|
||||
ExpectedSixDigitYear,
|
||||
ExpectedTimeDesignator,
|
||||
ExpectedTimeDesignatorFoundByte {
|
||||
byte: u8,
|
||||
},
|
||||
ExpectedTimeDesignatorFoundEndOfInput,
|
||||
ExpectedTimeUnits,
|
||||
ExpectedTwoDigitDay,
|
||||
ExpectedTwoDigitHour,
|
||||
ExpectedTwoDigitMinute,
|
||||
ExpectedTwoDigitMonth,
|
||||
ExpectedTwoDigitSecond,
|
||||
ExpectedTwoDigitWeekNumber,
|
||||
ExpectedWeekPrefixFoundByte {
|
||||
byte: u8,
|
||||
},
|
||||
ExpectedWeekPrefixFoundEndOfInput,
|
||||
FailedDayInDate,
|
||||
FailedDayInMonthDay,
|
||||
FailedFractionalSecondInTime,
|
||||
FailedHourInTime,
|
||||
FailedMinuteInTime,
|
||||
FailedMonthInDate,
|
||||
FailedMonthInMonthDay,
|
||||
FailedMonthInYearMonth,
|
||||
FailedOffsetNumeric,
|
||||
FailedSecondInTime,
|
||||
FailedSeparatorAfterMonth,
|
||||
FailedSeparatorAfterWeekNumber,
|
||||
FailedSeparatorAfterYear,
|
||||
FailedTzdbLookup,
|
||||
FailedWeekNumberInDate,
|
||||
FailedWeekNumberPrefixInDate,
|
||||
FailedWeekdayInDate,
|
||||
FailedYearInDate,
|
||||
FailedYearInYearMonth,
|
||||
InvalidDate,
|
||||
InvalidDay,
|
||||
InvalidHour,
|
||||
InvalidMinute,
|
||||
InvalidMonth,
|
||||
InvalidMonthDay,
|
||||
InvalidSecond,
|
||||
InvalidTimeZoneUtf8,
|
||||
InvalidWeekDate,
|
||||
InvalidWeekNumber,
|
||||
InvalidWeekday,
|
||||
InvalidYear,
|
||||
InvalidYearMonth,
|
||||
InvalidYearZero,
|
||||
MissingOffsetInTimestamp,
|
||||
MissingTimeInDate,
|
||||
MissingTimeInTimestamp,
|
||||
MissingTimeZoneAnnotation,
|
||||
ParseDayTwoDigit,
|
||||
ParseHourTwoDigit,
|
||||
ParseMinuteTwoDigit,
|
||||
ParseMonthTwoDigit,
|
||||
ParseSecondTwoDigit,
|
||||
ParseWeekNumberTwoDigit,
|
||||
ParseWeekdayOneDigit,
|
||||
ParseYearFourDigit,
|
||||
ParseYearSixDigit,
|
||||
// This is the only error for formatting a Temporal value. And
|
||||
// actually, it's not even part of Temporal, but just lives in that
|
||||
// module (for convenience reasons).
|
||||
PrintTimeZoneFailure,
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::FmtTemporal(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
#[cfg(not(feature = "alloc"))]
|
||||
AllocPosixTimeZone => f.write_str(
|
||||
"cannot parsed time zones other than IANA time zone \
|
||||
identifiers or fixed offsets \
|
||||
without the `alloc` crate feature enabled for `jiff`",
|
||||
),
|
||||
AmbiguousTimeMonthDay => {
|
||||
f.write_str("parsed time is ambiguous with a month-day date")
|
||||
}
|
||||
AmbiguousTimeYearMonth => {
|
||||
f.write_str("parsed time is ambiguous with a year-month date")
|
||||
}
|
||||
CivilDateTimeZulu => f.write_str(
|
||||
"cannot parse civil date/time from string with a Zulu \
|
||||
offset, parse as a `jiff::Timestamp` first \
|
||||
and convert to a civil date/time instead",
|
||||
),
|
||||
ConvertDateTimeToTimestamp { offset } => write!(
|
||||
f,
|
||||
"failed to convert civil datetime to timestamp \
|
||||
with offset {offset}",
|
||||
),
|
||||
EmptyTimeZone => {
|
||||
f.write_str("an empty string is not a valid time zone")
|
||||
}
|
||||
ExpectedDateDesignatorFoundByte { byte } => write!(
|
||||
f,
|
||||
"expected to find date unit designator suffix \
|
||||
(`Y`, `M`, `W` or `D`), but found `{byte}` instead",
|
||||
byte = escape::Byte(byte),
|
||||
),
|
||||
ExpectedDateDesignatorFoundEndOfInput => f.write_str(
|
||||
"expected to find date unit designator suffix \
|
||||
(`Y`, `M`, `W` or `D`), but found end of input",
|
||||
),
|
||||
ExpectedDurationDesignatorFoundByte { byte } => write!(
|
||||
f,
|
||||
"expected to find duration beginning with `P` or `p`, \
|
||||
but found `{byte}` instead",
|
||||
byte = escape::Byte(byte),
|
||||
),
|
||||
ExpectedDurationDesignatorFoundEndOfInput => f.write_str(
|
||||
"expected to find duration beginning with `P` or `p`, \
|
||||
but found end of input",
|
||||
),
|
||||
ExpectedFourDigitYear => f.write_str(
|
||||
"expected four digit year (or leading sign for \
|
||||
six digit year), but found end of input",
|
||||
),
|
||||
ExpectedNoSeparator => f.write_str(
|
||||
"expected no separator since none was \
|
||||
found after the year, but found a `-` separator",
|
||||
),
|
||||
ExpectedOneDigitWeekday => f.write_str(
|
||||
"expected one digit weekday, but found end of input",
|
||||
),
|
||||
ExpectedSeparatorFoundByte { byte } => write!(
|
||||
f,
|
||||
"expected `-` separator, but found `{byte}`",
|
||||
byte = escape::Byte(byte),
|
||||
),
|
||||
ExpectedSeparatorFoundEndOfInput => {
|
||||
f.write_str("expected `-` separator, but found end of input")
|
||||
}
|
||||
ExpectedSixDigitYear => f.write_str(
|
||||
"expected six digit year (because of a leading sign), \
|
||||
but found end of input",
|
||||
),
|
||||
ExpectedTimeDesignator => f.write_str(
|
||||
"parsing ISO 8601 duration in this context requires \
|
||||
that the duration contain a time component and no \
|
||||
components of days or greater",
|
||||
),
|
||||
ExpectedTimeDesignatorFoundByte { byte } => write!(
|
||||
f,
|
||||
"expected to find time unit designator suffix \
|
||||
(`H`, `M` or `S`), but found `{byte}` instead",
|
||||
byte = escape::Byte(byte),
|
||||
),
|
||||
ExpectedTimeDesignatorFoundEndOfInput => f.write_str(
|
||||
"expected to find time unit designator suffix \
|
||||
(`H`, `M` or `S`), but found end of input",
|
||||
),
|
||||
ExpectedTimeUnits => f.write_str(
|
||||
"found a time designator (`T` or `t`) in an ISO 8601 \
|
||||
duration string, but did not find any time units",
|
||||
),
|
||||
ExpectedTwoDigitDay => {
|
||||
f.write_str("expected two digit day, but found end of input")
|
||||
}
|
||||
ExpectedTwoDigitHour => {
|
||||
f.write_str("expected two digit hour, but found end of input")
|
||||
}
|
||||
ExpectedTwoDigitMinute => f.write_str(
|
||||
"expected two digit minute, but found end of input",
|
||||
),
|
||||
ExpectedTwoDigitMonth => {
|
||||
f.write_str("expected two digit month, but found end of input")
|
||||
}
|
||||
ExpectedTwoDigitSecond => f.write_str(
|
||||
"expected two digit second, but found end of input",
|
||||
),
|
||||
ExpectedTwoDigitWeekNumber => f.write_str(
|
||||
"expected two digit week number, but found end of input",
|
||||
),
|
||||
ExpectedWeekPrefixFoundByte { byte } => write!(
|
||||
f,
|
||||
"expected `W` or `w`, but found `{byte}` instead",
|
||||
byte = escape::Byte(byte),
|
||||
),
|
||||
ExpectedWeekPrefixFoundEndOfInput => {
|
||||
f.write_str("expected `W` or `w`, but found end of input")
|
||||
}
|
||||
FailedDayInDate => f.write_str("failed to parse day in date"),
|
||||
FailedDayInMonthDay => {
|
||||
f.write_str("failed to payse day in month-day")
|
||||
}
|
||||
FailedFractionalSecondInTime => {
|
||||
f.write_str("failed to parse fractional seconds in time")
|
||||
}
|
||||
FailedHourInTime => f.write_str("failed to parse hour in time"),
|
||||
FailedMinuteInTime => {
|
||||
f.write_str("failed to parse minute in time")
|
||||
}
|
||||
FailedMonthInDate => f.write_str("failed to parse month in date"),
|
||||
FailedMonthInMonthDay => {
|
||||
f.write_str("failed to parse month in month-day")
|
||||
}
|
||||
FailedMonthInYearMonth => {
|
||||
f.write_str("failed to parse month in year-month")
|
||||
}
|
||||
FailedOffsetNumeric => f.write_str(
|
||||
"offset successfully parsed, \
|
||||
but failed to convert to numeric `jiff::tz::Offset`",
|
||||
),
|
||||
FailedSecondInTime => {
|
||||
f.write_str("failed to parse second in time")
|
||||
}
|
||||
FailedSeparatorAfterMonth => {
|
||||
f.write_str("failed to parse separator after month")
|
||||
}
|
||||
FailedSeparatorAfterWeekNumber => {
|
||||
f.write_str("failed to parse separator after week number")
|
||||
}
|
||||
FailedSeparatorAfterYear => {
|
||||
f.write_str("failed to parse separator after year")
|
||||
}
|
||||
FailedTzdbLookup => f.write_str(
|
||||
"parsed apparent IANA time zone identifier, \
|
||||
but the tzdb lookup failed",
|
||||
),
|
||||
FailedWeekNumberInDate => {
|
||||
f.write_str("failed to parse week number in date")
|
||||
}
|
||||
FailedWeekNumberPrefixInDate => {
|
||||
f.write_str("failed to parse week number prefix in date")
|
||||
}
|
||||
FailedWeekdayInDate => {
|
||||
f.write_str("failed to parse weekday in date")
|
||||
}
|
||||
FailedYearInDate => f.write_str("failed to parse year in date"),
|
||||
FailedYearInYearMonth => {
|
||||
f.write_str("failed to parse year in year-month")
|
||||
}
|
||||
InvalidDate => f.write_str("parsed date is not valid"),
|
||||
InvalidDay => f.write_str("parsed day is not valid"),
|
||||
InvalidHour => f.write_str("parsed hour is not valid"),
|
||||
InvalidMinute => f.write_str("parsed minute is not valid"),
|
||||
InvalidMonth => f.write_str("parsed month is not valid"),
|
||||
InvalidMonthDay => f.write_str("parsed month-day is not valid"),
|
||||
InvalidSecond => f.write_str("parsed second is not valid"),
|
||||
InvalidTimeZoneUtf8 => f.write_str(
|
||||
"found plausible IANA time zone identifier, \
|
||||
but it is not valid UTF-8",
|
||||
),
|
||||
InvalidWeekDate => f.write_str("parsed week date is not valid"),
|
||||
InvalidWeekNumber => {
|
||||
f.write_str("parsed week number is not valid")
|
||||
}
|
||||
InvalidWeekday => f.write_str("parsed weekday is not valid"),
|
||||
InvalidYear => f.write_str("parsed year is not valid"),
|
||||
InvalidYearMonth => f.write_str("parsed year-month is not valid"),
|
||||
InvalidYearZero => f.write_str(
|
||||
"year zero must be written without a sign or a \
|
||||
positive sign, but not a negative sign",
|
||||
),
|
||||
MissingOffsetInTimestamp => f.write_str(
|
||||
"failed to find offset component, \
|
||||
which is required for parsing a timestamp",
|
||||
),
|
||||
MissingTimeInDate => f.write_str(
|
||||
"successfully parsed date, but no time component was found",
|
||||
),
|
||||
MissingTimeInTimestamp => f.write_str(
|
||||
"failed to find time component, \
|
||||
which is required for parsing a timestamp",
|
||||
),
|
||||
MissingTimeZoneAnnotation => f.write_str(
|
||||
"failed to find time zone annotation in square brackets, \
|
||||
which is required for parsing a zoned datetime",
|
||||
),
|
||||
ParseDayTwoDigit => {
|
||||
f.write_str("failed to parse two digit integer as day")
|
||||
}
|
||||
ParseHourTwoDigit => {
|
||||
f.write_str("failed to parse two digit integer as hour")
|
||||
}
|
||||
ParseMinuteTwoDigit => {
|
||||
f.write_str("failed to parse two digit integer as minute")
|
||||
}
|
||||
ParseMonthTwoDigit => {
|
||||
f.write_str("failed to parse two digit integer as month")
|
||||
}
|
||||
ParseSecondTwoDigit => {
|
||||
f.write_str("failed to parse two digit integer as second")
|
||||
}
|
||||
ParseWeekNumberTwoDigit => {
|
||||
f.write_str("failed to parse two digit integer as week number")
|
||||
}
|
||||
ParseWeekdayOneDigit => {
|
||||
f.write_str("failed to parse one digit integer as weekday")
|
||||
}
|
||||
ParseYearFourDigit => {
|
||||
f.write_str("failed to parse four digit integer as year")
|
||||
}
|
||||
ParseYearSixDigit => {
|
||||
f.write_str("failed to parse six digit integer as year")
|
||||
}
|
||||
PrintTimeZoneFailure => f.write_str(
|
||||
"time zones without IANA identifiers that aren't either \
|
||||
fixed offsets or a POSIX time zone can't be serialized \
|
||||
(this typically occurs when this is a system time zone \
|
||||
derived from `/etc/localtime` on Unix systems that \
|
||||
isn't symlinked to an entry in `/usr/share/zoneinfo`)",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
115
src/error/fmt/util.rs
Normal file
115
src/error/fmt/util.rs
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
use crate::{error, Unit};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
ConversionToSecondsFailed { unit: Unit },
|
||||
EmptyDuration,
|
||||
FailedValueSet { unit: Unit },
|
||||
InvalidFraction,
|
||||
InvalidFractionNanos,
|
||||
MissingFractionalDigits,
|
||||
NotAllowedCalendarUnit { unit: Unit },
|
||||
NotAllowedFractionalUnit { found: Unit },
|
||||
NotAllowedNegative,
|
||||
OutOfOrderHMS { found: Unit },
|
||||
OutOfOrderUnits { found: Unit, previous: Unit },
|
||||
OverflowForUnit { unit: Unit },
|
||||
OverflowForUnitFractional { unit: Unit },
|
||||
SignedOverflowForUnit { unit: Unit },
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::FmtUtil(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
ConversionToSecondsFailed { unit } => write!(
|
||||
f,
|
||||
"converting {unit} to seconds overflows \
|
||||
a signed 64-bit integer",
|
||||
unit = unit.plural(),
|
||||
),
|
||||
EmptyDuration => f.write_str("no parsed duration components"),
|
||||
FailedValueSet { unit } => write!(
|
||||
f,
|
||||
"failed to set value for {unit} unit on span",
|
||||
unit = unit.singular(),
|
||||
),
|
||||
InvalidFraction => f.write_str(
|
||||
"failed to parse fractional component \
|
||||
(up to 9 digits, nanosecond precision is allowed)",
|
||||
),
|
||||
InvalidFractionNanos => f.write_str(
|
||||
"failed to set nanosecond value from fractional component",
|
||||
),
|
||||
MissingFractionalDigits => f.write_str(
|
||||
"found decimal after seconds component, \
|
||||
but did not find any digits after decimal",
|
||||
),
|
||||
NotAllowedCalendarUnit { unit } => write!(
|
||||
f,
|
||||
"parsing calendar units ({unit} in this case) \
|
||||
in this context is not supported \
|
||||
(perhaps try parsing into a `jiff::Span` instead)",
|
||||
unit = unit.plural(),
|
||||
),
|
||||
NotAllowedFractionalUnit { found } => write!(
|
||||
f,
|
||||
"fractional {found} are not supported",
|
||||
found = found.plural(),
|
||||
),
|
||||
NotAllowedNegative => f.write_str(
|
||||
"cannot parse negative duration into unsigned \
|
||||
`std::time::Duration`",
|
||||
),
|
||||
OutOfOrderHMS { found } => write!(
|
||||
f,
|
||||
"found `HH:MM:SS` after unit {found}, \
|
||||
but `HH:MM:SS` can only appear after \
|
||||
years, months, weeks or days",
|
||||
found = found.singular(),
|
||||
),
|
||||
OutOfOrderUnits { found, previous } => write!(
|
||||
f,
|
||||
"found value with unit {found} \
|
||||
after unit {previous}, but units must be \
|
||||
written from largest to smallest \
|
||||
(and they can't be repeated)",
|
||||
found = found.singular(),
|
||||
previous = previous.singular(),
|
||||
),
|
||||
OverflowForUnit { unit } => write!(
|
||||
f,
|
||||
"accumulated duration \
|
||||
overflowed when adding value to unit {unit}",
|
||||
unit = unit.singular(),
|
||||
),
|
||||
OverflowForUnitFractional { unit } => write!(
|
||||
f,
|
||||
"accumulated duration \
|
||||
overflowed when adding fractional value to unit {unit}",
|
||||
unit = unit.singular(),
|
||||
),
|
||||
SignedOverflowForUnit { unit } => write!(
|
||||
f,
|
||||
"value for {unit} is too big (or small) to fit into \
|
||||
a signed 64-bit integer",
|
||||
unit = unit.plural(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,14 @@
|
|||
use crate::{shared::util::error::Error as SharedError, util::sync::Arc};
|
||||
use crate::util::sync::Arc;
|
||||
|
||||
/// Creates a new ad hoc error with no causal chain.
|
||||
///
|
||||
/// This accepts the same arguments as the `format!` macro. The error it
|
||||
/// creates is just a wrapper around the string created by `format!`.
|
||||
macro_rules! err {
|
||||
($($tt:tt)*) => {{
|
||||
crate::error::Error::adhoc_from_args(format_args!($($tt)*))
|
||||
}}
|
||||
}
|
||||
|
||||
pub(crate) use err;
|
||||
pub(crate) mod civil;
|
||||
pub(crate) mod duration;
|
||||
pub(crate) mod fmt;
|
||||
pub(crate) mod signed_duration;
|
||||
pub(crate) mod span;
|
||||
pub(crate) mod timestamp;
|
||||
pub(crate) mod tz;
|
||||
pub(crate) mod util;
|
||||
pub(crate) mod zoned;
|
||||
|
||||
/// An error that can occur in this crate.
|
||||
///
|
||||
|
|
@ -29,8 +27,11 @@ pub(crate) use err;
|
|||
///
|
||||
/// 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 no
|
||||
/// introspection capabilities.
|
||||
/// [`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.
|
||||
///
|
||||
/// # Design
|
||||
///
|
||||
|
|
@ -65,50 +66,6 @@ struct ErrorInner {
|
|||
cause: Option<Error>,
|
||||
}
|
||||
|
||||
/// The underlying kind of a [`Error`].
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(not(feature = "alloc"), derive(Clone))]
|
||||
enum ErrorKind {
|
||||
/// An ad hoc error that is constructed from anything that implements
|
||||
/// the `core::fmt::Display` trait.
|
||||
///
|
||||
/// In theory we try to avoid these, but they tend to be awfully
|
||||
/// convenient. In practice, we use them a lot, and only use a structured
|
||||
/// representation when a lot of different error cases fit neatly into a
|
||||
/// structure (like range errors).
|
||||
Adhoc(AdhocError),
|
||||
/// An error that occurs when a number is not within its allowed range.
|
||||
///
|
||||
/// This can occur directly as a result of a number provided by the caller
|
||||
/// of a public API, or as a result of an operation on a number that
|
||||
/// results in it being out of range.
|
||||
Range(RangeError),
|
||||
/// An error that occurs within `jiff::shared`.
|
||||
///
|
||||
/// It has its own error type to avoid bringing in this much bigger error
|
||||
/// type.
|
||||
Shared(SharedError),
|
||||
/// An error associated with a file path.
|
||||
///
|
||||
/// This is generally expected to always have a cause attached to it
|
||||
/// explaining what went wrong. The error variant is just a path to make
|
||||
/// it composable with other error types.
|
||||
///
|
||||
/// The cause is typically `Adhoc` or `IO`.
|
||||
///
|
||||
/// When `std` is not enabled, this variant can never be constructed.
|
||||
#[allow(dead_code)] // not used in some feature configs
|
||||
FilePath(FilePathError),
|
||||
/// An error that occurs when interacting with the file system.
|
||||
///
|
||||
/// This is effectively a wrapper around `std::io::Error` coupled with a
|
||||
/// `std::path::PathBuf`.
|
||||
///
|
||||
/// When `std` is not enabled, this variant can never be constructed.
|
||||
#[allow(dead_code)] // not used in some feature configs
|
||||
IO(IOError),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
/// Creates a new error value from `core::fmt::Arguments`.
|
||||
///
|
||||
|
|
@ -120,6 +77,14 @@ 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
|
||||
///
|
||||
/// ```
|
||||
|
|
@ -132,79 +97,114 @@ impl Error {
|
|||
Error::from(ErrorKind::Adhoc(AdhocError::from_args(message)))
|
||||
}
|
||||
|
||||
#[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
|
||||
}
|
||||
/// 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(_))
|
||||
}
|
||||
}
|
||||
|
||||
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,9 +220,36 @@ impl Error {
|
|||
Error::from(ErrorKind::Range(RangeError::new(what, given, min, max)))
|
||||
}
|
||||
|
||||
/// Creates a new error indicating that a `given` value is out of the
|
||||
/// allowed range.
|
||||
///
|
||||
/// This is similar to `Error::range`, but the error message doesn't
|
||||
/// include the illegal value or the allowed range. This is useful for
|
||||
/// ad hoc range errors but should generally be used sparingly.
|
||||
#[inline(never)]
|
||||
#[cold]
|
||||
pub(crate) fn slim_range(what: &'static str) -> Error {
|
||||
Error::from(ErrorKind::SlimRange(SlimRangeError::new(what)))
|
||||
}
|
||||
|
||||
/// Creates a new error from the special "shared" error type.
|
||||
pub(crate) fn shared(err: SharedError) -> Error {
|
||||
Error::from(ErrorKind::Shared(err))
|
||||
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))
|
||||
}
|
||||
|
||||
/// A convenience constructor for building an I/O error.
|
||||
|
|
@ -258,7 +285,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
|
||||
|
|
@ -268,6 +295,83 @@ 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")]
|
||||
|
|
@ -275,30 +379,14 @@ impl std::error::Error for Error {}
|
|||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
#[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),
|
||||
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(": ")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -328,14 +416,98 @@ 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 {
|
||||
ErrorKind::Adhoc(ref msg) => msg.fmt(f),
|
||||
ErrorKind::Range(ref err) => err.fmt(f),
|
||||
ErrorKind::Shared(ref err) => err.fmt(f),
|
||||
ErrorKind::FilePath(ref err) => err.fmt(f),
|
||||
ErrorKind::IO(ref err) => err.fmt(f),
|
||||
Adhoc(ref msg) => msg.fmt(f),
|
||||
Civil(ref err) => err.fmt(f),
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -355,10 +527,10 @@ impl From<ErrorKind> for Error {
|
|||
|
||||
/// A generic error 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.
|
||||
/// 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.
|
||||
#[cfg_attr(not(feature = "alloc"), derive(Clone))]
|
||||
struct AdhocError {
|
||||
#[cfg(feature = "alloc")]
|
||||
|
|
@ -368,18 +540,13 @@ struct AdhocError {
|
|||
}
|
||||
|
||||
impl AdhocError {
|
||||
#[cfg(feature = "alloc")]
|
||||
fn from_display<'a>(message: impl core::fmt::Display + 'a) -> AdhocError {
|
||||
use alloc::string::ToString;
|
||||
|
||||
let message = message.to_string().into_boxed_str();
|
||||
AdhocError { message }
|
||||
}
|
||||
|
||||
fn from_args<'a>(message: core::fmt::Arguments<'a>) -> AdhocError {
|
||||
#[cfg(feature = "alloc")]
|
||||
{
|
||||
AdhocError::from_display(message)
|
||||
use alloc::string::ToString;
|
||||
|
||||
let message = message.to_string().into_boxed_str();
|
||||
AdhocError { message }
|
||||
}
|
||||
#[cfg(not(feature = "alloc"))]
|
||||
{
|
||||
|
|
@ -387,17 +554,6 @@ impl AdhocError {
|
|||
"unknown Jiff error (better error messages require \
|
||||
enabling the `alloc` feature for the `jiff` crate)",
|
||||
);
|
||||
AdhocError::from_static_str(message)
|
||||
}
|
||||
}
|
||||
|
||||
fn from_static_str(message: &'static str) -> AdhocError {
|
||||
#[cfg(feature = "alloc")]
|
||||
{
|
||||
AdhocError::from_display(message)
|
||||
}
|
||||
#[cfg(not(feature = "alloc"))]
|
||||
{
|
||||
AdhocError { message }
|
||||
}
|
||||
}
|
||||
|
|
@ -476,6 +632,75 @@ 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
|
||||
|
|
@ -581,21 +806,6 @@ impl IntoError for Error {
|
|||
}
|
||||
}
|
||||
|
||||
impl IntoError for &'static str {
|
||||
#[inline(always)]
|
||||
fn into_error(self) -> Error {
|
||||
Error::adhoc_from_static_str(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
impl IntoError for alloc::string::String {
|
||||
#[inline(always)]
|
||||
fn into_error(self) -> Error {
|
||||
Error::adhoc_from_string(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for contextualizing error values.
|
||||
///
|
||||
/// This makes it easy to contextualize either `Error` or `Result<T, Error>`.
|
||||
|
|
@ -603,7 +813,7 @@ impl IntoError for alloc::string::String {
|
|||
/// `map_err` everywhere one wants to add context to an error.
|
||||
///
|
||||
/// This trick was borrowed from `anyhow`.
|
||||
pub(crate) trait ErrorContext {
|
||||
pub(crate) trait ErrorContext<T, E> {
|
||||
/// Contextualize the given consequent error with this (`self`) error as
|
||||
/// the cause.
|
||||
///
|
||||
|
|
@ -612,7 +822,7 @@ pub(crate) trait ErrorContext {
|
|||
/// Note that if an `Error` is given for `kind`, then this panics if it has
|
||||
/// a cause. (Because the cause would otherwise be dropped. An error causal
|
||||
/// chain is just a linked list, not a tree.)
|
||||
fn context(self, consequent: impl IntoError) -> Self;
|
||||
fn context(self, consequent: impl IntoError) -> Result<T, Error>;
|
||||
|
||||
/// Like `context`, but hides error construction within a closure.
|
||||
///
|
||||
|
|
@ -623,39 +833,31 @@ pub(crate) trait ErrorContext {
|
|||
///
|
||||
/// Usually this only makes sense to use on a `Result<T, Error>`, otherwise
|
||||
/// the closure is just executed immediately anyway.
|
||||
fn with_context<E: IntoError>(
|
||||
fn with_context<C: IntoError>(
|
||||
self,
|
||||
consequent: impl FnOnce() -> E,
|
||||
) -> Self;
|
||||
consequent: impl FnOnce() -> C,
|
||||
) -> Result<T, Error>;
|
||||
}
|
||||
|
||||
impl ErrorContext for Error {
|
||||
#[cfg_attr(feature = "perf-inline", inline(always))]
|
||||
fn context(self, consequent: impl IntoError) -> Error {
|
||||
self.context_impl(consequent.into_error())
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "perf-inline", inline(always))]
|
||||
fn with_context<E: IntoError>(
|
||||
self,
|
||||
consequent: impl FnOnce() -> E,
|
||||
) -> Error {
|
||||
self.context_impl(consequent().into_error())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ErrorContext for Result<T, Error> {
|
||||
impl<T, E> ErrorContext<T, E> for Result<T, E>
|
||||
where
|
||||
E: IntoError,
|
||||
{
|
||||
#[cfg_attr(feature = "perf-inline", inline(always))]
|
||||
fn context(self, consequent: impl IntoError) -> Result<T, Error> {
|
||||
self.map_err(|err| err.context_impl(consequent.into_error()))
|
||||
self.map_err(|err| {
|
||||
err.into_error().context_impl(consequent.into_error())
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "perf-inline", inline(always))]
|
||||
fn with_context<E: IntoError>(
|
||||
fn with_context<C: IntoError>(
|
||||
self,
|
||||
consequent: impl FnOnce() -> E,
|
||||
consequent: impl FnOnce() -> C,
|
||||
) -> Result<T, Error> {
|
||||
self.map_err(|err| err.context_impl(consequent().into_error()))
|
||||
self.map_err(|err| {
|
||||
err.into_error().context_impl(consequent().into_error())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -690,7 +892,12 @@ mod tests {
|
|||
// then we could make `Error` a zero sized type. Which might
|
||||
// actually be the right trade-off for core-only, but I'll hold off
|
||||
// until we have some real world use cases.
|
||||
expected_size *= 3;
|
||||
//
|
||||
// OK... after switching to structured errors, this jumped
|
||||
// back up to `expected_size *= 6`. And that was with me being
|
||||
// conscientious about what data we store inside of error types.
|
||||
// Blech.
|
||||
expected_size *= 6;
|
||||
}
|
||||
assert_eq!(expected_size, core::mem::size_of::<Error>());
|
||||
}
|
||||
55
src/error/signed_duration.rs
Normal file
55
src/error/signed_duration.rs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
use crate::{error, Unit};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
ConvertNonFinite,
|
||||
ConvertSystemTime,
|
||||
RoundCalendarUnit { unit: Unit },
|
||||
RoundOverflowed { unit: Unit },
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::SignedDuration(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
ConvertNonFinite => f.write_str(
|
||||
"could not convert non-finite \
|
||||
floating point seconds to signed duration \
|
||||
(floating point seconds must be finite)",
|
||||
),
|
||||
ConvertSystemTime => f.write_str(
|
||||
"failed to get duration between \
|
||||
`std::time::SystemTime` values",
|
||||
),
|
||||
RoundCalendarUnit { unit } => write!(
|
||||
f,
|
||||
"rounding `jiff::SignedDuration` failed because \
|
||||
a calendar unit of '{plural}' was provided \
|
||||
(to round by calendar units, you must use a `jiff::Span`)",
|
||||
plural = unit.plural(),
|
||||
),
|
||||
RoundOverflowed { unit } => write!(
|
||||
f,
|
||||
"rounding signed duration to nearest {singular} \
|
||||
resulted in a value outside the supported range \
|
||||
of a `jiff::SignedDuration`",
|
||||
singular = unit.singular(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
139
src/error/span.rs
Normal file
139
src/error/span.rs
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
use crate::{error, Unit};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
ConvertDateTimeToTimestamp,
|
||||
ConvertNanoseconds { unit: Unit },
|
||||
ConvertNegative,
|
||||
ConvertSpanToSignedDuration,
|
||||
FailedSpanBetweenDateTimes { unit: Unit },
|
||||
FailedSpanBetweenZonedDateTimes { unit: Unit },
|
||||
NotAllowedCalendarUnits { unit: Unit },
|
||||
NotAllowedLargestSmallerThanSmallest { smallest: Unit, largest: Unit },
|
||||
OptionLargest,
|
||||
OptionLargestInSpan,
|
||||
OptionSmallest,
|
||||
RequiresRelativeWeekOrDay { unit: Unit },
|
||||
RequiresRelativeYearOrMonth { unit: Unit },
|
||||
RequiresRelativeYearOrMonthGivenDaysAre24Hours { unit: Unit },
|
||||
ToDurationCivil,
|
||||
ToDurationDaysAre24Hours,
|
||||
ToDurationZoned,
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::Span(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
ConvertDateTimeToTimestamp => f.write_str(
|
||||
"failed to interpret datetime in UTC \
|
||||
in order to convert it to a timestamp",
|
||||
),
|
||||
ConvertNanoseconds { unit } => write!(
|
||||
f,
|
||||
"failed to convert rounded nanoseconds \
|
||||
to span for largest unit set to '{unit}'",
|
||||
unit = unit.plural(),
|
||||
),
|
||||
ConvertNegative => f.write_str(
|
||||
"cannot convert negative span \
|
||||
to unsigned `std::time::Duration`",
|
||||
),
|
||||
ConvertSpanToSignedDuration => f.write_str(
|
||||
"failed to convert span to duration without relative datetime \
|
||||
(must use `jiff::Span::to_duration` instead)",
|
||||
),
|
||||
FailedSpanBetweenDateTimes { unit } => write!(
|
||||
f,
|
||||
"failed to get span between datetimes \
|
||||
with largest unit set to '{unit}'",
|
||||
unit = unit.plural(),
|
||||
),
|
||||
FailedSpanBetweenZonedDateTimes { unit } => write!(
|
||||
f,
|
||||
"failed to get span between zoned datetimes \
|
||||
with largest unit set to '{unit}'",
|
||||
unit = unit.plural(),
|
||||
),
|
||||
OptionLargest => {
|
||||
f.write_str("error with `largest` rounding option")
|
||||
}
|
||||
OptionLargestInSpan => {
|
||||
f.write_str("error with largest unit in span to be rounded")
|
||||
}
|
||||
OptionSmallest => {
|
||||
f.write_str("error with `smallest` rounding option")
|
||||
}
|
||||
NotAllowedCalendarUnits { unit } => write!(
|
||||
f,
|
||||
"operation can only be performed with units of hours \
|
||||
or smaller, but found non-zero '{unit}' units \
|
||||
(operations on `jiff::Timestamp`, `jiff::tz::Offset` \
|
||||
and `jiff::civil::Time` don't support calendar \
|
||||
units in a `jiff::Span`)",
|
||||
unit = unit.singular(),
|
||||
),
|
||||
NotAllowedLargestSmallerThanSmallest { smallest, largest } => {
|
||||
write!(
|
||||
f,
|
||||
"largest unit ('{largest}') cannot be smaller than \
|
||||
smallest unit ('{smallest}')",
|
||||
largest = largest.singular(),
|
||||
smallest = smallest.singular(),
|
||||
)
|
||||
}
|
||||
RequiresRelativeWeekOrDay { unit } => write!(
|
||||
f,
|
||||
"using unit '{unit}' in a span or configuration \
|
||||
requires that either a relative reference time be given \
|
||||
or `jiff::SpanRelativeTo::days_are_24_hours()` is used to \
|
||||
indicate invariant 24-hour days, \
|
||||
but neither were provided",
|
||||
unit = unit.singular(),
|
||||
),
|
||||
RequiresRelativeYearOrMonth { unit } => write!(
|
||||
f,
|
||||
"using unit '{unit}' in a span or configuration \
|
||||
requires that a relative reference time be given, \
|
||||
but none was provided",
|
||||
unit = unit.singular(),
|
||||
),
|
||||
RequiresRelativeYearOrMonthGivenDaysAre24Hours { unit } => write!(
|
||||
f,
|
||||
"using unit '{unit}' in span or configuration \
|
||||
requires that a relative reference time be given \
|
||||
(`jiff::SpanRelativeTo::days_are_24_hours()` was given \
|
||||
but this only permits using days and weeks \
|
||||
without a relative reference time)",
|
||||
unit = unit.singular(),
|
||||
),
|
||||
ToDurationCivil => f.write_str(
|
||||
"could not compute normalized relative span \
|
||||
from civil datetime",
|
||||
),
|
||||
ToDurationDaysAre24Hours => f.write_str(
|
||||
"could not compute normalized relative span \
|
||||
when all days are assumed to be 24 hours",
|
||||
),
|
||||
ToDurationZoned => f.write_str(
|
||||
"could not compute normalized relative span \
|
||||
from zoned datetime",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
38
src/error/timestamp.rs
Normal file
38
src/error/timestamp.rs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
use crate::error;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
OverflowAddDuration,
|
||||
OverflowAddSpan,
|
||||
RequiresSaturatingTimeUnits,
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::Timestamp(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
OverflowAddDuration => {
|
||||
f.write_str("adding duration overflowed timestamp")
|
||||
}
|
||||
OverflowAddSpan => f.write_str("adding span overflowed timestamp"),
|
||||
RequiresSaturatingTimeUnits => f.write_str(
|
||||
"saturating timestamp arithmetic requires only time units",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
49
src/error/tz/ambiguous.rs
Normal file
49
src/error/tz/ambiguous.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
use crate::{
|
||||
error,
|
||||
tz::{Offset, TimeZone},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
BecauseFold { before: Offset, after: Offset },
|
||||
BecauseGap { before: Offset, after: Offset },
|
||||
InTimeZone { tz: TimeZone },
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::TzAmbiguous(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
BecauseFold { before, after } => write!(
|
||||
f,
|
||||
"datetime is ambiguous since it falls into a \
|
||||
fold between offsets {before} and {after}",
|
||||
),
|
||||
BecauseGap { before, after } => write!(
|
||||
f,
|
||||
"datetime is ambiguous since it falls into a \
|
||||
gap between offsets {before} and {after}",
|
||||
),
|
||||
InTimeZone { ref tz } => write!(
|
||||
f,
|
||||
"error converting datetime to instant in time zone {tz}",
|
||||
tz = tz.diagnostic_name(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
119
src/error/tz/concatenated.rs
Normal file
119
src/error/tz/concatenated.rs
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
use crate::error;
|
||||
|
||||
// At time of writing, the biggest TZif data file is a few KB. And the
|
||||
// index block is tens of KB. So impose a limit that is a couple of orders
|
||||
// of magnitude bigger, but still overall pretty small for... some systems.
|
||||
// Anyway, I welcome improvements to this heuristic!
|
||||
pub(crate) const ALLOC_LIMIT: usize = 10 * 1 << 20;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
AllocRequestOverLimit,
|
||||
AllocFailed,
|
||||
AllocOverflow,
|
||||
ExpectedFirstSixBytes,
|
||||
ExpectedIanaName,
|
||||
ExpectedLastByte,
|
||||
#[cfg(test)]
|
||||
ExpectedMoreData,
|
||||
ExpectedVersion,
|
||||
FailedReadData,
|
||||
FailedReadHeader,
|
||||
FailedReadIndex,
|
||||
#[cfg(all(feature = "std", all(not(unix), not(windows))))]
|
||||
FailedSeek,
|
||||
InvalidIndexDataOffsets,
|
||||
InvalidLengthIndexBlock,
|
||||
#[cfg(all(feature = "std", windows))]
|
||||
InvalidOffsetOverflowFile,
|
||||
#[cfg(test)]
|
||||
InvalidOffsetOverflowSlice,
|
||||
#[cfg(test)]
|
||||
InvalidOffsetTooBig,
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::TzConcatenated(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
AllocRequestOverLimit => write!(
|
||||
f,
|
||||
"attempted to allocate more than {ALLOC_LIMIT} bytes \
|
||||
while reading concatenated TZif data, which \
|
||||
exceeds a heuristic limit to prevent huge allocations \
|
||||
(please file a bug if this error is inappropriate)",
|
||||
),
|
||||
AllocFailed => f.write_str(
|
||||
"failed to allocate additional room \
|
||||
for reading concatenated TZif data",
|
||||
),
|
||||
AllocOverflow => {
|
||||
f.write_str("total allocation length overflowed `usize`")
|
||||
}
|
||||
ExpectedFirstSixBytes => f.write_str(
|
||||
"expected first 6 bytes of concatenated TZif header \
|
||||
to be `tzdata`",
|
||||
),
|
||||
ExpectedIanaName => f.write_str(
|
||||
"expected IANA time zone identifier to be valid UTF-8",
|
||||
),
|
||||
ExpectedLastByte => f.write_str(
|
||||
"expected last byte of concatenated TZif header \
|
||||
to be `NUL`",
|
||||
),
|
||||
#[cfg(test)]
|
||||
ExpectedMoreData => f.write_str(
|
||||
"unexpected EOF, expected more bytes based on size \
|
||||
of caller provided buffer",
|
||||
),
|
||||
ExpectedVersion => f.write_str(
|
||||
"expected version in concatenated TZif header to \
|
||||
be valid UTF-8",
|
||||
),
|
||||
FailedReadData => f.write_str("failed to read TZif data block"),
|
||||
FailedReadHeader => {
|
||||
f.write_str("failed to read concatenated TZif header")
|
||||
}
|
||||
FailedReadIndex => f.write_str("failed to read index block"),
|
||||
#[cfg(all(feature = "std", all(not(unix), not(windows))))]
|
||||
FailedSeek => {
|
||||
f.write_str("failed to seek to offset in `std::fs::File`")
|
||||
}
|
||||
InvalidIndexDataOffsets => f.write_str(
|
||||
"invalid index and data offsets, \
|
||||
expected index offset to be less than or equal \
|
||||
to data offset",
|
||||
),
|
||||
InvalidLengthIndexBlock => {
|
||||
f.write_str("length of index block is not a valid multiple")
|
||||
}
|
||||
#[cfg(all(feature = "std", windows))]
|
||||
InvalidOffsetOverflowFile => f.write_str(
|
||||
"offset overflow when reading from `std::fs::File`",
|
||||
),
|
||||
#[cfg(test)]
|
||||
InvalidOffsetOverflowSlice => {
|
||||
f.write_str("offset overflowed `usize`")
|
||||
}
|
||||
#[cfg(test)]
|
||||
InvalidOffsetTooBig => {
|
||||
f.write_str("offset too big for given slice of data")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
141
src/error/tz/db.rs
Normal file
141
src/error/tz/db.rs
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
use crate::error;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
#[cfg(feature = "tzdb-concatenated")]
|
||||
ConcatenatedMissingIanaIdentifiers,
|
||||
#[cfg(all(feature = "std", not(feature = "tzdb-concatenated")))]
|
||||
DisabledConcatenated,
|
||||
#[cfg(all(feature = "std", not(feature = "tzdb-zoneinfo")))]
|
||||
DisabledZoneInfo,
|
||||
FailedTimeZone {
|
||||
#[cfg(feature = "alloc")]
|
||||
name: alloc::boxed::Box<str>,
|
||||
},
|
||||
FailedTimeZoneNoDatabaseConfigured {
|
||||
#[cfg(feature = "alloc")]
|
||||
name: alloc::boxed::Box<str>,
|
||||
},
|
||||
#[cfg(feature = "tzdb-zoneinfo")]
|
||||
ZoneInfoNoTzifFiles,
|
||||
#[cfg(feature = "tzdb-zoneinfo")]
|
||||
ZoneInfoStripPrefix,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub(crate) fn failed_time_zone(_time_zone_name: &str) -> Error {
|
||||
Error::FailedTimeZone {
|
||||
#[cfg(feature = "alloc")]
|
||||
name: _time_zone_name.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn failed_time_zone_no_database_configured(
|
||||
_time_zone_name: &str,
|
||||
) -> Error {
|
||||
Error::FailedTimeZoneNoDatabaseConfigured {
|
||||
#[cfg(feature = "alloc")]
|
||||
name: _time_zone_name.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::TzDb(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
#[cfg(feature = "tzdb-concatenated")]
|
||||
ConcatenatedMissingIanaIdentifiers => f.write_str(
|
||||
"found no IANA time zone identifiers in \
|
||||
concatenated tzdata file",
|
||||
),
|
||||
#[cfg(all(feature = "std", not(feature = "tzdb-concatenated")))]
|
||||
DisabledConcatenated => {
|
||||
f.write_str("system concatenated tzdb unavailable")
|
||||
}
|
||||
#[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",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
8
src/error/tz/mod.rs
Normal file
8
src/error/tz/mod.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
pub(crate) mod ambiguous;
|
||||
pub(crate) mod concatenated;
|
||||
pub(crate) mod db;
|
||||
pub(crate) mod offset;
|
||||
pub(crate) mod posix;
|
||||
pub(crate) mod system;
|
||||
pub(crate) mod timezone;
|
||||
pub(crate) mod zic;
|
||||
110
src/error/tz/offset.rs
Normal file
110
src/error/tz/offset.rs
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
use crate::{
|
||||
error,
|
||||
tz::{Offset, TimeZone},
|
||||
Unit,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
ConvertDateTimeToTimestamp {
|
||||
offset: Offset,
|
||||
},
|
||||
OverflowAddSignedDuration,
|
||||
OverflowSignedDuration,
|
||||
ResolveRejectFold {
|
||||
given: Offset,
|
||||
before: Offset,
|
||||
after: Offset,
|
||||
tz: TimeZone,
|
||||
},
|
||||
ResolveRejectGap {
|
||||
given: Offset,
|
||||
before: Offset,
|
||||
after: Offset,
|
||||
tz: TimeZone,
|
||||
},
|
||||
ResolveRejectUnambiguous {
|
||||
given: Offset,
|
||||
offset: Offset,
|
||||
tz: TimeZone,
|
||||
},
|
||||
RoundInvalidUnit {
|
||||
unit: Unit,
|
||||
},
|
||||
RoundOverflow,
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::TzOffset(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
ConvertDateTimeToTimestamp { offset } => write!(
|
||||
f,
|
||||
"converting datetime with time zone offset `{offset}` \
|
||||
to timestamp overflowed",
|
||||
),
|
||||
OverflowAddSignedDuration => f.write_str(
|
||||
"adding signed duration to time zone offset overflowed",
|
||||
),
|
||||
OverflowSignedDuration => {
|
||||
f.write_str("signed duration overflows time zone offset")
|
||||
}
|
||||
ResolveRejectFold { given, before, after, ref tz } => write!(
|
||||
f,
|
||||
"datetime could not resolve to timestamp \
|
||||
since `reject` conflict resolution was chosen, and \
|
||||
because datetime has offset `{given}`, but the time \
|
||||
zone `{tzname}` for the given datetime falls in a fold \
|
||||
between offsets `{before}` and `{after}`, neither of which \
|
||||
match the offset",
|
||||
tzname = tz.diagnostic_name(),
|
||||
),
|
||||
ResolveRejectGap { given, before, after, ref tz } => write!(
|
||||
f,
|
||||
"datetime could not resolve to timestamp \
|
||||
since `reject` conflict resolution was chosen, and \
|
||||
because datetime has offset `{given}`, but the time \
|
||||
zone `{tzname}` for the given datetime falls in a gap \
|
||||
(between offsets `{before}` and `{after}`), and all \
|
||||
offsets for a gap are regarded as invalid",
|
||||
tzname = tz.diagnostic_name(),
|
||||
),
|
||||
ResolveRejectUnambiguous { given, offset, ref tz } => write!(
|
||||
f,
|
||||
"datetime could not resolve to a timestamp since \
|
||||
`reject` conflict resolution was chosen, and because \
|
||||
datetime has offset `{given}`, but the time \
|
||||
zone `{tzname}` for the given datetime \
|
||||
unambiguously has offset `{offset}`",
|
||||
tzname = tz.diagnostic_name(),
|
||||
),
|
||||
RoundInvalidUnit { unit } => write!(
|
||||
f,
|
||||
"rounding time zone offset failed because \
|
||||
a unit of {unit} was provided, \
|
||||
but time zone offset rounding \
|
||||
can only use hours, minutes or seconds",
|
||||
unit = unit.plural(),
|
||||
),
|
||||
RoundOverflow => f.write_str(
|
||||
"rounding time zone offset resulted in a duration \
|
||||
that overflows",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/error/tz/posix.rs
Normal file
35
src/error/tz/posix.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
use crate::error;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
ColonPrefixInvalidUtf8,
|
||||
InvalidPosixTz,
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::TzPosix(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
ColonPrefixInvalidUtf8 => f.write_str(
|
||||
"POSIX time zone string with a `:` prefix \
|
||||
contains invalid UTF-8",
|
||||
),
|
||||
InvalidPosixTz => f.write_str("invalid POSIX time zone string"),
|
||||
}
|
||||
}
|
||||
}
|
||||
104
src/error/tz/system.rs
Normal file
104
src/error/tz/system.rs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
#[cfg(not(feature = "tz-system"))]
|
||||
pub(crate) use self::disabled::*;
|
||||
#[cfg(feature = "tz-system")]
|
||||
pub(crate) use self::enabled::*;
|
||||
|
||||
#[cfg(not(feature = "tz-system"))]
|
||||
mod disabled {
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, _: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tz-system")]
|
||||
mod enabled {
|
||||
use crate::error;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
FailedEnvTz,
|
||||
FailedEnvTzAsTzif,
|
||||
FailedPosixTzAndUtf8,
|
||||
FailedSystemTimeZone,
|
||||
FailedUnnamedTzifInvalid,
|
||||
FailedUnnamedTzifRead,
|
||||
#[cfg(windows)]
|
||||
WindowsMissingIanaMapping,
|
||||
#[cfg(windows)]
|
||||
WindowsTimeZoneKeyName,
|
||||
#[cfg(windows)]
|
||||
WindowsUtf16DecodeInvalid,
|
||||
#[cfg(windows)]
|
||||
WindowsUtf16DecodeNul,
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::TzSystem(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
FailedEnvTz => f.write_str(
|
||||
"`TZ` environment variable set, but failed to read value",
|
||||
),
|
||||
FailedEnvTzAsTzif => f.write_str(
|
||||
"failed to read `TZ` environment variable value \
|
||||
as a TZif file after attempting (and failing) a tzdb \
|
||||
lookup for that same value",
|
||||
),
|
||||
FailedPosixTzAndUtf8 => f.write_str(
|
||||
"failed to parse `TZ` environment variable as either \
|
||||
a POSIX time zone transition string or as valid UTF-8",
|
||||
),
|
||||
FailedSystemTimeZone => {
|
||||
f.write_str("failed to find system time zone")
|
||||
}
|
||||
FailedUnnamedTzifInvalid => f.write_str(
|
||||
"found invalid TZif data in unnamed time zone file",
|
||||
),
|
||||
FailedUnnamedTzifRead => f.write_str(
|
||||
"failed to read TZif data from unnamed time zone file",
|
||||
),
|
||||
#[cfg(windows)]
|
||||
WindowsMissingIanaMapping => f.write_str(
|
||||
"found Windows time zone name, \
|
||||
but could not find a mapping for it to an \
|
||||
IANA time zone name",
|
||||
),
|
||||
#[cfg(windows)]
|
||||
WindowsTimeZoneKeyName => f.write_str(
|
||||
"could not get `TimeZoneKeyName` from \
|
||||
winapi `DYNAMIC_TIME_ZONE_INFORMATION`",
|
||||
),
|
||||
#[cfg(windows)]
|
||||
WindowsUtf16DecodeInvalid => f.write_str(
|
||||
"failed to convert `u16` slice to UTF-8 \
|
||||
(invalid UTF-16)",
|
||||
),
|
||||
#[cfg(windows)]
|
||||
WindowsUtf16DecodeNul => f.write_str(
|
||||
"failed to convert `u16` slice to UTF-8 \
|
||||
(no NUL terminator found)",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
40
src/error/tz/timezone.rs
Normal file
40
src/error/tz/timezone.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
323
src/error/tz/zic.rs
Normal file
323
src/error/tz/zic.rs
Normal file
|
|
@ -0,0 +1,323 @@
|
|||
#[cfg(not(test))]
|
||||
pub(crate) use self::disabled::*;
|
||||
#[cfg(test)]
|
||||
pub(crate) use self::enabled::*;
|
||||
|
||||
#[cfg(not(test))]
|
||||
mod disabled {
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, _: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod enabled {
|
||||
use alloc::boxed::Box;
|
||||
|
||||
use crate::error;
|
||||
|
||||
// `man zic` says that the max line length including the line
|
||||
// terminator is 2048. The `core::str::Lines` iterator doesn't include
|
||||
// the terminator, so we subtract 1 to account for that. Note that this
|
||||
// could potentially allow one extra byte in the case of a \r\n line
|
||||
// terminator, but this seems fine.
|
||||
pub(crate) const MAX_LINE_LEN: usize = 2047;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
DuplicateLink { name: Box<str> },
|
||||
DuplicateLinkZone { name: Box<str> },
|
||||
DuplicateZone { name: Box<str> },
|
||||
DuplicateZoneLink { name: Box<str> },
|
||||
ExpectedCloseQuote,
|
||||
ExpectedColonAfterHour,
|
||||
ExpectedColonAfterMinute,
|
||||
ExpectedContinuationZoneThreeFields,
|
||||
ExpectedContinuationZoneLine { name: Box<str> },
|
||||
ExpectedDotAfterSeconds,
|
||||
ExpectedFirstZoneFourFields,
|
||||
ExpectedLinkTwoFields,
|
||||
ExpectedMinuteAfterHours,
|
||||
ExpectedNameBegin,
|
||||
ExpectedNanosecondDigits,
|
||||
ExpectedNonEmptyAbbreviation,
|
||||
ExpectedNonEmptyAt,
|
||||
ExpectedNonEmptyName,
|
||||
ExpectedNonEmptySave,
|
||||
ExpectedNonEmptyZoneName,
|
||||
ExpectedNothingAfterTime,
|
||||
ExpectedRuleNineFields { got: usize },
|
||||
ExpectedSecondAfterMinutes,
|
||||
ExpectedTimeOneHour,
|
||||
ExpectedUntilYear,
|
||||
ExpectedWhitespaceAfterQuotedField,
|
||||
ExpectedZoneNameComponentNoDots { component: Box<str> },
|
||||
FailedContinuationZone,
|
||||
FailedLinkLine,
|
||||
FailedParseDay,
|
||||
FailedParseFieldAt,
|
||||
FailedParseFieldFormat,
|
||||
FailedParseFieldFrom,
|
||||
FailedParseFieldIn,
|
||||
FailedParseFieldLetters,
|
||||
FailedParseFieldLinkName,
|
||||
FailedParseFieldLinkTarget,
|
||||
FailedParseFieldName,
|
||||
FailedParseFieldOn,
|
||||
FailedParseFieldRules,
|
||||
FailedParseFieldSave,
|
||||
FailedParseFieldStdOff,
|
||||
FailedParseFieldTo,
|
||||
FailedParseFieldUntil,
|
||||
FailedParseHour,
|
||||
FailedParseMinute,
|
||||
FailedParseMonth,
|
||||
FailedParseNanosecond,
|
||||
FailedParseSecond,
|
||||
FailedParseTimeDuration,
|
||||
FailedParseYear,
|
||||
FailedRule { name: Box<str> },
|
||||
FailedRuleLine,
|
||||
FailedZoneFirst,
|
||||
Line { number: usize },
|
||||
LineMaxLength,
|
||||
LineNul,
|
||||
LineOverflow,
|
||||
InvalidAbbreviation,
|
||||
InvalidRuleYear { start: i16, end: i16 },
|
||||
InvalidUtf8,
|
||||
UnrecognizedAtTimeSuffix,
|
||||
UnrecognizedDayOfMonthFormat,
|
||||
UnrecognizedDayOfWeek,
|
||||
UnrecognizedMonthName,
|
||||
UnrecognizedSaveTimeSuffix,
|
||||
UnrecognizedTrailingTimeDuration,
|
||||
UnrecognizedZicLine,
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::TzZic(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
DuplicateLink { ref name } => {
|
||||
write!(f, "found duplicate link with name `{name}`")
|
||||
}
|
||||
DuplicateLinkZone { ref name } => write!(
|
||||
f,
|
||||
"found link with name `{name}` that conflicts \
|
||||
with a zone of the same name",
|
||||
),
|
||||
DuplicateZone { ref name } => {
|
||||
write!(f, "found duplicate zone with name `{name}`")
|
||||
}
|
||||
DuplicateZoneLink { ref name } => write!(
|
||||
f,
|
||||
"found zone with name `{name}` that conflicts \
|
||||
with a link of the same name",
|
||||
),
|
||||
ExpectedCloseQuote => {
|
||||
f.write_str("found unclosed quote for field")
|
||||
}
|
||||
ExpectedColonAfterHour => {
|
||||
f.write_str("expected `:` after hours")
|
||||
}
|
||||
ExpectedColonAfterMinute => {
|
||||
f.write_str("expected `:` after minutes")
|
||||
}
|
||||
ExpectedContinuationZoneLine { ref name } => write!(
|
||||
f,
|
||||
"expected continuation zone line for `{name}`, \
|
||||
but found end of data instead",
|
||||
),
|
||||
ExpectedContinuationZoneThreeFields => f.write_str(
|
||||
"expected continuation `ZONE` line \
|
||||
to have at least 3 fields",
|
||||
),
|
||||
ExpectedDotAfterSeconds => {
|
||||
f.write_str("expected `.` after seconds")
|
||||
}
|
||||
ExpectedFirstZoneFourFields => f.write_str(
|
||||
"expected first `ZONE` line to have at least 4 fields",
|
||||
),
|
||||
ExpectedLinkTwoFields => {
|
||||
f.write_str("expected exactly 2 fields after `LINK`")
|
||||
}
|
||||
ExpectedMinuteAfterHours => {
|
||||
f.write_str("expected minute digits after `HH:`")
|
||||
}
|
||||
ExpectedNameBegin => f.write_str(
|
||||
"`NAME` field cannot begin with a digit, `+` or `-`, \
|
||||
but found `NAME` that begins with one of those",
|
||||
),
|
||||
ExpectedNanosecondDigits => {
|
||||
f.write_str("expected nanosecond digits after `HH:MM:SS.`")
|
||||
}
|
||||
ExpectedNonEmptyAbbreviation => f.write_str(
|
||||
"empty time zone abbreviations are not allowed",
|
||||
),
|
||||
ExpectedNonEmptyAt => {
|
||||
f.write_str("`AT` field for rule cannot be empty")
|
||||
}
|
||||
ExpectedNonEmptyName => {
|
||||
f.write_str("`NAME` field for rule cannot be empty")
|
||||
}
|
||||
ExpectedNonEmptySave => {
|
||||
f.write_str("`SAVE` field for rule cannot be empty")
|
||||
}
|
||||
ExpectedNonEmptyZoneName => {
|
||||
f.write_str("zone names cannot be empty")
|
||||
}
|
||||
ExpectedNothingAfterTime => f.write_str(
|
||||
"expected no more fields after time of day, \
|
||||
but found at least one",
|
||||
),
|
||||
ExpectedRuleNineFields { got } => write!(
|
||||
f,
|
||||
"expected exactly 9 fields for rule, \
|
||||
but found {got} fields",
|
||||
),
|
||||
ExpectedSecondAfterMinutes => {
|
||||
f.write_str("expected second digits after `HH:MM:`")
|
||||
}
|
||||
ExpectedTimeOneHour => f.write_str(
|
||||
"expected time duration to contain \
|
||||
at least one hour digit",
|
||||
),
|
||||
ExpectedUntilYear => f.write_str("expected at least a year"),
|
||||
ExpectedWhitespaceAfterQuotedField => {
|
||||
f.write_str("expected whitespace after quoted field")
|
||||
}
|
||||
ExpectedZoneNameComponentNoDots { ref component } => write!(
|
||||
f,
|
||||
"component `{component}` in zone name cannot \
|
||||
be \".\" or \"..\"",
|
||||
),
|
||||
FailedContinuationZone => {
|
||||
f.write_str("failed to parse continuation `Zone` line")
|
||||
}
|
||||
FailedLinkLine => f.write_str("failed to parse `Link` line"),
|
||||
FailedParseDay => f.write_str("failed to parse day"),
|
||||
FailedParseFieldAt => {
|
||||
f.write_str("failed to parse `NAME` field")
|
||||
}
|
||||
FailedParseFieldFormat => {
|
||||
f.write_str("failed to parse `FORMAT` field")
|
||||
}
|
||||
FailedParseFieldFrom => {
|
||||
f.write_str("failed to parse `FROM` field")
|
||||
}
|
||||
FailedParseFieldIn => {
|
||||
f.write_str("failed to parse `IN` field")
|
||||
}
|
||||
FailedParseFieldLetters => {
|
||||
f.write_str("failed to parse `LETTERS` field")
|
||||
}
|
||||
FailedParseFieldLinkName => {
|
||||
f.write_str("failed to parse `LINK` name field")
|
||||
}
|
||||
FailedParseFieldLinkTarget => {
|
||||
f.write_str("failed to parse `LINK` target field")
|
||||
}
|
||||
FailedParseFieldName => {
|
||||
f.write_str("failed to parse `NAME` field")
|
||||
}
|
||||
FailedParseFieldOn => {
|
||||
f.write_str("failed to parse `ON` field")
|
||||
}
|
||||
FailedParseFieldRules => {
|
||||
f.write_str("failed to parse `RULES` field")
|
||||
}
|
||||
FailedParseFieldSave => {
|
||||
f.write_str("failed to parse `SAVE` field")
|
||||
}
|
||||
FailedParseFieldStdOff => {
|
||||
f.write_str("failed to parse `STDOFF` field")
|
||||
}
|
||||
FailedParseFieldTo => {
|
||||
f.write_str("failed to parse `TO` field")
|
||||
}
|
||||
FailedParseFieldUntil => {
|
||||
f.write_str("failed to parse `UNTIL` field")
|
||||
}
|
||||
FailedParseHour => f.write_str("failed to parse hour"),
|
||||
FailedParseMinute => f.write_str("failed to parse minute"),
|
||||
FailedParseMonth => f.write_str("failed to parse month"),
|
||||
FailedParseNanosecond => {
|
||||
f.write_str("failed to parse nanosecond")
|
||||
}
|
||||
FailedParseSecond => f.write_str("failed to parse second"),
|
||||
FailedParseTimeDuration => {
|
||||
f.write_str("failed to parse time duration")
|
||||
}
|
||||
FailedParseYear => f.write_str("failed to parse year"),
|
||||
FailedRule { name: ref rule } => {
|
||||
write!(f, "failed to parse rule `{rule}`")
|
||||
}
|
||||
FailedRuleLine => f.write_str("failed to parse `Rule` line"),
|
||||
FailedZoneFirst => {
|
||||
f.write_str("failed to parse first `Zone` line")
|
||||
}
|
||||
InvalidAbbreviation => f.write_str(
|
||||
"time zone abbreviation \
|
||||
contains invalid character; only \"+\", \"-\" and \
|
||||
ASCII alpha-numeric characters are allowed",
|
||||
),
|
||||
InvalidRuleYear { start, end } => write!(
|
||||
f,
|
||||
"found start year={start} \
|
||||
to be greater than end year={end}"
|
||||
),
|
||||
InvalidUtf8 => f.write_str("invalid UTF-8"),
|
||||
Line { number } => write!(f, "line {number}"),
|
||||
LineMaxLength => write!(
|
||||
f,
|
||||
"found line with length that exceeds \
|
||||
max length of {MAX_LINE_LEN}",
|
||||
),
|
||||
LineNul => f.write_str(
|
||||
"found line with NUL byte, which isn't allowed",
|
||||
),
|
||||
LineOverflow => f.write_str("line count overflowed"),
|
||||
UnrecognizedAtTimeSuffix => {
|
||||
f.write_str("unrecognized `AT` time suffix")
|
||||
}
|
||||
UnrecognizedDayOfMonthFormat => {
|
||||
f.write_str("unrecognized format for day-of-month")
|
||||
}
|
||||
UnrecognizedDayOfWeek => {
|
||||
f.write_str("unrecognized day of the week")
|
||||
}
|
||||
UnrecognizedMonthName => {
|
||||
f.write_str("unrecognized month name")
|
||||
}
|
||||
UnrecognizedSaveTimeSuffix => {
|
||||
f.write_str("unrecognized `SAVE` time suffix")
|
||||
}
|
||||
UnrecognizedTrailingTimeDuration => {
|
||||
f.write_str("found unrecognized suffix in time duration")
|
||||
}
|
||||
UnrecognizedZicLine => f.write_str("unrecognized zic line"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
194
src/error/util.rs
Normal file
194
src/error/util.rs
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
use crate::{error, util::escape::Byte, Unit};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum RoundingIncrementError {
|
||||
ForDateTime,
|
||||
ForSpan,
|
||||
ForTime,
|
||||
ForTimestamp,
|
||||
GreaterThanZero { unit: Unit },
|
||||
InvalidDivide { unit: Unit, must_divide: i64 },
|
||||
Unsupported { unit: Unit },
|
||||
}
|
||||
|
||||
impl From<RoundingIncrementError> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: RoundingIncrementError) -> error::Error {
|
||||
error::ErrorKind::RoundingIncrement(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl error::IntoError for RoundingIncrementError {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for RoundingIncrementError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::RoundingIncrementError::*;
|
||||
|
||||
match *self {
|
||||
ForDateTime => f.write_str("failed rounding datetime"),
|
||||
ForSpan => f.write_str("failed rounding span"),
|
||||
ForTime => f.write_str("failed rounding time"),
|
||||
ForTimestamp => f.write_str("failed rounding timestamp"),
|
||||
GreaterThanZero { unit } => write!(
|
||||
f,
|
||||
"rounding increment for {unit} must be greater than zero",
|
||||
unit = unit.plural(),
|
||||
),
|
||||
InvalidDivide { unit, must_divide } => write!(
|
||||
f,
|
||||
"increment for rounding to {unit} \
|
||||
must be 1) less than {must_divide}, 2) divide into \
|
||||
it evenly and 3) greater than zero",
|
||||
unit = unit.plural(),
|
||||
),
|
||||
Unsupported { unit } => write!(
|
||||
f,
|
||||
"rounding to {unit} is not supported",
|
||||
unit = unit.plural(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum ParseIntError {
|
||||
NoDigitsFound,
|
||||
InvalidDigit(u8),
|
||||
TooBig,
|
||||
}
|
||||
|
||||
impl From<ParseIntError> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: ParseIntError) -> error::Error {
|
||||
error::ErrorKind::ParseInt(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl error::IntoError for ParseIntError {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for ParseIntError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::ParseIntError::*;
|
||||
|
||||
match *self {
|
||||
NoDigitsFound => write!(f, "invalid number, no digits found"),
|
||||
InvalidDigit(got) => {
|
||||
write!(f, "invalid digit, expected 0-9 but got {}", Byte(got))
|
||||
}
|
||||
TooBig => {
|
||||
write!(f, "number too big to parse into 64-bit integer")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum ParseFractionError {
|
||||
NoDigitsFound,
|
||||
TooManyDigits,
|
||||
InvalidDigit(u8),
|
||||
TooBig,
|
||||
}
|
||||
|
||||
impl ParseFractionError {
|
||||
pub(crate) const MAX_PRECISION: usize = 9;
|
||||
}
|
||||
|
||||
impl From<ParseFractionError> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: ParseFractionError) -> error::Error {
|
||||
error::ErrorKind::ParseFraction(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl error::IntoError for ParseFractionError {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for ParseFractionError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::ParseFractionError::*;
|
||||
|
||||
match *self {
|
||||
NoDigitsFound => write!(f, "invalid fraction, no digits found"),
|
||||
TooManyDigits => write!(
|
||||
f,
|
||||
"invalid fraction, too many digits \
|
||||
(at most {max} are allowed)",
|
||||
max = ParseFractionError::MAX_PRECISION,
|
||||
),
|
||||
InvalidDigit(got) => {
|
||||
write!(
|
||||
f,
|
||||
"invalid fractional digit, expected 0-9 but got {}",
|
||||
Byte(got)
|
||||
)
|
||||
}
|
||||
TooBig => {
|
||||
write!(
|
||||
f,
|
||||
"fractional number too big to parse into 64-bit integer"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct OsStrUtf8Error {
|
||||
#[cfg(feature = "std")]
|
||||
value: alloc::boxed::Box<std::ffi::OsStr>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl From<&std::ffi::OsStr> for OsStrUtf8Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(value: &std::ffi::OsStr) -> OsStrUtf8Error {
|
||||
OsStrUtf8Error { value: value.into() }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OsStrUtf8Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: OsStrUtf8Error) -> error::Error {
|
||||
error::ErrorKind::OsStrUtf8(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl error::IntoError for OsStrUtf8Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for OsStrUtf8Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
#[cfg(feature = "std")]
|
||||
{
|
||||
write!(
|
||||
f,
|
||||
"environment value `{value:?}` is not valid UTF-8",
|
||||
value = self.value
|
||||
)
|
||||
}
|
||||
#[cfg(not(feature = "std"))]
|
||||
{
|
||||
write!(f, "<BUG: SHOULD NOT EXIST>")
|
||||
}
|
||||
}
|
||||
}
|
||||
71
src/error/zoned.rs
Normal file
71
src/error/zoned.rs
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
use crate::{error, Unit};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Error {
|
||||
AddDateTime,
|
||||
AddDays,
|
||||
AddTimestamp,
|
||||
ConvertDateTimeToTimestamp,
|
||||
ConvertIntermediateDatetime,
|
||||
FailedLengthOfDay,
|
||||
FailedSpanNanoseconds,
|
||||
FailedStartOfDay,
|
||||
MismatchTimeZoneUntil { largest: Unit },
|
||||
}
|
||||
|
||||
impl From<Error> for error::Error {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn from(err: Error) -> error::Error {
|
||||
error::ErrorKind::Zoned(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl error::IntoError for Error {
|
||||
fn into_error(self) -> error::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
use self::Error::*;
|
||||
|
||||
match *self {
|
||||
AddDateTime => f.write_str(
|
||||
"failed to add span to datetime from zoned datetime",
|
||||
),
|
||||
AddDays => {
|
||||
f.write_str("failed to add days to date in zoned datetime")
|
||||
}
|
||||
AddTimestamp => f.write_str(
|
||||
"failed to add span to timestamp from zoned datetime",
|
||||
),
|
||||
ConvertDateTimeToTimestamp => {
|
||||
f.write_str("failed to convert civil datetime to timestamp")
|
||||
}
|
||||
ConvertIntermediateDatetime => f.write_str(
|
||||
"failed to convert intermediate datetime \
|
||||
to zoned timestamp",
|
||||
),
|
||||
FailedLengthOfDay => f.write_str(
|
||||
"failed to add 1 day to zoned datetime to find length of day",
|
||||
),
|
||||
FailedSpanNanoseconds => f.write_str(
|
||||
"failed to compute span in nanoseconds \
|
||||
between zoned datetimes",
|
||||
),
|
||||
FailedStartOfDay => {
|
||||
f.write_str("failed to find start of day for zoned datetime")
|
||||
}
|
||||
MismatchTimeZoneUntil { largest } => write!(
|
||||
f,
|
||||
"computing the span between zoned datetimes, with \
|
||||
{largest} units, requires that the time zones are \
|
||||
equivalent, but the zoned datetimes have distinct \
|
||||
time zones",
|
||||
largest = largest.singular(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -256,7 +256,9 @@ assert_eq!(dur, Duration::new(30 * 24 * 60 * 60 + 38_016, 0));
|
|||
// In contrast, Jiff will reject `1M`:
|
||||
assert_eq!(
|
||||
"1M".parse::<jiff::Span>().unwrap_err().to_string(),
|
||||
"failed to parse \"1M\" in the \"friendly\" format: expected to find unit designator suffix (e.g., 'years' or 'secs'), but found input beginning with \"M\" instead",
|
||||
"failed to parse input in the \"friendly\" duration format: \
|
||||
expected to find unit designator suffix \
|
||||
(e.g., `years` or `secs`) after parsing integer",
|
||||
);
|
||||
|
||||
# Ok::<(), Box<dyn std::error::Error>>(())
|
||||
|
|
@ -335,7 +337,9 @@ assert_eq!(
|
|||
// Jiff is saving you from doing something wrong
|
||||
assert_eq!(
|
||||
"1 day".parse::<SignedDuration>().unwrap_err().to_string(),
|
||||
"failed to parse \"1 day\" in the \"friendly\" format: parsing day units into a `SignedDuration` is not supported (perhaps try parsing into a `Span` instead)",
|
||||
"failed to parse input in the \"friendly\" duration format: \
|
||||
parsing calendar units (days in this case) in this context \
|
||||
is not supported (perhaps try parsing into a `jiff::Span` instead)",
|
||||
);
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
use crate::{
|
||||
error::{err, ErrorContext},
|
||||
error::{fmt::friendly::Error as E, ErrorContext},
|
||||
fmt::{
|
||||
friendly::parser_label,
|
||||
util::{parse_temporal_fraction, DurationUnits},
|
||||
Parsed,
|
||||
},
|
||||
util::{c::Sign, escape, parse},
|
||||
util::{c::Sign, parse},
|
||||
Error, SignedDuration, Span, Unit,
|
||||
};
|
||||
|
||||
|
|
@ -188,12 +188,7 @@ impl SpanParser {
|
|||
}
|
||||
|
||||
let input = input.as_ref();
|
||||
imp(self, input).with_context(|| {
|
||||
err!(
|
||||
"failed to parse {input:?} in the \"friendly\" format",
|
||||
input = escape::Bytes(input)
|
||||
)
|
||||
})
|
||||
imp(self, input).context(E::Failed)
|
||||
}
|
||||
|
||||
/// Run the parser on the given string (which may be plain bytes) and,
|
||||
|
|
@ -248,12 +243,7 @@ impl SpanParser {
|
|||
}
|
||||
|
||||
let input = input.as_ref();
|
||||
imp(self, input).with_context(|| {
|
||||
err!(
|
||||
"failed to parse {input:?} in the \"friendly\" format",
|
||||
input = escape::Bytes(input)
|
||||
)
|
||||
})
|
||||
imp(self, input).context(E::Failed)
|
||||
}
|
||||
|
||||
/// Run the parser on the given string (which may be plain bytes) and,
|
||||
|
|
@ -312,12 +302,7 @@ impl SpanParser {
|
|||
}
|
||||
|
||||
let input = input.as_ref();
|
||||
imp(self, input).with_context(|| {
|
||||
err!(
|
||||
"failed to parse {input:?} in the \"friendly\" format",
|
||||
input = escape::Bytes(input)
|
||||
)
|
||||
})
|
||||
imp(self, input).context(E::Failed)
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "perf-inline", inline(always))]
|
||||
|
|
@ -327,7 +312,7 @@ impl SpanParser {
|
|||
builder: &mut DurationUnits,
|
||||
) -> Result<Parsed<'i, ()>, Error> {
|
||||
if input.is_empty() {
|
||||
return Err(err!("an empty string is not a valid duration"));
|
||||
return Err(Error::from(E::Empty));
|
||||
}
|
||||
// Guard prefix sign parsing to avoid the function call, which is
|
||||
// marked unlineable to keep the fast path tighter.
|
||||
|
|
@ -342,11 +327,7 @@ impl SpanParser {
|
|||
|
||||
let Parsed { value, input } = self.parse_unit_value(input)?;
|
||||
let Some(first_unit_value) = value else {
|
||||
return Err(err!(
|
||||
"parsing a friendly duration requires it to start \
|
||||
with a unit value (a decimal integer) after an \
|
||||
optional sign, but no integer was found",
|
||||
));
|
||||
return Err(Error::from(E::ExpectedIntegerAfterSign));
|
||||
};
|
||||
|
||||
let Parsed { input, .. } =
|
||||
|
|
@ -434,11 +415,7 @@ impl SpanParser {
|
|||
parsed_any_after_comma = true;
|
||||
}
|
||||
if !parsed_any_after_comma {
|
||||
return Err(err!(
|
||||
"found comma at the end of duration, \
|
||||
but a comma indicates at least one more \
|
||||
unit follows",
|
||||
));
|
||||
return Err(Error::from(E::ExpectedOneMoreUnitAfterComma));
|
||||
}
|
||||
Ok(Parsed { value: (), input })
|
||||
}
|
||||
|
|
@ -454,10 +431,13 @@ impl SpanParser {
|
|||
input: &'i [u8],
|
||||
hour: u64,
|
||||
) -> Result<Parsed<'i, Option<HMS>>, Error> {
|
||||
if !input.first().map_or(false, |&b| b == b':') {
|
||||
let Some((&first, tail)) = input.split_first() else {
|
||||
return Ok(Parsed { input, value: None });
|
||||
};
|
||||
if first != b':' {
|
||||
return Ok(Parsed { input, value: None });
|
||||
}
|
||||
let Parsed { input, value } = self.parse_hms(&input[1..], hour)?;
|
||||
let Parsed { input, value } = self.parse_hms(tail, hour)?;
|
||||
Ok(Parsed { input, value: Some(value) })
|
||||
}
|
||||
|
||||
|
|
@ -477,26 +457,16 @@ impl SpanParser {
|
|||
hour: u64,
|
||||
) -> Result<Parsed<'i, HMS>, Error> {
|
||||
let Parsed { input, value } = self.parse_unit_value(input)?;
|
||||
let Some(minute) = value else {
|
||||
return Err(err!(
|
||||
"expected to parse minute in 'HH:MM:SS' format \
|
||||
following parsed hour of {hour}",
|
||||
));
|
||||
};
|
||||
if !input.first().map_or(false, |&b| b == b':') {
|
||||
return Err(err!(
|
||||
"when parsing 'HH:MM:SS' format, expected to \
|
||||
see a ':' after the parsed minute of {minute}",
|
||||
));
|
||||
let minute = value.ok_or(E::ExpectedMinuteAfterHour)?;
|
||||
|
||||
let (&first, input) =
|
||||
input.split_first().ok_or(E::ExpectedColonAfterMinute)?;
|
||||
if first != b':' {
|
||||
return Err(Error::from(E::ExpectedColonAfterMinute));
|
||||
}
|
||||
let input = &input[1..];
|
||||
|
||||
let Parsed { input, value } = self.parse_unit_value(input)?;
|
||||
let Some(second) = value else {
|
||||
return Err(err!(
|
||||
"expected to parse second in 'HH:MM:SS' format \
|
||||
following parsed minute of {minute}",
|
||||
));
|
||||
};
|
||||
let second = value.ok_or(E::ExpectedSecondAfterMinute)?;
|
||||
let (fraction, input) =
|
||||
if input.first().map_or(false, |&b| b == b'.' || b == b',') {
|
||||
let parsed = parse_temporal_fraction(input)?;
|
||||
|
|
@ -540,22 +510,8 @@ impl SpanParser {
|
|||
&self,
|
||||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, Unit>, Error> {
|
||||
let Some((unit, len)) = parser_label::find(input) else {
|
||||
if input.is_empty() {
|
||||
return Err(err!(
|
||||
"expected to find unit designator suffix \
|
||||
(e.g., 'years' or 'secs'), \
|
||||
but found end of input",
|
||||
));
|
||||
} else {
|
||||
return Err(err!(
|
||||
"expected to find unit designator suffix \
|
||||
(e.g., 'years' or 'secs'), \
|
||||
but found input beginning with {found:?} instead",
|
||||
found = escape::Bytes(&input[..input.len().min(20)]),
|
||||
));
|
||||
}
|
||||
};
|
||||
let (unit, len) =
|
||||
parser_label::find(input).ok_or(E::ExpectedUnitSuffix)?;
|
||||
Ok(Parsed { value: unit, input: &input[len..] })
|
||||
}
|
||||
|
||||
|
|
@ -606,17 +562,15 @@ impl SpanParser {
|
|||
}
|
||||
// Eat any additional whitespace we find before looking for 'ago'.
|
||||
input = self.parse_optional_whitespace(&input[1..]).input;
|
||||
let (suffix_sign, input) = if input.starts_with(b"ago") {
|
||||
(Some(Sign::Negative), &input[3..])
|
||||
} else {
|
||||
(None, input)
|
||||
};
|
||||
let (suffix_sign, input) =
|
||||
if let Some(tail) = input.strip_prefix(b"ago") {
|
||||
(Some(Sign::Negative), tail)
|
||||
} else {
|
||||
(None, input)
|
||||
};
|
||||
let sign = match (prefix_sign, suffix_sign) {
|
||||
(Some(_), Some(_)) => {
|
||||
return Err(err!(
|
||||
"expected to find either a prefix sign (+/-) or \
|
||||
a suffix sign (ago), but found both",
|
||||
))
|
||||
return Err(Error::from(E::ExpectedOneSign));
|
||||
}
|
||||
(Some(sign), None) => sign,
|
||||
(None, Some(sign)) => sign,
|
||||
|
|
@ -637,24 +591,24 @@ impl SpanParser {
|
|||
#[inline(never)]
|
||||
fn parse_optional_comma<'i>(
|
||||
&self,
|
||||
mut input: &'i [u8],
|
||||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, ()>, Error> {
|
||||
if !input.first().map_or(false, |&b| b == b',') {
|
||||
let Some((&first, tail)) = input.split_first() else {
|
||||
return Ok(Parsed { value: (), input });
|
||||
};
|
||||
if first != b',' {
|
||||
return Ok(Parsed { value: (), input });
|
||||
}
|
||||
input = &input[1..];
|
||||
if input.is_empty() {
|
||||
return Err(err!(
|
||||
"expected whitespace after comma, but found end of input"
|
||||
));
|
||||
|
||||
let (second, input) = tail
|
||||
.split_first()
|
||||
.ok_or(E::ExpectedWhitespaceAfterCommaEndOfInput)?;
|
||||
if !is_whitespace(second) {
|
||||
return Err(Error::from(E::ExpectedWhitespaceAfterComma {
|
||||
byte: *second,
|
||||
}));
|
||||
}
|
||||
if !is_whitespace(&input[0]) {
|
||||
return Err(err!(
|
||||
"expected whitespace after comma, but found {found:?}",
|
||||
found = escape::Byte(input[0]),
|
||||
));
|
||||
}
|
||||
Ok(Parsed { value: (), input: &input[1..] })
|
||||
Ok(Parsed { value: (), input })
|
||||
}
|
||||
|
||||
/// Parses zero or more bytes of ASCII whitespace.
|
||||
|
|
@ -776,35 +730,35 @@ mod tests {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
p(""),
|
||||
@r###"failed to parse "" in the "friendly" format: an empty string is not a valid duration"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: an empty string is not valid"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p(" "),
|
||||
@r###"failed to parse " " in the "friendly" format: parsing a friendly duration requires it to start with a unit value (a decimal integer) after an optional sign, but no integer was found"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: expected duration to start with a unit value (a decimal integer) after an optional sign, but no integer was found"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("a"),
|
||||
@r###"failed to parse "a" in the "friendly" format: parsing a friendly duration requires it to start with a unit value (a decimal integer) after an optional sign, but no integer was found"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: expected duration to start with a unit value (a decimal integer) after an optional sign, but no integer was found"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("2 months 1 year"),
|
||||
@r###"failed to parse "2 months 1 year" in the "friendly" format: found value 1 with unit year after unit month, but units must be written from largest to smallest (and they can't be repeated)"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: found value with unit year after unit month, but units must be written from largest to smallest (and they can't be repeated)"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("1 year 1 mont"),
|
||||
@r###"failed to parse "1 year 1 mont" in the "friendly" format: parsed value 'P1Y1M', but unparsed input "nt" remains (expected no unparsed input)"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: parsed value 'P1Y1M', but unparsed input "nt" remains (expected no unparsed input)"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("2 months,"),
|
||||
@r###"failed to parse "2 months," in the "friendly" format: expected whitespace after comma, but found end of input"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: expected whitespace after comma, but found end of input"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("2 months, "),
|
||||
@r#"failed to parse "2 months, " in the "friendly" format: found comma at the end of duration, but a comma indicates at least one more unit follows"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: found comma at the end of duration, but a comma indicates at least one more unit follows"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("2 months ,"),
|
||||
@r###"failed to parse "2 months ," in the "friendly" format: parsed value 'P2M', but unparsed input "," remains (expected no unparsed input)"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: parsed value 'P2M', but unparsed input "," remains (expected no unparsed input)"#,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -814,19 +768,19 @@ mod tests {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
p("1yago"),
|
||||
@r###"failed to parse "1yago" in the "friendly" format: parsed value 'P1Y', but unparsed input "ago" remains (expected no unparsed input)"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: parsed value 'P1Y', but unparsed input "ago" remains (expected no unparsed input)"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("1 year 1 monthago"),
|
||||
@r###"failed to parse "1 year 1 monthago" in the "friendly" format: parsed value 'P1Y1M', but unparsed input "ago" remains (expected no unparsed input)"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: parsed value 'P1Y1M', but unparsed input "ago" remains (expected no unparsed input)"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("+1 year 1 month ago"),
|
||||
@r###"failed to parse "+1 year 1 month ago" in the "friendly" format: expected to find either a prefix sign (+/-) or a suffix sign (ago), but found both"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: expected to find either a prefix sign (+/-) or a suffix sign (`ago`), but found both"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("-1 year 1 month ago"),
|
||||
@r###"failed to parse "-1 year 1 month ago" in the "friendly" format: expected to find either a prefix sign (+/-) or a suffix sign (ago), but found both"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: expected to find either a prefix sign (+/-) or a suffix sign (`ago`), but found both"#,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -840,7 +794,7 @@ mod tests {
|
|||
// the maximum number of microseconds is subtracted off, and we're
|
||||
// left over with a value that overflows an i64.
|
||||
pe("640330789636854776 micros"),
|
||||
@r#"failed to parse "640330789636854776 micros" in the "friendly" format: failed to set value 640330789636854776 as microsecond unit on span: failed to set nanosecond value 9223372036854776000 (it overflows `i64`) on span determined from 640330789636854776.0"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: failed to set value for microsecond unit on span: failed to set nanosecond value from fractional component"#,
|
||||
);
|
||||
// one fewer is okay
|
||||
insta::assert_snapshot!(
|
||||
|
|
@ -853,7 +807,7 @@ mod tests {
|
|||
// different error path by using an explicit fraction. Here, if
|
||||
// we had x.807 micros, it would parse successfully.
|
||||
pe("640330789636854775.808 micros"),
|
||||
@r#"failed to parse "640330789636854775.808 micros" in the "friendly" format: failed to set nanosecond value 9223372036854775808 (it overflows `i64`) on span determined from 640330789636854775.808000000"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: failed to set nanosecond value from fractional component"#,
|
||||
);
|
||||
// one fewer is okay
|
||||
insta::assert_snapshot!(
|
||||
|
|
@ -868,47 +822,47 @@ mod tests {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
p("19999 years"),
|
||||
@r###"failed to parse "19999 years" in the "friendly" format: failed to set value 19999 as year unit on span: parameter 'years' with value 19999 is not in the required range of -19998..=19998"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: failed to set value for year unit on span: parameter 'years' with value 19999 is not in the required range of -19998..=19998"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("19999 years ago"),
|
||||
@r#"failed to parse "19999 years ago" in the "friendly" format: failed to set value -19999 as year unit on span: parameter 'years' with value -19999 is not in the required range of -19998..=19998"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: failed to set value for year unit on span: parameter 'years' with value -19999 is not in the required range of -19998..=19998"#,
|
||||
);
|
||||
|
||||
insta::assert_snapshot!(
|
||||
p("239977 months"),
|
||||
@r###"failed to parse "239977 months" in the "friendly" format: failed to set value 239977 as month unit on span: parameter 'months' with value 239977 is not in the required range of -239976..=239976"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: failed to set value for month unit on span: parameter 'months' with value 239977 is not in the required range of -239976..=239976"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("239977 months ago"),
|
||||
@r#"failed to parse "239977 months ago" in the "friendly" format: failed to set value -239977 as month unit on span: parameter 'months' with value -239977 is not in the required range of -239976..=239976"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: failed to set value for month unit on span: parameter 'months' with value -239977 is not in the required range of -239976..=239976"#,
|
||||
);
|
||||
|
||||
insta::assert_snapshot!(
|
||||
p("1043498 weeks"),
|
||||
@r###"failed to parse "1043498 weeks" in the "friendly" format: failed to set value 1043498 as week unit on span: parameter 'weeks' with value 1043498 is not in the required range of -1043497..=1043497"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: failed to set value for week unit on span: parameter 'weeks' with value 1043498 is not in the required range of -1043497..=1043497"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("1043498 weeks ago"),
|
||||
@r#"failed to parse "1043498 weeks ago" in the "friendly" format: failed to set value -1043498 as week unit on span: parameter 'weeks' with value -1043498 is not in the required range of -1043497..=1043497"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: failed to set value for week unit on span: parameter 'weeks' with value -1043498 is not in the required range of -1043497..=1043497"#,
|
||||
);
|
||||
|
||||
insta::assert_snapshot!(
|
||||
p("7304485 days"),
|
||||
@r###"failed to parse "7304485 days" in the "friendly" format: failed to set value 7304485 as day unit on span: parameter 'days' with value 7304485 is not in the required range of -7304484..=7304484"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: failed to set value for day unit on span: parameter 'days' with value 7304485 is not in the required range of -7304484..=7304484"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("7304485 days ago"),
|
||||
@r#"failed to parse "7304485 days ago" in the "friendly" format: failed to set value -7304485 as day unit on span: parameter 'days' with value -7304485 is not in the required range of -7304484..=7304484"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: failed to set value for day unit on span: parameter 'days' with value -7304485 is not in the required range of -7304484..=7304484"#,
|
||||
);
|
||||
|
||||
insta::assert_snapshot!(
|
||||
p("9223372036854775808 nanoseconds"),
|
||||
@r#"failed to parse "9223372036854775808 nanoseconds" in the "friendly" format: `9223372036854775808` nanoseconds is too big (or small) to fit into a signed 64-bit integer"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: value for nanoseconds is too big (or small) to fit into a signed 64-bit integer"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("9223372036854775808 nanoseconds ago"),
|
||||
@r#"failed to parse "9223372036854775808 nanoseconds ago" in the "friendly" format: failed to set value -9223372036854775808 as nanosecond unit on span: parameter 'nanoseconds' with value -9223372036854775808 is not in the required range of -9223372036854775807..=9223372036854775807"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: failed to set value for nanosecond unit on span: parameter 'nanoseconds' with value -9223372036854775808 is not in the required range of -9223372036854775807..=9223372036854775807"#,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -918,11 +872,11 @@ mod tests {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
p("1.5 years"),
|
||||
@r#"failed to parse "1.5 years" in the "friendly" format: fractional years are not supported"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: fractional years are not supported"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("1.5 nanos"),
|
||||
@r#"failed to parse "1.5 nanos" in the "friendly" format: fractional nanoseconds are not supported"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: fractional nanoseconds are not supported"#,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -932,19 +886,19 @@ mod tests {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
p("05:"),
|
||||
@r###"failed to parse "05:" in the "friendly" format: expected to parse minute in 'HH:MM:SS' format following parsed hour of 5"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse minute following hour"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("05:06"),
|
||||
@r###"failed to parse "05:06" in the "friendly" format: when parsing 'HH:MM:SS' format, expected to see a ':' after the parsed minute of 6"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse `:` following minute"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("05:06:"),
|
||||
@r###"failed to parse "05:06:" in the "friendly" format: expected to parse second in 'HH:MM:SS' format following parsed minute of 6"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse second following minute"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("2 hours, 05:06:07"),
|
||||
@r#"failed to parse "2 hours, 05:06:07" in the "friendly" format: found `HH:MM:SS` after unit hour, but `HH:MM:SS` can only appear after years, months, weeks or days"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: found `HH:MM:SS` after unit hour, but `HH:MM:SS` can only appear after years, months, weeks or days"#,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -968,7 +922,7 @@ mod tests {
|
|||
);
|
||||
insta::assert_snapshot!(
|
||||
perr("9223372036854775808s"),
|
||||
@r#"failed to parse "9223372036854775808s" in the "friendly" format: `9223372036854775808` seconds is too big (or small) to fit into a signed 64-bit integer"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: value for seconds is too big (or small) to fit into a signed 64-bit integer"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("-9223372036854775808s"),
|
||||
|
|
@ -1032,21 +986,21 @@ mod tests {
|
|||
insta::assert_snapshot!(p("-2562047788015215hours"), @"-PT2562047788015215H");
|
||||
insta::assert_snapshot!(
|
||||
pe("2562047788015216hrs"),
|
||||
@r#"failed to parse "2562047788015216hrs" in the "friendly" format: accumulated `SignedDuration` of `0s` overflowed when adding 2562047788015216 of unit hour"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: accumulated duration overflowed when adding value to unit hour"#,
|
||||
);
|
||||
|
||||
insta::assert_snapshot!(p("153722867280912930minutes"), @"PT2562047788015215H30M");
|
||||
insta::assert_snapshot!(p("153722867280912930minutes ago"), @"-PT2562047788015215H30M");
|
||||
insta::assert_snapshot!(
|
||||
pe("153722867280912931mins"),
|
||||
@r#"failed to parse "153722867280912931mins" in the "friendly" format: accumulated `SignedDuration` of `0s` overflowed when adding 153722867280912931 of unit minute"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: accumulated duration overflowed when adding value to unit minute"#,
|
||||
);
|
||||
|
||||
insta::assert_snapshot!(p("9223372036854775807seconds"), @"PT2562047788015215H30M7S");
|
||||
insta::assert_snapshot!(p("-9223372036854775807seconds"), @"-PT2562047788015215H30M7S");
|
||||
insta::assert_snapshot!(
|
||||
pe("9223372036854775808s"),
|
||||
@r#"failed to parse "9223372036854775808s" in the "friendly" format: `9223372036854775808` seconds is too big (or small) to fit into a signed 64-bit integer"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: value for seconds is too big (or small) to fit into a signed 64-bit integer"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("-9223372036854775808s"),
|
||||
|
|
@ -1060,39 +1014,39 @@ mod tests {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
p(""),
|
||||
@r###"failed to parse "" in the "friendly" format: an empty string is not a valid duration"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: an empty string is not valid"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p(" "),
|
||||
@r###"failed to parse " " in the "friendly" format: parsing a friendly duration requires it to start with a unit value (a decimal integer) after an optional sign, but no integer was found"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: expected duration to start with a unit value (a decimal integer) after an optional sign, but no integer was found"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("5"),
|
||||
@r###"failed to parse "5" in the "friendly" format: expected to find unit designator suffix (e.g., 'years' or 'secs'), but found end of input"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: expected to find unit designator suffix (e.g., `years` or `secs`) after parsing integer"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("a"),
|
||||
@r###"failed to parse "a" in the "friendly" format: parsing a friendly duration requires it to start with a unit value (a decimal integer) after an optional sign, but no integer was found"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: expected duration to start with a unit value (a decimal integer) after an optional sign, but no integer was found"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("2 minutes 1 hour"),
|
||||
@r###"failed to parse "2 minutes 1 hour" in the "friendly" format: found value 1 with unit hour after unit minute, but units must be written from largest to smallest (and they can't be repeated)"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: found value with unit hour after unit minute, but units must be written from largest to smallest (and they can't be repeated)"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("1 hour 1 minut"),
|
||||
@r###"failed to parse "1 hour 1 minut" in the "friendly" format: parsed value 'PT1H1M', but unparsed input "ut" remains (expected no unparsed input)"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: parsed value 'PT1H1M', but unparsed input "ut" remains (expected no unparsed input)"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("2 minutes,"),
|
||||
@r###"failed to parse "2 minutes," in the "friendly" format: expected whitespace after comma, but found end of input"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: expected whitespace after comma, but found end of input"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("2 minutes, "),
|
||||
@r#"failed to parse "2 minutes, " in the "friendly" format: found comma at the end of duration, but a comma indicates at least one more unit follows"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: found comma at the end of duration, but a comma indicates at least one more unit follows"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("2 minutes ,"),
|
||||
@r###"failed to parse "2 minutes ," in the "friendly" format: parsed value 'PT2M', but unparsed input "," remains (expected no unparsed input)"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: parsed value 'PT2M', but unparsed input "," remains (expected no unparsed input)"#,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1102,19 +1056,19 @@ mod tests {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
p("1hago"),
|
||||
@r###"failed to parse "1hago" in the "friendly" format: parsed value 'PT1H', but unparsed input "ago" remains (expected no unparsed input)"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: parsed value 'PT1H', but unparsed input "ago" remains (expected no unparsed input)"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("1 hour 1 minuteago"),
|
||||
@r###"failed to parse "1 hour 1 minuteago" in the "friendly" format: parsed value 'PT1H1M', but unparsed input "ago" remains (expected no unparsed input)"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: parsed value 'PT1H1M', but unparsed input "ago" remains (expected no unparsed input)"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("+1 hour 1 minute ago"),
|
||||
@r###"failed to parse "+1 hour 1 minute ago" in the "friendly" format: expected to find either a prefix sign (+/-) or a suffix sign (ago), but found both"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: expected to find either a prefix sign (+/-) or a suffix sign (`ago`), but found both"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("-1 hour 1 minute ago"),
|
||||
@r###"failed to parse "-1 hour 1 minute ago" in the "friendly" format: expected to find either a prefix sign (+/-) or a suffix sign (ago), but found both"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: expected to find either a prefix sign (+/-) or a suffix sign (`ago`), but found both"#,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1127,7 +1081,7 @@ mod tests {
|
|||
// Unlike `Span`, this just overflows because it can't be parsed
|
||||
// as a 64-bit integer.
|
||||
pe("9223372036854775808 micros"),
|
||||
@r#"failed to parse "9223372036854775808 micros" in the "friendly" format: `9223372036854775808` microseconds is too big (or small) to fit into a signed 64-bit integer"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: value for microseconds is too big (or small) to fit into a signed 64-bit integer"#,
|
||||
);
|
||||
// one fewer is okay
|
||||
insta::assert_snapshot!(
|
||||
|
|
@ -1142,7 +1096,7 @@ mod tests {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
p("1.5 nanos"),
|
||||
@r#"failed to parse "1.5 nanos" in the "friendly" format: fractional nanoseconds are not supported"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: fractional nanoseconds are not supported"#,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1152,19 +1106,19 @@ mod tests {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
p("05:"),
|
||||
@r###"failed to parse "05:" in the "friendly" format: expected to parse minute in 'HH:MM:SS' format following parsed hour of 5"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse minute following hour"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("05:06"),
|
||||
@r###"failed to parse "05:06" in the "friendly" format: when parsing 'HH:MM:SS' format, expected to see a ':' after the parsed minute of 6"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse `:` following minute"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("05:06:"),
|
||||
@r###"failed to parse "05:06:" in the "friendly" format: expected to parse second in 'HH:MM:SS' format following parsed minute of 6"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse second following minute"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("2 hours, 05:06:07"),
|
||||
@r#"failed to parse "2 hours, 05:06:07" in the "friendly" format: found `HH:MM:SS` after unit hour, but `HH:MM:SS` can only appear after years, months, weeks or days"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: found `HH:MM:SS` after unit hour, but `HH:MM:SS` can only appear after years, months, weeks or days"#,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1205,11 +1159,11 @@ mod tests {
|
|||
);
|
||||
insta::assert_snapshot!(
|
||||
perr("18446744073709551616s"),
|
||||
@r#"failed to parse "18446744073709551616s" in the "friendly" format: number `18446744073709551616` too big to parse into 64-bit integer"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: number too big to parse into 64-bit integer"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
perr("-1s"),
|
||||
@r#"failed to parse "-1s" in the "friendly" format: cannot parse negative duration into unsigned `std::time::Duration`"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: cannot parse negative duration into unsigned `std::time::Duration`"#,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1263,19 +1217,19 @@ mod tests {
|
|||
insta::assert_snapshot!(p("5124095576030431hours"), @"PT5124095576030431H");
|
||||
insta::assert_snapshot!(
|
||||
pe("5124095576030432hrs"),
|
||||
@r#"failed to parse "5124095576030432hrs" in the "friendly" format: accumulated `SignedDuration` of `0s` overflowed when adding 5124095576030432 of unit hour"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: accumulated duration overflowed when adding value to unit hour"#,
|
||||
);
|
||||
|
||||
insta::assert_snapshot!(p("307445734561825860minutes"), @"PT5124095576030431H");
|
||||
insta::assert_snapshot!(
|
||||
pe("307445734561825861mins"),
|
||||
@r#"failed to parse "307445734561825861mins" in the "friendly" format: accumulated `SignedDuration` of `0s` overflowed when adding 307445734561825861 of unit minute"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: accumulated duration overflowed when adding value to unit minute"#,
|
||||
);
|
||||
|
||||
insta::assert_snapshot!(p("18446744073709551615seconds"), @"PT5124095576030431H15S");
|
||||
insta::assert_snapshot!(
|
||||
pe("18446744073709551616s"),
|
||||
@r#"failed to parse "18446744073709551616s" in the "friendly" format: number `18446744073709551616` too big to parse into 64-bit integer"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: number too big to parse into 64-bit integer"#,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1287,39 +1241,39 @@ mod tests {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
p(""),
|
||||
@r###"failed to parse "" in the "friendly" format: an empty string is not a valid duration"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: an empty string is not valid"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p(" "),
|
||||
@r###"failed to parse " " in the "friendly" format: parsing a friendly duration requires it to start with a unit value (a decimal integer) after an optional sign, but no integer was found"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: expected duration to start with a unit value (a decimal integer) after an optional sign, but no integer was found"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("5"),
|
||||
@r###"failed to parse "5" in the "friendly" format: expected to find unit designator suffix (e.g., 'years' or 'secs'), but found end of input"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: expected to find unit designator suffix (e.g., `years` or `secs`) after parsing integer"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("a"),
|
||||
@r###"failed to parse "a" in the "friendly" format: parsing a friendly duration requires it to start with a unit value (a decimal integer) after an optional sign, but no integer was found"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: expected duration to start with a unit value (a decimal integer) after an optional sign, but no integer was found"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("2 minutes 1 hour"),
|
||||
@r###"failed to parse "2 minutes 1 hour" in the "friendly" format: found value 1 with unit hour after unit minute, but units must be written from largest to smallest (and they can't be repeated)"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: found value with unit hour after unit minute, but units must be written from largest to smallest (and they can't be repeated)"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("1 hour 1 minut"),
|
||||
@r#"failed to parse "1 hour 1 minut" in the "friendly" format: parsed value '3660s', but unparsed input "ut" remains (expected no unparsed input)"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: parsed value '3660s', but unparsed input "ut" remains (expected no unparsed input)"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("2 minutes,"),
|
||||
@r###"failed to parse "2 minutes," in the "friendly" format: expected whitespace after comma, but found end of input"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: expected whitespace after comma, but found end of input"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("2 minutes, "),
|
||||
@r#"failed to parse "2 minutes, " in the "friendly" format: found comma at the end of duration, but a comma indicates at least one more unit follows"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: found comma at the end of duration, but a comma indicates at least one more unit follows"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("2 minutes ,"),
|
||||
@r#"failed to parse "2 minutes ," in the "friendly" format: parsed value '120s', but unparsed input "," remains (expected no unparsed input)"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: parsed value '120s', but unparsed input "," remains (expected no unparsed input)"#,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1331,19 +1285,19 @@ mod tests {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
p("1hago"),
|
||||
@r#"failed to parse "1hago" in the "friendly" format: parsed value '3600s', but unparsed input "ago" remains (expected no unparsed input)"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: parsed value '3600s', but unparsed input "ago" remains (expected no unparsed input)"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("1 hour 1 minuteago"),
|
||||
@r#"failed to parse "1 hour 1 minuteago" in the "friendly" format: parsed value '3660s', but unparsed input "ago" remains (expected no unparsed input)"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: parsed value '3660s', but unparsed input "ago" remains (expected no unparsed input)"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("+1 hour 1 minute ago"),
|
||||
@r###"failed to parse "+1 hour 1 minute ago" in the "friendly" format: expected to find either a prefix sign (+/-) or a suffix sign (ago), but found both"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: expected to find either a prefix sign (+/-) or a suffix sign (`ago`), but found both"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("-1 hour 1 minute ago"),
|
||||
@r###"failed to parse "-1 hour 1 minute ago" in the "friendly" format: expected to find either a prefix sign (+/-) or a suffix sign (ago), but found both"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: expected to find either a prefix sign (+/-) or a suffix sign (`ago`), but found both"#,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1362,7 +1316,7 @@ mod tests {
|
|||
// Unlike `Span`, this just overflows because it can't be parsed
|
||||
// as a 64-bit integer.
|
||||
pe("18446744073709551616 micros"),
|
||||
@r#"failed to parse "18446744073709551616 micros" in the "friendly" format: number `18446744073709551616` too big to parse into 64-bit integer"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: number too big to parse into 64-bit integer"#,
|
||||
);
|
||||
// one fewer is okay
|
||||
insta::assert_snapshot!(
|
||||
|
|
@ -1379,7 +1333,7 @@ mod tests {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
p("1.5 nanos"),
|
||||
@r#"failed to parse "1.5 nanos" in the "friendly" format: fractional nanoseconds are not supported"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: fractional nanoseconds are not supported"#,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1391,19 +1345,19 @@ mod tests {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
p("05:"),
|
||||
@r###"failed to parse "05:" in the "friendly" format: expected to parse minute in 'HH:MM:SS' format following parsed hour of 5"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse minute following hour"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("05:06"),
|
||||
@r###"failed to parse "05:06" in the "friendly" format: when parsing 'HH:MM:SS' format, expected to see a ':' after the parsed minute of 6"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse `:` following minute"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("05:06:"),
|
||||
@r###"failed to parse "05:06:" in the "friendly" format: expected to parse second in 'HH:MM:SS' format following parsed minute of 6"###,
|
||||
@r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse second following minute"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("2 hours, 05:06:07"),
|
||||
@r#"failed to parse "2 hours, 05:06:07" in the "friendly" format: found `HH:MM:SS` after unit hour, but `HH:MM:SS` can only appear after years, months, weeks or days"#,
|
||||
@r#"failed to parse input in the "friendly" duration format: found `HH:MM:SS` after unit hour, but `HH:MM:SS` can only appear after years, months, weeks or days"#,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
fmt::{
|
||||
util::{DecimalFormatter, FractionalFormatter},
|
||||
util::{FractionalFormatter, IntegerFormatter},
|
||||
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())?;
|
||||
wtr.write(Unit::Year, span.get_years().unsigned_abs().into())?;
|
||||
}
|
||||
if span.get_months() != 0 {
|
||||
wtr.write(Unit::Month, span.get_months().unsigned_abs())?;
|
||||
wtr.write(Unit::Month, span.get_months().unsigned_abs().into())?;
|
||||
}
|
||||
if span.get_weeks() != 0 {
|
||||
wtr.write(Unit::Week, span.get_weeks().unsigned_abs())?;
|
||||
wtr.write(Unit::Week, span.get_weeks().unsigned_abs().into())?;
|
||||
}
|
||||
if span.get_days() != 0 {
|
||||
wtr.write(Unit::Day, span.get_days().unsigned_abs())?;
|
||||
wtr.write(Unit::Day, span.get_days().unsigned_abs().into())?;
|
||||
}
|
||||
if span.get_hours() != 0 {
|
||||
wtr.write(Unit::Hour, span.get_hours().unsigned_abs())?;
|
||||
wtr.write(Unit::Hour, span.get_hours().unsigned_abs().into())?;
|
||||
}
|
||||
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 =
|
||||
DecimalFormatter::new().padding(self.padding.unwrap_or(2));
|
||||
IntegerFormatter::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,10 +1366,16 @@ 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)?;
|
||||
wtr.write(
|
||||
Unit::Millisecond,
|
||||
(nanos / NANOS_PER_MILLI).into(),
|
||||
)?;
|
||||
nanos %= NANOS_PER_MILLI;
|
||||
wtr.write(Unit::Microsecond, nanos / NANOS_PER_MICRO)?;
|
||||
wtr.write(Unit::Nanosecond, nanos % NANOS_PER_MICRO)?;
|
||||
wtr.write(
|
||||
Unit::Microsecond,
|
||||
(nanos / NANOS_PER_MICRO).into(),
|
||||
)?;
|
||||
wtr.write(Unit::Nanosecond, (nanos % NANOS_PER_MICRO).into())?;
|
||||
}
|
||||
Some(FractionalUnit::Hour) => {
|
||||
wtr.write_fractional_duration(FractionalUnit::Hour, &dur)?;
|
||||
|
|
@ -1421,7 +1427,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)?;
|
||||
wtr.write(
|
||||
Unit::Millisecond,
|
||||
(nanos / NANOS_PER_MILLI).into(),
|
||||
)?;
|
||||
nanos %= NANOS_PER_MILLI;
|
||||
|
||||
let leftovers = core::time::Duration::new(0, nanos);
|
||||
|
|
@ -1479,7 +1488,7 @@ impl SpanPrinter {
|
|||
// bigger.
|
||||
|
||||
let fmtint =
|
||||
DecimalFormatter::new().padding(self.padding.unwrap_or(2));
|
||||
IntegerFormatter::new().padding(self.padding.unwrap_or(2));
|
||||
let fmtfraction = FractionalFormatter::new().precision(self.precision);
|
||||
|
||||
let mut secs = udur.as_secs();
|
||||
|
|
@ -1609,7 +1618,7 @@ struct DesignatorWriter<'p, 'w, W> {
|
|||
wtr: &'w mut W,
|
||||
desig: Designators,
|
||||
sign: Option<DirectionSign>,
|
||||
fmtint: DecimalFormatter,
|
||||
fmtint: IntegerFormatter,
|
||||
fmtfraction: FractionalFormatter,
|
||||
written_non_zero_unit: bool,
|
||||
}
|
||||
|
|
@ -1624,7 +1633,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 =
|
||||
DecimalFormatter::new().padding(printer.padding.unwrap_or(0));
|
||||
IntegerFormatter::new().padding(printer.padding.unwrap_or(0));
|
||||
let fmtfraction =
|
||||
FractionalFormatter::new().precision(printer.precision);
|
||||
DesignatorWriter {
|
||||
|
|
@ -1670,12 +1679,7 @@ impl<'p, 'w, W: Write> DesignatorWriter<'p, 'w, W> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn write(
|
||||
&mut self,
|
||||
unit: Unit,
|
||||
value: impl Into<u64>,
|
||||
) -> Result<(), Error> {
|
||||
let value = value.into();
|
||||
fn write(&mut self, unit: Unit, value: u64) -> Result<(), Error> {
|
||||
if value == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
|
@ -1729,7 +1733,7 @@ impl<'p, 'w, W: Write> DesignatorWriter<'p, 'w, W> {
|
|||
struct FractionalPrinter {
|
||||
integer: u64,
|
||||
fraction: u32,
|
||||
fmtint: DecimalFormatter,
|
||||
fmtint: IntegerFormatter,
|
||||
fmtfraction: FractionalFormatter,
|
||||
}
|
||||
|
||||
|
|
@ -1746,7 +1750,7 @@ impl FractionalPrinter {
|
|||
fn from_span(
|
||||
span: &Span,
|
||||
unit: FractionalUnit,
|
||||
fmtint: DecimalFormatter,
|
||||
fmtint: IntegerFormatter,
|
||||
fmtfraction: FractionalFormatter,
|
||||
) -> FractionalPrinter {
|
||||
debug_assert!(span.largest_unit() <= Unit::from(unit));
|
||||
|
|
@ -1758,7 +1762,7 @@ impl FractionalPrinter {
|
|||
fn from_duration(
|
||||
dur: &core::time::Duration,
|
||||
unit: FractionalUnit,
|
||||
fmtint: DecimalFormatter,
|
||||
fmtint: IntegerFormatter,
|
||||
fmtfraction: FractionalFormatter,
|
||||
) -> FractionalPrinter {
|
||||
match unit {
|
||||
|
|
|
|||
|
|
@ -166,11 +166,11 @@ and features.)
|
|||
*/
|
||||
|
||||
use crate::{
|
||||
error::{err, Error},
|
||||
error::{fmt::Error as E, Error},
|
||||
util::escape,
|
||||
};
|
||||
|
||||
use self::util::{Decimal, DecimalFormatter, Fractional, FractionalFormatter};
|
||||
use self::util::{Fractional, FractionalFormatter, Integer, IntegerFormatter};
|
||||
|
||||
pub mod friendly;
|
||||
mod offset;
|
||||
|
|
@ -218,12 +218,7 @@ impl<'i, V: core::fmt::Display> Parsed<'i, V> {
|
|||
if self.input.is_empty() {
|
||||
return Ok(self.value);
|
||||
}
|
||||
Err(err!(
|
||||
"parsed value '{value}', but unparsed input {unparsed:?} \
|
||||
remains (expected no unparsed input)",
|
||||
value = self.value,
|
||||
unparsed = escape::Bytes(self.input),
|
||||
))
|
||||
Err(Error::from(E::into_full_error(&self.value, self.input)))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -244,12 +239,7 @@ impl<'i, V> Parsed<'i, V> {
|
|||
if self.input.is_empty() {
|
||||
return Ok(self.value);
|
||||
}
|
||||
Err(err!(
|
||||
"parsed value '{value}', but unparsed input {unparsed:?} \
|
||||
remains (expected no unparsed input)",
|
||||
value = display,
|
||||
unparsed = escape::Bytes(self.input),
|
||||
))
|
||||
Err(Error::from(E::into_full_error(&display, self.input)))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -334,6 +324,17 @@ 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
|
||||
|
|
@ -368,7 +369,7 @@ pub struct StdIoWrite<W>(pub W);
|
|||
impl<W: std::io::Write> Write for StdIoWrite<W> {
|
||||
#[inline]
|
||||
fn write_str(&mut self, string: &str) -> Result<(), Error> {
|
||||
self.0.write_all(string.as_bytes()).map_err(Error::adhoc)
|
||||
self.0.write_all(string.as_bytes()).map_err(Error::io)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -411,7 +412,7 @@ impl<W: core::fmt::Write> Write for StdFmtWrite<W> {
|
|||
fn write_str(&mut self, string: &str) -> Result<(), Error> {
|
||||
self.0
|
||||
.write_str(string)
|
||||
.map_err(|_| err!("an error occurred when formatting an argument"))
|
||||
.map_err(|_| Error::from(E::StdFmtWriteAdapter))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -433,7 +434,7 @@ trait WriteExt: Write {
|
|||
#[inline]
|
||||
fn write_int(
|
||||
&mut self,
|
||||
formatter: &DecimalFormatter,
|
||||
formatter: &IntegerFormatter,
|
||||
n: impl Into<i64>,
|
||||
) -> Result<(), Error> {
|
||||
self.write_decimal(&formatter.format_signed(n.into()))
|
||||
|
|
@ -444,7 +445,7 @@ trait WriteExt: Write {
|
|||
#[inline]
|
||||
fn write_uint(
|
||||
&mut self,
|
||||
formatter: &DecimalFormatter,
|
||||
formatter: &IntegerFormatter,
|
||||
n: impl Into<u64>,
|
||||
) -> Result<(), Error> {
|
||||
self.write_decimal(&formatter.format_unsigned(n.into()))
|
||||
|
|
@ -463,7 +464,7 @@ trait WriteExt: Write {
|
|||
|
||||
/// Write the given decimal number to this buffer.
|
||||
#[inline]
|
||||
fn write_decimal(&mut self, decimal: &Decimal) -> Result<(), Error> {
|
||||
fn write_decimal(&mut self, decimal: &Integer) -> Result<(), Error> {
|
||||
self.write_str(decimal.as_str())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ from [Temporal's hybrid grammar].
|
|||
// support a span of time of about 52 hours or so.)
|
||||
|
||||
use crate::{
|
||||
error::{err, Error, ErrorContext},
|
||||
error::{fmt::offset::Error as E, Error, ErrorContext},
|
||||
fmt::{
|
||||
temporal::{PiecesNumericOffset, PiecesOffset},
|
||||
util::{parse_temporal_fraction, FractionalFormatter},
|
||||
|
|
@ -110,7 +110,7 @@ use crate::{
|
|||
},
|
||||
tz::Offset,
|
||||
util::{
|
||||
escape, parse,
|
||||
parse,
|
||||
rangeint::{ri8, RFrom},
|
||||
t::{self, C},
|
||||
},
|
||||
|
|
@ -237,13 +237,7 @@ impl Numeric {
|
|||
if part_nanoseconds >= C(500_000_000) {
|
||||
seconds = seconds
|
||||
.try_checked_add("offset-seconds", C(1))
|
||||
.with_context(|| {
|
||||
err!(
|
||||
"due to precision loss, UTC offset '{}' is \
|
||||
rounded to a value that is out of bounds",
|
||||
self,
|
||||
)
|
||||
})?;
|
||||
.context(E::PrecisionLoss)?;
|
||||
}
|
||||
}
|
||||
Ok(Offset::from_seconds_ranged(seconds * self.sign))
|
||||
|
|
@ -254,11 +248,7 @@ impl Numeric {
|
|||
// `Offset` fails.
|
||||
impl core::fmt::Display for Numeric {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
if self.sign == C(-1) {
|
||||
write!(f, "-")?;
|
||||
} else {
|
||||
write!(f, "+")?;
|
||||
}
|
||||
f.write_str(if self.sign == C(-1) { "-" } else { "+" })?;
|
||||
write!(f, "{:02}", self.hours)?;
|
||||
if let Some(minutes) = self.minutes {
|
||||
write!(f, ":{:02}", minutes)?;
|
||||
|
|
@ -268,11 +258,8 @@ impl core::fmt::Display for Numeric {
|
|||
}
|
||||
if let Some(nanos) = self.nanoseconds {
|
||||
static FMT: FractionalFormatter = FractionalFormatter::new();
|
||||
write!(
|
||||
f,
|
||||
".{}",
|
||||
FMT.format(i32::from(nanos).unsigned_abs()).as_str()
|
||||
)?;
|
||||
f.write_str(".")?;
|
||||
f.write_str(FMT.format(i32::from(nanos).unsigned_abs()).as_str())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -413,18 +400,14 @@ impl Parser {
|
|||
mut input: &'i [u8],
|
||||
) -> Result<Parsed<'i, ParsedOffset>, Error> {
|
||||
if input.is_empty() {
|
||||
return Err(err!("expected UTC offset, but found end of input"));
|
||||
return Err(Error::from(E::EndOfInput));
|
||||
}
|
||||
|
||||
if input[0] == b'Z' || input[0] == b'z' {
|
||||
if !self.zulu {
|
||||
return Err(err!(
|
||||
"found {z:?} in {original:?} where a numeric UTC offset \
|
||||
was expected (this context does not permit \
|
||||
the Zulu offset)",
|
||||
z = escape::Byte(input[0]),
|
||||
original = escape::Bytes(input),
|
||||
));
|
||||
return Err(Error::from(E::UnexpectedLetterOffsetNoZulu(
|
||||
input[0],
|
||||
)));
|
||||
}
|
||||
input = &input[1..];
|
||||
let value = ParsedOffset { kind: ParsedOffsetKind::Zulu };
|
||||
|
|
@ -464,40 +447,24 @@ impl Parser {
|
|||
&self,
|
||||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, Numeric>, Error> {
|
||||
let original = escape::Bytes(input);
|
||||
|
||||
// Parse sign component.
|
||||
let Parsed { value: sign, input } =
|
||||
self.parse_sign(input).with_context(|| {
|
||||
err!("failed to parse sign in UTC numeric offset {original:?}")
|
||||
})?;
|
||||
self.parse_sign(input).context(E::InvalidSign)?;
|
||||
|
||||
// Parse hours component.
|
||||
let Parsed { value: hours, input } =
|
||||
self.parse_hours(input).with_context(|| {
|
||||
err!(
|
||||
"failed to parse hours in UTC numeric offset {original:?}"
|
||||
)
|
||||
})?;
|
||||
self.parse_hours(input).context(E::InvalidHours)?;
|
||||
let extended = match self.colon {
|
||||
Colon::Optional => input.starts_with(b":"),
|
||||
Colon::Required => {
|
||||
if !input.is_empty() && !input.starts_with(b":") {
|
||||
return Err(err!(
|
||||
"parsed hour component of time zone offset from \
|
||||
{original:?}, but could not find required colon \
|
||||
separator",
|
||||
));
|
||||
return Err(Error::from(E::NoColonAfterHours));
|
||||
}
|
||||
true
|
||||
}
|
||||
Colon::Absent => {
|
||||
if !input.is_empty() && input.starts_with(b":") {
|
||||
return Err(err!(
|
||||
"parsed hour component of time zone offset from \
|
||||
{original:?}, but found colon after hours which \
|
||||
is not allowed",
|
||||
));
|
||||
return Err(Error::from(E::ColonAfterHours));
|
||||
}
|
||||
false
|
||||
}
|
||||
|
|
@ -513,32 +480,22 @@ impl Parser {
|
|||
};
|
||||
|
||||
// Parse optional separator after hours.
|
||||
let Parsed { value: has_minutes, input } =
|
||||
self.parse_separator(input, extended).with_context(|| {
|
||||
err!(
|
||||
"failed to parse separator after hours in \
|
||||
UTC numeric offset {original:?}"
|
||||
)
|
||||
})?;
|
||||
let Parsed { value: has_minutes, input } = self
|
||||
.parse_separator(input, extended)
|
||||
.context(E::SeparatorAfterHours)?;
|
||||
if !has_minutes {
|
||||
if self.require_minute || (self.subminute && self.require_second) {
|
||||
return Err(err!(
|
||||
"parsed hour component of time zone offset from \
|
||||
{original:?}, but could not find required minute \
|
||||
component",
|
||||
));
|
||||
}
|
||||
return Ok(Parsed { value: numeric, input });
|
||||
return if self.require_minute
|
||||
|| (self.subminute && self.require_second)
|
||||
{
|
||||
Err(Error::from(E::MissingMinuteAfterHour))
|
||||
} else {
|
||||
Ok(Parsed { value: numeric, input })
|
||||
};
|
||||
}
|
||||
|
||||
// Parse minutes component.
|
||||
let Parsed { value: minutes, input } =
|
||||
self.parse_minutes(input).with_context(|| {
|
||||
err!(
|
||||
"failed to parse minutes in UTC numeric offset \
|
||||
{original:?}"
|
||||
)
|
||||
})?;
|
||||
self.parse_minutes(input).context(E::InvalidMinutes)?;
|
||||
numeric.minutes = Some(minutes);
|
||||
|
||||
// If subminute resolution is not supported, then we're done here.
|
||||
|
|
@ -549,65 +506,42 @@ impl Parser {
|
|||
// more precision than is supported. So we return an error here.
|
||||
// If this winds up being problematic, we can make this error
|
||||
// configurable or remove it altogether (unfortunate).
|
||||
if input.get(0).map_or(false, |&b| b == b':') {
|
||||
return Err(err!(
|
||||
"subminute precision for UTC numeric offset {original:?} \
|
||||
is not enabled in this context (must provide only \
|
||||
integral minutes)",
|
||||
));
|
||||
}
|
||||
return Ok(Parsed { value: numeric, input });
|
||||
return if input.get(0).map_or(false, |&b| b == b':') {
|
||||
Err(Error::from(E::SubminutePrecisionNotEnabled))
|
||||
} else {
|
||||
Ok(Parsed { value: numeric, input })
|
||||
};
|
||||
}
|
||||
|
||||
// Parse optional separator after minutes.
|
||||
let Parsed { value: has_seconds, input } =
|
||||
self.parse_separator(input, extended).with_context(|| {
|
||||
err!(
|
||||
"failed to parse separator after minutes in \
|
||||
UTC numeric offset {original:?}"
|
||||
)
|
||||
})?;
|
||||
let Parsed { value: has_seconds, input } = self
|
||||
.parse_separator(input, extended)
|
||||
.context(E::SeparatorAfterMinutes)?;
|
||||
if !has_seconds {
|
||||
if self.require_second {
|
||||
return Err(err!(
|
||||
"parsed hour and minute components of time zone offset \
|
||||
from {original:?}, but could not find required second \
|
||||
component",
|
||||
));
|
||||
}
|
||||
return Ok(Parsed { value: numeric, input });
|
||||
return if self.require_second {
|
||||
Err(Error::from(E::MissingSecondAfterMinute))
|
||||
} else {
|
||||
Ok(Parsed { value: numeric, input })
|
||||
};
|
||||
}
|
||||
|
||||
// Parse seconds component.
|
||||
let Parsed { value: seconds, input } =
|
||||
self.parse_seconds(input).with_context(|| {
|
||||
err!(
|
||||
"failed to parse seconds in UTC numeric offset \
|
||||
{original:?}"
|
||||
)
|
||||
})?;
|
||||
self.parse_seconds(input).context(E::InvalidSeconds)?;
|
||||
numeric.seconds = Some(seconds);
|
||||
|
||||
// If subsecond resolution is not supported, then we're done here.
|
||||
if !self.subsecond {
|
||||
if input.get(0).map_or(false, |&b| b == b'.' || b == b',') {
|
||||
return Err(err!(
|
||||
"subsecond precision for UTC numeric offset {original:?} \
|
||||
is not enabled in this context (must provide only \
|
||||
integral minutes or seconds)",
|
||||
));
|
||||
return Err(Error::from(E::SubsecondPrecisionNotEnabled));
|
||||
}
|
||||
return Ok(Parsed { value: numeric, input });
|
||||
}
|
||||
|
||||
// Parse an optional fractional component.
|
||||
let Parsed { value: nanoseconds, input } =
|
||||
parse_temporal_fraction(input).with_context(|| {
|
||||
err!(
|
||||
"failed to parse fractional nanoseconds in \
|
||||
UTC numeric offset {original:?}",
|
||||
)
|
||||
})?;
|
||||
parse_temporal_fraction(input)
|
||||
.context(E::InvalidSecondsFractional)?;
|
||||
// OK because `parse_temporal_fraction` guarantees `0..=999_999_999`.
|
||||
numeric.nanoseconds =
|
||||
nanoseconds.map(|n| t::SubsecNanosecond::new(n).unwrap());
|
||||
|
|
@ -619,19 +553,13 @@ impl Parser {
|
|||
&self,
|
||||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, t::Sign>, Error> {
|
||||
let sign = input.get(0).copied().ok_or_else(|| {
|
||||
err!("expected UTC numeric offset, but found end of input")
|
||||
})?;
|
||||
let sign = input.get(0).copied().ok_or(E::EndOfInputNumeric)?;
|
||||
let sign = if sign == b'+' {
|
||||
t::Sign::N::<1>()
|
||||
} else if sign == b'-' {
|
||||
t::Sign::N::<-1>()
|
||||
} else {
|
||||
return Err(err!(
|
||||
"expected '+' or '-' sign at start of UTC numeric offset, \
|
||||
but found {found:?} instead",
|
||||
found = escape::Byte(sign),
|
||||
));
|
||||
return Err(Error::from(E::InvalidSignPlusOrMinus));
|
||||
};
|
||||
Ok(Parsed { value: sign, input: &input[1..] })
|
||||
}
|
||||
|
|
@ -641,22 +569,16 @@ impl Parser {
|
|||
&self,
|
||||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, ParsedOffsetHours>, Error> {
|
||||
let (hours, input) = parse::split(input, 2).ok_or_else(|| {
|
||||
err!("expected two digit hour after sign, but found end of input",)
|
||||
})?;
|
||||
let hours = parse::i64(hours).with_context(|| {
|
||||
err!(
|
||||
"failed to parse {hours:?} as hours (a two digit integer)",
|
||||
hours = escape::Bytes(hours),
|
||||
)
|
||||
})?;
|
||||
let (hours, input) =
|
||||
parse::split(input, 2).ok_or(E::EndOfInputHour)?;
|
||||
let hours = parse::i64(hours).context(E::ParseHours)?;
|
||||
// Note that we support a slightly bigger range of offsets than
|
||||
// Temporal. Temporal seems to support only up to 23 hours, but
|
||||
// we go up to 25 hours. This is done to support POSIX time zone
|
||||
// strings, which also require 25 hours (plus the maximal minute/second
|
||||
// components).
|
||||
let hours = ParsedOffsetHours::try_new("hours", hours)
|
||||
.context("offset hours are not valid")?;
|
||||
.context(E::RangeHours)?;
|
||||
Ok(Parsed { value: hours, input })
|
||||
}
|
||||
|
||||
|
|
@ -665,20 +587,11 @@ impl Parser {
|
|||
&self,
|
||||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, ParsedOffsetMinutes>, Error> {
|
||||
let (minutes, input) = parse::split(input, 2).ok_or_else(|| {
|
||||
err!(
|
||||
"expected two digit minute after hours, \
|
||||
but found end of input",
|
||||
)
|
||||
})?;
|
||||
let minutes = parse::i64(minutes).with_context(|| {
|
||||
err!(
|
||||
"failed to parse {minutes:?} as minutes (a two digit integer)",
|
||||
minutes = escape::Bytes(minutes),
|
||||
)
|
||||
})?;
|
||||
let (minutes, input) =
|
||||
parse::split(input, 2).ok_or(E::EndOfInputMinute)?;
|
||||
let minutes = parse::i64(minutes).context(E::ParseMinutes)?;
|
||||
let minutes = ParsedOffsetMinutes::try_new("minutes", minutes)
|
||||
.context("minutes are not valid")?;
|
||||
.context(E::RangeMinutes)?;
|
||||
Ok(Parsed { value: minutes, input })
|
||||
}
|
||||
|
||||
|
|
@ -687,20 +600,11 @@ impl Parser {
|
|||
&self,
|
||||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, ParsedOffsetSeconds>, Error> {
|
||||
let (seconds, input) = parse::split(input, 2).ok_or_else(|| {
|
||||
err!(
|
||||
"expected two digit second after hours, \
|
||||
but found end of input",
|
||||
)
|
||||
})?;
|
||||
let seconds = parse::i64(seconds).with_context(|| {
|
||||
err!(
|
||||
"failed to parse {seconds:?} as seconds (a two digit integer)",
|
||||
seconds = escape::Bytes(seconds),
|
||||
)
|
||||
})?;
|
||||
let (seconds, input) =
|
||||
parse::split(input, 2).ok_or(E::EndOfInputSecond)?;
|
||||
let seconds = parse::i64(seconds).context(E::ParseSeconds)?;
|
||||
let seconds = ParsedOffsetSeconds::try_new("seconds", seconds)
|
||||
.context("time zone offset seconds are not valid")?;
|
||||
.context(E::RangeSeconds)?;
|
||||
Ok(Parsed { value: seconds, input })
|
||||
}
|
||||
|
||||
|
|
@ -941,7 +845,7 @@ mod tests {
|
|||
fn err_numeric_empty() {
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse_numeric(b"").unwrap_err(),
|
||||
@r###"failed to parse sign in UTC numeric offset "": expected UTC numeric offset, but found end of input"###,
|
||||
@"failed to parse sign in UTC numeric offset: expected UTC numeric offset, but found end of input",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -950,7 +854,7 @@ mod tests {
|
|||
fn err_numeric_notsign() {
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse_numeric(b"*").unwrap_err(),
|
||||
@r###"failed to parse sign in UTC numeric offset "*": expected '+' or '-' sign at start of UTC numeric offset, but found "*" instead"###,
|
||||
@"failed to parse sign in UTC numeric offset: expected `+` or `-` sign at start of UTC numeric offset",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -959,7 +863,7 @@ mod tests {
|
|||
fn err_numeric_hours_too_short() {
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse_numeric(b"+a").unwrap_err(),
|
||||
@r###"failed to parse hours in UTC numeric offset "+a": expected two digit hour after sign, but found end of input"###,
|
||||
@"failed to parse hours in UTC numeric offset: expected two digit hour after sign, but found end of input",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -968,7 +872,7 @@ mod tests {
|
|||
fn err_numeric_hours_invalid_digits() {
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse_numeric(b"+ab").unwrap_err(),
|
||||
@r###"failed to parse hours in UTC numeric offset "+ab": failed to parse "ab" as hours (a two digit integer): invalid digit, expected 0-9 but got a"###,
|
||||
@"failed to parse hours in UTC numeric offset: failed to parse hours (requires a two digit integer): invalid digit, expected 0-9 but got a",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -977,7 +881,7 @@ mod tests {
|
|||
fn err_numeric_hours_out_of_range() {
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse_numeric(b"-26").unwrap_err(),
|
||||
@r###"failed to parse hours in UTC numeric offset "-26": offset hours are not valid: parameter 'hours' with value 26 is not in the required range of 0..=25"###,
|
||||
@"failed to parse hours in UTC numeric offset: hour in time zone offset is out of range: parameter 'hours' with value 26 is not in the required range of 0..=25",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -986,7 +890,7 @@ mod tests {
|
|||
fn err_numeric_minutes_too_short() {
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse_numeric(b"+05:a").unwrap_err(),
|
||||
@r###"failed to parse minutes in UTC numeric offset "+05:a": expected two digit minute after hours, but found end of input"###,
|
||||
@"failed to parse minutes in UTC numeric offset: expected two digit minute after hours, but found end of input",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -995,7 +899,7 @@ mod tests {
|
|||
fn err_numeric_minutes_invalid_digits() {
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse_numeric(b"+05:ab").unwrap_err(),
|
||||
@r###"failed to parse minutes in UTC numeric offset "+05:ab": failed to parse "ab" as minutes (a two digit integer): invalid digit, expected 0-9 but got a"###,
|
||||
@"failed to parse minutes in UTC numeric offset: failed to parse minutes (requires a two digit integer): invalid digit, expected 0-9 but got a",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1004,7 +908,7 @@ mod tests {
|
|||
fn err_numeric_minutes_out_of_range() {
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse_numeric(b"-05:60").unwrap_err(),
|
||||
@r###"failed to parse minutes in UTC numeric offset "-05:60": minutes are not valid: parameter 'minutes' with value 60 is not in the required range of 0..=59"###,
|
||||
@"failed to parse minutes in UTC numeric offset: minute in time zone offset is out of range: parameter 'minutes' with value 60 is not in the required range of 0..=59",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1013,7 +917,7 @@ mod tests {
|
|||
fn err_numeric_seconds_too_short() {
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse_numeric(b"+05:30:a").unwrap_err(),
|
||||
@r###"failed to parse seconds in UTC numeric offset "+05:30:a": expected two digit second after hours, but found end of input"###,
|
||||
@"failed to parse seconds in UTC numeric offset: expected two digit second after minutes, but found end of input",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1022,7 +926,7 @@ mod tests {
|
|||
fn err_numeric_seconds_invalid_digits() {
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse_numeric(b"+05:30:ab").unwrap_err(),
|
||||
@r###"failed to parse seconds in UTC numeric offset "+05:30:ab": failed to parse "ab" as seconds (a two digit integer): invalid digit, expected 0-9 but got a"###,
|
||||
@"failed to parse seconds in UTC numeric offset: failed to parse seconds (requires a two digit integer): invalid digit, expected 0-9 but got a",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1031,7 +935,7 @@ mod tests {
|
|||
fn err_numeric_seconds_out_of_range() {
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse_numeric(b"-05:30:60").unwrap_err(),
|
||||
@r###"failed to parse seconds in UTC numeric offset "-05:30:60": time zone offset seconds are not valid: parameter 'seconds' with value 60 is not in the required range of 0..=59"###,
|
||||
@"failed to parse seconds in UTC numeric offset: second in time zone offset is out of range: parameter 'seconds' with value 60 is not in the required range of 0..=59",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1041,31 +945,31 @@ mod tests {
|
|||
fn err_numeric_fraction_non_empty() {
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse_numeric(b"-05:30:44.").unwrap_err(),
|
||||
@r###"failed to parse fractional nanoseconds in UTC numeric offset "-05:30:44.": found decimal after seconds component, but did not find any decimal digits after decimal"###,
|
||||
@"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse_numeric(b"-05:30:44,").unwrap_err(),
|
||||
@r###"failed to parse fractional nanoseconds in UTC numeric offset "-05:30:44,": found decimal after seconds component, but did not find any decimal digits after decimal"###,
|
||||
@"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal",
|
||||
);
|
||||
|
||||
// Instead of end-of-string, add invalid digit.
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse_numeric(b"-05:30:44.a").unwrap_err(),
|
||||
@r###"failed to parse fractional nanoseconds in UTC numeric offset "-05:30:44.a": found decimal after seconds component, but did not find any decimal digits after decimal"###,
|
||||
@"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse_numeric(b"-05:30:44,a").unwrap_err(),
|
||||
@r###"failed to parse fractional nanoseconds in UTC numeric offset "-05:30:44,a": found decimal after seconds component, but did not find any decimal digits after decimal"###,
|
||||
@"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal",
|
||||
);
|
||||
|
||||
// And also test basic format.
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse_numeric(b"-053044.a").unwrap_err(),
|
||||
@r###"failed to parse fractional nanoseconds in UTC numeric offset "-053044.a": found decimal after seconds component, but did not find any decimal digits after decimal"###,
|
||||
@"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse_numeric(b"-053044,a").unwrap_err(),
|
||||
@r###"failed to parse fractional nanoseconds in UTC numeric offset "-053044,a": found decimal after seconds component, but did not find any decimal digits after decimal"###,
|
||||
@"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1076,7 +980,7 @@ mod tests {
|
|||
fn err_numeric_subminute_disabled_but_desired() {
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().subminute(false).parse_numeric(b"-05:59:32").unwrap_err(),
|
||||
@r###"subminute precision for UTC numeric offset "-05:59:32" is not enabled in this context (must provide only integral minutes)"###,
|
||||
@"subminute precision for UTC numeric offset is not enabled in this context (must provide only integral minutes)",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1086,11 +990,11 @@ mod tests {
|
|||
fn err_zulu_disabled_but_desired() {
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().zulu(false).parse(b"Z").unwrap_err(),
|
||||
@r###"found "Z" in "Z" where a numeric UTC offset was expected (this context does not permit the Zulu offset)"###,
|
||||
@"found `Z` where a numeric UTC offset was expected (this context does not permit the Zulu offset)",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().zulu(false).parse(b"z").unwrap_err(),
|
||||
@r###"found "z" in "z" where a numeric UTC offset was expected (this context does not permit the Zulu offset)"###,
|
||||
@"found `z` where a numeric UTC offset was expected (this context does not permit the Zulu offset)",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1118,7 +1022,7 @@ mod tests {
|
|||
};
|
||||
insta::assert_snapshot!(
|
||||
numeric.to_offset().unwrap_err(),
|
||||
@"due to precision loss, UTC offset '+25:59:59.5' is rounded to a value that is out of bounds: parameter 'offset-seconds' with value 1 is not in the required range of -93599..=93599",
|
||||
@"due to precision loss, offset is rounded to a value that is out of bounds: parameter 'offset-seconds' with value 1 is not in the required range of -93599..=93599",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1143,7 +1047,7 @@ mod tests {
|
|||
};
|
||||
insta::assert_snapshot!(
|
||||
numeric.to_offset().unwrap_err(),
|
||||
@"due to precision loss, UTC offset '-25:59:59.5' is rounded to a value that is out of bounds: parameter 'offset-seconds' with value 1 is not in the required range of -93599..=93599",
|
||||
@"due to precision loss, offset is rounded to a value that is out of bounds: parameter 'offset-seconds' with value 1 is not in the required range of -93599..=93599",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,11 +43,11 @@ general interchange format for new applications.
|
|||
|
||||
use crate::{
|
||||
civil::{Date, DateTime, Time, Weekday},
|
||||
error::{err, ErrorContext},
|
||||
fmt::{util::DecimalFormatter, Parsed, Write, WriteExt},
|
||||
error::{fmt::rfc2822::Error as E, ErrorContext},
|
||||
fmt::{util::IntegerFormatter, Parsed, Write, WriteExt},
|
||||
tz::{Offset, TimeZone},
|
||||
util::{
|
||||
escape, parse,
|
||||
parse,
|
||||
rangeint::{ri8, RFrom},
|
||||
t::{self, C},
|
||||
},
|
||||
|
|
@ -313,9 +313,7 @@ impl DateTimeParser {
|
|||
let input = input.as_ref();
|
||||
let zdt = self
|
||||
.parse_zoned_internal(input)
|
||||
.context(
|
||||
"failed to parse RFC 2822 datetime into Jiff zoned datetime",
|
||||
)?
|
||||
.context(E::FailedZoned)?
|
||||
.into_full()?;
|
||||
Ok(zdt)
|
||||
}
|
||||
|
|
@ -351,7 +349,7 @@ impl DateTimeParser {
|
|||
let input = input.as_ref();
|
||||
let ts = self
|
||||
.parse_timestamp_internal(input)
|
||||
.context("failed to parse RFC 2822 datetime into Jiff timestamp")?
|
||||
.context(E::FailedTimestamp)?
|
||||
.into_full()?;
|
||||
Ok(ts)
|
||||
}
|
||||
|
|
@ -367,9 +365,7 @@ impl DateTimeParser {
|
|||
) -> Result<Parsed<'i, Zoned>, Error> {
|
||||
let Parsed { value: (dt, offset), input } =
|
||||
self.parse_datetime_offset(input)?;
|
||||
let ts = offset
|
||||
.to_timestamp(dt)
|
||||
.context("RFC 2822 datetime out of Jiff's range")?;
|
||||
let ts = offset.to_timestamp(dt)?;
|
||||
let zdt = ts.to_zoned(TimeZone::fixed(offset));
|
||||
Ok(Parsed { value: zdt, input })
|
||||
}
|
||||
|
|
@ -385,9 +381,7 @@ impl DateTimeParser {
|
|||
) -> Result<Parsed<'i, Timestamp>, Error> {
|
||||
let Parsed { value: (dt, offset), input } =
|
||||
self.parse_datetime_offset(input)?;
|
||||
let ts = offset
|
||||
.to_timestamp(dt)
|
||||
.context("RFC 2822 datetime out of Jiff's range")?;
|
||||
let ts = offset.to_timestamp(dt)?;
|
||||
Ok(Parsed { value: ts, input })
|
||||
}
|
||||
|
||||
|
|
@ -425,16 +419,11 @@ impl DateTimeParser {
|
|||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, DateTime>, Error> {
|
||||
if input.is_empty() {
|
||||
return Err(err!(
|
||||
"expected RFC 2822 datetime, but got empty string"
|
||||
));
|
||||
return Err(Error::from(E::Empty));
|
||||
}
|
||||
let Parsed { input, .. } = self.skip_whitespace(input);
|
||||
if input.is_empty() {
|
||||
return Err(err!(
|
||||
"expected RFC 2822 datetime, but got empty string after \
|
||||
trimming whitespace",
|
||||
));
|
||||
return Err(Error::from(E::EmptyAfterWhitespace));
|
||||
}
|
||||
let Parsed { value: wd, input } = self.parse_weekday(input)?;
|
||||
let Parsed { value: day, input } = self.parse_day(input)?;
|
||||
|
|
@ -451,26 +440,19 @@ impl DateTimeParser {
|
|||
self.skip_whitespace(input);
|
||||
let (second, input) = if !input.starts_with(b":") {
|
||||
if !whitespace_after_minute {
|
||||
return Err(err!(
|
||||
"expected whitespace after parsing time: \
|
||||
expected at least one whitespace character \
|
||||
(space or tab), but found none",
|
||||
));
|
||||
return Err(Error::from(E::WhitespaceAfterTime));
|
||||
}
|
||||
(t::Second::N::<0>(), input)
|
||||
} else {
|
||||
let Parsed { input, .. } = self.parse_time_separator(input)?;
|
||||
let Parsed { input, .. } = self.skip_whitespace(input);
|
||||
let Parsed { value: second, input } = self.parse_second(input)?;
|
||||
let Parsed { input, .. } =
|
||||
self.parse_whitespace(input).with_context(|| {
|
||||
err!("expected whitespace after parsing time")
|
||||
})?;
|
||||
let Parsed { input, .. } = self.parse_whitespace(input)?;
|
||||
(second, input)
|
||||
};
|
||||
|
||||
let date =
|
||||
Date::new_ranged(year, month, day).context("invalid date")?;
|
||||
Date::new_ranged(year, month, day).context(E::InvalidDate)?;
|
||||
let time = Time::new_ranged(
|
||||
hour,
|
||||
minute,
|
||||
|
|
@ -480,13 +462,10 @@ impl DateTimeParser {
|
|||
let dt = DateTime::from_parts(date, time);
|
||||
if let Some(wd) = wd {
|
||||
if !self.relaxed_weekday && wd != dt.weekday() {
|
||||
return Err(err!(
|
||||
"found parsed weekday of {parsed}, \
|
||||
but parsed datetime of {dt} has weekday \
|
||||
{has}",
|
||||
parsed = weekday_abbrev(wd),
|
||||
has = weekday_abbrev(dt.weekday()),
|
||||
));
|
||||
return Err(Error::from(E::InconsistentWeekday {
|
||||
parsed: wd,
|
||||
from_date: dt.weekday(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
Ok(Parsed { value: dt, input })
|
||||
|
|
@ -517,15 +496,13 @@ impl DateTimeParser {
|
|||
if matches!(input[0], b'0'..=b'9') {
|
||||
return Ok(Parsed { value: None, input });
|
||||
}
|
||||
if input.len() < 4 {
|
||||
return Err(err!(
|
||||
"expected day at beginning of RFC 2822 datetime \
|
||||
since first non-whitespace byte, {first:?}, \
|
||||
is not a digit, but given string is too short \
|
||||
(length is {length})",
|
||||
first = escape::Byte(input[0]),
|
||||
length = input.len(),
|
||||
));
|
||||
if let Ok(len) = u8::try_from(input.len()) {
|
||||
if len < 4 {
|
||||
return Err(Error::from(E::TooShortWeekday {
|
||||
got_non_digit: input[0],
|
||||
len,
|
||||
}));
|
||||
}
|
||||
}
|
||||
let b1 = input[0];
|
||||
let b2 = input[1];
|
||||
|
|
@ -543,31 +520,19 @@ impl DateTimeParser {
|
|||
b"fri" => Weekday::Friday,
|
||||
b"sat" => Weekday::Saturday,
|
||||
_ => {
|
||||
return Err(err!(
|
||||
"expected day at beginning of RFC 2822 datetime \
|
||||
since first non-whitespace byte, {first:?}, \
|
||||
is not a digit, but did not recognize {got:?} \
|
||||
as a valid weekday abbreviation",
|
||||
first = escape::Byte(input[0]),
|
||||
got = escape::Bytes(&input[..3]),
|
||||
));
|
||||
return Err(Error::from(E::InvalidWeekday {
|
||||
got_non_digit: input[0],
|
||||
}));
|
||||
}
|
||||
};
|
||||
let Parsed { input, .. } = self.skip_whitespace(&input[3..]);
|
||||
let Some(should_be_comma) = input.get(0).copied() else {
|
||||
return Err(err!(
|
||||
"expected comma after parsed weekday `{weekday}` in \
|
||||
RFC 2822 datetime, but found end of string instead",
|
||||
weekday = escape::Bytes(&[b1, b2, b3]),
|
||||
));
|
||||
return Err(Error::from(E::EndOfInputComma));
|
||||
};
|
||||
if should_be_comma != b',' {
|
||||
return Err(err!(
|
||||
"expected comma after parsed weekday `{weekday}` in \
|
||||
RFC 2822 datetime, but found `{got:?}` instead",
|
||||
weekday = escape::Bytes(&[b1, b2, b3]),
|
||||
got = escape::Byte(should_be_comma),
|
||||
));
|
||||
return Err(Error::from(E::UnexpectedByteComma {
|
||||
byte: should_be_comma,
|
||||
}));
|
||||
}
|
||||
let Parsed { input, .. } = self.skip_whitespace(&input[1..]);
|
||||
Ok(Parsed { value: Some(wd), input })
|
||||
|
|
@ -586,21 +551,17 @@ impl DateTimeParser {
|
|||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, t::Day>, Error> {
|
||||
if input.is_empty() {
|
||||
return Err(err!("expected day, but found end of input"));
|
||||
return Err(Error::from(E::EndOfInputDay));
|
||||
}
|
||||
let mut digits = 1;
|
||||
if input.len() >= 2 && matches!(input[1], b'0'..=b'9') {
|
||||
digits = 2;
|
||||
}
|
||||
let (day, input) = input.split_at(digits);
|
||||
let day = parse::i64(day).with_context(|| {
|
||||
err!("failed to parse {day:?} as day", day = escape::Bytes(day))
|
||||
})?;
|
||||
let day = t::Day::try_new("day", day).context("day is not valid")?;
|
||||
let day = parse::i64(day).context(E::ParseDay)?;
|
||||
let day = t::Day::try_new("day", day).context(E::ParseDay)?;
|
||||
let Parsed { input, .. } =
|
||||
self.parse_whitespace(input).with_context(|| {
|
||||
err!("expected whitespace after parsing day {day}")
|
||||
})?;
|
||||
self.parse_whitespace(input).context(E::WhitespaceAfterDay)?;
|
||||
Ok(Parsed { value: day, input })
|
||||
}
|
||||
|
||||
|
|
@ -617,16 +578,12 @@ impl DateTimeParser {
|
|||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, t::Month>, Error> {
|
||||
if input.is_empty() {
|
||||
return Err(err!(
|
||||
"expected abbreviated month name, but found end of input"
|
||||
));
|
||||
return Err(Error::from(E::EndOfInputMonth));
|
||||
}
|
||||
if input.len() < 3 {
|
||||
return Err(err!(
|
||||
"expected abbreviated month name, but remaining input \
|
||||
is too short (remaining bytes is {length})",
|
||||
length = input.len(),
|
||||
));
|
||||
if let Ok(len) = u8::try_from(input.len()) {
|
||||
if len < 3 {
|
||||
return Err(Error::from(E::TooShortMonth { len }));
|
||||
}
|
||||
}
|
||||
let b1 = input[0].to_ascii_lowercase();
|
||||
let b2 = input[1].to_ascii_lowercase();
|
||||
|
|
@ -644,22 +601,14 @@ impl DateTimeParser {
|
|||
b"oct" => 10,
|
||||
b"nov" => 11,
|
||||
b"dec" => 12,
|
||||
_ => {
|
||||
return Err(err!(
|
||||
"expected abbreviated month name, \
|
||||
but did not recognize {got:?} \
|
||||
as a valid month",
|
||||
got = escape::Bytes(&input[..3]),
|
||||
));
|
||||
}
|
||||
_ => return Err(Error::from(E::InvalidMonth)),
|
||||
};
|
||||
// OK because we just assigned a numeric value ourselves
|
||||
// above, and all values are valid months.
|
||||
let month = t::Month::new(month).unwrap();
|
||||
let Parsed { input, .. } =
|
||||
self.parse_whitespace(&input[3..]).with_context(|| {
|
||||
err!("expected whitespace after parsing month name")
|
||||
})?;
|
||||
let Parsed { input, .. } = self
|
||||
.parse_whitespace(&input[3..])
|
||||
.context(E::WhitespaceAfterMonth)?;
|
||||
Ok(Parsed { value: month, input })
|
||||
}
|
||||
|
||||
|
|
@ -692,31 +641,22 @@ impl DateTimeParser {
|
|||
{
|
||||
digits += 1;
|
||||
}
|
||||
if digits <= 1 {
|
||||
return Err(err!(
|
||||
"expected at least two ASCII digits for parsing \
|
||||
a year, but only found {digits}",
|
||||
));
|
||||
if let Ok(len) = u8::try_from(digits) {
|
||||
if len <= 1 {
|
||||
return Err(Error::from(E::TooShortYear { len }));
|
||||
}
|
||||
}
|
||||
let (year, input) = input.split_at(digits);
|
||||
let year = parse::i64(year).with_context(|| {
|
||||
err!(
|
||||
"failed to parse {year:?} as year \
|
||||
(a two, three or four digit integer)",
|
||||
year = escape::Bytes(year),
|
||||
)
|
||||
})?;
|
||||
let year = parse::i64(year).context(E::ParseYear)?;
|
||||
let year = match digits {
|
||||
2 if year <= 49 => year + 2000,
|
||||
2 | 3 => year + 1900,
|
||||
4 => year,
|
||||
_ => unreachable!("digits={digits} must be 2, 3 or 4"),
|
||||
};
|
||||
let year =
|
||||
t::Year::try_new("year", year).context("year is not valid")?;
|
||||
let Parsed { input, .. } = self
|
||||
.parse_whitespace(input)
|
||||
.with_context(|| err!("expected whitespace after parsing year"))?;
|
||||
let year = t::Year::try_new("year", year).context(E::InvalidYear)?;
|
||||
let Parsed { input, .. } =
|
||||
self.parse_whitespace(input).context(E::WhitespaceAfterYear)?;
|
||||
Ok(Parsed { value: year, input })
|
||||
}
|
||||
|
||||
|
|
@ -730,17 +670,9 @@ impl DateTimeParser {
|
|||
&self,
|
||||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, t::Hour>, Error> {
|
||||
let (hour, input) = parse::split(input, 2).ok_or_else(|| {
|
||||
err!("expected two digit hour, but found end of input")
|
||||
})?;
|
||||
let hour = parse::i64(hour).with_context(|| {
|
||||
err!(
|
||||
"failed to parse {hour:?} as hour (a two digit integer)",
|
||||
hour = escape::Bytes(hour),
|
||||
)
|
||||
})?;
|
||||
let hour =
|
||||
t::Hour::try_new("hour", hour).context("hour is not valid")?;
|
||||
let (hour, input) = parse::split(input, 2).ok_or(E::EndOfInputHour)?;
|
||||
let hour = parse::i64(hour).context(E::ParseHour)?;
|
||||
let hour = t::Hour::try_new("hour", hour).context(E::InvalidHour)?;
|
||||
Ok(Parsed { value: hour, input })
|
||||
}
|
||||
|
||||
|
|
@ -751,17 +683,11 @@ impl DateTimeParser {
|
|||
&self,
|
||||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, t::Minute>, Error> {
|
||||
let (minute, input) = parse::split(input, 2).ok_or_else(|| {
|
||||
err!("expected two digit minute, but found end of input")
|
||||
})?;
|
||||
let minute = parse::i64(minute).with_context(|| {
|
||||
err!(
|
||||
"failed to parse {minute:?} as minute (a two digit integer)",
|
||||
minute = escape::Bytes(minute),
|
||||
)
|
||||
})?;
|
||||
let minute = t::Minute::try_new("minute", minute)
|
||||
.context("minute is not valid")?;
|
||||
let (minute, input) =
|
||||
parse::split(input, 2).ok_or(E::EndOfInputMinute)?;
|
||||
let minute = parse::i64(minute).context(E::ParseMinute)?;
|
||||
let minute =
|
||||
t::Minute::try_new("minute", minute).context(E::InvalidMinute)?;
|
||||
Ok(Parsed { value: minute, input })
|
||||
}
|
||||
|
||||
|
|
@ -772,20 +698,14 @@ impl DateTimeParser {
|
|||
&self,
|
||||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, t::Second>, Error> {
|
||||
let (second, input) = parse::split(input, 2).ok_or_else(|| {
|
||||
err!("expected two digit second, but found end of input")
|
||||
})?;
|
||||
let mut second = parse::i64(second).with_context(|| {
|
||||
err!(
|
||||
"failed to parse {second:?} as second (a two digit integer)",
|
||||
second = escape::Bytes(second),
|
||||
)
|
||||
})?;
|
||||
let (second, input) =
|
||||
parse::split(input, 2).ok_or(E::EndOfInputSecond)?;
|
||||
let mut second = parse::i64(second).context(E::ParseSecond)?;
|
||||
if second == 60 {
|
||||
second = 59;
|
||||
}
|
||||
let second = t::Second::try_new("second", second)
|
||||
.context("second is not valid")?;
|
||||
let second =
|
||||
t::Second::try_new("second", second).context(E::InvalidSecond)?;
|
||||
Ok(Parsed { value: second, input })
|
||||
}
|
||||
|
||||
|
|
@ -801,13 +721,7 @@ impl DateTimeParser {
|
|||
type ParsedOffsetHours = ri8<0, { t::SpanZoneOffsetHours::MAX }>;
|
||||
type ParsedOffsetMinutes = ri8<0, { t::SpanZoneOffsetMinutes::MAX }>;
|
||||
|
||||
let sign = input.get(0).copied().ok_or_else(|| {
|
||||
err!(
|
||||
"expected sign for time zone offset, \
|
||||
(or a legacy time zone name abbreviation), \
|
||||
but found end of input",
|
||||
)
|
||||
})?;
|
||||
let sign = input.get(0).copied().ok_or(E::EndOfInputOffset)?;
|
||||
let sign = if sign == b'+' {
|
||||
t::Sign::N::<1>()
|
||||
} else if sign == b'-' {
|
||||
|
|
@ -816,32 +730,16 @@ impl DateTimeParser {
|
|||
return self.parse_offset_obsolete(input);
|
||||
};
|
||||
let input = &input[1..];
|
||||
let (hhmm, input) = parse::split(input, 4).ok_or_else(|| {
|
||||
err!(
|
||||
"expected at least 4 digits for time zone offset \
|
||||
after sign, but found only {len} bytes remaining",
|
||||
len = input.len(),
|
||||
)
|
||||
})?;
|
||||
let (hhmm, input) = parse::split(input, 4).ok_or(E::TooShortOffset)?;
|
||||
|
||||
let hh = parse::i64(&hhmm[0..2]).with_context(|| {
|
||||
err!(
|
||||
"failed to parse hours from time zone offset {hhmm}",
|
||||
hhmm = escape::Bytes(hhmm)
|
||||
)
|
||||
})?;
|
||||
let hh = parse::i64(&hhmm[0..2]).context(E::ParseOffsetHour)?;
|
||||
let hh = ParsedOffsetHours::try_new("zone-offset-hours", hh)
|
||||
.context("time zone offset hours are not valid")?;
|
||||
.context(E::InvalidOffsetHour)?;
|
||||
let hh = t::SpanZoneOffset::rfrom(hh);
|
||||
|
||||
let mm = parse::i64(&hhmm[2..4]).with_context(|| {
|
||||
err!(
|
||||
"failed to parse minutes from time zone offset {hhmm}",
|
||||
hhmm = escape::Bytes(hhmm)
|
||||
)
|
||||
})?;
|
||||
let mm = parse::i64(&hhmm[2..4]).context(E::ParseOffsetMinute)?;
|
||||
let mm = ParsedOffsetMinutes::try_new("zone-offset-minutes", mm)
|
||||
.context("time zone offset minutes are not valid")?;
|
||||
.context(E::InvalidOffsetMinute)?;
|
||||
let mm = t::SpanZoneOffset::rfrom(mm);
|
||||
|
||||
let seconds = hh * C(3_600) + mm * C(60);
|
||||
|
|
@ -865,11 +763,7 @@ impl DateTimeParser {
|
|||
len += 1;
|
||||
}
|
||||
if len == 0 {
|
||||
return Err(err!(
|
||||
"expected obsolete RFC 2822 time zone abbreviation, \
|
||||
but found no remaining non-whitespace characters \
|
||||
after time",
|
||||
));
|
||||
return Err(Error::from(E::WhitespaceAfterTimeForObsoleteOffset));
|
||||
}
|
||||
let offset = match &letters[..len] {
|
||||
b"ut" | b"gmt" | b"z" => Offset::UTC,
|
||||
|
|
@ -917,11 +811,7 @@ impl DateTimeParser {
|
|||
Offset::UTC
|
||||
} else {
|
||||
// But anything else we throw our hands up I guess.
|
||||
return Err(err!(
|
||||
"expected obsolete RFC 2822 time zone abbreviation, \
|
||||
but found {found:?}",
|
||||
found = escape::Bytes(&input[..len]),
|
||||
));
|
||||
return Err(Error::from(E::InvalidObsoleteOffset));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -936,15 +826,12 @@ impl DateTimeParser {
|
|||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, ()>, Error> {
|
||||
if input.is_empty() {
|
||||
return Err(err!(
|
||||
"expected time separator of ':', but found end of input",
|
||||
));
|
||||
return Err(Error::from(E::EndOfInputTimeSeparator));
|
||||
}
|
||||
if input[0] != b':' {
|
||||
return Err(err!(
|
||||
"expected time separator of ':', but found {got}",
|
||||
got = escape::Byte(input[0]),
|
||||
));
|
||||
return Err(Error::from(E::UnexpectedByteTimeSeparator {
|
||||
byte: input[0],
|
||||
}));
|
||||
}
|
||||
Ok(Parsed { value: (), input: &input[1..] })
|
||||
}
|
||||
|
|
@ -959,10 +846,7 @@ impl DateTimeParser {
|
|||
let Parsed { input, value: had_whitespace } =
|
||||
self.skip_whitespace(input);
|
||||
if !had_whitespace {
|
||||
return Err(err!(
|
||||
"expected at least one whitespace character (space or tab), \
|
||||
but found none",
|
||||
));
|
||||
return Err(Error::from(E::WhitespaceAfterTime));
|
||||
}
|
||||
Ok(Parsed { value: (), input })
|
||||
}
|
||||
|
|
@ -1012,26 +896,20 @@ impl DateTimeParser {
|
|||
// I believe this error case is actually impossible, since as
|
||||
// soon as we hit 0, we break out. If there is more "comment,"
|
||||
// then it will flag an error as unparsed input.
|
||||
depth = depth.checked_sub(1).ok_or_else(|| {
|
||||
err!(
|
||||
"found closing parenthesis in comment with \
|
||||
no matching opening parenthesis"
|
||||
)
|
||||
})?;
|
||||
depth = depth
|
||||
.checked_sub(1)
|
||||
.ok_or(E::CommentClosingParenWithoutOpen)?;
|
||||
if depth == 0 {
|
||||
break;
|
||||
}
|
||||
} else if byte == b'(' {
|
||||
depth = depth.checked_add(1).ok_or_else(|| {
|
||||
err!("found too many nested parenthesis in comment")
|
||||
})?;
|
||||
depth = depth
|
||||
.checked_add(1)
|
||||
.ok_or(E::CommentTooManyNestedParens)?;
|
||||
}
|
||||
}
|
||||
if depth > 0 {
|
||||
return Err(err!(
|
||||
"found opening parenthesis in comment with \
|
||||
no matching closing parenthesis"
|
||||
));
|
||||
return Err(Error::from(E::CommentOpeningParenWithoutClose));
|
||||
}
|
||||
let Parsed { input, .. } = self.skip_whitespace(input);
|
||||
Ok(Parsed { value: (), input })
|
||||
|
|
@ -1414,19 +1292,16 @@ impl DateTimePrinter {
|
|||
offset: Option<Offset>,
|
||||
mut wtr: W,
|
||||
) -> Result<(), Error> {
|
||||
static FMT_DAY: DecimalFormatter = DecimalFormatter::new();
|
||||
static FMT_YEAR: DecimalFormatter = DecimalFormatter::new().padding(4);
|
||||
static FMT_TIME_UNIT: DecimalFormatter =
|
||||
DecimalFormatter::new().padding(2);
|
||||
static FMT_DAY: IntegerFormatter = IntegerFormatter::new();
|
||||
static FMT_YEAR: IntegerFormatter = IntegerFormatter::new().padding(4);
|
||||
static FMT_TIME_UNIT: IntegerFormatter =
|
||||
IntegerFormatter::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(err!(
|
||||
"datetime {dt} has negative year, \
|
||||
which cannot be formatted with RFC 2822",
|
||||
));
|
||||
return Err(Error::from(E::NegativeYear));
|
||||
}
|
||||
|
||||
wtr.write_str(weekday_abbrev(dt.weekday()))?;
|
||||
|
|
@ -1474,20 +1349,17 @@ impl DateTimePrinter {
|
|||
timestamp: &Timestamp,
|
||||
mut wtr: W,
|
||||
) -> Result<(), Error> {
|
||||
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);
|
||||
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);
|
||||
|
||||
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(err!(
|
||||
"datetime {dt} has negative year, \
|
||||
which cannot be formatted with RFC 2822",
|
||||
));
|
||||
return Err(Error::from(E::NegativeYear));
|
||||
}
|
||||
|
||||
wtr.write_str(weekday_abbrev(dt.weekday()))?;
|
||||
|
|
@ -1743,7 +1615,7 @@ mod tests {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
p("Thu, 10 Jan 2024 05:34:45 -0500"),
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: found parsed weekday of Thu, but parsed datetime of 2024-01-10T05:34:45 has weekday Wed",
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: found parsed weekday of `Thursday`, but parsed datetime has weekday `Wednesday`",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Wed, 29 Feb 2023 05:34:45 -0500"),
|
||||
|
|
@ -1755,11 +1627,11 @@ mod tests {
|
|||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Tue, 32 Jun 2024 05:34:45 -0500"),
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: day is not valid: parameter 'day' with value 32 is not in the required range of 1..=31",
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: failed to parse day: parameter 'day' with value 32 is not in the required range of 1..=31",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Sun, 30 Jun 2024 24:00:00 -0500"),
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: hour is not valid: parameter 'hour' with value 24 is not in the required range of 0..=23",
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: invalid hour: parameter 'hour' with value 24 is not in the required range of 0..=23",
|
||||
);
|
||||
// No whitespace after time
|
||||
insta::assert_snapshot!(
|
||||
|
|
@ -1780,43 +1652,43 @@ mod tests {
|
|||
);
|
||||
insta::assert_snapshot!(
|
||||
p(" "),
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected RFC 2822 datetime, but got empty string after trimming whitespace",
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected RFC 2822 datetime, but got empty string after trimming leading whitespace",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Wat"),
|
||||
@r###"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day at beginning of RFC 2822 datetime since first non-whitespace byte, "W", is not a digit, but given string is too short (length is 3)"###,
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day at beginning of RFC 2822 datetime since first non-whitespace byte, `W`, is not a digit, but given string is too short (length is 3)",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Wed"),
|
||||
@r###"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day at beginning of RFC 2822 datetime since first non-whitespace byte, "W", is not a digit, but given string is too short (length is 3)"###,
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day at beginning of RFC 2822 datetime since first non-whitespace byte, `W`, is not a digit, but given string is too short (length is 3)",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Wed "),
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected comma after parsed weekday `Wed` in RFC 2822 datetime, but found end of string instead",
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected comma after parsed weekday in RFC 2822 datetime, but found end of input instead",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Wed ,"),
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day, but found end of input",
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected numeric day, but found end of input",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Wed , "),
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day, but found end of input",
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected numeric day, but found end of input",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Wat, "),
|
||||
@r###"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day at beginning of RFC 2822 datetime since first non-whitespace byte, "W", is not a digit, but did not recognize "Wat" as a valid weekday abbreviation"###,
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day at beginning of RFC 2822 datetime since first non-whitespace byte, `W`, is not a digit, but did not recognize a valid weekday abbreviation",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Wed, "),
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day, but found end of input",
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected numeric day, but found end of input",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Wed, 1"),
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing day 1: expected at least one whitespace character (space or tab), but found none",
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing day: expected whitespace after parsing time: expected at least one whitespace character (space or tab), but found none",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Wed, 10"),
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing day 10: expected at least one whitespace character (space or tab), but found none",
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing day: expected whitespace after parsing time: expected at least one whitespace character (space or tab), but found none",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Wed, 10 J"),
|
||||
|
|
@ -1824,11 +1696,11 @@ mod tests {
|
|||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Wed, 10 Wat"),
|
||||
@r###"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected abbreviated month name, but did not recognize "Wat" as a valid month"###,
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected abbreviated month name, but did not recognize a valid abbreviated month name",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Wed, 10 Jan"),
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing month name: expected at least one whitespace character (space or tab), but found none",
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing abbreviated month name: expected whitespace after parsing time: expected at least one whitespace character (space or tab), but found none",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Wed, 10 Jan 2"),
|
||||
|
|
@ -1836,15 +1708,15 @@ mod tests {
|
|||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Wed, 10 Jan 2024"),
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing year: expected at least one whitespace character (space or tab), but found none",
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing year: expected whitespace after parsing time: expected at least one whitespace character (space or tab), but found none",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Wed, 10 Jan 2024 05"),
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected time separator of ':', but found end of input",
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected time separator of `:`, but found end of input",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Wed, 10 Jan 2024 053"),
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected time separator of ':', but found 3",
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected time separator of `:`, but found `3`",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Wed, 10 Jan 2024 05:34"),
|
||||
|
|
@ -1860,7 +1732,7 @@ mod tests {
|
|||
);
|
||||
insta::assert_snapshot!(
|
||||
p("Wed, 10 Jan 2024 05:34:45 J"),
|
||||
@r###"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected obsolete RFC 2822 time zone abbreviation, but found "J""###,
|
||||
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected obsolete RFC 2822 time zone abbreviation, but did not recognize a valid abbreviation",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -2040,7 +1912,7 @@ mod tests {
|
|||
.at(5, 34, 45, 0)
|
||||
.in_tz("America/New_York")
|
||||
.unwrap();
|
||||
insta::assert_snapshot!(p(&zdt), @"datetime -000001-01-10T05:34:45 has negative year, which cannot be formatted with RFC 2822");
|
||||
insta::assert_snapshot!(p(&zdt), @"datetime has negative year, which cannot be formatted with RFC 2822");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -2062,6 +1934,6 @@ mod tests {
|
|||
.in_tz("America/New_York")
|
||||
.unwrap()
|
||||
.timestamp();
|
||||
insta::assert_snapshot!(p(ts), @"datetime -000001-01-10T10:30:47 has negative year, which cannot be formatted with RFC 2822");
|
||||
insta::assert_snapshot!(p(ts), @"datetime has negative year, which cannot be formatted with RFC 2822");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,13 +95,13 @@ including by returning an error if it isn't supported.
|
|||
// UTCOffsetMinutePrecision
|
||||
|
||||
use crate::{
|
||||
error::{err, Error},
|
||||
error::{fmt::rfc9557::Error as E, Error},
|
||||
fmt::{
|
||||
offset::{self, ParsedOffset},
|
||||
temporal::{TimeZoneAnnotation, TimeZoneAnnotationKind},
|
||||
Parsed,
|
||||
},
|
||||
util::{escape, parse},
|
||||
util::parse,
|
||||
};
|
||||
|
||||
/// The result of parsing RFC 9557 annotations.
|
||||
|
|
@ -112,11 +112,6 @@ use crate::{
|
|||
/// only validated at a syntax level.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ParsedAnnotations<'i> {
|
||||
/// The original input that all of the annotations were parsed from.
|
||||
///
|
||||
/// N.B. This is currently unused, but potentially useful, so we leave it.
|
||||
#[allow(dead_code)]
|
||||
input: escape::Bytes<'i>,
|
||||
/// An optional time zone annotation that was extracted from the input.
|
||||
time_zone: Option<ParsedTimeZone<'i>>,
|
||||
// While we parse/validate them, we don't support any other annotations
|
||||
|
|
@ -127,7 +122,7 @@ pub(crate) struct ParsedAnnotations<'i> {
|
|||
impl<'i> ParsedAnnotations<'i> {
|
||||
/// Return an empty parsed annotations.
|
||||
pub(crate) fn none() -> ParsedAnnotations<'static> {
|
||||
ParsedAnnotations { input: escape::Bytes(&[]), time_zone: None }
|
||||
ParsedAnnotations { time_zone: None }
|
||||
}
|
||||
|
||||
/// Turns this parsed time zone into a structured time zone annotation,
|
||||
|
|
@ -212,8 +207,6 @@ impl Parser {
|
|||
&self,
|
||||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, ParsedAnnotations<'i>>, Error> {
|
||||
let mkslice = parse::slicer(input);
|
||||
|
||||
let Parsed { value: time_zone, mut input } =
|
||||
self.parse_time_zone_annotation(input)?;
|
||||
loop {
|
||||
|
|
@ -229,10 +222,7 @@ impl Parser {
|
|||
input = unconsumed;
|
||||
}
|
||||
|
||||
let value = ParsedAnnotations {
|
||||
input: escape::Bytes(mkslice(input)),
|
||||
time_zone,
|
||||
};
|
||||
let value = ParsedAnnotations { time_zone };
|
||||
Ok(Parsed { value, input })
|
||||
}
|
||||
|
||||
|
|
@ -241,14 +231,18 @@ impl Parser {
|
|||
mut input: &'i [u8],
|
||||
) -> Result<Parsed<'i, Option<ParsedTimeZone<'i>>>, Error> {
|
||||
let unconsumed = input;
|
||||
if input.is_empty() || input[0] != b'[' {
|
||||
let Some((&first, tail)) = input.split_first() else {
|
||||
return Ok(Parsed { value: None, input: unconsumed });
|
||||
};
|
||||
if first != b'[' {
|
||||
return Ok(Parsed { value: None, input: unconsumed });
|
||||
}
|
||||
input = &input[1..];
|
||||
input = tail;
|
||||
|
||||
let critical = input.starts_with(b"!");
|
||||
if critical {
|
||||
input = &input[1..];
|
||||
let mut critical = false;
|
||||
if let Some(tail) = input.strip_prefix(b"!") {
|
||||
critical = true;
|
||||
input = tail;
|
||||
}
|
||||
|
||||
// If we're starting with a `+` or `-`, then we know we MUST have a
|
||||
|
|
@ -284,8 +278,8 @@ impl Parser {
|
|||
// a generic key/value annotation.
|
||||
return Ok(Parsed { value: None, input: unconsumed });
|
||||
}
|
||||
while input.starts_with(b"/") {
|
||||
input = &input[1..];
|
||||
while let Some(tail) = input.strip_prefix(b"/") {
|
||||
input = tail;
|
||||
let Parsed { input: unconsumed, .. } =
|
||||
self.parse_tz_annotation_iana_name(input)?;
|
||||
input = unconsumed;
|
||||
|
|
@ -306,17 +300,21 @@ impl Parser {
|
|||
&self,
|
||||
mut input: &'i [u8],
|
||||
) -> Result<Parsed<'i, bool>, Error> {
|
||||
if input.is_empty() || input[0] != b'[' {
|
||||
let Some((&first, tail)) = input.split_first() else {
|
||||
return Ok(Parsed { value: false, input });
|
||||
};
|
||||
if first != b'[' {
|
||||
return Ok(Parsed { value: false, input });
|
||||
}
|
||||
input = &input[1..];
|
||||
input = tail;
|
||||
|
||||
let critical = input.starts_with(b"!");
|
||||
if critical {
|
||||
input = &input[1..];
|
||||
let mut critical = false;
|
||||
if let Some(tail) = input.strip_prefix(b"!") {
|
||||
critical = true;
|
||||
input = tail;
|
||||
}
|
||||
|
||||
let Parsed { value: key, input } = self.parse_annotation_key(input)?;
|
||||
let Parsed { input, .. } = self.parse_annotation_key(input)?;
|
||||
let Parsed { input, .. } = self.parse_annotation_separator(input)?;
|
||||
let Parsed { input, .. } = self.parse_annotation_values(input)?;
|
||||
let Parsed { input, .. } = self.parse_annotation_close(input)?;
|
||||
|
|
@ -326,11 +324,7 @@ impl Parser {
|
|||
// critical flag isn't set, we're "permissive" and just validate that
|
||||
// the syntax is correct (as we've already done at this point).
|
||||
if critical {
|
||||
return Err(err!(
|
||||
"found unsupported RFC 9557 annotation with key {key:?} \
|
||||
with the critical flag ('!') set",
|
||||
key = escape::Bytes(key),
|
||||
));
|
||||
return Err(Error::from(E::UnsupportedAnnotationCritical));
|
||||
}
|
||||
|
||||
Ok(Parsed { value: true, input })
|
||||
|
|
@ -381,8 +375,8 @@ impl Parser {
|
|||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, ()>, Error> {
|
||||
let Parsed { mut input, .. } = self.parse_annotation_value(input)?;
|
||||
while input.starts_with(b"-") {
|
||||
input = &input[1..];
|
||||
while let Some(tail) = input.strip_prefix(b"-") {
|
||||
input = tail;
|
||||
let Parsed { input: unconsumed, .. } =
|
||||
self.parse_annotation_value(input)?;
|
||||
input = unconsumed;
|
||||
|
|
@ -413,173 +407,137 @@ impl Parser {
|
|||
&self,
|
||||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, ()>, Error> {
|
||||
if input.is_empty() {
|
||||
return Err(err!(
|
||||
"expected the start of an RFC 9557 annotation or IANA \
|
||||
time zone component name, but found end of input instead",
|
||||
));
|
||||
let Some((&first, tail)) = input.split_first() else {
|
||||
return Err(Error::from(E::EndOfInputAnnotation));
|
||||
};
|
||||
if !matches!(first, b'_' | b'.' | b'A'..=b'Z' | b'a'..=b'z') {
|
||||
return Err(Error::from(E::UnexpectedByteAnnotation {
|
||||
byte: first,
|
||||
}));
|
||||
}
|
||||
if !matches!(input[0], b'_' | b'.' | b'A'..=b'Z' | b'a'..=b'z') {
|
||||
return Err(err!(
|
||||
"expected ASCII alphabetic byte (or underscore or period) \
|
||||
at the start of an RFC 9557 annotation or time zone \
|
||||
component name, but found {:?} instead",
|
||||
escape::Byte(input[0]),
|
||||
));
|
||||
}
|
||||
Ok(Parsed { value: (), input: &input[1..] })
|
||||
Ok(Parsed { value: (), input: tail })
|
||||
}
|
||||
|
||||
fn parse_tz_annotation_char<'i>(
|
||||
&self,
|
||||
input: &'i [u8],
|
||||
) -> Parsed<'i, bool> {
|
||||
let is_tz_annotation_char = |byte| {
|
||||
matches!(
|
||||
byte,
|
||||
b'_' | b'.' | b'+' | b'-' | b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z',
|
||||
)
|
||||
let Some((&first, tail)) = input.split_first() else {
|
||||
return Parsed { value: false, input };
|
||||
};
|
||||
if input.is_empty() || !is_tz_annotation_char(input[0]) {
|
||||
|
||||
if !matches!(
|
||||
first,
|
||||
b'_' | b'.' | b'+' | b'-' | b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z',
|
||||
) {
|
||||
return Parsed { value: false, input };
|
||||
}
|
||||
Parsed { value: true, input: &input[1..] }
|
||||
Parsed { value: true, input: tail }
|
||||
}
|
||||
|
||||
fn parse_annotation_key_leading_char<'i>(
|
||||
&self,
|
||||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, ()>, Error> {
|
||||
if input.is_empty() {
|
||||
return Err(err!(
|
||||
"expected the start of an RFC 9557 annotation key, \
|
||||
but found end of input instead",
|
||||
));
|
||||
let Some((&first, tail)) = input.split_first() else {
|
||||
return Err(Error::from(E::EndOfInputAnnotationKey));
|
||||
};
|
||||
if !matches!(first, b'_' | b'a'..=b'z') {
|
||||
return Err(Error::from(E::UnexpectedByteAnnotationKey {
|
||||
byte: first,
|
||||
}));
|
||||
}
|
||||
if !matches!(input[0], b'_' | b'a'..=b'z') {
|
||||
return Err(err!(
|
||||
"expected lowercase alphabetic byte (or underscore) \
|
||||
at the start of an RFC 9557 annotation key, \
|
||||
but found {:?} instead",
|
||||
escape::Byte(input[0]),
|
||||
));
|
||||
}
|
||||
Ok(Parsed { value: (), input: &input[1..] })
|
||||
Ok(Parsed { value: (), input: tail })
|
||||
}
|
||||
|
||||
fn parse_annotation_key_char<'i>(
|
||||
&self,
|
||||
input: &'i [u8],
|
||||
) -> Parsed<'i, bool> {
|
||||
let is_annotation_key_char =
|
||||
|byte| matches!(byte, b'_' | b'-' | b'0'..=b'9' | b'a'..=b'z');
|
||||
if input.is_empty() || !is_annotation_key_char(input[0]) {
|
||||
let Some((&first, tail)) = input.split_first() else {
|
||||
return Parsed { value: false, input };
|
||||
};
|
||||
if !matches!(first, b'_' | b'-' | b'0'..=b'9' | b'a'..=b'z') {
|
||||
return Parsed { value: false, input };
|
||||
}
|
||||
Parsed { value: true, input: &input[1..] }
|
||||
Parsed { value: true, input: tail }
|
||||
}
|
||||
|
||||
fn parse_annotation_value_leading_char<'i>(
|
||||
&self,
|
||||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, ()>, Error> {
|
||||
if input.is_empty() {
|
||||
return Err(err!(
|
||||
"expected the start of an RFC 9557 annotation value, \
|
||||
but found end of input instead",
|
||||
));
|
||||
let Some((&first, tail)) = input.split_first() else {
|
||||
return Err(Error::from(E::EndOfInputAnnotationValue));
|
||||
};
|
||||
if !matches!(first, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z') {
|
||||
return Err(Error::from(E::UnexpectedByteAnnotationValue {
|
||||
byte: first,
|
||||
}));
|
||||
}
|
||||
if !matches!(input[0], b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z') {
|
||||
return Err(err!(
|
||||
"expected alphanumeric ASCII byte \
|
||||
at the start of an RFC 9557 annotation value, \
|
||||
but found {:?} instead",
|
||||
escape::Byte(input[0]),
|
||||
));
|
||||
}
|
||||
Ok(Parsed { value: (), input: &input[1..] })
|
||||
Ok(Parsed { value: (), input: tail })
|
||||
}
|
||||
|
||||
fn parse_annotation_value_char<'i>(
|
||||
&self,
|
||||
input: &'i [u8],
|
||||
) -> Parsed<'i, bool> {
|
||||
let is_annotation_value_char =
|
||||
|byte| matches!(byte, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z');
|
||||
if input.is_empty() || !is_annotation_value_char(input[0]) {
|
||||
let Some((&first, tail)) = input.split_first() else {
|
||||
return Parsed { value: false, input };
|
||||
};
|
||||
if !matches!(first, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z') {
|
||||
return Parsed { value: false, input };
|
||||
}
|
||||
Parsed { value: true, input: &input[1..] }
|
||||
Parsed { value: true, input: tail }
|
||||
}
|
||||
|
||||
fn parse_annotation_separator<'i>(
|
||||
&self,
|
||||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, ()>, Error> {
|
||||
if input.is_empty() {
|
||||
return Err(err!(
|
||||
"expected an '=' after parsing an RFC 9557 annotation key, \
|
||||
but found end of input instead",
|
||||
));
|
||||
}
|
||||
if input[0] != b'=' {
|
||||
let Some((&first, tail)) = input.split_first() else {
|
||||
return Err(Error::from(E::EndOfInputAnnotationSeparator));
|
||||
};
|
||||
if first != b'=' {
|
||||
// If we see a /, then it's likely the user was trying to insert a
|
||||
// time zone annotation in the wrong place.
|
||||
return Err(if input[0] == b'/' {
|
||||
err!(
|
||||
"expected an '=' after parsing an RFC 9557 annotation \
|
||||
key, but found / instead (time zone annotations must \
|
||||
come first)",
|
||||
)
|
||||
return Err(Error::from(if first == b'/' {
|
||||
E::UnexpectedSlashAnnotationSeparator
|
||||
} else {
|
||||
err!(
|
||||
"expected an '=' after parsing an RFC 9557 annotation \
|
||||
key, but found {:?} instead",
|
||||
escape::Byte(input[0]),
|
||||
)
|
||||
});
|
||||
E::UnexpectedByteAnnotationSeparator { byte: first }
|
||||
}));
|
||||
}
|
||||
Ok(Parsed { value: (), input: &input[1..] })
|
||||
Ok(Parsed { value: (), input: tail })
|
||||
}
|
||||
|
||||
fn parse_annotation_close<'i>(
|
||||
&self,
|
||||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, ()>, Error> {
|
||||
if input.is_empty() {
|
||||
return Err(err!(
|
||||
"expected an ']' after parsing an RFC 9557 annotation key \
|
||||
and value, but found end of input instead",
|
||||
));
|
||||
let Some((&first, tail)) = input.split_first() else {
|
||||
return Err(Error::from(E::EndOfInputAnnotationClose));
|
||||
};
|
||||
if first != b']' {
|
||||
return Err(Error::from(E::UnexpectedByteAnnotationClose {
|
||||
byte: first,
|
||||
}));
|
||||
}
|
||||
if input[0] != b']' {
|
||||
return Err(err!(
|
||||
"expected an ']' after parsing an RFC 9557 annotation key \
|
||||
and value, but found {:?} instead",
|
||||
escape::Byte(input[0]),
|
||||
));
|
||||
}
|
||||
Ok(Parsed { value: (), input: &input[1..] })
|
||||
Ok(Parsed { value: (), input: tail })
|
||||
}
|
||||
|
||||
fn parse_tz_annotation_close<'i>(
|
||||
&self,
|
||||
input: &'i [u8],
|
||||
) -> Result<Parsed<'i, ()>, Error> {
|
||||
if input.is_empty() {
|
||||
return Err(err!(
|
||||
"expected an ']' after parsing an RFC 9557 time zone \
|
||||
annotation, but found end of input instead",
|
||||
));
|
||||
let Some((&first, tail)) = input.split_first() else {
|
||||
return Err(Error::from(E::EndOfInputTzAnnotationClose));
|
||||
};
|
||||
if first != b']' {
|
||||
return Err(Error::from(E::UnexpectedByteTzAnnotationClose {
|
||||
byte: first,
|
||||
}));
|
||||
}
|
||||
if input[0] != b']' {
|
||||
return Err(err!(
|
||||
"expected an ']' after parsing an RFC 9557 time zone \
|
||||
annotation, but found {:?} instead",
|
||||
escape::Byte(input[0]),
|
||||
));
|
||||
}
|
||||
Ok(Parsed { value: (), input: &input[1..] })
|
||||
Ok(Parsed { value: (), input: tail })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -665,24 +623,22 @@ mod tests {
|
|||
fn ok_empty() {
|
||||
let p = |input| Parser::new().parse(input).unwrap();
|
||||
|
||||
insta::assert_debug_snapshot!(p(b""), @r###"
|
||||
insta::assert_debug_snapshot!(p(b""), @r#"
|
||||
Parsed {
|
||||
value: ParsedAnnotations {
|
||||
input: "",
|
||||
time_zone: None,
|
||||
},
|
||||
input: "",
|
||||
}
|
||||
"###);
|
||||
insta::assert_debug_snapshot!(p(b"blah"), @r###"
|
||||
"#);
|
||||
insta::assert_debug_snapshot!(p(b"blah"), @r#"
|
||||
Parsed {
|
||||
value: ParsedAnnotations {
|
||||
input: "",
|
||||
time_zone: None,
|
||||
},
|
||||
input: "blah",
|
||||
}
|
||||
"###);
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -691,39 +647,36 @@ mod tests {
|
|||
|
||||
insta::assert_debug_snapshot!(
|
||||
p(b"[u-ca=chinese]"),
|
||||
@r###"
|
||||
@r#"
|
||||
Parsed {
|
||||
value: ParsedAnnotations {
|
||||
input: "[u-ca=chinese]",
|
||||
time_zone: None,
|
||||
},
|
||||
input: "",
|
||||
}
|
||||
"###,
|
||||
"#,
|
||||
);
|
||||
insta::assert_debug_snapshot!(
|
||||
p(b"[u-ca=chinese-japanese]"),
|
||||
@r###"
|
||||
@r#"
|
||||
Parsed {
|
||||
value: ParsedAnnotations {
|
||||
input: "[u-ca=chinese-japanese]",
|
||||
time_zone: None,
|
||||
},
|
||||
input: "",
|
||||
}
|
||||
"###,
|
||||
"#,
|
||||
);
|
||||
insta::assert_debug_snapshot!(
|
||||
p(b"[u-ca=chinese-japanese-russian]"),
|
||||
@r###"
|
||||
@r#"
|
||||
Parsed {
|
||||
value: ParsedAnnotations {
|
||||
input: "[u-ca=chinese-japanese-russian]",
|
||||
time_zone: None,
|
||||
},
|
||||
input: "",
|
||||
}
|
||||
"###,
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -731,10 +684,9 @@ mod tests {
|
|||
fn ok_iana() {
|
||||
let p = |input| Parser::new().parse(input).unwrap();
|
||||
|
||||
insta::assert_debug_snapshot!(p(b"[America/New_York]"), @r###"
|
||||
insta::assert_debug_snapshot!(p(b"[America/New_York]"), @r#"
|
||||
Parsed {
|
||||
value: ParsedAnnotations {
|
||||
input: "[America/New_York]",
|
||||
time_zone: Some(
|
||||
Named {
|
||||
critical: false,
|
||||
|
|
@ -744,11 +696,10 @@ mod tests {
|
|||
},
|
||||
input: "",
|
||||
}
|
||||
"###);
|
||||
insta::assert_debug_snapshot!(p(b"[!America/New_York]"), @r###"
|
||||
"#);
|
||||
insta::assert_debug_snapshot!(p(b"[!America/New_York]"), @r#"
|
||||
Parsed {
|
||||
value: ParsedAnnotations {
|
||||
input: "[!America/New_York]",
|
||||
time_zone: Some(
|
||||
Named {
|
||||
critical: true,
|
||||
|
|
@ -758,11 +709,10 @@ mod tests {
|
|||
},
|
||||
input: "",
|
||||
}
|
||||
"###);
|
||||
insta::assert_debug_snapshot!(p(b"[UTC]"), @r###"
|
||||
"#);
|
||||
insta::assert_debug_snapshot!(p(b"[UTC]"), @r#"
|
||||
Parsed {
|
||||
value: ParsedAnnotations {
|
||||
input: "[UTC]",
|
||||
time_zone: Some(
|
||||
Named {
|
||||
critical: false,
|
||||
|
|
@ -772,11 +722,10 @@ mod tests {
|
|||
},
|
||||
input: "",
|
||||
}
|
||||
"###);
|
||||
insta::assert_debug_snapshot!(p(b"[.._foo_../.0+-]"), @r###"
|
||||
"#);
|
||||
insta::assert_debug_snapshot!(p(b"[.._foo_../.0+-]"), @r#"
|
||||
Parsed {
|
||||
value: ParsedAnnotations {
|
||||
input: "[.._foo_../.0+-]",
|
||||
time_zone: Some(
|
||||
Named {
|
||||
critical: false,
|
||||
|
|
@ -786,17 +735,16 @@ mod tests {
|
|||
},
|
||||
input: "",
|
||||
}
|
||||
"###);
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ok_offset() {
|
||||
let p = |input| Parser::new().parse(input).unwrap();
|
||||
|
||||
insta::assert_debug_snapshot!(p(b"[-00]"), @r###"
|
||||
insta::assert_debug_snapshot!(p(b"[-00]"), @r#"
|
||||
Parsed {
|
||||
value: ParsedAnnotations {
|
||||
input: "[-00]",
|
||||
time_zone: Some(
|
||||
Offset {
|
||||
critical: false,
|
||||
|
|
@ -810,11 +758,10 @@ mod tests {
|
|||
},
|
||||
input: "",
|
||||
}
|
||||
"###);
|
||||
insta::assert_debug_snapshot!(p(b"[+00]"), @r###"
|
||||
"#);
|
||||
insta::assert_debug_snapshot!(p(b"[+00]"), @r#"
|
||||
Parsed {
|
||||
value: ParsedAnnotations {
|
||||
input: "[+00]",
|
||||
time_zone: Some(
|
||||
Offset {
|
||||
critical: false,
|
||||
|
|
@ -828,11 +775,10 @@ mod tests {
|
|||
},
|
||||
input: "",
|
||||
}
|
||||
"###);
|
||||
insta::assert_debug_snapshot!(p(b"[-05]"), @r###"
|
||||
"#);
|
||||
insta::assert_debug_snapshot!(p(b"[-05]"), @r#"
|
||||
Parsed {
|
||||
value: ParsedAnnotations {
|
||||
input: "[-05]",
|
||||
time_zone: Some(
|
||||
Offset {
|
||||
critical: false,
|
||||
|
|
@ -846,11 +792,10 @@ mod tests {
|
|||
},
|
||||
input: "",
|
||||
}
|
||||
"###);
|
||||
insta::assert_debug_snapshot!(p(b"[!+05:12]"), @r###"
|
||||
"#);
|
||||
insta::assert_debug_snapshot!(p(b"[!+05:12]"), @r#"
|
||||
Parsed {
|
||||
value: ParsedAnnotations {
|
||||
input: "[!+05:12]",
|
||||
time_zone: Some(
|
||||
Offset {
|
||||
critical: true,
|
||||
|
|
@ -864,7 +809,7 @@ mod tests {
|
|||
},
|
||||
input: "",
|
||||
}
|
||||
"###);
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -873,10 +818,9 @@ mod tests {
|
|||
|
||||
insta::assert_debug_snapshot!(
|
||||
p(b"[America/New_York][u-ca=chinese-japanese-russian]"),
|
||||
@r###"
|
||||
@r#"
|
||||
Parsed {
|
||||
value: ParsedAnnotations {
|
||||
input: "[America/New_York][u-ca=chinese-japanese-russian]",
|
||||
time_zone: Some(
|
||||
Named {
|
||||
critical: false,
|
||||
|
|
@ -886,7 +830,7 @@ mod tests {
|
|||
},
|
||||
input: "",
|
||||
}
|
||||
"###,
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -894,11 +838,11 @@ mod tests {
|
|||
fn err_iana() {
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[0/Foo]").unwrap_err(),
|
||||
@r###"expected ASCII alphabetic byte (or underscore or period) at the start of an RFC 9557 annotation or time zone component name, but found "0" instead"###,
|
||||
@"expected ASCII alphabetic byte (or underscore or period) at the start of an RFC 9557 annotation or time zone component name, but found `0` instead",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[Foo/0Bar]").unwrap_err(),
|
||||
@r###"expected ASCII alphabetic byte (or underscore or period) at the start of an RFC 9557 annotation or time zone component name, but found "0" instead"###,
|
||||
@"expected ASCII alphabetic byte (or underscore or period) at the start of an RFC 9557 annotation or time zone component name, but found `0` instead",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -906,23 +850,23 @@ mod tests {
|
|||
fn err_offset() {
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[+").unwrap_err(),
|
||||
@r###"failed to parse hours in UTC numeric offset "+": expected two digit hour after sign, but found end of input"###,
|
||||
@"failed to parse hours in UTC numeric offset: expected two digit hour after sign, but found end of input",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[+26]").unwrap_err(),
|
||||
@r###"failed to parse hours in UTC numeric offset "+26]": offset hours are not valid: parameter 'hours' with value 26 is not in the required range of 0..=25"###,
|
||||
@"failed to parse hours in UTC numeric offset: hour in time zone offset is out of range: parameter 'hours' with value 26 is not in the required range of 0..=25",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[-26]").unwrap_err(),
|
||||
@r###"failed to parse hours in UTC numeric offset "-26]": offset hours are not valid: parameter 'hours' with value 26 is not in the required range of 0..=25"###,
|
||||
@"failed to parse hours in UTC numeric offset: hour in time zone offset is out of range: parameter 'hours' with value 26 is not in the required range of 0..=25",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[+05:12:34]").unwrap_err(),
|
||||
@r###"subminute precision for UTC numeric offset "+05:12:34]" is not enabled in this context (must provide only integral minutes)"###,
|
||||
@"subminute precision for UTC numeric offset is not enabled in this context (must provide only integral minutes)",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[+05:12:34.123456789]").unwrap_err(),
|
||||
@r###"subminute precision for UTC numeric offset "+05:12:34.123456789]" is not enabled in this context (must provide only integral minutes)"###,
|
||||
@"subminute precision for UTC numeric offset is not enabled in this context (must provide only integral minutes)",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -930,7 +874,7 @@ mod tests {
|
|||
fn err_critical_unsupported() {
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[!u-ca=chinese]").unwrap_err(),
|
||||
@r###"found unsupported RFC 9557 annotation with key "u-ca" with the critical flag ('!') set"###,
|
||||
@"found unsupported RFC 9557 annotation with the critical flag (`!`) set",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -942,7 +886,7 @@ mod tests {
|
|||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[&").unwrap_err(),
|
||||
@r###"expected ASCII alphabetic byte (or underscore or period) at the start of an RFC 9557 annotation or time zone component name, but found "&" instead"###,
|
||||
@"expected ASCII alphabetic byte (or underscore or period) at the start of an RFC 9557 annotation or time zone component name, but found `&` instead",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[Foo][").unwrap_err(),
|
||||
|
|
@ -950,7 +894,7 @@ mod tests {
|
|||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[Foo][&").unwrap_err(),
|
||||
@r###"expected lowercase alphabetic byte (or underscore) at the start of an RFC 9557 annotation key, but found "&" instead"###,
|
||||
@"expected lowercase alphabetic byte (or underscore) at the start of an RFC 9557 annotation key, but found `&` instead",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -958,27 +902,27 @@ mod tests {
|
|||
fn err_separator() {
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[abc").unwrap_err(),
|
||||
@"expected an ']' after parsing an RFC 9557 time zone annotation, but found end of input instead",
|
||||
@"expected an `]` after parsing an RFC 9557 time zone annotation, but found end of input instead",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[_abc").unwrap_err(),
|
||||
@"expected an ']' after parsing an RFC 9557 time zone annotation, but found end of input instead",
|
||||
@"expected an `]` after parsing an RFC 9557 time zone annotation, but found end of input instead",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[abc^").unwrap_err(),
|
||||
@r###"expected an ']' after parsing an RFC 9557 time zone annotation, but found "^" instead"###,
|
||||
@"expected an `]` after parsing an RFC 9557 time zone annotation, but found `^` instead",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[Foo][abc").unwrap_err(),
|
||||
@"expected an '=' after parsing an RFC 9557 annotation key, but found end of input instead",
|
||||
@"expected an `=` after parsing an RFC 9557 annotation key, but found end of input instead",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[Foo][_abc").unwrap_err(),
|
||||
@"expected an '=' after parsing an RFC 9557 annotation key, but found end of input instead",
|
||||
@"expected an `=` after parsing an RFC 9557 annotation key, but found end of input instead",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[Foo][abc^").unwrap_err(),
|
||||
@r###"expected an '=' after parsing an RFC 9557 annotation key, but found "^" instead"###,
|
||||
@"expected an `=` after parsing an RFC 9557 annotation key, but found `^` instead",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -994,11 +938,11 @@ mod tests {
|
|||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[abc=^").unwrap_err(),
|
||||
@r###"expected alphanumeric ASCII byte at the start of an RFC 9557 annotation value, but found "^" instead"###,
|
||||
@"expected alphanumeric ASCII byte at the start of an RFC 9557 annotation value, but found `^` instead",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[abc=]").unwrap_err(),
|
||||
@r###"expected alphanumeric ASCII byte at the start of an RFC 9557 annotation value, but found "]" instead"###,
|
||||
@"expected alphanumeric ASCII byte at the start of an RFC 9557 annotation value, but found `]` instead",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1006,11 +950,11 @@ mod tests {
|
|||
fn err_close() {
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[abc=123").unwrap_err(),
|
||||
@"expected an ']' after parsing an RFC 9557 annotation key and value, but found end of input instead",
|
||||
@"expected an `]` after parsing an RFC 9557 annotation key and value, but found end of input instead",
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
Parser::new().parse(b"[abc=123*").unwrap_err(),
|
||||
@r###"expected an ']' after parsing an RFC 9557 annotation key and value, but found "*" instead"###,
|
||||
@"expected an `]` after parsing an RFC 9557 annotation key and value, but found `*` instead",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1046,7 +990,7 @@ mod tests {
|
|||
let p = |input| Parser::new().parse(input).unwrap_err();
|
||||
insta::assert_snapshot!(
|
||||
p(b"[america/new_york][america/new_york]"),
|
||||
@"expected an '=' after parsing an RFC 9557 annotation key, but found / instead (time zone annotations must come first)",
|
||||
@"expected an `=` after parsing an RFC 9557 annotation key, but found `/` instead (time zone annotations must come first)",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1778,28 +1778,24 @@ pub mod unsigned_duration {
|
|||
fn parse_iso_or_friendly(
|
||||
bytes: &[u8],
|
||||
) -> Result<core::time::Duration, crate::Error> {
|
||||
if bytes.is_empty() {
|
||||
return Err(crate::error::err!(
|
||||
"an empty string is not a valid `std::time::Duration`, \
|
||||
expected either a ISO 8601 or Jiff's 'friendly' \
|
||||
format",
|
||||
let Some((&byte, tail)) = bytes.split_first() else {
|
||||
return Err(crate::Error::from(
|
||||
crate::error::fmt::Error::HybridDurationEmpty,
|
||||
));
|
||||
}
|
||||
let mut first = bytes[0];
|
||||
};
|
||||
let mut first = byte;
|
||||
// N.B. Unsigned durations don't support negative durations (of
|
||||
// course), but we still check for it here so that we can defer to
|
||||
// the dedicated parsers. They will provide their own error messages.
|
||||
if first == b'+' || first == b'-' {
|
||||
if bytes.len() == 1 {
|
||||
return Err(crate::error::err!(
|
||||
"found nothing after sign `{sign}`, \
|
||||
which is not a valid `std::time::Duration`, \
|
||||
expected either a ISO 8601 or Jiff's 'friendly' \
|
||||
format",
|
||||
sign = crate::util::escape::Byte(first),
|
||||
let Some(&byte) = tail.first() else {
|
||||
return Err(crate::Error::from(
|
||||
crate::error::fmt::Error::HybridDurationPrefix {
|
||||
sign: first,
|
||||
},
|
||||
));
|
||||
}
|
||||
first = bytes[1];
|
||||
};
|
||||
first = byte;
|
||||
}
|
||||
let dur = if first == b'P' || first == b'p' {
|
||||
crate::fmt::temporal::DEFAULT_SPAN_PARSER
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
use crate::{
|
||||
error::{err, ErrorContext},
|
||||
error::{
|
||||
fmt::strtime::{Error as E, FormatError as FE},
|
||||
ErrorContext,
|
||||
},
|
||||
fmt::{
|
||||
strtime::{
|
||||
month_name_abbrev, month_name_full, weekday_name_abbrev,
|
||||
weekday_name_full, BrokenDownTime, Config, Custom, Extension,
|
||||
Flag,
|
||||
},
|
||||
util::{DecimalFormatter, FractionalFormatter},
|
||||
util::{FractionalFormatter, IntegerFormatter},
|
||||
Write, WriteExt,
|
||||
},
|
||||
tz::Offset,
|
||||
util::{escape, utf8},
|
||||
util::utf8,
|
||||
Error,
|
||||
};
|
||||
|
||||
|
|
@ -39,10 +42,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
self.wtr.write_str("%")?;
|
||||
break;
|
||||
}
|
||||
return Err(err!(
|
||||
"invalid format string, expected byte after '%', \
|
||||
but found end of format string",
|
||||
));
|
||||
return Err(E::UnexpectedEndAfterPercent.into());
|
||||
}
|
||||
let orig = self.fmt;
|
||||
if let Err(err) = self.format_one() {
|
||||
|
|
@ -61,100 +61,92 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
}
|
||||
|
||||
fn format_one(&mut self) -> Result<(), Error> {
|
||||
let failc =
|
||||
|directive, colons| E::DirectiveFailure { directive, colons };
|
||||
let fail = |directive| failc(directive, 0);
|
||||
|
||||
// Parse extensions like padding/case options and padding width.
|
||||
let ext = self.parse_extension()?;
|
||||
match self.f() {
|
||||
b'%' => self.wtr.write_str("%").context("%% failed")?,
|
||||
b'A' => self.fmt_weekday_full(&ext).context("%A failed")?,
|
||||
b'a' => self.fmt_weekday_abbrev(&ext).context("%a failed")?,
|
||||
b'B' => self.fmt_month_full(&ext).context("%B failed")?,
|
||||
b'b' => self.fmt_month_abbrev(&ext).context("%b failed")?,
|
||||
b'C' => self.fmt_century(&ext).context("%C failed")?,
|
||||
b'c' => self.fmt_datetime(&ext).context("%c failed")?,
|
||||
b'D' => self.fmt_american_date(&ext).context("%D failed")?,
|
||||
b'd' => self.fmt_day_zero(&ext).context("%d failed")?,
|
||||
b'e' => self.fmt_day_space(&ext).context("%e failed")?,
|
||||
b'F' => self.fmt_iso_date(&ext).context("%F failed")?,
|
||||
b'f' => self.fmt_fractional(&ext).context("%f failed")?,
|
||||
b'G' => self.fmt_iso_week_year(&ext).context("%G failed")?,
|
||||
b'g' => self.fmt_iso_week_year2(&ext).context("%g failed")?,
|
||||
b'H' => self.fmt_hour24_zero(&ext).context("%H failed")?,
|
||||
b'h' => self.fmt_month_abbrev(&ext).context("%b failed")?,
|
||||
b'I' => self.fmt_hour12_zero(&ext).context("%H failed")?,
|
||||
b'j' => self.fmt_day_of_year(&ext).context("%j failed")?,
|
||||
b'k' => self.fmt_hour24_space(&ext).context("%k failed")?,
|
||||
b'l' => self.fmt_hour12_space(&ext).context("%l failed")?,
|
||||
b'M' => self.fmt_minute(&ext).context("%M failed")?,
|
||||
b'm' => self.fmt_month(&ext).context("%m failed")?,
|
||||
b'N' => self.fmt_nanoseconds(&ext).context("%N failed")?,
|
||||
b'n' => self.fmt_literal("\n").context("%n failed")?,
|
||||
b'P' => self.fmt_ampm_lower(&ext).context("%P failed")?,
|
||||
b'p' => self.fmt_ampm_upper(&ext).context("%p failed")?,
|
||||
b'%' => self.wtr.write_str("%").context(fail(b'%')),
|
||||
b'A' => self.fmt_weekday_full(&ext).context(fail(b'A')),
|
||||
b'a' => self.fmt_weekday_abbrev(&ext).context(fail(b'a')),
|
||||
b'B' => self.fmt_month_full(&ext).context(fail(b'B')),
|
||||
b'b' => self.fmt_month_abbrev(&ext).context(fail(b'b')),
|
||||
b'C' => self.fmt_century(&ext).context(fail(b'C')),
|
||||
b'c' => self.fmt_datetime(&ext).context(fail(b'c')),
|
||||
b'D' => self.fmt_american_date(&ext).context(fail(b'D')),
|
||||
b'd' => self.fmt_day_zero(&ext).context(fail(b'd')),
|
||||
b'e' => self.fmt_day_space(&ext).context(fail(b'e')),
|
||||
b'F' => self.fmt_iso_date(&ext).context(fail(b'F')),
|
||||
b'f' => self.fmt_fractional(&ext).context(fail(b'f')),
|
||||
b'G' => self.fmt_iso_week_year(&ext).context(fail(b'G')),
|
||||
b'g' => self.fmt_iso_week_year2(&ext).context(fail(b'g')),
|
||||
b'H' => self.fmt_hour24_zero(&ext).context(fail(b'H')),
|
||||
b'h' => self.fmt_month_abbrev(&ext).context(fail(b'b')),
|
||||
b'I' => self.fmt_hour12_zero(&ext).context(fail(b'H')),
|
||||
b'j' => self.fmt_day_of_year(&ext).context(fail(b'j')),
|
||||
b'k' => self.fmt_hour24_space(&ext).context(fail(b'k')),
|
||||
b'l' => self.fmt_hour12_space(&ext).context(fail(b'l')),
|
||||
b'M' => self.fmt_minute(&ext).context(fail(b'M')),
|
||||
b'm' => self.fmt_month(&ext).context(fail(b'm')),
|
||||
b'N' => self.fmt_nanoseconds(&ext).context(fail(b'N')),
|
||||
b'n' => self.fmt_literal("\n").context(fail(b'n')),
|
||||
b'P' => self.fmt_ampm_lower(&ext).context(fail(b'P')),
|
||||
b'p' => self.fmt_ampm_upper(&ext).context(fail(b'p')),
|
||||
b'Q' => match ext.colons {
|
||||
0 => self.fmt_iana_nocolon().context("%Q failed")?,
|
||||
1 => self.fmt_iana_colon().context("%:Q failed")?,
|
||||
_ => {
|
||||
return Err(err!(
|
||||
"invalid number of `:` in `%Q` directive"
|
||||
))
|
||||
}
|
||||
0 => self.fmt_iana_nocolon().context(fail(b'Q')),
|
||||
1 => self.fmt_iana_colon().context(failc(b'Q', 1)),
|
||||
_ => return Err(E::ColonCount { directive: b'Q' }.into()),
|
||||
},
|
||||
b'q' => self.fmt_quarter(&ext).context("%q failed")?,
|
||||
b'R' => self.fmt_clock_nosecs(&ext).context("%R failed")?,
|
||||
b'r' => self.fmt_12hour_time(&ext).context("%r failed")?,
|
||||
b'S' => self.fmt_second(&ext).context("%S failed")?,
|
||||
b's' => self.fmt_timestamp(&ext).context("%s failed")?,
|
||||
b'T' => self.fmt_clock_secs(&ext).context("%T failed")?,
|
||||
b't' => self.fmt_literal("\t").context("%t failed")?,
|
||||
b'U' => self.fmt_week_sun(&ext).context("%U failed")?,
|
||||
b'u' => self.fmt_weekday_mon(&ext).context("%u failed")?,
|
||||
b'V' => self.fmt_week_iso(&ext).context("%V failed")?,
|
||||
b'W' => self.fmt_week_mon(&ext).context("%W failed")?,
|
||||
b'w' => self.fmt_weekday_sun(&ext).context("%w failed")?,
|
||||
b'X' => self.fmt_time(&ext).context("%X failed")?,
|
||||
b'x' => self.fmt_date(&ext).context("%x failed")?,
|
||||
b'Y' => self.fmt_year(&ext).context("%Y failed")?,
|
||||
b'y' => self.fmt_year2(&ext).context("%y failed")?,
|
||||
b'Z' => self.fmt_tzabbrev(&ext).context("%Z failed")?,
|
||||
b'q' => self.fmt_quarter(&ext).context(fail(b'q')),
|
||||
b'R' => self.fmt_clock_nosecs(&ext).context(fail(b'R')),
|
||||
b'r' => self.fmt_12hour_time(&ext).context(fail(b'r')),
|
||||
b'S' => self.fmt_second(&ext).context(fail(b'S')),
|
||||
b's' => self.fmt_timestamp(&ext).context(fail(b's')),
|
||||
b'T' => self.fmt_clock_secs(&ext).context(fail(b'T')),
|
||||
b't' => self.fmt_literal("\t").context(fail(b't')),
|
||||
b'U' => self.fmt_week_sun(&ext).context(fail(b'U')),
|
||||
b'u' => self.fmt_weekday_mon(&ext).context(fail(b'u')),
|
||||
b'V' => self.fmt_week_iso(&ext).context(fail(b'V')),
|
||||
b'W' => self.fmt_week_mon(&ext).context(fail(b'W')),
|
||||
b'w' => self.fmt_weekday_sun(&ext).context(fail(b'w')),
|
||||
b'X' => self.fmt_time(&ext).context(fail(b'X')),
|
||||
b'x' => self.fmt_date(&ext).context(fail(b'x')),
|
||||
b'Y' => self.fmt_year(&ext).context(fail(b'Y')),
|
||||
b'y' => self.fmt_year2(&ext).context(fail(b'y')),
|
||||
b'Z' => self.fmt_tzabbrev(&ext).context(fail(b'Z')),
|
||||
b'z' => match ext.colons {
|
||||
0 => self.fmt_offset_nocolon().context("%z failed")?,
|
||||
1 => self.fmt_offset_colon().context("%:z failed")?,
|
||||
2 => self.fmt_offset_colon2().context("%::z failed")?,
|
||||
3 => self.fmt_offset_colon3().context("%:::z failed")?,
|
||||
_ => {
|
||||
return Err(err!(
|
||||
"invalid number of `:` in `%z` directive"
|
||||
))
|
||||
}
|
||||
0 => self.fmt_offset_nocolon().context(fail(b'z')),
|
||||
1 => self.fmt_offset_colon().context(failc(b'z', 1)),
|
||||
2 => self.fmt_offset_colon2().context(failc(b'z', 2)),
|
||||
3 => self.fmt_offset_colon3().context(failc(b'z', 3)),
|
||||
_ => return Err(E::ColonCount { directive: b'z' }.into()),
|
||||
},
|
||||
b'.' => {
|
||||
if !self.bump_fmt() {
|
||||
return Err(err!(
|
||||
"invalid format string, expected directive after '%.'",
|
||||
));
|
||||
return Err(E::UnexpectedEndAfterDot.into());
|
||||
}
|
||||
// Parse precision settings after the `.`, effectively
|
||||
// overriding any digits that came before it.
|
||||
let ext = Extension { width: self.parse_width()?, ..ext };
|
||||
match self.f() {
|
||||
b'f' => {
|
||||
self.fmt_dot_fractional(&ext).context("%.f failed")?
|
||||
}
|
||||
b'f' => self
|
||||
.fmt_dot_fractional(&ext)
|
||||
.context(E::DirectiveFailureDot { directive: b'f' }),
|
||||
unk => {
|
||||
return Err(err!(
|
||||
"found unrecognized directive %{unk} following %.",
|
||||
unk = escape::Byte(unk),
|
||||
return Err(Error::from(
|
||||
E::UnknownDirectiveAfterDot { directive: unk },
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
unk => {
|
||||
return Err(err!(
|
||||
"found unrecognized specifier directive %{unk}",
|
||||
unk = escape::Byte(unk),
|
||||
));
|
||||
return Err(Error::from(E::UnknownDirective {
|
||||
directive: unk,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}?;
|
||||
self.bump_fmt();
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -200,21 +192,17 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
/// some remaining bytes to parse.
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn utf8_decode_and_bump(&mut self) -> Result<char, Error> {
|
||||
fn utf8_decode_and_bump(&mut self) -> Result<char, FE> {
|
||||
match utf8::decode(self.fmt).expect("non-empty fmt") {
|
||||
Ok(ch) => {
|
||||
self.fmt = &self.fmt[ch.len_utf8()..];
|
||||
return Ok(ch);
|
||||
}
|
||||
Err(errant_bytes) if self.config.lenient => {
|
||||
self.fmt = &self.fmt[errant_bytes.len()..];
|
||||
Err(err) if self.config.lenient => {
|
||||
self.fmt = &self.fmt[err.len()..];
|
||||
return Ok(char::REPLACEMENT_CHARACTER);
|
||||
}
|
||||
Err(errant_bytes) => Err(err!(
|
||||
"found invalid UTF-8 byte {errant_bytes:?} in format \
|
||||
string (format strings must be valid UTF-8)",
|
||||
errant_bytes = escape::Bytes(errant_bytes),
|
||||
)),
|
||||
Err(_) => Err(FE::InvalidUtf8),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -270,11 +258,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
|
||||
/// %P
|
||||
fn fmt_ampm_lower(&mut self, ext: &Extension) -> Result<(), Error> {
|
||||
let hour = self
|
||||
.tm
|
||||
.hour_ranged()
|
||||
.ok_or_else(|| err!("requires time to format AM/PM"))?
|
||||
.get();
|
||||
let hour = self.tm.hour_ranged().ok_or(FE::RequiresTime)?.get();
|
||||
ext.write_str(
|
||||
Case::AsIs,
|
||||
if hour < 12 { "am" } else { "pm" },
|
||||
|
|
@ -284,11 +268,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
|
||||
/// %p
|
||||
fn fmt_ampm_upper(&mut self, ext: &Extension) -> Result<(), Error> {
|
||||
let hour = self
|
||||
.tm
|
||||
.hour_ranged()
|
||||
.ok_or_else(|| err!("requires time to format AM/PM"))?
|
||||
.get();
|
||||
let hour = self.tm.hour_ranged().ok_or(FE::RequiresTime)?.get();
|
||||
// Manually specialize this case to avoid hitting `write_str_cold`.
|
||||
let s = if matches!(ext.flag, Some(Flag::Swapcase)) {
|
||||
if hour < 12 {
|
||||
|
|
@ -339,8 +319,11 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
let day = self
|
||||
.tm
|
||||
.day
|
||||
.or_else(|| self.tm.to_date().ok().map(|d| d.day_ranged()))
|
||||
.ok_or_else(|| err!("requires date to format day"))?
|
||||
.or_else(
|
||||
#[inline(never)]
|
||||
|| self.tm.to_date().ok().map(|d| d.day_ranged()),
|
||||
)
|
||||
.ok_or(FE::RequiresDate)?
|
||||
.get();
|
||||
ext.write_int(b'0', Some(2), day, self.wtr)
|
||||
}
|
||||
|
|
@ -350,19 +333,18 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
let day = self
|
||||
.tm
|
||||
.day
|
||||
.or_else(|| self.tm.to_date().ok().map(|d| d.day_ranged()))
|
||||
.ok_or_else(|| err!("requires date to format day"))?
|
||||
.or_else(
|
||||
#[inline(never)]
|
||||
|| self.tm.to_date().ok().map(|d| d.day_ranged()),
|
||||
)
|
||||
.ok_or(FE::RequiresDate)?
|
||||
.get();
|
||||
ext.write_int(b' ', Some(2), day, self.wtr)
|
||||
}
|
||||
|
||||
/// %I
|
||||
fn fmt_hour12_zero(&mut self, ext: &Extension) -> Result<(), Error> {
|
||||
let mut hour = self
|
||||
.tm
|
||||
.hour_ranged()
|
||||
.ok_or_else(|| err!("requires time to format hour"))?
|
||||
.get();
|
||||
let mut hour = self.tm.hour_ranged().ok_or(FE::RequiresTime)?.get();
|
||||
if hour == 0 {
|
||||
hour = 12;
|
||||
} else if hour > 12 {
|
||||
|
|
@ -373,21 +355,13 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
|
||||
/// %H
|
||||
fn fmt_hour24_zero(&mut self, ext: &Extension) -> Result<(), Error> {
|
||||
let hour = self
|
||||
.tm
|
||||
.hour_ranged()
|
||||
.ok_or_else(|| err!("requires time to format hour"))?
|
||||
.get();
|
||||
let hour = self.tm.hour_ranged().ok_or(FE::RequiresTime)?.get();
|
||||
ext.write_int(b'0', Some(2), hour, self.wtr)
|
||||
}
|
||||
|
||||
/// %l
|
||||
fn fmt_hour12_space(&mut self, ext: &Extension) -> Result<(), Error> {
|
||||
let mut hour = self
|
||||
.tm
|
||||
.hour_ranged()
|
||||
.ok_or_else(|| err!("requires time to format hour"))?
|
||||
.get();
|
||||
let mut hour = self.tm.hour_ranged().ok_or(FE::RequiresTime)?.get();
|
||||
if hour == 0 {
|
||||
hour = 12;
|
||||
} else if hour > 12 {
|
||||
|
|
@ -398,11 +372,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
|
||||
/// %k
|
||||
fn fmt_hour24_space(&mut self, ext: &Extension) -> Result<(), Error> {
|
||||
let hour = self
|
||||
.tm
|
||||
.hour_ranged()
|
||||
.ok_or_else(|| err!("requires time to format hour"))?
|
||||
.get();
|
||||
let hour = self.tm.hour_ranged().ok_or(FE::RequiresTime)?.get();
|
||||
ext.write_int(b' ', Some(2), hour, self.wtr)
|
||||
}
|
||||
|
||||
|
|
@ -418,11 +388,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
|
||||
/// %M
|
||||
fn fmt_minute(&mut self, ext: &Extension) -> Result<(), Error> {
|
||||
let minute = self
|
||||
.tm
|
||||
.minute
|
||||
.ok_or_else(|| err!("requires time to format minute"))?
|
||||
.get();
|
||||
let minute = self.tm.minute.ok_or(FE::RequiresTime)?.get();
|
||||
ext.write_int(b'0', Some(2), minute, self.wtr)
|
||||
}
|
||||
|
||||
|
|
@ -431,8 +397,11 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
let month = self
|
||||
.tm
|
||||
.month
|
||||
.or_else(|| self.tm.to_date().ok().map(|d| d.month_ranged()))
|
||||
.ok_or_else(|| err!("requires date to format month"))?
|
||||
.or_else(
|
||||
#[inline(never)]
|
||||
|| self.tm.to_date().ok().map(|d| d.month_ranged()),
|
||||
)
|
||||
.ok_or(FE::RequiresDate)?
|
||||
.get();
|
||||
ext.write_int(b'0', Some(2), month, self.wtr)
|
||||
}
|
||||
|
|
@ -442,8 +411,11 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
let month = self
|
||||
.tm
|
||||
.month
|
||||
.or_else(|| self.tm.to_date().ok().map(|d| d.month_ranged()))
|
||||
.ok_or_else(|| err!("requires date to format month"))?;
|
||||
.or_else(
|
||||
#[inline(never)]
|
||||
|| self.tm.to_date().ok().map(|d| d.month_ranged()),
|
||||
)
|
||||
.ok_or(FE::RequiresDate)?;
|
||||
ext.write_str(Case::AsIs, month_name_full(month), self.wtr)
|
||||
}
|
||||
|
||||
|
|
@ -452,20 +424,18 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
let month = self
|
||||
.tm
|
||||
.month
|
||||
.or_else(|| self.tm.to_date().ok().map(|d| d.month_ranged()))
|
||||
.ok_or_else(|| err!("requires date to format month"))?;
|
||||
.or_else(
|
||||
#[inline(never)]
|
||||
|| self.tm.to_date().ok().map(|d| d.month_ranged()),
|
||||
)
|
||||
.ok_or(FE::RequiresDate)?;
|
||||
ext.write_str(Case::AsIs, month_name_abbrev(month), self.wtr)
|
||||
}
|
||||
|
||||
/// %Q
|
||||
fn fmt_iana_nocolon(&mut self) -> Result<(), Error> {
|
||||
let Some(iana) = self.tm.iana_time_zone() else {
|
||||
let offset = self.tm.offset.ok_or_else(|| {
|
||||
err!(
|
||||
"requires IANA time zone identifier or time \
|
||||
zone offset, but none were present"
|
||||
)
|
||||
})?;
|
||||
let offset = self.tm.offset.ok_or(FE::RequiresTimeZoneOrOffset)?;
|
||||
return write_offset(offset, false, true, false, &mut self.wtr);
|
||||
};
|
||||
self.wtr.write_str(iana)?;
|
||||
|
|
@ -475,12 +445,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
/// %:Q
|
||||
fn fmt_iana_colon(&mut self) -> Result<(), Error> {
|
||||
let Some(iana) = self.tm.iana_time_zone() else {
|
||||
let offset = self.tm.offset.ok_or_else(|| {
|
||||
err!(
|
||||
"requires IANA time zone identifier or time \
|
||||
zone offset, but none were present"
|
||||
)
|
||||
})?;
|
||||
let offset = self.tm.offset.ok_or(FE::RequiresTimeZoneOrOffset)?;
|
||||
return write_offset(offset, true, true, false, &mut self.wtr);
|
||||
};
|
||||
self.wtr.write_str(iana)?;
|
||||
|
|
@ -489,62 +454,44 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
|
||||
/// %z
|
||||
fn fmt_offset_nocolon(&mut self) -> Result<(), Error> {
|
||||
let offset = self.tm.offset.ok_or_else(|| {
|
||||
err!("requires offset to format time zone offset")
|
||||
})?;
|
||||
let offset = self.tm.offset.ok_or(FE::RequiresOffset)?;
|
||||
write_offset(offset, false, true, false, self.wtr)
|
||||
}
|
||||
|
||||
/// %:z
|
||||
fn fmt_offset_colon(&mut self) -> Result<(), Error> {
|
||||
let offset = self.tm.offset.ok_or_else(|| {
|
||||
err!("requires offset to format time zone offset")
|
||||
})?;
|
||||
let offset = self.tm.offset.ok_or(FE::RequiresOffset)?;
|
||||
write_offset(offset, true, true, false, self.wtr)
|
||||
}
|
||||
|
||||
/// %::z
|
||||
fn fmt_offset_colon2(&mut self) -> Result<(), Error> {
|
||||
let offset = self.tm.offset.ok_or_else(|| {
|
||||
err!("requires offset to format time zone offset")
|
||||
})?;
|
||||
let offset = self.tm.offset.ok_or(FE::RequiresOffset)?;
|
||||
write_offset(offset, true, true, true, self.wtr)
|
||||
}
|
||||
|
||||
/// %:::z
|
||||
fn fmt_offset_colon3(&mut self) -> Result<(), Error> {
|
||||
let offset = self.tm.offset.ok_or_else(|| {
|
||||
err!("requires offset to format time zone offset")
|
||||
})?;
|
||||
let offset = self.tm.offset.ok_or(FE::RequiresOffset)?;
|
||||
write_offset(offset, true, false, false, self.wtr)
|
||||
}
|
||||
|
||||
/// %S
|
||||
fn fmt_second(&mut self, ext: &Extension) -> Result<(), Error> {
|
||||
let second = self
|
||||
.tm
|
||||
.second
|
||||
.ok_or_else(|| err!("requires time to format second"))?
|
||||
.get();
|
||||
let second = self.tm.second.ok_or(FE::RequiresTime)?.get();
|
||||
ext.write_int(b'0', Some(2), second, self.wtr)
|
||||
}
|
||||
|
||||
/// %s
|
||||
fn fmt_timestamp(&mut self, ext: &Extension) -> Result<(), Error> {
|
||||
let timestamp = self.tm.to_timestamp().map_err(|_| {
|
||||
err!(
|
||||
"requires instant (a date, time and offset) \
|
||||
to format Unix timestamp",
|
||||
)
|
||||
})?;
|
||||
let timestamp =
|
||||
self.tm.to_timestamp().map_err(|_| FE::RequiresInstant)?;
|
||||
ext.write_int(b' ', None, timestamp.as_second(), self.wtr)
|
||||
}
|
||||
|
||||
/// %f
|
||||
fn fmt_fractional(&mut self, ext: &Extension) -> Result<(), Error> {
|
||||
let subsec = self.tm.subsec.ok_or_else(|| {
|
||||
err!("requires time to format subsecond nanoseconds")
|
||||
})?;
|
||||
let subsec = self.tm.subsec.ok_or(FE::RequiresTime)?;
|
||||
let subsec = i32::from(subsec).unsigned_abs();
|
||||
// For %f, we always want to emit at least one digit. The only way we
|
||||
// wouldn't is if our fractional component is zero. One exception to
|
||||
|
|
@ -553,7 +500,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
// but this seems very odd. And an empty string cannot be parsed by
|
||||
// `%f`.
|
||||
if ext.width == Some(0) {
|
||||
return Err(err!("zero precision with %f is not allowed"));
|
||||
return Err(Error::from(FE::ZeroPrecisionFloat));
|
||||
}
|
||||
if subsec == 0 && ext.width.is_none() {
|
||||
self.wtr.write_str("0")?;
|
||||
|
|
@ -577,11 +524,9 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
|
||||
/// %N
|
||||
fn fmt_nanoseconds(&mut self, ext: &Extension) -> Result<(), Error> {
|
||||
let subsec = self.tm.subsec.ok_or_else(|| {
|
||||
err!("requires time to format subsecond nanoseconds")
|
||||
})?;
|
||||
let subsec = self.tm.subsec.ok_or(FE::RequiresTime)?;
|
||||
if ext.width == Some(0) {
|
||||
return Err(err!("zero precision with %N is not allowed"));
|
||||
return Err(Error::from(FE::ZeroPrecisionNano));
|
||||
}
|
||||
let subsec = i32::from(subsec).unsigned_abs();
|
||||
// Since `%N` is actually an alias for `%9f`, when the precision
|
||||
|
|
@ -596,14 +541,8 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
|
||||
/// %Z
|
||||
fn fmt_tzabbrev(&mut self, ext: &Extension) -> Result<(), Error> {
|
||||
let tz =
|
||||
self.tm.tz.as_ref().ok_or_else(|| {
|
||||
err!("requires time zone in broken down time")
|
||||
})?;
|
||||
let ts = self
|
||||
.tm
|
||||
.to_timestamp()
|
||||
.context("requires timestamp in broken down time")?;
|
||||
let tz = self.tm.tz.as_ref().ok_or(FE::RequiresTimeZone)?;
|
||||
let ts = self.tm.to_timestamp().map_err(|_| FE::RequiresInstant)?;
|
||||
let oinfo = tz.to_offset_info(ts);
|
||||
ext.write_str(Case::Upper, oinfo.abbreviation(), self.wtr)
|
||||
}
|
||||
|
|
@ -613,8 +552,11 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
let weekday = self
|
||||
.tm
|
||||
.weekday
|
||||
.or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
|
||||
.ok_or_else(|| err!("requires date to format weekday"))?;
|
||||
.or_else(
|
||||
#[inline(never)]
|
||||
|| self.tm.to_date().ok().map(|d| d.weekday()),
|
||||
)
|
||||
.ok_or(FE::RequiresDate)?;
|
||||
ext.write_str(Case::AsIs, weekday_name_full(weekday), self.wtr)
|
||||
}
|
||||
|
||||
|
|
@ -623,8 +565,11 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
let weekday = self
|
||||
.tm
|
||||
.weekday
|
||||
.or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
|
||||
.ok_or_else(|| err!("requires date to format weekday"))?;
|
||||
.or_else(
|
||||
#[inline(never)]
|
||||
|| self.tm.to_date().ok().map(|d| d.weekday()),
|
||||
)
|
||||
.ok_or(FE::RequiresDate)?;
|
||||
ext.write_str(Case::AsIs, weekday_name_abbrev(weekday), self.wtr)
|
||||
}
|
||||
|
||||
|
|
@ -633,8 +578,11 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
let weekday = self
|
||||
.tm
|
||||
.weekday
|
||||
.or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
|
||||
.ok_or_else(|| err!("requires date to format weekday number"))?;
|
||||
.or_else(
|
||||
#[inline(never)]
|
||||
|| self.tm.to_date().ok().map(|d| d.weekday()),
|
||||
)
|
||||
.ok_or(FE::RequiresDate)?;
|
||||
ext.write_int(b' ', None, weekday.to_monday_one_offset(), self.wtr)
|
||||
}
|
||||
|
||||
|
|
@ -643,8 +591,11 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
let weekday = self
|
||||
.tm
|
||||
.weekday
|
||||
.or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
|
||||
.ok_or_else(|| err!("requires date to format weekday number"))?;
|
||||
.or_else(
|
||||
#[inline(never)]
|
||||
|| self.tm.to_date().ok().map(|d| d.weekday()),
|
||||
)
|
||||
.ok_or(FE::RequiresDate)?;
|
||||
ext.write_int(b' ', None, weekday.to_sunday_zero_offset(), self.wtr)
|
||||
}
|
||||
|
||||
|
|
@ -658,17 +609,19 @@ 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(|| self.tm.to_date().ok().map(|d| d.day_of_year()))
|
||||
.ok_or_else(|| {
|
||||
err!("requires date to format Sunday-based week number")
|
||||
})?;
|
||||
.or_else(
|
||||
#[inline(never)]
|
||||
|| self.tm.to_date().ok().map(|d| d.day_of_year()),
|
||||
)
|
||||
.ok_or(FE::RequiresDate)?;
|
||||
let weekday = self
|
||||
.tm
|
||||
.weekday
|
||||
.or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
|
||||
.ok_or_else(|| {
|
||||
err!("requires date to format Sunday-based week number")
|
||||
})?
|
||||
.or_else(
|
||||
#[inline(never)]
|
||||
|| self.tm.to_date().ok().map(|d| d.weekday()),
|
||||
)
|
||||
.ok_or(FE::RequiresDate)?
|
||||
.to_sunday_zero_offset();
|
||||
// Example: 2025-01-05 is the first Sunday in 2025, and thus the start
|
||||
// of week 1. This means that 2025-01-04 (Saturday) is in week 0.
|
||||
|
|
@ -684,12 +637,16 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
let weeknum = self
|
||||
.tm
|
||||
.iso_week
|
||||
.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")
|
||||
})?;
|
||||
.or_else(
|
||||
#[inline(never)]
|
||||
|| {
|
||||
self.tm
|
||||
.to_date()
|
||||
.ok()
|
||||
.map(|d| d.iso_week_date().week_ranged())
|
||||
},
|
||||
)
|
||||
.ok_or(FE::RequiresDate)?;
|
||||
ext.write_int(b'0', Some(2), weeknum, self.wtr)
|
||||
}
|
||||
|
||||
|
|
@ -703,17 +660,19 @@ 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(|| self.tm.to_date().ok().map(|d| d.day_of_year()))
|
||||
.ok_or_else(|| {
|
||||
err!("requires date to format Monday-based week number")
|
||||
})?;
|
||||
.or_else(
|
||||
#[inline(never)]
|
||||
|| self.tm.to_date().ok().map(|d| d.day_of_year()),
|
||||
)
|
||||
.ok_or(FE::RequiresDate)?;
|
||||
let weekday = self
|
||||
.tm
|
||||
.weekday
|
||||
.or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
|
||||
.ok_or_else(|| {
|
||||
err!("requires date to format Monday-based week number")
|
||||
})?
|
||||
.or_else(
|
||||
#[inline(never)]
|
||||
|| self.tm.to_date().ok().map(|d| d.weekday()),
|
||||
)
|
||||
.ok_or(FE::RequiresDate)?
|
||||
.to_sunday_zero_offset();
|
||||
// Example: 2025-01-06 is the first Monday in 2025, and thus the start
|
||||
// of week 1. This means that 2025-01-05 (Sunday) is in week 0.
|
||||
|
|
@ -729,8 +688,11 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
let year = self
|
||||
.tm
|
||||
.year
|
||||
.or_else(|| self.tm.to_date().ok().map(|d| d.year_ranged()))
|
||||
.ok_or_else(|| err!("requires date to format year"))?
|
||||
.or_else(
|
||||
#[inline(never)]
|
||||
|| self.tm.to_date().ok().map(|d| d.year_ranged()),
|
||||
)
|
||||
.ok_or(FE::RequiresDate)?
|
||||
.get();
|
||||
ext.write_int(b'0', Some(4), year, self.wtr)
|
||||
}
|
||||
|
|
@ -740,8 +702,11 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
let year = self
|
||||
.tm
|
||||
.year
|
||||
.or_else(|| self.tm.to_date().ok().map(|d| d.year_ranged()))
|
||||
.ok_or_else(|| err!("requires date to format year (2-digit)"))?
|
||||
.or_else(
|
||||
#[inline(never)]
|
||||
|| self.tm.to_date().ok().map(|d| d.year_ranged()),
|
||||
)
|
||||
.ok_or(FE::RequiresDate)?
|
||||
.get();
|
||||
let year = year % 100;
|
||||
ext.write_int(b'0', Some(2), year, self.wtr)
|
||||
|
|
@ -752,8 +717,11 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
let year = self
|
||||
.tm
|
||||
.year
|
||||
.or_else(|| self.tm.to_date().ok().map(|d| d.year_ranged()))
|
||||
.ok_or_else(|| err!("requires date to format century (2-digit)"))?
|
||||
.or_else(
|
||||
#[inline(never)]
|
||||
|| self.tm.to_date().ok().map(|d| d.year_ranged()),
|
||||
)
|
||||
.ok_or(FE::RequiresDate)?
|
||||
.get();
|
||||
let century = year / 100;
|
||||
ext.write_int(b' ', None, century, self.wtr)
|
||||
|
|
@ -764,12 +732,16 @@ 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(|| {
|
||||
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")
|
||||
})?
|
||||
.or_else(
|
||||
#[inline(never)]
|
||||
|| {
|
||||
self.tm
|
||||
.to_date()
|
||||
.ok()
|
||||
.map(|d| d.iso_week_date().year_ranged())
|
||||
},
|
||||
)
|
||||
.ok_or(FE::RequiresDate)?
|
||||
.get();
|
||||
ext.write_int(b'0', Some(4), year, self.wtr)
|
||||
}
|
||||
|
|
@ -779,15 +751,16 @@ 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(|| {
|
||||
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)"
|
||||
)
|
||||
})?
|
||||
.or_else(
|
||||
#[inline(never)]
|
||||
|| {
|
||||
self.tm
|
||||
.to_date()
|
||||
.ok()
|
||||
.map(|d| d.iso_week_date().year_ranged())
|
||||
},
|
||||
)
|
||||
.ok_or(FE::RequiresDate)?
|
||||
.get();
|
||||
let year = year % 100;
|
||||
ext.write_int(b'0', Some(2), year, self.wtr)
|
||||
|
|
@ -798,8 +771,11 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
|
|||
let month = self
|
||||
.tm
|
||||
.month
|
||||
.or_else(|| self.tm.to_date().ok().map(|d| d.month_ranged()))
|
||||
.ok_or_else(|| err!("requires date to format quarter"))?
|
||||
.or_else(
|
||||
#[inline(never)]
|
||||
|| self.tm.to_date().ok().map(|d| d.month_ranged()),
|
||||
)
|
||||
.ok_or(FE::RequiresDate)?
|
||||
.get();
|
||||
let quarter = match month {
|
||||
1..=3 => 1,
|
||||
|
|
@ -817,8 +793,11 @@ 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(|| self.tm.to_date().ok().map(|d| d.day_of_year()))
|
||||
.ok_or_else(|| err!("requires date to format day of year"))?;
|
||||
.or_else(
|
||||
#[inline(never)]
|
||||
|| self.tm.to_date().ok().map(|d| d.day_of_year()),
|
||||
)
|
||||
.ok_or(FE::RequiresDate)?;
|
||||
ext.write_int(b'0', Some(3), day, self.wtr)
|
||||
}
|
||||
|
||||
|
|
@ -871,7 +850,7 @@ fn write_offset<W: Write>(
|
|||
second: bool,
|
||||
wtr: &mut W,
|
||||
) -> Result<(), Error> {
|
||||
static FMT_TWO: DecimalFormatter = DecimalFormatter::new().padding(2);
|
||||
static FMT_TWO: IntegerFormatter = IntegerFormatter::new().padding(2);
|
||||
|
||||
let hours = offset.part_hours_ranged().abs().get();
|
||||
let minutes = offset.part_minutes_ranged().abs().get();
|
||||
|
|
@ -967,7 +946,7 @@ impl Extension {
|
|||
self.width.or(pad_width)
|
||||
};
|
||||
|
||||
let mut formatter = DecimalFormatter::new().padding_byte(pad_byte);
|
||||
let mut formatter = IntegerFormatter::new().padding_byte(pad_byte);
|
||||
if let Some(width) = pad_width {
|
||||
formatter = formatter.padding(width);
|
||||
}
|
||||
|
|
@ -1508,7 +1487,7 @@ mod tests {
|
|||
let dt = date(2025, 1, 20).at(13, 9, 0, 0);
|
||||
insta::assert_snapshot!(
|
||||
f("%s", dt),
|
||||
@"strftime formatting failed: %s failed: requires instant (a date, time and offset) to format Unix timestamp",
|
||||
@"strftime formatting failed: %s failed: requires instant (a timestamp or a date, time and offset)",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1521,7 +1500,7 @@ mod tests {
|
|||
);
|
||||
insta::assert_snapshot!(
|
||||
format(b"abc %F \xFFxyz", d).unwrap_err(),
|
||||
@r#"strftime formatting failed: found invalid UTF-8 byte "\xff" in format string (format strings must be valid UTF-8)"#,
|
||||
@"strftime formatting failed: invalid format string, it must be valid UTF-8",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ use jiff::{civil::time, fmt::strtime};
|
|||
let t = time(23, 59, 59, 0);
|
||||
assert_eq!(
|
||||
strtime::format("%Y", t).unwrap_err().to_string(),
|
||||
"strftime formatting failed: %Y failed: requires date to format year",
|
||||
"strftime formatting failed: %Y failed: requires date to format",
|
||||
);
|
||||
```
|
||||
|
||||
|
|
@ -275,7 +275,7 @@ The following things are currently unsupported:
|
|||
|
||||
use crate::{
|
||||
civil::{Date, DateTime, ISOWeekDate, Time, Weekday},
|
||||
error::{err, ErrorContext},
|
||||
error::{fmt::strtime::Error as E, ErrorContext},
|
||||
fmt::{
|
||||
strtime::{format::Formatter, parse::Parser},
|
||||
Write,
|
||||
|
|
@ -555,7 +555,7 @@ impl<C> Config<C> {
|
|||
/// assert_eq!(
|
||||
/// tm.to_string("%F %z").unwrap_err().to_string(),
|
||||
/// "strftime formatting failed: %z failed: \
|
||||
/// requires offset to format time zone offset",
|
||||
/// requires time zone offset",
|
||||
/// );
|
||||
///
|
||||
/// // Now enable lenient mode:
|
||||
|
|
@ -946,13 +946,9 @@ impl BrokenDownTime {
|
|||
fn parse_mono(fmt: &[u8], inp: &[u8]) -> Result<BrokenDownTime, Error> {
|
||||
let mut pieces = BrokenDownTime::default();
|
||||
let mut p = Parser { fmt, inp, tm: &mut pieces };
|
||||
p.parse().context("strptime parsing failed")?;
|
||||
p.parse().context(E::FailedStrptime)?;
|
||||
if !p.inp.is_empty() {
|
||||
return Err(err!(
|
||||
"strptime expects to consume the entire input, but \
|
||||
{remaining:?} remains unparsed",
|
||||
remaining = escape::Bytes(p.inp),
|
||||
));
|
||||
return Err(Error::from(E::unconsumed(p.inp)));
|
||||
}
|
||||
Ok(pieces)
|
||||
}
|
||||
|
|
@ -1055,7 +1051,7 @@ impl BrokenDownTime {
|
|||
let mkoffset = util::parse::offseter(inp);
|
||||
let mut pieces = BrokenDownTime::default();
|
||||
let mut p = Parser { fmt, inp, tm: &mut pieces };
|
||||
p.parse().context("strptime parsing failed")?;
|
||||
p.parse().context(E::FailedStrptime)?;
|
||||
let remainder = mkoffset(p.inp);
|
||||
Ok((pieces, remainder))
|
||||
}
|
||||
|
|
@ -1158,7 +1154,7 @@ impl BrokenDownTime {
|
|||
) -> Result<(), Error> {
|
||||
let fmt = format.as_ref();
|
||||
let mut formatter = Formatter { config, fmt, tm: self, wtr };
|
||||
formatter.format().context("strftime formatting failed")?;
|
||||
formatter.format().context(E::FailedStrftime)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -1337,10 +1333,11 @@ impl BrokenDownTime {
|
|||
/// )?.to_zoned();
|
||||
/// assert_eq!(
|
||||
/// result.unwrap_err().to_string(),
|
||||
/// "datetime 2024-07-14T21:14:00 could not resolve to a \
|
||||
/// timestamp since 'reject' conflict resolution was chosen, \
|
||||
/// and because datetime has offset -05, but the time zone \
|
||||
/// US/Eastern for the given datetime unambiguously has offset -04",
|
||||
/// "datetime could not resolve to a timestamp since `reject` \
|
||||
/// conflict resolution was chosen, and because \
|
||||
/// datetime has offset `-05`, \
|
||||
/// but the time zone `US/Eastern` for the given datetime \
|
||||
/// unambiguously has offset `-04`",
|
||||
/// );
|
||||
///
|
||||
/// # Ok::<(), Box<dyn std::error::Error>>(())
|
||||
|
|
@ -1435,26 +1432,18 @@ impl BrokenDownTime {
|
|||
if let Some(ts) = self.timestamp {
|
||||
return Ok(ts.to_zoned(TimeZone::unknown()));
|
||||
}
|
||||
Err(err!(
|
||||
"either offset (from %z) or IANA time zone identifier \
|
||||
(from %Q) is required for parsing zoned datetime",
|
||||
))
|
||||
Err(Error::from(E::ZonedOffsetOrTz))
|
||||
}
|
||||
(Some(offset), None) => {
|
||||
let ts = match self.timestamp {
|
||||
Some(ts) => ts,
|
||||
None => {
|
||||
let dt = self.to_datetime().context(
|
||||
"datetime required to parse zoned datetime",
|
||||
)?;
|
||||
let ts =
|
||||
offset.to_timestamp(dt).with_context(|| {
|
||||
err!(
|
||||
"parsed datetime {dt} and offset {offset}, \
|
||||
but combining them into a zoned datetime \
|
||||
is outside Jiff's supported timestamp range",
|
||||
)
|
||||
})?;
|
||||
let dt = self
|
||||
.to_datetime()
|
||||
.context(E::RequiredDateTimeForZoned)?;
|
||||
let ts = offset
|
||||
.to_timestamp(dt)
|
||||
.context(E::RangeTimestamp)?;
|
||||
ts
|
||||
}
|
||||
};
|
||||
|
|
@ -1465,9 +1454,9 @@ impl BrokenDownTime {
|
|||
match self.timestamp {
|
||||
Some(ts) => Ok(ts.to_zoned(tz)),
|
||||
None => {
|
||||
let dt = self.to_datetime().context(
|
||||
"datetime required to parse zoned datetime",
|
||||
)?;
|
||||
let dt = self
|
||||
.to_datetime()
|
||||
.context(E::RequiredDateTimeForZoned)?;
|
||||
Ok(tz.to_zoned(dt)?)
|
||||
}
|
||||
}
|
||||
|
|
@ -1478,19 +1467,17 @@ impl BrokenDownTime {
|
|||
Some(ts) => {
|
||||
let zdt = ts.to_zoned(tz);
|
||||
if zdt.offset() != offset {
|
||||
return Err(err!(
|
||||
"parsed time zone offset `{offset}`, but \
|
||||
offset from timestamp `{ts}` for time zone \
|
||||
`{iana}` is `{got}`",
|
||||
got = zdt.offset(),
|
||||
));
|
||||
return Err(Error::from(E::MismatchOffset {
|
||||
parsed: offset,
|
||||
got: zdt.offset(),
|
||||
}));
|
||||
}
|
||||
Ok(zdt)
|
||||
}
|
||||
None => {
|
||||
let dt = self.to_datetime().context(
|
||||
"datetime required to parse zoned datetime",
|
||||
)?;
|
||||
let dt = self
|
||||
.to_datetime()
|
||||
.context(E::RequiredDateTimeForZoned)?;
|
||||
let azdt =
|
||||
OffsetConflict::Reject.resolve(dt, offset, tz)?;
|
||||
// Guaranteed that if OffsetConflict::Reject doesn't
|
||||
|
|
@ -1584,31 +1571,20 @@ 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("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)
|
||||
let dt =
|
||||
self.to_datetime().context(E::RequiredDateTimeForTimestamp)?;
|
||||
let offset = self.offset.ok_or(E::RequiredOffsetForTimestamp)?;
|
||||
offset.to_timestamp(dt).context(E::RangeTimestamp)
|
||||
}
|
||||
|
||||
/// Extracts a civil datetime from this broken down time.
|
||||
|
|
@ -1638,10 +1614,8 @@ impl BrokenDownTime {
|
|||
/// ```
|
||||
#[inline]
|
||||
pub fn to_datetime(&self) -> Result<DateTime, Error> {
|
||||
let date =
|
||||
self.to_date().context("date required to parse datetime")?;
|
||||
let time =
|
||||
self.to_time().context("time required to parse datetime")?;
|
||||
let date = self.to_date().context(E::RequiredDateForDateTime)?;
|
||||
let time = self.to_time().context(E::RequiredTimeForDateTime)?;
|
||||
Ok(DateTime::from_parts(date, time))
|
||||
}
|
||||
|
||||
|
|
@ -1660,8 +1634,12 @@ impl BrokenDownTime {
|
|||
/// # Errors
|
||||
///
|
||||
/// This returns an error if there weren't enough components to construct
|
||||
/// a civil date. This means there must be at least a year and a way to
|
||||
/// determine the day of the year.
|
||||
/// 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.
|
||||
///
|
||||
/// 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,
|
||||
|
|
@ -1681,42 +1659,62 @@ impl BrokenDownTime {
|
|||
/// ```
|
||||
#[inline]
|
||||
pub fn to_date(&self) -> Result<Date, Error> {
|
||||
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);
|
||||
#[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()?;
|
||||
}
|
||||
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",
|
||||
));
|
||||
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);
|
||||
};
|
||||
let date =
|
||||
Date::new_ranged(year, month, day).context(E::InvalidDate)?;
|
||||
if let Some(weekday) = self.weekday {
|
||||
if weekday != date.weekday() {
|
||||
return Err(err!(
|
||||
"parsed weekday {weekday} does not match \
|
||||
weekday {got} from parsed date {date}",
|
||||
weekday = weekday_name_full(weekday),
|
||||
got = weekday_name_full(date.weekday()),
|
||||
));
|
||||
return Err(Error::from(E::MismatchWeekday {
|
||||
parsed: weekday,
|
||||
got: date.weekday(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
Ok(date)
|
||||
|
|
@ -1730,7 +1728,7 @@ impl BrokenDownTime {
|
|||
let (Some(month), Some(day)) = (self.month, self.day) else {
|
||||
return Ok(None);
|
||||
};
|
||||
Ok(Some(Date::new_ranged(year, month, day).context("invalid date")?))
|
||||
Ok(Some(Date::new_ranged(year, month, day).context(E::InvalidDate)?))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
@ -1746,7 +1744,7 @@ impl BrokenDownTime {
|
|||
.with()
|
||||
.day_of_year(doy.get())
|
||||
.build()
|
||||
.context("invalid date")?
|
||||
.context(E::InvalidDate)?
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
@ -1757,8 +1755,8 @@ impl BrokenDownTime {
|
|||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let wd = ISOWeekDate::new_ranged(y, w, d)
|
||||
.context("invalid ISO 8601 week date")?;
|
||||
let wd =
|
||||
ISOWeekDate::new_ranged(y, w, d).context(E::InvalidISOWeekDate)?;
|
||||
Ok(Some(wd.date()))
|
||||
}
|
||||
|
||||
|
|
@ -1773,28 +1771,20 @@ impl BrokenDownTime {
|
|||
let week = i16::from(week);
|
||||
let wday = i16::from(weekday.to_sunday_zero_offset());
|
||||
let first_of_year = Date::new_ranged(year, C(1).rinto(), C(1).rinto())
|
||||
.context("invalid date")?;
|
||||
.context(E::InvalidDate)?;
|
||||
let first_sunday = first_of_year
|
||||
.nth_weekday_of_month(1, Weekday::Sunday)
|
||||
.map(|d| d.day_of_year())
|
||||
.context("invalid date")?;
|
||||
.context(E::InvalidDate)?;
|
||||
let doy = if week == 0 {
|
||||
let days_before_first_sunday = 7 - wday;
|
||||
let doy = first_sunday
|
||||
.checked_sub(days_before_first_sunday)
|
||||
.ok_or_else(|| {
|
||||
err!(
|
||||
"weekday `{weekday:?}` is not valid for \
|
||||
Sunday based week number `{week}` \
|
||||
in year `{year}`",
|
||||
)
|
||||
})?;
|
||||
.ok_or(E::InvalidWeekdaySunday { got: weekday })?;
|
||||
if doy == 0 {
|
||||
return Err(err!(
|
||||
"weekday `{weekday:?}` is not valid for \
|
||||
Sunday based week number `{week}` \
|
||||
in year `{year}`",
|
||||
));
|
||||
return Err(Error::from(E::InvalidWeekdaySunday {
|
||||
got: weekday,
|
||||
}));
|
||||
}
|
||||
doy
|
||||
} else {
|
||||
|
|
@ -1806,7 +1796,7 @@ impl BrokenDownTime {
|
|||
.with()
|
||||
.day_of_year(doy)
|
||||
.build()
|
||||
.context("invalid date")?;
|
||||
.context(E::InvalidDate)?;
|
||||
Ok(Some(date))
|
||||
}
|
||||
|
||||
|
|
@ -1821,28 +1811,20 @@ impl BrokenDownTime {
|
|||
let week = i16::from(week);
|
||||
let wday = i16::from(weekday.to_monday_zero_offset());
|
||||
let first_of_year = Date::new_ranged(year, C(1).rinto(), C(1).rinto())
|
||||
.context("invalid date")?;
|
||||
.context(E::InvalidDate)?;
|
||||
let first_monday = first_of_year
|
||||
.nth_weekday_of_month(1, Weekday::Monday)
|
||||
.map(|d| d.day_of_year())
|
||||
.context("invalid date")?;
|
||||
.context(E::InvalidDate)?;
|
||||
let doy = if week == 0 {
|
||||
let days_before_first_monday = 7 - wday;
|
||||
let doy = first_monday
|
||||
.checked_sub(days_before_first_monday)
|
||||
.ok_or_else(|| {
|
||||
err!(
|
||||
"weekday `{weekday:?}` is not valid for \
|
||||
Monday based week number `{week}` \
|
||||
in year `{year}`",
|
||||
)
|
||||
})?;
|
||||
.ok_or(E::InvalidWeekdayMonday { got: weekday })?;
|
||||
if doy == 0 {
|
||||
return Err(err!(
|
||||
"weekday `{weekday:?}` is not valid for \
|
||||
Monday based week number `{week}` \
|
||||
in year `{year}`",
|
||||
));
|
||||
return Err(Error::from(E::InvalidWeekdayMonday {
|
||||
got: weekday,
|
||||
}));
|
||||
}
|
||||
doy
|
||||
} else {
|
||||
|
|
@ -1854,7 +1836,7 @@ impl BrokenDownTime {
|
|||
.with()
|
||||
.day_of_year(doy)
|
||||
.build()
|
||||
.context("invalid date")?;
|
||||
.context(E::InvalidDate)?;
|
||||
Ok(Some(date))
|
||||
}
|
||||
|
||||
|
|
@ -1936,52 +1918,28 @@ impl BrokenDownTime {
|
|||
pub fn to_time(&self) -> Result<Time, Error> {
|
||||
let Some(hour) = self.hour_ranged() else {
|
||||
if self.minute.is_some() {
|
||||
return Err(err!(
|
||||
"parsing format did not include hour directive, \
|
||||
but did include minute directive (cannot have \
|
||||
smaller time units with bigger time units missing)",
|
||||
));
|
||||
return Err(Error::from(E::MissingTimeHourForMinute));
|
||||
}
|
||||
if self.second.is_some() {
|
||||
return Err(err!(
|
||||
"parsing format did not include hour directive, \
|
||||
but did include second directive (cannot have \
|
||||
smaller time units with bigger time units missing)",
|
||||
));
|
||||
return Err(Error::from(E::MissingTimeHourForSecond));
|
||||
}
|
||||
if self.subsec.is_some() {
|
||||
return Err(err!(
|
||||
"parsing format did not include hour directive, \
|
||||
but did include fractional second directive (cannot have \
|
||||
smaller time units with bigger time units missing)",
|
||||
));
|
||||
return Err(Error::from(E::MissingTimeHourForFractional));
|
||||
}
|
||||
return Ok(Time::midnight());
|
||||
};
|
||||
let Some(minute) = self.minute else {
|
||||
if self.second.is_some() {
|
||||
return Err(err!(
|
||||
"parsing format did not include minute directive, \
|
||||
but did include second directive (cannot have \
|
||||
smaller time units with bigger time units missing)",
|
||||
));
|
||||
return Err(Error::from(E::MissingTimeMinuteForSecond));
|
||||
}
|
||||
if self.subsec.is_some() {
|
||||
return Err(err!(
|
||||
"parsing format did not include minute directive, \
|
||||
but did include fractional second directive (cannot have \
|
||||
smaller time units with bigger time units missing)",
|
||||
));
|
||||
return Err(Error::from(E::MissingTimeMinuteForFractional));
|
||||
}
|
||||
return Ok(Time::new_ranged(hour, C(0), C(0), C(0)));
|
||||
};
|
||||
let Some(second) = self.second else {
|
||||
if self.subsec.is_some() {
|
||||
return Err(err!(
|
||||
"parsing format did not include second directive, \
|
||||
but did include fractional second directive (cannot have \
|
||||
smaller time units with bigger time units missing)",
|
||||
));
|
||||
return Err(Error::from(E::MissingTimeSecondForFractional));
|
||||
}
|
||||
return Ok(Time::new_ranged(hour, minute, C(0), C(0)));
|
||||
};
|
||||
|
|
@ -2121,8 +2079,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: day-of-year=366 is out of range \
|
||||
/// for year=2023, must be in range 1..=365",
|
||||
/// "invalid date: number of days for `2023` is invalid, \
|
||||
/// must be in range `1..=365`",
|
||||
/// );
|
||||
/// // But parsing a value that is always illegal will
|
||||
/// // result in an error:
|
||||
|
|
@ -3479,7 +3437,7 @@ impl Extension {
|
|||
fn parse_flag<'i>(
|
||||
fmt: &'i [u8],
|
||||
) -> Result<(Option<Flag>, &'i [u8]), Error> {
|
||||
let byte = fmt[0];
|
||||
let (&byte, tail) = fmt.split_first().unwrap();
|
||||
let flag = match byte {
|
||||
b'_' => Flag::PadSpace,
|
||||
b'0' => Flag::PadZero,
|
||||
|
|
@ -3488,15 +3446,12 @@ impl Extension {
|
|||
b'#' => Flag::Swapcase,
|
||||
_ => return Ok((None, fmt)),
|
||||
};
|
||||
let fmt = &fmt[1..];
|
||||
if fmt.is_empty() {
|
||||
return Err(err!(
|
||||
"expected to find specifier directive after flag \
|
||||
{byte:?}, but found end of format string",
|
||||
byte = escape::Byte(byte),
|
||||
));
|
||||
if tail.is_empty() {
|
||||
return Err(Error::from(E::ExpectedDirectiveAfterFlag {
|
||||
flag: byte,
|
||||
}));
|
||||
}
|
||||
Ok((Some(flag), fmt))
|
||||
Ok((Some(flag), tail))
|
||||
}
|
||||
|
||||
/// Parses an optional width that comes after a (possibly absent) flag and
|
||||
|
|
@ -3520,16 +3475,10 @@ impl Extension {
|
|||
return Ok((None, fmt));
|
||||
}
|
||||
let (digits, fmt) = util::parse::split(fmt, digits).unwrap();
|
||||
let width = util::parse::i64(digits)
|
||||
.context("failed to parse conversion specifier width")?;
|
||||
let width = u8::try_from(width).map_err(|_| {
|
||||
err!("{width} is too big, max is {max}", max = u8::MAX)
|
||||
})?;
|
||||
let width = util::parse::i64(digits).context(E::FailedWidth)?;
|
||||
let width = u8::try_from(width).map_err(|_| E::RangeWidth)?;
|
||||
if fmt.is_empty() {
|
||||
return Err(err!(
|
||||
"expected to find specifier directive after width \
|
||||
{width}, but found end of format string",
|
||||
));
|
||||
return Err(Error::from(E::ExpectedDirectiveAfterWidth));
|
||||
}
|
||||
Ok((Some(width), fmt))
|
||||
}
|
||||
|
|
@ -3549,10 +3498,7 @@ impl Extension {
|
|||
}
|
||||
let fmt = &fmt[usize::from(colons)..];
|
||||
if colons > 0 && fmt.is_empty() {
|
||||
return Err(err!(
|
||||
"expected to find specifier directive after {colons} colons, \
|
||||
but found end of format string",
|
||||
));
|
||||
return Err(Error::from(E::ExpectedDirectiveAfterColons));
|
||||
}
|
||||
Ok((u8::try_from(colons).unwrap(), fmt))
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -171,7 +171,7 @@ There is some more [background on Temporal's format] available.
|
|||
*/
|
||||
|
||||
use crate::{
|
||||
civil,
|
||||
civil::{self, ISOWeekDate},
|
||||
error::Error,
|
||||
fmt::Write,
|
||||
span::Span,
|
||||
|
|
@ -320,13 +320,12 @@ impl DateTimeParser {
|
|||
/// );
|
||||
/// assert_eq!(
|
||||
/// result.unwrap_err().to_string(),
|
||||
/// "parsing \"2006-04-02T02:30-05[America/Indiana/Vevay]\" failed: \
|
||||
/// datetime 2006-04-02T02:30:00 could not resolve to timestamp \
|
||||
/// since 'reject' conflict resolution was chosen, and because \
|
||||
/// datetime has offset -05, but the time zone America/Indiana/Vevay \
|
||||
/// for the given datetime falls in a gap \
|
||||
/// (between offsets -05 and -04), \
|
||||
/// and all offsets for a gap are regarded as invalid",
|
||||
/// "datetime could not resolve to timestamp since `reject` \
|
||||
/// conflict resolution was chosen, and because datetime \
|
||||
/// has offset `-05`, but the time zone `America/Indiana/Vevay` \
|
||||
/// for the given datetime falls in a gap (between offsets \
|
||||
/// `-05` and `-04`), and all offsets for a gap are \
|
||||
/// regarded as invalid",
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
|
|
@ -410,11 +409,10 @@ impl DateTimeParser {
|
|||
/// );
|
||||
/// assert_eq!(
|
||||
/// result.unwrap_err().to_string(),
|
||||
/// "parsing \"2025-06-20T17:30+00[America/New_York]\" failed: \
|
||||
/// datetime 2025-06-20T17:30:00 could not resolve to a timestamp \
|
||||
/// since 'reject' conflict resolution was chosen, and because \
|
||||
/// datetime has offset +00, but the time zone America/New_York \
|
||||
/// for the given datetime unambiguously has offset -04",
|
||||
/// "datetime could not resolve to a timestamp since `reject` \
|
||||
/// conflict resolution was chosen, and because datetime has \
|
||||
/// offset `+00`, but the time zone `America/New_York` \
|
||||
/// for the given datetime unambiguously has offset `-04`",
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
|
|
@ -1027,9 +1025,8 @@ impl DateTimeParser {
|
|||
/// // Normally this operation will fail.
|
||||
/// assert_eq!(
|
||||
/// PARSER.parse_zoned(timestamp).unwrap_err().to_string(),
|
||||
/// "failed to find time zone in square brackets in \
|
||||
/// \"2025-01-02T15:13-05\", which is required for \
|
||||
/// parsing a zoned instant",
|
||||
/// "failed to find time zone annotation in square brackets, \
|
||||
/// which is required for parsing a zoned datetime",
|
||||
/// );
|
||||
///
|
||||
/// // But you can work-around this with `Pieces`, which gives you direct
|
||||
|
|
@ -1073,8 +1070,8 @@ impl DateTimeParser {
|
|||
///
|
||||
/// assert_eq!(
|
||||
/// PARSER.parse_date("2024-03-10T00:00:00Z").unwrap_err().to_string(),
|
||||
/// "cannot parse civil date from string with a Zulu offset, \
|
||||
/// parse as a `Timestamp` and convert to a civil date instead",
|
||||
/// "cannot parse civil date/time from string with a Zulu offset, \
|
||||
/// parse as a `jiff::Timestamp` first and convert to a civil date/time instead",
|
||||
/// );
|
||||
///
|
||||
/// # Ok::<(), Box<dyn std::error::Error>>(())
|
||||
|
|
@ -1110,6 +1107,22 @@ 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.
|
||||
|
|
@ -1964,6 +1977,25 @@ 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.
|
||||
|
|
@ -2455,7 +2487,7 @@ mod tests {
|
|||
);
|
||||
insta::assert_snapshot!(
|
||||
DateTimeParser::new().parse_date("-000000-01-01").unwrap_err(),
|
||||
@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"###,
|
||||
@"failed to parse year in date: year zero must be written without a sign or a positive sign, but not a negative sign",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -146,9 +146,8 @@ use crate::{
|
|||
///
|
||||
/// assert_eq!(
|
||||
/// "2025-01-03T17:28-05".parse::<Zoned>().unwrap_err().to_string(),
|
||||
/// "failed to find time zone in square brackets in \
|
||||
/// \"2025-01-03T17:28-05\", which is required for \
|
||||
/// parsing a zoned instant",
|
||||
/// "failed to find time zone annotation in square brackets, \
|
||||
/// which is required for parsing a zoned datetime",
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
use crate::{
|
||||
civil::{Date, DateTime, Time},
|
||||
error::{err, Error},
|
||||
civil::{Date, DateTime, ISOWeekDate, Time},
|
||||
error::{fmt::temporal::Error as E, Error},
|
||||
fmt::{
|
||||
temporal::{Pieces, PiecesOffset, TimeZoneAnnotationKind},
|
||||
util::{DecimalFormatter, FractionalFormatter},
|
||||
util::{FractionalFormatter, IntegerFormatter},
|
||||
Write, WriteExt,
|
||||
},
|
||||
span::Span,
|
||||
|
|
@ -108,11 +108,11 @@ impl DateTimePrinter {
|
|||
date: &Date,
|
||||
mut wtr: W,
|
||||
) -> Result<(), Error> {
|
||||
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);
|
||||
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);
|
||||
|
||||
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: DecimalFormatter = DecimalFormatter::new().padding(2);
|
||||
static FMT_TWO: IntegerFormatter = IntegerFormatter::new().padding(2);
|
||||
static FMT_FRACTION: FractionalFormatter = FractionalFormatter::new();
|
||||
|
||||
wtr.write_int(&FMT_TWO, time.hour())?;
|
||||
|
|
@ -197,13 +197,7 @@ impl DateTimePrinter {
|
|||
//
|
||||
// Anyway, if you're seeing this error and think there should be a
|
||||
// different behavior, please file an issue.
|
||||
Err(err!(
|
||||
"time zones without IANA identifiers that aren't either \
|
||||
fixed offsets or a POSIX time zone can't be serialized \
|
||||
(this typically occurs when this is a system time zone \
|
||||
derived from `/etc/localtime` on Unix systems that \
|
||||
isn't symlinked to an entry in `/usr/share/zoneinfo`)",
|
||||
))
|
||||
Err(Error::from(E::PrintTimeZoneFailure))
|
||||
}
|
||||
|
||||
pub(super) fn print_pieces<W: Write>(
|
||||
|
|
@ -255,6 +249,34 @@ 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,
|
||||
|
|
@ -282,7 +304,7 @@ impl DateTimePrinter {
|
|||
offset: &Offset,
|
||||
mut wtr: W,
|
||||
) -> Result<(), Error> {
|
||||
static FMT_TWO: DecimalFormatter = DecimalFormatter::new().padding(2);
|
||||
static FMT_TWO: IntegerFormatter = IntegerFormatter::new().padding(2);
|
||||
|
||||
wtr.write_str(if offset.is_negative() { "-" } else { "+" })?;
|
||||
let mut hours = offset.part_hours_ranged().abs().get();
|
||||
|
|
@ -317,7 +339,7 @@ impl DateTimePrinter {
|
|||
offset: &Offset,
|
||||
mut wtr: W,
|
||||
) -> Result<(), Error> {
|
||||
static FMT_TWO: DecimalFormatter = DecimalFormatter::new().padding(2);
|
||||
static FMT_TWO: IntegerFormatter = IntegerFormatter::new().padding(2);
|
||||
|
||||
wtr.write_str(if offset.is_negative() { "-" } else { "+" })?;
|
||||
let hours = offset.part_hours_ranged().abs().get();
|
||||
|
|
@ -405,7 +427,7 @@ impl SpanPrinter {
|
|||
span: &Span,
|
||||
mut wtr: W,
|
||||
) -> Result<(), Error> {
|
||||
static FMT_INT: DecimalFormatter = DecimalFormatter::new();
|
||||
static FMT_INT: IntegerFormatter = IntegerFormatter::new();
|
||||
static FMT_FRACTION: FractionalFormatter = FractionalFormatter::new();
|
||||
|
||||
if span.is_negative() {
|
||||
|
|
@ -519,7 +541,7 @@ impl SpanPrinter {
|
|||
dur: &SignedDuration,
|
||||
mut wtr: W,
|
||||
) -> Result<(), Error> {
|
||||
static FMT_INT: DecimalFormatter = DecimalFormatter::new();
|
||||
static FMT_INT: IntegerFormatter = IntegerFormatter::new();
|
||||
static FMT_FRACTION: FractionalFormatter = FractionalFormatter::new();
|
||||
|
||||
let mut non_zero_greater_than_second = false;
|
||||
|
|
@ -568,7 +590,7 @@ impl SpanPrinter {
|
|||
dur: &core::time::Duration,
|
||||
mut wtr: W,
|
||||
) -> Result<(), Error> {
|
||||
static FMT_INT: DecimalFormatter = DecimalFormatter::new();
|
||||
static FMT_INT: IntegerFormatter = IntegerFormatter::new();
|
||||
static FMT_FRACTION: FractionalFormatter = FractionalFormatter::new();
|
||||
|
||||
let mut non_zero_greater_than_second = false;
|
||||
|
|
@ -620,7 +642,10 @@ impl SpanPrinter {
|
|||
mod tests {
|
||||
use alloc::string::String;
|
||||
|
||||
use crate::{civil::date, span::ToSpan};
|
||||
use crate::{
|
||||
civil::{date, Weekday},
|
||||
span::ToSpan,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
|
|
@ -922,4 +947,22 @@ 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",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
497
src/fmt/util.rs
497
src/fmt/util.rs
|
|
@ -1,7 +1,7 @@
|
|||
use crate::{
|
||||
error::{err, ErrorContext},
|
||||
error::{fmt::util::Error as E, ErrorContext},
|
||||
fmt::Parsed,
|
||||
util::{c::Sign, escape, parse, t},
|
||||
util::{c::Sign, parse, t},
|
||||
Error, SignedDuration, Span, Unit,
|
||||
};
|
||||
|
||||
|
|
@ -14,52 +14,31 @@ 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.
|
||||
///
|
||||
/// By default, this only includes the sign if it's negative. To always include
|
||||
/// the sign, set `force_sign` to `true`.
|
||||
/// This only includes the sign when formatting a negative signed integer.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct DecimalFormatter {
|
||||
force_sign: Option<bool>,
|
||||
pub(crate) struct IntegerFormatter {
|
||||
minimum_digits: u8,
|
||||
padding_byte: u8,
|
||||
}
|
||||
|
||||
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',
|
||||
}
|
||||
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' }
|
||||
}
|
||||
|
||||
/// Format the given value using this configuration as a signed decimal
|
||||
/// Format the given value using this configuration as a signed integer
|
||||
/// ASCII number.
|
||||
#[cfg_attr(feature = "perf-inline", inline(always))]
|
||||
pub(crate) const fn format_signed(&self, value: i64) -> Decimal {
|
||||
Decimal::signed(self, value)
|
||||
pub(crate) const fn format_signed(&self, value: i64) -> Integer {
|
||||
Integer::signed(self, value)
|
||||
}
|
||||
|
||||
/// Format the given value using this configuration as an unsigned decimal
|
||||
/// Format the given value using this configuration as an unsigned integer
|
||||
/// ASCII number.
|
||||
#[cfg_attr(feature = "perf-inline", inline(always))]
|
||||
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 }
|
||||
pub(crate) const fn format_unsigned(&self, value: u64) -> Integer {
|
||||
Integer::unsigned(self, value)
|
||||
}
|
||||
|
||||
/// The minimum number of digits/padding that this number should be
|
||||
|
|
@ -69,181 +48,113 @@ impl DecimalFormatter {
|
|||
///
|
||||
/// 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) -> DecimalFormatter {
|
||||
if digits > Decimal::MAX_I64_DIGITS {
|
||||
digits = Decimal::MAX_I64_DIGITS;
|
||||
pub(crate) const fn padding(self, mut digits: u8) -> IntegerFormatter {
|
||||
if digits > Integer::MAX_LEN {
|
||||
digits = Integer::MAX_LEN;
|
||||
}
|
||||
DecimalFormatter { minimum_digits: digits, ..self }
|
||||
IntegerFormatter { 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) -> DecimalFormatter {
|
||||
DecimalFormatter { padding_byte: byte, ..self }
|
||||
pub(crate) const fn padding_byte(self, byte: u8) -> IntegerFormatter {
|
||||
IntegerFormatter { padding_byte: byte, ..self }
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
/// Returns the minimum number of digits for an integer value.
|
||||
const fn get_minimum_digits(&self) -> u8 {
|
||||
if self.minimum_digits <= Integer::MAX_LEN {
|
||||
self.minimum_digits
|
||||
} else {
|
||||
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
|
||||
Integer::MAX_LEN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DecimalFormatter {
|
||||
fn default() -> DecimalFormatter {
|
||||
DecimalFormatter::new()
|
||||
impl Default for IntegerFormatter {
|
||||
fn default() -> IntegerFormatter {
|
||||
IntegerFormatter::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// A formatted decimal number that can be converted to a sequence of bytes.
|
||||
/// A formatted integer number that can be converted to a sequence of bytes.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Decimal {
|
||||
pub(crate) struct Integer {
|
||||
buf: [u8; Self::MAX_LEN as usize],
|
||||
start: u8,
|
||||
end: u8,
|
||||
}
|
||||
|
||||
impl Decimal {
|
||||
impl Integer {
|
||||
/// 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
|
||||
/// decimal representation using ASCII bytes.
|
||||
/// integer representation using ASCII bytes.
|
||||
#[cfg_attr(feature = "perf-inline", inline(always))]
|
||||
const fn unsigned(
|
||||
formatter: &DecimalFormatter,
|
||||
formatter: &IntegerFormatter,
|
||||
mut value: u64,
|
||||
) -> Decimal {
|
||||
let mut decimal = Decimal {
|
||||
buf: [0; Self::MAX_LEN as usize],
|
||||
start: Self::MAX_LEN,
|
||||
end: Self::MAX_LEN,
|
||||
};
|
||||
) -> Integer {
|
||||
let mut integer =
|
||||
Integer { buf: [0; Self::MAX_LEN as usize], start: Self::MAX_LEN };
|
||||
loop {
|
||||
decimal.start -= 1;
|
||||
integer.start -= 1;
|
||||
|
||||
let digit = (value % 10) as u8;
|
||||
value /= 10;
|
||||
decimal.buf[decimal.start as usize] = b'0' + digit;
|
||||
integer.buf[integer.start as usize] = b'0' + digit;
|
||||
if value == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
while decimal.len() < formatter.get_unsigned_minimum_digits() {
|
||||
decimal.start -= 1;
|
||||
decimal.buf[decimal.start as usize] = formatter.padding_byte;
|
||||
while integer.len() < formatter.get_minimum_digits() {
|
||||
integer.start -= 1;
|
||||
integer.buf[integer.start as usize] = formatter.padding_byte;
|
||||
}
|
||||
decimal
|
||||
integer
|
||||
}
|
||||
|
||||
/// Using the given formatter, turn the value given into a signed decimal
|
||||
/// Using the given formatter, turn the value given into a signed integer
|
||||
/// representation using ASCII bytes.
|
||||
#[cfg_attr(feature = "perf-inline", inline(always))]
|
||||
const fn signed(formatter: &DecimalFormatter, mut value: i64) -> Decimal {
|
||||
const fn signed(formatter: &IntegerFormatter, value: i64) -> Integer {
|
||||
// Specialize the common case to generate tighter codegen.
|
||||
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;
|
||||
if value >= 0 {
|
||||
return Integer::unsigned(formatter, value.unsigned_abs());
|
||||
}
|
||||
Decimal::signed_cold(formatter, value)
|
||||
Integer::signed_cold(formatter, value)
|
||||
}
|
||||
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
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;
|
||||
}
|
||||
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'-';
|
||||
}
|
||||
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
|
||||
integer
|
||||
}
|
||||
|
||||
/// Returns the total number of ASCII bytes (including the sign) that are
|
||||
/// used to represent this decimal number.
|
||||
/// used to represent this integer number.
|
||||
#[inline]
|
||||
const fn len(&self) -> u8 {
|
||||
self.end - self.start
|
||||
Self::MAX_LEN - self.start
|
||||
}
|
||||
|
||||
/// Returns the ASCII representation of this decimal as a byte slice.
|
||||
/// Returns the ASCII representation of this integer 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)..usize::from(self.end)]
|
||||
&self.buf[usize::from(self.start)..]
|
||||
}
|
||||
|
||||
/// Returns the ASCII representation of this decimal as a string slice.
|
||||
/// Returns the ASCII representation of this integer as a string slice.
|
||||
#[inline]
|
||||
pub(crate) fn as_str(&self) -> &str {
|
||||
// SAFETY: This is safe because all bytes written to `self.buf` are
|
||||
|
|
@ -462,14 +373,10 @@ impl DurationUnits {
|
|||
|
||||
if let Some(min) = self.min {
|
||||
if min <= unit {
|
||||
return Err(err!(
|
||||
"found value {value:?} with unit {unit} \
|
||||
after unit {prev_unit}, but units must be \
|
||||
written from largest to smallest \
|
||||
(and they can't be repeated)",
|
||||
unit = unit.singular(),
|
||||
prev_unit = min.singular(),
|
||||
));
|
||||
return Err(Error::from(E::OutOfOrderUnits {
|
||||
found: unit,
|
||||
previous: min,
|
||||
}));
|
||||
}
|
||||
}
|
||||
// Given the above check, the given unit must be smaller than any we
|
||||
|
|
@ -503,12 +410,7 @@ impl DurationUnits {
|
|||
) -> Result<(), Error> {
|
||||
if let Some(min) = self.min {
|
||||
if min <= Unit::Hour {
|
||||
return Err(err!(
|
||||
"found `HH:MM:SS` after unit {min}, \
|
||||
but `HH:MM:SS` can only appear after \
|
||||
years, months, weeks or days",
|
||||
min = min.singular(),
|
||||
));
|
||||
return Err(Error::from(E::OutOfOrderHMS { found: min }));
|
||||
}
|
||||
}
|
||||
self.set_unit_value(Unit::Hour, hours)?;
|
||||
|
|
@ -539,15 +441,11 @@ impl DurationUnits {
|
|||
/// return an error if the minimum unit is bigger than `Unit::Hour`.
|
||||
pub(crate) fn set_fraction(&mut self, fraction: u32) -> Result<(), Error> {
|
||||
assert!(fraction <= 999_999_999);
|
||||
if self.min == Some(Unit::Nanosecond) {
|
||||
return Err(err!("fractional nanoseconds are not supported"));
|
||||
}
|
||||
if let Some(min) = self.min {
|
||||
if min > Unit::Hour {
|
||||
return Err(err!(
|
||||
"fractional {plural} are not supported",
|
||||
plural = min.plural()
|
||||
));
|
||||
if min > Unit::Hour || min == Unit::Nanosecond {
|
||||
return Err(Error::from(E::NotAllowedFractionalUnit {
|
||||
found: min,
|
||||
}));
|
||||
}
|
||||
}
|
||||
self.fraction = Some(fraction);
|
||||
|
|
@ -642,13 +540,6 @@ impl DurationUnits {
|
|||
#[cold]
|
||||
#[inline(never)]
|
||||
fn to_span_general(&self) -> Result<Span, Error> {
|
||||
fn error_context(unit: Unit, value: i64) -> Error {
|
||||
err!(
|
||||
"failed to set value {value:?} as {unit} unit on span",
|
||||
unit = unit.singular(),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "perf-inline", inline(always))]
|
||||
fn set_time_unit(
|
||||
unit: Unit,
|
||||
|
|
@ -682,7 +573,7 @@ impl DurationUnits {
|
|||
|
||||
set(span)
|
||||
.or_else(|err| fractional_fallback(err, unit, value, span))
|
||||
.with_context(|| error_context(unit, value))
|
||||
.context(E::FailedValueSet { unit })
|
||||
}
|
||||
|
||||
let (min, _) = self.get_min_max_units()?;
|
||||
|
|
@ -692,25 +583,25 @@ impl DurationUnits {
|
|||
let value = self.get_unit_value(Unit::Year)?;
|
||||
span = span
|
||||
.try_years(value)
|
||||
.with_context(|| error_context(Unit::Year, value))?;
|
||||
.context(E::FailedValueSet { unit: Unit::Year })?;
|
||||
}
|
||||
if self.values[Unit::Month.as_usize()] != 0 {
|
||||
let value = self.get_unit_value(Unit::Month)?;
|
||||
span = span
|
||||
.try_months(value)
|
||||
.with_context(|| error_context(Unit::Month, value))?;
|
||||
.context(E::FailedValueSet { unit: Unit::Month })?;
|
||||
}
|
||||
if self.values[Unit::Week.as_usize()] != 0 {
|
||||
let value = self.get_unit_value(Unit::Week)?;
|
||||
span = span
|
||||
.try_weeks(value)
|
||||
.with_context(|| error_context(Unit::Week, value))?;
|
||||
.context(E::FailedValueSet { unit: Unit::Week })?;
|
||||
}
|
||||
if self.values[Unit::Day.as_usize()] != 0 {
|
||||
let value = self.get_unit_value(Unit::Day)?;
|
||||
span = span
|
||||
.try_days(value)
|
||||
.with_context(|| error_context(Unit::Day, value))?;
|
||||
.context(E::FailedValueSet { unit: Unit::Day })?;
|
||||
}
|
||||
if self.values[Unit::Hour.as_usize()] != 0 {
|
||||
let value = self.get_unit_value(Unit::Hour)?;
|
||||
|
|
@ -822,11 +713,7 @@ impl DurationUnits {
|
|||
fn to_signed_duration_general(&self) -> Result<SignedDuration, Error> {
|
||||
let (min, max) = self.get_min_max_units()?;
|
||||
if max > Unit::Hour {
|
||||
return Err(err!(
|
||||
"parsing {unit} units into a `SignedDuration` is not supported \
|
||||
(perhaps try parsing into a `Span` instead)",
|
||||
unit = max.singular(),
|
||||
));
|
||||
return Err(Error::from(E::NotAllowedCalendarUnit { unit: max }));
|
||||
}
|
||||
|
||||
let mut sdur = SignedDuration::ZERO;
|
||||
|
|
@ -834,85 +721,43 @@ impl DurationUnits {
|
|||
let value = self.get_unit_value(Unit::Hour)?;
|
||||
sdur = SignedDuration::try_from_hours(value)
|
||||
.and_then(|nanos| sdur.checked_add(nanos))
|
||||
.ok_or_else(|| {
|
||||
err!(
|
||||
"accumulated `SignedDuration` of `{sdur:?}` \
|
||||
overflowed when adding {value} of unit {unit}",
|
||||
unit = Unit::Hour.singular(),
|
||||
)
|
||||
})?;
|
||||
.ok_or(E::OverflowForUnit { unit: Unit::Hour })?;
|
||||
}
|
||||
if self.values[Unit::Minute.as_usize()] != 0 {
|
||||
let value = self.get_unit_value(Unit::Minute)?;
|
||||
sdur = SignedDuration::try_from_mins(value)
|
||||
.and_then(|nanos| sdur.checked_add(nanos))
|
||||
.ok_or_else(|| {
|
||||
err!(
|
||||
"accumulated `SignedDuration` of `{sdur:?}` \
|
||||
overflowed when adding {value} of unit {unit}",
|
||||
unit = Unit::Minute.singular(),
|
||||
)
|
||||
})?;
|
||||
.ok_or(E::OverflowForUnit { unit: Unit::Minute })?;
|
||||
}
|
||||
if self.values[Unit::Second.as_usize()] != 0 {
|
||||
let value = self.get_unit_value(Unit::Second)?;
|
||||
sdur = SignedDuration::from_secs(value)
|
||||
.checked_add(sdur)
|
||||
.ok_or_else(|| {
|
||||
err!(
|
||||
"accumulated `SignedDuration` of `{sdur:?}` \
|
||||
overflowed when adding {value} of unit {unit}",
|
||||
unit = Unit::Second.singular(),
|
||||
)
|
||||
})?;
|
||||
.ok_or(E::OverflowForUnit { unit: Unit::Second })?;
|
||||
}
|
||||
if self.values[Unit::Millisecond.as_usize()] != 0 {
|
||||
let value = self.get_unit_value(Unit::Millisecond)?;
|
||||
sdur = SignedDuration::from_millis(value)
|
||||
.checked_add(sdur)
|
||||
.ok_or_else(|| {
|
||||
err!(
|
||||
"accumulated `SignedDuration` of `{sdur:?}` \
|
||||
overflowed when adding {value} of unit {unit}",
|
||||
unit = Unit::Millisecond.singular(),
|
||||
)
|
||||
})?;
|
||||
.ok_or(E::OverflowForUnit { unit: Unit::Millisecond })?;
|
||||
}
|
||||
if self.values[Unit::Microsecond.as_usize()] != 0 {
|
||||
let value = self.get_unit_value(Unit::Microsecond)?;
|
||||
sdur = SignedDuration::from_micros(value)
|
||||
.checked_add(sdur)
|
||||
.ok_or_else(|| {
|
||||
err!(
|
||||
"accumulated `SignedDuration` of `{sdur:?}` \
|
||||
overflowed when adding {value} of unit {unit}",
|
||||
unit = Unit::Microsecond.singular(),
|
||||
)
|
||||
})?;
|
||||
.ok_or(E::OverflowForUnit { unit: Unit::Microsecond })?;
|
||||
}
|
||||
if self.values[Unit::Nanosecond.as_usize()] != 0 {
|
||||
let value = self.get_unit_value(Unit::Nanosecond)?;
|
||||
sdur = SignedDuration::from_nanos(value)
|
||||
.checked_add(sdur)
|
||||
.ok_or_else(|| {
|
||||
err!(
|
||||
"accumulated `SignedDuration` of `{sdur:?}` \
|
||||
overflowed when adding {value} of unit {unit}",
|
||||
unit = Unit::Nanosecond.singular(),
|
||||
)
|
||||
})?;
|
||||
.ok_or(E::OverflowForUnit { unit: Unit::Nanosecond })?;
|
||||
}
|
||||
|
||||
if let Some(fraction) = self.get_fraction()? {
|
||||
sdur = sdur
|
||||
.checked_add(fractional_duration(min, fraction)?)
|
||||
.ok_or_else(|| {
|
||||
err!(
|
||||
"accumulated `SignedDuration` of `{sdur:?}` \
|
||||
overflowed when adding 0.{fraction} of unit {unit}",
|
||||
unit = min.singular(),
|
||||
)
|
||||
})?;
|
||||
.ok_or(E::OverflowForUnitFractional { unit: min })?;
|
||||
}
|
||||
|
||||
Ok(sdur)
|
||||
|
|
@ -1003,19 +848,12 @@ impl DurationUnits {
|
|||
}
|
||||
|
||||
if self.sign.is_negative() {
|
||||
return Err(err!(
|
||||
"cannot parse negative duration into unsigned \
|
||||
`std::time::Duration`",
|
||||
));
|
||||
return Err(Error::from(E::NotAllowedNegative));
|
||||
}
|
||||
|
||||
let (min, max) = self.get_min_max_units()?;
|
||||
if max > Unit::Hour {
|
||||
return Err(err!(
|
||||
"parsing {unit} units into a `std::time::Duration` \
|
||||
is not supported (perhaps try parsing into a `Span` instead)",
|
||||
unit = max.singular(),
|
||||
));
|
||||
return Err(Error::from(E::NotAllowedCalendarUnit { unit: max }));
|
||||
}
|
||||
|
||||
let mut sdur = core::time::Duration::ZERO;
|
||||
|
|
@ -1023,73 +861,37 @@ impl DurationUnits {
|
|||
let value = self.values[Unit::Hour.as_usize()];
|
||||
sdur = try_from_hours(value)
|
||||
.and_then(|nanos| sdur.checked_add(nanos))
|
||||
.ok_or_else(|| {
|
||||
err!(
|
||||
"accumulated `std::time::Duration` of `{sdur:?}` \
|
||||
overflowed when adding {value} of unit {unit}",
|
||||
unit = Unit::Hour.singular(),
|
||||
)
|
||||
})?;
|
||||
.ok_or(E::OverflowForUnit { unit: Unit::Hour })?;
|
||||
}
|
||||
if self.values[Unit::Minute.as_usize()] != 0 {
|
||||
let value = self.values[Unit::Minute.as_usize()];
|
||||
sdur = try_from_mins(value)
|
||||
.and_then(|nanos| sdur.checked_add(nanos))
|
||||
.ok_or_else(|| {
|
||||
err!(
|
||||
"accumulated `std::time::Duration` of `{sdur:?}` \
|
||||
overflowed when adding {value} of unit {unit}",
|
||||
unit = Unit::Minute.singular(),
|
||||
)
|
||||
})?;
|
||||
.ok_or(E::OverflowForUnit { unit: Unit::Minute })?;
|
||||
}
|
||||
if self.values[Unit::Second.as_usize()] != 0 {
|
||||
let value = self.values[Unit::Second.as_usize()];
|
||||
sdur = core::time::Duration::from_secs(value)
|
||||
.checked_add(sdur)
|
||||
.ok_or_else(|| {
|
||||
err!(
|
||||
"accumulated `std::time::Duration` of `{sdur:?}` \
|
||||
overflowed when adding {value} of unit {unit}",
|
||||
unit = Unit::Second.singular(),
|
||||
)
|
||||
})?;
|
||||
.ok_or(E::OverflowForUnit { unit: Unit::Second })?;
|
||||
}
|
||||
if self.values[Unit::Millisecond.as_usize()] != 0 {
|
||||
let value = self.values[Unit::Millisecond.as_usize()];
|
||||
sdur = core::time::Duration::from_millis(value)
|
||||
.checked_add(sdur)
|
||||
.ok_or_else(|| {
|
||||
err!(
|
||||
"accumulated `std::time::Duration` of `{sdur:?}` \
|
||||
overflowed when adding {value} of unit {unit}",
|
||||
unit = Unit::Millisecond.singular(),
|
||||
)
|
||||
})?;
|
||||
.ok_or(E::OverflowForUnit { unit: Unit::Millisecond })?;
|
||||
}
|
||||
if self.values[Unit::Microsecond.as_usize()] != 0 {
|
||||
let value = self.values[Unit::Microsecond.as_usize()];
|
||||
sdur = core::time::Duration::from_micros(value)
|
||||
.checked_add(sdur)
|
||||
.ok_or_else(|| {
|
||||
err!(
|
||||
"accumulated `std::time::Duration` of `{sdur:?}` \
|
||||
overflowed when adding {value} of unit {unit}",
|
||||
unit = Unit::Microsecond.singular(),
|
||||
)
|
||||
})?;
|
||||
.ok_or(E::OverflowForUnit { unit: Unit::Microsecond })?;
|
||||
}
|
||||
if self.values[Unit::Nanosecond.as_usize()] != 0 {
|
||||
let value = self.values[Unit::Nanosecond.as_usize()];
|
||||
sdur = core::time::Duration::from_nanos(value)
|
||||
.checked_add(sdur)
|
||||
.ok_or_else(|| {
|
||||
err!(
|
||||
"accumulated `std::time::Duration` of `{sdur:?}` \
|
||||
overflowed when adding {value} of unit {unit}",
|
||||
unit = Unit::Nanosecond.singular(),
|
||||
)
|
||||
})?;
|
||||
.ok_or(E::OverflowForUnit { unit: Unit::Nanosecond })?;
|
||||
}
|
||||
|
||||
if let Some(fraction) = self.get_fraction()? {
|
||||
|
|
@ -1097,13 +899,7 @@ impl DurationUnits {
|
|||
.checked_add(
|
||||
fractional_duration(min, fraction)?.unsigned_abs(),
|
||||
)
|
||||
.ok_or_else(|| {
|
||||
err!(
|
||||
"accumulated `std::time::Duration` of `{sdur:?}` \
|
||||
overflowed when adding 0.{fraction} of unit {unit}",
|
||||
unit = min.singular(),
|
||||
)
|
||||
})?;
|
||||
.ok_or(E::OverflowForUnitFractional { unit: Unit::Hour })?;
|
||||
}
|
||||
|
||||
Ok(sdur)
|
||||
|
|
@ -1122,7 +918,7 @@ impl DurationUnits {
|
|||
/// were no parsed duration components.)
|
||||
fn get_min_max_units(&self) -> Result<(Unit, Unit), Error> {
|
||||
let (Some(min), Some(max)) = (self.min, self.max) else {
|
||||
return Err(err!("no parsed duration components"));
|
||||
return Err(Error::from(E::EmptyDuration));
|
||||
};
|
||||
Ok((min, max))
|
||||
}
|
||||
|
|
@ -1143,21 +939,12 @@ impl DurationUnits {
|
|||
}
|
||||
// Otherwise, if a conversion to `i64` fails, then that failure
|
||||
// is correct.
|
||||
let mut value = i64::try_from(value).map_err(|_| {
|
||||
err!(
|
||||
"`{sign}{value}` {unit} is too big (or small) \
|
||||
to fit into a signed 64-bit integer",
|
||||
unit = unit.plural()
|
||||
)
|
||||
})?;
|
||||
let mut value = i64::try_from(value)
|
||||
.map_err(|_| E::SignedOverflowForUnit { unit })?;
|
||||
if sign.is_negative() {
|
||||
value = value.checked_neg().ok_or_else(|| {
|
||||
err!(
|
||||
"`{sign}{value}` {unit} is too big (or small) \
|
||||
to fit into a signed 64-bit integer",
|
||||
unit = unit.plural()
|
||||
)
|
||||
})?;
|
||||
value = value
|
||||
.checked_neg()
|
||||
.ok_or(E::SignedOverflowForUnit { unit })?;
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
|
|
@ -1258,21 +1045,13 @@ pub(crate) fn parse_temporal_fraction<'i>(
|
|||
}
|
||||
let digits = mkdigits(input);
|
||||
if digits.is_empty() {
|
||||
return Err(err!(
|
||||
"found decimal after seconds component, \
|
||||
but did not find any decimal digits after decimal",
|
||||
));
|
||||
return Err(Error::from(E::MissingFractionalDigits));
|
||||
}
|
||||
// I believe this error can never happen, since we know we have no more
|
||||
// than 9 ASCII digits. Any sequence of 9 ASCII digits can be parsed
|
||||
// into an `i64`.
|
||||
let nanoseconds = parse::fraction(digits).map_err(|err| {
|
||||
err!(
|
||||
"failed to parse {digits:?} as fractional component \
|
||||
(up to 9 digits, nanosecond precision): {err}",
|
||||
digits = escape::Bytes(digits),
|
||||
)
|
||||
})?;
|
||||
let nanoseconds =
|
||||
parse::fraction(digits).context(E::InvalidFraction)?;
|
||||
// OK because parsing is forcefully limited to 9 digits,
|
||||
// which can never be greater than `999_999_99`,
|
||||
// which is less than `u32::MAX`.
|
||||
|
|
@ -1411,18 +1190,10 @@ fn fractional_time_to_span(
|
|||
}
|
||||
if !sdur.is_zero() {
|
||||
let nanos = sdur.as_nanos();
|
||||
let nanos64 = i64::try_from(nanos).map_err(|_| {
|
||||
err!(
|
||||
"failed to set nanosecond value {nanos} (it overflows \
|
||||
`i64`) on span determined from {value}.{fraction}",
|
||||
)
|
||||
})?;
|
||||
span = span.try_nanoseconds(nanos64).with_context(|| {
|
||||
err!(
|
||||
"failed to set nanosecond value {nanos64} on span \
|
||||
determined from {value}.{fraction}",
|
||||
)
|
||||
})?;
|
||||
let nanos64 =
|
||||
i64::try_from(nanos).map_err(|_| E::InvalidFractionNanos)?;
|
||||
span =
|
||||
span.try_nanoseconds(nanos64).context(E::InvalidFractionNanos)?;
|
||||
}
|
||||
|
||||
Ok(span)
|
||||
|
|
@ -1452,13 +1223,9 @@ fn fractional_time_to_duration(
|
|||
) -> Result<SignedDuration, Error> {
|
||||
let sdur = duration_unit_value(unit, value)?;
|
||||
let fraction_dur = fractional_duration(unit, fraction)?;
|
||||
sdur.checked_add(fraction_dur).ok_or_else(|| {
|
||||
err!(
|
||||
"accumulated `SignedDuration` of `{sdur:?}` overflowed \
|
||||
when adding `{fraction_dur:?}` (from fractional {unit} units)",
|
||||
unit = unit.singular(),
|
||||
)
|
||||
})
|
||||
Ok(sdur
|
||||
.checked_add(fraction_dur)
|
||||
.ok_or(E::OverflowForUnitFractional { unit })?)
|
||||
}
|
||||
|
||||
/// Converts the fraction of the given unit to a signed duration.
|
||||
|
|
@ -1488,10 +1255,9 @@ fn fractional_duration(
|
|||
Unit::Millisecond => fraction / t::NANOS_PER_MICRO.value(),
|
||||
Unit::Microsecond => fraction / t::NANOS_PER_MILLI.value(),
|
||||
unit => {
|
||||
return Err(err!(
|
||||
"fractional {unit} units are not allowed",
|
||||
unit = unit.singular(),
|
||||
))
|
||||
return Err(Error::from(E::NotAllowedFractionalUnit {
|
||||
found: unit,
|
||||
}));
|
||||
}
|
||||
};
|
||||
Ok(SignedDuration::from_nanos(nanos))
|
||||
|
|
@ -1516,17 +1282,13 @@ fn duration_unit_value(
|
|||
Unit::Hour => {
|
||||
let seconds = value
|
||||
.checked_mul(t::SECONDS_PER_HOUR.value())
|
||||
.ok_or_else(|| {
|
||||
err!("converting {value} hours to seconds overflows i64")
|
||||
})?;
|
||||
.ok_or(E::ConversionToSecondsFailed { unit: Unit::Hour })?;
|
||||
SignedDuration::from_secs(seconds)
|
||||
}
|
||||
Unit::Minute => {
|
||||
let seconds = value
|
||||
.checked_mul(t::SECONDS_PER_MINUTE.value())
|
||||
.ok_or_else(|| {
|
||||
err!("converting {value} minutes to seconds overflows i64")
|
||||
})?;
|
||||
.ok_or(E::ConversionToSecondsFailed { unit: Unit::Minute })?;
|
||||
SignedDuration::from_secs(seconds)
|
||||
}
|
||||
Unit::Second => SignedDuration::from_secs(value),
|
||||
|
|
@ -1534,11 +1296,9 @@ fn duration_unit_value(
|
|||
Unit::Microsecond => SignedDuration::from_micros(value),
|
||||
Unit::Nanosecond => SignedDuration::from_nanos(value),
|
||||
unsupported => {
|
||||
return Err(err!(
|
||||
"parsing {unit} units into a `SignedDuration` is not supported \
|
||||
(perhaps try parsing into a `Span` instead)",
|
||||
unit = unsupported.singular(),
|
||||
));
|
||||
return Err(Error::from(E::NotAllowedCalendarUnit {
|
||||
unit: unsupported,
|
||||
}))
|
||||
}
|
||||
};
|
||||
Ok(sdur)
|
||||
|
|
@ -1551,43 +1311,30 @@ mod tests {
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn decimal() {
|
||||
let x = DecimalFormatter::new().format_signed(i64::MIN);
|
||||
fn integer() {
|
||||
let x = IntegerFormatter::new().format_signed(i64::MIN);
|
||||
assert_eq!(x.as_str(), "-9223372036854775808");
|
||||
|
||||
let x = DecimalFormatter::new().format_signed(i64::MIN + 1);
|
||||
let x = IntegerFormatter::new().format_signed(i64::MIN + 1);
|
||||
assert_eq!(x.as_str(), "-9223372036854775807");
|
||||
|
||||
let x = DecimalFormatter::new().format_signed(i64::MAX);
|
||||
let x = IntegerFormatter::new().format_signed(i64::MAX);
|
||||
assert_eq!(x.as_str(), "9223372036854775807");
|
||||
|
||||
let x =
|
||||
DecimalFormatter::new().force_sign(true).format_signed(i64::MAX);
|
||||
assert_eq!(x.as_str(), "+9223372036854775807");
|
||||
|
||||
let x = DecimalFormatter::new().format_signed(0);
|
||||
let x = IntegerFormatter::new().format_signed(0);
|
||||
assert_eq!(x.as_str(), "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);
|
||||
let x = IntegerFormatter::new().padding(4).format_signed(0);
|
||||
assert_eq!(x.as_str(), "0000");
|
||||
|
||||
let x = DecimalFormatter::new().padding(4).format_signed(789);
|
||||
let x = IntegerFormatter::new().padding(4).format_signed(789);
|
||||
assert_eq!(x.as_str(), "0789");
|
||||
|
||||
let x = DecimalFormatter::new().padding(4).format_signed(-789);
|
||||
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");
|
||||
let x = IntegerFormatter::new().padding(4).format_signed(789);
|
||||
assert_eq!(x.as_str(), "0789");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -87,10 +87,10 @@ impl Logger {
|
|||
/// Create a new logger that logs to stderr and initialize it as the
|
||||
/// global logger. If there was a problem setting the logger, then an
|
||||
/// error is returned.
|
||||
pub(crate) fn init() -> Result<(), crate::Error> {
|
||||
pub(crate) fn init() -> Result<(), log::SetLoggerError> {
|
||||
#[cfg(all(feature = "std", feature = "logging"))]
|
||||
{
|
||||
log::set_logger(LOGGER).map_err(crate::Error::adhoc)?;
|
||||
log::set_logger(LOGGER)?;
|
||||
log::set_max_level(log::LevelFilter::Trace);
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
15
src/now.rs
15
src/now.rs
|
|
@ -83,15 +83,16 @@ mod sys {
|
|||
} else {
|
||||
SystemTime::UNIX_EPOCH.checked_sub(duration)
|
||||
};
|
||||
// It's a little sad that we have to panic here, but the standard
|
||||
// SystemTime::now() API is infallible, so we kind of have to match it.
|
||||
// With that said, a panic here would be highly unusual. It would imply
|
||||
// that the system time is set to some extreme timestamp very far in the
|
||||
// future or the past.
|
||||
// It's a little sad that we have to panic here, but the
|
||||
// standard SystemTime::now() API is infallible, so we kind
|
||||
// of have to match it. With that said, a panic here would be
|
||||
// highly unusual. It would imply that the system time is set
|
||||
// to some extreme timestamp very far in the future or the
|
||||
// past.
|
||||
let Some(timestamp) = result else {
|
||||
panic!(
|
||||
"failed to get current time: \
|
||||
subtracting {duration:?} from Unix epoch overflowed"
|
||||
"failed to get current time from Javascript date: \
|
||||
arithmetic on Unix epoch overflowed"
|
||||
)
|
||||
};
|
||||
timestamp
|
||||
|
|
|
|||
1282
src/shared/posix.rs
1282
src/shared/posix.rs
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,46 +0,0 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
/*!
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
|
@ -22,8 +22,6 @@ 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,11 +139,13 @@ impl IDateTime {
|
|||
pub(crate) fn checked_add_seconds(
|
||||
&self,
|
||||
seconds: i32,
|
||||
) -> Result<IDateTime, Error> {
|
||||
let day_second =
|
||||
self.time.to_second().second.checked_add(seconds).ok_or_else(
|
||||
|| err!("adding `{seconds}s` to datetime overflowed"),
|
||||
)?;
|
||||
) -> Result<IDateTime, RangeError> {
|
||||
let day_second = self
|
||||
.time
|
||||
.to_second()
|
||||
.second
|
||||
.checked_add(seconds)
|
||||
.ok_or_else(|| RangeError::DateTimeSeconds)?;
|
||||
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 {
|
||||
const MIN: IEpochDay = IEpochDay { epoch_day: -4371587 };
|
||||
const MAX: IEpochDay = IEpochDay { epoch_day: 2932896 };
|
||||
pub(crate) const MIN: IEpochDay = IEpochDay { epoch_day: -4371587 };
|
||||
pub(crate) const MAX: IEpochDay = IEpochDay { epoch_day: 2932896 };
|
||||
|
||||
/// Converts days since the Unix epoch to a Gregorian date.
|
||||
///
|
||||
|
|
@ -217,20 +217,17 @@ 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, Error> {
|
||||
pub(crate) fn checked_add(
|
||||
&self,
|
||||
amount: i32,
|
||||
) -> Result<IEpochDay, RangeError> {
|
||||
let epoch_day = self.epoch_day;
|
||||
let sum = epoch_day.checked_add(amount).ok_or_else(|| {
|
||||
err!("adding `{amount}` to epoch day `{epoch_day}` overflowed i32")
|
||||
})?;
|
||||
let sum = epoch_day
|
||||
.checked_add(amount)
|
||||
.ok_or_else(|| RangeError::EpochDayI32)?;
|
||||
let ret = IEpochDay { epoch_day: sum };
|
||||
if !(IEpochDay::MIN <= ret && ret <= IEpochDay::MAX) {
|
||||
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,
|
||||
));
|
||||
return Err(RangeError::EpochDayDays);
|
||||
}
|
||||
Ok(ret)
|
||||
}
|
||||
|
|
@ -258,14 +255,11 @@ impl IDate {
|
|||
year: i16,
|
||||
month: i8,
|
||||
day: i8,
|
||||
) -> Result<IDate, Error> {
|
||||
) -> Result<IDate, RangeError> {
|
||||
if day > 28 {
|
||||
let max_day = days_in_month(year, month);
|
||||
if day > max_day {
|
||||
return Err(err!(
|
||||
"day={day} is out of range for year={year} \
|
||||
and month={month}, must be in range 1..={max_day}",
|
||||
));
|
||||
return Err(RangeError::DateInvalidDays { year, month });
|
||||
}
|
||||
}
|
||||
Ok(IDate { year, month, day })
|
||||
|
|
@ -281,37 +275,22 @@ impl IDate {
|
|||
pub(crate) fn from_day_of_year(
|
||||
year: i16,
|
||||
day: i16,
|
||||
) -> Result<IDate, Error> {
|
||||
) -> Result<IDate, RangeError> {
|
||||
if !(1 <= day && day <= 366) {
|
||||
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),
|
||||
));
|
||||
return Err(RangeError::DateInvalidDayOfYear { year });
|
||||
}
|
||||
let start = IDate { year, month: 1, day: 1 }.to_epoch_day();
|
||||
let end = start
|
||||
.checked_add(i32::from(day) - 1)
|
||||
.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,
|
||||
)
|
||||
})?
|
||||
// This can only happen when `year=9999` and `day=366`.
|
||||
.map_err(|_| RangeError::DayOfYear)?
|
||||
.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(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),
|
||||
));
|
||||
return Err(RangeError::DateInvalidDayOfYear { year });
|
||||
}
|
||||
Ok(end)
|
||||
}
|
||||
|
|
@ -327,12 +306,9 @@ impl IDate {
|
|||
pub(crate) fn from_day_of_year_no_leap(
|
||||
year: i16,
|
||||
mut day: i16,
|
||||
) -> Result<IDate, Error> {
|
||||
) -> Result<IDate, RangeError> {
|
||||
if !(1 <= day && day <= 365) {
|
||||
return Err(err!(
|
||||
"day-of-year={day} is out of range for year={year}, \
|
||||
must be in range 1..=365",
|
||||
));
|
||||
return Err(RangeError::DateInvalidDayOfYearNoLeap);
|
||||
}
|
||||
if day >= 60 && is_leap_year(year) {
|
||||
day += 1;
|
||||
|
|
@ -390,12 +366,9 @@ impl IDate {
|
|||
&self,
|
||||
nth: i8,
|
||||
weekday: IWeekday,
|
||||
) -> Result<IDate, Error> {
|
||||
) -> Result<IDate, RangeError> {
|
||||
if nth == 0 || !(-5 <= nth && nth <= 5) {
|
||||
return Err(err!(
|
||||
"got nth weekday of `{nth}`, but \
|
||||
must be non-zero and in range `-5..=5`",
|
||||
));
|
||||
return Err(RangeError::NthWeekdayOfMonth);
|
||||
}
|
||||
if nth > 0 {
|
||||
let first_weekday = self.first_of_month().weekday();
|
||||
|
|
@ -412,13 +385,10 @@ impl IDate {
|
|||
// of `Day`, we can't let this boundary condition escape. So we
|
||||
// check it here.
|
||||
if day < 1 {
|
||||
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),
|
||||
));
|
||||
return Err(RangeError::DateInvalidDays {
|
||||
year: self.year,
|
||||
month: self.month,
|
||||
});
|
||||
}
|
||||
IDate::try_new(self.year, self.month, day)
|
||||
}
|
||||
|
|
@ -426,16 +396,12 @@ impl IDate {
|
|||
|
||||
/// Returns the day before this date.
|
||||
#[inline]
|
||||
pub(crate) fn yesterday(self) -> Result<IDate, Error> {
|
||||
pub(crate) fn yesterday(self) -> Result<IDate, RangeError> {
|
||||
if self.day == 1 {
|
||||
if self.month == 1 {
|
||||
let year = self.year - 1;
|
||||
if year <= -10000 {
|
||||
return Err(err!(
|
||||
"returning yesterday for -9999-01-01 is not \
|
||||
possible because it is less than Jiff's supported
|
||||
minimum date",
|
||||
));
|
||||
return Err(RangeError::Yesterday);
|
||||
}
|
||||
return Ok(IDate { year, month: 12, day: 31 });
|
||||
}
|
||||
|
|
@ -448,16 +414,12 @@ impl IDate {
|
|||
|
||||
/// Returns the day after this date.
|
||||
#[inline]
|
||||
pub(crate) fn tomorrow(self) -> Result<IDate, Error> {
|
||||
pub(crate) fn tomorrow(self) -> Result<IDate, RangeError> {
|
||||
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(err!(
|
||||
"returning tomorrow for 9999-12-31 is not \
|
||||
possible because it is greater than Jiff's supported
|
||||
maximum date",
|
||||
));
|
||||
return Err(RangeError::Tomorrow);
|
||||
}
|
||||
return Ok(IDate { year, month: 1, day: 1 });
|
||||
}
|
||||
|
|
@ -469,34 +431,20 @@ impl IDate {
|
|||
|
||||
/// Returns the year one year before this date.
|
||||
#[inline]
|
||||
pub(crate) fn prev_year(self) -> Result<i16, Error> {
|
||||
pub(crate) fn prev_year(self) -> Result<i16, RangeError> {
|
||||
let year = self.year - 1;
|
||||
if year <= -10_000 {
|
||||
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,
|
||||
));
|
||||
return Err(RangeError::YearPrevious);
|
||||
}
|
||||
Ok(year)
|
||||
}
|
||||
|
||||
/// Returns the year one year from this date.
|
||||
#[inline]
|
||||
pub(crate) fn next_year(self) -> Result<i16, Error> {
|
||||
pub(crate) fn next_year(self) -> Result<i16, RangeError> {
|
||||
let year = self.year + 1;
|
||||
if year >= 10_000 {
|
||||
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,
|
||||
));
|
||||
return Err(RangeError::YearNext);
|
||||
}
|
||||
Ok(year)
|
||||
}
|
||||
|
|
@ -506,7 +454,7 @@ impl IDate {
|
|||
pub(crate) fn checked_add_days(
|
||||
&self,
|
||||
amount: i32,
|
||||
) -> Result<IDate, Error> {
|
||||
) -> Result<IDate, RangeError> {
|
||||
match amount {
|
||||
0 => Ok(*self),
|
||||
-1 => self.yesterday(),
|
||||
|
|
@ -718,6 +666,84 @@ 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.
|
||||
|
|
@ -920,4 +946,20 @@ 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,2 @@
|
|||
pub(crate) mod array_str;
|
||||
pub(crate) mod error;
|
||||
pub(crate) mod escape;
|
||||
pub(crate) mod itime;
|
||||
pub(crate) mod utf8;
|
||||
|
|
|
|||
|
|
@ -1,37 +0,0 @@
|
|||
/// 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()))
|
||||
}
|
||||
|
|
@ -2,10 +2,10 @@ use core::time::Duration;
|
|||
|
||||
use crate::{
|
||||
civil::{Date, DateTime, Time},
|
||||
error::{err, ErrorContext},
|
||||
error::{signed_duration::Error as E, ErrorContext},
|
||||
fmt::{friendly, temporal},
|
||||
tz::Offset,
|
||||
util::{escape, rangeint::TryRFrom, t},
|
||||
util::{rangeint::TryRFrom, t},
|
||||
Error, RoundMode, Timestamp, Unit, Zoned,
|
||||
};
|
||||
|
||||
|
|
@ -65,8 +65,7 @@ const MINS_PER_HOUR: i64 = 60;
|
|||
///
|
||||
/// assert_eq!(
|
||||
/// "P1d".parse::<SignedDuration>().unwrap_err().to_string(),
|
||||
/// "failed to parse \"P1d\" as an ISO 8601 duration string: \
|
||||
/// parsing ISO 8601 duration into a `SignedDuration` requires that \
|
||||
/// "parsing ISO 8601 duration in this context requires that \
|
||||
/// the duration contain a time component and no components of days or \
|
||||
/// greater",
|
||||
/// );
|
||||
|
|
@ -1456,24 +1455,13 @@ impl SignedDuration {
|
|||
#[inline]
|
||||
pub fn try_from_secs_f64(secs: f64) -> Result<SignedDuration, Error> {
|
||||
if !secs.is_finite() {
|
||||
return Err(err!(
|
||||
"could not convert non-finite seconds \
|
||||
{secs} to signed duration",
|
||||
));
|
||||
return Err(Error::from(E::ConvertNonFinite));
|
||||
}
|
||||
if secs < (i64::MIN as f64) {
|
||||
return Err(err!(
|
||||
"floating point seconds {secs} overflows signed duration \
|
||||
minimum value of {:?}",
|
||||
SignedDuration::MIN,
|
||||
));
|
||||
return Err(Error::slim_range("floating point seconds"));
|
||||
}
|
||||
if secs > (i64::MAX as f64) {
|
||||
return Err(err!(
|
||||
"floating point seconds {secs} overflows signed duration \
|
||||
maximum value of {:?}",
|
||||
SignedDuration::MAX,
|
||||
));
|
||||
return Err(Error::slim_range("floating point seconds"));
|
||||
}
|
||||
|
||||
let mut int_secs = secs.trunc() as i64;
|
||||
|
|
@ -1481,15 +1469,9 @@ impl SignedDuration {
|
|||
(secs.fract() * (NANOS_PER_SEC as f64)).round() as i32;
|
||||
if int_nanos.unsigned_abs() == 1_000_000_000 {
|
||||
let increment = i64::from(int_nanos.signum());
|
||||
int_secs = int_secs.checked_add(increment).ok_or_else(|| {
|
||||
err!(
|
||||
"floating point seconds {secs} overflows signed duration \
|
||||
maximum value of {max:?} after rounding its fractional \
|
||||
component of {fract:?}",
|
||||
max = SignedDuration::MAX,
|
||||
fract = secs.fract(),
|
||||
)
|
||||
})?;
|
||||
int_secs = int_secs
|
||||
.checked_add(increment)
|
||||
.ok_or_else(|| Error::slim_range("floating point seconds"))?;
|
||||
int_nanos = 0;
|
||||
}
|
||||
Ok(SignedDuration::new_unchecked(int_secs, int_nanos))
|
||||
|
|
@ -1528,24 +1510,13 @@ impl SignedDuration {
|
|||
#[inline]
|
||||
pub fn try_from_secs_f32(secs: f32) -> Result<SignedDuration, Error> {
|
||||
if !secs.is_finite() {
|
||||
return Err(err!(
|
||||
"could not convert non-finite seconds \
|
||||
{secs} to signed duration",
|
||||
));
|
||||
return Err(Error::from(E::ConvertNonFinite));
|
||||
}
|
||||
if secs < (i64::MIN as f32) {
|
||||
return Err(err!(
|
||||
"floating point seconds {secs} overflows signed duration \
|
||||
minimum value of {:?}",
|
||||
SignedDuration::MIN,
|
||||
));
|
||||
return Err(Error::slim_range("floating point seconds"));
|
||||
}
|
||||
if secs > (i64::MAX as f32) {
|
||||
return Err(err!(
|
||||
"floating point seconds {secs} overflows signed duration \
|
||||
maximum value of {:?}",
|
||||
SignedDuration::MAX,
|
||||
));
|
||||
return Err(Error::slim_range("floating point seconds"));
|
||||
}
|
||||
let mut int_nanos =
|
||||
(secs.fract() * (NANOS_PER_SEC as f32)).round() as i32;
|
||||
|
|
@ -1553,15 +1524,9 @@ impl SignedDuration {
|
|||
if int_nanos.unsigned_abs() == 1_000_000_000 {
|
||||
let increment = i64::from(int_nanos.signum());
|
||||
// N.B. I haven't found a way to trigger this error path in tests.
|
||||
int_secs = int_secs.checked_add(increment).ok_or_else(|| {
|
||||
err!(
|
||||
"floating point seconds {secs} overflows signed duration \
|
||||
maximum value of {max:?} after rounding its fractional \
|
||||
component of {fract:?}",
|
||||
max = SignedDuration::MAX,
|
||||
fract = secs.fract(),
|
||||
)
|
||||
})?;
|
||||
int_secs = int_secs
|
||||
.checked_add(increment)
|
||||
.ok_or_else(|| Error::slim_range("floating point seconds"))?;
|
||||
int_nanos = 0;
|
||||
}
|
||||
Ok(SignedDuration::new_unchecked(int_secs, int_nanos))
|
||||
|
|
@ -2023,25 +1988,18 @@ impl SignedDuration {
|
|||
time2: std::time::SystemTime,
|
||||
) -> Result<SignedDuration, Error> {
|
||||
match time2.duration_since(time1) {
|
||||
Ok(dur) => SignedDuration::try_from(dur).with_context(|| {
|
||||
err!(
|
||||
"unsigned duration {dur:?} for system time since \
|
||||
Unix epoch overflowed signed duration"
|
||||
)
|
||||
}),
|
||||
Ok(dur) => {
|
||||
SignedDuration::try_from(dur).context(E::ConvertSystemTime)
|
||||
}
|
||||
Err(err) => {
|
||||
let dur = err.duration();
|
||||
let dur =
|
||||
SignedDuration::try_from(dur).with_context(|| {
|
||||
err!(
|
||||
"unsigned duration {dur:?} for system time before \
|
||||
Unix epoch overflowed signed duration"
|
||||
)
|
||||
})?;
|
||||
dur.checked_neg().ok_or_else(|| {
|
||||
err!("negating duration {dur:?} from before the Unix epoch \
|
||||
overflowed signed duration")
|
||||
})
|
||||
let dur = SignedDuration::try_from(dur)
|
||||
.context(E::ConvertSystemTime)?;
|
||||
dur.checked_neg()
|
||||
.ok_or_else(|| {
|
||||
Error::slim_range("signed duration seconds")
|
||||
})
|
||||
.context(E::ConvertSystemTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2155,17 +2113,15 @@ impl SignedDuration {
|
|||
///
|
||||
/// assert_eq!(
|
||||
/// SignedDuration::MAX.round(Unit::Hour).unwrap_err().to_string(),
|
||||
/// "rounding `2562047788015215h 30m 7s 999ms 999µs 999ns` to \
|
||||
/// nearest hour in increments of 1 resulted in \
|
||||
/// 9223372036854777600 seconds, which does not fit into an i64 \
|
||||
/// and thus overflows `SignedDuration`",
|
||||
/// "rounding signed duration to nearest hour \
|
||||
/// resulted in a value outside the supported \
|
||||
/// range of a `jiff::SignedDuration`",
|
||||
/// );
|
||||
/// assert_eq!(
|
||||
/// SignedDuration::MIN.round(Unit::Hour).unwrap_err().to_string(),
|
||||
/// "rounding `2562047788015215h 30m 8s 999ms 999µs 999ns ago` to \
|
||||
/// nearest hour in increments of 1 resulted in \
|
||||
/// -9223372036854777600 seconds, which does not fit into an i64 \
|
||||
/// and thus overflows `SignedDuration`",
|
||||
/// "rounding signed duration to nearest hour \
|
||||
/// resulted in a value outside the supported \
|
||||
/// range of a `jiff::SignedDuration`",
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
|
|
@ -2176,9 +2132,9 @@ impl SignedDuration {
|
|||
///
|
||||
/// assert_eq!(
|
||||
/// SignedDuration::ZERO.round(Unit::Day).unwrap_err().to_string(),
|
||||
/// "rounding `SignedDuration` failed \
|
||||
/// because a calendar unit of days was provided \
|
||||
/// (to round by calendar units, you must use a `Span`)",
|
||||
/// "rounding `jiff::SignedDuration` failed \
|
||||
/// because a calendar unit of 'days' was provided \
|
||||
/// (to round by calendar units, you must use a `jiff::Span`)",
|
||||
/// );
|
||||
/// ```
|
||||
#[inline]
|
||||
|
|
@ -2391,16 +2347,19 @@ impl core::fmt::Debug for SignedDuration {
|
|||
|
||||
if f.alternate() {
|
||||
if self.subsec_nanos() == 0 {
|
||||
write!(f, "{}s", self.as_secs())
|
||||
core::fmt::Display::fmt(&self.as_secs(), f)?;
|
||||
f.write_str("s")
|
||||
} else if self.as_secs() == 0 {
|
||||
write!(f, "{}ns", self.subsec_nanos())
|
||||
core::fmt::Display::fmt(&self.subsec_nanos(), f)?;
|
||||
f.write_str("ns")
|
||||
} else {
|
||||
write!(
|
||||
core::fmt::Display::fmt(&self.as_secs(), f)?;
|
||||
f.write_str("s ")?;
|
||||
core::fmt::Display::fmt(
|
||||
&self.subsec_nanos().unsigned_abs(),
|
||||
f,
|
||||
"{}s {}ns",
|
||||
self.as_secs(),
|
||||
self.subsec_nanos().unsigned_abs()
|
||||
)
|
||||
)?;
|
||||
f.write_str("ns")
|
||||
}
|
||||
} else {
|
||||
friendly::DEFAULT_SPAN_PRINTER
|
||||
|
|
@ -2414,9 +2373,8 @@ impl TryFrom<Duration> for SignedDuration {
|
|||
type Error = Error;
|
||||
|
||||
fn try_from(d: Duration) -> Result<SignedDuration, Error> {
|
||||
let secs = i64::try_from(d.as_secs()).map_err(|_| {
|
||||
err!("seconds in unsigned duration {d:?} overflowed i64")
|
||||
})?;
|
||||
let secs = i64::try_from(d.as_secs())
|
||||
.map_err(|_| Error::slim_range("unsigned duration seconds"))?;
|
||||
// Guaranteed to succeed since 0<=nanos<=999,999,999.
|
||||
let nanos = i32::try_from(d.subsec_nanos()).unwrap();
|
||||
Ok(SignedDuration::new_unchecked(secs, nanos))
|
||||
|
|
@ -2429,14 +2387,10 @@ impl TryFrom<SignedDuration> for Duration {
|
|||
fn try_from(sd: SignedDuration) -> Result<Duration, Error> {
|
||||
// This isn't needed, but improves error messages.
|
||||
if sd.is_negative() {
|
||||
return Err(err!(
|
||||
"cannot convert negative duration `{sd:?}` to \
|
||||
unsigned `std::time::Duration`",
|
||||
));
|
||||
return Err(Error::slim_range("negative duration seconds"));
|
||||
}
|
||||
let secs = u64::try_from(sd.as_secs()).map_err(|_| {
|
||||
err!("seconds in signed duration {sd:?} overflowed u64")
|
||||
})?;
|
||||
let secs = u64::try_from(sd.as_secs())
|
||||
.map_err(|_| Error::slim_range("signed duration seconds"))?;
|
||||
// Guaranteed to succeed because the above only succeeds
|
||||
// when `sd` is non-negative. And when `sd` is non-negative,
|
||||
// we are guaranteed that 0<=nanos<=999,999,999.
|
||||
|
|
@ -2771,12 +2725,9 @@ impl SignedDurationRound {
|
|||
/// Does the actual duration rounding.
|
||||
fn round(&self, dur: SignedDuration) -> Result<SignedDuration, Error> {
|
||||
if self.smallest > Unit::Hour {
|
||||
return Err(err!(
|
||||
"rounding `SignedDuration` failed because \
|
||||
a calendar unit of {plural} was provided \
|
||||
(to round by calendar units, you must use a `Span`)",
|
||||
plural = self.smallest.plural(),
|
||||
));
|
||||
return Err(Error::from(E::RoundCalendarUnit {
|
||||
unit: self.smallest,
|
||||
}));
|
||||
}
|
||||
let nanos = t::NoUnits128::new_unchecked(dur.as_nanos());
|
||||
let increment = t::NoUnits::new_unchecked(self.increment);
|
||||
|
|
@ -2789,12 +2740,7 @@ impl SignedDurationRound {
|
|||
let seconds = rounded / t::NANOS_PER_SECOND;
|
||||
let seconds =
|
||||
t::NoUnits::try_rfrom("seconds", seconds).map_err(|_| {
|
||||
err!(
|
||||
"rounding `{dur:#}` to nearest {singular} in increments \
|
||||
of {increment} resulted in {seconds} seconds, which does \
|
||||
not fit into an i64 and thus overflows `SignedDuration`",
|
||||
singular = self.smallest.singular(),
|
||||
)
|
||||
Error::from(E::RoundOverflowed { unit: self.smallest })
|
||||
})?;
|
||||
let subsec_nanos = rounded % t::NANOS_PER_SECOND;
|
||||
// OK because % 1_000_000_000 above guarantees that the result fits
|
||||
|
|
@ -2838,25 +2784,22 @@ impl From<(Unit, i64)> for SignedDurationRound {
|
|||
/// (We do the same thing for `Span`.)
|
||||
#[cfg_attr(feature = "perf-inline", inline(always))]
|
||||
fn parse_iso_or_friendly(bytes: &[u8]) -> Result<SignedDuration, Error> {
|
||||
if bytes.is_empty() {
|
||||
return Err(err!(
|
||||
"an empty string is not a valid `SignedDuration`, \
|
||||
expected either a ISO 8601 or Jiff's 'friendly' \
|
||||
format",
|
||||
let Some((&byte, tail)) = bytes.split_first() else {
|
||||
return Err(crate::Error::from(
|
||||
crate::error::fmt::Error::HybridDurationEmpty,
|
||||
));
|
||||
}
|
||||
let mut first = bytes[0];
|
||||
};
|
||||
let mut first = byte;
|
||||
// N.B. Unsigned durations don't support negative durations (of
|
||||
// course), but we still check for it here so that we can defer to
|
||||
// the dedicated parsers. They will provide their own error messages.
|
||||
if first == b'+' || first == b'-' {
|
||||
if bytes.len() == 1 {
|
||||
return Err(err!(
|
||||
"found nothing after sign `{sign}`, \
|
||||
which is not a valid `SignedDuration`, \
|
||||
expected either a ISO 8601 or Jiff's 'friendly' \
|
||||
format",
|
||||
sign = escape::Byte(first),
|
||||
let Some(&byte) = tail.first() else {
|
||||
return Err(crate::Error::from(
|
||||
crate::error::fmt::Error::HybridDurationPrefix { sign: first },
|
||||
));
|
||||
}
|
||||
first = bytes[1];
|
||||
};
|
||||
first = byte;
|
||||
}
|
||||
if first == b'P' || first == b'p' {
|
||||
temporal::DEFAULT_SPAN_PARSER.parse_duration(bytes)
|
||||
|
|
@ -3048,15 +2991,15 @@ mod tests {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
p("").unwrap_err(),
|
||||
@"an empty string is not a valid `SignedDuration`, expected either a ISO 8601 or Jiff's 'friendly' format",
|
||||
@r#"an empty string is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("+").unwrap_err(),
|
||||
@"found nothing after sign `+`, which is not a valid `SignedDuration`, expected either a ISO 8601 or Jiff's 'friendly' format",
|
||||
@r#"found nothing after sign `+`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("-").unwrap_err(),
|
||||
@"found nothing after sign `-`, which is not a valid `SignedDuration`, expected either a ISO 8601 or Jiff's 'friendly' format",
|
||||
@r#"found nothing after sign `-`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format"#,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -3093,15 +3036,15 @@ mod tests {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
p("").unwrap_err(),
|
||||
@"an empty string is not a valid `SignedDuration`, expected either a ISO 8601 or Jiff's 'friendly' format at line 1 column 2",
|
||||
@r#"an empty string is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format at line 1 column 2"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("+").unwrap_err(),
|
||||
@"found nothing after sign `+`, which is not a valid `SignedDuration`, expected either a ISO 8601 or Jiff's 'friendly' format at line 1 column 3",
|
||||
@r#"found nothing after sign `+`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format at line 1 column 3"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("-").unwrap_err(),
|
||||
@"found nothing after sign `-`, which is not a valid `SignedDuration`, expected either a ISO 8601 or Jiff's 'friendly' format at line 1 column 3",
|
||||
@r#"found nothing after sign `-`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format at line 1 column 3"#,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
355
src/span.rs
355
src/span.rs
|
|
@ -3,12 +3,11 @@ use core::{cmp::Ordering, time::Duration as UnsignedDuration};
|
|||
use crate::{
|
||||
civil::{Date, DateTime, Time},
|
||||
duration::{Duration, SDuration},
|
||||
error::{err, Error, ErrorContext},
|
||||
error::{span::Error as E, Error, ErrorContext},
|
||||
fmt::{friendly, temporal},
|
||||
tz::TimeZone,
|
||||
util::{
|
||||
borrow::DumbCow,
|
||||
escape,
|
||||
rangeint::{ri64, ri8, RFrom, RInto, TryRFrom, TryRInto},
|
||||
round::increment,
|
||||
t::{self, Constant, NoUnits, NoUnits128, Sign, C},
|
||||
|
|
@ -557,12 +556,13 @@ pub(crate) use span_eq;
|
|||
/// span.total(Unit::Hour).unwrap_err().to_string(),
|
||||
/// "using unit 'day' in a span or configuration requires that either \
|
||||
/// a relative reference time be given or \
|
||||
/// `SpanRelativeTo::days_are_24_hours()` is used to indicate \
|
||||
/// `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate \
|
||||
/// invariant 24-hour days, but neither were provided",
|
||||
/// );
|
||||
/// // Opt into invariant 24 hour days without a relative date:
|
||||
/// let marker = SpanRelativeTo::days_are_24_hours();
|
||||
/// let hours = span.total((Unit::Hour, marker))?;
|
||||
/// assert_eq!(hours, 24.0);
|
||||
/// // Or use a relative civil date, and all days are 24 hours:
|
||||
/// let date = civil::date(2020, 1, 1);
|
||||
/// let hours = span.total((Unit::Hour, date))?;
|
||||
|
|
@ -662,10 +662,11 @@ pub(crate) use span_eq;
|
|||
/// assert_eq!(
|
||||
/// Duration::try_from(span).unwrap_err().to_string(),
|
||||
/// "failed to convert span to duration without relative datetime \
|
||||
/// (must use `Span::to_duration` instead): using unit 'day' in a \
|
||||
/// span or configuration requires that either a relative reference \
|
||||
/// time be given or `SpanRelativeTo::days_are_24_hours()` is used \
|
||||
/// to indicate invariant 24-hour days, but neither were provided",
|
||||
/// (must use `jiff::Span::to_duration` instead): using unit 'day' \
|
||||
/// in a span or configuration requires that either a relative \
|
||||
/// reference time be given or \
|
||||
/// `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate \
|
||||
/// invariant 24-hour days, but neither were provided",
|
||||
/// );
|
||||
///
|
||||
/// # Ok::<(), Box<dyn std::error::Error>>(())
|
||||
|
|
@ -2325,7 +2326,8 @@ impl Span {
|
|||
/// Converts a `Span` to a [`SignedDuration`] relative to the date given.
|
||||
///
|
||||
/// In most cases, it is unlikely that you'll need to use this routine to
|
||||
/// convert a `Span` to a `SignedDuration`. Namely, by default:
|
||||
/// convert a `Span` to a `SignedDuration` and instead will be ably to
|
||||
/// use `SignedDuration::try_from(span)`. Namely, by default:
|
||||
///
|
||||
/// * [`Zoned::until`] guarantees that the biggest non-zero unit is hours.
|
||||
/// * [`Timestamp::until`] guarantees that the biggest non-zero unit is
|
||||
|
|
@ -2336,12 +2338,14 @@ impl Span {
|
|||
/// * [`Time::until`] guarantees that the biggest non-zero unit is hours.
|
||||
///
|
||||
/// In the above, only [`DateTime::until`] and [`Date::until`] return
|
||||
/// calendar units by default. In which case, one may pass
|
||||
/// [`SpanRelativeTo::days_are_24_hours`] or an actual relative date to
|
||||
/// resolve the length of a day.
|
||||
/// calendar units by default, and thus would require this routine. (In
|
||||
/// which case, one may pass [`SpanRelativeTo::days_are_24_hours`] or an
|
||||
/// actual relative date to resolve the length of a day.)
|
||||
///
|
||||
/// Of course, any of the above can be changed by asking, for example,
|
||||
/// `Zoned::until` to return units up to years.
|
||||
/// Of course, one may change the defaults. For example, if one
|
||||
/// uses `Zoned::until` with the largest unit set to `Unit::Year`
|
||||
/// and the resulting `Span` includes non-zero calendar units, then
|
||||
/// `SignedDuration::try_from` will fail because there is no relative date.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
|
|
@ -2398,24 +2402,10 @@ impl Span {
|
|||
let relspan = result
|
||||
.and_then(|r| r.into_relative_span(Unit::Second, *self))
|
||||
.with_context(|| match relative.kind {
|
||||
SpanRelativeToKind::Civil(dt) => {
|
||||
err!(
|
||||
"could not compute normalized relative span \
|
||||
from datetime {dt} and span {self}",
|
||||
)
|
||||
}
|
||||
SpanRelativeToKind::Zoned(ref zdt) => {
|
||||
err!(
|
||||
"could not compute normalized relative span \
|
||||
from datetime {zdt} and span {self}",
|
||||
)
|
||||
}
|
||||
SpanRelativeToKind::Civil(_) => E::ToDurationCivil,
|
||||
SpanRelativeToKind::Zoned(_) => E::ToDurationZoned,
|
||||
SpanRelativeToKind::DaysAre24Hours => {
|
||||
err!(
|
||||
"could not compute normalized relative span \
|
||||
from {self} when all days are assumed to be \
|
||||
24 hours",
|
||||
)
|
||||
E::ToDurationDaysAre24Hours
|
||||
}
|
||||
})?;
|
||||
debug_assert!(relspan.span.largest_unit() <= Unit::Second);
|
||||
|
|
@ -3198,13 +3188,7 @@ impl Span {
|
|||
&self,
|
||||
) -> Option<Error> {
|
||||
let non_time_unit = self.largest_calendar_unit()?;
|
||||
Some(err!(
|
||||
"operation can only be performed with units of hours \
|
||||
or smaller, but found non-zero {unit} units \
|
||||
(operations on `Timestamp`, `tz::Offset` and `civil::Time` \
|
||||
don't support calendar units in a `Span`)",
|
||||
unit = non_time_unit.singular(),
|
||||
))
|
||||
Some(Error::from(E::NotAllowedCalendarUnits { unit: non_time_unit }))
|
||||
}
|
||||
|
||||
/// Returns the largest non-zero calendar unit, or `None` if there are no
|
||||
|
|
@ -3272,7 +3256,7 @@ impl Span {
|
|||
if self.nanoseconds != C(0) {
|
||||
write!(buf, ", nanoseconds: {:?}", self.nanoseconds).unwrap();
|
||||
}
|
||||
write!(buf, " }}").unwrap();
|
||||
buf.push_str(" }}");
|
||||
buf
|
||||
}
|
||||
|
||||
|
|
@ -3305,8 +3289,8 @@ impl Span {
|
|||
match (span.is_zero(), new_is_zero) {
|
||||
(_, true) => Sign::N::<0>(),
|
||||
(true, false) => units.signum().rinto(),
|
||||
// If the old and new span are both non-zero, and we know our new
|
||||
// units are not negative, then the sign remains unchanged.
|
||||
// If the old and new span are both non-zero, and we know our
|
||||
// new units are not negative, then the sign remains unchanged.
|
||||
(false, false) => new.sign,
|
||||
}
|
||||
}
|
||||
|
|
@ -3473,10 +3457,7 @@ impl TryFrom<Span> for UnsignedDuration {
|
|||
fn try_from(sp: Span) -> Result<UnsignedDuration, Error> {
|
||||
// This isn't needed, but improves error messages.
|
||||
if sp.is_negative() {
|
||||
return Err(err!(
|
||||
"cannot convert negative span {sp:?} \
|
||||
to unsigned std::time::Duration",
|
||||
));
|
||||
return Err(Error::from(E::ConvertNegative));
|
||||
}
|
||||
SignedDuration::try_from(sp).and_then(UnsignedDuration::try_from)
|
||||
}
|
||||
|
|
@ -3543,18 +3524,15 @@ impl TryFrom<UnsignedDuration> for Span {
|
|||
|
||||
#[inline]
|
||||
fn try_from(d: UnsignedDuration) -> Result<Span, Error> {
|
||||
let seconds = i64::try_from(d.as_secs()).map_err(|_| {
|
||||
err!("seconds from {d:?} overflows a 64-bit signed integer")
|
||||
})?;
|
||||
let seconds = i64::try_from(d.as_secs())
|
||||
.map_err(|_| Error::slim_range("unsigned duration seconds"))?;
|
||||
let nanoseconds = i64::from(d.subsec_nanos());
|
||||
let milliseconds = nanoseconds / t::NANOS_PER_MILLI.value();
|
||||
let microseconds = (nanoseconds % t::NANOS_PER_MILLI.value())
|
||||
/ t::NANOS_PER_MICRO.value();
|
||||
let nanoseconds = nanoseconds % t::NANOS_PER_MICRO.value();
|
||||
|
||||
let span = Span::new().try_seconds(seconds).with_context(|| {
|
||||
err!("duration {d:?} overflows limits of a Jiff `Span`")
|
||||
})?;
|
||||
let span = Span::new().try_seconds(seconds)?;
|
||||
// These are all OK because `Duration::subsec_nanos` is guaranteed to
|
||||
// return less than 1_000_000_000 nanoseconds. And splitting that up
|
||||
// into millis, micros and nano components is guaranteed to fit into
|
||||
|
|
@ -3606,10 +3584,8 @@ impl TryFrom<Span> for SignedDuration {
|
|||
|
||||
#[inline]
|
||||
fn try_from(sp: Span) -> Result<SignedDuration, Error> {
|
||||
requires_relative_date_err(sp.largest_unit()).context(
|
||||
"failed to convert span to duration without relative datetime \
|
||||
(must use `Span::to_duration` instead)",
|
||||
)?;
|
||||
requires_relative_date_err(sp.largest_unit())
|
||||
.context(E::ConvertSpanToSignedDuration)?;
|
||||
Ok(sp.to_duration_invariant())
|
||||
}
|
||||
}
|
||||
|
|
@ -3678,9 +3654,7 @@ impl TryFrom<SignedDuration> for Span {
|
|||
/ t::NANOS_PER_MICRO.value();
|
||||
let nanoseconds = nanoseconds % t::NANOS_PER_MICRO.value();
|
||||
|
||||
let span = Span::new().try_seconds(seconds).with_context(|| {
|
||||
err!("signed duration {d:?} overflows limits of a Jiff `Span`")
|
||||
})?;
|
||||
let span = Span::new().try_seconds(seconds)?;
|
||||
// These are all OK because `|SignedDuration::subsec_nanos|` is
|
||||
// guaranteed to return less than 1_000_000_000 nanoseconds. And
|
||||
// splitting that up into millis, micros and nano components is
|
||||
|
|
@ -4454,7 +4428,7 @@ impl<'a> SpanArithmetic<'a> {
|
|||
/// span1.checked_add(span2).unwrap_err().to_string(),
|
||||
/// "using unit 'day' in a span or configuration requires that \
|
||||
/// either a relative reference time be given or \
|
||||
/// `SpanRelativeTo::days_are_24_hours()` is used to indicate \
|
||||
/// `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate \
|
||||
/// invariant 24-hour days, but neither were provided",
|
||||
/// );
|
||||
/// let sum = span1.checked_add(
|
||||
|
|
@ -4684,7 +4658,7 @@ impl<'a> SpanCompare<'a> {
|
|||
/// required. Otherwise, you get an error.
|
||||
///
|
||||
/// ```
|
||||
/// use jiff::{SpanCompare, ToSpan, Unit};
|
||||
/// use jiff::{SpanCompare, ToSpan};
|
||||
///
|
||||
/// let span1 = 2.days().hours(12);
|
||||
/// let span2 = 60.hours();
|
||||
|
|
@ -4693,7 +4667,7 @@ impl<'a> SpanCompare<'a> {
|
|||
/// span1.compare(span2).unwrap_err().to_string(),
|
||||
/// "using unit 'day' in a span or configuration requires that \
|
||||
/// either a relative reference time be given or \
|
||||
/// `SpanRelativeTo::days_are_24_hours()` is used to indicate \
|
||||
/// `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate \
|
||||
/// invariant 24-hour days, but neither were provided",
|
||||
/// );
|
||||
/// let ordering = span1.compare(
|
||||
|
|
@ -4924,7 +4898,7 @@ impl<'a> SpanTotal<'a> {
|
|||
/// span.total(Unit::Hour).unwrap_err().to_string(),
|
||||
/// "using unit 'day' in a span or configuration requires that either \
|
||||
/// a relative reference time be given or \
|
||||
/// `SpanRelativeTo::days_are_24_hours()` is used to indicate \
|
||||
/// `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate \
|
||||
/// invariant 24-hour days, but neither were provided",
|
||||
/// );
|
||||
///
|
||||
|
|
@ -5432,8 +5406,9 @@ impl<'a> SpanRound<'a> {
|
|||
/// span.round(Unit::Day).unwrap_err().to_string(),
|
||||
/// "error with `smallest` rounding option: using unit 'day' in a \
|
||||
/// span or configuration requires that either a relative reference \
|
||||
/// time be given or `SpanRelativeTo::days_are_24_hours()` is used \
|
||||
/// to indicate invariant 24-hour days, but neither were provided",
|
||||
/// time be given or `jiff::SpanRelativeTo::days_are_24_hours()` is \
|
||||
/// used to indicate invariant 24-hour days, but neither were \
|
||||
/// provided",
|
||||
/// );
|
||||
/// let rounded = span.round(
|
||||
/// SpanRound::new().smallest(Unit::Day).days_are_24_hours(),
|
||||
|
|
@ -5486,11 +5461,8 @@ impl<'a> SpanRound<'a> {
|
|||
let max = existing_largest.max(largest);
|
||||
let increment = increment::for_span(smallest, self.increment)?;
|
||||
if largest < smallest {
|
||||
return Err(err!(
|
||||
"largest unit ('{largest}') cannot be smaller than \
|
||||
smallest unit ('{smallest}')",
|
||||
largest = largest.singular(),
|
||||
smallest = smallest.singular(),
|
||||
return Err(Error::from(
|
||||
E::NotAllowedLargestSmallerThanSmallest { smallest, largest },
|
||||
));
|
||||
}
|
||||
let relative = match self.relative {
|
||||
|
|
@ -5516,14 +5488,13 @@ impl<'a> SpanRound<'a> {
|
|||
// no reasonable invariant interpretation of the span. And this
|
||||
// is only true when everything is less than 'day'.
|
||||
requires_relative_date_err(smallest)
|
||||
.context("error with `smallest` rounding option")?;
|
||||
.context(E::OptionSmallest)?;
|
||||
if let Some(largest) = self.largest {
|
||||
requires_relative_date_err(largest)
|
||||
.context("error with `largest` rounding option")?;
|
||||
.context(E::OptionLargest)?;
|
||||
}
|
||||
requires_relative_date_err(existing_largest).context(
|
||||
"error with largest unit in span to be rounded",
|
||||
)?;
|
||||
requires_relative_date_err(existing_largest)
|
||||
.context(E::OptionLargestInSpan)?;
|
||||
assert!(max <= Unit::Week);
|
||||
return Ok(round_span_invariant(
|
||||
span, smallest, largest, increment, mode,
|
||||
|
|
@ -5673,7 +5644,7 @@ impl<'a> SpanRelativeTo<'a> {
|
|||
/// span.total(Unit::Hour).unwrap_err().to_string(),
|
||||
/// "using unit 'day' in a span or configuration requires that either \
|
||||
/// a relative reference time be given or \
|
||||
/// `SpanRelativeTo::days_are_24_hours()` is used to indicate \
|
||||
/// `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate \
|
||||
/// invariant 24-hour days, but neither were provided",
|
||||
/// );
|
||||
/// // Opt into invariant 24 hour days without a relative date:
|
||||
|
|
@ -5709,7 +5680,7 @@ impl<'a> SpanRelativeTo<'a> {
|
|||
/// span.total(Unit::Hour).unwrap_err().to_string(),
|
||||
/// "using unit 'week' in a span or configuration requires that either \
|
||||
/// a relative reference time be given or \
|
||||
/// `SpanRelativeTo::days_are_24_hours()` is used to indicate \
|
||||
/// `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate \
|
||||
/// invariant 24-hour days, but neither were provided",
|
||||
/// );
|
||||
/// // Opt into invariant 24 hour days without a relative date:
|
||||
|
|
@ -5814,13 +5785,10 @@ impl<'a> SpanRelativeTo<'a> {
|
|||
}
|
||||
SpanRelativeToKind::DaysAre24Hours => {
|
||||
if matches!(unit, Unit::Year | Unit::Month) {
|
||||
return Err(err!(
|
||||
"using unit '{unit}' in span or configuration \
|
||||
requires that a relative reference time be given \
|
||||
(`SpanRelativeTo::days_are_24_hours()` was given \
|
||||
but this only permits using days and weeks \
|
||||
without a relative reference time)",
|
||||
unit = unit.singular(),
|
||||
return Err(Error::from(
|
||||
E::RequiresRelativeYearOrMonthGivenDaysAre24Hours {
|
||||
unit,
|
||||
},
|
||||
));
|
||||
}
|
||||
Ok(None)
|
||||
|
|
@ -6229,27 +6197,12 @@ impl<'a> RelativeSpanKind<'a> {
|
|||
RelativeSpanKind::Civil { ref start, ref end } => start
|
||||
.datetime
|
||||
.until((largest, end.datetime))
|
||||
.with_context(|| {
|
||||
err!(
|
||||
"failed to get span between {start} and {end} \
|
||||
with largest unit as {unit}",
|
||||
start = start.datetime,
|
||||
end = end.datetime,
|
||||
unit = largest.plural(),
|
||||
)
|
||||
})?,
|
||||
RelativeSpanKind::Zoned { ref start, ref end } => start
|
||||
.zoned
|
||||
.until((largest, &*end.zoned))
|
||||
.with_context(|| {
|
||||
err!(
|
||||
"failed to get span between {start} and {end} \
|
||||
with largest unit as {unit}",
|
||||
start = start.zoned,
|
||||
end = end.zoned,
|
||||
unit = largest.plural(),
|
||||
)
|
||||
})?,
|
||||
.context(E::FailedSpanBetweenDateTimes { unit: largest })?,
|
||||
RelativeSpanKind::Zoned { ref start, ref end } => {
|
||||
start.zoned.until((largest, &*end.zoned)).context(
|
||||
E::FailedSpanBetweenZonedDateTimes { unit: largest },
|
||||
)?
|
||||
}
|
||||
};
|
||||
Ok(RelativeSpan { span, kind: self })
|
||||
}
|
||||
|
|
@ -6290,9 +6243,7 @@ impl RelativeCivil {
|
|||
fn new(datetime: DateTime) -> Result<RelativeCivil, Error> {
|
||||
let timestamp = datetime
|
||||
.to_zoned(TimeZone::UTC)
|
||||
.with_context(|| {
|
||||
err!("failed to convert {datetime} to timestamp")
|
||||
})?
|
||||
.context(E::ConvertDateTimeToTimestamp)?
|
||||
.timestamp();
|
||||
Ok(RelativeCivil { datetime, timestamp })
|
||||
}
|
||||
|
|
@ -6308,14 +6259,10 @@ impl RelativeCivil {
|
|||
/// converted to a timestamp in UTC. This only occurs near the minimum and
|
||||
/// maximum datetime values.
|
||||
fn checked_add(&self, span: Span) -> Result<RelativeCivil, Error> {
|
||||
let datetime = self.datetime.checked_add(span).with_context(|| {
|
||||
err!("failed to add {span} to {dt}", dt = self.datetime)
|
||||
})?;
|
||||
let datetime = self.datetime.checked_add(span)?;
|
||||
let timestamp = datetime
|
||||
.to_zoned(TimeZone::UTC)
|
||||
.with_context(|| {
|
||||
err!("failed to convert {datetime} to timestamp")
|
||||
})?
|
||||
.context(E::ConvertDateTimeToTimestamp)?
|
||||
.timestamp();
|
||||
Ok(RelativeCivil { datetime, timestamp })
|
||||
}
|
||||
|
|
@ -6335,15 +6282,10 @@ impl RelativeCivil {
|
|||
&self,
|
||||
duration: SignedDuration,
|
||||
) -> Result<RelativeCivil, Error> {
|
||||
let datetime =
|
||||
self.datetime.checked_add(duration).with_context(|| {
|
||||
err!("failed to add {duration:?} to {dt}", dt = self.datetime)
|
||||
})?;
|
||||
let datetime = self.datetime.checked_add(duration)?;
|
||||
let timestamp = datetime
|
||||
.to_zoned(TimeZone::UTC)
|
||||
.with_context(|| {
|
||||
err!("failed to convert {datetime} to timestamp")
|
||||
})?
|
||||
.context(E::ConvertDateTimeToTimestamp)?
|
||||
.timestamp();
|
||||
Ok(RelativeCivil { datetime, timestamp })
|
||||
}
|
||||
|
|
@ -6361,15 +6303,9 @@ impl RelativeCivil {
|
|||
largest: Unit,
|
||||
other: &RelativeCivil,
|
||||
) -> Result<Span, Error> {
|
||||
self.datetime.until((largest, other.datetime)).with_context(|| {
|
||||
err!(
|
||||
"failed to get span between {dt1} and {dt2} \
|
||||
with largest unit as {unit}",
|
||||
unit = largest.plural(),
|
||||
dt1 = self.datetime,
|
||||
dt2 = other.datetime,
|
||||
)
|
||||
})
|
||||
self.datetime
|
||||
.until((largest, other.datetime))
|
||||
.context(E::FailedSpanBetweenDateTimes { unit: largest })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -6390,9 +6326,7 @@ impl<'a> RelativeZoned<'a> {
|
|||
&self,
|
||||
span: Span,
|
||||
) -> Result<RelativeZoned<'static>, Error> {
|
||||
let zoned = self.zoned.checked_add(span).with_context(|| {
|
||||
err!("failed to add {span} to {zoned}", zoned = self.zoned)
|
||||
})?;
|
||||
let zoned = self.zoned.checked_add(span)?;
|
||||
Ok(RelativeZoned { zoned: DumbCow::Owned(zoned) })
|
||||
}
|
||||
|
||||
|
|
@ -6406,9 +6340,7 @@ impl<'a> RelativeZoned<'a> {
|
|||
&self,
|
||||
duration: SignedDuration,
|
||||
) -> Result<RelativeZoned<'static>, Error> {
|
||||
let zoned = self.zoned.checked_add(duration).with_context(|| {
|
||||
err!("failed to add {duration:?} to {zoned}", zoned = self.zoned)
|
||||
})?;
|
||||
let zoned = self.zoned.checked_add(duration)?;
|
||||
Ok(RelativeZoned { zoned: DumbCow::Owned(zoned) })
|
||||
}
|
||||
|
||||
|
|
@ -6425,15 +6357,9 @@ impl<'a> RelativeZoned<'a> {
|
|||
largest: Unit,
|
||||
other: &RelativeZoned<'a>,
|
||||
) -> Result<Span, Error> {
|
||||
self.zoned.until((largest, &*other.zoned)).with_context(|| {
|
||||
err!(
|
||||
"failed to get span between {zdt1} and {zdt2} \
|
||||
with largest unit as {unit}",
|
||||
unit = largest.plural(),
|
||||
zdt1 = self.zoned,
|
||||
zdt2 = other.zoned,
|
||||
)
|
||||
})
|
||||
self.zoned
|
||||
.until((largest, &*other.zoned))
|
||||
.context(E::FailedSpanBetweenZonedDateTimes { unit: largest })
|
||||
}
|
||||
|
||||
/// Returns the borrowed version of self; useful when you need to convert
|
||||
|
|
@ -6512,13 +6438,7 @@ impl Nudge {
|
|||
increment,
|
||||
);
|
||||
let span = Span::from_invariant_nanoseconds(largest, rounded_nanos)
|
||||
.with_context(|| {
|
||||
err!(
|
||||
"failed to convert rounded nanoseconds {rounded_nanos} \
|
||||
to span for largest unit as {unit}",
|
||||
unit = largest.plural(),
|
||||
)
|
||||
})?
|
||||
.context(E::ConvertNanoseconds { unit: largest })?
|
||||
.years_ranged(balanced.get_years_ranged())
|
||||
.months_ranged(balanced.get_months_ranged())
|
||||
.weeks_ranged(balanced.get_weeks_ranged());
|
||||
|
|
@ -6551,13 +6471,7 @@ impl Nudge {
|
|||
* balanced.get_units_ranged(smallest).div_ceil(increment);
|
||||
let span = balanced
|
||||
.without_lower(smallest)
|
||||
.try_units_ranged(smallest, truncated.rinto())
|
||||
.with_context(|| {
|
||||
err!(
|
||||
"failed to set {unit} to {truncated} on span {balanced}",
|
||||
unit = smallest.singular()
|
||||
)
|
||||
})?;
|
||||
.try_units_ranged(smallest, truncated.rinto())?;
|
||||
let (relative0, relative1) = clamp_relative_span(
|
||||
relative_start,
|
||||
span,
|
||||
|
|
@ -6578,14 +6492,7 @@ impl Nudge {
|
|||
let grew_big_unit =
|
||||
((rounded.get() as f64) - exact).signum() == (sign.get() as f64);
|
||||
|
||||
let span = span
|
||||
.try_units_ranged(smallest, rounded.rinto())
|
||||
.with_context(|| {
|
||||
err!(
|
||||
"failed to set {unit} to {truncated} on span {span}",
|
||||
unit = smallest.singular()
|
||||
)
|
||||
})?;
|
||||
let span = span.try_units_ranged(smallest, rounded.rinto())?;
|
||||
let rounded_relative_end =
|
||||
if grew_big_unit { relative1 } else { relative0 };
|
||||
Ok(Nudge { span, rounded_relative_end, grew_big_unit })
|
||||
|
|
@ -6631,13 +6538,7 @@ impl Nudge {
|
|||
|
||||
let span =
|
||||
Span::from_invariant_nanoseconds(Unit::Hour, rounded_time_nanos)
|
||||
.with_context(|| {
|
||||
err!(
|
||||
"failed to convert rounded nanoseconds \
|
||||
{rounded_time_nanos} to span for largest unit as {unit}",
|
||||
unit = Unit::Hour.plural(),
|
||||
)
|
||||
})?
|
||||
.context(E::ConvertNanoseconds { unit: Unit::Hour })?
|
||||
.years_ranged(balanced.get_years_ranged())
|
||||
.months_ranged(balanced.get_months_ranged())
|
||||
.weeks_ranged(balanced.get_weeks_ranged())
|
||||
|
|
@ -6682,23 +6583,8 @@ impl Nudge {
|
|||
let span_start = balanced.without_lower(unit);
|
||||
let new_units = span_start
|
||||
.get_units_ranged(unit)
|
||||
.try_checked_add("bubble-units", sign)
|
||||
.with_context(|| {
|
||||
err!(
|
||||
"failed to add sign {sign} to {unit} value {value}",
|
||||
unit = unit.plural(),
|
||||
value = span_start.get_units_ranged(unit),
|
||||
)
|
||||
})?;
|
||||
let span_end = span_start
|
||||
.try_units_ranged(unit, new_units)
|
||||
.with_context(|| {
|
||||
err!(
|
||||
"failed to set {unit} to value \
|
||||
{new_units} on span {span_start}",
|
||||
unit = unit.plural(),
|
||||
)
|
||||
})?;
|
||||
.try_checked_add("bubble-units", sign)?;
|
||||
let span_end = span_start.try_units_ranged(unit, new_units)?;
|
||||
let threshold = match relative.kind {
|
||||
RelativeSpanKind::Civil { ref start, .. } => {
|
||||
start.checked_add(span_end)?.timestamp
|
||||
|
|
@ -6742,13 +6628,8 @@ fn round_span_invariant(
|
|||
let nanos = span.to_invariant_nanoseconds();
|
||||
let rounded =
|
||||
mode.round_by_unit_in_nanoseconds(nanos, smallest, increment);
|
||||
Span::from_invariant_nanoseconds(largest, rounded).with_context(|| {
|
||||
err!(
|
||||
"failed to convert rounded nanoseconds {rounded} \
|
||||
to span for largest unit as {unit}",
|
||||
unit = largest.plural(),
|
||||
)
|
||||
})
|
||||
Span::from_invariant_nanoseconds(largest, rounded)
|
||||
.context(E::ConvertNanoseconds { unit: largest })
|
||||
}
|
||||
|
||||
/// Returns the nanosecond timestamps of `relative + span` and `relative +
|
||||
|
|
@ -6772,24 +6653,9 @@ fn clamp_relative_span(
|
|||
unit: Unit,
|
||||
amount: NoUnits,
|
||||
) -> Result<(NoUnits128, NoUnits128), Error> {
|
||||
let amount = span
|
||||
.get_units_ranged(unit)
|
||||
.try_checked_add("clamp-units", amount)
|
||||
.with_context(|| {
|
||||
err!(
|
||||
"failed to add {amount} to {unit} \
|
||||
value {value} on span {span}",
|
||||
unit = unit.plural(),
|
||||
value = span.get_units_ranged(unit),
|
||||
)
|
||||
})?;
|
||||
let span_amount =
|
||||
span.try_units_ranged(unit, amount).with_context(|| {
|
||||
err!(
|
||||
"failed to set {unit} unit to {amount} on span {span}",
|
||||
unit = unit.plural(),
|
||||
)
|
||||
})?;
|
||||
let amount =
|
||||
span.get_units_ranged(unit).try_checked_add("clamp-units", amount)?;
|
||||
let span_amount = span.try_units_ranged(unit, amount)?;
|
||||
let relative0 = relative.checked_add(span)?.to_nanosecond();
|
||||
let relative1 = relative.checked_add(span_amount)?.to_nanosecond();
|
||||
Ok((relative0, relative1))
|
||||
|
|
@ -6811,25 +6677,22 @@ fn clamp_relative_span(
|
|||
/// (We do the same thing for `SignedDuration`.)
|
||||
#[cfg_attr(feature = "perf-inline", inline(always))]
|
||||
fn parse_iso_or_friendly(bytes: &[u8]) -> Result<Span, Error> {
|
||||
if bytes.is_empty() {
|
||||
return Err(err!(
|
||||
"an empty string is not a valid `Span`, \
|
||||
expected either a ISO 8601 or Jiff's 'friendly' \
|
||||
format",
|
||||
let Some((&byte, tail)) = bytes.split_first() else {
|
||||
return Err(crate::Error::from(
|
||||
crate::error::fmt::Error::HybridDurationEmpty,
|
||||
));
|
||||
}
|
||||
let mut first = bytes[0];
|
||||
};
|
||||
let mut first = byte;
|
||||
// N.B. Unsigned durations don't support negative durations (of
|
||||
// course), but we still check for it here so that we can defer to
|
||||
// the dedicated parsers. They will provide their own error messages.
|
||||
if first == b'+' || first == b'-' {
|
||||
if bytes.len() == 1 {
|
||||
return Err(err!(
|
||||
"found nothing after sign `{sign}`, \
|
||||
which is not a valid `Span`, \
|
||||
expected either a ISO 8601 or Jiff's 'friendly' \
|
||||
format",
|
||||
sign = escape::Byte(first),
|
||||
let Some(&byte) = tail.first() else {
|
||||
return Err(crate::Error::from(
|
||||
crate::error::fmt::Error::HybridDurationPrefix { sign: first },
|
||||
));
|
||||
}
|
||||
first = bytes[1];
|
||||
};
|
||||
first = byte;
|
||||
}
|
||||
if first == b'P' || first == b'p' {
|
||||
temporal::DEFAULT_SPAN_PARSER.parse_span(bytes)
|
||||
|
|
@ -6840,23 +6703,11 @@ fn parse_iso_or_friendly(bytes: &[u8]) -> Result<Span, Error> {
|
|||
|
||||
fn requires_relative_date_err(unit: Unit) -> Result<(), Error> {
|
||||
if unit.is_variable() {
|
||||
return Err(if matches!(unit, Unit::Week | Unit::Day) {
|
||||
err!(
|
||||
"using unit '{unit}' in a span or configuration \
|
||||
requires that either a relative reference time be given \
|
||||
or `SpanRelativeTo::days_are_24_hours()` is used to \
|
||||
indicate invariant 24-hour days, \
|
||||
but neither were provided",
|
||||
unit = unit.singular(),
|
||||
)
|
||||
return Err(Error::from(if matches!(unit, Unit::Week | Unit::Day) {
|
||||
E::RequiresRelativeWeekOrDay { unit }
|
||||
} else {
|
||||
err!(
|
||||
"using unit '{unit}' in a span or configuration \
|
||||
requires that a relative reference time be given, \
|
||||
but none was provided",
|
||||
unit = unit.singular(),
|
||||
)
|
||||
});
|
||||
E::RequiresRelativeYearOrMonth { unit }
|
||||
}));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -7390,15 +7241,15 @@ mod tests {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
p("").unwrap_err(),
|
||||
@"an empty string is not a valid `Span`, expected either a ISO 8601 or Jiff's 'friendly' format",
|
||||
@r#"an empty string is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("+").unwrap_err(),
|
||||
@"found nothing after sign `+`, which is not a valid `Span`, expected either a ISO 8601 or Jiff's 'friendly' format",
|
||||
@r#"found nothing after sign `+`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("-").unwrap_err(),
|
||||
@"found nothing after sign `-`, which is not a valid `Span`, expected either a ISO 8601 or Jiff's 'friendly' format",
|
||||
@r#"found nothing after sign `-`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format"#,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -7435,15 +7286,15 @@ mod tests {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
p("").unwrap_err(),
|
||||
@"an empty string is not a valid `Span`, expected either a ISO 8601 or Jiff's 'friendly' format at line 1 column 2",
|
||||
@r#"an empty string is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format at line 1 column 2"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("+").unwrap_err(),
|
||||
@"found nothing after sign `+`, which is not a valid `Span`, expected either a ISO 8601 or Jiff's 'friendly' format at line 1 column 3",
|
||||
@r#"found nothing after sign `+`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format at line 1 column 3"#,
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
p("-").unwrap_err(),
|
||||
@"found nothing after sign `-`, which is not a valid `Span`, expected either a ISO 8601 or Jiff's 'friendly' format at line 1 column 3",
|
||||
@r#"found nothing after sign `-`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format at line 1 column 3"#,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use core::time::Duration as UnsignedDuration;
|
|||
|
||||
use crate::{
|
||||
duration::{Duration, SDuration},
|
||||
error::{err, Error, ErrorContext},
|
||||
error::{timestamp::Error as E, Error, ErrorContext},
|
||||
fmt::{
|
||||
self,
|
||||
temporal::{self, DEFAULT_DATETIME_PARSER},
|
||||
|
|
@ -279,8 +279,7 @@ use crate::{
|
|||
/// let result = "2024-06-30 08:30[America/New_York]".parse::<Timestamp>();
|
||||
/// assert_eq!(
|
||||
/// result.unwrap_err().to_string(),
|
||||
/// "failed to find offset component in \
|
||||
/// \"2024-06-30 08:30[America/New_York]\", \
|
||||
/// "failed to find offset component, \
|
||||
/// which is required for parsing a timestamp",
|
||||
/// );
|
||||
/// ```
|
||||
|
|
@ -1520,9 +1519,7 @@ impl Timestamp {
|
|||
let time_seconds = self.as_second_ranged();
|
||||
let sum = time_seconds
|
||||
.try_checked_add("span", span_seconds)
|
||||
.with_context(|| {
|
||||
err!("adding {span} to {self} overflowed")
|
||||
})?;
|
||||
.context(E::OverflowAddSpan)?;
|
||||
return Ok(Timestamp::from_second_ranged(sum));
|
||||
}
|
||||
}
|
||||
|
|
@ -1530,7 +1527,7 @@ impl Timestamp {
|
|||
let span_nanos = span.to_invariant_nanoseconds();
|
||||
let sum = time_nanos
|
||||
.try_checked_add("span", span_nanos)
|
||||
.with_context(|| err!("adding {span} to {self} overflowed"))?;
|
||||
.context(E::OverflowAddSpan)?;
|
||||
Ok(Timestamp::from_nanosecond_ranged(sum))
|
||||
}
|
||||
|
||||
|
|
@ -1540,9 +1537,7 @@ impl Timestamp {
|
|||
duration: SignedDuration,
|
||||
) -> Result<Timestamp, Error> {
|
||||
let start = self.as_duration();
|
||||
let end = start.checked_add(duration).ok_or_else(|| {
|
||||
err!("overflow when adding {duration:?} to {self}")
|
||||
})?;
|
||||
let end = start.checked_add(duration).ok_or(E::OverflowAddDuration)?;
|
||||
Timestamp::from_duration(end)
|
||||
}
|
||||
|
||||
|
|
@ -1648,9 +1643,7 @@ impl Timestamp {
|
|||
duration: A,
|
||||
) -> Result<Timestamp, Error> {
|
||||
let duration: TimestampArithmetic = duration.into();
|
||||
duration.saturating_add(self).context(
|
||||
"saturating `Timestamp` arithmetic requires only time units",
|
||||
)
|
||||
duration.saturating_add(self).context(E::RequiresSaturatingTimeUnits)
|
||||
}
|
||||
|
||||
/// This routine is identical to [`Timestamp::saturating_add`] with the
|
||||
|
|
@ -3429,11 +3422,10 @@ impl TimestampDifference {
|
|||
.get_largest()
|
||||
.unwrap_or_else(|| self.round.get_smallest().max(Unit::Second));
|
||||
if largest >= Unit::Day {
|
||||
return Err(err!(
|
||||
"unit {largest} is not supported when computing the \
|
||||
difference between timestamps (must use units smaller \
|
||||
than 'day')",
|
||||
largest = largest.singular(),
|
||||
return Err(Error::from(
|
||||
crate::error::util::RoundingIncrementError::Unsupported {
|
||||
unit: largest,
|
||||
},
|
||||
));
|
||||
}
|
||||
let nano1 = t1.as_nanosecond_ranged().without_bounds();
|
||||
|
|
@ -3856,7 +3848,7 @@ mod tests {
|
|||
fn timestamp_saturating_add() {
|
||||
insta::assert_snapshot!(
|
||||
Timestamp::MIN.saturating_add(Span::new().days(1)).unwrap_err(),
|
||||
@"saturating `Timestamp` arithmetic requires only time units: operation can only be performed with units of hours or smaller, but found non-zero day units (operations on `Timestamp`, `tz::Offset` and `civil::Time` don't support calendar units in a `Span`)",
|
||||
@"saturating timestamp arithmetic requires only time units: operation can only be performed with units of hours or smaller, but found non-zero 'day' units (operations on `jiff::Timestamp`, `jiff::tz::Offset` and `jiff::civil::Time` don't support calendar units in a `jiff::Span`)",
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -3864,7 +3856,7 @@ mod tests {
|
|||
fn timestamp_saturating_sub() {
|
||||
insta::assert_snapshot!(
|
||||
Timestamp::MAX.saturating_sub(Span::new().days(1)).unwrap_err(),
|
||||
@"saturating `Timestamp` arithmetic requires only time units: operation can only be performed with units of hours or smaller, but found non-zero day units (operations on `Timestamp`, `tz::Offset` and `civil::Time` don't support calendar units in a `Span`)",
|
||||
@"saturating timestamp arithmetic requires only time units: operation can only be performed with units of hours or smaller, but found non-zero 'day' units (operations on `jiff::Timestamp`, `jiff::tz::Offset` and `jiff::civil::Time` don't support calendar units in a `jiff::Span`)",
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
civil::DateTime,
|
||||
error::{err, Error, ErrorContext},
|
||||
error::{tz::ambiguous::Error as E, Error, ErrorContext},
|
||||
shared::util::itime::IAmbiguousOffset,
|
||||
tz::{Offset, TimeZone},
|
||||
Timestamp, Zoned,
|
||||
|
|
@ -655,18 +655,10 @@ impl AmbiguousTimestamp {
|
|||
let offset = match self.offset() {
|
||||
AmbiguousOffset::Unambiguous { offset } => offset,
|
||||
AmbiguousOffset::Gap { before, after } => {
|
||||
return Err(err!(
|
||||
"the datetime {dt} is ambiguous since it falls into \
|
||||
a gap between offsets {before} and {after}",
|
||||
dt = self.dt,
|
||||
));
|
||||
return Err(Error::from(E::BecauseGap { before, after }));
|
||||
}
|
||||
AmbiguousOffset::Fold { before, after } => {
|
||||
return Err(err!(
|
||||
"the datetime {dt} is ambiguous since it falls into \
|
||||
a fold between offsets {before} and {after}",
|
||||
dt = self.dt,
|
||||
));
|
||||
return Err(Error::from(E::BecauseFold { before, after }));
|
||||
}
|
||||
};
|
||||
offset.to_timestamp(self.dt)
|
||||
|
|
@ -1039,13 +1031,10 @@ impl AmbiguousZoned {
|
|||
/// ```
|
||||
#[inline]
|
||||
pub fn compatible(self) -> Result<Zoned, Error> {
|
||||
let ts = self.ts.compatible().with_context(|| {
|
||||
err!(
|
||||
"error converting datetime {dt} to instant in time zone {tz}",
|
||||
dt = self.datetime(),
|
||||
tz = self.time_zone().diagnostic_name(),
|
||||
)
|
||||
})?;
|
||||
let ts = self
|
||||
.ts
|
||||
.compatible()
|
||||
.with_context(|| E::InTimeZone { tz: self.time_zone().clone() })?;
|
||||
Ok(ts.to_zoned(self.tz))
|
||||
}
|
||||
|
||||
|
|
@ -1101,13 +1090,10 @@ impl AmbiguousZoned {
|
|||
/// ```
|
||||
#[inline]
|
||||
pub fn earlier(self) -> Result<Zoned, Error> {
|
||||
let ts = self.ts.earlier().with_context(|| {
|
||||
err!(
|
||||
"error converting datetime {dt} to instant in time zone {tz}",
|
||||
dt = self.datetime(),
|
||||
tz = self.time_zone().diagnostic_name(),
|
||||
)
|
||||
})?;
|
||||
let ts = self
|
||||
.ts
|
||||
.earlier()
|
||||
.with_context(|| E::InTimeZone { tz: self.time_zone().clone() })?;
|
||||
Ok(ts.to_zoned(self.tz))
|
||||
}
|
||||
|
||||
|
|
@ -1163,13 +1149,10 @@ impl AmbiguousZoned {
|
|||
/// ```
|
||||
#[inline]
|
||||
pub fn later(self) -> Result<Zoned, Error> {
|
||||
let ts = self.ts.later().with_context(|| {
|
||||
err!(
|
||||
"error converting datetime {dt} to instant in time zone {tz}",
|
||||
dt = self.datetime(),
|
||||
tz = self.time_zone().diagnostic_name(),
|
||||
)
|
||||
})?;
|
||||
let ts = self
|
||||
.ts
|
||||
.later()
|
||||
.with_context(|| E::InTimeZone { tz: self.time_zone().clone() })?;
|
||||
Ok(ts.to_zoned(self.tz))
|
||||
}
|
||||
|
||||
|
|
@ -1220,13 +1203,10 @@ impl AmbiguousZoned {
|
|||
/// ```
|
||||
#[inline]
|
||||
pub fn unambiguous(self) -> Result<Zoned, Error> {
|
||||
let ts = self.ts.unambiguous().with_context(|| {
|
||||
err!(
|
||||
"error converting datetime {dt} to instant in time zone {tz}",
|
||||
dt = self.datetime(),
|
||||
tz = self.time_zone().diagnostic_name(),
|
||||
)
|
||||
})?;
|
||||
let ts = self
|
||||
.ts
|
||||
.unambiguous()
|
||||
.with_context(|| E::InTimeZone { tz: self.time_zone().clone() })?;
|
||||
Ok(ts.to_zoned(self.tz))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@ use alloc::{
|
|||
};
|
||||
|
||||
use crate::{
|
||||
error::{err, Error, ErrorContext},
|
||||
error::{
|
||||
tz::concatenated::{Error as E, ALLOC_LIMIT},
|
||||
Error, ErrorContext,
|
||||
},
|
||||
tz::TimeZone,
|
||||
util::{array_str::ArrayStr, escape, utf8},
|
||||
};
|
||||
|
|
@ -71,7 +74,7 @@ impl<R: Read> ConcatenatedTzif<R> {
|
|||
alloc(scratch1, self.header.index_len())?;
|
||||
self.rdr
|
||||
.read_exact_at(scratch1, self.header.index_offset)
|
||||
.context("failed to read index block")?;
|
||||
.context(E::FailedReadIndex)?;
|
||||
|
||||
let mut index = &**scratch1;
|
||||
while !index.is_empty() {
|
||||
|
|
@ -94,7 +97,7 @@ impl<R: Read> ConcatenatedTzif<R> {
|
|||
let start = self.header.data_offset.saturating_add(entry.start());
|
||||
self.rdr
|
||||
.read_exact_at(scratch2, start)
|
||||
.context("failed to read TZif data block")?;
|
||||
.context(E::FailedReadData)?;
|
||||
return TimeZone::tzif(name, scratch2).map(Some);
|
||||
}
|
||||
Ok(None)
|
||||
|
|
@ -114,7 +117,7 @@ impl<R: Read> ConcatenatedTzif<R> {
|
|||
alloc(scratch, self.header.index_len())?;
|
||||
self.rdr
|
||||
.read_exact_at(scratch, self.header.index_offset)
|
||||
.context("failed to read index block")?;
|
||||
.context(E::FailedReadIndex)?;
|
||||
|
||||
let names_len = self.header.index_len() / IndexEntry::LEN;
|
||||
// Why are we careless with this alloc? Well, its size is proportional
|
||||
|
|
@ -154,31 +157,17 @@ impl Header {
|
|||
fn read<R: Read + ?Sized>(rdr: &R) -> Result<Header, Error> {
|
||||
// 12 bytes plus 3 4-byte big endian integers.
|
||||
let mut buf = [0; 12 + 3 * 4];
|
||||
rdr.read_exact_at(&mut buf, 0)
|
||||
.context("failed to read concatenated TZif header")?;
|
||||
rdr.read_exact_at(&mut buf, 0).context(E::FailedReadHeader)?;
|
||||
if &buf[..6] != b"tzdata" {
|
||||
return Err(err!(
|
||||
"expected first 6 bytes of concatenated TZif header \
|
||||
to be `tzdata`, but found `{found}`",
|
||||
found = escape::Bytes(&buf[..6]),
|
||||
));
|
||||
return Err(Error::from(E::ExpectedFirstSixBytes));
|
||||
}
|
||||
if buf[11] != 0 {
|
||||
return Err(err!(
|
||||
"expected last byte of concatenated TZif header \
|
||||
to be NUL, but found `{found}`",
|
||||
found = escape::Bytes(&buf[..12]),
|
||||
));
|
||||
return Err(Error::from(E::ExpectedLastByte));
|
||||
}
|
||||
|
||||
let version = {
|
||||
let version = core::str::from_utf8(&buf[6..11]).map_err(|_| {
|
||||
err!(
|
||||
"expected version in concatenated TZif header to \
|
||||
be valid UTF-8, but found `{found}`",
|
||||
found = escape::Bytes(&buf[6..11]),
|
||||
)
|
||||
})?;
|
||||
let version = core::str::from_utf8(&buf[6..11])
|
||||
.map_err(|_| E::ExpectedVersion)?;
|
||||
// OK because `version` is exactly 5 bytes, by construction.
|
||||
ArrayStr::new(version).unwrap()
|
||||
};
|
||||
|
|
@ -187,19 +176,12 @@ impl Header {
|
|||
// OK because the sub-slice is sized to exactly 4 bytes.
|
||||
let data_offset = u64::from(read_be32(&buf[16..20]));
|
||||
if index_offset > data_offset {
|
||||
return Err(err!(
|
||||
"invalid index ({index_offset}) and data ({data_offset}) \
|
||||
offsets, expected index offset to be less than or equal \
|
||||
to data offset",
|
||||
));
|
||||
return Err(Error::from(E::InvalidIndexDataOffsets));
|
||||
}
|
||||
// we don't read 20..24 since we don't care about zonetab (yet)
|
||||
let header = Header { version, index_offset, data_offset };
|
||||
if header.index_len() % IndexEntry::LEN != 0 {
|
||||
return Err(err!(
|
||||
"length of index block is not a multiple {len}",
|
||||
len = IndexEntry::LEN,
|
||||
));
|
||||
return Err(Error::from(E::InvalidLengthIndexBlock));
|
||||
}
|
||||
Ok(header)
|
||||
}
|
||||
|
|
@ -268,12 +250,8 @@ impl<'a> IndexEntry<'a> {
|
|||
///
|
||||
/// This returns an error if the name isn't valid UTF-8.
|
||||
fn name(&self) -> Result<&str, Error> {
|
||||
core::str::from_utf8(self.name_bytes()).map_err(|_| {
|
||||
err!(
|
||||
"IANA time zone identifier `{name}` is not valid UTF-8",
|
||||
name = escape::Bytes(self.name_bytes()),
|
||||
)
|
||||
})
|
||||
core::str::from_utf8(self.name_bytes())
|
||||
.map_err(|_| Error::from(E::ExpectedIanaName))
|
||||
}
|
||||
|
||||
/// Returns the IANA time zone identifier as a byte slice.
|
||||
|
|
@ -350,20 +328,12 @@ fn read_be32(bytes: &[u8]) -> u32 {
|
|||
impl Read for [u8] {
|
||||
fn read_exact_at(&self, buf: &mut [u8], offset: u64) -> Result<(), Error> {
|
||||
let offset = usize::try_from(offset)
|
||||
.map_err(|_| err!("offset `{offset}` overflowed `usize`"))?;
|
||||
.map_err(|_| E::InvalidOffsetOverflowSlice)?;
|
||||
let Some(slice) = self.get(offset..) else {
|
||||
return Err(err!(
|
||||
"given offset `{offset}` is not valid \
|
||||
(only {len} bytes are available)",
|
||||
len = self.len(),
|
||||
));
|
||||
return Err(Error::from(E::InvalidOffsetTooBig));
|
||||
};
|
||||
if buf.len() > slice.len() {
|
||||
return Err(err!(
|
||||
"unexpected EOF, expected {len} bytes but only have {have}",
|
||||
len = buf.len(),
|
||||
have = slice.len()
|
||||
));
|
||||
return Err(Error::from(E::ExpectedMoreData));
|
||||
}
|
||||
buf.copy_from_slice(&slice[..buf.len()]);
|
||||
Ok(())
|
||||
|
|
@ -395,9 +365,7 @@ impl Read for std::fs::File {
|
|||
offset = u64::try_from(n)
|
||||
.ok()
|
||||
.and_then(|n| n.checked_add(offset))
|
||||
.ok_or_else(|| {
|
||||
err!("offset overflow when reading from `File`")
|
||||
})?;
|
||||
.ok_or(E::InvalidOffsetOverflowFile)?;
|
||||
}
|
||||
Err(ref e) if e.kind() == io::ErrorKind::Interrupted => {}
|
||||
Err(e) => return Err(Error::io(e)),
|
||||
|
|
@ -419,9 +387,9 @@ impl Read for std::fs::File {
|
|||
fn read_exact_at(&self, buf: &mut [u8], offset: u64) -> Result<(), Error> {
|
||||
use std::io::{Read as _, Seek as _, SeekFrom};
|
||||
let mut file = self;
|
||||
file.seek(SeekFrom::Start(offset)).map_err(Error::io).with_context(
|
||||
|| err!("failed to seek to offset {offset} in `File`"),
|
||||
)?;
|
||||
file.seek(SeekFrom::Start(offset))
|
||||
.map_err(Error::io)
|
||||
.context(E::FailedSeek)?;
|
||||
file.read_exact(buf).map_err(Error::io)
|
||||
}
|
||||
}
|
||||
|
|
@ -443,31 +411,13 @@ impl Read for std::fs::File {
|
|||
/// enough in that kind of environment by far. The goal is to avoid OOM for
|
||||
/// exorbitantly large allocations through some kind of attack vector.
|
||||
fn alloc(bytes: &mut Vec<u8>, additional: usize) -> Result<(), Error> {
|
||||
// At time of writing, the biggest TZif data file is a few KB. And the
|
||||
// index block is tens of KB. So impose a limit that is a couple of orders
|
||||
// of magnitude bigger, but still overall pretty small for... some systems.
|
||||
// Anyway, I welcome improvements to this heuristic!
|
||||
const LIMIT: usize = 10 * 1 << 20;
|
||||
|
||||
if additional > LIMIT {
|
||||
return Err(err!(
|
||||
"attempted to allocate more than {LIMIT} bytes \
|
||||
while reading concatenated TZif data, which \
|
||||
exceeds a heuristic limit to prevent huge allocations \
|
||||
(please file a bug if this error is inappropriate)",
|
||||
));
|
||||
if additional > ALLOC_LIMIT {
|
||||
return Err(Error::from(E::AllocRequestOverLimit));
|
||||
}
|
||||
bytes.try_reserve_exact(additional).map_err(|_| {
|
||||
err!(
|
||||
"failed to allocation {additional} bytes \
|
||||
for reading concatenated TZif data"
|
||||
)
|
||||
})?;
|
||||
bytes.try_reserve_exact(additional).map_err(|_| E::AllocFailed)?;
|
||||
// This... can't actually happen right?
|
||||
let new_len = bytes
|
||||
.len()
|
||||
.checked_add(additional)
|
||||
.ok_or_else(|| err!("total allocation length overflowed `usize`"))?;
|
||||
let new_len =
|
||||
bytes.len().checked_add(additional).ok_or(E::AllocOverflow)?;
|
||||
bytes.resize(new_len, 0);
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,6 @@ impl Database {
|
|||
|
||||
impl core::fmt::Debug for Database {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
write!(f, "Bundled(unavailable)")
|
||||
f.write_str("Bundled(unavailable)")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ impl Database {
|
|||
Err(_err) => {
|
||||
warn!(
|
||||
"failed to parse TZif data from bundled \
|
||||
tzdb for time zone {canonical_name} \
|
||||
(this is like a bug, please report it): {_err}"
|
||||
tzdb for time zone `{canonical_name}` \
|
||||
(this is likely 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 {
|
||||
write!(f, "Bundled(available)")
|
||||
f.write_str("Bundled(available)")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,14 +10,12 @@ impl Database {
|
|||
|
||||
#[cfg(feature = "std")]
|
||||
pub(crate) fn from_path(
|
||||
path: &std::path::Path,
|
||||
_path: &std::path::Path,
|
||||
) -> Result<Database, crate::Error> {
|
||||
Err(crate::error::err!(
|
||||
"system concatenated tzdb unavailable: \
|
||||
crate feature `tzdb-concatenated` is disabled, \
|
||||
opening tzdb at {path} has therefore failed",
|
||||
path = path.display(),
|
||||
))
|
||||
Err(crate::error::Error::from(
|
||||
crate::error::CrateFeatureError::TzdbConcatenated,
|
||||
)
|
||||
.context(crate::error::tz::db::Error::DisabledConcatenated))
|
||||
}
|
||||
|
||||
pub(crate) fn none() -> Database {
|
||||
|
|
@ -41,6 +39,6 @@ impl Database {
|
|||
|
||||
impl core::fmt::Debug for Database {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
write!(f, "Concatenated(unavailable)")
|
||||
f.write_str("Concatenated(unavailable)")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ use std::{
|
|||
};
|
||||
|
||||
use crate::{
|
||||
error::{err, Error},
|
||||
error::{tz::db::Error as E, Error},
|
||||
timestamp::Timestamp,
|
||||
tz::{
|
||||
concatenated::ConcatenatedTzif, db::special_time_zone, TimeZone,
|
||||
|
|
@ -203,13 +203,13 @@ impl Database {
|
|||
|
||||
impl core::fmt::Debug for Database {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
write!(f, "Concatenated(")?;
|
||||
f.write_str("Concatenated(")?;
|
||||
if let Some(ref path) = self.path {
|
||||
write!(f, "{}", path.display())?;
|
||||
path.display().fmt(f)?;
|
||||
} else {
|
||||
write!(f, "unavailable")?;
|
||||
f.write_str("unavailable")?;
|
||||
}
|
||||
write!(f, ")")
|
||||
f.write_str(")")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -540,11 +540,7 @@ fn read_names_and_version(
|
|||
let names: Vec<Arc<str>> =
|
||||
db.available(scratch)?.into_iter().map(Arc::from).collect();
|
||||
if names.is_empty() {
|
||||
return Err(err!(
|
||||
"found no IANA time zone identifiers in \
|
||||
concatenated tzdata file at {path}",
|
||||
path = path.display(),
|
||||
));
|
||||
return Err(Error::from(E::ConcatenatedMissingIanaIdentifiers));
|
||||
}
|
||||
Ok((names, db.version()))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use crate::{
|
||||
error::{err, Error},
|
||||
error::{tz::db::Error as E, Error},
|
||||
tz::TimeZone,
|
||||
util::{sync::Arc, utf8},
|
||||
};
|
||||
|
|
@ -457,22 +457,10 @@ impl TimeZoneDatabase {
|
|||
/// # Ok::<(), Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub fn get(&self, name: &str) -> Result<TimeZone, Error> {
|
||||
let inner = self.inner.as_deref().ok_or_else(|| {
|
||||
if cfg!(feature = "std") {
|
||||
err!(
|
||||
"failed to find time zone `{name}` since there is no \
|
||||
time zone database configured",
|
||||
)
|
||||
} else {
|
||||
err!(
|
||||
"failed to find time zone `{name}`, there is no \
|
||||
global time zone database configured (and is currently \
|
||||
impossible to do so without Jiff's `std` feature \
|
||||
enabled, if you need this functionality, please file \
|
||||
an issue on Jiff's tracker with your use case)",
|
||||
)
|
||||
}
|
||||
})?;
|
||||
let inner = self
|
||||
.inner
|
||||
.as_deref()
|
||||
.ok_or_else(|| E::failed_time_zone_no_database_configured(name))?;
|
||||
match *inner {
|
||||
Kind::ZoneInfo(ref db) => {
|
||||
if let Some(tz) = db.get(name) {
|
||||
|
|
@ -493,7 +481,7 @@ impl TimeZoneDatabase {
|
|||
}
|
||||
}
|
||||
}
|
||||
Err(err!("failed to find time zone `{name}` in time zone database"))
|
||||
Err(Error::from(E::failed_time_zone(name)))
|
||||
}
|
||||
|
||||
/// Returns a list of all available time zone identifiers from this
|
||||
|
|
@ -572,16 +560,16 @@ impl TimeZoneDatabase {
|
|||
|
||||
impl core::fmt::Debug for TimeZoneDatabase {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
write!(f, "TimeZoneDatabase(")?;
|
||||
f.write_str("TimeZoneDatabase(")?;
|
||||
let Some(inner) = self.inner.as_deref() else {
|
||||
return write!(f, "unavailable)");
|
||||
return f.write_str("unavailable)");
|
||||
};
|
||||
match *inner {
|
||||
Kind::ZoneInfo(ref db) => write!(f, "{db:?}")?,
|
||||
Kind::Concatenated(ref db) => write!(f, "{db:?}")?,
|
||||
Kind::Bundled(ref db) => write!(f, "{db:?}")?,
|
||||
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)?,
|
||||
}
|
||||
write!(f, ")")
|
||||
f.write_str(")")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -687,7 +675,7 @@ impl<'d> TimeZoneName<'d> {
|
|||
|
||||
impl<'d> core::fmt::Display for TimeZoneName<'d> {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
write!(f, "{}", self.as_str())
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,14 +10,12 @@ impl Database {
|
|||
|
||||
#[cfg(feature = "std")]
|
||||
pub(crate) fn from_dir(
|
||||
dir: &std::path::Path,
|
||||
_dir: &std::path::Path,
|
||||
) -> Result<Database, crate::Error> {
|
||||
Err(crate::error::err!(
|
||||
"system tzdb unavailable: \
|
||||
crate feature `tzdb-zoneinfo` is disabled, \
|
||||
opening tzdb at {dir} has therefore failed",
|
||||
dir = dir.display(),
|
||||
))
|
||||
Err(crate::error::Error::from(
|
||||
crate::error::CrateFeatureError::TzdbZoneInfo,
|
||||
)
|
||||
.context(crate::error::tz::db::Error::DisabledZoneInfo))
|
||||
}
|
||||
|
||||
pub(crate) fn none() -> Database {
|
||||
|
|
@ -41,6 +39,6 @@ impl Database {
|
|||
|
||||
impl core::fmt::Debug for Database {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
write!(f, "ZoneInfo(unavailable)")
|
||||
f.write_str("ZoneInfo(unavailable)")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ use std::{
|
|||
};
|
||||
|
||||
use crate::{
|
||||
error::{err, Error},
|
||||
error::{tz::db::Error as E, Error},
|
||||
timestamp::Timestamp,
|
||||
tz::{
|
||||
db::special_time_zone, tzif::is_possibly_tzif, TimeZone,
|
||||
|
|
@ -204,13 +204,13 @@ impl Database {
|
|||
|
||||
impl core::fmt::Debug for Database {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
write!(f, "ZoneInfo(")?;
|
||||
f.write_str("ZoneInfo(")?;
|
||||
if let Some(ref dir) = self.dir {
|
||||
write!(f, "{}", dir.display())?;
|
||||
core::fmt::Display::fmt(&dir.display(), f)?;
|
||||
} else {
|
||||
write!(f, "unavailable")?;
|
||||
f.write_str("unavailable")?;
|
||||
}
|
||||
write!(f, ")")
|
||||
f.write_str(")")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -560,7 +560,7 @@ impl ZoneInfoName {
|
|||
fn new(base: &Path, time_zone_name: &Path) -> Result<ZoneInfoName, Error> {
|
||||
let full = base.join(time_zone_name);
|
||||
let original = parse::os_str_utf8(time_zone_name.as_os_str())
|
||||
.map_err(|err| err.path(base))?;
|
||||
.map_err(|err| Error::from(err).path(base))?;
|
||||
let lower = original.to_ascii_lowercase();
|
||||
let inner = ZoneInfoNameInner {
|
||||
full,
|
||||
|
|
@ -792,14 +792,18 @@ fn walk(start: &Path) -> Result<Vec<ZoneInfoName>, Error> {
|
|||
|
||||
let time_zone_name = match path.strip_prefix(start) {
|
||||
Ok(time_zone_name) => time_zone_name,
|
||||
Err(err) => {
|
||||
// I think this error case is actually not possible.
|
||||
// Or if it does, is a legitimate bug. Namely, `start`
|
||||
// should always be a prefix of `path`, since `path`
|
||||
// is itself derived, ultimately, from `start`.
|
||||
Err(_err) => {
|
||||
trace!(
|
||||
"failed to extract time zone name from {} \
|
||||
using {} as a base: {err}",
|
||||
using {} as a base: {_err}",
|
||||
path.display(),
|
||||
start.display(),
|
||||
);
|
||||
seterr(&path, Error::adhoc(err));
|
||||
seterr(&path, Error::from(E::ZoneInfoStripPrefix));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
|
@ -817,7 +821,7 @@ fn walk(start: &Path) -> Result<Vec<ZoneInfoName>, Error> {
|
|||
if names.is_empty() {
|
||||
let err = first_err
|
||||
.take()
|
||||
.unwrap_or_else(|| err!("{}: no TZif files", start.display()));
|
||||
.unwrap_or_else(|| Error::from(E::ZoneInfoNoTzifFiles));
|
||||
Err(err)
|
||||
} else {
|
||||
// If we found at least one valid name, then we declare success and
|
||||
|
|
|
|||
112
src/tz/offset.rs
112
src/tz/offset.rs
|
|
@ -6,7 +6,7 @@ use core::{
|
|||
use crate::{
|
||||
civil,
|
||||
duration::{Duration, SDuration},
|
||||
error::{err, Error, ErrorContext},
|
||||
error::{tz::offset::Error as E, Error, ErrorContext},
|
||||
shared::util::itime::IOffset,
|
||||
span::Span,
|
||||
timestamp::Timestamp,
|
||||
|
|
@ -526,12 +526,8 @@ impl Offset {
|
|||
.to_idatetime()
|
||||
.zip2(self.to_ioffset())
|
||||
.map(|(idt, ioff)| idt.to_timestamp(ioff));
|
||||
Timestamp::from_itimestamp(its).with_context(|| {
|
||||
err!(
|
||||
"converting {dt} with offset {offset} to timestamp overflowed",
|
||||
offset = self,
|
||||
)
|
||||
})
|
||||
Timestamp::from_itimestamp(its)
|
||||
.context(E::ConvertDateTimeToTimestamp { offset: self })
|
||||
}
|
||||
|
||||
/// Adds the given span of time to this offset.
|
||||
|
|
@ -660,21 +656,11 @@ impl Offset {
|
|||
) -> Result<Offset, Error> {
|
||||
let duration =
|
||||
t::SpanZoneOffset::try_new("duration-seconds", duration.as_secs())
|
||||
.with_context(|| {
|
||||
err!(
|
||||
"adding signed duration {duration:?} \
|
||||
to offset {self} overflowed maximum offset seconds"
|
||||
)
|
||||
})?;
|
||||
.context(E::OverflowAddSignedDuration)?;
|
||||
let offset_seconds = self.seconds_ranged();
|
||||
let seconds = offset_seconds
|
||||
.try_checked_add("offset-seconds", duration)
|
||||
.with_context(|| {
|
||||
err!(
|
||||
"adding signed duration {duration:?} \
|
||||
to offset {self} overflowed"
|
||||
)
|
||||
})?;
|
||||
.context(E::OverflowAddSignedDuration)?;
|
||||
Ok(Offset::from_seconds_ranged(seconds))
|
||||
}
|
||||
|
||||
|
|
@ -975,8 +961,7 @@ impl Offset {
|
|||
/// assert_eq!(Offset::MAX.to_string(), "+25:59:59");
|
||||
/// assert_eq!(
|
||||
/// Offset::MAX.round(Unit::Minute).unwrap_err().to_string(),
|
||||
/// "rounding offset `+25:59:59` resulted in a duration of 26h, \
|
||||
/// which overflows `Offset`",
|
||||
/// "rounding time zone offset resulted in a duration that overflows",
|
||||
/// );
|
||||
/// ```
|
||||
#[inline]
|
||||
|
|
@ -1125,7 +1110,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 {
|
||||
write!(f, "+00")
|
||||
f.write_str("+00")
|
||||
} else if hours != 0 && minutes == 0 && seconds == 0 {
|
||||
write!(f, "{sign}{hours:02}")
|
||||
} else if minutes != 0 && seconds == 0 {
|
||||
|
|
@ -1347,11 +1332,10 @@ impl TryFrom<SignedDuration> for Offset {
|
|||
} else if subsec <= -500_000_000 {
|
||||
seconds = seconds.saturating_sub(1);
|
||||
}
|
||||
let seconds = i32::try_from(seconds).map_err(|_| {
|
||||
err!("`SignedDuration` of {sdur} overflows `Offset`")
|
||||
})?;
|
||||
let seconds =
|
||||
i32::try_from(seconds).map_err(|_| E::OverflowSignedDuration)?;
|
||||
Offset::from_seconds(seconds)
|
||||
.map_err(|_| err!("`SignedDuration` of {sdur} overflows `Offset`"))
|
||||
.map_err(|_| Error::from(E::OverflowSignedDuration))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1599,20 +1583,11 @@ impl OffsetRound {
|
|||
fn round(&self, offset: Offset) -> Result<Offset, Error> {
|
||||
let smallest = self.0.get_smallest();
|
||||
if !(Unit::Second <= smallest && smallest <= Unit::Hour) {
|
||||
return Err(err!(
|
||||
"rounding `Offset` failed because \
|
||||
a unit of {plural} was provided, but offset rounding \
|
||||
can only use hours, minutes or seconds",
|
||||
plural = smallest.plural(),
|
||||
));
|
||||
return Err(Error::from(E::RoundInvalidUnit { unit: smallest }));
|
||||
}
|
||||
let rounded_sdur = SignedDuration::from(offset).round(self.0)?;
|
||||
Offset::try_from(rounded_sdur).map_err(|_| {
|
||||
err!(
|
||||
"rounding offset `{offset}` resulted in a duration \
|
||||
of {rounded_sdur:?}, which overflows `Offset`",
|
||||
)
|
||||
})
|
||||
Offset::try_from(rounded_sdur)
|
||||
.map_err(|_| Error::from(E::RoundOverflow))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1888,10 +1863,10 @@ impl OffsetConflict {
|
|||
/// let result = OffsetConflict::Reject.resolve(dt, offset, tz.clone());
|
||||
/// assert_eq!(
|
||||
/// result.unwrap_err().to_string(),
|
||||
/// "datetime 1968-02-01T23:15:00 could not resolve to a timestamp \
|
||||
/// since 'reject' conflict resolution was chosen, and because \
|
||||
/// datetime has offset -00:45, but the time zone Africa/Monrovia \
|
||||
/// for the given datetime unambiguously has offset -00:44:30",
|
||||
/// "datetime could not resolve to a timestamp since `reject` \
|
||||
/// conflict resolution was chosen, and because datetime has offset \
|
||||
/// `-00:45`, but the time zone `Africa/Monrovia` for the given \
|
||||
/// datetime unambiguously has offset `-00:44:30`",
|
||||
/// );
|
||||
/// let is_equal = |parsed: Offset, candidate: Offset| {
|
||||
/// parsed == candidate || candidate.round(Unit::Minute).map_or(
|
||||
|
|
@ -1950,11 +1925,10 @@ impl OffsetConflict {
|
|||
/// let result = "1970-06-01T00-00:45:00[Africa/Monrovia]".parse::<Zoned>();
|
||||
/// assert_eq!(
|
||||
/// result.unwrap_err().to_string(),
|
||||
/// "parsing \"1970-06-01T00-00:45:00[Africa/Monrovia]\" failed: \
|
||||
/// datetime 1970-06-01T00:00:00 could not resolve to a timestamp \
|
||||
/// since 'reject' conflict resolution was chosen, and because \
|
||||
/// datetime has offset -00:45, but the time zone Africa/Monrovia \
|
||||
/// for the given datetime unambiguously has offset -00:44:30",
|
||||
/// "datetime could not resolve to a timestamp since `reject` \
|
||||
/// conflict resolution was chosen, and because datetime has offset \
|
||||
/// `-00:45`, but the time zone `Africa/Monrovia` for the given \
|
||||
/// datetime unambiguously has offset `-00:44:30`",
|
||||
/// );
|
||||
/// ```
|
||||
pub fn resolve_with<F>(
|
||||
|
|
@ -2046,13 +2020,13 @@ impl OffsetConflict {
|
|||
|
||||
let amb = tz.to_ambiguous_timestamp(dt);
|
||||
match amb.offset() {
|
||||
Unambiguous { offset } if !is_equal(given, offset) => Err(err!(
|
||||
"datetime {dt} could not resolve to a timestamp since \
|
||||
'reject' conflict resolution was chosen, and because \
|
||||
datetime has offset {given}, but the time zone {tzname} for \
|
||||
the given datetime unambiguously has offset {offset}",
|
||||
tzname = tz.diagnostic_name(),
|
||||
)),
|
||||
Unambiguous { offset } if !is_equal(given, offset) => {
|
||||
Err(Error::from(E::ResolveRejectUnambiguous {
|
||||
given,
|
||||
offset,
|
||||
tz,
|
||||
}))
|
||||
}
|
||||
Unambiguous { .. } => Ok(amb.into_ambiguous_zoned(tz)),
|
||||
Gap { before, after } => {
|
||||
// In `jiff 0.1`, we reported an error when we found a gap
|
||||
|
|
@ -2065,28 +2039,22 @@ impl OffsetConflict {
|
|||
// changed to treat all offsets in a gap as invalid).
|
||||
//
|
||||
// Ref: https://github.com/tc39/proposal-temporal/issues/2892
|
||||
Err(err!(
|
||||
"datetime {dt} could not resolve to timestamp \
|
||||
since 'reject' conflict resolution was chosen, and \
|
||||
because datetime has offset {given}, but the time \
|
||||
zone {tzname} for the given datetime falls in a gap \
|
||||
(between offsets {before} and {after}), and all \
|
||||
offsets for a gap are regarded as invalid",
|
||||
tzname = tz.diagnostic_name(),
|
||||
))
|
||||
Err(Error::from(E::ResolveRejectGap {
|
||||
given,
|
||||
before,
|
||||
after,
|
||||
tz,
|
||||
}))
|
||||
}
|
||||
Fold { before, after }
|
||||
if !is_equal(given, before) && !is_equal(given, after) =>
|
||||
{
|
||||
Err(err!(
|
||||
"datetime {dt} could not resolve to timestamp \
|
||||
since 'reject' conflict resolution was chosen, and \
|
||||
because datetime has offset {given}, but the time \
|
||||
zone {tzname} for the given datetime falls in a fold \
|
||||
between offsets {before} and {after}, neither of which \
|
||||
match the offset",
|
||||
tzname = tz.diagnostic_name(),
|
||||
))
|
||||
Err(Error::from(E::ResolveRejectFold {
|
||||
given,
|
||||
before,
|
||||
after,
|
||||
tz,
|
||||
}))
|
||||
}
|
||||
Fold { .. } => {
|
||||
let kind = Unambiguous { offset: given };
|
||||
|
|
|
|||
|
|
@ -72,14 +72,14 @@ use core::fmt::Debug;
|
|||
|
||||
use crate::{
|
||||
civil::DateTime,
|
||||
error::{err, Error, ErrorContext},
|
||||
error::{tz::posix::Error as E, Error, ErrorContext},
|
||||
shared,
|
||||
timestamp::Timestamp,
|
||||
tz::{
|
||||
timezone::TimeZoneAbbreviation, AmbiguousOffset, Dst, Offset,
|
||||
TimeZoneOffsetInfo, TimeZoneTransition,
|
||||
},
|
||||
util::{array_str::Abbreviation, escape::Bytes, parse},
|
||||
util::{array_str::Abbreviation, parse},
|
||||
};
|
||||
|
||||
/// The result of parsing the POSIX `TZ` environment variable.
|
||||
|
|
@ -114,11 +114,7 @@ impl PosixTzEnv {
|
|||
let bytes = bytes.as_ref();
|
||||
if bytes.get(0) == Some(&b':') {
|
||||
let Ok(string) = core::str::from_utf8(&bytes[1..]) else {
|
||||
return Err(err!(
|
||||
"POSIX time zone string with a ':' prefix contains \
|
||||
invalid UTF-8: {:?}",
|
||||
Bytes(&bytes[1..]),
|
||||
));
|
||||
return Err(Error::from(E::ColonPrefixInvalidUtf8));
|
||||
};
|
||||
Ok(PosixTzEnv::Implementation(string.into()))
|
||||
} else {
|
||||
|
|
@ -138,8 +134,11 @@ impl PosixTzEnv {
|
|||
impl core::fmt::Display for PosixTzEnv {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
match *self {
|
||||
PosixTzEnv::Rule(ref tz) => write!(f, "{tz}"),
|
||||
PosixTzEnv::Implementation(ref imp) => write!(f, ":{imp}"),
|
||||
PosixTzEnv::Rule(ref tz) => core::fmt::Display::fmt(tz, f),
|
||||
PosixTzEnv::Implementation(ref imp) => {
|
||||
f.write_str(":")?;
|
||||
core::fmt::Display::fmt(imp, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -211,10 +210,8 @@ impl PosixTimeZone<Abbreviation> {
|
|||
) -> Result<PosixTimeZoneOwned, Error> {
|
||||
let bytes = bytes.as_ref();
|
||||
let inner = shared::PosixTimeZone::parse(bytes.as_ref())
|
||||
.map_err(Error::shared)
|
||||
.map_err(|e| {
|
||||
e.context(err!("invalid POSIX TZ string {:?}", Bytes(bytes)))
|
||||
})?;
|
||||
.map_err(Error::posix_tz)
|
||||
.context(E::InvalidPosixTz)?;
|
||||
Ok(PosixTimeZone { inner })
|
||||
}
|
||||
|
||||
|
|
@ -227,13 +224,8 @@ impl PosixTimeZone<Abbreviation> {
|
|||
let bytes = bytes.as_ref();
|
||||
let (inner, remaining) =
|
||||
shared::PosixTimeZone::parse_prefix(bytes.as_ref())
|
||||
.map_err(Error::shared)
|
||||
.map_err(|e| {
|
||||
e.context(err!(
|
||||
"invalid POSIX TZ string {:?}",
|
||||
Bytes(bytes)
|
||||
))
|
||||
})?;
|
||||
.map_err(Error::posix_tz)
|
||||
.context(E::InvalidPosixTz)?;
|
||||
Ok((PosixTimeZone { inner }, remaining))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::{sync::RwLock, time::Duration};
|
|||
use alloc::string::ToString;
|
||||
|
||||
use crate::{
|
||||
error::{err, Error, ErrorContext},
|
||||
error::{tz::system::Error as E, Error, ErrorContext},
|
||||
tz::{posix::PosixTzEnv, TimeZone, TimeZoneDatabase},
|
||||
util::cache::Expiration,
|
||||
};
|
||||
|
|
@ -141,22 +141,20 @@ pub(crate) fn get(db: &TimeZoneDatabase) -> Result<TimeZone, Error> {
|
|||
pub(crate) fn get_force(db: &TimeZoneDatabase) -> Result<TimeZone, Error> {
|
||||
match get_env_tz(db) {
|
||||
Ok(Some(tz)) => {
|
||||
debug!("checked TZ environment variable and found {tz:?}");
|
||||
debug!("checked `TZ` environment variable and found {tz:?}");
|
||||
return Ok(tz);
|
||||
}
|
||||
Ok(None) => {
|
||||
debug!("TZ environment variable is not set");
|
||||
debug!("`TZ` environment variable is not set");
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(err.context(
|
||||
"TZ environment variable set, but failed to read value",
|
||||
));
|
||||
return Err(err.context(E::FailedEnvTz));
|
||||
}
|
||||
}
|
||||
if let Some(tz) = sys::get(db) {
|
||||
return Ok(tz);
|
||||
}
|
||||
Err(err!("failed to find system time zone"))
|
||||
Err(Error::from(E::FailedSystemTimeZone))
|
||||
}
|
||||
|
||||
/// Materializes a `TimeZone` from a `TZ` environment variable.
|
||||
|
|
@ -184,8 +182,8 @@ fn get_env_tz(db: &TimeZoneDatabase) -> Result<Option<TimeZone>, Error> {
|
|||
// `TZ=UTC`.
|
||||
if tzenv.is_empty() {
|
||||
debug!(
|
||||
"TZ environment variable set to empty value, \
|
||||
assuming TZ=UTC in order to conform to \
|
||||
"`TZ` environment variable set to empty value, \
|
||||
assuming `TZ=UTC` in order to conform to \
|
||||
widespread convention among Unix tooling",
|
||||
);
|
||||
return Ok(Some(TimeZone::UTC));
|
||||
|
|
@ -196,15 +194,7 @@ fn get_env_tz(db: &TimeZoneDatabase) -> Result<Option<TimeZone>, Error> {
|
|||
"failed to parse {tzenv:?} as POSIX TZ rule \
|
||||
(attempting to treat it as an IANA time zone): {_err}",
|
||||
);
|
||||
tzenv
|
||||
.to_str()
|
||||
.ok_or_else(|| {
|
||||
err!(
|
||||
"failed to parse {tzenv:?} as a POSIX TZ transition \
|
||||
string, or as valid UTF-8",
|
||||
)
|
||||
})?
|
||||
.to_string()
|
||||
tzenv.to_str().ok_or(E::FailedPosixTzAndUtf8)?.to_string()
|
||||
}
|
||||
Ok(PosixTzEnv::Implementation(string)) => string.to_string(),
|
||||
Ok(PosixTzEnv::Rule(tz)) => {
|
||||
|
|
@ -231,26 +221,20 @@ fn get_env_tz(db: &TimeZoneDatabase) -> Result<Option<TimeZone>, Error> {
|
|||
// No zoneinfo means this is probably a IANA Time Zone name. But...
|
||||
// it could just be a file path.
|
||||
debug!(
|
||||
"could not find {needle:?} in TZ={tz_name_or_path:?}, \
|
||||
therefore attempting lookup in {db:?}",
|
||||
"could not find {needle:?} in `TZ={tz_name_or_path:?}`, \
|
||||
therefore attempting lookup in `{db:?}`",
|
||||
);
|
||||
return match db.get(&tz_name_or_path) {
|
||||
Ok(tz) => Ok(Some(tz)),
|
||||
Err(_err) => {
|
||||
debug!(
|
||||
"using TZ={tz_name_or_path:?} as time zone name failed, \
|
||||
could not find time zone in zoneinfo database {db:?} \
|
||||
"using `TZ={tz_name_or_path:?}` as time zone name failed, \
|
||||
could not find time zone in zoneinfo database `{db:?}` \
|
||||
(continuing to try and read `{tz_name_or_path}` as \
|
||||
a TZif file)",
|
||||
);
|
||||
sys::read(db, &tz_name_or_path)
|
||||
.ok_or_else(|| {
|
||||
err!(
|
||||
"failed to read TZ={tz_name_or_path:?} \
|
||||
as a TZif file after attempting a tzdb \
|
||||
lookup for `{tz_name_or_path}`",
|
||||
)
|
||||
})
|
||||
.ok_or_else(|| Error::from(E::FailedEnvTzAsTzif))
|
||||
.map(Some)
|
||||
}
|
||||
};
|
||||
|
|
@ -260,16 +244,16 @@ fn get_env_tz(db: &TimeZoneDatabase) -> Result<Option<TimeZone>, Error> {
|
|||
// `zoneinfo/`. Once we have that, we try to look it up in our tzdb.
|
||||
let name = &tz_name_or_path[rpos + needle.len()..];
|
||||
debug!(
|
||||
"extracted {name:?} from TZ={tz_name_or_path:?} \
|
||||
"extracted `{name}` from `TZ={tz_name_or_path}` \
|
||||
and assuming it is an IANA time zone name",
|
||||
);
|
||||
match db.get(&name) {
|
||||
Ok(tz) => return Ok(Some(tz)),
|
||||
Err(_err) => {
|
||||
debug!(
|
||||
"using {name:?} from TZ={tz_name_or_path:?}, \
|
||||
could not find time zone in zoneinfo database {db:?} \
|
||||
(continuing to try and use {tz_name_or_path:?})",
|
||||
"using `{name}` from `TZ={tz_name_or_path}`, \
|
||||
could not find time zone in zoneinfo database `{db:?}` \
|
||||
(continuing to try and use `{tz_name_or_path}`)",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -279,13 +263,7 @@ fn get_env_tz(db: &TimeZoneDatabase) -> Result<Option<TimeZone>, Error> {
|
|||
// and read the data as TZif. This will give us time zone data if it works,
|
||||
// but without a name.
|
||||
sys::read(db, &tz_name_or_path)
|
||||
.ok_or_else(|| {
|
||||
err!(
|
||||
"failed to read TZ={tz_name_or_path:?} \
|
||||
as a TZif file after attempting a tzdb \
|
||||
lookup for `{name}`",
|
||||
)
|
||||
})
|
||||
.ok_or_else(|| Error::from(E::FailedEnvTzAsTzif))
|
||||
.map(Some)
|
||||
}
|
||||
|
||||
|
|
@ -298,8 +276,8 @@ fn get_env_tz(db: &TimeZoneDatabase) -> Result<Option<TimeZone>, Error> {
|
|||
fn read_unnamed_tzif_file(path: &str) -> Result<TimeZone, Error> {
|
||||
let data = std::fs::read(path)
|
||||
.map_err(Error::io)
|
||||
.with_context(|| err!("failed to read {path:?} as TZif file"))?;
|
||||
let tz = TimeZone::tzif_system(&data)
|
||||
.with_context(|| err!("found invalid TZif data at {path:?}"))?;
|
||||
.context(E::FailedUnnamedTzifRead)?;
|
||||
let tz =
|
||||
TimeZone::tzif_system(&data).context(E::FailedUnnamedTzifInvalid)?;
|
||||
Ok(tz)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use windows_sys::Win32::System::Time::{
|
|||
};
|
||||
|
||||
use crate::{
|
||||
error::{err, Error, ErrorContext},
|
||||
error::{tz::system::Error as E, Error, ErrorContext},
|
||||
tz::{TimeZone, TimeZoneDatabase},
|
||||
util::utf8,
|
||||
};
|
||||
|
|
@ -79,16 +79,12 @@ fn windows_to_iana(tz_key_name: &str) -> Result<&'static str, Error> {
|
|||
utf8::cmp_ignore_ascii_case(win_name, &tz_key_name)
|
||||
});
|
||||
let Ok(index) = result else {
|
||||
return Err(err!(
|
||||
"found Windows time zone name {tz_key_name}, \
|
||||
but could not find a mapping for it to an \
|
||||
IANA time zone name",
|
||||
));
|
||||
return Err(Error::from(E::WindowsMissingIanaMapping));
|
||||
};
|
||||
let iana_name = WINDOWS_TO_IANA[index].1;
|
||||
trace!(
|
||||
"found Windows time zone name {tz_key_name}, and \
|
||||
successfully mapped it to IANA time zone {iana_name}",
|
||||
"found Windows time zone name `{tz_key_name}`, and \
|
||||
successfully mapped it to IANA time zone `{iana_name}`",
|
||||
);
|
||||
Ok(iana_name)
|
||||
}
|
||||
|
|
@ -107,24 +103,19 @@ fn get_tz_key_name() -> Result<String, Error> {
|
|||
// when `info` is properly initialized.
|
||||
let info = unsafe { info.assume_init() };
|
||||
let tz_key_name = nul_terminated_utf16_to_string(&info.TimeZoneKeyName)
|
||||
.context(
|
||||
"could not get TimeZoneKeyName from \
|
||||
winapi DYNAMIC_TIME_ZONE_INFORMATION",
|
||||
)?;
|
||||
.context(E::WindowsTimeZoneKeyName)?;
|
||||
Ok(tz_key_name)
|
||||
}
|
||||
|
||||
fn nul_terminated_utf16_to_string(
|
||||
code_units: &[u16],
|
||||
) -> Result<String, Error> {
|
||||
let nul = code_units.iter().position(|&cu| cu == 0).ok_or_else(|| {
|
||||
err!("failed to convert u16 slice to UTF-8 (no NUL terminator found)")
|
||||
})?;
|
||||
let nul = code_units
|
||||
.iter()
|
||||
.position(|&cu| cu == 0)
|
||||
.ok_or(E::WindowsUtf16DecodeNul)?;
|
||||
let string = String::from_utf16(&code_units[..nul])
|
||||
.map_err(Error::adhoc)
|
||||
.with_context(|| {
|
||||
err!("failed to convert u16 slice to UTF-8 (invalid UTF-16)")
|
||||
})?;
|
||||
.map_err(|_| E::WindowsUtf16DecodeInvalid)?;
|
||||
Ok(string)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
civil::DateTime,
|
||||
error::{err, Error},
|
||||
error::{tz::timezone::Error as E, Error},
|
||||
tz::{
|
||||
ambiguous::{AmbiguousOffset, AmbiguousTimestamp, AmbiguousZoned},
|
||||
offset::{Dst, Offset},
|
||||
|
|
@ -392,10 +392,8 @@ impl TimeZone {
|
|||
pub fn try_system() -> Result<TimeZone, Error> {
|
||||
#[cfg(not(feature = "tz-system"))]
|
||||
{
|
||||
Err(err!(
|
||||
"failed to get system time zone since 'tz-system' \
|
||||
crate feature is not enabled",
|
||||
))
|
||||
Err(Error::from(crate::error::CrateFeatureError::TzSystem)
|
||||
.context(E::FailedSystem))
|
||||
}
|
||||
#[cfg(feature = "tz-system")]
|
||||
{
|
||||
|
|
@ -916,7 +914,7 @@ impl TimeZone {
|
|||
/// assert_eq!(
|
||||
/// tz.to_fixed_offset().unwrap_err().to_string(),
|
||||
/// "cannot convert non-fixed IANA time zone \
|
||||
/// to offset without timestamp or civil datetime",
|
||||
/// to offset without a timestamp or civil datetime",
|
||||
/// );
|
||||
///
|
||||
/// let tz = TimeZone::UTC;
|
||||
|
|
@ -935,11 +933,7 @@ impl TimeZone {
|
|||
#[inline]
|
||||
pub fn to_fixed_offset(&self) -> Result<Offset, Error> {
|
||||
let mkerr = || {
|
||||
err!(
|
||||
"cannot convert non-fixed {kind} time zone to offset \
|
||||
without timestamp or civil datetime",
|
||||
kind = self.kind_description(),
|
||||
)
|
||||
Error::from(E::ConvertNonFixed { kind: self.kind_description() })
|
||||
};
|
||||
repr::each! {
|
||||
&self.repr,
|
||||
|
|
@ -1392,7 +1386,7 @@ impl TimeZone {
|
|||
/// Returns a short description about the kind of this time zone.
|
||||
///
|
||||
/// This is useful in error messages.
|
||||
fn kind_description(&self) -> &str {
|
||||
fn kind_description(&self) -> &'static str {
|
||||
repr::each! {
|
||||
&self.repr,
|
||||
UTC => "UTC",
|
||||
|
|
@ -1887,12 +1881,12 @@ impl<'a> core::fmt::Display for DiagnosticName<'a> {
|
|||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
repr::each! {
|
||||
&self.0.repr,
|
||||
UTC => write!(f, "UTC"),
|
||||
UNKNOWN => write!(f, "Etc/Unknown"),
|
||||
FIXED(offset) => write!(f, "{offset}"),
|
||||
STATIC_TZIF(tzif) => write!(f, "{}", tzif.name().unwrap_or("Local")),
|
||||
ARC_TZIF(tzif) => write!(f, "{}", tzif.name().unwrap_or("Local")),
|
||||
ARC_POSIX(posix) => write!(f, "{posix}"),
|
||||
UTC => f.write_str("UTC"),
|
||||
UNKNOWN => f.write_str("Etc/Unknown"),
|
||||
FIXED(offset) => offset.fmt(f),
|
||||
STATIC_TZIF(tzif) => f.write_str(tzif.name().unwrap_or("Local")),
|
||||
ARC_TZIF(tzif) => f.write_str(tzif.name().unwrap_or("Local")),
|
||||
ARC_POSIX(posix) => posix.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1939,6 +1933,10 @@ 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;
|
||||
|
||||
|
|
@ -2271,9 +2269,9 @@ mod repr {
|
|||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
each! {
|
||||
self,
|
||||
UTC => write!(f, "UTC"),
|
||||
UNKNOWN => write!(f, "Etc/Unknown"),
|
||||
FIXED(offset) => write!(f, "{offset:?}"),
|
||||
UTC => f.write_str("UTC"),
|
||||
UNKNOWN => f.write_str("Etc/Unknown"),
|
||||
FIXED(offset) => core::fmt::Debug::fmt(&offset, f),
|
||||
STATIC_TZIF(tzif) => {
|
||||
// The full debug output is a bit much, so constrain it.
|
||||
let field = tzif.name().unwrap_or("Local");
|
||||
|
|
@ -2284,7 +2282,11 @@ mod repr {
|
|||
let field = tzif.name().unwrap_or("Local");
|
||||
f.debug_tuple("TZif").field(&field).finish()
|
||||
},
|
||||
ARC_POSIX(posix) => write!(f, "Posix({posix})"),
|
||||
ARC_POSIX(posix) => {
|
||||
f.write_str("Posix(")?;
|
||||
core::fmt::Display::fmt(&posix, f)?;
|
||||
f.write_str(")")
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -157,8 +157,7 @@ impl TzifOwned {
|
|||
name: Option<String>,
|
||||
bytes: &[u8],
|
||||
) -> Result<Self, Error> {
|
||||
let sh =
|
||||
shared::TzifOwned::parse(name, bytes).map_err(Error::shared)?;
|
||||
let sh = shared::TzifOwned::parse(name, bytes).map_err(Error::tzif)?;
|
||||
Ok(TzifOwned::from_shared_owned(sh))
|
||||
}
|
||||
|
||||
|
|
@ -571,9 +570,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 => write!(f, "local/wall"),
|
||||
shared::TzifIndicator::LocalStandard => write!(f, "local/std"),
|
||||
shared::TzifIndicator::UTStandard => write!(f, "ut/std"),
|
||||
shared::TzifIndicator::LocalWall => f.write_str("local/wall"),
|
||||
shared::TzifIndicator::LocalStandard => f.write_str("local/std"),
|
||||
shared::TzifIndicator::UTStandard => f.write_str("ut/std"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
320
src/tz/zic.rs
320
src/tz/zic.rs
|
|
@ -111,7 +111,10 @@ use alloc::{
|
|||
|
||||
use crate::{
|
||||
civil::{Date, DateTime, Time, Weekday},
|
||||
error::{err, Error, ErrorContext},
|
||||
error::{
|
||||
tz::zic::{Error as E, MAX_LINE_LEN},
|
||||
Error, ErrorContext,
|
||||
},
|
||||
span::{Span, SpanFieldwise, ToSpan},
|
||||
timestamp::Timestamp,
|
||||
tz::{Dst, Offset},
|
||||
|
|
@ -205,12 +208,12 @@ impl Rules {
|
|||
"every name in rule group must be identical"
|
||||
);
|
||||
let dst = Dst::from(r.save.suffix() == RuleSaveSuffixP::Dst);
|
||||
let offset = r.save.to_offset().map_err(|e| {
|
||||
err!("SAVE value in rule {:?} is too big: {e}", inner.name)
|
||||
let offset = r.save.to_offset().with_context(|| {
|
||||
E::FailedRule { name: inner.name.as_str().into() }
|
||||
})?;
|
||||
let years = r.years().with_context(|| E::FailedRule {
|
||||
name: inner.name.as_str().into(),
|
||||
})?;
|
||||
let years = r
|
||||
.years()
|
||||
.map_err(|e| e.context(err!("rule {:?}", inner.name)))?;
|
||||
let month = r.inn.month;
|
||||
let letters = r.letters.part;
|
||||
let day = r.on;
|
||||
|
|
@ -260,13 +263,12 @@ impl ZicP {
|
|||
) -> Result<(), Error> {
|
||||
while parser.read_next_fields()? {
|
||||
self.parse_one(&mut parser)
|
||||
.map_err(|e| e.context(err!("line {}", parser.line_number)))?;
|
||||
.context(E::Line { number: parser.line_number })?;
|
||||
}
|
||||
if let Some(ref name) = parser.continuation_zone_for {
|
||||
return Err(err!(
|
||||
"expected continuation zone line for {name:?}, \
|
||||
but found end of data instead",
|
||||
));
|
||||
return Err(Error::from(E::ExpectedContinuationZoneLine {
|
||||
name: name.as_str().into(),
|
||||
}));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -277,9 +279,8 @@ impl ZicP {
|
|||
assert!(!p.fields.is_empty());
|
||||
|
||||
if let Some(name) = p.continuation_zone_for.take() {
|
||||
let zone = ZoneContinuationP::parse(&p.fields).map_err(|e| {
|
||||
e.context("failed to parse continuation 'Zone' line")
|
||||
})?;
|
||||
let zone = ZoneContinuationP::parse(&p.fields)
|
||||
.context(E::FailedContinuationZone)?;
|
||||
let more_continuations = zone.until.is_some();
|
||||
// OK because `p.continuation_zone_for` is only set when we have
|
||||
// seen a first zone with the corresponding name.
|
||||
|
|
@ -293,51 +294,45 @@ impl ZicP {
|
|||
|
||||
let (first, rest) = (&p.fields[0], &p.fields[1..]);
|
||||
if first.starts_with("R") && "Rule".starts_with(first) {
|
||||
let rule = RuleP::parse(rest)
|
||||
.map_err(|e| e.context("failed to parse 'Rule' line"))?;
|
||||
let rule = RuleP::parse(rest).context(E::FailedRuleLine)?;
|
||||
let name = rule.name.name.clone();
|
||||
self.rules.entry(name).or_default().push(rule);
|
||||
} else if first.starts_with("Z") && "Zone".starts_with(first) {
|
||||
let first = ZoneFirstP::parse(rest)
|
||||
.map_err(|e| e.context("failed to parse first 'Zone' line"))?;
|
||||
let first = ZoneFirstP::parse(rest).context(E::FailedZoneFirst)?;
|
||||
let name = first.name.name.clone();
|
||||
if first.until.is_some() {
|
||||
p.continuation_zone_for = Some(name.clone());
|
||||
}
|
||||
let zone = ZoneP { first, continuations: vec![] };
|
||||
if self.links.contains_key(&name) {
|
||||
return Err(err!(
|
||||
"found zone with name {name:?} that conflicts \
|
||||
with a link of the same name",
|
||||
));
|
||||
return Err(Error::from(E::DuplicateZoneLink {
|
||||
name: name.into(),
|
||||
}));
|
||||
}
|
||||
if let Some(previous_zone) = self.zones.insert(name, zone) {
|
||||
return Err(err!(
|
||||
"found duplicate zone for {:?}",
|
||||
previous_zone.first.name.name,
|
||||
));
|
||||
return Err(Error::from(E::DuplicateZone {
|
||||
name: previous_zone.first.name.name.into(),
|
||||
}));
|
||||
}
|
||||
} else if first.starts_with("L") && "Link".starts_with(first) {
|
||||
let link = LinkP::parse(rest)
|
||||
.map_err(|e| e.context("failed to parse 'Link' line"))?;
|
||||
let link = LinkP::parse(rest).context(E::FailedLinkLine)?;
|
||||
let name = link.name.name.clone();
|
||||
if self.zones.contains_key(&name) {
|
||||
return Err(err!(
|
||||
"found link with name {name:?} that conflicts \
|
||||
with a zone of the same name",
|
||||
));
|
||||
return Err(Error::from(E::DuplicateLinkZone {
|
||||
name: name.into(),
|
||||
}));
|
||||
}
|
||||
if let Some(previous_link) = self.links.insert(name, link) {
|
||||
return Err(err!(
|
||||
"found duplicate link for {:?}",
|
||||
previous_link.name.name,
|
||||
));
|
||||
return Err(Error::from(E::DuplicateLink {
|
||||
name: previous_link.name.name.into(),
|
||||
}));
|
||||
}
|
||||
// N.B. We don't check that the link's target name refers to some
|
||||
// other zone/link here, because the corresponding zone/link might
|
||||
// be defined later.
|
||||
} else {
|
||||
return Err(err!("unrecognized zic line: {first:?}"));
|
||||
return Err(Error::from(E::UnrecognizedZicLine)
|
||||
.context(E::Line { number: p.line_number }));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -369,10 +364,9 @@ struct RuleP {
|
|||
impl RuleP {
|
||||
fn parse(fields: &[&str]) -> Result<RuleP, Error> {
|
||||
if fields.len() != 9 {
|
||||
return Err(err!(
|
||||
"expected exactly 9 fields for rule, but found {} fields",
|
||||
fields.len(),
|
||||
));
|
||||
return Err(Error::from(E::ExpectedRuleNineFields {
|
||||
got: fields.len(),
|
||||
}));
|
||||
}
|
||||
let (name_field, fields) = (fields[0], &fields[1..]);
|
||||
let (from_field, fields) = (fields[0], &fields[1..]);
|
||||
|
|
@ -386,28 +380,21 @@ impl RuleP {
|
|||
|
||||
let name = name_field
|
||||
.parse::<RuleNameP>()
|
||||
.map_err(|e| e.context("failed to parse NAME field"))?;
|
||||
.context(E::FailedParseFieldName)?;
|
||||
let from = from_field
|
||||
.parse::<RuleFromP>()
|
||||
.map_err(|e| e.context("failed to parse FROM field"))?;
|
||||
let to = to_field
|
||||
.parse::<RuleToP>()
|
||||
.map_err(|e| e.context("failed to parse TO field"))?;
|
||||
let inn = in_field
|
||||
.parse::<RuleInP>()
|
||||
.map_err(|e| e.context("failed to parse IN field"))?;
|
||||
let on = on_field
|
||||
.parse::<RuleOnP>()
|
||||
.map_err(|e| e.context("failed to parse ON field"))?;
|
||||
let at = at_field
|
||||
.parse::<RuleAtP>()
|
||||
.map_err(|e| e.context("failed to parse AT field"))?;
|
||||
.context(E::FailedParseFieldFrom)?;
|
||||
let to = to_field.parse::<RuleToP>().context(E::FailedParseFieldTo)?;
|
||||
let inn =
|
||||
in_field.parse::<RuleInP>().context(E::FailedParseFieldIn)?;
|
||||
let on = on_field.parse::<RuleOnP>().context(E::FailedParseFieldOn)?;
|
||||
let at = at_field.parse::<RuleAtP>().context(E::FailedParseFieldAt)?;
|
||||
let save = save_field
|
||||
.parse::<RuleSaveP>()
|
||||
.map_err(|e| e.context("failed to parse SAVE field"))?;
|
||||
.context(E::FailedParseFieldSave)?;
|
||||
let letters = letters_field
|
||||
.parse::<RuleLettersP>()
|
||||
.map_err(|e| e.context("failed to parse LETTERS field"))?;
|
||||
.context(E::FailedParseFieldLetters)?;
|
||||
|
||||
Ok(RuleP { name, from, to, inn, on, at, save, letters })
|
||||
}
|
||||
|
|
@ -420,9 +407,10 @@ impl RuleP {
|
|||
RuleToP::Year { year } => year,
|
||||
};
|
||||
if start > end {
|
||||
return Err(err!(
|
||||
"found start year {start} to be greater than end year {end}"
|
||||
));
|
||||
return Err(Error::from(E::InvalidRuleYear {
|
||||
start: start.get(),
|
||||
end: end.get(),
|
||||
}));
|
||||
}
|
||||
Ok(start..=end)
|
||||
}
|
||||
|
|
@ -462,7 +450,7 @@ struct ZoneFirstP {
|
|||
impl ZoneFirstP {
|
||||
fn parse(fields: &[&str]) -> Result<ZoneFirstP, Error> {
|
||||
if fields.len() < 4 {
|
||||
return Err(err!("first ZONE line must have at least 4 fields"));
|
||||
return Err(Error::from(E::ExpectedFirstZoneFourFields));
|
||||
}
|
||||
let (name_field, fields) = (fields[0], &fields[1..]);
|
||||
let (stdoff_field, fields) = (fields[0], &fields[1..]);
|
||||
|
|
@ -470,23 +458,20 @@ impl ZoneFirstP {
|
|||
let (format_field, fields) = (fields[0], &fields[1..]);
|
||||
let name = name_field
|
||||
.parse::<ZoneNameP>()
|
||||
.map_err(|e| e.context("failed to parse NAME field"))?;
|
||||
.context(E::FailedParseFieldName)?;
|
||||
let stdoff = stdoff_field
|
||||
.parse::<ZoneStdoffP>()
|
||||
.map_err(|e| e.context("failed to parse STDOFF field"))?;
|
||||
.context(E::FailedParseFieldStdOff)?;
|
||||
let rules = rules_field
|
||||
.parse::<ZoneRulesP>()
|
||||
.map_err(|e| e.context("failed to parse RULES field"))?;
|
||||
.context(E::FailedParseFieldRules)?;
|
||||
let format = format_field
|
||||
.parse::<ZoneFormatP>()
|
||||
.map_err(|e| e.context("failed to parse FORMAT field"))?;
|
||||
.context(E::FailedParseFieldFormat)?;
|
||||
let until = if fields.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
ZoneUntilP::parse(fields)
|
||||
.map_err(|e| e.context("failed to parse UNTIL field"))?,
|
||||
)
|
||||
Some(ZoneUntilP::parse(fields).context(E::FailedParseFieldUntil)?)
|
||||
};
|
||||
Ok(ZoneFirstP { name, stdoff, rules, format, until })
|
||||
}
|
||||
|
|
@ -512,29 +497,24 @@ struct ZoneContinuationP {
|
|||
impl ZoneContinuationP {
|
||||
fn parse(fields: &[&str]) -> Result<ZoneContinuationP, Error> {
|
||||
if fields.len() < 3 {
|
||||
return Err(err!(
|
||||
"continuation ZONE line must have at least 3 fields"
|
||||
));
|
||||
return Err(Error::from(E::ExpectedContinuationZoneThreeFields));
|
||||
}
|
||||
let (stdoff_field, fields) = (fields[0], &fields[1..]);
|
||||
let (rules_field, fields) = (fields[0], &fields[1..]);
|
||||
let (format_field, fields) = (fields[0], &fields[1..]);
|
||||
let stdoff = stdoff_field
|
||||
.parse::<ZoneStdoffP>()
|
||||
.map_err(|e| e.context("failed to parse STDOFF field"))?;
|
||||
.context(E::FailedParseFieldStdOff)?;
|
||||
let rules = rules_field
|
||||
.parse::<ZoneRulesP>()
|
||||
.map_err(|e| e.context("failed to parse RULES field"))?;
|
||||
.context(E::FailedParseFieldRules)?;
|
||||
let format = format_field
|
||||
.parse::<ZoneFormatP>()
|
||||
.map_err(|e| e.context("failed to parse FORMAT field"))?;
|
||||
.context(E::FailedParseFieldFormat)?;
|
||||
let until = if fields.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
ZoneUntilP::parse(fields)
|
||||
.map_err(|e| e.context("failed to parse UNTIL field"))?,
|
||||
)
|
||||
Some(ZoneUntilP::parse(fields).context(E::FailedParseFieldUntil)?)
|
||||
};
|
||||
Ok(ZoneContinuationP { stdoff, rules, format, until })
|
||||
}
|
||||
|
|
@ -553,17 +533,14 @@ struct LinkP {
|
|||
impl LinkP {
|
||||
fn parse(fields: &[&str]) -> Result<LinkP, Error> {
|
||||
if fields.len() != 2 {
|
||||
return Err(err!(
|
||||
"expected exactly 2 fields after LINK, but found {}",
|
||||
fields.len()
|
||||
));
|
||||
return Err(Error::from(E::ExpectedLinkTwoFields));
|
||||
}
|
||||
let target = fields[0]
|
||||
.parse::<ZoneNameP>()
|
||||
.map_err(|e| e.context("failed to parse LINK target"))?;
|
||||
.context(E::FailedParseFieldLinkTarget)?;
|
||||
let name = fields[1]
|
||||
.parse::<ZoneNameP>()
|
||||
.map_err(|e| e.context("failed to parse LINK name"))?;
|
||||
.context(E::FailedParseFieldLinkName)?;
|
||||
Ok(LinkP { target, name })
|
||||
}
|
||||
}
|
||||
|
|
@ -592,12 +569,9 @@ impl FromStr for RuleNameP {
|
|||
// or not. We erase that information. We could rejigger things to keep
|
||||
// that information around, but... Meh.
|
||||
if name.is_empty() {
|
||||
Err(err!("NAME field for rule cannot be empty"))
|
||||
Err(Error::from(E::ExpectedNonEmptyName))
|
||||
} else if name.starts_with(|ch| matches!(ch, '0'..='9' | '+' | '-')) {
|
||||
Err(err!(
|
||||
"NAME field cannot begin with a digit, + or -, \
|
||||
but {name:?} begins with one of those",
|
||||
))
|
||||
Err(Error::from(E::ExpectedNameBegin))
|
||||
} else {
|
||||
Ok(RuleNameP { name: name.to_string() })
|
||||
}
|
||||
|
|
@ -614,8 +588,7 @@ impl FromStr for RuleFromP {
|
|||
type Err = Error;
|
||||
|
||||
fn from_str(from: &str) -> Result<RuleFromP, Error> {
|
||||
let year = parse_year(from)
|
||||
.map_err(|e| e.context("failed to parse FROM field"))?;
|
||||
let year = parse_year(from).context(E::FailedParseFieldFrom)?;
|
||||
Ok(RuleFromP { year })
|
||||
}
|
||||
}
|
||||
|
|
@ -642,8 +615,7 @@ impl FromStr for RuleToP {
|
|||
} else if to.starts_with("o") && "only".starts_with(to) {
|
||||
Ok(RuleToP::Only)
|
||||
} else {
|
||||
let year = parse_year(to)
|
||||
.map_err(|e| e.context("failed to parse TO field"))?;
|
||||
let year = parse_year(to).context(E::FailedParseFieldTo)?;
|
||||
Ok(RuleToP::Year { year })
|
||||
}
|
||||
}
|
||||
|
|
@ -679,7 +651,7 @@ impl FromStr for RuleInP {
|
|||
return Ok(RuleInP { month });
|
||||
}
|
||||
}
|
||||
Err(err!("unrecognized month name: {field:?}"))
|
||||
Err(Error::from(E::UnrecognizedMonthName))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -747,7 +719,7 @@ impl FromStr for RuleOnP {
|
|||
// field. That gets checked at a higher level.
|
||||
Ok(RuleOnP::Day { day })
|
||||
} else {
|
||||
Err(err!("unrecognized format for day-of-month: {field:?}"))
|
||||
Err(Error::from(E::UnrecognizedDayOfMonthFormat))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -782,7 +754,7 @@ impl FromStr for RuleAtP {
|
|||
|
||||
fn from_str(at: &str) -> Result<RuleAtP, Error> {
|
||||
if at.is_empty() {
|
||||
return Err(err!("empty field is not a valid AT value"));
|
||||
return Err(Error::from(E::ExpectedNonEmptyAt));
|
||||
}
|
||||
let (span_string, suffix_string) = at.split_at(at.len() - 1);
|
||||
if suffix_string.chars().all(|ch| ch.is_ascii_alphabetic()) {
|
||||
|
|
@ -813,7 +785,7 @@ impl FromStr for RuleAtSuffixP {
|
|||
"w" => Ok(RuleAtSuffixP::Wall),
|
||||
"s" => Ok(RuleAtSuffixP::Standard),
|
||||
"u" | "g" | "z" => Ok(RuleAtSuffixP::Universal),
|
||||
_ => Err(err!("unrecognized AT time suffix {suffix:?}")),
|
||||
_ => Err(Error::from(E::UnrecognizedAtTimeSuffix)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -867,7 +839,7 @@ impl FromStr for RuleSaveP {
|
|||
|
||||
fn from_str(at: &str) -> Result<RuleSaveP, Error> {
|
||||
if at.is_empty() {
|
||||
return Err(err!("empty field is not a valid SAVE value"));
|
||||
return Err(Error::from(E::ExpectedNonEmptySave));
|
||||
}
|
||||
let (span_string, suffix_string) = at.split_at(at.len() - 1);
|
||||
if suffix_string.chars().all(|ch| ch.is_ascii_alphabetic()) {
|
||||
|
|
@ -902,7 +874,7 @@ impl FromStr for RuleSaveSuffixP {
|
|||
match suffix {
|
||||
"s" => Ok(RuleSaveSuffixP::Standard),
|
||||
"d" => Ok(RuleSaveSuffixP::Dst),
|
||||
_ => Err(err!("unrecognized SAVE time suffix {suffix:?}")),
|
||||
_ => Err(Error::from(E::UnrecognizedSaveTimeSuffix)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -939,14 +911,13 @@ impl FromStr for ZoneNameP {
|
|||
|
||||
fn from_str(name: &str) -> Result<ZoneNameP, Error> {
|
||||
if name.is_empty() {
|
||||
return Err(err!("zone names cannot be empty"));
|
||||
return Err(Error::from(E::ExpectedNonEmptyZoneName));
|
||||
}
|
||||
for component in name.split('/') {
|
||||
if component == "." || component == ".." {
|
||||
return Err(err!(
|
||||
"component {component:?} in zone name {name:?} cannot \
|
||||
be \".\" or \"..\"",
|
||||
));
|
||||
return Err(Error::from(E::ExpectedZoneNameComponentNoDots {
|
||||
component: component.into(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
Ok(ZoneNameP { name: name.to_string() })
|
||||
|
|
@ -1035,16 +1006,12 @@ impl FromStr for ZoneFormatP {
|
|||
fn from_str(format: &str) -> Result<ZoneFormatP, Error> {
|
||||
fn check_abbrev(abbrev: &str) -> Result<String, Error> {
|
||||
if abbrev.is_empty() {
|
||||
return Err(err!("empty abbreviations are not allowed"));
|
||||
return Err(Error::from(E::ExpectedNonEmptyAbbreviation));
|
||||
}
|
||||
let is_ok =
|
||||
|ch| matches!(ch, '+'|'-'|'0'..='9'|'A'..='Z'|'a'..='z');
|
||||
if !abbrev.chars().all(is_ok) {
|
||||
return Err(err!(
|
||||
"abbreviation {abbrev:?} \
|
||||
contains invalid character; only \"+\", \"-\" and \
|
||||
ASCII alpha-numeric characters are allowed"
|
||||
));
|
||||
return Err(Error::from(E::InvalidAbbreviation));
|
||||
}
|
||||
Ok(abbrev.to_string())
|
||||
}
|
||||
|
|
@ -1098,28 +1065,24 @@ enum ZoneUntilP {
|
|||
impl ZoneUntilP {
|
||||
fn parse(fields: &[&str]) -> Result<ZoneUntilP, Error> {
|
||||
if fields.is_empty() {
|
||||
return Err(err!("expected at least a year"));
|
||||
return Err(Error::from(E::ExpectedUntilYear));
|
||||
}
|
||||
|
||||
let (year_field, fields) = (fields[0], &fields[1..]);
|
||||
let year = parse_year(year_field)
|
||||
.map_err(|e| e.context("failed to parse year"))?;
|
||||
let year = parse_year(year_field).context(E::FailedParseYear)?;
|
||||
if fields.is_empty() {
|
||||
return Ok(ZoneUntilP::Year { year });
|
||||
}
|
||||
|
||||
let (month_field, fields) = (fields[0], &fields[1..]);
|
||||
let month = month_field
|
||||
.parse::<RuleInP>()
|
||||
.map_err(|e| e.context("failed to parse month"))?;
|
||||
let month =
|
||||
month_field.parse::<RuleInP>().context(E::FailedParseMonth)?;
|
||||
if fields.is_empty() {
|
||||
return Ok(ZoneUntilP::YearMonth { year, month });
|
||||
}
|
||||
|
||||
let (day_field, fields) = (fields[0], &fields[1..]);
|
||||
let day = day_field
|
||||
.parse::<RuleOnP>()
|
||||
.map_err(|e| e.context("failed to parse day"))?;
|
||||
let day = day_field.parse::<RuleOnP>().context(E::FailedParseDay)?;
|
||||
if fields.is_empty() {
|
||||
return Ok(ZoneUntilP::YearMonthDay { year, month, day });
|
||||
}
|
||||
|
|
@ -1127,13 +1090,9 @@ impl ZoneUntilP {
|
|||
let (duration_field, fields) = (fields[0], &fields[1..]);
|
||||
let duration = duration_field
|
||||
.parse::<RuleAtP>()
|
||||
.map_err(|e| e.context("failed to parse time duration"))?;
|
||||
.context(E::FailedParseTimeDuration)?;
|
||||
if !fields.is_empty() {
|
||||
return Err(err!(
|
||||
"expected no more fields after time of day, \
|
||||
but found: {fields:?}",
|
||||
fields = fields.join(" "),
|
||||
));
|
||||
return Err(Error::from(E::ExpectedNothingAfterTime));
|
||||
}
|
||||
Ok(ZoneUntilP::YearMonthDayTime { year, month, day, duration })
|
||||
}
|
||||
|
|
@ -1200,10 +1159,8 @@ fn parse_year(year: &str) -> Result<t::Year, Error> {
|
|||
} else {
|
||||
(t::Sign::N::<1>(), year)
|
||||
};
|
||||
let number = parse::i64(rest.as_bytes())
|
||||
.map_err(|e| e.context("failed to parse year"))?;
|
||||
let year = t::Year::new(number)
|
||||
.ok_or_else(|| err!("year is out of range: {number}"))?;
|
||||
let number = parse::i64(rest.as_bytes()).context(E::FailedParseYear)?;
|
||||
let year = t::Year::try_new("year", number)?;
|
||||
Ok(year * sign)
|
||||
}
|
||||
|
||||
|
|
@ -1239,36 +1196,28 @@ fn parse_span(span: &str) -> Result<Span, Error> {
|
|||
let hour_len = rest.chars().take_while(|c| c.is_ascii_digit()).count();
|
||||
let (hour_digits, rest) = rest.split_at(hour_len);
|
||||
if hour_digits.is_empty() {
|
||||
return Err(err!(
|
||||
"expected time duration to contain at least one hour digit"
|
||||
));
|
||||
return Err(Error::from(E::ExpectedTimeOneHour));
|
||||
}
|
||||
let hours = parse::i64(hour_digits.as_bytes())
|
||||
.map_err(|e| e.context("failed to parse hours in time duration"))?;
|
||||
span = span
|
||||
.try_hours(hours.saturating_mul(i64::from(sign.get())))
|
||||
.map_err(|_| err!("duration hours '{hours:?}' is out of range"))?;
|
||||
let hours =
|
||||
parse::i64(hour_digits.as_bytes()).context(E::FailedParseHour)?;
|
||||
span = span.try_hours(hours.saturating_mul(i64::from(sign.get())))?;
|
||||
if rest.is_empty() {
|
||||
return Ok(span);
|
||||
}
|
||||
|
||||
// Now pluck out the minute component.
|
||||
if !rest.starts_with(":") {
|
||||
return Err(err!("expected ':' after hours, but found {rest:?}"));
|
||||
return Err(Error::from(E::ExpectedColonAfterHour));
|
||||
}
|
||||
let rest = &rest[1..];
|
||||
let minute_len = rest.chars().take_while(|c| c.is_ascii_digit()).count();
|
||||
let (minute_digits, rest) = rest.split_at(minute_len);
|
||||
if minute_digits.is_empty() {
|
||||
return Err(err!(
|
||||
"expected minute digits after 'HH:', but found {rest:?} instead"
|
||||
));
|
||||
return Err(Error::from(E::ExpectedMinuteAfterHours));
|
||||
}
|
||||
let minutes = parse::i64(minute_digits.as_bytes())
|
||||
.map_err(|e| e.context("failed to parse minutes in time duration"))?;
|
||||
let minutes_ranged = t::Minute::new(minutes).ok_or_else(|| {
|
||||
err!("duration minutes '{minutes:?}' is out of range")
|
||||
})?;
|
||||
let minutes =
|
||||
parse::i64(minute_digits.as_bytes()).context(E::FailedParseMinute)?;
|
||||
let minutes_ranged = t::Minute::try_new("minutes", minutes)?;
|
||||
span = span.minutes_ranged((minutes_ranged * sign).rinto());
|
||||
if rest.is_empty() {
|
||||
return Ok(span);
|
||||
|
|
@ -1276,21 +1225,17 @@ fn parse_span(span: &str) -> Result<Span, Error> {
|
|||
|
||||
// Now pluck out the second component.
|
||||
if !rest.starts_with(":") {
|
||||
return Err(err!("expected ':' after minutes, but found {rest:?}"));
|
||||
return Err(Error::from(E::ExpectedColonAfterMinute));
|
||||
}
|
||||
let rest = &rest[1..];
|
||||
let second_len = rest.chars().take_while(|c| c.is_ascii_digit()).count();
|
||||
let (second_digits, rest) = rest.split_at(second_len);
|
||||
if second_digits.is_empty() {
|
||||
return Err(err!(
|
||||
"expected second digits after 'MM:', but found {rest:?} instead"
|
||||
));
|
||||
return Err(Error::from(E::ExpectedSecondAfterMinutes));
|
||||
}
|
||||
let seconds = parse::i64(second_digits.as_bytes())
|
||||
.map_err(|e| e.context("failed to parse seconds in time duration"))?;
|
||||
let seconds_ranged = t::Second::new(seconds).ok_or_else(|| {
|
||||
err!("duration seconds '{seconds:?}' is out of range")
|
||||
})?;
|
||||
let seconds =
|
||||
parse::i64(second_digits.as_bytes()).context(E::FailedParseSecond)?;
|
||||
let seconds_ranged = t::Second::try_new("seconds", seconds)?;
|
||||
span = span.seconds_ranged((seconds_ranged * sign).rinto());
|
||||
if rest.is_empty() {
|
||||
return Ok(span);
|
||||
|
|
@ -1298,33 +1243,24 @@ fn parse_span(span: &str) -> Result<Span, Error> {
|
|||
|
||||
// Now look for the fractional nanosecond component.
|
||||
if !rest.starts_with(".") {
|
||||
return Err(err!("expected '.' after seconds, but found {rest:?}"));
|
||||
return Err(Error::from(E::ExpectedDotAfterSeconds));
|
||||
}
|
||||
let rest = &rest[1..];
|
||||
let nanosecond_len =
|
||||
rest.chars().take_while(|c| c.is_ascii_digit()).count();
|
||||
let (nanosecond_digits, rest) = rest.split_at(nanosecond_len);
|
||||
if nanosecond_digits.is_empty() {
|
||||
return Err(err!(
|
||||
"expected nanosecond digits after 'SS.', \
|
||||
but found {rest:?} instead"
|
||||
));
|
||||
return Err(Error::from(E::ExpectedNanosecondDigits));
|
||||
}
|
||||
let nanoseconds =
|
||||
parse::fraction(nanosecond_digits.as_bytes()).map_err(|e| {
|
||||
e.context("failed to parse nanoseconds in time duration")
|
||||
})?;
|
||||
let nanoseconds_ranged = t::FractionalNanosecond::new(nanoseconds)
|
||||
.ok_or_else(|| {
|
||||
err!("duration nanoseconds '{nanoseconds:?}' is out of range")
|
||||
})?;
|
||||
let nanoseconds = parse::fraction(nanosecond_digits.as_bytes())
|
||||
.context(E::FailedParseNanosecond)?;
|
||||
let nanoseconds_ranged =
|
||||
t::FractionalNanosecond::try_new("nanoseconds", nanoseconds)?;
|
||||
span = span.nanoseconds_ranged((nanoseconds_ranged * sign).rinto());
|
||||
|
||||
// We should have consumed everything at this point.
|
||||
if !rest.is_empty() {
|
||||
return Err(err!(
|
||||
"found unrecognized trailing {rest:?} in time duration"
|
||||
));
|
||||
return Err(Error::from(E::UnrecognizedTrailingTimeDuration));
|
||||
}
|
||||
span.rebalance(Unit::Hour)
|
||||
}
|
||||
|
|
@ -1334,10 +1270,8 @@ fn parse_span(span: &str) -> Result<Span, Error> {
|
|||
/// This checks that the day is in the range 1-31, but otherwise doesn't
|
||||
/// check that it is valid for a particular month.
|
||||
fn parse_day(string: &str) -> Result<t::Day, Error> {
|
||||
let number = parse::i64(string.as_bytes())
|
||||
.map_err(|e| e.context("failed to parse number for day"))?;
|
||||
let day = t::Day::new(number)
|
||||
.ok_or_else(|| err!("{number} is not a valid day"))?;
|
||||
let number = parse::i64(string.as_bytes()).context(E::FailedParseDay)?;
|
||||
let day = t::Day::try_new("day", number)?;
|
||||
Ok(day)
|
||||
}
|
||||
|
||||
|
|
@ -1357,7 +1291,7 @@ fn parse_weekday(string: &str) -> Result<Weekday, Error> {
|
|||
return Ok(weekday);
|
||||
}
|
||||
}
|
||||
Err(err!("unrecognized day of the week: {string:?}"))
|
||||
Err(Error::from(E::UnrecognizedDayOfWeek))
|
||||
}
|
||||
|
||||
/// A parser that emits lines as sequences of fields.
|
||||
|
|
@ -1393,7 +1327,7 @@ impl<'a> FieldParser<'a> {
|
|||
/// This returns an error if the given bytes are not valid UTF-8.
|
||||
fn from_bytes(src: &'a [u8]) -> Result<FieldParser, Error> {
|
||||
let src = core::str::from_utf8(src)
|
||||
.map_err(|e| err!("invalid UTF-8: {e}"))?;
|
||||
.map_err(|_| Error::from(E::InvalidUtf8))?;
|
||||
Ok(FieldParser::new(src))
|
||||
}
|
||||
|
||||
|
|
@ -1409,12 +1343,10 @@ impl<'a> FieldParser<'a> {
|
|||
self.fields.clear();
|
||||
loop {
|
||||
let Some(mut line) = self.lines.next() else { return Ok(false) };
|
||||
self.line_number = self
|
||||
.line_number
|
||||
.checked_add(1)
|
||||
.ok_or_else(|| err!("line count overflowed"))?;
|
||||
self.line_number =
|
||||
self.line_number.checked_add(1).ok_or(E::LineOverflow)?;
|
||||
parse_fields(&line, &mut self.fields)
|
||||
.with_context(|| err!("line {}", self.line_number))?;
|
||||
.context(E::Line { number: self.line_number })?;
|
||||
if self.fields.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -1452,13 +1384,6 @@ fn parse_fields<'a>(
|
|||
matches!(ch, ' ' | '\x0C' | '\n' | '\r' | '\t' | '\x0B')
|
||||
}
|
||||
|
||||
// `man zic` says that the max line length including the line
|
||||
// terminator is 2048. The `core::str::Lines` iterator doesn't include
|
||||
// the terminator, so we subtract 1 to account for that. Note that this
|
||||
// could potentially allow one extra byte in the case of a \r\n line
|
||||
// terminator, but this seems fine.
|
||||
const MAX_LINE_LEN: usize = 2047;
|
||||
|
||||
// The different possible states of the field parser below.
|
||||
enum State {
|
||||
Whitespace,
|
||||
|
|
@ -1469,15 +1394,11 @@ fn parse_fields<'a>(
|
|||
|
||||
fields.clear();
|
||||
if line.len() > MAX_LINE_LEN {
|
||||
return Err(err!(
|
||||
"line with length {} exceeds \
|
||||
max length of {MAX_LINE_LEN}",
|
||||
line.len()
|
||||
));
|
||||
return Err(Error::from(E::LineMaxLength));
|
||||
}
|
||||
// Do a quick scan for a NUL terminator. They are illegal in all cases.
|
||||
if line.contains('\x00') {
|
||||
return Err(err!("found line with NUL byte, which isn't allowed"));
|
||||
return Err(Error::from(E::LineNul));
|
||||
}
|
||||
// The current state of the parser. We start at whitespace, since it also
|
||||
// means "before a field."
|
||||
|
|
@ -1522,9 +1443,8 @@ fn parse_fields<'a>(
|
|||
}
|
||||
State::AfterQuote => {
|
||||
if !is_space(ch) {
|
||||
return Err(err!(
|
||||
"expected whitespace after quoted field, \
|
||||
but found {ch:?} instead",
|
||||
return Err(Error::from(
|
||||
E::ExpectedWhitespaceAfterQuotedField,
|
||||
));
|
||||
}
|
||||
State::Whitespace
|
||||
|
|
@ -1537,7 +1457,7 @@ fn parse_fields<'a>(
|
|||
fields.push(&line[start..]);
|
||||
}
|
||||
State::InQuote => {
|
||||
return Err(err!("found unclosed quote"));
|
||||
return Err(Error::from(E::ExpectedCloseQuote));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
write!(f, "-")
|
||||
f.write_str("-")
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,15 +36,13 @@ impl Expiration {
|
|||
|
||||
impl core::fmt::Display for Expiration {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
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:?}")
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,121 @@ Provides convenience routines for escaping raw bytes.
|
|||
This was copied from `regex-automata` with a few light edits.
|
||||
*/
|
||||
|
||||
// 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};
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
use crate::{
|
||||
error::{err, Error},
|
||||
util::escape::{Byte, Bytes},
|
||||
};
|
||||
use crate::error::util::{ParseFractionError, ParseIntError};
|
||||
|
||||
/// Parses an `i64` number from the beginning to the end of the given slice of
|
||||
/// ASCII digit characters.
|
||||
|
|
@ -13,38 +10,20 @@ use crate::{
|
|||
/// integers, and because a higher level routine might want to parse the sign
|
||||
/// and then apply it to the result of this routine.)
|
||||
#[cfg_attr(feature = "perf-inline", inline(always))]
|
||||
pub(crate) fn i64(bytes: &[u8]) -> Result<i64, Error> {
|
||||
pub(crate) fn i64(bytes: &[u8]) -> Result<i64, ParseIntError> {
|
||||
if bytes.is_empty() {
|
||||
return Err(err!("invalid number, no digits found"));
|
||||
return Err(ParseIntError::NoDigitsFound);
|
||||
}
|
||||
let mut n: i64 = 0;
|
||||
for &byte in bytes {
|
||||
let digit = match byte.checked_sub(b'0') {
|
||||
None => {
|
||||
return Err(err!(
|
||||
"invalid digit, expected 0-9 but got {}",
|
||||
Byte(byte),
|
||||
));
|
||||
}
|
||||
Some(digit) if digit > 9 => {
|
||||
return Err(err!(
|
||||
"invalid digit, expected 0-9 but got {}",
|
||||
Byte(byte),
|
||||
))
|
||||
}
|
||||
Some(digit) => {
|
||||
debug_assert!((0..=9).contains(&digit));
|
||||
i64::from(digit)
|
||||
}
|
||||
};
|
||||
n = n.checked_mul(10).and_then(|n| n.checked_add(digit)).ok_or_else(
|
||||
|| {
|
||||
err!(
|
||||
"number '{}' too big to parse into 64-bit integer",
|
||||
Bytes(bytes),
|
||||
)
|
||||
},
|
||||
)?;
|
||||
if !(b'0' <= byte && byte <= b'9') {
|
||||
return Err(ParseIntError::InvalidDigit(byte));
|
||||
}
|
||||
let digit = i64::from(byte - b'0');
|
||||
n = n
|
||||
.checked_mul(10)
|
||||
.and_then(|n| n.checked_add(digit))
|
||||
.ok_or(ParseIntError::TooBig)?;
|
||||
}
|
||||
Ok(n)
|
||||
}
|
||||
|
|
@ -65,7 +44,9 @@ pub(crate) fn i64(bytes: &[u8]) -> Result<i64, Error> {
|
|||
///
|
||||
/// When the parsed integer cannot fit into a `u64`.
|
||||
#[cfg_attr(feature = "perf-inline", inline(always))]
|
||||
pub(crate) fn u64_prefix(bytes: &[u8]) -> Result<(Option<u64>, &[u8]), Error> {
|
||||
pub(crate) fn u64_prefix(
|
||||
bytes: &[u8],
|
||||
) -> Result<(Option<u64>, &[u8]), ParseIntError> {
|
||||
// Discovered via `u64::MAX.to_string().len()`.
|
||||
const MAX_U64_DIGITS: usize = 20;
|
||||
|
||||
|
|
@ -79,15 +60,10 @@ pub(crate) fn u64_prefix(bytes: &[u8]) -> Result<(Option<u64>, &[u8]), Error> {
|
|||
digit_count += 1;
|
||||
// OK because we confirmed `byte` is an ASCII digit.
|
||||
let digit = u64::from(byte - b'0');
|
||||
n = n.checked_mul(10).and_then(|n| n.checked_add(digit)).ok_or_else(
|
||||
#[inline(never)]
|
||||
|| {
|
||||
err!(
|
||||
"number `{}` too big to parse into 64-bit integer",
|
||||
Bytes(&bytes[..digit_count]),
|
||||
)
|
||||
},
|
||||
)?;
|
||||
n = n
|
||||
.checked_mul(10)
|
||||
.and_then(|n| n.checked_add(digit))
|
||||
.ok_or(ParseIntError::TooBig)?;
|
||||
}
|
||||
if digit_count == 0 {
|
||||
return Ok((None, bytes));
|
||||
|
|
@ -105,54 +81,33 @@ pub(crate) fn u64_prefix(bytes: &[u8]) -> Result<(Option<u64>, &[u8]), Error> {
|
|||
///
|
||||
/// If any byte in the given slice is not `[0-9]`, then this returns an error.
|
||||
/// Notably, this routine does not permit parsing a negative integer.
|
||||
pub(crate) fn fraction(bytes: &[u8]) -> Result<u32, Error> {
|
||||
const MAX_PRECISION: usize = 9;
|
||||
|
||||
pub(crate) fn fraction(bytes: &[u8]) -> Result<u32, ParseFractionError> {
|
||||
if bytes.is_empty() {
|
||||
return Err(err!("invalid fraction, no digits found"));
|
||||
} else if bytes.len() > MAX_PRECISION {
|
||||
return Err(err!(
|
||||
"invalid fraction, too many digits \
|
||||
(at most {MAX_PRECISION} are allowed"
|
||||
));
|
||||
return Err(ParseFractionError::NoDigitsFound);
|
||||
} else if bytes.len() > ParseFractionError::MAX_PRECISION {
|
||||
return Err(ParseFractionError::TooManyDigits);
|
||||
}
|
||||
let mut n: u32 = 0;
|
||||
for &byte in bytes {
|
||||
let digit = match byte.checked_sub(b'0') {
|
||||
None => {
|
||||
return Err(err!(
|
||||
"invalid fractional digit, expected 0-9 but got {}",
|
||||
Byte(byte),
|
||||
));
|
||||
return Err(ParseFractionError::InvalidDigit(byte));
|
||||
}
|
||||
Some(digit) if digit > 9 => {
|
||||
return Err(err!(
|
||||
"invalid fractional digit, expected 0-9 but got {}",
|
||||
Byte(byte),
|
||||
))
|
||||
return Err(ParseFractionError::InvalidDigit(byte));
|
||||
}
|
||||
Some(digit) => {
|
||||
debug_assert!((0..=9).contains(&digit));
|
||||
u32::from(digit)
|
||||
}
|
||||
};
|
||||
n = n.checked_mul(10).and_then(|n| n.checked_add(digit)).ok_or_else(
|
||||
|| {
|
||||
err!(
|
||||
"fractional '{}' too big to parse into 64-bit integer",
|
||||
Bytes(bytes),
|
||||
)
|
||||
},
|
||||
)?;
|
||||
n = n
|
||||
.checked_mul(10)
|
||||
.and_then(|n| n.checked_add(digit))
|
||||
.ok_or_else(|| ParseFractionError::TooBig)?;
|
||||
}
|
||||
for _ in bytes.len()..MAX_PRECISION {
|
||||
n = n.checked_mul(10).ok_or_else(|| {
|
||||
err!(
|
||||
"fractional '{}' too big to parse into 64-bit integer \
|
||||
(too much precision supported)",
|
||||
Bytes(bytes)
|
||||
)
|
||||
})?;
|
||||
for _ in bytes.len()..ParseFractionError::MAX_PRECISION {
|
||||
n = n.checked_mul(10).ok_or_else(|| ParseFractionError::TooBig)?;
|
||||
}
|
||||
Ok(n)
|
||||
}
|
||||
|
|
@ -161,15 +116,17 @@ pub(crate) fn fraction(bytes: &[u8]) -> Result<u32, Error> {
|
|||
///
|
||||
/// This is effectively `OsStr::to_str`, but with a slightly better error
|
||||
/// message.
|
||||
#[cfg(feature = "tzdb-zoneinfo")]
|
||||
pub(crate) fn os_str_utf8<'o, O>(os_str: &'o O) -> Result<&'o str, Error>
|
||||
#[cfg(any(feature = "tz-system", feature = "tzdb-zoneinfo"))]
|
||||
pub(crate) fn os_str_utf8<'o, O>(
|
||||
os_str: &'o O,
|
||||
) -> Result<&'o str, crate::error::util::OsStrUtf8Error>
|
||||
where
|
||||
O: ?Sized + AsRef<std::ffi::OsStr>,
|
||||
{
|
||||
let os_str = os_str.as_ref();
|
||||
os_str
|
||||
.to_str()
|
||||
.ok_or_else(|| err!("environment value {os_str:?} is not valid UTF-8"))
|
||||
.ok_or_else(|| crate::error::util::OsStrUtf8Error::from(os_str))
|
||||
}
|
||||
|
||||
/// Parses an `OsStr` into a `&str` when `&[u8]` isn't easily available.
|
||||
|
|
@ -178,7 +135,9 @@ where
|
|||
/// be a zero-cost conversion on Unix platforms to `&[u8]`. On Windows, this
|
||||
/// will do UTF-8 validation and return an error if it's invalid UTF-8.
|
||||
#[cfg(feature = "tz-system")]
|
||||
pub(crate) fn os_str_bytes<'o, O>(os_str: &'o O) -> Result<&'o [u8], Error>
|
||||
pub(crate) fn os_str_bytes<'o, O>(
|
||||
os_str: &'o O,
|
||||
) -> Result<&'o [u8], crate::error::util::OsStrUtf8Error>
|
||||
where
|
||||
O: ?Sized + AsRef<std::ffi::OsStr>,
|
||||
{
|
||||
|
|
@ -190,16 +149,13 @@ where
|
|||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
let string = os_str.to_str().ok_or_else(|| {
|
||||
err!("environment value {os_str:?} is not valid UTF-8")
|
||||
})?;
|
||||
// It is suspect that we're doing UTF-8 validation and then throwing
|
||||
// away the fact that we did UTF-8 validation. So this could lead
|
||||
// to an extra UTF-8 check if the caller ultimately needs UTF-8. If
|
||||
// that's important, we can add a new API that returns a `&str`. But it
|
||||
// probably won't matter because an `OsStr` in this crate is usually
|
||||
// just an environment variable.
|
||||
Ok(string.as_bytes())
|
||||
Ok(os_str_utf8(os_str)?.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue