timezone 1.0.0

A CLI timezone converter.
use crate::Time;
use crate::TimeFormat;
use crate::TimeFormat::*;
use crate::TZ_MAP;
use chrono::{Datelike, Duration, NaiveDate, Offset, TimeZone};
use chrono_tz::Tz;
use colored::*;
use regex::Regex;
use std::num::ParseIntError;

pub fn is_pm(time: String) -> bool {
    &time[time.len() - 2..time.len()] == "pm"
}

fn pm_offset(time: String) -> u32 {
    if is_pm(time) {
        12
    } else {
        0
    }
}

pub fn get_time_format(time: String) -> Option<TimeFormat> {
    let simple_am_pm_regex: Regex = Regex::new(r"^(\d){1,2}(am|pm)$").unwrap();
    let full_am_pm_regex: Regex = Regex::new(r"^(\d){1,2}\:(\d){2}(am|pm)$").unwrap();
    let military_colon_regex: Regex = Regex::new(r"^(\d){2}\:(\d){2}$").unwrap();
    let military_regex: Regex = Regex::new(r"^(\d){4}$").unwrap();

    if simple_am_pm_regex.is_match(&time) {
        return Some(SimpleAmPm);
    }

    if full_am_pm_regex.is_match(&time) {
        return Some(FullAmPm);
    }

    if military_colon_regex.is_match(&time) {
        return Some(MilitaryColon);
    }

    if military_regex.is_match(&time) {
        return Some(Military);
    }

    None
}

pub fn parse_time(time: String) -> Option<(u32, u32)> {
    match get_time_format(time.clone()) {
        Some(format) => match format {
            SimpleAmPm => {
                let offset = pm_offset(time.clone());

                let hours_string: String = if time.len() == 3 {
                    time[0..1].to_string()
                } else {
                    time[0..2].to_string()
                };

                match hours_string.parse::<u32>() {
                    Ok(hours_int) => match Time::hours(hours_int) {
                        Some(Time::Hours(h)) => Some((h + offset, 0)),
                        _ => None,
                    },
                    _ => None,
                }
            }
            FullAmPm => {
                let offset = pm_offset(time.clone());

                let truncated = match time.len() {
                    6 => Some(time[0..4].to_string()),
                    7 => Some(time[0..5].to_string()),
                    _ => None,
                };

                match truncated {
                    Some(time_with_colon) => {
                        let hm: Vec<Result<u32, ParseIntError>> = time_with_colon
                            .split(":")
                            .map(|x| x.parse::<u32>())
                            .collect();

                        let maybe_hours = &hm[0];
                        let maybe_minutes = &hm[1];

                        match (maybe_hours, maybe_minutes) {
                            (Ok(h), Ok(m)) => match (Time::hours(h + offset), Time::minutes(*m)) {
                                (Some(Time::Hours(hr)), Some(Time::Minutes(mn))) => Some((hr, mn)),
                                _ => None,
                            },
                            _ => None,
                        }
                    }
                    None => None,
                }
            }

            MilitaryColon => {
                let hm: Vec<Result<u32, ParseIntError>> =
                    time.split(":").map(|x| x.parse::<u32>()).collect();

                let maybe_hours = &hm[0];
                let maybe_minutes = &hm[1];

                match (maybe_hours, maybe_minutes) {
                    (Ok(h), Ok(m)) => match (Time::hours(*h), Time::minutes(*m)) {
                        (Some(Time::Hours(hr)), Some(Time::Minutes(mn))) => Some((hr, mn)),
                        _ => None,
                    },
                    _ => None,
                }
            }
            Military => {
                let maybe_hours = &time[0..2].parse::<u32>();
                let maybe_minutes = &time[2..4].parse::<u32>();

                match (maybe_hours, maybe_minutes) {
                    (Ok(h), Ok(m)) => match (Time::hours(*h), Time::minutes(*m)) {
                        (Some(Time::Hours(hr)), Some(Time::Minutes(mn))) => Some((hr, mn)),
                        _ => None,
                    },
                    _ => None,
                }
            }
        },
        None => None,
    }
}

pub fn parse_timezone(origin: String, destination: Option<String>) -> (Option<Tz>, Option<Tz>) {
    (
        TZ_MAP.get(&origin).copied(),
        match destination {
            Some(ref d) => TZ_MAP.get(&d).copied(),
            None => {
                let offset = chrono::Local
                    .timestamp_opt(0, 0)
                    .unwrap()
                    .offset()
                    .fix()
                    .local_minus_utc()
                    / 60
                    / 60;
                let local = if offset < 0 {
                    format!("utc-{offset}")
                } else {
                    format!("utc+{offset}")
                };
                println!(
                    "\n[{}] cannot parse destination {:?}. Using local timezone = {local}.",
                    "WARNING".yellow(),
                    destination
                );
                TZ_MAP.get(&local).copied()
            }
        },
    )
}

pub fn parse_day(maybe_day: Option<String>) -> Option<u32> {
    if maybe_day == None {
        return parse_day(Some("today".to_string()));
    }

    let day = maybe_day.unwrap().to_lowercase();
    let today = chrono::Utc::now();

    if day == "today" {
        return Some(today.day());
    }

    if day == "yesterday" {
        return Some((today - Duration::days(1)).day());
    }

    if day == "tomorrow" {
        return Some((today + Duration::days(1)).day());
    }

    match day.parse::<u32>() {
        Ok(n) => {
            match NaiveDate::from_ymd_opt(today.year(), today.month(), n.try_into().unwrap()) {
                Some(date) => Some(date.day().try_into().unwrap()),
                None => None,
            }
        }
        Err(_) => None,
    }
}

static MONTH_MAP: phf::Map<&'static str, u32> = phf::phf_map! {
    "jan" => 1,
    "january" => 1,
    "feb" => 2,
    "february" => 2,
    "mar" => 3,
    "march" => 3,
    "apr" => 4,
    "april" => 4,
    "may" => 5,
    "jun" => 6,
    "june" => 6,
    "jul" => 7,
    "july" => 7,
    "aug" => 8,
    "august" => 8,
    "sep" => 9,
    "sept" => 9,
    "september" => 9,
    "oct" => 10,
    "october" => 10,
    "nov" => 11,
    "november" => 11,
    "dec" => 12,
    "december" => 12,
    "1" => 1,
    "01" => 1,
    "2" => 2,
    "02" => 2,
    "3" => 3,
    "03" => 3,
    "4" => 4,
    "04" => 4,
    "5" => 5,
    "05" => 5,
    "6" => 6,
    "06" => 6,
    "7" => 7,
    "07" => 7,
    "8" => 8,
    "08" => 8,
    "9" => 9,
    "09" => 9,
    "10" => 10,
    "11" => 11,
    "12" => 12,
};

pub fn parse_month(maybe_month: Option<String>) -> Option<u32> {
    if maybe_month == None {
        return parse_month(Some(chrono::Utc::now().month().to_string()));
    }

    return MONTH_MAP.get(&maybe_month.unwrap()).copied();
}

pub fn parse_year(maybe_year: Option<String>) -> Option<i32> {
    if maybe_year == None {
        return parse_year(Some(chrono::Utc::now().year().to_string()));
    }

    match maybe_year.unwrap().parse::<i32>() {
        Ok(n) => match NaiveDate::from_ymd_opt(n, 1, 1) {
            Some(date) => Some(date.year().try_into().unwrap()),
            None => None,
        },
        Err(_) => None,
    }
}