use alloc::string::ToString;
use core::fmt;
use core::str::FromStr;
use crate::error::Error;
use crate::event::Atom;
use crate::ext::Extension;
use crate::ext::datetime::write_fraction;
use crate::ext::known::{WellKnown, impl_well_known, invalid, out_of_range, round, trunc};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct Duration {
pub seconds: i64,
pub nanosecond: i32,
}
impl Duration {
pub fn is_valid(&self) -> bool {
self.nanosecond.unsigned_abs() < 1_000_000_000
&& !(self.seconds > 0 && self.nanosecond < 0)
&& !(self.seconds < 0 && self.nanosecond > 0)
}
pub fn is_negative(&self) -> bool {
self.seconds < 0 || self.nanosecond < 0
}
pub fn as_secs_f64(&self) -> f64 {
self.seconds as f64 + f64::from(self.nanosecond) / 1e9
}
}
impl fmt::Display for Duration {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_negative() {
f.write_str("-")?;
}
f.write_str("PT")?;
let seconds = self.seconds.unsigned_abs();
let nanosecond = self.nanosecond.unsigned_abs();
let (hours, minutes, seconds) = (seconds / 3600, seconds / 60 % 60, seconds % 60);
if hours != 0 {
write!(f, "{}H", hours)?;
}
if minutes != 0 {
write!(f, "{}M", minutes)?;
}
if seconds != 0 || nanosecond != 0 || (hours == 0 && minutes == 0) {
write!(f, "{}", seconds)?;
write_fraction(f, nanosecond)?;
f.write_str("S")?;
}
Ok(())
}
}
impl FromStr for Duration {
type Err = Error;
fn from_str(s: &str) -> Result<Duration, Error> {
let (negative, rest) = match s.as_bytes().first() {
Some(b'-') => (true, &s[1..]),
Some(b'+') => (false, &s[1..]),
_ => (false, s),
};
let rest = rest
.strip_prefix("PT")
.or_else(|| rest.strip_prefix("pt"))
.ok_or_else(|| invalid("invalid duration, expected ISO 8601 duration (PT...)"))?;
if rest.is_empty() {
return Err(invalid("invalid duration"));
}
let overflow = || out_of_range("duration out of range");
let mut seconds: i64 = 0;
let mut nanosecond: i64 = 0;
let mut last_unit = 0;
let mut rest = rest.as_bytes();
while !rest.is_empty() {
let int_len = rest.iter().take_while(|x| x.is_ascii_digit()).count();
if int_len == 0 {
return Err(invalid("invalid duration"));
}
let value: i64 = core::str::from_utf8(&rest[..int_len])
.unwrap()
.parse()
.map_err(|_| overflow())?;
rest = &rest[int_len..];
let mut fraction = None;
if let Some(b'.' | b',') = rest.first() {
let frac_len = rest[1..].iter().take_while(|x| x.is_ascii_digit()).count();
if frac_len == 0 {
return Err(invalid("invalid duration"));
}
let mut nanos = 0i64;
for (idx, &c) in rest[1..1 + frac_len].iter().enumerate() {
if idx < 9 {
nanos = nanos * 10 + i64::from(c - b'0');
}
}
for _ in frac_len..9 {
nanos *= 10;
}
fraction = Some(nanos);
rest = &rest[1 + frac_len..];
}
let (unit, factor) = match rest.first() {
Some(b'H' | b'h') => (1, 3600),
Some(b'M' | b'm') => (2, 60),
Some(b'S' | b's') => (3, 1),
_ => return Err(invalid("invalid duration unit")),
};
if unit <= last_unit || (fraction.is_some() && unit != 3) {
return Err(invalid("invalid duration"));
}
last_unit = unit;
rest = &rest[1..];
seconds = value
.checked_mul(factor)
.and_then(|x| x.checked_add(seconds))
.ok_or_else(overflow)?;
nanosecond = fraction.unwrap_or(0);
}
if negative {
seconds = -seconds;
nanosecond = -nanosecond;
}
Ok(Duration {
seconds,
nanosecond: nanosecond as i32,
})
}
}
impl Extension for Duration {
fn name(&self) -> &str {
"duration"
}
fn fallback(&self) -> Atom<'_> {
Atom::Str(self.to_string().into())
}
}
impl WellKnown for Duration {
const EXPECTING: &'static str = "duration";
fn from_atom(atom: &Atom) -> Result<Option<Duration>, Error> {
Ok(Some(match *atom {
Atom::Ext(ref ext) => match ext.downcast_ref::<Duration>() {
Some(value) => *value,
None => return Ok(None),
},
Atom::Str(ref value) => value.parse()?,
Atom::U64(value) => Duration {
seconds: i64::try_from(value).map_err(|_| out_of_range("duration out of range"))?,
nanosecond: 0,
},
Atom::I64(value) => Duration {
seconds: value,
nanosecond: 0,
},
Atom::F64(value) => {
if !value.is_finite() || value.abs() >= 9.2e18 {
return Err(out_of_range("duration out of range"));
}
let seconds = trunc(value);
Duration {
seconds: seconds as i64,
nanosecond: round((value - seconds) * 1e9) as i32,
}
}
_ => return Ok(None),
}))
}
}
impl_well_known!(Duration);
#[test]
fn test_duration_format() {
let cases = [
(0, 0, "PT0S"),
(1, 500_000_000, "PT1.5S"),
(-1, -500_000_000, "-PT1.5S"),
(0, -1, "-PT0.000000001S"),
(3600, 0, "PT1H"),
(3661, 0, "PT1H1M1S"),
(90, 0, "PT1M30S"),
(i64::MAX, 999_999_999, "PT2562047788015215H30M7.999999999S"),
(
-i64::MAX,
-999_999_999,
"-PT2562047788015215H30M7.999999999S",
),
];
for (seconds, nanosecond, expected) in cases {
let duration = Duration {
seconds,
nanosecond,
};
assert_eq!(duration.to_string(), expected);
assert_eq!(expected.parse::<Duration>().unwrap(), duration);
}
assert_eq!(
"pt1m".parse::<Duration>().unwrap(),
Duration {
seconds: 60,
nanosecond: 0
}
);
for invalid in [
"", "P", "PT", "P1D", "PT1S1M", "PT1.5M", "PT.5S", "PT1", "PTS", "1S",
] {
assert!(invalid.parse::<Duration>().is_err(), "{}", invalid);
}
}