Vendor dependencies

This commit is contained in:
2026-08-01 16:11:49 +03:00
parent 7f139a0241
commit 6b5e7f0f8b
29706 changed files with 9575646 additions and 0 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+800
View File
@@ -0,0 +1,800 @@
use jcore::civil::ISOWeekDate as JISOWeekDate;
use crate::{
civil::{Date, DateTime, Weekday},
error::Error,
fmt::temporal::{DEFAULT_DATETIME_PARSER, DEFAULT_DATETIME_PRINTER},
Zoned,
};
/// A type representing an [ISO 8601 week date].
///
/// The ISO 8601 week date scheme devises a calendar where days are identified
/// by their year, week number and weekday. All years have either precisely
/// 52 or 53 weeks.
///
/// The first week of an ISO 8601 year corresponds to the week containing the
/// first Thursday of the year. For this reason, an ISO 8601 week year can be
/// mismatched with the day's corresponding Gregorian year. For example, the
/// ISO 8601 week date for `1995-01-01` is `1994-W52-7` (with `7` corresponding
/// to Sunday).
///
/// ISO 8601 also considers Monday to be the start of the week, and uses
/// a 1-based numbering system. That is, Monday corresponds to `1` while
/// Sunday corresponds to `7` and is the last day of the week. Weekdays are
/// encapsulated by the [`Weekday`] type, which provides routines for easily
/// converting between different schemes (such as weeks where Sunday is the
/// beginning).
///
/// [ISO 8601 week date]: https://en.wikipedia.org/wiki/ISO_week_date
///
/// # Use case
///
/// Some domains use this method of timekeeping. Otherwise, unless you
/// 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
/// value is the first day of the zeroth year. i.e., `0000-W1-1`.
///
/// # Example: sample dates
///
/// This example shows a couple ISO 8601 week dates and their corresponding
/// Gregorian equivalents:
///
/// ```
/// use jiff::civil::{ISOWeekDate, Weekday, date};
///
/// let d = date(2019, 12, 30);
/// let weekdate = ISOWeekDate::new(2020, 1, Weekday::Monday).unwrap();
/// assert_eq!(d.iso_week_date(), weekdate);
///
/// let d = date(2024, 3, 9);
/// let weekdate = ISOWeekDate::new(2024, 10, Weekday::Saturday).unwrap();
/// assert_eq!(d.iso_week_date(), weekdate);
/// ```
///
/// # Example: overlapping leap and long years
///
/// A "long" ISO 8601 week year is a year with 53 weeks. That is, it is a year
/// that includes a leap week. This example shows all years in the 20th
/// century that are both Gregorian leap years and long years.
///
/// ```
/// use jiff::civil::date;
///
/// let mut overlapping = vec![];
/// for year in 1900..=1999 {
/// let date = date(year, 1, 1);
/// if date.in_leap_year() && date.iso_week_date().in_long_year() {
/// overlapping.push(year);
/// }
/// }
/// assert_eq!(overlapping, vec![
/// 1904, 1908, 1920, 1932, 1936, 1948, 1960, 1964, 1976, 1988, 1992,
/// ]);
/// ```
///
/// # Example: printing all weeks in a year
///
/// The ISO 8601 week calendar can be useful when you want to categorize
/// things into buckets of weeks where all weeks are exactly 7 days, _and_
/// you don't care as much about the precise Gregorian year. Here's an example
/// that prints all of the ISO 8601 weeks in one ISO 8601 week year:
///
/// ```
/// use jiff::{civil::{ISOWeekDate, Weekday}, ToSpan};
///
/// let target_year = 2024;
/// let iso_week_date = ISOWeekDate::new(target_year, 1, Weekday::Monday)?;
/// // Create a series of dates via the Gregorian calendar. But since a
/// // Gregorian week and an ISO 8601 week calendar week are both 7 days,
/// // this works fine.
/// let weeks = iso_week_date
/// .date()
/// .series(1.week())
/// .map(|d| d.iso_week_date())
/// .take_while(|wd| wd.year() == target_year);
/// for start_of_week in weeks {
/// let end_of_week = start_of_week.last_of_week()?;
/// println!(
/// "ISO week {}: {} - {}",
/// start_of_week.week(),
/// start_of_week.date(),
/// end_of_week.date()
/// );
/// }
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub struct ISOWeekDate {
pub(crate) inner: JISOWeekDate,
}
impl ISOWeekDate {
/// The maximum representable ISO week date.
///
/// The maximum corresponds to the ISO week date of the maximum [`Date`]
/// value. That is, `-9999-01-01`.
pub const MIN: ISOWeekDate = ISOWeekDate { inner: JISOWeekDate::MIN };
/// The minimum representable ISO week date.
///
/// The minimum corresponds to the ISO week date of the minimum [`Date`]
/// value. That is, `9999-12-31`.
pub const MAX: ISOWeekDate = ISOWeekDate { inner: JISOWeekDate::MAX };
/// The first day of the zeroth year.
///
/// This is guaranteed to be equivalent to `ISOWeekDate::default()`. Note
/// that this is not equivalent to `Date::default()`.
///
/// # Example
///
/// ```
/// use jiff::civil::{ISOWeekDate, date};
///
/// assert_eq!(ISOWeekDate::ZERO, ISOWeekDate::default());
/// // The first day of the 0th year in the ISO week calendar is actually
/// // the third day of the 0th year in the proleptic Gregorian calendar!
/// assert_eq!(ISOWeekDate::default().date(), date(0, 1, 3));
/// ```
pub const ZERO: ISOWeekDate = ISOWeekDate { inner: JISOWeekDate::ZERO };
/// Create a new ISO week date from it constituent parts.
///
/// If the given values are out of range (based on what is representable
/// as a [`Date`]), then this returns an error. This will also return an
/// error if a leap week is given (week number `53`) for a year that does
/// not contain a leap week.
///
/// # Example
///
/// This example shows some the boundary conditions involving minimum
/// and maximum dates:
///
/// ```
/// use jiff::civil::{ISOWeekDate, Weekday, date};
///
/// // The year 1949 does not contain a leap week.
/// assert!(ISOWeekDate::new(1949, 53, Weekday::Monday).is_err());
///
/// // Examples of dates at or exceeding the maximum.
/// let max = ISOWeekDate::new(9999, 52, Weekday::Friday).unwrap();
/// assert_eq!(max, ISOWeekDate::MAX);
/// assert_eq!(max.date(), date(9999, 12, 31));
/// assert!(ISOWeekDate::new(9999, 52, Weekday::Saturday).is_err());
/// assert!(ISOWeekDate::new(9999, 53, Weekday::Monday).is_err());
///
/// // Examples of dates at or exceeding the minimum.
/// let min = ISOWeekDate::new(-9999, 1, Weekday::Monday).unwrap();
/// assert_eq!(min, ISOWeekDate::MIN);
/// assert_eq!(min.date(), date(-9999, 1, 1));
/// assert!(ISOWeekDate::new(-10000, 52, Weekday::Sunday).is_err());
/// ```
#[inline]
pub fn new(
year: i16,
week: i8,
weekday: Weekday,
) -> Result<ISOWeekDate, Error> {
JISOWeekDate::new(year, week, weekday.to_jcore())
.map(ISOWeekDate::from_jcore)
.map_err(Error::jcore_range)
}
/// Converts a Gregorian date to an ISO week date.
///
/// The minimum and maximum allowed values of an ISO week date are
/// set based on the minimum and maximum values of a `Date`. Therefore,
/// converting to and from `Date` values is non-lossy and infallible.
///
/// This routine is equivalent to [`Date::iso_week_date`]. This routine
/// is also available via a `From<Date>` trait implementation for
/// `ISOWeekDate`.
///
/// # Example
///
/// ```
/// use jiff::civil::{ISOWeekDate, Weekday, date};
///
/// let weekdate = ISOWeekDate::from_date(date(1948, 2, 10));
/// assert_eq!(
/// weekdate,
/// ISOWeekDate::new(1948, 7, Weekday::Tuesday).unwrap(),
/// );
/// ```
#[inline]
pub fn from_date(date: Date) -> ISOWeekDate {
date.iso_week_date()
}
// N.B. I tried defining a `ISOWeekDate::constant` for defining ISO week
// dates as constants, but it was too annoying to do. We could do it if
// there was a compelling reason for it though.
/// Returns the year component of this ISO 8601 week date.
///
/// The value returned is guaranteed to be in the range `-9999..=9999`.
///
/// # Example
///
/// ```
/// use jiff::civil::date;
///
/// let weekdate = date(2019, 12, 30).iso_week_date();
/// assert_eq!(weekdate.year(), 2020);
/// ```
#[inline]
pub fn year(self) -> i16 {
self.inner.year()
}
/// Returns the week component of this ISO 8601 week date.
///
/// The value returned is guaranteed to be in the range `1..=53`. A
/// value of `53` can only occur for "long" years. That is, years
/// with a leap week. This occurs precisely in cases for which
/// [`ISOWeekDate::in_long_year`] returns `true`.
///
/// # Example
///
/// ```
/// use jiff::civil::date;
///
/// let weekdate = date(2019, 12, 30).iso_week_date();
/// assert_eq!(weekdate.year(), 2020);
/// assert_eq!(weekdate.week(), 1);
///
/// let weekdate = date(1948, 12, 31).iso_week_date();
/// assert_eq!(weekdate.year(), 1948);
/// assert_eq!(weekdate.week(), 53);
/// ```
#[inline]
pub fn week(self) -> i8 {
self.inner.week()
}
/// Returns the day component of this ISO 8601 week date.
///
/// One can use methods on `Weekday` such as
/// [`Weekday::to_monday_one_offset`]
/// and
/// [`Weekday::to_sunday_zero_offset`]
/// to convert the weekday to a number.
///
/// # Example
///
/// ```
/// use jiff::civil::{date, Weekday};
///
/// let weekdate = date(1948, 12, 31).iso_week_date();
/// assert_eq!(weekdate.year(), 1948);
/// assert_eq!(weekdate.week(), 53);
/// assert_eq!(weekdate.weekday(), Weekday::Friday);
/// assert_eq!(weekdate.weekday().to_monday_zero_offset(), 4);
/// assert_eq!(weekdate.weekday().to_monday_one_offset(), 5);
/// assert_eq!(weekdate.weekday().to_sunday_zero_offset(), 5);
/// assert_eq!(weekdate.weekday().to_sunday_one_offset(), 6);
/// ```
#[inline]
pub fn weekday(self) -> Weekday {
Weekday::from_jcore(self.inner.weekday())
}
/// Returns the ISO 8601 week date corresponding to the first day in the
/// week of this week date. The date returned is guaranteed to have a
/// weekday of [`Weekday::Monday`].
///
/// # Errors
///
/// Since `-9999-01-01` falls on a Monday, it follows that the minimum
/// supported Gregorian date is exactly equivalent to the minimum supported
/// ISO 8601 week date. This means that this routine can never actually
/// fail, but only insomuch as the minimums line up. For that reason, and
/// for consistency with [`ISOWeekDate::last_of_week`], the API is
/// fallible.
///
/// # Example
///
/// ```
/// use jiff::civil::{ISOWeekDate, Weekday, date};
///
/// let wd = ISOWeekDate::new(2025, 5, Weekday::Wednesday).unwrap();
/// assert_eq!(wd.date(), date(2025, 1, 29));
/// assert_eq!(
/// wd.first_of_week()?,
/// ISOWeekDate::new(2025, 5, Weekday::Monday).unwrap(),
/// );
///
/// // Works even for the minimum date.
/// assert_eq!(
/// ISOWeekDate::MIN.first_of_week()?,
/// ISOWeekDate::new(-9999, 1, Weekday::Monday).unwrap(),
/// );
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
#[inline]
pub fn first_of_week(self) -> Result<ISOWeekDate, Error> {
self.inner
.first_of_week()
.map(ISOWeekDate::from_jcore)
.map_err(Error::jcore_range)
}
/// Returns the ISO 8601 week date corresponding to the last day in the
/// week of this week date. The date returned is guaranteed to have a
/// weekday of [`Weekday::Sunday`].
///
/// # Errors
///
/// This can return an error if the last day of the week exceeds Jiff's
/// maximum Gregorian date of `9999-12-31`. It turns out this can happen
/// since `9999-12-31` falls on a Friday.
///
/// # Example
///
/// ```
/// use jiff::civil::{ISOWeekDate, Weekday, date};
///
/// let wd = ISOWeekDate::new(2025, 5, Weekday::Wednesday).unwrap();
/// assert_eq!(wd.date(), date(2025, 1, 29));
/// assert_eq!(
/// wd.last_of_week()?,
/// ISOWeekDate::new(2025, 5, Weekday::Sunday).unwrap(),
/// );
///
/// // Unlike `first_of_week`, this routine can actually fail on real
/// // values, although, only when close to the maximum supported date.
/// assert_eq!(
/// ISOWeekDate::MAX.last_of_week().unwrap_err().to_string(),
/// "parameter 'weekday (Monday 1-indexed)' \
/// is not in the required range of 1..=7",
/// );
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
#[inline]
pub fn last_of_week(self) -> Result<ISOWeekDate, Error> {
self.inner
.last_of_week()
.map(ISOWeekDate::from_jcore)
.map_err(Error::jcore_range)
}
/// Returns the ISO 8601 week date corresponding to the first day in the
/// year of this week date. The date returned is guaranteed to have a
/// weekday of [`Weekday::Monday`].
///
/// # Errors
///
/// Since `-9999-01-01` falls on a Monday, it follows that the minimum
/// support Gregorian date is exactly equivalent to the minimum supported
/// ISO 8601 week date. This means that this routine can never actually
/// fail, but only insomuch as the minimums line up. For that reason, and
/// for consistency with [`ISOWeekDate::last_of_year`], the API is
/// fallible.
///
/// # Example
///
/// ```
/// use jiff::civil::{ISOWeekDate, Weekday, date};
///
/// let wd = ISOWeekDate::new(2025, 5, Weekday::Wednesday).unwrap();
/// assert_eq!(wd.date(), date(2025, 1, 29));
/// assert_eq!(
/// wd.first_of_year()?,
/// ISOWeekDate::new(2025, 1, Weekday::Monday).unwrap(),
/// );
///
/// // Works even for the minimum date.
/// assert_eq!(
/// ISOWeekDate::MIN.first_of_year()?,
/// ISOWeekDate::new(-9999, 1, Weekday::Monday).unwrap(),
/// );
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
#[inline]
pub fn first_of_year(self) -> Result<ISOWeekDate, Error> {
self.inner
.first_of_year()
.map(ISOWeekDate::from_jcore)
.map_err(Error::jcore_range)
}
/// Returns the ISO 8601 week date corresponding to the last day in the
/// year of this week date. The date returned is guaranteed to have a
/// weekday of [`Weekday::Sunday`].
///
/// # Errors
///
/// This can return an error if the last day of the year exceeds Jiff's
/// maximum Gregorian date of `9999-12-31`. It turns out this can happen
/// since `9999-12-31` falls on a Friday.
///
/// # Example
///
/// ```
/// use jiff::civil::{ISOWeekDate, Weekday, date};
///
/// let wd = ISOWeekDate::new(2025, 5, Weekday::Wednesday).unwrap();
/// assert_eq!(wd.date(), date(2025, 1, 29));
/// assert_eq!(
/// wd.last_of_year()?,
/// ISOWeekDate::new(2025, 52, Weekday::Sunday).unwrap(),
/// );
///
/// // Works correctly for "long" years.
/// let wd = ISOWeekDate::new(2026, 5, Weekday::Wednesday).unwrap();
/// assert_eq!(wd.date(), date(2026, 1, 28));
/// assert_eq!(
/// wd.last_of_year()?,
/// ISOWeekDate::new(2026, 53, Weekday::Sunday).unwrap(),
/// );
///
/// // Unlike `first_of_year`, this routine can actually fail on real
/// // values, although, only when close to the maximum supported date.
/// assert_eq!(
/// ISOWeekDate::MAX.last_of_year().unwrap_err().to_string(),
/// "parameter 'weekday (Monday 1-indexed)' \
/// is not in the required range of 1..=7",
/// );
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
#[inline]
pub fn last_of_year(self) -> Result<ISOWeekDate, Error> {
self.inner
.last_of_year()
.map(ISOWeekDate::from_jcore)
.map_err(Error::jcore_range)
}
/// Returns the total number of days in the year of this ISO 8601 week
/// date.
///
/// It is guaranteed that the value returned is either 364 or 371. The
/// latter case occurs precisely when [`ISOWeekDate::in_long_year`]
/// returns `true`.
///
/// # Example
///
/// ```
/// use jiff::civil::{ISOWeekDate, Weekday};
///
/// let weekdate = ISOWeekDate::new(2025, 7, Weekday::Monday).unwrap();
/// assert_eq!(weekdate.days_in_year(), 364);
/// let weekdate = ISOWeekDate::new(2026, 7, Weekday::Monday).unwrap();
/// assert_eq!(weekdate.days_in_year(), 371);
/// ```
#[inline]
pub fn days_in_year(self) -> i16 {
self.inner.days_in_year()
}
/// Returns the total number of weeks in the year of this ISO 8601 week
/// date.
///
/// It is guaranteed that the value returned is either 52 or 53. The
/// latter case occurs precisely when [`ISOWeekDate::in_long_year`]
/// returns `true`.
///
/// # Example
///
/// ```
/// use jiff::civil::{ISOWeekDate, Weekday};
///
/// let weekdate = ISOWeekDate::new(2025, 7, Weekday::Monday).unwrap();
/// assert_eq!(weekdate.weeks_in_year(), 52);
/// let weekdate = ISOWeekDate::new(2026, 7, Weekday::Monday).unwrap();
/// assert_eq!(weekdate.weeks_in_year(), 53);
/// ```
#[inline]
pub fn weeks_in_year(self) -> i8 {
self.inner.weeks_in_year()
}
/// Returns true if and only if the year of this week date is a "long"
/// year.
///
/// A long year is one that contains precisely 53 weeks. All other years
/// contain precisely 52 weeks.
///
/// # Example
///
/// ```
/// use jiff::civil::{ISOWeekDate, Weekday};
///
/// let weekdate = ISOWeekDate::new(1948, 7, Weekday::Monday).unwrap();
/// assert!(weekdate.in_long_year());
/// let weekdate = ISOWeekDate::new(1949, 7, Weekday::Monday).unwrap();
/// assert!(!weekdate.in_long_year());
/// ```
#[inline]
pub fn in_long_year(self) -> bool {
self.inner.in_long_year()
}
/// Returns the ISO 8601 date immediately following this one.
///
/// # Errors
///
/// This returns an error when this date is the maximum value.
///
/// # Example
///
/// ```
/// use jiff::civil::{ISOWeekDate, Weekday};
///
/// let wd = ISOWeekDate::new(2025, 5, Weekday::Wednesday).unwrap();
/// assert_eq!(
/// wd.tomorrow()?,
/// ISOWeekDate::new(2025, 5, Weekday::Thursday).unwrap(),
/// );
///
/// // The max doesn't have a tomorrow.
/// assert!(ISOWeekDate::MAX.tomorrow().is_err());
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
#[inline]
pub fn tomorrow(self) -> Result<ISOWeekDate, Error> {
self.inner
.tomorrow()
.map(ISOWeekDate::from_jcore)
.map_err(Error::jcore_range)
}
/// Returns the ISO 8601 week date immediately preceding this one.
///
/// # Errors
///
/// This returns an error when this date is the minimum value.
///
/// # Example
///
/// ```
/// use jiff::civil::{ISOWeekDate, Weekday};
///
/// let wd = ISOWeekDate::new(2025, 5, Weekday::Wednesday).unwrap();
/// assert_eq!(
/// wd.yesterday()?,
/// ISOWeekDate::new(2025, 5, Weekday::Tuesday).unwrap(),
/// );
///
/// // The min doesn't have a yesterday.
/// assert!(ISOWeekDate::MIN.yesterday().is_err());
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
#[inline]
pub fn yesterday(self) -> Result<ISOWeekDate, Error> {
self.inner
.yesterday()
.map(ISOWeekDate::from_jcore)
.map_err(Error::jcore_range)
}
/// Converts this ISO week date to a Gregorian [`Date`].
///
/// The minimum and maximum allowed values of an ISO week date are
/// set based on the minimum and maximum values of a `Date`. Therefore,
/// converting to and from `Date` values is non-lossy and infallible.
///
/// This routine is equivalent to [`Date::from_iso_week_date`].
///
/// # Example
///
/// ```
/// use jiff::civil::{ISOWeekDate, Weekday, date};
///
/// let weekdate = ISOWeekDate::new(1948, 7, Weekday::Tuesday).unwrap();
/// assert_eq!(weekdate.date(), date(1948, 2, 10));
/// ```
#[inline]
pub fn date(self) -> Date {
Date::from_iso_week_date(self)
}
#[inline]
pub(crate) const fn from_jcore(week_date: JISOWeekDate) -> ISOWeekDate {
ISOWeekDate { inner: week_date }
}
#[inline]
pub(crate) const fn to_jcore(self) -> JISOWeekDate {
self.inner
}
}
impl Default for ISOWeekDate {
fn default() -> ISOWeekDate {
ISOWeekDate::ZERO
}
}
impl core::fmt::Debug for ISOWeekDate {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
core::fmt::Display::fmt(self, f)
}
}
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 Ord for ISOWeekDate {
#[inline]
fn cmp(&self, other: &ISOWeekDate) -> core::cmp::Ordering {
(self.year(), self.week(), self.weekday().to_monday_one_offset()).cmp(
&(
other.year(),
other.week(),
other.weekday().to_monday_one_offset(),
),
)
}
}
impl PartialOrd for ISOWeekDate {
#[inline]
fn partial_cmp(&self, other: &ISOWeekDate) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl From<Date> for ISOWeekDate {
#[inline]
fn from(date: Date) -> ISOWeekDate {
ISOWeekDate::from_date(date)
}
}
impl From<DateTime> for ISOWeekDate {
#[inline]
fn from(dt: DateTime) -> ISOWeekDate {
ISOWeekDate::from(dt.date())
}
}
impl From<Zoned> for ISOWeekDate {
#[inline]
fn from(zdt: Zoned) -> ISOWeekDate {
ISOWeekDate::from(zdt.date())
}
}
impl<'a> From<&'a Zoned> for ISOWeekDate {
#[inline]
fn from(zdt: &'a Zoned) -> ISOWeekDate {
ISOWeekDate::from(zdt.date())
}
}
#[cfg(feature = "defmt")]
impl defmt::Format for ISOWeekDate {
fn format(&self, f: defmt::Formatter) {
use crate::fmt::DefmtWrite;
defmt::unwrap!(
DEFAULT_DATETIME_PRINTER.print_iso_week_date(self, DefmtWrite(f))
);
}
}
#[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)
}
}
+291
View File
@@ -0,0 +1,291 @@
/*!
Facilities for dealing with inexact dates and times.
# Overview
The essential types in this module are:
* [`Date`] is a specific day in the Gregorian calendar.
* [`Time`] is a specific wall clock time.
* [`DateTime`] is a combination of a day and a time.
Moreover, the [`date`](date()) and [`time`](time()) free functions can be used
to conveniently create values of any of three types above:
```
use jiff::civil::{date, time};
assert_eq!(date(2024, 7, 31).to_string(), "2024-07-31");
assert_eq!(time(15, 20, 0, 123).to_string(), "15:20:00.000000123");
assert_eq!(
date(2024, 7, 31).at(15, 20, 0, 123).to_string(),
"2024-07-31T15:20:00.000000123",
);
assert_eq!(
time(15, 20, 0, 123).on(2024, 7, 31).to_string(),
"2024-07-31T15:20:00.000000123",
);
```
# What is "civil" time?
A civil datetime is a calendar date and a clock time. It also goes by the
names "naive," "local" or "plain." The most important thing to understand
about civil time is that it does not correspond to a precise instant in
time. This is in contrast to types like [`Timestamp`](crate::Timestamp) and
[`Zoned`](crate::Zoned), which _do_ correspond to a precise instant in time (to
nanosecond precision).
Because a civil datetime _never_ has a time zone associated with it, and
because some time zones have transitions that skip or repeat clock times, it
follows that not all civil datetimes precisely map to a single instant in time.
For example, `2024-03-10 02:30` never existed on a clock in `America/New_York`
because the 2 o'clock hour was skipped when the clocks were "moved forward"
for daylight saving time. Conversely, `2024-11-03 01:30` occurred twice in
`America/New_York` because the 1 o'clock hour was repeated when clocks were
"moved backward" for daylight saving time. (When time is skipped, it's called a
"gap." When time is repeated, it's called a "fold.")
In contrast, an instant in time (that is, `Timestamp` or `Zoned`) can _always_
be converted to a civil datetime. And, when a civil datetime is combined
with its time zone identifier _and_ its offset, the resulting machine readable
string is unambiguous 100% of the time:
```
use jiff::{civil::date, tz::TimeZone};
let tz = TimeZone::get("America/New_York")?;
let dt = date(2024, 11, 3).at(1, 30, 0, 0);
// It's ambiguous, so asking for an unambiguous instant presents an error!
assert!(tz.to_ambiguous_zoned(dt).unambiguous().is_err());
// Gives you the earlier time in a fold, i.e., before DST ends:
assert_eq!(
tz.to_ambiguous_zoned(dt).earlier()?.to_string(),
"2024-11-03T01:30:00-04:00[America/New_York]",
);
// Gives you the later time in a fold, i.e., after DST ends.
// Notice the offset change from the previous example!
assert_eq!(
tz.to_ambiguous_zoned(dt).later()?.to_string(),
"2024-11-03T01:30:00-05:00[America/New_York]",
);
// "Just give me something reasonable"
assert_eq!(
tz.to_ambiguous_zoned(dt).compatible()?.to_string(),
"2024-11-03T01:30:00-04:00[America/New_York]",
);
# Ok::<(), Box<dyn std::error::Error>>(())
```
# When should I use civil time?
Here is a likely non-exhaustive list of reasons why you might want to use
civil time:
* When you want or need to deal with calendar and clock units as an
intermediate step before and/or after associating it with a time zone. For
example, perhaps you need to parse strings like `2000-01-01T00:00:00` from a
CSV file that have no time zone or offset information, but the time zone is
implied through some out-of-band mechanism.
* When time zone is actually irrelevant. For example, a fitness tracking app
that reminds you to work-out at 6am local time, regardless of which time zone
you're in.
* When you need to perform arithmetic that deliberately ignores daylight
saving time.
* When interacting with legacy systems or systems that specifically do not
support time zones.
*/
pub use self::{
date::{Date, DateArithmetic, DateDifference, DateSeries, DateWith},
datetime::{
DateTime, DateTimeArithmetic, DateTimeDifference, DateTimeRound,
DateTimeSeries, DateTimeWith,
},
iso_week_date::ISOWeekDate,
time::{
Time, TimeArithmetic, TimeDifference, TimeRound, TimeSeries, TimeWith,
},
weekday::{Weekday, WeekdaysForward, WeekdaysReverse},
};
mod date;
mod datetime;
mod iso_week_date;
mod time;
mod weekday;
/// The era corresponding to a particular year.
///
/// The BCE era corresponds to years less than or equal to `0`, while the CE
/// era corresponds to years greater than `0`.
///
/// In particular, this crate allows years to be negative and also to be `0`,
/// which is contrary to the common practice of excluding the year `0` when
/// writing dates for the Gregorian calendar. Moreover, common practice eschews
/// negative years in favor of labeling a year with an era notation. That is,
/// the year `1 BCE` is year `0` in this crate. The year `2 BCE` is the year
/// `-1` in this crate.
///
/// To get the year in its era format, use [`Date::era_year`].
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum Era {
/// The "before common era" era.
///
/// This corresponds to all years less than or equal to `0`.
///
/// This is precisely equivalent to the "BC" or "before Christ" era.
BCE,
/// The "common era" era.
///
/// This corresponds to all years greater than `0`.
///
/// This is precisely equivalent to the "AD" or "anno Domini" or "in the
/// year of the Lord" era.
CE,
}
/// Creates a new `DateTime` value in a `const` context.
///
/// This is a convenience free function for [`DateTime::constant`]. It is
/// intended to provide a terse syntax for constructing `DateTime` values from
/// parameters that are known to be valid.
///
/// # Panics
///
/// This routine panics when [`DateTime::new`] would return an error. That
/// is, when the given components do not correspond to a valid datetime.
/// Namely, all of the following must be true:
///
/// * The year must be in the range `-9999..=9999`.
/// * The month must be in the range `1..=12`.
/// * The day must be at least `1` and must be at most the number of days
/// in the corresponding month. So for example, `2024-02-29` is valid but
/// `2023-02-29` is not.
/// * `0 <= hour <= 23`
/// * `0 <= minute <= 59`
/// * `0 <= second <= 59`
/// * `0 <= subsec_nanosecond <= 999,999,999`
///
/// Similarly, when used in a const context, invalid parameters will prevent
/// your Rust program from compiling.
///
/// # Example
///
/// ```
/// use jiff::civil::datetime;
///
/// let dt = datetime(2024, 2, 29, 21, 30, 5, 123_456_789);
/// assert_eq!(dt.date().year(), 2024);
/// assert_eq!(dt.date().month(), 2);
/// assert_eq!(dt.date().day(), 29);
/// assert_eq!(dt.time().hour(), 21);
/// assert_eq!(dt.time().minute(), 30);
/// assert_eq!(dt.time().second(), 5);
/// assert_eq!(dt.time().millisecond(), 123);
/// assert_eq!(dt.time().microsecond(), 456);
/// assert_eq!(dt.time().nanosecond(), 789);
/// ```
#[inline]
pub const fn datetime(
year: i16,
month: i8,
day: i8,
hour: i8,
minute: i8,
second: i8,
subsec_nanosecond: i32,
) -> DateTime {
DateTime::constant(
year,
month,
day,
hour,
minute,
second,
subsec_nanosecond,
)
}
/// Creates a new `Date` value in a `const` context.
///
/// This is a convenience free function for [`Date::constant`]. It is intended
/// to provide a terse syntax for constructing `Date` values from parameters
/// that are known to be valid.
///
/// # Panics
///
/// This routine panics when [`Date::new`] would return an error. That is,
/// when the given year-month-day does not correspond to a valid date.
/// Namely, all of the following must be true:
///
/// * The year must be in the range `-9999..=9999`.
/// * The month must be in the range `1..=12`.
/// * The day must be at least `1` and must be at most the number of days
/// in the corresponding month. So for example, `2024-02-29` is valid but
/// `2023-02-29` is not.
///
/// Similarly, when used in a const context, invalid parameters will prevent
/// your Rust program from compiling.
///
/// # Example
///
/// ```
/// use jiff::civil::date;
///
/// let d = date(2024, 2, 29);
/// assert_eq!(d.year(), 2024);
/// assert_eq!(d.month(), 2);
/// assert_eq!(d.day(), 29);
/// ```
#[inline]
pub const fn date(year: i16, month: i8, day: i8) -> Date {
Date::constant(year, month, day)
}
/// Creates a new `Time` value in a `const` context.
///
/// This is a convenience free function for [`Time::constant`]. It is intended
/// to provide a terse syntax for constructing `Time` values from parameters
/// that are known to be valid.
///
/// # Panics
///
/// This panics if the given values do not correspond to a valid `Time`.
/// All of the following conditions must be true:
///
/// * `0 <= hour <= 23`
/// * `0 <= minute <= 59`
/// * `0 <= second <= 59`
/// * `0 <= subsec_nanosecond <= 999,999,999`
///
/// Similarly, when used in a const context, invalid parameters will
/// prevent your Rust program from compiling.
///
/// # Example
///
/// This shows an example of a valid time in a `const` context:
///
/// ```
/// use jiff::civil::time;
///
/// let t = time(21, 30, 5, 123_456_789);
/// assert_eq!(t.hour(), 21);
/// assert_eq!(t.minute(), 30);
/// assert_eq!(t.second(), 5);
/// assert_eq!(t.millisecond(), 123);
/// assert_eq!(t.microsecond(), 456);
/// assert_eq!(t.nanosecond(), 789);
/// assert_eq!(t.subsec_nanosecond(), 123_456_789);
/// ```
#[inline]
pub const fn time(
hour: i8,
minute: i8,
second: i8,
subsec_nanosecond: i32,
) -> Time {
Time::constant(hour, minute, second, subsec_nanosecond)
}
File diff suppressed because it is too large Load Diff
+754
View File
@@ -0,0 +1,754 @@
use crate::error::Error;
/// A representation for the day of the week.
///
/// The default representation follows ISO 8601. That is, the week starts with
/// Monday and numbering starts at `1`. However, the various constructors and
/// accessors support using other schemes in wide use:
///
/// * [`Weekday::from_monday_zero_offset`] builds a weekday from
/// a scheme that starts the week on Monday at offset `0`, while
/// [`Weekday::to_monday_zero_offset`] converts to it.
/// * [`Weekday::from_monday_one_offset`] builds a weekday from a scheme
/// that starts the week on Monday at offset `1` (the default representation),
/// while [`Weekday::to_monday_one_offset`] converts to it.
/// * [`Weekday::from_sunday_zero_offset`] builds a weekday from
/// a scheme that starts the week on Sunday at offset `0`, while
/// [`Weekday::to_sunday_zero_offset`] converts to it.
/// * [`Weekday::from_sunday_one_offset`] builds a weekday from
/// a scheme that starts the week on Sunday at offset `1`, while
/// [`Weekday::to_sunday_one_offset`] converts to it.
///
/// # Arithmetic
///
/// This type provides [`Weekday::wrapping_add`] and [`Weekday::wrapping_sub`]
/// for performing wrapping arithmetic on weekdays. These are also available
/// via operator overloading:
///
/// ```
/// use jiff::civil::Weekday;
///
/// assert_eq!(Weekday::Monday + 1, Weekday::Tuesday);
/// assert_eq!(Weekday::Sunday - 1, Weekday::Saturday);
/// ```
///
/// # Comparisons
///
/// This type provides `Eq` and `PartialEq` trait implementations for easy
/// comparison:
///
/// ```
/// use jiff::civil::Weekday;
///
/// assert_eq!(Weekday::Wednesday, Weekday::Wednesday + 7);
/// assert_ne!(Weekday::Thursday, Weekday::Friday);
/// ```
///
/// But this type specifically does not provide `Ord` or `PartialOrd` trait
/// implementations. Such an implementation would require deciding whether
/// Sunday is less than Monday or greater than Monday. The former case
/// corresponds to a week scheme where Sunday is the first day in the week,
/// where as the latter corresponds to a scheme where Monday is the first day.
/// Since both schemes are in widespread use, it would be inappropriate to bake
/// in an assumption of one or the other. Instead, one can convert a weekday
/// into the desired offset scheme, and then compare the offsets:
///
/// ```
/// use jiff::civil::Weekday;
///
/// let (sun, mon) = (Weekday::Sunday, Weekday::Monday);
/// assert!(sun.to_sunday_zero_offset() < mon.to_sunday_zero_offset());
/// assert!(sun.to_monday_zero_offset() > mon.to_monday_zero_offset());
/// ```
///
/// # Example
///
/// This example shows the result of converting to and from different schemes:
///
/// ```
/// use jiff::civil::Weekday;
///
/// // The different representations of Monday.
/// assert_eq!(Weekday::Monday.to_monday_zero_offset(), 0);
/// assert_eq!(Weekday::Monday.to_monday_one_offset(), 1);
/// assert_eq!(Weekday::Monday.to_sunday_zero_offset(), 1);
/// assert_eq!(Weekday::Monday.to_sunday_one_offset(), 2);
///
/// // The different representations of Sunday.
/// assert_eq!(Weekday::Sunday.to_monday_zero_offset(), 6);
/// assert_eq!(Weekday::Sunday.to_monday_one_offset(), 7);
/// assert_eq!(Weekday::Sunday.to_sunday_zero_offset(), 0);
/// assert_eq!(Weekday::Sunday.to_sunday_one_offset(), 1);
/// ```
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
#[allow(missing_docs)]
pub enum Weekday {
Monday = 1,
Tuesday = 2,
Wednesday = 3,
Thursday = 4,
Friday = 5,
Saturday = 6,
Sunday = 7,
}
impl Weekday {
/// Convert an offset to a structured `Weekday`.
///
/// The offset should be from a scheme where the first day of the week
/// is Monday and starts numbering at `0`.
///
/// # Errors
///
/// This returns an error when the given offset is not in the range
/// `0..=6`.
///
/// # Example
///
/// ```
/// use jiff::civil::Weekday;
///
/// let weekday = Weekday::from_monday_zero_offset(3)?;
/// assert_eq!(weekday, Weekday::Thursday);
///
/// assert!(Weekday::from_monday_zero_offset(7).is_err());
/// assert!(Weekday::from_monday_zero_offset(-1).is_err());
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
#[inline]
pub fn from_monday_zero_offset(offset: i8) -> Result<Weekday, Error> {
jcore::civil::Weekday::from_monday_zero_offset(offset)
.map_err(Error::jcore_range)
.map(Weekday::from_jcore)
}
/// Convert an offset to a structured `Weekday`.
///
/// The offset should be from a scheme where the first day of the week
/// is Monday and starts numbering at `1`.
///
/// # Errors
///
/// This returns an error when the given offset is not in the range
/// `1..=7`.
///
/// # Example
///
/// ```
/// use jiff::civil::Weekday;
///
/// let weekday = Weekday::from_monday_one_offset(4)?;
/// assert_eq!(weekday, Weekday::Thursday);
///
/// assert!(Weekday::from_monday_one_offset(8).is_err());
/// assert!(Weekday::from_monday_one_offset(0).is_err());
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
#[inline]
pub fn from_monday_one_offset(offset: i8) -> Result<Weekday, Error> {
jcore::civil::Weekday::from_monday_one_offset(offset)
.map_err(Error::jcore_range)
.map(Weekday::from_jcore)
}
/// Convert an offset to a structured `Weekday`.
///
/// The offset should be from a scheme where the first day of the week
/// is Sunday and starts numbering at `0`.
///
/// # Errors
///
/// This returns an error when the given offset is not in the range
/// `0..=6`.
///
/// # Example
///
/// ```
/// use jiff::civil::Weekday;
///
/// let weekday = Weekday::from_sunday_zero_offset(0)?;
/// assert_eq!(weekday, Weekday::Sunday);
/// let weekday = Weekday::from_sunday_zero_offset(4)?;
/// assert_eq!(weekday, Weekday::Thursday);
///
/// assert!(Weekday::from_sunday_zero_offset(7).is_err());
/// assert!(Weekday::from_sunday_zero_offset(-1).is_err());
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
#[inline]
pub fn from_sunday_zero_offset(offset: i8) -> Result<Weekday, Error> {
jcore::civil::Weekday::from_sunday_zero_offset(offset)
.map_err(Error::jcore_range)
.map(Weekday::from_jcore)
}
/// Convert an offset to a structured `Weekday`.
///
/// The offset should be from a scheme where the first day of the week
/// is Sunday and starts numbering at `1`.
///
/// # Errors
///
/// This returns an error when the given offset is not in the range
/// `1..=7`.
///
/// # Example
///
/// ```
/// use jiff::civil::Weekday;
///
/// let weekday = Weekday::from_sunday_one_offset(5)?;
/// assert_eq!(weekday, Weekday::Thursday);
///
/// assert!(Weekday::from_sunday_one_offset(8).is_err());
/// assert!(Weekday::from_sunday_one_offset(0).is_err());
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
#[inline]
pub fn from_sunday_one_offset(offset: i8) -> Result<Weekday, Error> {
jcore::civil::Weekday::from_sunday_one_offset(offset)
.map_err(Error::jcore_range)
.map(Weekday::from_jcore)
}
/// Returns this weekday as an offset.
///
/// The offset returned is computed based on a week that starts with Monday
/// and begins numbering at `0`.
///
/// # Example
///
/// ```
/// use jiff::civil::Weekday;
///
/// assert_eq!(Weekday::Thursday.to_monday_zero_offset(), 3);
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
#[inline]
pub fn to_monday_zero_offset(self) -> i8 {
self.to_jcore().to_monday_zero_offset()
}
/// Returns this weekday as an offset.
///
/// The offset returned is computed based on a week that starts with Monday
/// and begins numbering at `1`.
///
/// # Example
///
/// ```
/// use jiff::civil::Weekday;
///
/// assert_eq!(Weekday::Thursday.to_monday_one_offset(), 4);
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
#[inline]
pub fn to_monday_one_offset(self) -> i8 {
self.to_jcore().to_monday_one_offset()
}
/// Returns this weekday as an offset.
///
/// The offset returned is computed based on a week that starts with Sunday
/// and begins numbering at `0`.
///
/// # Example
///
/// ```
/// use jiff::civil::Weekday;
///
/// assert_eq!(Weekday::Thursday.to_sunday_zero_offset(), 4);
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
#[inline]
pub fn to_sunday_zero_offset(self) -> i8 {
self.to_jcore().to_sunday_zero_offset()
}
/// Returns this weekday as an offset.
///
/// The offset returned is computed based on a week that starts with Sunday
/// and begins numbering at `1`.
///
/// # Example
///
/// ```
/// use jiff::civil::Weekday;
///
/// assert_eq!(Weekday::Thursday.to_sunday_one_offset(), 5);
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
#[inline]
pub fn to_sunday_one_offset(self) -> i8 {
self.to_jcore().to_sunday_one_offset()
}
/// Returns the next weekday, wrapping around at the end of week to the
/// beginning of the week.
///
/// This is a convenience routing for calling [`Weekday::wrapping_add`]
/// with a value of `1`.
///
/// # Example
///
/// ```
/// use jiff::civil::Weekday;
///
/// assert_eq!(Weekday::Wednesday.next(), Weekday::Thursday);
/// assert_eq!(Weekday::Sunday.next(), Weekday::Monday);
/// assert_eq!(Weekday::Saturday.next(), Weekday::Sunday);
/// ```
#[inline]
pub fn next(self) -> Weekday {
self.wrapping_add(1)
}
/// Returns the previous weekday, wrapping around at the beginning of week
/// to the end of the week.
///
/// This is a convenience routing for calling [`Weekday::wrapping_sub`]
/// with a value of `1`.
///
/// # Example
///
/// ```
/// use jiff::civil::Weekday;
///
/// assert_eq!(Weekday::Wednesday.previous(), Weekday::Tuesday);
/// assert_eq!(Weekday::Sunday.previous(), Weekday::Saturday);
/// assert_eq!(Weekday::Saturday.previous(), Weekday::Friday);
/// ```
#[inline]
pub fn previous(self) -> Weekday {
self.wrapping_sub(1)
}
/// Returns the number of days from `other` to this weekday.
///
/// Adding the returned number of days to `other` is guaranteed to sum to
/// this weekday. The number of days returned is guaranteed to be in the
/// range `0..=6`.
///
/// # Example
///
/// ```
/// use jiff::civil::Weekday;
///
/// assert_eq!(Weekday::Friday.since(Weekday::Tuesday), 3);
/// assert_eq!(Weekday::Tuesday.since(Weekday::Tuesday), 0);
/// assert_eq!(Weekday::Monday.since(Weekday::Sunday), 1);
/// assert_eq!(Weekday::Sunday.since(Weekday::Monday), 6);
/// ```
#[inline]
pub fn since(self, other: Weekday) -> i8 {
self.to_jcore().since(other.to_jcore())
}
/// Returns the number of days until `other` from this weekday.
///
/// Adding the returned number of days to this weekday is guaranteed to sum
/// to `other` weekday. The number of days returned is guaranteed to be in
/// the range `0..=6`.
///
/// # Example
///
/// ```
/// use jiff::civil::Weekday;
///
/// assert_eq!(Weekday::Friday.until(Weekday::Tuesday), 4);
/// assert_eq!(Weekday::Tuesday.until(Weekday::Tuesday), 0);
/// assert_eq!(Weekday::Monday.until(Weekday::Sunday), 6);
/// assert_eq!(Weekday::Sunday.until(Weekday::Monday), 1);
/// ```
#[inline]
pub fn until(self, other: Weekday) -> i8 {
self.to_jcore().until(other.to_jcore())
}
/// Add the given number of days to this weekday, using wrapping arithmetic,
/// and return the resulting weekday.
///
/// Adding a multiple of `7` (including `0`) is guaranteed to produce the
/// same weekday as this one.
///
/// Note that this routine is also available via the `+` operator.
///
/// # Example
///
/// ```
/// use jiff::civil::Weekday;
///
/// assert_eq!(Weekday::Sunday.wrapping_add(1), Weekday::Monday);
/// assert_eq!(Weekday::Sunday.wrapping_add(2), Weekday::Tuesday);
/// assert_eq!(Weekday::Saturday.wrapping_add(1), Weekday::Sunday);
/// assert_eq!(Weekday::Saturday.wrapping_add(7), Weekday::Saturday);
/// assert_eq!(Weekday::Sunday.wrapping_add(-1), Weekday::Saturday);
/// ```
///
/// Wrapping arithmetic is also performed by the `+` operator:
///
/// ```
/// use jiff::civil::Weekday;
///
/// assert_eq!(Weekday::Sunday + 1, Weekday::Monday);
/// assert_eq!(Weekday::Sunday + 2, Weekday::Tuesday);
/// assert_eq!(Weekday::Saturday + 1, Weekday::Sunday);
/// assert_eq!(Weekday::Saturday + 7, Weekday::Saturday);
/// assert_eq!(Weekday::Sunday + -1, Weekday::Saturday);
/// // The weekday can also be on the right hand side of addition:
/// assert_eq!(1 + Weekday::Sunday, Weekday::Monday);
/// ```
#[inline]
pub fn wrapping_add<D: Into<i64>>(self, days: D) -> Weekday {
Weekday::from_jcore(self.to_jcore().wrapping_add(days.into()))
}
/// Subtract the given number of days from this weekday, using wrapping
/// arithmetic, and return the resulting weekday.
///
/// Subtracting a multiple of `7` (including `0`) is guaranteed to produce
/// the same weekday as this one.
///
/// Note that this routine is also available via the `-` operator.
///
/// # Example
///
/// ```
/// use jiff::civil::Weekday;
///
/// assert_eq!(Weekday::Sunday.wrapping_sub(1), Weekday::Saturday);
/// assert_eq!(Weekday::Sunday.wrapping_sub(2), Weekday::Friday);
/// assert_eq!(Weekday::Saturday.wrapping_sub(1), Weekday::Friday);
/// assert_eq!(Weekday::Saturday.wrapping_sub(7), Weekday::Saturday);
/// assert_eq!(Weekday::Sunday.wrapping_sub(-1), Weekday::Monday);
/// ```
///
/// Wrapping arithmetic is also performed by the `-` operator:
///
/// ```
/// use jiff::civil::Weekday;
///
/// assert_eq!(Weekday::Sunday - 1, Weekday::Saturday);
/// assert_eq!(Weekday::Sunday - 2, Weekday::Friday);
/// assert_eq!(Weekday::Saturday - 1, Weekday::Friday);
/// assert_eq!(Weekday::Saturday - 7, Weekday::Saturday);
/// assert_eq!(Weekday::Sunday - -1, Weekday::Monday);
/// ```
///
/// Unlike addition, since subtraction is not commutative and negating a
/// weekday has no semantic meaning, the weekday cannot be on the right
/// hand side of the `-` operator.
#[inline]
pub fn wrapping_sub<D: Into<i64>>(self, days: D) -> Weekday {
Weekday::from_jcore(self.to_jcore().wrapping_sub(days.into()))
}
/// Starting with this weekday, this returns an unending iterator that
/// cycles forward through the days of the week.
///
/// # Example
///
/// ```
/// use jiff::civil::Weekday;
///
/// let mut it = Weekday::Tuesday.cycle_forward();
/// assert_eq!(it.next(), Some(Weekday::Tuesday));
/// assert_eq!(it.next(), Some(Weekday::Wednesday));
/// assert_eq!(it.next(), Some(Weekday::Thursday));
/// assert_eq!(it.next(), Some(Weekday::Friday));
/// assert_eq!(it.next(), Some(Weekday::Saturday));
/// assert_eq!(it.next(), Some(Weekday::Sunday));
/// assert_eq!(it.next(), Some(Weekday::Monday));
/// assert_eq!(it.next(), Some(Weekday::Tuesday));
/// ```
#[inline]
pub fn cycle_forward(self) -> WeekdaysForward {
WeekdaysForward { it: self.to_jcore().cycle_forward() }
}
/// Starting with this weekday, this returns an unending iterator that
/// cycles backward through the days of the week.
///
/// # Example
///
/// ```
/// use jiff::civil::Weekday;
///
/// let mut it = Weekday::Tuesday.cycle_reverse();
/// assert_eq!(it.next(), Some(Weekday::Tuesday));
/// assert_eq!(it.next(), Some(Weekday::Monday));
/// assert_eq!(it.next(), Some(Weekday::Sunday));
/// assert_eq!(it.next(), Some(Weekday::Saturday));
/// assert_eq!(it.next(), Some(Weekday::Friday));
/// assert_eq!(it.next(), Some(Weekday::Thursday));
/// assert_eq!(it.next(), Some(Weekday::Wednesday));
/// assert_eq!(it.next(), Some(Weekday::Tuesday));
/// ```
#[inline]
pub fn cycle_reverse(self) -> WeekdaysReverse {
WeekdaysReverse { it: self.to_jcore().cycle_reverse() }
}
}
impl Weekday {
#[inline]
pub(crate) const fn from_jcore(weekday: jcore::civil::Weekday) -> Weekday {
match weekday {
jcore::civil::Weekday::Monday => Weekday::Monday,
jcore::civil::Weekday::Tuesday => Weekday::Tuesday,
jcore::civil::Weekday::Wednesday => Weekday::Wednesday,
jcore::civil::Weekday::Thursday => Weekday::Thursday,
jcore::civil::Weekday::Friday => Weekday::Friday,
jcore::civil::Weekday::Saturday => Weekday::Saturday,
jcore::civil::Weekday::Sunday => Weekday::Sunday,
}
}
#[inline]
pub(crate) const fn to_jcore(self) -> jcore::civil::Weekday {
match self {
Weekday::Monday => jcore::civil::Weekday::Monday,
Weekday::Tuesday => jcore::civil::Weekday::Tuesday,
Weekday::Wednesday => jcore::civil::Weekday::Wednesday,
Weekday::Thursday => jcore::civil::Weekday::Thursday,
Weekday::Friday => jcore::civil::Weekday::Friday,
Weekday::Saturday => jcore::civil::Weekday::Saturday,
Weekday::Sunday => jcore::civil::Weekday::Sunday,
}
}
}
impl core::ops::Add<i8> for Weekday {
type Output = Weekday;
#[inline]
fn add(self, rhs: i8) -> Weekday {
self.wrapping_add(rhs)
}
}
impl core::ops::Add<i16> for Weekday {
type Output = Weekday;
#[inline]
fn add(self, rhs: i16) -> Weekday {
self.wrapping_add(rhs)
}
}
impl core::ops::Add<i32> for Weekday {
type Output = Weekday;
#[inline]
fn add(self, rhs: i32) -> Weekday {
self.wrapping_add(rhs)
}
}
impl core::ops::Add<i64> for Weekday {
type Output = Weekday;
#[inline]
fn add(self, rhs: i64) -> Weekday {
self.wrapping_add(rhs)
}
}
// Since addition is commutative, we don't care if users write `n + weekday`
// or `weekday + n`.
impl core::ops::Add<Weekday> for i8 {
type Output = Weekday;
#[inline]
fn add(self, rhs: Weekday) -> Weekday {
rhs.wrapping_add(self)
}
}
impl core::ops::Add<Weekday> for i16 {
type Output = Weekday;
#[inline]
fn add(self, rhs: Weekday) -> Weekday {
rhs.wrapping_add(self)
}
}
impl core::ops::Add<Weekday> for i32 {
type Output = Weekday;
#[inline]
fn add(self, rhs: Weekday) -> Weekday {
rhs.wrapping_add(self)
}
}
impl core::ops::Add<Weekday> for i64 {
type Output = Weekday;
#[inline]
fn add(self, rhs: Weekday) -> Weekday {
rhs.wrapping_add(self)
}
}
impl core::ops::AddAssign<i8> for Weekday {
#[inline]
fn add_assign(&mut self, rhs: i8) {
*self = *self + rhs;
}
}
impl core::ops::AddAssign<i16> for Weekday {
#[inline]
fn add_assign(&mut self, rhs: i16) {
*self = *self + rhs;
}
}
impl core::ops::AddAssign<i32> for Weekday {
#[inline]
fn add_assign(&mut self, rhs: i32) {
*self = *self + rhs;
}
}
impl core::ops::AddAssign<i64> for Weekday {
#[inline]
fn add_assign(&mut self, rhs: i64) {
*self = *self + rhs;
}
}
// Subtraction isn't commutative, so we only define it when the right hand
// side is an integer. Otherwise we'd need a concept of what it means to
// "negate" a weekday, which doesn't really make sense?
impl core::ops::Sub<i8> for Weekday {
type Output = Weekday;
#[inline]
fn sub(self, rhs: i8) -> Weekday {
self.wrapping_sub(rhs)
}
}
impl core::ops::Sub<i16> for Weekday {
type Output = Weekday;
#[inline]
fn sub(self, rhs: i16) -> Weekday {
self.wrapping_sub(rhs)
}
}
impl core::ops::Sub<i32> for Weekday {
type Output = Weekday;
#[inline]
fn sub(self, rhs: i32) -> Weekday {
self.wrapping_sub(rhs)
}
}
impl core::ops::Sub<i64> for Weekday {
type Output = Weekday;
#[inline]
fn sub(self, rhs: i64) -> Weekday {
self.wrapping_sub(rhs)
}
}
impl core::ops::SubAssign<i8> for Weekday {
#[inline]
fn sub_assign(&mut self, rhs: i8) {
*self = *self - rhs;
}
}
impl core::ops::SubAssign<i16> for Weekday {
#[inline]
fn sub_assign(&mut self, rhs: i16) {
*self = *self - rhs;
}
}
impl core::ops::SubAssign<i32> for Weekday {
#[inline]
fn sub_assign(&mut self, rhs: i32) {
*self = *self - rhs;
}
}
impl core::ops::SubAssign<i64> for Weekday {
#[inline]
fn sub_assign(&mut self, rhs: i64) {
*self = *self - rhs;
}
}
#[cfg(test)]
impl quickcheck::Arbitrary for Weekday {
fn arbitrary(g: &mut quickcheck::Gen) -> Weekday {
let offset = crate::util::b::WeekdayMondayZero::arbitrary(g);
Weekday::from_monday_zero_offset(offset).unwrap()
}
fn shrink(&self) -> alloc::boxed::Box<dyn Iterator<Item = Weekday>> {
alloc::boxed::Box::new(
self.to_monday_zero_offset()
.shrink()
.filter_map(|n| Weekday::from_monday_zero_offset(n).ok()),
)
}
}
/// An unending iterator of the days of the week.
///
/// This iterator is created by calling [`Weekday::cycle_forward`].
#[derive(Clone, Debug)]
pub struct WeekdaysForward {
it: jcore::civil::WeekdaysForward,
}
impl Iterator for WeekdaysForward {
type Item = Weekday;
#[inline]
fn next(&mut self) -> Option<Weekday> {
self.it.next().map(Weekday::from_jcore)
}
}
impl core::iter::FusedIterator for WeekdaysForward {}
/// An unending iterator of the days of the week in reverse.
///
/// This iterator is created by calling [`Weekday::cycle_reverse`].
#[derive(Clone, Debug)]
pub struct WeekdaysReverse {
it: jcore::civil::WeekdaysReverse,
}
impl Iterator for WeekdaysReverse {
type Item = Weekday;
#[inline]
fn next(&mut self) -> Option<Weekday> {
self.it.next().map(Weekday::from_jcore)
}
}
impl core::iter::FusedIterator for WeekdaysReverse {}
+146
View File
@@ -0,0 +1,146 @@
use core::time::Duration as UnsignedDuration;
use crate::{
error::{duration::Error as E, ErrorContext},
Error, SignedDuration, Span,
};
/// An internal type for abstracting over different duration types.
#[derive(Clone, Copy, Debug)]
pub(crate) enum Duration {
Span(Span),
Signed(SignedDuration),
Unsigned(UnsignedDuration),
}
impl Duration {
/// Convert this to a signed duration.
///
/// This returns an error only in the case where this is an unsigned
/// duration with a number of whole seconds that exceeds `|i64::MIN|`.
#[cfg_attr(feature = "perf-inline", inline(always))]
pub(crate) fn to_signed(&self) -> Result<SDuration<'_>, Error> {
match *self {
Duration::Span(ref span) => Ok(SDuration::Span(span)),
Duration::Signed(sdur) => Ok(SDuration::Absolute(sdur)),
Duration::Unsigned(udur) => {
let sdur = SignedDuration::try_from(udur)
.context(E::RangeUnsignedDuration)?;
Ok(SDuration::Absolute(sdur))
}
}
}
/// Negates this duration.
///
/// When the duration is a span, this can never fail because a span defines
/// its min and max values such that negation is always possible.
///
/// When the duration is signed, then this attempts to return a signed
/// duration and only falling back to an unsigned duration when the number
/// of seconds corresponds to `i64::MIN`.
///
/// When the duration is unsigned, then this fails when the whole seconds
/// exceed the absolute value of `i64::MIN`. Otherwise, a signed duration
/// is returned.
///
/// The failures for large unsigned durations here are okay because the
/// point at which absolute durations overflow on negation, they would also
/// cause overflow when adding or subtracting to *any* valid datetime value
/// for *any* datetime type in this crate. So while the error message may
/// be different, the actual end result is the same (failure).
///
/// TODO: Write unit tests for this.
#[cfg_attr(feature = "perf-inline", inline(always))]
pub(crate) fn checked_neg(self) -> Result<Duration, Error> {
match self {
Duration::Span(span) => Ok(Duration::Span(span.negate())),
Duration::Signed(sdur) => {
// We try to stick with signed durations, but in the case
// where negation fails, we can represent its negation using
// an unsigned duration.
if let Some(sdur) = sdur.checked_neg() {
Ok(Duration::Signed(sdur))
} else {
let udur = UnsignedDuration::new(
i64::MIN.unsigned_abs(),
sdur.subsec_nanos().unsigned_abs(),
);
Ok(Duration::Unsigned(udur))
}
}
Duration::Unsigned(udur) => {
// We can permit negating i64::MIN.unsigned_abs() to
// i64::MIN, but we need to handle it specially since
// i64::MIN.unsigned_abs() exceeds i64::MAX.
let sdur = if udur.as_secs() == i64::MIN.unsigned_abs() {
SignedDuration::new_unchecked(
i64::MIN,
// OK because `udur.subsec_nanos()` < 999_999_999.
-i32::try_from(udur.subsec_nanos()).unwrap(),
)
} else {
// The negation here is always correct because it can only
// panic with `sdur.as_secs() == i64::MIN`, which is
// impossible because it must be positive.
//
// Otherwise, this is the only failure point in this entire
// routine. And specifically, we fail here in precisely
// the cases where `udur.as_secs() > |i64::MIN|`.
-SignedDuration::try_from(udur)
.context(E::FailedNegateUnsignedDuration)?
};
Ok(Duration::Signed(sdur))
}
}
}
/// Returns true if and only if this duration is negative.
#[cfg_attr(feature = "perf-inline", inline(always))]
pub(crate) fn is_negative(&self) -> bool {
match *self {
Duration::Span(ref span) => span.is_negative(),
Duration::Signed(ref sdur) => sdur.is_negative(),
Duration::Unsigned(_) => false,
}
}
}
impl From<Span> for Duration {
#[inline]
fn from(span: Span) -> Duration {
Duration::Span(span)
}
}
impl From<SignedDuration> for Duration {
#[inline]
fn from(sdur: SignedDuration) -> Duration {
Duration::Signed(sdur)
}
}
impl From<UnsignedDuration> for Duration {
#[inline]
fn from(udur: UnsignedDuration) -> Duration {
Duration::Unsigned(udur)
}
}
/// An internal type for abstracting over signed durations.
///
/// This is typically converted to from a `Duration`. It enables callers
/// downstream to implement datetime arithmetic on only two duration types
/// instead of doing it for three duration types (including
/// `std::time::Duration`).
///
/// The main thing making this idea work is that if an unsigned duration cannot
/// fit into a signed duration, then it would overflow any calculation on any
/// datetime type in Jiff anyway. If this weren't true, then we'd need to
/// support doing actual arithmetic with unsigned durations separately from
/// signed durations.
#[derive(Clone, Copy, Debug)]
pub(crate) enum SDuration<'a> {
Span(&'a Span),
Absolute(SignedDuration),
}
+67
View File
@@ -0,0 +1,67 @@
use crate::error;
#[derive(Clone, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub(crate) enum Error {
FailedAddDays,
FailedAddDurationOverflowing,
FailedAddSpanDate,
FailedAddSpanOverflowing,
FailedAddSpanTime,
IllegalTimeWithMicrosecond,
IllegalTimeWithMillisecond,
IllegalTimeWithNanosecond,
OverflowDaysDuration,
OverflowTimeNanoseconds,
}
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`",
),
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")
}
}
}
}
+37
View File
@@ -0,0 +1,37 @@
use crate::error;
#[derive(Clone, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
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")
}
}
}
}
+83
View File
@@ -0,0 +1,83 @@
use crate::{error, util::escape};
#[derive(Clone, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
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",
),
}
}
}
+88
View File
@@ -0,0 +1,88 @@
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)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
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")
}
}
}
}
+142
View File
@@ -0,0 +1,142 @@
use crate::{error, util::escape};
#[derive(Clone, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub(crate) enum Error {
ColonAfterHours,
EndOfInput,
EndOfInputHour,
EndOfInputMinute,
EndOfInputNumeric,
EndOfInputSecond,
InvalidHours,
InvalidMinutes,
InvalidSeconds,
InvalidSecondsFractional,
InvalidSign,
InvalidSignPlusOrMinus,
MissingMinuteAfterHour,
MissingSecondAfterMinute,
NoColonAfterHours,
ParseHours,
ParseMinutes,
ParseSeconds,
PrecisionLoss,
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 from fractional seconds, \
time zone offset is rounded to a value that is out of bounds",
),
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),
),
}
}
}
+218
View File
@@ -0,0 +1,218 @@
use crate::{civil::Weekday, error, util::escape};
#[derive(Clone, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
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,
InvalidMonth,
InvalidObsoleteOffset,
InvalidWeekday { got_non_digit: u8 },
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"),
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",
),
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),
),
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")
}
}
}
}
+115
View File
@@ -0,0 +1,115 @@
use crate::{error, util::escape};
#[derive(Clone, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
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",
),
}
}
}
+520
View File
@@ -0,0 +1,520 @@
use crate::{civil::Weekday, error, tz::Offset, util::escape};
#[derive(Clone, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
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)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
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)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
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")
}
}
}
}
+297
View File
@@ -0,0 +1,297 @@
use crate::{error, tz::Offset, util::escape};
#[derive(Clone, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
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,
FailedFractionalSecondInTime,
FailedOffsetNumeric,
FailedSeparatorAfterMonth,
FailedSeparatorAfterWeekNumber,
FailedSeparatorAfterYear,
FailedTzdbLookup,
FailedWeekNumberPrefixInDate,
InvalidDate,
InvalidMonthDay,
InvalidTimeZoneUtf8,
InvalidWeekDate,
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")
}
FailedFractionalSecondInTime => {
f.write_str("failed to parse fractional seconds in time")
}
FailedOffsetNumeric => f.write_str(
"offset successfully parsed, \
but failed to convert to numeric `jiff::tz::Offset`",
),
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",
),
FailedWeekNumberPrefixInDate => {
f.write_str("failed to parse week number prefix in date")
}
InvalidDate => f.write_str("parsed date is not valid"),
InvalidMonthDay => f.write_str("parsed month-day 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"),
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`)",
),
}
}
}
+116
View File
@@ -0,0 +1,116 @@
use crate::{error, Unit};
#[derive(Clone, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
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(),
),
}
}
}
+882
View File
@@ -0,0 +1,882 @@
use crate::util::{
b::{BoundsError, SpecialBoundsError},
sync::Arc,
};
pub(crate) mod civil;
pub(crate) mod duration;
pub(crate) mod fmt;
pub(crate) mod signed_duration;
pub(crate) mod span;
pub(crate) mod timestamp;
pub(crate) mod tz;
pub(crate) mod unit;
pub(crate) mod util;
pub(crate) mod zoned;
/// An error that can occur in this crate.
///
/// The most common type of error is a result of overflow. But other errors
/// exist as well:
///
/// * Time zone database lookup failure.
/// * Configuration problem. (For example, trying to round a span with calendar
/// units without providing a relative datetime.)
/// * An I/O error as a result of trying to open a time zone database from a
/// directory via
/// [`TimeZoneDatabase::from_dir`](crate::tz::TimeZoneDatabase::from_dir).
/// * Parse errors.
///
/// # Introspection is limited
///
/// Other than implementing the [`std::error::Error`] trait when the
/// `std` feature is enabled, the [`core::fmt::Debug`] trait and the
/// [`core::fmt::Display`] trait, this error type currently provides
/// very limited introspection capabilities. Simple predicates like
/// `Error::is_range` are provided, but the predicates are not
/// exhaustive. That is, there exist some errors that do not return
/// `true` for any of the `Error::is_*` predicates.
///
/// # Design
///
/// This crate follows the "One True God Error Type Pattern," where only one
/// error type exists for a variety of different operations. This design was
/// chosen after attempting to provide finer grained error types. But finer
/// grained error types proved difficult in the face of composition.
///
/// More about this design choice can be found in a GitHub issue
/// [about error types].
///
/// [about error types]: https://github.com/BurntSushi/jiff/issues/8
#[derive(Clone)]
pub struct Error {
/// The internal representation of an error.
///
/// This is in an `Arc` to make an `Error` cloneable. It could otherwise
/// be automatically cloneable, but it embeds a `std::io::Error` when the
/// `std` feature is enabled, which isn't cloneable.
///
/// This also makes clones cheap. And it also make the size of error equal
/// to one word (although a `Box` would achieve that last goal). This is
/// why we put the `Arc` here instead of on `std::io::Error` directly.
inner: Option<Arc<ErrorInner>>,
}
#[derive(Debug)]
#[cfg_attr(not(feature = "alloc"), derive(Clone))]
struct ErrorInner {
kind: ErrorKind,
#[cfg(feature = "alloc")]
cause: Option<Error>,
}
impl Error {
/// Creates a new error value from `core::fmt::Arguments`.
///
/// It is expected to use [`format_args!`](format_args) from
/// Rust's standard library (available in `core`) to create a
/// `core::fmt::Arguments`.
///
/// Callers should generally use their own error types. But in some
/// 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
///
/// ```
/// use jiff::Error;
///
/// let err = Error::from_args(format_args!("something failed"));
/// assert_eq!(err.to_string(), "something failed");
/// ```
pub fn from_args<'a>(message: core::fmt::Arguments<'a>) -> Error {
Error::from(ErrorKind::Adhoc(AdhocError::from_args(message)))
}
/// Returns true when this error originated as a result of a value being
/// out of Jiff's supported range.
///
/// # Example
///
/// ```
/// use jiff::civil::Date;
///
/// assert!(Date::new(2025, 2, 29).unwrap_err().is_range());
/// assert!("2025-02-29".parse::<Date>().unwrap_err().is_range());
/// assert!(Date::strptime("%Y-%m-%d", "2025-02-29").unwrap_err().is_range());
/// ```
pub fn is_range(&self) -> bool {
use self::ErrorKind::*;
matches!(
*self.root().kind(),
Bounds(_) | SpecialBounds(_) | JcoreRange(_)
)
}
/// 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::civil::Error as CivilError;
use self::ErrorKind::*;
matches!(
*self.root().kind(),
UnitConfig(_)
| Civil(
CivilError::IllegalTimeWithMicrosecond
| CivilError::IllegalTimeWithMillisecond
| CivilError::IllegalTimeWithNanosecond
)
)
}
/// 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 {
#[inline(never)]
#[cold]
pub(crate) fn bounds(err: BoundsError) -> Error {
Error::from(ErrorKind::Bounds(err))
}
/// Builds a `jiff::Error` from a `jcore::tz::posix::ParseError`.
///
/// This very much cannot be added as a `From` impl because that would
/// introduce a public dependency on `jcore`.
#[inline(never)]
#[cold]
pub(crate) fn jcore_posix_parse(
err: jcore::tz::posix::ParseError,
) -> Error {
Error::from(ErrorKind::JcorePosixParse(err))
}
/// Builds a `jiff::Error` from a `jcore::tz::tzif::ParseError`.
///
/// This very much cannot be added as a `From` impl because that would
/// introduce a public dependency on `jcore`.
#[cfg(feature = "alloc")]
#[inline(never)]
#[cold]
pub(crate) fn jcore_tzif_parse(err: jcore::tz::tzif::ParseError) -> Error {
Error::from(ErrorKind::JcoreTzifParse(err))
}
/// Builds a `jiff::Error` from a `jcore::bounds::RangeError`.
///
/// This very much cannot be added as a `From` impl because that would
/// introduce a public dependency on `jcore`.
#[inline(never)]
#[cold]
pub(crate) fn jcore_range(err: jcore::bounds::RangeError) -> Error {
Error::from(ErrorKind::JcoreRange(err))
}
#[inline(never)]
#[cold]
pub(crate) fn special_bounds(err: SpecialBoundsError) -> Error {
Error::from(ErrorKind::SpecialBounds(err))
}
/// A convenience constructor for building an I/O error.
///
/// This returns an error that is just a simple wrapper around the
/// `std::io::Error` type. In general, callers should always attach some
/// kind of context to this error (like a file path).
///
/// This is only available when the `std` feature is enabled.
#[cfg(feature = "std")]
#[inline(never)]
#[cold]
pub(crate) fn io(err: std::io::Error) -> Error {
Error::from(ErrorKind::IO(IOError { err }))
}
/// Contextualizes this error by associating the given file path with it.
///
/// This is a convenience routine for calling `Error::context` with a
/// `FilePathError`.
#[cfg(any(feature = "tzdb-zoneinfo", feature = "tzdb-concatenated"))]
#[inline(never)]
#[cold]
pub(crate) fn path(self, path: impl Into<std::path::PathBuf>) -> Error {
let err = Error::from(ErrorKind::FilePath(FilePathError {
path: path.into(),
}));
self.context(err)
}
/*
/// Creates a new "unknown" Jiff 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.
///
/// 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
/// `Error`. (Which is important, because of how many perf sensitive
/// APIs return a `Result<T, Error>` in Jiff.
pub(crate) const fn unknown() -> 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")]
impl std::error::Error for Error {}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
let mut it = self.chain().peekable();
while let Some(err) = it.next() {
core::fmt::Display::fmt(err.kind(), f)?;
if it.peek().is_some() {
f.write_str(": ")?;
}
}
Ok(())
}
}
impl core::fmt::Debug for Error {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
if !f.alternate() {
core::fmt::Display::fmt(self, f)
} else {
let Some(ref inner) = self.inner else {
return f
.debug_struct("Error")
.field("kind", &"None")
.finish();
};
#[cfg(feature = "alloc")]
{
f.debug_struct("Error")
.field("kind", &inner.kind)
.field("cause", &inner.cause)
.finish()
}
#[cfg(not(feature = "alloc"))]
{
f.debug_struct("Error").field("kind", &inner.kind).finish()
}
}
}
}
#[cfg(feature = "defmt")]
impl defmt::Format for Error {
fn format(&self, f: defmt::Formatter) {
let Some(ref inner) = self.inner else {
return defmt::write!(f, "Error {{ kind: None }}");
};
#[cfg(feature = "alloc")]
{
defmt::write!(
f,
"Error {{ kind: {}, cause: {} }}",
inner.kind,
inner.cause
);
}
#[cfg(not(feature = "alloc"))]
{
defmt::write!(f, "Error {{ kind: {} }}", inner.kind);
}
}
}
/// The underlying kind of a [`Error`].
#[derive(Debug)]
#[cfg_attr(not(feature = "alloc"), derive(Clone))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
enum ErrorKind {
Adhoc(AdhocError),
Bounds(BoundsError),
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),
JcorePosixParse(jcore::tz::posix::ParseError),
#[cfg(feature = "alloc")]
JcoreTzifParse(jcore::tz::tzif::ParseError),
JcoreRange(jcore::bounds::RangeError),
OsStrUtf8(self::util::OsStrUtf8Error),
ParseInt(self::util::ParseIntError),
ParseFraction(self::util::ParseFractionError),
RoundingIncrement(self::util::RoundingIncrementError),
SignedDuration(self::signed_duration::Error),
Span(self::span::Error),
UnitConfig(self::unit::UnitConfigError),
SpecialBounds(SpecialBoundsError),
Timestamp(self::timestamp::Error),
TzAmbiguous(self::tz::ambiguous::Error),
TzDb(self::tz::db::Error),
TzConcatenated(self::tz::concatenated::Error),
TzOffset(self::tz::offset::Error),
TzSystem(self::tz::system::Error),
TzTimeZone(self::tz::timezone::Error),
#[allow(dead_code)]
TzZic(self::tz::zic::Error),
Unknown,
Zoned(self::zoned::Error),
}
impl core::fmt::Display for ErrorKind {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::ErrorKind::*;
match *self {
Adhoc(ref msg) => msg.fmt(f),
Bounds(ref msg) => msg.fmt(f),
JcorePosixParse(ref msg) => msg.fmt(f),
#[cfg(feature = "alloc")]
JcoreTzifParse(ref msg) => msg.fmt(f),
JcoreRange(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),
OsStrUtf8(ref err) => err.fmt(f),
ParseInt(ref err) => err.fmt(f),
ParseFraction(ref err) => err.fmt(f),
RoundingIncrement(ref err) => err.fmt(f),
SignedDuration(ref err) => err.fmt(f),
Span(ref err) => err.fmt(f),
UnitConfig(ref err) => err.fmt(f),
SpecialBounds(ref msg) => msg.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),
TzSystem(ref err) => err.fmt(f),
TzTimeZone(ref err) => err.fmt(f),
TzZic(ref err) => err.fmt(f),
Unknown => f.write_str("unknown Jiff error"),
Zoned(ref err) => err.fmt(f),
}
}
}
impl From<ErrorKind> for Error {
fn from(kind: ErrorKind) -> Error {
#[cfg(feature = "alloc")]
{
Error { inner: Some(Arc::new(ErrorInner { kind, cause: None })) }
}
#[cfg(not(feature = "alloc"))]
{
Error { inner: Some(Arc::new(ErrorInner { kind })) }
}
}
}
/// A generic error message.
///
/// This used to be used to represent most errors in Jiff. But then I switched
/// to more structured error types (internally). We still keep this around to
/// support the `Error::from_args` public API, which permits users of Jiff to
/// manifest their own `Error` values from an arbitrary message.
#[cfg_attr(not(feature = "alloc"), derive(Clone))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
struct AdhocError {
#[cfg(feature = "alloc")]
message: alloc::boxed::Box<str>,
#[cfg(not(feature = "alloc"))]
message: &'static str,
}
impl AdhocError {
fn from_args<'a>(message: core::fmt::Arguments<'a>) -> AdhocError {
#[cfg(feature = "alloc")]
{
use alloc::string::ToString;
let message = message.to_string().into_boxed_str();
AdhocError { message }
}
#[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)",
);
AdhocError { message }
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for AdhocError {}
impl core::fmt::Display for AdhocError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
core::fmt::Display::fmt(&self.message, f)
}
}
impl core::fmt::Debug for AdhocError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
core::fmt::Debug::fmt(&self.message, f)
}
}
/// 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)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
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
/// enabled. When `std` is not enabled, a value of this type can never be
/// constructed.
///
/// Otherwise, this type is a simple wrapper around `std::io::Error`. Its
/// purpose is to encapsulate the conditional compilation based on the `std`
/// feature.
#[cfg_attr(not(feature = "alloc"), derive(Clone))]
struct IOError {
#[cfg(feature = "std")]
err: std::io::Error,
}
#[cfg(feature = "std")]
impl std::error::Error for IOError {}
impl core::fmt::Display for IOError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
#[cfg(feature = "std")]
{
self.err.fmt(f)
}
#[cfg(not(feature = "std"))]
{
f.write_str("<BUG: SHOULD NOT EXIST>")
}
}
}
impl core::fmt::Debug for IOError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
#[cfg(feature = "std")]
{
f.debug_struct("IOError").field("err", &self.err).finish()
}
#[cfg(not(feature = "std"))]
{
f.write_str("<BUG: SHOULD NOT EXIST>")
}
}
}
#[cfg(feature = "defmt")]
impl defmt::Format for IOError {
fn format(&self, f: defmt::Formatter) {
// `std::io::Error` does not implement `defmt::Format`. Since this
// error is std-only and defmt is mainly used in embedded contexts,
// omitting the error is probably fine.
defmt::write!(f, "IOError(unavailable)");
}
}
#[cfg(feature = "std")]
impl From<std::io::Error> for IOError {
fn from(err: std::io::Error) -> IOError {
IOError { err }
}
}
#[cfg_attr(not(feature = "alloc"), derive(Clone))]
struct FilePathError {
#[cfg(feature = "std")]
path: std::path::PathBuf,
}
#[cfg(feature = "std")]
impl std::error::Error for FilePathError {}
impl core::fmt::Display for FilePathError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
#[cfg(feature = "std")]
{
self.path.display().fmt(f)
}
#[cfg(not(feature = "std"))]
{
f.write_str("<BUG: SHOULD NOT EXIST>")
}
}
}
impl core::fmt::Debug for FilePathError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
#[cfg(feature = "std")]
{
f.debug_struct("FilePathError").field("path", &self.path).finish()
}
#[cfg(not(feature = "std"))]
{
f.write_str("<BUG: SHOULD NOT EXIST>")
}
}
}
#[cfg(feature = "defmt")]
impl defmt::Format for FilePathError {
fn format(&self, f: defmt::Formatter) {
// `std::path::PathBuf` does not implement `defmt::Format`. Since this
// error is std-only and defmt is mainly used in embedded contexts,
// omitting the path is probably fine.
defmt::write!(f, "FilePathError(unavailable)");
}
}
/// A simple trait to encapsulate automatic conversion to `Error`.
///
/// This trait basically exists to make `Error::context` work without needing
/// to rely on public `From` impls. For example, without this trait, we might
/// otherwise write `impl From<String> for Error`. But this would make it part
/// of the public API. Which... maybe we should do, but at time of writing,
/// I'm starting very conservative so that we can evolve errors in semver
/// compatible ways.
pub(crate) trait IntoError {
fn into_error(self) -> Error;
}
impl IntoError for Error {
#[inline(always)]
fn into_error(self) -> Error {
self
}
}
// This trait impl is okay because `IntoError` is a crate-internal trait. So
// this impl will not cause a public dependency on `jcore`.
impl IntoError for jcore::bounds::RangeError {
#[inline(always)]
fn into_error(self) -> Error {
Error::jcore_range(self)
}
}
/// A trait for contextualizing error values.
///
/// This makes it easy to contextualize either `Error` or `Result<T, Error>`.
/// Specifically, in the latter case, it absolves one of the need to call
/// `map_err` everywhere one wants to add context to an error.
///
/// This trick was borrowed from `anyhow`.
pub(crate) trait ErrorContext<T, E> {
/// Contextualize the given consequent error with this (`self`) error as
/// the cause.
///
/// This is equivalent to saying that "consequent is caused by self."
///
/// Note that if an `Error` is given for `kind`, then this panics if it has
/// a cause. (Because the cause would otherwise be dropped. An error causal
/// chain is just a linked list, not a tree.)
fn context(self, consequent: impl IntoError) -> Result<T, Error>;
/// Like `context`, but hides error construction within a closure.
///
/// This is useful if the creation of the consequent error is not otherwise
/// guarded and when error construction is potentially "costly" (i.e., it
/// allocates). The closure avoids paying the cost of contextual error
/// creation in the happy path.
///
/// Usually this only makes sense to use on a `Result<T, Error>`, otherwise
/// the closure is just executed immediately anyway.
fn with_context<C: IntoError>(
self,
consequent: impl FnOnce() -> C,
) -> 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.into_error().context_impl(consequent.into_error())
})
}
#[cfg_attr(feature = "perf-inline", inline(always))]
fn with_context<C: IntoError>(
self,
consequent: impl FnOnce() -> C,
) -> Result<T, Error> {
self.map_err(|err| {
err.into_error().context_impl(consequent().into_error())
})
}
}
#[cfg(test)]
mod tests {
use super::*;
// We test that our 'Error' type is the size we expect. This isn't an API
// guarantee, but if the size increases, we really want to make sure we
// decide to do that intentionally. So this should be a speed bump. And in
// general, we should not increase the size without a very good reason.
#[test]
fn error_size() {
if cfg!(feature = "alloc") {
let expected_size = core::mem::size_of::<usize>();
assert_eq!(expected_size, core::mem::size_of::<Error>());
return;
}
// oooowwwwwwwwwwwch.
//
// Like, this is horrible, right? core-only environments are
// precisely the place where one want to keep things slim. But
// in core-only, I don't know of a way to introduce any sort of
// indirection in the library level without using a completely
// different API.
//
// This is what makes me doubt that core-only Jiff is actually
// useful. In what context are people using a huge library like
// Jiff but can't define a small little heap allocator?
//
// OK, this used to be `expected_size *= 10`, but I slimmed it down
// to x3. Still kinda sucks right? If we tried harder, I think we
// could probably slim this down more. And if we were willing to
// sacrifice error message quality even more (like, all the way),
// then we could make `Error` a zero sized type. Which might
// actually be the right trade-off for core-only, but I'll hold off
// until we have some real world use cases.
//
// OK... after switching to structured errors, this jumped
// back up to `expected_size *= 6`. And that was with me being
// conscientious about what data we store inside of error types.
// Blech.
//
// 2026-01-14: A change to the `Offset` type made this move back
// down to `expected_size *= 4`.
//
// 2026-05-28: No changes here, but `4 * pointer-size` is not the
// right calculation here. This is unfortunately coupled with an
// internal representation for the biggest possible error variant.
// And also compiler optimizations. But at time of writing, it's
// from the `jiff::error::tz::offset::Error` enum. We get 4 32-bit
// integers (always 32-bit) plus one pointer sized discriminant.
//
// 2026-05-28 redux: this now seems coupled with compiler
// optimizations. So just give up and check that it's reasonable.
let got = core::mem::size_of::<Error>();
assert!(got <= 40, "wanted error size to be <= 40, but got {got}");
}
}
+48
View File
@@ -0,0 +1,48 @@
use crate::{error, Unit};
#[derive(Clone, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub(crate) enum Error {
ConvertNonFinite,
ConvertSystemTime,
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",
),
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(),
),
}
}
}
+92
View File
@@ -0,0 +1,92 @@
use crate::{error, Unit};
#[derive(Clone, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub(crate) enum Error {
ConvertDateTimeToTimestamp,
ConvertNanoseconds { unit: Unit },
ConvertNegative,
ConvertSpanToSignedDuration,
FailedSpanBetweenDateTimes { unit: Unit },
FailedSpanBetweenZonedDateTimes { unit: Unit },
OptionLargest,
OptionLargestInSpan,
OptionSmallest,
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")
}
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",
),
}
}
}
+35
View File
@@ -0,0 +1,35 @@
use crate::error;
#[derive(Clone, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub(crate) enum Error {
OverflowAddDuration,
OverflowAddSpan,
}
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"),
}
}
}
+50
View File
@@ -0,0 +1,50 @@
use crate::{
error,
tz::{Offset, TimeZone},
};
#[derive(Clone, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
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(),
),
}
}
}
+120
View File
@@ -0,0 +1,120 @@
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)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
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")
}
}
}
}
+142
View File
@@ -0,0 +1,142 @@
use crate::error;
#[derive(Clone, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
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",
),
}
}
}
+7
View File
@@ -0,0 +1,7 @@
pub(crate) mod ambiguous;
pub(crate) mod concatenated;
pub(crate) mod db;
pub(crate) mod offset;
pub(crate) mod system;
pub(crate) mod timezone;
pub(crate) mod zic;
+99
View File
@@ -0,0 +1,99 @@
use crate::{
error,
tz::{Offset, TimeZone},
};
#[derive(Clone, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
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,
},
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(),
),
RoundOverflow => f.write_str(
"rounding time zone offset resulted in a duration \
that overflows",
),
}
}
}
+106
View File
@@ -0,0 +1,106 @@
#[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)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
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)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
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)",
),
}
}
}
}
+41
View File
@@ -0,0 +1,41 @@
use crate::error;
#[derive(Clone, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
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"),
}
}
}
+325
View File
@@ -0,0 +1,325 @@
#[cfg(any(not(test), not(feature = "alloc")))]
pub(crate) use self::disabled::*;
#[cfg(all(test, feature = "alloc"))]
pub(crate) use self::enabled::*;
#[cfg(any(not(test), not(feature = "alloc")))]
mod disabled {
#[derive(Clone, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub(crate) enum Error {}
impl core::fmt::Display for Error {
fn fmt(&self, _: &mut core::fmt::Formatter) -> core::fmt::Result {
unreachable!()
}
}
}
#[cfg(all(test, feature = "alloc"))]
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)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
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"),
}
}
}
}
+166
View File
@@ -0,0 +1,166 @@
use jcore::constants as c;
use crate::{error, Unit};
#[derive(Clone, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub(crate) enum UnitConfigError {
CalendarUnitsNotAllowed { unit: Unit },
CivilDate { given: Unit },
CivilTime { given: Unit },
IncrementDivide { unit: Unit, must_divide: MustDivide },
LargestSmallerThanSmallest { smallest: Unit, largest: Unit },
RelativeWeekOrDay { unit: Unit },
RelativeYearOrMonth { unit: Unit },
RelativeYearOrMonthGivenDaysAre24Hours { unit: Unit },
RoundToUnitUnsupported { unit: Unit },
SignedDurationCalendarUnit { smallest: Unit },
TimeZoneOffset { given: Unit },
}
impl From<UnitConfigError> for error::Error {
#[cold]
#[inline(never)]
fn from(err: UnitConfigError) -> error::Error {
error::ErrorKind::UnitConfig(err).into()
}
}
impl error::IntoError for UnitConfigError {
fn into_error(self) -> error::Error {
self.into()
}
}
impl core::fmt::Display for UnitConfigError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::UnitConfigError::*;
match *self {
CalendarUnitsNotAllowed { 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(),
),
CivilDate { given } => write!(
f,
"'{unit}' not allowed as a date unit for rounding span",
unit = given.singular()
),
CivilTime { given } => write!(
f,
"'{unit}' not allowed as a time unit for rounding span",
unit = given.singular()
),
IncrementDivide { unit, must_divide: MustDivide::Days } => write!(
f,
"increment for rounding to '{unit}' \
must be equal to `1`",
unit = unit.plural(),
),
IncrementDivide { unit, must_divide } => write!(
f,
"increment for rounding to '{unit}' \
must divide into `{must_divide}` evenly",
unit = unit.plural(),
must_divide = must_divide.as_i64(),
),
LargestSmallerThanSmallest { smallest, largest } => {
write!(
f,
"largest unit ('{largest}') cannot be smaller than \
smallest unit ('{smallest}')",
largest = largest.singular(),
smallest = smallest.singular(),
)
}
RelativeWeekOrDay { 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(),
),
RelativeYearOrMonth { 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(),
),
RelativeYearOrMonthGivenDaysAre24Hours { 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(),
),
RoundToUnitUnsupported { unit } => write!(
f,
"rounding to '{unit}' is not supported",
unit = unit.plural(),
),
SignedDurationCalendarUnit { smallest } => write!(
f,
"rounding `jiff::SignedDuration` failed because \
the smallest unit provided, '{plural}', is a calendar unit \
(to round by calendar units, you must use a `jiff::Span`)",
plural = smallest.plural(),
),
TimeZoneOffset { given } => write!(
f,
"'{unit}' not allowed when rounding time zone offset \
(must use hours, minutes or seconds)",
unit = given.singular(),
),
}
}
}
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub(crate) enum MustDivide {
NanosPerCivilDay,
MicrosPerCivilDay,
MillisPerCivilDay,
SecsPerCivilDay,
MinsPerCivilDay,
HoursPerCivilDay,
NanosPerMicro,
MicrosPerMilli,
MillisPerSec,
SecsPerMin,
MinsPerHour,
// A weird case because we require that the rounding increment, when
// rounding to the nearest day for a date or datetime, is always `1`.
Days,
}
impl MustDivide {
pub(crate) fn as_i64(&self) -> i64 {
use self::MustDivide::*;
match *self {
NanosPerCivilDay => c::NANOS_PER_CIVIL_DAY,
MicrosPerCivilDay => c::MICROS_PER_CIVIL_DAY,
MillisPerCivilDay => c::MILLIS_PER_CIVIL_DAY,
SecsPerCivilDay => c::SECS_PER_CIVIL_DAY,
MinsPerCivilDay => c::MINS_PER_CIVIL_DAY,
HoursPerCivilDay => c::HOURS_PER_CIVIL_DAY,
NanosPerMicro => c::NANOS_PER_MICRO,
MicrosPerMilli => c::MICROS_PER_MILLI,
MillisPerSec => c::MILLIS_PER_SEC,
SecsPerMin => c::SECS_PER_MIN,
MinsPerHour => c::MINS_PER_HOUR,
Days => 2,
}
}
}
+192
View File
@@ -0,0 +1,192 @@
use crate::{error, util::escape::Byte};
#[derive(Clone, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub(crate) enum RoundingIncrementError {
ForDateTime,
ForOffset,
ForSignedDuration,
ForSpan,
ForTime,
ForTimestamp,
}
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"),
ForOffset => f.write_str("failed rounding time zone offset"),
ForSignedDuration => {
f.write_str("failed rounding signed duration")
}
ForSpan => f.write_str("failed rounding span"),
ForTime => f.write_str("failed rounding time"),
ForTimestamp => f.write_str("failed rounding timestamp"),
}
}
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
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)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
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>")
}
}
}
#[cfg(feature = "defmt")]
impl defmt::Format for OsStrUtf8Error {
fn format(&self, f: defmt::Formatter) {
// `OsStr` does not implement `defmt::Format`. Since this error is std-only
// and defmt is mainly used in embedded contexts, omitting the value is fine.
defmt::write!(f, "OsStrUtf8Error(unavailable)");
}
}
+72
View File
@@ -0,0 +1,72 @@
use crate::{error, Unit};
#[derive(Clone, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
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(),
),
}
}
}
File diff suppressed because it is too large Load Diff
+712
View File
@@ -0,0 +1,712 @@
/*!
A bespoke but easy to read format for [`Span`](crate::Span) and
[`SignedDuration`](crate::SignedDuration).
The "friendly" duration format is meant to be an alternative to [Temporal's
ISO 8601 duration format](super::temporal) that is both easier to read and can
losslessly serialize and deserialize all `Span` values.
Here are a variety of examples showing valid friendly durations for `Span`:
```
use jiff::{Span, ToSpan};
let spans = [
("40d", 40.days()),
("40 days", 40.days()),
("1y1d", 1.year().days(1)),
("1yr 1d", 1.year().days(1)),
("3d4h59m", 3.days().hours(4).minutes(59)),
("3 days, 4 hours, 59 minutes", 3.days().hours(4).minutes(59)),
("3d 4h 59m", 3.days().hours(4).minutes(59)),
("2h30m", 2.hours().minutes(30)),
("2h 30m", 2.hours().minutes(30)),
("1mo", 1.month()),
("1w", 1.week()),
("1 week", 1.week()),
("1w4d", 1.week().days(4)),
("1 wk 4 days", 1.week().days(4)),
("1m", 1.minute()),
("0.0021s", 2.milliseconds().microseconds(100)),
("0s", 0.seconds()),
("0d", 0.seconds()),
("0 days", 0.seconds()),
("3 mins 34s 123ms", 3.minutes().seconds(34).milliseconds(123)),
("3 mins 34.123 secs", 3.minutes().seconds(34).milliseconds(123)),
("3 mins 34,123s", 3.minutes().seconds(34).milliseconds(123)),
(
"1y1mo1d1h1m1.1s",
1.year().months(1).days(1).hours(1).minutes(1).seconds(1).milliseconds(100),
),
(
"1yr 1mo 1day 1hr 1min 1.1sec",
1.year().months(1).days(1).hours(1).minutes(1).seconds(1).milliseconds(100),
),
(
"1 year, 1 month, 1 day, 1 hour, 1 minute 1.1 seconds",
1.year().months(1).days(1).hours(1).minutes(1).seconds(1).milliseconds(100),
),
(
"1 year, 1 month, 1 day, 01:01:01.1",
1.year().months(1).days(1).hours(1).minutes(1).seconds(1).milliseconds(100),
),
];
for (string, span) in spans {
let parsed: Span = string.parse()?;
assert_eq!(
span.fieldwise(),
parsed.fieldwise(),
"result of parsing {string:?}",
);
}
# Ok::<(), Box<dyn std::error::Error>>(())
```
Note that for a `SignedDuration`, only units up to hours are supported. If you
need to support bigger units, then you'll need to convert it to a `Span` before
printing to the friendly format (or parse into a `Span` and then convert to a
`SignedDuration`).
# Integration points
While this module can of course be used to parse and print durations in the
friendly format, in most cases, you don't have to. Namely, it is already
integrated into the `Span` and `SignedDuration` types.
For example, the friendly format can be used by invoking the "alternate"
format when using the `std::fmt::Display` trait implementation:
```
use jiff::{SignedDuration, ToSpan};
let span = 2.months().days(35).hours(2).minutes(30);
assert_eq!(format!("{span}"), "P2M35DT2H30M"); // ISO 8601
assert_eq!(format!("{span:#}"), "2mo 35d 2h 30m"); // "friendly"
let sdur = SignedDuration::new(2 * 60 * 60 + 30 * 60, 123_456_789);
assert_eq!(format!("{sdur}"), "PT2H30M0.123456789S"); // ISO 8601
assert_eq!(format!("{sdur:#}"), "2h 30m 123ms 456µs 789ns"); // "friendly"
```
Both `Span` and `SignedDuration` use the "friendly" format for its
`std::fmt::Debug` trait implementation:
```
use jiff::{SignedDuration, ToSpan};
let span = 2.months().days(35).hours(2).minutes(30);
assert_eq!(format!("{span:?}"), "2mo 35d 2h 30m");
let sdur = SignedDuration::new(2 * 60 * 60 + 30 * 60, 123_456_789);
assert_eq!(format!("{sdur:?}"), "2h 30m 123ms 456µs 789ns");
```
Both `Span` and `SignedDuration` support parsing the ISO 8601 _and_ friendly
formats via its `std::str::FromStr` trait:
```
use jiff::{SignedDuration, Span, ToSpan};
let expected = 2.months().days(35).hours(2).minutes(30);
let span: Span = "2 months, 35 days, 02:30:00".parse()?;
assert_eq!(span, expected.fieldwise());
let span: Span = "P2M35DT2H30M".parse()?;
assert_eq!(span, expected.fieldwise());
let expected = SignedDuration::new(2 * 60 * 60 + 30 * 60, 123_456_789);
let sdur: SignedDuration = "2h 30m 0,123456789s".parse()?;
assert_eq!(sdur, expected);
let sdur: SignedDuration = "PT2h30m0.123456789s".parse()?;
assert_eq!(sdur, expected);
# Ok::<(), Box<dyn std::error::Error>>(())
```
If you need to parse _only_ the friendly format, then that would be a good use
case for using [`SpanParser`] in this module.
Finally, when the `serde` crate feature is enabled, the friendly format is
automatically supported via the `serde::Deserialize` trait implementation, just
like for the `std::str::FromStr` trait above. However, for `serde::Serialize`,
both types use ISO 8601. In order to serialize the friendly format,
you'll need to write your own serialization function or use one of the
[`fmt::serde`](crate::fmt::serde) helpers provided by Jiff. For example:
```
use jiff::{ToSpan, Span};
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct Record {
#[serde(
serialize_with = "jiff::fmt::serde::span::friendly::compact::required"
)]
span: Span,
}
let json = r#"{"span":"1 year 2 months 36 hours 1100ms"}"#;
let got: Record = serde_json::from_str(&json)?;
assert_eq!(
got.span.fieldwise(),
1.year().months(2).hours(36).milliseconds(1100),
);
let expected = r#"{"span":"1y 2mo 36h 1100ms"}"#;
assert_eq!(serde_json::to_string(&got).unwrap(), expected);
# Ok::<(), Box<dyn std::error::Error>>(())
```
The ISO 8601 format is used by default since it is part of a standard and is
more widely accepted. That is, if you need an interoperable interchange format,
then ISO 8601 is probably the right choice.
# Rounding
The printer in this module has no options for rounding. Instead, it is intended
for users to round a [`Span`](crate::Span) first, and then print it. The idea
is that printing a `Span` is a relatively "dumb" operation that just emits
whatever units are non-zero in the `Span`. This is possible with a `Span`
because it represents each unit distinctly. (With a [`std::time::Duration`] or
a [`jiff::SignedDuration`](crate::SignedDuration), more functionality would
need to be coupled with the printing logic to achieve a similar result.)
For example, if you want to print the duration since someone posted a comment
to an English speaking end user, precision below one half hour might be "too
much detail." You can remove this by rounding the `Span` to the nearest half
hour before printing:
```
use jiff::{civil, RoundMode, Unit, ZonedDifference};
let commented_at = civil::date(2024, 8, 1).at(19, 29, 13, 123_456_789).in_tz("US/Eastern")?;
let now = civil::date(2024, 12, 26).at(12, 49, 0, 0).in_tz("US/Eastern")?;
// The default, with units permitted up to years.
let span = now.since((Unit::Year, &commented_at))?;
assert_eq!(format!("{span:#}"), "4mo 24d 17h 19m 46s 876ms 543µs 211ns");
// The same subtraction, but with more options to control
// rounding the result. We could also do this with `Span::round`
// directly by providing `now` as our relative zoned datetime.
let rounded = now.since(
ZonedDifference::new(&commented_at)
.smallest(Unit::Minute)
.largest(Unit::Year)
.mode(RoundMode::HalfExpand)
.increment(30),
)?;
assert_eq!(format!("{rounded:#}"), "4mo 24d 17h 30m");
# Ok::<(), Box<dyn std::error::Error>>(())
```
# Comparison with the [`humantime`] crate
To a first approximation, Jiff should cover all `humantime` use cases,
including [`humantime-serde`] for serialization support.
To a second approximation, it was a design point of the friendly format to be
mostly interoperable with what `humantime` supports. For example, any duration
string formatted by `humantime` at time of writing is also a valid friendly
duration:
```
use std::time::Duration;
use jiff::{Span, ToSpan};
// Just a duration that includes as many unit designator labels as possible.
let dur = Duration::new(
2 * 31_557_600 + 1 * 2_630_016 + 15 * 86400 + 5 * 3600 + 59 * 60 + 1,
123_456_789,
);
let formatted = humantime::format_duration(dur).to_string();
assert_eq!(formatted, "2years 1month 15days 5h 59m 1s 123ms 456us 789ns");
let span: Span = formatted.parse()?;
let expected =
2.years()
.months(1)
.days(15)
.hours(5)
.minutes(59)
.seconds(1)
.milliseconds(123)
.microseconds(456)
.nanoseconds(789);
assert_eq!(span, expected.fieldwise());
# Ok::<(), Box<dyn std::error::Error>>(())
```
The above somewhat relies on the implementation details of `humantime`. Namely,
not everything parseable by `humantime` is also parseable by the friendly
format (and vice versa). For example, `humantime` parses `M` as a label for
months, but the friendly format specifically eschews `M` because of its
confusability with minutes:
```
use std::time::Duration;
let dur = humantime::parse_duration("1M")?;
// The +38,016 is because `humantime` assigns 30.44 24-hour days to all months.
assert_eq!(dur, Duration::new(30 * 24 * 60 * 60 + 38_016, 0));
// In contrast, Jiff will reject `1M`:
assert_eq!(
"1M".parse::<jiff::Span>().unwrap_err().to_string(),
"failed to parse input in the \"friendly\" duration format: \
expected to find unit designator suffix \
(e.g., `years` or `secs`) after parsing integer",
);
# Ok::<(), Box<dyn std::error::Error>>(())
```
In the other direction, Jiff's default formatting for the friendly duration
isn't always parsable by `humantime`. This is because, for example, depending
on the configuration, Jiff may use `mo` and `mos` for months, and `µs` for
microseconds, none of which are supported by `humantime`. If you need it, to
ensure `humantime` can parse a Jiff formatted friendly duration, Jiff provides
a special mode that attempts compatibility with `humantime`:
```
use jiff::{fmt::friendly::{Designator, SpanPrinter}, ToSpan};
let span =
2.years()
.months(1)
.days(15)
.hours(5)
.minutes(59)
.seconds(1)
.milliseconds(123)
.microseconds(456)
.nanoseconds(789);
let printer = SpanPrinter::new().designator(Designator::HumanTime);
assert_eq!(
printer.span_to_string(&span),
"2y 1month 15d 5h 59m 1s 123ms 456us 789ns",
);
```
It's hard to provide solid guarantees here because `humantime`'s behavior could
change, but at time of writing, `humantime` has not changed much in quite a
long time (its last release is almost 4 years ago at time of writing). So the
current behavior is likely pretty safe to rely upon.
More generally, the friendly format is more flexible than what `humantime`
supports. For example, the friendly format incorporates `HH:MM:SS` and
fractional time units. It also supports more unit labels and permits commas
to separate units.
```
use jiff::SignedDuration;
// 10 hours and 30 minutes
let expected = SignedDuration::new(10 * 60 * 60 + 30 * 60, 0);
assert_eq!(expected, "10h30m".parse()?);
assert_eq!(expected, "10hrs 30mins".parse()?);
assert_eq!(expected, "10 hours 30 minutes".parse()?);
assert_eq!(expected, "10 hours, 30 minutes".parse()?);
assert_eq!(expected, "10:30:00".parse()?);
assert_eq!(expected, "10.5 hours".parse()?);
# Ok::<(), Box<dyn std::error::Error>>(())
```
Finally, it's important to point out that `humantime` only supports parsing
variable width units like years, months and days by virtue of assigning fixed
static values to them that aren't always correct. In contrast, Jiff always
gets this right and specifically prevents you from getting it wrong.
To begin, Jiff returns an error if you try to parse a varying unit into a
[`SignedDuration`](crate::SignedDuration):
```
use jiff::SignedDuration;
// works fine
assert_eq!(
"1 hour".parse::<SignedDuration>().unwrap(),
SignedDuration::from_hours(1),
);
// Jiff is saving you from doing something wrong
assert_eq!(
"1 day".parse::<SignedDuration>().unwrap_err().to_string(),
"failed to parse input in the \"friendly\" duration format: \
parsing calendar units (days in this case) in this context \
is not supported (perhaps try parsing into a `jiff::Span` instead)",
);
```
As the error message suggests, parsing into a [`Span`](crate::Span) works fine:
```
use jiff::Span;
assert_eq!("1 day".parse::<Span>().unwrap(), Span::new().days(1).fieldwise());
```
Jiff has this behavior because it's not possible to determine, in general,
how long "1 day" (or "1 month" or "1 year") is without a reference date.
Since a `SignedDuration` (along with a [`std::time::Duration`]) does not
support expressing durations in anything other than a 96-bit integer number of
nanoseconds, it's not possible to represent concepts like "1 month." But a
[`Span`](crate::Span) can.
To see this more concretely, consider the different behavior resulting from
using `humantime` to parse durations and adding them to a date:
```
use jiff::{civil, Span};
let span: Span = "1 month".parse()?;
let dur = humantime::parse_duration("1 month")?;
let datetime = civil::date(2024, 5, 1).at(0, 0, 0, 0);
// Adding 1 month using a `Span` gives one possible expected result. That is,
// 2024-06-01T00:00:00 is exactly one month later than 2024-05-01T00:00:00.
assert_eq!(datetime + span, civil::date(2024, 6, 1).at(0, 0, 0, 0));
// But if we add the duration representing "1 month" as interpreted by
// humantime, we get a very odd result. This is because humantime uses
// a duration of 30.44 days (where every day is 24 hours exactly) for
// all months.
assert_eq!(datetime + dur, civil::date(2024, 5, 31).at(10, 33, 36, 0));
# Ok::<(), Box<dyn std::error::Error>>(())
```
The same is true for days when dealing with zoned date times:
```
use jiff::{civil, Span};
let span: Span = "1 day".parse()?;
let dur = humantime::parse_duration("1 day")?;
let zdt = civil::date(2024, 3, 9).at(17, 0, 0, 0).in_tz("US/Eastern")?;
// Adding 1 day gives the generally expected result of the same clock
// time on the following day when adding a `Span`.
assert_eq!(&zdt + span, civil::date(2024, 3, 10).at(17, 0, 0, 0).in_tz("US/Eastern")?);
// But with humantime, all days are assumed to be exactly 24 hours. So
// you get an instant in time that is 24 hours later, even when some
// days are shorter and some are longer.
assert_eq!(&zdt + dur, civil::date(2024, 3, 10).at(18, 0, 0, 0).in_tz("US/Eastern")?);
// Notice also that this inaccuracy can occur merely by a duration that
// _crosses_ a time zone transition boundary (like DST) at any point. It
// doesn't require your datetimes to be "close" to when DST occurred.
let dur = humantime::parse_duration("20 day")?;
let zdt = civil::date(2024, 3, 1).at(17, 0, 0, 0).in_tz("US/Eastern")?;
assert_eq!(&zdt + dur, civil::date(2024, 3, 21).at(18, 0, 0, 0).in_tz("US/Eastern")?);
# Ok::<(), Box<dyn std::error::Error>>(())
```
It's worth pointing out that in some applications, the fixed values assigned
by `humantime` might be perfectly acceptable. Namely, they introduce error
into calculations, but the error might be small enough to be a non-issue in
some applications. But this error _can_ be avoided and `humantime` commits
it silently. Indeed, `humantime`'s API is itself not possible without either
rejecting varying length units or assuming fixed values for them. This is
because it parses varying length units but returns a duration expressed as a
single 96-bit integer number of nanoseconds. In order to do this, you _must_
assume a definite length for those varying units. To do this _correctly_, you
really need to provide a reference date.
For example, Jiff can parse `1 month` into a `std::time::Duration` too, but
it requires parsing into a `Span` and then converting into a `Duration` by
providing a reference date:
```
use std::time::Duration;
use jiff::{civil, Span};
let span: Span = "1 month".parse()?;
// converts to signed duration
let sdur = span.to_duration(civil::date(2024, 5, 1))?;
// converts to standard library unsigned duration
let dur = Duration::try_from(sdur)?;
// exactly 31 days where each day is 24 hours long.
assert_eq!(dur, Duration::from_secs(31 * 24 * 60 * 60));
// Now change the reference date and notice that the
// resulting duration is changed but still correct.
let sdur = span.to_duration(civil::date(2024, 6, 1))?;
let dur = Duration::try_from(sdur)?;
// exactly 30 days where each day is 24 hours long.
assert_eq!(dur, Duration::from_secs(30 * 24 * 60 * 60));
# Ok::<(), Box<dyn std::error::Error>>(())
```
# Motivation
This format was devised, in part, because the standard duration interchange
format specified by [Temporal's ISO 8601 definition](super::temporal) is
sub-optimal in two important respects:
1. It doesn't support individual sub-second components.
2. It is difficult to read.
In the first case, ISO 8601 durations do support sub-second components, but are
only expressible as fractional seconds. For example:
```text
PT1.100S
```
This is problematic in some cases because it doesn't permit distinguishing
between some spans. For example, `1.second().milliseconds(100)` and
`1100.milliseconds()` both serialize to the same ISO 8601 duration as shown
above. At deserialization time, it's impossible to know what the span originally
looked like. Thus, using the ISO 8601 format means the serialization and
deserialization of [`Span`](crate::Span) values is lossy.
In the second case, ISO 8601 durations appear somewhat difficult to quickly
read. For example:
```text
P1Y2M3DT4H59M1.1S
P1y2m3dT4h59m1.1S
```
When all of the unit designators are capital letters in particular (which
is the default), everything runs together and it's hard for the eye to
distinguish where digits stop and letters begin. Using lowercase letters for
unit designators helps somewhat, but this is an extension to ISO 8601 that
isn't broadly supported.
The "friendly" format resolves both of these problems by permitting sub-second
components and allowing the use of whitespace and longer unit designator labels
to improve readability. For example, all of the following are equivalent and
will parse to the same `Span`:
```text
1y 2mo 3d 4h 59m 1100ms
1 year 2 months 3 days 4h59m1100ms
1 year, 2 months, 3 days, 4h59m1100ms
1 year, 2 months, 3 days, 4 hours 59 minutes 1100 milliseconds
```
At the same time, the friendly format continues to support fractional
time components since they may be desirable in some cases. For example, all
of the following are equivalent:
```text
1h 1m 1.5s
1h 1m 1,5s
01:01:01.5
01:01:01,5
```
The idea with the friendly format is that end users who know how to write
English durations are happy to both read and write durations in this format.
And moreover, the format is flexible enough that end users generally don't need
to stare at a grammar to figure out how to write a valid duration. Most of the
intuitive things you'd expect to work will work.
# Internationalization
Currently, only US English unit designator labels are supported. In general,
Jiff resists trying to solve the internationalization problem in favor
of punting it to another crate, such as [`icu`] via [`jiff-icu`]. Jiff
_could_ adopt unit designator labels for other languages, but it's not
totally clear whether that's the right path to follow given the complexity
of internationalization. If you'd like to discuss it, please
[file an issue](https://github.com/BurntSushi/jiff/issues).
# Grammar
This section gives a more precise description of the "friendly" duration format
in the form of a grammar.
```text
format =
format-signed-hms
| format-signed-designator
format-signed-hms =
sign? format-hms
format-hms =
[0-9]+ ':' [0-9]+ ':' [0-9]+ fractional?
format-signed-designator =
sign? format-designator-units
| format-designator-units direction?
format-designator-units =
years
| months
| weeks
| days
| hours
| minutes
| seconds
| milliseconds
| microseconds
| nanoseconds
# This dance below is basically to ensure a few things:
# First, that at least one unit appears. That is, that
# we don't accept the empty string. Secondly, when a
# fractional component appears in a time value, we don't
# allow any subsequent units to appear. Thirdly, that
# `HH:MM:SS[.f{1,9}]?` is allowed after years, months,
# weeks or days.
years =
unit-value unit-years comma? ws* format-hms
| unit-value unit-years comma? ws* months
| unit-value unit-years comma? ws* weeks
| unit-value unit-years comma? ws* days
| unit-value unit-years comma? ws* hours
| unit-value unit-years comma? ws* minutes
| unit-value unit-years comma? ws* seconds
| unit-value unit-years comma? ws* milliseconds
| unit-value unit-years comma? ws* microseconds
| unit-value unit-years comma? ws* nanoseconds
| unit-value unit-years
months =
unit-value unit-months comma? ws* format-hms
| unit-value unit-months comma? ws* weeks
| unit-value unit-months comma? ws* days
| unit-value unit-months comma? ws* hours
| unit-value unit-months comma? ws* minutes
| unit-value unit-months comma? ws* seconds
| unit-value unit-months comma? ws* milliseconds
| unit-value unit-months comma? ws* microseconds
| unit-value unit-months comma? ws* nanoseconds
| unit-value unit-months
weeks =
unit-value unit-weeks comma? ws* format-hms
| unit-value unit-weeks comma? ws* days
| unit-value unit-weeks comma? ws* hours
| unit-value unit-weeks comma? ws* minutes
| unit-value unit-weeks comma? ws* seconds
| unit-value unit-weeks comma? ws* milliseconds
| unit-value unit-weeks comma? ws* microseconds
| unit-value unit-weeks comma? ws* nanoseconds
| unit-value unit-weeks
days =
unit-value unit-days comma? ws* format-hms
| unit-value unit-days comma? ws* hours
| unit-value unit-days comma? ws* minutes
| unit-value unit-days comma? ws* seconds
| unit-value unit-days comma? ws* milliseconds
| unit-value unit-days comma? ws* microseconds
| unit-value unit-days comma? ws* nanoseconds
| unit-value unit-days
hours =
unit-value unit-hours comma? ws* minutes
| unit-value unit-hours comma? ws* seconds
| unit-value unit-hours comma? ws* milliseconds
| unit-value unit-hours comma? ws* microseconds
| unit-value unit-hours comma? ws* nanoseconds
| unit-value fractional? ws* unit-hours
minutes =
unit-value unit-minutes comma? ws* seconds
| unit-value unit-minutes comma? ws* milliseconds
| unit-value unit-minutes comma? ws* microseconds
| unit-value unit-minutes comma? ws* nanoseconds
| unit-value fractional? ws* unit-minutes
seconds =
unit-value unit-seconds comma? ws* milliseconds
| unit-value unit-seconds comma? ws* microseconds
| unit-value unit-seconds comma? ws* nanoseconds
| unit-value fractional? ws* unit-seconds
milliseconds =
unit-value unit-milliseconds comma? ws* microseconds
| unit-value unit-milliseconds comma? ws* nanoseconds
| unit-value fractional? ws* unit-milliseconds
microseconds =
unit-value unit-microseconds comma? ws* nanoseconds
| unit-value fractional? ws* unit-microseconds
nanoseconds =
unit-value fractional? ws* unit-nanoseconds
unit-value = [0-9]+ [ws*]
unit-years = 'years' | 'year' | 'yrs' | 'yr' | 'y'
unit-months = 'months' | 'month' | 'mos' | 'mo'
unit-weeks = 'weeks' | 'week' | 'wks' | 'wk' | 'w'
unit-days = 'days' | 'day' | 'd'
unit-hours = 'hours' | 'hour' | 'hrs' | 'hr' | 'h'
unit-minutes = 'minutes' | 'minute' | 'mins' | 'min' | 'm'
unit-seconds = 'seconds' | 'second' | 'secs' | 'sec' | 's'
unit-milliseconds =
'milliseconds'
| 'millisecond'
| 'millis'
| 'milli'
| 'msecs'
| 'msec'
| 'ms'
unit-microseconds =
'microseconds'
| 'microsecond'
| 'micros'
| 'micro'
| 'usecs'
| 'usec'
| 'µ' (U+00B5 MICRO SIGN) 'secs'
| 'µ' (U+00B5 MICRO SIGN) 'sec'
| 'us'
| 'µ' (U+00B5 MICRO SIGN) 's'
unit-nanoseconds =
'nanoseconds' | 'nanosecond' | 'nanos' | 'nano' | 'nsecs' | 'nsec' | 'ns'
fractional = decimal-separator decimal-fraction
decimal-separator = '.' | ','
decimal-fraction = [0-9]{1,9}
sign = '+' | '-'
direction = ws 'ago'
comma = ',' ws
ws =
U+0020 SPACE
| U+0009 HORIZONTAL TAB
| U+000A LINE FEED
| U+000C FORM FEED
| U+000D CARRIAGE RETURN
```
One thing not specified by the grammar above are maximum values. Namely,
there are no specific maximum values imposed for each individual unit, nor
a maximum value for the entire duration (say, when converted to nanoseconds).
Instead, implementations are expected to impose their own limitations.
For Jiff, a `Span` is more limited than a `SignedDuration`. For example, a the
year component of a `Span` is limited to `[-19,999, 19,999]`. In contrast,
a `SignedDuration` is a 96-bit signed integer number of nanoseconds with no
particular limits on the individual units. They just can't combine to something
that overflows a 96-bit signed integer number of nanoseconds. (And parsing into
a `SignedDuration` directly only supports units of hours or smaller, since
bigger units do not have an invariant length.) In general, implementations
should support a "reasonable" range of values.
[`humantime`]: https://docs.rs/humantime
[`humantime-serde`]: https://docs.rs/humantime-serde
[`icu`]: https://docs.rs/icu
[`jiff-icu`]: https://docs.rs/jiff-icu
*/
pub use self::{
parser::SpanParser,
printer::{Designator, Direction, FractionalUnit, Spacing, SpanPrinter},
};
/// The default span/duration parser that we use.
pub(crate) static DEFAULT_SPAN_PARSER: SpanParser = SpanParser::new();
/// The default span/duration printer that we use.
pub(crate) static DEFAULT_SPAN_PRINTER: SpanPrinter = SpanPrinter::new();
mod parser;
mod parser_label;
mod printer;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,90 @@
// auto-generated by: jiff-cli generate unit-designator-match
use crate::Unit;
#[inline(always)]
pub(super) fn find(haystack: &[u8]) -> Option<(Unit, usize)> {
match haystack {
&[b'm', b'i', b'l', b'l', b'i', b's', b'e', b'c', b'o', b'n', b'd', b's', ..] => {
Some((Unit::Millisecond, 12))
}
&[b'm', b'i', b'c', b'r', b'o', b's', b'e', b'c', b'o', b'n', b'd', b's', ..] => {
Some((Unit::Microsecond, 12))
}
&[b'n', b'a', b'n', b'o', b's', b'e', b'c', b'o', b'n', b'd', b's', ..] => {
Some((Unit::Nanosecond, 11))
}
&[b'm', b'i', b'l', b'l', b'i', b's', b'e', b'c', b'o', b'n', b'd', ..] => {
Some((Unit::Millisecond, 11))
}
&[b'm', b'i', b'c', b'r', b'o', b's', b'e', b'c', b'o', b'n', b'd', ..] => {
Some((Unit::Microsecond, 11))
}
&[b'n', b'a', b'n', b'o', b's', b'e', b'c', b'o', b'n', b'd', ..] => {
Some((Unit::Nanosecond, 10))
}
&[b's', b'e', b'c', b'o', b'n', b'd', b's', ..] => {
Some((Unit::Second, 7))
}
&[b'm', b'i', b'n', b'u', b't', b'e', b's', ..] => {
Some((Unit::Minute, 7))
}
&[b'\xc2', b'\xb5', b's', b'e', b'c', b's', ..] => {
Some((Unit::Microsecond, 6))
}
&[b's', b'e', b'c', b'o', b'n', b'd', ..] => Some((Unit::Second, 6)),
&[b'm', b'o', b'n', b't', b'h', b's', ..] => Some((Unit::Month, 6)),
&[b'm', b'i', b'n', b'u', b't', b'e', ..] => Some((Unit::Minute, 6)),
&[b'm', b'i', b'l', b'l', b'i', b's', ..] => {
Some((Unit::Millisecond, 6))
}
&[b'm', b'i', b'c', b'r', b'o', b's', ..] => {
Some((Unit::Microsecond, 6))
}
&[b'\xc2', b'\xb5', b's', b'e', b'c', ..] => {
Some((Unit::Microsecond, 5))
}
&[b'y', b'e', b'a', b'r', b's', ..] => Some((Unit::Year, 5)),
&[b'w', b'e', b'e', b'k', b's', ..] => Some((Unit::Week, 5)),
&[b'u', b's', b'e', b'c', b's', ..] => Some((Unit::Microsecond, 5)),
&[b'n', b's', b'e', b'c', b's', ..] => Some((Unit::Nanosecond, 5)),
&[b'n', b'a', b'n', b'o', b's', ..] => Some((Unit::Nanosecond, 5)),
&[b'm', b's', b'e', b'c', b's', ..] => Some((Unit::Millisecond, 5)),
&[b'm', b'o', b'n', b't', b'h', ..] => Some((Unit::Month, 5)),
&[b'm', b'i', b'l', b'l', b'i', ..] => Some((Unit::Millisecond, 5)),
&[b'm', b'i', b'c', b'r', b'o', ..] => Some((Unit::Microsecond, 5)),
&[b'h', b'o', b'u', b'r', b's', ..] => Some((Unit::Hour, 5)),
&[b'y', b'e', b'a', b'r', ..] => Some((Unit::Year, 4)),
&[b'w', b'e', b'e', b'k', ..] => Some((Unit::Week, 4)),
&[b'u', b's', b'e', b'c', ..] => Some((Unit::Microsecond, 4)),
&[b's', b'e', b'c', b's', ..] => Some((Unit::Second, 4)),
&[b'n', b's', b'e', b'c', ..] => Some((Unit::Nanosecond, 4)),
&[b'n', b'a', b'n', b'o', ..] => Some((Unit::Nanosecond, 4)),
&[b'm', b's', b'e', b'c', ..] => Some((Unit::Millisecond, 4)),
&[b'm', b'i', b'n', b's', ..] => Some((Unit::Minute, 4)),
&[b'h', b'o', b'u', b'r', ..] => Some((Unit::Hour, 4)),
&[b'd', b'a', b'y', b's', ..] => Some((Unit::Day, 4)),
&[b'\xc2', b'\xb5', b's', ..] => Some((Unit::Microsecond, 3)),
&[b'y', b'r', b's', ..] => Some((Unit::Year, 3)),
&[b'w', b'k', b's', ..] => Some((Unit::Week, 3)),
&[b's', b'e', b'c', ..] => Some((Unit::Second, 3)),
&[b'm', b'o', b's', ..] => Some((Unit::Month, 3)),
&[b'm', b'i', b'n', ..] => Some((Unit::Minute, 3)),
&[b'h', b'r', b's', ..] => Some((Unit::Hour, 3)),
&[b'd', b'a', b'y', ..] => Some((Unit::Day, 3)),
&[b'y', b'r', ..] => Some((Unit::Year, 2)),
&[b'w', b'k', ..] => Some((Unit::Week, 2)),
&[b'u', b's', ..] => Some((Unit::Microsecond, 2)),
&[b'n', b's', ..] => Some((Unit::Nanosecond, 2)),
&[b'm', b's', ..] => Some((Unit::Millisecond, 2)),
&[b'm', b'o', ..] => Some((Unit::Month, 2)),
&[b'h', b'r', ..] => Some((Unit::Hour, 2)),
&[b'y', ..] => Some((Unit::Year, 1)),
&[b'w', ..] => Some((Unit::Week, 1)),
&[b's', ..] => Some((Unit::Second, 1)),
&[b'm', ..] => Some((Unit::Minute, 1)),
&[b'h', ..] => Some((Unit::Hour, 1)),
&[b'd', ..] => Some((Unit::Day, 1)),
_ => None,
}
}
File diff suppressed because it is too large Load Diff
+525
View File
@@ -0,0 +1,525 @@
/*!
Configurable support for printing and parsing datetimes and durations.
Note that for most use cases, you should be using the corresponding
[`Display`](std::fmt::Display) or [`FromStr`](std::str::FromStr) trait
implementations for printing and parsing respectively. The APIs in this module
provide more configurable support for printing and parsing.
# Tables of examples
The tables below attempt to show some examples of datetime and duration
formatting, along with names and links to relevant routines and types. The
point of these tables is to give a general overview of the formatting and
parsing functionality in these sub-modules.
## Support for `FromStr` and `Display`
This table lists the formats supported by the [`FromStr`] and [`Display`]
trait implementations on the datetime and duration types in Jiff.
In all of these cases, the trait implementations are mere conveniences for
functionality provided by the [`temporal`] sub-module (and, in a couple cases,
the [`friendly`] sub-module). The sub-modules provide lower level control
(such as parsing from `&[u8]`) and more configuration (such as controlling the
disambiguation strategy used when parsing zoned datetime [RFC-9557] strings).
| Example | Format | Links |
| ------- | ------ | ----- |
| `2025-08-20T17:35:00Z` | [RFC-3339] | [`Timestamp`] |
| `2025-08-20T17:35:00-05` | [RFC-3339] | [`FromStr`] impl and<br>[`Timestamp::display_with_offset`] |
| `2025-08-20T17:35:00+02[Poland]` | [RFC-9557] | [`Zoned`] |
| `2025-08-20T17:35:00+02:00[+02:00]` | [RFC-9557] | [`Zoned`] |
| `2025-08-20T17:35:00` | [ISO-8601] | [`civil::DateTime`] |
| `2025-08-20` | [ISO-8601] | [`civil::Date`] |
| `17:35:00` | [ISO-8601] | [`civil::Time`] |
| `P1Y2M3W4DT5H6M7S` | [ISO-8601], [Temporal] | [`Span`] |
| `PT1H2M3S` | [ISO-8601] | [`SignedDuration`], [`Span`] |
| `PT1H2M3.123456789S` | [ISO-8601] | [`SignedDuration`], [`Span`] |
| `1d 2h 3m 5s` | [`friendly`] | [`FromStr`] impl and alternative [`Display`]<br>via `{:#}` for [`SignedDuration`], [`Span`] |
Note that for datetimes like `2025-08-20T17:35:00`, the following variants are
also accepted:
```text
2025-08-20 17:35:00
2025-08-20T17:35:00.123456789
2025-08-20T17:35
2025-08-20T17
```
This applies to RFC 3339 and RFC 9557 timestamps as well.
Also, for ISO 8601 durations, the unit designator labels are matched
case insensitively. For example, `PT1h2m3s` is recognized by Jiff.
## The "friendly" duration format
This table lists a few examples of the [`friendly`] duration format. Briefly,
it is a bespoke format for Jiff, but is meant to match similar bespoke formats
used elsewhere and be easier to read than the standard ISO 8601 duration
format.
All examples below can be parsed via a [`Span`]'s [`FromStr`] trait
implementation. All examples with units no bigger than hours can be parsed via
a [`SignedDuration`]'s [`FromStr`] trait implementation. This table otherwise
shows the options for printing durations in the format shown.
| Example | Print configuration |
| ------- | ------------------- |
| `1year 2months` | [`Designator::Verbose`] via [`SpanPrinter::designator`] |
| `1yr 2mos` | [`Designator::Short`] via [`SpanPrinter::designator`] |
| `1y 2mo` | [`Designator::Compact`] via [`SpanPrinter::designator`] (default) |
| `1h2m3s` | [`Spacing::None`] via [`SpanPrinter::spacing`] |
| `1h 2m 3s` | [`Spacing::BetweenUnits`] via [`SpanPrinter::spacing`] (default) |
| `1 h 2 m 3 s` | [`Spacing::BetweenUnitsAndDesignators`] via [`SpanPrinter::spacing`] |
| `2d 3h ago` | [`Direction::Auto`] via [`SpanPrinter::direction`] (default) |
| `-2d 3h` | [`Direction::Sign`] via [`SpanPrinter::direction`] |
| `+2d 3h` | [`Direction::ForceSign`] via [`SpanPrinter::direction`] |
| `2d 3h ago` | [`Direction::Suffix`] via [`SpanPrinter::direction`] |
| `9.123456789s` | [`FractionalUnit::Second`] via [`SpanPrinter::fractional`] |
| `1y, 2mo` | [`SpanPrinter::comma_after_designator`] |
| `15d 02:59:15.123` | [`SpanPrinter::hours_minutes_seconds`] |
## Bespoke datetime formats via `strptime` and `strftime`
Every datetime type has bespoke formatting routines defined on it. For
example, [`Zoned::strptime`] and [`civil::Date::strftime`]. Additionally, the
[`strtime`] sub-module also provides convenience routines, [`strtime::format`]
and [`strtime::parse`], where the former is generic over any datetime type in
Jiff and the latter provides a [`BrokenDownTime`] for granular parsing.
| Example | Format string |
| ------- | ------------- |
| `2025-05-20` | `%Y-%m-%d` |
| `2025-05-20` | `%F` |
| `2025-W21-2` | `%G-W%V-%u` |
| `05/20/25` | `%m/%d/%y` |
| `Monday, February 10, 2025 at 9:01pm -0500` | `%A, %B %d, %Y at %-I:%M%P %z` |
| `Monday, February 10, 2025 at 9:01pm EST` | `%A, %B %d, %Y at %-I:%M%P %Z` |
| `Monday, February 10, 2025 at 9:01pm America/New_York` | `%A, %B %d, %Y at %-I:%M%P %Q` |
The specific conversion specifiers supported are documented in the [`strtime`]
sub-module. While precise POSIX compatibility is not guaranteed, the conversion
specifiers are generally meant to match prevailing implementations. (Although
there are many such implementations and they each tend to have their own quirks
and features.)
## RFC 2822 parsing and printing
[RFC-2822] support is provided by the [`rfc2822`] sub-module.
| Example | Links |
| ------- | ----- |
| `Thu, 29 Feb 2024 05:34 -0500` | [`rfc2822::parse`] and [`rfc2822::to_string`] |
| `Thu, 01 Jan 1970 00:00:01 GMT` | [`DateTimePrinter::timestamp_to_rfc9110_string`] |
[Temporal]: https://tc39.es/proposal-temporal/#sec-temporal-iso8601grammar
[ISO-8601]: https://www.iso.org/iso-8601-date-and-time-format.html
[RFC-3339]: https://www.rfc-editor.org/rfc/rfc3339
[RFC-9557]: https://www.rfc-editor.org/rfc/rfc9557.html
[ISO-8601]: https://www.iso.org/iso-8601-date-and-time-format.html
[RFC-2822]: https://datatracker.ietf.org/doc/html/rfc2822
[RFC-9110]: https://datatracker.ietf.org/doc/html/rfc9110#section-5.6.7-15
[`Display`]: std::fmt::Display
[`FromStr`]: std::str::FromStr
[`friendly`]: crate::fmt::friendly
[`temporal`]: crate::fmt::temporal
[`rfc2822`]: crate::fmt::rfc2822
[`strtime`]: crate::fmt::strtime
[`civil::DateTime`]: crate::civil::DateTime
[`civil::Date`]: crate::civil::Date
[`civil::Date::strftime`]: crate::civil::Date::strftime
[`civil::Time`]: crate::civil::Time
[`SignedDuration`]: crate::SignedDuration
[`Span`]: crate::Span
[`Timestamp`]: crate::Timestamp
[`Timestamp::display_with_offset`]: crate::Timestamp::display_with_offset
[`Zoned`]: crate::Zoned
[`Zoned::strptime`]: crate::Zoned::strptime
[`Designator::Verbose`]: crate::fmt::friendly::Designator::Verbose
[`Designator::Short`]: crate::fmt::friendly::Designator::Short
[`Designator::Compact`]: crate::fmt::friendly::Designator::Compact
[`Spacing::None`]: crate::fmt::friendly::Spacing::None
[`Spacing::BetweenUnits`]: crate::fmt::friendly::Spacing::BetweenUnits
[`Spacing::BetweenUnitsAndDesignators`]: crate::fmt::friendly::Spacing::BetweenUnitsAndDesignators
[`Direction::Auto`]: crate::fmt::friendly::Direction::Auto
[`Direction::Sign`]: crate::fmt::friendly::Direction::Sign
[`Direction::ForceSign`]: crate::fmt::friendly::Direction::ForceSign
[`Direction::Suffix`]: crate::fmt::friendly::Direction::Suffix
[`FractionalUnit::Second`]: crate::fmt::friendly::FractionalUnit::Second
[`SpanPrinter::designator`]: crate::fmt::friendly::SpanPrinter::designator
[`SpanPrinter::spacing`]: crate::fmt::friendly::SpanPrinter::spacing
[`SpanPrinter::direction`]: crate::fmt::friendly::SpanPrinter::direction
[`SpanPrinter::fractional`]: crate::fmt::friendly::SpanPrinter::fractional
[`SpanPrinter::comma_after_designator`]: crate::fmt::friendly::SpanPrinter::comma_after_designator
[`SpanPrinter::hours_minutes_seconds`]: crate::fmt::friendly::SpanPrinter::hours_minutes_seconds
[`BrokenDownTime`]: crate::fmt::strtime::BrokenDownTime
[`strtime::parse`]: crate::fmt::strtime::parse
[`strtime::format`]: crate::fmt::strtime::format
[`rfc2822::parse`]: crate::fmt::rfc2822::parse
[`rfc2822::to_string`]: crate::fmt::rfc2822::to_string
[`DateTimePrinter::timestamp_to_rfc9110_string`]: crate::fmt::rfc2822::DateTimePrinter::timestamp_to_rfc9110_string
*/
use crate::{
error::{fmt::Error as E, Error},
util::escape,
};
mod buffer;
pub mod friendly;
mod offset;
pub mod rfc2822;
mod rfc9557;
#[cfg(feature = "serde")]
pub mod serde;
pub mod strtime;
pub mod temporal;
mod util;
/// The result of parsing a value out of a slice of bytes.
///
/// This contains both the parsed value and the offset at which the value
/// ended in the input given. This makes it possible to parse, for example, a
/// datetime value as a prefix of some larger string without knowing ahead of
/// time where it ends.
#[derive(Clone)]
pub(crate) struct Parsed<'i, V> {
/// The value parsed.
value: V,
/// The remaining unparsed input.
input: &'i [u8],
}
impl<'i, V> Parsed<'i, V> {
#[inline]
fn and_then<U>(
self,
map: impl FnOnce(V) -> Result<U, Error>,
) -> Result<Parsed<'i, U>, Error> {
let Parsed { value, input } = self;
Ok(Parsed { value: map(value)?, input })
}
}
impl<'i, V: core::fmt::Display> Parsed<'i, V> {
/// Ensures that the parsed value represents the entire input. This occurs
/// precisely when the `input` on this parsed value is empty.
///
/// This is useful when one expects a parsed value to consume the entire
/// input, and to consider it an error if it doesn't.
#[inline]
fn into_full(self) -> Result<V, Error> {
if self.input.is_empty() {
return Ok(self.value);
}
Err(Error::from(E::into_full_error(&self.value, self.input)))
}
}
impl<'i, V> Parsed<'i, V> {
/// Ensures that the parsed value represents the entire input. This occurs
/// precisely when the `input` on this parsed value is empty.
///
/// This is useful when one expects a parsed value to consume the entire
/// input, and to consider it an error if it doesn't.
///
/// This is like `Parsed::into_full`, but lets the caller provide a custom
/// `Display` implementation.
#[inline]
fn into_full_with(
self,
display: impl core::fmt::Display,
) -> Result<V, Error> {
if self.input.is_empty() {
return Ok(self.value);
}
Err(Error::from(E::into_full_error(&display, self.input)))
}
}
impl<'i, V: core::fmt::Debug> core::fmt::Debug for Parsed<'i, V> {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.debug_struct("Parsed")
.field("value", &self.value)
.field("input", &escape::Bytes(self.input))
.finish()
}
}
/// A trait for printing datetimes or spans into Unicode-accepting buffers or
/// streams.
///
/// The most useful implementations of this trait are for the `String` and
/// `Vec<u8>` types. But any implementation of [`std::fmt::Write`] and
/// [`std::io::Write`] can be used via the [`StdFmtWrite`] and [`StdIoWrite`]
/// adapters, respectively.
///
/// Most users of Jiff should not need to interact with this trait directly.
/// Instead, printing is handled via the [`Display`](std::fmt::Display)
/// implementation of the relevant type.
///
/// # Design
///
/// This trait is a near-clone of the `std::fmt::Write` trait. It's also very
/// similar to the `std::io::Write` trait, but like `std::fmt::Write`, this
/// trait is limited to writing valid UTF-8. The UTF-8 restriction was adopted
/// because we really want to support printing datetimes and spans to `String`
/// buffers. If we permitted writing `&[u8]` data, then writing to a `String`
/// buffer would always require a costly UTF-8 validation check.
///
/// The `std::fmt::Write` trait wasn't used itself because:
///
/// 1. Using a custom trait allows us to require using Jiff's error type.
/// (Although this extra flexibility isn't currently used much, since printing
/// rarely fails for any reason other than the underlying `jiff::fmt::Write`
/// implementation failing.)
/// 2. Using a custom trait allows us more control over the implementations of
/// the trait. For example, a custom trait means we can format directly into
/// a `Vec<u8>` buffer, which isn't possible with `std::fmt::Write` because
/// there is no `std::fmt::Write` trait implementation for `Vec<u8>`.
pub trait Write {
/// Write the given string to this writer, returning whether the write
/// succeeded or not.
fn write_str(&mut self, string: &str) -> Result<(), Error>;
/// Write the given character to this writer, returning whether the write
/// succeeded or not.
#[inline]
fn write_char(&mut self, char: char) -> Result<(), Error> {
self.write_str(char.encode_utf8(&mut [0; 4]))
}
/// Returns a `Vec<u8>` backing store for this implementation.
///
/// Consumers of this trait may call this method to get a view directly
/// into a buffer for more optimized writing.
///
/// The default implementation always returns `None`.
///
/// This method is only available when Jiff's `alloc` feature is enabled.
///
/// # Safety
///
/// Callers must ensure that only valid UTF-8 is written to the buffer
/// returned.
#[cfg(feature = "alloc")]
#[inline]
unsafe fn as_mut_vec(&mut self) -> Option<&mut alloc::vec::Vec<u8>> {
None
}
}
#[cfg(any(test, feature = "alloc"))]
impl Write for alloc::string::String {
#[inline]
fn write_str(&mut self, string: &str) -> Result<(), Error> {
self.push_str(string);
Ok(())
}
#[cfg(feature = "alloc")]
#[inline]
unsafe fn as_mut_vec(&mut self) -> Option<&mut alloc::vec::Vec<u8>> {
// SAFETY: The conditions are forwarded to the trait interface.
unsafe { Some(alloc::string::String::as_mut_vec(self)) }
}
}
#[cfg(any(test, feature = "alloc"))]
impl Write for alloc::vec::Vec<u8> {
#[inline]
fn write_str(&mut self, string: &str) -> Result<(), Error> {
self.extend_from_slice(string.as_bytes());
Ok(())
}
#[cfg(feature = "alloc")]
#[inline]
unsafe fn as_mut_vec(&mut self) -> Option<&mut alloc::vec::Vec<u8>> {
Some(self)
}
}
impl<W: Write> Write for &mut W {
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)
}
#[cfg(feature = "alloc")]
#[inline]
unsafe fn as_mut_vec(&mut self) -> Option<&mut alloc::vec::Vec<u8>> {
// SAFETY: The conditions are forwarded to the trait interface.
unsafe { (**self).as_mut_vec() }
}
}
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)
}
#[cfg(feature = "alloc")]
#[inline]
unsafe fn as_mut_vec(&mut self) -> Option<&mut alloc::vec::Vec<u8>> {
// SAFETY: The conditions are forwarded to the trait interface.
unsafe { (**self).as_mut_vec() }
}
}
/// An adapter for using `std::io::Write` implementations with `fmt::Write`.
///
/// This is useful when one wants to format a datetime or span directly
/// to something with a `std::io::Write` trait implementation but not a
/// `fmt::Write` implementation.
///
/// # Example
///
/// ```no_run
/// use std::{fs::File, io::{BufWriter, Write}, path::Path};
///
/// use jiff::{civil::date, fmt::{StdIoWrite, temporal::DateTimePrinter}};
///
/// let zdt = date(2024, 6, 15).at(7, 0, 0, 0).in_tz("America/New_York")?;
///
/// let path = Path::new("/tmp/output");
/// let mut file = BufWriter::new(File::create(path)?);
/// DateTimePrinter::new().print_zoned(&zdt, StdIoWrite(&mut file)).unwrap();
/// file.flush()?;
/// assert_eq!(
/// std::fs::read_to_string(path)?,
/// "2024-06-15T07:00:00-04:00[America/New_York]",
/// );
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
#[cfg(feature = "std")]
#[derive(Clone, Debug)]
pub struct StdIoWrite<W>(pub W);
#[cfg(feature = "std")]
impl<W: std::io::Write> Write for StdIoWrite<W> {
#[inline]
fn write_str(&mut self, string: &str) -> Result<(), Error> {
self.0.write_all(string.as_bytes()).map_err(Error::io)
}
}
/// An adapter for using `std::fmt::Write` implementations with `fmt::Write`.
///
/// This is useful when one wants to format a datetime or span directly
/// to something with a `std::fmt::Write` trait implementation but not a
/// `fmt::Write` implementation.
///
/// (Despite using `Std` in this name, this type is available in `core`-only
/// configurations.)
///
/// # Example
///
/// This example shows the `std::fmt::Display` trait implementation for
/// [`civil::DateTime`](crate::civil::DateTime) (but using a wrapper type).
///
/// ```
/// use jiff::{civil::DateTime, fmt::{temporal::DateTimePrinter, StdFmtWrite}};
///
/// struct MyDateTime(DateTime);
///
/// impl std::fmt::Display for MyDateTime {
/// fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
///
/// static P: DateTimePrinter = DateTimePrinter::new();
/// P.print_datetime(&self.0, StdFmtWrite(f))
/// .map_err(|_| std::fmt::Error)
/// }
/// }
///
/// let dt = MyDateTime(DateTime::constant(2024, 6, 15, 17, 30, 0, 0));
/// assert_eq!(dt.to_string(), "2024-06-15T17:30:00");
/// ```
#[derive(Clone, Debug)]
pub struct StdFmtWrite<W>(pub W);
impl<W: core::fmt::Write> Write for StdFmtWrite<W> {
#[inline]
fn write_str(&mut self, string: &str) -> Result<(), Error> {
self.0
.write_str(string)
.map_err(|_| Error::from(E::StdFmtWriteAdapter))
}
}
impl<W: Write> core::fmt::Write for StdFmtWrite<W> {
#[inline]
fn write_str(&mut self, string: &str) -> Result<(), core::fmt::Error> {
self.0.write_str(string).map_err(|_| core::fmt::Error)
}
}
/// An adapter for writing Jiff's formatted output to a
/// [`defmt`] formatter.
///
/// This is used by Jiff's [`defmt::Format`] trait implementations to share the
/// same printer code used for [`core::fmt`] output.
///
/// # Example
///
/// This example shows the [`defmt::Format`] trait implementation for
/// [`civil::DateTime`](crate::civil::DateTime) (but using a wrapper type).
///
/// ```
/// use jiff::{
/// civil::DateTime,
/// fmt::{temporal::DateTimePrinter, DefmtWrite},
/// };
///
/// struct MyDateTime(DateTime);
///
/// impl defmt::Format for MyDateTime {
/// fn format(&self, f: defmt::Formatter) {
/// static P: DateTimePrinter = DateTimePrinter::new();
/// defmt::unwrap!(P.print_datetime(&self.0, DefmtWrite(f)));
/// }
/// }
/// ```
#[cfg(feature = "defmt")]
#[derive(defmt::Format)]
pub struct DefmtWrite<'a>(pub defmt::Formatter<'a>);
#[cfg(feature = "defmt")]
impl<'a> core::fmt::Debug for DefmtWrite<'a> {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.debug_struct("DefmtWrite").finish()
}
}
#[cfg(feature = "defmt")]
impl<'a> core::fmt::Write for DefmtWrite<'a> {
#[inline]
fn write_str(&mut self, string: &str) -> Result<(), core::fmt::Error> {
defmt::write!(self.0, "{=str}", string);
Ok(())
}
}
#[cfg(feature = "defmt")]
impl<'a> Write for DefmtWrite<'a> {
#[inline]
fn write_str(&mut self, string: &str) -> Result<(), Error> {
defmt::write!(self.0, "{=str}", string);
Ok(())
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+876
View File
@@ -0,0 +1,876 @@
/*!
Jiff is a datetime library for Rust that encourages you to jump into the pit
of success. The focus of this library is providing high level datetime
primitives that are difficult to misuse and have reasonable performance.
Jiff takes enormous inspiration from [Temporal], which is a [TC39] proposal to
improve datetime handling in JavaScript.
Here is a quick example that shows how to parse a typical RFC 3339 instant,
convert it to a zone aware datetime, add a span of time and losslessly print
it:
```
use jiff::{Timestamp, ToSpan};
let time: Timestamp = "2024-07-11T01:14:00Z".parse()?;
let zoned = time.in_tz("America/New_York")?.checked_add(1.month().hours(2))?;
assert_eq!(zoned.to_string(), "2024-08-10T23:14:00-04:00[America/New_York]");
// Or, if you want an RFC3339 formatted string:
assert_eq!(zoned.timestamp().to_string(), "2024-08-11T03:14:00Z");
# Ok::<(), Box<dyn std::error::Error>>(())
```
[TC39]: https://tc39.es/
[Temporal]: https://tc39.es/proposal-temporal/docs/index.html
# Overview
The primary type in this crate is [`Zoned`]. A `Zoned` value is a datetime that
corresponds to a precise instant in time in a particular geographic region.
Users of this crate may find it helpful to think of a `Zoned` as a triple of
the following components:
* A [`Timestamp`] is a 96-bit integer of nanoseconds since the [Unix epoch].
A timestamp is a precise instant in time.
* A [`civil::DateTime`] is an inexact calendar date and clock time. The terms
"civil", "local", "plain" and "naive" are all used in various places to
describe this same concept.
* A [`tz::TimeZone`] is a set of rules for determining the civil time, via an
offset from [UTC], in a particular geographic region.
All three of these components are used to provide convenient high level
operations on `Zoned` such as computing durations, adding durations and
rounding.
A [`Span`] is this crate's primary duration type. It mixes calendar and clock
units into a single type. Jiff also provides [`SignedDuration`], which is like
[`std::time::Duration`], but signed. Users should default to a `Span` for
representing durations when using Jiff.
[Unix epoch]: https://en.wikipedia.org/wiki/Unix_time
[UTC]: https://en.wikipedia.org/wiki/Coordinated_Universal_Time
The remainder of this documentation is organized as follows:
* [Features](#features) gives a very brief summary of the features Jiff does
and does not support.
* [Usage](#usage) shows how to add Jiff to your Rust project.
* [Examples](#examples) shows a small cookbook of programs for common tasks.
* [Crate features](#crate-features) documents the Cargo features that can be
enabled or disabled for this crate.
Also, the `_documentation` sub-module serves to provide longer form
documentation:
* [Comparison with other Rust datetime crates](crate::_documentation::comparison)
* [The API design rationale for Jiff](crate::_documentation::design)
* [Platform support](crate::_documentation::platform)
* [CHANGELOG](crate::_documentation::changelog)
# Features
Here is a non-exhaustive list of the things that Jiff supports:
* Automatic and seamless integration with your system's copy of the
[IANA Time Zone Database]. When a platform doesn't have a time zone database,
Jiff automatically embeds a copy of it.
* A separation of datetime types between absolute times ([`Timestamp`] and
[`Zoned`]) and civil times ([`civil::DateTime`]).
* Nanosecond precision.
* Time zone and daylight saving time aware arithmetic.
* The primary duration type, [`Span`], mixes calendar and clock
units to provide an all-in-one human friendly experience that is time zone
aware.
* An "absolute" duration type, [`SignedDuration`], is like
[`std::time::Duration`] but signed.
* Datetime rounding.
* Span rounding, including calendar units and including taking time zones into
account.
* Formatting and parsing datetimes via a Temporal-specified hybrid format
that takes the best parts of [RFC 3339], [RFC 9557] and [ISO 8601]. This
includes lossless round tripping of zone aware datetimes and durations.
* Formatting and parsing according to [RFC 2822].
* Formatting and parsing via routines similar to [`strftime`] and [`strptime`].
* Formatting and parsing durations via a bespoke
["friendly" format](crate::fmt::friendly), with Serde support, that is meant
to service the [`humantime`](https://docs.rs/humantime) use cases.
* Opt-in Serde integration.
* Full support for dealing with ambiguous civil datetimes.
* Protection against deserializing datetimes in the future with an offset
different than what is possible with your copy of the Time Zone Database.
(This is done via [`tz::OffsetConflict`].)
* APIs that panic by design are clearly documented as such and few in number.
Otherwise, all operations that can fail, including because of overflow,
return a `Result`.
Here is also a list of things that Jiff doesn't currently support, along with
a link to a relevant issue (if one exists).
* [Leap seconds]. (Jiff will automatically constrain times like `23:59:60` to
`23:59:59`.)
* Time scales other than Unix.
* [Calendars other than Gregorian].
* [Localization].
* [Changing the representation size, precision or limits on the minimum and
maximum datetime values.][cppchrono]
* [Jiff aims to have reasonable performance and may not be capable of doing the
fastest possible thing.][perf]
At present, it is recommended to use the [`icu`] crate via [`jiff-icu`] for
localization and non-Gregorian use cases.
[Leap seconds]: https://github.com/BurntSushi/jiff/issues/7
[Calendars other than Gregorian]: https://github.com/BurntSushi/jiff/issues/6
[Localization]: https://github.com/BurntSushi/jiff/issues/4
[cppchrono]: https://github.com/BurntSushi/jiff/issues/3
[perf]: https://github.com/BurntSushi/jiff/issues/17
[`icu`]: https://docs.rs/icu
[`jiff-icu`]: https://docs.rs/jiff-icu
Please file an issue if you can think of more (substantial) things to add to
the above list.
[IANA Time Zone Database]: https://en.wikipedia.org/wiki/Tz_database
[RFC 3339]: https://www.rfc-editor.org/rfc/rfc3339
[RFC 9557]: https://www.rfc-editor.org/rfc/rfc9557.html
[ISO 8601]: https://www.iso.org/iso-8601-date-and-time-format.html
# Usage
Jiff is [on crates.io](https://crates.io/crates/jiff) and can be
used by adding `jiff` to your dependencies in your project's `Cargo.toml`.
Or more simply, just run `cargo add jiff`.
Here is a complete example that creates a new Rust project, adds a dependency
on `jiff`, creates the source code for a simple datetime program and then runs
it.
First, create the project in a new directory:
```text
$ cargo new jiff-example
$ cd jiff-example
```
Second, add a dependency on `jiff`:
```text
$ cargo add jiff
```
Third, edit `src/main.rs`. Delete what's there and replace it with this:
```
use jiff::{Unit, Zoned};
fn main() -> Result<(), jiff::Error> {
let now = Zoned::now().round(Unit::Second)?;
println!("{now}");
Ok(())
}
```
Fourth, run it with `cargo run`:
```text
$ cargo run
Compiling jiff v0.2.0 (/home/andrew/rust/jiff)
Compiling jiff-play v0.2.0 (/home/andrew/tmp/scratch/rust/jiff-play)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.37s
Running `target/debug/jiff-play`
2024-07-10T19:54:20-04:00[America/New_York]
```
The first time you run the program will show more output like above. But
subsequent runs shouldn't have to re-compile the dependencies.
# Examples
* [Get the current time in your system's time zone](#get-the-current-time-in-your-systems-time-zone)
* [Print the current time rounded to the nearest second](#print-the-current-time-rounded-to-the-nearest-second)
* [Print today's date at a specific time](#print-todays-date-at-a-specific-time)
* [Print the current Unix timestamp](#print-the-current-unix-timestamp)
* [Print the datetime for a timestamp](#print-the-datetime-for-a-timestamp)
* [Create a zoned datetime from civil time](#create-a-zoned-datetime-from-civil-time)
* [Change an instant from one time zone to another](#change-an-instant-from-one-time-zone-to-another)
* [Find the duration between two zoned datetimes](#find-the-duration-between-two-zoned-datetimes)
* [Add a duration to a zoned datetime](#add-a-duration-to-a-zoned-datetime)
* [Dealing with ambiguity](#dealing-with-ambiguity)
* [Parsing a span](#parsing-a-span)
* [Parsing an RFC 2822 datetime string](#parsing-an-rfc-2822-datetime-string)
* [Using `strftime` and `strptime` for formatting and parsing](#using-strftime-and-strptime-for-formatting-and-parsing)
* [Serializing and deserializing integer timestamps with Serde](#serializing-and-deserializing-integer-timestamps-with-serde)
### Get the current time in your system's time zone
The [`Zoned::now`] returns your system's time and also attempts
to automatically find your system's default time zone via
[`tz::TimeZone::system`]:
```
use jiff::Zoned;
let now = Zoned::now();
println!("{now}");
// Output: 2024-07-10T17:09:28.168146054-04:00[America/New_York]
```
### Print the current time rounded to the nearest second
This uses the [`Zoned::round`] API to round a zoned datetime to the nearest
second. This is useful, for example, if you don't care about fractional
seconds:
```
use jiff::{Unit, Zoned};
let now = Zoned::now().round(Unit::Second)?;
println!("{now}");
// Output: 2024-07-10T17:09:28-04:00[America/New_York]
# Ok::<(), Box<dyn std::error::Error>>(())
```
### Print today's date at a specific time
Let's say you want to get the current date at 2pm. Here's one way of doing it
that makes use of [`Zoned::with`]:
```
use jiff::Zoned;
let zdt = Zoned::now().with()
.hour(14)
.minute(0)
.second(0)
.subsec_nanosecond(0)
.build()?;
println!("{zdt}");
// Output: 2024-07-12T14:00:00-04:00[America/New_York]
# Ok::<(), Box<dyn std::error::Error>>(())
```
Or, if the time is known to be valid, you can use the infallible
[`civil::time`](civil::time()) convenience constructor:
```
use jiff::{civil::time, Zoned};
let zdt = Zoned::now().with().time(time(14, 0, 0, 0)).build()?;
println!("{zdt}");
// Output: 2024-07-12T14:00:00-04:00[America/New_York]
# Ok::<(), Box<dyn std::error::Error>>(())
```
You can eliminate the possibility of a panic at runtime by using `time` in
a `const` block:
```
use jiff::{civil::time, Zoned};
let zdt = Zoned::now().with().time(const { time(14, 0, 0, 0) }).build()?;
println!("{zdt}");
// Output: 2024-07-12T14:00:00-04:00[America/New_York]
# Ok::<(), Box<dyn std::error::Error>>(())
```
### Print the current Unix timestamp
This prints a Unix timestamp as the number of seconds since the Unix epoch
via [`Timestamp::now`]:
```
use jiff::Timestamp;
let now = Timestamp::now();
println!("{}", now.as_second());
// Output: 1720646365
```
Or print the current timestamp to nanosecond precision (which is the maximum
supported by Jiff):
```
use jiff::Timestamp;
let now = Timestamp::now();
println!("{}", now.as_nanosecond());
// Output: 1720646414218901664
```
### Print the datetime for a timestamp
This example shows how to convert a Unix timestamp, in milliseconds, to
a zoned datetime in the system's current time zone. This utilizes the
[`Timestamp::from_millisecond`] constructor, [`tz::TimeZone::system`] to get
the default time zone and the [`Timestamp::to_zoned`] routine to convert a
timestamp to a zoned datetime.
```
use jiff::{tz::TimeZone, Timestamp};
let ts = Timestamp::from_millisecond(1_720_646_365_567)?;
let zdt = ts.to_zoned(TimeZone::system());
println!("{zdt}");
// Output: 2024-07-10T17:19:25.567-04:00[America/New_York]
// Or if you just want the RFC 3339 time without bothering with time zones:
assert_eq!(ts.to_string(), "2024-07-10T21:19:25.567Z");
# Ok::<(), Box<dyn std::error::Error>>(())
```
### Create a zoned datetime from civil time
This example demonstrates the convenience constructor, [`civil::date`],
for a [`civil::Date`]. And use the [`civil::Date::at`] method to create
a [`civil::DateTime`]. Once we have a civil datetime, we can use
[`civil::DateTime::in_tz`] to do a time zone lookup and convert it to a precise
instant in time:
```
use jiff::civil::date;
let zdt = date(2023, 12, 31).at(18, 30, 0, 0).in_tz("America/New_York")?;
assert_eq!(zdt.to_string(), "2023-12-31T18:30:00-05:00[America/New_York]");
# Ok::<(), Box<dyn std::error::Error>>(())
```
Note that [`civil::date`] should only be used for inputs that are known to be
correct since it panics for an invalid date. If your date isn't known to be
valid, then use the fallible [`civil::Date::new`] constructor.
### Change an instant from one time zone to another
This shows how to find the civil time, in New York, when World War 1 ended:
```
use jiff::civil::date;
let zdt1 = date(1918, 11, 11).at(11, 0, 0, 0).in_tz("Europe/Paris")?;
let zdt2 = zdt1.in_tz("America/New_York")?;
assert_eq!(
zdt2.to_string(),
"1918-11-11T06:00:00-05:00[America/New_York]",
);
# Ok::<(), Box<dyn std::error::Error>>(())
```
### Find the duration between two zoned datetimes
This shows how to compute a span between two zoned datetimes. This utilizes
a [`Zoned`]'s implementation for `Sub`, permitting one to subtract two zoned
datetimes via the `-` operator:
```
use jiff::civil::date;
let zdt1 = date(2020, 8, 26).at(6, 27, 0, 0).in_tz("America/New_York")?;
let zdt2 = date(2023, 12, 31).at(18, 30, 0, 0).in_tz("America/New_York")?;
let span = zdt2 - zdt1;
assert_eq!(format!("{span:#}"), "29341h 3m");
# Ok::<(), Box<dyn std::error::Error>>(())
```
The above returns no units bigger than hours because it makes the operation
reversible in all cases. But if you don't need reversibility (i.e., adding the
span returned to `zdt1` gives you `zdt2`), then you can ask for bigger units
via [`Zoned::until`] to make the span more comprehensible:
```
use jiff::{civil::date, Unit};
let zdt1 = date(2020, 8, 26).at(6, 27, 0, 0).in_tz("America/New_York")?;
let zdt2 = date(2023, 12, 31).at(18, 30, 0, 0).in_tz("America/New_York")?;
let span = zdt1.until((Unit::Year, &zdt2))?;
assert_eq!(format!("{span:#}"), "3y 4mo 5d 12h 3m");
# Ok::<(), Box<dyn std::error::Error>>(())
```
### Add a duration to a zoned datetime
This example shows how one can add a [`Span`] to a [`Zoned`] via
[`Zoned::checked_add`] to get a new `Zoned` value. We utilize the [`ToSpan`]
trait for convenience construction of `Span` values.
```
use jiff::{civil::date, ToSpan};
let zdt1 = date(2020, 8, 26).at(6, 27, 0, 0).in_tz("America/New_York")?;
let span = 3.years().months(4).days(5).hours(12).minutes(3);
let zdt2 = zdt1.checked_add(span)?;
assert_eq!(zdt2.to_string(), "2023-12-31T18:30:00-05:00[America/New_York]");
# Ok::<(), Box<dyn std::error::Error>>(())
```
As with [`civil::date`], the [`ToSpan`] trait should only be used with inputs
that are known to be valid. If you aren't sure whether the inputs are valid,
then use [`Span::new`] and its fallible mutators like [`Span::try_years`].
### Dealing with ambiguity
In some cases, civil datetimes either don't exist in a particular time zone or
are repeated. By default, Jiff automatically uses the
[`tz::Disambiguation::Compatible`] strategy for choosing an instant in all
cases:
```
use jiff::civil::date;
// 2:30 on 2024-03-10 in New York didn't exist. It's a "gap."
// The compatible strategy selects the datetime after the gap.
let zdt = date(2024, 3, 10).at(2, 30, 0, 0).in_tz("America/New_York")?;
assert_eq!(zdt.to_string(), "2024-03-10T03:30:00-04:00[America/New_York]");
// 1:30 on 2024-11-03 in New York appeared twice. It's a "fold."
// The compatible strategy selects the datetime before the fold.
let zdt = date(2024, 11, 3).at(1, 30, 0, 0).in_tz("America/New_York")?;
assert_eq!(zdt.to_string(), "2024-11-03T01:30:00-04:00[America/New_York]");
# Ok::<(), Box<dyn std::error::Error>>(())
```
For more control over disambiguation, see
[`tz::TimeZone::to_ambiguous_zoned`]. Or
[`fmt::temporal::DateTimeParser::disambiguation`]
if you're parsing zoned datetimes.
### Parsing a span
Jiff supports parsing ISO 8601 duration strings:
```
use jiff::Span;
let span: Span = "P5y1w10dT5h59m".parse()?;
let expected = Span::new().years(5).weeks(1).days(10).hours(5).minutes(59);
assert_eq!(span, expected.fieldwise());
# Ok::<(), Box<dyn std::error::Error>>(())
```
The same format is used for serializing and deserializing `Span` values when
the `serde` feature is enabled.
Jiff also supports a bespoke ["friendly" format](crate::fmt::friendly) as
well:
```
use jiff::Span;
let expected = Span::new().years(5).weeks(1).days(10).hours(5).minutes(59);
let span: Span = "5 years, 1 week, 10 days, 5 hours, 59 minutes".parse()?;
assert_eq!(span, expected.fieldwise());
let span: Span = "5yrs 1wk 10d 5hrs 59mins".parse()?;
assert_eq!(span, expected.fieldwise());
let span: Span = "5y 1w 10d 5h 59m".parse()?;
assert_eq!(span, expected.fieldwise());
# Ok::<(), Box<dyn std::error::Error>>(())
```
### Parsing an RFC 2822 datetime string
While you probably shouldn't pick [RFC 2822] as a format for new things, it is
sometimes necessary to use it when something else requires it (like HTTP
or email). Parsing and printing of RFC 2822 datetimes is done via the
[`fmt::rfc2822`] module:
```
use jiff::fmt::rfc2822;
let zdt1 = rfc2822::parse("Thu, 29 Feb 2024 05:34 -0500")?;
let zdt2 = zdt1.in_tz("Australia/Tasmania")?;
assert_eq!(rfc2822::to_string(&zdt2)?, "Thu, 29 Feb 2024 21:34:00 +1100");
let zdt3 = zdt1.in_tz("Asia/Kolkata")?;
assert_eq!(rfc2822::to_string(&zdt3)?, "Thu, 29 Feb 2024 16:04:00 +0530");
# Ok::<(), Box<dyn std::error::Error>>(())
```
[RFC 2822]: https://datatracker.ietf.org/doc/html/rfc2822
### Using `strftime` and `strptime` for formatting and parsing
Jiff has support for the C style [`strftime`] and [`strptime`] functions for
formatting and parsing datetime types. All of Jiff's datetime types having a
`strptime` constructor for parsing, and a `strftime` method for formatting.
For example, this shows how to use [`Zoned::strptime`] to parsed a string in
a "odd" custom format into a zoned datetime:
```
use jiff::Zoned;
let zdt = Zoned::strptime(
"%A, %B %d, %Y at %I:%M%p %Q",
"Monday, July 15, 2024 at 5:30pm US/Eastern",
)?;
assert_eq!(zdt.to_string(), "2024-07-15T17:30:00-04:00[US/Eastern]");
# Ok::<(), Box<dyn std::error::Error>>(())
```
And this shows how to use [`Zoned::strftime`] to format a zoned datetime.
Note the use of `%Z`, which will print a time zone abbreviation (when one is
available) instead of an offset (`%Z` can't be used for parsing):
```
use jiff::civil::date;
let zdt = date(2024, 7, 15).at(17, 30, 59, 0).in_tz("Australia/Tasmania")?;
// %-I instead of %I means no padding.
let string = zdt.strftime("%A, %B %d, %Y at %-I:%M%P %Z").to_string();
assert_eq!(string, "Monday, July 15, 2024 at 5:30pm AEST");
# Ok::<(), Box<dyn std::error::Error>>(())
```
However, time zone abbreviations aren't parsable because they are ambiguous.
For example, `CST` can stand for `Central Standard Time`, `Cuba Standard Time`
or `China Standard Time`. Instead, it is recommended to use `%Q` to format an
IANA time zone identifier (which can be parsed, as shown above):
```
use jiff::civil::date;
let zdt = date(2024, 7, 15).at(17, 30, 59, 0).in_tz("Australia/Tasmania")?;
// %-I instead of %I means no padding.
let string = zdt.strftime("%A, %B %d, %Y at %-I:%M%P %Q").to_string();
assert_eq!(string, "Monday, July 15, 2024 at 5:30pm Australia/Tasmania");
# Ok::<(), Box<dyn std::error::Error>>(())
```
See the [`fmt::strtime`] module documentation for supported conversion
specifiers and other APIs.
[`strftime`]: https://pubs.opengroup.org/onlinepubs/009695399/functions/strftime.html
[`strptime`]: https://pubs.opengroup.org/onlinepubs/009695399/functions/strptime.html
### Serializing and deserializing integer timestamps with Serde
Sometimes you need to interact with external services that use integer timestamps
instead of something more civilized like RFC 3339. Since [`Timestamp`]'s
Serde integration uses RFC 3339, you'll need to override the default. While
you could hand-write this, Jiff provides convenience routines that do this
for you. But you do need to wire it up via [Serde's `with` attribute]:
```
use jiff::Timestamp;
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct Record {
#[serde(with = "jiff::fmt::serde::timestamp::second::required")]
timestamp: Timestamp,
}
let json = r#"{"timestamp":1517644800}"#;
let got: Record = serde_json::from_str(&json)?;
assert_eq!(got.timestamp, Timestamp::from_second(1517644800)?);
assert_eq!(serde_json::to_string(&got)?, json);
# Ok::<(), Box<dyn std::error::Error>>(())
```
If you need to support optional timestamps via `Option<Timestamp>`, then use
`jiff::fmt::serde::timestamp::second::optional` instead.
For more, see the [`fmt::serde`] sub-module. (This requires enabling Jiff's
`serde` crate feature.)
[Serde's `with` attribute]: https://serde.rs/field-attrs.html#with
# Crate features
### Ecosystem features
* **std** (enabled by default) -
When enabled, Jiff will depend on Rust's standard library. This is needed
for things that require interacting with your system, such as reading
`/usr/share/zoneinfo` on Unix systems for time zone information, or for
finding your system's default time zone. But if you don't need that (or can
bundle the Time Zone Database), then Jiff has nearly full functionality
without `std` enabled, excepting things like `std::error::Error` trait
implementations and a global time zone database (which is required for
things like [`Timestamp::in_tz`] to work).
* **alloc** (enabled by default) -
When enabled, Jiff will depend on the `alloc` crate. In particular, this
enables functionality that requires or greatly benefits from dynamic memory
allocation. If you can enable this, it is strongly encouraged that you do so.
Without it, only fixed time zones are supported and error messages are
significantly degraded. Also, the sizes of some types get bigger. If you
have use cases for Jiff in a no-std and no-alloc context, I would love
feedback on the issue tracker about your use cases.
* **logging** -
When enabled, the `log` crate is used to emit messages where appropriate.
Generally speaking, this is reserved for system interaction points, such as
finding the system copy of the Time Zone Database or finding the system's
default time zone.
* **serde** -
When enabled, all of the datetime and span types in Jiff implement
serde's `Serialize` and `Deserialize` traits. The format used is specified by
Temporal, but it's a mix of the "best" parts of RFC 3339, RFC 9557 and
ISO 8601. See the [`fmt::temporal`] module for more details on the format
used.
* **js** -
On _only_ the `wasm32-unknown-unknown` and `wasm64-unknown-unknown` targets,
the `js` feature will add dependencies on `js-sys` and `wasm-bindgen`.
These dependencies are used to determine the current datetime and time
zone from the web browser. On these targets without the `js` feature
enabled, getting the current datetime will panic (because that's what
`std::time::SystemTime::now()` does), and it won't be possible to determine
the time zone. This feature is disabled by default because not all uses
of `wasm{32,64}-unknown-unknown` are in a web context, although _many_ are
(for example, when using `wasm-pack`). Only binary, tests and benchmarks
should enable this feature. See
[Platform support](crate::_documentation::platform) for more details.
* **defmt** -
When enabled, Jiff will implement the [`defmt::Format`] trait for many
of its types. This is useful for embedded environments where the usual
`core::fmt` machinery is not practical to use. Note that, at time of writing
(2026-06-12), not all public types in this crate implement [`defmt::Format`].
If there are types for which you need `defmt` support but don't have it in
Jiff, then please [open a new issue][issue-new].
### Time zone features
* **tz-system** (enabled by default) -
When enabled, Jiff will include code that attempts to determine the "system"
time zone. For example, on Unix systems, this is usually determined by
looking at the symlink information on `/etc/localtime`. But in general, it's
very platform specific and heuristic oriented.
* **tz-fat** (enabled by default) -
When enabled, Jiff will "fatten" time zone data with extra transitions to
make time zone lookups faster. This may result in increased heap memory
(when loading time zones from `/usr/share/zoneinfo`) or increased binary
size (when using the `jiff-static` proc macros). Note that this doesn't add
more transitions than are likely already in `/usr/share/zoneinfo`, depending
on how it was generated.
* **tzdb-bundle-always** -
When enabled, Jiff will forcefully depend on the `jiff-tzdb` crate, which
embeds an entire copy of the Time Zone Database. You should avoid this unless
you have a specific need for it, since it is better to rely on your system's
copy of time zone information. (Which may be updated multiple times per
year.)
* **tzdb-bundle-platform** (enabled by default) -
When enabled, Jiff will depend on `jiff-tzdb` only for platforms where it is
known that there is no canonical copy of the Time Zone Database. For example,
Windows.
* **tzdb-zoneinfo** (enabled by default) -
When enabled, Jiff will attempt to look for your system's copy of the Time
Zone Database.
* **tzdb-concatenated** (enabled by default) -
When enabled, Jiff will attempt to look for a system copy of the
[Concatenated Time Zone Database]. This is primarily meant for reading time
zone information on Android platforms. The `ANDROID_ROOT` and `ANDROID_DATA`
environment variables (with sensible default fallbacks) are used to construct
candidate paths to look for this database. For more on this, see the
[Android section of the platform support documentation](crate::_documentation::platform#android).
* **static** -
When enabled, new procedural macros will be added to the `tz` sub-module for
creating static `TimeZone` values at compile-time. This adds a dependency on
[`jiff-static`] and [`jiff-tzdb`]. `jiff-static` defines the macros, and Jiff
re-exports them. This also enables `static-tz`.
* **static-tz** -
When enabled, a `jiff::tz::include` procedural macro will become available.
This takes a TZif file path, like `/usr/share/zoneinfo/Israel`, as input and
returns a `TimeZone` value at compile time.
### Performance features
* **perf-inline** (enabled by default) -
When enabled, a number of `inline(always)` annotations are used inside of
Jiff to improve performance. This can especially impact formatting and
parsing of datetimes. If the extra performance isn't needed or if you want
to prioritize smaller binary sizes and shorter compilation times over
runtime performance, then it can be useful to disable this feature.
[`jiff-static`]: https://docs.rs/jiff-static
[`jiff-tzdb`]: https://docs.rs/jiff-tzdb
[Concatenated Time Zone Database]: https://android.googlesource.com/platform/libcore/+/jb-mr2-release/luni/src/main/java/libcore/util/ZoneInfoDB.java
[issue-new]: https://github.com/BurntSushi/jiff/issues/new
*/
#![no_std]
// Lots of rustdoc links break when disabling default features because docs
// aren't written conditionally.
#![cfg_attr(
all(
feature = "std",
feature = "serde",
feature = "static",
feature = "tzdb-zoneinfo"
),
deny(rustdoc::broken_intra_doc_links)
)]
#![cfg_attr(
not(all(
feature = "std",
feature = "serde",
feature = "static",
feature = "tzdb-zoneinfo"
)),
allow(rustdoc::broken_intra_doc_links)
)]
// These are just too annoying to squash otherwise.
#![cfg_attr(
not(all(
feature = "std",
feature = "tzdb-zoneinfo",
feature = "tzdb-concatenated",
feature = "tz-system",
)),
allow(dead_code, unused_imports)
)]
#![cfg_attr(miri, allow(dead_code, unused_imports))]
// This adds Cargo feature annotations to items in the rustdoc output. Which is
// sadly hugely beneficial for this crate due to the number of features.
#![cfg_attr(docsrs_jiff, feature(doc_cfg))]
// We generally want all types to impl Debug.
#![warn(missing_debug_implementations)]
// Document ALL THE THINGS!
#![deny(missing_docs)]
// See: https://github.com/rust-lang/rust/pull/121364
#![allow(unknown_lints, ambiguous_negative_literals)]
// See: https://github.com/rust-lang/rust/pull/121364
#![doc(test(attr(allow(unknown_lints, ambiguous_negative_literals))))]
// It should be possible to support other pointer widths, but this library
// hasn't been tested nor thought about much in contexts with pointers less
// than 32 bits.
//
// If you need support for 8-bit or 16-bit, please submit a bug report at
// https://github.com/BurntSushi/jiff
#[cfg(not(any(target_pointer_width = "32", target_pointer_width = "64")))]
compile_error!("jiff currently not supported on non-{32,64}");
#[cfg(any(test, feature = "std"))]
extern crate std;
#[cfg(any(test, feature = "alloc"))]
extern crate alloc;
/// This is NOT part of Jiff's public API.
///
/// This is exported for use by `jiff-static`'s procedural macro.
#[doc(hidden)]
pub use jcore as __jcore;
pub use crate::{
error::Error,
signed_duration::{SignedDuration, SignedDurationRound},
span::{
Span, SpanArithmetic, SpanCompare, SpanFieldwise, SpanRelativeTo,
SpanRound, SpanTotal, ToSpan, Unit,
},
timestamp::{
Timestamp, TimestampArithmetic, TimestampDifference,
TimestampDisplayWithOffset, TimestampRound, TimestampSeries,
},
util::round::RoundMode,
zoned::{
Zoned, ZonedArithmetic, ZonedDifference, ZonedRound, ZonedSeries,
ZonedWith,
},
};
#[macro_use]
mod logging;
pub mod civil;
mod duration;
mod error;
pub mod fmt;
#[cfg(feature = "std")]
mod now;
mod signed_duration;
mod span;
mod timestamp;
pub mod tz;
mod util;
mod zoned;
/// Longer form documentation for Jiff.
pub mod _documentation {
#[doc = include_str!("../COMPARE.md")]
pub mod comparison {}
#[doc = include_str!("../DESIGN.md")]
pub mod design {}
#[doc = include_str!("../PLATFORM.md")]
pub mod platform {}
#[doc = include_str!("../CHANGELOG.md")]
pub mod changelog {}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "std")]
#[test]
fn now_works() {
let _ = crate::logging::Logger::init();
let zdt = Zoned::now();
std::println!("{zdt}");
}
#[cfg(feature = "std")]
#[test]
fn ranges() {
use crate::util::b;
dbg!((b::SpanYears::MIN, b::SpanYears::MAX));
dbg!((b::SpanMonths::MIN, b::SpanMonths::MAX));
dbg!((b::SpanWeeks::MIN, b::SpanWeeks::MAX));
dbg!((b::SpanDays::MIN, b::SpanDays::MAX));
dbg!((b::SpanHours::MIN, b::SpanHours::MAX));
dbg!((b::SpanMinutes::MIN, b::SpanMinutes::MAX));
dbg!((b::SpanSeconds::MIN, b::SpanSeconds::MAX));
dbg!((b::SpanMilliseconds::MIN, b::SpanMilliseconds::MAX));
dbg!((b::SpanMicroseconds::MIN, b::SpanMicroseconds::MAX));
dbg!((b::SpanNanoseconds::MIN, b::SpanNanoseconds::MAX));
dbg!((b::UnixEpochSeconds::MIN, b::UnixEpochSeconds::MAX));
dbg!((b::UnixEpochDays::MIN, b::UnixEpochDays::MAX));
}
#[cfg(feature = "std")]
#[test]
fn maximally_long_span() {
use crate::{fmt::friendly, util::b};
let span = Span::new()
.years(b::SpanYears::MAX)
.months(b::SpanMonths::MAX)
.weeks(b::SpanWeeks::MAX)
.days(b::SpanDays::MAX)
.hours(b::SpanHours::MAX)
.minutes(b::SpanMinutes::MAX)
.seconds(b::SpanSeconds::MAX)
.milliseconds(b::SpanMilliseconds::MAX)
.microseconds(b::SpanMicroseconds::MAX)
.nanoseconds(b::SpanNanoseconds::MAX)
.negate();
std::println!("{span}");
std::println!("{span:#}");
let long_printer = friendly::SpanPrinter::new()
.designator(friendly::Designator::Verbose)
.spacing(friendly::Spacing::BetweenUnitsAndDesignators)
.comma_after_designator(true);
std::println!("{}", long_printer.span_to_string(&span));
}
#[cfg(feature = "std")]
#[test]
fn maximally_long_duration() {
let sdur = SignedDuration::MIN;
std::println!("{sdur}");
std::println!("{sdur:#}");
}
}
+147
View File
@@ -0,0 +1,147 @@
// Some feature combinations result in some of these macros never being used.
// Which is fine. Just squash the warnings.
#![allow(dead_code, unused_macros)]
macro_rules! log {
($($tt:tt)*) => {
#[cfg(feature = "logging")]
{
$($tt)*
}
}
}
macro_rules! error {
($($tt:tt)*) => { log!(log::error!($($tt)*)) }
}
macro_rules! warn {
($($tt:tt)*) => { log!(log::warn!($($tt)*)) }
}
macro_rules! info {
($($tt:tt)*) => { log!(log::info!($($tt)*)) }
}
macro_rules! debug {
($($tt:tt)*) => { log!(log::debug!($($tt)*)) }
}
macro_rules! trace {
($($tt:tt)*) => { log!(log::trace!($($tt)*)) }
}
/// A copy of std's `dbg!` macro that doesn't do pretty printing.
///
/// This is nice because we usually want more compact output in this crate.
/// Also, because we don't import std's prelude, we have to use `std::dbg!`.
/// This macro definition makes it available as `dbg!`.
#[cfg(feature = "std")]
macro_rules! dbg {
() => {
std::eprintln!(
"[{}:{}:{}]",
$crate::file!(),
$crate::line!(),
$crate::column!(),
)
};
($val:expr $(,)?) => {
match $val {
tmp => {
std::eprintln!(
"[{}:{}:{}] {} = {:?}",
std::file!(),
std::line!(),
std::column!(),
std::stringify!($val),
&tmp,
);
tmp
}
}
};
($($val:expr),+ $(,)?) => {
($(dbg!($val)),+,)
};
}
/// The simplest possible logger that logs to stderr.
///
/// This logger does no filtering. Instead, it relies on the `log` crates
/// filtering via its global max_level setting.
///
/// This provides a super simple logger that works with the `log` crate.
/// We don't need anything fancy; just basic log levels and the ability to
/// print to stderr. We therefore avoid bringing in extra dependencies just
/// for this functionality.
#[cfg(all(test))]
#[derive(Debug)]
pub(crate) struct Logger(());
#[cfg(all(test, feature = "std", feature = "logging"))]
const LOGGER: &'static Logger = &Logger(());
#[cfg(all(test))]
impl Logger {
/// Create a new logger that logs to stderr and initialize it as the
/// global logger. If there was a problem setting the logger, then an
/// error is returned.
pub(crate) fn init() -> Result<(), log::SetLoggerError> {
#[cfg(all(feature = "std", feature = "logging"))]
{
log::set_logger(LOGGER)?;
log::set_max_level(log::LevelFilter::Trace);
Ok(())
}
#[cfg(not(all(feature = "std", feature = "logging")))]
{
Ok(())
}
}
}
#[cfg(all(test, feature = "std", feature = "logging"))]
impl log::Log for Logger {
fn enabled(&self, _: &log::Metadata<'_>) -> bool {
// We set the log level via log::set_max_level, so we don't need to
// implement filtering here.
true
}
fn log(&self, record: &log::Record<'_>) {
match (record.file(), record.line()) {
(Some(file), Some(line)) => {
std::eprintln!(
"{}|{}|{}:{}: {}",
record.level(),
record.target(),
file,
line,
record.args()
);
}
(Some(file), None) => {
std::eprintln!(
"{}|{}|{}: {}",
record.level(),
record.target(),
file,
record.args()
);
}
_ => {
std::eprintln!(
"{}|{}: {}",
record.level(),
record.target(),
record.args()
);
}
}
}
fn flush(&self) {
// We use eprintln! which is flushed on every call.
}
}
+107
View File
@@ -0,0 +1,107 @@
/*!
Provides a centralized helper for getting the current system time.
We generally rely on the standard library for this, but the standard library
(by design) does not provide this for the `wasm{32,64}-unknown-unknown`
targets. Instead, Jiff provides an opt-in `js` feature that only applies on the
aforementioned target. Specifically, when enabled, it assumes a web context and
runs JavaScript code to get the current time.
This also exposes a "fallible" API for querying monotonic time. Since we only
use monotonic time for managing expiration for caches, in the case where we
can't get monotonic time (easily), we just consider the cache to always be
expired. ¯\_(ツ)_/¯
*/
pub(crate) use self::sys::*;
#[cfg(not(all(
feature = "js",
any(target_arch = "wasm32", target_arch = "wasm64"),
target_os = "unknown"
)))]
mod sys {
pub(crate) fn system_time() -> std::time::SystemTime {
// `SystemTime::now()` should continue to panic on this exact target in
// the future as well; Instead of letting `std` panic, we panic first
// with a more informative error message.
if cfg!(all(
not(feature = "js"),
any(target_arch = "wasm32", target_arch = "wasm64"),
target_os = "unknown"
)) {
panic!(
"getting the current time in wasm32-unknown-unknown \
is not possible with just the standard library, \
enable Jiff's `js` feature if you are \
targeting a browser environment",
);
} else {
std::time::SystemTime::now()
}
}
#[cfg(any(
feature = "tz-system",
feature = "tzdb-zoneinfo",
feature = "tzdb-concatenated"
))]
pub(crate) fn monotonic_time() -> Option<std::time::Instant> {
// Same reasoning as above, but we return `None` instead of panicking,
// because Jiff can deal with environments that don't provide
// monotonic time.
if cfg!(all(
not(feature = "js"),
any(target_arch = "wasm32", target_arch = "wasm64"),
target_os = "unknown"
)) {
None
} else {
Some(std::time::Instant::now())
}
}
}
#[cfg(all(
feature = "js",
any(target_arch = "wasm32", target_arch = "wasm64"),
target_os = "unknown"
))]
mod sys {
pub(crate) fn system_time() -> std::time::SystemTime {
use std::time::{Duration, SystemTime};
let millis = js_sys::Date::new_0().get_time();
let is_positive = millis.is_sign_positive();
let millis = millis.abs() as u64;
let duration = Duration::from_millis(millis);
let result = if is_positive {
SystemTime::UNIX_EPOCH.checked_add(duration)
} 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.
let Some(timestamp) = result else {
panic!(
"failed to get current time from Javascript date: \
arithmetic on Unix epoch overflowed"
)
};
timestamp
}
#[cfg(any(
feature = "tz-system",
feature = "tzdb-zoneinfo",
feature = "tzdb-concatenated"
))]
pub(crate) fn monotonic_time() -> Option<std::time::Instant> {
// :-(
None
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+30
View File
@@ -0,0 +1,30 @@
use crate::tz::{TimeZone, TimeZoneNameIter};
#[derive(Clone)]
pub(crate) struct Database;
impl Database {
pub(crate) const fn new() -> Database {
Database
}
pub(crate) fn reset(&self) {}
pub(crate) fn get(&self, _query: &str) -> Option<TimeZone> {
None
}
pub(crate) fn available<'d>(&'d self) -> TimeZoneNameIter<'d> {
TimeZoneNameIter::empty()
}
pub(crate) fn is_definitively_empty(&self) -> bool {
true
}
}
impl core::fmt::Debug for Database {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.write_str("Bundled(unavailable)")
}
}
+152
View File
@@ -0,0 +1,152 @@
use crate::tz::{db::special_time_zone, TimeZone, TimeZoneNameIter};
#[derive(Clone)]
pub(crate) struct Database;
impl Database {
pub(crate) const fn new() -> Database {
Database
}
pub(crate) fn reset(&self) {
#[cfg(feature = "std")]
self::global::clear();
}
pub(crate) fn get(&self, name: &str) -> Option<TimeZone> {
#[cfg(feature = "std")]
if let Some(tz) = self::global::get(name) {
return Some(tz);
}
if let Some(tz) = special_time_zone(name) {
return Some(tz);
}
let (canonical_name, tzif) = lookup(name)?;
let tz = match TimeZone::tzif(canonical_name, tzif) {
Ok(tz) => tz,
Err(_err) => {
warn!(
"failed to parse TZif data from bundled \
tzdb for time zone `{canonical_name}` \
(this is likely a bug, please report it): {_err}"
);
return None;
}
};
#[cfg(feature = "std")]
self::global::add(canonical_name, &tz);
Some(tz)
}
pub(crate) fn available<'d>(&'d self) -> TimeZoneNameIter<'d> {
TimeZoneNameIter::from_iter(available())
}
pub(crate) fn is_definitively_empty(&self) -> bool {
false
}
}
impl core::fmt::Debug for Database {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.write_str("Bundled(available)")
}
}
fn available() -> impl Iterator<Item = &'static str> {
#[cfg(feature = "tzdb-bundle-always")]
{
jiff_tzdb::available()
}
#[cfg(not(feature = "tzdb-bundle-always"))]
{
jiff_tzdb_platform::jiff_tzdb::available()
}
}
fn lookup(name: &str) -> Option<(&'static str, &'static [u8])> {
#[cfg(feature = "tzdb-bundle-always")]
{
jiff_tzdb::get(name)
}
#[cfg(not(feature = "tzdb-bundle-always"))]
{
jiff_tzdb_platform::jiff_tzdb::get(name)
}
}
/// A simple global cache of parsed time zones.
///
/// When tzdb is bundled, all of the binary data is in the binary. But in order
/// to use it, it needs to be parsed. And parsing takes non-trivial work.
///
/// When std is enabled, we can keep a global cache of parsed time zones.
/// Currently, it is very simple and can never contract. This probably isn't
/// a big deal in practice since even if all time zones are loaded into memory,
/// its total memory usage is likely insignificant in most environments where
/// `std` is supported.
#[cfg(feature = "std")]
mod global {
use std::{string::String, string::ToString, sync::RwLock, vec::Vec};
use crate::{tz::TimeZone, util::utf8};
static CACHED_ZONES: RwLock<CachedZones> =
RwLock::new(CachedZones { zones: Vec::new() });
/// Returns a cached time zone for the given name if it exists.
///
/// If the time zone isn't in the cache, then this returns `None`.
pub(super) fn get(name: &str) -> Option<TimeZone> {
CACHED_ZONES.read().unwrap().get(name).cloned()
}
/// Associates the given time zone name with the given time zone in this
/// cache.
///
/// If the given time zone is already cached, then this is a no-op.
///
/// The only way a time zone can be remove from the cache is if it's
/// overwritten or if the cache is cleared entirely.
pub(super) fn add(name: &str, tz: &TimeZone) {
let mut cache = CACHED_ZONES.write().unwrap();
if let Err(i) = cache.get_zone_index(name) {
cache.zones.insert(
i,
CachedZone { name: name.to_string(), tz: tz.clone() },
);
}
}
/// Clear the entire global cache.
pub(super) fn clear() {
CACHED_ZONES.write().unwrap().clear();
}
#[derive(Debug)]
struct CachedZones {
zones: Vec<CachedZone>,
}
impl CachedZones {
fn get(&self, query: &str) -> Option<&TimeZone> {
self.get_zone_index(query).ok().map(|i| &self.zones[i].tz)
}
fn get_zone_index(&self, query: &str) -> Result<usize, usize> {
self.zones.binary_search_by(|entry| {
utf8::cmp_ignore_ascii_case(&entry.name, query)
})
}
fn clear(&mut self) {
self.zones.clear();
}
}
#[derive(Debug)]
struct CachedZone {
name: String,
tz: TimeZone,
}
}
+20
View File
@@ -0,0 +1,20 @@
pub(crate) use self::inner::*;
#[cfg(not(any(
feature = "tzdb-bundle-always",
all(
feature = "tzdb-bundle-platform",
any(windows, target_family = "wasm"),
),
)))]
#[path = "disabled.rs"]
mod inner;
#[cfg(any(
feature = "tzdb-bundle-always",
all(
feature = "tzdb-bundle-platform",
any(windows, target_family = "wasm"),
),
))]
#[path = "enabled.rs"]
mod inner;
@@ -0,0 +1,44 @@
use crate::tz::{TimeZone, TimeZoneNameIter};
#[derive(Clone)]
pub(crate) struct Database;
impl Database {
pub(crate) fn from_env() -> Database {
Database
}
#[cfg(feature = "std")]
pub(crate) fn from_path(
_path: &std::path::Path,
) -> Result<Database, crate::Error> {
Err(crate::error::Error::from(
crate::error::CrateFeatureError::TzdbConcatenated,
)
.context(crate::error::tz::db::Error::DisabledConcatenated))
}
pub(crate) fn none() -> Database {
Database
}
pub(crate) fn reset(&self) {}
pub(crate) fn get(&self, _query: &str) -> Option<TimeZone> {
None
}
pub(crate) fn available<'d>(&'d self) -> TimeZoneNameIter<'d> {
TimeZoneNameIter::empty()
}
pub(crate) fn is_definitively_empty(&self) -> bool {
true
}
}
impl core::fmt::Debug for Database {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.write_str("Concatenated(unavailable)")
}
}
@@ -0,0 +1,573 @@
use alloc::{
string::{String, ToString},
vec,
vec::Vec,
};
use std::{
ffi::OsString,
fs::File,
path::{Path, PathBuf},
sync::{Arc, RwLock},
time::Duration,
};
use jcore::util::ArrayStr;
use crate::{
error::{tz::db::Error as E, Error},
timestamp::Timestamp,
tz::{
concatenated::ConcatenatedTzif, db::special_time_zone, TimeZone,
TimeZoneNameIter,
},
util::{self, cache::Expiration, utf8},
};
const DEFAULT_TTL: Duration = Duration::new(5 * 60, 0);
/// The places to look for a concatenated `tzdata` file.
static TZDATA_LOCATIONS: &[TzdataLocation] = &[
TzdataLocation::Env {
name: "ANDROID_ROOT",
default: "/system",
suffix: "usr/share/zoneinfo/tzdata",
},
TzdataLocation::Env {
name: "ANDROID_DATA",
default: "/data/misc",
suffix: "zoneinfo/current/tzdata",
},
];
pub(crate) struct Database {
path: Option<PathBuf>,
names: Option<Names>,
zones: RwLock<CachedZones>,
}
impl Database {
pub(crate) fn from_env() -> Database {
let mut attempted = vec![];
for loc in TZDATA_LOCATIONS {
let path = loc.to_path_buf();
trace!(
"opening concatenated tzdata database at {}",
path.display()
);
match Database::from_path(&path) {
Ok(db) => return db,
Err(_err) => {
trace!("failed opening {}: {_err}", path.display());
}
}
attempted.push(path.to_string_lossy().into_owned());
}
debug!(
"could not find concatenated tzdata database at any of the \
following paths: {}",
attempted.join(", "),
);
Database::none()
}
pub(crate) fn from_path(path: &Path) -> Result<Database, Error> {
let names = Some(Names::new(path)?);
let zones = RwLock::new(CachedZones::new());
Ok(Database { path: Some(path.to_path_buf()), names, zones })
}
/// Creates a "dummy" zoneinfo database in which all lookups fail.
pub(crate) fn none() -> Database {
let path = None;
let names = None;
let zones = RwLock::new(CachedZones::new());
Database { path, names, zones }
}
pub(crate) fn reset(&self) {
let mut zones = self.zones.write().unwrap();
if let Some(ref names) = self.names {
names.reset();
}
zones.reset();
}
pub(crate) fn get(&self, query: &str) -> Option<TimeZone> {
if let Some(tz) = special_time_zone(query) {
return Some(tz);
}
let path = self.path.as_ref()?;
// The fast path is when the query matches a pre-existing unexpired
// time zone.
{
let zones = self.zones.read().unwrap();
if let Some(czone) = zones.get(query) {
if !czone.is_expired() {
trace!(
"for time zone query `{query}`, \
found cached zone `{}` \
(expiration={}, last_modified={:?})",
czone.tz.diagnostic_name(),
czone.expiration,
czone.last_modified,
);
return Some(czone.tz.clone());
}
}
}
// At this point, one of three possible cases is true:
//
// 1. The given query does not match any time zone in this database.
// 2. A time zone exists, but isn't cached.
// 3. A zime exists and is cached, but needs to be revalidated.
//
// While (3) is probably the common case since our TTLs are pretty
// short, both (2) and (3) require write access. Thus we rule out (1)
// before acquiring a write lock on the entire database. Plus, we'll
// need the zone info for case (2) and possibly for (3) if cache
// revalidation fails.
//
// I feel kind of bad about all this because it seems to me like there
// is too much work being done while holding on to the write lock.
// In particular, it seems like bad juju to do any I/O of any kind
// while holding any lock at all. I think I could design something
// that avoids doing I/O while holding a lock, but it seems a lot more
// complicated. (And what happens if the I/O becomes outdated by the
// time you acquire the lock?)
let mut zones = self.zones.write().unwrap();
let ttl = zones.ttl;
match zones.get_zone_index(query) {
Ok(i) => {
let czone = &mut zones.zones[i];
if czone.revalidate(path, ttl) {
// Metadata on the file didn't change, so we assume the
// file hasn't either.
return Some(czone.tz.clone());
}
// Revalidation failed. Re-read the TZif data.
let (scratch1, scratch2) = zones.scratch();
let czone = match CachedTimeZone::new(
path, query, ttl, scratch1, scratch2,
) {
Ok(Some(czone)) => czone,
Ok(None) => return None,
Err(_err) => {
warn!(
"failed to re-cache time zone {query} \
from {path}: {_err}",
path = path.display(),
);
return None;
}
};
let tz = czone.tz.clone();
zones.zones[i] = czone;
Some(tz)
}
Err(i) => {
let (scratch1, scratch2) = zones.scratch();
let czone = match CachedTimeZone::new(
path, query, ttl, scratch1, scratch2,
) {
Ok(Some(czone)) => czone,
Ok(None) => return None,
Err(_err) => {
warn!(
"failed to cache time zone {query} \
from {path}: {_err}",
path = path.display(),
);
return None;
}
};
let tz = czone.tz.clone();
zones.zones.insert(i, czone);
Some(tz)
}
}
}
pub(crate) fn available<'d>(&'d self) -> TimeZoneNameIter<'d> {
let Some(path) = self.path.as_ref() else {
return TimeZoneNameIter::empty();
};
let Some(names) = self.names.as_ref() else {
return TimeZoneNameIter::empty();
};
TimeZoneNameIter::from_iter(names.available(path).into_iter())
}
pub(crate) fn is_definitively_empty(&self) -> bool {
self.names.is_none()
}
}
impl core::fmt::Debug for Database {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.write_str("Concatenated(")?;
if let Some(ref path) = self.path {
path.display().fmt(f)?;
} else {
f.write_str("unavailable")?;
}
f.write_str(")")
}
}
#[derive(Debug)]
struct CachedZones {
zones: Vec<CachedTimeZone>,
ttl: Duration,
scratch1: Vec<u8>,
scratch2: Vec<u8>,
}
impl CachedZones {
const DEFAULT_TTL: Duration = DEFAULT_TTL;
fn new() -> CachedZones {
CachedZones {
zones: vec![],
ttl: CachedZones::DEFAULT_TTL,
scratch1: vec![],
scratch2: vec![],
}
}
fn get(&self, query: &str) -> Option<&CachedTimeZone> {
self.get_zone_index(query).ok().map(|i| &self.zones[i])
}
fn get_zone_index(&self, query: &str) -> Result<usize, usize> {
self.zones.binary_search_by(|zone| {
utf8::cmp_ignore_ascii_case(zone.name(), query)
})
}
fn reset(&mut self) {
self.zones.clear();
}
fn scratch(&mut self) -> (&mut Vec<u8>, &mut Vec<u8>) {
(&mut self.scratch1, &mut self.scratch2)
}
}
#[derive(Clone, Debug)]
struct CachedTimeZone {
tz: TimeZone,
expiration: Expiration,
last_modified: Option<Timestamp>,
}
impl CachedTimeZone {
/// Create a new cached time zone.
///
/// `path` should be a concatenated `tzdata` file. `query` is the IANA time
/// zone identifier we're looing for. The `ttl` says how long
/// the cached time zone should minimally remain fresh for.
///
/// The `scratch1` and `scratch2` given are used to help amortize
/// allocation when deserializing TZif data from the concatenated `tzdata`
/// file.
///
/// If no such time zone exists and no other error occurred, then
/// `Ok(None)` is returned.
fn new(
path: &Path,
query: &str,
ttl: Duration,
scratch1: &mut Vec<u8>,
scratch2: &mut Vec<u8>,
) -> Result<Option<CachedTimeZone>, Error> {
let file = File::open(path).map_err(|e| Error::io(e).path(path))?;
let db = ConcatenatedTzif::open(&file)?;
let Some(tz) = db.get(query, scratch1, scratch2)? else {
return Ok(None);
};
let last_modified = util::fs::last_modified_from_file(path, &file);
let expiration = Expiration::after(ttl);
Ok(Some(CachedTimeZone { tz, expiration, last_modified }))
}
/// Returns true if this time zone has gone stale and should, at minimum,
/// be revalidated.
fn is_expired(&self) -> bool {
self.expiration.is_expired()
}
/// Returns the IANA time zone identifier of this cached time zone.
fn name(&self) -> &str {
// OK because `ConcatenatedTzif` guarantees all `TimeZone` values it
// returns have an IANA name.
self.tz.iana_name().unwrap()
}
/// Attempts to revalidate this cached time zone.
///
/// Upon successful revalidation (that is, the cached time zone is still
/// fresh and okay to use), this returns true. Otherwise, the cached time
/// zone should be considered stale and must be re-created.
///
/// Note that technically another layer of revalidation could be done.
/// For example, we could keep a checksum of the TZif data, and only
/// consider rebuilding the time zone when the checksum changes. But I
/// think the last modified metadata will in practice be good enough, and
/// parsing TZif data should be quite fast.
///
/// `path` should be a concatenated `tzdata` file.
fn revalidate(&mut self, path: &Path, ttl: Duration) -> bool {
// If we started with no last modified timestamp, then I guess we
// should always fail revalidation? I suppose a case could be made to
// do the opposite: always pass revalidation.
let Some(old_last_modified) = self.last_modified else {
trace!(
"revalidation for {name} in {path} failed because \
old last modified time is unavailable",
name = self.name(),
path = path.display(),
);
return false;
};
let Some(new_last_modified) = util::fs::last_modified_from_path(path)
else {
trace!(
"revalidation for {name} in {path} failed because \
new last modified time is unavailable",
name = self.name(),
path = path.display(),
);
return false;
};
// We consider any change to invalidate cache.
if old_last_modified != new_last_modified {
trace!(
"revalidation for {name} in {path} failed because \
last modified times do not match: old = {old} != {new} = new",
name = self.name(),
path = path.display(),
old = old_last_modified,
new = new_last_modified,
);
return false;
}
trace!(
"revalidation for {name} in {path} succeeded because \
last modified times match: old = {old} == {new} = new",
name = self.name(),
path = path.display(),
old = old_last_modified,
new = new_last_modified,
);
self.expiration = Expiration::after(ttl);
true
}
}
/// A collection of time zone names extracted from a concatenated tzdata file.
///
/// This type is responsible not just for providing the names, but also for
/// updating them periodically.
///
/// Every name _should_ correspond to an entry in the data block of the
/// corresponding `tzdata` file, but we generally don't take advantage of this.
/// The reason is that the file could theoretically change. Between when we
/// extract the names and when we do a TZif lookup later. This is all perfectly
/// manageable, but it should only be done if there's a benchmark demanding
/// more effort be spent here. As it stands, we do have a rudimentary caching
/// mechanism, so not all time zone lookups go through this slower path. (This
/// is also why `Names` has no lookup routine. There's just a routine to return
/// all names.)
#[derive(Debug)]
struct Names {
inner: RwLock<NamesInner>,
}
#[derive(Debug)]
struct NamesInner {
/// All available names from the `tzdata` file.
names: Vec<Arc<str>>,
/// The version string read from the `tzdata` file.
version: ArrayStr<5>,
/// Scratch space used to help amortize allocation when extracting names
/// from a `tzdata` file.
scratch: Vec<u8>,
/// The expiration time of these cached names.
///
/// Note that this is a necessary but not sufficient criterion for
/// invalidating the cached value.
ttl: Duration,
/// The time at which the data in `names` becomes stale.
expiration: Expiration,
}
impl Names {
/// See comments in `tz/db/zoneinfo/enabled.rs` about this. We just copied
/// it from there.
const DEFAULT_TTL: Duration = DEFAULT_TTL;
/// Create a new collection of names from the concatenated `tzdata` file
/// path given.
///
/// If no names of time zones could be found in the given directory, then
/// an error is returned.
fn new(path: &Path) -> Result<Names, Error> {
let path = path.to_path_buf();
let mut scratch = vec![];
let (names, version) = read_names_and_version(&path, &mut scratch)?;
trace!(
"found concatenated tzdata at {path} \
with version {version} and {len} \
IANA time zone identifiers",
path = path.display(),
len = names.len(),
);
let ttl = Names::DEFAULT_TTL;
let expiration = Expiration::after(ttl);
let inner = NamesInner { names, version, scratch, ttl, expiration };
Ok(Names { inner: RwLock::new(inner) })
}
/// Returns all available time zone names after attempting a refresh of
/// the underlying data if it's stale.
fn available(&self, path: &Path) -> Vec<String> {
let mut inner = self.inner.write().unwrap();
inner.attempt_refresh(path);
inner.available()
}
fn reset(&self) {
self.inner.write().unwrap().reset();
}
}
impl NamesInner {
/// Returns all available time zone names.
fn available(&self) -> Vec<String> {
self.names.iter().map(|name| name.to_string()).collect()
}
/// Attempts a refresh, but only follows through if the TTL has been
/// exceeded.
///
/// The caller must ensure that the other cache invalidation criteria
/// have been upheld. For example, this should only be called for a missed
/// zone name lookup.
fn attempt_refresh(&mut self, path: &Path) {
if self.expiration.is_expired() {
self.refresh(path);
}
}
/// Forcefully refreshes the cached names with possibly new data from disk.
/// If an error occurs when fetching the names, then no names are updated
/// (but the `expires_at` is updated). This will also emit a warning log on
/// failure.
fn refresh(&mut self, path: &Path) {
// PERF: Should we try to move this tzdb handling to run outside of a
// lock? It probably happens pretty rarely, so it might not matter.
let result = read_names_and_version(path, &mut self.scratch);
self.expiration = Expiration::after(self.ttl);
match result {
Ok((names, version)) => {
trace!(
"refreshed concatenated tzdata at {path} \
with version {version} and {len} \
IANA time zone identifiers",
path = path.display(),
len = names.len(),
);
self.names = names;
self.version = version;
}
Err(_err) => {
warn!(
"failed to refresh concatenated time zone name cache \
for {path}: {_err}",
path = path.display(),
)
}
}
}
/// Resets the state such that the next lookup is guaranteed to force a
/// cache refresh, and that it is impossible for any data to be stale.
fn reset(&mut self) {
// This will force the next lookup to fail.
self.names.clear();
// And this will force the next failed lookup to result in a refresh.
self.expiration = Expiration::expired();
}
}
/// A type representing how to find a `tzdata` file.
///
/// This currently only supports an Android-centric lookup via env vars, but if
/// we wanted to check a fixed path like we do for `ZoneInfo`, then adding a
/// `Fixed` variant here would be appropriate.
#[derive(Debug)]
enum TzdataLocation {
Env { name: &'static str, default: &'static str, suffix: &'static str },
}
impl TzdataLocation {
/// Converts this location to an actual path, which might involve an
/// environment variable lookup.
fn to_path_buf(&self) -> PathBuf {
match *self {
TzdataLocation::Env { name, default, suffix } => {
let var = std::env::var_os(name)
.unwrap_or_else(|| OsString::from(default));
let prefix = PathBuf::from(var);
prefix.join(suffix)
}
}
}
}
/// Reads only the IANA time zone identifiers from the given path (and the
/// version of the database).
///
/// The `scratch` given is used to help amortize allocation when deserializing
/// names from the concatenated `tzdata` file.
///
/// This returns an error if reading was successful but no names were found.
fn read_names_and_version(
path: &Path,
scratch: &mut Vec<u8>,
) -> Result<(Vec<Arc<str>>, ArrayStr<5>), Error> {
let file = File::open(path).map_err(|e| Error::io(e).path(path))?;
let db = ConcatenatedTzif::open(file)?;
let names: Vec<Arc<str>> =
db.available(scratch)?.into_iter().map(Arc::from).collect();
if names.is_empty() {
return Err(Error::from(E::ConcatenatedMissingIanaIdentifiers));
}
Ok((names, db.version()))
}
#[cfg(test)]
mod tests {
use super::*;
/// DEBUG COMMAND
///
/// Takes environment variable `JIFF_DEBUG_ZONEINFO_DIR` as input and
/// prints a list of all time zone names in the directory (one per line).
///
/// Callers may also set `RUST_LOG` to get extra debugging output.
#[test]
fn debug_tzdata_list() -> anyhow::Result<()> {
let _ = crate::logging::Logger::init();
const ENV: &str = "JIFF_DEBUG_CONCATENATED_TZDATA";
let Some(val) = std::env::var_os(ENV) else { return Ok(()) };
let path = PathBuf::from(val);
let db = Database::from_path(&path)?;
for name in db.available() {
std::eprintln!("{name}");
}
Ok(())
}
}
@@ -0,0 +1,8 @@
pub(crate) use self::inner::*;
#[cfg(not(feature = "tzdb-concatenated"))]
#[path = "disabled.rs"]
mod inner;
#[cfg(feature = "tzdb-concatenated")]
#[path = "enabled.rs"]
mod inner;
+907
View File
@@ -0,0 +1,907 @@
use crate::{
error::{tz::db::Error as E, Error},
tz::TimeZone,
util::{sync::Arc, utf8},
};
mod bundled;
mod concatenated;
mod zoneinfo;
/// Returns a copy of the global [`TimeZoneDatabase`].
///
/// This is the same database used for convenience routines like
/// [`Timestamp::in_tz`](crate::Timestamp::in_tz) and parsing routines
/// for [`Zoned`](crate::Zoned) that need to do IANA time zone identifier
/// lookups. Basically, whenever an implicit time zone database is needed,
/// it is *this* copy of the time zone database that is used.
///
/// In feature configurations where a time zone database cannot interact with
/// the file system (like when `std` is not enabled), this returns a database
/// where every lookup will fail.
///
/// # Example
///
/// ```
/// use jiff::tz;
///
/// assert!(tz::db().get("Antarctica/Troll").is_ok());
/// assert!(tz::db().get("does-not-exist").is_err());
/// ```
pub fn db() -> &'static TimeZoneDatabase {
// #[cfg(any(not(feature = "std"), miri))]
#[cfg(not(feature = "std"))]
{
// Without `std` there's no lazily initialized global state. But when
// the tzdb is bundled into the binary, we can still hand out a usable
// database: the bundled database needs no allocation, so it can be
// constructed in a `const`. (Without `std`, the zoneinfo and
// concatenated databases are both unavailable, so there's nothing to
// prefer over the bundle.)
//
// Ref: https://github.com/BurntSushi/jiff/issues/533
#[cfg(any(
feature = "tzdb-bundle-always",
all(
feature = "tzdb-bundle-platform",
any(windows, target_family = "wasm"),
),
))]
static DB: TimeZoneDatabase = TimeZoneDatabase {
inner: Repr::Bundled(bundled::Database::new()),
};
#[cfg(not(any(
feature = "tzdb-bundle-always",
all(
feature = "tzdb-bundle-platform",
any(windows, target_family = "wasm"),
),
)))]
static DB: TimeZoneDatabase = TimeZoneDatabase::none();
&DB
}
// #[cfg(all(feature = "std", not(miri)))]
#[cfg(feature = "std")]
{
use std::sync::OnceLock;
static DB: OnceLock<TimeZoneDatabase> = OnceLock::new();
DB.get_or_init(|| {
let db = TimeZoneDatabase::from_env();
debug!("initialized global time zone database: {db:?}");
db
})
}
}
/// A handle to a [IANA Time Zone Database].
///
/// A `TimeZoneDatabase` provides a way to lookup [`TimeZone`]s by their
/// human readable identifiers, such as `America/Los_Angeles` and
/// `Europe/Warsaw`.
///
/// It is rare to need to create or use this type directly. Routines
/// like zoned datetime parsing and time zone conversion provide
/// convenience routines for using an implicit global time zone database
/// by default. This global time zone database is available via
/// [`jiff::tz::db`](crate::tz::db()`). But lower level parsing routines
/// such as
/// [`fmt::temporal::DateTimeParser::parse_zoned_with`](crate::fmt::temporal::DateTimeParser::parse_zoned_with)
/// and
/// [`civil::DateTime::to_zoned`](crate::civil::DateTime::to_zoned) provide a
/// means to use a custom copy of a `TimeZoneDatabase`.
///
/// # Platform behavior
///
/// This behavior is subject to change.
///
/// On Unix systems, and when the `tzdb-zoneinfo` crate feature is enabled
/// (which it is by default), Jiff will read the `/usr/share/zoneinfo`
/// directory for time zone data.
///
/// On Windows systems and when the `tzdb-bundle-platform` crate feature is
/// enabled (which it is by default), _or_ when the `tzdb-bundle-always` crate
/// feature is enabled, then the `jiff-tzdb` crate will be used to embed the
/// entire Time Zone Database into the compiled artifact.
///
/// On Android systems, and when the `tzdb-concatenated` crate feature is
/// enabled (which it is by default), Jiff will attempt to read a concatenated
/// zoneinfo database using the `ANDROID_DATA` or `ANDROID_ROOT` environment
/// variables.
///
/// In general, using `/usr/share/zoneinfo` (or an equivalent) is heavily
/// preferred in lieu of embedding the database into your compiled artifact.
/// The reason is because your system copy of the Time Zone Database may be
/// updated, perhaps a few times a year, and it is better to get seamless
/// updates through your system rather than needing to wait on a Rust crate
/// to update and then rebuild your software. The bundling approach should
/// only be used when there is no plausible alternative. For example, Windows
/// has no canonical location for a copy of the Time Zone Database. Indeed,
/// this is why the Cargo configuration of Jiff specifically does not enabled
/// bundling by default on Unix systems, but does enable it by default on
/// Windows systems. Of course, if you really do need a copy of the database
/// bundled, then you can enable the `tzdb-bundle-always` crate feature.
///
/// # Cloning
///
/// A `TimeZoneDatabase` can be cheaply cloned. It will share a thread safe
/// cache with other copies of the same `TimeZoneDatabase`.
///
/// # Caching
///
/// Because looking up a time zone on disk, reading the file into memory
/// and parsing the time zone transitions out of that file requires
/// a fair amount of work, a `TimeZoneDatabase` does a fair bit of
/// caching. This means that the vast majority of calls to, for example,
/// [`Timestamp::in_tz`](crate::Timestamp::in_tz) don't actually need to hit
/// disk. It will just find a cached copy of a [`TimeZone`] and return that.
///
/// Of course, with caching comes problems of cache invalidation. Invariably,
/// there are parameters that Jiff uses to manage when the cache should be
/// invalidated. Jiff tries to emit log messages about this when it happens. If
/// you find the caching behavior of Jiff to be sub-optimal for your use case,
/// please create an issue. (The plan is likely to expose some options for
/// configuring the behavior of a `TimeZoneDatabase`, but I wanted to collect
/// user feedback first.)
///
/// [IANA Time Zone Database]: https://en.wikipedia.org/wiki/Tz_database
///
/// # Example: list all available time zones
///
/// ```no_run
/// use jiff::tz;
///
/// for tzid in tz::db().available() {
/// println!("{tzid}");
/// }
/// ```
///
/// # Example: using multiple time zone databases
///
/// Jiff supports opening and using multiple time zone databases by default.
/// All you need to do is point [`TimeZoneDatabase::from_dir`] to your own
/// copy of the Time Zone Database, and it will handle the rest.
///
/// This example shows how to utilize multiple databases by parsing a datetime
/// using an older copy of the IANA Time Zone Database. This example leverages
/// the fact that the 2018 copy of the database preceded Brazil's announcement
/// that daylight saving time would be abolished. This meant that datetimes
/// in the future, when parsed with the older copy of the Time Zone Database,
/// would still follow the old daylight saving time rules. But a mere update of
/// the database would otherwise change the meaning of the datetime.
///
/// This scenario can come up if one stores datetimes in the future. This is
/// also why the default offset conflict resolution strategy when parsing zoned
/// datetimes is [`OffsetConflict::Reject`](crate::tz::OffsetConflict::Reject),
/// which prevents one from silently re-interpreting datetimes to a different
/// timestamp.
///
/// ```no_run
/// use jiff::{fmt::temporal::DateTimeParser, tz::{self, TimeZoneDatabase}};
///
/// static PARSER: DateTimeParser = DateTimeParser::new();
///
/// // Open a version of tzdb from before Brazil announced its abolition
/// // of daylight saving time.
/// let tzdb2018 = TimeZoneDatabase::from_dir("path/to/tzdb-2018b")?;
/// // Open the system tzdb.
/// let tzdb = tz::db();
///
/// // Parse the same datetime string with the same parser, but using two
/// // different versions of tzdb.
/// let dt = "2020-01-15T12:00[America/Sao_Paulo]";
/// let zdt2018 = PARSER.parse_zoned_with(&tzdb2018, dt)?;
/// let zdt = PARSER.parse_zoned_with(tzdb, dt)?;
///
/// // Before DST was abolished, 2020-01-15 was in DST, which corresponded
/// // to UTC offset -02. Since DST rules applied to datetimes in the
/// // future, the 2018 version of tzdb would lead one to interpret
/// // 2020-01-15 as being in DST.
/// assert_eq!(zdt2018.offset(), tz::offset(-2));
/// // But DST was abolished in 2019, which means that 2020-01-15 was no
/// // no longer in DST. So after a tzdb update, the same datetime as above
/// // now has a different offset.
/// assert_eq!(zdt.offset(), tz::offset(-3));
///
/// // So if you try to parse a datetime serialized from an older copy of
/// // tzdb, you'll get an error under the default configuration because
/// // of `OffsetConflict::Reject`. This would succeed if you parsed it
/// // using tzdb2018!
/// assert!(PARSER.parse_zoned_with(tzdb, zdt2018.to_string()).is_err());
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
#[derive(Clone)]
pub struct TimeZoneDatabase {
inner: Repr,
}
/// The internal representation of a `TimeZoneDatabase`.
///
/// The bundled database is kept out of the `Arc` because it carries no data
/// of its own (the tzdb is compiled into the binary and any parsed zones are
/// cached in a global). This lets it be constructed in a `const`, which is
/// what makes it possible for `db()` to return the bundled database even when
/// `std` is unavailable (and so there is no lazily initialized global state).
///
/// Ref: https://github.com/BurntSushi/jiff/issues/533
#[derive(Clone)]
enum Repr {
/// A database for which all lookups fail.
Empty,
/// The bundled database. Needs no allocation, so no `Arc`.
Bundled(bundled::Database),
/// A database backed by an `Arc` so that clones share its cache.
Arc(Arc<Kind>),
}
#[derive(Debug)]
// Needed for core-only "dumb" `Arc`.
#[cfg_attr(not(feature = "alloc"), derive(Clone))]
enum Kind {
ZoneInfo(zoneinfo::Database),
Concatenated(concatenated::Database),
}
impl TimeZoneDatabase {
/// Returns a database for which all time zone lookups fail.
///
/// # Example
///
/// ```
/// use jiff::tz::TimeZoneDatabase;
///
/// let db = TimeZoneDatabase::none();
/// assert_eq!(db.available().count(), 0);
/// ```
pub const fn none() -> TimeZoneDatabase {
TimeZoneDatabase { inner: Repr::Empty }
}
/// Returns a time zone database initialized from the current environment.
///
/// This routine never fails, but it may not be able to find a copy of
/// your Time Zone Database. When this happens, log messages (with some
/// at least at the `WARN` level) will be emitted. They can be viewed by
/// installing a [`log`] compatible logger such as [`env_logger`].
///
/// Typically, one does not need to call this routine directly. Instead,
/// it's done for you as part of [`jiff::tz::db`](crate::tz::db()).
/// This does require Jiff's `std` feature to be enabled though. So for
/// example, you might use this constructor when the features `alloc`
/// and `tzdb-bundle-always` are enabled to get access to a bundled
/// copy of the IANA time zone database. (Accessing the system copy at
/// `/usr/share/zoneinfo` requires `std`.)
///
/// Beware that calling this constructor will create a new _distinct_
/// handle from the one returned by `jiff::tz::db` with its own cache.
///
/// [`log`]: https://docs.rs/log
/// [`env_logger`]: https://docs.rs/env_logger
///
/// # Platform behavior
///
/// When the `TZDIR` environment variable is set, this will attempt to
/// open the Time Zone Database at the directory specified. Otherwise,
/// this will search a list of predefined directories for a system
/// installation of the Time Zone Database. Typically, it's found at
/// `/usr/share/zoneinfo`.
///
/// On Windows systems, under the default crate configuration, this will
/// return an embedded copy of the Time Zone Database since Windows does
/// not have a canonical installation of the Time Zone Database.
pub fn from_env() -> TimeZoneDatabase {
// On Android, try the concatenated database first, since that's
// typically what is used.
//
// Overall this logic might be sub-optimal. Like, does it really make
// sense to check for the zoneinfo or concatenated database on non-Unix
// platforms? Probably not to be honest. But these should only be
// executed ~once generally, so it doesn't seem like a big deal to try.
// And trying makes things a little more flexible I think.
#[cfg(not(miri))]
{
if cfg!(target_os = "android") {
let db = concatenated::Database::from_env();
if !db.is_definitively_empty() {
return TimeZoneDatabase::new(Kind::Concatenated(db));
}
let db = zoneinfo::Database::from_env();
if !db.is_definitively_empty() {
return TimeZoneDatabase::new(Kind::ZoneInfo(db));
}
} else {
let db = zoneinfo::Database::from_env();
if !db.is_definitively_empty() {
return TimeZoneDatabase::new(Kind::ZoneInfo(db));
}
let db = concatenated::Database::from_env();
if !db.is_definitively_empty() {
return TimeZoneDatabase::new(Kind::Concatenated(db));
}
}
}
let db = bundled::Database::new();
if !db.is_definitively_empty() {
return TimeZoneDatabase { inner: Repr::Bundled(db) };
}
warn!(
"could not find zoneinfo, concatenated tzdata or \
bundled time zone database",
);
TimeZoneDatabase::none()
}
/// Returns a time zone database initialized from the given directory.
///
/// Unlike [`TimeZoneDatabase::from_env`], this always attempts to look for
/// a copy of the Time Zone Database at the directory given. And if it
/// fails to find one at that directory, then an error is returned.
///
/// Basically, you should use this when you need to use a _specific_
/// copy of the Time Zone Database, and use `TimeZoneDatabase::from_env`
/// when you just want Jiff to try and "do the right thing for you."
///
/// # Errors
///
/// This returns an error if the given directory does not contain a valid
/// copy of the Time Zone Database. Generally, this means a directory with
/// at least one valid TZif file.
#[cfg(feature = "std")]
pub fn from_dir<P: AsRef<std::path::Path>>(
path: P,
) -> Result<TimeZoneDatabase, Error> {
let path = path.as_ref();
let db = zoneinfo::Database::from_dir(path)?;
if db.is_definitively_empty() {
warn!(
"could not find zoneinfo data at directory {path}",
path = path.display(),
);
}
Ok(TimeZoneDatabase::new(Kind::ZoneInfo(db)))
}
/// Returns a time zone database initialized from a path pointing to a
/// concatenated `tzdata` file. This type of format is only known to be
/// found on Android environments. The specific format for this file isn't
/// defined formally anywhere, but Jiff parses the same format supported
/// by the [Android Platform].
///
/// Unlike [`TimeZoneDatabase::from_env`], this always attempts to look for
/// a copy of the Time Zone Database at the path given. And if it
/// fails to find one at that path, then an error is returned.
///
/// Basically, you should use this when you need to use a _specific_
/// copy of the Time Zone Database in its concatenated format, and use
/// `TimeZoneDatabase::from_env` when you just want Jiff to try and "do the
/// right thing for you." (`TimeZoneDatabase::from_env` will attempt to
/// automatically detect the presence of a system concatenated `tzdata`
/// file on Android.)
///
/// # Errors
///
/// This returns an error if the given path does not contain a valid
/// copy of the concatenated Time Zone Database.
///
/// [Android Platform]: https://android.googlesource.com/platform/libcore/+/jb-mr2-release/luni/src/main/java/libcore/util/ZoneInfoDB.java
#[cfg(feature = "std")]
pub fn from_concatenated_path<P: AsRef<std::path::Path>>(
path: P,
) -> Result<TimeZoneDatabase, Error> {
let path = path.as_ref();
let db = concatenated::Database::from_path(path)?;
if db.is_definitively_empty() {
warn!(
"could not find concatenated tzdata in file {path}",
path = path.display(),
);
}
Ok(TimeZoneDatabase::new(Kind::Concatenated(db)))
}
/// Returns a time zone database initialized from the bundled copy of
/// the [IANA Time Zone Database].
///
/// While this API is always available, in order to get a non-empty
/// database back, this requires that one of the crate features
/// `tzdb-bundle-always` or `tzdb-bundle-platform` is enabled. In the
/// latter case, the bundled database is only available on platforms known
/// to lack a system copy of the IANA Time Zone Database (i.e., non-Unix
/// systems).
///
/// This routine is infallible, but it may return a database
/// that is definitively empty if the bundled data is not
/// available. To query whether the data is empty or not, use
/// [`TimeZoneDatabase::is_definitively_empty`].
///
/// # Data generation
///
/// The data in this crate comes from the [IANA Time Zone Database] "data
/// only" distribution. [`jiff-cli`] is used to first compile the release
/// into binary TZif data using the `zic` compiler, and secondly, converts
/// the binary data into a flattened and de-duplicated representation that
/// is embedded into this crate's source code.
///
/// The conversion into the TZif binary data uses the following settings:
///
/// * The "rearguard" data is used (see below).
/// * The binary data itself is compiled using the "slim" format. Which
/// effectively means that the TZif data primarily only uses explicit
/// time zone transitions for historical data and POSIX time zones for
/// current time zone transition rules. This doesn't have any impact
/// on the actual results. The reason that there are "slim" and "fat"
/// formats is to support legacy applications that can't deal with
/// POSIX time zones. For example, `/usr/share/zoneinfo` on my modern
/// Archlinux installation (2025-02-27) is in the "fat" format.
///
/// The reason that rearguard data is used is a bit more subtle and has
/// to do with a difference in how the IANA Time Zone Database treats its
/// internal "daylight saving time" flag and what people in the "real
/// world" consider "daylight saving time." For example, in the standard
/// distribution of the IANA Time Zone Database, `Europe/Dublin` has its
/// daylight saving time flag set to _true_ during Winter and set to
/// _false_ during Summer. The actual time shifts are the same as, e.g.,
/// `Europe/London`, but which one is actually labeled "daylight saving
/// time" is not.
///
/// The IANA Time Zone Database does this for `Europe/Dublin`, presumably,
/// because _legally_, time during the Summer in Ireland is called `Irish
/// Standard Time`, and time during the Winter is called `Greenwich Mean
/// Time`. These legal names are reversed from what is typically the case,
/// where "standard" time is during the Winter and daylight saving time is
/// during the Summer. The IANA Time Zone Database implements this tweak in
/// legal language via a "negative daylight saving time offset." This is
/// somewhat odd, and some consumers of the IANA Time Zone Database cannot
/// handle it. Thus, the rearguard format was born for, seemingly, legacy
/// programs.
///
/// Jiff can handle negative daylight saving time offsets just fine,
/// but we use the rearguard format anyway so that the underlying data
/// more accurately reflects on-the-ground reality for humans living in
/// `Europe/Dublin`. In particular, using the rearguard data enables
/// [localization of time zone names] to be done correctly.
///
/// [IANA Time Zone Database]: https://en.wikipedia.org/wiki/Tz_database
/// [`jiff-cli`]: https://github.com/BurntSushi/jiff/tree/master/crates/jiff-cli
/// [localization of time zone names]: https://github.com/BurntSushi/jiff/issues/258
pub fn bundled() -> TimeZoneDatabase {
let db = bundled::Database::new();
if db.is_definitively_empty() {
warn!("could not find embedded/bundled zoneinfo");
}
TimeZoneDatabase { inner: Repr::Bundled(db) }
}
/// Creates a new DB from the internal kind.
fn new(kind: Kind) -> TimeZoneDatabase {
TimeZoneDatabase { inner: Repr::Arc(Arc::new(kind)) }
}
/// Returns a [`TimeZone`] corresponding to the IANA time zone identifier
/// given.
///
/// The lookup is performed without regard to ASCII case.
///
/// To see a list of all available time zone identifiers for this database,
/// use [`TimeZoneDatabase::available`].
///
/// It is guaranteed that if the given time zone name is case insensitively
/// equivalent to `UTC`, then the time zone returned will be equivalent to
/// `TimeZone::UTC`. Similarly for `Etc/Unknown` and `TimeZone::unknown()`.
///
/// # Example
///
/// ```
/// use jiff::tz;
///
/// let tz = tz::db().get("america/NEW_YORK")?;
/// assert_eq!(tz.iana_name(), Some("America/New_York"));
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
pub fn get(&self, name: &str) -> Result<TimeZone, Error> {
let found = match self.inner {
Repr::Empty => {
return Err(Error::from(
E::failed_time_zone_no_database_configured(name),
));
}
Repr::Bundled(ref db) => db.get(name),
Repr::Arc(ref kind) => match **kind {
Kind::ZoneInfo(ref db) => db.get(name),
Kind::Concatenated(ref db) => db.get(name),
},
};
if let Some(tz) = found {
trace!("found time zone `{name}` in {db:?}", db = self);
return Ok(tz);
}
Err(Error::from(E::failed_time_zone(name)))
}
/// Returns a list of all available time zone identifiers from this
/// database.
///
/// Note that time zone identifiers are more of a machine readable
/// abstraction and not an end user level abstraction. Still, users
/// comfortable with configuring their system's default time zone through
/// IANA time zone identifiers are probably comfortable interacting with
/// the identifiers returned here.
///
/// # Example
///
/// ```no_run
/// use jiff::tz;
///
/// for tzid in tz::db().available() {
/// println!("{tzid}");
/// }
/// ```
pub fn available<'d>(&'d self) -> TimeZoneNameIter<'d> {
match self.inner {
Repr::Empty => TimeZoneNameIter::empty(),
Repr::Bundled(ref db) => db.available(),
Repr::Arc(ref kind) => match **kind {
Kind::ZoneInfo(ref db) => db.available(),
Kind::Concatenated(ref db) => db.available(),
},
}
}
/// Resets the internal cache of this database.
///
/// Subsequent interactions with this database will need to re-read time
/// zone data from disk.
///
/// It might be useful to call this if you know the time zone database
/// has changed on disk and want to force Jiff to re-load it immediately
/// without spawning a new process or waiting for Jiff's internal cache
/// invalidation heuristics to kick in.
pub fn reset(&self) {
match self.inner {
Repr::Empty => {}
Repr::Bundled(ref db) => db.reset(),
Repr::Arc(ref kind) => match **kind {
Kind::ZoneInfo(ref db) => db.reset(),
Kind::Concatenated(ref db) => db.reset(),
},
}
}
/// Returns true if it is known that this time zone database is empty.
///
/// When this returns true, it is guaranteed that all
/// [`TimeZoneDatabase::get`] calls will fail, and that
/// [`TimeZoneDatabase::available`] will always return an empty iterator.
///
/// Note that if this returns false, it is still possible for this database
/// to be empty.
///
/// # Example
///
/// ```
/// use jiff::tz::TimeZoneDatabase;
///
/// let db = TimeZoneDatabase::none();
/// assert!(db.is_definitively_empty());
/// ```
pub fn is_definitively_empty(&self) -> bool {
match self.inner {
Repr::Empty => true,
Repr::Bundled(ref db) => db.is_definitively_empty(),
Repr::Arc(ref kind) => match **kind {
Kind::ZoneInfo(ref db) => db.is_definitively_empty(),
Kind::Concatenated(ref db) => db.is_definitively_empty(),
},
}
}
}
impl core::fmt::Debug for TimeZoneDatabase {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.write_str("TimeZoneDatabase(")?;
match self.inner {
Repr::Empty => return f.write_str("unavailable)"),
Repr::Bundled(ref db) => core::fmt::Debug::fmt(db, f)?,
Repr::Arc(ref kind) => match **kind {
Kind::ZoneInfo(ref db) => core::fmt::Debug::fmt(db, f)?,
Kind::Concatenated(ref db) => core::fmt::Debug::fmt(db, f)?,
},
}
f.write_str(")")
}
}
#[cfg(feature = "defmt")]
impl defmt::Format for TimeZoneDatabase {
fn format(&self, f: defmt::Formatter) {
// `Kind` doesn't implement `defmt::Format`, so we only emit the type
// name. On embedded targets the database is usually bundled or unused,
// so the internal backend probably isn't meaningful to log either way.
defmt::write!(f, "TimeZoneDatabase(unavailable)");
}
}
/// An iterator over the time zone identifiers in a [`TimeZoneDatabase`].
///
/// This iterator is created by [`TimeZoneDatabase::available`].
///
/// There are no guarantees about the order in which this iterator yields
/// time zone identifiers.
///
/// The lifetime parameter corresponds to the lifetime of the
/// `TimeZoneDatabase` from which this iterator was created.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct TimeZoneNameIter<'d> {
#[cfg(feature = "alloc")]
it: alloc::vec::IntoIter<TimeZoneName<'d>>,
#[cfg(not(feature = "alloc"))]
it: core::iter::Empty<TimeZoneName<'d>>,
}
impl<'d> TimeZoneNameIter<'d> {
/// Creates a time zone name iterator that never yields any elements.
fn empty() -> TimeZoneNameIter<'d> {
#[cfg(feature = "alloc")]
{
TimeZoneNameIter { it: alloc::vec::Vec::new().into_iter() }
}
#[cfg(not(feature = "alloc"))]
{
TimeZoneNameIter { it: core::iter::empty() }
}
}
/// Creates a time zone name iterator that yields the elements from the
/// iterator given. (They are collected into a `Vec`.)
#[cfg(feature = "alloc")]
fn from_iter(
it: impl Iterator<Item = impl Into<alloc::string::String>>,
) -> TimeZoneNameIter<'d> {
let names: alloc::vec::Vec<TimeZoneName<'d>> =
it.map(|name| TimeZoneName::new(name.into())).collect();
TimeZoneNameIter { it: names.into_iter() }
}
}
impl<'d> Iterator for TimeZoneNameIter<'d> {
type Item = TimeZoneName<'d>;
fn next(&mut self) -> Option<TimeZoneName<'d>> {
self.it.next()
}
}
/// A name for a time zone yield by the [`TimeZoneNameIter`] iterator.
///
/// The iterator is created by [`TimeZoneDatabase::available`].
///
/// The lifetime parameter corresponds to the lifetime of the
/// `TimeZoneDatabase` from which this name was created.
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct TimeZoneName<'d> {
/// The lifetime of the tzdb.
///
/// We don't currently use this, but it could be quite useful if we ever
/// adopt a "compile time" tzdb like what `chrono-tz` has. Then we could
/// return strings directly from the embedded data. Or perhaps a "compile
/// time" TZif or some such.
lifetime: core::marker::PhantomData<&'d str>,
#[cfg(feature = "alloc")]
name: alloc::string::String,
#[cfg(not(feature = "alloc"))]
name: core::convert::Infallible,
}
impl<'d> TimeZoneName<'d> {
/// Returns a new time zone name from the string given.
///
/// The lifetime returned is inferred according to the caller's context.
#[cfg(feature = "alloc")]
fn new(name: alloc::string::String) -> TimeZoneName<'d> {
TimeZoneName { lifetime: core::marker::PhantomData, name }
}
/// Returns this time zone name as a borrowed string.
///
/// Note that the lifetime of the string returned is tied to `self`,
/// which may be shorter than the lifetime `'d` of the originating
/// `TimeZoneDatabase`.
#[inline]
pub fn as_str<'a>(&'a self) -> &'a str {
#[cfg(feature = "alloc")]
{
self.name.as_str()
}
#[cfg(not(feature = "alloc"))]
{
// Can never be reached because `TimeZoneName` cannot currently
// be constructed in core-only environments.
unreachable!()
}
}
}
impl<'d> core::fmt::Display for TimeZoneName<'d> {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.write_str(self.as_str())
}
}
/// Checks if `name` is a "special" time zone and returns one if so.
///
/// This is limited to special constants that should have consistent values
/// across time zone database implementations. For example, `UTC`.
fn special_time_zone(name: &str) -> Option<TimeZone> {
if utf8::cmp_ignore_ascii_case("utc", name).is_eq() {
return Some(TimeZone::UTC);
}
if utf8::cmp_ignore_ascii_case("etc/unknown", name).is_eq() {
return Some(TimeZone::unknown());
}
None
}
#[cfg(test)]
mod tests {
use super::*;
/// Tests that the global database returns the bundled tzdb when the bundle
/// is enabled but neither the zoneinfo nor concatenated databases are.
///
/// This configuration doesn't have `std` (since both `tzdb-zoneinfo` and
/// `tzdb-concatenated` imply it), and so without special handling `db()`
/// used to return an empty database despite the tzdb being compiled in.
///
/// Regression test for: https://github.com/BurntSushi/jiff/issues/533
#[cfg(all(
feature = "tzdb-bundle-always",
not(feature = "tzdb-zoneinfo"),
not(feature = "tzdb-concatenated"),
))]
#[test]
fn bundled_db_when_only_bundle_enabled() {
let db = db();
assert!(!db.is_definitively_empty());
assert!(db.get("America/New_York").is_ok());
// The convenience APIs route through the global `db()`, so this is
// the symptom originally reported in #533.
assert!(crate::civil::date(2024, 7, 4)
.at(12, 0, 0, 0)
.in_tz("America/New_York")
.is_ok());
}
/// This tests that the size of a time zone database is kept small.
///
/// It's two words because the bundled database is stored outside the
/// `Arc` (so it can be constructed in a `const`, see `db()` and #533),
/// which means the representation needs a tag to distinguish it from the
/// empty and `Arc`-backed variants.
///
/// I think it would probably be okay to make this bigger if we had a
/// good reason to, but it seems sensible to put a road-block to avoid
/// accidentally increasing its size.
#[test]
fn time_zone_database_size() {
#[cfg(feature = "alloc")]
{
let word = core::mem::size_of::<usize>();
assert_eq!(2 * word, core::mem::size_of::<TimeZoneDatabase>());
}
// A `TimeZoneDatabase` in core-only is vapid.
#[cfg(not(feature = "alloc"))]
{
assert_eq!(1, core::mem::size_of::<TimeZoneDatabase>());
}
}
/// Time zone databases should always return `TimeZone::UTC` if the time
/// zone is known to be UTC.
///
/// Regression test for: https://github.com/BurntSushi/jiff/issues/346
#[test]
fn bundled_returns_utc_constant() {
let db = TimeZoneDatabase::bundled();
if db.is_definitively_empty() {
return;
}
assert_eq!(db.get("UTC").unwrap(), TimeZone::UTC);
assert_eq!(db.get("utc").unwrap(), TimeZone::UTC);
assert_eq!(db.get("uTc").unwrap(), TimeZone::UTC);
assert_eq!(db.get("UtC").unwrap(), TimeZone::UTC);
// Also, similarly, for `Etc/Unknown`.
assert_eq!(db.get("Etc/Unknown").unwrap(), TimeZone::unknown());
assert_eq!(db.get("etc/UNKNOWN").unwrap(), TimeZone::unknown());
}
/// Time zone databases should always return `TimeZone::UTC` if the time
/// zone is known to be UTC.
///
/// Regression test for: https://github.com/BurntSushi/jiff/issues/346
#[cfg(all(feature = "std", not(miri)))]
#[test]
fn zoneinfo_returns_utc_constant() {
let Ok(db) = TimeZoneDatabase::from_dir("/usr/share/zoneinfo") else {
return;
};
if db.is_definitively_empty() {
return;
}
assert_eq!(db.get("UTC").unwrap(), TimeZone::UTC);
assert_eq!(db.get("utc").unwrap(), TimeZone::UTC);
assert_eq!(db.get("uTc").unwrap(), TimeZone::UTC);
assert_eq!(db.get("UtC").unwrap(), TimeZone::UTC);
// Also, similarly, for `Etc/Unknown`.
assert_eq!(db.get("Etc/Unknown").unwrap(), TimeZone::unknown());
assert_eq!(db.get("etc/UNKNOWN").unwrap(), TimeZone::unknown());
}
/// This checks that our zoneinfo database never returns a time zone
/// identifier that isn't presumed to correspond to a real and valid
/// TZif file in the tzdb.
///
/// This test was added when I optimized the initialized of Jiff's zoneinfo
/// database. Originally, it did a directory traversal along with a 4-byte
/// read of every file in the directory to check if the file was TZif or
/// something else. This turned out to be quite slow on slow file systems.
/// I rejiggered it so that the reads of every file were removed. But this
/// meant we could have loaded a name from a file that wasn't TZif into
/// our in-memory cache.
///
/// For doing a single time zone lookup, this isn't a problem, since we
/// have to read the TZif data anyway. If it's invalid, then we just
/// return `None` and log a warning. No big deal.
///
/// But for the `TimeZoneDatabase::available()` API, we were previously
/// just returning a list of names under the presumption that every such
/// name corresponds to a valid TZif file. This test checks that we don't
/// emit junk. (Which was in practice accomplished to moving the 4-byte
/// read to when we call `TimeZoneDatabase::available()`.)
///
/// Ref: https://github.com/BurntSushi/jiff/issues/366
#[cfg(all(feature = "std", not(miri)))]
#[test]
fn zoneinfo_available_returns_only_tzif() {
use alloc::{
collections::BTreeSet,
string::{String, ToString},
};
let Ok(db) = TimeZoneDatabase::from_dir("/usr/share/zoneinfo") else {
return;
};
if db.is_definitively_empty() {
return;
}
let names: BTreeSet<String> =
db.available().map(|n| n.as_str().to_string()).collect();
// Not all zoneinfo directories are created equal. Some have more or
// less junk than others. So just try a few things.
let should_be_absent = [
"leapseconds",
"tzdata.zi",
"leap-seconds.list",
"SECURITY",
"zone1970.tab",
"iso3166.tab",
"zonenow.tab",
"zone.tab",
];
for name in should_be_absent {
assert!(
!names.contains(name),
"found `{name}` in time zone list, but it shouldn't be there",
);
}
}
}
@@ -0,0 +1,44 @@
use crate::tz::{TimeZone, TimeZoneNameIter};
#[derive(Clone)]
pub(crate) struct Database;
impl Database {
pub(crate) fn from_env() -> Database {
Database
}
#[cfg(feature = "std")]
pub(crate) fn from_dir(
_dir: &std::path::Path,
) -> Result<Database, crate::Error> {
Err(crate::error::Error::from(
crate::error::CrateFeatureError::TzdbZoneInfo,
)
.context(crate::error::tz::db::Error::DisabledZoneInfo))
}
pub(crate) fn none() -> Database {
Database
}
pub(crate) fn reset(&self) {}
pub(crate) fn get(&self, _query: &str) -> Option<TimeZone> {
None
}
pub(crate) fn available<'d>(&'d self) -> TimeZoneNameIter<'d> {
TimeZoneNameIter::empty()
}
pub(crate) fn is_definitively_empty(&self) -> bool {
true
}
}
impl core::fmt::Debug for Database {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.write_str("ZoneInfo(unavailable)")
}
}
+902
View File
@@ -0,0 +1,902 @@
use alloc::{string::String, vec, vec::Vec};
use std::{
ffi::OsStr,
fs::File,
io::Read,
path::{Component, Path, PathBuf},
sync::{
atomic::{AtomicUsize, Ordering},
Arc, RwLock,
},
time::Duration,
};
use crate::{
error::{tz::db::Error as E, Error},
timestamp::Timestamp,
tz::{db::special_time_zone, TimeZone, TimeZoneNameIter},
util::{self, cache::Expiration, parse, utf8},
};
const DEFAULT_TTL: Duration = Duration::new(5 * 60, 0);
#[cfg(unix)]
static ZONEINFO_DIRECTORIES: &[&str] =
&["/usr/share/zoneinfo", "/usr/share/lib/zoneinfo", "/etc/zoneinfo"];
// In non-Unix environments, there is (as of 2025-05-17) no standard location
// for the zoneinfo database. And we specifically do not search the Unix-style
// directories because this can have weird and undesirable effects on Windows.
//
// Ref https://github.com/BurntSushi/jiff/issues/376
#[cfg(not(unix))]
static ZONEINFO_DIRECTORIES: &[&str] = &[];
pub(crate) struct Database {
dir: Option<PathBuf>,
names: Option<ZoneInfoNames>,
zones: RwLock<CachedZones>,
}
impl Database {
pub(crate) fn from_env() -> Database {
if let Some(tzdir) = std::env::var_os("TZDIR") {
let tzdir = PathBuf::from(tzdir);
trace!("opening zoneinfo database at TZDIR={}", tzdir.display());
match Database::from_dir(&tzdir) {
Ok(db) => return db,
Err(_err) => {
// This is a WARN because it represents a failure to
// satisfy a more direct request, which should be louder
// than failures related to auto-detection.
warn!("failed opening TZDIR={}: {_err}", tzdir.display());
// fall through to attempt default directories
}
}
}
for dir in ZONEINFO_DIRECTORIES {
let tzdir = Path::new(dir);
trace!("opening zoneinfo database at {}", tzdir.display());
match Database::from_dir(&tzdir) {
Ok(db) => return db,
Err(_err) => {
trace!("failed opening {}: {_err}", tzdir.display());
}
}
}
debug!(
"could not find zoneinfo database at any of the following \
paths: {}",
ZONEINFO_DIRECTORIES.join(", "),
);
Database::none()
}
pub(crate) fn from_dir(dir: &Path) -> Result<Database, Error> {
let names = Some(ZoneInfoNames::new(dir)?);
let zones = RwLock::new(CachedZones::new());
Ok(Database { dir: Some(dir.to_path_buf()), names, zones })
}
/// Creates a "dummy" zoneinfo database in which all lookups fail.
pub(crate) fn none() -> Database {
let dir = None;
let names = None;
let zones = RwLock::new(CachedZones::new());
Database { dir, names, zones }
}
pub(crate) fn reset(&self) {
let mut zones = self.zones.write().unwrap();
if let Some(ref names) = self.names {
names.reset();
}
zones.reset();
}
pub(crate) fn get(&self, query: &str) -> Option<TimeZone> {
if let Some(tz) = special_time_zone(query) {
return Some(tz);
}
// If we couldn't build any time zone names, then every lookup will
// fail. So just bail now.
let names = self.names.as_ref()?;
// The fast path is when the query matches a pre-existing unexpired
// time zone.
{
let zones = self.zones.read().unwrap();
if let Some(czone) = zones.get(query) {
if !czone.is_expired() {
trace!(
"for time zone query `{query}`, \
found cached zone `{}` \
(expiration={}, last_modified={:?})",
czone.tz.diagnostic_name(),
czone.expiration,
czone.last_modified,
);
return Some(czone.tz.clone());
}
}
}
// At this point, one of three possible cases is true:
//
// 1. The given query does not match any time zone in this database.
// 2. A time zone exists, but isn't cached.
// 3. A zime exists and is cached, but needs to be revalidated.
//
// While (3) is probably the common case since our TTLs are pretty
// short, both (2) and (3) require write access. Thus we rule out (1)
// before acquiring a write lock on the entire database. Plus, we'll
// need the zone info for case (2) and possibly for (3) if cache
// revalidation fails.
//
// I feel kind of bad about all this because it seems to me like there
// is too much work being done while holding on to the write lock.
// In particular, it seems like bad juju to do any I/O of any kind
// while holding any lock at all. I think I could design something
// that avoids doing I/O while holding a lock, but it seems a lot more
// complicated. (And what happens if the I/O becomes outdated by the
// time you acquire the lock?)
let info = names.get(query)?;
let mut zones = self.zones.write().unwrap();
let ttl = zones.ttl;
match zones.get_zone_index(query) {
Ok(i) => {
let czone = &mut zones.zones[i];
if czone.revalidate(&info, ttl) {
// Metadata on the file didn't change, so we assume the
// file hasn't either.
return Some(czone.tz.clone());
}
// Revalidation failed. Re-read the TZif data.
let czone = match CachedTimeZone::new(&info, zones.ttl) {
Ok(czone) => czone,
Err(_err) => {
warn!(
"failed to re-cache time zone from file {}: {_err}",
info.inner.full.display(),
);
return None;
}
};
let tz = czone.tz.clone();
zones.zones[i] = czone;
Some(tz)
}
Err(i) => {
let czone = match CachedTimeZone::new(&info, ttl) {
Ok(czone) => czone,
Err(_err) => {
warn!(
"failed to cache time zone from file {}: {_err}",
info.inner.full.display(),
);
return None;
}
};
let tz = czone.tz.clone();
zones.zones.insert(i, czone);
Some(tz)
}
}
}
pub(crate) fn available<'d>(&'d self) -> TimeZoneNameIter<'d> {
let Some(names) = self.names.as_ref() else {
return TimeZoneNameIter::empty();
};
TimeZoneNameIter::from_iter(names.available().into_iter())
}
pub(crate) fn is_definitively_empty(&self) -> bool {
self.names.is_none()
}
}
impl core::fmt::Debug for Database {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.write_str("ZoneInfo(")?;
if let Some(ref dir) = self.dir {
core::fmt::Display::fmt(&dir.display(), f)?;
} else {
f.write_str("unavailable")?;
}
f.write_str(")")
}
}
#[derive(Debug)]
struct CachedZones {
zones: Vec<CachedTimeZone>,
ttl: Duration,
}
impl CachedZones {
const DEFAULT_TTL: Duration = DEFAULT_TTL;
fn new() -> CachedZones {
CachedZones { zones: vec![], ttl: CachedZones::DEFAULT_TTL }
}
fn get(&self, query: &str) -> Option<&CachedTimeZone> {
self.get_zone_index(query).ok().map(|i| &self.zones[i])
}
fn get_zone_index(&self, query: &str) -> Result<usize, usize> {
// The common case is that our query matches the time zone name case
// sensitively, so check for that first. It's a bit cheaper than doing
// a case insensitive search.
if let Ok(i) = self
.zones
.binary_search_by(|zone| zone.name.original().cmp(&query))
{
return Ok(i);
}
self.zones.binary_search_by(|zone| {
utf8::cmp_ignore_ascii_case(zone.name.lower(), query)
})
}
fn reset(&mut self) {
self.zones.clear();
}
}
#[derive(Clone, Debug)]
struct CachedTimeZone {
tz: TimeZone,
name: ZoneInfoName,
expiration: Expiration,
last_modified: Option<Timestamp>,
}
impl CachedTimeZone {
/// Create a new cached time zone.
///
/// The `info` says which time zone to create and where to find it. The
/// `ttl` says how long the cached time zone should minimally remain fresh
/// for.
fn new(
info: &ZoneInfoName,
ttl: Duration,
) -> Result<CachedTimeZone, Error> {
fn imp(
info: &ZoneInfoName,
ttl: Duration,
) -> Result<CachedTimeZone, Error> {
let path = info.path();
let mut file =
File::open(path).map_err(|e| Error::io(e).path(path))?;
let mut data = vec![];
file.read_to_end(&mut data)
.map_err(|e| Error::io(e).path(path))?;
let tz = TimeZone::tzif(&info.inner.original, &data)
.map_err(|e| e.path(path))?;
let name = info.clone();
let last_modified = util::fs::last_modified_from_file(path, &file);
let expiration = Expiration::after(ttl);
Ok(CachedTimeZone { tz, name, expiration, last_modified })
}
let result = imp(info, ttl);
info.set_validity(result.is_ok());
result
}
/// Returns true if this time zone has gone stale and should, at minimum,
/// be revalidated.
fn is_expired(&self) -> bool {
self.expiration.is_expired()
}
/// Attempts to revalidate this cached time zone.
///
/// Upon successful revalidation (that is, the cached time zone is still
/// fresh and okay to use), this returns true. Otherwise, the cached time
/// zone should be considered stale and must be re-created.
///
/// Note that technically another layer of revalidation could be done.
/// For example, we could keep a checksum of the TZif data, and only
/// consider rebuilding the time zone when the checksum changes. But I
/// think the last modified metadata will in practice be good enough, and
/// parsing a TZif file should be quite fast.
fn revalidate(&mut self, info: &ZoneInfoName, ttl: Duration) -> bool {
// If we started with no last modified timestamp, then I guess we
// should always fail revalidation? I suppose a case could be made to
// do the opposite: always pass revalidation.
let Some(old_last_modified) = self.last_modified else {
trace!(
"revalidation for {} failed because old last modified time \
is unavailable",
info.inner.full.display(),
);
return false;
};
let Some(new_last_modified) =
util::fs::last_modified_from_path(info.path())
else {
trace!(
"revalidation for {} failed because new last modified time \
is unavailable",
info.inner.full.display(),
);
return false;
};
// We consider any change to invalidate cache.
if old_last_modified != new_last_modified {
trace!(
"revalidation for {} failed because last modified times \
do not match: old = {} != {} = new",
info.inner.full.display(),
old_last_modified,
new_last_modified,
);
return false;
}
trace!(
"revalidation for {} succeeded because last modified times \
match: old = {} == {} = new",
info.inner.full.display(),
old_last_modified,
new_last_modified,
);
self.expiration = Expiration::after(ttl);
true
}
}
/// A collection of time zone names extracted from a zoneinfo directory.
///
/// Each time zone name maps to a full path on the file system corresponding
/// to the TZif formatted data file for that time zone.
///
/// This type is responsible not just for providing the names, but also for
/// updating them periodically.
#[derive(Debug)]
struct ZoneInfoNames {
inner: RwLock<ZoneInfoNamesInner>,
}
#[derive(Debug)]
struct ZoneInfoNamesInner {
/// The directory from which we collected time zone names.
dir: PathBuf,
/// All available names from the `zoneinfo` directory.
///
/// Each name corresponds to the suffix of a file path
/// starting with `dir`. For example, `America/New_York` in
/// `/usr/share/zoneinfo/America/New_York`. Each name also has a normalized
/// lowercase version of the name for easy case insensitive lookup.
names: Vec<ZoneInfoName>,
/// The expiration time of this cached value.
///
/// Note that this is a necessary but not sufficient criterion for
/// invalidating the cached value.
ttl: Duration,
/// The time at which the data in `names` becomes stale.
expiration: Expiration,
}
impl ZoneInfoNames {
/// The default amount of time to wait before checking for added/removed
/// time zones.
///
/// Note that this TTL is a necessary but not sufficient criterion to
/// provoke cache invalidation. Namely, since we don't expect the set of
/// possible time zone names to change often, we only invalidate the cache
/// under these circumstances:
///
/// 1. The TTL or more has passed since the last time the names were
/// attempted to be refreshed (even if it wasn't successful).
/// 2. A name lookup is attempted and it isn't found. This is required
/// because otherwise there isn't much point in refreshing the names.
///
/// This logic does not deal as well with removals from the underlying time
/// zone database. That in turn is covered by the TTL on constructing the
/// `TimeZone` values themselves.
///
/// We could just use the second criterion on its own, but we require the
/// TTL to expire out of "good sense." Namely, if there is something borked
/// in the environment, the TTL will prevent doing a full scan of the
/// zoneinfo directory for every missed time zone lookup.
const DEFAULT_TTL: Duration = DEFAULT_TTL;
/// Create a new collection of names from the zoneinfo database directory
/// given.
///
/// If no names of time zones with corresponding TZif data files could be
/// found in the given directory, then an error is returned.
fn new(dir: &Path) -> Result<ZoneInfoNames, Error> {
let names = walk(dir)?;
let dir = dir.to_path_buf();
let ttl = ZoneInfoNames::DEFAULT_TTL;
let expiration = Expiration::after(ttl);
let inner = ZoneInfoNamesInner { dir, names, ttl, expiration };
Ok(ZoneInfoNames { inner: RwLock::new(inner) })
}
/// Attempts to find the name entry for the given query using a case
/// insensitive search.
///
/// If no match is found and the data is stale, then the time zone names
/// are refreshed from the file system before doing another check.
fn get(&self, query: &str) -> Option<ZoneInfoName> {
{
let inner = self.inner.read().unwrap();
if let Some(zone_info_name) = inner.get(query) {
return Some(zone_info_name);
}
drop(inner); // unlock
}
let mut inner = self.inner.write().unwrap();
inner.attempt_refresh();
inner.get(query)
}
/// Returns all available time zone names after attempting a refresh of
/// the underlying data if it's stale.
fn available(&self) -> Vec<String> {
let mut inner = self.inner.write().unwrap();
inner.attempt_refresh();
inner.available()
}
fn reset(&self) {
self.inner.write().unwrap().reset();
}
}
impl ZoneInfoNamesInner {
/// Attempts to find the name entry for the given query using a case
/// insensitive search.
///
/// `None` is returned if one isn't found.
fn get(&self, query: &str) -> Option<ZoneInfoName> {
self.names
.binary_search_by(|n| {
utf8::cmp_ignore_ascii_case(&n.inner.lower, query)
})
.ok()
.map(|i| self.names[i].clone())
}
/// Returns all available time zone names.
fn available(&self) -> Vec<String> {
self.names
.iter()
.filter(|n| n.is_valid())
.map(|n| n.inner.original.clone())
.collect()
}
/// Attempts a refresh, but only follows through if the TTL has been
/// exceeded.
///
/// The caller must ensure that the other cache invalidation criteria
/// have been upheld. For example, this should only be called for a missed
/// zone name lookup.
fn attempt_refresh(&mut self) {
if self.expiration.is_expired() {
self.refresh();
}
}
/// Forcefully refreshes the cached names with possibly new data from disk.
/// If an error occurs when fetching the names, then no names are updated
/// (but the `expires_at` is updated). This will also emit a warning log on
/// failure.
fn refresh(&mut self) {
// PERF: Should we try to move this `walk` call to run outside of a
// lock? It probably happens pretty rarely, so it might not matter.
let result = walk(&self.dir);
self.expiration = Expiration::after(self.ttl);
match result {
Ok(names) => {
self.names = names;
}
Err(_err) => {
warn!(
"failed to refresh zoneinfo time zone name cache \
for {}: {_err}",
self.dir.display(),
)
}
}
}
/// Resets the state such that the next lookup is guaranteed to force a
/// cache refresh, and that it is impossible for any data to be stale.
fn reset(&mut self) {
// This will force the next lookup to fail.
self.names.clear();
// And this will force the next failed lookup to result in a refresh.
self.expiration = Expiration::expired();
}
}
/// A single TZif entry in a zoneinfo database directory.
#[derive(Clone, Debug)]
struct ZoneInfoName {
inner: Arc<ZoneInfoNameInner>,
}
#[derive(Debug)]
struct ZoneInfoNameInner {
/// A file path resolvable to the corresponding file relative to the
/// working directory of this program.
///
/// Should we canonicalize this to a absolute path? I guess in practice it
/// is an absolute path in most cases.
full: PathBuf,
/// The original name of this time zone taken from the file path with
/// no additional changes.
original: String,
/// The lowercase version of `original`. This is how we determine name
/// equality.
lower: String,
/// The known validity state of this time zone name. `0` means unknown (and
/// thus we need to check), `1` means "presumably valid" and `2` means
/// "known invalid." The "presumably valid" means that the file has a
/// 4-byte TZif header and the odds of a false positive a low enough that
/// we can and should behave as if that file is actually TZif and thus a
/// valid IANA time zone identifier.
validity: AtomicUsize,
}
impl ZoneInfoName {
/// Create a new time zone info name.
///
/// `base` should corresponding to the zoneinfo directory from which the
/// suffix `time_zone_name` path was returned.
fn new(base: &Path, time_zone_name: &Path) -> Result<ZoneInfoName, Error> {
let full = base.join(time_zone_name);
let original = iana_name_from_path(time_zone_name)
.map_err(|err| Error::from(err).path(base))?;
let lower = original.to_ascii_lowercase();
let inner = ZoneInfoNameInner {
full,
original,
lower,
validity: AtomicUsize::new(ZONE_INFO_NAME_UNKNOWN),
};
Ok(ZoneInfoName { inner: Arc::new(inner) })
}
/// Returns the path to the corresponding (presumed) TZif file.
fn path(&self) -> &Path {
&self.inner.full
}
/// Returns the original name of this time zone.
fn original(&self) -> &str {
&self.inner.original
}
/// Returns the lowercase name of this time zone.
fn lower(&self) -> &str {
&self.inner.lower
}
/// Returns true if it is presumed that this points to a valid TZif file.
///
/// The result of this function may use a cached value.
fn is_valid(&self) -> bool {
let validity = self.inner.validity.load(Ordering::Relaxed);
if validity == ZONE_INFO_NAME_VALID {
return true;
} else if validity == ZONE_INFO_NAME_INVALID {
return false;
}
if self.is_valid_impl() {
self.inner.validity.store(ZONE_INFO_NAME_VALID, Ordering::Relaxed);
true
} else {
self.inner
.validity
.store(ZONE_INFO_NAME_INVALID, Ordering::Relaxed);
false
}
}
/// Marks this zone info name as known to be valid or invalid.
///
/// e.g., After doing a successful time zone lookup.
fn set_validity(&self, is_valid: bool) {
let validity = if is_valid {
ZONE_INFO_NAME_VALID
} else {
ZONE_INFO_NAME_INVALID
};
self.inner.validity.store(validity, Ordering::Relaxed);
}
/// Performs the actual validity check.
///
/// This is generally only needed for APIs like
/// `TimeZoneDatabase::available()`, where we don't want to load every time
/// zone into memory but we also don't want to return IANA time zone ids
/// like `zone.tab`. (Because a `/usr/share/zoneinfo` directory might have
/// random junk in it.)
fn is_valid_impl(&self) -> bool {
let path = self.path();
let mut f = match File::open(path) {
Ok(f) => f,
Err(_err) => {
trace!("failed to open {}: {_err}", path.display());
return false;
}
};
let mut buf = [0; 4];
if let Err(_err) = f.read_exact(&mut buf) {
trace!(
"failed to read first 4 bytes of {}: {_err}",
path.display()
);
return false;
}
if !jcore::tz::tzif::is_possibly_tzif(&buf) {
// This is a trace because it's perfectly normal for a
// non-TZif file to be in a zoneinfo directory. But it could
// still be potentially useful debugging info.
trace!(
"found file {} that isn't TZif since its first \
four bytes are {:?}",
path.display(),
crate::util::escape::Bytes(&buf),
);
return false;
}
true
}
}
fn iana_name_from_path(
path: &Path,
) -> Result<String, crate::error::util::OsStrUtf8Error> {
let mut name = String::new();
for component in path.components() {
let Component::Normal(part) = component else { continue };
let part = parse::os_str_utf8(part)?;
if !name.is_empty() {
name.push('/');
}
name.push_str(part);
}
Ok(name)
}
impl Eq for ZoneInfoName {}
impl PartialEq for ZoneInfoName {
fn eq(&self, rhs: &ZoneInfoName) -> bool {
self.inner.lower == rhs.inner.lower
}
}
impl Ord for ZoneInfoName {
fn cmp(&self, rhs: &ZoneInfoName) -> core::cmp::Ordering {
self.inner.lower.cmp(&rhs.inner.lower)
}
}
impl PartialOrd for ZoneInfoName {
fn partial_cmp(&self, rhs: &ZoneInfoName) -> Option<core::cmp::Ordering> {
Some(self.cmp(rhs))
}
}
impl core::hash::Hash for ZoneInfoName {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.inner.lower.hash(state);
}
}
static ZONE_INFO_NAME_UNKNOWN: usize = 0;
static ZONE_INFO_NAME_VALID: usize = 1;
static ZONE_INFO_NAME_INVALID: usize = 2;
/// Recursively walks the given directory and returns the names of all time
/// zones found.
///
/// This is guaranteed to return either one or more time zone names OR an
/// error. That is, `Ok(vec![])` is an impossible result.
///
/// This will attempt to collect as many names as possible, even if some I/O
/// operations fail.
///
/// The names returned are sorted in lexicographic order according to the
/// lowercase form of each name.
///
/// # Performance
///
/// Note that this routine is written in a way that, at least on Unix, we
/// should not be doing a syscall for every file. We need to do one for every
/// directory, but that should be comparatively rare. It's done this way to
/// avoid long initialization times when `/usr/share/zoneinfo` is on a slow
/// file system.
///
/// See: https://github.com/BurntSushi/jiff/issues/366
fn walk(start: &Path) -> Result<Vec<ZoneInfoName>, Error> {
struct StackEntry {
dir: PathBuf,
depth: usize,
}
let mut first_err: Option<Error> = None;
let mut seterr = |path: &Path, err: Error| {
if first_err.is_none() {
first_err = Some(err.path(path));
}
};
let mut names = vec![];
let mut stack = vec![StackEntry { dir: start.to_path_buf(), depth: 0 }];
while let Some(StackEntry { dir, depth }) = stack.pop() {
let readdir = match dir.read_dir() {
Ok(readdir) => readdir,
Err(err) => {
info!(
"error when reading {} as a directory: {err}",
dir.display()
);
seterr(&dir, Error::io(err));
continue;
}
};
for result in readdir {
let dent = match result {
Ok(dent) => dent,
Err(err) => {
info!(
"error when reading directory entry from {}: {err}",
dir.display()
);
seterr(&dir, Error::io(err));
continue;
}
};
let file_type = match dent.file_type() {
Ok(file_type) => file_type,
Err(err) => {
let path = dent.path();
info!(
"error when reading file type from {}: {err}",
path.display()
);
seterr(&path, Error::io(err));
continue;
}
};
let path = dent.path();
if file_type.is_dir() {
// We ignore the `posix` and `right` directories because Jiff
// doesn't care about them. They tend to bloat the output of
// `TimeZoneDatabase::available()` for no appreciable reason.
// If callers want to use them, they can do, e.g.,
// `TZDIR=/usr/share/zoneinfo/posix`. Moreover, they slow down
// initialization time in environments with very slow file
// systems.
if depth == 0
&& (dent.file_name() == OsStr::new("posix")
|| dent.file_name() == OsStr::new("right"))
{
continue;
}
stack.push(StackEntry {
dir: path,
depth: depth.saturating_add(1),
});
continue;
}
trace!(
"zoneinfo database initialization visiting {path}",
path = path.display(),
);
// We assume symlinks are files, although this may not be
// appropriate. If we need to also handle the case when they're
// directories, then we'll need to add symlink loop detection.
let time_zone_name = match path.strip_prefix(start) {
Ok(time_zone_name) => time_zone_name,
// I think this error case is actually not possible.
// Or if it does, is a legitimate bug. Namely, `start`
// should always be a prefix of `path`, since `path`
// is itself derived, ultimately, from `start`.
Err(_err) => {
trace!(
"failed to extract time zone name from {} \
using {} as a base: {_err}",
path.display(),
start.display(),
);
seterr(&path, Error::from(E::ZoneInfoStripPrefix));
continue;
}
};
let zone_info_name =
match ZoneInfoName::new(&start, time_zone_name) {
Ok(zone_info_name) => zone_info_name,
Err(err) => {
seterr(&path, err);
continue;
}
};
names.push(zone_info_name);
}
}
if names.is_empty() {
let err = first_err
.take()
.unwrap_or_else(|| Error::from(E::ZoneInfoNoTzifFiles));
Err(err)
} else {
// If we found at least one valid name, then we declare success and
// drop any error we might have found. They do all get logged above
// though.
names.sort();
Ok(names)
}
}
#[cfg(all(
test,
not(miri),
not(target_arch = "wasm32"),
not(target_arch = "wasm64"),
))]
mod tests {
use super::*;
fn mktempdir() -> tempfile::TempDir {
tempfile::TempDir::with_prefix("jiff-zoneinfo-").unwrap()
}
#[test]
fn iana_name_always_uses_slashes() {
let name = ZoneInfoName::new(
Path::new("/"),
&Path::new("Asia").join("Tokyo"),
)
.unwrap();
assert_eq!("Asia/Tokyo", name.original());
}
#[test]
fn from_dir_uses_iana_name_separators() {
let tmpdir = mktempdir();
let zoneinfo = tmpdir.path().join("zoneinfo");
let asia = zoneinfo.join("Asia");
std::fs::create_dir_all(&asia).unwrap();
std::fs::write(
asia.join("Tokyo"),
crate::tz::testdata::TzifTestFile::get("Asia/Tokyo").data,
)
.unwrap();
let db = Database::from_dir(&zoneinfo).unwrap();
assert!(db.get("Asia/Tokyo").is_some());
assert!(db.get(r"Asia\Tokyo").is_none());
}
/// DEBUG COMMAND
///
/// Takes environment variable `JIFF_DEBUG_ZONEINFO_DIR` as input and
/// prints a list of all time zone names in the directory (one per line).
///
/// Callers may also set `RUST_LOG` to get extra debugging output.
#[test]
fn debug_zoneinfo_walk() -> anyhow::Result<()> {
let _ = crate::logging::Logger::init();
const ENV: &str = "JIFF_DEBUG_ZONEINFO_DIR";
let Some(val) = std::env::var_os(ENV) else { return Ok(()) };
let dir = PathBuf::from(val);
let names = walk(&dir)?;
for n in names {
std::eprintln!("{}", n.inner.original);
}
Ok(())
}
}
+8
View File
@@ -0,0 +1,8 @@
pub(crate) use self::inner::*;
#[cfg(not(feature = "tzdb-zoneinfo"))]
#[path = "disabled.rs"]
mod inner;
#[cfg(feature = "tzdb-zoneinfo")]
#[path = "enabled.rs"]
mod inner;
+358
View File
@@ -0,0 +1,358 @@
/*!
Routines for interacting with time zones and the zoneinfo database.
The main type in this module is [`TimeZone`]. For most use cases, you may not
even need to interact with this type at all. For example, this code snippet
converts a civil datetime to a zone aware datetime:
```
use jiff::civil::date;
let zdt = date(2024, 7, 10).at(20, 48, 0, 0).in_tz("America/New_York")?;
assert_eq!(zdt.to_string(), "2024-07-10T20:48:00-04:00[America/New_York]");
# Ok::<(), Box<dyn std::error::Error>>(())
```
And this example parses a zone aware datetime from a string:
```
use jiff::Zoned;
let zdt: Zoned = "2024-07-10 20:48[america/new_york]".parse()?;
assert_eq!(zdt.year(), 2024);
assert_eq!(zdt.month(), 7);
assert_eq!(zdt.day(), 10);
assert_eq!(zdt.hour(), 20);
assert_eq!(zdt.minute(), 48);
assert_eq!(zdt.offset().seconds(), -4 * 60 * 60);
assert_eq!(zdt.time_zone().iana_name(), Some("America/New_York"));
# Ok::<(), Box<dyn std::error::Error>>(())
```
Yet, neither of the above examples require uttering [`TimeZone`]. This is
because the datetime types in this crate provide higher level abstractions for
working with time zone identifiers. Nevertheless, sometimes it is useful to
work with a `TimeZone` directly. For example, if one has a `TimeZone`, then
conversion from a [`Timestamp`](crate::Timestamp) to a [`Zoned`](crate::Zoned)
is infallible:
```
use jiff::{tz::TimeZone, Timestamp};
let tz = TimeZone::get("America/New_York")?;
let ts = Timestamp::UNIX_EPOCH;
let zdt = ts.to_zoned(tz);
assert_eq!(zdt.to_string(), "1969-12-31T19:00:00-05:00[America/New_York]");
# Ok::<(), Box<dyn std::error::Error>>(())
```
# The [IANA Time Zone Database]
Since a time zone is a set of rules for determining the civil time, via an
offset from UTC, in a particular geographic region, a database is required to
represent the full complexity of these rules in practice. The standard database
is widespread use is the [IANA Time Zone Database]. On Unix systems, this is
typically found at `/usr/share/zoneinfo`, and Jiff will read it automatically.
On Windows systems, there is no canonical Time Zone Database installation, and
so Jiff embeds it into the compiled artifact. (This does not happen on Unix
by default.)
See the [`TimeZoneDatabase`] for more information.
# The system or "local" time zone
In many cases, the operating system manages a "default" time zone. It might,
for example, be how the `date` program converts a Unix timestamp to a time that
is "local" to you.
Unfortunately, there is no universal approach to discovering a system's default
time zone. Instead, Jiff uses heuristics like reading `/etc/localtime` on Unix,
and calling [`GetDynamicTimeZoneInformation`] on Windows. But in all cases,
Jiff will always use the IANA Time Zone Database for implementing time zone
transition rules. (For example, Windows specific APIs for time zone transitions
are not supported by Jiff.)
Moreover, Jiff supports reading the `TZ` environment variable, as specified
by POSIX, on all systems.
To get the system's default time zone, use [`TimeZone::system`].
# Core-only environments
By default, Jiff attempts to read time zone rules from `/usr/share/zoneinfo`
on Unix and a bundled database on other platforms (like on Windows). This happens
at runtime, and aside from requiring APIs to interact with the file system
on Unix, it also requires dynamic memory allocation.
For core-only environments that don't have file system APIs or dynamic
memory allocation, Jiff provides a way to construct `TimeZone` values at
compile time by compiling time zone rules into your binary. This does mean
that your program will need to be re-compiled if the time zone rules change
(in contrast to Jiff's default behavior of reading `/usr/share/zoneinfo` at
runtime on Unix), but sometimes there isn't a practical alternative.
With the `static` crate feature enabled, the [`jiff::tz::get`](crate::tz::get)
macro becomes available in this module. This example shows how use it to build
a `TimeZone` at compile time. Here, we find the next DST transition from a
particular timestamp in `Europe/Zurich`, and then print that in local time for
Zurich:
```
use jiff::{tz::{self, TimeZone}, Timestamp};
static TZ: TimeZone = tz::get!("Europe/Zurich");
let ts: Timestamp = "2025-02-25T00:00Z".parse()?;
let Some(next_transition) = TZ.following(ts).next() else {
return Err("no time zone transitions".into());
};
let zdt = next_transition.timestamp().to_zoned(TZ.clone());
assert_eq!(zdt.to_string(), "2025-03-30T03:00:00+02:00[Europe/Zurich]");
# Ok::<(), Box<dyn std::error::Error>>(())
```
The above example does not require dynamic memory allocation or access to file
system APIs. It also _only_ embeds the `Europe/Zurich` time zone into your
compiled binary.
[IANA Time Zone Database]: https://en.wikipedia.org/wiki/Tz_database
[`GetDynamicTimeZoneInformation`]: https://learn.microsoft.com/en-us/windows/win32/api/timezoneapi/nf-timezoneapi-getdynamictimezoneinformation
*/
pub use self::{
ambiguous::{
AmbiguousOffset, AmbiguousTimestamp, AmbiguousZoned, Disambiguation,
},
db::{db, TimeZoneDatabase, TimeZoneName, TimeZoneNameIter},
offset::{Dst, Offset, OffsetArithmetic, OffsetConflict, OffsetRound},
timezone::{
TimeZone, TimeZoneFollowingTransitions, TimeZoneOffsetInfo,
TimeZonePrecedingTransitions, TimeZoneTransition,
},
};
mod ambiguous;
#[cfg(feature = "tzdb-concatenated")]
mod concatenated;
mod db;
mod offset;
pub(crate) mod posix;
#[cfg(feature = "tz-system")]
mod system;
#[cfg(all(test, feature = "alloc"))]
pub(crate) mod testdata;
mod timezone;
mod tzif;
// See module comment for WIP status. :-(
#[cfg(all(test, feature = "alloc"))]
mod zic;
/// Create a `TimeZone` value from TZif data in [`jiff-tzdb`] at compile time.
///
/// This reads the data for the time zone with the IANA identifier given from
/// [`jiff-tzdb`], parses it as TZif specified by [RFC 9636], and constructs a
/// `TimeZone` value for use in a `const` context. This enables using IANA time
/// zones with Jiff in core-only environments. No dynamic memory allocation is
/// used.
///
/// # Input
///
/// This macro takes one positional parameter that must be a literal string.
/// The string should be an IANA time zone identifier, e.g.,
/// `America/New_York`.
///
/// # Return type
///
/// This macro returns a value with type `TimeZone`. To get a `&'static
/// TimeZone`, simply use `&get!("...")`.
///
/// # Usage
///
/// Callers should only call this macro once for each unique IANA time zone
/// identifier you need. Otherwise, multiple copies of the same embedded
/// time zone data could appear in your binary. There are no correctness
/// issues with this, but it could make your binary bigger than it needs to be.
///
/// # When should I use this?
///
/// Users should only use this macro if they have a _specific need_ for it
/// (like using a time zone on an embedded device). In particular, this will
/// embed the time zone transition rules into your binary. If the time zone
/// rules change, your program will need to be re-compiled.
///
/// In contrast, Jiff's default configuration on Unix is to read from
/// `/usr/share/zoneinfo` at runtime. This means your application will
/// automatically use time zone updates and doesn't need to be re-compiled.
///
/// Using a static `TimeZone` may also be faster in some cases. In particular,
/// a `TimeZone` created at runtime from a `/usr/share/zoneinfo` uses
/// automatic reference counting internally. In contrast, a `TimeZone` created
/// with this macro does not.
///
/// # Example
///
/// This example shows how to find the next DST transition from a particular
/// timestamp in `Europe/Zurich`, and then print that in local time for Zurich:
///
/// ```
/// use jiff::{tz::{self, TimeZone}, Timestamp};
///
/// static TZ: TimeZone = tz::get!("Europe/Zurich");
///
/// let ts: Timestamp = "2025-02-25T00:00Z".parse()?;
/// let Some(next_transition) = TZ.following(ts).next() else {
/// return Err("no time zone transitions".into());
/// };
/// let zdt = next_transition.timestamp().to_zoned(TZ.clone());
/// assert_eq!(zdt.to_string(), "2025-03-30T03:00:00+02:00[Europe/Zurich]");
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
///
/// [RFC 9636]: https://datatracker.ietf.org/doc/rfc9636/
/// [`jiff-tzdb`]: https://docs.rs/jiff-tzdb
#[cfg(feature = "static")]
pub use jiff_static::get;
/// Create a `TimeZone` value from TZif data in a file at compile time.
///
/// This reads the data in the file path given, parses it as TZif specified by
/// [RFC 9636], and constructs a `TimeZone` value for use in a `const` context.
/// This enables using IANA time zones with Jiff in core-only environments. No
/// dynamic memory allocation is used.
///
/// Unlike [`jiff::tz::get`](get), this reads TZif data from a file.
/// `jiff::tz::get`, in contrast, reads TZif data from the [`jiff-tzdb`] crate.
/// `jiff::tz::get` is more convenient and doesn't require managing your own
/// TZif files, but it comes at the cost of a compile-time dependency on
/// `jiff-tzdb` and being forced to use whatever data is in `jiff-tzdb`.
///
/// # Input
///
/// This macro takes two positional parameters that must be literal strings.
///
/// The first is required and is a path to a file containing TZif data. For
/// example, `/usr/share/zoneinfo/America/New_York`.
///
/// The second parameter is an IANA time zone identifier, e.g.,
/// `America/New_York`, and is required only when an IANA time zone identifier
/// could not be determined from the file path. The macro will automatically
/// infer an IANA time zone identifier as anything after the last occurrence
/// of the literal `zoneinfo/` in the file path.
///
/// # Return type
///
/// This macro returns a value with type `TimeZone`. To get a `&'static
/// TimeZone`, simply use `&include!("...")`.
///
/// # Usage
///
/// Callers should only call this macro once for each unique IANA time zone
/// identifier you need. Otherwise, multiple copies of the same embedded
/// time zone data could appear in your binary. There are no correctness
/// issues with this, but it could make your binary bigger than it needs to be.
///
/// # When should I use this?
///
/// Users should only use this macro if they have a _specific need_ for it
/// (like using a time zone on an embedded device). In particular, this will
/// embed the time zone transition rules into your binary. If the time zone
/// rules change, your program will need to be re-compiled.
///
/// In contrast, Jiff's default configuration on Unix is to read from
/// `/usr/share/zoneinfo` at runtime. This means your application will
/// automatically use time zone updates and doesn't need to be re-compiled.
///
/// Using a static `TimeZone` may also be faster in some cases. In particular,
/// a `TimeZone` created at runtime from a `/usr/share/zoneinfo` uses
/// automatic reference counting internally. In contrast, a `TimeZone` created
/// with this macro does not.
///
/// # Example
///
/// This example shows how to find the next DST transition from a particular
/// timestamp in `Europe/Zurich`, and then print that in local time for Zurich:
///
/// ```ignore
/// use jiff::{tz::{self, TimeZone}, Timestamp};
///
/// static TZ: TimeZone = tz::include!("/usr/share/zoneinfo/Europe/Zurich");
///
/// let ts: Timestamp = "2025-02-25T00:00Z".parse()?;
/// let Some(next_transition) = TZ.following(ts).next() else {
/// return Err("no time zone transitions".into());
/// };
/// let zdt = next_transition.timestamp().to_zoned(TZ.clone());
/// assert_eq!(zdt.to_string(), "2025-03-30T03:00:00+02:00[Europe/Zurich]");
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
///
/// # Example: using `/etc/localtime`
///
/// On most Unix systems, `/etc/localtime` is a symbolic link to a file in
/// your `/usr/share/zoneinfo` directory. This means it is a valid input to
/// this macro. However, Jiff currently does not detect the IANA time zone
/// identifier, so you'll need to provide it yourself:
///
/// ```ignore
/// use jiff::{tz::{self, TimeZone}, Timestamp};
///
/// static TZ: TimeZone = tz::include!("/etc/localtime", "America/New_York");
///
/// let ts: Timestamp = "2025-02-25T00:00Z".parse()?;
/// let zdt = ts.to_zoned(TZ.clone());
/// assert_eq!(zdt.to_string(), "2025-02-24T19:00:00-05:00[America/New_York]");
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
///
/// Note that this is reading `/etc/localtime` _at compile time_, which means
/// that the program will only use the time zone on the system in which it
/// was compiled. It will _not_ use the time zone of the system running it.
///
/// [RFC 9636]: https://datatracker.ietf.org/doc/rfc9636/
/// [`jiff-tzdb`]: https://docs.rs/jiff-tzdb
#[cfg(feature = "static-tz")]
pub use jiff_static::include;
/// Creates a new time zone offset in a `const` context from a given number
/// of hours.
///
/// Negative offsets correspond to time zones west of the prime meridian,
/// while positive offsets correspond to time zones east of the prime
/// meridian. Equivalently, in all cases, `civil-time - offset = UTC`.
///
/// The fallible non-const version of this constructor is
/// [`Offset::from_hours`].
///
/// This is a convenience free function for [`Offset::constant`]. It is
/// intended to provide a terse syntax for constructing `Offset` values from
/// a value that is known to be valid.
///
/// # Panics
///
/// This routine panics when the given number of hours is out of range.
/// Namely, `hours` must be in the range `-25..=25`.
///
/// Similarly, when used in a const context, an out of bounds hour will prevent
/// your Rust program from compiling.
///
/// # Example
///
/// ```
/// use jiff::tz::offset;
///
/// let o = offset(-5);
/// assert_eq!(o.seconds(), -18_000);
/// let o = offset(5);
/// assert_eq!(o.seconds(), 18_000);
/// ```
#[inline]
pub const fn offset(hours: i8) -> Offset {
Offset::constant(hours)
}
File diff suppressed because it is too large Load Diff
+101
View File
@@ -0,0 +1,101 @@
/*!
Provides a `Display` impl wrapper for POSIX time zones.
This lives here because none of the formatting machinery is available
in `jiff-core`. And while we could still write out a `Display` impl in
`jiff-core`, it doesn't seem worth that duplication.
*/
use jcore::tz::posix;
use crate::fmt;
/// A wrapper around jcore's posix time zone implementation.
///
/// This wrapper provides `Display`, `Debug` and `Format` impls using the
/// formatter in this crate. (jcore does not do formatting.)
pub(crate) struct TimeZoneFormatter<'a>(pub(crate) &'a posix::TimeZone);
impl<'a> core::fmt::Display for TimeZoneFormatter<'a> {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
fmt::temporal::DateTimePrinter::new()
.print_posix_time_zone(self.0, fmt::StdFmtWrite(f))
.map_err(|_| core::fmt::Error)
}
}
impl<'a> core::fmt::Debug for TimeZoneFormatter<'a> {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
core::fmt::Display::fmt(self, f)
}
}
#[cfg(feature = "defmt")]
impl<'a> defmt::Format for TimeZoneFormatter<'a> {
fn format(&self, f: defmt::Formatter) {
defmt::unwrap!(fmt::temporal::DateTimePrinter::new()
.print_posix_time_zone(self.0, fmt::DefmtWrite(f)))
}
}
// The tests below require parsing which requires alloc.
#[cfg(feature = "alloc")]
#[cfg(test)]
mod tests {
use super::*;
/// Assert that converting the TZ back to a string matches what we got. In
/// the original version of the POSIX TZ parser, we were very meticulous
/// about capturing the exact AST of the time zone. But I've since
/// simplified the data structure considerably such that it is lossy in
/// terms of what was actually parsed (but of course, not lossy in terms of
/// the semantic meaning of the time zone).
///
/// So to account for this, we serialize to a string and then parse it
/// back. We should get what we started with.
#[cfg(feature = "alloc")]
#[test]
fn roundtrips() {
fn assert_roundtrip(tz: &str) {
use alloc::string::{String, ToString};
use jcore::tz::posix;
fn to_string(tz: &posix::TimeZone) -> String {
TimeZoneFormatter(tz).to_string()
}
let tz = posix::TimeZone::parse(tz).unwrap();
let reparsed = posix::TimeZone::parse(to_string(&tz)).unwrap();
assert_eq!(tz, reparsed);
assert_eq!(to_string(&tz), to_string(&reparsed));
}
let cases = [
"WART4WARST,J1/-3,J365/20",
"WART4WARST,J1/-4,J365/21",
"EST5EDT,M3.2.0,M11.1.0",
"XXX-2<+01>-1,0/0,J365/23",
"EST24EDT,J1,J365",
"EST-24EDT,J1,J365",
"EST5EDT,J1,J365/5:12:34",
"EST+5EDT,M3.2.0/2,M11.1.0/2",
"EST+5EDT,M1.1.1,M12.5.2",
"EST5EDT,0/0,J365/25",
"XXX3EDT4,0/0,J365/23",
"XXX3EDT4,0/0,365",
"XXX3EDT4,J1/-167:59:59,J365/167:59:59",
"EST5EDT,J1,J365/5:12:34",
"EST+5EDT,M3.2.0/2,M11.1.0/2",
"EST+5EDT,J60,J365",
"EST+5EDT,59,365",
"EST+5EDT,M1.1.1,M12.5.2",
"EST5EDT,J1,J365/5:12:34",
"EST5EDT,J1/23:59:59,J365/24:00:00",
"EST5EDT,J1/-1,J365/167:00:00",
];
for tz in cases {
assert_roundtrip(tz);
}
}
}
+268
View File
@@ -0,0 +1,268 @@
use std::{
ffi::{c_char, c_void},
sync::OnceLock,
};
use alloc::vec::Vec;
use core::{ffi::CStr, mem, ptr::NonNull};
use crate::tz::{TimeZone, TimeZoneDatabase};
/// Attempts to find the default "system" time zone.
pub(super) fn get(db: &TimeZoneDatabase) -> Option<TimeZone> {
static PROPERTY_NAME: &str = "persist.sys.timezone\0";
static GETTER: OnceLock<Option<PropertyGetter>> = OnceLock::new();
let Some(getter) = GETTER.get_or_init(|| PropertyGetter::new()) else {
// We don't emit any messages here because `PropertyGetter::new()` will
// have already done so.
return None;
};
let tzname = getter.get(cstr(PROPERTY_NAME))?;
let Some(tzname) = core::str::from_utf8(&tzname).ok() else {
warn!(
"found `{PROPERTY_NAME}` name `{name}` on Android, \
but it's not valid UTF-8",
name = crate::util::escape::Bytes(&tzname),
);
return None;
};
let tz = match db.get(tzname) {
Ok(tz) => tz,
Err(_err) => {
warn!(
"found `{PROPERTY_NAME}` name `{tzname}` on Android, \
but could not find it in time zone database {db:?}",
);
return None;
}
};
debug!(
"found system time zone `{tzname}` from Android property \
`{PROPERTY_NAME}` and found entry for it in time zone \
database {db:?}",
);
Some(tz)
}
/// Given a path to a system default TZif file, return its corresponding
/// time zone.
///
/// This doesn't do any symlink shenanigans like in other Unix environments,
/// although we could consider doing that. I think probably this is very
/// unlikely to be used on Android, although it can be by setting `TZ`.
pub(super) fn read(_db: &TimeZoneDatabase, path: &str) -> Option<TimeZone> {
match super::read_unnamed_tzif_file(path) {
Ok(tz) => Some(tz),
Err(_err) => {
trace!("failed to read {path} as unnamed time zone: {_err}");
None
}
}
}
/// An abstraction for safely reading Android system properties.
///
/// Initialization of this should only be done once. Initialization
/// dynamically loads `libc`. But it does not read any properties. Instead,
/// we permit the time zone property to be looked up repeatedly, in case it
/// changes. So, our `libc` library handle remains invariant, but the values
/// of properties can change over the lifetime of the process.
///
/// This copies the technique used by the `android-system-properties` crate.
/// Namely, we use `dlopen` instead of hard-coding our linking requirements
/// since this is apparently more flexible. I guess in the past, the hard-coded
/// extern functions broke, but this technique doesn't. Or at least, it "fails
/// gracefully" since this won't result in build errors but just runtime
/// errors that result in no time zone being found (and thus will result in an
/// automatic fallback to UTC).
///
/// Our implementation of this idea is perhaps a bit simpler than what
/// `android-system-properties` does though. We don't bother supporting >10
/// year old versions of Android. (Although support for that could be added
/// if there was a real need.) Also, when this fails, we give better error
/// messages via `dlerror()` in the logs. UPDATE: We don't give better error
/// messages any more because `dlerror()` isn't sound to use for all possible
/// libc implementations in a multi-threaded environment.
struct PropertyGetter {
/// A `dlopen` handle to `libc.so`.
///
/// Note that since this is a bespoke property getter and we only ever
/// create a single instance in a process global static, this never gets
/// dropped. So we don't bother writing a `Drop` impl that calls `dlclose`.
/// Because it never gets dropped and because we load the symbols right
/// away at construction time, we never actually end up using it after
/// construction. We keep it around in case we want to refactor to this
/// to actually drop it for some reason.
_libc: NonNull<c_void>,
system_property_find: SystemPropertyFind,
system_property_read: SystemPropertyRead,
}
// SAFETY: It is presumably safe to call functions derived from `dlsym`
// symbols from multiple threads simultaneously. And it is presumably safe
// to call Android's property getter APIs from multiple threads simultaneously.
// This isn't technically documented (as far as I can see), but it would be
// crazytown if this weren't true.
unsafe impl Send for PropertyGetter {}
// SAFETY: It is presumably safe to call functions derived from `dlsym`
// symbols from multiple threads simultaneously. And it is presumably safe
// to call Android's property getter APIs from multiple threads simultaneously.
// This isn't technically documented (as far as I can see), but it would be
// crazytown if this weren't true.
unsafe impl Sync for PropertyGetter {}
impl PropertyGetter {
/// Creates a new property getter by `dlopen`'ing `libc.so`.
///
/// If this fails for whatever reason, `None` is returned and WARN-level
/// log messages are emitted stating the reason for failure if it is known.
fn new() -> Option<PropertyGetter> {
// SAFETY: OK because we provide a valid NUL terminated string.
let handle = unsafe { dlopen(cstr("libc.so\0").as_ptr(), 0) };
let Some(libc) = NonNull::new(handle) else {
warn!("could not open libc.so via `dlopen`:");
return None;
};
// SAFETY: Our `SystemPropertyFind` type definition matches what is
// declared in `include/sys/system_properties.h` on Android.
let system_property_find: SystemPropertyFind =
unsafe { load_symbol(libc, cstr("__system_property_find\0"))? };
// SAFETY: Our `SystemPropertyRead` type definition matches what is
// declared in `include/sys/system_properties.h` on Android.
let system_property_read: SystemPropertyRead = unsafe {
load_symbol(libc, cstr("__system_property_read_callback\0"))?
};
Some(PropertyGetter {
_libc: libc,
system_property_find,
system_property_read,
})
}
/// Reads the given property name into the `Vec<u8>` returned.
///
/// If the property doesn't exist, then `None` is returned and a WARN-level
/// log message is emitted explaining why.
fn get(&self, name: &CStr) -> Option<Vec<u8>> {
unsafe extern "C" fn callback(
buf: *mut c_void,
_name: *const c_char,
value: *const c_char,
_serial: u32,
) {
let buf = buf.cast::<Vec<u8>>();
// SAFETY: The implied contract of `__system_property_read_callback`
// is that `value` is a valid NUL terminated C string.
let value = unsafe { CStr::from_ptr(value) };
// SAFETY: We passed a valid `*mut Vec<u8>` to the callback, so
// casting it back to it is safe.
unsafe {
(*buf).extend_from_slice(value.to_bytes());
}
}
// SAFETY: `name` is a valid NUL terminated string and
// `system_property_find` is a valid function read from `dlsym`
// according to the declaration in `include/sys/system_properties.h`.
let prop_info = unsafe { (self.system_property_find)(name.as_ptr()) };
if prop_info.is_null() {
warn!(
"Android property name `{name}` not found",
name = crate::util::escape::Bytes(name.to_bytes()),
);
return None;
}
// N.B. A `prop_info` is an opaque pointer[1]... which means the
// implementation is probably allocating something in order to create
// it... right? But there's no API to free the pointer returned. And
// no other indication as to its lifetime. Once again, C is awful.
//
// [1]: https://android.googlesource.com/platform/bionic/+/master/libc/include/sys/system_properties.h#44
let mut buf = Vec::new();
// SAFETY: `name` is a valid NUL terminated string and
// `system_property_find` is a valid function read from `dlsym`
// according to the declaration in `include/sys/system_properties.h`.
unsafe {
let buf: *mut Vec<u8> = &mut buf;
(self.system_property_read)(
prop_info,
callback,
buf.cast::<c_void>(),
);
}
if buf.is_empty() {
warn!(
"reading Android property `{name}` resulted in empty value",
name = crate::util::escape::Bytes(name.to_bytes()),
);
return None;
}
Some(buf)
}
}
/// Loads a function symbol, of type `F`, from the given `dlopen` handle.
///
/// If this fails, then `handle` is closed and `None` is returned and a
/// WARN-level log message is emitted with an error message if possible.
///
/// # Safety
///
/// Callers must ensure that `F` is a function type that matches the ABI of
/// the `symbol` in `handle`.
unsafe fn load_symbol<F>(handle: NonNull<c_void>, symbol: &CStr) -> Option<F> {
let sym =
// SAFETY: We know `handle` is non-null.
unsafe { dlsym(handle.as_ptr(), symbol.as_ptr()) };
if sym.is_null() {
// SAFETY: We know `handle` is non-null.
let _ = unsafe { dlclose(handle.as_ptr()) };
warn!(
"could not load `{symbol}` symbol from `libc.so",
symbol = crate::util::escape::Bytes(symbol.to_bytes()),
);
return None;
}
// SAFETY: The safety obligation here is forwarded to the caller. They
// must guarantee that `F` is an appropriate type.
// declared in `include/sys/system_properties.h` on Android.
let function = unsafe { mem::transmute_copy::<*mut c_void, F>(&sym) };
Some(function)
}
/// Creates a C string "literal" and returns it as a raw pointer.
///
/// This panics if the string given contains a NUL byte.
fn cstr(string: &'static str) -> &'static CStr {
CStr::from_bytes_with_nul(string.as_bytes()).unwrap()
}
// We just define the FFI bindings ourselves instead of bringing in libc for
// this. We're only doing it for one platform, so it doesn't seem like a huge
// deal. But if this turns out to be a problem in practice, I'm fine accepting
// a target specific dependency on `libc` for Android.
extern "C" {
fn dlopen(filename: *const c_char, flag: i32) -> *mut c_void;
fn dlclose(handle: *mut c_void) -> i32;
fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void;
}
// These types come from:
// https://android.googlesource.com/platform/bionic/+/master/libc/include/sys/system_properties.h
type PropInfo = c_void;
type SystemPropertyFind =
unsafe extern "C" fn(*const c_char) -> *const PropInfo;
type SystemPropertyRead = unsafe extern "C" fn(
*const PropInfo,
SystemPropertyReadCallback,
*mut c_void,
);
type SystemPropertyReadCallback =
unsafe extern "C" fn(*mut c_void, *const c_char, *const c_char, u32);
+289
View File
@@ -0,0 +1,289 @@
use std::{sync::RwLock, time::Duration};
use alloc::string::ToString;
use jcore::tz::posix;
use crate::{
error::{tz::system::Error as E, Error, ErrorContext},
tz::{TimeZone, TimeZoneDatabase},
util::cache::Expiration,
};
#[cfg(all(unix, not(any(target_os = "android", target_os = "emscripten"))))]
#[path = "unix.rs"]
mod sys;
#[cfg(all(unix, target_os = "android"))]
#[path = "android.rs"]
mod sys;
#[cfg(windows)]
#[path = "windows/mod.rs"]
mod sys;
#[cfg(all(
feature = "js",
any(target_arch = "wasm32", target_arch = "wasm64"),
target_os = "unknown"
))]
#[path = "wasm_js.rs"]
mod sys;
#[cfg(all(target_family = "wasm", target_os = "emscripten"))]
#[path = "wasm_emscripten.rs"]
mod sys;
#[cfg(not(any(
unix,
windows,
all(
feature = "js",
any(target_arch = "wasm32", target_arch = "wasm64"),
target_os = "unknown"
)
)))]
mod sys {
use crate::tz::{TimeZone, TimeZoneDatabase};
pub(super) fn get(_db: &TimeZoneDatabase) -> Option<TimeZone> {
warn!("getting system time zone on this platform is unsupported");
None
}
pub(super) fn read(
_db: &TimeZoneDatabase,
path: &str,
) -> Option<TimeZone> {
match super::read_unnamed_tzif_file(path) {
Ok(tz) => Some(tz),
Err(_err) => {
debug!("failed to read {path} as unnamed time zone: {_err}");
None
}
}
}
}
/// The duration of time that a cached time zone should be considered valid.
static TTL: Duration = Duration::new(5 * 60, 0);
/// A cached time zone.
///
/// When there's a cached time zone that hasn't expired, then we return what's
/// in the cache. This is because determining the time zone can be mildly
/// expensive. For example, doing syscalls and potentially parsing TZif data.
///
/// We could use a `thread_local!` for this instead which may perhaps be
/// faster.
///
/// Note that our cache here is somewhat simplistic because we lean on the
/// fact that: 1) in the vast majority of cases, our platform specific code is
/// limited to finding a time zone name, and 2) looking up a time zone name in
/// `TimeZoneDatabase` has its own cache. The main cases this doesn't really
/// cover are when we can't find a time zone name. In which case, we might be
/// re-parsing POSIX TZ strings or TZif data unnecessarily. But it's not clear
/// this matters much. It might matter more if we shrink our TTL though.
static CACHE: RwLock<Cache> = RwLock::new(Cache::empty());
/// A simple global mutable cache of the most recently created system
/// `TimeZone`.
///
/// This gets clearer periodically where subsequent calls to `get` must
/// re-create the time zone. This is likely wasted work in the vast majority
/// of cases, but the TTL should ensure it doesn't happen too often.
///
/// Of course, in those cases where you want it to happen faster, we provide
/// a way to reset this cache and force a re-creation of the time zone.
struct Cache {
tz: Option<TimeZone>,
expiration: Expiration,
}
impl Cache {
/// Create an empty cache. The default state.
const fn empty() -> Cache {
Cache { tz: None, expiration: Expiration::expired() }
}
}
/// Retrieve the "system" time zone.
///
/// If there is a cached time zone that isn't stale, then that is returned
/// instead.
///
/// If there is no cached time zone, then this tries to determine the system
/// time zone in a platform specific manner. This may involve reading files
/// or making system calls. If that fails then an error is returned.
///
/// Note that the `TimeZone` returned may not have an IANA name! In some cases,
/// it is just impractical to determine the time zone name. For example, when
/// `/etc/localtime` is a hard link to a TZif file instead of a symlink and
/// when the time zone name isn't recorded in any of the other obvious places.
pub(crate) fn get(db: &TimeZoneDatabase) -> Result<TimeZone, Error> {
{
let cache = CACHE.read().unwrap();
if let Some(ref tz) = cache.tz {
if !cache.expiration.is_expired() {
return Ok(tz.clone());
}
}
}
let tz = get_force(db)?;
{
// It's okay that we race here. We basically assume that any
// sufficiently close but approximately simultaneous detection of
// "system" time will lead to the same result. Of course, this is not
// strictly true, but since we invalidate the cache after a TTL, it
// will eventually be true in any sane environment.
let mut cache = CACHE.write().unwrap();
cache.tz = Some(tz.clone());
cache.expiration = Expiration::after(TTL);
}
Ok(tz)
}
/// Always attempt retrieve the system time zone. This never uses a cache.
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:?}");
return Ok(tz);
}
Ok(None) => {
debug!("`TZ` environment variable is not set");
}
Err(err) => {
return Err(err.context(E::FailedEnvTz));
}
}
if let Some(tz) = sys::get(db) {
return Ok(tz);
}
Err(Error::from(E::FailedSystemTimeZone))
}
/// Materializes a `TimeZone` from a `TZ` environment variable.
///
/// Basically, `TZ` is usually just an IANA Time Zone Database name like
/// `TZ=America/New_York` or `TZ=UTC`. But it can also be a POSIX time zone
/// transition string like `TZ=EST5EDTM3.2.0,M11.1.0` or it can be a file path
/// (absolute or relative) to a TZif file.
///
/// We try very hard to extract a time zone name from `TZ` and use that to look
/// it up via `TimeZoneDatabase`. But we will fall back to unnamed TZif
/// `TimeZone` if necessary.
///
/// This routine only returns `Ok(None)` when `TZ` is not set. If it is set
/// but a `TimeZone` could not be extracted, then an error is returned.
fn get_env_tz(db: &TimeZoneDatabase) -> Result<Option<TimeZone>, Error> {
// This routine is pretty Unix-y, but there's no reason it can't
// partially work on Windows. For example, setting TZ=America/New_York
// should work totally fine on Windows. I don't see a good reason not to
// support it anyway.
let Some(tzenv) = std::env::var_os("TZ") else { return Ok(None) };
// It is commonly agreed (across GNU and BSD tooling at least), but
// not standard, that setting an empty `TZ=` is indistinguishable from
// `TZ=UTC`.
if tzenv.is_empty() {
debug!(
"`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));
}
let tz_name_or_path = match posix::TzEnv::parse_os_str(&tzenv) {
Err(_err) => {
debug!(
"failed to parse {tzenv:?} as POSIX TZ rule \
(attempting to treat it as an IANA time zone): {_err}",
);
tzenv.to_str().ok_or(E::FailedPosixTzAndUtf8)?.to_string()
}
Ok(posix::TzEnv::Implementation(string)) => string.to_string(),
Ok(posix::TzEnv::Rule(tz)) => {
return Ok(Some(TimeZone::from_posix_tz(tz)))
}
};
// At this point, TZ is set to something that is definitively not a
// POSIX TZ transition string. Some possible values at this point are:
//
// TZ=America/New_York
// TZ=:America/New_York
// TZ=EST5EDT
// TZ=:EST5EDT
// TZ=/usr/share/zoneinfo/America/New_York
// TZ=:/usr/share/zoneinfo/America/New_York
// TZ=../zoneinfo/America/New_York
// TZ=:../zoneinfo/America/New_York
//
// `zoneinfo` is the common thread here. So we look for that first. If we
// can't find it, then we assume the entire string is a time zone name
// that we can look up in the system zoneinfo database.
let needle = "zoneinfo/";
let Some(rpos) = tz_name_or_path.rfind(needle) else {
// 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:?}`",
);
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:?}` \
(continuing to try and read `{tz_name_or_path}` as \
a TZif file)",
);
sys::read(db, &tz_name_or_path)
.ok_or_else(|| Error::from(E::FailedEnvTzAsTzif))
.map(Some)
}
};
};
// We now try to be a little cute here and extract the IANA time zone name
// from what we now believe is a file path by taking everything after
// `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}` \
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}`)",
);
}
}
// At this point, we have tried our hardest but we just cannot seem to
// extract an IANA time zone name out of the `TZ` environment variable.
// The only thing left for us to do is treat the value as a file path
// and read the data as TZif. This will give us time zone data if it works,
// but without a name.
sys::read(db, &tz_name_or_path)
.ok_or_else(|| Error::from(E::FailedEnvTzAsTzif))
.map(Some)
}
/// Returns the given file path as TZif data without a time zone name.
///
/// Normally we require TZif time zones to have a name associated with it.
/// But because there are likely platforms that hardlink /etc/localtime and
/// perhaps have no other way to get a time zone name, we choose to support
/// that use case. Although I cannot actually name such a platform...
fn read_unnamed_tzif_file(path: &str) -> Result<TimeZone, Error> {
let data = std::fs::read(path)
.map_err(Error::io)
.context(E::FailedUnnamedTzifRead)?;
let tz =
TimeZone::tzif_system(&data).context(E::FailedUnnamedTzifInvalid)?;
Ok(tz)
}
+117
View File
@@ -0,0 +1,117 @@
use crate::tz::{TimeZone, TimeZoneDatabase};
static UNIX_LOCALTIME_PATH: &str = "/etc/localtime";
/// Attempts to find the default "system" time zone.
///
/// In the happy path, this looks at `/etc/localtime`, assumes it is a
/// symlink to something in `/usr/share/zoneinfo` and extracts out the IANA
/// time zone name. From there, it looks up that time zone name in the given
/// time zone database.
///
/// When `/etc/localtime` isn't a symlink, or if the time zone couldn't be
/// found in the given time zone database, then `/etc/localtime` is read
/// directly as a TZif data file describing a time zone. The reason why we
/// try to avoid this is because a TZif data file does not contain the time
/// zone name. And we *really* want the time zone name as it is the only
/// standardized way to roundtrip a datetime in a particular time zone.
pub(super) fn get(db: &TimeZoneDatabase) -> Option<TimeZone> {
read(db, UNIX_LOCALTIME_PATH)
}
/// Given a path to a system default TZif file, return its corresponding
/// time zone.
///
/// In Unix, we attempt to read it as a symlink and extract an IANA time zone
/// identifier. If that ID exists in the tzdb, we return that. Otherwise, we
/// read the TZif file as an unnamed time zone.
pub(super) fn read(db: &TimeZoneDatabase, path: &str) -> Option<TimeZone> {
if let Some(tz) = read_link_to_zoneinfo(db, path) {
return Some(tz);
}
trace!(
"failed to find time zone name using Unix-specific heuristics, \
attempting to read {UNIX_LOCALTIME_PATH} as unnamed time zone",
);
match super::read_unnamed_tzif_file(path) {
Ok(tz) => Some(tz),
Err(_err) => {
trace!("failed to read {path} as unnamed time zone: {_err}");
None
}
}
}
/// Attempt to determine the time zone name from the symlink path given.
///
/// If the path isn't a symlink or if its target wasn't recognized as
/// pointing into a known zoneinfo database, then this returns `None` (and
/// emits some log messages).
fn read_link_to_zoneinfo(
db: &TimeZoneDatabase,
path: &str,
) -> Option<TimeZone> {
let target = match std::fs::read_link(path) {
Ok(target) => target,
Err(_err) => {
trace!("failed to read {path} as symbolic link: {_err}");
return None;
}
};
let Some(target) = target.to_str() else {
trace!("symlink target {target:?} for {path:?} is not valid UTF-8");
return None;
};
let needle = "zoneinfo/";
let Some(rpos) = target.rfind(needle) else {
trace!(
"could not find {needle:?} in symlink target {target:?} \
for path {path:?}, so could not determine time zone name \
from symlink",
);
return None;
};
let name = &target[rpos + needle.len()..];
trace!(
"extracted {name:?} from symlink target {target:?} \
for path {path:?} and assuming it is an IANA time zone name",
);
let tz = match db.get(&name) {
Ok(tz) => tz,
Err(_err) => {
trace!(
"using {name:?} symlink target {target:?} \
for path {path:?} as time zone name, \
but failed to find time zone with that name in \
zoneinfo database {db:?}",
);
return None;
}
};
Some(tz)
}
#[cfg(not(miri))]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn get_time_zone_name_etc_localtime() {
let _ = crate::logging::Logger::init();
let db = crate::tz::db();
if crate::tz::db().is_definitively_empty() {
return;
}
let path = std::path::Path::new("/etc/localtime");
if !path.exists() {
return;
}
// It's hard to assert much other than that a time zone could be
// successfully constructed. Presumably this may fail in certain
// environments, but hopefully the `is_definitively_empty` check above
// will filter most out.
assert!(get(db).is_some());
}
}
@@ -0,0 +1,87 @@
use core::ffi::{c_char, CStr};
use core::ptr::NonNull;
use crate::{
tz::{TimeZone, TimeZoneDatabase},
util::constant::unwrapr,
};
extern "C" {
/// <https://emscripten.org/docs/api_reference/emscripten.h.html#c.emscripten_run_script_string>
fn emscripten_run_script_string(script: *const c_char) -> *mut c_char;
}
pub(super) fn get(db: &TimeZoneDatabase) -> Option<TimeZone> {
const SCRIPT: &CStr = unwrapr!(
CStr::from_bytes_with_nul(
"Intl.DateTimeFormat().resolvedOptions().timeZone\0".as_bytes(),
),
"not a valid C string literal"
);
// SAFETY: SCRIPT is a valid pointer to a null-terminated string. The
// result will be a pointer to a null-terminated C string, or null if the
// script failed to run. The returned value is valid until the next call
// to `emscripten_run_script_string` in the same thread, so it is valid
// for the duration of this function. This likely relies on emscripten
// managing its string lifetimes carefully, possibly through TLS or some
// other shenanigans. Either way, this usage seems clearly okay even in a
// multi-threaded environment according to emscripten docs:
//
// > This function can be called from a pthread, and it is executed
// > in the scope of the Web Worker that is hosting the pthread. To
// > evaluate a function in the scope of the main runtime thread,
// > see the function emscripten_sync_run_in_main_runtime_thread().
//
// From: <https://emscripten.org/docs/api_reference/emscripten.h.html#c.emscripten_run_script_string>
let script_result =
unsafe { emscripten_run_script_string(SCRIPT.as_ptr()) };
let script_ptr = match NonNull::new(script_result) {
Some(script_cstr) => script_cstr,
None => {
trace!(
"executing JavaScript to discover IANA time zone \
identifier returned a null pointer",
);
return None;
}
};
// SAFETY: OK because of the contract of `emscripten_run_script_string`.
// We assume that when it returns a non-null pointer (which is checked
// above) that it is a valid C string.
let script_cstr = unsafe { CStr::from_ptr(script_ptr.as_ptr()) };
let name = match script_cstr.to_str().ok() {
Some(name) => name,
None => {
trace!(
"executing JavaScript to discover IANA time zone \
identifier returned invalid UTF-8: {script_cstr}",
script_cstr =
crate::util::escape::Bytes(script_cstr.to_bytes()),
);
return None;
}
};
let tz = match db.get(name) {
Ok(tz) => tz,
Err(_err) => {
trace!(
"got {name:?} as time zone name, \
but failed to find time zone with that name in \
zoneinfo database {db:?}: {_err}",
);
return None;
}
};
Some(tz)
}
pub(super) fn read(_db: &TimeZoneDatabase, path: &str) -> Option<TimeZone> {
match super::read_unnamed_tzif_file(path) {
Ok(tz) => Some(tz),
Err(_err) => {
trace!("failed to read {path} as unnamed time zone: {_err}");
None
}
}
}
+57
View File
@@ -0,0 +1,57 @@
use alloc::string::String;
use crate::tz::{TimeZone, TimeZoneDatabase};
pub(super) fn get(db: &TimeZoneDatabase) -> Option<TimeZone> {
let fmt = js_sys::Intl::DateTimeFormat::new(
&js_sys::Array::new(),
&js_sys::Object::new(),
);
let options = fmt.resolved_options();
// Documented to be an IANA tz ID:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/resolvedOptions#timezone
let key = wasm_bindgen::JsValue::from("timeZone");
let val = match js_sys::Reflect::get(&options, &key) {
Ok(val) => val,
Err(_err) => {
trace!(
"failed to get `timeZone` key on \
Intl.DateTimeFormat options: {_err:?}"
);
return None;
}
};
trace!("got `timeZone` value from Intl.DateTimeFormat options: {val:?}");
let name = match String::try_from(val) {
Ok(string) => string,
Err(_err) => {
trace!(
"failed to convert `timeZone` on \
Intl.DateTimeFormat to string"
);
return None;
}
};
let tz = match db.get(&name) {
Ok(tz) => tz,
Err(_err) => {
trace!(
"got {name:?} as time zone name, \
but failed to find time zone with that name in \
zoneinfo database {db:?}: {_err}",
);
return None;
}
};
Some(tz)
}
pub(super) fn read(_db: &TimeZoneDatabase, path: &str) -> Option<TimeZone> {
match super::read_unnamed_tzif_file(path) {
Ok(tz) => Some(tz),
Err(_err) => {
trace!("failed to read {path} as unnamed time zone: {_err}");
None
}
}
}
+40
View File
@@ -0,0 +1,40 @@
pub const TIME_ZONE_ID_INVALID: u32 = 0xffffffff;
// MSRV(??): Must be marked as public to avoid errors in older Rust compilers.
// Rust 1.71 needs this. But Rust 1.95 does not. I'm not sure where the cross
// over point is, but it'd be nice to tighten this up a bit. In any case, this
// isn't actually re-exported anywhere in Jiff's public API, so it's fine.
// ---AG
#[repr(C)]
pub struct DYNAMIC_TIME_ZONE_INFORMATION {
bias: i32,
standard_name: [u16; 32],
standard_date: SYSTEMTIME,
standard_bias: i32,
daylight_name: [u16; 32],
daylight_date: SYSTEMTIME,
daylight_bias: i32,
// This is the only field we actually need.
pub timezone_key_name: [u16; 128],
dynamic_daylight_time_disabled: u8,
}
#[repr(C)]
struct SYSTEMTIME {
year: u16,
month: u16,
day_of_week: u16,
day: u16,
hour: u16,
minute: u16,
seconds: u16,
milliseconds: u16,
}
/// Retrieves the current time zone and dynamic daylight saving time settings.
///
/// These settings control the translations between Coordinated Universal Time
/// (UTC) and local time..
///
/// See: <https://learn.microsoft.com/en-us/windows/win32/api/timezoneapi/nf-timezoneapi-getdynamictimezoneinformation>
windows_link::link!("kernel32.dll" "system" fn GetDynamicTimeZoneInformation(info: *mut DYNAMIC_TIME_ZONE_INFORMATION) -> u32);
+146
View File
@@ -0,0 +1,146 @@
use core::mem::MaybeUninit;
use alloc::string::String;
use crate::{
error::{tz::system::Error as E, Error, ErrorContext},
tz::{TimeZone, TimeZoneDatabase},
util::utf8,
};
use self::windows_zones::WINDOWS_TO_IANA;
#[allow(dead_code)] // we don't currently read the version
mod windows_zones;
mod ffi;
use ffi::{
GetDynamicTimeZoneInformation, DYNAMIC_TIME_ZONE_INFORMATION,
TIME_ZONE_ID_INVALID,
};
/// Attempts to find the default "system" time zone.
///
/// This works by querying `GetDynamicTimeZoneInformation` via the Windows
/// API, and mapping the time zone key name returned to an IANA time zone
/// name via the [CLDR XML data].
///
/// If the API call fails or a valid mapping could not be found, then `None`
/// is returned and some log messages are emitted.
///
/// Windows does provide a [WinRT GetTimeZone] call that will return the IANA
/// time zone name directly, but it looks like a mess to use WinRT from Rust
/// currently. And this approach enjoys wider platform support.
///
/// [CLDR XML data]: https://github.com/unicode-org/cldr/raw/main/common/supplemental/windowsZones.xml
/// [WinRT GetTimeZone]: https://learn.microsoft.com/en-us/uwp/api/windows.globalization.calendar.gettimezone?view=winrt-22621
pub(super) fn get(db: &TimeZoneDatabase) -> Option<TimeZone> {
let tz_key_name = match get_tz_key_name() {
Ok(tz_key_name) => tz_key_name,
Err(_err) => {
warn!(
"failed to discover current time zone via \
winapi GetDynamicTimeZoneInformation: {_err}",
);
return None;
}
};
let iana_name = match windows_to_iana(&tz_key_name) {
Ok(iana_name) => iana_name,
Err(_err) => {
warn!("could not find IANA time zone name: {_err}");
return None;
}
};
let tz = match db.get(iana_name) {
Ok(tz) => tz,
Err(_err) => {
warn!(
"could not find mapped IANA time zone {iana_name} \
in zoneinfo database {db:?}: {_err}",
);
return None;
}
};
Some(tz)
}
pub(super) fn read(_db: &TimeZoneDatabase, path: &str) -> Option<TimeZone> {
match super::read_unnamed_tzif_file(path) {
Ok(tz) => Some(tz),
Err(_err) => {
trace!("failed to read {path} as unnamed time zone: {_err}");
None
}
}
}
fn windows_to_iana(tz_key_name: &str) -> Result<&'static str, Error> {
let result = WINDOWS_TO_IANA.binary_search_by(|(win_name, _)| {
utf8::cmp_ignore_ascii_case(win_name, &tz_key_name)
});
let Ok(index) = result else {
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}`",
);
Ok(iana_name)
}
fn get_tz_key_name() -> Result<String, Error> {
let mut info: MaybeUninit<DYNAMIC_TIME_ZONE_INFORMATION> =
MaybeUninit::uninit();
// SAFETY: We pass a pointer to the expected input and signal it as
// unitializaed to rustc via MaybeUninit.
let rc = unsafe { GetDynamicTimeZoneInformation(info.as_mut_ptr()) };
if rc == TIME_ZONE_ID_INVALID {
return Err(Error::io(std::io::Error::last_os_error()));
}
// SAFETY: Windows API docs indicate that the pointer is correctly written
// to unless it fails, and we check for failure above. So we're only here
// when `info` is properly initialized.
let info = unsafe { info.assume_init() };
let tz_key_name = nul_terminated_utf16_to_string(&info.timezone_key_name)
.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(E::WindowsUtf16DecodeNul)?;
let string = String::from_utf16(&code_units[..nul])
.map_err(|_| E::WindowsUtf16DecodeInvalid)?;
Ok(string)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn get_time_zone_name_windows_dynamic_time_zone() {
let _ = crate::logging::Logger::init();
let db = crate::tz::db();
if crate::tz::db().is_definitively_empty() {
return;
}
let path = std::path::Path::new("/etc/localtime");
if !path.exists() {
return;
}
// It's hard to assert much other than that a time zone could be
// successfully constructed. Presumably this may fail in certain
// environments, but hopefully the `is_definitively_empty` check above
// will filter most out.
assert!(get(db).is_some());
}
}
@@ -0,0 +1,143 @@
pub(super) static VERSION: &str = r"2021a";
pub(super) static WINDOWS_TO_IANA: &[(&str, &str)] = &[
(r"Afghanistan Standard Time", r"Asia/Kabul"),
(r"Alaskan Standard Time", r"America/Anchorage"),
(r"Aleutian Standard Time", r"America/Adak"),
(r"Altai Standard Time", r"Asia/Barnaul"),
(r"Arab Standard Time", r"Asia/Riyadh"),
(r"Arabian Standard Time", r"Asia/Dubai"),
(r"Arabic Standard Time", r"Asia/Baghdad"),
(r"Argentina Standard Time", r"America/Buenos_Aires"),
(r"Astrakhan Standard Time", r"Europe/Astrakhan"),
(r"Atlantic Standard Time", r"America/Halifax"),
(r"AUS Central Standard Time", r"Australia/Darwin"),
(r"Aus Central W. Standard Time", r"Australia/Eucla"),
(r"AUS Eastern Standard Time", r"Australia/Sydney"),
(r"Azerbaijan Standard Time", r"Asia/Baku"),
(r"Azores Standard Time", r"Atlantic/Azores"),
(r"Bahia Standard Time", r"America/Bahia"),
(r"Bangladesh Standard Time", r"Asia/Dhaka"),
(r"Belarus Standard Time", r"Europe/Minsk"),
(r"Bougainville Standard Time", r"Pacific/Bougainville"),
(r"Canada Central Standard Time", r"America/Regina"),
(r"Cape Verde Standard Time", r"Atlantic/Cape_Verde"),
(r"Caucasus Standard Time", r"Asia/Yerevan"),
(r"Cen. Australia Standard Time", r"Australia/Adelaide"),
(r"Central America Standard Time", r"America/Guatemala"),
(r"Central Asia Standard Time", r"Asia/Bishkek"),
(r"Central Brazilian Standard Time", r"America/Cuiaba"),
(r"Central Europe Standard Time", r"Europe/Budapest"),
(r"Central European Standard Time", r"Europe/Warsaw"),
(r"Central Pacific Standard Time", r"Pacific/Guadalcanal"),
(r"Central Standard Time", r"America/Chicago"),
(r"Central Standard Time (Mexico)", r"America/Mexico_City"),
(r"Chatham Islands Standard Time", r"Pacific/Chatham"),
(r"China Standard Time", r"Asia/Shanghai"),
(r"Cuba Standard Time", r"America/Havana"),
(r"Dateline Standard Time", r"Etc/GMT+12"),
(r"E. Africa Standard Time", r"Africa/Nairobi"),
(r"E. Australia Standard Time", r"Australia/Brisbane"),
(r"E. Europe Standard Time", r"Europe/Chisinau"),
(r"E. South America Standard Time", r"America/Sao_Paulo"),
(r"Easter Island Standard Time", r"Pacific/Easter"),
(r"Eastern Standard Time", r"America/New_York"),
(r"Eastern Standard Time (Mexico)", r"America/Cancun"),
(r"Egypt Standard Time", r"Africa/Cairo"),
(r"Ekaterinburg Standard Time", r"Asia/Yekaterinburg"),
(r"Fiji Standard Time", r"Pacific/Fiji"),
(r"FLE Standard Time", r"Europe/Kiev"),
(r"Georgian Standard Time", r"Asia/Tbilisi"),
(r"GMT Standard Time", r"Europe/London"),
(r"Greenland Standard Time", r"America/Godthab"),
(r"Greenwich Standard Time", r"Atlantic/Reykjavik"),
(r"GTB Standard Time", r"Europe/Bucharest"),
(r"Haiti Standard Time", r"America/Port-au-Prince"),
(r"Hawaiian Standard Time", r"Pacific/Honolulu"),
(r"India Standard Time", r"Asia/Calcutta"),
(r"Iran Standard Time", r"Asia/Tehran"),
(r"Israel Standard Time", r"Asia/Jerusalem"),
(r"Jordan Standard Time", r"Asia/Amman"),
(r"Kaliningrad Standard Time", r"Europe/Kaliningrad"),
(r"Korea Standard Time", r"Asia/Seoul"),
(r"Libya Standard Time", r"Africa/Tripoli"),
(r"Line Islands Standard Time", r"Pacific/Kiritimati"),
(r"Lord Howe Standard Time", r"Australia/Lord_Howe"),
(r"Magadan Standard Time", r"Asia/Magadan"),
(r"Magallanes Standard Time", r"America/Punta_Arenas"),
(r"Marquesas Standard Time", r"Pacific/Marquesas"),
(r"Mauritius Standard Time", r"Indian/Mauritius"),
(r"Middle East Standard Time", r"Asia/Beirut"),
(r"Montevideo Standard Time", r"America/Montevideo"),
(r"Morocco Standard Time", r"Africa/Casablanca"),
(r"Mountain Standard Time", r"America/Denver"),
(r"Mountain Standard Time (Mexico)", r"America/Mazatlan"),
(r"Myanmar Standard Time", r"Asia/Rangoon"),
(r"N. Central Asia Standard Time", r"Asia/Novosibirsk"),
(r"Namibia Standard Time", r"Africa/Windhoek"),
(r"Nepal Standard Time", r"Asia/Katmandu"),
(r"New Zealand Standard Time", r"Pacific/Auckland"),
(r"Newfoundland Standard Time", r"America/St_Johns"),
(r"Norfolk Standard Time", r"Pacific/Norfolk"),
(r"North Asia East Standard Time", r"Asia/Irkutsk"),
(r"North Asia Standard Time", r"Asia/Krasnoyarsk"),
(r"North Korea Standard Time", r"Asia/Pyongyang"),
(r"Omsk Standard Time", r"Asia/Omsk"),
(r"Pacific SA Standard Time", r"America/Santiago"),
(r"Pacific Standard Time", r"America/Los_Angeles"),
(r"Pacific Standard Time (Mexico)", r"America/Tijuana"),
(r"Pakistan Standard Time", r"Asia/Karachi"),
(r"Paraguay Standard Time", r"America/Asuncion"),
(r"Qyzylorda Standard Time", r"Asia/Qyzylorda"),
(r"Romance Standard Time", r"Europe/Paris"),
(r"Russia Time Zone 10", r"Asia/Srednekolymsk"),
(r"Russia Time Zone 11", r"Asia/Kamchatka"),
(r"Russia Time Zone 3", r"Europe/Samara"),
(r"Russian Standard Time", r"Europe/Moscow"),
(r"SA Eastern Standard Time", r"America/Cayenne"),
(r"SA Pacific Standard Time", r"America/Bogota"),
(r"SA Western Standard Time", r"America/La_Paz"),
(r"Saint Pierre Standard Time", r"America/Miquelon"),
(r"Sakhalin Standard Time", r"Asia/Sakhalin"),
(r"Samoa Standard Time", r"Pacific/Apia"),
(r"Sao Tome Standard Time", r"Africa/Sao_Tome"),
(r"Saratov Standard Time", r"Europe/Saratov"),
(r"SE Asia Standard Time", r"Asia/Bangkok"),
(r"Singapore Standard Time", r"Asia/Singapore"),
(r"South Africa Standard Time", r"Africa/Johannesburg"),
(r"South Sudan Standard Time", r"Africa/Juba"),
(r"Sri Lanka Standard Time", r"Asia/Colombo"),
(r"Sudan Standard Time", r"Africa/Khartoum"),
(r"Syria Standard Time", r"Asia/Damascus"),
(r"Taipei Standard Time", r"Asia/Taipei"),
(r"Tasmania Standard Time", r"Australia/Hobart"),
(r"Tocantins Standard Time", r"America/Araguaina"),
(r"Tokyo Standard Time", r"Asia/Tokyo"),
(r"Tomsk Standard Time", r"Asia/Tomsk"),
(r"Tonga Standard Time", r"Pacific/Tongatapu"),
(r"Transbaikal Standard Time", r"Asia/Chita"),
(r"Turkey Standard Time", r"Europe/Istanbul"),
(r"Turks And Caicos Standard Time", r"America/Grand_Turk"),
(r"Ulaanbaatar Standard Time", r"Asia/Ulaanbaatar"),
(r"US Eastern Standard Time", r"America/Indianapolis"),
(r"US Mountain Standard Time", r"America/Phoenix"),
(r"UTC", r"Etc/UTC"),
(r"UTC+12", r"Etc/GMT-12"),
(r"UTC+13", r"Etc/GMT-13"),
(r"UTC-02", r"Etc/GMT+2"),
(r"UTC-08", r"Etc/GMT+8"),
(r"UTC-09", r"Etc/GMT+9"),
(r"UTC-11", r"Etc/GMT+11"),
(r"Venezuela Standard Time", r"America/Caracas"),
(r"Vladivostok Standard Time", r"Asia/Vladivostok"),
(r"Volgograd Standard Time", r"Europe/Volgograd"),
(r"W. Australia Standard Time", r"Australia/Perth"),
(r"W. Central Africa Standard Time", r"Africa/Lagos"),
(r"W. Europe Standard Time", r"Europe/Berlin"),
(r"W. Mongolia Standard Time", r"Asia/Hovd"),
(r"West Asia Standard Time", r"Asia/Tashkent"),
(r"West Bank Standard Time", r"Asia/Hebron"),
(r"West Pacific Standard Time", r"Pacific/Port_Moresby"),
(r"Yakutsk Standard Time", r"Asia/Yakutsk"),
(r"Yukon Standard Time", r"America/Whitehorse"),
];
+132
View File
@@ -0,0 +1,132 @@
#[cfg(not(miri))]
use jcore::tz::tzif;
/// A concatenated list of TZif data with a header and an index block.
///
/// This was exactracted from an Android emulator file system via `adb`.
pub(crate) static ANDROID_CONCATENATED_TZIF: &'static [u8] =
include_bytes!("testdata/android/tzdata");
/// A list of all TZif files in our testdata directory.
///
/// Feel free to add more if there are other "interesting" cases. Note that
/// some tests iterate over this slice to check each entry. So adding a new
/// test file here might require updating some tests.
///
/// # Revisions
///
/// * 2024-03-27: Initial set pulled from my local copy of `tzdata 2024a`.
/// * 2024-07-05: Added `UTC`.
/// * 2024-11-30: Added special Sydney time zone from RHEL8.
pub(crate) static TZIF_TEST_FILES: &[TzifTestFile] = &[
TzifTestFile {
name: "America/New_York",
data: include_bytes!("testdata/america-new-york.tzif"),
},
TzifTestFile {
name: "America/Sitka",
data: include_bytes!("testdata/america-sitka.tzif"),
},
TzifTestFile {
name: "America/St_Johns",
data: include_bytes!("testdata/america-st-johns.tzif"),
},
TzifTestFile {
name: "right/America/New_York",
data: include_bytes!("testdata/right-america-new-york.tzif"),
},
TzifTestFile {
name: "Antarctica/Troll",
data: include_bytes!("testdata/antarctica-troll.tzif"),
},
TzifTestFile {
name: "Australia/Tasmania",
data: include_bytes!("testdata/australia-tasmania.tzif"),
},
TzifTestFile {
name: "Europe/Dublin",
data: include_bytes!("testdata/europe-dublin.tzif"),
},
TzifTestFile {
name: "Pacific/Honolulu",
data: include_bytes!("testdata/pacific-honolulu.tzif"),
},
// This TZif file comes from a RHEL8 installation[1]. Apparently RHEL
// added a special first transition far back in the past. It is probably
// similar in purpose to what Jiff does internally, which is to add a
// dummy "minimum" transition. We test this case here because RHEL's dummy
// transition falls outside of Jiff's supported `Timestamp` range, which
// did result in an error at parse time. But we should be able to parse it,
// so we test it here.
//
// [1]: https://github.com/BurntSushi/jiff/issues/163
TzifTestFile {
name: "Australia/Sydney/RHEL8",
data: include_bytes!("testdata/australia-sydney-rhel8.tzif"),
},
// I added this to test finding previous time zone transitions in a time
// zone that had somewhat recently eliminated DST. The bug was that Jiff
// wasn't reporting *any* previous time zone transitions.
TzifTestFile {
name: "America/Sao_Paulo",
data: include_bytes!("testdata/america-sao-paulo.tzif"),
},
// Another test file I added for a region that eliminated DST and thus
// has a "final" time zone transition.
TzifTestFile {
name: "America/Boa_Vista",
data: include_bytes!("testdata/america-boa-vista.tzif"),
},
// Just another random test file.
TzifTestFile {
name: "Asia/Tokyo",
data: include_bytes!("testdata/asia-tokyo.tzif"),
},
TzifTestFile { name: "UTC", data: include_bytes!("testdata/utc.tzif") },
];
/// A single TZif datum.
///
/// It contains the name of the time zone and the raw bytes of the
/// corresponding TZif file.
#[derive(Clone, Copy)]
pub(crate) struct TzifTestFile {
pub(crate) name: &'static str,
pub(crate) data: &'static [u8],
}
impl TzifTestFile {
/// Look up the TZif test file for the given time zone name.
///
/// If one doesn't exist, then this panics and fails the current
/// test.
pub(crate) fn get(name: &str) -> TzifTestFile {
for &tzif_file in TZIF_TEST_FILES {
if tzif_file.name == name {
return tzif_file;
}
}
panic!("could not find TZif test file for {name:?}")
}
/// Parse this test TZif data into a structured representation.
#[cfg(not(miri))]
pub(crate) fn parse(self) -> tzif::TimeZone {
tzif::TimeZone::parse(self.data).unwrap_or_else(|err| {
panic!("failed to parse TZif test file for {:?}: {err}", self.name)
})
}
/// Parse this test TZif data as if it were V1.
#[cfg(not(miri))]
pub(crate) fn parse_v1(self) -> tzif::TimeZone {
let mut data = self.data.to_vec();
data[4] = 0;
tzif::TimeZone::parse(&data).unwrap_or_else(|err| {
panic!(
"failed to parse V1 TZif test file for {:?}: {err}",
self.name
)
})
}
}
File diff suppressed because it is too large Load Diff
+213
View File
@@ -0,0 +1,213 @@
/*!
Contains some tests for the TZif parser in `jiff-core`.
These tests are a little awkward to have live in `jiff-core` because of the use
of `insta` and because I didn't want to duplicate the time zone data at the
time of writing. For the former, I didn't want to use `insta` in `jiff-core`
because I've found it to be somewhat painful to use.
*/
#[cfg(all(test, feature = "alloc"))]
mod tests {
use alloc::{
string::{String, ToString},
vec,
};
use jcore::tz::tzif;
#[cfg(not(miri))]
use crate::tz::testdata::TZIF_TEST_FILES;
/// This converts TZif data into a human readable format.
///
/// This is useful for debugging (via `./scripts/jiff-debug tzif`), but we
/// also use it for snapshot testing to make reading the test output at
/// least *somewhat* comprehensible for humans. Otherwise, one needs to
/// read and understand Unix timestamps. That ain't going to fly.
///
/// For this to work, we make sure everything in a `Tzif` value is
/// represented in some way in this output.
fn tzif_to_human_readable(tzif: &tzif::TimeZone) -> String {
use std::io::Write;
struct IndicatorDisplay(tzif::Indicator);
impl core::fmt::Display for IndicatorDisplay {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
match self.0 {
tzif::Indicator::LocalWall => write!(f, "local/wall"),
tzif::Indicator::LocalStandard => write!(f, "local/std"),
tzif::Indicator::UTStandard => write!(f, "ut/std"),
}
}
}
fn datetime(dt: tzif::DateTime) -> crate::civil::DateTime {
crate::civil::DateTime::constant(
dt.year(),
dt.month(),
dt.day(),
dt.hour(),
dt.minute(),
dt.second(),
0,
)
}
let mut out = tabwriter::TabWriter::new(vec![])
.alignment(tabwriter::Alignment::Left);
writeln!(out, "TIME ZONE VERSION").unwrap();
writeln!(out, " {}", char::try_from(tzif.version).unwrap()).unwrap();
writeln!(out, "LOCAL TIME TYPES").unwrap();
for (i, typ) in tzif.types.iter().enumerate() {
writeln!(
out,
" {i:03}:\toffset={off}\t\
designation={desig}\t{dst}\tindicator={ind}",
off = jcore::tz::Offset::from_seconds(typ.offset.seconds())
.unwrap(),
desig = tzif.designations[usize::from(typ.designation)],
dst = if typ.dst.is_dst() { "dst" } else { "" },
ind = IndicatorDisplay(typ.indicator),
)
.unwrap();
}
if !tzif.transitions.timestamps.is_empty() {
writeln!(out, "TRANSITIONS").unwrap();
for i in 0..tzif.transitions.timestamps.len() {
let trans = &tzif.transitions;
let timestamp = trans.timestamps[i];
let dt = jcore::tz::Offset::UTC
.to_datetime(timestamp.to_standard_timestamp());
let typ = tzif.types[usize::from(trans.infos[i].type_index)];
let wall =
alloc::format!("{}", datetime(trans.civil_starts[i]));
let ambiguous = match trans.infos[i].kind {
tzif::TransitionKind::Unambiguous => {
"unambiguous".to_string()
}
tzif::TransitionKind::Gap => {
let end = datetime(trans.civil_ends[i]);
alloc::format!(" gap-until({end})")
}
tzif::TransitionKind::Fold => {
let end = datetime(trans.civil_ends[i]);
alloc::format!("fold-until({end})")
}
};
writeln!(
out,
" {i:04}:\t{dt:?}Z\tunix={ts}\twall={wall}\t\
{ambiguous}\t\
type={type_index}\t{off}\t\
{desig}\t{dst}",
ts = timestamp.as_second(),
type_index = trans.infos[i].type_index,
off =
jcore::tz::Offset::from_seconds(typ.offset.seconds())
.unwrap(),
desig = tzif.designations[usize::from(typ.designation)],
dst = if typ.dst.is_dst() { "dst" } else { "" },
)
.unwrap();
}
}
if let Some(ref posix_tz) = tzif.posix_tz {
writeln!(out, "POSIX TIME ZONE STRING").unwrap();
writeln!(
out,
" {}",
crate::tz::posix::TimeZoneFormatter(posix_tz)
)
.unwrap();
}
String::from_utf8(out.into_inner().unwrap()).unwrap()
}
/// DEBUG COMMAND
///
/// Takes environment variable `JIFF_DEBUG_TZIF_PATH` as input, and treats
/// the value as a TZif file path. This test will open the file, parse it
/// as a TZif and then dump debug data about the file in a human readable
/// plain text format.
#[cfg(feature = "std")]
#[test]
fn debug_tzif() -> anyhow::Result<()> {
use anyhow::Context;
let _ = crate::logging::Logger::init();
const ENV: &str = "JIFF_DEBUG_TZIF_PATH";
let Some(val) = std::env::var_os(ENV) else { return Ok(()) };
let Ok(val) = val.into_string() else {
anyhow::bail!("{ENV} has invalid UTF-8")
};
let bytes =
std::fs::read(&val).with_context(|| alloc::format!("{val:?}"))?;
let tzif = tzif::TimeZone::parse(&bytes)?;
std::eprint!("{}", tzif_to_human_readable(&tzif));
Ok(())
}
#[cfg(not(miri))]
#[test]
fn tzif_parse_v2plus() {
for tzif_test in TZIF_TEST_FILES {
insta::assert_snapshot!(
alloc::format!("{}_v2+", tzif_test.name),
tzif_to_human_readable(&tzif_test.parse())
);
}
}
#[cfg(not(miri))]
#[test]
fn tzif_parse_v1() {
for tzif_test in TZIF_TEST_FILES {
insta::assert_snapshot!(
alloc::format!("{}_v1", tzif_test.name),
tzif_to_human_readable(&tzif_test.parse_v1())
);
}
}
/// This tests walks the /usr/share/zoneinfo directory (if it exists) and
/// tries to parse every TZif formatted file it can find. We don't really
/// do much with it other than to ensure we don't panic or return an error.
/// That is, we check that we can parse each file, but not that we do so
/// correctly.
#[cfg(not(miri))]
#[cfg(feature = "tzdb-zoneinfo")]
#[cfg(target_os = "linux")]
#[test]
fn zoneinfo() {
const TZDIR: &str = "/usr/share/zoneinfo";
for result in walkdir::WalkDir::new(TZDIR) {
// Just skip if we got an error traversing the directory tree.
// These aren't related to our parsing, so it's some other problem
// (like the directory not existing).
let Ok(dent) = result else { continue };
// This test can take some time in debug mode, so skip parsing
// some of the less frequently used TZif files.
let Some(name) = dent.path().to_str() else { continue };
if name.contains("right/") || name.contains("posix/") {
continue;
}
// Again, skip if we can't read. Not my monkeys, not my circus.
let Ok(bytes) = std::fs::read(dent.path()) else { continue };
if !tzif::is_possibly_tzif(&bytes) {
continue;
}
// OK at this point, we're pretty sure `bytes` should be a TZif
// binary file. So try to parse it and fail the test if it fails.
if let Err(err) = tzif::TimeZone::parse(&bytes) {
panic!("failed to parse TZif file {:?}: {err}", dent.path());
}
}
}
}
File diff suppressed because it is too large Load Diff
+377
View File
@@ -0,0 +1,377 @@
/*!
A module for constants and various base utilities.
This module is a work-in-progress that may lead to helping us move off of
ranged integers. I'm not quite sure where this will go.
*/
use jcore::{
bounds::{self, const_check, Bounds, RawBoundsError},
constants as c,
};
use crate::Error;
/// Writes the definition of a single boundary type.
///
/// When only the name and type are given, then this copies the `WHAT`,
/// `MIN` and `MAX` definitions from `jcore`. This avoids copying the specific
/// range values and maintains a single point of truth.
macro_rules! define_one_boundary_type {
(
// The name of the boundary type.
$name:ident,
// The underlying primitive type. This is usually, but not always,
// the smallest signed primitive integer type that can represent both
// the minimum and maximum boundary values.
$ty:ident,
// A short human readable description that appears in error messages
// when the boundaries of this type are violated.
$what:expr,
// The minimum value.
$min:expr,
// The maximum value.
$max:expr $(,)?
) => {
pub(crate) struct $name(());
impl Bounds for $name {
const WHAT: &'static str = $what;
const MIN: Self::Primitive = $min;
const MAX: Self::Primitive = $max;
type Primitive = $ty;
type Error = BoundsError;
#[cold]
#[inline(never)]
fn error() -> BoundsError {
Self::error()
}
}
#[allow(dead_code)]
impl $name {
pub(crate) const MIN: $ty = <$name as Bounds>::MIN;
pub(crate) const MAX: $ty = <$name as Bounds>::MAX;
const LEN: i128 = Self::MAX as i128 - Self::MIN as i128 + 1;
#[cold]
pub(crate) const fn error() -> BoundsError {
BoundsError::$name(RawBoundsError::new())
}
#[cfg_attr(feature = "perf-inline", inline(always))]
pub(crate) fn check(
n: impl Into<i64>,
) -> Result<$ty, BoundsError> {
<$name as Bounds>::check(n)
}
#[cfg_attr(feature = "perf-inline", inline(always))]
pub(crate) const fn checkc(n: i64) -> Result<$ty, BoundsError> {
match const_check::$ty(n) {
Ok(n) => Ok(n),
Err(err) => Err(BoundsError::$name(err)),
}
}
#[cfg_attr(feature = "perf-inline", inline(always))]
pub(crate) fn checked_add(
n1: $ty,
n2: $ty,
) -> Result<$ty, BoundsError> {
<$name as Bounds>::checked_add(n1, n2)
}
#[cfg_attr(feature = "perf-inline", inline(always))]
pub(crate) fn checked_mul(
n1: $ty,
n2: $ty,
) -> Result<$ty, BoundsError> {
<$name as Bounds>::checked_mul(n1, n2)
}
#[cfg(test)]
pub(crate) fn arbitrary(g: &mut quickcheck::Gen) -> $ty {
use quickcheck::Arbitrary;
let mut n: $ty = <$ty>::arbitrary(g);
n = n.wrapping_rem_euclid(Self::LEN as $ty);
n += Self::MIN;
n
}
}
};
// A shortcut for defining a boundary type in `jiff` that is just a mirror
// of a boundary type defined in `jiff-core`. We do this instead of using
// the boundary type from `jiff-core` directly because we can't have a
// `From` impl on a `jiff-core` error type for `jiff::Error`. (Because
// that would make `jiff-core` a public dependency of `jiff`. Which means
// any semver incompatible release of `jiff-core` would require a semver
// incompatible release of `jiff`. Since `jiff-core` evolves more quickly
// than `jiff`, this would be bad juju.)
($name:ident, $ty:ident $(,)?) => {
define_one_boundary_type!(
$name,
$ty,
jcore::bounds::$name::WHAT,
jcore::bounds::$name::MIN,
jcore::bounds::$name::MAX,
);
};
}
/// This macro writes out the boiler plate to define a boundary type.
///
/// Specifically, it implements the `Bounds` trait and provides a few
/// concrete methods. The concrete methods are mostly wrappers around
/// the generic trait methods. They are provided so that callers don't
/// have to import the `Bounds` trait to use them.
macro_rules! define_bounds {
($((
$name:ident,
$ty:ident
$($tt:tt)*
)),* $(,)?) => {
$(
define_one_boundary_type!($name, $ty $($tt)*);
)*
/// An error that indicates a value is out of its intended range.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub(crate) enum BoundsError {
$($name(RawBoundsError<$name>),)*
}
impl core::fmt::Display for BoundsError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
match *self {
$(BoundsError::$name(ref err) => err.fmt(f),)*
}
}
}
}
}
// This is where we define all of Jiff's ranges.
//
// Well, that's not quite true. Many of them are actually defined in
// `jiff-core`. For those, we still have an entry here that still produces
// a boundary type, but it reuses the minimum and maximum values from
// `jiff-core`. This is so `jiff` completely owns its own boundary types
// (to avoid a public dep on `jiff-core`) while simultaneously preserving a
// single point of truth for every range type.
define_bounds! {
(Century, i8, "century", 0, 99),
(CivilDayNanosecond, i64),
(CivilDaySecond, i32),
(Day, i8),
(DayOfYear, i16),
(Hour, i8),
(Hour12, i8, "hour (12 hour clock)", 1, 12),
(ISOWeek, i8),
(ISOYear, i16),
// This matches Temporal's range.
// See: https://github.com/tc39/proposal-temporal/issues/2458#issuecomment-1380742911
(Increment, i64, "rounding increment", 1, 1_000_000_000),
(Increment32, i32, "rounding increment", 1, 1_000_000_000),
// This is only used in parsing. A value of `60` gets clamped to `59`.
(LeapSecond, i8, "second", 0, 60),
(Microsecond, i16),
(Millisecond, i16),
(Minute, i8),
(Month, i8),
(Nanosecond, i16),
(NthWeekday, i32),
(OffsetHours, i8),
(OffsetMinutes, i8),
(OffsetSeconds, i8),
(OffsetTotalSeconds, i32),
(Second, i8),
(SignedDurationSeconds, i64, "signed duration seconds", i64::MIN, i64::MAX),
(SpanYears, i16, "years", -Self::MAX, (Year::LEN - 1) as i16),
(SpanMonths, i32, "months", -Self::MAX, SpanYears::MAX as i32 * 12),
(SpanWeeks, i32, "weeks", -Self::MAX, SpanDays::MAX / c::DAYS_PER_CIVIL_WEEK_32),
(SpanDays, i32, "days", -Self::MAX, SpanHours::MAX / c::HOURS_PER_CIVIL_DAY_32),
(SpanHours, i32, "hours", -Self::MAX, (SpanMinutes::MAX / c::MINS_PER_HOUR) as i32),
(SpanMinutes, i64, "minutes", -Self::MAX, SpanSeconds::MAX / c::SECS_PER_MIN),
(
SpanSeconds,
i64,
"seconds",
bounds::DeltaSeconds::MIN,
bounds::DeltaSeconds::MAX,
),
(
SpanMilliseconds,
i64,
"milliseconds",
-Self::MAX,
SpanSeconds::MAX * c::MILLIS_PER_SEC,
),
(
SpanMicroseconds,
i64,
"microseconds",
-Self::MAX,
SpanMilliseconds::MAX * c::MICROS_PER_MILLI,
),
(SpanMultiple, i64, "span multiple", i64::MIN + 1, i64::MAX),
// A range of the allowed number of nanoseconds.
//
// For this, we cannot cover the full span of supported time instants since
// `UnixSeconds::MAX * NANOSECONDS_PER_SECOND` cannot fit into 64-bits. We
// could use a `i128`, but it doesn't seem worth bloating up `Span`.
//
// Also note that our min is equal to -max, so that the total number of
// values in this range is one less than the number of distinct `i64`
// values. We do that so that the absolute value is always defined.
(SpanNanoseconds, i64, "nanoseconds", i64::MIN + 1, i64::MAX),
(SubsecNanosecond, i32),
(SignedSubsecNanosecond, i32),
(UnixEpochDays, i32),
(
UnixEpochMilliseconds,
i64,
"Unix timestamp milliseconds",
UnixEpochSeconds::MIN * c::MILLIS_PER_SEC,
UnixEpochSeconds::MAX * c::MILLIS_PER_SEC,
),
(
UnixEpochMicroseconds,
i64,
"Unix timestamp microseconds",
UnixEpochMilliseconds::MIN * c::MICROS_PER_MILLI,
UnixEpochMilliseconds::MAX * c::MICROS_PER_MILLI,
),
(UnixEpochSeconds, i64),
(WeekNum, i8, "week-number", 0, 53),
(WeekdayMondayZero, i8),
(WeekdayMondayOne, i8),
(WeekdaySundayZero, i8),
(WeekdaySundayOne, i8),
// The range of years supported by Jiff.
//
// This is ultimately where some of the other ranges (like `UnixSeconds`)
// were determined from. That is, the range of years is the primary point
// at which the space of supported time instants is derived from. If one
// wanted to expand this range, you'd need to change it here and then
// compute the corresponding min/max values for `UnixSeconds`. (Among other
// things... Increasing the supported Jiff range is far more complicated
// than just changing some ranges here.)
(Year, i16),
(YearCE, i16),
(YearBCE, i16),
(YearTwoDigit, i16, "year (2 digits)", 0, 99),
(
ZonedDayNanoseconds,
i64,
"nanoseconds (in one zoned datetime day)",
ZonedDaySeconds::MIN as i64 * c::NANOS_PER_SEC,
ZonedDaySeconds::MAX as i64 * c::NANOS_PER_SEC,
),
// The number of seconds permitted in a single day.
//
// This is mostly just a "sensible" cap on what is possible. We allow one
// day to span up to 7 civil days.
//
// It must also be at least 1 second long.
(
ZonedDaySeconds,
i32,
"seconds (in one zoned datetime day)",
1,
7 * c::SECS_PER_CIVIL_DAY_32,
),
}
impl From<BoundsError> for Error {
fn from(err: BoundsError) -> Error {
Error::bounds(err)
}
}
impl crate::error::IntoError for BoundsError {
fn into_error(self) -> Error {
self.into()
}
}
/// Like `BoundsError`, but maintained manually.
///
/// This is useful for range errors outside of the framework above.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub(crate) enum SpecialBoundsError {
SignedDurationFloatOutOfRangeF32,
SignedDurationFloatOutOfRangeF64,
SignedToUnsignedDuration,
}
impl core::fmt::Display for SpecialBoundsError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::SpecialBoundsError::*;
match *self {
SignedToUnsignedDuration => f.write_str(
"negative signed durations cannot be converted \
to an unsigned duration",
),
SignedDurationFloatOutOfRangeF32 => {
write!(
f,
"parameter 'floating point seconds' is not in \
the required range of {min}..={max}",
min = i64::MIN as f32,
max = i64::MAX as f32,
)
}
SignedDurationFloatOutOfRangeF64 => {
write!(
f,
"parameter 'floating point seconds' is not in \
the required range of {min}..={max}",
min = i64::MIN as f64,
max = i64::MAX as f64,
)
}
}
}
}
impl From<SpecialBoundsError> for Error {
fn from(err: SpecialBoundsError) -> Error {
Error::special_bounds(err)
}
}
impl crate::error::IntoError for SpecialBoundsError {
fn into_error(self) -> Error {
self.into()
}
}
#[cfg(test)]
mod tests {
use alloc::string::ToString;
use super::*;
#[test]
fn size_of_bounds_error() {
// It's okay if this grows, but I wrote this test initially
// to confirm that a `BoundsError` is very small. (Since it's
// just a bunch of variants of ZSTs.)
assert_eq!(1, core::mem::size_of::<BoundsError>());
}
#[test]
fn basic_error_functionality() {
let err = Year::check(10_000).unwrap_err();
assert_eq!(
err.to_string(),
"parameter 'year' is not in the required range of -9999..=9999",
);
}
}
+106
View File
@@ -0,0 +1,106 @@
/*!
This module re-exports a "dumb" version of `std::borrow::Cow`.
It doesn't have any of the generic goodness and doesn't support dynamically
sized types. It's just either a `T` or a `&T`.
We have pretty simplistic needs, and we use this simpler type that works
in core-only mode.
*/
#[derive(Clone, Debug)]
pub(crate) enum DumbCow<'a, T> {
Owned(T),
Borrowed(&'a T),
}
impl<'a, T> DumbCow<'a, T> {
pub(crate) fn borrowed(&self) -> DumbCow<'_, T> {
match *self {
DumbCow::Owned(ref this) => DumbCow::Borrowed(this),
DumbCow::Borrowed(ref this) => DumbCow::Borrowed(this),
}
}
}
impl<'a, T> core::ops::Deref for DumbCow<'a, T> {
type Target = T;
fn deref(&self) -> &T {
match *self {
DumbCow::Owned(ref t) => t,
DumbCow::Borrowed(t) => t,
}
}
}
impl<'a, T: core::fmt::Display> core::fmt::Display for DumbCow<'a, T> {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
core::fmt::Display::fmt(core::ops::Deref::deref(self), f)
}
}
/// A `Cow` for strings, but can be used in core-only mode.
///
/// In core-only, the `Owned` variant doesn't exist.
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub(crate) enum StringCow<'a> {
#[cfg(feature = "alloc")]
Owned(alloc::string::String),
Borrowed(&'a str),
}
impl<'a> StringCow<'a> {
/// Returns this `String` or `&str` as a `&str`.
///
/// Like `std::borrow::Cow`, the lifetime of the string slice returned
/// is tied to `StringCow`, and _not_ the original lifetime of the string
/// slice.
pub(crate) fn as_str<'s>(&'s self) -> &'s str {
match *self {
#[cfg(feature = "alloc")]
StringCow::Owned(ref s) => s,
StringCow::Borrowed(s) => s,
}
}
/// Converts this cow into an "owned" variant, copying if necessary.
///
/// If this cow is already an "owned" variant, then this is a no-op.
#[cfg(feature = "alloc")]
pub(crate) fn into_owned(self) -> StringCow<'static> {
use alloc::string::ToString;
match self {
StringCow::Owned(string) => StringCow::Owned(string),
StringCow::Borrowed(string) => {
StringCow::Owned(string.to_string())
}
}
}
}
impl<'a> core::ops::Deref for StringCow<'a> {
type Target = str;
fn deref(&self) -> &str {
self.as_str()
}
}
impl<'a> core::fmt::Display for StringCow<'a> {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
core::fmt::Display::fmt(self.as_str(), f)
}
}
#[cfg(feature = "alloc")]
impl From<alloc::string::String> for StringCow<'static> {
fn from(string: alloc::string::String) -> StringCow<'static> {
StringCow::Owned(string)
}
}
impl<'a> From<&'a str> for StringCow<'a> {
fn from(string: &'a str) -> StringCow<'a> {
StringCow::Borrowed(string)
}
}
+48
View File
@@ -0,0 +1,48 @@
use std::time::{Duration, Instant as MonotonicInstant};
/// A little helper for representing expiration time.
///
/// An overflowing expiration time is treated identically to a time that is
/// always expired.
///
/// When `None` internally, it implies that the expiration time is at some
/// arbitrary point in the past beyond all possible "time to live" values.
/// i.e., A `None` value invalidates the cache at the next failed lookup.
#[derive(Clone, Copy, Debug)]
pub(crate) struct Expiration(Option<MonotonicInstant>);
impl Expiration {
/// Returns an expiration time for which `is_expired` returns true after
/// the given duration has elapsed from this instant.
pub(crate) fn after(ttl: Duration) -> Expiration {
Expiration(
crate::now::monotonic_time().and_then(|now| now.checked_add(ttl)),
)
}
/// Returns an expiration time for which `is_expired` always returns true.
pub(crate) const fn expired() -> Expiration {
Expiration(None)
}
/// Whether expiration has occurred or not.
pub(crate) fn is_expired(self) -> bool {
self.0.map_or(true, |t| {
let Some(now) = crate::now::monotonic_time() else { return true };
now > t
})
}
}
impl core::fmt::Display for Expiration {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
let maybe_duration = self.0.and_then(|instant| {
crate::now::monotonic_time()
.and_then(|now| instant.checked_duration_since(now))
});
match maybe_duration {
None => f.write_str("expired"),
Some(duration) => core::fmt::Debug::fmt(&duration, f),
}
}
}
+25
View File
@@ -0,0 +1,25 @@
/// Unwrap an `Option<T>` in a `const` context.
///
/// If it fails, panics with the given message.
macro_rules! unwrap {
($val:expr, $msg:expr$(,)?) => {
match $val {
Some(val) => val,
None => panic!($msg),
}
};
}
/// Unwrap a `Result<T, E>` in a `const` context.
///
/// If it fails, panics with the given message.
macro_rules! unwrapr {
($val:expr, $msg:expr$(,)?) => {
match $val {
Ok(val) => val,
Err(_) => panic!($msg),
}
};
}
pub(crate) use {unwrap, unwrapr};
+124
View File
@@ -0,0 +1,124 @@
/*!
Provides convenience routines for escaping raw bytes.
This was copied from `regex-automata` with a few light edits.
*/
use super::utf8;
/// Provides a convenient `Debug` implementation for a `u8`.
///
/// The `Debug` impl treats the byte as an ASCII, and emits a human
/// readable representation of it. If the byte isn't ASCII, then it's
/// emitted as a hex escape sequence.
#[derive(Clone, Copy)]
pub(crate) struct Byte(pub u8);
impl core::fmt::Display for Byte {
#[inline(never)]
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
if self.0 == b' ' {
return f.write_str(" ");
}
// 10 bytes is enough for any output from ascii::escape_default.
let mut bytes = [0u8; 10];
let mut len = 0;
for (i, mut b) in core::ascii::escape_default(self.0).enumerate() {
// capitalize \xab to \xAB
if i >= 2 && b'a' <= b && b <= b'f' {
b -= 32;
}
bytes[len] = b;
len += 1;
}
f.write_str(core::str::from_utf8(&bytes[..len]).unwrap())
}
}
impl core::fmt::Debug for Byte {
#[inline(never)]
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.write_str("\"")?;
core::fmt::Display::fmt(self, f)?;
f.write_str("\"")?;
Ok(())
}
}
/// Provides a convenient `Debug` implementation for `&[u8]`.
///
/// This generally works best when the bytes are presumed to be mostly
/// UTF-8, but will work for anything. For any bytes that aren't UTF-8,
/// they are emitted as hex escape sequences.
#[derive(Clone, Copy)]
pub(crate) struct Bytes<'a>(pub &'a [u8]);
impl<'a> core::fmt::Display for Bytes<'a> {
#[inline(never)]
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
// This is a sad re-implementation of a similar impl found in bstr.
let mut bytes = self.0;
while let Some(result) = utf8::decode(bytes) {
let ch = match result {
Ok(ch) => ch,
Err(err) => {
// The decode API guarantees `errant_bytes` is non-empty.
write!(f, r"\x{:02x}", err.as_slice()[0])?;
bytes = &bytes[1..];
continue;
}
};
bytes = &bytes[ch.len_utf8()..];
match ch {
'\0' => f.write_str(r"\0")?,
'\x01'..='\x7f' => {
core::fmt::Display::fmt(&(ch as u8).escape_ascii(), f)?;
}
_ => {
core::fmt::Display::fmt(&ch.escape_debug(), f)?;
}
}
}
Ok(())
}
}
impl<'a> core::fmt::Debug for Bytes<'a> {
#[inline(never)]
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.write_str("\"")?;
core::fmt::Display::fmt(self, f)?;
f.write_str("\"")?;
Ok(())
}
}
/// A helper for repeating a single byte utilizing `Byte`.
///
/// This is limited to repeating a byte up to `u8::MAX` times in order
/// to reduce its size overhead. And in practice, Jiff just doesn't
/// need more than this (at time of writing, 2025-11-29).
pub(crate) struct RepeatByte {
pub(crate) byte: u8,
pub(crate) count: u8,
}
impl core::fmt::Display for RepeatByte {
#[inline(never)]
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
for _ in 0..self.count {
core::fmt::Display::fmt(&Byte(self.byte), f)?;
}
Ok(())
}
}
impl core::fmt::Debug for RepeatByte {
#[inline(never)]
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.write_str("\"")?;
core::fmt::Display::fmt(self, f)?;
f.write_str("\"")?;
Ok(())
}
}
+70
View File
@@ -0,0 +1,70 @@
use std::{fs::File, path::Path};
use crate::Timestamp;
/// Returns the last modified time for the given file path as a Jiff timestamp.
///
/// If there was a problem accessing the last modified time or if it could not
/// fit in a Jiff timestamp, then a warning message is logged and `None` is
/// returned.
pub(crate) fn last_modified_from_path(path: &Path) -> Option<Timestamp> {
let file = match File::open(path) {
Ok(file) => file,
Err(_err) => {
warn!(
"failed to open file to get last modified time {}: {_err}",
path.display(),
);
return None;
}
};
last_modified_from_file(path, &file)
}
/// Returns the last modified time for the given file as a Jiff timestamp.
///
/// If there was a problem accessing the last modified time or if it could not
/// fit in a Jiff timestamp, then a warning message is logged and `None` is
/// returned.
///
/// The path given should be the path to the given file. It is used for
/// diagnostic purposes.
pub(crate) fn last_modified_from_file(
_path: &Path,
file: &File,
) -> Option<Timestamp> {
let md = match file.metadata() {
Ok(md) => md,
Err(_err) => {
warn!(
"failed to get metadata (for last modified time) \
for {}: {_err}",
_path.display(),
);
return None;
}
};
let systime = match md.modified() {
Ok(systime) => systime,
Err(_err) => {
warn!(
"failed to get last modified time for {}: {_err}",
_path.display()
);
return None;
}
};
let timestamp = match Timestamp::try_from(systime) {
Ok(timestamp) => timestamp,
Err(_err) => {
warn!(
"system time {systime:?} out of bounds \
for Jiff timestamp for last modified time \
from {}: {_err}",
_path.display(),
);
return None;
}
};
Some(timestamp)
}
+104
View File
@@ -0,0 +1,104 @@
/*!
A no-std module for some floating point operations.
These were vendored from the [`libm`] crate. For no-std specifically, I
wouldn't necessarily mind depending on `libm`, but I don't think there's a
way to express "only depend on a crate if some on-by-default feature is not
enabled."
We also very intentionally have very minimal floating point requirements. It's
used for `Span::total` where it's part of the API and thus necessary. It's also
used in floating point conversion routings to `jiff::SignedDuration`.
[`libm`]: https://github.com/rust-lang/libm
*/
pub(crate) trait Float {
fn round(self) -> Self;
fn trunc(self) -> Self;
fn fract(self) -> Self;
}
const TOINT64: f64 = 1. / f64::EPSILON;
impl Float for f64 {
fn round(self) -> f64 {
(self + copysign64(0.5 - 0.25 * f64::EPSILON, self)).trunc()
}
fn trunc(self) -> f64 {
let x = self;
let x1p120 = f64::from_bits(0x4770000000000000); // 0x1p120f === 2 ^ 120
let mut i: u64 = x.to_bits();
let mut e: i64 = (i >> 52 & 0x7ff) as i64 - 0x3ff + 12;
let m: u64;
if e >= 52 + 12 {
return x;
}
if e < 12 {
e = 1;
}
m = -1i64 as u64 >> e;
if (i & m) == 0 {
return x;
}
core::hint::black_box(x + x1p120);
i &= !m;
f64::from_bits(i)
}
fn fract(self) -> f64 {
self - self.trunc()
}
}
impl Float for f32 {
fn round(self) -> f32 {
(self + copysign32(0.5 - 0.25 * f32::EPSILON, self)).trunc()
}
fn trunc(self) -> f32 {
let x = self;
let x1p120 = f32::from_bits(0x7b800000); // 0x1p120f === 2 ^ 120
let mut i: u32 = x.to_bits();
let mut e: i32 = (i >> 23 & 0xff) as i32 - 0x7f + 9;
let m: u32;
if e >= 23 + 9 {
return x;
}
if e < 9 {
e = 1;
}
m = -1i32 as u32 >> e;
if (i & m) == 0 {
return x;
}
core::hint::black_box(x + x1p120);
i &= !m;
f32::from_bits(i)
}
fn fract(self) -> f32 {
self - self.trunc()
}
}
fn copysign64(x: f64, y: f64) -> f64 {
let mut ux = x.to_bits();
let uy = y.to_bits();
ux &= (!0) >> 1;
ux |= uy & (1 << 63);
f64::from_bits(ux)
}
fn copysign32(x: f32, y: f32) -> f32 {
let mut ux = x.to_bits();
let uy = y.to_bits();
ux &= 0x7fffffff;
ux |= uy & 0x80000000;
f32::from_bits(ux)
}
+18
View File
@@ -0,0 +1,18 @@
pub(crate) mod b;
pub(crate) mod borrow;
#[cfg(any(
feature = "tz-system",
feature = "tzdb-zoneinfo",
feature = "tzdb-concatenated"
))]
pub(crate) mod cache;
pub(crate) mod constant;
pub(crate) mod escape;
#[cfg(feature = "std")]
pub(crate) mod fs;
#[cfg(not(feature = "std"))]
pub(crate) mod libm;
pub(crate) mod parse;
pub(crate) mod round;
pub(crate) mod sync;
pub(crate) mod utf8;
+208
View File
@@ -0,0 +1,208 @@
use jcore::bounds::Bounds;
use crate::{
error::util::{ParseFractionError, ParseIntError},
Error,
};
/// Parses an `i64` number from the beginning to the end of the given slice of
/// ASCII digit characters.
///
/// If any byte in the given slice is not `[0-9]`, then this returns an error.
/// Similarly, if the number parsed does not fit into a `i64`, then this
/// returns an error. Notably, this routine does not permit parsing a negative
/// integer. (We use `i64` because everything in this crate uses signed
/// integers, and because a higher level routine might want to parse the sign
/// and then apply it to the result of this routine.)
#[cfg_attr(feature = "perf-inline", inline(always))]
pub(crate) fn i64(bytes: &[u8]) -> Result<i64, ParseIntError> {
if bytes.is_empty() {
return Err(ParseIntError::NoDigitsFound);
}
let mut n: i64 = 0;
for &byte in bytes {
if !(b'0' <= byte && byte <= b'9') {
return Err(ParseIntError::InvalidDigit(byte));
}
let digit = i64::from(byte - b'0');
n = n
.checked_mul(10)
.and_then(|n| n.checked_add(digit))
.ok_or(ParseIntError::TooBig)?;
}
Ok(n)
}
/// Like `self::i64`, but also does a boundary check for the given type.
///
/// # Errors
///
/// If the given slice is not a valid integer (i.e., overflow or contains
/// anything other than `[0-9]`) or is not in the bounds for the given `Bounds`
/// implementation, then an error is returned.
///
/// Note that the error can either be a parsing error or it can be a
/// boundary error.
#[cfg_attr(feature = "perf-inline", inline(always))]
pub(crate) fn bi64<B>(bytes: &[u8]) -> Result<B::Primitive, Error>
where
B: Bounds,
Error: From<B::Error>,
{
Ok(B::check(self::i64(bytes)?)?)
}
/// Parsed an optional `u64` that is a prefix of `bytes`.
///
/// If no digits (`[0-9]`) were found at the beginning of `bytes`, then `None`
/// is returned.
///
/// Note that this is safe to call on untrusted input. It will not attempt
/// to consume more input than could possibly fit into a parsed integer.
///
/// Since this returns a `u64`, it is possible that an integer that cannot
/// fit into an `i64` is returned. Callers should handle this. (Indeed,
/// `DurationUnits` handles this case.)
///
/// # Errors
///
/// When the parsed integer cannot fit into a `u64`.
#[cfg_attr(feature = "perf-inline", inline(always))]
pub(crate) fn u64_prefix(
bytes: &[u8],
) -> Result<(Option<u64>, &[u8]), ParseIntError> {
// Discovered via `u64::MAX.to_string().len()`.
const MAX_U64_DIGITS: usize = 20;
let mut digit_count = 0;
let mut n: u64 = 0;
while digit_count <= MAX_U64_DIGITS {
let Some(&byte) = bytes.get(digit_count) else { break };
if !byte.is_ascii_digit() {
break;
}
digit_count += 1;
// OK because we confirmed `byte` is an ASCII digit.
let digit = u64::from(byte - b'0');
n = n
.checked_mul(10)
.and_then(|n| n.checked_add(digit))
.ok_or(ParseIntError::TooBig)?;
}
if digit_count == 0 {
return Ok((None, bytes));
}
Ok((Some(n), &bytes[digit_count..]))
}
/// Parses a `u32` fractional number from the beginning to the end of the given
/// slice of ASCII digit characters.
///
/// The fraction's maximum precision is always 9 digits. The returned integer
/// will always be in units of `10^{max_precision}`. For example, this
/// will parse a fractional amount of seconds with a maximum precision of
/// nanoseconds.
///
/// If any byte in the given slice is not `[0-9]`, then this returns an error.
/// Notably, this routine does not permit parsing a negative integer.
pub(crate) fn fraction(bytes: &[u8]) -> Result<u32, ParseFractionError> {
if bytes.is_empty() {
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(ParseFractionError::InvalidDigit(byte));
}
Some(digit) if digit > 9 => {
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(|| ParseFractionError::TooBig)?;
}
for _ in bytes.len()..ParseFractionError::MAX_PRECISION {
n = n.checked_mul(10).ok_or_else(|| ParseFractionError::TooBig)?;
}
Ok(n)
}
/// Parses an `OsStr` into a `&str` when `&[u8]` isn't easily available.
///
/// This is effectively `OsStr::to_str`, but with a slightly better error
/// message.
#[cfg(any(feature = "tz-system", feature = "tzdb-zoneinfo"))]
pub(crate) fn os_str_utf8<'o, O>(
os_str: &'o O,
) -> Result<&'o str, crate::error::util::OsStrUtf8Error>
where
O: ?Sized + AsRef<std::ffi::OsStr>,
{
let os_str = os_str.as_ref();
os_str
.to_str()
.ok_or_else(|| crate::error::util::OsStrUtf8Error::from(os_str))
}
/// Splits the given input into two slices at the given position.
///
/// If the position is greater than the length of the slice given, then this
/// returns `None`.
#[cfg_attr(feature = "perf-inline", inline(always))]
pub(crate) fn split(input: &[u8], at: usize) -> Option<(&[u8], &[u8])> {
if at > input.len() {
None
} else {
Some(input.split_at(at))
}
}
/// Returns a function that converts two slices to an offset.
///
/// It takes the starting point as input and returns a function that, when
/// given an ending point (greater than or equal to the starting point), then
/// the corresponding pointers are subtracted and an offset relative to the
/// starting point is returned.
///
/// This is useful as a helper function in parsing routines that use slices
/// but want to report offsets.
///
/// # Panics
///
/// This may panic if the ending point is not a suffix slice of `start`.
pub(crate) fn offseter<'a>(
start: &'a [u8],
) -> impl Fn(&'a [u8]) -> usize + 'a {
move |end| (end.as_ptr() as usize) - (start.as_ptr() as usize)
}
/// Returns a function that converts two slices to the slice between them.
///
/// This takes a starting point as input and returns a function that, when
/// given an ending point (greater than or equal to the starting point), it
/// returns a slice beginning at the starting point and ending just at the
/// ending point.
///
/// This is useful as a helper function in parsing routines.
///
/// # Panics
///
/// This may panic if the ending point is not a suffix slice of `start`.
pub(crate) fn slicer<'a>(
start: &'a [u8],
) -> impl Fn(&'a [u8]) -> &'a [u8] + 'a {
let mkoffset = offseter(start);
move |end| {
let offset = mkoffset(end);
&start[..offset]
}
}
File diff suppressed because it is too large Load Diff
+48
View File
@@ -0,0 +1,48 @@
/*!
This module re-exports `Arc`.
That is, it provides some indirection for the case when `alloc::sync::Arc` is
unavailable.
It also defines a "dumb" `Arc` in core-only mode that doesn't actually do
anything (no indirection, no reference counting).
*/
#[cfg(all(feature = "alloc", not(target_has_atomic = "ptr")))]
pub(crate) use portable_atomic_util::Arc;
#[cfg(all(feature = "alloc", target_has_atomic = "ptr"))]
pub(crate) use alloc::sync::Arc;
/// A "fake" `Arc`.
///
/// Basically, it exposes the `Arc` APIs we use in Jiff, but doesn't
/// actually introduce indirection or reference counting. It's only used
/// in core-only mode and in effect results in inlining all data into its
/// container.
///
/// Not ideal, but we use `Arc` in very few places. One is `TimeZone`,
/// which ends up being pretty small in core-only mode since it doesn't
/// support carrying TZif data.
#[cfg(not(feature = "alloc"))]
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct Arc<T>(T);
#[cfg(not(feature = "alloc"))]
impl<T> Arc<T> {
pub(crate) fn new(t: T) -> Arc<T> {
Arc(t)
}
pub(crate) fn get_mut(this: &mut Arc<T>) -> Option<&mut T> {
Some(&mut this.0)
}
}
#[cfg(not(feature = "alloc"))]
impl<T> core::ops::Deref for Arc<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
+122
View File
@@ -0,0 +1,122 @@
use core::cmp::Ordering;
/// 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::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
/// 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, 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(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()))
}
/// Like std's `eq_ignore_ascii_case`, but returns a full `Ordering`.
#[inline]
pub(crate) fn cmp_ignore_ascii_case(s1: &str, s2: &str) -> Ordering {
cmp_ignore_ascii_case_bytes(s1.as_bytes(), s2.as_bytes())
}
/// Like std's `eq_ignore_ascii_case`, but returns a full `Ordering` on
/// `&[u8]`.
#[inline]
pub(crate) fn cmp_ignore_ascii_case_bytes(s1: &[u8], s2: &[u8]) -> Ordering {
// This function used to look like this:
//
// let it1 = s1.iter().map(|&b| b.to_ascii_lowercase());
// let it2 = s2.iter().map(|&b| b.to_ascii_lowercase());
// it1.cmp(it2)
//
// But the code below seems to do better in microbenchmarks.
let mut i = 0;
loop {
let b1 = s1.get(i).copied().map(|b| b.to_ascii_lowercase());
let b2 = s2.get(i).copied().map(|b| b.to_ascii_lowercase());
match (b1, b2) {
(None, None) => return Ordering::Equal,
(Some(_), None) => return Ordering::Greater,
(None, Some(_)) => return Ordering::Less,
(Some(b1), Some(b2)) if b1 == b2 => i += 1,
(Some(b1), Some(b2)) => return b1.cmp(&b2),
}
}
}
File diff suppressed because it is too large Load Diff