use crate::error::{Error, ErrorKind};
pub fn parse_duration(input: &str) -> crate::error::Result<f64> {
let s = input.trim();
if !s.starts_with('P') {
return Err(Error::new(
ErrorKind::InvalidDuration,
format!("duration must start with 'P': {input}"),
));
}
let s = &s[1..];
if s.is_empty() {
return Err(Error::new(
ErrorKind::InvalidDuration,
format!("empty duration: {input}"),
));
}
let mut seconds: f64 = 0.0;
let mut in_time = false;
let mut num_start: Option<usize> = None;
let mut has_component = false;
for (i, c) in s.char_indices() {
match c {
'T' => {
in_time = true;
num_start = None;
}
'0'..='9' | '.' => {
if num_start.is_none() {
num_start = Some(i);
}
}
'Y' | 'W' => {
let val = parse_number(s, num_start, i, input)?;
match c {
'Y' => seconds += val * 365.25 * 86400.0,
'W' => seconds += val * 7.0 * 86400.0,
_ => unreachable!(),
}
num_start = None;
has_component = true;
}
'D' => {
let val = parse_number(s, num_start, i, input)?;
seconds += val * 86400.0;
num_start = None;
has_component = true;
}
'H' => {
if !in_time {
return Err(Error::new(
ErrorKind::InvalidDuration,
format!("'H' without 'T': {input}"),
));
}
let val = parse_number(s, num_start, i, input)?;
seconds += val * 3600.0;
num_start = None;
has_component = true;
}
'M' => {
let val = parse_number(s, num_start, i, input)?;
if in_time {
seconds += val * 60.0;
} else {
seconds += val * 30.44 * 86400.0;
}
num_start = None;
has_component = true;
}
'S' => {
if !in_time {
return Err(Error::new(
ErrorKind::InvalidDuration,
format!("'S' without 'T': {input}"),
));
}
let val = parse_number(s, num_start, i, input)?;
seconds += val;
num_start = None;
has_component = true;
}
_ => {
return Err(Error::new(
ErrorKind::InvalidDuration,
format!("unexpected character '{c}': {input}"),
));
}
}
}
if num_start.is_some() {
return Err(Error::new(
ErrorKind::InvalidDuration,
format!("trailing number without unit: {input}"),
));
}
if !has_component {
return Err(Error::new(
ErrorKind::InvalidDuration,
format!("no duration components: {input}"),
));
}
Ok(seconds)
}
fn parse_number(
s: &str,
num_start: Option<usize>,
end: usize,
original: &str,
) -> crate::error::Result<f64> {
let start = num_start.ok_or_else(|| {
Error::new(
ErrorKind::InvalidDuration,
format!("unit without number: {original}"),
)
})?;
s[start..end].parse::<f64>().map_err(|_| {
Error::new(
ErrorKind::InvalidDuration,
format!("invalid number '{}': {original}", &s[start..end]),
)
})
}
pub fn format_duration(seconds: f64) -> String {
if seconds == 0.0 {
return "PT0S".to_string();
}
let mut s = String::from("PT");
let mut remaining = seconds;
let hours = (remaining / 3600.0).floor() as u64;
if hours > 0 {
s.push_str(&format!("{hours}H"));
remaining -= hours as f64 * 3600.0;
}
let minutes = (remaining / 60.0).floor() as u64;
if minutes > 0 {
s.push_str(&format!("{minutes}M"));
remaining -= minutes as f64 * 60.0;
}
if remaining > 0.0 || s == "PT" {
if remaining == remaining.floor() {
s.push_str(&format!("{}S", remaining as u64));
} else {
let formatted = format!("{remaining:.3}");
let trimmed = formatted.trim_end_matches('0').trim_end_matches('.');
s.push_str(trimmed);
s.push('S');
}
}
s
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_durations() {
assert_eq!(parse_duration("PT30S").unwrap(), 30.0);
assert_eq!(parse_duration("PT0.5S").unwrap(), 0.5);
assert_eq!(parse_duration("PT1M").unwrap(), 60.0);
assert_eq!(parse_duration("PT1H").unwrap(), 3600.0);
assert_eq!(parse_duration("PT1H2M3S").unwrap(), 3723.0);
assert_eq!(parse_duration("PT1H2M3.4S").unwrap(), 3723.4);
}
#[test]
fn test_days() {
assert_eq!(parse_duration("P1D").unwrap(), 86400.0);
assert_eq!(parse_duration("P1DT12H").unwrap(), 86400.0 + 43200.0);
}
#[test]
fn test_fractional_seconds() {
assert_eq!(parse_duration("PT0.010S").unwrap(), 0.010);
assert_eq!(parse_duration("PT4.000S").unwrap(), 4.0);
}
#[test]
fn test_whitespace_trimmed() {
assert_eq!(parse_duration(" PT30S ").unwrap(), 30.0);
}
#[test]
fn test_invalid() {
assert!(parse_duration("").is_err());
assert!(parse_duration("30S").is_err());
assert!(parse_duration("PT").is_err());
assert!(parse_duration("PTS").is_err());
assert!(parse_duration("PT1H2").is_err());
assert!(parse_duration("PTxS").is_err());
}
}