feat: add display UI with splash screen, dashboard, and RSSI sparkline
Implement full SSD1306 OLED display support: - Splash screen on boot (1.5s) with project name, version, board - Dashboard showing radio state, config, counters, RSSI/SNR - RSSI history sparkline (64-sample ring buffer, 2px-wide bars) - DisplayOn/DisplayOff host commands routed via dedicated channel - Pure render functions against generic DrawTarget (no hardware coupling) - Display on/off control bypasses radio task entirely Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
17624 files changed
+4564928
-38
No files matched your search
Vendored
+677
@@ -0,0 +1,677 @@
|
||||
// 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]
|
||||
#[track_caller]
|
||||
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]
|
||||
#[track_caller]
|
||||
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]
|
||||
#[track_caller]
|
||||
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]
|
||||
#[track_caller]
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "defmt")]
|
||||
impl<Tz: TimeZone> defmt::Format for Date<Tz>
|
||||
where
|
||||
Tz::Offset: defmt::Format,
|
||||
{
|
||||
fn format(&self, fmt: defmt::Formatter) {
|
||||
defmt::write!(fmt, "{}{}", self.naive_local(), self.offset);
|
||||
}
|
||||
}
|
||||
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
Vendored
+2016
File diff suppressed because it is too large.
Load diff
Vendored
+1351
File diff suppressed because it is too large.
Load diff
Vendored
+1912
File diff suppressed because it is too large.
Load diff
+951
@@ -0,0 +1,951 @@
|
||||
// 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 + ?Sized)) -> 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 + ?Sized),
|
||||
spec: &Numeric,
|
||||
pad: Pad,
|
||||
) -> fmt::Result {
|
||||
use self::Numeric::*;
|
||||
|
||||
fn write_one(w: &mut (impl Write + ?Sized), v: u8) -> fmt::Result {
|
||||
w.write_char((b'0' + v) as char)
|
||||
}
|
||||
|
||||
fn write_two(w: &mut (impl Write + ?Sized), 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 + ?Sized), 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 + ?Sized),
|
||||
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 + ?Sized), 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 + ?Sized), 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)]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
#[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 + ?Sized),
|
||||
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 + ?Sized),
|
||||
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 + ?Sized), 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"],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Vendored
+103
@@ -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::*;
|
||||
Vendored
+593
@@ -0,0 +1,593 @@
|
||||
// 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;
|
||||
#[cfg(all(feature = "core-error", not(feature = "std")))]
|
||||
use core::error::Error;
|
||||
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)]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
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)]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
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>")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "defmt")]
|
||||
impl defmt::Format for InternalNumeric {
|
||||
fn format(&self, f: defmt::Formatter) {
|
||||
defmt::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)]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
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)]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
pub struct InternalFixed {
|
||||
val: InternalInternal,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
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)]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
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)]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
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)]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
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,
|
||||
}
|
||||
|
||||
#[cfg(feature = "defmt")]
|
||||
impl<'a> defmt::Format for Item<'a> {
|
||||
fn format(&self, f: defmt::Formatter) {
|
||||
match self {
|
||||
Item::Literal(v) => defmt::write!(f, "Literal {{ {} }}", v),
|
||||
#[cfg(feature = "alloc")]
|
||||
Item::OwnedLiteral(_) => {}
|
||||
Item::Space(v) => defmt::write!(f, "Space {{ {} }}", v),
|
||||
#[cfg(feature = "alloc")]
|
||||
Item::OwnedSpace(_) => {}
|
||||
Item::Numeric(u, v) => defmt::write!(f, "Numeric {{ {}, {} }}", u, v),
|
||||
Item::Fixed(v) => defmt::write!(f, "Fixed {{ {} }}", v),
|
||||
Item::Error => defmt::write!(f, "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)]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
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)]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
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(any(feature = "core-error", 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: () })
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+1941
File diff suppressed because it is too large.
Load diff
Vendored
+1913
File diff suppressed because it is too large.
Load diff
Vendored
+432
@@ -0,0 +1,432 @@
|
||||
// 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, u32)> {
|
||||
// record the number of digits consumed for later scaling.
|
||||
let origlen = s.len();
|
||||
let (s, v) = number(s, 1, 9)?;
|
||||
let v = u32::try_from(v).expect("999,999,999 should fit u32");
|
||||
let consumed = origlen - s.len();
|
||||
|
||||
// scale the number accordingly.
|
||||
const SCALE: [u32; 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("2Ù").unwrap(), ("Ù", 200000000));
|
||||
assert_eq!(nanosecond("8").unwrap(), ("", 800000000));
|
||||
}
|
||||
}
|
||||
Vendored
+1217
File diff suppressed because it is too large.
Load diff
Vendored
+743
@@ -0,0 +1,743 @@
|
||||
//! # 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_secs(1_500_000_000).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))))]
|
||||
#![warn(unreachable_pub)]
|
||||
#![warn(clippy::tests_outside_test_module)]
|
||||
#![cfg_attr(not(any(feature = "std", test)), no_std)]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
extern crate alloc;
|
||||
|
||||
mod time_delta;
|
||||
#[doc(no_inline)]
|
||||
#[cfg(any(feature = "std", feature = "core-error"))]
|
||||
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", 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 = "defmt")]
|
||||
impl defmt::Format for OutOfRange {
|
||||
fn format(&self, fmt: defmt::Formatter) {
|
||||
defmt::write!(fmt, "out of range");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for OutOfRange {}
|
||||
|
||||
#[cfg(all(not(feature = "std"), feature = "core-error"))]
|
||||
impl core::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);
|
||||
}
|
||||
}
|
||||
Vendored
+484
@@ -0,0 +1,484 @@
|
||||
use core::fmt;
|
||||
|
||||
#[cfg(any(feature = "rkyv", 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", feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"),
|
||||
derive(Archive, Deserialize, Serialize),
|
||||
archive(compare(PartialEq, PartialOrd)),
|
||||
archive_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))]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
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))]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
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 {}
|
||||
|
||||
#[cfg(all(not(feature = "std"), feature = "core-error"))]
|
||||
impl core::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 = "defmt")]
|
||||
impl defmt::Format for ParseMonthError {
|
||||
fn format(&self, fmt: defmt::Formatter) {
|
||||
defmt::write!(fmt, "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));
|
||||
}
|
||||
}
|
||||
Vendored
+2634
File diff suppressed because it is too large.
Load diff
+920
@@ -0,0 +1,920 @@
|
||||
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_epoch_days() {
|
||||
let from_epoch_days = NaiveDate::from_epoch_days;
|
||||
assert_eq!(from_epoch_days(-719_162), Some(NaiveDate::from_ymd_opt(1, 1, 1).unwrap()));
|
||||
assert_eq!(from_epoch_days(0), Some(NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()));
|
||||
assert_eq!(from_epoch_days(1), Some(NaiveDate::from_ymd_opt(1970, 1, 2).unwrap()));
|
||||
assert_eq!(from_epoch_days(2), Some(NaiveDate::from_ymd_opt(1970, 1, 3).unwrap()));
|
||||
assert_eq!(from_epoch_days(30), Some(NaiveDate::from_ymd_opt(1970, 1, 31).unwrap()));
|
||||
assert_eq!(from_epoch_days(31), Some(NaiveDate::from_ymd_opt(1970, 2, 1).unwrap()));
|
||||
assert_eq!(from_epoch_days(58), Some(NaiveDate::from_ymd_opt(1970, 2, 28).unwrap()));
|
||||
assert_eq!(from_epoch_days(59), Some(NaiveDate::from_ymd_opt(1970, 3, 1).unwrap()));
|
||||
assert_eq!(from_epoch_days(364), Some(NaiveDate::from_ymd_opt(1970, 12, 31).unwrap()));
|
||||
assert_eq!(from_epoch_days(365), Some(NaiveDate::from_ymd_opt(1971, 1, 1).unwrap()));
|
||||
assert_eq!(from_epoch_days(365 * 2), Some(NaiveDate::from_ymd_opt(1972, 1, 1).unwrap()));
|
||||
assert_eq!(from_epoch_days(365 * 3 + 1), Some(NaiveDate::from_ymd_opt(1973, 1, 1).unwrap()));
|
||||
assert_eq!(from_epoch_days(365 * 4 + 1), Some(NaiveDate::from_ymd_opt(1974, 1, 1).unwrap()));
|
||||
assert_eq!(from_epoch_days(13036), Some(NaiveDate::from_ymd_opt(2005, 9, 10).unwrap()));
|
||||
assert_eq!(from_epoch_days(-365), Some(NaiveDate::from_ymd_opt(1969, 1, 1).unwrap()));
|
||||
assert_eq!(from_epoch_days(-366), Some(NaiveDate::from_ymd_opt(1968, 12, 31).unwrap()));
|
||||
|
||||
for days in (-9999..10001).map(|x| x * 100) {
|
||||
assert_eq!(from_epoch_days(days).map(|d| d.to_epoch_days()), Some(days));
|
||||
}
|
||||
|
||||
assert_eq!(from_epoch_days(NaiveDate::MIN.to_epoch_days()), Some(NaiveDate::MIN));
|
||||
assert_eq!(from_epoch_days(NaiveDate::MIN.to_epoch_days() - 1), None);
|
||||
assert_eq!(from_epoch_days(NaiveDate::MAX.to_epoch_days()), Some(NaiveDate::MAX));
|
||||
assert_eq!(from_epoch_days(NaiveDate::MAX.to_epoch_days() + 1), None);
|
||||
|
||||
assert_eq!(from_epoch_days(i32::MIN), None);
|
||||
assert_eq!(from_epoch_days(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_to_epoch_days() {
|
||||
assert_eq!(NaiveDate::from_ymd_opt(1970, 1, 1).unwrap().to_epoch_days(), 0);
|
||||
|
||||
for year in -9999..10001 {
|
||||
assert_eq!(
|
||||
NaiveDate::from_ymd_opt(year, 1, 1).unwrap().to_epoch_days(),
|
||||
NaiveDate::from_ymd_opt(year - 1, 12, 31).unwrap().to_epoch_days() + 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;
|
||||
+2171
File diff suppressed because it is too large.
Load diff
+1304
File diff suppressed because it is too large.
Load diff
+405
@@ -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);
|
||||
}
|
||||
Vendored
+591
@@ -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());
|
||||
}
|
||||
}
|
||||
Vendored
+248
@@ -0,0 +1,248 @@
|
||||
// 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", 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", feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"),
|
||||
derive(Archive, Deserialize, Serialize),
|
||||
archive(compare(PartialEq, PartialOrd)),
|
||||
archive_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(feature = "defmt")]
|
||||
impl defmt::Format for IsoWeek {
|
||||
fn format(&self, fmt: defmt::Formatter) {
|
||||
let year = self.year();
|
||||
let week = self.week();
|
||||
if (0..=9999).contains(&year) {
|
||||
defmt::write!(fmt, "{:04}-W{:02}", year, week)
|
||||
} else {
|
||||
// ISO 8601 requires the explicit sign for out-of-range years
|
||||
let sign = ['+', '-'][(year < 0) as usize];
|
||||
defmt::write!(fmt, "{}{:05}-W{:02}", sign, year.abs(), week)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
Vendored
+340
@@ -0,0 +1,340 @@
|
||||
//! 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)]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
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]
|
||||
#[track_caller]
|
||||
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]
|
||||
#[track_caller]
|
||||
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]
|
||||
#[track_caller]
|
||||
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)]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
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);
|
||||
}
|
||||
}
|
||||
Vendored
+1668
File diff suppressed because it is too large.
Load diff
+143
@@ -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);
|
||||
}
|
||||
}
|
||||
+390
@@ -0,0 +1,390 @@
|
||||
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 `{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::<NaiveTime>() {
|
||||
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
|
||||
"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);
|
||||
}
|
||||
Vendored
+253
@@ -0,0 +1,253 @@
|
||||
// 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", 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", feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"),
|
||||
derive(Archive, Deserialize, Serialize),
|
||||
archive(compare(PartialEq)),
|
||||
archive_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(feature = "defmt")]
|
||||
impl defmt::Format for FixedOffset {
|
||||
fn format(&self, f: defmt::Formatter) {
|
||||
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 {
|
||||
defmt::write!(f, "{}{:02}:{:02}", sign, hour, min)
|
||||
} else {
|
||||
defmt::write!(f, "{}{:02}:{:02}:{:02}", sign, hour, min, sec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
+543
@@ -0,0 +1,543 @@
|
||||
// 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", 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", feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"),
|
||||
derive(Archive, Deserialize, Serialize),
|
||||
archive(compare(PartialEq)),
|
||||
archive_attr(derive(Clone, Copy, Debug))
|
||||
)]
|
||||
#[cfg_attr(feature = "rkyv-validation", archive(check_bytes))]
|
||||
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
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.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
+267
@@ -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());
|
||||
}
|
||||
}
|
||||
+116
@@ -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];
|
||||
+348
@@ -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,
|
||||
}
|
||||
+1037
File diff suppressed because it is too large.
Load diff
+948
@@ -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(())
|
||||
}
|
||||
}
|
||||
+171
@@ -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()))
|
||||
}
|
||||
}
|
||||
+59
@@ -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() }
|
||||
}
|
||||
}
|
||||
+293
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+715
@@ -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)]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
/// 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 {e:?} instead of an okay answer"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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");
|
||||
}
|
||||
}
|
||||
Vendored
+159
@@ -0,0 +1,159 @@
|
||||
// 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", 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", feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"),
|
||||
derive(Archive, Deserialize, Serialize),
|
||||
archive(compare(PartialEq)),
|
||||
archive_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")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "defmt")]
|
||||
impl defmt::Format for Utc {
|
||||
fn format(&self, fmt: defmt::Formatter) {
|
||||
defmt::write!(fmt, "Z");
|
||||
}
|
||||
}
|
||||
Vendored
+1095
File diff suppressed because it is too large.
Load diff
Vendored
+1456
File diff suppressed because it is too large.
Load diff
Vendored
+409
@@ -0,0 +1,409 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Vendored
+419
@@ -0,0 +1,419 @@
|
||||
use core::fmt;
|
||||
|
||||
#[cfg(any(feature = "rkyv", 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", feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"),
|
||||
derive(Archive, Deserialize, Serialize),
|
||||
archive(compare(PartialEq)),
|
||||
archive_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))]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
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(all(not(feature = "std"), feature = "core-error"))]
|
||||
impl core::error::Error for ParseWeekdayError {}
|
||||
|
||||
#[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 {{ .. }}")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "defmt")]
|
||||
impl defmt::Format for ParseWeekdayError {
|
||||
fn format(&self, fmt: defmt::Formatter) {
|
||||
defmt::write!(fmt, "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);
|
||||
}
|
||||
}
|
||||
Vendored
+500
@@ -0,0 +1,500 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "defmt")]
|
||||
impl defmt::Format for WeekdaySet {
|
||||
fn format(&self, f: defmt::Formatter<'_>) {
|
||||
defmt::write!(
|
||||
f,
|
||||
"WeekdaySet({}{}{}{}{}{}{})",
|
||||
0x1 & (self.0 >> 6),
|
||||
0x1 & (self.0 >> 5),
|
||||
0x1 & (self.0 >> 4),
|
||||
0x1 & (self.0 >> 3),
|
||||
0x1 & (self.0 >> 2),
|
||||
0x1 & (self.0 >> 1),
|
||||
0x1 & (self.0 >> 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}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user