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));
}