use std::str::FromStr; use chrono::prelude::*; use chrono_tz::Tz; use colored::*; lazy_static::lazy_static! { static ref HOUR_AMPM: regex::Regex = regex::Regex::new(r#"(?P
\d{1,2})\s?(?P(am|pm|AM|PM))"#).unwrap(); static ref HOUR_MINUTE_AMPM: regex::Regex = regex::Regex::new(r#"(?P
\d{1,2}):(?P\d{2})\s?(?P(am|pm|AM|PM))"#).unwrap(); static ref ZERO_PADDED_OFFSET: regex::Regex = regex::Regex::new(r#"(?P[+-])0(?P
\d)h"#).unwrap(); } fn parse_time(time_input: &str, fromtz: &Tz) -> NaiveTime { let time_input = time_input.trim(); if time_input.eq_ignore_ascii_case("now") { return Utc::now().with_timezone(fromtz).time() } let time: String = time_input.split_whitespace().collect::>().join(""); match NaiveTime::parse_from_str(&time, "%H:%M") { Ok(t) => return t, Err(_e) => {} } if HOUR_MINUTE_AMPM.is_match(&time) { return NaiveTime::parse_from_str(&time, "%I:%M%p").unwrap(); } let t2 = HOUR_AMPM.replace(&time, "$hr:00$ampm"); match NaiveTime::parse_from_str(&t2, "%I:%M%p") { Ok(t) => return t, Err(_e) => {} } panic!("failed to parse time -- try HH:MM format (input = '{}')", time) } fn convert_common_tz_abbrs(tz: &str) -> &str { let tz = tz.trim(); if tz.eq_ignore_ascii_case("EST") { return "America/New_York" } if tz.eq_ignore_ascii_case("EPT") { return "America/New_York" } if tz.eq_ignore_ascii_case("EDT") { return "America/New_York" } if tz.eq_ignore_ascii_case("US/Eastern") { return "America/New_York" } if tz.eq_ignore_ascii_case("CET") { return "Europe/Brussels" } if tz.eq_ignore_ascii_case("CEST") { return "Europe/Brussels" } if tz.eq_ignore_ascii_case("Brussels") { return "Europe/Brussels" } tz } pub fn convert(from: &str, to: &str, time: &str, day: Option, date: Option) { let from = convert_common_tz_abbrs(from); let to = convert_common_tz_abbrs(to); let fromtz: Tz = match from.parse() { Ok(tz) => tz, Err(e) => panic!("failed to parse from tz: {} (input = '{}')", e, from), }; let totz: Tz = match to.parse() { Ok(tz) => tz, Err(e) => panic!("failed to parse to tz: {} (input = '{}')", e, to), }; let tm = parse_time(time, &fromtz); let dt: Date = match date { Some(dt) => fromtz.from_local_date(&dt).unwrap(), None => Utc::today().with_timezone(&fromtz), }; let dt = match day { Some(day_str) => { let target_day_of_week: Weekday = Weekday::from_str(&day_str) .map_err(|e| { eprintln!("failed to parse day (input = '{}')", day_str); e }).unwrap(); let mut cur = dt; while cur.weekday() != target_day_of_week { cur = cur.succ(); } cur } None => dt, }; let src = dt.and_time(tm).unwrap(); let dst = src.with_timezone(&totz); // let indent = " "; // println!(); // println!("{}{} ... {} ... {}", // indent, // pad_spaces(fromtz_str, n_spaces), // src.format("%a, %b %e"), // src.format("%l:%M%P"), // ); // println!(); // let dst_line = format!("{}{} ... {} ... {}", // indent, // pad_spaces(totz_str, n_spaces), // dst.format("%a, %b %e"), // dst.format("%l:%M%P").to_string(), // ); // println!("{}", dst_line.bold()); println!("tzconvert v{}", structopt::clap::crate_version!()); println!(); let line = get_output(src, dst); println!(" {}", line); println!(); } fn get_output(src: DateTime, dst: DateTime) -> String { let mut out = String::with_capacity(128); let fromtz_str = src.timezone().to_string(); let totz_str = dst.timezone().to_string(); let n_spaces = std::cmp::max(fromtz_str.len(), totz_str.len()); //let mut src_str = format!("{tz} | {offset}h | {dt} | {tm}", let mut src_str = format!("{tz} --+-- {offset}h --+-- {dt} --+-- {tm}", tz = pad(fromtz_str, n_spaces, " "), offset = src.offset().fix(), dt = src.format("%a, %b %e"), tm = src.format("%l:%M%P"), ); src_str = clean(src_str); out.push_str(&format!("\n {}\n {}\n {}\n", pad("", src_str.len(), " "), src_str, pad("", src_str.len(), " "))); //out.push_str(" -> "); let abs_diff_in_hours = (src.offset().fix().local_minus_utc() - dst.offset().fix().local_minus_utc()).abs() / 60 / 60; out.push_str(&format!(" .\n .\n . {diff}h\n .\n .\n ", diff = abs_diff_in_hours)); //let mut dst_str = format!("{tz} | {offset}h | {dt} | {tm}", let mut dst_str = format!("{tz} --+-- {offset}h --+-- {dt} --+-- {tm}", //tz = pad_spaces(dst.timezone(), n_spaces), tz = pad(totz_str, n_spaces, " "), offset = dst.offset().fix(), dt = dst.format("%a, %b %e"), tm = dst.format("%l:%M%P"), ); dst_str = clean(dst_str); out.push_str(&format!(" {}\n {}\n {}\n", pad("", dst_str.len(), " "), dst_str.bold(), pad("", dst_str.len(), " ")) ); //out.push_str(&format!(" {}\n {}\n {}\n\n", // pad("", dst_str.len(), "-"), dst_str.bold(), pad("", dst_str.len(), "-")) //); //out = out.replace( // " -> ", // &format!("\n\n .\n . {diff}h\n .\n\n ", diff = abs_diff_in_hours), //); out } fn clean(mut out: String) -> String { out = out.replace(":00", ""); //while out.contains(" ") { // out = out.replace(" ", " "); //} out = ZERO_PADDED_OFFSET.replace_all(&out, "${sign}${hr}h").to_string(); out } fn pad(s: S, n: usize, with: &str) -> String { let s = s.to_string(); let mut out = String::with_capacity(n); out.push_str(&s); while out.len() < n { out.push_str(with); } out } #[test] fn parse_time_like_2pm() { dbg!(parse_time("2pm")); assert_eq!(parse_time("2pm"), NaiveTime::from_hms(14, 0, 0)); assert_eq!(parse_time("7am"), NaiveTime::from_hms(7, 0, 0)); dbg!(HOUR_AMPM.replace("8:30am", "$hr:00$ampm")); assert_eq!(parse_time("8:30am"), NaiveTime::from_hms(8, 30, 0)); }