todo_lib 11.2.0

Collection of utilities for todo.txt format
Documentation
use chrono::NaiveDate;
use lazy_static::lazy_static;

use crate::human_date;
use crate::todotxt;

const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
const TB: u64 = GB * 1024;
const PB: u64 = TB * 1024;
const EB: u64 = PB * 1024;

pub const SEC_IN_MINUTE: i64 = 60;
pub const SEC_IN_MINUTE_U32: u32 = 60;
const SEC_IN_HOUR: i64 = SEC_IN_MINUTE * 60;
const SEC_IN_DAY: i64 = SEC_IN_HOUR * 24;
const SEC_IN_WEEK: i64 = SEC_IN_DAY * 7;

struct ByteSize {
    s: &'static str,
    m: u64,
}

lazy_static! {
    static ref BYTES: [ByteSize; 18] = [
        ByteSize { s: "eib", m: EB },
        ByteSize { s: "eb", m: EB },
        ByteSize { s: "e", m: EB },
        ByteSize { s: "pib", m: PB },
        ByteSize { s: "pb", m: PB },
        ByteSize { s: "p", m: PB },
        ByteSize { s: "tib", m: TB },
        ByteSize { s: "tb", m: TB },
        ByteSize { s: "t", m: TB },
        ByteSize { s: "gib", m: GB },
        ByteSize { s: "gb", m: GB },
        ByteSize { s: "g", m: GB },
        ByteSize { s: "mib", m: MB },
        ByteSize { s: "mb", m: MB },
        ByteSize { s: "m", m: MB },
        ByteSize { s: "kib", m: KB },
        ByteSize { s: "kb", m: KB },
        ByteSize { s: "k", m: KB },
    ];
}

#[derive(Debug, PartialEq, Clone, Copy)]
pub enum TimeInterval {
    Single(Option<u32>),
    Range(Option<u32>, Option<u32>),
}

pub fn str_to_bytes(s: &str) -> Option<u64> {
    let l = s.to_lowercase();
    for sz in BYTES.iter() {
        if l.ends_with(sz.s) {
            let lv = l.trim_end_matches(sz.s);
            let sb = lv.parse::<u64>().ok()?;
            return Some(sb * sz.m);
        }
    }
    s.parse::<u64>().ok()
}
pub fn str_to_duration(s: &str) -> Option<i64> {
    let l = s.to_lowercase();
    let s = l.as_str();
    let mut dur: i64 = 0;
    let (sgn, mut s) = if s.starts_with('-') { (-1i64, s.trim_start_matches('-')) } else { (1i64, s) };
    loop {
        if s.is_empty() {
            return Some(dur * sgn);
        }
        match s.find(|c: char| !c.is_ascii_digit()) {
            None => {
                let v = s.parse::<u32>().ok()?;
                dur = (dur + v as i64) * sgn;
                return Some(dur);
            }
            Some(pos) => {
                let vs = &s[..pos];
                let value = vs.parse::<u32>().ok()?;
                s = &s[pos..];
                let suffix = match s.find(|c: char| c.is_ascii_digit()) {
                    None => {
                        let save = s;
                        s = &s[..0];
                        save
                    }
                    Some(pos) => {
                        let save = &s[..pos];
                        s = &s[pos..];
                        save
                    }
                };
                let value: i64 = match suffix {
                    "w" => value as i64 * SEC_IN_WEEK,
                    "d" => value as i64 * SEC_IN_DAY,
                    "h" => value as i64 * SEC_IN_HOUR,
                    "m" => value as i64 * SEC_IN_MINUTE,
                    "s" | "" => value as i64,
                    _ => return None,
                };
                dur += value;
            }
        }
    }
}
pub fn str_to_time(s: &str) -> Option<u32> {
    let s = s.to_lowercase();
    match s.find(|c: char| !c.is_ascii_digit()) {
        None => {
            if s.len() < 3 {
                return None;
            }
            let v = s.parse::<u32>().ok()?;
            let h = v / 100;
            let m = v % 100;
            if h > 23 || m > 59 {
                return None;
            }
            Some(v)
        }
        Some(pos) => {
            let vs = &s[..pos];
            let mut v = vs.parse::<u32>().ok()?;
            let suffix = &s[pos..];
            if suffix != "am" && suffix != "pm" {
                return None;
            }
            let h = v / 100;
            let m = v % 100;
            if h > 12 || h == 0 || m > 59 {
                return None;
            }
            if (1200..=1259).contains(&v) && suffix == "am" {
                v -= 1200;
            }
            if v < 1200 && suffix == "pm" {
                v += 1200;
            }
            Some(v)
        }
    }
}

