commit cab79d44ec9f3d12aeef595c1f2913c841d90b7f Author: Jonathan Strong Date: Thu Aug 26 09:12:02 2021 -0400 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..46f3847 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +Cargo.lock +*.swp diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3a19b2e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "tzconvert" +version = "1.0.0" +edition = "2018" +authors = ["Jonathan Strong "] + +[lib] +name = "tzconvert" +path = "src/lib.rs" + +[[bin]] +name = "tzconvert" +path = "src/main.rs" + +[[bin]] +name = "cet2est" +path = "src/cet2est.rs" + +[[bin]] +name = "est2cet" +path = "src/est2cet.rs" + +[dependencies] +chrono = { version = "0.4", features = ["serde"] } +chrono-tz = { version = "0.5", features = ["serde"] } +structopt = "0.3" +regex = "1.5" +lazy_static = "1.4" + diff --git a/justfile b/justfile new file mode 100644 index 0000000..e975e09 --- /dev/null +++ b/justfile @@ -0,0 +1,16 @@ +release-build: + cargo build --bin tzconvert --release + cargo build --bin cet2est --release + cargo build --bin est2cet --release + +install: release-build + cp target/release/tzconvert ~/.cargo/bin/ + cp target/release/cet2est ~/.cargo/bin/ + cp target/release/est2cet ~/.cargo/bin/ + +debug-build: + cargo build --bin tzconvert + cargo build --bin cet2est + cargo build --bin est2cet + + diff --git a/src/cet2est.rs b/src/cet2est.rs new file mode 100644 index 0000000..34efd20 --- /dev/null +++ b/src/cet2est.rs @@ -0,0 +1,19 @@ +use structopt::StructOpt; +use chrono::prelude::*; + +#[derive(StructOpt, Debug)] +#[structopt(author = env!("CARGO_PKG_AUTHORS"))] +struct Opt { + /// time to convert + #[structopt(value_name = "TIME")] + time: String, + + /// specify the date on which the time lies. defaults to today. + #[structopt(long, short)] + date: Option, +} + +fn main() { + let Opt { time, date } = Opt::from_args(); + tzconvert::convert("Europe/Brussels", "EST", &time, date); +} diff --git a/src/est2cet.rs b/src/est2cet.rs new file mode 100644 index 0000000..504d9dc --- /dev/null +++ b/src/est2cet.rs @@ -0,0 +1,19 @@ +use structopt::StructOpt; +use chrono::prelude::*; + +#[derive(StructOpt, Debug)] +#[structopt(author = env!("CARGO_PKG_AUTHORS"))] +struct Opt { + /// time to convert + #[structopt(value_name = "HH:MM")] + time: String, + + /// specify the date on which the time lies. defaults to today. + #[structopt(long, short)] + date: Option, +} + +fn main() { + let Opt { time, date } = Opt::from_args(); + tzconvert::convert("EST", "Europe/Brussels", &time, date); +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..4b19eff --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,110 @@ +use chrono::prelude::*; +use chrono_tz::Tz; + +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(); +} + +fn parse_time(time_input: &str) -> NaiveTime { + 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, 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); + + let dt: Date = match date { + Some(dt) => fromtz.from_local_date(&dt).unwrap(), + None => Utc::today().with_timezone(&fromtz), + }; + + let src = dt.and_time(tm).unwrap(); + + let dst = src.with_timezone(&totz); + + let fromtz_str = fromtz.to_string(); + let totz_str = totz.to_string(); + let n_spaces = std::cmp::max(fromtz_str.len(), totz_str.len()); + + let indent = " "; + println!(); + println!("{} {} ... {} ... {}", + indent, + pad_spaces(fromtz_str, n_spaces), + src.format("%a, %b %e"), + src.format("%l:%M%P"), + ); + + println!(); + + println!("{} {} ... {} ... {}", + indent, + pad_spaces(totz_str, n_spaces), + dst.format("%a, %b %e"), + dst.format("%l:%M%P"), + ); + + println!(); +} + +fn pad_spaces(s: S, n: usize) -> String { + let s = s.to_string(); + let mut out = String::with_capacity(n); + out.push_str(&s); + while out.len() < n { + out.push_str(" "); + } + 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)); +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..323f0fe --- /dev/null +++ b/src/main.rs @@ -0,0 +1,33 @@ +use structopt::StructOpt; +use chrono::prelude::*; + +/// timezone converter +/// +/// Examples: +/// +/// - tzconvert CET EST 2pm +/// +/// - tzconvert Africa/Timbuktu America/Jamaica 18:30 --date 2021-11-03 +/// +#[derive(StructOpt, Debug)] +#[structopt(author = env!("CARGO_PKG_AUTHORS"))] +struct Opt { + /// originating time zone + #[structopt(value_name = "FROM TIMEZONE")] + from: String, + /// destination time zone + #[structopt(value_name = "TO TIMEZONE")] + to: String, + /// time to convert (HH:MM or HH[:MM]am/HH[:MM]pm) + #[structopt(value_name = "TIME")] + time: String, + + /// specify the date on which the time lies; defaults to today + #[structopt(long, short)] + date: Option, +} + +fn main() { + let Opt { from, to, time, date } = Opt::from_args(); + tzconvert::convert(&from, &to, &time, date); +}