use core::time::Duration as UnsignedDuration;
use crate::{
civil::{
Date, DateTime, DateTimeRound, DateTimeWith, Era, ISOWeekDate, Time,
Weekday,
},
duration::{Duration, SDuration},
error::{zoned::Error as E, Error, ErrorContext},
fmt::{
self,
temporal::{self, DEFAULT_DATETIME_PARSER},
},
tz::{AmbiguousOffset, Disambiguation, Offset, OffsetConflict, TimeZone},
util::{b, round::Increment},
RoundMode, SignedDuration, Span, SpanRound, Timestamp, Unit,
};
#[derive(Clone)]
pub struct Zoned {
inner: ZonedInner,
}
#[derive(Clone)]
struct ZonedInner {
timestamp: Timestamp,
datetime: DateTime,
offset: Offset,
time_zone: TimeZone,
}
impl Zoned {
const DEFAULT: Zoned = Zoned {
inner: ZonedInner {
timestamp: Timestamp::UNIX_EPOCH,
datetime: DateTime::constant(1970, 1, 1, 0, 0, 0, 0),
offset: Offset::UTC,
time_zone: TimeZone::UTC,
},
};
#[cfg(feature = "std")]
#[inline]
pub fn now() -> Zoned {
Zoned::try_from(crate::now::system_time())
.expect("system time is valid")
}
#[inline]
pub fn new(timestamp: Timestamp, time_zone: TimeZone) -> Zoned {
let offset = time_zone.to_offset(timestamp);
let datetime = offset.to_datetime(timestamp);
let inner = ZonedInner { timestamp, datetime, offset, time_zone };
Zoned { inner }
}
#[inline]
pub(crate) fn from_parts(
timestamp: Timestamp,
time_zone: TimeZone,
offset: Offset,
datetime: DateTime,
) -> Zoned {
let inner = ZonedInner { timestamp, datetime, offset, time_zone };
Zoned { inner }
}
#[inline]
pub fn with(&self) -> ZonedWith {
ZonedWith::new(self.clone())
}
#[inline]
pub fn with_time_zone(&self, time_zone: TimeZone) -> Zoned {
Zoned::new(self.timestamp(), time_zone)
}
#[inline]
pub fn in_tz(&self, name: &str) -> Result<Zoned, Error> {
let tz = crate::tz::db().get(name)?;
Ok(self.with_time_zone(tz))
}
#[inline]
pub fn time_zone(&self) -> &TimeZone {
&self.inner.time_zone
}
#[inline]
pub fn year(&self) -> i16 {
self.date().year()
}
#[inline]
pub fn era_year(&self) -> (i16, Era) {
self.date().era_year()
}
#[inline]
pub fn month(&self) -> i8 {
self.date().month()
}
#[inline]
pub fn day(&self) -> i8 {
self.date().day()
}
#[inline]
pub fn hour(&self) -> i8 {
self.time().hour()
}
#[inline]
pub fn minute(&self) -> i8 {
self.time().minute()
}
#[inline]
pub fn second(&self) -> i8 {
self.time().second()
}
#[inline]
pub fn millisecond(&self) -> i16 {
self.time().millisecond()
}
#[inline]
pub fn microsecond(&self) -> i16 {
self.time().microsecond()
}
#[inline]
pub fn nanosecond(&self) -> i16 {
self.time().nanosecond()
}
#[inline]
pub fn subsec_nanosecond(&self) -> i32 {
self.time().subsec_nanosecond()
}
#[inline]
pub fn weekday(&self) -> Weekday {
self.date().weekday()
}
#[inline]
pub fn day_of_year(&self) -> i16 {
self.date().day_of_year()
}
#[inline]
pub fn day_of_year_no_leap(&self) -> Option<i16> {
self.date().day_of_year_no_leap()
}
#[inline]
pub fn start_of_day(&self) -> Result<Zoned, Error> {
self.datetime().start_of_day().to_zoned(self.time_zone().clone())
}
#[inline]
pub fn end_of_day(&self) -> Result<Zoned, Error> {
let end_of_civil_day = self.datetime().end_of_day();
let ambts = self.time_zone().to_ambiguous_timestamp(end_of_civil_day);
let offset = match ambts.offset() {
AmbiguousOffset::Unambiguous { offset } => offset,
AmbiguousOffset::Gap { after, .. } => after,
AmbiguousOffset::Fold { after, .. } => after,
};
offset
.to_timestamp(end_of_civil_day)
.map(|ts| ts.to_zoned(self.time_zone().clone()))
}
#[inline]
pub fn first_of_month(&self) -> Result<Zoned, Error> {
self.datetime().first_of_month().to_zoned(self.time_zone().clone())
}
#[inline]
pub fn last_of_month(&self) -> Result<Zoned, Error> {
self.datetime().last_of_month().to_zoned(self.time_zone().clone())
}
#[inline]
pub fn days_in_month(&self) -> i8 {
self.date().days_in_month()
}
#[inline]
pub fn first_of_year(&self) -> Result<Zoned, Error> {
self.datetime().first_of_year().to_zoned(self.time_zone().clone())
}
#[inline]
pub fn last_of_year(&self) -> Result<Zoned, Error> {
self.datetime().last_of_year().to_zoned(self.time_zone().clone())
}
#[inline]
pub fn days_in_year(&self) -> i16 {
self.date().days_in_year()
}
#[inline]
pub fn in_leap_year(&self) -> bool {
self.date().in_leap_year()
}
#[inline]
pub fn tomorrow(&self) -> Result<Zoned, Error> {
self.datetime().tomorrow()?.to_zoned(self.time_zone().clone())
}
#[inline]
pub fn yesterday(&self) -> Result<Zoned, Error> {
self.datetime().yesterday()?.to_zoned(self.time_zone().clone())
}
#[inline]
pub fn nth_weekday_of_month(
&self,
nth: i8,
weekday: Weekday,
) -> Result<Zoned, Error> {
self.datetime()
.nth_weekday_of_month(nth, weekday)?
.to_zoned(self.time_zone().clone())
}
#[inline]
pub fn nth_weekday(
&self,
nth: i32,
weekday: Weekday,
) -> Result<Zoned, Error> {
self.datetime()
.nth_weekday(nth, weekday)?
.to_zoned(self.time_zone().clone())
}
#[inline]
pub fn timestamp(&self) -> Timestamp {
self.inner.timestamp
}
#[inline]
pub fn datetime(&self) -> DateTime {
self.inner.datetime
}
#[inline]
pub fn date(&self) -> Date {
self.datetime().date()
}
#[inline]
pub fn time(&self) -> Time {
self.datetime().time()
}
#[inline]
pub fn iso_week_date(self) -> ISOWeekDate {
self.date().iso_week_date()
}
#[inline]
pub fn offset(&self) -> Offset {
self.inner.offset
}
#[inline]
pub fn checked_add<A: Into<ZonedArithmetic>>(
&self,
duration: A,
) -> Result<Zoned, Error> {
self.clone().checked_add_consuming(duration)
}
#[inline]
fn checked_add_consuming<A: Into<ZonedArithmetic>>(
self,
duration: A,
) -> Result<Zoned, Error> {
let duration: ZonedArithmetic = duration.into();
duration.checked_add(self)
}
#[inline]
fn checked_add_span(self, span: &Span) -> Result<Zoned, Error> {
let span_calendar = span.only_calendar();
if span_calendar.is_zero() {
return self
.timestamp()
.checked_add(span)
.map(|ts| ts.to_zoned(self.time_zone().clone()))
.context(E::AddTimestamp);
}
let span_time = span.only_time();
let dt = self
.datetime()
.checked_add(span_calendar)
.context(E::AddDateTime)?;
let tz = self.inner.time_zone;
let mut ts = tz
.to_ambiguous_timestamp(dt)
.compatible()
.context(E::ConvertDateTimeToTimestamp)?;
ts = ts.checked_add(span_time).context(E::AddTimestamp)?;
Ok(ts.to_zoned(tz))
}
#[inline]
fn checked_add_duration(
self,
duration: SignedDuration,
) -> Result<Zoned, Error> {
self.timestamp()
.checked_add(duration)
.map(|ts| ts.to_zoned(self.inner.time_zone))
}
#[inline]
pub fn checked_sub<A: Into<ZonedArithmetic>>(
&self,
duration: A,
) -> Result<Zoned, Error> {
self.clone().checked_sub_consuming(duration)
}
#[inline]
fn checked_sub_consuming<A: Into<ZonedArithmetic>>(
self,
duration: A,
) -> Result<Zoned, Error> {
let duration: ZonedArithmetic = duration.into();
duration.checked_neg().and_then(|za| za.checked_add(self))
}
#[inline]
pub fn saturating_add<A: Into<ZonedArithmetic>>(
&self,
duration: A,
) -> Zoned {
let duration: ZonedArithmetic = duration.into();
self.checked_add(duration).unwrap_or_else(|_| {
let ts = if duration.is_negative() {
Timestamp::MIN
} else {
Timestamp::MAX
};
ts.to_zoned(self.time_zone().clone())
})
}
#[inline]
pub fn saturating_sub<A: Into<ZonedArithmetic>>(
&self,
duration: A,
) -> Zoned {
let duration: ZonedArithmetic = duration.into();
let Ok(duration) = duration.checked_neg() else {
return Timestamp::MIN.to_zoned(self.time_zone().clone());
};
self.saturating_add(duration)
}
#[inline]
pub fn until<'a, A: Into<ZonedDifference<'a>>>(
&self,
other: A,
) -> Result<Span, Error> {
let args: ZonedDifference = other.into();
let span = args.until_with_largest_unit(self)?;
if args.rounding_may_change_span() {
span.round(args.round.relative(self))
} else {
Ok(span)
}
}
#[inline]
pub fn since<'a, A: Into<ZonedDifference<'a>>>(
&self,
other: A,
) -> Result<Span, Error> {
let args: ZonedDifference = other.into();
let span = -args.until_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: &Zoned) -> SignedDuration {
SignedDuration::zoned_until(self, other)
}
#[inline]
pub fn duration_since(&self, other: &Zoned) -> SignedDuration {
SignedDuration::zoned_until(other, self)
}
#[inline]
pub fn round<R: Into<ZonedRound>>(
&self,
options: R,
) -> Result<Zoned, Error> {
let options: ZonedRound = options.into();
options.round(self)
}
#[inline]
pub fn series(&self, period: Span) -> ZonedSeries {
ZonedSeries { start: self.clone(), prev: None, period, step: 0 }
}
#[inline]
fn into_parts(self) -> (Timestamp, DateTime, Offset, TimeZone) {
let inner = self.inner;
let ZonedInner { timestamp, datetime, offset, time_zone } = inner;
(timestamp, datetime, offset, time_zone)
}
}
impl Zoned {
#[inline]
pub fn strptime(
format: impl AsRef<[u8]>,
input: impl AsRef<[u8]>,
) -> Result<Zoned, Error> {
fmt::strtime::parse(format, input).and_then(|tm| tm.to_zoned())
}
#[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 Default for Zoned {
#[inline]
fn default() -> Zoned {
Zoned::DEFAULT
}
}
impl core::fmt::Debug for Zoned {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
core::fmt::Display::fmt(self, f)
}
}
impl core::fmt::Display for Zoned {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use crate::fmt::StdFmtWrite;
let precision =
f.precision().map(|p| u8::try_from(p).unwrap_or(u8::MAX));
temporal::DateTimePrinter::new()
.precision(precision)
.print_zoned(self, StdFmtWrite(f))
.map_err(|_| core::fmt::Error)
}
}
impl core::str::FromStr for Zoned {
type Err = Error;
fn from_str(string: &str) -> Result<Zoned, Error> {
DEFAULT_DATETIME_PARSER.parse_zoned(string)
}
}
impl Eq for Zoned {}
impl PartialEq for Zoned {
#[inline]
fn eq(&self, rhs: &Zoned) -> bool {
self.timestamp().eq(&rhs.timestamp())
}
}
impl<'a> PartialEq<Zoned> for &'a Zoned {
#[inline]
fn eq(&self, rhs: &Zoned) -> bool {
(**self).eq(rhs)
}
}
impl Ord for Zoned {
#[inline]
fn cmp(&self, rhs: &Zoned) -> core::cmp::Ordering {
self.timestamp().cmp(&rhs.timestamp())
}
}
impl PartialOrd for Zoned {
#[inline]
fn partial_cmp(&self, rhs: &Zoned) -> Option<core::cmp::Ordering> {
Some(self.cmp(rhs))
}
}
impl<'a> PartialOrd<Zoned> for &'a Zoned {
#[inline]
fn partial_cmp(&self, rhs: &Zoned) -> Option<core::cmp::Ordering> {
(**self).partial_cmp(rhs)
}
}
impl core::hash::Hash for Zoned {
#[inline]
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.timestamp().hash(state);
}
}
#[cfg(feature = "std")]
impl TryFrom<std::time::SystemTime> for Zoned {
type Error = Error;
#[inline]
fn try_from(system_time: std::time::SystemTime) -> Result<Zoned, Error> {
let timestamp = Timestamp::try_from(system_time)?;
Ok(Zoned::new(timestamp, TimeZone::system()))
}
}
#[cfg(feature = "std")]
impl From<Zoned> for std::time::SystemTime {
#[inline]
fn from(time: Zoned) -> std::time::SystemTime {
time.timestamp().into()
}
}
#[cfg(feature = "std")]
impl<'a> From<&'a Zoned> for std::time::SystemTime {
#[inline]
fn from(time: &'a Zoned) -> std::time::SystemTime {
time.timestamp().into()
}
}
impl<'a> core::ops::Add<Span> for Zoned {
type Output = Zoned;
#[inline]
fn add(self, rhs: Span) -> Zoned {
self.checked_add_consuming(rhs)
.expect("adding span to zoned datetime overflowed")
}
}
impl<'a> core::ops::Add<Span> for &'a Zoned {
type Output = Zoned;
#[inline]
fn add(self, rhs: Span) -> Zoned {
self.checked_add(rhs)
.expect("adding span to zoned datetime overflowed")
}
}
impl core::ops::AddAssign<Span> for Zoned {
#[inline]
fn add_assign(&mut self, rhs: Span) {
*self = core::mem::take(self) + rhs;
}
}
impl<'a> core::ops::Sub<Span> for Zoned {
type Output = Zoned;
#[inline]
fn sub(self, rhs: Span) -> Zoned {
self.checked_sub_consuming(rhs)
.expect("subtracting span from zoned datetime overflowed")
}
}
impl<'a> core::ops::Sub<Span> for &'a Zoned {
type Output = Zoned;
#[inline]
fn sub(self, rhs: Span) -> Zoned {
self.checked_sub(rhs)
.expect("subtracting span from zoned datetime overflowed")
}
}
impl core::ops::SubAssign<Span> for Zoned {
#[inline]
fn sub_assign(&mut self, rhs: Span) {
*self = core::mem::take(self) - rhs;
}
}
impl core::ops::Sub for Zoned {
type Output = Span;
#[inline]
fn sub(self, rhs: Zoned) -> Span {
(&self).sub(&rhs)
}
}
impl<'a> core::ops::Sub for &'a Zoned {
type Output = Span;
#[inline]
fn sub(self, rhs: &'a Zoned) -> Span {
self.since(rhs).expect("since never fails when given Zoned")
}
}
impl core::ops::Add<SignedDuration> for Zoned {
type Output = Zoned;
#[inline]
fn add(self, rhs: SignedDuration) -> Zoned {
self.checked_add_consuming(rhs)
.expect("adding signed duration to zoned datetime overflowed")
}
}
impl<'a> core::ops::Add<SignedDuration> for &'a Zoned {
type Output = Zoned;
#[inline]
fn add(self, rhs: SignedDuration) -> Zoned {
self.checked_add(rhs)
.expect("adding signed duration to zoned datetime overflowed")
}
}
impl core::ops::AddAssign<SignedDuration> for Zoned {
#[inline]
fn add_assign(&mut self, rhs: SignedDuration) {
*self = core::mem::take(self) + rhs;
}
}
impl core::ops::Sub<SignedDuration> for Zoned {
type Output = Zoned;
#[inline]
fn sub(self, rhs: SignedDuration) -> Zoned {
self.checked_sub_consuming(rhs).expect(
"subtracting signed duration from zoned datetime overflowed",
)
}
}
impl<'a> core::ops::Sub<SignedDuration> for &'a Zoned {
type Output = Zoned;
#[inline]
fn sub(self, rhs: SignedDuration) -> Zoned {
self.checked_sub(rhs).expect(
"subtracting signed duration from zoned datetime overflowed",
)
}
}
impl core::ops::SubAssign<SignedDuration> for Zoned {
#[inline]
fn sub_assign(&mut self, rhs: SignedDuration) {
*self = core::mem::take(self) - rhs;
}
}
impl core::ops::Add<UnsignedDuration> for Zoned {
type Output = Zoned;
#[inline]
fn add(self, rhs: UnsignedDuration) -> Zoned {
self.checked_add_consuming(rhs)
.expect("adding unsigned duration to zoned datetime overflowed")
}
}
impl<'a> core::ops::Add<UnsignedDuration> for &'a Zoned {
type Output = Zoned;
#[inline]
fn add(self, rhs: UnsignedDuration) -> Zoned {
self.checked_add(rhs)
.expect("adding unsigned duration to zoned datetime overflowed")
}
}
impl core::ops::AddAssign<UnsignedDuration> for Zoned {
#[inline]
fn add_assign(&mut self, rhs: UnsignedDuration) {
*self = core::mem::take(self) + rhs;
}
}
impl core::ops::Sub<UnsignedDuration> for Zoned {
type Output = Zoned;
#[inline]
fn sub(self, rhs: UnsignedDuration) -> Zoned {
self.checked_sub_consuming(rhs).expect(
"subtracting unsigned duration from zoned datetime overflowed",
)
}
}
impl<'a> core::ops::Sub<UnsignedDuration> for &'a Zoned {
type Output = Zoned;
#[inline]
fn sub(self, rhs: UnsignedDuration) -> Zoned {
self.checked_sub(rhs).expect(
"subtracting unsigned duration from zoned datetime overflowed",
)
}
}
impl core::ops::SubAssign<UnsignedDuration> for Zoned {
#[inline]
fn sub_assign(&mut self, rhs: UnsignedDuration) {
*self = core::mem::take(self) - rhs;
}
}
#[cfg(feature = "serde")]
impl serde_core::Serialize for Zoned {
#[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 Zoned {
#[inline]
fn deserialize<D: serde_core::Deserializer<'de>>(
deserializer: D,
) -> Result<Zoned, D::Error> {
use serde_core::de;
struct ZonedVisitor;
impl<'de> de::Visitor<'de> for ZonedVisitor {
type Value = Zoned;
fn expecting(
&self,
f: &mut core::fmt::Formatter,
) -> core::fmt::Result {
f.write_str("a zoned datetime string")
}
#[inline]
fn visit_bytes<E: de::Error>(
self,
value: &[u8],
) -> Result<Zoned, E> {
DEFAULT_DATETIME_PARSER
.parse_zoned(value)
.map_err(de::Error::custom)
}
#[inline]
fn visit_str<E: de::Error>(self, value: &str) -> Result<Zoned, E> {
self.visit_bytes(value.as_bytes())
}
}
deserializer.deserialize_str(ZonedVisitor)
}
}
#[cfg(test)]
impl quickcheck::Arbitrary for Zoned {
fn arbitrary(g: &mut quickcheck::Gen) -> Zoned {
let timestamp = Timestamp::arbitrary(g);
let tz = TimeZone::UTC; Zoned::new(timestamp, tz)
}
fn shrink(&self) -> alloc::boxed::Box<dyn Iterator<Item = Self>> {
let timestamp = self.timestamp();
alloc::boxed::Box::new(
timestamp
.shrink()
.map(|timestamp| Zoned::new(timestamp, TimeZone::UTC)),
)
}
}
#[derive(Clone, Debug)]
pub struct ZonedSeries {
start: Zoned,
prev: Option<Timestamp>,
period: Span,
step: i64,
}
impl Iterator for ZonedSeries {
type Item = Zoned;
#[inline]
fn next(&mut self) -> Option<Zoned> {
loop {
let span = self.period.checked_mul(self.step).ok()?;
self.step = self.step.checked_add(1)?;
let zdt = self.start.checked_add(span).ok()?;
if self.prev.map_or(true, |prev| {
if self.period.is_positive() {
prev < zdt.timestamp()
} else if self.period.is_negative() {
prev > zdt.timestamp()
} else {
assert!(self.period.is_zero());
true
}
}) {
self.prev = Some(zdt.timestamp());
return Some(zdt);
}
}
}
}
impl core::iter::FusedIterator for ZonedSeries {}
#[derive(Clone, Copy, Debug)]
pub struct ZonedArithmetic {
duration: Duration,
}
impl ZonedArithmetic {
#[inline]
fn checked_add(self, zdt: Zoned) -> Result<Zoned, Error> {
match self.duration.to_signed()? {
SDuration::Span(span) => zdt.checked_add_span(span),
SDuration::Absolute(sdur) => zdt.checked_add_duration(sdur),
}
}
#[inline]
fn checked_neg(self) -> Result<ZonedArithmetic, Error> {
let duration = self.duration.checked_neg()?;
Ok(ZonedArithmetic { duration })
}
#[inline]
fn is_negative(&self) -> bool {
self.duration.is_negative()
}
}
impl From<Span> for ZonedArithmetic {
fn from(span: Span) -> ZonedArithmetic {
let duration = Duration::from(span);
ZonedArithmetic { duration }
}
}
impl From<SignedDuration> for ZonedArithmetic {
fn from(sdur: SignedDuration) -> ZonedArithmetic {
let duration = Duration::from(sdur);
ZonedArithmetic { duration }
}
}
impl From<UnsignedDuration> for ZonedArithmetic {
fn from(udur: UnsignedDuration) -> ZonedArithmetic {
let duration = Duration::from(udur);
ZonedArithmetic { duration }
}
}
impl<'a> From<&'a Span> for ZonedArithmetic {
fn from(span: &'a Span) -> ZonedArithmetic {
ZonedArithmetic::from(*span)
}
}
impl<'a> From<&'a SignedDuration> for ZonedArithmetic {
fn from(sdur: &'a SignedDuration) -> ZonedArithmetic {
ZonedArithmetic::from(*sdur)
}
}
impl<'a> From<&'a UnsignedDuration> for ZonedArithmetic {
fn from(udur: &'a UnsignedDuration) -> ZonedArithmetic {
ZonedArithmetic::from(*udur)
}
}
#[derive(Clone, Copy, Debug)]
pub struct ZonedDifference<'a> {
zoned: &'a Zoned,
round: SpanRound<'static>,
}
impl<'a> ZonedDifference<'a> {
#[inline]
pub fn new(zoned: &'a Zoned) -> ZonedDifference<'a> {
let round = SpanRound::new().mode(RoundMode::Trunc);
ZonedDifference { zoned, round }
}
#[inline]
pub fn smallest(self, unit: Unit) -> ZonedDifference<'a> {
ZonedDifference { round: self.round.smallest(unit), ..self }
}
#[inline]
pub fn largest(self, unit: Unit) -> ZonedDifference<'a> {
ZonedDifference { round: self.round.largest(unit), ..self }
}
#[inline]
pub fn mode(self, mode: RoundMode) -> ZonedDifference<'a> {
ZonedDifference { round: self.round.mode(mode), ..self }
}
#[inline]
pub fn increment(self, increment: i64) -> ZonedDifference<'a> {
ZonedDifference { round: self.round.increment(increment), ..self }
}
#[inline]
fn rounding_may_change_span(&self) -> bool {
self.round.rounding_may_change_span()
}
#[inline]
fn until_with_largest_unit(&self, zdt1: &Zoned) -> Result<Span, Error> {
let zdt2 = self.zoned;
let sign = b::Sign::from_ordinals(zdt2, zdt1);
if sign.is_zero() {
return Ok(Span::new());
}
let largest = self
.round
.get_largest()
.unwrap_or_else(|| self.round.get_smallest().max(Unit::Hour));
if largest < Unit::Day {
return zdt1.timestamp().until((largest, zdt2.timestamp()));
}
if zdt1.time_zone() != zdt2.time_zone() {
return Err(Error::from(E::MismatchTimeZoneUntil { largest }));
}
let tz = zdt1.time_zone();
let (dt1, mut dt2) = (zdt1.datetime(), zdt2.datetime());
let mut day_correct: i32 = 0;
if b::Sign::from_ordinals(dt1.time(), dt2.time()) == sign {
day_correct += 1;
}
let mut mid = dt2
.date()
.checked_add(Span::new().days(day_correct * -sign))
.context(E::AddDays)?
.to_datetime(dt1.time());
let mut zmid: Zoned = mid
.to_zoned(tz.clone())
.context(E::ConvertIntermediateDatetime)?;
if b::Sign::from_ordinals(zdt2, &zmid) == -sign {
if sign.is_negative() {
panic!("this should be an error");
}
day_correct += 1;
mid = dt2
.date()
.checked_add(Span::new().days(day_correct * -sign))
.context(E::AddDays)?
.to_datetime(dt1.time());
zmid = mid
.to_zoned(tz.clone())
.context(E::ConvertIntermediateDatetime)?;
if b::Sign::from_ordinals(zdt2, &zmid) == -sign {
panic!("this should be an error too");
}
}
let remainder =
zdt2.timestamp().as_duration() - zmid.timestamp().as_duration();
dt2 = mid;
let date_span = dt1.date().until((largest, dt2.date()))?;
Ok(Span::from_invariant_duration(Unit::Hour, remainder)
.expect("difference between time always fits in span")
.years(date_span.get_years())
.months(date_span.get_months())
.weeks(date_span.get_weeks())
.days(date_span.get_days()))
}
}
impl<'a> From<&'a Zoned> for ZonedDifference<'a> {
#[inline]
fn from(zdt: &'a Zoned) -> ZonedDifference<'a> {
ZonedDifference::new(zdt)
}
}
impl<'a> From<(Unit, &'a Zoned)> for ZonedDifference<'a> {
#[inline]
fn from((largest, zdt): (Unit, &'a Zoned)) -> ZonedDifference<'a> {
ZonedDifference::new(zdt).largest(largest)
}
}
#[derive(Clone, Copy, Debug)]
pub struct ZonedRound {
round: DateTimeRound,
}
impl ZonedRound {
#[inline]
pub fn new() -> ZonedRound {
ZonedRound { round: DateTimeRound::new() }
}
#[inline]
pub fn smallest(self, unit: Unit) -> ZonedRound {
ZonedRound { round: self.round.smallest(unit) }
}
#[inline]
pub fn mode(self, mode: RoundMode) -> ZonedRound {
ZonedRound { round: self.round.mode(mode) }
}
#[inline]
pub fn increment(self, increment: i64) -> ZonedRound {
ZonedRound { round: self.round.increment(increment) }
}
pub(crate) fn round(&self, zdt: &Zoned) -> Result<Zoned, Error> {
let start = zdt.datetime();
if self.round.get_smallest() == Unit::Day {
return self.round_days(zdt);
}
let end = self.round.round(start)?;
let amb = OffsetConflict::PreferOffset.resolve(
end,
zdt.offset(),
zdt.time_zone().clone(),
)?;
amb.compatible()
}
fn round_days(&self, zdt: &Zoned) -> Result<Zoned, Error> {
debug_assert_eq!(self.round.get_smallest(), Unit::Day);
Increment::for_datetime(Unit::Day, self.round.get_increment())?;
let start = zdt.start_of_day().context(E::FailedStartOfDay)?;
let end = start.tomorrow().context(E::FailedLengthOfDay)?;
if start.timestamp() == end.timestamp() {
return Err(Error::from(E::FailedLengthOfDay));
}
let day_length =
end.timestamp().as_duration() - start.timestamp().as_duration();
let progress =
zdt.timestamp().as_duration() - start.timestamp().as_duration();
let rounded =
self.round.get_mode().round_by_duration(progress, day_length)?;
let nanos = start
.timestamp()
.as_duration()
.checked_add(rounded)
.ok_or(E::FailedSpanNanoseconds)?;
Ok(Timestamp::from_duration(nanos)?.to_zoned(zdt.time_zone().clone()))
}
}
impl Default for ZonedRound {
#[inline]
fn default() -> ZonedRound {
ZonedRound::new()
}
}
impl From<Unit> for ZonedRound {
#[inline]
fn from(unit: Unit) -> ZonedRound {
ZonedRound::default().smallest(unit)
}
}
impl From<(Unit, i64)> for ZonedRound {
#[inline]
fn from((unit, increment): (Unit, i64)) -> ZonedRound {
ZonedRound::from(unit).increment(increment)
}
}
#[derive(Clone, Debug)]
pub struct ZonedWith {
original: Zoned,
datetime_with: DateTimeWith,
offset: Option<Offset>,
disambiguation: Disambiguation,
offset_conflict: OffsetConflict,
}
impl ZonedWith {
#[inline]
fn new(original: Zoned) -> ZonedWith {
let datetime_with = original.datetime().with();
ZonedWith {
original,
datetime_with,
offset: None,
disambiguation: Disambiguation::default(),
offset_conflict: OffsetConflict::PreferOffset,
}
}
#[inline]
pub fn build(self) -> Result<Zoned, Error> {
let dt = self.datetime_with.build()?;
let (_, _, offset, time_zone) = self.original.into_parts();
let offset = self.offset.unwrap_or(offset);
let ambiguous = self.offset_conflict.resolve(dt, offset, time_zone)?;
ambiguous.disambiguate(self.disambiguation)
}
#[inline]
pub fn date(self, date: Date) -> ZonedWith {
ZonedWith { datetime_with: self.datetime_with.date(date), ..self }
}
#[inline]
pub fn time(self, time: Time) -> ZonedWith {
ZonedWith { datetime_with: self.datetime_with.time(time), ..self }
}
#[inline]
pub fn year(self, year: i16) -> ZonedWith {
ZonedWith { datetime_with: self.datetime_with.year(year), ..self }
}
#[inline]
pub fn era_year(self, year: i16, era: Era) -> ZonedWith {
ZonedWith {
datetime_with: self.datetime_with.era_year(year, era),
..self
}
}
#[inline]
pub fn month(self, month: i8) -> ZonedWith {
ZonedWith { datetime_with: self.datetime_with.month(month), ..self }
}
#[inline]
pub fn day(self, day: i8) -> ZonedWith {
ZonedWith { datetime_with: self.datetime_with.day(day), ..self }
}
#[inline]
pub fn day_of_year(self, day: i16) -> ZonedWith {
ZonedWith {
datetime_with: self.datetime_with.day_of_year(day),
..self
}
}
#[inline]
pub fn day_of_year_no_leap(self, day: i16) -> ZonedWith {
ZonedWith {
datetime_with: self.datetime_with.day_of_year_no_leap(day),
..self
}
}
#[inline]
pub fn hour(self, hour: i8) -> ZonedWith {
ZonedWith { datetime_with: self.datetime_with.hour(hour), ..self }
}
#[inline]
pub fn minute(self, minute: i8) -> ZonedWith {
ZonedWith { datetime_with: self.datetime_with.minute(minute), ..self }
}
#[inline]
pub fn second(self, second: i8) -> ZonedWith {
ZonedWith { datetime_with: self.datetime_with.second(second), ..self }
}
#[inline]
pub fn millisecond(self, millisecond: i16) -> ZonedWith {
ZonedWith {
datetime_with: self.datetime_with.millisecond(millisecond),
..self
}
}
#[inline]
pub fn microsecond(self, microsecond: i16) -> ZonedWith {
ZonedWith {
datetime_with: self.datetime_with.microsecond(microsecond),
..self
}
}
#[inline]
pub fn nanosecond(self, nanosecond: i16) -> ZonedWith {
ZonedWith {
datetime_with: self.datetime_with.nanosecond(nanosecond),
..self
}
}
#[inline]
pub fn subsec_nanosecond(self, subsec_nanosecond: i32) -> ZonedWith {
ZonedWith {
datetime_with: self
.datetime_with
.subsec_nanosecond(subsec_nanosecond),
..self
}
}
#[inline]
pub fn offset(self, offset: Offset) -> ZonedWith {
ZonedWith { offset: Some(offset), ..self }
}
#[inline]
pub fn offset_conflict(self, strategy: OffsetConflict) -> ZonedWith {
ZonedWith { offset_conflict: strategy, ..self }
}
#[inline]
pub fn disambiguation(self, strategy: Disambiguation) -> ZonedWith {
ZonedWith { disambiguation: strategy, ..self }
}
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use alloc::string::ToString;
use crate::{
civil::{date, datetime},
span::span_eq,
tz, ToSpan,
};
use super::*;
#[test]
fn until_with_largest_unit() {
if crate::tz::db().is_definitively_empty() {
return;
}
let zdt1: Zoned = date(1995, 12, 7)
.at(3, 24, 30, 3500)
.in_tz("Asia/Kolkata")
.unwrap();
let zdt2: Zoned =
date(2019, 1, 31).at(15, 30, 0, 0).in_tz("Asia/Kolkata").unwrap();
let span = zdt1.until(&zdt2).unwrap();
span_eq!(
span,
202956
.hours()
.minutes(5)
.seconds(29)
.milliseconds(999)
.microseconds(996)
.nanoseconds(500)
);
let span = zdt1.until((Unit::Year, &zdt2)).unwrap();
span_eq!(
span,
23.years()
.months(1)
.days(24)
.hours(12)
.minutes(5)
.seconds(29)
.milliseconds(999)
.microseconds(996)
.nanoseconds(500)
);
let span = zdt2.until((Unit::Year, &zdt1)).unwrap();
span_eq!(
span,
-23.years()
.months(1)
.days(24)
.hours(12)
.minutes(5)
.seconds(29)
.milliseconds(999)
.microseconds(996)
.nanoseconds(500)
);
let span = zdt1.until((Unit::Nanosecond, &zdt2)).unwrap();
span_eq!(span, 730641929999996500i64.nanoseconds());
let zdt1: Zoned =
date(2020, 1, 1).at(0, 0, 0, 0).in_tz("America/New_York").unwrap();
let zdt2: Zoned = date(2020, 4, 24)
.at(21, 0, 0, 0)
.in_tz("America/New_York")
.unwrap();
let span = zdt1.until(&zdt2).unwrap();
span_eq!(span, 2756.hours());
let span = zdt1.until((Unit::Year, &zdt2)).unwrap();
span_eq!(span, 3.months().days(23).hours(21));
let zdt1: Zoned = date(2000, 10, 29)
.at(0, 0, 0, 0)
.in_tz("America/Vancouver")
.unwrap();
let zdt2: Zoned = date(2000, 10, 29)
.at(23, 0, 0, 5)
.in_tz("America/Vancouver")
.unwrap();
let span = zdt1.until((Unit::Day, &zdt2)).unwrap();
span_eq!(span, 24.hours().nanoseconds(5));
}
#[cfg(target_pointer_width = "64")]
#[test]
fn zoned_size() {
#[cfg(debug_assertions)]
{
#[cfg(feature = "alloc")]
{
assert_eq!(40, core::mem::size_of::<Zoned>());
}
#[cfg(all(target_pointer_width = "64", not(feature = "alloc")))]
{
assert_eq!(40, core::mem::size_of::<Zoned>());
}
}
#[cfg(not(debug_assertions))]
{
#[cfg(feature = "alloc")]
{
assert_eq!(40, core::mem::size_of::<Zoned>());
}
#[cfg(all(target_pointer_width = "64", not(feature = "alloc")))]
{
assert_eq!(40, core::mem::size_of::<Zoned>());
}
}
}
#[test]
fn zoned_deserialize_yaml() {
if crate::tz::db().is_definitively_empty() {
return;
}
let expected = datetime(2024, 10, 31, 16, 33, 53, 123456789)
.in_tz("UTC")
.unwrap();
let deserialized: Zoned =
serde_yaml::from_str("2024-10-31T16:33:53.123456789+00:00[UTC]")
.unwrap();
assert_eq!(deserialized, expected);
let deserialized: Zoned = serde_yaml::from_slice(
"2024-10-31T16:33:53.123456789+00:00[UTC]".as_bytes(),
)
.unwrap();
assert_eq!(deserialized, expected);
let cursor = Cursor::new(b"2024-10-31T16:33:53.123456789+00:00[UTC]");
let deserialized: Zoned = serde_yaml::from_reader(cursor).unwrap();
assert_eq!(deserialized, expected);
}
#[test]
fn zoned_with_time_dst_after_gap() {
if crate::tz::db().is_definitively_empty() {
return;
}
let zdt1: Zoned = "2024-03-31T12:00[Atlantic/Azores]".parse().unwrap();
assert_eq!(
zdt1.to_string(),
"2024-03-31T12:00:00+00:00[Atlantic/Azores]"
);
let zdt2 = zdt1.with().time(Time::midnight()).build().unwrap();
assert_eq!(
zdt2.to_string(),
"2024-03-31T01:00:00+00:00[Atlantic/Azores]"
);
}
#[test]
fn zoned_with_time_dst_us_eastern() {
if crate::tz::db().is_definitively_empty() {
return;
}
let zdt1: Zoned = "2024-03-10T01:30[US/Eastern]".parse().unwrap();
assert_eq!(zdt1.to_string(), "2024-03-10T01:30:00-05:00[US/Eastern]");
let zdt2 = zdt1.with().hour(2).build().unwrap();
assert_eq!(zdt2.to_string(), "2024-03-10T03:30:00-04:00[US/Eastern]");
let zdt1: Zoned = "2024-03-10T03:30[US/Eastern]".parse().unwrap();
assert_eq!(zdt1.to_string(), "2024-03-10T03:30:00-04:00[US/Eastern]");
let zdt2 = zdt1.with().hour(2).build().unwrap();
assert_eq!(zdt2.to_string(), "2024-03-10T03:30:00-04:00[US/Eastern]");
let zdt1: Zoned = "2024-03-10T01:30[US/Eastern]".parse().unwrap();
assert_eq!(zdt1.to_string(), "2024-03-10T01:30:00-05:00[US/Eastern]");
let zdt2 = zdt1
.with()
.offset(tz::offset(10))
.hour(2)
.disambiguation(Disambiguation::Earlier)
.build()
.unwrap();
assert_eq!(zdt2.to_string(), "2024-03-10T01:30:00-05:00[US/Eastern]");
let zdt1: Zoned = "2024-03-10T01:30[US/Eastern]".parse().unwrap();
assert_eq!(zdt1.to_string(), "2024-03-10T01:30:00-05:00[US/Eastern]");
let zdt2 = zdt1
.with()
.hour(2)
.disambiguation(Disambiguation::Earlier)
.build()
.unwrap();
assert_eq!(zdt2.to_string(), "2024-03-10T01:30:00-05:00[US/Eastern]");
}
#[test]
fn zoned_precision_loss() {
if crate::tz::db().is_definitively_empty() {
return;
}
let zdt1: Zoned = "2025-01-25T19:32:21.783444592+01:00[Europe/Paris]"
.parse()
.unwrap();
let span = 1.second();
let zdt2 = &zdt1 + span;
assert_eq!(
zdt2.to_string(),
"2025-01-25T19:32:22.783444592+01:00[Europe/Paris]"
);
assert_eq!(zdt1, &zdt2 - span, "should be reversible");
}
#[test]
fn zoned_roundtrip_regression() {
if crate::tz::db().is_definitively_empty() {
return;
}
let zdt: Zoned =
"2063-03-31T10:00:00+11:00[Australia/Sydney]".parse().unwrap();
assert_eq!(zdt.offset(), super::Offset::constant(11));
let roundtrip = zdt.time_zone().to_zoned(zdt.datetime()).unwrap();
assert_eq!(zdt, roundtrip);
}
#[test]
fn zoned_round_dst_day_length() {
if crate::tz::db().is_definitively_empty() {
return;
}
let zdt1: Zoned =
"2025-03-09T12:15[America/New_York]".parse().unwrap();
let zdt2 = zdt1.round(Unit::Day).unwrap();
assert_eq!(
zdt2.to_string(),
"2025-03-09T00:00:00-05:00[America/New_York]"
);
}
#[test]
fn zoned_round_errors() {
if crate::tz::db().is_definitively_empty() {
return;
}
let zdt: Zoned = "2025-03-09T12:15[America/New_York]".parse().unwrap();
insta::assert_snapshot!(
zdt.round(Unit::Year).unwrap_err(),
@"failed rounding datetime: rounding to 'years' is not supported"
);
insta::assert_snapshot!(
zdt.round(Unit::Month).unwrap_err(),
@"failed rounding datetime: rounding to 'months' is not supported"
);
insta::assert_snapshot!(
zdt.round(Unit::Week).unwrap_err(),
@"failed rounding datetime: rounding to 'weeks' is not supported"
);
let options = ZonedRound::new().smallest(Unit::Day).increment(2);
insta::assert_snapshot!(
zdt.round(options).unwrap_err(),
@"failed rounding datetime: increment for rounding to 'days' must be equal to `1`"
);
}
#[test]
fn time_zone_offset_seconds_exact_match() {
if crate::tz::db().is_definitively_empty() {
return;
}
let zdt: Zoned =
"1970-06-01T00:00:00-00:45[Africa/Monrovia]".parse().unwrap();
assert_eq!(
zdt.to_string(),
"1970-06-01T00:00:00-00:45[Africa/Monrovia]"
);
let zdt: Zoned =
"1970-06-01T00:00:00-00:44:30[Africa/Monrovia]".parse().unwrap();
assert_eq!(
zdt.to_string(),
"1970-06-01T00:00:00-00:45[Africa/Monrovia]"
);
insta::assert_snapshot!(
"1970-06-01T00:00:00-00:44:40[Africa/Monrovia]".parse::<Zoned>().unwrap_err(),
@"datetime could not resolve to a timestamp since `reject` conflict resolution was chosen, and because datetime has offset `-00:44:40`, but the time zone `Africa/Monrovia` for the given datetime unambiguously has offset `-00:44:30`",
);
insta::assert_snapshot!(
"1970-06-01T00:00:00-00:45:00[Africa/Monrovia]".parse::<Zoned>().unwrap_err(),
@"datetime could not resolve to a timestamp since `reject` conflict resolution was chosen, and because datetime has offset `-00:45`, but the time zone `Africa/Monrovia` for the given datetime unambiguously has offset `-00:44:30`",
);
}
#[test]
fn weird_time_zone_transitions() {
if crate::tz::db().is_definitively_empty() {
return;
}
let zdt: Zoned =
"2000-10-08T01:00:00-01:00[America/Noronha]".parse().unwrap();
let sod = zdt.start_of_day().unwrap();
assert_eq!(
sod.to_string(),
"2000-10-08T01:00:00-01:00[America/Noronha]"
);
let zdt: Zoned =
"2000-10-08T03:00:00-03:00[America/Boa_Vista]".parse().unwrap();
let sod = zdt.start_of_day().unwrap();
assert_eq!(
sod.to_string(),
"2000-10-08T01:00:00-03:00[America/Boa_Vista]",
);
}
#[test]
fn no_reject_in_fold_when_using_with() {
if crate::tz::db().is_definitively_empty() {
return;
}
let zdt1: Zoned =
"2016-09-30T02:01+02:00[Europe/Amsterdam]".parse().unwrap();
let zdt2 = zdt1
.with()
.month(10)
.disambiguation(Disambiguation::Reject)
.offset_conflict(OffsetConflict::Reject)
.build()
.unwrap();
assert_eq!(
zdt2.to_string(),
"2016-10-30T02:01:00+02:00[Europe/Amsterdam]"
);
let zdt3: Zoned =
"2016-10-30T02:01+02:00[Europe/Amsterdam]".parse().unwrap();
assert_eq!(
zdt3.to_string(),
"2016-10-30T02:01:00+02:00[Europe/Amsterdam]"
);
let zdt4: Zoned =
"2016-10-30T02:01+01:00[Europe/Amsterdam]".parse().unwrap();
assert_eq!(
zdt4.to_string(),
"2016-10-30T02:01:00+01:00[Europe/Amsterdam]"
);
}
}