use super::funejson::Value; const MAX_DIGITS: usize = 6; fn not_a_duration(text: &str) -> ! { panic!("\"{}\" is not a duration: write it as 1h30m, 90m or 01:30", text) } /// Read a run of digits from `at`; returns (value, next index), or None if there is none. fn read_number(text: &str, s: &[u8], at: usize) -> Option<(i64, usize)> { let mut end = at; while end < s.len() && s[end].is_ascii_digit() { end += 1; } if end == at { return None; } if end - at > MAX_DIGITS { not_a_duration(text); } let mut value = 0i64; for b in &s[at..end] { value = value * 10 + i64::from(b - b'0'); } Some((value, end)) } fn skip_spaces(s: &[u8], mut at: usize) -> usize { while at < s.len() && (s[at] == b' ' || s[at] == b'\t') { at += 1; } at } /// One written duration, in minutes. fn parse_one(text: &str) -> i64 { let s = text.trim_matches(|c| c == ' ' || c == '\t').as_bytes(); if let Some(colon) = s.iter().position(|&b| b == b':') { let hours = match read_number(text, s, 0) { Some(h) if h.1 == colon => h, _ => not_a_duration(text), }; let minutes = match read_number(text, s, colon + 1) { Some(m) if m.1 == s.len() && m.1 - colon - 1 == 2 => m, _ => not_a_duration(text), }; if minutes.0 >= 60 { panic!("minutes must be under 60 when hours are given, in \"{}\"", text); } return hours.0 * 60 + minutes.0; } let mut at = 0usize; let mut hours: Option = None; let mut minutes: Option = None; while at < s.len() { let read = match read_number(text, s, at) { Some(r) => r, None => not_a_duration(text), }; at = skip_spaces(s, read.1); let unit = if at < s.len() { s[at] } else { 0 }; if (unit == b'h' || unit == b'H') && hours.is_none() && minutes.is_none() { hours = Some(read.0); } else if (unit == b'm' || unit == b'M') && minutes.is_none() { minutes = Some(read.0); } else { not_a_duration(text); } at = skip_spaces(s, at + 1); } if hours.is_none() && minutes.is_none() { not_a_duration(text); } if let (Some(_), Some(m)) = (hours, minutes) { if m >= 60 { panic!("minutes must be under 60 when hours are given, in \"{}\"", text); } } hours.unwrap_or(0) * 60 + minutes.unwrap_or(0) } /// Parse each written duration, add them, and give the total in minutes and in /// both written forms. The clock form does not wrap at 24 hours, because a total /// of work is not a time of day. /// /// # Panics /// Panics on any part that is not a duration in one of the accepted forms. pub fn duration(parts: &[String]) -> Duration { let total: i64 = parts.iter().map(|p| parse_one(p)).sum(); let hours = total / 60; let minutes = total % 60; let text = if hours > 0 && minutes > 0 { format!("{}h{}m", hours, minutes) } else if hours > 0 { format!("{}h", hours) } else { format!("{}m", minutes) }; Duration { minutes: total, text, clock: format!("{:02}:{:02}", hours, minutes), } } pub fn duration_to_value(d: &Duration) -> Value { Value::obj(vec![ ("minutes", Value::Int(d.minutes)), ("text", Value::str(&d.text)), ("clock", Value::str(&d.clock)), ]) } pub fn fune_vector(args: &[Value]) -> Value { let parts: Vec = args[0].as_arr().iter().map(|v| v.as_str().to_string()).collect(); duration_to_value(&duration(&parts)) }