Skip to main content

sonos_cli/cli/
parse.rs

1use crate::errors::CliError;
2
3/// Validate a seek time string (H:MM:SS or HH:MM:SS format).
4pub fn validate_seek_time(input: &str) -> Result<(), CliError> {
5    let parts: Vec<&str> = input.split(':').collect();
6    if parts.len() != 3 {
7        return Err(CliError::Validation(format!(
8            "invalid position \"{input}\" — expected H:MM:SS format"
9        )));
10    }
11    parts[0].parse::<u32>().map_err(|_| {
12        CliError::Validation(format!(
13            "invalid position \"{input}\" — hours must be a number"
14        ))
15    })?;
16    let minutes: u32 = parts[1].parse().map_err(|_| {
17        CliError::Validation(format!(
18            "invalid position \"{input}\" — minutes must be a number"
19        ))
20    })?;
21    let seconds: u32 = parts[2].parse().map_err(|_| {
22        CliError::Validation(format!(
23            "invalid position \"{input}\" — seconds must be a number"
24        ))
25    })?;
26    if minutes > 59 {
27        return Err(CliError::Validation(format!(
28            "invalid position \"{input}\" — minutes must be 0–59"
29        )));
30    }
31    if seconds > 59 {
32        return Err(CliError::Validation(format!(
33            "invalid position \"{input}\" — seconds must be 0–59"
34        )));
35    }
36    Ok(())
37}
38
39/// Parse a duration string (e.g. "30m", "1h", "90m") into HH:MM:SS format.
40pub fn parse_duration(input: &str) -> Result<String, CliError> {
41    let (num_str, unit) = if let Some(s) = input.strip_suffix('m') {
42        (s, 'm')
43    } else if let Some(s) = input.strip_suffix('h') {
44        (s, 'h')
45    } else {
46        return Err(CliError::Validation(format!(
47            "invalid duration \"{input}\" — use a unit suffix: 30m or 1h"
48        )));
49    };
50
51    let value: u32 = num_str.parse().map_err(|_| {
52        CliError::Validation(format!(
53            "invalid duration \"{input}\" — use a unit suffix: 30m or 1h"
54        ))
55    })?;
56
57    let total_minutes = match unit {
58        'h' => value * 60,
59        'm' => value,
60        _ => unreachable!(),
61    };
62
63    let hours = total_minutes / 60;
64    let minutes = total_minutes % 60;
65    Ok(format!("{hours:02}:{minutes:02}:00"))
66}
67
68/// Format a duration string for human-readable output.
69pub fn format_duration_human(input: &str) -> String {
70    if let Some(num) = input.strip_suffix('m') {
71        if num == "1" {
72            "1 minute".to_string()
73        } else {
74            format!("{num} minutes")
75        }
76    } else if let Some(num) = input.strip_suffix('h') {
77        if num == "1" {
78            "1 hour".to_string()
79        } else {
80            format!("{num} hours")
81        }
82    } else {
83        input.to_string()
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn valid_seek_times() {
93        assert!(validate_seek_time("0:00:00").is_ok());
94        assert!(validate_seek_time("0:02:30").is_ok());
95        assert!(validate_seek_time("1:30:00").is_ok());
96        assert!(validate_seek_time("12:59:59").is_ok());
97    }
98
99    #[test]
100    fn invalid_seek_time_bad_seconds() {
101        let result = validate_seek_time("0:02:70");
102        assert!(matches!(result, Err(CliError::Validation(ref s)) if s.contains("seconds")));
103    }
104
105    #[test]
106    fn invalid_seek_time_bad_minutes() {
107        let result = validate_seek_time("0:70:00");
108        assert!(matches!(result, Err(CliError::Validation(ref s)) if s.contains("minutes")));
109    }
110
111    #[test]
112    fn invalid_seek_time_bad_format() {
113        assert!(validate_seek_time("2:30").is_err());
114        assert!(validate_seek_time("abc").is_err());
115    }
116
117    #[test]
118    fn parse_duration_minutes() {
119        assert_eq!(parse_duration("30m").unwrap(), "00:30:00");
120        assert_eq!(parse_duration("90m").unwrap(), "01:30:00");
121        assert_eq!(parse_duration("1m").unwrap(), "00:01:00");
122    }
123
124    #[test]
125    fn parse_duration_hours() {
126        assert_eq!(parse_duration("1h").unwrap(), "01:00:00");
127        assert_eq!(parse_duration("2h").unwrap(), "02:00:00");
128    }
129
130    #[test]
131    fn parse_duration_invalid() {
132        assert!(parse_duration("30").is_err());
133        assert!(parse_duration("abc").is_err());
134        assert!(parse_duration("").is_err());
135    }
136
137    #[test]
138    fn format_duration_human_values() {
139        assert_eq!(format_duration_human("30m"), "30 minutes");
140        assert_eq!(format_duration_human("1m"), "1 minute");
141        assert_eq!(format_duration_human("1h"), "1 hour");
142        assert_eq!(format_duration_human("2h"), "2 hours");
143    }
144}