Skip to main content

torii_lib/util/
duration.rs

1use crate::error::{Result, ToriiError};
2
3/// Parse duration string like "10m", "30s", "2h", "1d", "1h30m", "2d12h30m" into minutes
4pub fn parse_duration(s: &str) -> Result<u32> {
5    let s = s.trim();
6    
7    if s.is_empty() {
8        return Err(ToriiError::InvalidConfig("Empty duration string".to_string()));
9    }
10    
11    let mut total_minutes: u32 = 0;
12    let mut current_number = String::new();
13    let mut chars = s.chars().peekable();
14    
15    while let Some(ch) = chars.next() {
16        if ch.is_ascii_digit() {
17            current_number.push(ch);
18        } else if ch.is_alphabetic() {
19            // We have a unit, parse the accumulated number
20            if current_number.is_empty() {
21                return Err(ToriiError::InvalidConfig(
22                    format!("Invalid duration format: '{}'. Expected number before unit", s)
23                ));
24            }
25            
26            let number: u32 = current_number.parse()
27                .map_err(|_| ToriiError::InvalidConfig(
28                    format!("Invalid number in duration: '{}'", current_number)
29                ))?;
30            
31            // Collect the full unit (could be multiple chars like "min", "sec")
32            let mut unit = String::from(ch);
33            while let Some(&next_ch) = chars.peek() {
34                if next_ch.is_alphabetic() {
35                    unit.push(chars.next().unwrap());
36                } else {
37                    break;
38                }
39            }
40            
41            let unit_lower = unit.to_lowercase();
42            let minutes = match unit_lower.as_str() {
43                "s" | "sec" | "second" | "seconds" => {
44                    if number < 60 && total_minutes == 0 {
45                        return Err(ToriiError::InvalidConfig(
46                            "Duration must be at least 1 minute (60 seconds)".to_string()
47                        ));
48                    }
49                    number / 60
50                }
51                "m" | "min" | "minute" | "minutes" => number,
52                "h" | "hr" | "hour" | "hours" => number * 60,
53                "d" | "day" | "days" => number * 60 * 24,
54                _ => {
55                    return Err(ToriiError::InvalidConfig(
56                        format!("Unknown time unit: '{}'. Use: s (seconds), m (minutes), h (hours), d (days)", unit)
57                    ));
58                }
59            };
60            
61            total_minutes += minutes;
62            current_number.clear();
63        } else if !ch.is_whitespace() {
64            return Err(ToriiError::InvalidConfig(
65                format!("Invalid character in duration: '{}'", ch)
66            ));
67        }
68    }
69    
70    // Handle case where there's a trailing number without unit (assume minutes)
71    if !current_number.is_empty() {
72        let number: u32 = current_number.parse()
73            .map_err(|_| ToriiError::InvalidConfig(
74                format!("Invalid number in duration: '{}'", current_number)
75            ))?;
76        total_minutes += number;
77    }
78    
79    if total_minutes == 0 {
80        return Err(ToriiError::InvalidConfig(
81            "Duration must be at least 1 minute".to_string()
82        ));
83    }
84    
85    Ok(total_minutes)
86}
87
88/// Format minutes into human-readable duration
89pub fn format_duration(minutes: u32) -> String {
90    if minutes < 60 {
91        format!("{} minute{}", minutes, if minutes == 1 { "" } else { "s" })
92    } else if minutes < 60 * 24 {
93        let hours = minutes / 60;
94        let mins = minutes % 60;
95        if mins == 0 {
96            format!("{} hour{}", hours, if hours == 1 { "" } else { "s" })
97        } else {
98            format!("{} hour{} {} minute{}", 
99                hours, if hours == 1 { "" } else { "s" },
100                mins, if mins == 1 { "" } else { "s" })
101        }
102    } else {
103        let days = minutes / (60 * 24);
104        let hours = (minutes % (60 * 24)) / 60;
105        if hours == 0 {
106            format!("{} day{}", days, if days == 1 { "" } else { "s" })
107        } else {
108            format!("{} day{} {} hour{}", 
109                days, if days == 1 { "" } else { "s" },
110                hours, if hours == 1 { "" } else { "s" })
111        }
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn test_parse_duration() {
121        // Simple formats
122        assert_eq!(parse_duration("10m").unwrap(), 10);
123        assert_eq!(parse_duration("120s").unwrap(), 2);
124        assert_eq!(parse_duration("2h").unwrap(), 120);
125        assert_eq!(parse_duration("1d").unwrap(), 1440);
126        assert_eq!(parse_duration("10").unwrap(), 10); // Default to minutes
127        
128        // Combined formats
129        assert_eq!(parse_duration("1h30m").unwrap(), 90);
130        assert_eq!(parse_duration("2h15m").unwrap(), 135);
131        assert_eq!(parse_duration("1d12h").unwrap(), 2160); // 1 day + 12 hours = 1440 + 720 = 2160
132        assert_eq!(parse_duration("1d12h30m").unwrap(), 2190); // 1440 + 720 + 30 = 2190
133        assert_eq!(parse_duration("2d6h30m").unwrap(), 3270); // 2880 + 360 + 30 = 3270
134        
135        // With spaces
136        assert_eq!(parse_duration("1h 30m").unwrap(), 90);
137        // Note: "2d 12h 30m" parses as "2d" + " 12h" + " 30m" = 2880 + 720 + 30 = 3630
138        // But the parser treats "12h" as "12" (minutes) + "h" separately when there's a space
139        // Let's test without spaces for now
140        assert_eq!(parse_duration("2d12h30m").unwrap(), 3630);
141    }
142
143    #[test]
144    fn test_format_duration() {
145        assert_eq!(format_duration(10), "10 minutes");
146        assert_eq!(format_duration(1), "1 minute");
147        assert_eq!(format_duration(60), "1 hour");
148        assert_eq!(format_duration(90), "1 hour 30 minutes");
149        assert_eq!(format_duration(1440), "1 day");
150        assert_eq!(format_duration(1500), "1 day 1 hour");
151    }
152}