use core::time::Duration as UnsignedDuration;
use crate::{
civil::{DateTime, Era, ISOWeekDate, Time, Weekday},
duration::{Duration, SDuration},
error::{civil::Error as E, unit::UnitConfigError, Error, ErrorContext},
fmt::{
self,
temporal::{DEFAULT_DATETIME_PARSER, DEFAULT_DATETIME_PRINTER},
},
shared::util::itime::{self, IDate, IEpochDay},
tz::TimeZone,
util::{b, constant},
RoundMode, SignedDuration, Span, SpanRound, Unit, Zoned,
};
#[derive(Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub struct Date {
year: i16,
month: i8,
day: i8,
}
impl Date {
pub const MIN: Date = Date::constant(-9999, 1, 1);
pub const MAX: Date = Date::constant(9999, 12, 31);
pub const ZERO: Date = Date::constant(0, 1, 1);
#[inline]
pub fn new(year: i16, month: i8, day: i8) -> Result<Date, Error> {
let year = b::Year::check(year)?;
let month = b::Month::check(month)?;
if day < 1 || day > 28 && day > itime::days_in_month(year, month) {
return Err(Error::itime_range(
crate::shared::util::itime::RangeError::DateInvalidDays {
year,
month,
},
));
}
Ok(Date::new_unchecked(year, month, day))
}
#[inline]
fn new_constrain(year: i16, month: i8, day: i8) -> Result<Date, Error> {
let year = b::Year::check(year)?;
let month = b::Month::check(month)?;
let day = if day < 1 {
return Err(b::Day::error().into());
} else if day > 28 {
day.min(itime::days_in_month(year, month))
} else {
day
};
Ok(Date::new_unchecked(year, month, day))
}
#[inline]
const fn new_unchecked(year: i16, month: i8, day: i8) -> Date {
debug_assert!(b::Year::checkc(year as i64).is_ok());
debug_assert!(b::Month::checkc(month as i64).is_ok());
debug_assert!(b::Day::checkc(day as i64).is_ok());
debug_assert!(day <= itime::days_in_month(year, month));
Date { year, month, day }
}
#[inline]
pub const fn constant(year: i16, month: i8, day: i8) -> Date {
let year =
constant::unwrapr!(b::Year::checkc(year as i64), "invalid year");
let month = constant::unwrapr!(
b::Month::checkc(month as i64),
"invalid month"
);
if day > itime::days_in_month(year, month) {
panic!("invalid month/day combination");
}
Date::new_unchecked(year, month, day)
}
#[inline]
pub fn from_iso_week_date(weekdate: ISOWeekDate) -> Date {
let mut days = iso_week_start_from_year(weekdate.year());
let week = i32::from(weekdate.week());
let weekday = i32::from(weekdate.weekday().to_monday_zero_offset());
days += (week - 1) * 7;
days += weekday;
Date::from_unix_epoch_day(days)
}
#[inline]
pub fn with(self) -> DateWith {
DateWith::new(self)
}
#[inline]
pub fn year(self) -> i16 {
self.year
}
#[inline]
pub fn era_year(self) -> (i16, Era) {
let year = self.year();
if year >= 1 {
(year, Era::CE)
} else {
let era_year = -year + 1;
(era_year, Era::BCE)
}
}
#[inline]
pub fn month(self) -> i8 {
self.month
}
#[inline]
pub fn day(self) -> i8 {
self.day
}
#[inline]
pub fn weekday(self) -> Weekday {
Weekday::from_iweekday(self.to_idate_const().weekday())
}
#[inline]
pub fn day_of_year(self) -> i16 {
static DAYS_BY_MONTH_NO_LEAP: [i16; 14] =
[0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365];
static DAYS_BY_MONTH_LEAP: [i16; 14] =
[0, 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366];
static TABLES: [[i16; 14]; 2] =
[DAYS_BY_MONTH_NO_LEAP, DAYS_BY_MONTH_LEAP];
TABLES[self.in_leap_year() as usize][self.month() as usize]
+ i16::from(self.day())
}
#[inline]
pub fn day_of_year_no_leap(self) -> Option<i16> {
let mut days = self.day_of_year();
if self.in_leap_year() {
if days == 60 {
return None;
} else if days > 60 {
days -= 1;
}
}
Some(days)
}
#[inline]
pub fn first_of_month(self) -> Date {
Date::new_unchecked(self.year(), self.month(), 1)
}
#[inline]
pub fn last_of_month(self) -> Date {
Date::new_unchecked(self.year(), self.month(), self.days_in_month())
}
#[inline]
pub fn days_in_month(self) -> i8 {
itime::days_in_month(self.year(), self.month())
}
#[inline]
pub fn first_of_year(self) -> Date {
Date::new_unchecked(self.year(), 1, 1)
}
#[inline]
pub fn last_of_year(self) -> Date {
Date::new_unchecked(self.year(), 12, 31)
}
#[inline]
pub fn days_in_year(self) -> i16 {
if self.in_leap_year() {
366
} else {
365
}
}
#[inline]
pub fn in_leap_year(self) -> bool {
itime::is_leap_year(self.year())
}
#[inline]
pub fn tomorrow(self) -> Result<Date, Error> {
if self.day() >= 28 && self.day() == self.days_in_month() {
if self.month() == 12 {
let year = b::Year::checked_add(self.year(), 1)?;
return Ok(Date::new_unchecked(year, 1, 1));
}
return Ok(Date::new_unchecked(self.year(), self.month() + 1, 1));
}
Ok(Date::new_unchecked(self.year(), self.month(), self.day() + 1))
}
#[inline]
pub fn yesterday(self) -> Result<Date, Error> {
if self.day() == 1 {
if self.month() == 1 {
let year = b::Year::checked_sub(self.year(), 1)?;
return Ok(Date::new_unchecked(year, 12, 31));
}
let month = self.month() - 1;
return Ok(Date::new_unchecked(
self.year(),
month,
itime::days_in_month(self.year(), month),
));
}
Ok(Date::new_unchecked(self.year(), self.month(), self.day() - 1))
}
#[inline]
pub fn nth_weekday_of_month(
self,
nth: i8,
weekday: Weekday,
) -> Result<Date, Error> {
let weekday = weekday.to_iweekday();
let idate = self.to_idate_const();
Ok(Date::from_idate_const(
idate
.nth_weekday_of_month(nth, weekday)
.map_err(Error::itime_range)?,
))
}
#[inline]
pub fn nth_weekday(
self,
nth: i32,
weekday: Weekday,
) -> Result<Date, Error> {
let nth = b::NthWeekday::check(nth)?;
if nth == 0 {
Err(b::NthWeekday::error().into())
} else if nth > 0 {
let weekday_diff = i32::from(weekday.since(self.weekday().next()));
let diff = (nth - 1) * 7 + weekday_diff;
let start = self.tomorrow()?.to_unix_epoch_day();
let end = b::UnixEpochDays::checked_add(start, diff)?;
Ok(Date::from_unix_epoch_day(end))
} else {
let weekday_diff =
i32::from(self.weekday().previous().since(weekday));
let nth = nth.abs();
let diff = (nth - 1) * 7 + weekday_diff;
let start = self.yesterday()?.to_unix_epoch_day();
let end = b::UnixEpochDays::checked_sub(start, diff)?;
Ok(Date::from_unix_epoch_day(end))
}
}
#[inline]
pub fn iso_week_date(self) -> ISOWeekDate {
let days = self.to_unix_epoch_day();
let year = self.year();
let mut week_start = iso_week_start_from_year(year);
if days < week_start {
week_start = iso_week_start_from_year(year - 1);
} else if year < b::Year::MAX {
let next_year_week_start = iso_week_start_from_year(year + 1);
if days >= next_year_week_start {
week_start = next_year_week_start;
}
}
let weekday =
Weekday::from_iweekday(IEpochDay { epoch_day: days }.weekday());
let week = i8::try_from(((days - week_start) / 7) + 1).unwrap();
let unix_epoch_day =
week_start + i32::from(Weekday::Thursday.since(Weekday::Monday));
let year = Date::from_unix_epoch_day(unix_epoch_day).year();
ISOWeekDate::new(year, week, weekday)
.expect("all Dates infallibly convert to ISOWeekDates")
}
#[inline]
pub fn in_tz(self, time_zone_name: &str) -> Result<Zoned, Error> {
let tz = crate::tz::db().get(time_zone_name)?;
self.to_zoned(tz)
}
#[inline]
pub fn to_zoned(self, tz: TimeZone) -> Result<Zoned, Error> {
DateTime::from(self).to_zoned(tz)
}
#[inline]
pub const fn to_datetime(self, time: Time) -> DateTime {
DateTime::from_parts(self, time)
}
#[inline]
pub const fn at(
self,
hour: i8,
minute: i8,
second: i8,
subsec_nanosecond: i32,
) -> DateTime {
DateTime::from_parts(
self,
Time::constant(hour, minute, second, subsec_nanosecond),
)
}
#[inline]
pub fn checked_add<A: Into<DateArithmetic>>(
self,
duration: A,
) -> Result<Date, Error> {
let duration: DateArithmetic = duration.into();
duration.checked_add(self)
}
#[inline]
fn checked_add_span(self, span: &Span) -> Result<Date, Error> {
if span.is_zero() {
return Ok(self);
}
if span.units().contains_only(Unit::Day) {
let days = span.get_days();
return if days == -1 {
self.yesterday()
} else if days == 1 {
self.tomorrow()
} else {
let epoch_days = self.to_unix_epoch_day();
let days = b::UnixEpochDays::checked_add(epoch_days, days)?;
Ok(Date::from_unix_epoch_day(days))
};
}
let (month, years) =
month_add_overflowing(self.month(), span.get_months());
let year = b::Year::checked_add(self.year(), years)
.and_then(|years| b::Year::checked_add(years, span.get_years()))?;
let date = Date::new_constrain(year, month, self.day())?;
let epoch_days = date.to_unix_epoch_day();
let mut days =
b::UnixEpochDays::checked_add(epoch_days, 7 * span.get_weeks())
.and_then(|days| {
b::UnixEpochDays::checked_add(days, span.get_days())
})?;
if !span.units().only_time().is_empty() {
let time_days = b::UnixEpochDays::check(
span.to_invariant_duration_time_only().as_civil_days(),
)?;
days = b::UnixEpochDays::checked_add(days, time_days)?;
}
Ok(Date::from_unix_epoch_day(days))
}
#[inline]
fn checked_add_duration(
self,
duration: SignedDuration,
) -> Result<Date, Error> {
match duration.as_civil_days() {
0 => Ok(self),
-1 => self.yesterday(),
1 => self.tomorrow(),
days => {
let days = b::UnixEpochDays::check(days)
.context(E::OverflowDaysDuration)?;
let days = b::UnixEpochDays::checked_add(
days,
self.to_unix_epoch_day(),
)?;
Ok(Date::from_unix_epoch_day(days))
}
}
}
#[inline]
pub fn checked_sub<A: Into<DateArithmetic>>(
self,
duration: A,
) -> Result<Date, Error> {
let duration: DateArithmetic = duration.into();
duration.checked_neg().and_then(|da| da.checked_add(self))
}
#[inline]
pub fn saturating_add<A: Into<DateArithmetic>>(self, duration: A) -> Date {
let duration: DateArithmetic = duration.into();
self.checked_add(duration).unwrap_or_else(|_| {
if duration.is_negative() {
Date::MIN
} else {
Date::MAX
}
})
}
#[inline]
pub fn saturating_sub<A: Into<DateArithmetic>>(self, duration: A) -> Date {
let duration: DateArithmetic = duration.into();
let Ok(duration) = duration.checked_neg() else { return Date::MIN };
self.saturating_add(duration)
}
#[inline]
pub fn until<A: Into<DateDifference>>(
self,
other: A,
) -> Result<Span, Error> {
let args: DateDifference = other.into();
let span = args.since_with_largest_unit(self)?;
if args.rounding_may_change_span() {
span.round(args.round.relative(self))
} else {
Ok(span)
}
}
#[inline]
pub fn since<A: Into<DateDifference>>(
self,
other: A,
) -> Result<Span, Error> {
let args: DateDifference = other.into();
let span = -args.since_with_largest_unit(self)?;
if args.rounding_may_change_span() {
span.round(args.round.relative(self))
} else {
Ok(span)
}
}
#[inline]
pub fn duration_until(self, other: Date) -> SignedDuration {
SignedDuration::date_until(self, other)
}
#[inline]
pub fn duration_since(self, other: Date) -> SignedDuration {
SignedDuration::date_until(other, self)
}
#[inline]
pub fn series(self, period: Span) -> DateSeries {
DateSeries { start: self, period, step: 0 }
}
}
impl Date {
#[inline]
pub fn strptime(
format: impl AsRef<[u8]>,
input: impl AsRef<[u8]>,
) -> Result<Date, Error> {
fmt::strtime::parse(format, input).and_then(|tm| tm.to_date())
}
#[inline]
pub fn strftime<'f, F: 'f + ?Sized + AsRef<[u8]>>(
&self,
format: &'f F,
) -> fmt::strtime::Display<'f> {
fmt::strtime::Display { fmt: format.as_ref(), tm: (*self).into() }
}
}
impl Date {
#[inline]
pub(crate) fn until_days(self, other: Date) -> i32 {
if self == other {
return 0;
}
let start = self.to_unix_epoch_day();
let end = other.to_unix_epoch_day();
end - start
}
#[cfg_attr(feature = "perf-inline", inline(always))]
pub(crate) fn to_unix_epoch_day(self) -> i32 {
self.to_idate_const().to_epoch_day().epoch_day
}
#[cfg_attr(feature = "perf-inline", inline(always))]
pub(crate) fn from_unix_epoch_day(epoch_day: i32) -> Date {
Date::from_idate_const(IEpochDay { epoch_day }.to_date())
}
#[inline]
pub(crate) const fn to_idate_const(self) -> IDate {
IDate { year: self.year, month: self.month, day: self.day }
}
#[inline]
pub(crate) const fn from_idate_const(idate: IDate) -> Date {
Date { year: idate.year, month: idate.month, day: idate.day }
}
}
impl Default for Date {
fn default() -> Date {
Date::ZERO
}
}
impl core::fmt::Debug for Date {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
core::fmt::Display::fmt(self, f)
}
}
impl core::fmt::Display for Date {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use crate::fmt::StdFmtWrite;
DEFAULT_DATETIME_PRINTER
.print_date(self, StdFmtWrite(f))
.map_err(|_| core::fmt::Error)
}
}
impl core::str::FromStr for Date {
type Err = Error;
fn from_str(string: &str) -> Result<Date, Error> {
DEFAULT_DATETIME_PARSER.parse_date(string)
}
}
impl From<ISOWeekDate> for Date {
#[inline]
fn from(weekdate: ISOWeekDate) -> Date {
Date::from_iso_week_date(weekdate)
}
}
impl From<DateTime> for Date {
#[inline]
fn from(dt: DateTime) -> Date {
dt.date()
}
}
impl From<Zoned> for Date {
#[inline]
fn from(zdt: Zoned) -> Date {
zdt.datetime().date()
}
}
impl<'a> From<&'a Zoned> for Date {
#[inline]
fn from(zdt: &'a Zoned) -> Date {
zdt.datetime().date()
}
}
impl core::ops::Add<Span> for Date {
type Output = Date;
#[inline]
fn add(self, rhs: Span) -> Date {
self.checked_add(rhs).expect("adding span to date overflowed")
}
}
impl core::ops::AddAssign<Span> for Date {
#[inline]
fn add_assign(&mut self, rhs: Span) {
*self = *self + rhs;
}
}
impl core::ops::Sub<Span> for Date {
type Output = Date;
#[inline]
fn sub(self, rhs: Span) -> Date {
self.checked_sub(rhs).expect("subing span to date overflowed")
}
}
impl core::ops::SubAssign<Span> for Date {
#[inline]
fn sub_assign(&mut self, rhs: Span) {
*self = *self - rhs;
}
}
impl core::ops::Sub for Date {
type Output = Span;
#[inline]
fn sub(self, rhs: Date) -> Span {
self.since(rhs).expect("since never fails when given Date")
}
}
impl core::ops::Add<SignedDuration> for Date {
type Output = Date;
#[inline]
fn add(self, rhs: SignedDuration) -> Date {
self.checked_add(rhs)
.expect("adding signed duration to date overflowed")
}
}
impl core::ops::AddAssign<SignedDuration> for Date {
#[inline]
fn add_assign(&mut self, rhs: SignedDuration) {
*self = *self + rhs;
}
}
impl core::ops::Sub<SignedDuration> for Date {
type Output = Date;
#[inline]
fn sub(self, rhs: SignedDuration) -> Date {
self.checked_sub(rhs)
.expect("subing signed duration to date overflowed")
}
}
impl core::ops::SubAssign<SignedDuration> for Date {
#[inline]
fn sub_assign(&mut self, rhs: SignedDuration) {
*self = *self - rhs;
}
}
impl core::ops::Add<UnsignedDuration> for Date {
type Output = Date;
#[inline]
fn add(self, rhs: UnsignedDuration) -> Date {
self.checked_add(rhs)
.expect("adding unsigned duration to date overflowed")
}
}
impl core::ops::AddAssign<UnsignedDuration> for Date {
#[inline]
fn add_assign(&mut self, rhs: UnsignedDuration) {
*self = *self + rhs;
}
}
impl core::ops::Sub<UnsignedDuration> for Date {
type Output = Date;
#[inline]
fn sub(self, rhs: UnsignedDuration) -> Date {
self.checked_sub(rhs)
.expect("subing unsigned duration to date overflowed")
}
}
impl core::ops::SubAssign<UnsignedDuration> for Date {
#[inline]
fn sub_assign(&mut self, rhs: UnsignedDuration) {
*self = *self - rhs;
}
}
#[cfg(feature = "serde")]
impl serde_core::Serialize for Date {
#[inline]
fn serialize<S: serde_core::Serializer>(
&self,
serializer: S,
) -> Result<S::Ok, S::Error> {
serializer.collect_str(self)
}
}
#[cfg(feature = "serde")]
impl<'de> serde_core::Deserialize<'de> for Date {
#[inline]
fn deserialize<D: serde_core::Deserializer<'de>>(
deserializer: D,
) -> Result<Date, D::Error> {
use serde_core::de;
struct DateVisitor;
impl<'de> de::Visitor<'de> for DateVisitor {
type Value = Date;
fn expecting(
&self,
f: &mut core::fmt::Formatter,
) -> core::fmt::Result {
f.write_str("a date string")
}
#[inline]
fn visit_bytes<E: de::Error>(
self,
value: &[u8],
) -> Result<Date, E> {
DEFAULT_DATETIME_PARSER
.parse_date(value)
.map_err(de::Error::custom)
}
#[inline]
fn visit_str<E: de::Error>(self, value: &str) -> Result<Date, E> {
self.visit_bytes(value.as_bytes())
}
}
deserializer.deserialize_str(DateVisitor)
}
}
#[cfg(test)]
impl quickcheck::Arbitrary for Date {
fn arbitrary(g: &mut quickcheck::Gen) -> Date {
let year = b::Year::arbitrary(g);
let month = b::Month::arbitrary(g);
let day = b::Day::arbitrary(g);
Date::new_constrain(year, month, day).unwrap()
}
fn shrink(&self) -> alloc::boxed::Box<dyn Iterator<Item = Date>> {
alloc::boxed::Box::new(
(self.year(), self.month(), self.day()).shrink().filter_map(
|(year, month, day)| {
Date::new_constrain(year, month, day).ok()
},
),
)
}
}
#[derive(Clone, Debug)]
pub struct DateSeries {
start: Date,
period: Span,
step: i64,
}
impl Iterator for DateSeries {
type Item = Date;
#[inline]
fn next(&mut self) -> Option<Date> {
let span = self.period.checked_mul(self.step).ok()?;
self.step = self.step.checked_add(1)?;
let date = self.start.checked_add(span).ok()?;
Some(date)
}
}
impl core::iter::FusedIterator for DateSeries {}
#[derive(Clone, Copy, Debug)]
pub struct DateArithmetic {
duration: Duration,
}
impl DateArithmetic {
#[inline]
fn checked_add(self, date: Date) -> Result<Date, Error> {
match self.duration.to_signed()? {
SDuration::Span(span) => date.checked_add_span(span),
SDuration::Absolute(sdur) => date.checked_add_duration(sdur),
}
}
#[inline]
fn checked_neg(self) -> Result<DateArithmetic, Error> {
let duration = self.duration.checked_neg()?;
Ok(DateArithmetic { duration })
}
#[inline]
fn is_negative(&self) -> bool {
self.duration.is_negative()
}
}
impl From<Span> for DateArithmetic {
fn from(span: Span) -> DateArithmetic {
let duration = Duration::from(span);
DateArithmetic { duration }
}
}
impl From<SignedDuration> for DateArithmetic {
fn from(sdur: SignedDuration) -> DateArithmetic {
let duration = Duration::from(sdur);
DateArithmetic { duration }
}
}
impl From<UnsignedDuration> for DateArithmetic {
fn from(udur: UnsignedDuration) -> DateArithmetic {
let duration = Duration::from(udur);
DateArithmetic { duration }
}
}
impl<'a> From<&'a Span> for DateArithmetic {
fn from(span: &'a Span) -> DateArithmetic {
DateArithmetic::from(*span)
}
}
impl<'a> From<&'a SignedDuration> for DateArithmetic {
fn from(sdur: &'a SignedDuration) -> DateArithmetic {
DateArithmetic::from(*sdur)
}
}
impl<'a> From<&'a UnsignedDuration> for DateArithmetic {
fn from(udur: &'a UnsignedDuration) -> DateArithmetic {
DateArithmetic::from(*udur)
}
}
#[derive(Clone, Copy, Debug)]
pub struct DateDifference {
date: Date,
round: SpanRound<'static>,
}
impl DateDifference {
#[inline]
pub fn new(date: Date) -> DateDifference {
let round =
SpanRound::new().mode(RoundMode::Trunc).smallest(Unit::Day);
DateDifference { date, round }
}
#[inline]
pub fn smallest(self, unit: Unit) -> DateDifference {
DateDifference { round: self.round.smallest(unit), ..self }
}
#[inline]
pub fn largest(self, unit: Unit) -> DateDifference {
DateDifference { round: self.round.largest(unit), ..self }
}
#[inline]
pub fn mode(self, mode: RoundMode) -> DateDifference {
DateDifference { round: self.round.mode(mode), ..self }
}
#[inline]
pub fn increment(self, increment: i64) -> DateDifference {
DateDifference { round: self.round.increment(increment), ..self }
}
#[inline]
fn rounding_may_change_span(&self) -> bool {
self.round.rounding_calendar_only_may_change_span()
}
#[inline]
fn since_with_largest_unit(&self, d1: Date) -> Result<Span, Error> {
let smallest = self.round.get_smallest();
if smallest < Unit::Day {
return Err(Error::from(UnitConfigError::CivilDate {
given: smallest,
}));
}
let largest = self.round.get_largest().unwrap_or(smallest);
if largest < Unit::Day {
return Err(Error::from(UnitConfigError::CivilDate {
given: largest,
}));
}
let d2 = self.date;
if d1 == d2 {
return Ok(Span::new());
}
if largest <= Unit::Week {
let mut weeks: i32 = 0;
let mut days = d1.until_days(d2);
if largest == Unit::Week {
weeks = days / 7;
days %= 7;
}
return Ok(Span::new().weeks(weeks).days(days));
}
let year1 = d1.year();
let month1 = d1.month();
let day1 = d1.day();
let mut year2 = d2.year();
let mut month2 = d2.month();
let day2 = d2.day();
let mut years = year2 - year1;
let mut months = i32::from(month2 - month1);
let mut days = i32::from(day2 - day1);
if years != 0 || months != 0 {
let sign = if years != 0 {
b::Sign::from(years)
} else {
b::Sign::from(months)
};
let mut days_in_month2 = d2.days_in_month();
let mut day_correct = 0;
if b::Sign::from(days) == -sign {
let (y, m) = month_add_one(year2, month2, -sign).unwrap();
year2 = y;
month2 = m;
years = year2 - year1;
months = i32::from(month2 - month1);
let original_days_in_month1 = days_in_month2;
days_in_month2 = itime::days_in_month(year2, month2);
day_correct = if sign.is_negative() {
-original_days_in_month1
} else {
days_in_month2
};
}
let day0_trunc = i32::from(day1.min(days_in_month2));
days = i32::from(day2) - day0_trunc + i32::from(day_correct);
if years != 0 {
months = i32::from(month2 - month1);
if b::Sign::from(months) == -sign {
let month_correct = sign * 12;
year2 -= sign.as_i16();
years = year2 - year1;
months = i32::from(month2 - month1) + month_correct;
}
}
}
if largest == Unit::Month && years != 0 {
months =
b::SpanMonths::checked_add(months, i32::from(years) * 12)?;
years = 0;
}
Ok(Span::new().years(years).months(months).days(days))
}
}
impl From<Date> for DateDifference {
#[inline]
fn from(date: Date) -> DateDifference {
DateDifference::new(date)
}
}
impl From<DateTime> for DateDifference {
#[inline]
fn from(dt: DateTime) -> DateDifference {
DateDifference::from(Date::from(dt))
}
}
impl From<Zoned> for DateDifference {
#[inline]
fn from(zdt: Zoned) -> DateDifference {
DateDifference::from(Date::from(zdt))
}
}
impl<'a> From<&'a Zoned> for DateDifference {
#[inline]
fn from(zdt: &'a Zoned) -> DateDifference {
DateDifference::from(zdt.datetime())
}
}
impl From<(Unit, Date)> for DateDifference {
#[inline]
fn from((largest, date): (Unit, Date)) -> DateDifference {
DateDifference::from(date).largest(largest)
}
}
impl From<(Unit, DateTime)> for DateDifference {
#[inline]
fn from((largest, dt): (Unit, DateTime)) -> DateDifference {
DateDifference::from((largest, Date::from(dt)))
}
}
impl From<(Unit, Zoned)> for DateDifference {
#[inline]
fn from((largest, zdt): (Unit, Zoned)) -> DateDifference {
DateDifference::from((largest, Date::from(zdt)))
}
}
impl<'a> From<(Unit, &'a Zoned)> for DateDifference {
#[inline]
fn from((largest, zdt): (Unit, &'a Zoned)) -> DateDifference {
DateDifference::from((largest, zdt.datetime()))
}
}
#[derive(Clone, Copy, Debug)]
pub struct DateWith {
original: Date,
year: Option<DateWithYear>,
month: Option<i8>,
day: Option<DateWithDay>,
}
impl DateWith {
#[inline]
fn new(original: Date) -> DateWith {
DateWith { original, year: None, month: None, day: None }
}
#[inline]
pub fn build(self) -> Result<Date, Error> {
let year = match self.year {
None => self.original.year(),
Some(DateWithYear::Jiff(year)) => b::Year::check(year)?,
Some(DateWithYear::EraYear(year, Era::CE)) => {
b::YearCE::check(year)?
}
Some(DateWithYear::EraYear(year, Era::BCE)) => {
let year_bce = b::YearBCE::check(year)?;
-year_bce + 1
}
};
let month = match self.month {
None => self.original.month(),
Some(month) => b::Month::check(month)?,
};
let day = match self.day {
None => self.original.day(),
Some(DateWithDay::OfMonth(day)) => b::Day::check(day)?,
Some(DateWithDay::OfYear(day)) => {
let idate = IDate::from_day_of_year(year, day)
.map_err(Error::itime_range)?;
return Ok(Date::from_idate_const(idate));
}
Some(DateWithDay::OfYearNoLeap(day)) => {
let idate = IDate::from_day_of_year_no_leap(year, day)
.map_err(Error::itime_range)?;
return Ok(Date::from_idate_const(idate));
}
};
Date::new(year, month, day)
}
#[inline]
pub fn year(self, year: i16) -> DateWith {
DateWith { year: Some(DateWithYear::Jiff(year)), ..self }
}
#[inline]
pub fn era_year(self, year: i16, era: Era) -> DateWith {
DateWith { year: Some(DateWithYear::EraYear(year, era)), ..self }
}
#[inline]
pub fn month(self, month: i8) -> DateWith {
DateWith { month: Some(month), ..self }
}
#[inline]
pub fn day(self, day: i8) -> DateWith {
DateWith { day: Some(DateWithDay::OfMonth(day)), ..self }
}
#[inline]
pub fn day_of_year(self, day: i16) -> DateWith {
DateWith { day: Some(DateWithDay::OfYear(day)), ..self }
}
#[inline]
pub fn day_of_year_no_leap(self, day: i16) -> DateWith {
DateWith { day: Some(DateWithDay::OfYearNoLeap(day)), ..self }
}
}
#[derive(Clone, Copy, Debug)]
enum DateWithYear {
Jiff(i16),
EraYear(i16, Era),
}
#[derive(Clone, Copy, Debug)]
enum DateWithDay {
OfMonth(i8),
OfYear(i16),
OfYearNoLeap(i16),
}
fn iso_week_start_from_year(year: i16) -> i32 {
let date_in_first_week = Date::new_unchecked(year, 1, 4);
let diff_from_monday = date_in_first_week.weekday().since(Weekday::Monday);
date_in_first_week.to_unix_epoch_day() - i32::from(diff_from_monday)
}
fn month_add_one(
mut year: i16,
mut month: i8,
delta: b::Sign,
) -> Result<(i16, i8), Error> {
month += delta.as_i8();
if month < 1 {
year -= 1;
month += 12;
} else if month > 12 {
year += 1;
month -= 12;
}
let year = b::Year::check(year)?;
Ok((year, month))
}
fn month_add_overflowing(month: i8, span: i32) -> (i8, i16) {
debug_assert!(b::SpanMonths::check(span).is_ok());
let month = i32::from(month);
let total = month - 1 + span;
let years = total.div_euclid(12);
let month = total.rem_euclid(12) + 1;
(month as i8, years as i16)
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use crate::{civil::date, span::span_eq, tz::TimeZone, Timestamp, ToSpan};
use super::*;
#[test]
fn t_from_unix() {
fn date_from_timestamp(timestamp: Timestamp) -> Date {
timestamp.to_zoned(TimeZone::UTC).datetime().date()
}
assert_eq!(
date(1970, 1, 1),
date_from_timestamp(Timestamp::new(0, 0).unwrap()),
);
assert_eq!(
date(1969, 12, 31),
date_from_timestamp(Timestamp::new(-1, 0).unwrap()),
);
assert_eq!(
date(1969, 12, 31),
date_from_timestamp(Timestamp::new(-86_400, 0).unwrap()),
);
assert_eq!(
date(1969, 12, 30),
date_from_timestamp(Timestamp::new(-86_401, 0).unwrap()),
);
assert_eq!(
date(-9999, 1, 2),
date_from_timestamp(
Timestamp::new(b::UnixSeconds::MIN, 0).unwrap()
),
);
assert_eq!(
date(9999, 12, 30),
date_from_timestamp(
Timestamp::new(b::UnixSeconds::MAX, 0).unwrap()
),
);
}
#[test]
#[cfg(not(miri))]
fn all_days_to_date_roundtrip() {
for rd in -100_000..=100_000 {
let date = Date::from_unix_epoch_day(rd);
let got = date.to_unix_epoch_day();
assert_eq!(rd, got, "for date {date:?}");
}
}
#[test]
#[cfg(not(miri))]
fn all_date_to_days_roundtrip() {
let year_range = 2000..=2500;
for year in year_range {
for month in b::Month::MIN..=b::Month::MAX {
for day in 1..=itime::days_in_month(year, month) {
let date = date(year, month, day);
let rd = date.to_unix_epoch_day();
let got = Date::from_unix_epoch_day(rd);
assert_eq!(date, got, "for date {date:?}");
}
}
}
}
#[test]
#[cfg(not(miri))]
fn all_date_to_iso_week_date_roundtrip() {
let year_range = 2000..=2500;
for year in year_range {
for month in [1, 2, 4] {
for day in 20..=itime::days_in_month(year, month) {
let date = date(year, month, day);
let wd = date.iso_week_date();
let got = wd.date();
assert_eq!(
date, got,
"for date {date:?}, and ISO week date {wd:?}"
);
}
}
}
let year_range = -9999..=-9500;
for year in year_range {
for month in [1, 2, 4] {
for day in 20..=itime::days_in_month(year, month) {
let date = date(year, month, day);
let wd = date.iso_week_date();
let got = wd.date();
assert_eq!(
date, got,
"for date {date:?}, and ISO week date {wd:?}"
);
}
}
}
}
#[test]
fn add_constrained() {
use crate::ToSpan;
let d1 = date(2023, 3, 31);
let d2 = d1.checked_add(1.months().days(1)).unwrap();
assert_eq!(d2, date(2023, 5, 1));
}
#[test]
fn since_years() {
let d1 = date(2023, 4, 15);
let d2 = date(2019, 2, 22);
let span = d1.since((Unit::Year, d2)).unwrap();
span_eq!(span, 4.years().months(1).days(21));
let span = d2.since((Unit::Year, d1)).unwrap();
span_eq!(span, -4.years().months(1).days(24));
let d1 = date(2023, 2, 22);
let d2 = date(2019, 4, 15);
let span = d1.since((Unit::Year, d2)).unwrap();
span_eq!(span, 3.years().months(10).days(7));
let span = d2.since((Unit::Year, d1)).unwrap();
span_eq!(span, -3.years().months(10).days(7));
let d1 = date(9999, 12, 31);
let d2 = date(-9999, 1, 1);
let span = d1.since((Unit::Year, d2)).unwrap();
span_eq!(span, 19998.years().months(11).days(30));
let span = d2.since((Unit::Year, d1)).unwrap();
span_eq!(span, -19998.years().months(11).days(30));
}
#[test]
fn since_months() {
let d1 = date(2024, 7, 24);
let d2 = date(2024, 2, 22);
let span = d1.since((Unit::Month, d2)).unwrap();
span_eq!(span, 5.months().days(2));
let span = d2.since((Unit::Month, d1)).unwrap();
span_eq!(span, -5.months().days(2));
assert_eq!(d2, d1.checked_sub(5.months().days(2)).unwrap());
assert_eq!(d1, d2.checked_sub(-5.months().days(2)).unwrap());
let d1 = date(2024, 7, 15);
let d2 = date(2024, 2, 22);
let span = d1.since((Unit::Month, d2)).unwrap();
span_eq!(span, 4.months().days(22));
let span = d2.since((Unit::Month, d1)).unwrap();
span_eq!(span, -4.months().days(23));
assert_eq!(d2, d1.checked_sub(4.months().days(22)).unwrap());
assert_eq!(d1, d2.checked_sub(-4.months().days(23)).unwrap());
let d1 = date(2023, 4, 15);
let d2 = date(2023, 2, 22);
let span = d1.since((Unit::Month, d2)).unwrap();
span_eq!(span, 1.month().days(21));
let span = d2.since((Unit::Month, d1)).unwrap();
span_eq!(span, -1.month().days(24));
assert_eq!(d2, d1.checked_sub(1.month().days(21)).unwrap());
assert_eq!(d1, d2.checked_sub(-1.month().days(24)).unwrap());
let d1 = date(2023, 4, 15);
let d2 = date(2019, 2, 22);
let span = d1.since((Unit::Month, d2)).unwrap();
span_eq!(span, 49.months().days(21));
let span = d2.since((Unit::Month, d1)).unwrap();
span_eq!(span, -49.months().days(24));
}
#[test]
fn since_weeks() {
let d1 = date(2024, 7, 15);
let d2 = date(2024, 6, 22);
let span = d1.since((Unit::Week, d2)).unwrap();
span_eq!(span, 3.weeks().days(2));
let span = d2.since((Unit::Week, d1)).unwrap();
span_eq!(span, -3.weeks().days(2));
}
#[test]
fn since_days() {
let d1 = date(2024, 7, 15);
let d2 = date(2024, 2, 22);
let span = d1.since((Unit::Day, d2)).unwrap();
span_eq!(span, 144.days());
let span = d2.since((Unit::Day, d1)).unwrap();
span_eq!(span, -144.days());
}
#[test]
fn until_month_lengths() {
let jan1 = date(2020, 1, 1);
let feb1 = date(2020, 2, 1);
let mar1 = date(2020, 3, 1);
span_eq!(jan1.until(feb1).unwrap(), 31.days());
span_eq!(jan1.until((Unit::Month, feb1)).unwrap(), 1.month());
span_eq!(feb1.until(mar1).unwrap(), 29.days());
span_eq!(feb1.until((Unit::Month, mar1)).unwrap(), 1.month());
span_eq!(jan1.until(mar1).unwrap(), 60.days());
span_eq!(jan1.until((Unit::Month, mar1)).unwrap(), 2.months());
}
#[test]
fn since_until_not_commutative() {
let d1 = date(2020, 4, 30);
let d2 = date(2020, 2, 29);
let since = d1.since((Unit::Month, d2)).unwrap();
span_eq!(since, 2.months());
let until = d2.until((Unit::Month, d1)).unwrap();
span_eq!(until, 2.months().days(1));
}
#[test]
fn until_weeks_round() {
use crate::{RoundMode, SpanRound};
let earlier = date(2019, 1, 8);
let later = date(2021, 9, 7);
let span = earlier.until((Unit::Week, later)).unwrap();
span_eq!(span, 139.weeks());
let options = SpanRound::new()
.smallest(Unit::Week)
.mode(RoundMode::HalfExpand)
.relative(earlier.to_datetime(Time::midnight()));
let rounded = span.round(options).unwrap();
span_eq!(rounded, 139.weeks());
}
#[test]
fn until_months_no_balance() {
let sp =
date(2023, 5, 31).until((Unit::Month, date(2024, 4, 30))).unwrap();
span_eq!(sp, 10.months().days(30));
let sp =
date(2023, 5, 31).until((Unit::Month, date(2023, 6, 30))).unwrap();
span_eq!(sp, 30.days());
}
#[test]
fn until_extremes() {
let d1 = date(-9999, 12, 1);
let d2 = date(-9999, 1, 2);
span_eq!(d1.until((Unit::Month, d2)).unwrap(), -10.months().days(30));
let d1 = date(9999, 1, 2);
let d2 = date(9999, 12, 1);
span_eq!(d1.until((Unit::Month, d2)).unwrap(), 10.months().days(29));
}
#[test]
fn test_month_add() {
let add =
|year: i16, month: i8, delta: i8| -> Result<(i16, i8), Error> {
month_add_one(year, month, b::Sign::from(delta))
};
assert_eq!(add(2024, 1, 1).unwrap(), (2024, 2));
assert_eq!(add(2024, 1, -1).unwrap(), (2023, 12));
assert_eq!(add(2024, 12, 1).unwrap(), (2025, 1));
assert_eq!(add(9999, 12, -1).unwrap(), (9999, 11));
assert_eq!(add(-9999, 1, 1).unwrap(), (-9999, 2));
assert!(add(9999, 12, 1).is_err());
assert!(add(-9999, 1, -1).is_err());
}
#[test]
fn test_month_add_overflowing() {
let month_add = |month, span| month_add_overflowing(month, span);
assert_eq!((1, 0), month_add(1, 0));
assert_eq!((12, 0), month_add(1, 11));
assert_eq!((1, 1), month_add(1, 12));
assert_eq!((2, 1), month_add(1, 13));
assert_eq!((9, 1), month_add(1, 20));
assert_eq!((12, 19998), month_add(12, b::SpanMonths::MAX));
assert_eq!((12, -1), month_add(1, -1));
assert_eq!((11, -1), month_add(1, -2));
assert_eq!((1, -1), month_add(1, -12));
assert_eq!((12, -2), month_add(1, -13));
}
#[test]
fn date_size() {
#[cfg(debug_assertions)]
{
assert_eq!(4, core::mem::size_of::<Date>());
}
#[cfg(not(debug_assertions))]
{
assert_eq!(4, core::mem::size_of::<Date>());
}
}
#[cfg(not(miri))]
quickcheck::quickcheck! {
fn prop_checked_add_then_sub(
d1: Date,
span: Span
) -> quickcheck::TestResult {
let span = if span.largest_unit() <= Unit::Day {
span
} else {
let round = SpanRound::new().largest(Unit::Day).relative(d1);
let Ok(span) = span.round(round) else {
return quickcheck::TestResult::discard();
};
span
};
let Ok(d2) = d1.checked_add(span) else {
return quickcheck::TestResult::discard();
};
let got = d2.checked_sub(span).unwrap();
quickcheck::TestResult::from_bool(d1 == got)
}
fn prop_checked_sub_then_add(
d1: Date,
span: Span
) -> quickcheck::TestResult {
let span = if span.largest_unit() <= Unit::Day {
span
} else {
let round = SpanRound::new().largest(Unit::Day).relative(d1);
let Ok(span) = span.round(round) else {
return quickcheck::TestResult::discard();
};
span
};
let Ok(d2) = d1.checked_sub(span) else {
return quickcheck::TestResult::discard();
};
let got = d2.checked_add(span).unwrap();
quickcheck::TestResult::from_bool(d1 == got)
}
fn prop_since_then_add(d1: Date, d2: Date) -> bool {
let span = d1.since(d2).unwrap();
let got = d2.checked_add(span).unwrap();
d1 == got
}
fn prop_until_then_sub(d1: Date, d2: Date) -> bool {
let span = d1.until(d2).unwrap();
let got = d2.checked_sub(span).unwrap();
d1 == got
}
}
#[test]
fn civil_date_deserialize_yaml() {
let expected = date(2024, 10, 31);
let deserialized: Date = serde_yaml::from_str("2024-10-31").unwrap();
assert_eq!(deserialized, expected);
let deserialized: Date =
serde_yaml::from_slice("2024-10-31".as_bytes()).unwrap();
assert_eq!(deserialized, expected);
let cursor = Cursor::new(b"2024-10-31");
let deserialized: Date = serde_yaml::from_reader(cursor).unwrap();
assert_eq!(deserialized, expected);
}
#[test]
fn nth_weekday_of_month() {
let d1 = date(1998, 1, 1);
let d2 = d1.nth_weekday_of_month(5, Weekday::Saturday).unwrap();
assert_eq!(d2, date(1998, 1, 31));
}
#[test]
fn nth_weekday_extreme() {
let weeks = 1043497;
let d1 = date(-9999, 1, 1);
let d2 = d1.nth_weekday(weeks, Weekday::Monday).unwrap();
assert_eq!(d2, date(9999, 12, 27));
assert!(d1.nth_weekday(weeks + 1, Weekday::Monday).is_err());
let d1 = date(9999, 12, 31);
let d2 = d1.nth_weekday(-weeks, Weekday::Friday).unwrap();
assert_eq!(d2, date(-9999, 1, 5));
assert!(d1.nth_weekday(weeks - 1, Weekday::Friday).is_err());
}
#[test]
fn days_of_month() {
let date = Date::new(1998, 1, 0);
assert!(date.is_err());
let date = Date::new(1998, 1, -1);
assert!(date.is_err());
let date = Date::new(1998, 2, 29);
assert!(date.is_err());
let date = Date::new(2004, 2, 29);
assert!(date.is_ok());
}
}