0.3.6: WIP

This commit is contained in:
wisdgod
2025-07-27 08:46:05 +08:00
parent c3bfb3b66e
commit c1a7dd1acb
438 changed files with 169172 additions and 15998 deletions

View File

@@ -0,0 +1,663 @@
// This is a part of Chrono.
// See README.md and LICENSE.txt for details.
//! ISO 8601 calendar date with time zone.
#![allow(deprecated)]
#[cfg(feature = "alloc")]
use core::borrow::Borrow;
use core::cmp::Ordering;
use core::ops::{Add, AddAssign, Sub, SubAssign};
use core::{fmt, hash};
// #[cfg(feature = "rkyv")]
// use rkyv::{Archive, Deserialize, Serialize};
#[cfg(all(feature = "unstable-locales", feature = "alloc"))]
use crate::format::Locale;
#[cfg(feature = "alloc")]
use crate::format::{DelayedFormat, Item, StrftimeItems};
use crate::naive::{IsoWeek, NaiveDate, NaiveTime};
use crate::offset::{TimeZone, Utc};
use crate::{DateTime, Datelike, TimeDelta, Weekday};
/// ISO 8601 calendar date with time zone.
///
/// You almost certainly want to be using a [`NaiveDate`] instead of this type.
///
/// This type primarily exists to aid in the construction of DateTimes that
/// have a timezone by way of the [`TimeZone`] datelike constructors (e.g.
/// [`TimeZone::ymd`]).
///
/// This type should be considered ambiguous at best, due to the inherent lack
/// of precision required for the time zone resolution.
///
/// There are some guarantees on the usage of `Date<Tz>`:
///
/// - If properly constructed via [`TimeZone::ymd`] and others without an error,
/// the corresponding local date should exist for at least a moment.
/// (It may still have a gap from the offset changes.)
///
/// - The `TimeZone` is free to assign *any* [`Offset`](crate::offset::Offset) to the
/// local date, as long as that offset did occur in given day.
///
/// For example, if `2015-03-08T01:59-08:00` is followed by `2015-03-08T03:00-07:00`,
/// it may produce either `2015-03-08-08:00` or `2015-03-08-07:00`
/// but *not* `2015-03-08+00:00` and others.
///
/// - Once constructed as a full `DateTime`, [`DateTime::date`] and other associated
/// methods should return those for the original `Date`. For example, if `dt =
/// tz.ymd_opt(y,m,d).unwrap().hms(h,n,s)` were valid, `dt.date() == tz.ymd_opt(y,m,d).unwrap()`.
///
/// - The date is timezone-agnostic up to one day (i.e. practically always),
/// so the local date and UTC date should be equal for most cases
/// even though the raw calculation between `NaiveDate` and `TimeDelta` may not.
#[deprecated(since = "0.4.23", note = "Use `NaiveDate` or `DateTime<Tz>` instead")]
#[derive(Clone)]
// #[cfg_attr(feature = "rkyv", derive(Archive, Deserialize, Serialize))]
pub struct Date<Tz: TimeZone> {
date: NaiveDate,
offset: Tz::Offset,
}
/// The minimum possible `Date`.
#[allow(deprecated)]
#[deprecated(since = "0.4.20", note = "Use Date::MIN_UTC instead")]
pub const MIN_DATE: Date<Utc> = Date::<Utc>::MIN_UTC;
/// The maximum possible `Date`.
#[allow(deprecated)]
#[deprecated(since = "0.4.20", note = "Use Date::MAX_UTC instead")]
pub const MAX_DATE: Date<Utc> = Date::<Utc>::MAX_UTC;
impl<Tz: TimeZone> Date<Tz> {
/// Makes a new `Date` with given *UTC* date and offset.
/// The local date should be constructed via the `TimeZone` trait.
#[inline]
#[must_use]
pub fn from_utc(date: NaiveDate, offset: Tz::Offset) -> Date<Tz> {
Date { date, offset }
}
/// Makes a new `DateTime` from the current date and given `NaiveTime`.
/// The offset in the current date is preserved.
///
/// Returns `None` on invalid datetime.
#[inline]
#[must_use]
pub fn and_time(&self, time: NaiveTime) -> Option<DateTime<Tz>> {
let localdt = self.naive_local().and_time(time);
self.timezone().from_local_datetime(&localdt).single()
}
/// Makes a new `DateTime` from the current date, hour, minute and second.
/// The offset in the current date is preserved.
///
/// Panics on invalid hour, minute and/or second.
#[deprecated(since = "0.4.23", note = "Use and_hms_opt() instead")]
#[inline]
#[must_use]
pub fn and_hms(&self, hour: u32, min: u32, sec: u32) -> DateTime<Tz> {
self.and_hms_opt(hour, min, sec).expect("invalid time")
}
/// Makes a new `DateTime` from the current date, hour, minute and second.
/// The offset in the current date is preserved.
///
/// Returns `None` on invalid hour, minute and/or second.
#[inline]
#[must_use]
pub fn and_hms_opt(&self, hour: u32, min: u32, sec: u32) -> Option<DateTime<Tz>> {
NaiveTime::from_hms_opt(hour, min, sec).and_then(|time| self.and_time(time))
}
/// Makes a new `DateTime` from the current date, hour, minute, second and millisecond.
/// The millisecond part can exceed 1,000 in order to represent the leap second.
/// The offset in the current date is preserved.
///
/// Panics on invalid hour, minute, second and/or millisecond.
#[deprecated(since = "0.4.23", note = "Use and_hms_milli_opt() instead")]
#[inline]
#[must_use]
pub fn and_hms_milli(&self, hour: u32, min: u32, sec: u32, milli: u32) -> DateTime<Tz> {
self.and_hms_milli_opt(hour, min, sec, milli).expect("invalid time")
}
/// Makes a new `DateTime` from the current date, hour, minute, second and millisecond.
/// The millisecond part can exceed 1,000 in order to represent the leap second.
/// The offset in the current date is preserved.
///
/// Returns `None` on invalid hour, minute, second and/or millisecond.
#[inline]
#[must_use]
pub fn and_hms_milli_opt(
&self,
hour: u32,
min: u32,
sec: u32,
milli: u32,
) -> Option<DateTime<Tz>> {
NaiveTime::from_hms_milli_opt(hour, min, sec, milli).and_then(|time| self.and_time(time))
}
/// Makes a new `DateTime` from the current date, hour, minute, second and microsecond.
/// The microsecond part can exceed 1,000,000 in order to represent the leap second.
/// The offset in the current date is preserved.
///
/// Panics on invalid hour, minute, second and/or microsecond.
#[deprecated(since = "0.4.23", note = "Use and_hms_micro_opt() instead")]
#[inline]
#[must_use]
pub fn and_hms_micro(&self, hour: u32, min: u32, sec: u32, micro: u32) -> DateTime<Tz> {
self.and_hms_micro_opt(hour, min, sec, micro).expect("invalid time")
}
/// Makes a new `DateTime` from the current date, hour, minute, second and microsecond.
/// The microsecond part can exceed 1,000,000 in order to represent the leap second.
/// The offset in the current date is preserved.
///
/// Returns `None` on invalid hour, minute, second and/or microsecond.
#[inline]
#[must_use]
pub fn and_hms_micro_opt(
&self,
hour: u32,
min: u32,
sec: u32,
micro: u32,
) -> Option<DateTime<Tz>> {
NaiveTime::from_hms_micro_opt(hour, min, sec, micro).and_then(|time| self.and_time(time))
}
/// Makes a new `DateTime` from the current date, hour, minute, second and nanosecond.
/// The nanosecond part can exceed 1,000,000,000 in order to represent the leap second.
/// The offset in the current date is preserved.
///
/// Panics on invalid hour, minute, second and/or nanosecond.
#[deprecated(since = "0.4.23", note = "Use and_hms_nano_opt() instead")]
#[inline]
#[must_use]
pub fn and_hms_nano(&self, hour: u32, min: u32, sec: u32, nano: u32) -> DateTime<Tz> {
self.and_hms_nano_opt(hour, min, sec, nano).expect("invalid time")
}
/// Makes a new `DateTime` from the current date, hour, minute, second and nanosecond.
/// The nanosecond part can exceed 1,000,000,000 in order to represent the leap second.
/// The offset in the current date is preserved.
///
/// Returns `None` on invalid hour, minute, second and/or nanosecond.
#[inline]
#[must_use]
pub fn and_hms_nano_opt(
&self,
hour: u32,
min: u32,
sec: u32,
nano: u32,
) -> Option<DateTime<Tz>> {
NaiveTime::from_hms_nano_opt(hour, min, sec, nano).and_then(|time| self.and_time(time))
}
/// Makes a new `Date` for the next date.
///
/// Panics when `self` is the last representable date.
#[deprecated(since = "0.4.23", note = "Use succ_opt() instead")]
#[inline]
#[must_use]
pub fn succ(&self) -> Date<Tz> {
self.succ_opt().expect("out of bound")
}
/// Makes a new `Date` for the next date.
///
/// Returns `None` when `self` is the last representable date.
#[inline]
#[must_use]
pub fn succ_opt(&self) -> Option<Date<Tz>> {
self.date.succ_opt().map(|date| Date::from_utc(date, self.offset.clone()))
}
/// Makes a new `Date` for the prior date.
///
/// Panics when `self` is the first representable date.
#[deprecated(since = "0.4.23", note = "Use pred_opt() instead")]
#[inline]
#[must_use]
pub fn pred(&self) -> Date<Tz> {
self.pred_opt().expect("out of bound")
}
/// Makes a new `Date` for the prior date.
///
/// Returns `None` when `self` is the first representable date.
#[inline]
#[must_use]
pub fn pred_opt(&self) -> Option<Date<Tz>> {
self.date.pred_opt().map(|date| Date::from_utc(date, self.offset.clone()))
}
/// Retrieves an associated offset from UTC.
#[inline]
#[must_use]
pub fn offset(&self) -> &Tz::Offset {
&self.offset
}
/// Retrieves an associated time zone.
#[inline]
#[must_use]
pub fn timezone(&self) -> Tz {
TimeZone::from_offset(&self.offset)
}
/// Changes the associated time zone.
/// This does not change the actual `Date` (but will change the string representation).
#[inline]
#[must_use]
pub fn with_timezone<Tz2: TimeZone>(&self, tz: &Tz2) -> Date<Tz2> {
tz.from_utc_date(&self.date)
}
/// Adds given `TimeDelta` to the current date.
///
/// Returns `None` when it will result in overflow.
#[inline]
#[must_use]
pub fn checked_add_signed(self, rhs: TimeDelta) -> Option<Date<Tz>> {
let date = self.date.checked_add_signed(rhs)?;
Some(Date { date, offset: self.offset })
}
/// Subtracts given `TimeDelta` from the current date.
///
/// Returns `None` when it will result in overflow.
#[inline]
#[must_use]
pub fn checked_sub_signed(self, rhs: TimeDelta) -> Option<Date<Tz>> {
let date = self.date.checked_sub_signed(rhs)?;
Some(Date { date, offset: self.offset })
}
/// Subtracts another `Date` from the current date.
/// Returns a `TimeDelta` of integral numbers.
///
/// This does not overflow or underflow at all,
/// as all possible output fits in the range of `TimeDelta`.
#[inline]
#[must_use]
pub fn signed_duration_since<Tz2: TimeZone>(self, rhs: Date<Tz2>) -> TimeDelta {
self.date.signed_duration_since(rhs.date)
}
/// Returns a view to the naive UTC date.
#[inline]
#[must_use]
pub fn naive_utc(&self) -> NaiveDate {
self.date
}
/// Returns a view to the naive local date.
///
/// This is technically the same as [`naive_utc`](#method.naive_utc)
/// because the offset is restricted to never exceed one day,
/// but provided for the consistency.
#[inline]
#[must_use]
pub fn naive_local(&self) -> NaiveDate {
self.date
}
/// Returns the number of whole years from the given `base` until `self`.
#[must_use]
pub fn years_since(&self, base: Self) -> Option<u32> {
self.date.years_since(base.date)
}
/// The minimum possible `Date`.
pub const MIN_UTC: Date<Utc> = Date { date: NaiveDate::MIN, offset: Utc };
/// The maximum possible `Date`.
pub const MAX_UTC: Date<Utc> = Date { date: NaiveDate::MAX, offset: Utc };
}
/// Maps the local date to other date with given conversion function.
fn map_local<Tz: TimeZone, F>(d: &Date<Tz>, mut f: F) -> Option<Date<Tz>>
where
F: FnMut(NaiveDate) -> Option<NaiveDate>,
{
f(d.naive_local()).and_then(|date| d.timezone().from_local_date(&date).single())
}
impl<Tz: TimeZone> Date<Tz>
where
Tz::Offset: fmt::Display,
{
/// Formats the date with the specified formatting items.
#[cfg(feature = "alloc")]
#[inline]
#[must_use]
pub fn format_with_items<'a, I, B>(&self, items: I) -> DelayedFormat<I>
where
I: Iterator<Item = B> + Clone,
B: Borrow<Item<'a>>,
{
DelayedFormat::new_with_offset(Some(self.naive_local()), None, &self.offset, items)
}
/// Formats the date with the specified format string.
/// See the [`crate::format::strftime`] module
/// on the supported escape sequences.
#[cfg(feature = "alloc")]
#[inline]
#[must_use]
pub fn format<'a>(&self, fmt: &'a str) -> DelayedFormat<StrftimeItems<'a>> {
self.format_with_items(StrftimeItems::new(fmt))
}
/// Formats the date with the specified formatting items and locale.
#[cfg(all(feature = "unstable-locales", feature = "alloc"))]
#[inline]
#[must_use]
pub fn format_localized_with_items<'a, I, B>(
&self,
items: I,
locale: Locale,
) -> DelayedFormat<I>
where
I: Iterator<Item = B> + Clone,
B: Borrow<Item<'a>>,
{
DelayedFormat::new_with_offset_and_locale(
Some(self.naive_local()),
None,
&self.offset,
items,
locale,
)
}
/// Formats the date with the specified format string and locale.
/// See the [`crate::format::strftime`] module
/// on the supported escape sequences.
#[cfg(all(feature = "unstable-locales", feature = "alloc"))]
#[inline]
#[must_use]
pub fn format_localized<'a>(
&self,
fmt: &'a str,
locale: Locale,
) -> DelayedFormat<StrftimeItems<'a>> {
self.format_localized_with_items(StrftimeItems::new_with_locale(fmt, locale), locale)
}
}
impl<Tz: TimeZone> Datelike for Date<Tz> {
#[inline]
fn year(&self) -> i32 {
self.naive_local().year()
}
#[inline]
fn month(&self) -> u32 {
self.naive_local().month()
}
#[inline]
fn month0(&self) -> u32 {
self.naive_local().month0()
}
#[inline]
fn day(&self) -> u32 {
self.naive_local().day()
}
#[inline]
fn day0(&self) -> u32 {
self.naive_local().day0()
}
#[inline]
fn ordinal(&self) -> u32 {
self.naive_local().ordinal()
}
#[inline]
fn ordinal0(&self) -> u32 {
self.naive_local().ordinal0()
}
#[inline]
fn weekday(&self) -> Weekday {
self.naive_local().weekday()
}
#[inline]
fn iso_week(&self) -> IsoWeek {
self.naive_local().iso_week()
}
#[inline]
fn with_year(&self, year: i32) -> Option<Date<Tz>> {
map_local(self, |date| date.with_year(year))
}
#[inline]
fn with_month(&self, month: u32) -> Option<Date<Tz>> {
map_local(self, |date| date.with_month(month))
}
#[inline]
fn with_month0(&self, month0: u32) -> Option<Date<Tz>> {
map_local(self, |date| date.with_month0(month0))
}
#[inline]
fn with_day(&self, day: u32) -> Option<Date<Tz>> {
map_local(self, |date| date.with_day(day))
}
#[inline]
fn with_day0(&self, day0: u32) -> Option<Date<Tz>> {
map_local(self, |date| date.with_day0(day0))
}
#[inline]
fn with_ordinal(&self, ordinal: u32) -> Option<Date<Tz>> {
map_local(self, |date| date.with_ordinal(ordinal))
}
#[inline]
fn with_ordinal0(&self, ordinal0: u32) -> Option<Date<Tz>> {
map_local(self, |date| date.with_ordinal0(ordinal0))
}
}
// we need them as automatic impls cannot handle associated types
impl<Tz: TimeZone> Copy for Date<Tz> where <Tz as TimeZone>::Offset: Copy {}
unsafe impl<Tz: TimeZone> Send for Date<Tz> where <Tz as TimeZone>::Offset: Send {}
impl<Tz: TimeZone, Tz2: TimeZone> PartialEq<Date<Tz2>> for Date<Tz> {
fn eq(&self, other: &Date<Tz2>) -> bool {
self.date == other.date
}
}
impl<Tz: TimeZone> Eq for Date<Tz> {}
impl<Tz: TimeZone> PartialOrd for Date<Tz> {
fn partial_cmp(&self, other: &Date<Tz>) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl<Tz: TimeZone> Ord for Date<Tz> {
fn cmp(&self, other: &Date<Tz>) -> Ordering {
self.date.cmp(&other.date)
}
}
impl<Tz: TimeZone> hash::Hash for Date<Tz> {
fn hash<H: hash::Hasher>(&self, state: &mut H) {
self.date.hash(state)
}
}
impl<Tz: TimeZone> Add<TimeDelta> for Date<Tz> {
type Output = Date<Tz>;
#[inline]
fn add(self, rhs: TimeDelta) -> Date<Tz> {
self.checked_add_signed(rhs).expect("`Date + TimeDelta` overflowed")
}
}
impl<Tz: TimeZone> AddAssign<TimeDelta> for Date<Tz> {
#[inline]
fn add_assign(&mut self, rhs: TimeDelta) {
self.date = self.date.checked_add_signed(rhs).expect("`Date + TimeDelta` overflowed");
}
}
impl<Tz: TimeZone> Sub<TimeDelta> for Date<Tz> {
type Output = Date<Tz>;
#[inline]
fn sub(self, rhs: TimeDelta) -> Date<Tz> {
self.checked_sub_signed(rhs).expect("`Date - TimeDelta` overflowed")
}
}
impl<Tz: TimeZone> SubAssign<TimeDelta> for Date<Tz> {
#[inline]
fn sub_assign(&mut self, rhs: TimeDelta) {
self.date = self.date.checked_sub_signed(rhs).expect("`Date - TimeDelta` overflowed");
}
}
impl<Tz: TimeZone> Sub<Date<Tz>> for Date<Tz> {
type Output = TimeDelta;
#[inline]
fn sub(self, rhs: Date<Tz>) -> TimeDelta {
self.signed_duration_since(rhs)
}
}
impl<Tz: TimeZone> fmt::Debug for Date<Tz> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.naive_local().fmt(f)?;
self.offset.fmt(f)
}
}
impl<Tz: TimeZone> fmt::Display for Date<Tz>
where
Tz::Offset: fmt::Display,
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.naive_local().fmt(f)?;
self.offset.fmt(f)
}
}
// Note that implementation of Arbitrary cannot be automatically derived for Date<Tz>, due to
// the nontrivial bound <Tz as TimeZone>::Offset: Arbitrary.
#[cfg(all(feature = "arbitrary", feature = "std"))]
impl<'a, Tz> arbitrary::Arbitrary<'a> for Date<Tz>
where
Tz: TimeZone,
<Tz as TimeZone>::Offset: arbitrary::Arbitrary<'a>,
{
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Date<Tz>> {
let date = NaiveDate::arbitrary(u)?;
let offset = <Tz as TimeZone>::Offset::arbitrary(u)?;
Ok(Date::from_utc(date, offset))
}
}
#[cfg(test)]
mod tests {
use super::Date;
use crate::{FixedOffset, NaiveDate, TimeDelta, Utc};
#[cfg(feature = "clock")]
use crate::offset::{Local, TimeZone};
#[test]
#[cfg(feature = "clock")]
fn test_years_elapsed() {
const WEEKS_PER_YEAR: f32 = 52.1775;
// This is always at least one year because 1 year = 52.1775 weeks.
let one_year_ago = Utc::today() - TimeDelta::weeks((WEEKS_PER_YEAR * 1.5).ceil() as i64);
// A bit more than 2 years.
let two_year_ago = Utc::today() - TimeDelta::weeks((WEEKS_PER_YEAR * 2.5).ceil() as i64);
assert_eq!(Utc::today().years_since(one_year_ago), Some(1));
assert_eq!(Utc::today().years_since(two_year_ago), Some(2));
// If the given DateTime is later than now, the function will always return 0.
let future = Utc::today() + TimeDelta::weeks(12);
assert_eq!(Utc::today().years_since(future), None);
}
#[test]
fn test_date_add_assign() {
let naivedate = NaiveDate::from_ymd_opt(2000, 1, 1).unwrap();
let date = Date::<Utc>::from_utc(naivedate, Utc);
let mut date_add = date;
date_add += TimeDelta::days(5);
assert_eq!(date_add, date + TimeDelta::days(5));
let timezone = FixedOffset::east_opt(60 * 60).unwrap();
let date = date.with_timezone(&timezone);
let date_add = date_add.with_timezone(&timezone);
assert_eq!(date_add, date + TimeDelta::days(5));
let timezone = FixedOffset::west_opt(2 * 60 * 60).unwrap();
let date = date.with_timezone(&timezone);
let date_add = date_add.with_timezone(&timezone);
assert_eq!(date_add, date + TimeDelta::days(5));
}
#[test]
#[cfg(feature = "clock")]
fn test_date_add_assign_local() {
let naivedate = NaiveDate::from_ymd_opt(2000, 1, 1).unwrap();
let date = Local.from_utc_date(&naivedate);
let mut date_add = date;
date_add += TimeDelta::days(5);
assert_eq!(date_add, date + TimeDelta::days(5));
}
#[test]
fn test_date_sub_assign() {
let naivedate = NaiveDate::from_ymd_opt(2000, 1, 1).unwrap();
let date = Date::<Utc>::from_utc(naivedate, Utc);
let mut date_sub = date;
date_sub -= TimeDelta::days(5);
assert_eq!(date_sub, date - TimeDelta::days(5));
let timezone = FixedOffset::east_opt(60 * 60).unwrap();
let date = date.with_timezone(&timezone);
let date_sub = date_sub.with_timezone(&timezone);
assert_eq!(date_sub, date - TimeDelta::days(5));
let timezone = FixedOffset::west_opt(2 * 60 * 60).unwrap();
let date = date.with_timezone(&timezone);
let date_sub = date_sub.with_timezone(&timezone);
assert_eq!(date_sub, date - TimeDelta::days(5));
}
#[test]
#[cfg(feature = "clock")]
fn test_date_sub_assign_local() {
let naivedate = NaiveDate::from_ymd_opt(2000, 1, 1).unwrap();
let date = Local.from_utc_date(&naivedate);
let mut date_sub = date;
date_sub -= TimeDelta::days(5);
assert_eq!(date_sub, date - TimeDelta::days(5));
}
}

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

View File

@@ -0,0 +1,945 @@
// This is a part of Chrono.
// See README.md and LICENSE.txt for details.
//! Date and time formatting routines.
#[cfg(all(feature = "alloc", not(feature = "std"), not(test)))]
use alloc::string::{String, ToString};
#[cfg(feature = "alloc")]
use core::borrow::Borrow;
#[cfg(feature = "alloc")]
use core::fmt::Display;
use core::fmt::{self, Write};
#[cfg(feature = "alloc")]
use crate::offset::Offset;
#[cfg(any(feature = "alloc", feature = "serde"))]
use crate::{Datelike, FixedOffset, NaiveDateTime, Timelike};
#[cfg(feature = "alloc")]
use crate::{NaiveDate, NaiveTime, Weekday};
#[cfg(feature = "alloc")]
use super::locales;
#[cfg(any(feature = "alloc", feature = "serde"))]
use super::{Colons, OffsetFormat, OffsetPrecision, Pad};
#[cfg(feature = "alloc")]
use super::{Fixed, InternalFixed, InternalInternal, Item, Numeric};
#[cfg(feature = "alloc")]
use locales::*;
/// A *temporary* object which can be used as an argument to `format!` or others.
/// This is normally constructed via `format` methods of each date and time type.
#[cfg(feature = "alloc")]
#[derive(Debug)]
pub struct DelayedFormat<I> {
/// The date view, if any.
date: Option<NaiveDate>,
/// The time view, if any.
time: Option<NaiveTime>,
/// The name and local-to-UTC difference for the offset (timezone), if any.
off: Option<(String, FixedOffset)>,
/// An iterator returning formatting items.
items: I,
/// Locale used for text.
/// ZST if the `unstable-locales` feature is not enabled.
locale: Locale,
}
#[cfg(feature = "alloc")]
impl<'a, I: Iterator<Item = B> + Clone, B: Borrow<Item<'a>>> DelayedFormat<I> {
/// Makes a new `DelayedFormat` value out of local date and time.
#[must_use]
pub fn new(date: Option<NaiveDate>, time: Option<NaiveTime>, items: I) -> DelayedFormat<I> {
DelayedFormat { date, time, off: None, items, locale: default_locale() }
}
/// Makes a new `DelayedFormat` value out of local date and time and UTC offset.
#[must_use]
pub fn new_with_offset<Off>(
date: Option<NaiveDate>,
time: Option<NaiveTime>,
offset: &Off,
items: I,
) -> DelayedFormat<I>
where
Off: Offset + Display,
{
let name_and_diff = (offset.to_string(), offset.fix());
DelayedFormat { date, time, off: Some(name_and_diff), items, locale: default_locale() }
}
/// Makes a new `DelayedFormat` value out of local date and time and locale.
#[cfg(feature = "unstable-locales")]
#[must_use]
pub fn new_with_locale(
date: Option<NaiveDate>,
time: Option<NaiveTime>,
items: I,
locale: Locale,
) -> DelayedFormat<I> {
DelayedFormat { date, time, off: None, items, locale }
}
/// Makes a new `DelayedFormat` value out of local date and time, UTC offset and locale.
#[cfg(feature = "unstable-locales")]
#[must_use]
pub fn new_with_offset_and_locale<Off>(
date: Option<NaiveDate>,
time: Option<NaiveTime>,
offset: &Off,
items: I,
locale: Locale,
) -> DelayedFormat<I>
where
Off: Offset + Display,
{
let name_and_diff = (offset.to_string(), offset.fix());
DelayedFormat { date, time, off: Some(name_and_diff), items, locale }
}
/// Formats `DelayedFormat` into a `core::fmt::Write` instance.
/// # Errors
/// This function returns a `core::fmt::Error` if formatting into the `core::fmt::Write` instance fails.
///
/// # Example
/// ### Writing to a String
/// ```
/// let dt = chrono::DateTime::from_timestamp(1643723400, 123456789).unwrap();
/// let df = dt.format("%Y-%m-%d %H:%M:%S%.9f");
/// let mut buffer = String::new();
/// let _ = df.write_to(&mut buffer);
/// ```
pub fn write_to(&self, w: &mut impl Write) -> fmt::Result {
for item in self.items.clone() {
match *item.borrow() {
Item::Literal(s) | Item::Space(s) => w.write_str(s),
#[cfg(feature = "alloc")]
Item::OwnedLiteral(ref s) | Item::OwnedSpace(ref s) => w.write_str(s),
Item::Numeric(ref spec, pad) => self.format_numeric(w, spec, pad),
Item::Fixed(ref spec) => self.format_fixed(w, spec),
Item::Error => Err(fmt::Error),
}?;
}
Ok(())
}
#[cfg(feature = "alloc")]
fn format_numeric(&self, w: &mut impl Write, spec: &Numeric, pad: Pad) -> fmt::Result {
use self::Numeric::*;
fn write_one(w: &mut impl Write, v: u8) -> fmt::Result {
w.write_char((b'0' + v) as char)
}
fn write_two(w: &mut impl Write, v: u8, pad: Pad) -> fmt::Result {
let ones = b'0' + v % 10;
match (v / 10, pad) {
(0, Pad::None) => {}
(0, Pad::Space) => w.write_char(' ')?,
(tens, _) => w.write_char((b'0' + tens) as char)?,
}
w.write_char(ones as char)
}
#[inline]
fn write_year(w: &mut impl Write, year: i32, pad: Pad) -> fmt::Result {
if (1000..=9999).contains(&year) {
// fast path
write_hundreds(w, (year / 100) as u8)?;
write_hundreds(w, (year % 100) as u8)
} else {
write_n(w, 4, year as i64, pad, !(0..10_000).contains(&year))
}
}
fn write_n(
w: &mut impl Write,
n: usize,
v: i64,
pad: Pad,
always_sign: bool,
) -> fmt::Result {
if always_sign {
match pad {
Pad::None => write!(w, "{v:+}"),
Pad::Zero => write!(w, "{:+01$}", v, n + 1),
Pad::Space => write!(w, "{:+1$}", v, n + 1),
}
} else {
match pad {
Pad::None => write!(w, "{v}"),
Pad::Zero => write!(w, "{v:0n$}"),
Pad::Space => write!(w, "{v:n$}"),
}
}
}
match (spec, self.date, self.time) {
(Year, Some(d), _) => write_year(w, d.year(), pad),
(YearDiv100, Some(d), _) => write_two(w, d.year().div_euclid(100) as u8, pad),
(YearMod100, Some(d), _) => write_two(w, d.year().rem_euclid(100) as u8, pad),
(IsoYear, Some(d), _) => write_year(w, d.iso_week().year(), pad),
(IsoYearDiv100, Some(d), _) => {
write_two(w, d.iso_week().year().div_euclid(100) as u8, pad)
}
(IsoYearMod100, Some(d), _) => {
write_two(w, d.iso_week().year().rem_euclid(100) as u8, pad)
}
(Quarter, Some(d), _) => write_one(w, d.quarter() as u8),
(Month, Some(d), _) => write_two(w, d.month() as u8, pad),
(Day, Some(d), _) => write_two(w, d.day() as u8, pad),
(WeekFromSun, Some(d), _) => write_two(w, d.weeks_from(Weekday::Sun) as u8, pad),
(WeekFromMon, Some(d), _) => write_two(w, d.weeks_from(Weekday::Mon) as u8, pad),
(IsoWeek, Some(d), _) => write_two(w, d.iso_week().week() as u8, pad),
(NumDaysFromSun, Some(d), _) => write_one(w, d.weekday().num_days_from_sunday() as u8),
(WeekdayFromMon, Some(d), _) => write_one(w, d.weekday().number_from_monday() as u8),
(Ordinal, Some(d), _) => write_n(w, 3, d.ordinal() as i64, pad, false),
(Hour, _, Some(t)) => write_two(w, t.hour() as u8, pad),
(Hour12, _, Some(t)) => write_two(w, t.hour12().1 as u8, pad),
(Minute, _, Some(t)) => write_two(w, t.minute() as u8, pad),
(Second, _, Some(t)) => {
write_two(w, (t.second() + t.nanosecond() / 1_000_000_000) as u8, pad)
}
(Nanosecond, _, Some(t)) => {
write_n(w, 9, (t.nanosecond() % 1_000_000_000) as i64, pad, false)
}
(Timestamp, Some(d), Some(t)) => {
let offset = self.off.as_ref().map(|(_, o)| i64::from(o.local_minus_utc()));
let timestamp = d.and_time(t).and_utc().timestamp() - offset.unwrap_or(0);
write_n(w, 9, timestamp, pad, false)
}
(Internal(_), _, _) => Ok(()), // for future expansion
_ => Err(fmt::Error), // insufficient arguments for given format
}
}
#[cfg(feature = "alloc")]
fn format_fixed(&self, w: &mut impl Write, spec: &Fixed) -> fmt::Result {
use Fixed::*;
use InternalInternal::*;
match (spec, self.date, self.time, self.off.as_ref()) {
(ShortMonthName, Some(d), _, _) => {
w.write_str(short_months(self.locale)[d.month0() as usize])
}
(LongMonthName, Some(d), _, _) => {
w.write_str(long_months(self.locale)[d.month0() as usize])
}
(ShortWeekdayName, Some(d), _, _) => w.write_str(
short_weekdays(self.locale)[d.weekday().num_days_from_sunday() as usize],
),
(LongWeekdayName, Some(d), _, _) => {
w.write_str(long_weekdays(self.locale)[d.weekday().num_days_from_sunday() as usize])
}
(LowerAmPm, _, Some(t), _) => {
let ampm = if t.hour12().0 { am_pm(self.locale)[1] } else { am_pm(self.locale)[0] };
for c in ampm.chars().flat_map(|c| c.to_lowercase()) {
w.write_char(c)?
}
Ok(())
}
(UpperAmPm, _, Some(t), _) => {
let ampm = if t.hour12().0 { am_pm(self.locale)[1] } else { am_pm(self.locale)[0] };
w.write_str(ampm)
}
(Nanosecond, _, Some(t), _) => {
let nano = t.nanosecond() % 1_000_000_000;
if nano == 0 {
Ok(())
} else {
w.write_str(decimal_point(self.locale))?;
if nano % 1_000_000 == 0 {
write!(w, "{:03}", nano / 1_000_000)
} else if nano % 1_000 == 0 {
write!(w, "{:06}", nano / 1_000)
} else {
write!(w, "{nano:09}")
}
}
}
(Nanosecond3, _, Some(t), _) => {
w.write_str(decimal_point(self.locale))?;
write!(w, "{:03}", t.nanosecond() / 1_000_000 % 1000)
}
(Nanosecond6, _, Some(t), _) => {
w.write_str(decimal_point(self.locale))?;
write!(w, "{:06}", t.nanosecond() / 1_000 % 1_000_000)
}
(Nanosecond9, _, Some(t), _) => {
w.write_str(decimal_point(self.locale))?;
write!(w, "{:09}", t.nanosecond() % 1_000_000_000)
}
(Internal(InternalFixed { val: Nanosecond3NoDot }), _, Some(t), _) => {
write!(w, "{:03}", t.nanosecond() / 1_000_000 % 1_000)
}
(Internal(InternalFixed { val: Nanosecond6NoDot }), _, Some(t), _) => {
write!(w, "{:06}", t.nanosecond() / 1_000 % 1_000_000)
}
(Internal(InternalFixed { val: Nanosecond9NoDot }), _, Some(t), _) => {
write!(w, "{:09}", t.nanosecond() % 1_000_000_000)
}
(TimezoneName, _, _, Some((tz_name, _))) => write!(w, "{tz_name}"),
(TimezoneOffset | TimezoneOffsetZ, _, _, Some((_, off))) => {
let offset_format = OffsetFormat {
precision: OffsetPrecision::Minutes,
colons: Colons::Maybe,
allow_zulu: *spec == TimezoneOffsetZ,
padding: Pad::Zero,
};
offset_format.format(w, *off)
}
(TimezoneOffsetColon | TimezoneOffsetColonZ, _, _, Some((_, off))) => {
let offset_format = OffsetFormat {
precision: OffsetPrecision::Minutes,
colons: Colons::Colon,
allow_zulu: *spec == TimezoneOffsetColonZ,
padding: Pad::Zero,
};
offset_format.format(w, *off)
}
(TimezoneOffsetDoubleColon, _, _, Some((_, off))) => {
let offset_format = OffsetFormat {
precision: OffsetPrecision::Seconds,
colons: Colons::Colon,
allow_zulu: false,
padding: Pad::Zero,
};
offset_format.format(w, *off)
}
(TimezoneOffsetTripleColon, _, _, Some((_, off))) => {
let offset_format = OffsetFormat {
precision: OffsetPrecision::Hours,
colons: Colons::None,
allow_zulu: false,
padding: Pad::Zero,
};
offset_format.format(w, *off)
}
(RFC2822, Some(d), Some(t), Some((_, off))) => {
write_rfc2822(w, crate::NaiveDateTime::new(d, t), *off)
}
(RFC3339, Some(d), Some(t), Some((_, off))) => write_rfc3339(
w,
crate::NaiveDateTime::new(d, t),
*off,
SecondsFormat::AutoSi,
false,
),
_ => Err(fmt::Error), // insufficient arguments for given format
}
}
}
#[cfg(feature = "alloc")]
impl<'a, I: Iterator<Item = B> + Clone, B: Borrow<Item<'a>>> Display for DelayedFormat<I> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut result = String::new();
self.write_to(&mut result)?;
f.pad(&result)
}
}
/// Tries to format given arguments with given formatting items.
/// Internally used by `DelayedFormat`.
#[cfg(feature = "alloc")]
#[deprecated(since = "0.4.32", note = "Use DelayedFormat::fmt or DelayedFormat::write_to instead")]
pub fn format<'a, I, B>(
w: &mut fmt::Formatter,
date: Option<&NaiveDate>,
time: Option<&NaiveTime>,
off: Option<&(String, FixedOffset)>,
items: I,
) -> fmt::Result
where
I: Iterator<Item = B> + Clone,
B: Borrow<Item<'a>>,
{
DelayedFormat {
date: date.copied(),
time: time.copied(),
off: off.cloned(),
items,
locale: default_locale(),
}
.fmt(w)
}
/// Formats single formatting item
#[cfg(feature = "alloc")]
#[deprecated(since = "0.4.32", note = "Use DelayedFormat::fmt or DelayedFormat::write_to instead")]
pub fn format_item(
w: &mut fmt::Formatter,
date: Option<&NaiveDate>,
time: Option<&NaiveTime>,
off: Option<&(String, FixedOffset)>,
item: &Item<'_>,
) -> fmt::Result {
DelayedFormat {
date: date.copied(),
time: time.copied(),
off: off.cloned(),
items: [item].into_iter(),
locale: default_locale(),
}
.fmt(w)
}
#[cfg(any(feature = "alloc", feature = "serde"))]
impl OffsetFormat {
/// Writes an offset from UTC with the format defined by `self`.
fn format(&self, w: &mut impl Write, off: FixedOffset) -> fmt::Result {
let off = off.local_minus_utc();
if self.allow_zulu && off == 0 {
w.write_char('Z')?;
return Ok(());
}
let (sign, off) = if off < 0 { ('-', -off) } else { ('+', off) };
let hours;
let mut mins = 0;
let mut secs = 0;
let precision = match self.precision {
OffsetPrecision::Hours => {
// Minutes and seconds are simply truncated
hours = (off / 3600) as u8;
OffsetPrecision::Hours
}
OffsetPrecision::Minutes | OffsetPrecision::OptionalMinutes => {
// Round seconds to the nearest minute.
let minutes = (off + 30) / 60;
mins = (minutes % 60) as u8;
hours = (minutes / 60) as u8;
if self.precision == OffsetPrecision::OptionalMinutes && mins == 0 {
OffsetPrecision::Hours
} else {
OffsetPrecision::Minutes
}
}
OffsetPrecision::Seconds
| OffsetPrecision::OptionalSeconds
| OffsetPrecision::OptionalMinutesAndSeconds => {
let minutes = off / 60;
secs = (off % 60) as u8;
mins = (minutes % 60) as u8;
hours = (minutes / 60) as u8;
if self.precision != OffsetPrecision::Seconds && secs == 0 {
if self.precision == OffsetPrecision::OptionalMinutesAndSeconds && mins == 0 {
OffsetPrecision::Hours
} else {
OffsetPrecision::Minutes
}
} else {
OffsetPrecision::Seconds
}
}
};
let colons = self.colons == Colons::Colon;
if hours < 10 {
if self.padding == Pad::Space {
w.write_char(' ')?;
}
w.write_char(sign)?;
if self.padding == Pad::Zero {
w.write_char('0')?;
}
w.write_char((b'0' + hours) as char)?;
} else {
w.write_char(sign)?;
write_hundreds(w, hours)?;
}
if let OffsetPrecision::Minutes | OffsetPrecision::Seconds = precision {
if colons {
w.write_char(':')?;
}
write_hundreds(w, mins)?;
}
if let OffsetPrecision::Seconds = precision {
if colons {
w.write_char(':')?;
}
write_hundreds(w, secs)?;
}
Ok(())
}
}
/// Specific formatting options for seconds. This may be extended in the
/// future, so exhaustive matching in external code is not recommended.
///
/// See the `TimeZone::to_rfc3339_opts` function for usage.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
#[allow(clippy::manual_non_exhaustive)]
pub enum SecondsFormat {
/// Format whole seconds only, with no decimal point nor subseconds.
Secs,
/// Use fixed 3 subsecond digits. This corresponds to [Fixed::Nanosecond3].
Millis,
/// Use fixed 6 subsecond digits. This corresponds to [Fixed::Nanosecond6].
Micros,
/// Use fixed 9 subsecond digits. This corresponds to [Fixed::Nanosecond9].
Nanos,
/// Automatically select one of `Secs`, `Millis`, `Micros`, or `Nanos` to display all available
/// non-zero sub-second digits. This corresponds to [Fixed::Nanosecond].
AutoSi,
// Do not match against this.
#[doc(hidden)]
__NonExhaustive,
}
/// Writes the date, time and offset to the string. same as `%Y-%m-%dT%H:%M:%S%.f%:z`
#[inline]
#[cfg(any(feature = "alloc", feature = "serde"))]
pub(crate) fn write_rfc3339(
w: &mut impl Write,
dt: NaiveDateTime,
off: FixedOffset,
secform: SecondsFormat,
use_z: bool,
) -> fmt::Result {
let year = dt.date().year();
if (0..=9999).contains(&year) {
write_hundreds(w, (year / 100) as u8)?;
write_hundreds(w, (year % 100) as u8)?;
} else {
// ISO 8601 requires the explicit sign for out-of-range years
write!(w, "{year:+05}")?;
}
w.write_char('-')?;
write_hundreds(w, dt.date().month() as u8)?;
w.write_char('-')?;
write_hundreds(w, dt.date().day() as u8)?;
w.write_char('T')?;
let (hour, min, mut sec) = dt.time().hms();
let mut nano = dt.nanosecond();
if nano >= 1_000_000_000 {
sec += 1;
nano -= 1_000_000_000;
}
write_hundreds(w, hour as u8)?;
w.write_char(':')?;
write_hundreds(w, min as u8)?;
w.write_char(':')?;
let sec = sec;
write_hundreds(w, sec as u8)?;
match secform {
SecondsFormat::Secs => {}
SecondsFormat::Millis => write!(w, ".{:03}", nano / 1_000_000)?,
SecondsFormat::Micros => write!(w, ".{:06}", nano / 1000)?,
SecondsFormat::Nanos => write!(w, ".{nano:09}")?,
SecondsFormat::AutoSi => {
if nano == 0 {
} else if nano % 1_000_000 == 0 {
write!(w, ".{:03}", nano / 1_000_000)?
} else if nano % 1_000 == 0 {
write!(w, ".{:06}", nano / 1_000)?
} else {
write!(w, ".{nano:09}")?
}
}
SecondsFormat::__NonExhaustive => unreachable!(),
};
OffsetFormat {
precision: OffsetPrecision::Minutes,
colons: Colons::Colon,
allow_zulu: use_z,
padding: Pad::Zero,
}
.format(w, off)
}
#[cfg(feature = "alloc")]
/// write datetimes like `Tue, 1 Jul 2003 10:52:37 +0200`, same as `%a, %d %b %Y %H:%M:%S %z`
pub(crate) fn write_rfc2822(
w: &mut impl Write,
dt: NaiveDateTime,
off: FixedOffset,
) -> fmt::Result {
let year = dt.year();
// RFC2822 is only defined on years 0 through 9999
if !(0..=9999).contains(&year) {
return Err(fmt::Error);
}
let english = default_locale();
w.write_str(short_weekdays(english)[dt.weekday().num_days_from_sunday() as usize])?;
w.write_str(", ")?;
let day = dt.day();
if day < 10 {
w.write_char((b'0' + day as u8) as char)?;
} else {
write_hundreds(w, day as u8)?;
}
w.write_char(' ')?;
w.write_str(short_months(english)[dt.month0() as usize])?;
w.write_char(' ')?;
write_hundreds(w, (year / 100) as u8)?;
write_hundreds(w, (year % 100) as u8)?;
w.write_char(' ')?;
let (hour, min, sec) = dt.time().hms();
write_hundreds(w, hour as u8)?;
w.write_char(':')?;
write_hundreds(w, min as u8)?;
w.write_char(':')?;
let sec = sec + dt.nanosecond() / 1_000_000_000;
write_hundreds(w, sec as u8)?;
w.write_char(' ')?;
OffsetFormat {
precision: OffsetPrecision::Minutes,
colons: Colons::None,
allow_zulu: false,
padding: Pad::Zero,
}
.format(w, off)
}
/// Equivalent to `{:02}` formatting for n < 100.
pub(crate) fn write_hundreds(w: &mut impl Write, n: u8) -> fmt::Result {
if n >= 100 {
return Err(fmt::Error);
}
let tens = b'0' + n / 10;
let ones = b'0' + n % 10;
w.write_char(tens as char)?;
w.write_char(ones as char)
}
#[cfg(test)]
#[cfg(feature = "alloc")]
mod tests {
use super::{Colons, OffsetFormat, OffsetPrecision, Pad};
use crate::FixedOffset;
#[cfg(feature = "alloc")]
use crate::{NaiveDate, NaiveTime, TimeZone, Timelike, Utc};
#[cfg(feature = "alloc")]
#[test]
fn test_delayed_write_to() {
let dt = crate::DateTime::from_timestamp(1643723400, 123456789).unwrap();
let df = dt.format("%Y-%m-%d %H:%M:%S%.9f");
let mut dt_str = String::new();
df.write_to(&mut dt_str).unwrap();
assert_eq!(dt_str, "2022-02-01 13:50:00.123456789");
}
#[cfg(all(feature = "std", feature = "unstable-locales", feature = "alloc"))]
#[test]
fn test_with_locale_delayed_write_to() {
use crate::DateTime;
use crate::format::locales::Locale;
let dt = DateTime::from_timestamp(1643723400, 123456789).unwrap();
let df = dt.format_localized("%A, %B %d, %Y", Locale::ja_JP);
let mut dt_str = String::new();
df.write_to(&mut dt_str).unwrap();
assert_eq!(dt_str, "火曜日, 2月 01, 2022");
}
#[test]
#[cfg(feature = "alloc")]
fn test_date_format() {
let d = NaiveDate::from_ymd_opt(2012, 3, 4).unwrap();
assert_eq!(d.format("%Y,%C,%y,%G,%g").to_string(), "2012,20,12,2012,12");
assert_eq!(d.format("%m,%b,%h,%B").to_string(), "03,Mar,Mar,March");
assert_eq!(d.format("%q").to_string(), "1");
assert_eq!(d.format("%d,%e").to_string(), "04, 4");
assert_eq!(d.format("%U,%W,%V").to_string(), "10,09,09");
assert_eq!(d.format("%a,%A,%w,%u").to_string(), "Sun,Sunday,0,7");
assert_eq!(d.format("%j").to_string(), "064"); // since 2012 is a leap year
assert_eq!(d.format("%D,%x").to_string(), "03/04/12,03/04/12");
assert_eq!(d.format("%F").to_string(), "2012-03-04");
assert_eq!(d.format("%v").to_string(), " 4-Mar-2012");
assert_eq!(d.format("%t%n%%%n%t").to_string(), "\t\n%\n\t");
// non-four-digit years
assert_eq!(
NaiveDate::from_ymd_opt(12345, 1, 1).unwrap().format("%Y").to_string(),
"+12345"
);
assert_eq!(NaiveDate::from_ymd_opt(1234, 1, 1).unwrap().format("%Y").to_string(), "1234");
assert_eq!(NaiveDate::from_ymd_opt(123, 1, 1).unwrap().format("%Y").to_string(), "0123");
assert_eq!(NaiveDate::from_ymd_opt(12, 1, 1).unwrap().format("%Y").to_string(), "0012");
assert_eq!(NaiveDate::from_ymd_opt(1, 1, 1).unwrap().format("%Y").to_string(), "0001");
assert_eq!(NaiveDate::from_ymd_opt(0, 1, 1).unwrap().format("%Y").to_string(), "0000");
assert_eq!(NaiveDate::from_ymd_opt(-1, 1, 1).unwrap().format("%Y").to_string(), "-0001");
assert_eq!(NaiveDate::from_ymd_opt(-12, 1, 1).unwrap().format("%Y").to_string(), "-0012");
assert_eq!(NaiveDate::from_ymd_opt(-123, 1, 1).unwrap().format("%Y").to_string(), "-0123");
assert_eq!(NaiveDate::from_ymd_opt(-1234, 1, 1).unwrap().format("%Y").to_string(), "-1234");
assert_eq!(
NaiveDate::from_ymd_opt(-12345, 1, 1).unwrap().format("%Y").to_string(),
"-12345"
);
// corner cases
assert_eq!(
NaiveDate::from_ymd_opt(2007, 12, 31).unwrap().format("%G,%g,%U,%W,%V").to_string(),
"2008,08,52,53,01"
);
assert_eq!(
NaiveDate::from_ymd_opt(2010, 1, 3).unwrap().format("%G,%g,%U,%W,%V").to_string(),
"2009,09,01,00,53"
);
}
#[test]
#[cfg(feature = "alloc")]
fn test_time_format() {
let t = NaiveTime::from_hms_nano_opt(3, 5, 7, 98765432).unwrap();
assert_eq!(t.format("%H,%k,%I,%l,%P,%p").to_string(), "03, 3,03, 3,am,AM");
assert_eq!(t.format("%M").to_string(), "05");
assert_eq!(t.format("%S,%f,%.f").to_string(), "07,098765432,.098765432");
assert_eq!(t.format("%.3f,%.6f,%.9f").to_string(), ".098,.098765,.098765432");
assert_eq!(t.format("%R").to_string(), "03:05");
assert_eq!(t.format("%T,%X").to_string(), "03:05:07,03:05:07");
assert_eq!(t.format("%r").to_string(), "03:05:07 AM");
assert_eq!(t.format("%t%n%%%n%t").to_string(), "\t\n%\n\t");
let t = NaiveTime::from_hms_micro_opt(3, 5, 7, 432100).unwrap();
assert_eq!(t.format("%S,%f,%.f").to_string(), "07,432100000,.432100");
assert_eq!(t.format("%.3f,%.6f,%.9f").to_string(), ".432,.432100,.432100000");
let t = NaiveTime::from_hms_milli_opt(3, 5, 7, 210).unwrap();
assert_eq!(t.format("%S,%f,%.f").to_string(), "07,210000000,.210");
assert_eq!(t.format("%.3f,%.6f,%.9f").to_string(), ".210,.210000,.210000000");
let t = NaiveTime::from_hms_opt(3, 5, 7).unwrap();
assert_eq!(t.format("%S,%f,%.f").to_string(), "07,000000000,");
assert_eq!(t.format("%.3f,%.6f,%.9f").to_string(), ".000,.000000,.000000000");
// corner cases
assert_eq!(
NaiveTime::from_hms_opt(13, 57, 9).unwrap().format("%r").to_string(),
"01:57:09 PM"
);
assert_eq!(
NaiveTime::from_hms_milli_opt(23, 59, 59, 1_000).unwrap().format("%X").to_string(),
"23:59:60"
);
}
#[test]
#[cfg(feature = "alloc")]
fn test_datetime_format() {
let dt =
NaiveDate::from_ymd_opt(2010, 9, 8).unwrap().and_hms_milli_opt(7, 6, 54, 321).unwrap();
assert_eq!(dt.format("%c").to_string(), "Wed Sep 8 07:06:54 2010");
assert_eq!(dt.format("%s").to_string(), "1283929614");
assert_eq!(dt.format("%t%n%%%n%t").to_string(), "\t\n%\n\t");
// a horror of leap second: coming near to you.
let dt = NaiveDate::from_ymd_opt(2012, 6, 30)
.unwrap()
.and_hms_milli_opt(23, 59, 59, 1_000)
.unwrap();
assert_eq!(dt.format("%c").to_string(), "Sat Jun 30 23:59:60 2012");
assert_eq!(dt.format("%s").to_string(), "1341100799"); // not 1341100800, it's intentional.
}
#[test]
#[cfg(feature = "alloc")]
fn test_datetime_format_alignment() {
let datetime = Utc
.with_ymd_and_hms(2007, 1, 2, 12, 34, 56)
.unwrap()
.with_nanosecond(123456789)
.unwrap();
// Item::Literal, odd number of padding bytes.
let percent = datetime.format("%%");
assert_eq!(" %", format!("{percent:>4}"));
assert_eq!("% ", format!("{percent:<4}"));
assert_eq!(" % ", format!("{percent:^4}"));
// Item::Numeric, custom non-ASCII padding character
let year = datetime.format("%Y");
assert_eq!("——2007", format!("{year:—>6}"));
assert_eq!("2007——", format!("{year:—<6}"));
assert_eq!("—2007—", format!("{year:—^6}"));
// Item::Fixed
let tz = datetime.format("%Z");
assert_eq!(" UTC", format!("{tz:>5}"));
assert_eq!("UTC ", format!("{tz:<5}"));
assert_eq!(" UTC ", format!("{tz:^5}"));
// [Item::Numeric, Item::Space, Item::Literal, Item::Space, Item::Numeric]
let ymd = datetime.format("%Y %B %d");
assert_eq!(" 2007 January 02", format!("{ymd:>17}"));
assert_eq!("2007 January 02 ", format!("{ymd:<17}"));
assert_eq!(" 2007 January 02 ", format!("{ymd:^17}"));
// Truncated
let time = datetime.format("%T%.6f");
assert_eq!("12:34:56.1234", format!("{time:.13}"));
}
#[test]
fn test_offset_formatting() {
fn check_all(precision: OffsetPrecision, expected: [[&str; 7]; 12]) {
fn check(
precision: OffsetPrecision,
colons: Colons,
padding: Pad,
allow_zulu: bool,
offsets: [FixedOffset; 7],
expected: [&str; 7],
) {
let offset_format = OffsetFormat { precision, colons, allow_zulu, padding };
for (offset, expected) in offsets.iter().zip(expected.iter()) {
let mut output = String::new();
offset_format.format(&mut output, *offset).unwrap();
assert_eq!(&output, expected);
}
}
// +03:45, -03:30, +11:00, -11:00:22, +02:34:26, -12:34:30, +00:00
let offsets = [
FixedOffset::east_opt(13_500).unwrap(),
FixedOffset::east_opt(-12_600).unwrap(),
FixedOffset::east_opt(39_600).unwrap(),
FixedOffset::east_opt(-39_622).unwrap(),
FixedOffset::east_opt(9266).unwrap(),
FixedOffset::east_opt(-45270).unwrap(),
FixedOffset::east_opt(0).unwrap(),
];
check(precision, Colons::Colon, Pad::Zero, false, offsets, expected[0]);
check(precision, Colons::Colon, Pad::Zero, true, offsets, expected[1]);
check(precision, Colons::Colon, Pad::Space, false, offsets, expected[2]);
check(precision, Colons::Colon, Pad::Space, true, offsets, expected[3]);
check(precision, Colons::Colon, Pad::None, false, offsets, expected[4]);
check(precision, Colons::Colon, Pad::None, true, offsets, expected[5]);
check(precision, Colons::None, Pad::Zero, false, offsets, expected[6]);
check(precision, Colons::None, Pad::Zero, true, offsets, expected[7]);
check(precision, Colons::None, Pad::Space, false, offsets, expected[8]);
check(precision, Colons::None, Pad::Space, true, offsets, expected[9]);
check(precision, Colons::None, Pad::None, false, offsets, expected[10]);
check(precision, Colons::None, Pad::None, true, offsets, expected[11]);
// `Colons::Maybe` should format the same as `Colons::None`
check(precision, Colons::Maybe, Pad::Zero, false, offsets, expected[6]);
check(precision, Colons::Maybe, Pad::Zero, true, offsets, expected[7]);
check(precision, Colons::Maybe, Pad::Space, false, offsets, expected[8]);
check(precision, Colons::Maybe, Pad::Space, true, offsets, expected[9]);
check(precision, Colons::Maybe, Pad::None, false, offsets, expected[10]);
check(precision, Colons::Maybe, Pad::None, true, offsets, expected[11]);
}
check_all(
OffsetPrecision::Hours,
[
["+03", "-03", "+11", "-11", "+02", "-12", "+00"],
["+03", "-03", "+11", "-11", "+02", "-12", "Z"],
[" +3", " -3", "+11", "-11", " +2", "-12", " +0"],
[" +3", " -3", "+11", "-11", " +2", "-12", "Z"],
["+3", "-3", "+11", "-11", "+2", "-12", "+0"],
["+3", "-3", "+11", "-11", "+2", "-12", "Z"],
["+03", "-03", "+11", "-11", "+02", "-12", "+00"],
["+03", "-03", "+11", "-11", "+02", "-12", "Z"],
[" +3", " -3", "+11", "-11", " +2", "-12", " +0"],
[" +3", " -3", "+11", "-11", " +2", "-12", "Z"],
["+3", "-3", "+11", "-11", "+2", "-12", "+0"],
["+3", "-3", "+11", "-11", "+2", "-12", "Z"],
],
);
check_all(
OffsetPrecision::Minutes,
[
["+03:45", "-03:30", "+11:00", "-11:00", "+02:34", "-12:35", "+00:00"],
["+03:45", "-03:30", "+11:00", "-11:00", "+02:34", "-12:35", "Z"],
[" +3:45", " -3:30", "+11:00", "-11:00", " +2:34", "-12:35", " +0:00"],
[" +3:45", " -3:30", "+11:00", "-11:00", " +2:34", "-12:35", "Z"],
["+3:45", "-3:30", "+11:00", "-11:00", "+2:34", "-12:35", "+0:00"],
["+3:45", "-3:30", "+11:00", "-11:00", "+2:34", "-12:35", "Z"],
["+0345", "-0330", "+1100", "-1100", "+0234", "-1235", "+0000"],
["+0345", "-0330", "+1100", "-1100", "+0234", "-1235", "Z"],
[" +345", " -330", "+1100", "-1100", " +234", "-1235", " +000"],
[" +345", " -330", "+1100", "-1100", " +234", "-1235", "Z"],
["+345", "-330", "+1100", "-1100", "+234", "-1235", "+000"],
["+345", "-330", "+1100", "-1100", "+234", "-1235", "Z"],
],
);
#[rustfmt::skip]
check_all(
OffsetPrecision::Seconds,
[
["+03:45:00", "-03:30:00", "+11:00:00", "-11:00:22", "+02:34:26", "-12:34:30", "+00:00:00"],
["+03:45:00", "-03:30:00", "+11:00:00", "-11:00:22", "+02:34:26", "-12:34:30", "Z"],
[" +3:45:00", " -3:30:00", "+11:00:00", "-11:00:22", " +2:34:26", "-12:34:30", " +0:00:00"],
[" +3:45:00", " -3:30:00", "+11:00:00", "-11:00:22", " +2:34:26", "-12:34:30", "Z"],
["+3:45:00", "-3:30:00", "+11:00:00", "-11:00:22", "+2:34:26", "-12:34:30", "+0:00:00"],
["+3:45:00", "-3:30:00", "+11:00:00", "-11:00:22", "+2:34:26", "-12:34:30", "Z"],
["+034500", "-033000", "+110000", "-110022", "+023426", "-123430", "+000000"],
["+034500", "-033000", "+110000", "-110022", "+023426", "-123430", "Z"],
[" +34500", " -33000", "+110000", "-110022", " +23426", "-123430", " +00000"],
[" +34500", " -33000", "+110000", "-110022", " +23426", "-123430", "Z"],
["+34500", "-33000", "+110000", "-110022", "+23426", "-123430", "+00000"],
["+34500", "-33000", "+110000", "-110022", "+23426", "-123430", "Z"],
],
);
check_all(
OffsetPrecision::OptionalMinutes,
[
["+03:45", "-03:30", "+11", "-11", "+02:34", "-12:35", "+00"],
["+03:45", "-03:30", "+11", "-11", "+02:34", "-12:35", "Z"],
[" +3:45", " -3:30", "+11", "-11", " +2:34", "-12:35", " +0"],
[" +3:45", " -3:30", "+11", "-11", " +2:34", "-12:35", "Z"],
["+3:45", "-3:30", "+11", "-11", "+2:34", "-12:35", "+0"],
["+3:45", "-3:30", "+11", "-11", "+2:34", "-12:35", "Z"],
["+0345", "-0330", "+11", "-11", "+0234", "-1235", "+00"],
["+0345", "-0330", "+11", "-11", "+0234", "-1235", "Z"],
[" +345", " -330", "+11", "-11", " +234", "-1235", " +0"],
[" +345", " -330", "+11", "-11", " +234", "-1235", "Z"],
["+345", "-330", "+11", "-11", "+234", "-1235", "+0"],
["+345", "-330", "+11", "-11", "+234", "-1235", "Z"],
],
);
check_all(
OffsetPrecision::OptionalSeconds,
[
["+03:45", "-03:30", "+11:00", "-11:00:22", "+02:34:26", "-12:34:30", "+00:00"],
["+03:45", "-03:30", "+11:00", "-11:00:22", "+02:34:26", "-12:34:30", "Z"],
[" +3:45", " -3:30", "+11:00", "-11:00:22", " +2:34:26", "-12:34:30", " +0:00"],
[" +3:45", " -3:30", "+11:00", "-11:00:22", " +2:34:26", "-12:34:30", "Z"],
["+3:45", "-3:30", "+11:00", "-11:00:22", "+2:34:26", "-12:34:30", "+0:00"],
["+3:45", "-3:30", "+11:00", "-11:00:22", "+2:34:26", "-12:34:30", "Z"],
["+0345", "-0330", "+1100", "-110022", "+023426", "-123430", "+0000"],
["+0345", "-0330", "+1100", "-110022", "+023426", "-123430", "Z"],
[" +345", " -330", "+1100", "-110022", " +23426", "-123430", " +000"],
[" +345", " -330", "+1100", "-110022", " +23426", "-123430", "Z"],
["+345", "-330", "+1100", "-110022", "+23426", "-123430", "+000"],
["+345", "-330", "+1100", "-110022", "+23426", "-123430", "Z"],
],
);
check_all(
OffsetPrecision::OptionalMinutesAndSeconds,
[
["+03:45", "-03:30", "+11", "-11:00:22", "+02:34:26", "-12:34:30", "+00"],
["+03:45", "-03:30", "+11", "-11:00:22", "+02:34:26", "-12:34:30", "Z"],
[" +3:45", " -3:30", "+11", "-11:00:22", " +2:34:26", "-12:34:30", " +0"],
[" +3:45", " -3:30", "+11", "-11:00:22", " +2:34:26", "-12:34:30", "Z"],
["+3:45", "-3:30", "+11", "-11:00:22", "+2:34:26", "-12:34:30", "+0"],
["+3:45", "-3:30", "+11", "-11:00:22", "+2:34:26", "-12:34:30", "Z"],
["+0345", "-0330", "+11", "-110022", "+023426", "-123430", "+00"],
["+0345", "-0330", "+11", "-110022", "+023426", "-123430", "Z"],
[" +345", " -330", "+11", "-110022", " +23426", "-123430", " +0"],
[" +345", " -330", "+11", "-110022", " +23426", "-123430", "Z"],
["+345", "-330", "+11", "-110022", "+23426", "-123430", "+0"],
["+345", "-330", "+11", "-110022", "+23426", "-123430", "Z"],
],
);
}
}

View File

@@ -0,0 +1,103 @@
#[cfg(feature = "unstable-locales")]
mod localized {
use pure_rust_locales::{Locale, locale_match};
pub(crate) const fn default_locale() -> Locale {
Locale::POSIX
}
pub(crate) const fn short_months(locale: Locale) -> &'static [&'static str] {
locale_match!(locale => LC_TIME::ABMON)
}
pub(crate) const fn long_months(locale: Locale) -> &'static [&'static str] {
locale_match!(locale => LC_TIME::MON)
}
pub(crate) const fn short_weekdays(locale: Locale) -> &'static [&'static str] {
locale_match!(locale => LC_TIME::ABDAY)
}
pub(crate) const fn long_weekdays(locale: Locale) -> &'static [&'static str] {
locale_match!(locale => LC_TIME::DAY)
}
pub(crate) const fn am_pm(locale: Locale) -> &'static [&'static str] {
locale_match!(locale => LC_TIME::AM_PM)
}
pub(crate) const fn decimal_point(locale: Locale) -> &'static str {
locale_match!(locale => LC_NUMERIC::DECIMAL_POINT)
}
pub(crate) const fn d_fmt(locale: Locale) -> &'static str {
locale_match!(locale => LC_TIME::D_FMT)
}
pub(crate) const fn d_t_fmt(locale: Locale) -> &'static str {
locale_match!(locale => LC_TIME::D_T_FMT)
}
pub(crate) const fn t_fmt(locale: Locale) -> &'static str {
locale_match!(locale => LC_TIME::T_FMT)
}
pub(crate) const fn t_fmt_ampm(locale: Locale) -> &'static str {
locale_match!(locale => LC_TIME::T_FMT_AMPM)
}
}
#[cfg(feature = "unstable-locales")]
pub(crate) use localized::*;
#[cfg(feature = "unstable-locales")]
pub use pure_rust_locales::Locale;
#[cfg(not(feature = "unstable-locales"))]
mod unlocalized {
#[derive(Copy, Clone, Debug)]
pub(crate) struct Locale;
pub(crate) const fn default_locale() -> Locale {
Locale
}
pub(crate) const fn short_months(_locale: Locale) -> &'static [&'static str] {
&["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
}
pub(crate) const fn long_months(_locale: Locale) -> &'static [&'static str] {
&[
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
]
}
pub(crate) const fn short_weekdays(_locale: Locale) -> &'static [&'static str] {
&["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
}
pub(crate) const fn long_weekdays(_locale: Locale) -> &'static [&'static str] {
&["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
}
pub(crate) const fn am_pm(_locale: Locale) -> &'static [&'static str] {
&["AM", "PM"]
}
pub(crate) const fn decimal_point(_locale: Locale) -> &'static str {
"."
}
}
#[cfg(not(feature = "unstable-locales"))]
pub(crate) use unlocalized::*;

View File

@@ -0,0 +1,557 @@
// This is a part of Chrono.
// See README.md and LICENSE.txt for details.
//! Formatting (and parsing) utilities for date and time.
//!
//! This module provides the common types and routines to implement,
//! for example, [`DateTime::format`](../struct.DateTime.html#method.format) or
//! [`DateTime::parse_from_str`](../struct.DateTime.html#method.parse_from_str) methods.
//! For most cases you should use these high-level interfaces.
//!
//! Internally the formatting and parsing shares the same abstract **formatting items**,
//! which are just an [`Iterator`](https://doc.rust-lang.org/std/iter/trait.Iterator.html) of
//! the [`Item`](./enum.Item.html) type.
//! They are generated from more readable **format strings**;
//! currently Chrono supports a built-in syntax closely resembling
//! C's `strftime` format. The available options can be found [here](./strftime/index.html).
//!
//! # Example
//! ```
//! # #[cfg(feature = "alloc")] {
//! use chrono::{NaiveDateTime, TimeZone, Utc};
//!
//! let date_time = Utc.with_ymd_and_hms(2020, 11, 10, 0, 1, 32).unwrap();
//!
//! let formatted = format!("{}", date_time.format("%Y-%m-%d %H:%M:%S"));
//! assert_eq!(formatted, "2020-11-10 00:01:32");
//!
//! let parsed = NaiveDateTime::parse_from_str(&formatted, "%Y-%m-%d %H:%M:%S")?.and_utc();
//! assert_eq!(parsed, date_time);
//! # }
//! # Ok::<(), chrono::ParseError>(())
//! ```
#[cfg(all(feature = "alloc", not(feature = "std"), not(test)))]
use alloc::boxed::Box;
use core::fmt;
use core::str::FromStr;
#[cfg(feature = "std")]
use std::error::Error;
use crate::{Month, ParseMonthError, ParseWeekdayError, Weekday};
mod formatting;
mod parsed;
// due to the size of parsing routines, they are in separate modules.
mod parse;
pub(crate) mod scan;
pub mod strftime;
#[allow(unused)]
// TODO: remove '#[allow(unused)]' once we use this module for parsing or something else that does
// not require `alloc`.
pub(crate) mod locales;
pub use formatting::SecondsFormat;
pub(crate) use formatting::write_hundreds;
#[cfg(feature = "alloc")]
pub(crate) use formatting::write_rfc2822;
#[cfg(any(feature = "alloc", feature = "serde"))]
pub(crate) use formatting::write_rfc3339;
#[cfg(feature = "alloc")]
#[allow(deprecated)]
pub use formatting::{DelayedFormat, format, format_item};
#[cfg(feature = "unstable-locales")]
pub use locales::Locale;
pub(crate) use parse::parse_rfc3339;
pub use parse::{parse, parse_and_remainder};
pub use parsed::Parsed;
pub use strftime::StrftimeItems;
/// An uninhabited type used for `InternalNumeric` and `InternalFixed` below.
#[derive(Clone, PartialEq, Eq, Hash)]
enum Void {}
/// Padding characters for numeric items.
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
pub enum Pad {
/// No padding.
None,
/// Zero (`0`) padding.
Zero,
/// Space padding.
Space,
}
/// Numeric item types.
/// They have associated formatting width (FW) and parsing width (PW).
///
/// The **formatting width** is the minimal width to be formatted.
/// If the number is too short, and the padding is not [`Pad::None`](./enum.Pad.html#variant.None),
/// then it is left-padded.
/// If the number is too long or (in some cases) negative, it is printed as is.
///
/// The **parsing width** is the maximal width to be scanned.
/// The parser only tries to consume from one to given number of digits (greedily).
/// It also trims the preceding whitespace if any.
/// It cannot parse the negative number, so some date and time cannot be formatted then
/// parsed with the same formatting items.
#[non_exhaustive]
#[derive(Clone, PartialEq, Eq, Debug, Hash)]
pub enum Numeric {
/// Full Gregorian year (FW=4, PW=∞).
/// May accept years before 1 BCE or after 9999 CE, given an initial sign (+/-).
Year,
/// Gregorian year divided by 100 (century number; FW=PW=2). Implies the non-negative year.
YearDiv100,
/// Gregorian year modulo 100 (FW=PW=2). Cannot be negative.
YearMod100,
/// Year in the ISO week date (FW=4, PW=∞).
/// May accept years before 1 BCE or after 9999 CE, given an initial sign.
IsoYear,
/// Year in the ISO week date, divided by 100 (FW=PW=2). Implies the non-negative year.
IsoYearDiv100,
/// Year in the ISO week date, modulo 100 (FW=PW=2). Cannot be negative.
IsoYearMod100,
/// Quarter (FW=PW=1).
Quarter,
/// Month (FW=PW=2).
Month,
/// Day of the month (FW=PW=2).
Day,
/// Week number, where the week 1 starts at the first Sunday of January (FW=PW=2).
WeekFromSun,
/// Week number, where the week 1 starts at the first Monday of January (FW=PW=2).
WeekFromMon,
/// Week number in the ISO week date (FW=PW=2).
IsoWeek,
/// Day of the week, where Sunday = 0 and Saturday = 6 (FW=PW=1).
NumDaysFromSun,
/// Day of the week, where Monday = 1 and Sunday = 7 (FW=PW=1).
WeekdayFromMon,
/// Day of the year (FW=PW=3).
Ordinal,
/// Hour number in the 24-hour clocks (FW=PW=2).
Hour,
/// Hour number in the 12-hour clocks (FW=PW=2).
Hour12,
/// The number of minutes since the last whole hour (FW=PW=2).
Minute,
/// The number of seconds since the last whole minute (FW=PW=2).
Second,
/// The number of nanoseconds since the last whole second (FW=PW=9).
/// Note that this is *not* left-aligned;
/// see also [`Fixed::Nanosecond`](./enum.Fixed.html#variant.Nanosecond).
Nanosecond,
/// The number of non-leap seconds since the midnight UTC on January 1, 1970 (FW=1, PW=∞).
/// For formatting, it assumes UTC upon the absence of time zone offset.
Timestamp,
/// Internal uses only.
///
/// This item exists so that one can add additional internal-only formatting
/// without breaking major compatibility (as enum variants cannot be selectively private).
Internal(InternalNumeric),
}
/// An opaque type representing numeric item types for internal uses only.
#[derive(Clone, Eq, Hash, PartialEq)]
pub struct InternalNumeric {
_dummy: Void,
}
impl fmt::Debug for InternalNumeric {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "<InternalNumeric>")
}
}
/// Fixed-format item types.
///
/// They have their own rules of formatting and parsing.
/// Otherwise noted, they print in the specified cases but parse case-insensitively.
#[non_exhaustive]
#[derive(Clone, PartialEq, Eq, Debug, Hash)]
pub enum Fixed {
/// Abbreviated month names.
///
/// Prints a three-letter-long name in the title case, reads the same name in any case.
ShortMonthName,
/// Full month names.
///
/// Prints a full name in the title case, reads either a short or full name in any case.
LongMonthName,
/// Abbreviated day of the week names.
///
/// Prints a three-letter-long name in the title case, reads the same name in any case.
ShortWeekdayName,
/// Full day of the week names.
///
/// Prints a full name in the title case, reads either a short or full name in any case.
LongWeekdayName,
/// AM/PM.
///
/// Prints in lower case, reads in any case.
LowerAmPm,
/// AM/PM.
///
/// Prints in upper case, reads in any case.
UpperAmPm,
/// An optional dot plus one or more digits for left-aligned nanoseconds.
/// May print nothing, 3, 6 or 9 digits according to the available accuracy.
/// See also [`Numeric::Nanosecond`](./enum.Numeric.html#variant.Nanosecond).
Nanosecond,
/// Same as [`Nanosecond`](#variant.Nanosecond) but the accuracy is fixed to 3.
Nanosecond3,
/// Same as [`Nanosecond`](#variant.Nanosecond) but the accuracy is fixed to 6.
Nanosecond6,
/// Same as [`Nanosecond`](#variant.Nanosecond) but the accuracy is fixed to 9.
Nanosecond9,
/// Timezone name.
///
/// It does not support parsing, its use in the parser is an immediate failure.
TimezoneName,
/// Offset from the local time to UTC (`+09:00` or `-04:00` or `+00:00`).
///
/// In the parser, the colon can be omitted and/or surrounded with any amount of whitespace.
/// The offset is limited from `-24:00` to `+24:00`,
/// which is the same as [`FixedOffset`](../offset/struct.FixedOffset.html)'s range.
TimezoneOffsetColon,
/// Offset from the local time to UTC with seconds (`+09:00:00` or `-04:00:00` or `+00:00:00`).
///
/// In the parser, the colon can be omitted and/or surrounded with any amount of whitespace.
/// The offset is limited from `-24:00:00` to `+24:00:00`,
/// which is the same as [`FixedOffset`](../offset/struct.FixedOffset.html)'s range.
TimezoneOffsetDoubleColon,
/// Offset from the local time to UTC without minutes (`+09` or `-04` or `+00`).
///
/// In the parser, the colon can be omitted and/or surrounded with any amount of whitespace.
/// The offset is limited from `-24` to `+24`,
/// which is the same as [`FixedOffset`](../offset/struct.FixedOffset.html)'s range.
TimezoneOffsetTripleColon,
/// Offset from the local time to UTC (`+09:00` or `-04:00` or `Z`).
///
/// In the parser, the colon can be omitted and/or surrounded with any amount of whitespace,
/// and `Z` can be either in upper case or in lower case.
/// The offset is limited from `-24:00` to `+24:00`,
/// which is the same as [`FixedOffset`](../offset/struct.FixedOffset.html)'s range.
TimezoneOffsetColonZ,
/// Same as [`TimezoneOffsetColon`](#variant.TimezoneOffsetColon) but prints no colon.
/// Parsing allows an optional colon.
TimezoneOffset,
/// Same as [`TimezoneOffsetColonZ`](#variant.TimezoneOffsetColonZ) but prints no colon.
/// Parsing allows an optional colon.
TimezoneOffsetZ,
/// RFC 2822 date and time syntax. Commonly used for email and MIME date and time.
RFC2822,
/// RFC 3339 & ISO 8601 date and time syntax.
RFC3339,
/// Internal uses only.
///
/// This item exists so that one can add additional internal-only formatting
/// without breaking major compatibility (as enum variants cannot be selectively private).
Internal(InternalFixed),
}
/// An opaque type representing fixed-format item types for internal uses only.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct InternalFixed {
val: InternalInternal,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum InternalInternal {
/// Same as [`TimezoneOffsetColonZ`](#variant.TimezoneOffsetColonZ), but
/// allows missing minutes (per [ISO 8601][iso8601]).
///
/// # Panics
///
/// If you try to use this for printing.
///
/// [iso8601]: https://en.wikipedia.org/wiki/ISO_8601#Time_offsets_from_UTC
TimezoneOffsetPermissive,
/// Same as [`Nanosecond`](#variant.Nanosecond) but the accuracy is fixed to 3 and there is no leading dot.
Nanosecond3NoDot,
/// Same as [`Nanosecond`](#variant.Nanosecond) but the accuracy is fixed to 6 and there is no leading dot.
Nanosecond6NoDot,
/// Same as [`Nanosecond`](#variant.Nanosecond) but the accuracy is fixed to 9 and there is no leading dot.
Nanosecond9NoDot,
}
/// Type for specifying the format of UTC offsets.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct OffsetFormat {
/// See `OffsetPrecision`.
pub precision: OffsetPrecision,
/// Separator between hours, minutes and seconds.
pub colons: Colons,
/// Represent `+00:00` as `Z`.
pub allow_zulu: bool,
/// Pad the hour value to two digits.
pub padding: Pad,
}
/// The precision of an offset from UTC formatting item.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum OffsetPrecision {
/// Format offset from UTC as only hours. Not recommended, it is not uncommon for timezones to
/// have an offset of 30 minutes, 15 minutes, etc.
/// Any minutes and seconds get truncated.
Hours,
/// Format offset from UTC as hours and minutes.
/// Any seconds will be rounded to the nearest minute.
Minutes,
/// Format offset from UTC as hours, minutes and seconds.
Seconds,
/// Format offset from UTC as hours, and optionally with minutes.
/// Any seconds will be rounded to the nearest minute.
OptionalMinutes,
/// Format offset from UTC as hours and minutes, and optionally seconds.
OptionalSeconds,
/// Format offset from UTC as hours and optionally minutes and seconds.
OptionalMinutesAndSeconds,
}
/// The separator between hours and minutes in an offset.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum Colons {
/// No separator
None,
/// Colon (`:`) as separator
Colon,
/// No separator when formatting, colon allowed when parsing.
Maybe,
}
/// A single formatting item. This is used for both formatting and parsing.
#[derive(Clone, PartialEq, Eq, Debug, Hash)]
pub enum Item<'a> {
/// A literally printed and parsed text.
Literal(&'a str),
/// Same as `Literal` but with the string owned by the item.
#[cfg(feature = "alloc")]
OwnedLiteral(Box<str>),
/// Whitespace. Prints literally but reads zero or more whitespace.
Space(&'a str),
/// Same as `Space` but with the string owned by the item.
#[cfg(feature = "alloc")]
OwnedSpace(Box<str>),
/// Numeric item. Can be optionally padded to the maximal length (if any) when formatting;
/// the parser simply ignores any padded whitespace and zeroes.
Numeric(Numeric, Pad),
/// Fixed-format item.
Fixed(Fixed),
/// Issues a formatting error. Used to signal an invalid format string.
Error,
}
const fn num(numeric: Numeric) -> Item<'static> {
Item::Numeric(numeric, Pad::None)
}
const fn num0(numeric: Numeric) -> Item<'static> {
Item::Numeric(numeric, Pad::Zero)
}
const fn nums(numeric: Numeric) -> Item<'static> {
Item::Numeric(numeric, Pad::Space)
}
const fn fixed(fixed: Fixed) -> Item<'static> {
Item::Fixed(fixed)
}
const fn internal_fixed(val: InternalInternal) -> Item<'static> {
Item::Fixed(Fixed::Internal(InternalFixed { val }))
}
impl Item<'_> {
/// Convert items that contain a reference to the format string into an owned variant.
#[cfg(any(feature = "alloc", feature = "std"))]
pub fn to_owned(self) -> Item<'static> {
match self {
Item::Literal(s) => Item::OwnedLiteral(Box::from(s)),
Item::Space(s) => Item::OwnedSpace(Box::from(s)),
Item::Numeric(n, p) => Item::Numeric(n, p),
Item::Fixed(f) => Item::Fixed(f),
Item::OwnedLiteral(l) => Item::OwnedLiteral(l),
Item::OwnedSpace(s) => Item::OwnedSpace(s),
Item::Error => Item::Error,
}
}
}
/// An error from the `parse` function.
#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash)]
pub struct ParseError(ParseErrorKind);
impl ParseError {
/// The category of parse error
pub const fn kind(&self) -> ParseErrorKind {
self.0
}
}
/// The category of parse error
#[allow(clippy::manual_non_exhaustive)]
#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash)]
pub enum ParseErrorKind {
/// Given field is out of permitted range.
OutOfRange,
/// There is no possible date and time value with given set of fields.
///
/// This does not include the out-of-range conditions, which are trivially invalid.
/// It includes the case that there are one or more fields that are inconsistent to each other.
Impossible,
/// Given set of fields is not enough to make a requested date and time value.
///
/// Note that there *may* be a case that given fields constrain the possible values so much
/// that there is a unique possible value. Chrono only tries to be correct for
/// most useful sets of fields however, as such constraint solving can be expensive.
NotEnough,
/// The input string has some invalid character sequence for given formatting items.
Invalid,
/// The input string has been prematurely ended.
TooShort,
/// All formatting items have been read but there is a remaining input.
TooLong,
/// There was an error on the formatting string, or there were non-supported formatting items.
BadFormat,
// TODO: Change this to `#[non_exhaustive]` (on the enum) with the next breaking release.
#[doc(hidden)]
__Nonexhaustive,
}
/// Same as `Result<T, ParseError>`.
pub type ParseResult<T> = Result<T, ParseError>;
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self.0 {
ParseErrorKind::OutOfRange => write!(f, "input is out of range"),
ParseErrorKind::Impossible => write!(f, "no possible date and time matching input"),
ParseErrorKind::NotEnough => write!(f, "input is not enough for unique date and time"),
ParseErrorKind::Invalid => write!(f, "input contains invalid characters"),
ParseErrorKind::TooShort => write!(f, "premature end of input"),
ParseErrorKind::TooLong => write!(f, "trailing input"),
ParseErrorKind::BadFormat => write!(f, "bad or unsupported format string"),
_ => unreachable!(),
}
}
}
#[cfg(feature = "std")]
impl Error for ParseError {
#[allow(deprecated)]
fn description(&self) -> &str {
"parser error, see to_string() for details"
}
}
// to be used in this module and submodules
pub(crate) const OUT_OF_RANGE: ParseError = ParseError(ParseErrorKind::OutOfRange);
const IMPOSSIBLE: ParseError = ParseError(ParseErrorKind::Impossible);
const NOT_ENOUGH: ParseError = ParseError(ParseErrorKind::NotEnough);
const INVALID: ParseError = ParseError(ParseErrorKind::Invalid);
const TOO_SHORT: ParseError = ParseError(ParseErrorKind::TooShort);
pub(crate) const TOO_LONG: ParseError = ParseError(ParseErrorKind::TooLong);
const BAD_FORMAT: ParseError = ParseError(ParseErrorKind::BadFormat);
// this implementation is here only because we need some private code from `scan`
/// Parsing a `str` into a `Weekday` uses the format [`%A`](./format/strftime/index.html).
///
/// # Example
///
/// ```
/// use chrono::Weekday;
///
/// assert_eq!("Sunday".parse::<Weekday>(), Ok(Weekday::Sun));
/// assert!("any day".parse::<Weekday>().is_err());
/// ```
///
/// The parsing is case-insensitive.
///
/// ```
/// # use chrono::Weekday;
/// assert_eq!("mON".parse::<Weekday>(), Ok(Weekday::Mon));
/// ```
///
/// Only the shortest form (e.g. `sun`) and the longest form (e.g. `sunday`) is accepted.
///
/// ```
/// # use chrono::Weekday;
/// assert!("thurs".parse::<Weekday>().is_err());
/// ```
impl FromStr for Weekday {
type Err = ParseWeekdayError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Ok(("", w)) = scan::short_or_long_weekday(s) {
Ok(w)
} else {
Err(ParseWeekdayError { _dummy: () })
}
}
}
/// Parsing a `str` into a `Month` uses the format [`%B`](./format/strftime/index.html).
///
/// # Example
///
/// ```
/// use chrono::Month;
///
/// assert_eq!("January".parse::<Month>(), Ok(Month::January));
/// assert!("any day".parse::<Month>().is_err());
/// ```
///
/// The parsing is case-insensitive.
///
/// ```
/// # use chrono::Month;
/// assert_eq!("fEbruARy".parse::<Month>(), Ok(Month::February));
/// ```
///
/// Only the shortest form (e.g. `jan`) and the longest form (e.g. `january`) is accepted.
///
/// ```
/// # use chrono::Month;
/// assert!("septem".parse::<Month>().is_err());
/// assert!("Augustin".parse::<Month>().is_err());
/// ```
impl FromStr for Month {
type Err = ParseMonthError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Ok(("", w)) = scan::short_or_long_month0(s) {
match w {
0 => Ok(Month::January),
1 => Ok(Month::February),
2 => Ok(Month::March),
3 => Ok(Month::April),
4 => Ok(Month::May),
5 => Ok(Month::June),
6 => Ok(Month::July),
7 => Ok(Month::August),
8 => Ok(Month::September),
9 => Ok(Month::October),
10 => Ok(Month::November),
11 => Ok(Month::December),
_ => Err(ParseMonthError { _dummy: () }),
}
} else {
Err(ParseMonthError { _dummy: () })
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,431 @@
// This is a part of Chrono.
// See README.md and LICENSE.txt for details.
/*!
* Various scanning routines for the parser.
*/
use super::{INVALID, OUT_OF_RANGE, ParseResult, TOO_SHORT};
use crate::Weekday;
/// Tries to parse the non-negative number from `min` to `max` digits.
///
/// The absence of digits at all is an unconditional error.
/// More than `max` digits are consumed up to the first `max` digits.
/// Any number that does not fit in `i64` is an error.
#[inline]
pub(super) fn number(s: &str, min: usize, max: usize) -> ParseResult<(&str, i64)> {
assert!(min <= max);
// We are only interested in ascii numbers, so we can work with the `str` as bytes. We stop on
// the first non-numeric byte, which may be another ascii character or beginning of multi-byte
// UTF-8 character.
let bytes = s.as_bytes();
if bytes.len() < min {
return Err(TOO_SHORT);
}
let mut n = 0i64;
for (i, c) in bytes.iter().take(max).cloned().enumerate() {
// cloned() = copied()
if !c.is_ascii_digit() {
if i < min {
return Err(INVALID);
} else {
return Ok((&s[i..], n));
}
}
n = match n.checked_mul(10).and_then(|n| n.checked_add((c - b'0') as i64)) {
Some(n) => n,
None => return Err(OUT_OF_RANGE),
};
}
Ok((&s[core::cmp::min(max, bytes.len())..], n))
}
/// Tries to consume at least one digits as a fractional second.
/// Returns the number of whole nanoseconds (0--999,999,999).
pub(super) fn nanosecond(s: &str) -> ParseResult<(&str, i64)> {
// record the number of digits consumed for later scaling.
let origlen = s.len();
let (s, v) = number(s, 1, 9)?;
let consumed = origlen - s.len();
// scale the number accordingly.
static SCALE: [i64; 10] =
[0, 100_000_000, 10_000_000, 1_000_000, 100_000, 10_000, 1_000, 100, 10, 1];
let v = v.checked_mul(SCALE[consumed]).ok_or(OUT_OF_RANGE)?;
// if there are more than 9 digits, skip next digits.
let s = s.trim_start_matches(|c: char| c.is_ascii_digit());
Ok((s, v))
}
/// Tries to consume a fixed number of digits as a fractional second.
/// Returns the number of whole nanoseconds (0--999,999,999).
pub(super) fn nanosecond_fixed(s: &str, digits: usize) -> ParseResult<(&str, i64)> {
// record the number of digits consumed for later scaling.
let (s, v) = number(s, digits, digits)?;
// scale the number accordingly.
static SCALE: [i64; 10] =
[0, 100_000_000, 10_000_000, 1_000_000, 100_000, 10_000, 1_000, 100, 10, 1];
let v = v.checked_mul(SCALE[digits]).ok_or(OUT_OF_RANGE)?;
Ok((s, v))
}
/// Tries to parse the month index (0 through 11) with the first three ASCII letters.
pub(super) fn short_month0(s: &str) -> ParseResult<(&str, u8)> {
if s.len() < 3 {
return Err(TOO_SHORT);
}
let buf = s.as_bytes();
let month0 = match (buf[0] | 32, buf[1] | 32, buf[2] | 32) {
(b'j', b'a', b'n') => 0,
(b'f', b'e', b'b') => 1,
(b'm', b'a', b'r') => 2,
(b'a', b'p', b'r') => 3,
(b'm', b'a', b'y') => 4,
(b'j', b'u', b'n') => 5,
(b'j', b'u', b'l') => 6,
(b'a', b'u', b'g') => 7,
(b's', b'e', b'p') => 8,
(b'o', b'c', b't') => 9,
(b'n', b'o', b'v') => 10,
(b'd', b'e', b'c') => 11,
_ => return Err(INVALID),
};
Ok((&s[3..], month0))
}
/// Tries to parse the weekday with the first three ASCII letters.
pub(super) fn short_weekday(s: &str) -> ParseResult<(&str, Weekday)> {
if s.len() < 3 {
return Err(TOO_SHORT);
}
let buf = s.as_bytes();
let weekday = match (buf[0] | 32, buf[1] | 32, buf[2] | 32) {
(b'm', b'o', b'n') => Weekday::Mon,
(b't', b'u', b'e') => Weekday::Tue,
(b'w', b'e', b'd') => Weekday::Wed,
(b't', b'h', b'u') => Weekday::Thu,
(b'f', b'r', b'i') => Weekday::Fri,
(b's', b'a', b't') => Weekday::Sat,
(b's', b'u', b'n') => Weekday::Sun,
_ => return Err(INVALID),
};
Ok((&s[3..], weekday))
}
/// Tries to parse the month index (0 through 11) with short or long month names.
/// It prefers long month names to short month names when both are possible.
pub(super) fn short_or_long_month0(s: &str) -> ParseResult<(&str, u8)> {
// lowercased month names, minus first three chars
static LONG_MONTH_SUFFIXES: [&[u8]; 12] = [
b"uary", b"ruary", b"ch", b"il", b"", b"e", b"y", b"ust", b"tember", b"ober", b"ember",
b"ember",
];
let (mut s, month0) = short_month0(s)?;
// tries to consume the suffix if possible
let suffix = LONG_MONTH_SUFFIXES[month0 as usize];
if s.len() >= suffix.len() && s.as_bytes()[..suffix.len()].eq_ignore_ascii_case(suffix) {
s = &s[suffix.len()..];
}
Ok((s, month0))
}
/// Tries to parse the weekday with short or long weekday names.
/// It prefers long weekday names to short weekday names when both are possible.
pub(super) fn short_or_long_weekday(s: &str) -> ParseResult<(&str, Weekday)> {
// lowercased weekday names, minus first three chars
static LONG_WEEKDAY_SUFFIXES: [&[u8]; 7] =
[b"day", b"sday", b"nesday", b"rsday", b"day", b"urday", b"day"];
let (mut s, weekday) = short_weekday(s)?;
// tries to consume the suffix if possible
let suffix = LONG_WEEKDAY_SUFFIXES[weekday.num_days_from_monday() as usize];
if s.len() >= suffix.len() && s.as_bytes()[..suffix.len()].eq_ignore_ascii_case(suffix) {
s = &s[suffix.len()..];
}
Ok((s, weekday))
}
/// Tries to consume exactly one given character.
pub(super) fn char(s: &str, c1: u8) -> ParseResult<&str> {
match s.as_bytes().first() {
Some(&c) if c == c1 => Ok(&s[1..]),
Some(_) => Err(INVALID),
None => Err(TOO_SHORT),
}
}
/// Tries to consume one or more whitespace.
pub(super) fn space(s: &str) -> ParseResult<&str> {
let s_ = s.trim_start();
if s_.len() < s.len() {
Ok(s_)
} else if s.is_empty() {
Err(TOO_SHORT)
} else {
Err(INVALID)
}
}
/// Consumes any number (including zero) of colon or spaces.
pub(crate) fn colon_or_space(s: &str) -> ParseResult<&str> {
Ok(s.trim_start_matches(|c: char| c == ':' || c.is_whitespace()))
}
/// Parse a timezone from `s` and return the offset in seconds.
///
/// The `consume_colon` function is used to parse a mandatory or optional `:`
/// separator between hours offset and minutes offset.
///
/// The `allow_missing_minutes` flag allows the timezone minutes offset to be
/// missing from `s`.
///
/// The `allow_tz_minus_sign` flag allows the timezone offset negative character
/// to also be `` MINUS SIGN (U+2212) in addition to the typical
/// ASCII-compatible `-` HYPHEN-MINUS (U+2D).
/// This is part of [RFC 3339 & ISO 8601].
///
/// [RFC 3339 & ISO 8601]: https://en.wikipedia.org/w/index.php?title=ISO_8601&oldid=1114309368#Time_offsets_from_UTC
pub(crate) fn timezone_offset<F>(
mut s: &str,
mut consume_colon: F,
allow_zulu: bool,
allow_missing_minutes: bool,
allow_tz_minus_sign: bool,
) -> ParseResult<(&str, i32)>
where
F: FnMut(&str) -> ParseResult<&str>,
{
if allow_zulu {
if let Some(&b'Z' | &b'z') = s.as_bytes().first() {
return Ok((&s[1..], 0));
}
}
const fn digits(s: &str) -> ParseResult<(u8, u8)> {
let b = s.as_bytes();
if b.len() < 2 { Err(TOO_SHORT) } else { Ok((b[0], b[1])) }
}
let negative = match s.chars().next() {
Some('+') => {
// PLUS SIGN (U+2B)
s = &s['+'.len_utf8()..];
false
}
Some('-') => {
// HYPHEN-MINUS (U+2D)
s = &s['-'.len_utf8()..];
true
}
Some('') => {
// MINUS SIGN (U+2212)
if !allow_tz_minus_sign {
return Err(INVALID);
}
s = &s[''.len_utf8()..];
true
}
Some(_) => return Err(INVALID),
None => return Err(TOO_SHORT),
};
// hours (00--99)
let hours = match digits(s)? {
(h1 @ b'0'..=b'9', h2 @ b'0'..=b'9') => i32::from((h1 - b'0') * 10 + (h2 - b'0')),
_ => return Err(INVALID),
};
s = &s[2..];
// colons (and possibly other separators)
s = consume_colon(s)?;
// minutes (00--59)
// if the next two items are digits then we have to add minutes
let minutes = if let Ok(ds) = digits(s) {
match ds {
(m1 @ b'0'..=b'5', m2 @ b'0'..=b'9') => i32::from((m1 - b'0') * 10 + (m2 - b'0')),
(b'6'..=b'9', b'0'..=b'9') => return Err(OUT_OF_RANGE),
_ => return Err(INVALID),
}
} else if allow_missing_minutes {
0
} else {
return Err(TOO_SHORT);
};
s = match s.len() {
len if len >= 2 => &s[2..],
0 => s,
_ => return Err(TOO_SHORT),
};
let seconds = hours * 3600 + minutes * 60;
Ok((s, if negative { -seconds } else { seconds }))
}
/// Same as `timezone_offset` but also allows for RFC 2822 legacy timezones.
/// May return `None` which indicates an insufficient offset data (i.e. `-0000`).
/// See [RFC 2822 Section 4.3].
///
/// [RFC 2822 Section 4.3]: https://tools.ietf.org/html/rfc2822#section-4.3
pub(super) fn timezone_offset_2822(s: &str) -> ParseResult<(&str, i32)> {
// tries to parse legacy time zone names
let upto = s.as_bytes().iter().position(|&c| !c.is_ascii_alphabetic()).unwrap_or(s.len());
if upto > 0 {
let name = &s.as_bytes()[..upto];
let s = &s[upto..];
let offset_hours = |o| Ok((s, o * 3600));
// RFC 2822 requires support for some named North America timezones, a small subset of all
// named timezones.
if name.eq_ignore_ascii_case(b"gmt")
|| name.eq_ignore_ascii_case(b"ut")
|| name.eq_ignore_ascii_case(b"z")
{
return offset_hours(0);
} else if name.eq_ignore_ascii_case(b"edt") {
return offset_hours(-4);
} else if name.eq_ignore_ascii_case(b"est") || name.eq_ignore_ascii_case(b"cdt") {
return offset_hours(-5);
} else if name.eq_ignore_ascii_case(b"cst") || name.eq_ignore_ascii_case(b"mdt") {
return offset_hours(-6);
} else if name.eq_ignore_ascii_case(b"mst") || name.eq_ignore_ascii_case(b"pdt") {
return offset_hours(-7);
} else if name.eq_ignore_ascii_case(b"pst") {
return offset_hours(-8);
} else if name.len() == 1 {
if let b'a'..=b'i' | b'k'..=b'y' | b'A'..=b'I' | b'K'..=b'Y' = name[0] {
// recommended by RFC 2822: consume but treat it as -0000
return Ok((s, 0));
}
}
Err(INVALID)
} else {
timezone_offset(s, |s| Ok(s), false, false, false)
}
}
/// Tries to consume an RFC2822 comment including preceding ` `.
///
/// Returns the remaining string after the closing parenthesis.
pub(super) fn comment_2822(s: &str) -> ParseResult<(&str, ())> {
use CommentState::*;
let s = s.trim_start();
let mut state = Start;
for (i, c) in s.bytes().enumerate() {
state = match (state, c) {
(Start, b'(') => Next(1),
(Next(1), b')') => return Ok((&s[i + 1..], ())),
(Next(depth), b'\\') => Escape(depth),
(Next(depth), b'(') => Next(depth + 1),
(Next(depth), b')') => Next(depth - 1),
(Next(depth), _) | (Escape(depth), _) => Next(depth),
_ => return Err(INVALID),
};
}
Err(TOO_SHORT)
}
enum CommentState {
Start,
Next(usize),
Escape(usize),
}
#[cfg(test)]
mod tests {
use super::{
comment_2822, nanosecond, nanosecond_fixed, short_or_long_month0, short_or_long_weekday,
timezone_offset_2822,
};
use crate::Weekday;
use crate::format::{INVALID, TOO_SHORT};
#[test]
fn test_rfc2822_comments() {
let testdata = [
("", Err(TOO_SHORT)),
(" ", Err(TOO_SHORT)),
("x", Err(INVALID)),
("(", Err(TOO_SHORT)),
("()", Ok("")),
(" \r\n\t()", Ok("")),
("() ", Ok(" ")),
("()z", Ok("z")),
("(x)", Ok("")),
("(())", Ok("")),
("((()))", Ok("")),
("(x(x(x)x)x)", Ok("")),
("( x ( x ( x ) x ) x )", Ok("")),
(r"(\)", Err(TOO_SHORT)),
(r"(\()", Ok("")),
(r"(\))", Ok("")),
(r"(\\)", Ok("")),
("(()())", Ok("")),
("( x ( x ) x ( x ) x )", Ok("")),
];
for (test_in, expected) in testdata.iter() {
let actual = comment_2822(test_in).map(|(s, _)| s);
assert_eq!(
*expected, actual,
"{test_in:?} expected to produce {expected:?}, but produced {actual:?}."
);
}
}
#[test]
fn test_timezone_offset_2822() {
assert_eq!(timezone_offset_2822("cSt").unwrap(), ("", -21600));
assert_eq!(timezone_offset_2822("pSt").unwrap(), ("", -28800));
assert_eq!(timezone_offset_2822("mSt").unwrap(), ("", -25200));
assert_eq!(timezone_offset_2822("-1551").unwrap(), ("", -57060));
assert_eq!(timezone_offset_2822("Gp"), Err(INVALID));
}
#[test]
fn test_short_or_long_month0() {
assert_eq!(short_or_long_month0("JUn").unwrap(), ("", 5));
assert_eq!(short_or_long_month0("mAy").unwrap(), ("", 4));
assert_eq!(short_or_long_month0("AuG").unwrap(), ("", 7));
assert_eq!(short_or_long_month0("Aprâ").unwrap(), ("â", 3));
assert_eq!(short_or_long_month0("JUl").unwrap(), ("", 6));
assert_eq!(short_or_long_month0("mAr").unwrap(), ("", 2));
assert_eq!(short_or_long_month0("Jan").unwrap(), ("", 0));
}
#[test]
fn test_short_or_long_weekday() {
assert_eq!(short_or_long_weekday("sAtu").unwrap(), ("u", Weekday::Sat));
assert_eq!(short_or_long_weekday("thu").unwrap(), ("", Weekday::Thu));
}
#[test]
fn test_nanosecond_fixed() {
assert_eq!(nanosecond_fixed("", 0usize).unwrap(), ("", 0));
assert!(nanosecond_fixed("", 1usize).is_err());
}
#[test]
fn test_nanosecond() {
assert_eq!(nanosecond("").unwrap(), ("Ù", 200000000));
assert_eq!(nanosecond("8").unwrap(), ("", 800000000));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,735 @@
//! # Chrono: Date and Time for Rust
//!
//! Chrono aims to provide all functionality needed to do correct operations on dates and times in
//! the [proleptic Gregorian calendar]:
//!
//! * The [`DateTime`] type is timezone-aware by default, with separate timezone-naive types.
//! * Operations that may produce an invalid or ambiguous date and time return `Option` or
//! [`MappedLocalTime`].
//! * Configurable parsing and formatting with a `strftime` inspired date and time formatting
//! syntax.
//! * The [`Local`] timezone works with the current timezone of the OS.
//! * Types and operations are implemented to be reasonably efficient.
//!
//! Timezone data is not shipped with chrono by default to limit binary sizes. Use the companion
//! crate [Chrono-TZ] or [`tzfile`] for full timezone support.
//!
//! [proleptic Gregorian calendar]: https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar
//! [Chrono-TZ]: https://crates.io/crates/chrono-tz
//! [`tzfile`]: https://crates.io/crates/tzfile
//!
//! ### Features
//!
//! Chrono supports various runtime environments and operating systems, and has several features
//! that may be enabled or disabled.
//!
//! Default features:
//!
//! - `alloc`: Enable features that depend on allocation (primarily string formatting).
//! - `std`: Enables functionality that depends on the standard library. This is a superset of
//! `alloc` and adds interoperation with standard library types and traits.
//! - `clock`: Enables reading the local timezone (`Local`). This is a superset of `now`.
//! - `now`: Enables reading the system time (`now`).
//! - `wasmbind`: Interface with the JS Date API for the `wasm32` target.
//!
//! Optional features:
//!
//! - `serde`: Enable serialization/deserialization via [serde].
//! - `rkyv`: Deprecated, use the `rkyv-*` features.
//! - `rkyv-16`: Enable serialization/deserialization via [rkyv],
//! using 16-bit integers for integral `*size` types.
//! - `rkyv-32`: Enable serialization/deserialization via [rkyv],
//! using 32-bit integers for integral `*size` types.
//! - `rkyv-64`: Enable serialization/deserialization via [rkyv],
//! using 64-bit integers for integral `*size` types.
//! - `rkyv-validation`: Enable rkyv validation support using `bytecheck`.
//! - `arbitrary`: Construct arbitrary instances of a type with the Arbitrary crate.
//! - `unstable-locales`: Enable localization. This adds various methods with a `_localized` suffix.
//! The implementation and API may change or even be removed in a patch release. Feedback welcome.
//! - `oldtime`: This feature no longer has any effect; it used to offer compatibility with the
//! `time` 0.1 crate.
//!
//! Note: The `rkyv{,-16,-32,-64}` features are mutually exclusive.
//!
//! See the [cargo docs] for examples of specifying features.
//!
//! [serde]: https://github.com/serde-rs/serde
//! [rkyv]: https://github.com/rkyv/rkyv
//! [cargo docs]: https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#choosing-features
//!
//! ## Overview
//!
//! ### Time delta / Duration
//!
//! Chrono has a [`TimeDelta`] type to represent the magnitude of a time span. This is an "accurate"
//! duration represented as seconds and nanoseconds, and does not represent "nominal" components
//! such as days or months.
//!
//! The [`TimeDelta`] type was previously named `Duration` (and is still available as a type alias
//! with that name). A notable difference with the similar [`core::time::Duration`] is that it is a
//! signed value instead of unsigned.
//!
//! Chrono currently only supports a small number of operations with [`core::time::Duration`].
//! You can convert between both types with the [`TimeDelta::from_std`] and [`TimeDelta::to_std`]
//! methods.
//!
//! ### Date and Time
//!
//! Chrono provides a [`DateTime`] type to represent a date and a time in a timezone.
//!
//! For more abstract moment-in-time tracking such as internal timekeeping that is unconcerned with
//! timezones, consider [`std::time::SystemTime`], which tracks your system clock, or
//! [`std::time::Instant`], which is an opaque but monotonically-increasing representation of a
//! moment in time.
//!
//! [`DateTime`] is timezone-aware and must be constructed from a [`TimeZone`] object, which defines
//! how the local date is converted to and back from the UTC date.
//! There are three well-known [`TimeZone`] implementations:
//!
//! * [`Utc`] specifies the UTC time zone. It is most efficient.
//!
//! * [`Local`] specifies the system local time zone.
//!
//! * [`FixedOffset`] specifies an arbitrary, fixed time zone such as UTC+09:00 or UTC-10:30.
//! This often results from the parsed textual date and time. Since it stores the most information
//! and does not depend on the system environment, you would want to normalize other `TimeZone`s
//! into this type.
//!
//! [`DateTime`]s with different [`TimeZone`] types are distinct and do not mix, but can be
//! converted to each other using the [`DateTime::with_timezone`] method.
//!
//! You can get the current date and time in the UTC time zone ([`Utc::now()`]) or in the local time
//! zone ([`Local::now()`]).
//!
//! ```
//! # #[cfg(feature = "now")] {
//! use chrono::prelude::*;
//!
//! let utc: DateTime<Utc> = Utc::now(); // e.g. `2014-11-28T12:45:59.324310806Z`
//! # let _ = utc;
//! # }
//! ```
//!
//! ```
//! # #[cfg(feature = "clock")] {
//! use chrono::prelude::*;
//!
//! let local: DateTime<Local> = Local::now(); // e.g. `2014-11-28T21:45:59.324310806+09:00`
//! # let _ = local;
//! # }
//! ```
//!
//! Alternatively, you can create your own date and time. This is a bit verbose due to Rust's lack
//! of function and method overloading, but in turn we get a rich combination of initialization
//! methods.
//!
//! ```
//! use chrono::offset::MappedLocalTime;
//! use chrono::prelude::*;
//!
//! # fn doctest() -> Option<()> {
//!
//! let dt = Utc.with_ymd_and_hms(2014, 7, 8, 9, 10, 11).unwrap(); // `2014-07-08T09:10:11Z`
//! assert_eq!(
//! dt,
//! NaiveDate::from_ymd_opt(2014, 7, 8)?
//! .and_hms_opt(9, 10, 11)?
//! .and_utc()
//! );
//!
//! // July 8 is 188th day of the year 2014 (`o` for "ordinal")
//! assert_eq!(dt, NaiveDate::from_yo_opt(2014, 189)?.and_hms_opt(9, 10, 11)?.and_utc());
//! // July 8 is Tuesday in ISO week 28 of the year 2014.
//! assert_eq!(
//! dt,
//! NaiveDate::from_isoywd_opt(2014, 28, Weekday::Tue)?.and_hms_opt(9, 10, 11)?.and_utc()
//! );
//!
//! let dt = NaiveDate::from_ymd_opt(2014, 7, 8)?
//! .and_hms_milli_opt(9, 10, 11, 12)?
//! .and_utc(); // `2014-07-08T09:10:11.012Z`
//! assert_eq!(
//! dt,
//! NaiveDate::from_ymd_opt(2014, 7, 8)?
//! .and_hms_micro_opt(9, 10, 11, 12_000)?
//! .and_utc()
//! );
//! assert_eq!(
//! dt,
//! NaiveDate::from_ymd_opt(2014, 7, 8)?
//! .and_hms_nano_opt(9, 10, 11, 12_000_000)?
//! .and_utc()
//! );
//!
//! // dynamic verification
//! assert_eq!(
//! Utc.with_ymd_and_hms(2014, 7, 8, 21, 15, 33),
//! MappedLocalTime::Single(
//! NaiveDate::from_ymd_opt(2014, 7, 8)?.and_hms_opt(21, 15, 33)?.and_utc()
//! )
//! );
//! assert_eq!(Utc.with_ymd_and_hms(2014, 7, 8, 80, 15, 33), MappedLocalTime::None);
//! assert_eq!(Utc.with_ymd_and_hms(2014, 7, 38, 21, 15, 33), MappedLocalTime::None);
//!
//! # #[cfg(feature = "clock")] {
//! // other time zone objects can be used to construct a local datetime.
//! // obviously, `local_dt` is normally different from `dt`, but `fixed_dt` should be identical.
//! let local_dt = Local
//! .from_local_datetime(
//! &NaiveDate::from_ymd_opt(2014, 7, 8).unwrap().and_hms_milli_opt(9, 10, 11, 12).unwrap(),
//! )
//! .unwrap();
//! let fixed_dt = FixedOffset::east_opt(9 * 3600)
//! .unwrap()
//! .from_local_datetime(
//! &NaiveDate::from_ymd_opt(2014, 7, 8)
//! .unwrap()
//! .and_hms_milli_opt(18, 10, 11, 12)
//! .unwrap(),
//! )
//! .unwrap();
//! assert_eq!(dt, fixed_dt);
//! # let _ = local_dt;
//! # }
//! # Some(())
//! # }
//! # doctest().unwrap();
//! ```
//!
//! Various properties are available to the date and time, and can be altered individually. Most of
//! them are defined in the traits [`Datelike`] and [`Timelike`] which you should `use` before.
//! Addition and subtraction is also supported.
//! The following illustrates most supported operations to the date and time:
//!
//! ```rust
//! use chrono::prelude::*;
//! use chrono::TimeDelta;
//!
//! // assume this returned `2014-11-28T21:45:59.324310806+09:00`:
//! let dt = FixedOffset::east_opt(9 * 3600)
//! .unwrap()
//! .from_local_datetime(
//! &NaiveDate::from_ymd_opt(2014, 11, 28)
//! .unwrap()
//! .and_hms_nano_opt(21, 45, 59, 324310806)
//! .unwrap(),
//! )
//! .unwrap();
//!
//! // property accessors
//! assert_eq!((dt.year(), dt.month(), dt.day()), (2014, 11, 28));
//! assert_eq!((dt.month0(), dt.day0()), (10, 27)); // for unfortunate souls
//! assert_eq!((dt.hour(), dt.minute(), dt.second()), (21, 45, 59));
//! assert_eq!(dt.weekday(), Weekday::Fri);
//! assert_eq!(dt.weekday().number_from_monday(), 5); // Mon=1, ..., Sun=7
//! assert_eq!(dt.ordinal(), 332); // the day of year
//! assert_eq!(dt.num_days_from_ce(), 735565); // the number of days from and including Jan 1, 1
//!
//! // time zone accessor and manipulation
//! assert_eq!(dt.offset().fix().local_minus_utc(), 9 * 3600);
//! assert_eq!(dt.timezone(), FixedOffset::east_opt(9 * 3600).unwrap());
//! assert_eq!(
//! dt.with_timezone(&Utc),
//! NaiveDate::from_ymd_opt(2014, 11, 28)
//! .unwrap()
//! .and_hms_nano_opt(12, 45, 59, 324310806)
//! .unwrap()
//! .and_utc()
//! );
//!
//! // a sample of property manipulations (validates dynamically)
//! assert_eq!(dt.with_day(29).unwrap().weekday(), Weekday::Sat); // 2014-11-29 is Saturday
//! assert_eq!(dt.with_day(32), None);
//! assert_eq!(dt.with_year(-300).unwrap().num_days_from_ce(), -109606); // November 29, 301 BCE
//!
//! // arithmetic operations
//! let dt1 = Utc.with_ymd_and_hms(2014, 11, 14, 8, 9, 10).unwrap();
//! let dt2 = Utc.with_ymd_and_hms(2014, 11, 14, 10, 9, 8).unwrap();
//! assert_eq!(dt1.signed_duration_since(dt2), TimeDelta::try_seconds(-2 * 3600 + 2).unwrap());
//! assert_eq!(dt2.signed_duration_since(dt1), TimeDelta::try_seconds(2 * 3600 - 2).unwrap());
//! assert_eq!(
//! Utc.with_ymd_and_hms(1970, 1, 1, 0, 0, 0).unwrap()
//! + TimeDelta::try_seconds(1_000_000_000).unwrap(),
//! Utc.with_ymd_and_hms(2001, 9, 9, 1, 46, 40).unwrap()
//! );
//! assert_eq!(
//! Utc.with_ymd_and_hms(1970, 1, 1, 0, 0, 0).unwrap()
//! - TimeDelta::try_seconds(1_000_000_000).unwrap(),
//! Utc.with_ymd_and_hms(1938, 4, 24, 22, 13, 20).unwrap()
//! );
//! ```
//!
//! ### Formatting and Parsing
//!
//! Formatting is done via the [`format`](DateTime::format()) method, which format is equivalent to
//! the familiar `strftime` format.
//!
//! See [`format::strftime`](format::strftime#specifiers) documentation for full syntax and list of
//! specifiers.
//!
//! The default `to_string` method and `{:?}` specifier also give a reasonable representation.
//! Chrono also provides [`to_rfc2822`](DateTime::to_rfc2822) and
//! [`to_rfc3339`](DateTime::to_rfc3339) methods for well-known formats.
//!
//! Chrono now also provides date formatting in almost any language without the help of an
//! additional C library. This functionality is under the feature `unstable-locales`:
//!
//! ```toml
//! chrono = { version = "0.4", features = ["unstable-locales"] }
//! ```
//!
//! The `unstable-locales` feature requires and implies at least the `alloc` feature.
//!
//! ```rust
//! # #[allow(unused_imports)]
//! use chrono::prelude::*;
//!
//! # #[cfg(all(feature = "unstable-locales", feature = "alloc"))]
//! # fn test() {
//! let dt = Utc.with_ymd_and_hms(2014, 11, 28, 12, 0, 9).unwrap();
//! assert_eq!(dt.format("%Y-%m-%d %H:%M:%S").to_string(), "2014-11-28 12:00:09");
//! assert_eq!(dt.format("%a %b %e %T %Y").to_string(), "Fri Nov 28 12:00:09 2014");
//! assert_eq!(
//! dt.format_localized("%A %e %B %Y, %T", Locale::fr_BE).to_string(),
//! "vendredi 28 novembre 2014, 12:00:09"
//! );
//!
//! assert_eq!(dt.format("%a %b %e %T %Y").to_string(), dt.format("%c").to_string());
//! assert_eq!(dt.to_string(), "2014-11-28 12:00:09 UTC");
//! assert_eq!(dt.to_rfc2822(), "Fri, 28 Nov 2014 12:00:09 +0000");
//! assert_eq!(dt.to_rfc3339(), "2014-11-28T12:00:09+00:00");
//! assert_eq!(format!("{:?}", dt), "2014-11-28T12:00:09Z");
//!
//! // Note that milli/nanoseconds are only printed if they are non-zero
//! let dt_nano = NaiveDate::from_ymd_opt(2014, 11, 28)
//! .unwrap()
//! .and_hms_nano_opt(12, 0, 9, 1)
//! .unwrap()
//! .and_utc();
//! assert_eq!(format!("{:?}", dt_nano), "2014-11-28T12:00:09.000000001Z");
//! # }
//! # #[cfg(not(all(feature = "unstable-locales", feature = "alloc")))]
//! # fn test() {}
//! # if cfg!(all(feature = "unstable-locales", feature = "alloc")) {
//! # test();
//! # }
//! ```
//!
//! Parsing can be done with two methods:
//!
//! 1. The standard [`FromStr`](std::str::FromStr) trait (and [`parse`](str::parse) method on a
//! string) can be used for parsing `DateTime<FixedOffset>`, `DateTime<Utc>` and
//! `DateTime<Local>` values. This parses what the `{:?}` ([`std::fmt::Debug`] format specifier
//! prints, and requires the offset to be present.
//!
//! 2. [`DateTime::parse_from_str`] parses a date and time with offsets and returns
//! `DateTime<FixedOffset>`. This should be used when the offset is a part of input and the
//! caller cannot guess that. It *cannot* be used when the offset can be missing.
//! [`DateTime::parse_from_rfc2822`] and [`DateTime::parse_from_rfc3339`] are similar but for
//! well-known formats.
//!
//! More detailed control over the parsing process is available via [`format`](mod@format) module.
//!
//! ```rust
//! use chrono::prelude::*;
//!
//! let dt = Utc.with_ymd_and_hms(2014, 11, 28, 12, 0, 9).unwrap();
//! let fixed_dt = dt.with_timezone(&FixedOffset::east_opt(9 * 3600).unwrap());
//!
//! // method 1
//! assert_eq!("2014-11-28T12:00:09Z".parse::<DateTime<Utc>>(), Ok(dt.clone()));
//! assert_eq!("2014-11-28T21:00:09+09:00".parse::<DateTime<Utc>>(), Ok(dt.clone()));
//! assert_eq!("2014-11-28T21:00:09+09:00".parse::<DateTime<FixedOffset>>(), Ok(fixed_dt.clone()));
//!
//! // method 2
//! assert_eq!(
//! DateTime::parse_from_str("2014-11-28 21:00:09 +09:00", "%Y-%m-%d %H:%M:%S %z"),
//! Ok(fixed_dt.clone())
//! );
//! assert_eq!(
//! DateTime::parse_from_rfc2822("Fri, 28 Nov 2014 21:00:09 +0900"),
//! Ok(fixed_dt.clone())
//! );
//! assert_eq!(DateTime::parse_from_rfc3339("2014-11-28T21:00:09+09:00"), Ok(fixed_dt.clone()));
//!
//! // oops, the year is missing!
//! assert!(DateTime::parse_from_str("Fri Nov 28 12:00:09", "%a %b %e %T %Y").is_err());
//! // oops, the format string does not include the year at all!
//! assert!(DateTime::parse_from_str("Fri Nov 28 12:00:09", "%a %b %e %T").is_err());
//! // oops, the weekday is incorrect!
//! assert!(DateTime::parse_from_str("Sat Nov 28 12:00:09 2014", "%a %b %e %T %Y").is_err());
//! ```
//!
//! Again: See [`format::strftime`](format::strftime#specifiers) documentation for full syntax and
//! list of specifiers.
//!
//! ### Conversion from and to EPOCH timestamps
//!
//! Use [`DateTime::from_timestamp(seconds, nanoseconds)`](DateTime::from_timestamp)
//! to construct a [`DateTime<Utc>`] from a UNIX timestamp
//! (seconds, nanoseconds that passed since January 1st 1970).
//!
//! Use [`DateTime.timestamp`](DateTime::timestamp) to get the timestamp (in seconds)
//! from a [`DateTime`]. Additionally, you can use
//! [`DateTime.timestamp_subsec_nanos`](DateTime::timestamp_subsec_nanos)
//! to get the number of additional number of nanoseconds.
//!
//! ```
//! # #[cfg(feature = "alloc")] {
//! // We need the trait in scope to use Utc::timestamp().
//! use chrono::{DateTime, Utc};
//!
//! // Construct a datetime from epoch:
//! let dt: DateTime<Utc> = DateTime::from_timestamp(1_500_000_000, 0).unwrap();
//! assert_eq!(dt.to_rfc2822(), "Fri, 14 Jul 2017 02:40:00 +0000");
//!
//! // Get epoch value from a datetime:
//! let dt = DateTime::parse_from_rfc2822("Fri, 14 Jul 2017 02:40:00 +0000").unwrap();
//! assert_eq!(dt.timestamp(), 1_500_000_000);
//! # }
//! ```
//!
//! ### Naive date and time
//!
//! Chrono provides naive counterparts to `Date`, (non-existent) `Time` and `DateTime` as
//! [`NaiveDate`], [`NaiveTime`] and [`NaiveDateTime`] respectively.
//!
//! They have almost equivalent interfaces as their timezone-aware twins, but are not associated to
//! time zones obviously and can be quite low-level. They are mostly useful for building blocks for
//! higher-level types.
//!
//! Timezone-aware `DateTime` and `Date` types have two methods returning naive versions:
//! [`naive_local`](DateTime::naive_local) returns a view to the naive local time,
//! and [`naive_utc`](DateTime::naive_utc) returns a view to the naive UTC time.
//!
//! ## Limitations
//!
//! * Only the proleptic Gregorian calendar (i.e. extended to support older dates) is supported.
//! * Date types are limited to about +/- 262,000 years from the common epoch.
//! * Time types are limited to nanosecond accuracy.
//! * Leap seconds can be represented, but Chrono does not fully support them.
//! See [Leap Second Handling](NaiveTime#leap-second-handling).
//!
//! ## Rust version requirements
//!
//! The Minimum Supported Rust Version (MSRV) is currently **Rust 1.61.0**.
//!
//! The MSRV is explicitly tested in CI. It may be bumped in minor releases, but this is not done
//! lightly.
//!
//! ## Relation between chrono and time 0.1
//!
//! Rust first had a `time` module added to `std` in its 0.7 release. It later moved to
//! `libextra`, and then to a `libtime` library shipped alongside the standard library. In 2014
//! work on chrono started in order to provide a full-featured date and time library in Rust.
//! Some improvements from chrono made it into the standard library; notably, `chrono::Duration`
//! was included as `std::time::Duration` ([rust#15934]) in 2014.
//!
//! In preparation of Rust 1.0 at the end of 2014 `libtime` was moved out of the Rust distro and
//! into the `time` crate to eventually be redesigned ([rust#18832], [rust#18858]), like the
//! `num` and `rand` crates. Of course chrono kept its dependency on this `time` crate. `time`
//! started re-exporting `std::time::Duration` during this period. Later, the standard library was
//! changed to have a more limited unsigned `Duration` type ([rust#24920], [RFC 1040]), while the
//! `time` crate kept the full functionality with `time::Duration`. `time::Duration` had been a
//! part of chrono's public API.
//!
//! By 2016 `time` 0.1 lived under the `rust-lang-deprecated` organisation and was not actively
//! maintained ([time#136]). chrono absorbed the platform functionality and `Duration` type of the
//! `time` crate in [chrono#478] (the work started in [chrono#286]). In order to preserve
//! compatibility with downstream crates depending on `time` and `chrono` sharing a `Duration`
//! type, chrono kept depending on time 0.1. chrono offered the option to opt out of the `time`
//! dependency by disabling the `oldtime` feature (swapping it out for an effectively similar
//! chrono type). In 2019, @jhpratt took over maintenance on the `time` crate and released what
//! amounts to a new crate as `time` 0.2.
//!
//! [rust#15934]: https://github.com/rust-lang/rust/pull/15934
//! [rust#18832]: https://github.com/rust-lang/rust/pull/18832#issuecomment-62448221
//! [rust#18858]: https://github.com/rust-lang/rust/pull/18858
//! [rust#24920]: https://github.com/rust-lang/rust/pull/24920
//! [RFC 1040]: https://rust-lang.github.io/rfcs/1040-duration-reform.html
//! [time#136]: https://github.com/time-rs/time/issues/136
//! [chrono#286]: https://github.com/chronotope/chrono/pull/286
//! [chrono#478]: https://github.com/chronotope/chrono/pull/478
//!
//! ## Security advisories
//!
//! In November of 2020 [CVE-2020-26235] and [RUSTSEC-2020-0071] were opened against the `time` crate.
//! @quininer had found that calls to `localtime_r` may be unsound ([chrono#499]). Eventually, almost
//! a year later, this was also made into a security advisory against chrono as [RUSTSEC-2020-0159],
//! which had platform code similar to `time`.
//!
//! On Unix-like systems a process is given a timezone id or description via the `TZ` environment
//! variable. We need this timezone data to calculate the current local time from a value that is
//! in UTC, such as the time from the system clock. `time` 0.1 and chrono used the POSIX function
//! `localtime_r` to do the conversion to local time, which reads the `TZ` variable.
//!
//! Rust assumes the environment to be writable and uses locks to access it from multiple threads.
//! Some other programming languages and libraries use similar locking strategies, but these are
//! typically not shared across languages. More importantly, POSIX declares modifying the
//! environment in a multi-threaded process as unsafe, and `getenv` in libc can't be changed to
//! take a lock because it returns a pointer to the data (see [rust#27970] for more discussion).
//!
//! Since version 4.20 chrono no longer uses `localtime_r`, instead using Rust code to query the
//! timezone (from the `TZ` variable or via `iana-time-zone` as a fallback) and work with data
//! from the system timezone database directly. The code for this was forked from the [tz-rs crate]
//! by @x-hgg-x. As such, chrono now respects the Rust lock when reading the `TZ` environment
//! variable. In general, code should avoid modifying the environment.
//!
//! [CVE-2020-26235]: https://nvd.nist.gov/vuln/detail/CVE-2020-26235
//! [RUSTSEC-2020-0071]: https://rustsec.org/advisories/RUSTSEC-2020-0071
//! [chrono#499]: https://github.com/chronotope/chrono/pull/499
//! [RUSTSEC-2020-0159]: https://rustsec.org/advisories/RUSTSEC-2020-0159.html
//! [rust#27970]: https://github.com/rust-lang/rust/issues/27970
//! [chrono#677]: https://github.com/chronotope/chrono/pull/677
//! [tz-rs crate]: https://crates.io/crates/tz-rs
//!
//! ## Removing time 0.1
//!
//! Because time 0.1 has been unmaintained for years, however, the security advisory mentioned
//! above has not been addressed. While chrono maintainers were careful not to break backwards
//! compatibility with the `time::Duration` type, there has been a long stream of issues from
//! users inquiring about the time 0.1 dependency with the vulnerability. We investigated the
//! potential breakage of removing the time 0.1 dependency in [chrono#1095] using a crater-like
//! experiment and determined that the potential for breaking (public) dependencies is very low.
//! We reached out to those few crates that did still depend on compatibility with time 0.1.
//!
//! As such, for chrono 0.4.30 we have decided to swap out the time 0.1 `Duration` implementation
//! for a local one that will offer a strict superset of the existing API going forward. This
//! will prevent most downstream users from being affected by the security vulnerability in time
//! 0.1 while minimizing the ecosystem impact of semver-incompatible version churn.
//!
//! [chrono#1095]: https://github.com/chronotope/chrono/pull/1095
#![doc(html_root_url = "https://docs.rs/chrono/latest/", test(attr(deny(warnings))))]
#![deny(missing_docs)]
#![deny(missing_debug_implementations)]
#![warn(unreachable_pub)]
#![deny(clippy::tests_outside_test_module)]
#![cfg_attr(not(any(feature = "std", test)), no_std)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#[cfg(feature = "alloc")]
extern crate alloc;
mod time_delta;
#[cfg(feature = "std")]
#[doc(no_inline)]
pub use time_delta::OutOfRangeError;
pub use time_delta::TimeDelta;
/// Alias of [`TimeDelta`].
pub type Duration = TimeDelta;
use core::fmt;
/// A convenience module appropriate for glob imports (`use chrono::prelude::*;`).
pub mod prelude {
#[allow(deprecated)]
pub use crate::Date;
#[cfg(feature = "clock")]
pub use crate::Local;
#[cfg(all(feature = "unstable-locales", feature = "alloc"))]
pub use crate::Locale;
pub use crate::SubsecRound;
pub use crate::{DateTime, SecondsFormat};
pub use crate::{Datelike, Month, Timelike, Weekday};
pub use crate::{FixedOffset, Utc};
pub use crate::{NaiveDate, NaiveDateTime, NaiveTime};
pub use crate::{Offset, TimeZone};
}
mod date;
#[allow(deprecated)]
pub use date::Date;
#[doc(no_inline)]
#[allow(deprecated)]
pub use date::{MAX_DATE, MIN_DATE};
mod datetime;
pub use datetime::DateTime;
#[allow(deprecated)]
#[doc(no_inline)]
pub use datetime::{MAX_DATETIME, MIN_DATETIME};
pub mod format;
/// L10n locales.
#[cfg(feature = "unstable-locales")]
pub use format::Locale;
pub use format::{ParseError, ParseResult, SecondsFormat};
pub mod naive;
#[doc(inline)]
pub use naive::{Days, NaiveDate, NaiveDateTime, NaiveTime};
pub use naive::{IsoWeek, NaiveWeek};
pub mod offset;
#[cfg(feature = "clock")]
#[doc(inline)]
pub use offset::Local;
#[doc(hidden)]
pub use offset::LocalResult;
pub use offset::MappedLocalTime;
#[doc(inline)]
pub use offset::{FixedOffset, Offset, TimeZone, Utc};
pub mod round;
pub use round::{DurationRound, RoundingError, SubsecRound};
mod weekday;
#[doc(no_inline)]
pub use weekday::ParseWeekdayError;
pub use weekday::Weekday;
mod weekday_set;
pub use weekday_set::WeekdaySet;
mod month;
#[doc(no_inline)]
pub use month::ParseMonthError;
pub use month::{Month, Months};
mod traits;
pub use traits::{Datelike, Timelike};
#[cfg(feature = "__internal_bench")]
#[doc(hidden)]
pub use naive::__BenchYearFlags;
/// Serialization/Deserialization with serde
///
/// The [`DateTime`] type has default implementations for (de)serializing to/from the [RFC 3339]
/// format. This module provides alternatives for serializing to timestamps.
///
/// The alternatives are for use with serde's [`with` annotation] combined with the module name.
/// Alternatively the individual `serialize` and `deserialize` functions in each module can be used
/// with serde's [`serialize_with`] and [`deserialize_with`] annotations.
///
/// *Available on crate feature 'serde' only.*
///
/// [RFC 3339]: https://tools.ietf.org/html/rfc3339
/// [`with` annotation]: https://serde.rs/field-attrs.html#with
/// [`serialize_with`]: https://serde.rs/field-attrs.html#serialize_with
/// [`deserialize_with`]: https://serde.rs/field-attrs.html#deserialize_with
#[cfg(feature = "serde")]
pub mod serde {
use core::fmt;
use serde::de;
pub use super::datetime::serde::*;
/// Create a custom `de::Error` with `SerdeError::InvalidTimestamp`.
pub(crate) fn invalid_ts<E, T>(value: T) -> E
where
E: de::Error,
T: fmt::Display,
{
E::custom(SerdeError::InvalidTimestamp(value))
}
enum SerdeError<T: fmt::Display> {
InvalidTimestamp(T),
}
impl<T: fmt::Display> fmt::Display for SerdeError<T> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
SerdeError::InvalidTimestamp(ts) => {
write!(f, "value is not a legal timestamp: {ts}")
}
}
}
}
}
/// Zero-copy serialization/deserialization with rkyv.
///
/// This module re-exports the `Archived*` versions of chrono's types.
#[cfg(any(feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"))]
pub mod rkyv {
pub use crate::datetime::ArchivedDateTime;
pub use crate::month::ArchivedMonth;
pub use crate::naive::date::ArchivedNaiveDate;
pub use crate::naive::datetime::ArchivedNaiveDateTime;
pub use crate::naive::isoweek::ArchivedIsoWeek;
pub use crate::naive::time::ArchivedNaiveTime;
pub use crate::offset::fixed::ArchivedFixedOffset;
#[cfg(feature = "clock")]
pub use crate::offset::local::ArchivedLocal;
pub use crate::offset::utc::ArchivedUtc;
pub use crate::time_delta::ArchivedTimeDelta;
pub use crate::weekday::ArchivedWeekday;
/// Alias of [`ArchivedTimeDelta`]
pub type ArchivedDuration = ArchivedTimeDelta;
}
/// Out of range error type used in various converting APIs
#[derive(Clone, Copy, Hash, PartialEq, Eq)]
pub struct OutOfRange {
_private: (),
}
impl OutOfRange {
const fn new() -> OutOfRange {
OutOfRange { _private: () }
}
}
impl fmt::Display for OutOfRange {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "out of range")
}
}
impl fmt::Debug for OutOfRange {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "out of range")
}
}
#[cfg(feature = "std")]
impl std::error::Error for OutOfRange {}
/// Workaround because `?` is not (yet) available in const context.
#[macro_export]
#[doc(hidden)]
macro_rules! try_opt {
($e:expr) => {
match $e {
Some(v) => v,
None => return None,
}
};
}
/// Workaround because `.expect()` is not (yet) available in const context.
pub(crate) const fn expect<T: Copy>(opt: Option<T>, msg: &str) -> T {
match opt {
Some(val) => val,
None => panic!("{}", msg),
}
}
#[cfg(test)]
mod tests {
#[cfg(feature = "clock")]
use crate::{DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, NaiveTime, Utc};
#[test]
#[allow(deprecated)]
#[cfg(feature = "clock")]
fn test_type_sizes() {
use core::mem::size_of;
assert_eq!(size_of::<NaiveDate>(), 4);
assert_eq!(size_of::<Option<NaiveDate>>(), 4);
assert_eq!(size_of::<NaiveTime>(), 8);
assert_eq!(size_of::<Option<NaiveTime>>(), 12);
assert_eq!(size_of::<NaiveDateTime>(), 12);
assert_eq!(size_of::<Option<NaiveDateTime>>(), 12);
assert_eq!(size_of::<DateTime<Utc>>(), 12);
assert_eq!(size_of::<DateTime<FixedOffset>>(), 16);
assert_eq!(size_of::<DateTime<Local>>(), 16);
assert_eq!(size_of::<Option<DateTime<FixedOffset>>>(), 16);
}
}

View File

@@ -0,0 +1,472 @@
use core::fmt;
#[cfg(any(feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"))]
use rkyv::{Archive, Deserialize, Serialize};
use crate::OutOfRange;
use crate::naive::NaiveDate;
/// The month of the year.
///
/// This enum is just a convenience implementation.
/// The month in dates created by DateLike objects does not return this enum.
///
/// It is possible to convert from a date to a month independently
/// ```
/// use chrono::prelude::*;
/// let date = Utc.with_ymd_and_hms(2019, 10, 28, 9, 10, 11).unwrap();
/// // `2019-10-28T09:10:11Z`
/// let month = Month::try_from(u8::try_from(date.month()).unwrap()).ok();
/// assert_eq!(month, Some(Month::October))
/// ```
/// Or from a Month to an integer usable by dates
/// ```
/// # use chrono::prelude::*;
/// let month = Month::January;
/// let dt = Utc.with_ymd_and_hms(2019, month.number_from_month(), 28, 9, 10, 11).unwrap();
/// assert_eq!((dt.year(), dt.month(), dt.day()), (2019, 1, 28));
/// ```
/// Allows mapping from and to month, from 1-January to 12-December.
/// Can be Serialized/Deserialized with serde
// Actual implementation is zero-indexed, API intended as 1-indexed for more intuitive behavior.
#[derive(PartialEq, Eq, Copy, Clone, Debug, Hash, PartialOrd, Ord)]
#[cfg_attr(
any(feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"),
derive(Archive, Deserialize, Serialize),
rkyv(compare(PartialEq, PartialOrd)),
rkyv(attr(derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)))
)]
#[cfg_attr(feature = "rkyv-validation", archive(check_bytes))]
#[cfg_attr(all(feature = "arbitrary", feature = "std"), derive(arbitrary::Arbitrary))]
pub enum Month {
/// January
January = 0,
/// February
February = 1,
/// March
March = 2,
/// April
April = 3,
/// May
May = 4,
/// June
June = 5,
/// July
July = 6,
/// August
August = 7,
/// September
September = 8,
/// October
October = 9,
/// November
November = 10,
/// December
December = 11,
}
impl Month {
/// The next month.
///
/// `m`: | `January` | `February` | `...` | `December`
/// ----------- | --------- | ---------- | --- | ---------
/// `m.succ()`: | `February` | `March` | `...` | `January`
#[inline]
#[must_use]
pub const fn succ(&self) -> Month {
match *self {
Month::January => Month::February,
Month::February => Month::March,
Month::March => Month::April,
Month::April => Month::May,
Month::May => Month::June,
Month::June => Month::July,
Month::July => Month::August,
Month::August => Month::September,
Month::September => Month::October,
Month::October => Month::November,
Month::November => Month::December,
Month::December => Month::January,
}
}
/// The previous month.
///
/// `m`: | `January` | `February` | `...` | `December`
/// ----------- | --------- | ---------- | --- | ---------
/// `m.pred()`: | `December` | `January` | `...` | `November`
#[inline]
#[must_use]
pub const fn pred(&self) -> Month {
match *self {
Month::January => Month::December,
Month::February => Month::January,
Month::March => Month::February,
Month::April => Month::March,
Month::May => Month::April,
Month::June => Month::May,
Month::July => Month::June,
Month::August => Month::July,
Month::September => Month::August,
Month::October => Month::September,
Month::November => Month::October,
Month::December => Month::November,
}
}
/// Returns a month-of-year number starting from January = 1.
///
/// `m`: | `January` | `February` | `...` | `December`
/// -------------------------| --------- | ---------- | --- | -----
/// `m.number_from_month()`: | 1 | 2 | `...` | 12
#[inline]
#[must_use]
pub const fn number_from_month(&self) -> u32 {
match *self {
Month::January => 1,
Month::February => 2,
Month::March => 3,
Month::April => 4,
Month::May => 5,
Month::June => 6,
Month::July => 7,
Month::August => 8,
Month::September => 9,
Month::October => 10,
Month::November => 11,
Month::December => 12,
}
}
/// Get the name of the month
///
/// ```
/// use chrono::Month;
///
/// assert_eq!(Month::January.name(), "January")
/// ```
#[must_use]
pub const fn name(&self) -> &'static str {
match *self {
Month::January => "January",
Month::February => "February",
Month::March => "March",
Month::April => "April",
Month::May => "May",
Month::June => "June",
Month::July => "July",
Month::August => "August",
Month::September => "September",
Month::October => "October",
Month::November => "November",
Month::December => "December",
}
}
/// Get the length in days of the month
///
/// Yields `None` if `year` is out of range for `NaiveDate`.
pub fn num_days(&self, year: i32) -> Option<u8> {
Some(match *self {
Month::January => 31,
Month::February => match NaiveDate::from_ymd_opt(year, 2, 1)?.leap_year() {
true => 29,
false => 28,
},
Month::March => 31,
Month::April => 30,
Month::May => 31,
Month::June => 30,
Month::July => 31,
Month::August => 31,
Month::September => 30,
Month::October => 31,
Month::November => 30,
Month::December => 31,
})
}
}
impl TryFrom<u8> for Month {
type Error = OutOfRange;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
1 => Ok(Month::January),
2 => Ok(Month::February),
3 => Ok(Month::March),
4 => Ok(Month::April),
5 => Ok(Month::May),
6 => Ok(Month::June),
7 => Ok(Month::July),
8 => Ok(Month::August),
9 => Ok(Month::September),
10 => Ok(Month::October),
11 => Ok(Month::November),
12 => Ok(Month::December),
_ => Err(OutOfRange::new()),
}
}
}
impl num_traits::FromPrimitive for Month {
/// Returns an `Option<Month>` from a i64, assuming a 1-index, January = 1.
///
/// `Month::from_i64(n: i64)`: | `1` | `2` | ... | `12`
/// ---------------------------| -------------------- | --------------------- | ... | -----
/// ``: | Some(Month::January) | Some(Month::February) | ... | Some(Month::December)
#[inline]
fn from_u64(n: u64) -> Option<Month> {
Self::from_u32(n as u32)
}
#[inline]
fn from_i64(n: i64) -> Option<Month> {
Self::from_u32(n as u32)
}
#[inline]
fn from_u32(n: u32) -> Option<Month> {
match n {
1 => Some(Month::January),
2 => Some(Month::February),
3 => Some(Month::March),
4 => Some(Month::April),
5 => Some(Month::May),
6 => Some(Month::June),
7 => Some(Month::July),
8 => Some(Month::August),
9 => Some(Month::September),
10 => Some(Month::October),
11 => Some(Month::November),
12 => Some(Month::December),
_ => None,
}
}
}
/// A duration in calendar months
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
#[cfg_attr(all(feature = "arbitrary", feature = "std"), derive(arbitrary::Arbitrary))]
pub struct Months(pub(crate) u32);
impl Months {
/// Construct a new `Months` from a number of months
pub const fn new(num: u32) -> Self {
Self(num)
}
/// Returns the total number of months in the `Months` instance.
#[inline]
pub const fn as_u32(&self) -> u32 {
self.0
}
}
/// An error resulting from reading `<Month>` value with `FromStr`.
#[derive(Clone, PartialEq, Eq)]
pub struct ParseMonthError {
pub(crate) _dummy: (),
}
#[cfg(feature = "std")]
impl std::error::Error for ParseMonthError {}
impl fmt::Display for ParseMonthError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "ParseMonthError {{ .. }}")
}
}
impl fmt::Debug for ParseMonthError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "ParseMonthError {{ .. }}")
}
}
#[cfg(feature = "serde")]
mod month_serde {
use super::Month;
use serde::{de, ser};
use core::fmt;
impl ser::Serialize for Month {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: ser::Serializer,
{
serializer.collect_str(self.name())
}
}
struct MonthVisitor;
impl de::Visitor<'_> for MonthVisitor {
type Value = Month;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("Month")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
value.parse().map_err(|_| E::custom("short (3-letter) or full month names expected"))
}
}
impl<'de> de::Deserialize<'de> for Month {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
deserializer.deserialize_str(MonthVisitor)
}
}
}
#[cfg(test)]
mod tests {
use super::Month;
use crate::{Datelike, Months, OutOfRange, TimeZone, Utc};
#[test]
fn test_month_enum_try_from() {
assert_eq!(Month::try_from(1), Ok(Month::January));
assert_eq!(Month::try_from(2), Ok(Month::February));
assert_eq!(Month::try_from(12), Ok(Month::December));
assert_eq!(Month::try_from(13), Err(OutOfRange::new()));
let date = Utc.with_ymd_and_hms(2019, 10, 28, 9, 10, 11).unwrap();
assert_eq!(Month::try_from(date.month() as u8), Ok(Month::October));
let month = Month::January;
let dt = Utc.with_ymd_and_hms(2019, month.number_from_month(), 28, 9, 10, 11).unwrap();
assert_eq!((dt.year(), dt.month(), dt.day()), (2019, 1, 28));
}
#[test]
fn test_month_enum_primitive_parse() {
use num_traits::FromPrimitive;
let jan_opt = Month::from_u32(1);
let feb_opt = Month::from_u64(2);
let dec_opt = Month::from_i64(12);
let no_month = Month::from_u32(13);
assert_eq!(jan_opt, Some(Month::January));
assert_eq!(feb_opt, Some(Month::February));
assert_eq!(dec_opt, Some(Month::December));
assert_eq!(no_month, None);
let date = Utc.with_ymd_and_hms(2019, 10, 28, 9, 10, 11).unwrap();
assert_eq!(Month::from_u32(date.month()), Some(Month::October));
let month = Month::January;
let dt = Utc.with_ymd_and_hms(2019, month.number_from_month(), 28, 9, 10, 11).unwrap();
assert_eq!((dt.year(), dt.month(), dt.day()), (2019, 1, 28));
}
#[test]
fn test_month_enum_succ_pred() {
assert_eq!(Month::January.succ(), Month::February);
assert_eq!(Month::December.succ(), Month::January);
assert_eq!(Month::January.pred(), Month::December);
assert_eq!(Month::February.pred(), Month::January);
}
#[test]
fn test_month_partial_ord() {
assert!(Month::January <= Month::January);
assert!(Month::January < Month::February);
assert!(Month::January < Month::December);
assert!(Month::July >= Month::May);
assert!(Month::September > Month::March);
}
#[test]
fn test_months_as_u32() {
assert_eq!(Months::new(0).as_u32(), 0);
assert_eq!(Months::new(1).as_u32(), 1);
assert_eq!(Months::new(u32::MAX).as_u32(), u32::MAX);
}
#[test]
#[cfg(feature = "serde")]
fn test_serde_serialize() {
use Month::*;
use serde_json::to_string;
let cases: Vec<(Month, &str)> = vec![
(January, "\"January\""),
(February, "\"February\""),
(March, "\"March\""),
(April, "\"April\""),
(May, "\"May\""),
(June, "\"June\""),
(July, "\"July\""),
(August, "\"August\""),
(September, "\"September\""),
(October, "\"October\""),
(November, "\"November\""),
(December, "\"December\""),
];
for (month, expected_str) in cases {
let string = to_string(&month).unwrap();
assert_eq!(string, expected_str);
}
}
#[test]
#[cfg(feature = "serde")]
fn test_serde_deserialize() {
use Month::*;
use serde_json::from_str;
let cases: Vec<(&str, Month)> = vec![
("\"january\"", January),
("\"jan\"", January),
("\"FeB\"", February),
("\"MAR\"", March),
("\"mar\"", March),
("\"april\"", April),
("\"may\"", May),
("\"june\"", June),
("\"JULY\"", July),
("\"august\"", August),
("\"september\"", September),
("\"October\"", October),
("\"November\"", November),
("\"DECEmbEr\"", December),
];
for (string, expected_month) in cases {
let month = from_str::<Month>(string).unwrap();
assert_eq!(month, expected_month);
}
let errors: Vec<&str> =
vec!["\"not a month\"", "\"ja\"", "\"Dece\"", "Dec", "\"Augustin\""];
for string in errors {
from_str::<Month>(string).unwrap_err();
}
}
#[test]
#[cfg(feature = "rkyv-validation")]
fn test_rkyv_validation() {
let month = Month::January;
let bytes = rkyv::to_bytes::<_, 1>(&month).unwrap();
assert_eq!(rkyv::from_bytes::<Month>(&bytes).unwrap(), month);
}
#[test]
fn num_days() {
assert_eq!(Month::January.num_days(2020), Some(31));
assert_eq!(Month::February.num_days(2020), Some(29));
assert_eq!(Month::February.num_days(2019), Some(28));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,875 @@
use super::{Days, MAX_YEAR, MIN_YEAR, Months, NaiveDate};
use crate::naive::internals::{A, AG, B, BA, C, CB, D, DC, E, ED, F, FE, G, GF, YearFlags};
use crate::{Datelike, TimeDelta, Weekday};
// as it is hard to verify year flags in `NaiveDate::MIN` and `NaiveDate::MAX`,
// we use a separate run-time test.
#[test]
fn test_date_bounds() {
let calculated_min = NaiveDate::from_ymd_opt(MIN_YEAR, 1, 1).unwrap();
let calculated_max = NaiveDate::from_ymd_opt(MAX_YEAR, 12, 31).unwrap();
assert!(
NaiveDate::MIN == calculated_min,
"`NaiveDate::MIN` should have year flag {:?}",
calculated_min.year_flags()
);
assert!(
NaiveDate::MAX == calculated_max,
"`NaiveDate::MAX` should have year flag {:?} and ordinal {}",
calculated_max.year_flags(),
calculated_max.ordinal()
);
// let's also check that the entire range do not exceed 2^44 seconds
// (sometimes used for bounding `TimeDelta` against overflow)
let maxsecs = NaiveDate::MAX.signed_duration_since(NaiveDate::MIN).num_seconds();
let maxsecs = maxsecs + 86401; // also take care of DateTime
assert!(
maxsecs < (1 << MAX_BITS),
"The entire `NaiveDate` range somehow exceeds 2^{MAX_BITS} seconds"
);
const BEFORE_MIN: NaiveDate = NaiveDate::BEFORE_MIN;
assert_eq!(BEFORE_MIN.year_flags(), YearFlags::from_year(BEFORE_MIN.year()));
assert_eq!((BEFORE_MIN.month(), BEFORE_MIN.day()), (12, 31));
const AFTER_MAX: NaiveDate = NaiveDate::AFTER_MAX;
assert_eq!(AFTER_MAX.year_flags(), YearFlags::from_year(AFTER_MAX.year()));
assert_eq!((AFTER_MAX.month(), AFTER_MAX.day()), (1, 1));
}
#[test]
fn diff_months() {
// identity
assert_eq!(
NaiveDate::from_ymd_opt(2022, 8, 3).unwrap().checked_add_months(Months::new(0)),
Some(NaiveDate::from_ymd_opt(2022, 8, 3).unwrap())
);
// add with months exceeding `i32::MAX`
assert_eq!(
NaiveDate::from_ymd_opt(2022, 8, 3)
.unwrap()
.checked_add_months(Months::new(i32::MAX as u32 + 1)),
None
);
// sub with months exceeding `i32::MIN`
assert_eq!(
NaiveDate::from_ymd_opt(2022, 8, 3)
.unwrap()
.checked_sub_months(Months::new(i32::MIN.unsigned_abs() + 1)),
None
);
// add overflowing year
assert_eq!(NaiveDate::MAX.checked_add_months(Months::new(1)), None);
// add underflowing year
assert_eq!(NaiveDate::MIN.checked_sub_months(Months::new(1)), None);
// sub crossing year 0 boundary
assert_eq!(
NaiveDate::from_ymd_opt(2022, 8, 3).unwrap().checked_sub_months(Months::new(2050 * 12)),
Some(NaiveDate::from_ymd_opt(-28, 8, 3).unwrap())
);
// add crossing year boundary
assert_eq!(
NaiveDate::from_ymd_opt(2022, 8, 3).unwrap().checked_add_months(Months::new(6)),
Some(NaiveDate::from_ymd_opt(2023, 2, 3).unwrap())
);
// sub crossing year boundary
assert_eq!(
NaiveDate::from_ymd_opt(2022, 8, 3).unwrap().checked_sub_months(Months::new(10)),
Some(NaiveDate::from_ymd_opt(2021, 10, 3).unwrap())
);
// add clamping day, non-leap year
assert_eq!(
NaiveDate::from_ymd_opt(2022, 1, 29).unwrap().checked_add_months(Months::new(1)),
Some(NaiveDate::from_ymd_opt(2022, 2, 28).unwrap())
);
// add to leap day
assert_eq!(
NaiveDate::from_ymd_opt(2022, 10, 29).unwrap().checked_add_months(Months::new(16)),
Some(NaiveDate::from_ymd_opt(2024, 2, 29).unwrap())
);
// add into december
assert_eq!(
NaiveDate::from_ymd_opt(2022, 10, 31).unwrap().checked_add_months(Months::new(2)),
Some(NaiveDate::from_ymd_opt(2022, 12, 31).unwrap())
);
// sub into december
assert_eq!(
NaiveDate::from_ymd_opt(2022, 10, 31).unwrap().checked_sub_months(Months::new(10)),
Some(NaiveDate::from_ymd_opt(2021, 12, 31).unwrap())
);
// add into january
assert_eq!(
NaiveDate::from_ymd_opt(2022, 8, 3).unwrap().checked_add_months(Months::new(5)),
Some(NaiveDate::from_ymd_opt(2023, 1, 3).unwrap())
);
// sub into january
assert_eq!(
NaiveDate::from_ymd_opt(2022, 8, 3).unwrap().checked_sub_months(Months::new(7)),
Some(NaiveDate::from_ymd_opt(2022, 1, 3).unwrap())
);
}
#[test]
fn test_readme_doomsday() {
for y in NaiveDate::MIN.year()..=NaiveDate::MAX.year() {
// even months
let d4 = NaiveDate::from_ymd_opt(y, 4, 4).unwrap();
let d6 = NaiveDate::from_ymd_opt(y, 6, 6).unwrap();
let d8 = NaiveDate::from_ymd_opt(y, 8, 8).unwrap();
let d10 = NaiveDate::from_ymd_opt(y, 10, 10).unwrap();
let d12 = NaiveDate::from_ymd_opt(y, 12, 12).unwrap();
// nine to five, seven-eleven
let d59 = NaiveDate::from_ymd_opt(y, 5, 9).unwrap();
let d95 = NaiveDate::from_ymd_opt(y, 9, 5).unwrap();
let d711 = NaiveDate::from_ymd_opt(y, 7, 11).unwrap();
let d117 = NaiveDate::from_ymd_opt(y, 11, 7).unwrap();
// "March 0"
let d30 = NaiveDate::from_ymd_opt(y, 3, 1).unwrap().pred_opt().unwrap();
let weekday = d30.weekday();
let other_dates = [d4, d6, d8, d10, d12, d59, d95, d711, d117];
assert!(other_dates.iter().all(|d| d.weekday() == weekday));
}
}
#[test]
fn test_date_from_ymd() {
let from_ymd = NaiveDate::from_ymd_opt;
assert!(from_ymd(2012, 0, 1).is_none());
assert!(from_ymd(2012, 1, 1).is_some());
assert!(from_ymd(2012, 2, 29).is_some());
assert!(from_ymd(2014, 2, 29).is_none());
assert!(from_ymd(2014, 3, 0).is_none());
assert!(from_ymd(2014, 3, 1).is_some());
assert!(from_ymd(2014, 3, 31).is_some());
assert!(from_ymd(2014, 3, 32).is_none());
assert!(from_ymd(2014, 12, 31).is_some());
assert!(from_ymd(2014, 13, 1).is_none());
}
#[test]
fn test_date_from_yo() {
let from_yo = NaiveDate::from_yo_opt;
let ymd = |y, m, d| NaiveDate::from_ymd_opt(y, m, d).unwrap();
assert_eq!(from_yo(2012, 0), None);
assert_eq!(from_yo(2012, 1), Some(ymd(2012, 1, 1)));
assert_eq!(from_yo(2012, 2), Some(ymd(2012, 1, 2)));
assert_eq!(from_yo(2012, 32), Some(ymd(2012, 2, 1)));
assert_eq!(from_yo(2012, 60), Some(ymd(2012, 2, 29)));
assert_eq!(from_yo(2012, 61), Some(ymd(2012, 3, 1)));
assert_eq!(from_yo(2012, 100), Some(ymd(2012, 4, 9)));
assert_eq!(from_yo(2012, 200), Some(ymd(2012, 7, 18)));
assert_eq!(from_yo(2012, 300), Some(ymd(2012, 10, 26)));
assert_eq!(from_yo(2012, 366), Some(ymd(2012, 12, 31)));
assert_eq!(from_yo(2012, 367), None);
assert_eq!(from_yo(2012, (1 << 28) | 60), None);
assert_eq!(from_yo(2014, 0), None);
assert_eq!(from_yo(2014, 1), Some(ymd(2014, 1, 1)));
assert_eq!(from_yo(2014, 2), Some(ymd(2014, 1, 2)));
assert_eq!(from_yo(2014, 32), Some(ymd(2014, 2, 1)));
assert_eq!(from_yo(2014, 59), Some(ymd(2014, 2, 28)));
assert_eq!(from_yo(2014, 60), Some(ymd(2014, 3, 1)));
assert_eq!(from_yo(2014, 100), Some(ymd(2014, 4, 10)));
assert_eq!(from_yo(2014, 200), Some(ymd(2014, 7, 19)));
assert_eq!(from_yo(2014, 300), Some(ymd(2014, 10, 27)));
assert_eq!(from_yo(2014, 365), Some(ymd(2014, 12, 31)));
assert_eq!(from_yo(2014, 366), None);
}
#[test]
fn test_date_from_isoywd() {
let from_isoywd = NaiveDate::from_isoywd_opt;
let ymd = |y, m, d| NaiveDate::from_ymd_opt(y, m, d).unwrap();
assert_eq!(from_isoywd(2004, 0, Weekday::Sun), None);
assert_eq!(from_isoywd(2004, 1, Weekday::Mon), Some(ymd(2003, 12, 29)));
assert_eq!(from_isoywd(2004, 1, Weekday::Sun), Some(ymd(2004, 1, 4)));
assert_eq!(from_isoywd(2004, 2, Weekday::Mon), Some(ymd(2004, 1, 5)));
assert_eq!(from_isoywd(2004, 2, Weekday::Sun), Some(ymd(2004, 1, 11)));
assert_eq!(from_isoywd(2004, 52, Weekday::Mon), Some(ymd(2004, 12, 20)));
assert_eq!(from_isoywd(2004, 52, Weekday::Sun), Some(ymd(2004, 12, 26)));
assert_eq!(from_isoywd(2004, 53, Weekday::Mon), Some(ymd(2004, 12, 27)));
assert_eq!(from_isoywd(2004, 53, Weekday::Sun), Some(ymd(2005, 1, 2)));
assert_eq!(from_isoywd(2004, 54, Weekday::Mon), None);
assert_eq!(from_isoywd(2011, 0, Weekday::Sun), None);
assert_eq!(from_isoywd(2011, 1, Weekday::Mon), Some(ymd(2011, 1, 3)));
assert_eq!(from_isoywd(2011, 1, Weekday::Sun), Some(ymd(2011, 1, 9)));
assert_eq!(from_isoywd(2011, 2, Weekday::Mon), Some(ymd(2011, 1, 10)));
assert_eq!(from_isoywd(2011, 2, Weekday::Sun), Some(ymd(2011, 1, 16)));
assert_eq!(from_isoywd(2018, 51, Weekday::Mon), Some(ymd(2018, 12, 17)));
assert_eq!(from_isoywd(2018, 51, Weekday::Sun), Some(ymd(2018, 12, 23)));
assert_eq!(from_isoywd(2018, 52, Weekday::Mon), Some(ymd(2018, 12, 24)));
assert_eq!(from_isoywd(2018, 52, Weekday::Sun), Some(ymd(2018, 12, 30)));
assert_eq!(from_isoywd(2018, 53, Weekday::Mon), None);
}
#[test]
fn test_date_from_isoywd_and_iso_week() {
for year in 2000..2401 {
for week in 1..54 {
for &weekday in [
Weekday::Mon,
Weekday::Tue,
Weekday::Wed,
Weekday::Thu,
Weekday::Fri,
Weekday::Sat,
Weekday::Sun,
]
.iter()
{
let d = NaiveDate::from_isoywd_opt(year, week, weekday);
if let Some(d) = d {
assert_eq!(d.weekday(), weekday);
let w = d.iso_week();
assert_eq!(w.year(), year);
assert_eq!(w.week(), week);
}
}
}
}
for year in 2000..2401 {
for month in 1..13 {
for day in 1..32 {
let d = NaiveDate::from_ymd_opt(year, month, day);
if let Some(d) = d {
let w = d.iso_week();
let d_ = NaiveDate::from_isoywd_opt(w.year(), w.week(), d.weekday());
assert_eq!(d, d_.unwrap());
}
}
}
}
}
#[test]
fn test_date_from_num_days_from_ce() {
let from_ndays_from_ce = NaiveDate::from_num_days_from_ce_opt;
assert_eq!(from_ndays_from_ce(1), Some(NaiveDate::from_ymd_opt(1, 1, 1).unwrap()));
assert_eq!(from_ndays_from_ce(2), Some(NaiveDate::from_ymd_opt(1, 1, 2).unwrap()));
assert_eq!(from_ndays_from_ce(31), Some(NaiveDate::from_ymd_opt(1, 1, 31).unwrap()));
assert_eq!(from_ndays_from_ce(32), Some(NaiveDate::from_ymd_opt(1, 2, 1).unwrap()));
assert_eq!(from_ndays_from_ce(59), Some(NaiveDate::from_ymd_opt(1, 2, 28).unwrap()));
assert_eq!(from_ndays_from_ce(60), Some(NaiveDate::from_ymd_opt(1, 3, 1).unwrap()));
assert_eq!(from_ndays_from_ce(365), Some(NaiveDate::from_ymd_opt(1, 12, 31).unwrap()));
assert_eq!(from_ndays_from_ce(365 + 1), Some(NaiveDate::from_ymd_opt(2, 1, 1).unwrap()));
assert_eq!(from_ndays_from_ce(365 * 2 + 1), Some(NaiveDate::from_ymd_opt(3, 1, 1).unwrap()));
assert_eq!(from_ndays_from_ce(365 * 3 + 1), Some(NaiveDate::from_ymd_opt(4, 1, 1).unwrap()));
assert_eq!(from_ndays_from_ce(365 * 4 + 2), Some(NaiveDate::from_ymd_opt(5, 1, 1).unwrap()));
assert_eq!(from_ndays_from_ce(146097 + 1), Some(NaiveDate::from_ymd_opt(401, 1, 1).unwrap()));
assert_eq!(
from_ndays_from_ce(146097 * 5 + 1),
Some(NaiveDate::from_ymd_opt(2001, 1, 1).unwrap())
);
assert_eq!(from_ndays_from_ce(719163), Some(NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()));
assert_eq!(from_ndays_from_ce(0), Some(NaiveDate::from_ymd_opt(0, 12, 31).unwrap())); // 1 BCE
assert_eq!(from_ndays_from_ce(-365), Some(NaiveDate::from_ymd_opt(0, 1, 1).unwrap()));
assert_eq!(from_ndays_from_ce(-366), Some(NaiveDate::from_ymd_opt(-1, 12, 31).unwrap())); // 2 BCE
for days in (-9999..10001).map(|x| x * 100) {
assert_eq!(from_ndays_from_ce(days).map(|d| d.num_days_from_ce()), Some(days));
}
assert_eq!(from_ndays_from_ce(NaiveDate::MIN.num_days_from_ce()), Some(NaiveDate::MIN));
assert_eq!(from_ndays_from_ce(NaiveDate::MIN.num_days_from_ce() - 1), None);
assert_eq!(from_ndays_from_ce(NaiveDate::MAX.num_days_from_ce()), Some(NaiveDate::MAX));
assert_eq!(from_ndays_from_ce(NaiveDate::MAX.num_days_from_ce() + 1), None);
assert_eq!(from_ndays_from_ce(i32::MIN), None);
assert_eq!(from_ndays_from_ce(i32::MAX), None);
}
#[test]
fn test_date_from_weekday_of_month_opt() {
let ymwd = NaiveDate::from_weekday_of_month_opt;
assert_eq!(ymwd(2018, 8, Weekday::Tue, 0), None);
assert_eq!(ymwd(2018, 8, Weekday::Wed, 1), Some(NaiveDate::from_ymd_opt(2018, 8, 1).unwrap()));
assert_eq!(ymwd(2018, 8, Weekday::Thu, 1), Some(NaiveDate::from_ymd_opt(2018, 8, 2).unwrap()));
assert_eq!(ymwd(2018, 8, Weekday::Sun, 1), Some(NaiveDate::from_ymd_opt(2018, 8, 5).unwrap()));
assert_eq!(ymwd(2018, 8, Weekday::Mon, 1), Some(NaiveDate::from_ymd_opt(2018, 8, 6).unwrap()));
assert_eq!(ymwd(2018, 8, Weekday::Tue, 1), Some(NaiveDate::from_ymd_opt(2018, 8, 7).unwrap()));
assert_eq!(ymwd(2018, 8, Weekday::Wed, 2), Some(NaiveDate::from_ymd_opt(2018, 8, 8).unwrap()));
assert_eq!(ymwd(2018, 8, Weekday::Sun, 2), Some(NaiveDate::from_ymd_opt(2018, 8, 12).unwrap()));
assert_eq!(ymwd(2018, 8, Weekday::Thu, 3), Some(NaiveDate::from_ymd_opt(2018, 8, 16).unwrap()));
assert_eq!(ymwd(2018, 8, Weekday::Thu, 4), Some(NaiveDate::from_ymd_opt(2018, 8, 23).unwrap()));
assert_eq!(ymwd(2018, 8, Weekday::Thu, 5), Some(NaiveDate::from_ymd_opt(2018, 8, 30).unwrap()));
assert_eq!(ymwd(2018, 8, Weekday::Fri, 5), Some(NaiveDate::from_ymd_opt(2018, 8, 31).unwrap()));
assert_eq!(ymwd(2018, 8, Weekday::Sat, 5), None);
}
#[test]
fn test_date_fields() {
fn check(year: i32, month: u32, day: u32, ordinal: u32) {
let d1 = NaiveDate::from_ymd_opt(year, month, day).unwrap();
assert_eq!(d1.year(), year);
assert_eq!(d1.month(), month);
assert_eq!(d1.day(), day);
assert_eq!(d1.ordinal(), ordinal);
let d2 = NaiveDate::from_yo_opt(year, ordinal).unwrap();
assert_eq!(d2.year(), year);
assert_eq!(d2.month(), month);
assert_eq!(d2.day(), day);
assert_eq!(d2.ordinal(), ordinal);
assert_eq!(d1, d2);
}
check(2012, 1, 1, 1);
check(2012, 1, 2, 2);
check(2012, 2, 1, 32);
check(2012, 2, 29, 60);
check(2012, 3, 1, 61);
check(2012, 4, 9, 100);
check(2012, 7, 18, 200);
check(2012, 10, 26, 300);
check(2012, 12, 31, 366);
check(2014, 1, 1, 1);
check(2014, 1, 2, 2);
check(2014, 2, 1, 32);
check(2014, 2, 28, 59);
check(2014, 3, 1, 60);
check(2014, 4, 10, 100);
check(2014, 7, 19, 200);
check(2014, 10, 27, 300);
check(2014, 12, 31, 365);
}
#[test]
fn test_date_weekday() {
assert_eq!(NaiveDate::from_ymd_opt(1582, 10, 15).unwrap().weekday(), Weekday::Fri);
// May 20, 1875 = ISO 8601 reference date
assert_eq!(NaiveDate::from_ymd_opt(1875, 5, 20).unwrap().weekday(), Weekday::Thu);
assert_eq!(NaiveDate::from_ymd_opt(2000, 1, 1).unwrap().weekday(), Weekday::Sat);
}
#[test]
fn test_date_with_fields() {
let d = NaiveDate::from_ymd_opt(2000, 2, 29).unwrap();
assert_eq!(d.with_year(-400), Some(NaiveDate::from_ymd_opt(-400, 2, 29).unwrap()));
assert_eq!(d.with_year(-100), None);
assert_eq!(d.with_year(1600), Some(NaiveDate::from_ymd_opt(1600, 2, 29).unwrap()));
assert_eq!(d.with_year(1900), None);
assert_eq!(d.with_year(2000), Some(NaiveDate::from_ymd_opt(2000, 2, 29).unwrap()));
assert_eq!(d.with_year(2001), None);
assert_eq!(d.with_year(2004), Some(NaiveDate::from_ymd_opt(2004, 2, 29).unwrap()));
assert_eq!(d.with_year(i32::MAX), None);
let d = NaiveDate::from_ymd_opt(2000, 4, 30).unwrap();
assert_eq!(d.with_month(0), None);
assert_eq!(d.with_month(1), Some(NaiveDate::from_ymd_opt(2000, 1, 30).unwrap()));
assert_eq!(d.with_month(2), None);
assert_eq!(d.with_month(3), Some(NaiveDate::from_ymd_opt(2000, 3, 30).unwrap()));
assert_eq!(d.with_month(4), Some(NaiveDate::from_ymd_opt(2000, 4, 30).unwrap()));
assert_eq!(d.with_month(12), Some(NaiveDate::from_ymd_opt(2000, 12, 30).unwrap()));
assert_eq!(d.with_month(13), None);
assert_eq!(d.with_month(u32::MAX), None);
let d = NaiveDate::from_ymd_opt(2000, 2, 8).unwrap();
assert_eq!(d.with_day(0), None);
assert_eq!(d.with_day(1), Some(NaiveDate::from_ymd_opt(2000, 2, 1).unwrap()));
assert_eq!(d.with_day(29), Some(NaiveDate::from_ymd_opt(2000, 2, 29).unwrap()));
assert_eq!(d.with_day(30), None);
assert_eq!(d.with_day(u32::MAX), None);
}
#[test]
fn test_date_with_ordinal() {
let d = NaiveDate::from_ymd_opt(2000, 5, 5).unwrap();
assert_eq!(d.with_ordinal(0), None);
assert_eq!(d.with_ordinal(1), Some(NaiveDate::from_ymd_opt(2000, 1, 1).unwrap()));
assert_eq!(d.with_ordinal(60), Some(NaiveDate::from_ymd_opt(2000, 2, 29).unwrap()));
assert_eq!(d.with_ordinal(61), Some(NaiveDate::from_ymd_opt(2000, 3, 1).unwrap()));
assert_eq!(d.with_ordinal(366), Some(NaiveDate::from_ymd_opt(2000, 12, 31).unwrap()));
assert_eq!(d.with_ordinal(367), None);
assert_eq!(d.with_ordinal((1 << 28) | 60), None);
let d = NaiveDate::from_ymd_opt(1999, 5, 5).unwrap();
assert_eq!(d.with_ordinal(366), None);
assert_eq!(d.with_ordinal(u32::MAX), None);
}
#[test]
fn test_date_num_days_from_ce() {
assert_eq!(NaiveDate::from_ymd_opt(1, 1, 1).unwrap().num_days_from_ce(), 1);
for year in -9999..10001 {
assert_eq!(
NaiveDate::from_ymd_opt(year, 1, 1).unwrap().num_days_from_ce(),
NaiveDate::from_ymd_opt(year - 1, 12, 31).unwrap().num_days_from_ce() + 1
);
}
}
#[test]
fn test_date_succ() {
let ymd = |y, m, d| NaiveDate::from_ymd_opt(y, m, d).unwrap();
assert_eq!(ymd(2014, 5, 6).succ_opt(), Some(ymd(2014, 5, 7)));
assert_eq!(ymd(2014, 5, 31).succ_opt(), Some(ymd(2014, 6, 1)));
assert_eq!(ymd(2014, 12, 31).succ_opt(), Some(ymd(2015, 1, 1)));
assert_eq!(ymd(2016, 2, 28).succ_opt(), Some(ymd(2016, 2, 29)));
assert_eq!(ymd(NaiveDate::MAX.year(), 12, 31).succ_opt(), None);
}
#[test]
fn test_date_pred() {
let ymd = |y, m, d| NaiveDate::from_ymd_opt(y, m, d).unwrap();
assert_eq!(ymd(2016, 3, 1).pred_opt(), Some(ymd(2016, 2, 29)));
assert_eq!(ymd(2015, 1, 1).pred_opt(), Some(ymd(2014, 12, 31)));
assert_eq!(ymd(2014, 6, 1).pred_opt(), Some(ymd(2014, 5, 31)));
assert_eq!(ymd(2014, 5, 7).pred_opt(), Some(ymd(2014, 5, 6)));
assert_eq!(ymd(NaiveDate::MIN.year(), 1, 1).pred_opt(), None);
}
#[test]
fn test_date_checked_add_signed() {
fn check(lhs: Option<NaiveDate>, delta: TimeDelta, rhs: Option<NaiveDate>) {
assert_eq!(lhs.unwrap().checked_add_signed(delta), rhs);
assert_eq!(lhs.unwrap().checked_sub_signed(-delta), rhs);
}
let ymd = NaiveDate::from_ymd_opt;
check(ymd(2014, 1, 1), TimeDelta::zero(), ymd(2014, 1, 1));
check(ymd(2014, 1, 1), TimeDelta::try_seconds(86399).unwrap(), ymd(2014, 1, 1));
// always round towards zero
check(ymd(2014, 1, 1), TimeDelta::try_seconds(-86399).unwrap(), ymd(2014, 1, 1));
check(ymd(2014, 1, 1), TimeDelta::try_days(1).unwrap(), ymd(2014, 1, 2));
check(ymd(2014, 1, 1), TimeDelta::try_days(-1).unwrap(), ymd(2013, 12, 31));
check(ymd(2014, 1, 1), TimeDelta::try_days(364).unwrap(), ymd(2014, 12, 31));
check(ymd(2014, 1, 1), TimeDelta::try_days(365 * 4 + 1).unwrap(), ymd(2018, 1, 1));
check(ymd(2014, 1, 1), TimeDelta::try_days(365 * 400 + 97).unwrap(), ymd(2414, 1, 1));
check(ymd(-7, 1, 1), TimeDelta::try_days(365 * 12 + 3).unwrap(), ymd(5, 1, 1));
// overflow check
check(
ymd(0, 1, 1),
TimeDelta::try_days(MAX_DAYS_FROM_YEAR_0 as i64).unwrap(),
ymd(MAX_YEAR, 12, 31),
);
check(ymd(0, 1, 1), TimeDelta::try_days(MAX_DAYS_FROM_YEAR_0 as i64 + 1).unwrap(), None);
check(ymd(0, 1, 1), TimeDelta::MAX, None);
check(
ymd(0, 1, 1),
TimeDelta::try_days(MIN_DAYS_FROM_YEAR_0 as i64).unwrap(),
ymd(MIN_YEAR, 1, 1),
);
check(ymd(0, 1, 1), TimeDelta::try_days(MIN_DAYS_FROM_YEAR_0 as i64 - 1).unwrap(), None);
check(ymd(0, 1, 1), TimeDelta::MIN, None);
}
#[test]
fn test_date_signed_duration_since() {
fn check(lhs: Option<NaiveDate>, rhs: Option<NaiveDate>, delta: TimeDelta) {
assert_eq!(lhs.unwrap().signed_duration_since(rhs.unwrap()), delta);
assert_eq!(rhs.unwrap().signed_duration_since(lhs.unwrap()), -delta);
}
let ymd = NaiveDate::from_ymd_opt;
check(ymd(2014, 1, 1), ymd(2014, 1, 1), TimeDelta::zero());
check(ymd(2014, 1, 2), ymd(2014, 1, 1), TimeDelta::try_days(1).unwrap());
check(ymd(2014, 12, 31), ymd(2014, 1, 1), TimeDelta::try_days(364).unwrap());
check(ymd(2015, 1, 3), ymd(2014, 1, 1), TimeDelta::try_days(365 + 2).unwrap());
check(ymd(2018, 1, 1), ymd(2014, 1, 1), TimeDelta::try_days(365 * 4 + 1).unwrap());
check(ymd(2414, 1, 1), ymd(2014, 1, 1), TimeDelta::try_days(365 * 400 + 97).unwrap());
check(
ymd(MAX_YEAR, 12, 31),
ymd(0, 1, 1),
TimeDelta::try_days(MAX_DAYS_FROM_YEAR_0 as i64).unwrap(),
);
check(
ymd(MIN_YEAR, 1, 1),
ymd(0, 1, 1),
TimeDelta::try_days(MIN_DAYS_FROM_YEAR_0 as i64).unwrap(),
);
}
#[test]
fn test_date_add_days() {
fn check(lhs: Option<NaiveDate>, days: Days, rhs: Option<NaiveDate>) {
assert_eq!(lhs.unwrap().checked_add_days(days), rhs);
}
let ymd = NaiveDate::from_ymd_opt;
check(ymd(2014, 1, 1), Days::new(0), ymd(2014, 1, 1));
// always round towards zero
check(ymd(2014, 1, 1), Days::new(1), ymd(2014, 1, 2));
check(ymd(2014, 1, 1), Days::new(364), ymd(2014, 12, 31));
check(ymd(2014, 1, 1), Days::new(365 * 4 + 1), ymd(2018, 1, 1));
check(ymd(2014, 1, 1), Days::new(365 * 400 + 97), ymd(2414, 1, 1));
check(ymd(-7, 1, 1), Days::new(365 * 12 + 3), ymd(5, 1, 1));
// overflow check
check(ymd(0, 1, 1), Days::new(MAX_DAYS_FROM_YEAR_0.try_into().unwrap()), ymd(MAX_YEAR, 12, 31));
check(ymd(0, 1, 1), Days::new(u64::try_from(MAX_DAYS_FROM_YEAR_0).unwrap() + 1), None);
}
#[test]
fn test_date_sub_days() {
fn check(lhs: Option<NaiveDate>, days: Days, rhs: Option<NaiveDate>) {
assert_eq!(lhs.unwrap().checked_sub_days(days), rhs);
}
let ymd = NaiveDate::from_ymd_opt;
check(ymd(2014, 1, 1), Days::new(0), ymd(2014, 1, 1));
check(ymd(2014, 1, 2), Days::new(1), ymd(2014, 1, 1));
check(ymd(2014, 12, 31), Days::new(364), ymd(2014, 1, 1));
check(ymd(2015, 1, 3), Days::new(365 + 2), ymd(2014, 1, 1));
check(ymd(2018, 1, 1), Days::new(365 * 4 + 1), ymd(2014, 1, 1));
check(ymd(2414, 1, 1), Days::new(365 * 400 + 97), ymd(2014, 1, 1));
check(ymd(MAX_YEAR, 12, 31), Days::new(MAX_DAYS_FROM_YEAR_0.try_into().unwrap()), ymd(0, 1, 1));
check(
ymd(0, 1, 1),
Days::new((-MIN_DAYS_FROM_YEAR_0).try_into().unwrap()),
ymd(MIN_YEAR, 1, 1),
);
}
#[test]
fn test_date_addassignment() {
let ymd = |y, m, d| NaiveDate::from_ymd_opt(y, m, d).unwrap();
let mut date = ymd(2016, 10, 1);
date += TimeDelta::try_days(10).unwrap();
assert_eq!(date, ymd(2016, 10, 11));
date += TimeDelta::try_days(30).unwrap();
assert_eq!(date, ymd(2016, 11, 10));
}
#[test]
fn test_date_subassignment() {
let ymd = |y, m, d| NaiveDate::from_ymd_opt(y, m, d).unwrap();
let mut date = ymd(2016, 10, 11);
date -= TimeDelta::try_days(10).unwrap();
assert_eq!(date, ymd(2016, 10, 1));
date -= TimeDelta::try_days(2).unwrap();
assert_eq!(date, ymd(2016, 9, 29));
}
#[test]
fn test_date_fmt() {
assert_eq!(format!("{:?}", NaiveDate::from_ymd_opt(2012, 3, 4).unwrap()), "2012-03-04");
assert_eq!(format!("{:?}", NaiveDate::from_ymd_opt(0, 3, 4).unwrap()), "0000-03-04");
assert_eq!(format!("{:?}", NaiveDate::from_ymd_opt(-307, 3, 4).unwrap()), "-0307-03-04");
assert_eq!(format!("{:?}", NaiveDate::from_ymd_opt(12345, 3, 4).unwrap()), "+12345-03-04");
assert_eq!(NaiveDate::from_ymd_opt(2012, 3, 4).unwrap().to_string(), "2012-03-04");
assert_eq!(NaiveDate::from_ymd_opt(0, 3, 4).unwrap().to_string(), "0000-03-04");
assert_eq!(NaiveDate::from_ymd_opt(-307, 3, 4).unwrap().to_string(), "-0307-03-04");
assert_eq!(NaiveDate::from_ymd_opt(12345, 3, 4).unwrap().to_string(), "+12345-03-04");
// the format specifier should have no effect on `NaiveTime`
assert_eq!(format!("{:+30?}", NaiveDate::from_ymd_opt(1234, 5, 6).unwrap()), "1234-05-06");
assert_eq!(format!("{:30?}", NaiveDate::from_ymd_opt(12345, 6, 7).unwrap()), "+12345-06-07");
}
#[test]
fn test_date_from_str() {
// valid cases
let valid = [
"-0000000123456-1-2",
" -123456 - 1 - 2 ",
"-12345-1-2",
"-1234-12-31",
"-7-6-5",
"350-2-28",
"360-02-29",
"0360-02-29",
"2015-2 -18",
"2015-02-18",
"+70-2-18",
"+70000-2-18",
"+00007-2-18",
];
for &s in &valid {
eprintln!("test_date_from_str valid {s:?}");
let d = match s.parse::<NaiveDate>() {
Ok(d) => d,
Err(e) => panic!("parsing `{s}` has failed: {e}"),
};
eprintln!("d {d:?} (NaiveDate)");
let s_ = format!("{d:?}");
eprintln!("s_ {s_:?}");
// `s` and `s_` may differ, but `s.parse()` and `s_.parse()` must be same
let d_ = match s_.parse::<NaiveDate>() {
Ok(d) => d,
Err(e) => {
panic!("`{s}` is parsed into `{d:?}`, but reparsing that has failed: {e}")
}
};
eprintln!("d_ {d_:?} (NaiveDate)");
assert!(
d == d_,
"`{s}` is parsed into `{d:?}`, but reparsed result \
`{d_:?}` does not match"
);
}
// some invalid cases
// since `ParseErrorKind` is private, all we can do is to check if there was an error
let invalid = [
"", // empty
"x", // invalid
"Fri, 09 Aug 2013 GMT", // valid date, wrong format
"Sat Jun 30 2012", // valid date, wrong format
"1441497364.649", // valid datetime, wrong format
"+1441497364.649", // valid datetime, wrong format
"+1441497364", // valid datetime, wrong format
"2014/02/03", // valid date, wrong format
"2014", // datetime missing data
"2014-01", // datetime missing data
"2014-01-00", // invalid day
"2014-11-32", // invalid day
"2014-13-01", // invalid month
"2014-13-57", // invalid month, day
"9999999-9-9", // invalid year (out of bounds)
];
for &s in &invalid {
eprintln!("test_date_from_str invalid {s:?}");
assert!(s.parse::<NaiveDate>().is_err());
}
}
#[test]
fn test_date_parse_from_str() {
let ymd = |y, m, d| NaiveDate::from_ymd_opt(y, m, d).unwrap();
assert_eq!(
NaiveDate::parse_from_str("2014-5-7T12:34:56+09:30", "%Y-%m-%dT%H:%M:%S%z"),
Ok(ymd(2014, 5, 7))
); // ignore time and offset
assert_eq!(
NaiveDate::parse_from_str("2015-W06-1=2015-033 Q1", "%G-W%V-%u = %Y-%j Q%q"),
Ok(ymd(2015, 2, 2))
);
assert_eq!(NaiveDate::parse_from_str("Fri, 09 Aug 13", "%a, %d %b %y"), Ok(ymd(2013, 8, 9)));
assert!(NaiveDate::parse_from_str("Sat, 09 Aug 2013", "%a, %d %b %Y").is_err());
assert!(NaiveDate::parse_from_str("2014-57", "%Y-%m-%d").is_err());
assert!(NaiveDate::parse_from_str("2014", "%Y").is_err()); // insufficient
assert!(NaiveDate::parse_from_str("2014-5-7 Q3", "%Y-%m-%d Q%q").is_err()); // mismatched quarter
assert_eq!(
NaiveDate::parse_from_str("2020-01-0", "%Y-%W-%w").ok(),
NaiveDate::from_ymd_opt(2020, 1, 12),
);
assert_eq!(
NaiveDate::parse_from_str("2019-01-0", "%Y-%W-%w").ok(),
NaiveDate::from_ymd_opt(2019, 1, 13),
);
}
#[test]
fn test_day_iterator_limit() {
assert_eq!(NaiveDate::from_ymd_opt(MAX_YEAR, 12, 29).unwrap().iter_days().take(4).count(), 2);
assert_eq!(
NaiveDate::from_ymd_opt(MIN_YEAR, 1, 3).unwrap().iter_days().rev().take(4).count(),
2
);
}
#[test]
fn test_week_iterator_limit() {
assert_eq!(NaiveDate::from_ymd_opt(MAX_YEAR, 12, 12).unwrap().iter_weeks().take(4).count(), 2);
assert_eq!(
NaiveDate::from_ymd_opt(MIN_YEAR, 1, 15).unwrap().iter_weeks().rev().take(4).count(),
2
);
}
#[test]
fn test_weeks_from() {
// tests per: https://github.com/chronotope/chrono/issues/961
// these internally use `weeks_from` via the parsing infrastructure
assert_eq!(
NaiveDate::parse_from_str("2020-01-0", "%Y-%W-%w").ok(),
NaiveDate::from_ymd_opt(2020, 1, 12),
);
assert_eq!(
NaiveDate::parse_from_str("2019-01-0", "%Y-%W-%w").ok(),
NaiveDate::from_ymd_opt(2019, 1, 13),
);
// direct tests
for (y, starts_on) in &[
(2019, Weekday::Tue),
(2020, Weekday::Wed),
(2021, Weekday::Fri),
(2022, Weekday::Sat),
(2023, Weekday::Sun),
(2024, Weekday::Mon),
(2025, Weekday::Wed),
(2026, Weekday::Thu),
] {
for day in &[
Weekday::Mon,
Weekday::Tue,
Weekday::Wed,
Weekday::Thu,
Weekday::Fri,
Weekday::Sat,
Weekday::Sun,
] {
assert_eq!(
NaiveDate::from_ymd_opt(*y, 1, 1).map(|d| d.weeks_from(*day)),
Some(if day == starts_on { 1 } else { 0 })
);
// last day must always be in week 52 or 53
assert!(
[52, 53].contains(&NaiveDate::from_ymd_opt(*y, 12, 31).unwrap().weeks_from(*day)),
);
}
}
let base = NaiveDate::from_ymd_opt(2019, 1, 1).unwrap();
// 400 years covers all year types
for day in &[
Weekday::Mon,
Weekday::Tue,
Weekday::Wed,
Weekday::Thu,
Weekday::Fri,
Weekday::Sat,
Weekday::Sun,
] {
// must always be below 54
for dplus in 1..(400 * 366) {
assert!((base + Days::new(dplus)).weeks_from(*day) < 54)
}
}
}
#[test]
fn test_with_0_overflow() {
let dt = NaiveDate::from_ymd_opt(2023, 4, 18).unwrap();
assert!(dt.with_month0(4294967295).is_none());
assert!(dt.with_day0(4294967295).is_none());
assert!(dt.with_ordinal0(4294967295).is_none());
}
#[test]
fn test_leap_year() {
for year in 0..=MAX_YEAR {
let date = NaiveDate::from_ymd_opt(year, 1, 1).unwrap();
let is_leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
assert_eq!(date.leap_year(), is_leap);
assert_eq!(date.leap_year(), date.with_ordinal(366).is_some());
}
}
#[test]
fn test_date_yearflags() {
for (year, year_flags, _) in YEAR_FLAGS {
assert_eq!(NaiveDate::from_yo_opt(year, 1).unwrap().year_flags(), year_flags);
}
}
#[test]
fn test_weekday_with_yearflags() {
for (year, year_flags, first_weekday) in YEAR_FLAGS {
let first_day_of_year = NaiveDate::from_yo_opt(year, 1).unwrap();
dbg!(year);
assert_eq!(first_day_of_year.year_flags(), year_flags);
assert_eq!(first_day_of_year.weekday(), first_weekday);
let mut prev = first_day_of_year.weekday();
for ordinal in 2u32..=year_flags.ndays() {
let date = NaiveDate::from_yo_opt(year, ordinal).unwrap();
let expected = prev.succ();
assert_eq!(date.weekday(), expected);
prev = expected;
}
}
}
#[test]
fn test_isoweekdate_with_yearflags() {
for (year, year_flags, _) in YEAR_FLAGS {
// January 4 should be in the first week
let jan4 = NaiveDate::from_ymd_opt(year, 1, 4).unwrap();
let iso_week = jan4.iso_week();
assert_eq!(jan4.year_flags(), year_flags);
assert_eq!(iso_week.week(), 1);
}
}
#[test]
fn test_date_to_mdf_to_date() {
for (year, year_flags, _) in YEAR_FLAGS {
for ordinal in 1..=year_flags.ndays() {
let date = NaiveDate::from_yo_opt(year, ordinal).unwrap();
assert_eq!(date, NaiveDate::from_mdf(date.year(), date.mdf()).unwrap());
}
}
}
// Used for testing some methods with all combinations of `YearFlags`.
// (year, flags, first weekday of year)
const YEAR_FLAGS: [(i32, YearFlags, Weekday); 14] = [
(2006, A, Weekday::Sun),
(2005, B, Weekday::Sat),
(2010, C, Weekday::Fri),
(2009, D, Weekday::Thu),
(2003, E, Weekday::Wed),
(2002, F, Weekday::Tue),
(2001, G, Weekday::Mon),
(2012, AG, Weekday::Sun),
(2000, BA, Weekday::Sat),
(2016, CB, Weekday::Fri),
(2004, DC, Weekday::Thu),
(2020, ED, Weekday::Wed),
(2008, FE, Weekday::Tue),
(2024, GF, Weekday::Mon),
];
#[test]
#[cfg(feature = "rkyv-validation")]
fn test_rkyv_validation() {
let date_min = NaiveDate::MIN;
let bytes = rkyv::to_bytes::<_, 4>(&date_min).unwrap();
assert_eq!(rkyv::from_bytes::<NaiveDate>(&bytes).unwrap(), date_min);
let date_max = NaiveDate::MAX;
let bytes = rkyv::to_bytes::<_, 4>(&date_max).unwrap();
assert_eq!(rkyv::from_bytes::<NaiveDate>(&bytes).unwrap(), date_max);
}
// MAX_YEAR-12-31 minus 0000-01-01
// = (MAX_YEAR-12-31 minus 0000-12-31) + (0000-12-31 - 0000-01-01)
// = MAX_YEAR * 365 + (# of leap years from 0001 to MAX_YEAR) + 365
// = (MAX_YEAR + 1) * 365 + (# of leap years from 0001 to MAX_YEAR)
const MAX_DAYS_FROM_YEAR_0: i32 =
(MAX_YEAR + 1) * 365 + MAX_YEAR / 4 - MAX_YEAR / 100 + MAX_YEAR / 400;
// MIN_YEAR-01-01 minus 0000-01-01
// = MIN_YEAR * 365 + (# of leap years from MIN_YEAR to 0000)
const MIN_DAYS_FROM_YEAR_0: i32 = MIN_YEAR * 365 + MIN_YEAR / 4 - MIN_YEAR / 100 + MIN_YEAR / 400;
// only used for testing, but duplicated in naive::datetime
const MAX_BITS: usize = 44;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,405 @@
use super::NaiveDateTime;
use crate::{Datelike, FixedOffset, MappedLocalTime, NaiveDate, TimeDelta, Utc};
#[test]
fn test_datetime_add() {
fn check(
(y, m, d, h, n, s): (i32, u32, u32, u32, u32, u32),
rhs: TimeDelta,
result: Option<(i32, u32, u32, u32, u32, u32)>,
) {
let lhs = NaiveDate::from_ymd_opt(y, m, d).unwrap().and_hms_opt(h, n, s).unwrap();
let sum = result.map(|(y, m, d, h, n, s)| {
NaiveDate::from_ymd_opt(y, m, d).unwrap().and_hms_opt(h, n, s).unwrap()
});
assert_eq!(lhs.checked_add_signed(rhs), sum);
assert_eq!(lhs.checked_sub_signed(-rhs), sum);
}
let seconds = |s| TimeDelta::try_seconds(s).unwrap();
check((2014, 5, 6, 7, 8, 9), seconds(3600 + 60 + 1), Some((2014, 5, 6, 8, 9, 10)));
check((2014, 5, 6, 7, 8, 9), seconds(-(3600 + 60 + 1)), Some((2014, 5, 6, 6, 7, 8)));
check((2014, 5, 6, 7, 8, 9), seconds(86399), Some((2014, 5, 7, 7, 8, 8)));
check((2014, 5, 6, 7, 8, 9), seconds(86_400 * 10), Some((2014, 5, 16, 7, 8, 9)));
check((2014, 5, 6, 7, 8, 9), seconds(-86_400 * 10), Some((2014, 4, 26, 7, 8, 9)));
check((2014, 5, 6, 7, 8, 9), seconds(86_400 * 10), Some((2014, 5, 16, 7, 8, 9)));
// overflow check
// assumes that we have correct values for MAX/MIN_DAYS_FROM_YEAR_0 from `naive::date`.
// (they are private constants, but the equivalence is tested in that module.)
let max_days_from_year_0 =
NaiveDate::MAX.signed_duration_since(NaiveDate::from_ymd_opt(0, 1, 1).unwrap());
check((0, 1, 1, 0, 0, 0), max_days_from_year_0, Some((NaiveDate::MAX.year(), 12, 31, 0, 0, 0)));
check(
(0, 1, 1, 0, 0, 0),
max_days_from_year_0 + seconds(86399),
Some((NaiveDate::MAX.year(), 12, 31, 23, 59, 59)),
);
check((0, 1, 1, 0, 0, 0), max_days_from_year_0 + seconds(86_400), None);
check((0, 1, 1, 0, 0, 0), TimeDelta::MAX, None);
let min_days_from_year_0 =
NaiveDate::MIN.signed_duration_since(NaiveDate::from_ymd_opt(0, 1, 1).unwrap());
check((0, 1, 1, 0, 0, 0), min_days_from_year_0, Some((NaiveDate::MIN.year(), 1, 1, 0, 0, 0)));
check((0, 1, 1, 0, 0, 0), min_days_from_year_0 - seconds(1), None);
check((0, 1, 1, 0, 0, 0), TimeDelta::MIN, None);
}
#[test]
fn test_datetime_sub() {
let ymdhms =
|y, m, d, h, n, s| NaiveDate::from_ymd_opt(y, m, d).unwrap().and_hms_opt(h, n, s).unwrap();
let since = NaiveDateTime::signed_duration_since;
assert_eq!(since(ymdhms(2014, 5, 6, 7, 8, 9), ymdhms(2014, 5, 6, 7, 8, 9)), TimeDelta::zero());
assert_eq!(
since(ymdhms(2014, 5, 6, 7, 8, 10), ymdhms(2014, 5, 6, 7, 8, 9)),
TimeDelta::try_seconds(1).unwrap()
);
assert_eq!(
since(ymdhms(2014, 5, 6, 7, 8, 9), ymdhms(2014, 5, 6, 7, 8, 10)),
TimeDelta::try_seconds(-1).unwrap()
);
assert_eq!(
since(ymdhms(2014, 5, 7, 7, 8, 9), ymdhms(2014, 5, 6, 7, 8, 10)),
TimeDelta::try_seconds(86399).unwrap()
);
assert_eq!(
since(ymdhms(2001, 9, 9, 1, 46, 39), ymdhms(1970, 1, 1, 0, 0, 0)),
TimeDelta::try_seconds(999_999_999).unwrap()
);
}
#[test]
fn test_datetime_addassignment() {
let ymdhms =
|y, m, d, h, n, s| NaiveDate::from_ymd_opt(y, m, d).unwrap().and_hms_opt(h, n, s).unwrap();
let mut date = ymdhms(2016, 10, 1, 10, 10, 10);
date += TimeDelta::try_minutes(10_000_000).unwrap();
assert_eq!(date, ymdhms(2035, 10, 6, 20, 50, 10));
date += TimeDelta::try_days(10).unwrap();
assert_eq!(date, ymdhms(2035, 10, 16, 20, 50, 10));
}
#[test]
fn test_datetime_subassignment() {
let ymdhms =
|y, m, d, h, n, s| NaiveDate::from_ymd_opt(y, m, d).unwrap().and_hms_opt(h, n, s).unwrap();
let mut date = ymdhms(2016, 10, 1, 10, 10, 10);
date -= TimeDelta::try_minutes(10_000_000).unwrap();
assert_eq!(date, ymdhms(1997, 9, 26, 23, 30, 10));
date -= TimeDelta::try_days(10).unwrap();
assert_eq!(date, ymdhms(1997, 9, 16, 23, 30, 10));
}
#[test]
fn test_core_duration_ops() {
use core::time::Duration;
let mut dt = NaiveDate::from_ymd_opt(2023, 8, 29).unwrap().and_hms_opt(11, 34, 12).unwrap();
let same = dt + Duration::ZERO;
assert_eq!(dt, same);
dt += Duration::new(3600, 0);
assert_eq!(dt, NaiveDate::from_ymd_opt(2023, 8, 29).unwrap().and_hms_opt(12, 34, 12).unwrap());
}
#[test]
#[should_panic]
fn test_core_duration_max() {
use core::time::Duration;
let mut utc_dt = NaiveDate::from_ymd_opt(2023, 8, 29).unwrap().and_hms_opt(11, 34, 12).unwrap();
utc_dt += Duration::MAX;
}
#[test]
fn test_datetime_from_str() {
// valid cases
let valid = [
"2001-02-03T04:05:06",
"2012-12-12T12:12:12",
"2015-02-18T23:16:09.153",
"2015-2-18T23:16:09.153",
"-77-02-18T23:16:09",
"+82701-05-6T15:9:60.898989898989",
" +82701 - 05 - 6 T 15 : 9 : 60.898989898989 ",
];
for &s in &valid {
eprintln!("test_parse_naivedatetime valid {s:?}");
let d = match s.parse::<NaiveDateTime>() {
Ok(d) => d,
Err(e) => panic!("parsing `{s}` has failed: {e}"),
};
let s_ = format!("{d:?}");
// `s` and `s_` may differ, but `s.parse()` and `s_.parse()` must be same
let d_ = match s_.parse::<NaiveDateTime>() {
Ok(d) => d,
Err(e) => {
panic!("`{s}` is parsed into `{d:?}`, but reparsing that has failed: {e}")
}
};
assert!(
d == d_,
"`{s}` is parsed into `{d:?}`, but reparsed result \
`{d_:?}` does not match"
);
}
// some invalid cases
// since `ParseErrorKind` is private, all we can do is to check if there was an error
let invalid = [
"", // empty
"x", // invalid / missing data
"15", // missing data
"15:8:9", // looks like a time (invalid date)
"15-8-9", // looks like a date (invalid)
"Fri, 09 Aug 2013 23:54:35 GMT", // valid date, wrong format
"Sat Jun 30 23:59:60 2012", // valid date, wrong format
"1441497364.649", // valid date, wrong format
"+1441497364.649", // valid date, wrong format
"+1441497364", // valid date, wrong format
"2014/02/03 04:05:06", // valid date, wrong format
"2015-15-15T15:15:15", // invalid date
"2012-12-12T12:12:12x", // bad timezone / trailing literal
"2012-12-12T12:12:12+00:00", // unexpected timezone / trailing literal
"2012-12-12T12:12:12 +00:00", // unexpected timezone / trailing literal
"2012-12-12T12:12:12 GMT", // unexpected timezone / trailing literal
"2012-123-12T12:12:12", // invalid month
"2012-12-12t12:12:12", // bad divider 't'
"2012-12-12 12:12:12", // missing divider 'T'
"2012-12-12T12:12:12Z", // trailing char 'Z'
"+ 82701-123-12T12:12:12", // strange year, invalid month
"+802701-123-12T12:12:12", // out-of-bound year, invalid month
];
for &s in &invalid {
eprintln!("test_datetime_from_str invalid {s:?}");
assert!(s.parse::<NaiveDateTime>().is_err());
}
}
#[test]
fn test_datetime_parse_from_str() {
let ymdhms =
|y, m, d, h, n, s| NaiveDate::from_ymd_opt(y, m, d).unwrap().and_hms_opt(h, n, s).unwrap();
let ymdhmsn = |y, m, d, h, n, s, nano| {
NaiveDate::from_ymd_opt(y, m, d).unwrap().and_hms_nano_opt(h, n, s, nano).unwrap()
};
assert_eq!(
NaiveDateTime::parse_from_str("2014-5-7T12:34:56+09:30", "%Y-%m-%dT%H:%M:%S%z"),
Ok(ymdhms(2014, 5, 7, 12, 34, 56))
); // ignore offset
assert_eq!(
NaiveDateTime::parse_from_str("2015-W06-1 000000", "%G-W%V-%u%H%M%S"),
Ok(ymdhms(2015, 2, 2, 0, 0, 0))
);
assert_eq!(
NaiveDateTime::parse_from_str("Fri, 09 Aug 2013 23:54:35 GMT", "%a, %d %b %Y %H:%M:%S GMT"),
Ok(ymdhms(2013, 8, 9, 23, 54, 35))
);
assert!(
NaiveDateTime::parse_from_str("Sat, 09 Aug 2013 23:54:35 GMT", "%a, %d %b %Y %H:%M:%S GMT")
.is_err()
);
assert!(NaiveDateTime::parse_from_str("2014-5-7 Q2 12:3456", "%Y-%m-%d Q%q %H:%M:%S").is_err());
assert!(NaiveDateTime::parse_from_str("12:34:56", "%H:%M:%S").is_err()); // insufficient
assert_eq!(
NaiveDateTime::parse_from_str("1441497364", "%s"),
Ok(ymdhms(2015, 9, 5, 23, 56, 4))
);
assert_eq!(
NaiveDateTime::parse_from_str("1283929614.1234", "%s.%f"),
Ok(ymdhmsn(2010, 9, 8, 7, 6, 54, 1234))
);
assert_eq!(
NaiveDateTime::parse_from_str("1441497364.649", "%s%.3f"),
Ok(ymdhmsn(2015, 9, 5, 23, 56, 4, 649000000))
);
assert_eq!(
NaiveDateTime::parse_from_str("1497854303.087654", "%s%.6f"),
Ok(ymdhmsn(2017, 6, 19, 6, 38, 23, 87654000))
);
assert_eq!(
NaiveDateTime::parse_from_str("1437742189.918273645", "%s%.9f"),
Ok(ymdhmsn(2015, 7, 24, 12, 49, 49, 918273645))
);
}
#[test]
fn test_datetime_parse_from_str_with_spaces() {
let parse_from_str = NaiveDateTime::parse_from_str;
let dt = NaiveDate::from_ymd_opt(2013, 8, 9).unwrap().and_hms_opt(23, 54, 35).unwrap();
// with varying spaces - should succeed
assert_eq!(parse_from_str(" Aug 09 2013 23:54:35", " %b %d %Y %H:%M:%S"), Ok(dt));
assert_eq!(parse_from_str("Aug 09 2013 23:54:35 ", "%b %d %Y %H:%M:%S "), Ok(dt));
assert_eq!(parse_from_str(" Aug 09 2013 23:54:35 ", " %b %d %Y %H:%M:%S "), Ok(dt));
assert_eq!(parse_from_str(" Aug 09 2013 23:54:35", " %b %d %Y %H:%M:%S"), Ok(dt));
assert_eq!(parse_from_str(" Aug 09 2013 23:54:35", " %b %d %Y %H:%M:%S"), Ok(dt));
assert_eq!(parse_from_str("\n\tAug 09 2013 23:54:35 ", "\n\t%b %d %Y %H:%M:%S "), Ok(dt));
assert_eq!(parse_from_str("\tAug 09 2013 23:54:35\t", "\t%b %d %Y %H:%M:%S\t"), Ok(dt));
assert_eq!(parse_from_str("Aug 09 2013 23:54:35", "%b %d %Y %H:%M:%S"), Ok(dt));
assert_eq!(parse_from_str("Aug 09 2013 23:54:35", "%b %d %Y %H:%M:%S"), Ok(dt));
assert_eq!(parse_from_str("Aug 09 2013\t23:54:35", "%b %d %Y\t%H:%M:%S"), Ok(dt));
assert_eq!(parse_from_str("Aug 09 2013\t\t23:54:35", "%b %d %Y\t\t%H:%M:%S"), Ok(dt));
assert_eq!(parse_from_str("Aug 09 2013 23:54:35 ", "%b %d %Y %H:%M:%S\n"), Ok(dt));
assert_eq!(parse_from_str("Aug 09 2013 23:54:35", "%b %d %Y\t%H:%M:%S"), Ok(dt));
assert_eq!(parse_from_str("Aug 09 2013 23:54:35", "%b %d %Y %H:%M:%S "), Ok(dt));
assert_eq!(parse_from_str("Aug 09 2013 23:54:35", " %b %d %Y %H:%M:%S"), Ok(dt));
assert_eq!(parse_from_str("Aug 09 2013 23:54:35", "%b %d %Y %H:%M:%S\n"), Ok(dt));
// with varying spaces - should fail
// leading space in data
assert!(parse_from_str(" Aug 09 2013 23:54:35", "%b %d %Y %H:%M:%S").is_err());
// trailing space in data
assert!(parse_from_str("Aug 09 2013 23:54:35 ", "%b %d %Y %H:%M:%S").is_err());
// trailing tab in data
assert!(parse_from_str("Aug 09 2013 23:54:35\t", "%b %d %Y %H:%M:%S").is_err());
// mismatched newlines
assert!(parse_from_str("\nAug 09 2013 23:54:35", "%b %d %Y %H:%M:%S\n").is_err());
// trailing literal in data
assert!(parse_from_str("Aug 09 2013 23:54:35 !!!", "%b %d %Y %H:%M:%S ").is_err());
}
#[test]
fn test_datetime_add_sub_invariant() {
// issue #37
let base = NaiveDate::from_ymd_opt(2000, 1, 1).unwrap().and_hms_opt(0, 0, 0).unwrap();
let t = -946684799990000;
let time = base + TimeDelta::microseconds(t);
assert_eq!(t, time.signed_duration_since(base).num_microseconds().unwrap());
}
#[test]
fn test_and_local_timezone() {
let ndt = NaiveDate::from_ymd_opt(2022, 6, 15).unwrap().and_hms_opt(18, 59, 36).unwrap();
let dt_utc = ndt.and_utc();
assert_eq!(dt_utc.naive_local(), ndt);
assert_eq!(dt_utc.timezone(), Utc);
let offset_tz = FixedOffset::west_opt(4 * 3600).unwrap();
let dt_offset = ndt.and_local_timezone(offset_tz).unwrap();
assert_eq!(dt_offset.naive_local(), ndt);
assert_eq!(dt_offset.timezone(), offset_tz);
}
#[test]
fn test_and_utc() {
let ndt = NaiveDate::from_ymd_opt(2023, 1, 30).unwrap().and_hms_opt(19, 32, 33).unwrap();
let dt_utc = ndt.and_utc();
assert_eq!(dt_utc.naive_local(), ndt);
assert_eq!(dt_utc.timezone(), Utc);
}
#[test]
fn test_checked_add_offset() {
let ymdhmsm = |y, m, d, h, mn, s, mi| {
NaiveDate::from_ymd_opt(y, m, d).unwrap().and_hms_milli_opt(h, mn, s, mi)
};
let positive_offset = FixedOffset::east_opt(2 * 60 * 60).unwrap();
// regular date
let dt = ymdhmsm(2023, 5, 5, 20, 10, 0, 0).unwrap();
assert_eq!(dt.checked_add_offset(positive_offset), ymdhmsm(2023, 5, 5, 22, 10, 0, 0));
// leap second is preserved
let dt = ymdhmsm(2023, 6, 30, 23, 59, 59, 1_000).unwrap();
assert_eq!(dt.checked_add_offset(positive_offset), ymdhmsm(2023, 7, 1, 1, 59, 59, 1_000));
// out of range
assert!(NaiveDateTime::MAX.checked_add_offset(positive_offset).is_none());
let negative_offset = FixedOffset::west_opt(2 * 60 * 60).unwrap();
// regular date
let dt = ymdhmsm(2023, 5, 5, 20, 10, 0, 0).unwrap();
assert_eq!(dt.checked_add_offset(negative_offset), ymdhmsm(2023, 5, 5, 18, 10, 0, 0));
// leap second is preserved
let dt = ymdhmsm(2023, 6, 30, 23, 59, 59, 1_000).unwrap();
assert_eq!(dt.checked_add_offset(negative_offset), ymdhmsm(2023, 6, 30, 21, 59, 59, 1_000));
// out of range
assert!(NaiveDateTime::MIN.checked_add_offset(negative_offset).is_none());
}
#[test]
fn test_checked_sub_offset() {
let ymdhmsm = |y, m, d, h, mn, s, mi| {
NaiveDate::from_ymd_opt(y, m, d).unwrap().and_hms_milli_opt(h, mn, s, mi)
};
let positive_offset = FixedOffset::east_opt(2 * 60 * 60).unwrap();
// regular date
let dt = ymdhmsm(2023, 5, 5, 20, 10, 0, 0).unwrap();
assert_eq!(dt.checked_sub_offset(positive_offset), ymdhmsm(2023, 5, 5, 18, 10, 0, 0));
// leap second is preserved
let dt = ymdhmsm(2023, 6, 30, 23, 59, 59, 1_000).unwrap();
assert_eq!(dt.checked_sub_offset(positive_offset), ymdhmsm(2023, 6, 30, 21, 59, 59, 1_000));
// out of range
assert!(NaiveDateTime::MIN.checked_sub_offset(positive_offset).is_none());
let negative_offset = FixedOffset::west_opt(2 * 60 * 60).unwrap();
// regular date
let dt = ymdhmsm(2023, 5, 5, 20, 10, 0, 0).unwrap();
assert_eq!(dt.checked_sub_offset(negative_offset), ymdhmsm(2023, 5, 5, 22, 10, 0, 0));
// leap second is preserved
let dt = ymdhmsm(2023, 6, 30, 23, 59, 59, 1_000).unwrap();
assert_eq!(dt.checked_sub_offset(negative_offset), ymdhmsm(2023, 7, 1, 1, 59, 59, 1_000));
// out of range
assert!(NaiveDateTime::MAX.checked_sub_offset(negative_offset).is_none());
assert_eq!(dt.checked_add_offset(positive_offset), Some(dt + positive_offset));
assert_eq!(dt.checked_sub_offset(positive_offset), Some(dt - positive_offset));
}
#[test]
fn test_overflowing_add_offset() {
let ymdhmsm = |y, m, d, h, mn, s, mi| {
NaiveDate::from_ymd_opt(y, m, d).unwrap().and_hms_milli_opt(h, mn, s, mi).unwrap()
};
let positive_offset = FixedOffset::east_opt(2 * 60 * 60).unwrap();
// regular date
let dt = ymdhmsm(2023, 5, 5, 20, 10, 0, 0);
assert_eq!(dt.overflowing_add_offset(positive_offset), ymdhmsm(2023, 5, 5, 22, 10, 0, 0));
// leap second is preserved
let dt = ymdhmsm(2023, 6, 30, 23, 59, 59, 1_000);
assert_eq!(dt.overflowing_add_offset(positive_offset), ymdhmsm(2023, 7, 1, 1, 59, 59, 1_000));
// out of range
assert!(NaiveDateTime::MAX.overflowing_add_offset(positive_offset) > NaiveDateTime::MAX);
let negative_offset = FixedOffset::west_opt(2 * 60 * 60).unwrap();
// regular date
let dt = ymdhmsm(2023, 5, 5, 20, 10, 0, 0);
assert_eq!(dt.overflowing_add_offset(negative_offset), ymdhmsm(2023, 5, 5, 18, 10, 0, 0));
// leap second is preserved
let dt = ymdhmsm(2023, 6, 30, 23, 59, 59, 1_000);
assert_eq!(dt.overflowing_add_offset(negative_offset), ymdhmsm(2023, 6, 30, 21, 59, 59, 1_000));
// out of range
assert!(NaiveDateTime::MIN.overflowing_add_offset(negative_offset) < NaiveDateTime::MIN);
}
#[test]
fn test_and_timezone_min_max_dates() {
for offset_hour in -23..=23 {
dbg!(offset_hour);
let offset = FixedOffset::east_opt(offset_hour * 60 * 60).unwrap();
let local_max = NaiveDateTime::MAX.and_local_timezone(offset);
if offset_hour >= 0 {
assert_eq!(local_max.unwrap().naive_local(), NaiveDateTime::MAX);
} else {
assert_eq!(local_max, MappedLocalTime::None);
}
let local_min = NaiveDateTime::MIN.and_local_timezone(offset);
if offset_hour <= 0 {
assert_eq!(local_min.unwrap().naive_local(), NaiveDateTime::MIN);
} else {
assert_eq!(local_min, MappedLocalTime::None);
}
}
}
#[test]
#[cfg(feature = "rkyv-validation")]
fn test_rkyv_validation() {
let dt_min = NaiveDateTime::MIN;
let bytes = rkyv::to_bytes::<_, 12>(&dt_min).unwrap();
assert_eq!(rkyv::from_bytes::<NaiveDateTime>(&bytes).unwrap(), dt_min);
let dt_max = NaiveDateTime::MAX;
let bytes = rkyv::to_bytes::<_, 12>(&dt_max).unwrap();
assert_eq!(rkyv::from_bytes::<NaiveDateTime>(&bytes).unwrap(), dt_max);
}

View File

@@ -0,0 +1,591 @@
//! Internal helper types for working with dates.
#![cfg_attr(feature = "__internal_bench", allow(missing_docs))]
use core::fmt;
/// Year flags (aka the dominical letter).
///
/// `YearFlags` are used as the last four bits of `NaiveDate`, `Mdf` and `IsoWeek`.
///
/// There are 14 possible classes of year in the Gregorian calendar:
/// common and leap years starting with Monday through Sunday.
///
/// The `YearFlags` stores this information into 4 bits `LWWW`. `L` is the leap year flag, with `1`
/// for the common year (this simplifies validating an ordinal in `NaiveDate`). `WWW` is a non-zero
/// `Weekday` of the last day in the preceding year.
#[allow(unreachable_pub)] // public as an alias for benchmarks only
#[derive(PartialEq, Eq, Copy, Clone, Hash)]
pub struct YearFlags(pub(super) u8);
// Weekday of the last day in the preceding year.
// Allows for quick day of week calculation from the 1-based ordinal.
const YEAR_STARTS_AFTER_MONDAY: u8 = 7; // non-zero to allow use with `NonZero*`.
const YEAR_STARTS_AFTER_THUESDAY: u8 = 1;
const YEAR_STARTS_AFTER_WEDNESDAY: u8 = 2;
const YEAR_STARTS_AFTER_THURSDAY: u8 = 3;
const YEAR_STARTS_AFTER_FRIDAY: u8 = 4;
const YEAR_STARTS_AFTER_SATURDAY: u8 = 5;
const YEAR_STARTS_AFTER_SUNDAY: u8 = 6;
const COMMON_YEAR: u8 = 1 << 3;
const LEAP_YEAR: u8 = 0 << 3;
pub(super) const A: YearFlags = YearFlags(COMMON_YEAR | YEAR_STARTS_AFTER_SATURDAY);
pub(super) const AG: YearFlags = YearFlags(LEAP_YEAR | YEAR_STARTS_AFTER_SATURDAY);
pub(super) const B: YearFlags = YearFlags(COMMON_YEAR | YEAR_STARTS_AFTER_FRIDAY);
pub(super) const BA: YearFlags = YearFlags(LEAP_YEAR | YEAR_STARTS_AFTER_FRIDAY);
pub(super) const C: YearFlags = YearFlags(COMMON_YEAR | YEAR_STARTS_AFTER_THURSDAY);
pub(super) const CB: YearFlags = YearFlags(LEAP_YEAR | YEAR_STARTS_AFTER_THURSDAY);
pub(super) const D: YearFlags = YearFlags(COMMON_YEAR | YEAR_STARTS_AFTER_WEDNESDAY);
pub(super) const DC: YearFlags = YearFlags(LEAP_YEAR | YEAR_STARTS_AFTER_WEDNESDAY);
pub(super) const E: YearFlags = YearFlags(COMMON_YEAR | YEAR_STARTS_AFTER_THUESDAY);
pub(super) const ED: YearFlags = YearFlags(LEAP_YEAR | YEAR_STARTS_AFTER_THUESDAY);
pub(super) const F: YearFlags = YearFlags(COMMON_YEAR | YEAR_STARTS_AFTER_MONDAY);
pub(super) const FE: YearFlags = YearFlags(LEAP_YEAR | YEAR_STARTS_AFTER_MONDAY);
pub(super) const G: YearFlags = YearFlags(COMMON_YEAR | YEAR_STARTS_AFTER_SUNDAY);
pub(super) const GF: YearFlags = YearFlags(LEAP_YEAR | YEAR_STARTS_AFTER_SUNDAY);
const YEAR_TO_FLAGS: &[YearFlags; 400] = &[
BA, G, F, E, DC, B, A, G, FE, D, C, B, AG, F, E, D, CB, A, G, F, ED, C, B, A, GF, E, D, C, BA,
G, F, E, DC, B, A, G, FE, D, C, B, AG, F, E, D, CB, A, G, F, ED, C, B, A, GF, E, D, C, BA, G,
F, E, DC, B, A, G, FE, D, C, B, AG, F, E, D, CB, A, G, F, ED, C, B, A, GF, E, D, C, BA, G, F,
E, DC, B, A, G, FE, D, C, B, AG, F, E, D, // 100
C, B, A, G, FE, D, C, B, AG, F, E, D, CB, A, G, F, ED, C, B, A, GF, E, D, C, BA, G, F, E, DC,
B, A, G, FE, D, C, B, AG, F, E, D, CB, A, G, F, ED, C, B, A, GF, E, D, C, BA, G, F, E, DC, B,
A, G, FE, D, C, B, AG, F, E, D, CB, A, G, F, ED, C, B, A, GF, E, D, C, BA, G, F, E, DC, B, A,
G, FE, D, C, B, AG, F, E, D, CB, A, G, F, // 200
E, D, C, B, AG, F, E, D, CB, A, G, F, ED, C, B, A, GF, E, D, C, BA, G, F, E, DC, B, A, G, FE,
D, C, B, AG, F, E, D, CB, A, G, F, ED, C, B, A, GF, E, D, C, BA, G, F, E, DC, B, A, G, FE, D,
C, B, AG, F, E, D, CB, A, G, F, ED, C, B, A, GF, E, D, C, BA, G, F, E, DC, B, A, G, FE, D, C,
B, AG, F, E, D, CB, A, G, F, ED, C, B, A, // 300
G, F, E, D, CB, A, G, F, ED, C, B, A, GF, E, D, C, BA, G, F, E, DC, B, A, G, FE, D, C, B, AG,
F, E, D, CB, A, G, F, ED, C, B, A, GF, E, D, C, BA, G, F, E, DC, B, A, G, FE, D, C, B, AG, F,
E, D, CB, A, G, F, ED, C, B, A, GF, E, D, C, BA, G, F, E, DC, B, A, G, FE, D, C, B, AG, F, E,
D, CB, A, G, F, ED, C, B, A, GF, E, D, C, // 400
];
impl YearFlags {
#[allow(unreachable_pub)] // public as an alias for benchmarks only
#[doc(hidden)] // for benchmarks only
#[inline]
#[must_use]
pub const fn from_year(year: i32) -> YearFlags {
let year = year.rem_euclid(400);
YearFlags::from_year_mod_400(year)
}
#[inline]
pub(super) const fn from_year_mod_400(year: i32) -> YearFlags {
YEAR_TO_FLAGS[year as usize]
}
#[inline]
pub(super) const fn ndays(&self) -> u32 {
let YearFlags(flags) = *self;
366 - (flags >> 3) as u32
}
#[inline]
pub(super) const fn isoweek_delta(&self) -> u32 {
let YearFlags(flags) = *self;
let mut delta = (flags & 0b0111) as u32;
if delta < 3 {
delta += 7;
}
delta
}
#[inline]
pub(super) const fn nisoweeks(&self) -> u32 {
let YearFlags(flags) = *self;
52 + ((0b0000_0100_0000_0110 >> flags as usize) & 1)
}
}
impl fmt::Debug for YearFlags {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let YearFlags(flags) = *self;
match flags {
0o15 => "A".fmt(f),
0o05 => "AG".fmt(f),
0o14 => "B".fmt(f),
0o04 => "BA".fmt(f),
0o13 => "C".fmt(f),
0o03 => "CB".fmt(f),
0o12 => "D".fmt(f),
0o02 => "DC".fmt(f),
0o11 => "E".fmt(f),
0o01 => "ED".fmt(f),
0o10 => "F?".fmt(f),
0o00 => "FE?".fmt(f), // non-canonical
0o17 => "F".fmt(f),
0o07 => "FE".fmt(f),
0o16 => "G".fmt(f),
0o06 => "GF".fmt(f),
_ => write!(f, "YearFlags({flags})"),
}
}
}
// OL: (ordinal << 1) | leap year flag
const MAX_OL: u32 = 366 << 1; // `(366 << 1) | 1` would be day 366 in a non-leap year
const MAX_MDL: u32 = (12 << 6) | (31 << 1) | 1;
// The next table are adjustment values to convert a date encoded as month-day-leapyear to
// ordinal-leapyear. OL = MDL - adjustment.
// Dates that do not exist are encoded as `XX`.
const XX: i8 = 0;
const MDL_TO_OL: &[i8; MAX_MDL as usize + 1] = &[
XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX,
XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX,
XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, // 0
XX, XX, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // 1
XX, XX, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66,
66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66,
66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, XX, XX, XX, XX, XX, // 2
XX, XX, 72, 74, 72, 74, 72, 74, 72, 74, 72, 74, 72, 74, 72, 74, 72, 74, 72, 74, 72, 74, 72, 74,
72, 74, 72, 74, 72, 74, 72, 74, 72, 74, 72, 74, 72, 74, 72, 74, 72, 74, 72, 74, 72, 74, 72, 74,
72, 74, 72, 74, 72, 74, 72, 74, 72, 74, 72, 74, 72, 74, 72, 74, // 3
XX, XX, 74, 76, 74, 76, 74, 76, 74, 76, 74, 76, 74, 76, 74, 76, 74, 76, 74, 76, 74, 76, 74, 76,
74, 76, 74, 76, 74, 76, 74, 76, 74, 76, 74, 76, 74, 76, 74, 76, 74, 76, 74, 76, 74, 76, 74, 76,
74, 76, 74, 76, 74, 76, 74, 76, 74, 76, 74, 76, 74, 76, XX, XX, // 4
XX, XX, 78, 80, 78, 80, 78, 80, 78, 80, 78, 80, 78, 80, 78, 80, 78, 80, 78, 80, 78, 80, 78, 80,
78, 80, 78, 80, 78, 80, 78, 80, 78, 80, 78, 80, 78, 80, 78, 80, 78, 80, 78, 80, 78, 80, 78, 80,
78, 80, 78, 80, 78, 80, 78, 80, 78, 80, 78, 80, 78, 80, 78, 80, // 5
XX, XX, 80, 82, 80, 82, 80, 82, 80, 82, 80, 82, 80, 82, 80, 82, 80, 82, 80, 82, 80, 82, 80, 82,
80, 82, 80, 82, 80, 82, 80, 82, 80, 82, 80, 82, 80, 82, 80, 82, 80, 82, 80, 82, 80, 82, 80, 82,
80, 82, 80, 82, 80, 82, 80, 82, 80, 82, 80, 82, 80, 82, XX, XX, // 6
XX, XX, 84, 86, 84, 86, 84, 86, 84, 86, 84, 86, 84, 86, 84, 86, 84, 86, 84, 86, 84, 86, 84, 86,
84, 86, 84, 86, 84, 86, 84, 86, 84, 86, 84, 86, 84, 86, 84, 86, 84, 86, 84, 86, 84, 86, 84, 86,
84, 86, 84, 86, 84, 86, 84, 86, 84, 86, 84, 86, 84, 86, 84, 86, // 7
XX, XX, 86, 88, 86, 88, 86, 88, 86, 88, 86, 88, 86, 88, 86, 88, 86, 88, 86, 88, 86, 88, 86, 88,
86, 88, 86, 88, 86, 88, 86, 88, 86, 88, 86, 88, 86, 88, 86, 88, 86, 88, 86, 88, 86, 88, 86, 88,
86, 88, 86, 88, 86, 88, 86, 88, 86, 88, 86, 88, 86, 88, 86, 88, // 8
XX, XX, 88, 90, 88, 90, 88, 90, 88, 90, 88, 90, 88, 90, 88, 90, 88, 90, 88, 90, 88, 90, 88, 90,
88, 90, 88, 90, 88, 90, 88, 90, 88, 90, 88, 90, 88, 90, 88, 90, 88, 90, 88, 90, 88, 90, 88, 90,
88, 90, 88, 90, 88, 90, 88, 90, 88, 90, 88, 90, 88, 90, XX, XX, // 9
XX, XX, 92, 94, 92, 94, 92, 94, 92, 94, 92, 94, 92, 94, 92, 94, 92, 94, 92, 94, 92, 94, 92, 94,
92, 94, 92, 94, 92, 94, 92, 94, 92, 94, 92, 94, 92, 94, 92, 94, 92, 94, 92, 94, 92, 94, 92, 94,
92, 94, 92, 94, 92, 94, 92, 94, 92, 94, 92, 94, 92, 94, 92, 94, // 10
XX, XX, 94, 96, 94, 96, 94, 96, 94, 96, 94, 96, 94, 96, 94, 96, 94, 96, 94, 96, 94, 96, 94, 96,
94, 96, 94, 96, 94, 96, 94, 96, 94, 96, 94, 96, 94, 96, 94, 96, 94, 96, 94, 96, 94, 96, 94, 96,
94, 96, 94, 96, 94, 96, 94, 96, 94, 96, 94, 96, 94, 96, XX, XX, // 11
XX, XX, 98, 100, 98, 100, 98, 100, 98, 100, 98, 100, 98, 100, 98, 100, 98, 100, 98, 100, 98,
100, 98, 100, 98, 100, 98, 100, 98, 100, 98, 100, 98, 100, 98, 100, 98, 100, 98, 100, 98, 100,
98, 100, 98, 100, 98, 100, 98, 100, 98, 100, 98, 100, 98, 100, 98, 100, 98, 100, 98, 100, 98,
100, // 12
];
const OL_TO_MDL: &[u8; MAX_OL as usize + 1] = &[
0, 0, // 0
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // 1
66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66,
66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66,
66, 66, 66, 66, 66, 66, 66, 66, 66, // 2
74, 72, 74, 72, 74, 72, 74, 72, 74, 72, 74, 72, 74, 72, 74, 72, 74, 72, 74, 72, 74, 72, 74, 72,
74, 72, 74, 72, 74, 72, 74, 72, 74, 72, 74, 72, 74, 72, 74, 72, 74, 72, 74, 72, 74, 72, 74, 72,
74, 72, 74, 72, 74, 72, 74, 72, 74, 72, 74, 72, 74, 72, // 3
76, 74, 76, 74, 76, 74, 76, 74, 76, 74, 76, 74, 76, 74, 76, 74, 76, 74, 76, 74, 76, 74, 76, 74,
76, 74, 76, 74, 76, 74, 76, 74, 76, 74, 76, 74, 76, 74, 76, 74, 76, 74, 76, 74, 76, 74, 76, 74,
76, 74, 76, 74, 76, 74, 76, 74, 76, 74, 76, 74, // 4
80, 78, 80, 78, 80, 78, 80, 78, 80, 78, 80, 78, 80, 78, 80, 78, 80, 78, 80, 78, 80, 78, 80, 78,
80, 78, 80, 78, 80, 78, 80, 78, 80, 78, 80, 78, 80, 78, 80, 78, 80, 78, 80, 78, 80, 78, 80, 78,
80, 78, 80, 78, 80, 78, 80, 78, 80, 78, 80, 78, 80, 78, // 5
82, 80, 82, 80, 82, 80, 82, 80, 82, 80, 82, 80, 82, 80, 82, 80, 82, 80, 82, 80, 82, 80, 82, 80,
82, 80, 82, 80, 82, 80, 82, 80, 82, 80, 82, 80, 82, 80, 82, 80, 82, 80, 82, 80, 82, 80, 82, 80,
82, 80, 82, 80, 82, 80, 82, 80, 82, 80, 82, 80, // 6
86, 84, 86, 84, 86, 84, 86, 84, 86, 84, 86, 84, 86, 84, 86, 84, 86, 84, 86, 84, 86, 84, 86, 84,
86, 84, 86, 84, 86, 84, 86, 84, 86, 84, 86, 84, 86, 84, 86, 84, 86, 84, 86, 84, 86, 84, 86, 84,
86, 84, 86, 84, 86, 84, 86, 84, 86, 84, 86, 84, 86, 84, // 7
88, 86, 88, 86, 88, 86, 88, 86, 88, 86, 88, 86, 88, 86, 88, 86, 88, 86, 88, 86, 88, 86, 88, 86,
88, 86, 88, 86, 88, 86, 88, 86, 88, 86, 88, 86, 88, 86, 88, 86, 88, 86, 88, 86, 88, 86, 88, 86,
88, 86, 88, 86, 88, 86, 88, 86, 88, 86, 88, 86, 88, 86, // 8
90, 88, 90, 88, 90, 88, 90, 88, 90, 88, 90, 88, 90, 88, 90, 88, 90, 88, 90, 88, 90, 88, 90, 88,
90, 88, 90, 88, 90, 88, 90, 88, 90, 88, 90, 88, 90, 88, 90, 88, 90, 88, 90, 88, 90, 88, 90, 88,
90, 88, 90, 88, 90, 88, 90, 88, 90, 88, 90, 88, // 9
94, 92, 94, 92, 94, 92, 94, 92, 94, 92, 94, 92, 94, 92, 94, 92, 94, 92, 94, 92, 94, 92, 94, 92,
94, 92, 94, 92, 94, 92, 94, 92, 94, 92, 94, 92, 94, 92, 94, 92, 94, 92, 94, 92, 94, 92, 94, 92,
94, 92, 94, 92, 94, 92, 94, 92, 94, 92, 94, 92, 94, 92, // 10
96, 94, 96, 94, 96, 94, 96, 94, 96, 94, 96, 94, 96, 94, 96, 94, 96, 94, 96, 94, 96, 94, 96, 94,
96, 94, 96, 94, 96, 94, 96, 94, 96, 94, 96, 94, 96, 94, 96, 94, 96, 94, 96, 94, 96, 94, 96, 94,
96, 94, 96, 94, 96, 94, 96, 94, 96, 94, 96, 94, // 11
100, 98, 100, 98, 100, 98, 100, 98, 100, 98, 100, 98, 100, 98, 100, 98, 100, 98, 100, 98, 100,
98, 100, 98, 100, 98, 100, 98, 100, 98, 100, 98, 100, 98, 100, 98, 100, 98, 100, 98, 100, 98,
100, 98, 100, 98, 100, 98, 100, 98, 100, 98, 100, 98, 100, 98, 100, 98, 100, 98, 100,
98, // 12
];
/// Month, day of month and year flags: `(month << 9) | (day << 4) | flags`
/// `M_MMMD_DDDD_LFFF`
///
/// The whole bits except for the least 3 bits are referred as `Mdl` (month, day of month, and leap
/// year flag), which is an index to the `MDL_TO_OL` lookup table.
///
/// The conversion between the packed calendar date (`Mdf`) and the ordinal date (`NaiveDate`) is
/// based on the moderately-sized lookup table (~1.5KB) and the packed representation is chosen for
/// efficient lookup.
///
/// The methods of `Mdf` validate their inputs as late as possible. Dates that can't exist, like
/// February 30, can still be represented. This allows the validation to be combined with the final
/// table lookup, which is good for performance.
#[derive(PartialEq, PartialOrd, Copy, Clone)]
pub(super) struct Mdf(u32);
impl Mdf {
/// Makes a new `Mdf` value from month, day and `YearFlags`.
///
/// This method doesn't fully validate the range of the `month` and `day` parameters, only as
/// much as what can't be deferred until later. The year `flags` are trusted to be correct.
///
/// # Errors
///
/// Returns `None` if `month > 12` or `day > 31`.
#[inline]
pub(super) const fn new(month: u32, day: u32, YearFlags(flags): YearFlags) -> Option<Mdf> {
match month <= 12 && day <= 31 {
true => Some(Mdf((month << 9) | (day << 4) | flags as u32)),
false => None,
}
}
/// Makes a new `Mdf` value from an `i32` with an ordinal and a leap year flag, and year
/// `flags`.
///
/// The `ol` is trusted to be valid, and the `flags` are trusted to match it.
#[inline]
pub(super) const fn from_ol(ol: i32, YearFlags(flags): YearFlags) -> Mdf {
debug_assert!(ol > 1 && ol <= MAX_OL as i32);
// Array is indexed from `[2..=MAX_OL]`, with a `0` index having a meaningless value.
Mdf(((ol as u32 + OL_TO_MDL[ol as usize] as u32) << 3) | flags as u32)
}
/// Returns the month of this `Mdf`.
#[inline]
pub(super) const fn month(&self) -> u32 {
let Mdf(mdf) = *self;
mdf >> 9
}
/// Replaces the month of this `Mdf`, keeping the day and flags.
///
/// # Errors
///
/// Returns `None` if `month > 12`.
#[inline]
pub(super) const fn with_month(&self, month: u32) -> Option<Mdf> {
if month > 12 {
return None;
}
let Mdf(mdf) = *self;
Some(Mdf((mdf & 0b1_1111_1111) | (month << 9)))
}
/// Returns the day of this `Mdf`.
#[inline]
pub(super) const fn day(&self) -> u32 {
let Mdf(mdf) = *self;
(mdf >> 4) & 0b1_1111
}
/// Replaces the day of this `Mdf`, keeping the month and flags.
///
/// # Errors
///
/// Returns `None` if `day > 31`.
#[inline]
pub(super) const fn with_day(&self, day: u32) -> Option<Mdf> {
if day > 31 {
return None;
}
let Mdf(mdf) = *self;
Some(Mdf((mdf & !0b1_1111_0000) | (day << 4)))
}
/// Replaces the flags of this `Mdf`, keeping the month and day.
#[inline]
pub(super) const fn with_flags(&self, YearFlags(flags): YearFlags) -> Mdf {
let Mdf(mdf) = *self;
Mdf((mdf & !0b1111) | flags as u32)
}
/// Returns the ordinal that corresponds to this `Mdf`.
///
/// This does a table lookup to calculate the corresponding ordinal. It will return an error if
/// the `Mdl` turns out not to be a valid date.
///
/// # Errors
///
/// Returns `None` if `month == 0` or `day == 0`, or if a the given day does not exist in the
/// given month.
#[inline]
pub(super) const fn ordinal(&self) -> Option<u32> {
let mdl = self.0 >> 3;
match MDL_TO_OL[mdl as usize] {
XX => None,
v => Some((mdl - v as u8 as u32) >> 1),
}
}
/// Returns the year flags of this `Mdf`.
#[inline]
pub(super) const fn year_flags(&self) -> YearFlags {
YearFlags((self.0 & 0b1111) as u8)
}
/// Returns the ordinal that corresponds to this `Mdf`, encoded as a value including year flags.
///
/// This does a table lookup to calculate the corresponding ordinal. It will return an error if
/// the `Mdl` turns out not to be a valid date.
///
/// # Errors
///
/// Returns `None` if `month == 0` or `day == 0`, or if a the given day does not exist in the
/// given month.
#[inline]
pub(super) const fn ordinal_and_flags(&self) -> Option<i32> {
let mdl = self.0 >> 3;
match MDL_TO_OL[mdl as usize] {
XX => None,
v => Some(self.0 as i32 - ((v as i32) << 3)),
}
}
#[cfg(test)]
fn valid(&self) -> bool {
let mdl = self.0 >> 3;
MDL_TO_OL[mdl as usize] > 0
}
}
impl fmt::Debug for Mdf {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let Mdf(mdf) = *self;
write!(
f,
"Mdf(({} << 9) | ({} << 4) | {:#04o} /*{:?}*/)",
mdf >> 9,
(mdf >> 4) & 0b1_1111,
mdf & 0b1111,
YearFlags((mdf & 0b1111) as u8)
)
}
}
#[cfg(test)]
mod tests {
use super::Mdf;
use super::{A, AG, B, BA, C, CB, D, DC, E, ED, F, FE, G, GF, YearFlags};
const NONLEAP_FLAGS: [YearFlags; 7] = [A, B, C, D, E, F, G];
const LEAP_FLAGS: [YearFlags; 7] = [AG, BA, CB, DC, ED, FE, GF];
const FLAGS: [YearFlags; 14] = [A, B, C, D, E, F, G, AG, BA, CB, DC, ED, FE, GF];
#[test]
fn test_year_flags_ndays_from_year() {
assert_eq!(YearFlags::from_year(2014).ndays(), 365);
assert_eq!(YearFlags::from_year(2012).ndays(), 366);
assert_eq!(YearFlags::from_year(2000).ndays(), 366);
assert_eq!(YearFlags::from_year(1900).ndays(), 365);
assert_eq!(YearFlags::from_year(1600).ndays(), 366);
assert_eq!(YearFlags::from_year(1).ndays(), 365);
assert_eq!(YearFlags::from_year(0).ndays(), 366); // 1 BCE (proleptic Gregorian)
assert_eq!(YearFlags::from_year(-1).ndays(), 365); // 2 BCE
assert_eq!(YearFlags::from_year(-4).ndays(), 366); // 5 BCE
assert_eq!(YearFlags::from_year(-99).ndays(), 365); // 100 BCE
assert_eq!(YearFlags::from_year(-100).ndays(), 365); // 101 BCE
assert_eq!(YearFlags::from_year(-399).ndays(), 365); // 400 BCE
assert_eq!(YearFlags::from_year(-400).ndays(), 366); // 401 BCE
}
#[test]
fn test_year_flags_nisoweeks() {
assert_eq!(A.nisoweeks(), 52);
assert_eq!(B.nisoweeks(), 52);
assert_eq!(C.nisoweeks(), 52);
assert_eq!(D.nisoweeks(), 53);
assert_eq!(E.nisoweeks(), 52);
assert_eq!(F.nisoweeks(), 52);
assert_eq!(G.nisoweeks(), 52);
assert_eq!(AG.nisoweeks(), 52);
assert_eq!(BA.nisoweeks(), 52);
assert_eq!(CB.nisoweeks(), 52);
assert_eq!(DC.nisoweeks(), 53);
assert_eq!(ED.nisoweeks(), 53);
assert_eq!(FE.nisoweeks(), 52);
assert_eq!(GF.nisoweeks(), 52);
}
#[test]
fn test_mdf_valid() {
fn check(expected: bool, flags: YearFlags, month1: u32, day1: u32, month2: u32, day2: u32) {
for month in month1..=month2 {
for day in day1..=day2 {
let mdf = match Mdf::new(month, day, flags) {
Some(mdf) => mdf,
None if !expected => continue,
None => panic!("Mdf::new({month}, {day}, {flags:?}) returned None"),
};
assert!(
mdf.valid() == expected,
"month {} day {} = {:?} should be {} for dominical year {:?}",
month,
day,
mdf,
if expected { "valid" } else { "invalid" },
flags
);
}
}
}
for &flags in NONLEAP_FLAGS.iter() {
check(false, flags, 0, 0, 0, 1024);
check(false, flags, 0, 0, 16, 0);
check(true, flags, 1, 1, 1, 31);
check(false, flags, 1, 32, 1, 1024);
check(true, flags, 2, 1, 2, 28);
check(false, flags, 2, 29, 2, 1024);
check(true, flags, 3, 1, 3, 31);
check(false, flags, 3, 32, 3, 1024);
check(true, flags, 4, 1, 4, 30);
check(false, flags, 4, 31, 4, 1024);
check(true, flags, 5, 1, 5, 31);
check(false, flags, 5, 32, 5, 1024);
check(true, flags, 6, 1, 6, 30);
check(false, flags, 6, 31, 6, 1024);
check(true, flags, 7, 1, 7, 31);
check(false, flags, 7, 32, 7, 1024);
check(true, flags, 8, 1, 8, 31);
check(false, flags, 8, 32, 8, 1024);
check(true, flags, 9, 1, 9, 30);
check(false, flags, 9, 31, 9, 1024);
check(true, flags, 10, 1, 10, 31);
check(false, flags, 10, 32, 10, 1024);
check(true, flags, 11, 1, 11, 30);
check(false, flags, 11, 31, 11, 1024);
check(true, flags, 12, 1, 12, 31);
check(false, flags, 12, 32, 12, 1024);
check(false, flags, 13, 0, 16, 1024);
check(false, flags, u32::MAX, 0, u32::MAX, 1024);
check(false, flags, 0, u32::MAX, 16, u32::MAX);
check(false, flags, u32::MAX, u32::MAX, u32::MAX, u32::MAX);
}
for &flags in LEAP_FLAGS.iter() {
check(false, flags, 0, 0, 0, 1024);
check(false, flags, 0, 0, 16, 0);
check(true, flags, 1, 1, 1, 31);
check(false, flags, 1, 32, 1, 1024);
check(true, flags, 2, 1, 2, 29);
check(false, flags, 2, 30, 2, 1024);
check(true, flags, 3, 1, 3, 31);
check(false, flags, 3, 32, 3, 1024);
check(true, flags, 4, 1, 4, 30);
check(false, flags, 4, 31, 4, 1024);
check(true, flags, 5, 1, 5, 31);
check(false, flags, 5, 32, 5, 1024);
check(true, flags, 6, 1, 6, 30);
check(false, flags, 6, 31, 6, 1024);
check(true, flags, 7, 1, 7, 31);
check(false, flags, 7, 32, 7, 1024);
check(true, flags, 8, 1, 8, 31);
check(false, flags, 8, 32, 8, 1024);
check(true, flags, 9, 1, 9, 30);
check(false, flags, 9, 31, 9, 1024);
check(true, flags, 10, 1, 10, 31);
check(false, flags, 10, 32, 10, 1024);
check(true, flags, 11, 1, 11, 30);
check(false, flags, 11, 31, 11, 1024);
check(true, flags, 12, 1, 12, 31);
check(false, flags, 12, 32, 12, 1024);
check(false, flags, 13, 0, 16, 1024);
check(false, flags, u32::MAX, 0, u32::MAX, 1024);
check(false, flags, 0, u32::MAX, 16, u32::MAX);
check(false, flags, u32::MAX, u32::MAX, u32::MAX, u32::MAX);
}
}
#[test]
fn test_mdf_fields() {
for &flags in FLAGS.iter() {
for month in 1u32..=12 {
for day in 1u32..31 {
let mdf = match Mdf::new(month, day, flags) {
Some(mdf) => mdf,
None => continue,
};
if mdf.valid() {
assert_eq!(mdf.month(), month);
assert_eq!(mdf.day(), day);
}
}
}
}
}
#[test]
fn test_mdf_with_fields() {
fn check(flags: YearFlags, month: u32, day: u32) {
let mdf = Mdf::new(month, day, flags).unwrap();
for month in 0u32..=16 {
let mdf = match mdf.with_month(month) {
Some(mdf) => mdf,
None if month > 12 => continue,
None => panic!("failed to create Mdf with month {month}"),
};
if mdf.valid() {
assert_eq!(mdf.month(), month);
assert_eq!(mdf.day(), day);
}
}
for day in 0u32..=1024 {
let mdf = match mdf.with_day(day) {
Some(mdf) => mdf,
None if day > 31 => continue,
None => panic!("failed to create Mdf with month {month}"),
};
if mdf.valid() {
assert_eq!(mdf.month(), month);
assert_eq!(mdf.day(), day);
}
}
}
for &flags in NONLEAP_FLAGS.iter() {
check(flags, 1, 1);
check(flags, 1, 31);
check(flags, 2, 1);
check(flags, 2, 28);
check(flags, 2, 29);
check(flags, 12, 31);
}
for &flags in LEAP_FLAGS.iter() {
check(flags, 1, 1);
check(flags, 1, 31);
check(flags, 2, 1);
check(flags, 2, 29);
check(flags, 2, 30);
check(flags, 12, 31);
}
}
#[test]
fn test_mdf_new_range() {
let flags = YearFlags::from_year(2023);
assert!(Mdf::new(13, 1, flags).is_none());
assert!(Mdf::new(1, 32, flags).is_none());
}
}

View File

@@ -0,0 +1,233 @@
// This is a part of Chrono.
// See README.md and LICENSE.txt for details.
//! ISO 8601 week.
use core::fmt;
use super::internals::YearFlags;
#[cfg(any(feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"))]
use rkyv::{Archive, Deserialize, Serialize};
/// ISO 8601 week.
///
/// This type, combined with [`Weekday`](../enum.Weekday.html),
/// constitutes the ISO 8601 [week date](./struct.NaiveDate.html#week-date).
/// One can retrieve this type from the existing [`Datelike`](../trait.Datelike.html) types
/// via the [`Datelike::iso_week`](../trait.Datelike.html#tymethod.iso_week) method.
#[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)]
#[cfg_attr(
any(feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"),
derive(Archive, Deserialize, Serialize),
rkyv(compare(PartialEq, PartialOrd)),
rkyv(attr(derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)))
)]
#[cfg_attr(feature = "rkyv-validation", archive(check_bytes))]
pub struct IsoWeek {
// Note that this allows for larger year range than `NaiveDate`.
// This is crucial because we have an edge case for the first and last week supported,
// which year number might not match the calendar year number.
ywf: i32, // (year << 10) | (week << 4) | flag
}
impl IsoWeek {
/// Returns the corresponding `IsoWeek` from the year and the `Of` internal value.
//
// Internal use only. We don't expose the public constructor for `IsoWeek` for now
// because the year range for the week date and the calendar date do not match, and
// it is confusing to have a date that is out of range in one and not in another.
// Currently we sidestep this issue by making `IsoWeek` fully dependent of `Datelike`.
pub(super) fn from_yof(year: i32, ordinal: u32, year_flags: YearFlags) -> Self {
let rawweek = (ordinal + year_flags.isoweek_delta()) / 7;
let (year, week) = if rawweek < 1 {
// previous year
let prevlastweek = YearFlags::from_year(year - 1).nisoweeks();
(year - 1, prevlastweek)
} else {
let lastweek = year_flags.nisoweeks();
if rawweek > lastweek {
// next year
(year + 1, 1)
} else {
(year, rawweek)
}
};
let flags = YearFlags::from_year(year);
IsoWeek { ywf: (year << 10) | (week << 4) as i32 | i32::from(flags.0) }
}
/// Returns the year number for this ISO week.
///
/// # Example
///
/// ```
/// use chrono::{Datelike, NaiveDate, Weekday};
///
/// let d = NaiveDate::from_isoywd_opt(2015, 1, Weekday::Mon).unwrap();
/// assert_eq!(d.iso_week().year(), 2015);
/// ```
///
/// This year number might not match the calendar year number.
/// Continuing the example...
///
/// ```
/// # use chrono::{NaiveDate, Datelike, Weekday};
/// # let d = NaiveDate::from_isoywd_opt(2015, 1, Weekday::Mon).unwrap();
/// assert_eq!(d.year(), 2014);
/// assert_eq!(d, NaiveDate::from_ymd_opt(2014, 12, 29).unwrap());
/// ```
#[inline]
pub const fn year(&self) -> i32 {
self.ywf >> 10
}
/// Returns the ISO week number starting from 1.
///
/// The return value ranges from 1 to 53. (The last week of year differs by years.)
///
/// # Example
///
/// ```
/// use chrono::{Datelike, NaiveDate, Weekday};
///
/// let d = NaiveDate::from_isoywd_opt(2015, 15, Weekday::Mon).unwrap();
/// assert_eq!(d.iso_week().week(), 15);
/// ```
#[inline]
pub const fn week(&self) -> u32 {
((self.ywf >> 4) & 0x3f) as u32
}
/// Returns the ISO week number starting from 0.
///
/// The return value ranges from 0 to 52. (The last week of year differs by years.)
///
/// # Example
///
/// ```
/// use chrono::{Datelike, NaiveDate, Weekday};
///
/// let d = NaiveDate::from_isoywd_opt(2015, 15, Weekday::Mon).unwrap();
/// assert_eq!(d.iso_week().week0(), 14);
/// ```
#[inline]
pub const fn week0(&self) -> u32 {
((self.ywf >> 4) & 0x3f) as u32 - 1
}
}
/// The `Debug` output of the ISO week `w` is the same as
/// [`d.format("%G-W%V")`](../format/strftime/index.html)
/// where `d` is any `NaiveDate` value in that week.
///
/// # Example
///
/// ```
/// use chrono::{Datelike, NaiveDate};
///
/// assert_eq!(
/// format!("{:?}", NaiveDate::from_ymd_opt(2015, 9, 5).unwrap().iso_week()),
/// "2015-W36"
/// );
/// assert_eq!(format!("{:?}", NaiveDate::from_ymd_opt(0, 1, 3).unwrap().iso_week()), "0000-W01");
/// assert_eq!(
/// format!("{:?}", NaiveDate::from_ymd_opt(9999, 12, 31).unwrap().iso_week()),
/// "9999-W52"
/// );
/// ```
///
/// ISO 8601 requires an explicit sign for years before 1 BCE or after 9999 CE.
///
/// ```
/// # use chrono::{NaiveDate, Datelike};
/// assert_eq!(format!("{:?}", NaiveDate::from_ymd_opt(0, 1, 2).unwrap().iso_week()), "-0001-W52");
/// assert_eq!(
/// format!("{:?}", NaiveDate::from_ymd_opt(10000, 12, 31).unwrap().iso_week()),
/// "+10000-W52"
/// );
/// ```
impl fmt::Debug for IsoWeek {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let year = self.year();
let week = self.week();
if (0..=9999).contains(&year) {
write!(f, "{year:04}-W{week:02}")
} else {
// ISO 8601 requires the explicit sign for out-of-range years
write!(f, "{year:+05}-W{week:02}")
}
}
}
#[cfg(test)]
mod tests {
#[cfg(feature = "rkyv-validation")]
use super::IsoWeek;
use crate::Datelike;
use crate::naive::date::{self, NaiveDate};
#[test]
fn test_iso_week_extremes() {
let minweek = NaiveDate::MIN.iso_week();
let maxweek = NaiveDate::MAX.iso_week();
assert_eq!(minweek.year(), date::MIN_YEAR);
assert_eq!(minweek.week(), 1);
assert_eq!(minweek.week0(), 0);
#[cfg(feature = "alloc")]
assert_eq!(format!("{:?}", minweek), NaiveDate::MIN.format("%G-W%V").to_string());
assert_eq!(maxweek.year(), date::MAX_YEAR + 1);
assert_eq!(maxweek.week(), 1);
assert_eq!(maxweek.week0(), 0);
#[cfg(feature = "alloc")]
assert_eq!(format!("{:?}", maxweek), NaiveDate::MAX.format("%G-W%V").to_string());
}
#[test]
fn test_iso_week_equivalence_for_first_week() {
let monday = NaiveDate::from_ymd_opt(2024, 12, 30).unwrap();
let friday = NaiveDate::from_ymd_opt(2025, 1, 3).unwrap();
assert_eq!(monday.iso_week(), friday.iso_week());
}
#[test]
fn test_iso_week_equivalence_for_last_week() {
let monday = NaiveDate::from_ymd_opt(2026, 12, 28).unwrap();
let friday = NaiveDate::from_ymd_opt(2027, 1, 1).unwrap();
assert_eq!(monday.iso_week(), friday.iso_week());
}
#[test]
fn test_iso_week_ordering_for_first_week() {
let monday = NaiveDate::from_ymd_opt(2024, 12, 30).unwrap();
let friday = NaiveDate::from_ymd_opt(2025, 1, 3).unwrap();
assert!(monday.iso_week() >= friday.iso_week());
assert!(monday.iso_week() <= friday.iso_week());
}
#[test]
fn test_iso_week_ordering_for_last_week() {
let monday = NaiveDate::from_ymd_opt(2026, 12, 28).unwrap();
let friday = NaiveDate::from_ymd_opt(2027, 1, 1).unwrap();
assert!(monday.iso_week() >= friday.iso_week());
assert!(monday.iso_week() <= friday.iso_week());
}
#[test]
#[cfg(feature = "rkyv-validation")]
fn test_rkyv_validation() {
let minweek = NaiveDate::MIN.iso_week();
let bytes = rkyv::to_bytes::<_, 4>(&minweek).unwrap();
assert_eq!(rkyv::from_bytes::<IsoWeek>(&bytes).unwrap(), minweek);
let maxweek = NaiveDate::MAX.iso_week();
let bytes = rkyv::to_bytes::<_, 4>(&maxweek).unwrap();
assert_eq!(rkyv::from_bytes::<IsoWeek>(&bytes).unwrap(), maxweek);
}
}

View File

@@ -0,0 +1,335 @@
//! Date and time types unconcerned with timezones.
//!
//! They are primarily building blocks for other types
//! (e.g. [`TimeZone`](../offset/trait.TimeZone.html)),
//! but can be also used for the simpler date and time handling.
use core::hash::{Hash, Hasher};
use core::ops::RangeInclusive;
use crate::Weekday;
use crate::expect;
pub(crate) mod date;
pub(crate) mod datetime;
mod internals;
pub(crate) mod isoweek;
pub(crate) mod time;
#[allow(deprecated)]
pub use self::date::{MAX_DATE, MIN_DATE};
pub use self::date::{NaiveDate, NaiveDateDaysIterator, NaiveDateWeeksIterator};
#[allow(deprecated)]
pub use self::datetime::{MAX_DATETIME, MIN_DATETIME, NaiveDateTime};
pub use self::isoweek::IsoWeek;
pub use self::time::NaiveTime;
#[cfg(feature = "__internal_bench")]
#[doc(hidden)]
pub use self::internals::YearFlags as __BenchYearFlags;
/// A week represented by a [`NaiveDate`] and a [`Weekday`] which is the first
/// day of the week.
#[derive(Clone, Copy, Debug, Eq)]
pub struct NaiveWeek {
date: NaiveDate,
start: Weekday,
}
impl NaiveWeek {
/// Create a new `NaiveWeek`
pub(crate) const fn new(date: NaiveDate, start: Weekday) -> Self {
Self { date, start }
}
/// Returns a date representing the first day of the week.
///
/// # Panics
///
/// Panics if the first day of the week happens to fall just out of range of `NaiveDate`
/// (more than ca. 262,000 years away from common era).
///
/// # Examples
///
/// ```
/// use chrono::{NaiveDate, Weekday};
///
/// let date = NaiveDate::from_ymd_opt(2022, 4, 18).unwrap();
/// let week = date.week(Weekday::Mon);
/// assert!(week.first_day() <= date);
/// ```
#[inline]
#[must_use]
pub const fn first_day(&self) -> NaiveDate {
expect(self.checked_first_day(), "first weekday out of range for `NaiveDate`")
}
/// Returns a date representing the first day of the week or
/// `None` if the date is out of `NaiveDate`'s range
/// (more than ca. 262,000 years away from common era).
///
/// # Examples
///
/// ```
/// use chrono::{NaiveDate, Weekday};
///
/// let date = NaiveDate::MIN;
/// let week = date.week(Weekday::Mon);
/// if let Some(first_day) = week.checked_first_day() {
/// assert!(first_day == date);
/// } else {
/// // error handling code
/// return;
/// };
/// ```
#[inline]
#[must_use]
pub const fn checked_first_day(&self) -> Option<NaiveDate> {
let start = self.start.num_days_from_monday() as i32;
let ref_day = self.date.weekday().num_days_from_monday() as i32;
// Calculate the number of days to subtract from `self.date`.
// Do not construct an intermediate date beyond `self.date`, because that may be out of
// range if `date` is close to `NaiveDate::MAX`.
let days = start - ref_day - if start > ref_day { 7 } else { 0 };
self.date.add_days(days)
}
/// Returns a date representing the last day of the week.
///
/// # Panics
///
/// Panics if the last day of the week happens to fall just out of range of `NaiveDate`
/// (more than ca. 262,000 years away from common era).
///
/// # Examples
///
/// ```
/// use chrono::{NaiveDate, Weekday};
///
/// let date = NaiveDate::from_ymd_opt(2022, 4, 18).unwrap();
/// let week = date.week(Weekday::Mon);
/// assert!(week.last_day() >= date);
/// ```
#[inline]
#[must_use]
pub const fn last_day(&self) -> NaiveDate {
expect(self.checked_last_day(), "last weekday out of range for `NaiveDate`")
}
/// Returns a date representing the last day of the week or
/// `None` if the date is out of `NaiveDate`'s range
/// (more than ca. 262,000 years away from common era).
///
/// # Examples
///
/// ```
/// use chrono::{NaiveDate, Weekday};
///
/// let date = NaiveDate::MAX;
/// let week = date.week(Weekday::Mon);
/// if let Some(last_day) = week.checked_last_day() {
/// assert!(last_day == date);
/// } else {
/// // error handling code
/// return;
/// };
/// ```
#[inline]
#[must_use]
pub const fn checked_last_day(&self) -> Option<NaiveDate> {
let end = self.start.pred().num_days_from_monday() as i32;
let ref_day = self.date.weekday().num_days_from_monday() as i32;
// Calculate the number of days to add to `self.date`.
// Do not construct an intermediate date before `self.date` (like with `first_day()`),
// because that may be out of range if `date` is close to `NaiveDate::MIN`.
let days = end - ref_day + if end < ref_day { 7 } else { 0 };
self.date.add_days(days)
}
/// Returns a [`RangeInclusive<T>`] representing the whole week bounded by
/// [first_day](NaiveWeek::first_day) and [last_day](NaiveWeek::last_day) functions.
///
/// # Panics
///
/// Panics if the either the first or last day of the week happens to fall just out of range of
/// `NaiveDate` (more than ca. 262,000 years away from common era).
///
/// # Examples
///
/// ```
/// use chrono::{NaiveDate, Weekday};
///
/// let date = NaiveDate::from_ymd_opt(2022, 4, 18).unwrap();
/// let week = date.week(Weekday::Mon);
/// let days = week.days();
/// assert!(days.contains(&date));
/// ```
#[inline]
#[must_use]
pub const fn days(&self) -> RangeInclusive<NaiveDate> {
// `expect` doesn't work because `RangeInclusive` is not `Copy`
match self.checked_days() {
Some(val) => val,
None => panic!("{}", "first or last weekday is out of range for `NaiveDate`"),
}
}
/// Returns an [`Option<RangeInclusive<T>>`] representing the whole week bounded by
/// [checked_first_day](NaiveWeek::checked_first_day) and
/// [checked_last_day](NaiveWeek::checked_last_day) functions.
///
/// Returns `None` if either of the boundaries are out of `NaiveDate`'s range
/// (more than ca. 262,000 years away from common era).
///
///
/// # Examples
///
/// ```
/// use chrono::{NaiveDate, Weekday};
///
/// let date = NaiveDate::MAX;
/// let week = date.week(Weekday::Mon);
/// let _days = match week.checked_days() {
/// Some(d) => d,
/// None => {
/// // error handling code
/// return;
/// }
/// };
/// ```
#[inline]
#[must_use]
pub const fn checked_days(&self) -> Option<RangeInclusive<NaiveDate>> {
match (self.checked_first_day(), self.checked_last_day()) {
(Some(first), Some(last)) => Some(first..=last),
(_, _) => None,
}
}
}
impl PartialEq for NaiveWeek {
fn eq(&self, other: &Self) -> bool {
self.first_day() == other.first_day()
}
}
impl Hash for NaiveWeek {
fn hash<H: Hasher>(&self, state: &mut H) {
self.first_day().hash(state);
}
}
/// A duration in calendar days.
///
/// This is useful because when using `TimeDelta` it is possible that adding `TimeDelta::days(1)`
/// doesn't increment the day value as expected due to it being a fixed number of seconds. This
/// difference applies only when dealing with `DateTime<TimeZone>` data types and in other cases
/// `TimeDelta::days(n)` and `Days::new(n)` are equivalent.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub struct Days(pub(crate) u64);
impl Days {
/// Construct a new `Days` from a number of days
pub const fn new(num: u64) -> Self {
Self(num)
}
}
/// Serialization/Deserialization of `NaiveDateTime` in alternate formats
///
/// The various modules in here are intended to be used with serde's [`with` annotation] to
/// serialize as something other than the default ISO 8601 format.
///
/// [`with` annotation]: https://serde.rs/field-attrs.html#with
#[cfg(feature = "serde")]
pub mod serde {
pub use super::datetime::serde::*;
}
#[cfg(test)]
mod test {
use crate::{NaiveDate, NaiveWeek, Weekday};
use std::hash::{DefaultHasher, Hash, Hasher};
#[test]
fn test_naiveweek() {
let date = NaiveDate::from_ymd_opt(2022, 5, 18).unwrap();
let asserts = [
(Weekday::Mon, "Mon 2022-05-16", "Sun 2022-05-22"),
(Weekday::Tue, "Tue 2022-05-17", "Mon 2022-05-23"),
(Weekday::Wed, "Wed 2022-05-18", "Tue 2022-05-24"),
(Weekday::Thu, "Thu 2022-05-12", "Wed 2022-05-18"),
(Weekday::Fri, "Fri 2022-05-13", "Thu 2022-05-19"),
(Weekday::Sat, "Sat 2022-05-14", "Fri 2022-05-20"),
(Weekday::Sun, "Sun 2022-05-15", "Sat 2022-05-21"),
];
for (start, first_day, last_day) in asserts {
let week = date.week(start);
let days = week.days();
assert_eq!(Ok(week.first_day()), NaiveDate::parse_from_str(first_day, "%a %Y-%m-%d"));
assert_eq!(Ok(week.last_day()), NaiveDate::parse_from_str(last_day, "%a %Y-%m-%d"));
assert!(days.contains(&date));
}
}
#[test]
fn test_naiveweek_min_max() {
let date_max = NaiveDate::MAX;
assert!(date_max.week(Weekday::Mon).first_day() <= date_max);
let date_min = NaiveDate::MIN;
assert!(date_min.week(Weekday::Mon).last_day() >= date_min);
}
#[test]
fn test_naiveweek_checked_no_panic() {
let date_max = NaiveDate::MAX;
if let Some(last) = date_max.week(Weekday::Mon).checked_last_day() {
assert!(last == date_max);
}
let date_min = NaiveDate::MIN;
if let Some(first) = date_min.week(Weekday::Mon).checked_first_day() {
assert!(first == date_min);
}
let _ = date_min.week(Weekday::Mon).checked_days();
let _ = date_max.week(Weekday::Mon).checked_days();
}
#[test]
fn test_naiveweek_eq() {
let a =
NaiveWeek { date: NaiveDate::from_ymd_opt(2025, 4, 3).unwrap(), start: Weekday::Mon };
let b =
NaiveWeek { date: NaiveDate::from_ymd_opt(2025, 4, 4).unwrap(), start: Weekday::Mon };
assert_eq!(a, b);
let c =
NaiveWeek { date: NaiveDate::from_ymd_opt(2025, 4, 3).unwrap(), start: Weekday::Sun };
assert_ne!(a, c);
assert_ne!(b, c);
}
#[test]
fn test_naiveweek_hash() {
let a =
NaiveWeek { date: NaiveDate::from_ymd_opt(2025, 4, 3).unwrap(), start: Weekday::Mon };
let b =
NaiveWeek { date: NaiveDate::from_ymd_opt(2025, 4, 4).unwrap(), start: Weekday::Mon };
let c =
NaiveWeek { date: NaiveDate::from_ymd_opt(2025, 4, 3).unwrap(), start: Weekday::Sun };
let mut hasher = DefaultHasher::default();
a.hash(&mut hasher);
let a_hash = hasher.finish();
hasher = DefaultHasher::default();
b.hash(&mut hasher);
let b_hash = hasher.finish();
hasher = DefaultHasher::default();
c.hash(&mut hasher);
let c_hash = hasher.finish();
assert_eq!(a_hash, b_hash);
assert_ne!(b_hash, c_hash);
assert_ne!(a_hash, c_hash);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,143 @@
use super::NaiveTime;
use core::fmt;
use serde::{de, ser};
// TODO not very optimized for space (binary formats would want something better)
// TODO round-trip for general leap seconds (not just those with second = 60)
impl ser::Serialize for NaiveTime {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: ser::Serializer,
{
serializer.collect_str(&self)
}
}
struct NaiveTimeVisitor;
impl de::Visitor<'_> for NaiveTimeVisitor {
type Value = NaiveTime;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a formatted time string")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
value.parse().map_err(E::custom)
}
}
impl<'de> de::Deserialize<'de> for NaiveTime {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
deserializer.deserialize_str(NaiveTimeVisitor)
}
}
#[cfg(test)]
mod tests {
use crate::NaiveTime;
#[test]
fn test_serde_serialize() {
assert_eq!(
serde_json::to_string(&NaiveTime::from_hms_opt(0, 0, 0).unwrap()).ok(),
Some(r#""00:00:00""#.into())
);
assert_eq!(
serde_json::to_string(&NaiveTime::from_hms_milli_opt(0, 0, 0, 950).unwrap()).ok(),
Some(r#""00:00:00.950""#.into())
);
assert_eq!(
serde_json::to_string(&NaiveTime::from_hms_milli_opt(0, 0, 59, 1_000).unwrap()).ok(),
Some(r#""00:00:60""#.into())
);
assert_eq!(
serde_json::to_string(&NaiveTime::from_hms_opt(0, 1, 2).unwrap()).ok(),
Some(r#""00:01:02""#.into())
);
assert_eq!(
serde_json::to_string(&NaiveTime::from_hms_nano_opt(3, 5, 7, 98765432).unwrap()).ok(),
Some(r#""03:05:07.098765432""#.into())
);
assert_eq!(
serde_json::to_string(&NaiveTime::from_hms_opt(7, 8, 9).unwrap()).ok(),
Some(r#""07:08:09""#.into())
);
assert_eq!(
serde_json::to_string(&NaiveTime::from_hms_micro_opt(12, 34, 56, 789).unwrap()).ok(),
Some(r#""12:34:56.000789""#.into())
);
let leap = NaiveTime::from_hms_nano_opt(23, 59, 59, 1_999_999_999).unwrap();
assert_eq!(serde_json::to_string(&leap).ok(), Some(r#""23:59:60.999999999""#.into()));
}
#[test]
fn test_serde_deserialize() {
let from_str = serde_json::from_str::<NaiveTime>;
assert_eq!(from_str(r#""00:00:00""#).ok(), Some(NaiveTime::from_hms_opt(0, 0, 0).unwrap()));
assert_eq!(from_str(r#""0:0:0""#).ok(), Some(NaiveTime::from_hms_opt(0, 0, 0).unwrap()));
assert_eq!(
from_str(r#""00:00:00.950""#).ok(),
Some(NaiveTime::from_hms_milli_opt(0, 0, 0, 950).unwrap())
);
assert_eq!(
from_str(r#""0:0:0.95""#).ok(),
Some(NaiveTime::from_hms_milli_opt(0, 0, 0, 950).unwrap())
);
assert_eq!(
from_str(r#""00:00:60""#).ok(),
Some(NaiveTime::from_hms_milli_opt(0, 0, 59, 1_000).unwrap())
);
assert_eq!(from_str(r#""00:01:02""#).ok(), Some(NaiveTime::from_hms_opt(0, 1, 2).unwrap()));
assert_eq!(
from_str(r#""03:05:07.098765432""#).ok(),
Some(NaiveTime::from_hms_nano_opt(3, 5, 7, 98765432).unwrap())
);
assert_eq!(from_str(r#""07:08:09""#).ok(), Some(NaiveTime::from_hms_opt(7, 8, 9).unwrap()));
assert_eq!(
from_str(r#""12:34:56.000789""#).ok(),
Some(NaiveTime::from_hms_micro_opt(12, 34, 56, 789).unwrap())
);
assert_eq!(
from_str(r#""23:59:60.999999999""#).ok(),
Some(NaiveTime::from_hms_nano_opt(23, 59, 59, 1_999_999_999).unwrap())
);
assert_eq!(
from_str(r#""23:59:60.9999999999997""#).ok(), // excess digits are ignored
Some(NaiveTime::from_hms_nano_opt(23, 59, 59, 1_999_999_999).unwrap())
);
// bad formats
assert!(from_str(r#""""#).is_err());
assert!(from_str(r#""000000""#).is_err());
assert!(from_str(r#""00:00:61""#).is_err());
assert!(from_str(r#""00:60:00""#).is_err());
assert!(from_str(r#""24:00:00""#).is_err());
assert!(from_str(r#""23:59:59,1""#).is_err());
assert!(from_str(r#""012:34:56""#).is_err());
assert!(from_str(r#""hh:mm:ss""#).is_err());
assert!(from_str(r#"0"#).is_err());
assert!(from_str(r#"86399"#).is_err());
assert!(from_str(r#"{}"#).is_err());
}
#[test]
fn test_serde_bincode() {
// Bincode is relevant to test separately from JSON because
// it is not self-describing.
use bincode::{deserialize, serialize};
let t = NaiveTime::from_hms_nano_opt(3, 5, 7, 98765432).unwrap();
let encoded = serialize(&t).unwrap();
let decoded: NaiveTime = deserialize(&encoded).unwrap();
assert_eq!(t, decoded);
}
}

View File

@@ -0,0 +1,393 @@
use super::NaiveTime;
use crate::{FixedOffset, TimeDelta, Timelike};
#[test]
fn test_time_from_hms_milli() {
assert_eq!(
NaiveTime::from_hms_milli_opt(3, 5, 7, 0),
Some(NaiveTime::from_hms_nano_opt(3, 5, 7, 0).unwrap())
);
assert_eq!(
NaiveTime::from_hms_milli_opt(3, 5, 7, 777),
Some(NaiveTime::from_hms_nano_opt(3, 5, 7, 777_000_000).unwrap())
);
assert_eq!(
NaiveTime::from_hms_milli_opt(3, 5, 59, 1_999),
Some(NaiveTime::from_hms_nano_opt(3, 5, 59, 1_999_000_000).unwrap())
);
assert_eq!(NaiveTime::from_hms_milli_opt(3, 5, 59, 2_000), None);
assert_eq!(NaiveTime::from_hms_milli_opt(3, 5, 59, 5_000), None); // overflow check
assert_eq!(NaiveTime::from_hms_milli_opt(3, 5, 59, u32::MAX), None);
}
#[test]
fn test_time_from_hms_micro() {
assert_eq!(
NaiveTime::from_hms_micro_opt(3, 5, 7, 0),
Some(NaiveTime::from_hms_nano_opt(3, 5, 7, 0).unwrap())
);
assert_eq!(
NaiveTime::from_hms_micro_opt(3, 5, 7, 333),
Some(NaiveTime::from_hms_nano_opt(3, 5, 7, 333_000).unwrap())
);
assert_eq!(
NaiveTime::from_hms_micro_opt(3, 5, 7, 777_777),
Some(NaiveTime::from_hms_nano_opt(3, 5, 7, 777_777_000).unwrap())
);
assert_eq!(
NaiveTime::from_hms_micro_opt(3, 5, 59, 1_999_999),
Some(NaiveTime::from_hms_nano_opt(3, 5, 59, 1_999_999_000).unwrap())
);
assert_eq!(NaiveTime::from_hms_micro_opt(3, 5, 59, 2_000_000), None);
assert_eq!(NaiveTime::from_hms_micro_opt(3, 5, 59, 5_000_000), None); // overflow check
assert_eq!(NaiveTime::from_hms_micro_opt(3, 5, 59, u32::MAX), None);
}
#[test]
fn test_time_hms() {
assert_eq!(NaiveTime::from_hms_opt(3, 5, 7).unwrap().hour(), 3);
assert_eq!(
NaiveTime::from_hms_opt(3, 5, 7).unwrap().with_hour(0),
Some(NaiveTime::from_hms_opt(0, 5, 7).unwrap())
);
assert_eq!(
NaiveTime::from_hms_opt(3, 5, 7).unwrap().with_hour(23),
Some(NaiveTime::from_hms_opt(23, 5, 7).unwrap())
);
assert_eq!(NaiveTime::from_hms_opt(3, 5, 7).unwrap().with_hour(24), None);
assert_eq!(NaiveTime::from_hms_opt(3, 5, 7).unwrap().with_hour(u32::MAX), None);
assert_eq!(NaiveTime::from_hms_opt(3, 5, 7).unwrap().minute(), 5);
assert_eq!(
NaiveTime::from_hms_opt(3, 5, 7).unwrap().with_minute(0),
Some(NaiveTime::from_hms_opt(3, 0, 7).unwrap())
);
assert_eq!(
NaiveTime::from_hms_opt(3, 5, 7).unwrap().with_minute(59),
Some(NaiveTime::from_hms_opt(3, 59, 7).unwrap())
);
assert_eq!(NaiveTime::from_hms_opt(3, 5, 7).unwrap().with_minute(60), None);
assert_eq!(NaiveTime::from_hms_opt(3, 5, 7).unwrap().with_minute(u32::MAX), None);
assert_eq!(NaiveTime::from_hms_opt(3, 5, 7).unwrap().second(), 7);
assert_eq!(
NaiveTime::from_hms_opt(3, 5, 7).unwrap().with_second(0),
Some(NaiveTime::from_hms_opt(3, 5, 0).unwrap())
);
assert_eq!(
NaiveTime::from_hms_opt(3, 5, 7).unwrap().with_second(59),
Some(NaiveTime::from_hms_opt(3, 5, 59).unwrap())
);
assert_eq!(NaiveTime::from_hms_opt(3, 5, 7).unwrap().with_second(60), None);
assert_eq!(NaiveTime::from_hms_opt(3, 5, 7).unwrap().with_second(u32::MAX), None);
}
#[test]
fn test_time_add() {
macro_rules! check {
($lhs:expr, $rhs:expr, $sum:expr) => {{
assert_eq!($lhs + $rhs, $sum);
//assert_eq!($rhs + $lhs, $sum);
}};
}
let hmsm = |h, m, s, ms| NaiveTime::from_hms_milli_opt(h, m, s, ms).unwrap();
check!(hmsm(3, 5, 59, 900), TimeDelta::zero(), hmsm(3, 5, 59, 900));
check!(hmsm(3, 5, 59, 900), TimeDelta::try_milliseconds(100).unwrap(), hmsm(3, 6, 0, 0));
check!(hmsm(3, 5, 59, 1_300), TimeDelta::try_milliseconds(-1800).unwrap(), hmsm(3, 5, 58, 500));
check!(hmsm(3, 5, 59, 1_300), TimeDelta::try_milliseconds(-800).unwrap(), hmsm(3, 5, 59, 500));
check!(
hmsm(3, 5, 59, 1_300),
TimeDelta::try_milliseconds(-100).unwrap(),
hmsm(3, 5, 59, 1_200)
);
check!(hmsm(3, 5, 59, 1_300), TimeDelta::try_milliseconds(100).unwrap(), hmsm(3, 5, 59, 1_400));
check!(hmsm(3, 5, 59, 1_300), TimeDelta::try_milliseconds(800).unwrap(), hmsm(3, 6, 0, 100));
check!(hmsm(3, 5, 59, 1_300), TimeDelta::try_milliseconds(1800).unwrap(), hmsm(3, 6, 1, 100));
check!(hmsm(3, 5, 59, 900), TimeDelta::try_seconds(86399).unwrap(), hmsm(3, 5, 58, 900)); // overwrap
check!(hmsm(3, 5, 59, 900), TimeDelta::try_seconds(-86399).unwrap(), hmsm(3, 6, 0, 900));
check!(hmsm(3, 5, 59, 900), TimeDelta::try_days(12345).unwrap(), hmsm(3, 5, 59, 900));
check!(hmsm(3, 5, 59, 1_300), TimeDelta::try_days(1).unwrap(), hmsm(3, 5, 59, 300));
check!(hmsm(3, 5, 59, 1_300), TimeDelta::try_days(-1).unwrap(), hmsm(3, 6, 0, 300));
// regression tests for #37
check!(hmsm(0, 0, 0, 0), TimeDelta::try_milliseconds(-990).unwrap(), hmsm(23, 59, 59, 10));
check!(hmsm(0, 0, 0, 0), TimeDelta::try_milliseconds(-9990).unwrap(), hmsm(23, 59, 50, 10));
}
#[test]
fn test_time_overflowing_add() {
let hmsm = |h, m, s, ms| NaiveTime::from_hms_milli_opt(h, m, s, ms).unwrap();
assert_eq!(
hmsm(3, 4, 5, 678).overflowing_add_signed(TimeDelta::try_hours(11).unwrap()),
(hmsm(14, 4, 5, 678), 0)
);
assert_eq!(
hmsm(3, 4, 5, 678).overflowing_add_signed(TimeDelta::try_hours(23).unwrap()),
(hmsm(2, 4, 5, 678), 86_400)
);
assert_eq!(
hmsm(3, 4, 5, 678).overflowing_add_signed(TimeDelta::try_hours(-7).unwrap()),
(hmsm(20, 4, 5, 678), -86_400)
);
// overflowing_add_signed with leap seconds may be counter-intuitive
assert_eq!(
hmsm(3, 4, 59, 1_678).overflowing_add_signed(TimeDelta::try_days(1).unwrap()),
(hmsm(3, 4, 59, 678), 86_400)
);
assert_eq!(
hmsm(3, 4, 59, 1_678).overflowing_add_signed(TimeDelta::try_days(-1).unwrap()),
(hmsm(3, 5, 0, 678), -86_400)
);
}
#[test]
fn test_time_addassignment() {
let hms = |h, m, s| NaiveTime::from_hms_opt(h, m, s).unwrap();
let mut time = hms(12, 12, 12);
time += TimeDelta::try_hours(10).unwrap();
assert_eq!(time, hms(22, 12, 12));
time += TimeDelta::try_hours(10).unwrap();
assert_eq!(time, hms(8, 12, 12));
}
#[test]
fn test_time_subassignment() {
let hms = |h, m, s| NaiveTime::from_hms_opt(h, m, s).unwrap();
let mut time = hms(12, 12, 12);
time -= TimeDelta::try_hours(10).unwrap();
assert_eq!(time, hms(2, 12, 12));
time -= TimeDelta::try_hours(10).unwrap();
assert_eq!(time, hms(16, 12, 12));
}
#[test]
fn test_time_sub() {
macro_rules! check {
($lhs:expr, $rhs:expr, $diff:expr) => {{
// `time1 - time2 = duration` is equivalent to `time2 - time1 = -duration`
assert_eq!($lhs.signed_duration_since($rhs), $diff);
assert_eq!($rhs.signed_duration_since($lhs), -$diff);
}};
}
let hmsm = |h, m, s, ms| NaiveTime::from_hms_milli_opt(h, m, s, ms).unwrap();
check!(hmsm(3, 5, 7, 900), hmsm(3, 5, 7, 900), TimeDelta::zero());
check!(hmsm(3, 5, 7, 900), hmsm(3, 5, 7, 600), TimeDelta::try_milliseconds(300).unwrap());
check!(hmsm(3, 5, 7, 200), hmsm(2, 4, 6, 200), TimeDelta::try_seconds(3600 + 60 + 1).unwrap());
check!(
hmsm(3, 5, 7, 200),
hmsm(2, 4, 6, 300),
TimeDelta::try_seconds(3600 + 60).unwrap() + TimeDelta::try_milliseconds(900).unwrap()
);
// treats the leap second as if it coincides with the prior non-leap second,
// as required by `time1 - time2 = duration` and `time2 - time1 = -duration` equivalence.
check!(hmsm(3, 6, 0, 200), hmsm(3, 5, 59, 1_800), TimeDelta::try_milliseconds(400).unwrap());
//check!(hmsm(3, 5, 7, 1_200), hmsm(3, 5, 6, 1_800), TimeDelta::try_milliseconds(1400).unwrap());
//check!(hmsm(3, 5, 7, 1_200), hmsm(3, 5, 6, 800), TimeDelta::try_milliseconds(1400).unwrap());
// additional equality: `time1 + duration = time2` is equivalent to
// `time2 - time1 = duration` IF AND ONLY IF `time2` represents a non-leap second.
assert_eq!(hmsm(3, 5, 6, 800) + TimeDelta::try_milliseconds(400).unwrap(), hmsm(3, 5, 7, 200));
//assert_eq!(hmsm(3, 5, 6, 1_800) + TimeDelta::try_milliseconds(400).unwrap(), hmsm(3, 5, 7, 200));
}
#[test]
fn test_core_duration_ops() {
use core::time::Duration;
let mut t = NaiveTime::from_hms_opt(11, 34, 23).unwrap();
let same = t + Duration::ZERO;
assert_eq!(t, same);
t += Duration::new(3600, 0);
assert_eq!(t, NaiveTime::from_hms_opt(12, 34, 23).unwrap());
t -= Duration::new(7200, 0);
assert_eq!(t, NaiveTime::from_hms_opt(10, 34, 23).unwrap());
}
#[test]
fn test_time_fmt() {
assert_eq!(
format!("{}", NaiveTime::from_hms_milli_opt(23, 59, 59, 999).unwrap()),
"23:59:59.999"
);
assert_eq!(
format!("{}", NaiveTime::from_hms_milli_opt(23, 59, 59, 1_000).unwrap()),
"23:59:60"
);
assert_eq!(
format!("{}", NaiveTime::from_hms_milli_opt(23, 59, 59, 1_001).unwrap()),
"23:59:60.001"
);
assert_eq!(
format!("{}", NaiveTime::from_hms_micro_opt(0, 0, 0, 43210).unwrap()),
"00:00:00.043210"
);
assert_eq!(
format!("{}", NaiveTime::from_hms_nano_opt(0, 0, 0, 6543210).unwrap()),
"00:00:00.006543210"
);
// the format specifier should have no effect on `NaiveTime`
assert_eq!(
format!("{:30}", NaiveTime::from_hms_milli_opt(3, 5, 7, 9).unwrap()),
"03:05:07.009"
);
}
#[test]
fn test_time_from_str() {
// valid cases
let valid = [
"0:0:0",
"0:0:0.0000000",
"0:0:0.0000003",
" 4 : 3 : 2.1 ",
" 09:08:07 ",
" 09:08 ",
" 9:8:07 ",
"01:02:03",
"4:3:2.1",
"9:8:7",
"09:8:7",
"9:08:7",
"9:8:07",
"09:08:7",
"09:8:07",
"09:08:7",
"9:08:07",
"09:08:07",
"9:8:07.123",
"9:08:7.123",
"09:8:7.123",
"09:08:7.123",
"9:08:07.123",
"09:8:07.123",
"09:08:07.123",
"09:08:07.123",
"09:08:07.1234",
"09:08:07.12345",
"09:08:07.123456",
"09:08:07.1234567",
"09:08:07.12345678",
"09:08:07.123456789",
"09:08:07.1234567891",
"09:08:07.12345678912",
"23:59:60.373929310237",
];
for &s in &valid {
eprintln!("test_time_parse_from_str valid {:?}", s);
let d = match s.parse::<NaiveTime>() {
Ok(d) => d,
Err(e) => panic!("parsing `{}` has failed: {}", s, e),
};
let s_ = format!("{:?}", d);
// `s` and `s_` may differ, but `s.parse()` and `s_.parse()` must be same
let d_ = match s_.parse::<NaiveTime>() {
Ok(d) => d,
Err(e) => {
panic!("`{}` is parsed into `{:?}`, but reparsing that has failed: {}", s, d, e)
}
};
assert!(
d == d_,
"`{}` is parsed into `{:?}`, but reparsed result \
`{:?}` does not match",
s,
d,
d_
);
}
// some invalid cases
// since `ParseErrorKind` is private, all we can do is to check if there was an error
let invalid = [
"", // empty
"x", // invalid
"15", // missing data
"15:8:", // trailing colon
"15:8:x", // invalid data
"15:8:9x", // invalid data
"23:59:61", // invalid second (out of bounds)
"23:54:35 GMT", // invalid (timezone non-sensical for NaiveTime)
"23:54:35 +0000", // invalid (timezone non-sensical for NaiveTime)
"1441497364.649", // valid datetime, not a NaiveTime
"+1441497364.649", // valid datetime, not a NaiveTime
"+1441497364", // valid datetime, not a NaiveTime
"001:02:03", // invalid hour
"01:002:03", // invalid minute
"01:02:003", // invalid second
"12:34:56.x", // invalid fraction
"12:34:56. 0", // invalid fraction format
"09:08:00000000007", // invalid second / invalid fraction format
];
for &s in &invalid {
eprintln!("test_time_parse_from_str invalid {:?}", s);
assert!(s.parse::<NaiveTime>().is_err());
}
}
#[test]
fn test_time_parse_from_str() {
let hms = |h, m, s| NaiveTime::from_hms_opt(h, m, s).unwrap();
assert_eq!(
NaiveTime::parse_from_str("2014-5-7T12:34:56+09:30", "%Y-%m-%dT%H:%M:%S%z"),
Ok(hms(12, 34, 56))
); // ignore date and offset
assert_eq!(NaiveTime::parse_from_str("PM 12:59", "%P %H:%M"), Ok(hms(12, 59, 0)));
assert_eq!(NaiveTime::parse_from_str("12:59 \n\t PM", "%H:%M \n\t %P"), Ok(hms(12, 59, 0)));
assert_eq!(NaiveTime::parse_from_str("\t\t12:59\tPM\t", "\t\t%H:%M\t%P\t"), Ok(hms(12, 59, 0)));
assert_eq!(
NaiveTime::parse_from_str("\t\t1259\t\tPM\t", "\t\t%H%M\t\t%P\t"),
Ok(hms(12, 59, 0))
);
assert!(NaiveTime::parse_from_str("12:59 PM", "%H:%M\t%P").is_ok());
assert!(NaiveTime::parse_from_str("\t\t12:59 PM\t", "\t\t%H:%M\t%P\t").is_ok());
assert!(NaiveTime::parse_from_str("12:59 PM", "%H:%M %P").is_ok());
assert!(NaiveTime::parse_from_str("12:3456", "%H:%M:%S").is_err());
}
#[test]
fn test_overflowing_offset() {
let hmsm = |h, m, s, n| NaiveTime::from_hms_milli_opt(h, m, s, n).unwrap();
let positive_offset = FixedOffset::east_opt(4 * 60 * 60).unwrap();
// regular time
let t = hmsm(5, 6, 7, 890);
assert_eq!(t.overflowing_add_offset(positive_offset), (hmsm(9, 6, 7, 890), 0));
assert_eq!(t.overflowing_sub_offset(positive_offset), (hmsm(1, 6, 7, 890), 0));
// leap second is preserved, and wrap to next day
let t = hmsm(23, 59, 59, 1_000);
assert_eq!(t.overflowing_add_offset(positive_offset), (hmsm(3, 59, 59, 1_000), 1));
assert_eq!(t.overflowing_sub_offset(positive_offset), (hmsm(19, 59, 59, 1_000), 0));
// wrap to previous day
let t = hmsm(1, 2, 3, 456);
assert_eq!(t.overflowing_sub_offset(positive_offset), (hmsm(21, 2, 3, 456), -1));
// an odd offset
let negative_offset = FixedOffset::west_opt(((2 * 60) + 3) * 60 + 4).unwrap();
let t = hmsm(5, 6, 7, 890);
assert_eq!(t.overflowing_add_offset(negative_offset), (hmsm(3, 3, 3, 890), 0));
assert_eq!(t.overflowing_sub_offset(negative_offset), (hmsm(7, 9, 11, 890), 0));
assert_eq!(t.overflowing_add_offset(positive_offset).0, t + positive_offset);
assert_eq!(t.overflowing_sub_offset(positive_offset).0, t - positive_offset);
}
#[test]
#[cfg(feature = "rkyv-validation")]
fn test_rkyv_validation() {
let t_min = NaiveTime::MIN;
let bytes = rkyv::to_bytes::<_, 8>(&t_min).unwrap();
assert_eq!(rkyv::from_bytes::<NaiveTime>(&bytes).unwrap(), t_min);
let t_max = NaiveTime::MAX;
let bytes = rkyv::to_bytes::<_, 8>(&t_max).unwrap();
assert_eq!(rkyv::from_bytes::<NaiveTime>(&bytes).unwrap(), t_max);
}

View File

@@ -0,0 +1,236 @@
// This is a part of Chrono.
// See README.md and LICENSE.txt for details.
//! The time zone which has a fixed offset from UTC.
use core::fmt;
use core::str::FromStr;
#[cfg(any(feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"))]
use rkyv::{Archive, Deserialize, Serialize};
use super::{MappedLocalTime, Offset, TimeZone};
use crate::format::{OUT_OF_RANGE, ParseError, scan};
use crate::naive::{NaiveDate, NaiveDateTime};
/// The time zone with fixed offset, from UTC-23:59:59 to UTC+23:59:59.
///
/// Using the [`TimeZone`](./trait.TimeZone.html) methods
/// on a `FixedOffset` struct is the preferred way to construct
/// `DateTime<FixedOffset>` instances. See the [`east_opt`](#method.east_opt) and
/// [`west_opt`](#method.west_opt) methods for examples.
#[derive(PartialEq, Eq, Hash, Copy, Clone)]
#[cfg_attr(
any(feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"),
derive(Archive, Deserialize, Serialize),
rkyv(compare(PartialEq)),
rkyv(attr(derive(Clone, Copy, PartialEq, Eq, Hash, Debug)))
)]
#[cfg_attr(feature = "rkyv-validation", archive(check_bytes))]
pub struct FixedOffset {
local_minus_utc: i32,
}
impl FixedOffset {
/// Makes a new `FixedOffset` for the Eastern Hemisphere with given timezone difference.
/// The negative `secs` means the Western Hemisphere.
///
/// Panics on the out-of-bound `secs`.
#[deprecated(since = "0.4.23", note = "use `east_opt()` instead")]
#[must_use]
pub fn east(secs: i32) -> FixedOffset {
FixedOffset::east_opt(secs).expect("FixedOffset::east out of bounds")
}
/// Makes a new `FixedOffset` for the Eastern Hemisphere with given timezone difference.
/// The negative `secs` means the Western Hemisphere.
///
/// Returns `None` on the out-of-bound `secs`.
///
/// # Example
///
/// ```
/// # #[cfg(feature = "alloc")] {
/// use chrono::{FixedOffset, TimeZone};
/// let hour = 3600;
/// let datetime =
/// FixedOffset::east_opt(5 * hour).unwrap().with_ymd_and_hms(2016, 11, 08, 0, 0, 0).unwrap();
/// assert_eq!(&datetime.to_rfc3339(), "2016-11-08T00:00:00+05:00")
/// # }
/// ```
#[must_use]
pub const fn east_opt(secs: i32) -> Option<FixedOffset> {
if -86_400 < secs && secs < 86_400 {
Some(FixedOffset { local_minus_utc: secs })
} else {
None
}
}
/// Makes a new `FixedOffset` for the Western Hemisphere with given timezone difference.
/// The negative `secs` means the Eastern Hemisphere.
///
/// Panics on the out-of-bound `secs`.
#[deprecated(since = "0.4.23", note = "use `west_opt()` instead")]
#[must_use]
pub fn west(secs: i32) -> FixedOffset {
FixedOffset::west_opt(secs).expect("FixedOffset::west out of bounds")
}
/// Makes a new `FixedOffset` for the Western Hemisphere with given timezone difference.
/// The negative `secs` means the Eastern Hemisphere.
///
/// Returns `None` on the out-of-bound `secs`.
///
/// # Example
///
/// ```
/// # #[cfg(feature = "alloc")] {
/// use chrono::{FixedOffset, TimeZone};
/// let hour = 3600;
/// let datetime =
/// FixedOffset::west_opt(5 * hour).unwrap().with_ymd_and_hms(2016, 11, 08, 0, 0, 0).unwrap();
/// assert_eq!(&datetime.to_rfc3339(), "2016-11-08T00:00:00-05:00")
/// # }
/// ```
#[must_use]
pub const fn west_opt(secs: i32) -> Option<FixedOffset> {
if -86_400 < secs && secs < 86_400 {
Some(FixedOffset { local_minus_utc: -secs })
} else {
None
}
}
/// Returns the number of seconds to add to convert from UTC to the local time.
#[inline]
pub const fn local_minus_utc(&self) -> i32 {
self.local_minus_utc
}
/// Returns the number of seconds to add to convert from the local time to UTC.
#[inline]
pub const fn utc_minus_local(&self) -> i32 {
-self.local_minus_utc
}
}
/// Parsing a `str` into a `FixedOffset` uses the format [`%z`](crate::format::strftime).
impl FromStr for FixedOffset {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (_, offset) = scan::timezone_offset(s, scan::colon_or_space, false, false, true)?;
Self::east_opt(offset).ok_or(OUT_OF_RANGE)
}
}
impl TimeZone for FixedOffset {
type Offset = FixedOffset;
fn from_offset(offset: &FixedOffset) -> FixedOffset {
*offset
}
fn offset_from_local_date(&self, _local: &NaiveDate) -> MappedLocalTime<FixedOffset> {
MappedLocalTime::Single(*self)
}
fn offset_from_local_datetime(&self, _local: &NaiveDateTime) -> MappedLocalTime<FixedOffset> {
MappedLocalTime::Single(*self)
}
fn offset_from_utc_date(&self, _utc: &NaiveDate) -> FixedOffset {
*self
}
fn offset_from_utc_datetime(&self, _utc: &NaiveDateTime) -> FixedOffset {
*self
}
}
impl Offset for FixedOffset {
fn fix(&self) -> FixedOffset {
*self
}
}
impl fmt::Debug for FixedOffset {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let offset = self.local_minus_utc;
let (sign, offset) = if offset < 0 { ('-', -offset) } else { ('+', offset) };
let sec = offset.rem_euclid(60);
let mins = offset.div_euclid(60);
let min = mins.rem_euclid(60);
let hour = mins.div_euclid(60);
if sec == 0 {
write!(f, "{sign}{hour:02}:{min:02}")
} else {
write!(f, "{sign}{hour:02}:{min:02}:{sec:02}")
}
}
}
impl fmt::Display for FixedOffset {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Debug::fmt(self, f)
}
}
#[cfg(all(feature = "arbitrary", feature = "std"))]
impl arbitrary::Arbitrary<'_> for FixedOffset {
fn arbitrary(u: &mut arbitrary::Unstructured) -> arbitrary::Result<FixedOffset> {
let secs = u.int_in_range(-86_399..=86_399)?;
let fixed_offset = FixedOffset::east_opt(secs)
.expect("Could not generate a valid chrono::FixedOffset. It looks like implementation of Arbitrary for FixedOffset is erroneous.");
Ok(fixed_offset)
}
}
#[cfg(test)]
mod tests {
use super::FixedOffset;
use crate::offset::TimeZone;
use std::str::FromStr;
#[test]
fn test_date_extreme_offset() {
// starting from 0.3 we don't have an offset exceeding one day.
// this makes everything easier!
let offset = FixedOffset::east_opt(86399).unwrap();
assert_eq!(
format!("{:?}", offset.with_ymd_and_hms(2012, 2, 29, 5, 6, 7).unwrap()),
"2012-02-29T05:06:07+23:59:59"
);
let offset = FixedOffset::east_opt(-86399).unwrap();
assert_eq!(
format!("{:?}", offset.with_ymd_and_hms(2012, 2, 29, 5, 6, 7).unwrap()),
"2012-02-29T05:06:07-23:59:59"
);
let offset = FixedOffset::west_opt(86399).unwrap();
assert_eq!(
format!("{:?}", offset.with_ymd_and_hms(2012, 3, 4, 5, 6, 7).unwrap()),
"2012-03-04T05:06:07-23:59:59"
);
let offset = FixedOffset::west_opt(-86399).unwrap();
assert_eq!(
format!("{:?}", offset.with_ymd_and_hms(2012, 3, 4, 5, 6, 7).unwrap()),
"2012-03-04T05:06:07+23:59:59"
);
}
#[test]
fn test_parse_offset() {
let offset = FixedOffset::from_str("-0500").unwrap();
assert_eq!(offset.local_minus_utc, -5 * 3600);
let offset = FixedOffset::from_str("-08:00").unwrap();
assert_eq!(offset.local_minus_utc, -8 * 3600);
let offset = FixedOffset::from_str("+06:30").unwrap();
assert_eq!(offset.local_minus_utc, (6 * 3600) + 1800);
}
#[test]
#[cfg(feature = "rkyv-validation")]
fn test_rkyv_validation() {
let offset = FixedOffset::from_str("-0500").unwrap();
let bytes = rkyv::to_bytes::<_, 4>(&offset).unwrap();
assert_eq!(rkyv::from_bytes::<FixedOffset>(&bytes).unwrap(), offset);
}
}

View File

@@ -0,0 +1,544 @@
// This is a part of Chrono.
// See README.md and LICENSE.txt for details.
//! The local (system) time zone.
#[cfg(windows)]
use std::cmp::Ordering;
#[cfg(any(feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"))]
use rkyv::{Archive, Deserialize, Serialize};
use super::fixed::FixedOffset;
use super::{MappedLocalTime, TimeZone};
#[allow(deprecated)]
use crate::Date;
use crate::naive::{NaiveDate, NaiveDateTime, NaiveTime};
use crate::{DateTime, Utc};
#[cfg(unix)]
#[path = "unix.rs"]
mod inner;
#[cfg(windows)]
#[path = "windows.rs"]
mod inner;
#[cfg(all(windows, feature = "clock"))]
#[allow(unreachable_pub)]
mod win_bindings;
#[cfg(all(any(target_os = "android", target_env = "ohos", test), feature = "clock"))]
mod tz_data;
#[cfg(all(
not(unix),
not(windows),
not(all(
target_arch = "wasm32",
feature = "wasmbind",
not(any(target_os = "emscripten", target_os = "wasi"))
))
))]
mod inner {
use crate::{FixedOffset, MappedLocalTime, NaiveDateTime};
pub(super) fn offset_from_utc_datetime(
_utc_time: &NaiveDateTime,
) -> MappedLocalTime<FixedOffset> {
MappedLocalTime::Single(FixedOffset::east_opt(0).unwrap())
}
pub(super) fn offset_from_local_datetime(
_local_time: &NaiveDateTime,
) -> MappedLocalTime<FixedOffset> {
MappedLocalTime::Single(FixedOffset::east_opt(0).unwrap())
}
}
#[cfg(all(
target_arch = "wasm32",
feature = "wasmbind",
not(any(target_os = "emscripten", target_os = "wasi", target_os = "linux"))
))]
mod inner {
use crate::{Datelike, FixedOffset, MappedLocalTime, NaiveDateTime, Timelike};
pub(super) fn offset_from_utc_datetime(utc: &NaiveDateTime) -> MappedLocalTime<FixedOffset> {
let offset = js_sys::Date::from(utc.and_utc()).get_timezone_offset();
MappedLocalTime::Single(FixedOffset::west_opt((offset as i32) * 60).unwrap())
}
pub(super) fn offset_from_local_datetime(
local: &NaiveDateTime,
) -> MappedLocalTime<FixedOffset> {
let mut year = local.year();
if year < 100 {
// The API in `js_sys` does not let us create a `Date` with negative years.
// And values for years from `0` to `99` map to the years `1900` to `1999`.
// Shift the value by a multiple of 400 years until it is `>= 100`.
let shift_cycles = (year - 100).div_euclid(400);
year -= shift_cycles * 400;
}
let js_date = js_sys::Date::new_with_year_month_day_hr_min_sec(
year as u32,
local.month0() as i32,
local.day() as i32,
local.hour() as i32,
local.minute() as i32,
local.second() as i32,
// ignore milliseconds, our representation of leap seconds may be problematic
);
let offset = js_date.get_timezone_offset();
// We always get a result, even if this time does not exist or is ambiguous.
MappedLocalTime::Single(FixedOffset::west_opt((offset as i32) * 60).unwrap())
}
}
#[cfg(unix)]
mod tz_info;
/// The local timescale.
///
/// Using the [`TimeZone`](./trait.TimeZone.html) methods
/// on the Local struct is the preferred way to construct `DateTime<Local>`
/// instances.
///
/// # Example
///
/// ```
/// use chrono::{DateTime, Local, TimeZone};
///
/// let dt1: DateTime<Local> = Local::now();
/// let dt2: DateTime<Local> = Local.timestamp_opt(0, 0).unwrap();
/// assert!(dt1 >= dt2);
/// ```
#[derive(Copy, Clone, Debug)]
#[cfg_attr(
any(feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"),
derive(Archive, Deserialize, Serialize),
rkyv(compare(PartialEq)),
rkyv(attr(derive(Clone, Copy, Debug)))
)]
#[cfg_attr(feature = "rkyv-validation", archive(check_bytes))]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct Local;
impl Local {
/// Returns a `Date` which corresponds to the current date.
#[deprecated(since = "0.4.23", note = "use `Local::now()` instead")]
#[allow(deprecated)]
#[must_use]
pub fn today() -> Date<Local> {
Local::now().date()
}
/// Returns a `DateTime<Local>` which corresponds to the current date, time and offset from
/// UTC.
///
/// See also the similar [`Utc::now()`] which returns `DateTime<Utc>`, i.e. without the local
/// offset.
///
/// # Example
///
/// ```
/// # #![allow(unused_variables)]
/// # use chrono::{DateTime, FixedOffset, Local};
/// // Current local time
/// let now = Local::now();
///
/// // Current local date
/// let today = now.date_naive();
///
/// // Current local time, converted to `DateTime<FixedOffset>`
/// let now_fixed_offset = Local::now().fixed_offset();
/// // or
/// let now_fixed_offset: DateTime<FixedOffset> = Local::now().into();
///
/// // Current time in some timezone (let's use +05:00)
/// // Note that it is usually more efficient to use `Utc::now` for this use case.
/// let offset = FixedOffset::east_opt(5 * 60 * 60).unwrap();
/// let now_with_offset = Local::now().with_timezone(&offset);
/// ```
pub fn now() -> DateTime<Local> {
Utc::now().with_timezone(&Local)
}
}
impl TimeZone for Local {
type Offset = FixedOffset;
fn from_offset(_offset: &FixedOffset) -> Local {
Local
}
#[allow(deprecated)]
fn offset_from_local_date(&self, local: &NaiveDate) -> MappedLocalTime<FixedOffset> {
// Get the offset at local midnight.
self.offset_from_local_datetime(&local.and_time(NaiveTime::MIN))
}
fn offset_from_local_datetime(&self, local: &NaiveDateTime) -> MappedLocalTime<FixedOffset> {
inner::offset_from_local_datetime(local)
}
#[allow(deprecated)]
fn offset_from_utc_date(&self, utc: &NaiveDate) -> FixedOffset {
// Get the offset at midnight.
self.offset_from_utc_datetime(&utc.and_time(NaiveTime::MIN))
}
fn offset_from_utc_datetime(&self, utc: &NaiveDateTime) -> FixedOffset {
inner::offset_from_utc_datetime(utc).unwrap()
}
}
#[cfg(windows)]
#[derive(Copy, Clone, Eq, PartialEq)]
struct Transition {
transition_utc: NaiveDateTime,
offset_before: FixedOffset,
offset_after: FixedOffset,
}
#[cfg(windows)]
impl Transition {
fn new(
transition_local: NaiveDateTime,
offset_before: FixedOffset,
offset_after: FixedOffset,
) -> Transition {
// It is no problem if the transition time in UTC falls a couple of hours inside the buffer
// space around the `NaiveDateTime` range (although it is very theoretical to have a
// transition at midnight around `NaiveDate::(MIN|MAX)`.
let transition_utc = transition_local.overflowing_sub_offset(offset_before);
Transition { transition_utc, offset_before, offset_after }
}
}
#[cfg(windows)]
impl PartialOrd for Transition {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.transition_utc.cmp(&other.transition_utc))
}
}
#[cfg(windows)]
impl Ord for Transition {
fn cmp(&self, other: &Self) -> Ordering {
self.transition_utc.cmp(&other.transition_utc)
}
}
// Calculate the time in UTC given a local time and transitions.
// `transitions` must be sorted.
#[cfg(windows)]
fn lookup_with_dst_transitions(
transitions: &[Transition],
dt: NaiveDateTime,
) -> MappedLocalTime<FixedOffset> {
for t in transitions.iter() {
// A transition can result in the wall clock time going forward (creating a gap) or going
// backward (creating a fold). We are interested in the earliest and latest wall time of the
// transition, as this are the times between which `dt` does may not exist or is ambiguous.
//
// It is no problem if the transition times falls a couple of hours inside the buffer
// space around the `NaiveDateTime` range (although it is very theoretical to have a
// transition at midnight around `NaiveDate::(MIN|MAX)`.
let (offset_min, offset_max) =
match t.offset_after.local_minus_utc() > t.offset_before.local_minus_utc() {
true => (t.offset_before, t.offset_after),
false => (t.offset_after, t.offset_before),
};
let wall_earliest = t.transition_utc.overflowing_add_offset(offset_min);
let wall_latest = t.transition_utc.overflowing_add_offset(offset_max);
if dt < wall_earliest {
return MappedLocalTime::Single(t.offset_before);
} else if dt <= wall_latest {
return match t.offset_after.local_minus_utc().cmp(&t.offset_before.local_minus_utc()) {
Ordering::Equal => MappedLocalTime::Single(t.offset_before),
Ordering::Less => MappedLocalTime::Ambiguous(t.offset_before, t.offset_after),
Ordering::Greater => {
if dt == wall_earliest {
MappedLocalTime::Single(t.offset_before)
} else if dt == wall_latest {
MappedLocalTime::Single(t.offset_after)
} else {
MappedLocalTime::None
}
}
};
}
}
MappedLocalTime::Single(transitions.last().unwrap().offset_after)
}
#[cfg(test)]
mod tests {
use super::Local;
use crate::offset::TimeZone;
#[cfg(windows)]
use crate::offset::local::{Transition, lookup_with_dst_transitions};
use crate::{Datelike, Days, Utc};
#[cfg(windows)]
use crate::{FixedOffset, MappedLocalTime, NaiveDate, NaiveDateTime};
#[test]
fn verify_correct_offsets() {
let now = Local::now();
let from_local = Local.from_local_datetime(&now.naive_local()).unwrap();
let from_utc = Local.from_utc_datetime(&now.naive_utc());
assert_eq!(now.offset().local_minus_utc(), from_local.offset().local_minus_utc());
assert_eq!(now.offset().local_minus_utc(), from_utc.offset().local_minus_utc());
assert_eq!(now, from_local);
assert_eq!(now, from_utc);
}
#[test]
fn verify_correct_offsets_distant_past() {
let distant_past = Local::now() - Days::new(365 * 500);
let from_local = Local.from_local_datetime(&distant_past.naive_local()).unwrap();
let from_utc = Local.from_utc_datetime(&distant_past.naive_utc());
assert_eq!(distant_past.offset().local_minus_utc(), from_local.offset().local_minus_utc());
assert_eq!(distant_past.offset().local_minus_utc(), from_utc.offset().local_minus_utc());
assert_eq!(distant_past, from_local);
assert_eq!(distant_past, from_utc);
}
#[test]
fn verify_correct_offsets_distant_future() {
let distant_future = Local::now() + Days::new(365 * 35000);
let from_local = Local.from_local_datetime(&distant_future.naive_local()).unwrap();
let from_utc = Local.from_utc_datetime(&distant_future.naive_utc());
assert_eq!(
distant_future.offset().local_minus_utc(),
from_local.offset().local_minus_utc()
);
assert_eq!(distant_future.offset().local_minus_utc(), from_utc.offset().local_minus_utc());
assert_eq!(distant_future, from_local);
assert_eq!(distant_future, from_utc);
}
#[test]
fn test_local_date_sanity_check() {
// issue #27
assert_eq!(Local.with_ymd_and_hms(2999, 12, 28, 0, 0, 0).unwrap().day(), 28);
}
#[test]
fn test_leap_second() {
// issue #123
let today = Utc::now().date_naive();
if let Some(dt) = today.and_hms_milli_opt(15, 2, 59, 1000) {
let timestr = dt.time().to_string();
// the OS API may or may not support the leap second,
// but there are only two sensible options.
assert!(
timestr == "15:02:60" || timestr == "15:03:00",
"unexpected timestr {:?}",
timestr
);
}
if let Some(dt) = today.and_hms_milli_opt(15, 2, 3, 1234) {
let timestr = dt.time().to_string();
assert!(
timestr == "15:02:03.234" || timestr == "15:02:04.234",
"unexpected timestr {:?}",
timestr
);
}
}
#[test]
#[cfg(windows)]
fn test_lookup_with_dst_transitions() {
let ymdhms = |y, m, d, h, n, s| {
NaiveDate::from_ymd_opt(y, m, d).unwrap().and_hms_opt(h, n, s).unwrap()
};
#[track_caller]
#[allow(clippy::too_many_arguments)]
fn compare_lookup(
transitions: &[Transition],
y: i32,
m: u32,
d: u32,
h: u32,
n: u32,
s: u32,
result: MappedLocalTime<FixedOffset>,
) {
let dt = NaiveDate::from_ymd_opt(y, m, d).unwrap().and_hms_opt(h, n, s).unwrap();
assert_eq!(lookup_with_dst_transitions(transitions, dt), result);
}
// dst transition before std transition
// dst offset > std offset
let std = FixedOffset::east_opt(3 * 60 * 60).unwrap();
let dst = FixedOffset::east_opt(4 * 60 * 60).unwrap();
let transitions = [
Transition::new(ymdhms(2023, 3, 26, 2, 0, 0), std, dst),
Transition::new(ymdhms(2023, 10, 29, 3, 0, 0), dst, std),
];
compare_lookup(&transitions, 2023, 3, 26, 1, 0, 0, MappedLocalTime::Single(std));
compare_lookup(&transitions, 2023, 3, 26, 2, 0, 0, MappedLocalTime::Single(std));
compare_lookup(&transitions, 2023, 3, 26, 2, 30, 0, MappedLocalTime::None);
compare_lookup(&transitions, 2023, 3, 26, 3, 0, 0, MappedLocalTime::Single(dst));
compare_lookup(&transitions, 2023, 3, 26, 4, 0, 0, MappedLocalTime::Single(dst));
compare_lookup(&transitions, 2023, 10, 29, 1, 0, 0, MappedLocalTime::Single(dst));
compare_lookup(&transitions, 2023, 10, 29, 2, 0, 0, MappedLocalTime::Ambiguous(dst, std));
compare_lookup(&transitions, 2023, 10, 29, 2, 30, 0, MappedLocalTime::Ambiguous(dst, std));
compare_lookup(&transitions, 2023, 10, 29, 3, 0, 0, MappedLocalTime::Ambiguous(dst, std));
compare_lookup(&transitions, 2023, 10, 29, 4, 0, 0, MappedLocalTime::Single(std));
// std transition before dst transition
// dst offset > std offset
let std = FixedOffset::east_opt(-5 * 60 * 60).unwrap();
let dst = FixedOffset::east_opt(-4 * 60 * 60).unwrap();
let transitions = [
Transition::new(ymdhms(2023, 3, 24, 3, 0, 0), dst, std),
Transition::new(ymdhms(2023, 10, 27, 2, 0, 0), std, dst),
];
compare_lookup(&transitions, 2023, 3, 24, 1, 0, 0, MappedLocalTime::Single(dst));
compare_lookup(&transitions, 2023, 3, 24, 2, 0, 0, MappedLocalTime::Ambiguous(dst, std));
compare_lookup(&transitions, 2023, 3, 24, 2, 30, 0, MappedLocalTime::Ambiguous(dst, std));
compare_lookup(&transitions, 2023, 3, 24, 3, 0, 0, MappedLocalTime::Ambiguous(dst, std));
compare_lookup(&transitions, 2023, 3, 24, 4, 0, 0, MappedLocalTime::Single(std));
compare_lookup(&transitions, 2023, 10, 27, 1, 0, 0, MappedLocalTime::Single(std));
compare_lookup(&transitions, 2023, 10, 27, 2, 0, 0, MappedLocalTime::Single(std));
compare_lookup(&transitions, 2023, 10, 27, 2, 30, 0, MappedLocalTime::None);
compare_lookup(&transitions, 2023, 10, 27, 3, 0, 0, MappedLocalTime::Single(dst));
compare_lookup(&transitions, 2023, 10, 27, 4, 0, 0, MappedLocalTime::Single(dst));
// dst transition before std transition
// dst offset < std offset
let std = FixedOffset::east_opt(3 * 60 * 60).unwrap();
let dst = FixedOffset::east_opt((2 * 60 + 30) * 60).unwrap();
let transitions = [
Transition::new(ymdhms(2023, 3, 26, 2, 30, 0), std, dst),
Transition::new(ymdhms(2023, 10, 29, 2, 0, 0), dst, std),
];
compare_lookup(&transitions, 2023, 3, 26, 1, 0, 0, MappedLocalTime::Single(std));
compare_lookup(&transitions, 2023, 3, 26, 2, 0, 0, MappedLocalTime::Ambiguous(std, dst));
compare_lookup(&transitions, 2023, 3, 26, 2, 15, 0, MappedLocalTime::Ambiguous(std, dst));
compare_lookup(&transitions, 2023, 3, 26, 2, 30, 0, MappedLocalTime::Ambiguous(std, dst));
compare_lookup(&transitions, 2023, 3, 26, 3, 0, 0, MappedLocalTime::Single(dst));
compare_lookup(&transitions, 2023, 10, 29, 1, 0, 0, MappedLocalTime::Single(dst));
compare_lookup(&transitions, 2023, 10, 29, 2, 0, 0, MappedLocalTime::Single(dst));
compare_lookup(&transitions, 2023, 10, 29, 2, 15, 0, MappedLocalTime::None);
compare_lookup(&transitions, 2023, 10, 29, 2, 30, 0, MappedLocalTime::Single(std));
compare_lookup(&transitions, 2023, 10, 29, 3, 0, 0, MappedLocalTime::Single(std));
// std transition before dst transition
// dst offset < std offset
let std = FixedOffset::east_opt(-(4 * 60 + 30) * 60).unwrap();
let dst = FixedOffset::east_opt(-5 * 60 * 60).unwrap();
let transitions = [
Transition::new(ymdhms(2023, 3, 24, 2, 0, 0), dst, std),
Transition::new(ymdhms(2023, 10, 27, 2, 30, 0), std, dst),
];
compare_lookup(&transitions, 2023, 3, 24, 1, 0, 0, MappedLocalTime::Single(dst));
compare_lookup(&transitions, 2023, 3, 24, 2, 0, 0, MappedLocalTime::Single(dst));
compare_lookup(&transitions, 2023, 3, 24, 2, 15, 0, MappedLocalTime::None);
compare_lookup(&transitions, 2023, 3, 24, 2, 30, 0, MappedLocalTime::Single(std));
compare_lookup(&transitions, 2023, 3, 24, 3, 0, 0, MappedLocalTime::Single(std));
compare_lookup(&transitions, 2023, 10, 27, 1, 0, 0, MappedLocalTime::Single(std));
compare_lookup(&transitions, 2023, 10, 27, 2, 0, 0, MappedLocalTime::Ambiguous(std, dst));
compare_lookup(&transitions, 2023, 10, 27, 2, 15, 0, MappedLocalTime::Ambiguous(std, dst));
compare_lookup(&transitions, 2023, 10, 27, 2, 30, 0, MappedLocalTime::Ambiguous(std, dst));
compare_lookup(&transitions, 2023, 10, 27, 3, 0, 0, MappedLocalTime::Single(dst));
// offset stays the same
let std = FixedOffset::east_opt(3 * 60 * 60).unwrap();
let transitions = [
Transition::new(ymdhms(2023, 3, 26, 2, 0, 0), std, std),
Transition::new(ymdhms(2023, 10, 29, 3, 0, 0), std, std),
];
compare_lookup(&transitions, 2023, 3, 26, 2, 0, 0, MappedLocalTime::Single(std));
compare_lookup(&transitions, 2023, 10, 29, 3, 0, 0, MappedLocalTime::Single(std));
// single transition
let std = FixedOffset::east_opt(3 * 60 * 60).unwrap();
let dst = FixedOffset::east_opt(4 * 60 * 60).unwrap();
let transitions = [Transition::new(ymdhms(2023, 3, 26, 2, 0, 0), std, dst)];
compare_lookup(&transitions, 2023, 3, 26, 1, 0, 0, MappedLocalTime::Single(std));
compare_lookup(&transitions, 2023, 3, 26, 2, 0, 0, MappedLocalTime::Single(std));
compare_lookup(&transitions, 2023, 3, 26, 2, 30, 0, MappedLocalTime::None);
compare_lookup(&transitions, 2023, 3, 26, 3, 0, 0, MappedLocalTime::Single(dst));
compare_lookup(&transitions, 2023, 3, 26, 4, 0, 0, MappedLocalTime::Single(dst));
}
#[test]
#[cfg(windows)]
fn test_lookup_with_dst_transitions_limits() {
// Transition beyond UTC year end doesn't panic in year of `NaiveDate::MAX`
let std = FixedOffset::east_opt(3 * 60 * 60).unwrap();
let dst = FixedOffset::east_opt(4 * 60 * 60).unwrap();
let transitions = [
Transition::new(NaiveDateTime::MAX.with_month(7).unwrap(), std, dst),
Transition::new(NaiveDateTime::MAX, dst, std),
];
assert_eq!(
lookup_with_dst_transitions(&transitions, NaiveDateTime::MAX.with_month(3).unwrap()),
MappedLocalTime::Single(std)
);
assert_eq!(
lookup_with_dst_transitions(&transitions, NaiveDateTime::MAX.with_month(8).unwrap()),
MappedLocalTime::Single(dst)
);
// Doesn't panic with `NaiveDateTime::MAX` as argument (which would be out of range when
// converted to UTC).
assert_eq!(
lookup_with_dst_transitions(&transitions, NaiveDateTime::MAX),
MappedLocalTime::Ambiguous(dst, std)
);
// Transition before UTC year end doesn't panic in year of `NaiveDate::MIN`
let std = FixedOffset::west_opt(3 * 60 * 60).unwrap();
let dst = FixedOffset::west_opt(4 * 60 * 60).unwrap();
let transitions = [
Transition::new(NaiveDateTime::MIN, std, dst),
Transition::new(NaiveDateTime::MIN.with_month(6).unwrap(), dst, std),
];
assert_eq!(
lookup_with_dst_transitions(&transitions, NaiveDateTime::MIN.with_month(3).unwrap()),
MappedLocalTime::Single(dst)
);
assert_eq!(
lookup_with_dst_transitions(&transitions, NaiveDateTime::MIN.with_month(8).unwrap()),
MappedLocalTime::Single(std)
);
// Doesn't panic with `NaiveDateTime::MIN` as argument (which would be out of range when
// converted to UTC).
assert_eq!(
lookup_with_dst_transitions(&transitions, NaiveDateTime::MIN),
MappedLocalTime::Ambiguous(std, dst)
);
}
#[test]
#[cfg(feature = "rkyv-validation")]
fn test_rkyv_validation() {
let local = Local;
// Local is a ZST and serializes to 0 bytes
let bytes = rkyv::to_bytes::<_, 0>(&local).unwrap();
assert_eq!(bytes.len(), 0);
// but is deserialized to an archived variant without a
// wrapping object
assert_eq!(rkyv::from_bytes::<Local>(&bytes).unwrap(), super::ArchivedLocal);
}
}

View File

@@ -0,0 +1,267 @@
//! Rust parser of ZoneInfoDb(`tzdata`) on Android and OpenHarmony
//!
//! Ported from: https://android.googlesource.com/platform/prebuilts/fullsdk/sources/+/refs/heads/androidx-appcompat-release/android-34/com/android/i18n/timezone/ZoneInfoDb.java
use std::{
ffi::CStr,
fmt::Debug,
fs::File,
io::{Error, ErrorKind, Read, Result, Seek, SeekFrom},
};
/// Get timezone data from the `tzdata` file of HarmonyOS NEXT.
#[cfg(target_env = "ohos")]
pub(crate) fn for_zone(tz_string: &str) -> Result<Option<Vec<u8>>> {
let mut file = File::open("/system/etc/zoneinfo/tzdata")?;
find_tz_data::<OHOS_ENTRY_LEN>(&mut file, tz_string.as_bytes())
}
/// Get timezone data from the `tzdata` file of Android.
#[cfg(target_os = "android")]
pub(crate) fn for_zone(tz_string: &str) -> Result<Option<Vec<u8>>> {
let mut file = open_android_tz_data_file()?;
find_tz_data::<ANDROID_ENTRY_LEN>(&mut file, tz_string.as_bytes())
}
/// Open the `tzdata` file of Android from the environment variables.
#[cfg(target_os = "android")]
fn open_android_tz_data_file() -> Result<File> {
for (env_var, path) in
[("ANDROID_DATA", "/misc/zoneinfo"), ("ANDROID_ROOT", "/usr/share/zoneinfo")]
{
if let Ok(env_value) = std::env::var(env_var) {
if let Ok(file) = File::open(format!("{}{}/tzdata", env_value, path)) {
return Ok(file);
}
}
}
Err(Error::from(ErrorKind::NotFound))
}
/// Get timezone data from the `tzdata` file reader
#[cfg(any(test, target_env = "ohos", target_os = "android"))]
fn find_tz_data<const ENTRY_LEN: usize>(
mut reader: impl Read + Seek,
tz_name: &[u8],
) -> Result<Option<Vec<u8>>> {
let header = TzDataHeader::new(&mut reader)?;
let index = TzDataIndexes::new::<ENTRY_LEN>(&mut reader, &header)?;
Ok(if let Some(entry) = index.find_timezone(tz_name) {
Some(index.find_tzdata(reader, &header, entry)?)
} else {
None
})
}
/// Header of the `tzdata` file.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct TzDataHeader {
version: [u8; 5],
index_offset: u32,
data_offset: u32,
zonetab_offset: u32,
}
impl TzDataHeader {
/// Parse the header of the `tzdata` file.
fn new(mut data: impl Read) -> Result<Self> {
let version = {
let mut magic = [0; TZDATA_VERSION_LEN];
data.read_exact(&mut magic)?;
if !magic.starts_with(b"tzdata") || magic[TZDATA_VERSION_LEN - 1] != 0 {
return Err(Error::new(ErrorKind::Other, "invalid tzdata header magic"));
}
let mut version = [0; 5];
version.copy_from_slice(&magic[6..11]);
version
};
let mut offset = [0; 4];
data.read_exact(&mut offset)?;
let index_offset = u32::from_be_bytes(offset);
data.read_exact(&mut offset)?;
let data_offset = u32::from_be_bytes(offset);
data.read_exact(&mut offset)?;
let zonetab_offset = u32::from_be_bytes(offset);
Ok(Self { version, index_offset, data_offset, zonetab_offset })
}
}
/// Indexes of the `tzdata` file.
struct TzDataIndexes {
indexes: Vec<TzDataIndex>,
}
impl TzDataIndexes {
/// Create a new `TzDataIndexes` from the `tzdata` file reader.
fn new<const ENTRY_LEN: usize>(mut reader: impl Read, header: &TzDataHeader) -> Result<Self> {
let mut buf = vec![0; header.data_offset.saturating_sub(header.index_offset) as usize];
reader.read_exact(&mut buf)?;
// replace chunks with array_chunks when it's stable
Ok(TzDataIndexes {
indexes: buf
.chunks(ENTRY_LEN)
.filter_map(|chunk| {
from_bytes_until_nul(&chunk[..TZ_NAME_LEN]).map(|name| {
let name = name.to_bytes().to_vec().into_boxed_slice();
let offset = u32::from_be_bytes(
chunk[TZ_NAME_LEN..TZ_NAME_LEN + 4].try_into().unwrap(),
);
let length = u32::from_be_bytes(
chunk[TZ_NAME_LEN + 4..TZ_NAME_LEN + 8].try_into().unwrap(),
);
TzDataIndex { name, offset, length }
})
})
.collect(),
})
}
/// Find a timezone by name.
fn find_timezone(&self, timezone: &[u8]) -> Option<&TzDataIndex> {
// timezones in tzdata are sorted by name.
self.indexes.binary_search_by_key(&timezone, |x| &x.name).map(|x| &self.indexes[x]).ok()
}
/// Retrieve a chunk of timezone data by the index.
fn find_tzdata(
&self,
mut reader: impl Read + Seek,
header: &TzDataHeader,
index: &TzDataIndex,
) -> Result<Vec<u8>> {
reader.seek(SeekFrom::Start(index.offset as u64 + header.data_offset as u64))?;
let mut buffer = vec![0; index.length as usize];
reader.read_exact(&mut buffer)?;
Ok(buffer)
}
}
/// Index entry of the `tzdata` file.
struct TzDataIndex {
name: Box<[u8]>,
offset: u32,
length: u32,
}
/// TODO: Change this `CStr::from_bytes_until_nul` once MSRV was bumped above 1.72.0
fn from_bytes_until_nul(bytes: &[u8]) -> Option<&CStr> {
let nul_pos = bytes.iter().position(|&b| b == 0)?;
// SAFETY:
// 1. nul_pos + 1 <= bytes.len()
// 2. We know there is a nul byte at nul_pos, so this slice (ending at the nul byte) is a well-formed C string.
Some(unsafe { CStr::from_bytes_with_nul_unchecked(&bytes[..=nul_pos]) })
}
/// Ohos tzdata index entry size: `name + offset + length`
#[cfg(any(test, target_env = "ohos"))]
const OHOS_ENTRY_LEN: usize = TZ_NAME_LEN + 2 * size_of::<u32>();
/// Android tzdata index entry size: `name + offset + length + raw_utc_offset(legacy)`:
/// [reference](https://android.googlesource.com/platform/prebuilts/fullsdk/sources/+/refs/heads/androidx-appcompat-release/android-34/com/android/i18n/timezone/ZoneInfoDb.java#271)
#[cfg(any(test, target_os = "android"))]
const ANDROID_ENTRY_LEN: usize = TZ_NAME_LEN + 3 * size_of::<u32>();
/// The database reserves 40 bytes for each id.
const TZ_NAME_LEN: usize = 40;
/// Size of the version string in the header of `tzdata` file.
/// e.g. `tzdata2024b\0`
const TZDATA_VERSION_LEN: usize = 12;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ohos_tzdata_header_and_index() {
let file = File::open("./tests/ohos/tzdata").unwrap();
let header = TzDataHeader::new(&file).unwrap();
assert_eq!(header.version, *b"2024a");
assert_eq!(header.index_offset, 24);
assert_eq!(header.data_offset, 21240);
assert_eq!(header.zonetab_offset, 272428);
let iter = TzDataIndexes::new::<OHOS_ENTRY_LEN>(&file, &header).unwrap();
assert_eq!(iter.indexes.len(), 442);
assert!(iter.find_timezone(b"Asia/Shanghai").is_some());
assert!(iter.find_timezone(b"Pacific/Noumea").is_some());
}
#[test]
fn test_ohos_tzdata_loading() {
let file = File::open("./tests/ohos/tzdata").unwrap();
let header = TzDataHeader::new(&file).unwrap();
let iter = TzDataIndexes::new::<OHOS_ENTRY_LEN>(&file, &header).unwrap();
let timezone = iter.find_timezone(b"Asia/Shanghai").unwrap();
let tzdata = iter.find_tzdata(&file, &header, timezone).unwrap();
assert_eq!(tzdata.len(), 393);
}
#[test]
fn test_invalid_tzdata_header() {
TzDataHeader::new(&b"tzdaaa2024aaaaaaaaaaaaaaa\0"[..]).unwrap_err();
}
#[test]
fn test_android_tzdata_header_and_index() {
let file = File::open("./tests/android/tzdata").unwrap();
let header = TzDataHeader::new(&file).unwrap();
assert_eq!(header.version, *b"2021a");
assert_eq!(header.index_offset, 24);
assert_eq!(header.data_offset, 30860);
assert_eq!(header.zonetab_offset, 491837);
let iter = TzDataIndexes::new::<ANDROID_ENTRY_LEN>(&file, &header).unwrap();
assert_eq!(iter.indexes.len(), 593);
assert!(iter.find_timezone(b"Asia/Shanghai").is_some());
assert!(iter.find_timezone(b"Pacific/Noumea").is_some());
}
#[test]
fn test_android_tzdata_loading() {
let file = File::open("./tests/android/tzdata").unwrap();
let header = TzDataHeader::new(&file).unwrap();
let iter = TzDataIndexes::new::<ANDROID_ENTRY_LEN>(&file, &header).unwrap();
let timezone = iter.find_timezone(b"Asia/Shanghai").unwrap();
let tzdata = iter.find_tzdata(&file, &header, timezone).unwrap();
assert_eq!(tzdata.len(), 573);
}
#[test]
fn test_ohos_tzdata_find() {
let file = File::open("./tests/ohos/tzdata").unwrap();
let tzdata = find_tz_data::<OHOS_ENTRY_LEN>(file, b"Asia/Shanghai").unwrap().unwrap();
assert_eq!(tzdata.len(), 393);
}
#[test]
fn test_ohos_tzdata_find_missing() {
let file = File::open("./tests/ohos/tzdata").unwrap();
assert!(find_tz_data::<OHOS_ENTRY_LEN>(file, b"Asia/Sjasdfai").unwrap().is_none());
}
#[test]
fn test_android_tzdata_find() {
let file = File::open("./tests/android/tzdata").unwrap();
let tzdata = find_tz_data::<ANDROID_ENTRY_LEN>(file, b"Asia/Shanghai").unwrap().unwrap();
assert_eq!(tzdata.len(), 573);
}
#[test]
fn test_android_tzdata_find_missing() {
let file = File::open("./tests/android/tzdata").unwrap();
assert!(find_tz_data::<ANDROID_ENTRY_LEN>(file, b"Asia/S000000i").unwrap().is_none());
}
#[cfg(target_env = "ohos")]
#[test]
fn test_ohos_machine_tz_data_loading() {
let tzdata = for_zone(b"Asia/Shanghai").unwrap().unwrap();
assert!(!tzdata.is_empty());
}
#[cfg(target_os = "android")]
#[test]
fn test_android_machine_tz_data_loading() {
let tzdata = for_zone(b"Asia/Shanghai").unwrap().unwrap();
assert!(!tzdata.is_empty());
}
}

View File

@@ -0,0 +1,116 @@
#![deny(missing_docs)]
#![allow(dead_code)]
#![warn(unreachable_pub)]
use std::num::ParseIntError;
use std::str::Utf8Error;
use std::time::SystemTimeError;
use std::{error, fmt, io};
mod timezone;
pub(crate) use timezone::TimeZone;
mod parser;
mod rule;
/// Unified error type for everything in the crate
#[derive(Debug)]
pub(crate) enum Error {
/// Date time error
DateTime(&'static str),
/// Local time type search error
FindLocalTimeType(&'static str),
/// Local time type error
LocalTimeType(&'static str),
/// Invalid slice for integer conversion
InvalidSlice(&'static str),
/// Invalid Tzif file
InvalidTzFile(&'static str),
/// Invalid TZ string
InvalidTzString(&'static str),
/// I/O error
Io(io::Error),
/// Out of range error
OutOfRange(&'static str),
/// Integer parsing error
ParseInt(ParseIntError),
/// Date time projection error
ProjectDateTime(&'static str),
/// System time error
SystemTime(SystemTimeError),
/// Time zone error
TimeZone(&'static str),
/// Transition rule error
TransitionRule(&'static str),
/// Unsupported Tzif file
UnsupportedTzFile(&'static str),
/// Unsupported TZ string
UnsupportedTzString(&'static str),
/// UTF-8 error
Utf8(Utf8Error),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use Error::*;
match self {
DateTime(error) => write!(f, "invalid date time: {error}"),
FindLocalTimeType(error) => error.fmt(f),
LocalTimeType(error) => write!(f, "invalid local time type: {error}"),
InvalidSlice(error) => error.fmt(f),
InvalidTzString(error) => write!(f, "invalid TZ string: {error}"),
InvalidTzFile(error) => error.fmt(f),
Io(error) => error.fmt(f),
OutOfRange(error) => error.fmt(f),
ParseInt(error) => error.fmt(f),
ProjectDateTime(error) => error.fmt(f),
SystemTime(error) => error.fmt(f),
TransitionRule(error) => write!(f, "invalid transition rule: {error}"),
TimeZone(error) => write!(f, "invalid time zone: {error}"),
UnsupportedTzFile(error) => error.fmt(f),
UnsupportedTzString(error) => write!(f, "unsupported TZ string: {error}"),
Utf8(error) => error.fmt(f),
}
}
}
impl error::Error for Error {}
impl From<io::Error> for Error {
fn from(error: io::Error) -> Self {
Error::Io(error)
}
}
impl From<ParseIntError> for Error {
fn from(error: ParseIntError) -> Self {
Error::ParseInt(error)
}
}
impl From<SystemTimeError> for Error {
fn from(error: SystemTimeError) -> Self {
Error::SystemTime(error)
}
}
impl From<Utf8Error> for Error {
fn from(error: Utf8Error) -> Self {
Error::Utf8(error)
}
}
/// Number of hours in one day
const HOURS_PER_DAY: i64 = 24;
/// Number of seconds in one hour
const SECONDS_PER_HOUR: i64 = 3600;
/// Number of seconds in one day
const SECONDS_PER_DAY: i64 = SECONDS_PER_HOUR * HOURS_PER_DAY;
/// Number of days in one week
const DAYS_PER_WEEK: i64 = 7;
/// Month days in a normal year
const DAY_IN_MONTHS_NORMAL_YEAR: [i64; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
/// Cumulated month days in a normal year
const CUMUL_DAY_IN_MONTHS_NORMAL_YEAR: [i64; 12] =
[0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];

View File

@@ -0,0 +1,348 @@
use std::io::{self, ErrorKind};
use std::iter;
use std::num::ParseIntError;
use std::str::{self, FromStr};
use super::Error;
use super::rule::TransitionRule;
use super::timezone::{LeapSecond, LocalTimeType, TimeZone, Transition};
pub(super) fn parse(bytes: &[u8]) -> Result<TimeZone, Error> {
let mut cursor = Cursor::new(bytes);
let state = State::new(&mut cursor, true)?;
let (state, footer) = match state.header.version {
Version::V1 => match cursor.is_empty() {
true => (state, None),
false => {
return Err(Error::InvalidTzFile("remaining data after end of TZif v1 data block"));
}
},
Version::V2 | Version::V3 => {
let state = State::new(&mut cursor, false)?;
(state, Some(cursor.remaining()))
}
};
let mut transitions = Vec::with_capacity(state.header.transition_count);
for (arr_time, &local_time_type_index) in
state.transition_times.chunks_exact(state.time_size).zip(state.transition_types)
{
let unix_leap_time =
state.parse_time(&arr_time[0..state.time_size], state.header.version)?;
let local_time_type_index = local_time_type_index as usize;
transitions.push(Transition::new(unix_leap_time, local_time_type_index));
}
let mut local_time_types = Vec::with_capacity(state.header.type_count);
for arr in state.local_time_types.chunks_exact(6) {
let ut_offset = read_be_i32(&arr[..4])?;
let is_dst = match arr[4] {
0 => false,
1 => true,
_ => return Err(Error::InvalidTzFile("invalid DST indicator")),
};
let char_index = arr[5] as usize;
if char_index >= state.header.char_count {
return Err(Error::InvalidTzFile("invalid time zone name char index"));
}
let position = match state.names[char_index..].iter().position(|&c| c == b'\0') {
Some(position) => position,
None => return Err(Error::InvalidTzFile("invalid time zone name char index")),
};
let name = &state.names[char_index..char_index + position];
let name = if !name.is_empty() { Some(name) } else { None };
local_time_types.push(LocalTimeType::new(ut_offset, is_dst, name)?);
}
let mut leap_seconds = Vec::with_capacity(state.header.leap_count);
for arr in state.leap_seconds.chunks_exact(state.time_size + 4) {
let unix_leap_time = state.parse_time(&arr[0..state.time_size], state.header.version)?;
let correction = read_be_i32(&arr[state.time_size..state.time_size + 4])?;
leap_seconds.push(LeapSecond::new(unix_leap_time, correction));
}
let std_walls_iter = state.std_walls.iter().copied().chain(iter::repeat(0));
let ut_locals_iter = state.ut_locals.iter().copied().chain(iter::repeat(0));
if std_walls_iter.zip(ut_locals_iter).take(state.header.type_count).any(|pair| pair == (0, 1)) {
return Err(Error::InvalidTzFile(
"invalid couple of standard/wall and UT/local indicators",
));
}
let extra_rule = match footer {
Some(footer) => {
let footer = str::from_utf8(footer)?;
if !(footer.starts_with('\n') && footer.ends_with('\n')) {
return Err(Error::InvalidTzFile("invalid footer"));
}
let tz_string = footer.trim_matches(|c: char| c.is_ascii_whitespace());
if tz_string.starts_with(':') || tz_string.contains('\0') {
return Err(Error::InvalidTzFile("invalid footer"));
}
match tz_string.is_empty() {
true => None,
false => Some(TransitionRule::from_tz_string(
tz_string.as_bytes(),
state.header.version == Version::V3,
)?),
}
}
None => None,
};
TimeZone::new(transitions, local_time_types, leap_seconds, extra_rule)
}
/// TZif data blocks
struct State<'a> {
header: Header,
/// Time size in bytes
time_size: usize,
/// Transition times data block
transition_times: &'a [u8],
/// Transition types data block
transition_types: &'a [u8],
/// Local time types data block
local_time_types: &'a [u8],
/// Time zone names data block
names: &'a [u8],
/// Leap seconds data block
leap_seconds: &'a [u8],
/// UT/local indicators data block
std_walls: &'a [u8],
/// Standard/wall indicators data block
ut_locals: &'a [u8],
}
impl<'a> State<'a> {
/// Read TZif data blocks
fn new(cursor: &mut Cursor<'a>, first: bool) -> Result<Self, Error> {
let header = Header::new(cursor)?;
let time_size = match first {
true => 4, // We always parse V1 first
false => 8,
};
Ok(Self {
time_size,
transition_times: cursor.read_exact(header.transition_count * time_size)?,
transition_types: cursor.read_exact(header.transition_count)?,
local_time_types: cursor.read_exact(header.type_count * 6)?,
names: cursor.read_exact(header.char_count)?,
leap_seconds: cursor.read_exact(header.leap_count * (time_size + 4))?,
std_walls: cursor.read_exact(header.std_wall_count)?,
ut_locals: cursor.read_exact(header.ut_local_count)?,
header,
})
}
/// Parse time values
fn parse_time(&self, arr: &[u8], version: Version) -> Result<i64, Error> {
match version {
Version::V1 => Ok(read_be_i32(&arr[..4])?.into()),
Version::V2 | Version::V3 => read_be_i64(arr),
}
}
}
/// TZif header
#[derive(Debug)]
struct Header {
/// TZif version
version: Version,
/// Number of UT/local indicators
ut_local_count: usize,
/// Number of standard/wall indicators
std_wall_count: usize,
/// Number of leap-second records
leap_count: usize,
/// Number of transition times
transition_count: usize,
/// Number of local time type records
type_count: usize,
/// Number of time zone names bytes
char_count: usize,
}
impl Header {
fn new(cursor: &mut Cursor) -> Result<Self, Error> {
let magic = cursor.read_exact(4)?;
if magic != *b"TZif" {
return Err(Error::InvalidTzFile("invalid magic number"));
}
let version = match cursor.read_exact(1)? {
[0x00] => Version::V1,
[0x32] => Version::V2,
[0x33] => Version::V3,
_ => return Err(Error::UnsupportedTzFile("unsupported TZif version")),
};
cursor.read_exact(15)?;
let ut_local_count = cursor.read_be_u32()?;
let std_wall_count = cursor.read_be_u32()?;
let leap_count = cursor.read_be_u32()?;
let transition_count = cursor.read_be_u32()?;
let type_count = cursor.read_be_u32()?;
let char_count = cursor.read_be_u32()?;
if !(type_count != 0
&& char_count != 0
&& (ut_local_count == 0 || ut_local_count == type_count)
&& (std_wall_count == 0 || std_wall_count == type_count))
{
return Err(Error::InvalidTzFile("invalid header"));
}
Ok(Self {
version,
ut_local_count: ut_local_count as usize,
std_wall_count: std_wall_count as usize,
leap_count: leap_count as usize,
transition_count: transition_count as usize,
type_count: type_count as usize,
char_count: char_count as usize,
})
}
}
/// A `Cursor` contains a slice of a buffer and a read count.
#[derive(Debug, Eq, PartialEq)]
pub(crate) struct Cursor<'a> {
/// Slice representing the remaining data to be read
remaining: &'a [u8],
/// Number of already read bytes
read_count: usize,
}
impl<'a> Cursor<'a> {
/// Construct a new `Cursor` from remaining data
pub(crate) const fn new(remaining: &'a [u8]) -> Self {
Self { remaining, read_count: 0 }
}
pub(crate) fn peek(&self) -> Option<&u8> {
self.remaining().first()
}
/// Returns remaining data
pub(crate) const fn remaining(&self) -> &'a [u8] {
self.remaining
}
/// Returns `true` if data is remaining
pub(crate) const fn is_empty(&self) -> bool {
self.remaining.is_empty()
}
pub(crate) fn read_be_u32(&mut self) -> Result<u32, Error> {
let mut buf = [0; 4];
buf.copy_from_slice(self.read_exact(4)?);
Ok(u32::from_be_bytes(buf))
}
#[cfg(target_env = "ohos")]
pub(crate) fn seek_after(&mut self, offset: usize) -> Result<usize, io::Error> {
if offset < self.read_count {
return Err(io::Error::from(ErrorKind::UnexpectedEof));
}
match self.remaining.get((offset - self.read_count)..) {
Some(remaining) => {
self.remaining = remaining;
self.read_count = offset;
Ok(offset)
}
_ => Err(io::Error::from(ErrorKind::UnexpectedEof)),
}
}
/// Read exactly `count` bytes, reducing remaining data and incrementing read count
pub(crate) fn read_exact(&mut self, count: usize) -> Result<&'a [u8], io::Error> {
match (self.remaining.get(..count), self.remaining.get(count..)) {
(Some(result), Some(remaining)) => {
self.remaining = remaining;
self.read_count += count;
Ok(result)
}
_ => Err(io::Error::from(ErrorKind::UnexpectedEof)),
}
}
/// Read bytes and compare them to the provided tag
pub(crate) fn read_tag(&mut self, tag: &[u8]) -> Result<(), io::Error> {
if self.read_exact(tag.len())? == tag {
Ok(())
} else {
Err(io::Error::from(ErrorKind::InvalidData))
}
}
/// Read bytes if the remaining data is prefixed by the provided tag
pub(crate) fn read_optional_tag(&mut self, tag: &[u8]) -> Result<bool, io::Error> {
if self.remaining.starts_with(tag) {
self.read_exact(tag.len())?;
Ok(true)
} else {
Ok(false)
}
}
/// Read bytes as long as the provided predicate is true
pub(crate) fn read_while<F: Fn(&u8) -> bool>(&mut self, f: F) -> Result<&'a [u8], io::Error> {
match self.remaining.iter().position(|x| !f(x)) {
None => self.read_exact(self.remaining.len()),
Some(position) => self.read_exact(position),
}
}
// Parse an integer out of the ASCII digits
pub(crate) fn read_int<T: FromStr<Err = ParseIntError>>(&mut self) -> Result<T, Error> {
let bytes = self.read_while(u8::is_ascii_digit)?;
Ok(str::from_utf8(bytes)?.parse()?)
}
/// Read bytes until the provided predicate is true
pub(crate) fn read_until<F: Fn(&u8) -> bool>(&mut self, f: F) -> Result<&'a [u8], io::Error> {
match self.remaining.iter().position(f) {
None => self.read_exact(self.remaining.len()),
Some(position) => self.read_exact(position),
}
}
}
pub(crate) fn read_be_i32(bytes: &[u8]) -> Result<i32, Error> {
if bytes.len() != 4 {
return Err(Error::InvalidSlice("too short for i32"));
}
let mut buf = [0; 4];
buf.copy_from_slice(bytes);
Ok(i32::from_be_bytes(buf))
}
pub(crate) fn read_be_i64(bytes: &[u8]) -> Result<i64, Error> {
if bytes.len() != 8 {
return Err(Error::InvalidSlice("too short for i64"));
}
let mut buf = [0; 8];
buf.copy_from_slice(bytes);
Ok(i64::from_be_bytes(buf))
}
/// TZif version
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
enum Version {
/// Version 1
V1,
/// Version 2
V2,
/// Version 3
V3,
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,948 @@
//! Types related to a time zone.
use std::fs::{self, File};
use std::io::{self, Read};
use std::path::{Path, PathBuf};
use std::{cmp::Ordering, fmt, str};
use super::rule::{AlternateTime, TransitionRule};
use super::{DAYS_PER_WEEK, Error, SECONDS_PER_DAY, parser};
use crate::NaiveDateTime;
/// Time zone
#[derive(Debug, Clone, Eq, PartialEq)]
pub(crate) struct TimeZone {
/// List of transitions
transitions: Vec<Transition>,
/// List of local time types (cannot be empty)
local_time_types: Vec<LocalTimeType>,
/// List of leap seconds
leap_seconds: Vec<LeapSecond>,
/// Extra transition rule applicable after the last transition
extra_rule: Option<TransitionRule>,
}
impl TimeZone {
/// Returns local time zone.
///
/// This method in not supported on non-UNIX platforms, and returns the UTC time zone instead.
pub(crate) fn local(env_tz: Option<&str>) -> Result<Self, Error> {
match env_tz {
Some(tz) => Self::from_posix_tz(tz),
None => Self::from_posix_tz("localtime"),
}
}
/// Construct a time zone from a POSIX TZ string, as described in [the POSIX documentation of the `TZ` environment variable](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html).
fn from_posix_tz(tz_string: &str) -> Result<Self, Error> {
// It is commonly agreed (but not standard) that setting an empty `TZ=` uses UTC.
if tz_string.is_empty() {
return Ok(Self::utc());
}
if tz_string == "localtime" {
return Self::from_tz_data(&fs::read("/etc/localtime")?);
}
// attributes are not allowed on if blocks in Rust 1.38
#[cfg(any(target_os = "android", target_env = "ohos"))]
{
if let Ok(Some(bytes)) = crate::offset::local::tz_data::for_zone(tz_string) {
return Self::from_tz_data(&bytes);
}
}
let mut chars = tz_string.chars();
if chars.next() == Some(':') {
return Self::from_file(&mut find_tz_file(chars.as_str())?);
}
if let Ok(mut file) = find_tz_file(tz_string) {
return Self::from_file(&mut file);
}
// TZ string extensions are not allowed
let tz_string = tz_string.trim_matches(|c: char| c.is_ascii_whitespace());
let rule = TransitionRule::from_tz_string(tz_string.as_bytes(), false)?;
Self::new(
vec![],
match rule {
TransitionRule::Fixed(local_time_type) => vec![local_time_type],
TransitionRule::Alternate(AlternateTime { std, dst, .. }) => vec![std, dst],
},
vec![],
Some(rule),
)
}
/// Construct a time zone
pub(super) fn new(
transitions: Vec<Transition>,
local_time_types: Vec<LocalTimeType>,
leap_seconds: Vec<LeapSecond>,
extra_rule: Option<TransitionRule>,
) -> Result<Self, Error> {
let new = Self { transitions, local_time_types, leap_seconds, extra_rule };
new.as_ref().validate()?;
Ok(new)
}
/// Construct a time zone from the contents of a time zone file
fn from_file(file: &mut File) -> Result<Self, Error> {
let mut bytes = Vec::new();
file.read_to_end(&mut bytes)?;
Self::from_tz_data(&bytes)
}
/// Construct a time zone from the contents of a time zone file
///
/// Parse TZif data as described in [RFC 8536](https://datatracker.ietf.org/doc/html/rfc8536).
pub(crate) fn from_tz_data(bytes: &[u8]) -> Result<Self, Error> {
parser::parse(bytes)
}
/// Construct a time zone with the specified UTC offset in seconds
fn fixed(ut_offset: i32) -> Result<Self, Error> {
Ok(Self {
transitions: Vec::new(),
local_time_types: vec![LocalTimeType::with_offset(ut_offset)?],
leap_seconds: Vec::new(),
extra_rule: None,
})
}
/// Construct the time zone associated to UTC
pub(crate) fn utc() -> Self {
Self {
transitions: Vec::new(),
local_time_types: vec![LocalTimeType::UTC],
leap_seconds: Vec::new(),
extra_rule: None,
}
}
/// Find the local time type associated to the time zone at the specified Unix time in seconds
pub(crate) fn find_local_time_type(&self, unix_time: i64) -> Result<&LocalTimeType, Error> {
self.as_ref().find_local_time_type(unix_time)
}
pub(crate) fn find_local_time_type_from_local(
&self,
local_time: NaiveDateTime,
) -> Result<crate::MappedLocalTime<LocalTimeType>, Error> {
self.as_ref().find_local_time_type_from_local(local_time)
}
/// Returns a reference to the time zone
fn as_ref(&'_ self) -> TimeZoneRef<'_> {
TimeZoneRef {
transitions: &self.transitions,
local_time_types: &self.local_time_types,
leap_seconds: &self.leap_seconds,
extra_rule: &self.extra_rule,
}
}
}
/// Reference to a time zone
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub(crate) struct TimeZoneRef<'a> {
/// List of transitions
transitions: &'a [Transition],
/// List of local time types (cannot be empty)
local_time_types: &'a [LocalTimeType],
/// List of leap seconds
leap_seconds: &'a [LeapSecond],
/// Extra transition rule applicable after the last transition
extra_rule: &'a Option<TransitionRule>,
}
impl<'a> TimeZoneRef<'a> {
/// Find the local time type associated to the time zone at the specified Unix time in seconds
pub(crate) fn find_local_time_type(&self, unix_time: i64) -> Result<&'a LocalTimeType, Error> {
let extra_rule = match self.transitions.last() {
None => match self.extra_rule {
Some(extra_rule) => extra_rule,
None => return Ok(&self.local_time_types[0]),
},
Some(last_transition) => {
let unix_leap_time = match self.unix_time_to_unix_leap_time(unix_time) {
Ok(unix_leap_time) => unix_leap_time,
Err(Error::OutOfRange(error)) => return Err(Error::FindLocalTimeType(error)),
Err(err) => return Err(err),
};
if unix_leap_time >= last_transition.unix_leap_time {
match self.extra_rule {
Some(extra_rule) => extra_rule,
None => {
// RFC 8536 3.2:
// "Local time for timestamps on or after the last transition is
// specified by the TZ string in the footer (Section 3.3) if present
// and nonempty; otherwise, it is unspecified."
//
// Older versions of macOS (1.12 and before?) have TZif file with a
// missing TZ string, and use the offset given by the last transition.
return Ok(
&self.local_time_types[last_transition.local_time_type_index]
);
}
}
} else {
let index = match self
.transitions
.binary_search_by_key(&unix_leap_time, Transition::unix_leap_time)
{
Ok(x) => x + 1,
Err(x) => x,
};
let local_time_type_index = if index > 0 {
self.transitions[index - 1].local_time_type_index
} else {
0
};
return Ok(&self.local_time_types[local_time_type_index]);
}
}
};
match extra_rule.find_local_time_type(unix_time) {
Ok(local_time_type) => Ok(local_time_type),
Err(Error::OutOfRange(error)) => Err(Error::FindLocalTimeType(error)),
err => err,
}
}
pub(crate) fn find_local_time_type_from_local(
&self,
local_time: NaiveDateTime,
) -> Result<crate::MappedLocalTime<LocalTimeType>, Error> {
// #TODO: this is wrong as we need 'local_time_to_local_leap_time ?
// but ... does the local time even include leap seconds ??
// let unix_leap_time = match self.unix_time_to_unix_leap_time(local_time) {
// Ok(unix_leap_time) => unix_leap_time,
// Err(Error::OutOfRange(error)) => return Err(Error::FindLocalTimeType(error)),
// Err(err) => return Err(err),
// };
let local_leap_time = local_time.and_utc().timestamp();
// if we have at least one transition,
// we must check _all_ of them, in case of any Overlapping (MappedLocalTime::Ambiguous) or Skipping (MappedLocalTime::None) transitions
let offset_after_last = if !self.transitions.is_empty() {
let mut prev = self.local_time_types[0];
for transition in self.transitions {
let after_ltt = self.local_time_types[transition.local_time_type_index];
// the end and start here refers to where the time starts prior to the transition
// and where it ends up after. not the temporal relationship.
let transition_end = transition.unix_leap_time + i64::from(after_ltt.ut_offset);
let transition_start = transition.unix_leap_time + i64::from(prev.ut_offset);
match transition_start.cmp(&transition_end) {
Ordering::Greater => {
// backwards transition, eg from DST to regular
// this means a given local time could have one of two possible offsets
if local_leap_time < transition_end {
return Ok(crate::MappedLocalTime::Single(prev));
} else if local_leap_time >= transition_end
&& local_leap_time <= transition_start
{
if prev.ut_offset < after_ltt.ut_offset {
return Ok(crate::MappedLocalTime::Ambiguous(prev, after_ltt));
} else {
return Ok(crate::MappedLocalTime::Ambiguous(after_ltt, prev));
}
}
}
Ordering::Equal => {
// should this ever happen? presumably we have to handle it anyway.
if local_leap_time < transition_start {
return Ok(crate::MappedLocalTime::Single(prev));
} else if local_leap_time == transition_end {
if prev.ut_offset < after_ltt.ut_offset {
return Ok(crate::MappedLocalTime::Ambiguous(prev, after_ltt));
} else {
return Ok(crate::MappedLocalTime::Ambiguous(after_ltt, prev));
}
}
}
Ordering::Less => {
// forwards transition, eg from regular to DST
// this means that times that are skipped are invalid local times
if local_leap_time <= transition_start {
return Ok(crate::MappedLocalTime::Single(prev));
} else if local_leap_time < transition_end {
return Ok(crate::MappedLocalTime::None);
} else if local_leap_time == transition_end {
return Ok(crate::MappedLocalTime::Single(after_ltt));
}
}
}
// try the next transition, we are fully after this one
prev = after_ltt;
}
prev
} else {
self.local_time_types[0]
};
if let Some(extra_rule) = self.extra_rule {
match extra_rule.find_local_time_type_from_local(local_time) {
Ok(local_time_type) => Ok(local_time_type),
Err(Error::OutOfRange(error)) => Err(Error::FindLocalTimeType(error)),
err => err,
}
} else {
Ok(crate::MappedLocalTime::Single(offset_after_last))
}
}
/// Check time zone inputs
fn validate(&self) -> Result<(), Error> {
// Check local time types
let local_time_types_size = self.local_time_types.len();
if local_time_types_size == 0 {
return Err(Error::TimeZone("list of local time types must not be empty"));
}
// Check transitions
let mut i_transition = 0;
while i_transition < self.transitions.len() {
if self.transitions[i_transition].local_time_type_index >= local_time_types_size {
return Err(Error::TimeZone("invalid local time type index"));
}
if i_transition + 1 < self.transitions.len()
&& self.transitions[i_transition].unix_leap_time
>= self.transitions[i_transition + 1].unix_leap_time
{
return Err(Error::TimeZone("invalid transition"));
}
i_transition += 1;
}
// Check leap seconds
if !(self.leap_seconds.is_empty()
|| self.leap_seconds[0].unix_leap_time >= 0
&& self.leap_seconds[0].correction.saturating_abs() == 1)
{
return Err(Error::TimeZone("invalid leap second"));
}
let min_interval = SECONDS_PER_28_DAYS - 1;
let mut i_leap_second = 0;
while i_leap_second < self.leap_seconds.len() {
if i_leap_second + 1 < self.leap_seconds.len() {
let x0 = &self.leap_seconds[i_leap_second];
let x1 = &self.leap_seconds[i_leap_second + 1];
let diff_unix_leap_time = x1.unix_leap_time.saturating_sub(x0.unix_leap_time);
let abs_diff_correction =
x1.correction.saturating_sub(x0.correction).saturating_abs();
if !(diff_unix_leap_time >= min_interval && abs_diff_correction == 1) {
return Err(Error::TimeZone("invalid leap second"));
}
}
i_leap_second += 1;
}
// Check extra rule
let (extra_rule, last_transition) = match (&self.extra_rule, self.transitions.last()) {
(Some(rule), Some(trans)) => (rule, trans),
_ => return Ok(()),
};
let last_local_time_type = &self.local_time_types[last_transition.local_time_type_index];
let unix_time = match self.unix_leap_time_to_unix_time(last_transition.unix_leap_time) {
Ok(unix_time) => unix_time,
Err(Error::OutOfRange(error)) => return Err(Error::TimeZone(error)),
Err(err) => return Err(err),
};
let rule_local_time_type = match extra_rule.find_local_time_type(unix_time) {
Ok(rule_local_time_type) => rule_local_time_type,
Err(Error::OutOfRange(error)) => return Err(Error::TimeZone(error)),
Err(err) => return Err(err),
};
let check = last_local_time_type.ut_offset == rule_local_time_type.ut_offset
&& last_local_time_type.is_dst == rule_local_time_type.is_dst
&& match (&last_local_time_type.name, &rule_local_time_type.name) {
(Some(x), Some(y)) => x.equal(y),
(None, None) => true,
_ => false,
};
if !check {
return Err(Error::TimeZone(
"extra transition rule is inconsistent with the last transition",
));
}
Ok(())
}
/// Convert Unix time to Unix leap time, from the list of leap seconds in a time zone
const fn unix_time_to_unix_leap_time(&self, unix_time: i64) -> Result<i64, Error> {
let mut unix_leap_time = unix_time;
let mut i = 0;
while i < self.leap_seconds.len() {
let leap_second = &self.leap_seconds[i];
if unix_leap_time < leap_second.unix_leap_time {
break;
}
unix_leap_time = match unix_time.checked_add(leap_second.correction as i64) {
Some(unix_leap_time) => unix_leap_time,
None => return Err(Error::OutOfRange("out of range operation")),
};
i += 1;
}
Ok(unix_leap_time)
}
/// Convert Unix leap time to Unix time, from the list of leap seconds in a time zone
fn unix_leap_time_to_unix_time(&self, unix_leap_time: i64) -> Result<i64, Error> {
if unix_leap_time == i64::MIN {
return Err(Error::OutOfRange("out of range operation"));
}
let index = match self
.leap_seconds
.binary_search_by_key(&(unix_leap_time - 1), LeapSecond::unix_leap_time)
{
Ok(x) => x + 1,
Err(x) => x,
};
let correction = if index > 0 { self.leap_seconds[index - 1].correction } else { 0 };
match unix_leap_time.checked_sub(correction as i64) {
Some(unix_time) => Ok(unix_time),
None => Err(Error::OutOfRange("out of range operation")),
}
}
/// The UTC time zone
const UTC: TimeZoneRef<'static> = TimeZoneRef {
transitions: &[],
local_time_types: &[LocalTimeType::UTC],
leap_seconds: &[],
extra_rule: &None,
};
}
/// Transition of a TZif file
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub(super) struct Transition {
/// Unix leap time
unix_leap_time: i64,
/// Index specifying the local time type of the transition
local_time_type_index: usize,
}
impl Transition {
/// Construct a TZif file transition
pub(super) const fn new(unix_leap_time: i64, local_time_type_index: usize) -> Self {
Self { unix_leap_time, local_time_type_index }
}
/// Returns Unix leap time
const fn unix_leap_time(&self) -> i64 {
self.unix_leap_time
}
}
/// Leap second of a TZif file
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub(super) struct LeapSecond {
/// Unix leap time
unix_leap_time: i64,
/// Leap second correction
correction: i32,
}
impl LeapSecond {
/// Construct a TZif file leap second
pub(super) const fn new(unix_leap_time: i64, correction: i32) -> Self {
Self { unix_leap_time, correction }
}
/// Returns Unix leap time
const fn unix_leap_time(&self) -> i64 {
self.unix_leap_time
}
}
/// ASCII-encoded fixed-capacity string, used for storing time zone names
#[derive(Copy, Clone, Eq, PartialEq)]
struct TimeZoneName {
/// Length-prefixed string buffer
bytes: [u8; 8],
}
impl TimeZoneName {
/// Construct a time zone name
///
/// man tzfile(5):
/// Time zone designations should consist of at least three (3) and no more than six (6) ASCII
/// characters from the set of alphanumerics, “-”, and “+”. This is for compatibility with
/// POSIX requirements for time zone abbreviations.
fn new(input: &[u8]) -> Result<Self, Error> {
let len = input.len();
if !(3..=7).contains(&len) {
return Err(Error::LocalTimeType(
"time zone name must have between 3 and 7 characters",
));
}
let mut bytes = [0; 8];
bytes[0] = input.len() as u8;
let mut i = 0;
while i < len {
let b = input[i];
match b {
b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'+' | b'-' => {}
_ => return Err(Error::LocalTimeType("invalid characters in time zone name")),
}
bytes[i + 1] = b;
i += 1;
}
Ok(Self { bytes })
}
/// Returns time zone name as a byte slice
fn as_bytes(&self) -> &[u8] {
match self.bytes[0] {
3 => &self.bytes[1..4],
4 => &self.bytes[1..5],
5 => &self.bytes[1..6],
6 => &self.bytes[1..7],
7 => &self.bytes[1..8],
_ => unreachable!(),
}
}
/// Check if two time zone names are equal
fn equal(&self, other: &Self) -> bool {
self.bytes == other.bytes
}
}
impl AsRef<str> for TimeZoneName {
fn as_ref(&self) -> &str {
// SAFETY: ASCII is valid UTF-8
unsafe { str::from_utf8_unchecked(self.as_bytes()) }
}
}
impl fmt::Debug for TimeZoneName {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.as_ref().fmt(f)
}
}
/// Local time type associated to a time zone
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub(crate) struct LocalTimeType {
/// Offset from UTC in seconds
pub(super) ut_offset: i32,
/// Daylight Saving Time indicator
is_dst: bool,
/// Time zone name
name: Option<TimeZoneName>,
}
impl LocalTimeType {
/// Construct a local time type
pub(super) fn new(ut_offset: i32, is_dst: bool, name: Option<&[u8]>) -> Result<Self, Error> {
if ut_offset == i32::MIN {
return Err(Error::LocalTimeType("invalid UTC offset"));
}
let name = match name {
Some(name) => TimeZoneName::new(name)?,
None => return Ok(Self { ut_offset, is_dst, name: None }),
};
Ok(Self { ut_offset, is_dst, name: Some(name) })
}
/// Construct a local time type with the specified UTC offset in seconds
pub(super) const fn with_offset(ut_offset: i32) -> Result<Self, Error> {
if ut_offset == i32::MIN {
return Err(Error::LocalTimeType("invalid UTC offset"));
}
Ok(Self { ut_offset, is_dst: false, name: None })
}
/// Returns offset from UTC in seconds
pub(crate) const fn offset(&self) -> i32 {
self.ut_offset
}
/// Returns daylight saving time indicator
pub(super) const fn is_dst(&self) -> bool {
self.is_dst
}
pub(super) const UTC: LocalTimeType = Self { ut_offset: 0, is_dst: false, name: None };
}
/// Open the TZif file corresponding to a TZ string
fn find_tz_file(path: impl AsRef<Path>) -> Result<File, Error> {
// Don't check system timezone directories on non-UNIX platforms
#[cfg(not(unix))]
return Ok(File::open(path)?);
#[cfg(unix)]
{
let path = path.as_ref();
if path.is_absolute() {
return Ok(File::open(path)?);
}
for folder in &ZONE_INFO_DIRECTORIES {
if let Ok(file) = File::open(PathBuf::from(folder).join(path)) {
return Ok(file);
}
}
Err(Error::Io(io::ErrorKind::NotFound.into()))
}
}
// Possible system timezone directories
#[cfg(unix)]
const ZONE_INFO_DIRECTORIES: [&str; 4] =
["/usr/share/zoneinfo", "/share/zoneinfo", "/etc/zoneinfo", "/usr/share/lib/zoneinfo"];
/// Number of seconds in one week
pub(crate) const SECONDS_PER_WEEK: i64 = SECONDS_PER_DAY * DAYS_PER_WEEK;
/// Number of seconds in 28 days
const SECONDS_PER_28_DAYS: i64 = SECONDS_PER_DAY * 28;
#[cfg(test)]
mod tests {
use super::super::Error;
use super::{LeapSecond, LocalTimeType, TimeZone, TimeZoneName, Transition, TransitionRule};
#[test]
fn test_no_dst() -> Result<(), Error> {
let tz_string = b"HST10";
let transition_rule = TransitionRule::from_tz_string(tz_string, false)?;
assert_eq!(transition_rule, LocalTimeType::new(-36000, false, Some(b"HST"))?.into());
Ok(())
}
#[test]
fn test_error() -> Result<(), Error> {
assert!(matches!(
TransitionRule::from_tz_string(b"IST-1GMT0", false),
Err(Error::UnsupportedTzString(_))
));
assert!(matches!(
TransitionRule::from_tz_string(b"EET-2EEST", false),
Err(Error::UnsupportedTzString(_))
));
Ok(())
}
#[test]
fn test_v1_file_with_leap_seconds() -> Result<(), Error> {
let bytes = b"TZif\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\x01\0\0\0\x1b\0\0\0\0\0\0\0\x01\0\0\0\x04\0\0\0\0\0\0UTC\0\x04\xb2\x58\0\0\0\0\x01\x05\xa4\xec\x01\0\0\0\x02\x07\x86\x1f\x82\0\0\0\x03\x09\x67\x53\x03\0\0\0\x04\x0b\x48\x86\x84\0\0\0\x05\x0d\x2b\x0b\x85\0\0\0\x06\x0f\x0c\x3f\x06\0\0\0\x07\x10\xed\x72\x87\0\0\0\x08\x12\xce\xa6\x08\0\0\0\x09\x15\x9f\xca\x89\0\0\0\x0a\x17\x80\xfe\x0a\0\0\0\x0b\x19\x62\x31\x8b\0\0\0\x0c\x1d\x25\xea\x0c\0\0\0\x0d\x21\xda\xe5\x0d\0\0\0\x0e\x25\x9e\x9d\x8e\0\0\0\x0f\x27\x7f\xd1\x0f\0\0\0\x10\x2a\x50\xf5\x90\0\0\0\x11\x2c\x32\x29\x11\0\0\0\x12\x2e\x13\x5c\x92\0\0\0\x13\x30\xe7\x24\x13\0\0\0\x14\x33\xb8\x48\x94\0\0\0\x15\x36\x8c\x10\x15\0\0\0\x16\x43\xb7\x1b\x96\0\0\0\x17\x49\x5c\x07\x97\0\0\0\x18\x4f\xef\x93\x18\0\0\0\x19\x55\x93\x2d\x99\0\0\0\x1a\x58\x68\x46\x9a\0\0\0\x1b\0\0";
let time_zone = TimeZone::from_tz_data(bytes)?;
let time_zone_result = TimeZone::new(
Vec::new(),
vec![LocalTimeType::new(0, false, Some(b"UTC"))?],
vec![
LeapSecond::new(78796800, 1),
LeapSecond::new(94694401, 2),
LeapSecond::new(126230402, 3),
LeapSecond::new(157766403, 4),
LeapSecond::new(189302404, 5),
LeapSecond::new(220924805, 6),
LeapSecond::new(252460806, 7),
LeapSecond::new(283996807, 8),
LeapSecond::new(315532808, 9),
LeapSecond::new(362793609, 10),
LeapSecond::new(394329610, 11),
LeapSecond::new(425865611, 12),
LeapSecond::new(489024012, 13),
LeapSecond::new(567993613, 14),
LeapSecond::new(631152014, 15),
LeapSecond::new(662688015, 16),
LeapSecond::new(709948816, 17),
LeapSecond::new(741484817, 18),
LeapSecond::new(773020818, 19),
LeapSecond::new(820454419, 20),
LeapSecond::new(867715220, 21),
LeapSecond::new(915148821, 22),
LeapSecond::new(1136073622, 23),
LeapSecond::new(1230768023, 24),
LeapSecond::new(1341100824, 25),
LeapSecond::new(1435708825, 26),
LeapSecond::new(1483228826, 27),
],
None,
)?;
assert_eq!(time_zone, time_zone_result);
Ok(())
}
#[test]
fn test_v2_file() -> Result<(), Error> {
let bytes = b"TZif2\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x06\0\0\0\x06\0\0\0\0\0\0\0\x07\0\0\0\x06\0\0\0\x14\x80\0\0\0\xbb\x05\x43\x48\xbb\x21\x71\x58\xcb\x89\x3d\xc8\xd2\x23\xf4\x70\xd2\x61\x49\x38\xd5\x8d\x73\x48\x01\x02\x01\x03\x04\x01\x05\xff\xff\x6c\x02\0\0\xff\xff\x6c\x58\0\x04\xff\xff\x7a\x68\x01\x08\xff\xff\x7a\x68\x01\x0c\xff\xff\x7a\x68\x01\x10\xff\xff\x73\x60\0\x04LMT\0HST\0HDT\0HWT\0HPT\0\0\0\0\0\x01\0\0\0\0\0\x01\0TZif2\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x06\0\0\0\x06\0\0\0\0\0\0\0\x07\0\0\0\x06\0\0\0\x14\xff\xff\xff\xff\x74\xe0\x70\xbe\xff\xff\xff\xff\xbb\x05\x43\x48\xff\xff\xff\xff\xbb\x21\x71\x58\xff\xff\xff\xff\xcb\x89\x3d\xc8\xff\xff\xff\xff\xd2\x23\xf4\x70\xff\xff\xff\xff\xd2\x61\x49\x38\xff\xff\xff\xff\xd5\x8d\x73\x48\x01\x02\x01\x03\x04\x01\x05\xff\xff\x6c\x02\0\0\xff\xff\x6c\x58\0\x04\xff\xff\x7a\x68\x01\x08\xff\xff\x7a\x68\x01\x0c\xff\xff\x7a\x68\x01\x10\xff\xff\x73\x60\0\x04LMT\0HST\0HDT\0HWT\0HPT\0\0\0\0\0\x01\0\0\0\0\0\x01\0\x0aHST10\x0a";
let time_zone = TimeZone::from_tz_data(bytes)?;
let time_zone_result = TimeZone::new(
vec![
Transition::new(-2334101314, 1),
Transition::new(-1157283000, 2),
Transition::new(-1155436200, 1),
Transition::new(-880198200, 3),
Transition::new(-769395600, 4),
Transition::new(-765376200, 1),
Transition::new(-712150200, 5),
],
vec![
LocalTimeType::new(-37886, false, Some(b"LMT"))?,
LocalTimeType::new(-37800, false, Some(b"HST"))?,
LocalTimeType::new(-34200, true, Some(b"HDT"))?,
LocalTimeType::new(-34200, true, Some(b"HWT"))?,
LocalTimeType::new(-34200, true, Some(b"HPT"))?,
LocalTimeType::new(-36000, false, Some(b"HST"))?,
],
Vec::new(),
Some(TransitionRule::from(LocalTimeType::new(-36000, false, Some(b"HST"))?)),
)?;
assert_eq!(time_zone, time_zone_result);
assert_eq!(
*time_zone.find_local_time_type(-1156939200)?,
LocalTimeType::new(-34200, true, Some(b"HDT"))?
);
assert_eq!(
*time_zone.find_local_time_type(1546300800)?,
LocalTimeType::new(-36000, false, Some(b"HST"))?
);
Ok(())
}
#[test]
fn test_no_tz_string() -> Result<(), Error> {
// Guayaquil from macOS 10.11
let bytes = b"TZif\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x02\0\0\0\x02\0\0\0\0\0\0\0\x01\0\0\0\x02\0\0\0\x08\xb6\xa4B\x18\x01\xff\xff\xb6h\0\0\xff\xff\xb9\xb0\0\x04QMT\0ECT\0\0\0\0\0";
let time_zone = TimeZone::from_tz_data(bytes)?;
dbg!(&time_zone);
let time_zone_result = TimeZone::new(
vec![Transition::new(-1230749160, 1)],
vec![
LocalTimeType::new(-18840, false, Some(b"QMT"))?,
LocalTimeType::new(-18000, false, Some(b"ECT"))?,
],
Vec::new(),
None,
)?;
assert_eq!(time_zone, time_zone_result);
assert_eq!(
*time_zone.find_local_time_type(-1500000000)?,
LocalTimeType::new(-18840, false, Some(b"QMT"))?
);
assert_eq!(
*time_zone.find_local_time_type(0)?,
LocalTimeType::new(-18000, false, Some(b"ECT"))?
);
Ok(())
}
#[test]
fn test_tz_ascii_str() -> Result<(), Error> {
assert!(matches!(TimeZoneName::new(b""), Err(Error::LocalTimeType(_))));
assert!(matches!(TimeZoneName::new(b"A"), Err(Error::LocalTimeType(_))));
assert!(matches!(TimeZoneName::new(b"AB"), Err(Error::LocalTimeType(_))));
assert_eq!(TimeZoneName::new(b"CET")?.as_bytes(), b"CET");
assert_eq!(TimeZoneName::new(b"CHADT")?.as_bytes(), b"CHADT");
assert_eq!(TimeZoneName::new(b"abcdefg")?.as_bytes(), b"abcdefg");
assert_eq!(TimeZoneName::new(b"UTC+02")?.as_bytes(), b"UTC+02");
assert_eq!(TimeZoneName::new(b"-1230")?.as_bytes(), b"-1230");
assert!(matches!(TimeZoneName::new("0330".as_bytes()), Err(Error::LocalTimeType(_)))); // MINUS SIGN (U+2212)
assert!(matches!(TimeZoneName::new(b"\x00123"), Err(Error::LocalTimeType(_))));
assert!(matches!(TimeZoneName::new(b"12345678"), Err(Error::LocalTimeType(_))));
assert!(matches!(TimeZoneName::new(b"GMT\0\0\0"), Err(Error::LocalTimeType(_))));
Ok(())
}
#[test]
fn test_time_zone() -> Result<(), Error> {
let utc = LocalTimeType::UTC;
let cet = LocalTimeType::with_offset(3600)?;
let utc_local_time_types = vec![utc];
let fixed_extra_rule = TransitionRule::from(cet);
let time_zone_1 = TimeZone::new(vec![], utc_local_time_types.clone(), vec![], None)?;
let time_zone_2 =
TimeZone::new(vec![], utc_local_time_types.clone(), vec![], Some(fixed_extra_rule))?;
let time_zone_3 =
TimeZone::new(vec![Transition::new(0, 0)], utc_local_time_types.clone(), vec![], None)?;
let time_zone_4 = TimeZone::new(
vec![Transition::new(i32::MIN.into(), 0), Transition::new(0, 1)],
vec![utc, cet],
Vec::new(),
Some(fixed_extra_rule),
)?;
assert_eq!(*time_zone_1.find_local_time_type(0)?, utc);
assert_eq!(*time_zone_2.find_local_time_type(0)?, cet);
assert_eq!(*time_zone_3.find_local_time_type(-1)?, utc);
assert_eq!(*time_zone_3.find_local_time_type(0)?, utc);
assert_eq!(*time_zone_4.find_local_time_type(-1)?, utc);
assert_eq!(*time_zone_4.find_local_time_type(0)?, cet);
let time_zone_err = TimeZone::new(
vec![Transition::new(0, 0)],
utc_local_time_types,
vec![],
Some(fixed_extra_rule),
);
assert!(time_zone_err.is_err());
Ok(())
}
#[test]
fn test_time_zone_from_posix_tz() -> Result<(), Error> {
#[cfg(unix)]
{
// if the TZ var is set, this essentially _overrides_ the
// time set by the localtime symlink
// so just ensure that ::local() acts as expected
// in this case
if let Ok(tz) = std::env::var("TZ") {
let time_zone_local = TimeZone::local(Some(tz.as_str()))?;
let time_zone_local_1 = TimeZone::from_posix_tz(&tz)?;
assert_eq!(time_zone_local, time_zone_local_1);
}
// `TimeZone::from_posix_tz("UTC")` will return `Error` if the environment does not have
// a time zone database, like for example some docker containers.
// In that case skip the test.
if let Ok(time_zone_utc) = TimeZone::from_posix_tz("UTC") {
assert_eq!(time_zone_utc.find_local_time_type(0)?.offset(), 0);
}
}
assert!(TimeZone::from_posix_tz("EST5EDT,0/0,J365/25").is_err());
assert_eq!(TimeZone::from_posix_tz("").unwrap().find_local_time_type(0)?.offset(), 0);
Ok(())
}
#[test]
fn test_leap_seconds() -> Result<(), Error> {
let time_zone = TimeZone::new(
Vec::new(),
vec![LocalTimeType::new(0, false, Some(b"UTC"))?],
vec![
LeapSecond::new(78796800, 1),
LeapSecond::new(94694401, 2),
LeapSecond::new(126230402, 3),
LeapSecond::new(157766403, 4),
LeapSecond::new(189302404, 5),
LeapSecond::new(220924805, 6),
LeapSecond::new(252460806, 7),
LeapSecond::new(283996807, 8),
LeapSecond::new(315532808, 9),
LeapSecond::new(362793609, 10),
LeapSecond::new(394329610, 11),
LeapSecond::new(425865611, 12),
LeapSecond::new(489024012, 13),
LeapSecond::new(567993613, 14),
LeapSecond::new(631152014, 15),
LeapSecond::new(662688015, 16),
LeapSecond::new(709948816, 17),
LeapSecond::new(741484817, 18),
LeapSecond::new(773020818, 19),
LeapSecond::new(820454419, 20),
LeapSecond::new(867715220, 21),
LeapSecond::new(915148821, 22),
LeapSecond::new(1136073622, 23),
LeapSecond::new(1230768023, 24),
LeapSecond::new(1341100824, 25),
LeapSecond::new(1435708825, 26),
LeapSecond::new(1483228826, 27),
],
None,
)?;
let time_zone_ref = time_zone.as_ref();
assert!(matches!(time_zone_ref.unix_leap_time_to_unix_time(1136073621), Ok(1136073599)));
assert!(matches!(time_zone_ref.unix_leap_time_to_unix_time(1136073622), Ok(1136073600)));
assert!(matches!(time_zone_ref.unix_leap_time_to_unix_time(1136073623), Ok(1136073600)));
assert!(matches!(time_zone_ref.unix_leap_time_to_unix_time(1136073624), Ok(1136073601)));
assert!(matches!(time_zone_ref.unix_time_to_unix_leap_time(1136073599), Ok(1136073621)));
assert!(matches!(time_zone_ref.unix_time_to_unix_leap_time(1136073600), Ok(1136073623)));
assert!(matches!(time_zone_ref.unix_time_to_unix_leap_time(1136073601), Ok(1136073624)));
Ok(())
}
#[test]
fn test_leap_seconds_overflow() -> Result<(), Error> {
let time_zone_err = TimeZone::new(
vec![Transition::new(i64::MIN, 0)],
vec![LocalTimeType::UTC],
vec![LeapSecond::new(0, 1)],
Some(TransitionRule::from(LocalTimeType::UTC)),
);
assert!(time_zone_err.is_err());
let time_zone = TimeZone::new(
vec![Transition::new(i64::MAX, 0)],
vec![LocalTimeType::UTC],
vec![LeapSecond::new(0, 1)],
None,
)?;
assert!(matches!(
time_zone.find_local_time_type(i64::MAX),
Err(Error::FindLocalTimeType(_))
));
Ok(())
}
}

View File

@@ -0,0 +1,171 @@
// Copyright 2012-2014 The Rust Project Developers. See the COPYRIGHT
// file at the top-level directory of this distribution and at
// http://rust-lang.org/COPYRIGHT.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
use std::{cell::RefCell, collections::hash_map, env, fs, hash::Hasher, time::SystemTime};
use super::tz_info::TimeZone;
use super::{FixedOffset, NaiveDateTime};
use crate::MappedLocalTime;
pub(super) fn offset_from_utc_datetime(utc: &NaiveDateTime) -> MappedLocalTime<FixedOffset> {
offset(utc, false)
}
pub(super) fn offset_from_local_datetime(local: &NaiveDateTime) -> MappedLocalTime<FixedOffset> {
offset(local, true)
}
fn offset(d: &NaiveDateTime, local: bool) -> MappedLocalTime<FixedOffset> {
TZ_INFO.with(|maybe_cache| {
maybe_cache.borrow_mut().get_or_insert_with(Cache::default).offset(*d, local)
})
}
// we have to store the `Cache` in an option as it can't
// be initialized in a static context.
thread_local! {
static TZ_INFO: RefCell<Option<Cache>> = Default::default();
}
enum Source {
LocalTime { mtime: SystemTime },
Environment { hash: u64 },
}
impl Source {
fn new(env_tz: Option<&str>) -> Source {
match env_tz {
Some(tz) => {
let mut hasher = hash_map::DefaultHasher::new();
hasher.write(tz.as_bytes());
let hash = hasher.finish();
Source::Environment { hash }
}
None => match fs::symlink_metadata("/etc/localtime") {
Ok(data) => Source::LocalTime {
// we have to pick a sensible default when the mtime fails
// by picking SystemTime::now() we raise the probability of
// the cache being invalidated if/when the mtime starts working
mtime: data.modified().unwrap_or_else(|_| SystemTime::now()),
},
Err(_) => {
// as above, now() should be a better default than some constant
// TODO: see if we can improve caching in the case where the fallback is a valid timezone
Source::LocalTime { mtime: SystemTime::now() }
}
},
}
}
}
struct Cache {
zone: TimeZone,
source: Source,
last_checked: SystemTime,
}
#[cfg(target_os = "aix")]
const TZDB_LOCATION: &str = "/usr/share/lib/zoneinfo";
#[cfg(not(any(target_os = "android", target_os = "aix", target_env = "ohos")))]
const TZDB_LOCATION: &str = "/usr/share/zoneinfo";
fn fallback_timezone() -> Option<TimeZone> {
let tz_name = iana_time_zone::get_timezone().ok()?;
#[cfg(not(any(target_os = "android", target_env = "ohos")))]
let bytes = fs::read(format!("{TZDB_LOCATION}/{tz_name}")).ok()?;
#[cfg(any(target_os = "android", target_env = "ohos"))]
let bytes = crate::offset::local::tz_data::for_zone(&tz_name).ok()??;
TimeZone::from_tz_data(&bytes).ok()
}
impl Default for Cache {
fn default() -> Cache {
// default to UTC if no local timezone can be found
let env_tz = env::var("TZ").ok();
let env_ref = env_tz.as_deref();
Cache {
last_checked: SystemTime::now(),
source: Source::new(env_ref),
zone: current_zone(env_ref),
}
}
}
fn current_zone(var: Option<&str>) -> TimeZone {
TimeZone::local(var).ok().or_else(fallback_timezone).unwrap_or_else(TimeZone::utc)
}
impl Cache {
fn offset(&mut self, d: NaiveDateTime, local: bool) -> MappedLocalTime<FixedOffset> {
let now = SystemTime::now();
match now.duration_since(self.last_checked) {
// If the cache has been around for less than a second then we reuse it
// unconditionally. This is a reasonable tradeoff because the timezone
// generally won't be changing _that_ often, but if the time zone does
// change, it will reflect sufficiently quickly from an application
// user's perspective.
Ok(d) if d.as_secs() < 1 => (),
Ok(_) | Err(_) => {
let env_tz = env::var("TZ").ok();
let env_ref = env_tz.as_deref();
let new_source = Source::new(env_ref);
let out_of_date = match (&self.source, &new_source) {
// change from env to file or file to env, must recreate the zone
(Source::Environment { .. }, Source::LocalTime { .. })
| (Source::LocalTime { .. }, Source::Environment { .. }) => true,
// stay as file, but mtime has changed
(Source::LocalTime { mtime: old_mtime }, Source::LocalTime { mtime })
if old_mtime != mtime =>
{
true
}
// stay as env, but hash of variable has changed
(Source::Environment { hash: old_hash }, Source::Environment { hash })
if old_hash != hash =>
{
true
}
// cache can be reused
_ => false,
};
if out_of_date {
self.zone = current_zone(env_ref);
}
self.last_checked = now;
self.source = new_source;
}
}
if !local {
let offset = self
.zone
.find_local_time_type(d.and_utc().timestamp())
.expect("unable to select local time type")
.offset();
return match FixedOffset::east_opt(offset) {
Some(offset) => MappedLocalTime::Single(offset),
None => MappedLocalTime::None,
};
}
// we pass through the year as the year of a local point in time must either be valid in that locale, or
// the entire time was skipped in which case we will return MappedLocalTime::None anyway.
self.zone
.find_local_time_type_from_local(d)
.expect("unable to select local time type")
.and_then(|o| FixedOffset::east_opt(o.offset()))
}
}

View File

@@ -0,0 +1,59 @@
#![allow(non_snake_case, non_upper_case_globals, non_camel_case_types, dead_code, clippy::all)]
windows_link::link!("kernel32.dll" "system" fn GetTimeZoneInformationForYear(wyear : u16, pdtzi : *const DYNAMIC_TIME_ZONE_INFORMATION, ptzi : *mut TIME_ZONE_INFORMATION) -> BOOL);
windows_link::link!("kernel32.dll" "system" fn SystemTimeToFileTime(lpsystemtime : *const SYSTEMTIME, lpfiletime : *mut FILETIME) -> BOOL);
windows_link::link!("kernel32.dll" "system" fn SystemTimeToTzSpecificLocalTime(lptimezoneinformation : *const TIME_ZONE_INFORMATION, lpuniversaltime : *const SYSTEMTIME, lplocaltime : *mut SYSTEMTIME) -> BOOL);
windows_link::link!("kernel32.dll" "system" fn TzSpecificLocalTimeToSystemTime(lptimezoneinformation : *const TIME_ZONE_INFORMATION, lplocaltime : *const SYSTEMTIME, lpuniversaltime : *mut SYSTEMTIME) -> BOOL);
pub type BOOL = i32;
#[repr(C)]
#[derive(Clone, Copy)]
pub struct DYNAMIC_TIME_ZONE_INFORMATION {
pub Bias: i32,
pub StandardName: [u16; 32],
pub StandardDate: SYSTEMTIME,
pub StandardBias: i32,
pub DaylightName: [u16; 32],
pub DaylightDate: SYSTEMTIME,
pub DaylightBias: i32,
pub TimeZoneKeyName: [u16; 128],
pub DynamicDaylightTimeDisabled: bool,
}
impl Default for DYNAMIC_TIME_ZONE_INFORMATION {
fn default() -> Self {
unsafe { core::mem::zeroed() }
}
}
#[repr(C)]
#[derive(Clone, Copy, Default)]
pub struct FILETIME {
pub dwLowDateTime: u32,
pub dwHighDateTime: u32,
}
#[repr(C)]
#[derive(Clone, Copy, Default)]
pub struct SYSTEMTIME {
pub wYear: u16,
pub wMonth: u16,
pub wDayOfWeek: u16,
pub wDay: u16,
pub wHour: u16,
pub wMinute: u16,
pub wSecond: u16,
pub wMilliseconds: u16,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct TIME_ZONE_INFORMATION {
pub Bias: i32,
pub StandardName: [u16; 32],
pub StandardDate: SYSTEMTIME,
pub StandardBias: i32,
pub DaylightName: [u16; 32],
pub DaylightDate: SYSTEMTIME,
pub DaylightBias: i32,
}
impl Default for TIME_ZONE_INFORMATION {
fn default() -> Self {
unsafe { core::mem::zeroed() }
}
}

View File

@@ -0,0 +1,7 @@
--out src/offset/local/win_bindings.rs
--flat --sys --no-comment
--filter
GetTimeZoneInformationForYear
SystemTimeToFileTime
SystemTimeToTzSpecificLocalTime
TzSpecificLocalTimeToSystemTime

View File

@@ -0,0 +1,293 @@
// Copyright 2012-2014 The Rust Project Developers. See the COPYRIGHT
// file at the top-level directory of this distribution and at
// http://rust-lang.org/COPYRIGHT.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
use std::cmp::Ordering;
use std::mem::MaybeUninit;
use std::ptr;
use super::win_bindings::{GetTimeZoneInformationForYear, SYSTEMTIME, TIME_ZONE_INFORMATION};
use crate::offset::local::{Transition, lookup_with_dst_transitions};
use crate::{Datelike, FixedOffset, MappedLocalTime, NaiveDate, NaiveDateTime, NaiveTime, Weekday};
// We don't use `SystemTimeToTzSpecificLocalTime` because it doesn't support the same range of dates
// as Chrono. Also it really isn't that difficult to work out the correct offset from the provided
// DST rules.
//
// This method uses `overflowing_sub_offset` because it is no problem if the transition time in UTC
// falls a couple of hours inside the buffer space around the `NaiveDateTime` range (although it is
// very theoretical to have a transition at midnight around `NaiveDate::(MIN|MAX)`.
pub(super) fn offset_from_utc_datetime(utc: &NaiveDateTime) -> MappedLocalTime<FixedOffset> {
// Using a `TzInfo` based on the year of an UTC datetime is technically wrong, we should be
// using the rules for the year of the corresponding local time. But this matches what
// `SystemTimeToTzSpecificLocalTime` is documented to do.
let tz_info = match TzInfo::for_year(utc.year()) {
Some(tz_info) => tz_info,
None => return MappedLocalTime::None,
};
let offset = match (tz_info.std_transition, tz_info.dst_transition) {
(Some(std_transition), Some(dst_transition)) => {
let std_transition_utc = std_transition.overflowing_sub_offset(tz_info.dst_offset);
let dst_transition_utc = dst_transition.overflowing_sub_offset(tz_info.std_offset);
if dst_transition_utc < std_transition_utc {
match utc >= &dst_transition_utc && utc < &std_transition_utc {
true => tz_info.dst_offset,
false => tz_info.std_offset,
}
} else {
match utc >= &std_transition_utc && utc < &dst_transition_utc {
true => tz_info.std_offset,
false => tz_info.dst_offset,
}
}
}
(Some(std_transition), None) => {
let std_transition_utc = std_transition.overflowing_sub_offset(tz_info.dst_offset);
match utc < &std_transition_utc {
true => tz_info.dst_offset,
false => tz_info.std_offset,
}
}
(None, Some(dst_transition)) => {
let dst_transition_utc = dst_transition.overflowing_sub_offset(tz_info.std_offset);
match utc < &dst_transition_utc {
true => tz_info.std_offset,
false => tz_info.dst_offset,
}
}
(None, None) => tz_info.std_offset,
};
MappedLocalTime::Single(offset)
}
// We don't use `TzSpecificLocalTimeToSystemTime` because it doesn't let us choose how to handle
// ambiguous cases (during a DST transition). Instead we get the timezone information for the
// current year and compute it ourselves, like we do on Unix.
pub(super) fn offset_from_local_datetime(local: &NaiveDateTime) -> MappedLocalTime<FixedOffset> {
let tz_info = match TzInfo::for_year(local.year()) {
Some(tz_info) => tz_info,
None => return MappedLocalTime::None,
};
// Create a sorted slice of transitions and use `lookup_with_dst_transitions`.
match (tz_info.std_transition, tz_info.dst_transition) {
(Some(std_transition), Some(dst_transition)) => {
let std_transition =
Transition::new(std_transition, tz_info.dst_offset, tz_info.std_offset);
let dst_transition =
Transition::new(dst_transition, tz_info.std_offset, tz_info.dst_offset);
let transitions = match std_transition.cmp(&dst_transition) {
Ordering::Less => [std_transition, dst_transition],
Ordering::Greater => [dst_transition, std_transition],
Ordering::Equal => {
// This doesn't make sense. Let's just return the standard offset.
return MappedLocalTime::Single(tz_info.std_offset);
}
};
lookup_with_dst_transitions(&transitions, *local)
}
(Some(std_transition), None) => {
let transitions =
[Transition::new(std_transition, tz_info.dst_offset, tz_info.std_offset)];
lookup_with_dst_transitions(&transitions, *local)
}
(None, Some(dst_transition)) => {
let transitions =
[Transition::new(dst_transition, tz_info.std_offset, tz_info.dst_offset)];
lookup_with_dst_transitions(&transitions, *local)
}
(None, None) => MappedLocalTime::Single(tz_info.std_offset),
}
}
// The basis for Windows timezone and DST support has been in place since Windows 2000. It does not
// allow for complex rules like the IANA timezone database:
// - A timezone has the same base offset the whole year.
// - There seem to be either zero or two DST transitions (but we support having just one).
// - As of Vista(?) only years from 2004 until a few years into the future are supported.
// - All other years get the base settings, which seem to be that of the current year.
//
// These details don't matter much, we just work with the offsets and transition dates Windows
// returns through `GetTimeZoneInformationForYear` for a particular year.
struct TzInfo {
// Offset from UTC during standard time.
std_offset: FixedOffset,
// Offset from UTC during daylight saving time.
dst_offset: FixedOffset,
// Transition from standard time to daylight saving time, given in local standard time.
std_transition: Option<NaiveDateTime>,
// Transition from daylight saving time to standard time, given in local daylight saving time.
dst_transition: Option<NaiveDateTime>,
}
impl TzInfo {
fn for_year(year: i32) -> Option<TzInfo> {
// The API limits years to 1601..=30827.
// Working with timezones and daylight saving time this far into the past or future makes
// little sense. But whatever is extrapolated for 1601 or 30827 is what can be extrapolated
// for years beyond.
let ref_year = year.clamp(1601, 30827) as u16;
let tz_info = unsafe {
let mut tz_info = MaybeUninit::<TIME_ZONE_INFORMATION>::uninit();
if GetTimeZoneInformationForYear(ref_year, ptr::null_mut(), tz_info.as_mut_ptr()) == 0 {
return None;
}
tz_info.assume_init()
};
let std_offset = (tz_info.Bias)
.checked_add(tz_info.StandardBias)
.and_then(|o| o.checked_mul(60))
.and_then(FixedOffset::west_opt)?;
let dst_offset = (tz_info.Bias)
.checked_add(tz_info.DaylightBias)
.and_then(|o| o.checked_mul(60))
.and_then(FixedOffset::west_opt)?;
Some(TzInfo {
std_offset,
dst_offset,
std_transition: naive_date_time_from_system_time(tz_info.StandardDate, year).ok()?,
dst_transition: naive_date_time_from_system_time(tz_info.DaylightDate, year).ok()?,
})
}
}
/// Resolve a `SYSTEMTIME` object to an `Option<NaiveDateTime>`.
///
/// A `SYSTEMTIME` within a `TIME_ZONE_INFORMATION` struct can be zero to indicate there is no
/// transition.
/// If it has year, month and day values it is a concrete date.
/// If the year is missing the `SYSTEMTIME` is a rule, which this method resolves for the provided
/// year. A rule has a month, weekday, and nth weekday of the month as components.
///
/// Returns `Err` if any of the values is invalid, which should never happen.
fn naive_date_time_from_system_time(
st: SYSTEMTIME,
year: i32,
) -> Result<Option<NaiveDateTime>, ()> {
if st.wYear == 0 && st.wMonth == 0 {
return Ok(None);
}
let time = NaiveTime::from_hms_milli_opt(
st.wHour as u32,
st.wMinute as u32,
st.wSecond as u32,
st.wMilliseconds as u32,
)
.ok_or(())?;
if st.wYear != 0 {
// We have a concrete date.
let date =
NaiveDate::from_ymd_opt(st.wYear as i32, st.wMonth as u32, st.wDay as u32).ok_or(())?;
return Ok(Some(date.and_time(time)));
}
// Resolve a rule with month, weekday, and nth weekday of the month to a date in the current
// year.
let weekday = match st.wDayOfWeek {
0 => Weekday::Sun,
1 => Weekday::Mon,
2 => Weekday::Tue,
3 => Weekday::Wed,
4 => Weekday::Thu,
5 => Weekday::Fri,
6 => Weekday::Sat,
_ => return Err(()),
};
let nth_day = match st.wDay {
1..=5 => st.wDay as u8,
_ => return Err(()),
};
let date = NaiveDate::from_weekday_of_month_opt(year, st.wMonth as u32, weekday, nth_day)
.or_else(|| NaiveDate::from_weekday_of_month_opt(year, st.wMonth as u32, weekday, 4))
.ok_or(())?; // `st.wMonth` must be invalid
Ok(Some(date.and_time(time)))
}
#[cfg(test)]
mod tests {
use crate::offset::local::win_bindings::{
FILETIME, SYSTEMTIME, SystemTimeToFileTime, TzSpecificLocalTimeToSystemTime,
};
use crate::{DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, TimeDelta};
use crate::{Datelike, TimeZone, Timelike};
use std::mem::MaybeUninit;
use std::ptr;
#[test]
fn verify_against_tz_specific_local_time_to_system_time() {
// The implementation in Windows itself is the source of truth on how to work with the OS
// timezone information. This test compares for every hour over a period of 125 years our
// implementation to `TzSpecificLocalTimeToSystemTime`.
//
// This uses parts of a previous Windows `Local` implementation in chrono.
fn from_local_time(dt: &NaiveDateTime) -> DateTime<Local> {
let st = system_time_from_naive_date_time(dt);
let utc_time = local_to_utc_time(&st);
let utc_secs = system_time_as_unix_seconds(&utc_time);
let local_secs = system_time_as_unix_seconds(&st);
let offset = (local_secs - utc_secs) as i32;
let offset = FixedOffset::east_opt(offset).unwrap();
DateTime::from_naive_utc_and_offset(*dt - offset, offset)
}
fn system_time_from_naive_date_time(dt: &NaiveDateTime) -> SYSTEMTIME {
SYSTEMTIME {
// Valid values: 1601-30827
wYear: dt.year() as u16,
// Valid values:1-12
wMonth: dt.month() as u16,
// Valid values: 0-6, starting Sunday.
// NOTE: enum returns 1-7, starting Monday, so we are
// off here, but this is not currently used in local.
wDayOfWeek: dt.weekday() as u16,
// Valid values: 1-31
wDay: dt.day() as u16,
// Valid values: 0-23
wHour: dt.hour() as u16,
// Valid values: 0-59
wMinute: dt.minute() as u16,
// Valid values: 0-59
wSecond: dt.second() as u16,
// Valid values: 0-999
wMilliseconds: 0,
}
}
fn local_to_utc_time(local: &SYSTEMTIME) -> SYSTEMTIME {
let mut sys_time = MaybeUninit::<SYSTEMTIME>::uninit();
unsafe { TzSpecificLocalTimeToSystemTime(ptr::null(), local, sys_time.as_mut_ptr()) };
// SAFETY: TzSpecificLocalTimeToSystemTime must have succeeded at this point, so we can
// assume the value is initialized.
unsafe { sys_time.assume_init() }
}
const HECTONANOSECS_IN_SEC: i64 = 10_000_000;
const HECTONANOSEC_TO_UNIX_EPOCH: i64 = 11_644_473_600 * HECTONANOSECS_IN_SEC;
fn system_time_as_unix_seconds(st: &SYSTEMTIME) -> i64 {
let mut init = MaybeUninit::<FILETIME>::uninit();
unsafe {
SystemTimeToFileTime(st, init.as_mut_ptr());
}
// SystemTimeToFileTime must have succeeded at this point, so we can assume the value is
// initialized.
let filetime = unsafe { init.assume_init() };
let bit_shift =
((filetime.dwHighDateTime as u64) << 32) | (filetime.dwLowDateTime as u64);
(bit_shift as i64 - HECTONANOSEC_TO_UNIX_EPOCH) / HECTONANOSECS_IN_SEC
}
let mut date = NaiveDate::from_ymd_opt(1975, 1, 1).unwrap().and_hms_opt(0, 30, 0).unwrap();
while date.year() < 2078 {
// Windows doesn't handle non-existing dates, it just treats it as valid.
if let Some(our_result) = Local.from_local_datetime(&date).earliest() {
assert_eq!(from_local_time(&date), our_result);
}
date += TimeDelta::try_hours(1).unwrap();
}
}
}

View File

@@ -0,0 +1,715 @@
// This is a part of Chrono.
// See README.md and LICENSE.txt for details.
//! The time zone, which calculates offsets from the local time to UTC.
//!
//! There are four operations provided by the `TimeZone` trait:
//!
//! 1. Converting the local `NaiveDateTime` to `DateTime<Tz>`
//! 2. Converting the UTC `NaiveDateTime` to `DateTime<Tz>`
//! 3. Converting `DateTime<Tz>` to the local `NaiveDateTime`
//! 4. Constructing `DateTime<Tz>` objects from various offsets
//!
//! 1 is used for constructors. 2 is used for the `with_timezone` method of date and time types.
//! 3 is used for other methods, e.g. `year()` or `format()`, and provided by an associated type
//! which implements `Offset` (which then passed to `TimeZone` for actual implementations).
//! Technically speaking `TimeZone` has a total knowledge about given timescale,
//! but `Offset` is used as a cache to avoid the repeated conversion
//! and provides implementations for 1 and 3.
//! An `TimeZone` instance can be reconstructed from the corresponding `Offset` instance.
use core::fmt;
use crate::Weekday;
use crate::format::{ParseResult, Parsed, StrftimeItems, parse};
use crate::naive::{NaiveDate, NaiveDateTime, NaiveTime};
#[allow(deprecated)]
use crate::{Date, DateTime};
pub(crate) mod fixed;
pub use self::fixed::FixedOffset;
#[cfg(feature = "clock")]
pub(crate) mod local;
#[cfg(feature = "clock")]
pub use self::local::Local;
pub(crate) mod utc;
pub use self::utc::Utc;
/// The result of mapping a local time to a concrete instant in a given time zone.
///
/// The calculation to go from a local time (wall clock time) to an instant in UTC can end up in
/// three cases:
/// * A single, simple result.
/// * An ambiguous result when the clock is turned backwards during a transition due to for example
/// DST.
/// * No result when the clock is turned forwards during a transition due to for example DST.
///
/// <div class="warning">
///
/// In wasm, when using [`Local`], only the [`LocalResult::Single`] variant is returned.
/// Specifically:
///
/// * When the clock is turned backwards, where `Ambiguous(earliest, latest)` would be expected,
/// `Single(earliest)` is returned instead.
/// * When the clock is turned forwards, where `None` would be expected, `Single(t)` is returned,
/// with `t` being the requested local time represented as though there is no transition on that
/// day (i.e. still "summer time")
///
/// This is caused because of limitations in the JavaScript
/// [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date)
/// API, which always parses a local time as a single, valid time - even for an
/// input which describes a nonexistent or ambiguous time.
///
/// See further discussion and workarounds in <https://github.com/chronotope/chrono/issues/1701>.
///
/// </div>
///
/// When the clock is turned backwards it creates a _fold_ in local time, during which the local
/// time is _ambiguous_. When the clock is turned forwards it creates a _gap_ in local time, during
/// which the local time is _missing_, or does not exist.
///
/// Chrono does not return a default choice or invalid data during time zone transitions, but has
/// the `MappedLocalTime` type to help deal with the result correctly.
///
/// The type of `T` is usually a [`DateTime`] but may also be only an offset.
pub type MappedLocalTime<T> = LocalResult<T>;
#[derive(Clone, PartialEq, Debug, Copy, Eq, Hash)]
/// Old name of [`MappedLocalTime`]. See that type for more documentation.
pub enum LocalResult<T> {
/// The local time maps to a single unique result.
Single(T),
/// The local time is _ambiguous_ because there is a _fold_ in the local time.
///
/// This variant contains the two possible results, in the order `(earliest, latest)`.
Ambiguous(T, T),
/// The local time does not exist because there is a _gap_ in the local time.
///
/// This variant may also be returned if there was an error while resolving the local time,
/// caused by for example missing time zone data files, an error in an OS API, or overflow.
None,
}
impl<T> MappedLocalTime<T> {
/// Returns `Some` if the time zone mapping has a single result.
///
/// # Errors
///
/// Returns `None` if local time falls in a _fold_ or _gap_ in the local time, or if there was
/// an error.
#[must_use]
pub fn single(self) -> Option<T> {
match self {
MappedLocalTime::Single(t) => Some(t),
_ => None,
}
}
/// Returns the earliest possible result of the time zone mapping.
///
/// # Errors
///
/// Returns `None` if local time falls in a _gap_ in the local time, or if there was an error.
#[must_use]
pub fn earliest(self) -> Option<T> {
match self {
MappedLocalTime::Single(t) | MappedLocalTime::Ambiguous(t, _) => Some(t),
_ => None,
}
}
/// Returns the latest possible result of the time zone mapping.
///
/// # Errors
///
/// Returns `None` if local time falls in a _gap_ in the local time, or if there was an error.
#[must_use]
pub fn latest(self) -> Option<T> {
match self {
MappedLocalTime::Single(t) | MappedLocalTime::Ambiguous(_, t) => Some(t),
_ => None,
}
}
/// Maps a `MappedLocalTime<T>` into `MappedLocalTime<U>` with given function.
#[must_use]
pub fn map<U, F: FnMut(T) -> U>(self, mut f: F) -> MappedLocalTime<U> {
match self {
MappedLocalTime::None => MappedLocalTime::None,
MappedLocalTime::Single(v) => MappedLocalTime::Single(f(v)),
MappedLocalTime::Ambiguous(min, max) => MappedLocalTime::Ambiguous(f(min), f(max)),
}
}
/// Maps a `MappedLocalTime<T>` into `MappedLocalTime<U>` with given function.
///
/// Returns `MappedLocalTime::None` if the function returns `None`.
#[must_use]
pub(crate) fn and_then<U, F: FnMut(T) -> Option<U>>(self, mut f: F) -> MappedLocalTime<U> {
match self {
MappedLocalTime::None => MappedLocalTime::None,
MappedLocalTime::Single(v) => match f(v) {
Some(new) => MappedLocalTime::Single(new),
None => MappedLocalTime::None,
},
MappedLocalTime::Ambiguous(min, max) => match (f(min), f(max)) {
(Some(min), Some(max)) => MappedLocalTime::Ambiguous(min, max),
_ => MappedLocalTime::None,
},
}
}
}
#[allow(deprecated)]
impl<Tz: TimeZone> MappedLocalTime<Date<Tz>> {
/// Makes a new `DateTime` from the current date and given `NaiveTime`.
/// The offset in the current date is preserved.
///
/// Propagates any error. Ambiguous result would be discarded.
#[inline]
#[must_use]
pub fn and_time(self, time: NaiveTime) -> MappedLocalTime<DateTime<Tz>> {
match self {
MappedLocalTime::Single(d) => {
d.and_time(time).map_or(MappedLocalTime::None, MappedLocalTime::Single)
}
_ => MappedLocalTime::None,
}
}
/// Makes a new `DateTime` from the current date, hour, minute and second.
/// The offset in the current date is preserved.
///
/// Propagates any error. Ambiguous result would be discarded.
#[inline]
#[must_use]
pub fn and_hms_opt(self, hour: u32, min: u32, sec: u32) -> MappedLocalTime<DateTime<Tz>> {
match self {
MappedLocalTime::Single(d) => {
d.and_hms_opt(hour, min, sec).map_or(MappedLocalTime::None, MappedLocalTime::Single)
}
_ => MappedLocalTime::None,
}
}
/// Makes a new `DateTime` from the current date, hour, minute, second and millisecond.
/// The millisecond part can exceed 1,000 in order to represent the leap second.
/// The offset in the current date is preserved.
///
/// Propagates any error. Ambiguous result would be discarded.
#[inline]
#[must_use]
pub fn and_hms_milli_opt(
self,
hour: u32,
min: u32,
sec: u32,
milli: u32,
) -> MappedLocalTime<DateTime<Tz>> {
match self {
MappedLocalTime::Single(d) => d
.and_hms_milli_opt(hour, min, sec, milli)
.map_or(MappedLocalTime::None, MappedLocalTime::Single),
_ => MappedLocalTime::None,
}
}
/// Makes a new `DateTime` from the current date, hour, minute, second and microsecond.
/// The microsecond part can exceed 1,000,000 in order to represent the leap second.
/// The offset in the current date is preserved.
///
/// Propagates any error. Ambiguous result would be discarded.
#[inline]
#[must_use]
pub fn and_hms_micro_opt(
self,
hour: u32,
min: u32,
sec: u32,
micro: u32,
) -> MappedLocalTime<DateTime<Tz>> {
match self {
MappedLocalTime::Single(d) => d
.and_hms_micro_opt(hour, min, sec, micro)
.map_or(MappedLocalTime::None, MappedLocalTime::Single),
_ => MappedLocalTime::None,
}
}
/// Makes a new `DateTime` from the current date, hour, minute, second and nanosecond.
/// The nanosecond part can exceed 1,000,000,000 in order to represent the leap second.
/// The offset in the current date is preserved.
///
/// Propagates any error. Ambiguous result would be discarded.
#[inline]
#[must_use]
pub fn and_hms_nano_opt(
self,
hour: u32,
min: u32,
sec: u32,
nano: u32,
) -> MappedLocalTime<DateTime<Tz>> {
match self {
MappedLocalTime::Single(d) => d
.and_hms_nano_opt(hour, min, sec, nano)
.map_or(MappedLocalTime::None, MappedLocalTime::Single),
_ => MappedLocalTime::None,
}
}
}
impl<T: fmt::Debug> MappedLocalTime<T> {
/// Returns a single unique conversion result or panics.
///
/// `unwrap()` is best combined with time zone types where the mapping can never fail like
/// [`Utc`] and [`FixedOffset`]. Note that for [`FixedOffset`] there is a rare case where a
/// resulting [`DateTime`] can be out of range.
///
/// # Panics
///
/// Panics if the local time falls within a _fold_ or a _gap_ in the local time, and on any
/// error that may have been returned by the type implementing [`TimeZone`].
#[must_use]
#[track_caller]
pub fn unwrap(self) -> T {
match self {
MappedLocalTime::None => panic!("No such local time"),
MappedLocalTime::Single(t) => t,
MappedLocalTime::Ambiguous(t1, t2) => {
panic!("Ambiguous local time, ranging from {t1:?} to {t2:?}")
}
}
}
}
/// The offset from the local time to UTC.
pub trait Offset: Sized + Clone + fmt::Debug {
/// Returns the fixed offset from UTC to the local time stored.
fn fix(&self) -> FixedOffset;
}
/// The time zone.
///
/// The methods here are the primary constructors for the [`DateTime`] type.
pub trait TimeZone: Sized + Clone {
/// An associated offset type.
/// This type is used to store the actual offset in date and time types.
/// The original `TimeZone` value can be recovered via `TimeZone::from_offset`.
type Offset: Offset;
/// Make a new `DateTime` from year, month, day, time components and current time zone.
///
/// This assumes the proleptic Gregorian calendar, with the year 0 being 1 BCE.
///
/// Returns `MappedLocalTime::None` on invalid input data.
fn with_ymd_and_hms(
&self,
year: i32,
month: u32,
day: u32,
hour: u32,
min: u32,
sec: u32,
) -> MappedLocalTime<DateTime<Self>> {
match NaiveDate::from_ymd_opt(year, month, day).and_then(|d| d.and_hms_opt(hour, min, sec))
{
Some(dt) => self.from_local_datetime(&dt),
None => MappedLocalTime::None,
}
}
/// Makes a new `Date` from year, month, day and the current time zone.
/// This assumes the proleptic Gregorian calendar, with the year 0 being 1 BCE.
///
/// The time zone normally does not affect the date (unless it is between UTC-24 and UTC+24),
/// but it will propagate to the `DateTime` values constructed via this date.
///
/// Panics on the out-of-range date, invalid month and/or day.
#[deprecated(since = "0.4.23", note = "use `with_ymd_and_hms()` instead")]
#[allow(deprecated)]
fn ymd(&self, year: i32, month: u32, day: u32) -> Date<Self> {
self.ymd_opt(year, month, day).unwrap()
}
/// Makes a new `Date` from year, month, day and the current time zone.
/// This assumes the proleptic Gregorian calendar, with the year 0 being 1 BCE.
///
/// The time zone normally does not affect the date (unless it is between UTC-24 and UTC+24),
/// but it will propagate to the `DateTime` values constructed via this date.
///
/// Returns `None` on the out-of-range date, invalid month and/or day.
#[deprecated(since = "0.4.23", note = "use `with_ymd_and_hms()` instead")]
#[allow(deprecated)]
fn ymd_opt(&self, year: i32, month: u32, day: u32) -> MappedLocalTime<Date<Self>> {
match NaiveDate::from_ymd_opt(year, month, day) {
Some(d) => self.from_local_date(&d),
None => MappedLocalTime::None,
}
}
/// Makes a new `Date` from year, day of year (DOY or "ordinal") and the current time zone.
/// This assumes the proleptic Gregorian calendar, with the year 0 being 1 BCE.
///
/// The time zone normally does not affect the date (unless it is between UTC-24 and UTC+24),
/// but it will propagate to the `DateTime` values constructed via this date.
///
/// Panics on the out-of-range date and/or invalid DOY.
#[deprecated(
since = "0.4.23",
note = "use `from_local_datetime()` with a `NaiveDateTime` instead"
)]
#[allow(deprecated)]
fn yo(&self, year: i32, ordinal: u32) -> Date<Self> {
self.yo_opt(year, ordinal).unwrap()
}
/// Makes a new `Date` from year, day of year (DOY or "ordinal") and the current time zone.
/// This assumes the proleptic Gregorian calendar, with the year 0 being 1 BCE.
///
/// The time zone normally does not affect the date (unless it is between UTC-24 and UTC+24),
/// but it will propagate to the `DateTime` values constructed via this date.
///
/// Returns `None` on the out-of-range date and/or invalid DOY.
#[deprecated(
since = "0.4.23",
note = "use `from_local_datetime()` with a `NaiveDateTime` instead"
)]
#[allow(deprecated)]
fn yo_opt(&self, year: i32, ordinal: u32) -> MappedLocalTime<Date<Self>> {
match NaiveDate::from_yo_opt(year, ordinal) {
Some(d) => self.from_local_date(&d),
None => MappedLocalTime::None,
}
}
/// Makes a new `Date` from ISO week date (year and week number), day of the week (DOW) and
/// the current time zone.
/// This assumes the proleptic Gregorian calendar, with the year 0 being 1 BCE.
/// The resulting `Date` may have a different year from the input year.
///
/// The time zone normally does not affect the date (unless it is between UTC-24 and UTC+24),
/// but it will propagate to the `DateTime` values constructed via this date.
///
/// Panics on the out-of-range date and/or invalid week number.
#[deprecated(
since = "0.4.23",
note = "use `from_local_datetime()` with a `NaiveDateTime` instead"
)]
#[allow(deprecated)]
fn isoywd(&self, year: i32, week: u32, weekday: Weekday) -> Date<Self> {
self.isoywd_opt(year, week, weekday).unwrap()
}
/// Makes a new `Date` from ISO week date (year and week number), day of the week (DOW) and
/// the current time zone.
/// This assumes the proleptic Gregorian calendar, with the year 0 being 1 BCE.
/// The resulting `Date` may have a different year from the input year.
///
/// The time zone normally does not affect the date (unless it is between UTC-24 and UTC+24),
/// but it will propagate to the `DateTime` values constructed via this date.
///
/// Returns `None` on the out-of-range date and/or invalid week number.
#[deprecated(
since = "0.4.23",
note = "use `from_local_datetime()` with a `NaiveDateTime` instead"
)]
#[allow(deprecated)]
fn isoywd_opt(&self, year: i32, week: u32, weekday: Weekday) -> MappedLocalTime<Date<Self>> {
match NaiveDate::from_isoywd_opt(year, week, weekday) {
Some(d) => self.from_local_date(&d),
None => MappedLocalTime::None,
}
}
/// Makes a new `DateTime` from the number of non-leap seconds
/// since January 1, 1970 0:00:00 UTC (aka "UNIX timestamp")
/// and the number of nanoseconds since the last whole non-leap second.
///
/// The nanosecond part can exceed 1,000,000,000 in order to represent a
/// [leap second](crate::NaiveTime#leap-second-handling), but only when `secs % 60 == 59`.
/// (The true "UNIX timestamp" cannot represent a leap second unambiguously.)
///
/// # Panics
///
/// Panics on the out-of-range number of seconds and/or invalid nanosecond,
/// for a non-panicking version see [`timestamp_opt`](#method.timestamp_opt).
#[deprecated(since = "0.4.23", note = "use `timestamp_opt()` instead")]
fn timestamp(&self, secs: i64, nsecs: u32) -> DateTime<Self> {
self.timestamp_opt(secs, nsecs).unwrap()
}
/// Makes a new `DateTime` from the number of non-leap seconds
/// since January 1, 1970 0:00:00 UTC (aka "UNIX timestamp")
/// and the number of nanoseconds since the last whole non-leap second.
///
/// The nanosecond part can exceed 1,000,000,000 in order to represent a
/// [leap second](crate::NaiveTime#leap-second-handling), but only when `secs % 60 == 59`.
/// (The true "UNIX timestamp" cannot represent a leap second unambiguously.)
///
/// # Errors
///
/// Returns `MappedLocalTime::None` on out-of-range number of seconds and/or
/// invalid nanosecond, otherwise always returns `MappedLocalTime::Single`.
///
/// # Example
///
/// ```
/// use chrono::{TimeZone, Utc};
///
/// assert_eq!(Utc.timestamp_opt(1431648000, 0).unwrap().to_string(), "2015-05-15 00:00:00 UTC");
/// ```
fn timestamp_opt(&self, secs: i64, nsecs: u32) -> MappedLocalTime<DateTime<Self>> {
match DateTime::from_timestamp(secs, nsecs) {
Some(dt) => MappedLocalTime::Single(self.from_utc_datetime(&dt.naive_utc())),
None => MappedLocalTime::None,
}
}
/// Makes a new `DateTime` from the number of non-leap milliseconds
/// since January 1, 1970 0:00:00 UTC (aka "UNIX timestamp").
///
/// Panics on out-of-range number of milliseconds for a non-panicking
/// version see [`timestamp_millis_opt`](#method.timestamp_millis_opt).
#[deprecated(since = "0.4.23", note = "use `timestamp_millis_opt()` instead")]
fn timestamp_millis(&self, millis: i64) -> DateTime<Self> {
self.timestamp_millis_opt(millis).unwrap()
}
/// Makes a new `DateTime` from the number of non-leap milliseconds
/// since January 1, 1970 0:00:00 UTC (aka "UNIX timestamp").
///
///
/// Returns `MappedLocalTime::None` on out-of-range number of milliseconds
/// and/or invalid nanosecond, otherwise always returns
/// `MappedLocalTime::Single`.
///
/// # Example
///
/// ```
/// use chrono::{MappedLocalTime, TimeZone, Utc};
/// match Utc.timestamp_millis_opt(1431648000) {
/// MappedLocalTime::Single(dt) => assert_eq!(dt.timestamp(), 1431648),
/// _ => panic!("Incorrect timestamp_millis"),
/// };
/// ```
fn timestamp_millis_opt(&self, millis: i64) -> MappedLocalTime<DateTime<Self>> {
match DateTime::from_timestamp_millis(millis) {
Some(dt) => MappedLocalTime::Single(self.from_utc_datetime(&dt.naive_utc())),
None => MappedLocalTime::None,
}
}
/// Makes a new `DateTime` from the number of non-leap nanoseconds
/// since January 1, 1970 0:00:00 UTC (aka "UNIX timestamp").
///
/// Unlike [`timestamp_millis_opt`](#method.timestamp_millis_opt), this never fails.
///
/// # Example
///
/// ```
/// use chrono::{TimeZone, Utc};
///
/// assert_eq!(Utc.timestamp_nanos(1431648000000000).timestamp(), 1431648);
/// ```
fn timestamp_nanos(&self, nanos: i64) -> DateTime<Self> {
self.from_utc_datetime(&DateTime::from_timestamp_nanos(nanos).naive_utc())
}
/// Makes a new `DateTime` from the number of non-leap microseconds
/// since January 1, 1970 0:00:00 UTC (aka "UNIX timestamp").
///
/// # Example
///
/// ```
/// use chrono::{TimeZone, Utc};
///
/// assert_eq!(Utc.timestamp_micros(1431648000000).unwrap().timestamp(), 1431648);
/// ```
fn timestamp_micros(&self, micros: i64) -> MappedLocalTime<DateTime<Self>> {
match DateTime::from_timestamp_micros(micros) {
Some(dt) => MappedLocalTime::Single(self.from_utc_datetime(&dt.naive_utc())),
None => MappedLocalTime::None,
}
}
/// Parses a string with the specified format string and returns a
/// `DateTime` with the current offset.
///
/// See the [`crate::format::strftime`] module on the
/// supported escape sequences.
///
/// If the to-be-parsed string includes an offset, it *must* match the
/// offset of the TimeZone, otherwise an error will be returned.
///
/// See also [`DateTime::parse_from_str`] which gives a [`DateTime`] with
/// parsed [`FixedOffset`].
///
/// See also [`NaiveDateTime::parse_from_str`] which gives a [`NaiveDateTime`] without
/// an offset, but can be converted to a [`DateTime`] with [`NaiveDateTime::and_utc`] or
/// [`NaiveDateTime::and_local_timezone`].
#[deprecated(
since = "0.4.29",
note = "use `DateTime::parse_from_str` or `NaiveDateTime::parse_from_str` with `and_utc()` or `and_local_timezone()` instead"
)]
fn datetime_from_str(&self, s: &str, fmt: &str) -> ParseResult<DateTime<Self>> {
let mut parsed = Parsed::new();
parse(&mut parsed, s, StrftimeItems::new(fmt))?;
parsed.to_datetime_with_timezone(self)
}
/// Reconstructs the time zone from the offset.
fn from_offset(offset: &Self::Offset) -> Self;
/// Creates the offset(s) for given local `NaiveDate` if possible.
fn offset_from_local_date(&self, local: &NaiveDate) -> MappedLocalTime<Self::Offset>;
/// Creates the offset(s) for given local `NaiveDateTime` if possible.
fn offset_from_local_datetime(&self, local: &NaiveDateTime) -> MappedLocalTime<Self::Offset>;
/// Converts the local `NaiveDate` to the timezone-aware `Date` if possible.
#[allow(clippy::wrong_self_convention)]
#[deprecated(since = "0.4.23", note = "use `from_local_datetime()` instead")]
#[allow(deprecated)]
fn from_local_date(&self, local: &NaiveDate) -> MappedLocalTime<Date<Self>> {
self.offset_from_local_date(local).map(|offset| {
// since FixedOffset is within +/- 1 day, the date is never affected
Date::from_utc(*local, offset)
})
}
/// Converts the local `NaiveDateTime` to the timezone-aware `DateTime` if possible.
#[allow(clippy::wrong_self_convention)]
fn from_local_datetime(&self, local: &NaiveDateTime) -> MappedLocalTime<DateTime<Self>> {
self.offset_from_local_datetime(local).and_then(|off| {
local
.checked_sub_offset(off.fix())
.map(|dt| DateTime::from_naive_utc_and_offset(dt, off))
})
}
/// Creates the offset for given UTC `NaiveDate`. This cannot fail.
fn offset_from_utc_date(&self, utc: &NaiveDate) -> Self::Offset;
/// Creates the offset for given UTC `NaiveDateTime`. This cannot fail.
fn offset_from_utc_datetime(&self, utc: &NaiveDateTime) -> Self::Offset;
/// Converts the UTC `NaiveDate` to the local time.
/// The UTC is continuous and thus this cannot fail (but can give the duplicate local time).
#[allow(clippy::wrong_self_convention)]
#[deprecated(since = "0.4.23", note = "use `from_utc_datetime()` instead")]
#[allow(deprecated)]
fn from_utc_date(&self, utc: &NaiveDate) -> Date<Self> {
Date::from_utc(*utc, self.offset_from_utc_date(utc))
}
/// Converts the UTC `NaiveDateTime` to the local time.
/// The UTC is continuous and thus this cannot fail (but can give the duplicate local time).
#[allow(clippy::wrong_self_convention)]
fn from_utc_datetime(&self, utc: &NaiveDateTime) -> DateTime<Self> {
DateTime::from_naive_utc_and_offset(*utc, self.offset_from_utc_datetime(utc))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fixed_offset_min_max_dates() {
for offset_hour in -23..=23 {
dbg!(offset_hour);
let offset = FixedOffset::east_opt(offset_hour * 60 * 60).unwrap();
let local_max = offset.from_utc_datetime(&NaiveDateTime::MAX);
assert_eq!(local_max.naive_utc(), NaiveDateTime::MAX);
let local_min = offset.from_utc_datetime(&NaiveDateTime::MIN);
assert_eq!(local_min.naive_utc(), NaiveDateTime::MIN);
let local_max = offset.from_local_datetime(&NaiveDateTime::MAX);
if offset_hour >= 0 {
assert_eq!(local_max.unwrap().naive_local(), NaiveDateTime::MAX);
} else {
assert_eq!(local_max, MappedLocalTime::None);
}
let local_min = offset.from_local_datetime(&NaiveDateTime::MIN);
if offset_hour <= 0 {
assert_eq!(local_min.unwrap().naive_local(), NaiveDateTime::MIN);
} else {
assert_eq!(local_min, MappedLocalTime::None);
}
}
}
#[test]
fn test_negative_millis() {
let dt = Utc.timestamp_millis_opt(-1000).unwrap();
assert_eq!(dt.to_string(), "1969-12-31 23:59:59 UTC");
let dt = Utc.timestamp_millis_opt(-7000).unwrap();
assert_eq!(dt.to_string(), "1969-12-31 23:59:53 UTC");
let dt = Utc.timestamp_millis_opt(-7001).unwrap();
assert_eq!(dt.to_string(), "1969-12-31 23:59:52.999 UTC");
let dt = Utc.timestamp_millis_opt(-7003).unwrap();
assert_eq!(dt.to_string(), "1969-12-31 23:59:52.997 UTC");
let dt = Utc.timestamp_millis_opt(-999).unwrap();
assert_eq!(dt.to_string(), "1969-12-31 23:59:59.001 UTC");
let dt = Utc.timestamp_millis_opt(-1).unwrap();
assert_eq!(dt.to_string(), "1969-12-31 23:59:59.999 UTC");
let dt = Utc.timestamp_millis_opt(-60000).unwrap();
assert_eq!(dt.to_string(), "1969-12-31 23:59:00 UTC");
let dt = Utc.timestamp_millis_opt(-3600000).unwrap();
assert_eq!(dt.to_string(), "1969-12-31 23:00:00 UTC");
for (millis, expected) in &[
(-7000, "1969-12-31 23:59:53 UTC"),
(-7001, "1969-12-31 23:59:52.999 UTC"),
(-7003, "1969-12-31 23:59:52.997 UTC"),
] {
match Utc.timestamp_millis_opt(*millis) {
MappedLocalTime::Single(dt) => {
assert_eq!(dt.to_string(), *expected);
}
e => panic!("Got {:?} instead of an okay answer", e),
}
}
}
#[test]
fn test_negative_nanos() {
let dt = Utc.timestamp_nanos(-1_000_000_000);
assert_eq!(dt.to_string(), "1969-12-31 23:59:59 UTC");
let dt = Utc.timestamp_nanos(-999_999_999);
assert_eq!(dt.to_string(), "1969-12-31 23:59:59.000000001 UTC");
let dt = Utc.timestamp_nanos(-1);
assert_eq!(dt.to_string(), "1969-12-31 23:59:59.999999999 UTC");
let dt = Utc.timestamp_nanos(-60_000_000_000);
assert_eq!(dt.to_string(), "1969-12-31 23:59:00 UTC");
let dt = Utc.timestamp_nanos(-3_600_000_000_000);
assert_eq!(dt.to_string(), "1969-12-31 23:00:00 UTC");
}
#[test]
fn test_nanos_never_panics() {
Utc.timestamp_nanos(i64::MAX);
Utc.timestamp_nanos(i64::default());
Utc.timestamp_nanos(i64::MIN);
}
#[test]
fn test_negative_micros() {
let dt = Utc.timestamp_micros(-1_000_000).unwrap();
assert_eq!(dt.to_string(), "1969-12-31 23:59:59 UTC");
let dt = Utc.timestamp_micros(-999_999).unwrap();
assert_eq!(dt.to_string(), "1969-12-31 23:59:59.000001 UTC");
let dt = Utc.timestamp_micros(-1).unwrap();
assert_eq!(dt.to_string(), "1969-12-31 23:59:59.999999 UTC");
let dt = Utc.timestamp_micros(-60_000_000).unwrap();
assert_eq!(dt.to_string(), "1969-12-31 23:59:00 UTC");
let dt = Utc.timestamp_micros(-3_600_000_000).unwrap();
assert_eq!(dt.to_string(), "1969-12-31 23:00:00 UTC");
}
}

View File

@@ -0,0 +1,152 @@
// This is a part of Chrono.
// See README.md and LICENSE.txt for details.
//! The UTC (Coordinated Universal Time) time zone.
use core::fmt;
#[cfg(all(
feature = "now",
not(all(
target_arch = "wasm32",
feature = "wasmbind",
not(any(target_os = "emscripten", target_os = "wasi", target_os = "linux"))
))
))]
use std::time::{SystemTime, UNIX_EPOCH};
#[cfg(any(feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"))]
use rkyv::{Archive, Deserialize, Serialize};
use super::{FixedOffset, MappedLocalTime, Offset, TimeZone};
use crate::naive::{NaiveDate, NaiveDateTime};
#[cfg(feature = "now")]
#[allow(deprecated)]
use crate::{Date, DateTime};
/// The UTC time zone. This is the most efficient time zone when you don't need the local time.
/// It is also used as an offset (which is also a dummy type).
///
/// Using the [`TimeZone`](./trait.TimeZone.html) methods
/// on the UTC struct is the preferred way to construct `DateTime<Utc>`
/// instances.
///
/// # Example
///
/// ```
/// use chrono::{DateTime, TimeZone, Utc};
///
/// let dt = DateTime::from_timestamp(61, 0).unwrap();
///
/// assert_eq!(Utc.timestamp_opt(61, 0).unwrap(), dt);
/// assert_eq!(Utc.with_ymd_and_hms(1970, 1, 1, 0, 1, 1).unwrap(), dt);
/// ```
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(
any(feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"),
derive(Archive, Deserialize, Serialize),
rkyv(compare(PartialEq)),
rkyv(attr(derive(Clone, Copy, PartialEq, Eq, Debug, Hash)))
)]
#[cfg_attr(feature = "rkyv-validation", archive(check_bytes))]
#[cfg_attr(all(feature = "arbitrary", feature = "std"), derive(arbitrary::Arbitrary))]
pub struct Utc;
#[cfg(feature = "now")]
impl Utc {
/// Returns a `Date` which corresponds to the current date.
#[deprecated(
since = "0.4.23",
note = "use `Utc::now()` instead, potentially with `.date_naive()`"
)]
#[allow(deprecated)]
#[must_use]
pub fn today() -> Date<Utc> {
Utc::now().date()
}
/// Returns a `DateTime<Utc>` which corresponds to the current date and time in UTC.
///
/// See also the similar [`Local::now()`] which returns `DateTime<Local>`, i.e. the local date
/// and time including offset from UTC.
///
/// [`Local::now()`]: crate::Local::now
///
/// # Example
///
/// ```
/// # #![allow(unused_variables)]
/// # use chrono::{FixedOffset, Utc};
/// // Current time in UTC
/// let now_utc = Utc::now();
///
/// // Current date in UTC
/// let today_utc = now_utc.date_naive();
///
/// // Current time in some timezone (let's use +05:00)
/// let offset = FixedOffset::east_opt(5 * 60 * 60).unwrap();
/// let now_with_offset = Utc::now().with_timezone(&offset);
/// ```
#[cfg(not(all(
target_arch = "wasm32",
feature = "wasmbind",
not(any(target_os = "emscripten", target_os = "wasi", target_os = "linux"))
)))]
#[must_use]
pub fn now() -> DateTime<Utc> {
let now =
SystemTime::now().duration_since(UNIX_EPOCH).expect("system time before Unix epoch");
DateTime::from_timestamp(now.as_secs() as i64, now.subsec_nanos()).unwrap()
}
/// Returns a `DateTime` which corresponds to the current date and time.
#[cfg(all(
target_arch = "wasm32",
feature = "wasmbind",
not(any(target_os = "emscripten", target_os = "wasi", target_os = "linux"))
))]
#[must_use]
pub fn now() -> DateTime<Utc> {
let now = js_sys::Date::new_0();
DateTime::<Utc>::from(now)
}
}
impl TimeZone for Utc {
type Offset = Utc;
fn from_offset(_state: &Utc) -> Utc {
Utc
}
fn offset_from_local_date(&self, _local: &NaiveDate) -> MappedLocalTime<Utc> {
MappedLocalTime::Single(Utc)
}
fn offset_from_local_datetime(&self, _local: &NaiveDateTime) -> MappedLocalTime<Utc> {
MappedLocalTime::Single(Utc)
}
fn offset_from_utc_date(&self, _utc: &NaiveDate) -> Utc {
Utc
}
fn offset_from_utc_datetime(&self, _utc: &NaiveDateTime) -> Utc {
Utc
}
}
impl Offset for Utc {
fn fix(&self) -> FixedOffset {
FixedOffset::east_opt(0).unwrap()
}
}
impl fmt::Debug for Utc {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Z")
}
}
impl fmt::Display for Utc {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "UTC")
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,415 @@
use crate::{IsoWeek, Month, Weekday};
/// The common set of methods for date component.
///
/// Methods such as [`year`], [`month`], [`day`] and [`weekday`] can be used to get basic
/// information about the date.
///
/// The `with_*` methods can change the date.
///
/// # Warning
///
/// The `with_*` methods can be convenient to change a single component of a date, but they must be
/// used with some care. Examples to watch out for:
///
/// - [`with_year`] changes the year component of a year-month-day value. Don't use this method if
/// you want the ordinal to stay the same after changing the year, of if you want the week and
/// weekday values to stay the same.
/// - Don't combine two `with_*` methods to change two components of the date. For example to
/// change both the year and month components of a date. This could fail because an intermediate
/// value does not exist, while the final date would be valid.
///
/// For more complex changes to a date, it is best to use the methods on [`NaiveDate`] to create a
/// new value instead of altering an existing date.
///
/// [`year`]: Datelike::year
/// [`month`]: Datelike::month
/// [`day`]: Datelike::day
/// [`weekday`]: Datelike::weekday
/// [`with_year`]: Datelike::with_year
/// [`NaiveDate`]: crate::NaiveDate
pub trait Datelike: Sized {
/// Returns the year number in the [calendar date](./naive/struct.NaiveDate.html#calendar-date).
fn year(&self) -> i32;
/// Returns the absolute year number starting from 1 with a boolean flag,
/// which is false when the year predates the epoch (BCE/BC) and true otherwise (CE/AD).
#[inline]
fn year_ce(&self) -> (bool, u32) {
let year = self.year();
if year < 1 { (false, (1 - year) as u32) } else { (true, year as u32) }
}
/// Returns the quarter number starting from 1.
///
/// The return value ranges from 1 to 4.
#[inline]
fn quarter(&self) -> u32 {
(self.month() - 1).div_euclid(3) + 1
}
/// Returns the month number starting from 1.
///
/// The return value ranges from 1 to 12.
fn month(&self) -> u32;
/// Returns the month number starting from 0.
///
/// The return value ranges from 0 to 11.
fn month0(&self) -> u32;
/// Returns the day of month starting from 1.
///
/// The return value ranges from 1 to 31. (The last day of month differs by months.)
fn day(&self) -> u32;
/// Returns the day of month starting from 0.
///
/// The return value ranges from 0 to 30. (The last day of month differs by months.)
fn day0(&self) -> u32;
/// Returns the day of year starting from 1.
///
/// The return value ranges from 1 to 366. (The last day of year differs by years.)
fn ordinal(&self) -> u32;
/// Returns the day of year starting from 0.
///
/// The return value ranges from 0 to 365. (The last day of year differs by years.)
fn ordinal0(&self) -> u32;
/// Returns the day of week.
fn weekday(&self) -> Weekday;
/// Returns the ISO week.
fn iso_week(&self) -> IsoWeek;
/// Makes a new value with the year number changed, while keeping the same month and day.
///
/// This method assumes you want to work on the date as a year-month-day value. Don't use it if
/// you want the ordinal to stay the same after changing the year, of if you want the week and
/// weekday values to stay the same.
///
/// # Errors
///
/// Returns `None` when:
///
/// - The resulting date does not exist (February 29 in a non-leap year).
/// - The year is out of range for [`NaiveDate`].
/// - In case of [`DateTime<Tz>`] if the resulting date and time fall within a timezone
/// transition such as from DST to standard time.
///
/// [`NaiveDate`]: crate::NaiveDate
/// [`DateTime<Tz>`]: crate::DateTime
///
/// # Examples
///
/// ```
/// use chrono::{Datelike, NaiveDate};
///
/// assert_eq!(
/// NaiveDate::from_ymd_opt(2020, 5, 13).unwrap().with_year(2023).unwrap(),
/// NaiveDate::from_ymd_opt(2023, 5, 13).unwrap()
/// );
/// // Resulting date 2023-02-29 does not exist:
/// assert!(NaiveDate::from_ymd_opt(2020, 2, 29).unwrap().with_year(2023).is_none());
///
/// // Don't use `with_year` if you want the ordinal date to stay the same:
/// assert_ne!(
/// NaiveDate::from_yo_opt(2020, 100).unwrap().with_year(2023).unwrap(),
/// NaiveDate::from_yo_opt(2023, 100).unwrap() // result is 2023-101
/// );
/// ```
fn with_year(&self, year: i32) -> Option<Self>;
/// Makes a new value with the month number (starting from 1) changed.
///
/// # Errors
///
/// Returns `None` when:
///
/// - The resulting date does not exist (for example `month(4)` when day of the month is 31).
/// - In case of [`DateTime<Tz>`] if the resulting date and time fall within a timezone
/// transition such as from DST to standard time.
/// - The value for `month` is out of range.
///
/// [`DateTime<Tz>`]: crate::DateTime
///
/// # Examples
///
/// ```
/// use chrono::{Datelike, NaiveDate};
///
/// assert_eq!(
/// NaiveDate::from_ymd_opt(2023, 5, 12).unwrap().with_month(9).unwrap(),
/// NaiveDate::from_ymd_opt(2023, 9, 12).unwrap()
/// );
/// // Resulting date 2023-09-31 does not exist:
/// assert!(NaiveDate::from_ymd_opt(2023, 5, 31).unwrap().with_month(9).is_none());
/// ```
///
/// Don't combine multiple `Datelike::with_*` methods. The intermediate value may not exist.
/// ```
/// use chrono::{Datelike, NaiveDate};
///
/// fn with_year_month(date: NaiveDate, year: i32, month: u32) -> Option<NaiveDate> {
/// date.with_year(year)?.with_month(month)
/// }
/// let d = NaiveDate::from_ymd_opt(2020, 2, 29).unwrap();
/// assert!(with_year_month(d, 2019, 1).is_none()); // fails because of invalid intermediate value
///
/// // Correct version:
/// fn with_year_month_fixed(date: NaiveDate, year: i32, month: u32) -> Option<NaiveDate> {
/// NaiveDate::from_ymd_opt(year, month, date.day())
/// }
/// let d = NaiveDate::from_ymd_opt(2020, 2, 29).unwrap();
/// assert_eq!(with_year_month_fixed(d, 2019, 1), NaiveDate::from_ymd_opt(2019, 1, 29));
/// ```
fn with_month(&self, month: u32) -> Option<Self>;
/// Makes a new value with the month number (starting from 0) changed.
///
/// # Errors
///
/// Returns `None` when:
///
/// - The resulting date does not exist (for example `month0(3)` when day of the month is 31).
/// - In case of [`DateTime<Tz>`] if the resulting date and time fall within a timezone
/// transition such as from DST to standard time.
/// - The value for `month0` is out of range.
///
/// [`DateTime<Tz>`]: crate::DateTime
fn with_month0(&self, month0: u32) -> Option<Self>;
/// Makes a new value with the day of month (starting from 1) changed.
///
/// # Errors
///
/// Returns `None` when:
///
/// - The resulting date does not exist (for example `day(31)` in April).
/// - In case of [`DateTime<Tz>`] if the resulting date and time fall within a timezone
/// transition such as from DST to standard time.
/// - The value for `day` is out of range.
///
/// [`DateTime<Tz>`]: crate::DateTime
fn with_day(&self, day: u32) -> Option<Self>;
/// Makes a new value with the day of month (starting from 0) changed.
///
/// # Errors
///
/// Returns `None` when:
///
/// - The resulting date does not exist (for example `day0(30)` in April).
/// - In case of [`DateTime<Tz>`] if the resulting date and time fall within a timezone
/// transition such as from DST to standard time.
/// - The value for `day0` is out of range.
///
/// [`DateTime<Tz>`]: crate::DateTime
fn with_day0(&self, day0: u32) -> Option<Self>;
/// Makes a new value with the day of year (starting from 1) changed.
///
/// # Errors
///
/// Returns `None` when:
///
/// - The resulting date does not exist (`with_ordinal(366)` in a non-leap year).
/// - In case of [`DateTime<Tz>`] if the resulting date and time fall within a timezone
/// transition such as from DST to standard time.
/// - The value for `ordinal` is out of range.
///
/// [`DateTime<Tz>`]: crate::DateTime
fn with_ordinal(&self, ordinal: u32) -> Option<Self>;
/// Makes a new value with the day of year (starting from 0) changed.
///
/// # Errors
///
/// Returns `None` when:
///
/// - The resulting date does not exist (`with_ordinal0(365)` in a non-leap year).
/// - In case of [`DateTime<Tz>`] if the resulting date and time fall within a timezone
/// transition such as from DST to standard time.
/// - The value for `ordinal0` is out of range.
///
/// [`DateTime<Tz>`]: crate::DateTime
fn with_ordinal0(&self, ordinal0: u32) -> Option<Self>;
/// Counts the days in the proleptic Gregorian calendar, with January 1, Year 1 (CE) as day 1.
///
/// # Examples
///
/// ```
/// use chrono::{Datelike, NaiveDate};
///
/// assert_eq!(NaiveDate::from_ymd_opt(1970, 1, 1).unwrap().num_days_from_ce(), 719_163);
/// assert_eq!(NaiveDate::from_ymd_opt(2, 1, 1).unwrap().num_days_from_ce(), 366);
/// assert_eq!(NaiveDate::from_ymd_opt(1, 1, 1).unwrap().num_days_from_ce(), 1);
/// assert_eq!(NaiveDate::from_ymd_opt(0, 1, 1).unwrap().num_days_from_ce(), -365);
/// ```
fn num_days_from_ce(&self) -> i32 {
// See test_num_days_from_ce_against_alternative_impl below for a more straightforward
// implementation.
// we know this wouldn't overflow since year is limited to 1/2^13 of i32's full range.
let mut year = self.year() - 1;
let mut ndays = 0;
if year < 0 {
let excess = 1 + (-year) / 400;
year += excess * 400;
ndays -= excess * 146_097;
}
let div_100 = year / 100;
ndays += ((year * 1461) >> 2) - div_100 + (div_100 >> 2);
ndays + self.ordinal() as i32
}
/// Get the length in days of the month
fn num_days_in_month(&self) -> u8 {
use num_traits::FromPrimitive;
// The value returned from `self.month()` is guaranteed to be in the
// range [1,12], which will never result in a `None` value here.
let month = Month::from_u32(self.month()).unwrap();
// `Month::num_days` will only return `None` if the provided year is out
// of range. Since we are passing it directly from a verified date, we
// know it is in range, and the result will never be `None`.
month.num_days(self.year()).unwrap()
}
}
/// The common set of methods for time component.
pub trait Timelike: Sized {
/// Returns the hour number from 0 to 23.
fn hour(&self) -> u32;
/// Returns the hour number from 1 to 12 with a boolean flag,
/// which is false for AM and true for PM.
#[inline]
fn hour12(&self) -> (bool, u32) {
let hour = self.hour();
let mut hour12 = hour % 12;
if hour12 == 0 {
hour12 = 12;
}
(hour >= 12, hour12)
}
/// Returns the minute number from 0 to 59.
fn minute(&self) -> u32;
/// Returns the second number from 0 to 59.
fn second(&self) -> u32;
/// Returns the number of nanoseconds since the whole non-leap second.
/// The range from 1,000,000,000 to 1,999,999,999 represents
/// the [leap second](./naive/struct.NaiveTime.html#leap-second-handling).
fn nanosecond(&self) -> u32;
/// Makes a new value with the hour number changed.
///
/// Returns `None` when the resulting value would be invalid.
fn with_hour(&self, hour: u32) -> Option<Self>;
/// Makes a new value with the minute number changed.
///
/// Returns `None` when the resulting value would be invalid.
fn with_minute(&self, min: u32) -> Option<Self>;
/// Makes a new value with the second number changed.
///
/// Returns `None` when the resulting value would be invalid.
/// As with the [`second`](#tymethod.second) method,
/// the input range is restricted to 0 through 59.
fn with_second(&self, sec: u32) -> Option<Self>;
/// Makes a new value with nanoseconds since the whole non-leap second changed.
///
/// Returns `None` when the resulting value would be invalid.
/// As with the [`nanosecond`](#tymethod.nanosecond) method,
/// the input range can exceed 1,000,000,000 for leap seconds.
fn with_nanosecond(&self, nano: u32) -> Option<Self>;
/// Returns the number of non-leap seconds past the last midnight.
///
/// Every value in 00:00:00-23:59:59 maps to an integer in 0-86399.
///
/// This method is not intended to provide the real number of seconds since midnight on a given
/// day. It does not take things like DST transitions into account.
#[inline]
fn num_seconds_from_midnight(&self) -> u32 {
self.hour() * 3600 + self.minute() * 60 + self.second()
}
}
#[cfg(test)]
mod tests {
use super::Datelike;
use crate::{Days, NaiveDate};
/// Tests `Datelike::num_days_from_ce` against an alternative implementation.
///
/// The alternative implementation is not as short as the current one but it is simpler to
/// understand, with less unexplained magic constants.
#[test]
fn test_num_days_from_ce_against_alternative_impl() {
/// Returns the number of multiples of `div` in the range `start..end`.
///
/// If the range `start..end` is back-to-front, i.e. `start` is greater than `end`, the
/// behaviour is defined by the following equation:
/// `in_between(start, end, div) == - in_between(end, start, div)`.
///
/// When `div` is 1, this is equivalent to `end - start`, i.e. the length of `start..end`.
///
/// # Panics
///
/// Panics if `div` is not positive.
fn in_between(start: i32, end: i32, div: i32) -> i32 {
assert!(div > 0, "in_between: nonpositive div = {}", div);
let start = (start.div_euclid(div), start.rem_euclid(div));
let end = (end.div_euclid(div), end.rem_euclid(div));
// The lowest multiple of `div` greater than or equal to `start`, divided.
let start = start.0 + (start.1 != 0) as i32;
// The lowest multiple of `div` greater than or equal to `end`, divided.
let end = end.0 + (end.1 != 0) as i32;
end - start
}
/// Alternative implementation to `Datelike::num_days_from_ce`
fn num_days_from_ce<Date: Datelike>(date: &Date) -> i32 {
let year = date.year();
let diff = move |div| in_between(1, year, div);
// 365 days a year, one more in leap years. In the gregorian calendar, leap years are all
// the multiples of 4 except multiples of 100 but including multiples of 400.
date.ordinal() as i32 + 365 * diff(1) + diff(4) - diff(100) + diff(400)
}
for year in NaiveDate::MIN.year()..=NaiveDate::MAX.year() {
let jan1_year = NaiveDate::from_ymd_opt(year, 1, 1).unwrap();
assert_eq!(
jan1_year.num_days_from_ce(),
num_days_from_ce(&jan1_year),
"on {:?}",
jan1_year
);
let mid_year = jan1_year + Days::new(133);
assert_eq!(
mid_year.num_days_from_ce(),
num_days_from_ce(&mid_year),
"on {:?}",
mid_year
);
}
}
#[test]
fn test_num_days_in_month() {
let feb_leap_year = NaiveDate::from_ymd_opt(2004, 2, 1).unwrap();
assert_eq!(feb_leap_year.num_days_in_month(), 29);
let feb = feb_leap_year.with_year(2005).unwrap();
assert_eq!(feb.num_days_in_month(), 28);
let march = feb.with_month(3).unwrap();
assert_eq!(march.num_days_in_month(), 31);
}
}

View File

@@ -0,0 +1,408 @@
use core::fmt;
#[cfg(any(feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"))]
use rkyv::{Archive, Deserialize, Serialize};
use crate::OutOfRange;
/// The day of week.
///
/// The order of the days of week depends on the context.
/// (This is why this type does *not* implement `PartialOrd` or `Ord` traits.)
/// One should prefer `*_from_monday` or `*_from_sunday` methods to get the correct result.
///
/// # Example
/// ```
/// use chrono::Weekday;
///
/// let monday = "Monday".parse::<Weekday>().unwrap();
/// assert_eq!(monday, Weekday::Mon);
///
/// let sunday = Weekday::try_from(6).unwrap();
/// assert_eq!(sunday, Weekday::Sun);
///
/// assert_eq!(sunday.num_days_from_monday(), 6); // starts counting with Monday = 0
/// assert_eq!(sunday.number_from_monday(), 7); // starts counting with Monday = 1
/// assert_eq!(sunday.num_days_from_sunday(), 0); // starts counting with Sunday = 0
/// assert_eq!(sunday.number_from_sunday(), 1); // starts counting with Sunday = 1
///
/// assert_eq!(sunday.succ(), monday);
/// assert_eq!(sunday.pred(), Weekday::Sat);
/// ```
#[derive(PartialEq, Eq, Copy, Clone, Debug, Hash)]
#[cfg_attr(
any(feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"),
derive(Archive, Deserialize, Serialize),
rkyv(compare(PartialEq)),
rkyv(attr(derive(Clone, Copy, PartialEq, Eq, Debug, Hash)))
)]
#[cfg_attr(feature = "rkyv-validation", archive(check_bytes))]
#[cfg_attr(all(feature = "arbitrary", feature = "std"), derive(arbitrary::Arbitrary))]
pub enum Weekday {
/// Monday.
Mon = 0,
/// Tuesday.
Tue = 1,
/// Wednesday.
Wed = 2,
/// Thursday.
Thu = 3,
/// Friday.
Fri = 4,
/// Saturday.
Sat = 5,
/// Sunday.
Sun = 6,
}
impl Weekday {
/// The next day in the week.
///
/// `w`: | `Mon` | `Tue` | `Wed` | `Thu` | `Fri` | `Sat` | `Sun`
/// ----------- | ----- | ----- | ----- | ----- | ----- | ----- | -----
/// `w.succ()`: | `Tue` | `Wed` | `Thu` | `Fri` | `Sat` | `Sun` | `Mon`
#[inline]
#[must_use]
pub const fn succ(&self) -> Weekday {
match *self {
Weekday::Mon => Weekday::Tue,
Weekday::Tue => Weekday::Wed,
Weekday::Wed => Weekday::Thu,
Weekday::Thu => Weekday::Fri,
Weekday::Fri => Weekday::Sat,
Weekday::Sat => Weekday::Sun,
Weekday::Sun => Weekday::Mon,
}
}
/// The previous day in the week.
///
/// `w`: | `Mon` | `Tue` | `Wed` | `Thu` | `Fri` | `Sat` | `Sun`
/// ----------- | ----- | ----- | ----- | ----- | ----- | ----- | -----
/// `w.pred()`: | `Sun` | `Mon` | `Tue` | `Wed` | `Thu` | `Fri` | `Sat`
#[inline]
#[must_use]
pub const fn pred(&self) -> Weekday {
match *self {
Weekday::Mon => Weekday::Sun,
Weekday::Tue => Weekday::Mon,
Weekday::Wed => Weekday::Tue,
Weekday::Thu => Weekday::Wed,
Weekday::Fri => Weekday::Thu,
Weekday::Sat => Weekday::Fri,
Weekday::Sun => Weekday::Sat,
}
}
/// Returns a day-of-week number starting from Monday = 1. (ISO 8601 weekday number)
///
/// `w`: | `Mon` | `Tue` | `Wed` | `Thu` | `Fri` | `Sat` | `Sun`
/// ------------------------- | ----- | ----- | ----- | ----- | ----- | ----- | -----
/// `w.number_from_monday()`: | 1 | 2 | 3 | 4 | 5 | 6 | 7
#[inline]
pub const fn number_from_monday(&self) -> u32 {
self.days_since(Weekday::Mon) + 1
}
/// Returns a day-of-week number starting from Sunday = 1.
///
/// `w`: | `Mon` | `Tue` | `Wed` | `Thu` | `Fri` | `Sat` | `Sun`
/// ------------------------- | ----- | ----- | ----- | ----- | ----- | ----- | -----
/// `w.number_from_sunday()`: | 2 | 3 | 4 | 5 | 6 | 7 | 1
#[inline]
pub const fn number_from_sunday(&self) -> u32 {
self.days_since(Weekday::Sun) + 1
}
/// Returns a day-of-week number starting from Monday = 0.
///
/// `w`: | `Mon` | `Tue` | `Wed` | `Thu` | `Fri` | `Sat` | `Sun`
/// --------------------------- | ----- | ----- | ----- | ----- | ----- | ----- | -----
/// `w.num_days_from_monday()`: | 0 | 1 | 2 | 3 | 4 | 5 | 6
///
/// # Example
///
/// ```
/// # #[cfg(feature = "clock")] {
/// # use chrono::{Local, Datelike};
/// // MTWRFSU is occasionally used as a single-letter abbreviation of the weekdays.
/// // Use `num_days_from_monday` to index into the array.
/// const MTWRFSU: [char; 7] = ['M', 'T', 'W', 'R', 'F', 'S', 'U'];
///
/// let today = Local::now().weekday();
/// println!("{}", MTWRFSU[today.num_days_from_monday() as usize]);
/// # }
/// ```
#[inline]
pub const fn num_days_from_monday(&self) -> u32 {
self.days_since(Weekday::Mon)
}
/// Returns a day-of-week number starting from Sunday = 0.
///
/// `w`: | `Mon` | `Tue` | `Wed` | `Thu` | `Fri` | `Sat` | `Sun`
/// --------------------------- | ----- | ----- | ----- | ----- | ----- | ----- | -----
/// `w.num_days_from_sunday()`: | 1 | 2 | 3 | 4 | 5 | 6 | 0
#[inline]
pub const fn num_days_from_sunday(&self) -> u32 {
self.days_since(Weekday::Sun)
}
/// The number of days since the given day.
///
/// # Examples
///
/// ```
/// use chrono::Weekday::*;
/// assert_eq!(Mon.days_since(Mon), 0);
/// assert_eq!(Sun.days_since(Tue), 5);
/// assert_eq!(Wed.days_since(Sun), 3);
/// ```
pub const fn days_since(&self, other: Weekday) -> u32 {
let lhs = *self as u32;
let rhs = other as u32;
if lhs < rhs { 7 + lhs - rhs } else { lhs - rhs }
}
}
impl fmt::Display for Weekday {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.pad(match *self {
Weekday::Mon => "Mon",
Weekday::Tue => "Tue",
Weekday::Wed => "Wed",
Weekday::Thu => "Thu",
Weekday::Fri => "Fri",
Weekday::Sat => "Sat",
Weekday::Sun => "Sun",
})
}
}
/// Any weekday can be represented as an integer from 0 to 6, which equals to
/// [`Weekday::num_days_from_monday`](#method.num_days_from_monday) in this implementation.
/// Do not heavily depend on this though; use explicit methods whenever possible.
impl TryFrom<u8> for Weekday {
type Error = OutOfRange;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0 => Ok(Weekday::Mon),
1 => Ok(Weekday::Tue),
2 => Ok(Weekday::Wed),
3 => Ok(Weekday::Thu),
4 => Ok(Weekday::Fri),
5 => Ok(Weekday::Sat),
6 => Ok(Weekday::Sun),
_ => Err(OutOfRange::new()),
}
}
}
/// Any weekday can be represented as an integer from 0 to 6, which equals to
/// [`Weekday::num_days_from_monday`](#method.num_days_from_monday) in this implementation.
/// Do not heavily depend on this though; use explicit methods whenever possible.
impl num_traits::FromPrimitive for Weekday {
#[inline]
fn from_i64(n: i64) -> Option<Weekday> {
match n {
0 => Some(Weekday::Mon),
1 => Some(Weekday::Tue),
2 => Some(Weekday::Wed),
3 => Some(Weekday::Thu),
4 => Some(Weekday::Fri),
5 => Some(Weekday::Sat),
6 => Some(Weekday::Sun),
_ => None,
}
}
#[inline]
fn from_u64(n: u64) -> Option<Weekday> {
match n {
0 => Some(Weekday::Mon),
1 => Some(Weekday::Tue),
2 => Some(Weekday::Wed),
3 => Some(Weekday::Thu),
4 => Some(Weekday::Fri),
5 => Some(Weekday::Sat),
6 => Some(Weekday::Sun),
_ => None,
}
}
}
/// An error resulting from reading `Weekday` value with `FromStr`.
#[derive(Clone, PartialEq, Eq)]
pub struct ParseWeekdayError {
pub(crate) _dummy: (),
}
#[cfg(feature = "std")]
impl std::error::Error for ParseWeekdayError {}
impl fmt::Display for ParseWeekdayError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_fmt(format_args!("{self:?}"))
}
}
impl fmt::Debug for ParseWeekdayError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "ParseWeekdayError {{ .. }}")
}
}
// the actual `FromStr` implementation is in the `format` module to leverage the existing code
#[cfg(feature = "serde")]
mod weekday_serde {
use super::Weekday;
use core::fmt;
use serde::{de, ser};
impl ser::Serialize for Weekday {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: ser::Serializer,
{
serializer.collect_str(&self)
}
}
struct WeekdayVisitor;
impl de::Visitor<'_> for WeekdayVisitor {
type Value = Weekday;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("Weekday")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
value.parse().map_err(|_| E::custom("short or long weekday names expected"))
}
}
impl<'de> de::Deserialize<'de> for Weekday {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
deserializer.deserialize_str(WeekdayVisitor)
}
}
}
#[cfg(test)]
mod tests {
use super::Weekday;
#[test]
fn test_days_since() {
for i in 0..7 {
let base_day = Weekday::try_from(i).unwrap();
assert_eq!(base_day.num_days_from_monday(), base_day.days_since(Weekday::Mon));
assert_eq!(base_day.num_days_from_sunday(), base_day.days_since(Weekday::Sun));
assert_eq!(base_day.days_since(base_day), 0);
assert_eq!(base_day.days_since(base_day.pred()), 1);
assert_eq!(base_day.days_since(base_day.pred().pred()), 2);
assert_eq!(base_day.days_since(base_day.pred().pred().pred()), 3);
assert_eq!(base_day.days_since(base_day.pred().pred().pred().pred()), 4);
assert_eq!(base_day.days_since(base_day.pred().pred().pred().pred().pred()), 5);
assert_eq!(base_day.days_since(base_day.pred().pred().pred().pred().pred().pred()), 6);
assert_eq!(base_day.days_since(base_day.succ()), 6);
assert_eq!(base_day.days_since(base_day.succ().succ()), 5);
assert_eq!(base_day.days_since(base_day.succ().succ().succ()), 4);
assert_eq!(base_day.days_since(base_day.succ().succ().succ().succ()), 3);
assert_eq!(base_day.days_since(base_day.succ().succ().succ().succ().succ()), 2);
assert_eq!(base_day.days_since(base_day.succ().succ().succ().succ().succ().succ()), 1);
}
}
#[test]
fn test_formatting_alignment() {
// No exhaustive testing here as we just delegate the
// implementation to Formatter::pad. Just some basic smoke
// testing to ensure that it's in fact being done.
assert_eq!(format!("{:x>7}", Weekday::Mon), "xxxxMon");
assert_eq!(format!("{:^7}", Weekday::Mon), " Mon ");
assert_eq!(format!("{:Z<7}", Weekday::Mon), "MonZZZZ");
}
#[test]
#[cfg(feature = "serde")]
fn test_serde_serialize() {
use Weekday::*;
use serde_json::to_string;
let cases: Vec<(Weekday, &str)> = vec![
(Mon, "\"Mon\""),
(Tue, "\"Tue\""),
(Wed, "\"Wed\""),
(Thu, "\"Thu\""),
(Fri, "\"Fri\""),
(Sat, "\"Sat\""),
(Sun, "\"Sun\""),
];
for (weekday, expected_str) in cases {
let string = to_string(&weekday).unwrap();
assert_eq!(string, expected_str);
}
}
#[test]
#[cfg(feature = "serde")]
fn test_serde_deserialize() {
use Weekday::*;
use serde_json::from_str;
let cases: Vec<(&str, Weekday)> = vec![
("\"mon\"", Mon),
("\"MONDAY\"", Mon),
("\"MonDay\"", Mon),
("\"mOn\"", Mon),
("\"tue\"", Tue),
("\"tuesday\"", Tue),
("\"wed\"", Wed),
("\"wednesday\"", Wed),
("\"thu\"", Thu),
("\"thursday\"", Thu),
("\"fri\"", Fri),
("\"friday\"", Fri),
("\"sat\"", Sat),
("\"saturday\"", Sat),
("\"sun\"", Sun),
("\"sunday\"", Sun),
];
for (str, expected_weekday) in cases {
let weekday = from_str::<Weekday>(str).unwrap();
assert_eq!(weekday, expected_weekday);
}
let errors: Vec<&str> =
vec!["\"not a weekday\"", "\"monDAYs\"", "\"mond\"", "mon", "\"thur\"", "\"thurs\""];
for str in errors {
from_str::<Weekday>(str).unwrap_err();
}
}
#[test]
#[cfg(feature = "rkyv-validation")]
fn test_rkyv_validation() {
let mon = Weekday::Mon;
let bytes = rkyv::to_bytes::<_, 1>(&mon).unwrap();
assert_eq!(rkyv::from_bytes::<Weekday>(&bytes).unwrap(), mon);
}
}

View File

@@ -0,0 +1,483 @@
use core::{
fmt::{self, Debug},
iter::FusedIterator,
};
use crate::Weekday;
/// A collection of [`Weekday`]s stored as a single byte.
///
/// This type is `Copy` and provides efficient set-like and slice-like operations.
/// Many operations are `const` as well.
///
/// Implemented as a bitmask where bits 1-7 correspond to Monday-Sunday.
#[derive(Clone, Copy, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct WeekdaySet(u8); // Invariant: the 8-th bit is always 0.
impl WeekdaySet {
/// Create a `WeekdaySet` from an array of [`Weekday`]s.
///
/// # Example
/// ```
/// # use chrono::WeekdaySet;
/// use chrono::Weekday::*;
/// assert_eq!(WeekdaySet::EMPTY, WeekdaySet::from_array([]));
/// assert_eq!(WeekdaySet::single(Mon), WeekdaySet::from_array([Mon]));
/// assert_eq!(WeekdaySet::ALL, WeekdaySet::from_array([Mon, Tue, Wed, Thu, Fri, Sat, Sun]));
/// ```
pub const fn from_array<const C: usize>(days: [Weekday; C]) -> Self {
let mut acc = Self::EMPTY;
let mut idx = 0;
while idx < days.len() {
acc.0 |= Self::single(days[idx]).0;
idx += 1;
}
acc
}
/// Create a `WeekdaySet` from a single [`Weekday`].
pub const fn single(weekday: Weekday) -> Self {
match weekday {
Weekday::Mon => Self(0b000_0001),
Weekday::Tue => Self(0b000_0010),
Weekday::Wed => Self(0b000_0100),
Weekday::Thu => Self(0b000_1000),
Weekday::Fri => Self(0b001_0000),
Weekday::Sat => Self(0b010_0000),
Weekday::Sun => Self(0b100_0000),
}
}
/// Returns `Some(day)` if this collection contains exactly one day.
///
/// Returns `None` otherwise.
///
/// # Example
/// ```
/// # use chrono::WeekdaySet;
/// use chrono::Weekday::*;
/// assert_eq!(WeekdaySet::single(Mon).single_day(), Some(Mon));
/// assert_eq!(WeekdaySet::from_array([Mon, Tue]).single_day(), None);
/// assert_eq!(WeekdaySet::EMPTY.single_day(), None);
/// assert_eq!(WeekdaySet::ALL.single_day(), None);
/// ```
pub const fn single_day(self) -> Option<Weekday> {
match self {
Self(0b000_0001) => Some(Weekday::Mon),
Self(0b000_0010) => Some(Weekday::Tue),
Self(0b000_0100) => Some(Weekday::Wed),
Self(0b000_1000) => Some(Weekday::Thu),
Self(0b001_0000) => Some(Weekday::Fri),
Self(0b010_0000) => Some(Weekday::Sat),
Self(0b100_0000) => Some(Weekday::Sun),
_ => None,
}
}
/// Adds a day to the collection.
///
/// Returns `true` if the day was new to the collection.
///
/// # Example
/// ```
/// # use chrono::WeekdaySet;
/// use chrono::Weekday::*;
/// let mut weekdays = WeekdaySet::single(Mon);
/// assert!(weekdays.insert(Tue));
/// assert!(!weekdays.insert(Tue));
/// ```
pub fn insert(&mut self, day: Weekday) -> bool {
if self.contains(day) {
return false;
}
self.0 |= Self::single(day).0;
true
}
/// Removes a day from the collection.
///
/// Returns `true` if the collection did contain the day.
///
/// # Example
/// ```
/// # use chrono::WeekdaySet;
/// use chrono::Weekday::*;
/// let mut weekdays = WeekdaySet::single(Mon);
/// assert!(weekdays.remove(Mon));
/// assert!(!weekdays.remove(Mon));
/// ```
pub fn remove(&mut self, day: Weekday) -> bool {
if self.contains(day) {
self.0 &= !Self::single(day).0;
return true;
}
false
}
/// Returns `true` if `other` contains all days in `self`.
///
/// # Example
/// ```
/// # use chrono::WeekdaySet;
/// use chrono::Weekday::*;
/// assert!(WeekdaySet::single(Mon).is_subset(WeekdaySet::ALL));
/// assert!(!WeekdaySet::single(Mon).is_subset(WeekdaySet::EMPTY));
/// assert!(WeekdaySet::EMPTY.is_subset(WeekdaySet::single(Mon)));
/// ```
pub const fn is_subset(self, other: Self) -> bool {
self.intersection(other).0 == self.0
}
/// Returns days that are in both `self` and `other`.
///
/// # Example
/// ```
/// # use chrono::WeekdaySet;
/// use chrono::Weekday::*;
/// assert_eq!(WeekdaySet::single(Mon).intersection(WeekdaySet::single(Mon)), WeekdaySet::single(Mon));
/// assert_eq!(WeekdaySet::single(Mon).intersection(WeekdaySet::single(Tue)), WeekdaySet::EMPTY);
/// assert_eq!(WeekdaySet::ALL.intersection(WeekdaySet::single(Mon)), WeekdaySet::single(Mon));
/// assert_eq!(WeekdaySet::ALL.intersection(WeekdaySet::EMPTY), WeekdaySet::EMPTY);
/// ```
pub const fn intersection(self, other: Self) -> Self {
Self(self.0 & other.0)
}
/// Returns days that are in either `self` or `other`.
///
/// # Example
/// ```
/// # use chrono::WeekdaySet;
/// use chrono::Weekday::*;
/// assert_eq!(WeekdaySet::single(Mon).union(WeekdaySet::single(Mon)), WeekdaySet::single(Mon));
/// assert_eq!(WeekdaySet::single(Mon).union(WeekdaySet::single(Tue)), WeekdaySet::from_array([Mon, Tue]));
/// assert_eq!(WeekdaySet::ALL.union(WeekdaySet::single(Mon)), WeekdaySet::ALL);
/// assert_eq!(WeekdaySet::ALL.union(WeekdaySet::EMPTY), WeekdaySet::ALL);
/// ```
pub const fn union(self, other: Self) -> Self {
Self(self.0 | other.0)
}
/// Returns days that are in `self` or `other` but not in both.
///
/// # Example
/// ```
/// # use chrono::WeekdaySet;
/// use chrono::Weekday::*;
/// assert_eq!(WeekdaySet::single(Mon).symmetric_difference(WeekdaySet::single(Mon)), WeekdaySet::EMPTY);
/// assert_eq!(WeekdaySet::single(Mon).symmetric_difference(WeekdaySet::single(Tue)), WeekdaySet::from_array([Mon, Tue]));
/// assert_eq!(
/// WeekdaySet::ALL.symmetric_difference(WeekdaySet::single(Mon)),
/// WeekdaySet::from_array([Tue, Wed, Thu, Fri, Sat, Sun]),
/// );
/// assert_eq!(WeekdaySet::ALL.symmetric_difference(WeekdaySet::EMPTY), WeekdaySet::ALL);
/// ```
pub const fn symmetric_difference(self, other: Self) -> Self {
Self(self.0 ^ other.0)
}
/// Returns days that are in `self` but not in `other`.
///
/// # Example
/// ```
/// # use chrono::WeekdaySet;
/// use chrono::Weekday::*;
/// assert_eq!(WeekdaySet::single(Mon).difference(WeekdaySet::single(Mon)), WeekdaySet::EMPTY);
/// assert_eq!(WeekdaySet::single(Mon).difference(WeekdaySet::single(Tue)), WeekdaySet::single(Mon));
/// assert_eq!(WeekdaySet::EMPTY.difference(WeekdaySet::single(Mon)), WeekdaySet::EMPTY);
/// ```
pub const fn difference(self, other: Self) -> Self {
Self(self.0 & !other.0)
}
/// Get the first day in the collection, starting from Monday.
///
/// Returns `None` if the collection is empty.
///
/// # Example
/// ```
/// # use chrono::WeekdaySet;
/// use chrono::Weekday::*;
/// assert_eq!(WeekdaySet::single(Mon).first(), Some(Mon));
/// assert_eq!(WeekdaySet::single(Tue).first(), Some(Tue));
/// assert_eq!(WeekdaySet::ALL.first(), Some(Mon));
/// assert_eq!(WeekdaySet::EMPTY.first(), None);
/// ```
pub const fn first(self) -> Option<Weekday> {
if self.is_empty() {
return None;
}
// Find the first non-zero bit.
let bit = 1 << self.0.trailing_zeros();
Self(bit).single_day()
}
/// Get the last day in the collection, starting from Sunday.
///
/// Returns `None` if the collection is empty.
///
/// # Example
/// ```
/// # use chrono::WeekdaySet;
/// use chrono::Weekday::*;
/// assert_eq!(WeekdaySet::single(Mon).last(), Some(Mon));
/// assert_eq!(WeekdaySet::single(Sun).last(), Some(Sun));
/// assert_eq!(WeekdaySet::from_array([Mon, Tue]).last(), Some(Tue));
/// assert_eq!(WeekdaySet::EMPTY.last(), None);
/// ```
pub fn last(self) -> Option<Weekday> {
if self.is_empty() {
return None;
}
// Find the last non-zero bit.
let bit = 1 << (7 - self.0.leading_zeros());
Self(bit).single_day()
}
/// Split the collection in two at the given day.
///
/// Returns a tuple `(before, after)`. `before` contains all days starting from Monday
/// up to but __not__ including `weekday`. `after` contains all days starting from `weekday`
/// up to and including Sunday.
const fn split_at(self, weekday: Weekday) -> (Self, Self) {
let days_after = 0b1000_0000 - Self::single(weekday).0;
let days_before = days_after ^ 0b0111_1111;
(Self(self.0 & days_before), Self(self.0 & days_after))
}
/// Iterate over the [`Weekday`]s in the collection starting from a given day.
///
/// Wraps around from Sunday to Monday if necessary.
///
/// # Example
/// ```
/// # use chrono::WeekdaySet;
/// use chrono::Weekday::*;
/// let weekdays = WeekdaySet::from_array([Mon, Wed, Fri]);
/// let mut iter = weekdays.iter(Wed);
/// assert_eq!(iter.next(), Some(Wed));
/// assert_eq!(iter.next(), Some(Fri));
/// assert_eq!(iter.next(), Some(Mon));
/// assert_eq!(iter.next(), None);
/// ```
pub const fn iter(self, start: Weekday) -> WeekdaySetIter {
WeekdaySetIter { days: self, start }
}
/// Returns `true` if the collection contains the given day.
///
/// # Example
/// ```
/// # use chrono::WeekdaySet;
/// use chrono::Weekday::*;
/// assert!(WeekdaySet::single(Mon).contains(Mon));
/// assert!(WeekdaySet::from_array([Mon, Tue]).contains(Tue));
/// assert!(!WeekdaySet::single(Mon).contains(Tue));
/// ```
pub const fn contains(self, day: Weekday) -> bool {
self.0 & Self::single(day).0 != 0
}
/// Returns `true` if the collection is empty.
///
/// # Example
/// ```
/// # use chrono::{Weekday, WeekdaySet};
/// assert!(WeekdaySet::EMPTY.is_empty());
/// assert!(!WeekdaySet::single(Weekday::Mon).is_empty());
/// ```
pub const fn is_empty(self) -> bool {
self.len() == 0
}
/// Returns the number of days in the collection.
///
/// # Example
/// ```
/// # use chrono::WeekdaySet;
/// use chrono::Weekday::*;
/// assert_eq!(WeekdaySet::single(Mon).len(), 1);
/// assert_eq!(WeekdaySet::from_array([Mon, Wed, Fri]).len(), 3);
/// assert_eq!(WeekdaySet::ALL.len(), 7);
/// ```
pub const fn len(self) -> u8 {
self.0.count_ones() as u8
}
/// An empty `WeekdaySet`.
pub const EMPTY: Self = Self(0b000_0000);
/// A `WeekdaySet` containing all seven `Weekday`s.
pub const ALL: Self = Self(0b111_1111);
}
/// Print the underlying bitmask, padded to 7 bits.
///
/// # Example
/// ```
/// # use chrono::WeekdaySet;
/// use chrono::Weekday::*;
/// assert_eq!(format!("{:?}", WeekdaySet::single(Mon)), "WeekdaySet(0000001)");
/// assert_eq!(format!("{:?}", WeekdaySet::single(Tue)), "WeekdaySet(0000010)");
/// assert_eq!(format!("{:?}", WeekdaySet::ALL), "WeekdaySet(1111111)");
/// ```
impl Debug for WeekdaySet {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "WeekdaySet({:0>7b})", self.0)
}
}
/// An iterator over a collection of weekdays, starting from a given day.
///
/// See [`WeekdaySet::iter()`].
#[derive(Debug, Clone)]
pub struct WeekdaySetIter {
days: WeekdaySet,
start: Weekday,
}
impl Iterator for WeekdaySetIter {
type Item = Weekday;
fn next(&mut self) -> Option<Self::Item> {
if self.days.is_empty() {
return None;
}
// Split the collection in two at `start`.
// Look for the first day among the days after `start` first, including `start` itself.
// If there are no days after `start`, look for the first day among the days before `start`.
let (before, after) = self.days.split_at(self.start);
let days = if after.is_empty() { before } else { after };
let next = days.first().expect("the collection is not empty");
self.days.remove(next);
Some(next)
}
}
impl DoubleEndedIterator for WeekdaySetIter {
fn next_back(&mut self) -> Option<Self::Item> {
if self.days.is_empty() {
return None;
}
// Split the collection in two at `start`.
// Look for the last day among the days before `start` first, NOT including `start` itself.
// If there are no days before `start`, look for the last day among the days after `start`.
let (before, after) = self.days.split_at(self.start);
let days = if before.is_empty() { after } else { before };
let next_back = days.last().expect("the collection is not empty");
self.days.remove(next_back);
Some(next_back)
}
}
impl ExactSizeIterator for WeekdaySetIter {
fn len(&self) -> usize {
self.days.len().into()
}
}
impl FusedIterator for WeekdaySetIter {}
/// Print the collection as a slice-like list of weekdays.
///
/// # Example
/// ```
/// # use chrono::WeekdaySet;
/// use chrono::Weekday::*;
/// assert_eq!("[]", WeekdaySet::EMPTY.to_string());
/// assert_eq!("[Mon]", WeekdaySet::single(Mon).to_string());
/// assert_eq!("[Mon, Fri, Sun]", WeekdaySet::from_array([Mon, Fri, Sun]).to_string());
/// ```
impl fmt::Display for WeekdaySet {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "[")?;
let mut iter = self.iter(Weekday::Mon);
if let Some(first) = iter.next() {
write!(f, "{first}")?;
}
for weekday in iter {
write!(f, ", {weekday}")?;
}
write!(f, "]")
}
}
impl FromIterator<Weekday> for WeekdaySet {
fn from_iter<T: IntoIterator<Item = Weekday>>(iter: T) -> Self {
iter.into_iter().map(Self::single).fold(Self::EMPTY, Self::union)
}
}
#[cfg(test)]
mod tests {
use crate::Weekday;
use super::WeekdaySet;
impl WeekdaySet {
/// Iterate over all 128 possible sets, from `EMPTY` to `ALL`.
fn iter_all() -> impl Iterator<Item = Self> {
(0b0000_0000..0b1000_0000).map(Self)
}
}
/// Panics if the 8-th bit of `self` is not 0.
fn assert_8th_bit_invariant(days: WeekdaySet) {
assert!(days.0 & 0b1000_0000 == 0, "the 8-th bit of {days:?} is not 0");
}
#[test]
fn debug_prints_8th_bit_if_not_zero() {
assert_eq!(format!("{:?}", WeekdaySet(0b1000_0000)), "WeekdaySet(10000000)");
}
#[test]
fn bitwise_set_operations_preserve_8th_bit_invariant() {
for set1 in WeekdaySet::iter_all() {
for set2 in WeekdaySet::iter_all() {
assert_8th_bit_invariant(set1.union(set2));
assert_8th_bit_invariant(set1.intersection(set2));
assert_8th_bit_invariant(set1.symmetric_difference(set2));
}
}
}
/// Test `split_at` on all possible arguments.
#[test]
fn split_at_is_equivalent_to_iterating() {
use Weekday::*;
// `split_at()` is used in `iter()`, so we must not iterate
// over all days with `WeekdaySet::ALL.iter(Mon)`.
const WEEK: [Weekday; 7] = [Mon, Tue, Wed, Thu, Fri, Sat, Sun];
for weekdays in WeekdaySet::iter_all() {
for split_day in WEEK {
let expected_before: WeekdaySet = WEEK
.into_iter()
.take_while(|&day| day != split_day)
.filter(|&day| weekdays.contains(day))
.collect();
let expected_after: WeekdaySet = WEEK
.into_iter()
.skip_while(|&day| day != split_day)
.filter(|&day| weekdays.contains(day))
.collect();
assert_eq!(
(expected_before, expected_after),
weekdays.split_at(split_day),
"split_at({split_day}) failed for {weekdays}",
);
}
}
}
}