use alloc::string::String;
use core::fmt;
use core::str::FromStr;
use crate::date::Date;
use crate::datetime::CivilDateTime;
use crate::duration::Duration;
use crate::error::Result;
use crate::format::{self, FractionDigits};
use crate::offset::Offset;
use crate::strftime;
use crate::ticks::Ticks;
use crate::time::TimeOfDay;
use crate::units::{Days, Months};
use crate::zone::Zone;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Zoned {
ticks: Ticks,
zone: Zone,
}
impl Zoned {
pub fn new(ticks: Ticks, zone: Zone) -> Zoned {
Zoned { ticks, zone }
}
pub fn from_ticks(ticks: Ticks, zone: Zone) -> Zoned {
Zoned::new(ticks, zone)
}
pub fn from_civil(civil: CivilDateTime, zone: Zone) -> Result<Zoned> {
let utc = civil.to_ticks_utc()?;
let shifted = utc.checked_sub(Duration::from_seconds(zone.offset().as_seconds() as i64))?;
Ok(Zoned {
ticks: shifted,
zone,
})
}
pub const fn ticks(self) -> Ticks {
self.ticks
}
pub const fn zone(self) -> Zone {
self.zone
}
pub const fn offset(self) -> Offset {
self.zone.offset()
}
pub const fn to_utc(self) -> Ticks {
self.ticks
}
pub fn civil(self) -> Result<CivilDateTime> {
let shifted = self
.ticks
.checked_add(Duration::from_seconds(self.offset().as_seconds() as i64))?;
shifted.to_civil_utc()
}
pub fn date(self) -> Result<Date> {
Ok(self.civil()?.date())
}
pub fn time(self) -> Result<TimeOfDay> {
Ok(self.civil()?.time())
}
pub fn checked_add(self, delta: Duration) -> Result<Zoned> {
Ok(Zoned {
ticks: self.ticks.checked_add(delta)?,
zone: self.zone,
})
}
pub fn checked_sub(self, delta: Duration) -> Result<Zoned> {
self.checked_add(delta.checked_neg()?)
}
pub fn checked_add_signed(self, rhs: Duration) -> Result<Zoned> {
self.checked_add(rhs)
}
pub fn checked_sub_signed(self, rhs: Duration) -> Result<Zoned> {
self.checked_sub(rhs)
}
pub fn signed_duration_since(self, earlier: Zoned) -> Duration {
self.ticks.duration_since(earlier.ticks)
}
pub fn checked_add_days(self, days: Days) -> Result<Zoned> {
let days =
i64::try_from(days.get()).map_err(|_| crate::error::Error::out_of_range("days"))?;
self.checked_add(Duration::from_days(days))
}
pub fn checked_sub_days(self, days: Days) -> Result<Zoned> {
let days =
i64::try_from(days.get()).map_err(|_| crate::error::Error::out_of_range("days"))?;
self.checked_sub(Duration::from_days(days))
}
pub fn checked_add_months(self, months: Months) -> Result<Zoned> {
let civil = self.civil()?.checked_add_months(months)?;
Zoned::from_civil(civil, self.zone)
}
pub fn checked_sub_months(self, months: Months) -> Result<Zoned> {
let civil = self.civil()?.checked_sub_months(months)?;
Zoned::from_civil(civil, self.zone)
}
pub fn checked_add_years(self, years: i32) -> Result<Zoned> {
let civil = self.civil()?.checked_add_years(years)?;
Zoned::from_civil(civil, self.zone)
}
pub fn timestamp(self) -> Result<i64> {
self.ticks.timestamp()
}
pub fn timestamp_millis(self) -> Result<i64> {
self.ticks.timestamp_millis()
}
pub fn timestamp_micros(self) -> Result<i64> {
self.ticks.timestamp_micros()
}
pub fn timestamp_nanos(self) -> Result<i64> {
self.ticks.timestamp_nanos()
}
#[cfg(feature = "std")]
pub fn now_utc() -> Result<Zoned> {
Ok(Zoned::new(Ticks::now()?, Zone::Utc))
}
#[cfg(feature = "std")]
pub fn now() -> Result<Zoned> {
Zoned::now_utc()
}
pub fn with_zone(self, zone: Zone) -> Zoned {
Zoned {
ticks: self.ticks,
zone,
}
}
pub fn to_rfc3339(self, fraction: FractionDigits) -> String {
match self.civil() {
Ok(dt) => {
let mut out = alloc::string::String::new();
format::format_rfc3339_into(
&mut out,
dt.date(),
dt.time(),
self.offset(),
fraction,
);
out
}
Err(_) => alloc::format!("{}{}", self.ticks.as_unix_nanos(), self.offset().to_iso()),
}
}
pub fn from_rfc3339(s: &str) -> Result<Zoned> {
let (date, time, offset) = format::parse_rfc3339(s)?;
Zoned::from_civil(CivilDateTime::new(date, time), Zone::fixed(offset))
}
pub fn format(self, fmt: &str) -> Result<String> {
strftime::format_zoned(self, fmt)
}
pub fn parse_from_str(s: &str, fmt: &str) -> Result<Zoned> {
strftime::parse_zoned(fmt, s)
}
pub fn to_rfc2822(self) -> String {
match self.civil() {
Ok(dt) => strftime::format_rfc2822(dt.date(), dt.time(), self.offset()),
Err(_) => alloc::format!("{}{}", self.ticks.as_unix_nanos(), self.offset().to_iso()),
}
}
pub fn from_rfc2822(s: &str) -> Result<Zoned> {
let (date, time, offset) = strftime::parse_rfc2822(s)?;
Zoned::from_civil(CivilDateTime::new(date, time), Zone::fixed(offset))
}
}
impl fmt::Display for Zoned {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_rfc3339(FractionDigits::Auto))
}
}
impl FromStr for Zoned {
type Err = crate::error::Error;
fn from_str(s: &str) -> Result<Zoned> {
Zoned::from_rfc3339(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::Error;
#[test]
fn civil_anchor_and_projection() {
let zone = Zone::fixed(Offset::from_hms(8, 0, 0).unwrap());
let civil = CivilDateTime::from_ymd_hms(2024, 6, 15, 12, 0, 0).unwrap();
let zoned = Zoned::from_civil(civil, zone).unwrap();
assert_eq!(
zoned.to_utc().to_rfc3339(FractionDigits::None),
"2024-06-15T04:00:00Z"
);
assert_eq!(zoned.civil().unwrap(), civil);
assert_eq!(zoned.date().unwrap(), civil.date());
assert_eq!(zoned.time().unwrap(), civil.time());
}
#[test]
fn zone_aware_month_math() {
let zone = Zone::fixed(Offset::from_hms(-5, 0, 0).unwrap());
let zoned = Zoned::from_civil(
CivilDateTime::from_ymd_hms(2024, 1, 31, 12, 0, 0).unwrap(),
zone,
)
.unwrap();
let next = zoned.checked_add_months(Months::new(1)).unwrap();
assert_eq!(next.civil().unwrap().to_iso(), "2024-02-29T12:00:00");
assert_eq!(
zoned
.checked_sub_months(Months::new(1))
.unwrap()
.civil()
.unwrap()
.to_iso(),
"2023-12-31T12:00:00"
);
}
#[test]
fn strftime_round_trips() {
let z = Zoned::from_rfc3339("2024-06-15T07:00:00+08:00").unwrap();
assert_eq!(
z.format("%Y-%m-%d %H:%M:%S %z").unwrap(),
"2024-06-15 07:00:00 +0800"
);
assert_eq!(
z.format("%A %e %B %Y %I:%M %p").unwrap(),
"Saturday 15 June 2024 07:00 AM"
);
assert_eq!(
Zoned::parse_from_str("2024-06-15 07:00:00 +0800", "%Y-%m-%d %H:%M:%S %z").unwrap(),
z
);
assert_eq!(
z.timestamp().unwrap(),
Ticks::from_rfc3339("2024-06-14T23:00:00Z")
.unwrap()
.timestamp()
.unwrap()
);
}
#[test]
fn rfc3339_round_trips() {
for s in [
"2024-06-15T12:00:00Z",
"2024-06-15T12:00:00+08:00",
"2024-06-15T12:00:00.5-05:30",
"2024-06-15T12:00:00-04:00",
] {
let z = Zoned::from_rfc3339(s).unwrap_or_else(|e| panic!("{s}: {e}"));
assert_eq!(z.to_rfc3339(FractionDigits::Auto), s, "{s}");
}
let z = Zoned::from_rfc3339("2024-06-15T12:00:00+00:00").unwrap();
assert_eq!(z.zone(), Zone::Utc);
assert_eq!(z.to_rfc3339(FractionDigits::None), "2024-06-15T12:00:00Z");
assert_eq!(
Zoned::from_rfc3339("2024-06-15T12:00:00"),
Err(Error::parse("expected a timezone offset", 19))
);
}
#[test]
fn with_zone_keeps_instant() {
let z1 = Zoned::from_rfc3339("2024-06-15T12:00:00+08:00").unwrap();
let z2 = z1.with_zone(Zone::Utc);
assert_eq!(z1.to_utc(), z2.to_utc());
assert_eq!(z2.to_rfc3339(FractionDigits::None), "2024-06-15T04:00:00Z");
}
}