rust_filesearch/
util.rs

1use crate::errors::{FsError, Result};
2use chrono::{DateTime, NaiveDate, TimeZone, Utc};
3use humansize::{format_size, BINARY};
4
5/// Parse human-readable size string (e.g., "10KB", "2 MiB", "500B")
6pub fn parse_size(input: &str) -> Result<u64> {
7    let input = input.trim().to_uppercase();
8    let input = input.replace(" ", "");
9
10    // Try to split number from unit
11    let (num_str, unit) = input
12        .char_indices()
13        .find(|(_, c)| c.is_alphabetic())
14        .map(|(idx, _)| input.split_at(idx))
15        .unwrap_or((&input, ""));
16
17    let number: f64 = num_str.parse().map_err(|_| FsError::InvalidSize {
18        input: input.clone(),
19    })?;
20
21    let multiplier: u64 = match unit {
22        "" | "B" => 1,
23        "KB" => 1_000,
24        "KIB" => 1_024,
25        "MB" => 1_000_000,
26        "MIB" => 1_048_576,
27        "GB" => 1_000_000_000,
28        "GIB" => 1_073_741_824,
29        "TB" => 1_000_000_000_000,
30        "TIB" => 1_099_511_627_776,
31        _ => {
32            return Err(FsError::InvalidSize {
33                input: input.clone(),
34            })
35        }
36    };
37
38    Ok((number * multiplier as f64) as u64)
39}
40
41/// Format size in human-readable format using binary units
42pub fn format_size_human(size: u64) -> String {
43    format_size(size, BINARY)
44}
45
46/// Parse date string (ISO8601, YYYY-MM-DD, or relative like "7 days ago")
47pub fn parse_date(input: &str) -> Result<DateTime<Utc>> {
48    // Try parsing as RFC3339/ISO8601 first
49    if let Ok(dt) = DateTime::parse_from_rfc3339(input) {
50        return Ok(dt.with_timezone(&Utc));
51    }
52
53    // Try YYYY-MM-DD format
54    if let Ok(date) = NaiveDate::parse_from_str(input, "%Y-%m-%d") {
55        return Utc
56            .from_local_datetime(&date.and_hms_opt(0, 0, 0).unwrap())
57            .single()
58            .ok_or_else(|| FsError::InvalidDate {
59                input: input.to_string(),
60            });
61    }
62
63    // Try relative date parsing (e.g., "7 days ago", "2 weeks ago", "1 month ago")
64    if let Some(relative_date) = parse_relative_date(input) {
65        return Ok(relative_date);
66    }
67
68    Err(FsError::InvalidDate {
69        input: input.to_string(),
70    })
71}
72
73/// Parse relative date strings like "7 days ago", "2 weeks ago", "1 month ago"
74fn parse_relative_date(input: &str) -> Option<DateTime<Utc>> {
75    use chrono::Duration;
76
77    let input = input.trim().to_lowercase();
78    let parts: Vec<&str> = input.split_whitespace().collect();
79
80    // Expected format: "<number> <unit> ago"
81    if parts.len() != 3 || parts[2] != "ago" {
82        return None;
83    }
84
85    let number: i64 = parts[0].parse().ok()?;
86    let unit = parts[1];
87
88    let now = Utc::now();
89
90    match unit {
91        "second" | "seconds" | "sec" | "secs" => Some(now - Duration::seconds(number)),
92        "minute" | "minutes" | "min" | "mins" => Some(now - Duration::minutes(number)),
93        "hour" | "hours" | "hr" | "hrs" => Some(now - Duration::hours(number)),
94        "day" | "days" => Some(now - Duration::days(number)),
95        "week" | "weeks" => Some(now - Duration::weeks(number)),
96        "month" | "months" => Some(now - Duration::days(number * 30)),
97        "year" | "years" => Some(now - Duration::days(number * 365)),
98        _ => None,
99    }
100}
101
102/// Check if output is to a TTY (terminal)
103pub fn is_tty() -> bool {
104    crossterm::tty::IsTty::is_tty(&std::io::stdout())
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn test_parse_size() {
113        assert_eq!(parse_size("100").unwrap(), 100);
114        assert_eq!(parse_size("10KB").unwrap(), 10_000);
115        assert_eq!(parse_size("10KiB").unwrap(), 10_240);
116        assert_eq!(parse_size("2MB").unwrap(), 2_000_000);
117        assert_eq!(parse_size("2 MiB").unwrap(), 2_097_152);
118        assert_eq!(parse_size("1GB").unwrap(), 1_000_000_000);
119        assert_eq!(parse_size("1 GiB").unwrap(), 1_073_741_824);
120        assert!(parse_size("invalid").is_err());
121        assert!(parse_size("10XB").is_err());
122    }
123
124    #[test]
125    fn test_format_size_human() {
126        assert_eq!(format_size_human(0), "0 B");
127        assert_eq!(format_size_human(1023), "1023 B");
128        assert_eq!(format_size_human(1024), "1 KiB");
129        assert_eq!(format_size_human(1_048_576), "1 MiB");
130    }
131
132    #[test]
133    fn test_parse_date() {
134        // YYYY-MM-DD format
135        let result = parse_date("2024-01-01");
136        assert!(result.is_ok());
137
138        // ISO8601 format
139        let result = parse_date("2024-01-01T12:00:00Z");
140        assert!(result.is_ok());
141
142        // Invalid format
143        assert!(parse_date("invalid").is_err());
144    }
145}