use rust_decimal::prelude::ToPrimitive as _;
use rust_decimal::Decimal;
use serde::de::Error as DeError;
use serde::de::Visitor;
use serde::Deserialize;
use serde::Deserializer;
use serde::Serialize;
use serde::Serializer;
use std::convert::TryFrom;
use std::error::Error as StdError;
use std::fmt::Display;
use std::fmt::Formatter;
use std::fmt::Result as FmtResult;
use std::str::Chars;
use std::str::FromStr;
use std::time::Duration as StdDuration;
#[derive(Debug, Clone, Copy)]
#[repr(transparent)]
pub struct EdmDuration(Decimal);
impl EdmDuration {
#[must_use]
pub fn as_f64_seconds(&self) -> f64 {
Self::decimal_to_f64_lossy(self.0)
}
#[must_use]
pub const fn as_decimal(&self) -> Decimal {
self.0
}
fn take_digits<'a>(chars: &Chars<'a>) -> (&'a str, Option<char>, Chars<'a>) {
let s = chars.as_str();
for (i, ch) in s.char_indices() {
if ch.is_ascii_digit() || ch == '.' {
continue;
}
let digits = &s[..i];
let rest = &s[i + ch.len_utf8()..];
return (digits, Some(ch), rest.chars());
}
(s, None, "".chars())
}
fn decimal_to_f64_lossy(d: Decimal) -> f64 {
d.to_f64().unwrap_or_else(|| {
if d.is_sign_negative() {
f64::NEG_INFINITY
} else {
f64::INFINITY
}
})
}
fn div_with_reminder(v: Decimal, d: Decimal) -> (Decimal, Decimal) {
let reminder = v % d;
((v - reminder) / d, reminder)
}
}
#[derive(Debug)]
pub enum Error {
InvalidEdmDuration(String),
Overflow(String),
CannotConvertNegativeEdmDuration,
ValueTooBig,
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self {
Self::InvalidEdmDuration(v) => write!(f, "invalid duration: {v}"),
Self::Overflow(v) => write!(f, "invalid duration: number overflow: {v}"),
Self::CannotConvertNegativeEdmDuration => "cannot convert negative duration".fmt(f),
Self::ValueTooBig => "duration: value to big".fmt(f),
}
}
}
impl StdError for Error {}
impl TryFrom<EdmDuration> for StdDuration {
type Error = Error;
fn try_from(v: EdmDuration) -> Result<Self, Error> {
if v.0.is_sign_negative() {
return Err(Error::CannotConvertNegativeEdmDuration);
}
if v.0.is_integer() {
let p = u64::try_from(v.0).map_err(|_| Error::ValueTooBig)?;
return Ok(Self::from_secs(p));
}
let v =
v.0.checked_mul(Decimal::ONE_THOUSAND)
.ok_or(Error::ValueTooBig)?;
if v.is_integer() {
let p = u64::try_from(v).map_err(|_| Error::ValueTooBig)?;
return Ok(Self::from_millis(p));
}
let v = v
.checked_mul(Decimal::ONE_THOUSAND)
.ok_or(Error::ValueTooBig)?;
if v.is_integer() {
let p = u64::try_from(v).map_err(|_| Error::ValueTooBig)?;
return Ok(Self::from_micros(p));
}
let v = v
.checked_mul(Decimal::ONE_THOUSAND)
.ok_or(Error::ValueTooBig)?
.round();
let p = u64::try_from(v).map_err(|_| Error::ValueTooBig)?;
Ok(Self::from_nanos(p))
}
}
impl FromStr for EdmDuration {
type Err = Error;
fn from_str(v: &str) -> Result<Self, Error> {
let mut chars = v.chars();
let make_err = || Error::InvalidEdmDuration(v.into());
let overflow_err = || Error::Overflow(v.into());
let maybe_sign = chars.next().ok_or_else(make_err)?;
let (neg, p) = if maybe_sign == '-' {
(Decimal::NEGATIVE_ONE, chars.next().ok_or_else(make_err)?)
} else {
(Decimal::ONE, maybe_sign)
};
(p == 'P').then_some(()).ok_or_else(make_err)?;
let to_decimal = |val: &str, mul| {
Decimal::from_str_exact(val)
.map(|d| d * Decimal::from(mul))
.map_err(|_| make_err())
};
let mut result = Decimal::ZERO;
let (val, maybe_next, mut chars) = Self::take_digits(&chars);
match maybe_next {
Some('T') => (),
Some('D') => match chars.next() {
Some('T') => {
result = result
.checked_add(to_decimal(val, 3600 * 24)?)
.ok_or_else(overflow_err)?;
}
None => return to_decimal(val, 3600 * 24).map(|v| Self(v * neg)),
_ => Err(make_err())?,
},
_ => Err(make_err())?,
}
loop {
let (val, maybe_next, new_chars) = Self::take_digits(&chars);
chars = new_chars;
let mul = match maybe_next {
Some('H') => 3600,
Some('M') => 60,
Some('S') => 1,
Some(_) => Err(make_err())?,
None => break,
};
result = result
.checked_add(to_decimal(val, mul)?)
.ok_or_else(overflow_err)?;
}
Ok(Self(result * neg))
}
}
impl Display for EdmDuration {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
if self.0 == Decimal::ZERO {
return write!(f, "PT0S");
}
let value = if self.0.is_sign_negative() {
write!(f, "-P")?;
-self.0
} else {
write!(f, "P")?;
self.0
};
let (days, value) = Self::div_with_reminder(value, Decimal::from(24 * 3600));
if days != Decimal::ZERO {
write!(f, "{}D", days.normalize())?;
}
if value != Decimal::ZERO {
write!(f, "T")?;
let (hours, value) = Self::div_with_reminder(value, Decimal::from(3600));
if hours != Decimal::ZERO {
write!(f, "{}H", hours.normalize())?;
}
let (mins, value) = Self::div_with_reminder(value, Decimal::from(60));
if mins != Decimal::ZERO {
write!(f, "{}M", mins.normalize())?;
}
write!(f, "{}S", value.normalize())?;
}
Ok(())
}
}
impl<'de> Deserialize<'de> for EdmDuration {
fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
struct ValVisitor {}
impl Visitor<'_> for ValVisitor {
type Value = EdmDuration;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> FmtResult {
formatter.write_str("Edm.Duration string")
}
fn visit_str<E: DeError>(self, value: &str) -> Result<Self::Value, E> {
value.parse().map_err(DeError::custom)
}
}
de.deserialize_string(ValVisitor {})
}
}
impl Serialize for EdmDuration {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.to_string().serialize(serializer)
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal::Decimal;
fn dec(s: &str) -> Decimal {
Decimal::from_str_exact(s).unwrap()
}
#[test]
fn parses_time_only_hms() {
let d = EdmDuration::from_str("PT1H2M3S").unwrap();
assert_eq!(d.0, Decimal::from(3600 + 120 + 3));
}
#[test]
fn parses_day_only() {
let d = EdmDuration::from_str("P3D").unwrap();
assert_eq!(d.0, Decimal::from(3 * 86400));
}
#[test]
fn parses_day_and_time() {
let d = EdmDuration::from_str("P1DT1H").unwrap();
assert_eq!(d.0, Decimal::from(86400 + 3600));
}
#[test]
fn parses_fractional_seconds() {
let d = EdmDuration::from_str("PT0.25S").unwrap();
assert_eq!(d.0, dec("0.25"));
}
#[test]
fn parses_fractional_minutes_and_days() {
let d1 = EdmDuration::from_str("PT1.5M").unwrap();
assert_eq!(d1.0, Decimal::from(90));
let d2 = EdmDuration::from_str("P1.5D").unwrap();
assert_eq!(d2.0, Decimal::from(129600));
}
#[test]
fn parses_negative_durations() {
let d1 = EdmDuration::from_str("-PT2M").unwrap();
assert_eq!(d1.0, Decimal::from(-120));
let d2 = EdmDuration::from_str("-P1D").unwrap();
assert_eq!(d2.0, Decimal::from(-86400));
}
#[test]
fn parses_zero_variants() {
let d1 = EdmDuration::from_str("PT0S").unwrap();
assert_eq!(d1.0, Decimal::from(0));
let d2 = EdmDuration::from_str("PT").unwrap();
assert_eq!(d2.0, Decimal::from(0));
}
#[test]
fn rejects_malformed_inputs() {
assert!(EdmDuration::from_str("").is_err());
assert!(EdmDuration::from_str("P").is_err());
assert!(EdmDuration::from_str("T1H").is_err());
assert!(EdmDuration::from_str("PT1X").is_err());
assert!(EdmDuration::from_str("-P").is_err());
}
#[test]
fn formats_zero_duration() {
let d = EdmDuration::from_str("PT").unwrap();
assert_eq!(format!("{}", d), "PT0S");
}
#[test]
fn formats_seconds_only() {
let d = EdmDuration::from_str("PT3S").unwrap();
assert_eq!(format!("{}", d), "PT3S");
}
#[test]
fn formats_fractional_seconds() {
let d = EdmDuration::from_str("PT0.25S").unwrap();
assert_eq!(format!("{}", d), "PT0.25S");
}
#[test]
fn formats_minutes_and_hours_with_zero_seconds() {
let d1 = EdmDuration::from_str("PT2M").unwrap();
assert_eq!(format!("{}", d1), "PT2M0S");
let d2 = EdmDuration::from_str("PT1H").unwrap();
assert_eq!(format!("{}", d2), "PT1H0S");
let d3 = EdmDuration::from_str("PT1H2M").unwrap();
assert_eq!(format!("{}", d3), "PT1H2M0S");
}
#[test]
fn formats_days_only_and_day_time() {
let d1 = EdmDuration::from_str("P3D").unwrap();
assert_eq!(format!("{}", d1), "P3D");
let d2 = EdmDuration::from_str("P1DT1H").unwrap();
assert_eq!(format!("{}", d2), "P1DT1H0S");
}
#[test]
fn formats_negative_durations() {
let d1 = EdmDuration::from_str("-PT2M").unwrap();
assert_eq!(format!("{}", d1), "-PT2M0S");
let d2 = EdmDuration::from_str("-P1D").unwrap();
assert_eq!(format!("{}", d2), "-P1D");
}
#[test]
fn normalizes_fractional_minutes_on_display() {
let d = EdmDuration::from_str("PT1.5M").unwrap();
assert_eq!(format!("{}", d), "PT1M30S");
}
#[test]
fn formats_trims_trailing_zero_seconds() {
let d = EdmDuration::from_str("PT30.0S").unwrap();
assert_eq!(format!("{}", d), "PT30S");
}
#[test]
fn formats_leading_zero_inputs() {
let d = EdmDuration::from_str("PT01S").unwrap();
assert_eq!(format!("{}", d), "PT1S");
}
#[test]
fn formats_large_hours_breakdown() {
let d = EdmDuration::from_str("PT100000H").unwrap();
assert_eq!(format!("{}", d), "P4166DT16H0S");
}
#[test]
fn formats_fractional_hours() {
let d = EdmDuration::from_str("PT1.75H").unwrap();
assert_eq!(format!("{}", d), "PT1H45M0S");
}
#[test]
fn formats_fractional_days() {
let d = EdmDuration::from_str("P1.25D").unwrap();
assert_eq!(format!("{}", d), "P1DT6H0S");
}
#[test]
fn formats_minute_and_hour_carry_from_seconds() {
let d1 = EdmDuration::from_str("PT60S").unwrap();
assert_eq!(format!("{}", d1), "PT1M0S");
let d2 = EdmDuration::from_str("PT3600S").unwrap();
assert_eq!(format!("{}", d2), "PT1H0S");
}
#[test]
fn formats_trims_excess_zero_fraction() {
let d = EdmDuration::from_str("PT1.2300S").unwrap();
assert_eq!(format!("{}", d), "PT1.23S");
}
#[test]
fn test_exact_division() {
let (q, r) = EdmDuration::div_with_reminder(Decimal::new(10, 0), Decimal::new(5, 0));
assert_eq!(q, Decimal::new(2, 0));
assert_eq!(r, Decimal::new(0, 0));
}
#[test]
fn test_positive_non_exact() {
let (q, r) = EdmDuration::div_with_reminder(Decimal::new(10, 0), Decimal::new(4, 0));
assert_eq!(q, Decimal::new(2, 0));
assert_eq!(r, Decimal::new(2, 0));
}
#[test]
fn test_non_integer_division() {
let v = Decimal::new(105, 1); let d = Decimal::new(4, 0); let (q, r) = EdmDuration::div_with_reminder(v, d);
assert_eq!(q, Decimal::new(2, 0));
assert_eq!(r, Decimal::new(25, 1)); }
#[test]
fn test_zero_dividend() {
let (q, r) = EdmDuration::div_with_reminder(Decimal::new(0, 0), Decimal::new(5, 0));
assert_eq!(q, Decimal::new(0, 0));
assert_eq!(r, Decimal::new(0, 0));
}
}