You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

197 lines
6.3KB

  1. use std::str::FromStr;
  2. use chrono::prelude::*;
  3. use chrono_tz::Tz;
  4. use colored::*;
  5. lazy_static::lazy_static! {
  6. static ref HOUR_AMPM: regex::Regex = regex::Regex::new(r#"(?P<hr>\d{1,2})\s?(?P<ampm>(am|pm|AM|PM))"#).unwrap();
  7. static ref HOUR_MINUTE_AMPM: regex::Regex = regex::Regex::new(r#"(?P<hr>\d{1,2}):(?P<minute>\d{2})\s?(?P<ampm>(am|pm|AM|PM))"#).unwrap();
  8. static ref ZERO_PADDED_OFFSET: regex::Regex = regex::Regex::new(r#"(?P<sign>[+-])0(?P<hr>\d)h"#).unwrap();
  9. }
  10. fn parse_time(time_input: &str, fromtz: &Tz) -> NaiveTime {
  11. let time_input = time_input.trim();
  12. if time_input.eq_ignore_ascii_case("now") {
  13. return Utc::now().with_timezone(fromtz).time()
  14. }
  15. let time: String = time_input.split_whitespace().collect::<Vec<_>>().join("");
  16. match NaiveTime::parse_from_str(&time, "%H:%M") {
  17. Ok(t) => return t,
  18. Err(_e) => {}
  19. }
  20. if HOUR_MINUTE_AMPM.is_match(&time) {
  21. return NaiveTime::parse_from_str(&time, "%I:%M%p").unwrap();
  22. }
  23. let t2 = HOUR_AMPM.replace(&time, "$hr:00$ampm");
  24. match NaiveTime::parse_from_str(&t2, "%I:%M%p") {
  25. Ok(t) => return t,
  26. Err(_e) => {}
  27. }
  28. panic!("failed to parse time -- try HH:MM format (input = '{}')", time)
  29. }
  30. fn convert_common_tz_abbrs(tz: &str) -> &str {
  31. let tz = tz.trim();
  32. if tz.eq_ignore_ascii_case("EST") { return "America/New_York" }
  33. if tz.eq_ignore_ascii_case("EPT") { return "America/New_York" }
  34. if tz.eq_ignore_ascii_case("EDT") { return "America/New_York" }
  35. if tz.eq_ignore_ascii_case("US/Eastern") { return "America/New_York" }
  36. if tz.eq_ignore_ascii_case("CET") { return "Europe/Brussels" }
  37. if tz.eq_ignore_ascii_case("CEST") { return "Europe/Brussels" }
  38. if tz.eq_ignore_ascii_case("Brussels") { return "Europe/Brussels" }
  39. if tz.eq_ignore_ascii_case("utc") { return "UTC" }
  40. if tz.eq_ignore_ascii_case("z") { return "UTC" }
  41. tz
  42. }
  43. pub fn convert(from: &str, to: &str, time: &str, day: Option<String>, date: Option<NaiveDate>) {
  44. let from = convert_common_tz_abbrs(from);
  45. let to = convert_common_tz_abbrs(to);
  46. let fromtz: Tz = match from.parse() {
  47. Ok(tz) => tz,
  48. Err(e) => panic!("failed to parse from tz: {} (input = '{}')", e, from),
  49. };
  50. let totz: Tz = match to.parse() {
  51. Ok(tz) => tz,
  52. Err(e) => panic!("failed to parse to tz: {} (input = '{}')", e, to),
  53. };
  54. let tm = parse_time(time, &fromtz);
  55. let dt: Date<Tz> = match date {
  56. Some(dt) => fromtz.from_local_date(&dt).unwrap(),
  57. None => Utc::today().with_timezone(&fromtz),
  58. };
  59. let dt = match day {
  60. Some(day_str) => {
  61. let target_day_of_week: Weekday = Weekday::from_str(&day_str)
  62. .map_err(|e| {
  63. eprintln!("failed to parse day (input = '{}')", day_str);
  64. e
  65. }).unwrap();
  66. let mut cur = dt;
  67. while cur.weekday() != target_day_of_week {
  68. cur = cur.succ();
  69. }
  70. cur
  71. }
  72. None => dt,
  73. };
  74. let src = dt.and_time(tm).unwrap();
  75. let dst = src.with_timezone(&totz);
  76. // let indent = " ";
  77. // println!();
  78. // println!("{}{} ... {} ... {}",
  79. // indent,
  80. // pad_spaces(fromtz_str, n_spaces),
  81. // src.format("%a, %b %e"),
  82. // src.format("%l:%M%P"),
  83. // );
  84. // println!();
  85. // let dst_line = format!("{}{} ... {} ... {}",
  86. // indent,
  87. // pad_spaces(totz_str, n_spaces),
  88. // dst.format("%a, %b %e"),
  89. // dst.format("%l:%M%P").to_string(),
  90. // );
  91. // println!("{}", dst_line.bold());
  92. println!("tzconvert v{}", structopt::clap::crate_version!());
  93. println!();
  94. let line = get_output(src, dst);
  95. println!(" {}", line);
  96. println!();
  97. }
  98. fn get_output(src: DateTime<Tz>, dst: DateTime<Tz>) -> String {
  99. let mut out = String::with_capacity(128);
  100. let fromtz_str = src.timezone().to_string();
  101. let totz_str = dst.timezone().to_string();
  102. let n_spaces = std::cmp::max(fromtz_str.len(), totz_str.len());
  103. //let mut src_str = format!("{tz} | {offset}h | {dt} | {tm}",
  104. let mut src_str = format!("{tz} --+-- {offset}h --+-- {dt} --+-- {tm}",
  105. tz = pad(fromtz_str, n_spaces, " "),
  106. offset = src.offset().fix(),
  107. dt = src.format("%a, %b %e"),
  108. tm = src.format("%l:%M%P"),
  109. );
  110. src_str = clean(src_str);
  111. out.push_str(&format!("\n {}\n {}\n {}\n", pad("", src_str.len(), " "), src_str, pad("", src_str.len(), " ")));
  112. //out.push_str(" -> ");
  113. let abs_diff_in_hours = (src.offset().fix().local_minus_utc() - dst.offset().fix().local_minus_utc()).abs() / 60 / 60;
  114. out.push_str(&format!(" .\n .\n . {diff}h\n .\n .\n ", diff = abs_diff_in_hours));
  115. //let mut dst_str = format!("{tz} | {offset}h | {dt} | {tm}",
  116. let mut dst_str = format!("{tz} --+-- {offset}h --+-- {dt} --+-- {tm}",
  117. //tz = pad_spaces(dst.timezone(), n_spaces),
  118. tz = pad(totz_str, n_spaces, " "),
  119. offset = dst.offset().fix(),
  120. dt = dst.format("%a, %b %e"),
  121. tm = dst.format("%l:%M%P"),
  122. );
  123. dst_str = clean(dst_str);
  124. out.push_str(&format!(" {}\n {}\n {}\n",
  125. pad("", dst_str.len(), " "),
  126. dst_str.bold(),
  127. pad("", dst_str.len(), " "))
  128. );
  129. //out.push_str(&format!(" {}\n {}\n {}\n\n",
  130. // pad("", dst_str.len(), "-"), dst_str.bold(), pad("", dst_str.len(), "-"))
  131. //);
  132. //out = out.replace(
  133. // " -> ",
  134. // &format!("\n\n .\n . {diff}h\n .\n\n ", diff = abs_diff_in_hours),
  135. //);
  136. out
  137. }
  138. fn clean(mut out: String) -> String {
  139. out = out.replace(":00", "");
  140. //while out.contains(" ") {
  141. // out = out.replace(" ", " ");
  142. //}
  143. out = ZERO_PADDED_OFFSET.replace_all(&out, "${sign}${hr}h").to_string();
  144. out
  145. }
  146. fn pad<S: ToString>(s: S, n: usize, with: &str) -> String {
  147. let s = s.to_string();
  148. let mut out = String::with_capacity(n);
  149. out.push_str(&s);
  150. while out.len() < n {
  151. out.push_str(with);
  152. }
  153. out
  154. }
  155. #[test]
  156. fn parse_time_like_2pm() {
  157. dbg!(parse_time("2pm"));
  158. assert_eq!(parse_time("2pm"), NaiveTime::from_hms(14, 0, 0));
  159. assert_eq!(parse_time("7am"), NaiveTime::from_hms(7, 0, 0));
  160. dbg!(HOUR_AMPM.replace("8:30am", "$hr:00$ampm"));
  161. assert_eq!(parse_time("8:30am"), NaiveTime::from_hms(8, 30, 0));
  162. }