use alloc::string::String;
use core::fmt;
use core::str::FromStr;
use crate::calendar::{NS_PER_DAY, NS_PER_HOUR, NS_PER_MIN, NS_PER_SEC};
use crate::error::{Error, Result};
use crate::format;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Duration(i128);
impl Duration {
pub const ZERO: Duration = Duration(0);
pub const fn from_nanos(nanos: i128) -> Duration {
Duration(nanos)
}
pub const fn from_micros(micros: i128) -> Duration {
Duration(micros * 1_000)
}
pub const fn from_millis(millis: i128) -> Duration {
Duration(millis * 1_000_000)
}
pub const fn from_seconds(seconds: i64) -> Duration {
Duration(seconds as i128 * NS_PER_SEC)
}
pub const fn seconds(seconds: i64) -> Duration {
Duration::from_seconds(seconds)
}
pub const fn minutes(minutes: i64) -> Duration {
Duration(minutes as i128 * NS_PER_MIN)
}
pub const fn hours(hours: i64) -> Duration {
Duration(hours as i128 * NS_PER_HOUR)
}
pub const fn days(days: i64) -> Duration {
Duration(days as i128 * NS_PER_DAY)
}
pub const fn from_minutes(minutes: i64) -> Duration {
Duration(minutes as i128 * NS_PER_MIN)
}
pub const fn from_hours(hours: i64) -> Duration {
Duration(hours as i128 * NS_PER_HOUR)
}
pub const fn from_days(days: i64) -> Duration {
Duration(days as i128 * NS_PER_DAY)
}
pub const fn weeks(weeks: i64) -> Duration {
Duration(weeks as i128 * 7 * NS_PER_DAY)
}
pub const fn milliseconds(millis: i64) -> Duration {
Duration::from_millis(millis as i128)
}
pub const fn microseconds(micros: i64) -> Duration {
Duration::from_micros(micros as i128)
}
pub const fn nanoseconds(nanos: i64) -> Duration {
Duration::from_nanos(nanos as i128)
}
pub const fn zero() -> Duration {
Duration::ZERO
}
pub const fn as_nanos(self) -> i128 {
self.0
}
pub const fn as_micros(self) -> i128 {
self.0 / 1_000
}
pub const fn as_millis(self) -> i128 {
self.0 / 1_000_000
}
pub const fn as_seconds(self) -> i128 {
self.0 / NS_PER_SEC
}
pub fn as_seconds_f64(self) -> f64 {
self.0 as f64 / NS_PER_SEC as f64
}
pub fn as_f64(self) -> f64 {
self.0 as f64
}
pub fn to_seconds_f64(self) -> f64 {
self.as_seconds_f64()
}
pub fn num_seconds(self) -> Result<i64> {
i64::try_from(self.as_seconds()).map_err(|_| Error::out_of_range("duration"))
}
pub fn num_milliseconds(self) -> Result<i64> {
i64::try_from(self.as_millis()).map_err(|_| Error::out_of_range("duration"))
}
pub fn num_microseconds(self) -> Result<i64> {
i64::try_from(self.as_micros()).map_err(|_| Error::out_of_range("duration"))
}
pub fn num_nanoseconds(self) -> Result<i64> {
i64::try_from(self.0).map_err(|_| Error::out_of_range("duration"))
}
pub fn num_minutes(self) -> Result<i64> {
i64::try_from(self.0 / NS_PER_MIN).map_err(|_| Error::out_of_range("duration"))
}
pub fn num_hours(self) -> Result<i64> {
i64::try_from(self.0 / NS_PER_HOUR).map_err(|_| Error::out_of_range("duration"))
}
pub fn num_days(self) -> Result<i64> {
i64::try_from(self.0 / NS_PER_DAY).map_err(|_| Error::out_of_range("duration"))
}
pub fn num_weeks(self) -> Result<i64> {
i64::try_from(self.0 / (7 * NS_PER_DAY)).map_err(|_| Error::out_of_range("duration"))
}
pub const fn is_zero(self) -> bool {
self.0 == 0
}
pub const fn is_positive(self) -> bool {
self.0 > 0
}
pub const fn is_negative(self) -> bool {
self.0 < 0
}
pub fn checked_abs(self) -> Result<Duration> {
if self.0 == i128::MIN {
return Err(Error::overflow());
}
Ok(Duration(if self.0 < 0 { -self.0 } else { self.0 }))
}
pub const fn unsigned_abs(self) -> u128 {
self.0.unsigned_abs()
}
pub fn checked_neg(self) -> Result<Duration> {
self.0
.checked_neg()
.map(Duration)
.ok_or_else(Error::overflow)
}
pub fn checked_add(self, rhs: Duration) -> Result<Duration> {
self.0
.checked_add(rhs.0)
.map(Duration)
.ok_or_else(Error::overflow)
}
pub fn checked_sub(self, rhs: Duration) -> Result<Duration> {
self.0
.checked_sub(rhs.0)
.map(Duration)
.ok_or_else(Error::overflow)
}
pub fn checked_mul(self, rhs: i128) -> Result<Duration> {
self.0
.checked_mul(rhs)
.map(Duration)
.ok_or_else(Error::overflow)
}
pub fn checked_div(self, rhs: i128) -> Result<Duration> {
if rhs == 0 {
return Err(Error::invalid("division by zero"));
}
Ok(Duration(self.0 / rhs))
}
pub fn checked_div_duration(self, rhs: Duration) -> Result<i128> {
if rhs.0 == 0 {
return Err(Error::invalid("division by zero"));
}
Ok(self.0 / rhs.0)
}
pub fn saturating_add(self, rhs: Duration) -> Duration {
Duration(self.0.saturating_add(rhs.0))
}
pub fn saturating_sub(self, rhs: Duration) -> Duration {
Duration(self.0.saturating_sub(rhs.0))
}
pub fn saturating_mul(self, rhs: i128) -> Duration {
Duration(self.0.saturating_mul(rhs))
}
pub fn from_std(d: core::time::Duration) -> Duration {
Duration(d.as_secs() as i128 * NS_PER_SEC + d.subsec_nanos() as i128)
}
pub fn to_std(self) -> Result<core::time::Duration> {
if self.0 < 0 {
return Err(Error::invalid("negative duration"));
}
let secs = self.0.div_euclid(NS_PER_SEC);
let nanos = self.0.rem_euclid(NS_PER_SEC);
let secs = u64::try_from(secs).map_err(|_| Error::out_of_range("duration"))?;
Ok(core::time::Duration::new(secs, nanos as u32))
}
pub fn to_iso8601(self) -> String {
format::format_duration_iso(self)
}
pub fn from_iso8601(s: &str) -> Result<Duration> {
format::parse_duration_iso(s)
}
}
impl fmt::Display for Duration {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_iso8601())
}
}
impl FromStr for Duration {
type Err = Error;
fn from_str(s: &str) -> Result<Duration> {
Duration::from_iso8601(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn units() {
assert_eq!(Duration::from_minutes(2), Duration::from_seconds(120));
assert_eq!(Duration::from_hours(1), Duration::from_seconds(3600));
assert_eq!(Duration::from_days(1), Duration::from_seconds(86_400));
assert_eq!(Duration::from_millis(1500).as_micros(), 1_500_000);
assert_eq!(Duration::from_seconds(5).as_nanos(), 5_000_000_000);
assert_eq!(Duration::from_nanos(-1).as_seconds(), 0); }
#[test]
fn constructors_never_overflow() {
assert_eq!(
Duration::from_days(i64::MAX).as_nanos(),
i64::MAX as i128 * NS_PER_DAY
);
assert_eq!(
Duration::from_minutes(i64::MAX).as_nanos(),
i64::MAX as i128 * NS_PER_MIN
);
assert_eq!(
Duration::from_hours(i64::MAX).as_nanos(),
i64::MAX as i128 * NS_PER_HOUR
);
assert_eq!(
Duration::weeks(i64::MAX).as_nanos(),
i64::MAX as i128 * 7 * NS_PER_DAY
);
assert_eq!(
Duration::minutes(i64::MAX),
Duration::from_minutes(i64::MAX)
);
assert_eq!(Duration::hours(i64::MAX), Duration::from_hours(i64::MAX));
assert_eq!(Duration::days(i64::MAX), Duration::from_days(i64::MAX));
assert_eq!(Duration::from_days(-1).as_nanos(), -NS_PER_DAY);
assert_eq!(Duration::from_minutes(-1).as_nanos(), -NS_PER_MIN);
}
#[test]
fn std_bridge() {
let d = Duration::from_std(core::time::Duration::new(1, 500));
assert_eq!(d, Duration::from_nanos(1_000_000_500));
assert_eq!(d.to_std().unwrap(), core::time::Duration::new(1, 500));
assert!(Duration::from_nanos(-1).to_std().is_err());
}
#[test]
fn iso_round_trips() {
for text in [
"PT0S",
"PT1S",
"PT1.5S",
"PT0.5S",
"PT1M30S",
"PT1H",
"P1D",
"P1DT2H3M4.5S",
"-PT1S",
"P1DT0.25S",
] {
let d = Duration::from_iso8601(text).unwrap_or_else(|e| panic!("{text}: {e}"));
assert_eq!(d.to_iso8601(), text, "{text}");
}
assert_eq!(
Duration::from_iso8601("P2W").unwrap().as_nanos(),
1_209_600_000_000_000
);
assert_eq!(Duration::from_iso8601("P2W").unwrap().to_iso8601(), "P14D");
assert_eq!(Duration::from_iso8601("P1W").unwrap().to_iso8601(), "P7D");
}
#[test]
fn iso_rejects_calendar_ambiguous() {
for s in [
"P1Y", "P1M", "P", "PT", "P1S", "P1DT", "P1DT5", "abc", "PT1H2D",
] {
assert!(Duration::from_iso8601(s).is_err(), "{s} should fail");
}
}
}