| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | pub fn parse_timeframe(input: &str) -> Result<u64, String> {
|
| | let s = input.trim();
|
| | if s.is_empty() {
|
| | return Err("Empty timeframe string".to_string());
|
| | }
|
| |
|
| |
|
| | if s.contains(':') {
|
| | return parse_colon_format(s);
|
| | }
|
| |
|
| |
|
| | if s.chars().any(|c| matches!(c, 'h' | 'm' | 's')) {
|
| | return parse_unit_format(s);
|
| | }
|
| |
|
| |
|
| | s.parse::<u64>()
|
| | .map_err(|_| format!("Invalid timeframe: '{}'", s))
|
| | }
|
| |
|
| | fn parse_colon_format(s: &str) -> Result<u64, String> {
|
| | let parts: Vec<&str> = s.split(':').collect();
|
| | match parts.len() {
|
| | 2 => {
|
| |
|
| | let mm: u64 = parts[0].parse().map_err(|_| format!("Invalid minutes: '{}'", parts[0]))?;
|
| | let ss: u64 = parts[1].parse().map_err(|_| format!("Invalid seconds: '{}'", parts[1]))?;
|
| | Ok(mm * 60 + ss)
|
| | }
|
| | 3 => {
|
| |
|
| | let hh: u64 = parts[0].parse().map_err(|_| format!("Invalid hours: '{}'", parts[0]))?;
|
| | let mm: u64 = parts[1].parse().map_err(|_| format!("Invalid minutes: '{}'", parts[1]))?;
|
| | let ss: u64 = parts[2].parse().map_err(|_| format!("Invalid seconds: '{}'", parts[2]))?;
|
| | Ok(hh * 3600 + mm * 60 + ss)
|
| | }
|
| | _ => Err(format!("Invalid colon format: '{}'", s)),
|
| | }
|
| | }
|
| |
|
| | fn parse_unit_format(s: &str) -> Result<u64, String> {
|
| | let mut total: u64 = 0;
|
| | let mut current_num = String::new();
|
| |
|
| | for ch in s.chars() {
|
| | if ch.is_ascii_digit() {
|
| | current_num.push(ch);
|
| | } else {
|
| | let n: u64 = if current_num.is_empty() {
|
| | return Err(format!("Missing number before '{}' in '{}'", ch, s));
|
| | } else {
|
| | current_num.parse().map_err(|_| format!("Invalid number in '{}'", s))?
|
| | };
|
| | current_num.clear();
|
| |
|
| | match ch {
|
| | 'h' | 'H' => total += n * 3600,
|
| | 'm' | 'M' => total += n * 60,
|
| | 's' | 'S' => total += n,
|
| | _ => return Err(format!("Unknown unit '{}' in '{}'", ch, s)),
|
| | }
|
| | }
|
| | }
|
| |
|
| |
|
| | if !current_num.is_empty() {
|
| | let n: u64 = current_num.parse().map_err(|_| format!("Invalid number in '{}'", s))?;
|
| | total += n;
|
| | }
|
| |
|
| | if total == 0 {
|
| | return Err(format!("Timeframe resolves to 0 seconds: '{}'", s));
|
| | }
|
| |
|
| | Ok(total)
|
| | }
|
| |
|
| |
|
| | pub fn format_seconds(secs: u64) -> String {
|
| | if secs == 0 {
|
| | return "0s".to_string();
|
| | }
|
| | let h = secs / 3600;
|
| | let m = (secs % 3600) / 60;
|
| | let s = secs % 60;
|
| |
|
| | let mut out = String::new();
|
| | if h > 0 { out.push_str(&format!("{}h", h)); }
|
| | if m > 0 { out.push_str(&format!("{}m", m)); }
|
| | if s > 0 { out.push_str(&format!("{}s", s)); }
|
| | out
|
| | }
|
| |
|
| | #[cfg(test)]
|
| | mod tests {
|
| | use super::*;
|
| |
|
| | #[test]
|
| | fn test_colon_mm_ss() {
|
| | assert_eq!(parse_timeframe("1:30").unwrap(), 90);
|
| | assert_eq!(parse_timeframe("0:45").unwrap(), 45);
|
| | assert_eq!(parse_timeframe("10:00").unwrap(), 600);
|
| | }
|
| |
|
| | #[test]
|
| | fn test_colon_hh_mm_ss() {
|
| | assert_eq!(parse_timeframe("1:00:00").unwrap(), 3600);
|
| | assert_eq!(parse_timeframe("4:30:00").unwrap(), 16200);
|
| | }
|
| |
|
| | #[test]
|
| | fn test_unit_format() {
|
| | assert_eq!(parse_timeframe("4h30m").unwrap(), 16200);
|
| | assert_eq!(parse_timeframe("2m").unwrap(), 120);
|
| | assert_eq!(parse_timeframe("45s").unwrap(), 45);
|
| | assert_eq!(parse_timeframe("1h2m3s").unwrap(), 3723);
|
| | }
|
| |
|
| | #[test]
|
| | fn test_plain_number() {
|
| | assert_eq!(parse_timeframe("90").unwrap(), 90);
|
| | assert_eq!(parse_timeframe("3600").unwrap(), 3600);
|
| | }
|
| |
|
| | #[test]
|
| | fn test_format_seconds() {
|
| | assert_eq!(format_seconds(90), "1m30s");
|
| | assert_eq!(format_seconds(16200), "4h30m");
|
| | assert_eq!(format_seconds(3600), "1h");
|
| | assert_eq!(format_seconds(45), "45s");
|
| | }
|
| | }
|
| |
|