use regex::Regex;
use std::str::FromStr;
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DurationSeconds(pub f32);
impl From<DurationSeconds> for Duration {
fn from(ds: DurationSeconds) -> Self {
Duration::from_secs_f32(ds.0)
}
}
impl FromStr for DurationSeconds {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Ok(val) = s.parse::<f32>() {
return Ok(DurationSeconds(val));
}
let re = Regex::new(r"^(?:(\d+(?:\.\d+)?)h)?(?:(\d+(?:\.\d+)?)m)?(?:(\d+(?:\.\d+)?)s)?$")
.unwrap();
if let Some(caps) = re.captures(s) {
let mut secs = 0.0;
if let Some(h) = caps.get(1) {
secs += h
.as_str()
.parse::<f32>()
.map_err(|_| format!("Invalid hours: {:?}", h))?
* 3600.0;
}
if let Some(m) = caps.get(2) {
secs += m
.as_str()
.parse::<f32>()
.map_err(|_| format!("Invalid minutes: {:?}", m))?
* 60.0;
}
if let Some(s) = caps.get(3) {
secs += s
.as_str()
.parse::<f32>()
.map_err(|_| format!("Invalid seconds: {:?}", s))?;
}
if secs >= 0.0 {
return Ok(DurationSeconds(secs));
} else {
return Err(format!(
"Invalid seconds amount {:?} in format {:?}",
secs, s
));
}
}
Err(format!("Invalid duration format: {:?}", s))
}
}
use serde::{Deserialize, Deserializer};
impl<'de> Deserialize<'de> for DurationSeconds {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let raw = String::deserialize(deserializer)?;
raw.parse::<DurationSeconds>()
.map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_secs(input: &str, expected: f32) {
let dur: DurationSeconds = input.parse().unwrap();
assert!(
(dur.0 - expected).abs() < f32::EPSILON,
"expected {} got {}",
expected,
dur.0
);
}
#[test]
fn test_plain_numbers() {
assert_secs("30", 30.0);
assert_secs("30.0", 30.0);
assert_secs("45.1", 45.1);
}
#[test]
fn test_seconds() {
assert_secs("0s", 0.0);
assert_secs("30s", 30.0);
assert_secs("30.5s", 30.5);
}
#[test]
fn test_minutes() {
assert_secs("0.5m", 30.0);
assert_secs("60m", 3600.0);
assert_secs("1m30s", 90.0);
assert_secs("0.5m10.5s", 40.5);
}
#[test]
fn test_hours() {
assert_secs("1h", 3600.0);
assert_secs("3h30m", 3.0 * 3600.0 + 30.0 * 60.0);
assert_secs("0.5h", 1800.0);
assert_secs("1h2m3s", 3723.0);
}
#[test]
fn test_invalid() {
assert!("abc".parse::<DurationSeconds>().is_err());
assert!("1x".parse::<DurationSeconds>().is_err());
assert!("1d".parse::<DurationSeconds>().is_err());
assert!(".5h".parse::<DurationSeconds>().is_err());
assert!(".5m".parse::<DurationSeconds>().is_err());
assert!(".5s".parse::<DurationSeconds>().is_err());
}
}