use serde::{Deserialize, Serialize};
use std::fmt;
const NANOS_PER_SECOND: i64 = 1_000_000_000;
const NANOS_PER_MINUTE: i64 = 60 * NANOS_PER_SECOND;
const NANOS_PER_HOUR: i64 = 60 * NANOS_PER_MINUTE;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
pub struct Duration {
months: i64,
days: i64,
nanos: i64,
}
impl Duration {
#[must_use]
pub const fn new(months: i64, days: i64, nanos: i64) -> Self {
Self {
months,
days,
nanos,
}
}
#[must_use]
pub const fn from_months(months: i64) -> Self {
Self {
months,
days: 0,
nanos: 0,
}
}
#[must_use]
pub const fn from_days(days: i64) -> Self {
Self {
months: 0,
days,
nanos: 0,
}
}
#[must_use]
pub const fn from_nanos(nanos: i64) -> Self {
Self {
months: 0,
days: 0,
nanos,
}
}
#[must_use]
pub const fn from_seconds(secs: i64) -> Self {
Self {
months: 0,
days: 0,
nanos: secs * NANOS_PER_SECOND,
}
}
#[must_use]
pub const fn months(&self) -> i64 {
self.months
}
#[must_use]
pub const fn days(&self) -> i64 {
self.days
}
#[must_use]
pub const fn nanos(&self) -> i64 {
self.nanos
}
#[must_use]
pub const fn is_zero(&self) -> bool {
self.months == 0 && self.days == 0 && self.nanos == 0
}
#[must_use]
pub const fn neg(self) -> Self {
Self {
months: -self.months,
days: -self.days,
nanos: -self.nanos,
}
}
#[must_use]
pub const fn add(self, other: Self) -> Self {
Self {
months: self.months + other.months,
days: self.days + other.days,
nanos: self.nanos + other.nanos,
}
}
#[must_use]
pub const fn sub(self, other: Self) -> Self {
Self {
months: self.months - other.months,
days: self.days - other.days,
nanos: self.nanos - other.nanos,
}
}
#[must_use]
pub const fn mul(self, factor: i64) -> Self {
Self {
months: self.months * factor,
days: self.days * factor,
nanos: self.nanos * factor,
}
}
#[must_use]
pub const fn div(self, divisor: i64) -> Self {
Self {
months: self.months / divisor,
days: self.days / divisor,
nanos: self.nanos / divisor,
}
}
#[must_use]
pub fn parse(s: &str) -> Option<Self> {
let (negative, s) = if let Some(rest) = s.strip_prefix('-') {
(true, rest)
} else {
(false, s)
};
let s = s.strip_prefix('P')?;
let mut months: i64 = 0;
let mut days: i64 = 0;
let mut nanos: i64 = 0;
let mut in_time = false;
let mut num_start = 0;
let mut has_content = false;
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'T' => {
in_time = true;
i += 1;
num_start = i;
}
b'Y' if !in_time => {
let n: i64 = s[num_start..i].parse().ok()?;
months += n * 12;
has_content = true;
i += 1;
num_start = i;
}
b'M' if !in_time => {
let n: i64 = s[num_start..i].parse().ok()?;
months += n;
has_content = true;
i += 1;
num_start = i;
}
b'W' if !in_time => {
let n: i64 = s[num_start..i].parse().ok()?;
days += n * 7;
has_content = true;
i += 1;
num_start = i;
}
b'D' if !in_time => {
let n: i64 = s[num_start..i].parse().ok()?;
days += n;
has_content = true;
i += 1;
num_start = i;
}
b'H' if in_time => {
let n: i64 = s[num_start..i].parse().ok()?;
nanos += n * NANOS_PER_HOUR;
has_content = true;
i += 1;
num_start = i;
}
b'M' if in_time => {
let n: i64 = s[num_start..i].parse().ok()?;
nanos += n * NANOS_PER_MINUTE;
has_content = true;
i += 1;
num_start = i;
}
b'S' if in_time => {
let text = &s[num_start..i];
if let Some(dot_pos) = text.find('.') {
let int_part: i64 = text[..dot_pos].parse().ok()?;
let frac_str = &text[dot_pos + 1..];
let frac_len = frac_str.len().min(9);
let frac: i64 = frac_str[..frac_len].parse().ok()?;
#[allow(clippy::cast_possible_truncation)]
let scale = 10i64.pow(9 - frac_len as u32);
nanos += int_part * NANOS_PER_SECOND + frac * scale;
} else {
let n: i64 = text.parse().ok()?;
nanos += n * NANOS_PER_SECOND;
}
has_content = true;
i += 1;
num_start = i;
}
_ => {
i += 1;
}
}
}
if !has_content {
return None;
}
let dur = Self {
months,
days,
nanos,
};
Some(if negative { dur.neg() } else { dur })
}
}
impl PartialOrd for Duration {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
let m = self.months.cmp(&other.months);
let d = self.days.cmp(&other.days);
let n = self.nanos.cmp(&other.nanos);
if m == d && d == n {
Some(m)
} else if (m == std::cmp::Ordering::Equal || m == d)
&& (d == std::cmp::Ordering::Equal || d == n)
&& (m == std::cmp::Ordering::Equal || m == n)
{
if m != std::cmp::Ordering::Equal {
Some(m)
} else if d != std::cmp::Ordering::Equal {
Some(d)
} else {
Some(n)
}
} else {
None }
}
}
impl fmt::Debug for Duration {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Duration({})", self)
}
}
impl fmt::Display for Duration {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let (neg, months, days, nanos) = if self.months < 0 || self.days < 0 || self.nanos < 0 {
if self.months <= 0 && self.days <= 0 && self.nanos <= 0 {
(true, -self.months, -self.days, -self.nanos)
} else {
(false, self.months, self.days, self.nanos)
}
} else {
(false, self.months, self.days, self.nanos)
};
if neg {
write!(f, "-")?;
}
write!(f, "P")?;
let years = months / 12;
let m = months % 12;
if years != 0 {
write!(f, "{years}Y")?;
}
if m != 0 {
write!(f, "{m}M")?;
}
if days != 0 {
write!(f, "{days}D")?;
}
let hours = nanos / NANOS_PER_HOUR;
let remaining = nanos % NANOS_PER_HOUR;
let minutes = remaining / NANOS_PER_MINUTE;
let remaining = remaining % NANOS_PER_MINUTE;
let secs = remaining / NANOS_PER_SECOND;
let sub_nanos = remaining % NANOS_PER_SECOND;
if hours != 0 || minutes != 0 || secs != 0 || sub_nanos != 0 {
write!(f, "T")?;
if hours != 0 {
write!(f, "{hours}H")?;
}
if minutes != 0 {
write!(f, "{minutes}M")?;
}
if secs != 0 || sub_nanos != 0 {
if sub_nanos != 0 {
let frac = format!("{:09}", sub_nanos);
let trimmed = frac.trim_end_matches('0');
write!(f, "{secs}.{trimmed}S")?;
} else {
write!(f, "{secs}S")?;
}
}
}
if !neg && years == 0 && m == 0 && days == 0 && nanos == 0 {
write!(f, "T0S")?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_constructors() {
let d = Duration::new(14, 3, 0);
assert_eq!(d.months(), 14);
assert_eq!(d.days(), 3);
assert_eq!(d.nanos(), 0);
assert_eq!(Duration::from_months(6).months(), 6);
assert_eq!(Duration::from_days(10).days(), 10);
assert_eq!(Duration::from_nanos(1_000_000_000).nanos(), 1_000_000_000);
}
#[test]
fn test_parse_full() {
let d = Duration::parse("P1Y2M3DT4H5M6S").unwrap();
assert_eq!(d.months(), 14); assert_eq!(d.days(), 3);
assert_eq!(
d.nanos(),
4 * NANOS_PER_HOUR + 5 * NANOS_PER_MINUTE + 6 * NANOS_PER_SECOND
);
}
#[test]
fn test_parse_partial() {
let d = Duration::parse("P1Y").unwrap();
assert_eq!(d.months(), 12);
assert_eq!(d.days(), 0);
let d = Duration::parse("PT30S").unwrap();
assert_eq!(d.months(), 0);
assert_eq!(d.nanos(), 30 * NANOS_PER_SECOND);
let d = Duration::parse("P2W").unwrap();
assert_eq!(d.days(), 14);
}
#[test]
fn test_parse_fractional_seconds() {
let d = Duration::parse("PT0.5S").unwrap();
assert_eq!(d.nanos(), 500_000_000);
let d = Duration::parse("PT1.123S").unwrap();
assert_eq!(d.nanos(), 1_123_000_000);
}
#[test]
fn test_parse_negative() {
let d = Duration::parse("-P1Y").unwrap();
assert_eq!(d.months(), -12);
}
#[test]
fn test_parse_invalid() {
assert!(Duration::parse("").is_none());
assert!(Duration::parse("P").is_none());
assert!(Duration::parse("not-a-duration").is_none());
}
#[test]
fn test_display_roundtrip() {
let cases = ["P1Y2M3DT4H5M6S", "P1Y", "P3D", "PT30S", "PT0.5S", "P2W"];
for case in cases {
let d = Duration::parse(case).unwrap();
let reparsed = Duration::parse(&d.to_string()).unwrap();
assert_eq!(d, reparsed, "roundtrip failed for {case}");
}
}
#[test]
fn test_arithmetic() {
let a = Duration::new(1, 2, 3);
let b = Duration::new(4, 5, 6);
assert_eq!(a.add(b), Duration::new(5, 7, 9));
assert_eq!(a.sub(b), Duration::new(-3, -3, -3));
assert_eq!(a.mul(3), Duration::new(3, 6, 9));
assert_eq!(a.neg(), Duration::new(-1, -2, -3));
}
#[test]
fn test_partial_ord() {
let a = Duration::new(1, 2, 3);
let b = Duration::new(2, 3, 4);
assert!(a < b);
let c = Duration::new(2, 1, 0);
let d = Duration::new(1, 100, 0);
assert_eq!(c.partial_cmp(&d), None);
}
#[test]
fn test_zero() {
let z = Duration::default();
assert!(z.is_zero());
assert_eq!(z.to_string(), "PT0S");
}
}