You've already forked parse_datetime
mirror of
https://github.com/uutils/parse_datetime.git
synced 2026-06-10 16:13:15 -07:00
Merge pull request #97 from jfinkels/military-time-zones
Add support for military time zones
This commit is contained in:
+102
-12
@@ -7,11 +7,75 @@ mod time_only_formats {
|
||||
pub const TWELVEHOUR: &str = "%r";
|
||||
}
|
||||
|
||||
/// Convert a military time zone string to a time zone offset.
|
||||
///
|
||||
/// Military time zones are the letters A through Z except J. They are
|
||||
/// described in RFC 5322.
|
||||
fn to_offset(tz: &str) -> Option<FixedOffset> {
|
||||
let hour = match tz {
|
||||
"A" => 1,
|
||||
"B" => 2,
|
||||
"C" => 3,
|
||||
"D" => 4,
|
||||
"E" => 5,
|
||||
"F" => 6,
|
||||
"G" => 7,
|
||||
"H" => 8,
|
||||
"I" => 9,
|
||||
"K" => 10,
|
||||
"L" => 11,
|
||||
"M" => 12,
|
||||
"N" => -1,
|
||||
"O" => -2,
|
||||
"P" => -3,
|
||||
"Q" => -4,
|
||||
"R" => -5,
|
||||
"S" => -6,
|
||||
"T" => -7,
|
||||
"U" => -8,
|
||||
"V" => -9,
|
||||
"W" => -10,
|
||||
"X" => -11,
|
||||
"Y" => -12,
|
||||
"Z" => 0,
|
||||
_ => return None,
|
||||
};
|
||||
let offset_in_sec = hour * 3600;
|
||||
FixedOffset::east_opt(offset_in_sec)
|
||||
}
|
||||
|
||||
/// Parse a time string without an offset and apply an offset to it.
|
||||
///
|
||||
/// Multiple formats are attempted when parsing the string.
|
||||
fn parse_time_with_offset_multi(
|
||||
date: DateTime<Local>,
|
||||
offset: FixedOffset,
|
||||
s: &str,
|
||||
) -> Option<DateTime<FixedOffset>> {
|
||||
for fmt in [
|
||||
time_only_formats::HH_MM,
|
||||
time_only_formats::HH_MM_SS,
|
||||
time_only_formats::TWELVEHOUR,
|
||||
] {
|
||||
let parsed = match NaiveTime::parse_from_str(s, fmt) {
|
||||
Ok(t) => t,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let parsed_dt = date.date_naive().and_time(parsed);
|
||||
match offset.from_local_datetime(&parsed_dt).single() {
|
||||
Some(dt) => return Some(dt),
|
||||
None => continue,
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn parse_time_only(date: DateTime<Local>, s: &str) -> Option<DateTime<FixedOffset>> {
|
||||
let re =
|
||||
Regex::new(r"^(?<time>.*?)(?:(?<sign>\+|-)(?<h>[0-9]{1,2}):?(?<m>[0-9]{0,2}))?$").unwrap();
|
||||
let captures = re.captures(s)?;
|
||||
|
||||
// Parse the sign, hour, and minute to get a `FixedOffset`, if possible.
|
||||
let parsed_offset = match captures.name("h") {
|
||||
Some(hours) if !(hours.as_str().is_empty()) => {
|
||||
let mut offset_in_sec = hours.as_str().parse::<i32>().unwrap() * 3600;
|
||||
@@ -27,18 +91,33 @@ pub(crate) fn parse_time_only(date: DateTime<Local>, s: &str) -> Option<DateTime
|
||||
_ => None,
|
||||
};
|
||||
|
||||
for fmt in [
|
||||
time_only_formats::HH_MM,
|
||||
time_only_formats::HH_MM_SS,
|
||||
time_only_formats::TWELVEHOUR,
|
||||
] {
|
||||
if let Ok(parsed) = NaiveTime::parse_from_str(captures["time"].trim(), fmt) {
|
||||
let parsed_dt = date.date_naive().and_time(parsed);
|
||||
let offset = match parsed_offset {
|
||||
Some(offset) => offset,
|
||||
None => *date.offset(),
|
||||
};
|
||||
return offset.from_local_datetime(&parsed_dt).single();
|
||||
// Parse the time and apply the parsed offset.
|
||||
let s = captures["time"].trim();
|
||||
let offset = match parsed_offset {
|
||||
Some(offset) => offset,
|
||||
None => *date.offset(),
|
||||
};
|
||||
if let Some(result) = parse_time_with_offset_multi(date, offset, s) {
|
||||
return Some(result);
|
||||
}
|
||||
|
||||
// Military time zones are specified in RFC 5322, Section 4.3
|
||||
// "Obsolete Date and Time".
|
||||
// <https://datatracker.ietf.org/doc/html/rfc5322>
|
||||
//
|
||||
// We let the parsing above handle "5:00 AM" so at this point we
|
||||
// should be guaranteed that we don't have an AM/PM suffix. That
|
||||
// way, we can safely parse "5:00M" here without interference.
|
||||
let re = Regex::new(r"(?<time>.*?)(?<tz>[A-IKLMN-YZ])").unwrap();
|
||||
let captures = re.captures(s)?;
|
||||
if let Some(tz) = captures.name("tz") {
|
||||
let s = captures["time"].trim();
|
||||
let offset = match to_offset(tz.as_str()) {
|
||||
Some(offset) => offset,
|
||||
None => *date.offset(),
|
||||
};
|
||||
if let Some(result) = parse_time_with_offset_multi(date, offset, s) {
|
||||
return Some(result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +143,17 @@ mod tests {
|
||||
assert_eq!(parsed_time, 1709499840)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_military_time_zones() {
|
||||
env::set_var("TZ", "UTC");
|
||||
let date = get_test_date();
|
||||
let actual = parse_time_only(date, "05:00C").unwrap().timestamp();
|
||||
// Computed via `date -u -d "2024-03-03 05:00:00C" +%s`, using a
|
||||
// version of GNU date after v8.32 (earlier versions had a bug).
|
||||
let expected = 1709431200;
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_time_with_offset() {
|
||||
env::set_var("TZ", "UTC");
|
||||
|
||||
Reference in New Issue
Block a user