algorembrant's picture
Upload 37 files
e15ab27 verified
// ============================================================================
// timeframe.rs -- Custom Timeframe String Parser
// ============================================================================
// Parses human-readable timeframe strings into seconds.
// "1:30" -> 90s
// "4h30m" -> 16200s
// "2m" -> 120s
// "45s" -> 45s
// "90" -> 90s (plain number = seconds)
// ============================================================================
/// Parse a timeframe string into total seconds.
///
/// Supported formats:
/// - `"MM:SS"` or `"HH:MM:SS"` (colon-separated)
/// - `"4h30m10s"` (unit suffixes: h, m, s)
/// - `"90"` (plain integer = seconds)
pub fn parse_timeframe(input: &str) -> Result<u64, String> {
let s = input.trim();
if s.is_empty() {
return Err("Empty timeframe string".to_string());
}
// Colon format: "MM:SS" or "HH:MM:SS"
if s.contains(':') {
return parse_colon_format(s);
}
// Unit-suffix format: "4h30m10s", "2m", "45s"
if s.chars().any(|c| matches!(c, 'h' | 'm' | 's')) {
return parse_unit_format(s);
}
// Plain number: treat as seconds
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 => {
// MM:SS
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 => {
// HH:MM:SS
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 there are trailing digits with no unit, treat as seconds
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)
}
/// Format seconds back into a human-readable string for display.
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");
}
}