use core::time::Duration as UnsignedDuration;
use crate::{
civil::{DateTime, Era, ISOWeekDate, Time, Weekday},
duration::{Duration, SDuration},
error::{err, Error, ErrorContext},
fmt::{
self,
temporal::{DEFAULT_DATETIME_PARSER, DEFAULT_DATETIME_PRINTER},
},
shared::util::itime::{self, IDate, IEpochDay},
tz::TimeZone,
util::{
rangeint::{self, Composite, RFrom, RInto, TryRFrom},
t::{self, Day, Month, Sign, UnixEpochDay, Year, C},
},
RoundMode, SignedDuration, Span, SpanRound, Unit, Zoned,
};
#[derive(Clone, Copy, Hash)]
pub struct Date {
year: Year,
month: Month,
day: Day,
}
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 = Year::try_new("year", year)?;
let month = Month::try_new("month", month)?;
let day = Day::try_new("day", day)?;
Date::new_ranged(year, month, day)
}
#[inline]
pub const fn constant(year: i16, month: i8, day: i8) -> Date {
if !Year::contains(year) {
panic!("invalid year");
}
if !Month::contains(month) {
panic!("invalid month");
}
if day > itime::days_in_month(year, month) {
panic!("invalid day");
}
let year = Year::new_unchecked(year);
let month = Month::new_unchecked(month);
let day = Day::new_unchecked(day);
Date { year, month, day }
}
#[inline]
pub fn from_iso_week_date(weekdate: ISOWeekDate) -> Date {
let mut days = iso_week_start_from_year(weekdate.year_ranged());
let year = t::NoUnits16::rfrom(weekdate.year_ranged());
let week = t::NoUnits16::rfrom(weekdate.week_ranged());
let weekday = t::NoUnits16::rfrom(
weekdate.weekday().to_monday_zero_offset_ranged(),
);
let [week, weekday] = t::NoUnits16::vary_many(
[year, week, weekday],
|[year, week, weekday]| {
if year == C(9999) {
if week >= C(52) {
[week.min(C(52)), weekday.min(C(4))]
} else {
[week, weekday]
}
} else {
[week, weekday]
}
},
);
days += (UnixEpochDay::rfrom(week) - C(1)) * C(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_ranged().get()
}
#[inline]
pub fn era_year(self) -> (i16, Era) {
let year = self.year_ranged();
if year >= C(1) {
(year.get(), Era::CE)
} else {
let year = -t::YearBCE::rfrom(year.min(C(0)));
let era_year = year + C(1);
(era_year.get(), Era::BCE)
}
}
#[inline]
pub fn month(self) -> i8 {
self.month_ranged().get()
}
#[inline]
pub fn day(self) -> i8 {
self.day_ranged().get()
}
#[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_ranged_unchecked(
self.year_ranged(),
self.month_ranged(),
C(1).rinto(),
)
}
#[inline]
pub fn last_of_month(self) -> Date {
let max_day = self.days_in_month_ranged();
Date::new_ranged_unchecked(
self.year_ranged(),
self.month_ranged(),
max_day,
)
}
#[inline]
pub fn days_in_month(self) -> i8 {
self.days_in_month_ranged().get()
}
#[inline]
pub fn first_of_year(self) -> Date {
Date::new_ranged_unchecked(
self.year_ranged(),
C(1).rinto(),
C(1).rinto(),
)
}
#[inline]
pub fn last_of_year(self) -> Date {
Date::new_ranged_unchecked(
self.year_ranged(),
C(12).rinto(),
C(31).rinto(),
)
}
#[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_ranged().get())
}
#[inline]
pub fn tomorrow(self) -> Result<Date, Error> {
if self.day() >= 28 && self.day() == self.days_in_month() {
if self.month() == 12 {
let year = self.year_ranged().try_checked_add("year", C(1))?;
let month = Month::new_unchecked(1);
let day = Day::new_unchecked(1);
return Ok(Date::new_ranged_unchecked(year, month, day));
}
let year = self.year_ranged();
let month = Month::new_unchecked(self.month() + 1);
let day = Day::new_unchecked(1);
return Ok(Date::new_ranged_unchecked(year, month, day));
}
let year = self.year_ranged();
let month = self.month_ranged();
let day = Day::new_unchecked(self.day() + 1);
Ok(Date::new_ranged_unchecked(year, month, day))
}
#[inline]
pub fn yesterday(self) -> Result<Date, Error> {
if self.day() == 1 {
if self.month() == 1 {
let year = self.year_ranged().try_checked_sub("year", C(1))?;
let month = Month::new_unchecked(12);
let day = Day::new_unchecked(31);
return Ok(Date::new_ranged_unchecked(year, month, day));
}
let year = self.year_ranged();
let month = Month::new_unchecked(self.month() - 1);
let day = days_in_month(year, month);
return Ok(Date::new_ranged_unchecked(year, month, day));
}
let year = self.year_ranged();
let month = self.month_ranged();
let day = Day::new_unchecked(self.day() - 1);
Ok(Date::new_ranged_unchecked(year, month, day))
}
#[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::shared)?,
))
}
#[inline]
pub fn nth_weekday(
self,
nth: i32,
weekday: Weekday,
) -> Result<Date, Error> {
let nth = t::SpanWeeks::try_new("nth weekday", nth)?;
if nth == C(0) {
Err(err!("nth weekday cannot be `0`"))
} else if nth > C(0) {
let nth = nth.max(C(1));
let weekday_diff = weekday.since_ranged(self.weekday().next());
let diff = (nth - C(1)) * C(7) + weekday_diff;
let start = self.tomorrow()?.to_unix_epoch_day();
let end = start.try_checked_add("days", diff)?;
Ok(Date::from_unix_epoch_day(end))
} else {
let nth: t::SpanWeeks = nth.min(C(-1)).abs();
let weekday_diff = self.weekday().previous().since_ranged(weekday);
let diff = (nth - C(1)) * C(7) + weekday_diff;
let start = self.yesterday()?.to_unix_epoch_day();
let end = start.try_checked_sub("days", diff)?;
Ok(Date::from_unix_epoch_day(end))
}
}
#[inline]
pub fn iso_week_date(self) -> ISOWeekDate {
let days = t::NoUnits32::rfrom(self.to_unix_epoch_day());
let year = t::NoUnits32::rfrom(self.year_ranged());
let week_start = t::NoUnits32::vary([days, year], |[days, year]| {
let mut week_start =
t::NoUnits32::rfrom(iso_week_start_from_year(year.rinto()));
if days < week_start {
week_start = t::NoUnits32::rfrom(iso_week_start_from_year(
(year - C(1)).rinto(),
));
} else {
let next_year_week_start = t::NoUnits32::rfrom(
iso_week_start_from_year((year + C(1)).rinto()),
);
if days >= next_year_week_start {
week_start = next_year_week_start;
}
}
week_start
});
let weekday = Weekday::from_iweekday(
IEpochDay { epoch_day: days.get() }.weekday(),
);
let week = ((days - week_start) / C(7)) + C(1);
let unix_epoch_day = week_start
+ t::NoUnits32::rfrom(
Weekday::Thursday.since_ranged(Weekday::Monday),
);
let year =
Date::from_unix_epoch_day(unix_epoch_day.rinto()).year_ranged();
ISOWeekDate::new_ranged(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 span_days = span.get_days_ranged();
return if span_days == C(-1) {
self.yesterday()
} else if span_days == C(1) {
self.tomorrow()
} else {
let epoch_days = self.to_unix_epoch_day();
let days = epoch_days.try_checked_add(
"days",
UnixEpochDay::rfrom(span.get_days_ranged()),
)?;
Ok(Date::from_unix_epoch_day(days))
};
}
let (month, years) =
month_add_overflowing(self.month, span.get_months_ranged());
let year = self
.year
.try_checked_add("years", years)?
.try_checked_add("years", span.get_years_ranged())?;
let date = Date::constrain_ranged(year, month, self.day);
let epoch_days = date.to_unix_epoch_day();
let mut days = epoch_days
.try_checked_add(
"days",
C(7) * UnixEpochDay::rfrom(span.get_weeks_ranged()),
)?
.try_checked_add(
"days",
UnixEpochDay::rfrom(span.get_days_ranged()),
)?;
if !span.units().only_time().is_empty() {
let time_days = span
.only_lower(Unit::Day)
.to_invariant_nanoseconds()
.div_ceil(t::NANOS_PER_CIVIL_DAY);
days = days.try_checked_add("time", time_days)?;
}
Ok(Date::from_unix_epoch_day(days))
}
#[inline]
fn checked_add_duration(
self,
duration: SignedDuration,
) -> Result<Date, Error> {
match duration.as_hours() / 24 {
0 => Ok(self),
-1 => self.yesterday(),
1 => self.tomorrow(),
days => {
let days = UnixEpochDay::try_new("days", days).with_context(
|| {
err!(
"{days} computed from duration {duration:?} \
overflows Jiff's datetime limits",
)
},
)?;
let days =
self.to_unix_epoch_day().try_checked_add("days", days)?;
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 new_ranged(
year: Year,
month: Month,
day: Day,
) -> Result<Date, Error> {
if day > C(28) {
let max_day = days_in_month(year, month);
if day > max_day {
return Err(day.to_error_with_bounds("day", 1, max_day));
}
}
Ok(Date::new_ranged_unchecked(year, month, day))
}
#[inline]
pub(crate) fn new_ranged_unchecked(
year: Year,
month: Month,
day: Day,
) -> Date {
Date { year, month, day }
}
#[inline]
fn constrain_ranged(year: Year, month: Month, day: Day) -> Date {
let (year, month, mut day) =
(year.rinto(), month.rinto(), day.rinto());
day = saturate_day_in_month(year, month, day);
Date { year, month, day }
}
#[inline]
pub(crate) fn year_ranged(self) -> Year {
self.year
}
#[inline]
pub(crate) fn month_ranged(self) -> Month {
self.month
}
#[inline]
pub(crate) fn day_ranged(self) -> Day {
self.day
}
#[inline]
pub(crate) fn days_in_month_ranged(self) -> Day {
days_in_month(self.year_ranged(), self.month_ranged())
}
#[inline]
pub(crate) fn until_days_ranged(self, other: Date) -> t::SpanDays {
if self == other {
return C(0).rinto();
}
let start = self.to_unix_epoch_day();
let end = other.to_unix_epoch_day();
(end - start).rinto()
}
#[cfg_attr(feature = "perf-inline", inline(always))]
pub(crate) fn to_unix_epoch_day(self) -> UnixEpochDay {
self.to_idate().map(|x| x.to_epoch_day().epoch_day).to_rint()
}
#[cfg_attr(feature = "perf-inline", inline(always))]
pub(crate) fn from_unix_epoch_day(epoch_day: UnixEpochDay) -> Date {
let epoch_day = rangeint::composite!((epoch_day) => {
IEpochDay { epoch_day }
});
Date::from_idate(epoch_day.map(|x| x.to_date()))
}
#[inline]
pub(crate) fn to_idate(&self) -> Composite<IDate> {
rangeint::composite! {
(year = self.year, month = self.month, day = self.day) => {
IDate { year, month, day }
}
}
}
#[inline]
pub(crate) fn from_idate(idate: Composite<IDate>) -> Date {
let (year, month, day) =
rangeint::uncomposite!(idate, c => (c.year, c.month, c.day));
Date {
year: year.to_rint(),
month: month.to_rint(),
day: day.to_rint(),
}
}
#[inline]
pub(crate) const fn to_idate_const(self) -> IDate {
IDate {
year: self.year.get_unchecked(),
month: self.month.get_unchecked(),
day: self.day.get_unchecked(),
}
}
#[inline]
pub(crate) const fn from_idate_const(idate: IDate) -> Date {
Date {
year: Year::new_unchecked(idate.year),
month: Month::new_unchecked(idate.month),
day: Day::new_unchecked(idate.day),
}
}
}
impl Eq for Date {}
impl PartialEq for Date {
#[inline]
fn eq(&self, other: &Date) -> bool {
self.day.get() == other.day.get()
&& self.month.get() == other.month.get()
&& self.year.get() == other.year.get()
}
}
impl Ord for Date {
#[inline]
fn cmp(&self, other: &Date) -> core::cmp::Ordering {
(self.year.get(), self.month.get(), self.day.get()).cmp(&(
other.year.get(),
other.month.get(),
other.day.get(),
))
}
}
impl PartialOrd for Date {
#[inline]
fn partial_cmp(&self, other: &Date) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
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 = Year::arbitrary(g);
let month = Month::arbitrary(g);
let day = Day::arbitrary(g);
Date::constrain_ranged(year, month, day)
}
fn shrink(&self) -> alloc::boxed::Box<dyn Iterator<Item = Date>> {
alloc::boxed::Box::new(
(self.year_ranged(), self.month_ranged(), self.day_ranged())
.shrink()
.map(|(year, month, day)| {
Date::constrain_ranged(year, month, day)
}),
)
}
}
#[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);
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_may_change_span_ignore_largest()
}
#[inline]
fn since_with_largest_unit(&self, d1: Date) -> Result<Span, Error> {
let d2 = self.date;
let largest = self
.round
.get_largest()
.unwrap_or_else(|| self.round.get_smallest().max(Unit::Day));
if largest < Unit::Day {
return Err(err!(
"rounding the span between two dates must use days \
or bigger for its units, but found {units}",
units = largest.plural(),
));
}
if largest <= Unit::Week {
let mut weeks = t::SpanWeeks::rfrom(C(0));
let mut days = d1.until_days_ranged(d2);
if largest == Unit::Week {
weeks = days.div_ceil(C(7)).rinto();
days = days.rem_ceil(C(7));
}
return Ok(Span::new().weeks_ranged(weeks).days_ranged(days));
}
let year1 = d1.year_ranged();
let month1 = d1.month_ranged();
let day1 = d1.day_ranged();
let mut year2 = d2.year_ranged();
let mut month2 = d2.month_ranged();
let day2 = d2.day_ranged();
let mut years =
t::SpanYears::rfrom(year2) - t::SpanYears::rfrom(year1);
let mut months =
t::SpanMonths::rfrom(month2) - t::SpanMonths::rfrom(month1);
let mut days = t::SpanDays::rfrom(day2) - t::SpanMonths::rfrom(day1);
if years != C(0) || months != C(0) {
let sign = if years != C(0) {
Sign::rfrom(years.signum())
} else {
Sign::rfrom(months.signum())
};
let mut days_in_month2 =
t::SpanDays::rfrom(days_in_month(year2, month2));
let mut day_correct = t::SpanDays::N::<0>();
if days.signum() == -sign {
let original_days_in_month1 = days_in_month2;
let (y, m) = month_add_one(year2, month2, -sign).unwrap();
year2 = y;
month2 = m;
years =
t::SpanYears::rfrom(year2) - t::SpanYears::rfrom(year1);
months = t::SpanMonths::rfrom(month2)
- t::SpanMonths::rfrom(month1);
days_in_month2 = days_in_month(year2, month2).rinto();
day_correct = if sign < C(0) {
-original_days_in_month1
} else {
days_in_month2
};
}
let day0_trunc = t::SpanDays::rfrom(day1.min(days_in_month2));
days = t::SpanDays::rfrom(day2) - day0_trunc + day_correct;
if years != C(0) {
months = t::SpanMonths::rfrom(month2)
- t::SpanMonths::rfrom(month1);
if months.signum() == -sign {
let month_correct = if sign < C(0) {
-t::MONTHS_PER_YEAR
} else {
t::MONTHS_PER_YEAR
};
year2 -= sign;
years = t::SpanYears::rfrom(year2)
- t::SpanYears::rfrom(year1);
months = t::SpanMonths::rfrom(month2)
- t::SpanMonths::rfrom(month1)
+ month_correct;
}
}
}
if largest == Unit::Month && years != C(0) {
months = months.try_checked_add(
"months",
t::SpanMonths::rfrom(years) * t::MONTHS_PER_YEAR,
)?;
years = C(0).rinto();
}
Ok(Span::new()
.years_ranged(years)
.months_ranged(months)
.days_ranged(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_ranged(),
Some(DateWithYear::Jiff(year)) => Year::try_new("year", year)?,
Some(DateWithYear::EraYear(year, Era::CE)) => {
let year_ce = t::YearCE::try_new("CE year", year)?;
t::Year::try_rfrom("CE year", year_ce)?
}
Some(DateWithYear::EraYear(year, Era::BCE)) => {
let year_bce = t::YearBCE::try_new("BCE year", year)?;
t::Year::try_rfrom("BCE year", -year_bce + C(1))?
}
};
let month = match self.month {
None => self.original.month_ranged(),
Some(month) => Month::try_new("month", month)?,
};
let day = match self.day {
None => self.original.day_ranged(),
Some(DateWithDay::OfMonth(day)) => Day::try_new("day", day)?,
Some(DateWithDay::OfYear(day)) => {
let year = year.get_unchecked();
let idate = IDate::from_day_of_year(year, day)
.map_err(Error::shared)?;
return Ok(Date::from_idate_const(idate));
}
Some(DateWithDay::OfYearNoLeap(day)) => {
let year = year.get_unchecked();
let idate = IDate::from_day_of_year_no_leap(year, day)
.map_err(Error::shared)?;
return Ok(Date::from_idate_const(idate));
}
};
Date::new_ranged(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: t::ISOYear) -> UnixEpochDay {
let date_in_first_week =
Date::new_ranged(year.rinto(), C(1).rinto(), C(4).rinto())
.expect("Jan 4 is valid for all valid years");
let diff_from_monday =
date_in_first_week.weekday().since_ranged(Weekday::Monday);
date_in_first_week.to_unix_epoch_day() - diff_from_monday
}
fn month_add_one(
mut year: Year,
mut month: Month,
delta: Sign,
) -> Result<(Year, Month), Error> {
month += delta;
if month < C(1) {
year -= C(1);
month += t::MONTHS_PER_YEAR;
} else if month > t::MONTHS_PER_YEAR {
year += C(1);
month -= t::MONTHS_PER_YEAR;
}
let year = Year::try_rfrom("year", year)?;
let month = Month::try_rfrom("year", month)?;
Ok((year, month))
}
fn month_add_overflowing(
month: t::Month,
span: t::SpanMonths,
) -> (t::Month, t::SpanYears) {
let month = t::SpanMonths::rfrom(month);
let total = month - C(1) + span;
let years = total / C(12);
let month = (total % C(12)) + C(1);
(month.rinto(), years.rinto())
}
#[inline]
fn saturate_day_in_month(year: Year, month: Month, day: Day) -> Day {
day.min(days_in_month(year, month))
}
#[inline]
fn days_in_month(year: Year, month: Month) -> Day {
let c = rangeint::composite!((year, month) => {
itime::days_in_month(year, month)
});
c.to_rint()
}
#[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(t::UnixSeconds::MIN_REPR, 0).unwrap()
),
);
assert_eq!(
date(9999, 12, 30),
date_from_timestamp(
Timestamp::new(t::UnixSeconds::MAX_REPR, 0).unwrap()
),
);
}
#[test]
#[cfg(not(miri))]
fn all_days_to_date_roundtrip() {
for rd in -100_000..=100_000 {
let rd = UnixEpochDay::new(rd).unwrap();
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 {
let year = Year::new(year).unwrap();
for month in Month::MIN_REPR..=Month::MAX_REPR {
let month = Month::new(month).unwrap();
for day in 1..=days_in_month(year, month).get() {
let date = date(year.get(), month.get(), 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 {
let year = Year::new(year).unwrap();
for month in [1, 2, 4] {
let month = Month::new(month).unwrap();
for day in 20..=days_in_month(year, month).get() {
let date = date(year.get(), month.get(), 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 test_month_add() {
let add =
|year: i16, month: i8, delta: i8| -> Result<(i16, i8), Error> {
let year = Year::new(year).unwrap();
let month = Month::new(month).unwrap();
let delta = Sign::new(delta).unwrap();
let (year, month) = month_add_one(year, month, delta)?;
Ok((year.get(), month.get()))
};
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| {
let month = t::Month::new(month).unwrap();
let span = t::SpanMonths::new(span).unwrap();
let (month, years) = month_add_overflowing(month, span);
(month.get(), years.get())
};
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, t::SpanMonths::MAX_REPR));
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!(12, 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));
}
}