1use crate::errors::CliError;
2
3pub 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
39pub 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
68pub 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}