#![allow(dead_code)]
#[derive(Debug, Clone, PartialEq, Default)]
pub struct IsoDuration {
pub years: u32,
pub months: u32,
pub weeks: u32,
pub days: u32,
pub hours: u32,
pub minutes: u32,
pub seconds: u32,
}
impl IsoDuration {
pub fn new() -> Self {
IsoDuration::default()
}
pub fn approx_seconds(&self) -> u64 {
let days_total = self.years as u64 * 365
+ self.months as u64 * 30
+ self.weeks as u64 * 7
+ self.days as u64;
days_total * 86400
+ self.hours as u64 * 3600
+ self.minutes as u64 * 60
+ self.seconds as u64
}
pub fn is_zero(&self) -> bool {
self.approx_seconds() == 0
}
}
pub fn parse_duration(s: &str) -> Option<IsoDuration> {
let s = s.trim();
if !s.starts_with('P') {
return None;
}
let mut dur = IsoDuration::default();
let inner = &s[1..];
let (date_part, time_part) = if let Some(t_pos) = inner.find('T') {
(&inner[..t_pos], Some(&inner[t_pos + 1..]))
} else {
(inner, None)
};
parse_date_part(date_part, &mut dur)?;
if let Some(tp) = time_part {
parse_time_part(tp, &mut dur)?;
}
Some(dur)
}
fn parse_date_part(s: &str, dur: &mut IsoDuration) -> Option<()> {
let mut buf = String::new();
for ch in s.chars() {
if ch.is_ascii_digit() {
buf.push(ch);
} else {
let n: u32 = buf.parse().ok()?;
buf.clear();
match ch {
'Y' => dur.years = n,
'M' => dur.months = n,
'W' => dur.weeks = n,
'D' => dur.days = n,
_ => return None,
}
}
}
Some(())
}
fn parse_time_part(s: &str, dur: &mut IsoDuration) -> Option<()> {
let mut buf = String::new();
for ch in s.chars() {
if ch.is_ascii_digit() {
buf.push(ch);
} else {
let n: u32 = buf.parse().ok()?;
buf.clear();
match ch {
'H' => dur.hours = n,
'M' => dur.minutes = n,
'S' => dur.seconds = n,
_ => return None,
}
}
}
Some(())
}
pub fn duration_to_string(d: &IsoDuration) -> String {
let mut s = String::from("P");
if d.years > 0 {
s.push_str(&format!("{}Y", d.years));
}
if d.months > 0 {
s.push_str(&format!("{}M", d.months));
}
if d.weeks > 0 {
s.push_str(&format!("{}W", d.weeks));
}
if d.days > 0 {
s.push_str(&format!("{}D", d.days));
}
if d.hours > 0 || d.minutes > 0 || d.seconds > 0 {
s.push('T');
if d.hours > 0 {
s.push_str(&format!("{}H", d.hours));
}
if d.minutes > 0 {
s.push_str(&format!("{}M", d.minutes));
}
if d.seconds > 0 {
s.push_str(&format!("{}S", d.seconds));
}
}
if s == "P" {
s.push_str("0D");
}
s
}
pub fn add_durations(a: &IsoDuration, b: &IsoDuration) -> IsoDuration {
IsoDuration {
years: a.years + b.years,
months: a.months + b.months,
weeks: a.weeks + b.weeks,
days: a.days + b.days,
hours: a.hours + b.hours,
minutes: a.minutes + b.minutes,
seconds: a.seconds + b.seconds,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_full() {
let d = parse_duration("P1Y2M3DT4H5M6S").expect("should succeed");
assert_eq!(d.years, 1);
assert_eq!(d.months, 2);
assert_eq!(d.days, 3);
assert_eq!(d.hours, 4);
assert_eq!(d.minutes, 5);
assert_eq!(d.seconds, 6);
}
#[test]
fn test_parse_date_only() {
let d = parse_duration("P10D").expect("should succeed");
assert_eq!(d.days, 10);
assert_eq!(d.hours, 0);
}
#[test]
fn test_parse_time_only() {
let d = parse_duration("PT30M").expect("should succeed");
assert_eq!(d.minutes, 30);
}
#[test]
fn test_invalid_no_p() {
assert!(parse_duration("1Y2M").is_none(), );
}
#[test]
fn test_approx_seconds_one_day() {
let d = parse_duration("P1D").expect("should succeed");
assert_eq!(d.approx_seconds(), 86400);
}
#[test]
fn test_is_zero() {
let d = IsoDuration::default();
assert!(d.is_zero() ,);
}
#[test]
fn test_to_string_roundtrip() {
let d = parse_duration("P1Y3DT2H").expect("should succeed");
let s = duration_to_string(&d);
let d2 = parse_duration(&s).expect("should succeed");
assert_eq!(d, d2);
}
#[test]
fn test_add_durations() {
let a = parse_duration("P1DT1H").expect("should succeed");
let b = parse_duration("P2DT2H").expect("should succeed");
let c = add_durations(&a, &b);
assert_eq!(c.days, 3);
assert_eq!(c.hours, 3);
}
#[test]
fn test_parse_week() {
let d = parse_duration("P2W").expect("should succeed");
assert_eq!(d.weeks, 2);
assert_eq!(d.approx_seconds(), 2 * 7 * 86400);
}
}