pub fn str_to_time_interval(s: &str) -> TimeInterval {
    let parts = if let Some(spl) = s.split_once('-') {
        vec![spl.0, spl.1]
    } else if let Some(spl) = s.split_once("..") {
        vec![spl.0, spl.1]
    } else {
        vec![s]
    };

    if parts.len() == 1 {
        TimeInterval::Single(str_to_time(parts[0]))
    } else {
        TimeInterval::Range(str_to_time(parts[0]), str_to_time(parts[1]))
    }
}

pub fn str_to_date(s: &str, today: NaiveDate) -> Option<NaiveDate> {
    if let Ok(d) = todotxt::parse_date(s, today) { Some(d) } else { human_date::human_to_date(today, s, 7).ok() }
}

pub fn format_time_in_minutes(tm: u32) -> String {
    let h = tm / 60;
    let m = tm % 60;
    format!("{h:02}:{m:02}")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn bytes_test() {
        struct Test {
            s: &'static str,
            v: u64,
        }
        let tests: Vec<Test> = vec![
            Test { s: "4789", v: 4789 },
            Test { s: "5k", v: 5 * KB },
            Test { s: "2MiB", v: 2 * MB },
            Test { s: "188Gb", v: 188 * GB },
            Test { s: "24P", v: 24 * PB },
        ];
        for test in tests.iter() {
            let v = str_to_bytes(test.s).unwrap();
            assert_eq!(v, test.v, "\n{}: {} != {}", test.s, test.v, v);
        }
    }

    #[test]
    fn duration_test() {
        struct Test {
            s: &'static str,
            d: i64,
        }
        let tests: Vec<Test> = vec![
            Test { s: "7829", d: 7829 },
            Test { s: "1d89", d: SEC_IN_DAY + 89 },
            Test { s: "21h44m", d: SEC_IN_MINUTE * 44 + SEC_IN_HOUR * 21 },
            Test { s: "3w2s", d: SEC_IN_WEEK * 3 + 2 },
            Test { s: "-1m5s", d: -SEC_IN_MINUTE - 5 },
            Test {
                s: "11d12w13m14h10s",
                d: SEC_IN_WEEK * 12 + SEC_IN_DAY * 11 + SEC_IN_HOUR * 14 + SEC_IN_MINUTE * 13 + 10,
            },
        ];
        for test in tests.iter() {
            let v = str_to_duration(test.s).unwrap();
            assert_eq!(v, test.d, "\n{}: {} != {}", test.s, test.d, v);
        }
    }
    #[test]
    fn str_time_test() {
        struct Test {
            s: &'static str,
            d: Option<u32>,
        }
        let tests: Vec<Test> = vec![
            Test { s: "7829", d: None },
            Test { s: "1060", d: None },
            Test { s: "60", d: None },
            Test { s: "60am", d: None },
            Test { s: "1320am", d: None },
            Test { s: "1011tm", d: None },
            Test { s: "1030", d: Some(1030) },
            Test { s: "2359", d: Some(2359) },
            Test { s: "1011am", d: Some(1011) },
            Test { s: "1011pm", d: Some(2211) },
        ];
        for test in tests.iter() {
            let v = str_to_time(test.s);
            assert_eq!(v, test.d, "\n{0:?}: {1:?} != {2:?}", test.s, test.d, v);
        }
    }
    #[test]
    fn str_time_interval_test() {
        struct Test {
            s: &'static str,
            d: TimeInterval,
        }
        let tests: Vec<Test> = vec![
            Test { s: "7829", d: TimeInterval::Single(None) },
            Test { s: "1020", d: TimeInterval::Single(Some(1020)) },
            Test { s: "..930", d: TimeInterval::Range(None, Some(930)) },
            Test { s: "930am-", d: TimeInterval::Range(Some(930), None) },
            Test { s: "930am-930pm", d: TimeInterval::Range(Some(930), Some(2130)) },
        ];
        for test in tests.iter() {
            let v = str_to_time_interval(test.s);
            assert_eq!(v, test.d, "\n{0:?}: {1:?} != {2:?}", test.s, test.d, v);
        }
    }
}