use alloc::string::String;
use core::fmt;
use core::str::FromStr;
use crate::calendar::{Weekday, NS_PER_DAY, NS_PER_SEC};
use crate::date::Date;
use crate::datetime::CivilDateTime;
use crate::duration::Duration;
use crate::error::{Error, Result};
use crate::format::{self, FractionDigits};
use crate::offset::Offset;
use crate::strftime;
use crate::time::TimeOfDay;
use crate::units::{Days, Months};
use crate::zone::Zone;
use crate::zoned::Zoned;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Ticks(i128);
impl Ticks {
pub const EPOCH: Ticks = Ticks(0);
pub const MIN: Ticks = Ticks(i128::MIN);
pub const MAX: Ticks = Ticks(i128::MAX);
pub const fn from_unix_nanos(nanos: i128) -> Ticks {
Ticks(nanos)
}
pub const fn from_unix_seconds(seconds: i64, nanos: u32) -> Result<Ticks> {
if nanos >= 1_000_000_000 {
return Err(Error::out_of_range("nanosecond"));
}
Ok(Ticks(seconds as i128 * NS_PER_SEC + nanos as i128))
}
pub fn from_timestamp(seconds: i64, nanos: u32) -> Result<Ticks> {
Ticks::from_unix_seconds(seconds, nanos)
}
pub fn from_timestamp_millis(millis: i64) -> Result<Ticks> {
let ns = millis as i128 * 1_000_000;
Ok(Ticks::from_unix_nanos(ns))
}
pub fn from_timestamp_micros(micros: i64) -> Result<Ticks> {
let ns = micros as i128 * 1_000;
Ok(Ticks::from_unix_nanos(ns))
}
pub const fn from_timestamp_nanos(nanos: i128) -> Ticks {
Ticks::from_unix_nanos(nanos)
}
pub fn timestamp(self) -> Result<i64> {
let secs = self.0.div_euclid(NS_PER_SEC);
i64::try_from(secs).map_err(|_| Error::out_of_range("instant"))
}
pub fn timestamp_millis(self) -> Result<i64> {
let ms = self.0.div_euclid(1_000_000);
i64::try_from(ms).map_err(|_| Error::out_of_range("instant"))
}
pub fn timestamp_micros(self) -> Result<i64> {
let us = self.0.div_euclid(1_000);
i64::try_from(us).map_err(|_| Error::out_of_range("instant"))
}
pub fn timestamp_nanos(self) -> Result<i64> {
i64::try_from(self.0).map_err(|_| Error::out_of_range("instant"))
}
pub const fn as_unix_nanos(self) -> i128 {
self.0
}
pub fn to_unix_seconds(self) -> Result<(i64, u32)> {
let secs = self.0.div_euclid(NS_PER_SEC);
let nanos = self.0.rem_euclid(NS_PER_SEC);
let secs = i64::try_from(secs).map_err(|_| Error::out_of_range("instant"))?;
Ok((secs, nanos as u32))
}
#[cfg(feature = "std")]
pub fn now() -> Result<Ticks> {
let d = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_err(|_| Error::invalid("system clock precedes the unix epoch"))?;
Ok(Ticks(d.as_nanos() as i128))
}
#[cfg(feature = "std")]
pub fn to_std_time(self) -> Result<std::time::SystemTime> {
let (secs, nanos) = self.to_unix_seconds()?;
if secs < 0 {
return Err(Error::out_of_range("instant (before 1970)"));
}
Ok(std::time::UNIX_EPOCH + std::time::Duration::new(secs as u64, nanos))
}
pub fn checked_add(self, delta: Duration) -> Result<Ticks> {
self.0
.checked_add(delta.as_nanos())
.map(Ticks)
.ok_or_else(Error::overflow)
}
pub fn checked_sub(self, delta: Duration) -> Result<Ticks> {
self.0
.checked_sub(delta.as_nanos())
.map(Ticks)
.ok_or_else(Error::overflow)
}
pub fn checked_add_signed(self, rhs: Duration) -> Result<Ticks> {
self.checked_add(rhs)
}
pub fn checked_sub_signed(self, rhs: Duration) -> Result<Ticks> {
self.checked_sub(rhs)
}
pub fn checked_add_days(self, days: Days) -> Result<Ticks> {
let days = i64::try_from(days.get()).map_err(|_| Error::out_of_range("days"))?;
self.checked_add(Duration::from_days(days))
}
pub fn checked_sub_days(self, days: Days) -> Result<Ticks> {
let days = i64::try_from(days.get()).map_err(|_| Error::out_of_range("days"))?;
self.checked_sub(Duration::from_days(days))
}
pub fn saturating_add(self, delta: Duration) -> Ticks {
Ticks(self.0.saturating_add(delta.as_nanos()))
}
pub fn saturating_sub(self, delta: Duration) -> Ticks {
Ticks(self.0.saturating_sub(delta.as_nanos()))
}
pub fn duration_since(self, earlier: Ticks) -> Duration {
Duration::from_nanos(self.0.saturating_sub(earlier.0))
}
pub fn checked_add_months(self, months: Months) -> Result<Ticks> {
let dt = self.to_civil_utc()?;
let dt = dt.checked_add_months(months)?;
dt.to_ticks_utc()
}
pub fn checked_sub_months(self, months: Months) -> Result<Ticks> {
let dt = self.to_civil_utc()?;
let dt = dt.checked_sub_months(months)?;
dt.to_ticks_utc()
}
pub fn checked_add_years(self, years: i32) -> Result<Ticks> {
let dt = self.to_civil_utc()?;
let dt = dt.checked_add_years(years)?;
dt.to_ticks_utc()
}
pub fn to_civil_utc(self) -> Result<CivilDateTime> {
let days = self.0.div_euclid(NS_PER_DAY);
let rem = self.0.rem_euclid(NS_PER_DAY);
let days = i64::try_from(days).map_err(|_| Error::out_of_range("instant"))?;
let date = Date::from_days_checked(days)?;
let time = TimeOfDay::from_nanos_since_midnight(rem as u64)?;
Ok(CivilDateTime::new(date, time))
}
pub fn date_utc(self) -> Result<Date> {
Ok(self.to_civil_utc()?.date())
}
pub fn time_utc(self) -> Result<TimeOfDay> {
Ok(self.to_civil_utc()?.time())
}
pub fn weekday_utc(self) -> Result<Weekday> {
Ok(self.to_civil_utc()?.weekday())
}
pub fn to_zoned(self, zone: Zone) -> Zoned {
Zoned::new(self, zone)
}
pub fn to_rfc3339(self, fraction: FractionDigits) -> String {
match self.to_civil_utc() {
Ok(dt) => {
let mut out = alloc::string::String::new();
format::format_rfc3339_into(&mut out, dt.date(), dt.time(), Offset::UTC, fraction);
out
}
Err(_) => alloc::format!("{}s", self.0),
}
}
pub fn from_rfc3339(s: &str) -> Result<Ticks> {
let (date, time, offset) = format::parse_rfc3339(s)?;
let dt = CivilDateTime::new(date, time);
let utc = dt.to_ticks_utc()?;
utc.checked_sub(Duration::from_seconds(offset.as_seconds() as i64))
}
pub fn format(self, fmt: &str) -> Result<String> {
strftime::format_ticks(self, fmt)
}
pub fn parse_from_str(s: &str, fmt: &str) -> Result<Ticks> {
strftime::parse_ticks(fmt, s)
}
pub fn to_rfc2822(self) -> String {
match self.to_civil_utc() {
Ok(dt) => strftime::format_rfc2822(dt.date(), dt.time(), Offset::UTC),
Err(_) => alloc::format!("{} +0000", self.0),
}
}
pub fn from_rfc2822(s: &str) -> Result<Ticks> {
let (date, time, offset) = strftime::parse_rfc2822(s)?;
let utc = CivilDateTime::new(date, time).to_ticks_utc()?;
utc.checked_sub(Duration::from_seconds(offset.as_seconds() as i64))
}
}
impl fmt::Display for Ticks {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_rfc3339(FractionDigits::Auto))
}
}
impl FromStr for Ticks {
type Err = Error;
fn from_str(s: &str) -> Result<Ticks> {
Ticks::from_rfc3339(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn epoch_parts() {
let t = Ticks::EPOCH;
assert_eq!(t.to_unix_seconds().unwrap(), (0, 0));
let dt = t.to_civil_utc().unwrap();
assert_eq!((dt.year(), dt.month(), dt.day()), (1970, 1, 1));
assert_eq!(dt.hour(), 0);
}
#[test]
fn add_duration_crosses_midnight() {
let t = Ticks::from_unix_seconds(86_399, 500_000_000).unwrap();
let next = t
.checked_add(Duration::from_seconds(1))
.unwrap()
.to_civil_utc()
.unwrap();
assert_eq!((next.year(), next.month(), next.day()), (1970, 1, 2));
assert_eq!(next.time().hour(), 0);
}
#[test]
fn sub_seconds_floor() {
let t = Ticks::from_unix_seconds(0, 0)
.unwrap()
.checked_sub(Duration::from_nanos(1))
.unwrap();
assert_eq!(t.to_unix_seconds().unwrap(), (-1, 999_999_999));
let (y, m, d) = t.to_civil_utc().unwrap().date().parts();
assert_eq!((y, m, d), (1969, 12, 31));
}
#[test]
fn calendar_months_clamp() {
let jan31 = Ticks::from_rfc3339("2023-01-31T12:00:00Z").unwrap();
let feb = jan31.checked_add_months(Months::new(1)).unwrap();
assert_eq!(feb.to_rfc3339(FractionDigits::None), "2023-02-28T12:00:00Z");
let leap = Ticks::from_rfc3339("2024-01-31T12:00:00Z").unwrap();
assert_eq!(
leap.checked_add_months(Months::new(1))
.unwrap()
.to_rfc3339(FractionDigits::None),
"2024-02-29T12:00:00Z"
);
assert_eq!(
jan31
.checked_add_years(2)
.unwrap()
.to_rfc3339(FractionDigits::None),
"2025-01-31T12:00:00Z"
);
}
#[test]
fn timestamp_helpers() {
let t = Ticks::from_timestamp(1_700_000_000, 123_456_789).unwrap();
assert_eq!(t.timestamp().unwrap(), 1_700_000_000);
assert_eq!(t.timestamp_millis().unwrap(), 1_700_000_000_123);
assert_eq!(t.timestamp_micros().unwrap(), 1_700_000_000_123_456);
assert_eq!(t.timestamp_nanos().unwrap(), 1_700_000_000_123_456_789);
assert_eq!(
Ticks::from_timestamp_millis(1_700_000_000_123).unwrap(),
Ticks::from_unix_nanos(1_700_000_000_123_000_000)
);
assert_eq!(
Ticks::from_timestamp_micros(1_700_000_000_123_456).unwrap(),
Ticks::from_unix_nanos(1_700_000_000_123_456_000)
);
assert_eq!(Ticks::from_timestamp_nanos(1_700_000_000_123_456_789), t);
let before = Ticks::EPOCH
.checked_sub(Duration::from_millis(500))
.unwrap();
assert_eq!(before.timestamp().unwrap(), -1);
assert!(Ticks::from_timestamp(0, 1_000_000_000).is_err());
}
#[test]
fn rfc3339_round_trips() {
for s in [
"1970-01-01T00:00:00Z",
"2024-02-29T23:59:59.5Z",
"2024-02-29T23:59:59.123456789Z",
"2024-06-15T08:30:00+08:00",
"2024-06-15T00:30:00+08:00",
"2024-06-14T20:30:00-04:00",
"2024-06-15T08:30:00+0530",
"2024-06-15T08:30:00+05",
"1969-12-31T23:59:59Z",
"2021-01-01t00:00:00z",
] {
let t = Ticks::from_rfc3339(s).unwrap_or_else(|e| panic!("{s}: {e}"));
let (date, time, off) = format::parse_rfc3339(s).unwrap();
let shifted = t
.checked_add(Duration::from_seconds(off.as_seconds() as i64))
.unwrap();
let mut expect = alloc::string::String::new();
format::format_rfc3339_into(&mut expect, date, time, off, FractionDigits::Auto);
let mut got = alloc::string::String::new();
format::format_rfc3339_into(
&mut got,
shifted.to_civil_utc().unwrap().date(),
shifted.to_civil_utc().unwrap().time(),
off,
FractionDigits::Auto,
);
assert_eq!(got, expect, "{s}");
}
}
#[test]
fn rfc3339_rejects_garbage() {
for s in [
"",
"2024",
"2024-01-01",
"2024-01-01T12:00:00",
"2024-13-01T00:00:00Z",
"2024-01-32T00:00:00Z",
"2024-01-01T24:00:00Z",
"2024-01-01T12:00:00+24:00",
"2024-01-01T12:00:00.1234567890Z",
"2024-01-01T12:00:00Z trailing",
"abcd-01-01T00:00:00Z",
] {
assert!(Ticks::from_rfc3339(s).is_err(), "{s} should fail");
}
}
#[test]
fn duration_since_sign() {
let a = Ticks::from_unix_seconds(10, 0).unwrap();
let b = Ticks::from_unix_seconds(20, 0).unwrap();
assert_eq!(b.duration_since(a), Duration::from_seconds(10));
assert_eq!(a.duration_since(b), Duration::from_seconds(-10));
}
#[cfg(feature = "std")]
#[test]
fn now_is_sane() {
let now = Ticks::now().unwrap();
let year = now.to_civil_utc().unwrap().year();
assert!((2000..=3000).contains(&year));
}
}