You've already forked parse_datetime
mirror of
https://github.com/uutils/parse_datetime.git
synced 2026-06-10 16:13:15 -07:00
refactor: extract timezone functionality from time module to timezone module.
Move timezone-related code from `time.rs` to `timezone.rs` to improve separation of concerns. The `Offset` struct and all timezone parsing functions are now properly separated from time parsing logic.
This commit is contained in:
committed by
Daniel Hofstetter
parent
3eb58ab00a
commit
2bc18c52b7
@@ -3,7 +3,7 @@
|
||||
|
||||
use chrono::{DateTime, Datelike, FixedOffset, NaiveDate, TimeZone, Timelike};
|
||||
|
||||
use super::{date, relative, time, weekday, year};
|
||||
use super::{date, relative, time, timezone, weekday, year};
|
||||
|
||||
/// The builder is used to construct a DateTime object from various components.
|
||||
/// The parser creates a `DateTimeBuilder` object with the parsed components,
|
||||
@@ -17,7 +17,7 @@ pub(crate) struct DateTimeBuilder {
|
||||
date: Option<date::Date>,
|
||||
time: Option<time::Time>,
|
||||
weekday: Option<weekday::Weekday>,
|
||||
timezone: Option<time::Offset>,
|
||||
timezone: Option<timezone::Offset>,
|
||||
relative: Vec<relative::Relative>,
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ impl DateTimeBuilder {
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub(super) fn set_timezone(mut self, timezone: time::Offset) -> Result<Self, &'static str> {
|
||||
pub(super) fn set_timezone(mut self, timezone: timezone::Offset) -> Result<Self, &'static str> {
|
||||
if self.timestamp.is_some() {
|
||||
return Err("timestamp cannot be combined with other date/time items");
|
||||
} else if self.timezone.is_some() {
|
||||
@@ -338,7 +338,7 @@ fn new_date(
|
||||
/// Restores year, month, day, etc after applying the timezone
|
||||
/// returns None if timezone overflows the date
|
||||
fn with_timezone_restore(
|
||||
offset: time::Offset,
|
||||
offset: timezone::Offset,
|
||||
at: DateTime<FixedOffset>,
|
||||
) -> Option<DateTime<FixedOffset>> {
|
||||
let offset: FixedOffset = chrono::FixedOffset::try_from(offset).ok()?;
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ pub(crate) enum Item {
|
||||
Time(time::Time),
|
||||
Weekday(weekday::Weekday),
|
||||
Relative(relative::Relative),
|
||||
TimeZone(time::Offset),
|
||||
TimeZone(timezone::Offset),
|
||||
Pure(String),
|
||||
}
|
||||
|
||||
|
||||
@@ -132,6 +132,14 @@ where
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse a colon preceded by whitespace
|
||||
pub(super) fn colon<'a, E>(input: &mut &'a str) -> winnow::Result<(), E>
|
||||
where
|
||||
E: ParserError<&'a str>,
|
||||
{
|
||||
s(':').void().parse_next(input)
|
||||
}
|
||||
|
||||
/// Create a context error with a reason.
|
||||
pub(super) fn ctx_err(reason: &'static str) -> ContextError {
|
||||
let mut err = ContextError::new();
|
||||
|
||||
+10
-332
@@ -3,7 +3,7 @@
|
||||
|
||||
// spell-checker:ignore shhmm colonless
|
||||
|
||||
//! Parse a time item (without a date)
|
||||
//! Parse a time item (without a date).
|
||||
//!
|
||||
//! The GNU docs state:
|
||||
//!
|
||||
@@ -37,124 +37,39 @@
|
||||
//! >
|
||||
//! > Either ‘am’/‘pm’ or a time zone correction may be specified, but not both.
|
||||
|
||||
use std::fmt::Display;
|
||||
|
||||
use chrono::FixedOffset;
|
||||
use winnow::{
|
||||
combinator::{alt, opt, peek, preceded},
|
||||
error::{ContextError, ErrMode},
|
||||
seq,
|
||||
stream::AsChar,
|
||||
token::take_while,
|
||||
ModalResult, Parser,
|
||||
combinator::{alt, opt, preceded},
|
||||
error::ErrMode,
|
||||
seq, ModalResult, Parser,
|
||||
};
|
||||
|
||||
use crate::ParseDateTimeError;
|
||||
|
||||
use super::{
|
||||
primitive::{ctx_err, dec_uint, float, s},
|
||||
relative,
|
||||
primitive::{colon, ctx_err, dec_uint, float, s},
|
||||
timezone::{timezone_num, Offset},
|
||||
};
|
||||
|
||||
#[derive(PartialEq, Debug, Clone, Default)]
|
||||
pub struct Offset {
|
||||
pub(crate) negative: bool,
|
||||
pub(crate) hours: u32,
|
||||
pub(crate) minutes: u32,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Debug, Default)]
|
||||
pub struct Time {
|
||||
pub(crate) struct Time {
|
||||
pub hour: u32,
|
||||
pub minute: u32,
|
||||
pub second: f64,
|
||||
pub offset: Option<Offset>,
|
||||
}
|
||||
|
||||
impl Offset {
|
||||
fn merge(self, offset: Offset) -> Offset {
|
||||
fn combine(a: u32, neg_a: bool, b: u32, neg_b: bool) -> (u32, bool) {
|
||||
if neg_a == neg_b {
|
||||
(a + b, neg_a)
|
||||
} else if a > b {
|
||||
(a - b, neg_a)
|
||||
} else {
|
||||
(b - a, neg_b)
|
||||
}
|
||||
}
|
||||
let (hours_minutes, negative) = combine(
|
||||
self.hours * 60 + self.minutes,
|
||||
self.negative,
|
||||
offset.hours * 60 + offset.minutes,
|
||||
offset.negative,
|
||||
);
|
||||
let hours = hours_minutes / 60;
|
||||
let minutes = hours_minutes % 60;
|
||||
|
||||
Offset {
|
||||
negative,
|
||||
hours,
|
||||
minutes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Offset> for chrono::FixedOffset {
|
||||
type Error = ParseDateTimeError;
|
||||
|
||||
fn try_from(
|
||||
Offset {
|
||||
negative,
|
||||
hours,
|
||||
minutes,
|
||||
}: Offset,
|
||||
) -> Result<Self, Self::Error> {
|
||||
let secs = hours * 3600 + minutes * 60;
|
||||
|
||||
let offset = if negative {
|
||||
FixedOffset::west_opt(
|
||||
secs.try_into()
|
||||
.map_err(|_| ParseDateTimeError::InvalidInput)?,
|
||||
)
|
||||
.ok_or(ParseDateTimeError::InvalidInput)?
|
||||
} else {
|
||||
FixedOffset::east_opt(
|
||||
secs.try_into()
|
||||
.map_err(|_| ParseDateTimeError::InvalidInput)?,
|
||||
)
|
||||
.ok_or(ParseDateTimeError::InvalidInput)?
|
||||
};
|
||||
|
||||
Ok(offset)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Offset {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
write!(
|
||||
fmt,
|
||||
"{}{:02}:{:02}",
|
||||
if self.negative { "-" } else { "+" },
|
||||
self.hours,
|
||||
self.minutes
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Meridiem {
|
||||
Am,
|
||||
Pm,
|
||||
}
|
||||
|
||||
pub fn parse(input: &mut &str) -> ModalResult<Time> {
|
||||
pub(crate) fn parse(input: &mut &str) -> ModalResult<Time> {
|
||||
alt((am_pm_time, iso)).parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse an ISO 8601 time string
|
||||
///
|
||||
/// Also used by the [`combined`](super::combined) module
|
||||
pub fn iso(input: &mut &str) -> ModalResult<Time> {
|
||||
pub(super) fn iso(input: &mut &str) -> ModalResult<Time> {
|
||||
alt((
|
||||
(hour24, timezone_num).map(|(hour, offset)| Time {
|
||||
hour,
|
||||
@@ -208,11 +123,6 @@ fn am_pm_time(input: &mut &str) -> ModalResult<Time> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse a colon preceded by whitespace
|
||||
fn colon(input: &mut &str) -> ModalResult<()> {
|
||||
s(':').void().parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse a number of hours in `0..24`(preceded by whitespace)
|
||||
pub(super) fn hour24(input: &mut &str) -> ModalResult<u32> {
|
||||
s(dec_uint).verify(|x| *x < 24).parse_next(input)
|
||||
@@ -240,195 +150,9 @@ fn second(input: &mut &str) -> ModalResult<f64> {
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
pub(crate) fn timezone(input: &mut &str) -> ModalResult<Offset> {
|
||||
timezone_name_offset.parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse a timezone starting with `+` or `-`
|
||||
fn timezone_num(input: &mut &str) -> ModalResult<Offset> {
|
||||
// Strings like "+8 years" are ambiguous, they can either be parsed as a
|
||||
// timezone offset "+8" and a relative time "years", or just a relative time
|
||||
// "+8 years". GNU date parses them the second way, so we do the same here.
|
||||
//
|
||||
// Return early if the input can be parsed as a relative time.
|
||||
if peek(relative::parse).parse_next(input).is_ok() {
|
||||
return Err(ErrMode::Backtrack(ContextError::new()));
|
||||
}
|
||||
|
||||
seq!(plus_or_minus, alt((timezone_colon, timezone_colonless)))
|
||||
.parse_next(input)
|
||||
.and_then(|(negative, (hours, minutes))| {
|
||||
if !(0..=12).contains(&hours) {
|
||||
return Err(ErrMode::Cut(ctx_err("timezone hour between 0 and 12")));
|
||||
}
|
||||
|
||||
if !(0..=60).contains(&minutes) {
|
||||
return Err(ErrMode::Cut(ctx_err("timezone minute between 0 and 60")));
|
||||
}
|
||||
|
||||
Ok(Offset {
|
||||
negative,
|
||||
hours,
|
||||
minutes,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse a timezone offset with a colon separating hours and minutes
|
||||
fn timezone_colon(input: &mut &str) -> ModalResult<(u32, u32)> {
|
||||
seq!(
|
||||
s(take_while(1..=2, AsChar::is_dec_digit)).try_map(|x: &str| x.parse()),
|
||||
_: colon,
|
||||
s(take_while(1..=2, AsChar::is_dec_digit)).try_map(|x: &str| x.parse()),
|
||||
)
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse a timezone offset without colon
|
||||
fn timezone_colonless(input: &mut &str) -> ModalResult<(u32, u32)> {
|
||||
s(take_while(0.., AsChar::is_dec_digit))
|
||||
.verify_map(|s: &str| {
|
||||
// GNU date supports number strings with leading zeroes, e.g.,
|
||||
// `UTC+000001100` is valid.
|
||||
let s = if s.len() > 4 {
|
||||
s.trim_start_matches('0')
|
||||
} else {
|
||||
s
|
||||
};
|
||||
Some(match s.len() {
|
||||
0 => (0, 0),
|
||||
1 | 2 => (s.parse().ok()?, 0),
|
||||
// The minutes are the last two characters here, for some reason.
|
||||
3 => (s[..1].parse().ok()?, s[1..].parse().ok()?),
|
||||
4 => (s[..2].parse().ok()?, s[2..].parse().ok()?),
|
||||
_ => return None,
|
||||
})
|
||||
})
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse a timezone by name
|
||||
fn timezone_name_offset(input: &mut &str) -> ModalResult<Offset> {
|
||||
/// I'm assuming there are no timezone abbreviations with more
|
||||
/// than 6 charactres
|
||||
const MAX_TZ_SIZE: usize = 6;
|
||||
let nextword = s(take_while(1..=MAX_TZ_SIZE, AsChar::is_alpha)).parse_next(input)?;
|
||||
let tz = tzname_to_offset(nextword)?;
|
||||
|
||||
// Strings like "UTC +8 years" are ambiguous, they can either be parsed as
|
||||
// "UTC+8" and "years", or "UTC" and "+8 years". GNU date parses them the
|
||||
// second way, so we do the same here.
|
||||
//
|
||||
// Only process if the input cannot be parsed as a relative time.
|
||||
if peek(relative::parse).parse_next(input).is_err() {
|
||||
if let Ok(other_tz) = timezone_num.parse_next(input) {
|
||||
let newtz = tz.merge(other_tz);
|
||||
|
||||
return Ok(newtz);
|
||||
};
|
||||
}
|
||||
|
||||
Ok(tz)
|
||||
}
|
||||
|
||||
/// Named timezone list.
|
||||
///
|
||||
/// The full list of timezones can be extracted from
|
||||
/// https://www.timeanddate.com/time/zones/. GNU date only supports a subset of
|
||||
/// these. We support the same subset as GNU date.
|
||||
///
|
||||
/// From the GNU date manual:
|
||||
///
|
||||
/// > Time zone items other than ‘UTC’ and ‘Z’ are obsolescent and are not
|
||||
/// > recommended, because they are ambiguous; for example, ‘EST’ has a
|
||||
/// > different meaning in Australia than in the United States, and ‘A’ has
|
||||
/// > different meaning as a military time zone than as an obsolete RFC 822
|
||||
/// > time zone. Instead, it's better to use unambiguous numeric time zone
|
||||
/// > corrections like ‘-0500’.
|
||||
fn tzname_to_offset(input: &str) -> ModalResult<Offset> {
|
||||
let mut offset_str = match input {
|
||||
"z" => Ok("+0"),
|
||||
"y" => Ok("-12"),
|
||||
"x" => Ok("-11"),
|
||||
"wet" => Ok("+0"),
|
||||
"west" => Ok("+1"),
|
||||
"wat" => Ok("+1"),
|
||||
"w" => Ok("-10"),
|
||||
"v" => Ok("-9"),
|
||||
"utc" => Ok("+0"),
|
||||
"u" => Ok("-8"),
|
||||
"t" => Ok("-7"),
|
||||
"sst" => Ok("-11"),
|
||||
"sgt" => Ok("+8"),
|
||||
"sast" => Ok("+2"),
|
||||
"s" => Ok("-6"),
|
||||
"r" => Ok("-5"),
|
||||
"q" => Ok("-4"),
|
||||
"pst" => Ok("-8"),
|
||||
"pdt" => Ok("-7"),
|
||||
"p" => Ok("-3"),
|
||||
"o" => Ok("-2"),
|
||||
"nzst" => Ok("+12"),
|
||||
"nzdt" => Ok("+13"),
|
||||
"nst" => Ok("-3:30"),
|
||||
"ndt" => Ok("-2:30"),
|
||||
"n" => Ok("-1"),
|
||||
"mst" => Ok("-7"),
|
||||
"msk" => Ok("+3"),
|
||||
"msd" => Ok("+4"),
|
||||
"mdt" => Ok("-6"),
|
||||
"m" => Ok("+12"),
|
||||
"l" => Ok("+11"),
|
||||
"k" => Ok("+10"),
|
||||
"jst" => Ok("+9"),
|
||||
"ist" => Ok("+5:30"),
|
||||
"i" => Ok("+9"),
|
||||
"hst" => Ok("-10"),
|
||||
"h" => Ok("+8"),
|
||||
"gst" => Ok("+4"),
|
||||
"gmt" => Ok("+0"),
|
||||
"g" => Ok("+7"),
|
||||
"f" => Ok("+6"),
|
||||
"est" => Ok("-5"),
|
||||
"eet" => Ok("+2"),
|
||||
"eest" => Ok("+3"),
|
||||
"edt" => Ok("-4"),
|
||||
"eat" => Ok("+3"),
|
||||
"e" => Ok("+5"),
|
||||
"d" => Ok("+4"),
|
||||
"cst" => Ok("-6"),
|
||||
"clt" => Ok("-4"),
|
||||
"clst" => Ok("-3"),
|
||||
"cet" => Ok("+1"),
|
||||
"cest" => Ok("+2"),
|
||||
"cdt" => Ok("-5"),
|
||||
"cat" => Ok("+2"),
|
||||
"c" => Ok("+3"),
|
||||
"bst" => Ok("+6"),
|
||||
"brt" => Ok("-3"),
|
||||
"brst" => Ok("-2"),
|
||||
"b" => Ok("+2"),
|
||||
"ast" => Ok("-3"),
|
||||
"art" => Ok("-3"),
|
||||
"akst" => Ok("-9"),
|
||||
"akdt" => Ok("-8"),
|
||||
"adt" => Ok("+4"),
|
||||
"a" => Ok("+1"),
|
||||
_ => Err(ErrMode::Backtrack(ContextError::new())),
|
||||
}?;
|
||||
|
||||
timezone_num(&mut offset_str)
|
||||
}
|
||||
|
||||
/// Parse the plus or minus character and return whether it was negative
|
||||
fn plus_or_minus(input: &mut &str) -> ModalResult<bool> {
|
||||
s(alt(("+".value(false), "-".value(true)))).parse_next(input)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Offset, Time};
|
||||
use crate::items::time::parse;
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn simple() {
|
||||
@@ -667,50 +391,4 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timezone_colonless() {
|
||||
use super::timezone_colonless;
|
||||
|
||||
fn aux(inp: &mut &str) -> String {
|
||||
format!("{:?}", timezone_colonless(inp).expect("timezone_colonless"))
|
||||
}
|
||||
|
||||
assert_eq!("(0, 0)", aux(&mut "0000"));
|
||||
assert_eq!("(12, 34)", aux(&mut "1234"));
|
||||
assert_eq!("(12, 34)", aux(&mut "00001234"));
|
||||
assert!(timezone_colonless(&mut "12345").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timezone() {
|
||||
use super::timezone;
|
||||
let make_timezone = |input: &mut &str| {
|
||||
timezone(input)
|
||||
.map_err(|e| eprintln!("TEST FAILED AT:\n{e}"))
|
||||
.map(|offset| format!("{offset}"))
|
||||
.expect("expect tests to succeed")
|
||||
};
|
||||
|
||||
assert_eq!("+00:00", make_timezone(&mut "gmt"));
|
||||
assert_eq!("+01:00", make_timezone(&mut "a"));
|
||||
assert_eq!("+00:00", make_timezone(&mut "utc"));
|
||||
assert_eq!("-02:00", make_timezone(&mut "brst"));
|
||||
assert_eq!("-03:00", make_timezone(&mut "brt"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timezone_num() {
|
||||
use super::timezone_num;
|
||||
let make_timezone = |input: &mut &str| {
|
||||
timezone_num(input)
|
||||
.map_err(|e| eprintln!("TEST FAILED AT:\n{e}"))
|
||||
.map(|offset| format!("{offset}"))
|
||||
.expect("expect tests to succeed")
|
||||
};
|
||||
|
||||
assert_eq!("+00:00", make_timezone(&mut "+00:00"));
|
||||
assert_eq!("+00:00", make_timezone(&mut "+0000"));
|
||||
assert_eq!("-00:00", make_timezone(&mut "-0000"));
|
||||
}
|
||||
}
|
||||
|
||||
+342
-4
@@ -1,10 +1,348 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
use winnow::ModalResult;
|
||||
//! Parse a timezone item.
|
||||
//!
|
||||
//! The GNU docs state:
|
||||
//!
|
||||
//! > A “time zone item” specifies an international time zone, indicated by a
|
||||
//! > small set of letters, e.g., ‘UTC’ or ‘Z’ for Coordinated Universal Time.
|
||||
//! > Any included periods are ignored. By following a non-daylight-saving
|
||||
//! > time zone by the string ‘DST’ in a separate word (that is, separated by
|
||||
//! > some white space), the corresponding daylight saving time zone may be
|
||||
//! > specified. Alternatively, a non-daylight-saving time zone can be
|
||||
//! > followed by a time zone correction, to add the two values. This is
|
||||
//! > normally done only for ‘UTC’; for example, ‘UTC+05:30’ is equivalent to
|
||||
//! > ‘+05:30’.
|
||||
//! >
|
||||
//! > Time zone items other than ‘UTC’ and ‘Z’ are obsolescent and are not
|
||||
//! > recommended, because they are ambiguous; for example, ‘EST’ has a
|
||||
//! > different meaning in Australia than in the United States, and ‘A’ has
|
||||
//! > different meaning as a military time zone than as an obsolete RFC 822
|
||||
//! > time zone. Instead, it's better to use unambiguous numeric time zone
|
||||
//! > corrections like ‘-0500’, as described in the previous section.
|
||||
//! >
|
||||
//! > If neither a time zone item nor a time zone correction is supplied,
|
||||
//! > timestamps are interpreted using the rules of the default time zone
|
||||
|
||||
use super::time;
|
||||
use std::fmt::Display;
|
||||
|
||||
pub(crate) fn parse(input: &mut &str) -> ModalResult<time::Offset> {
|
||||
time::timezone(input)
|
||||
use chrono::FixedOffset;
|
||||
use winnow::{
|
||||
combinator::{alt, peek, seq},
|
||||
error::{ContextError, ErrMode},
|
||||
stream::AsChar,
|
||||
token::take_while,
|
||||
ModalResult, Parser,
|
||||
};
|
||||
|
||||
use crate::{items::primitive::colon, ParseDateTimeError};
|
||||
|
||||
use super::{
|
||||
primitive::{ctx_err, s},
|
||||
relative,
|
||||
};
|
||||
|
||||
#[derive(PartialEq, Debug, Clone, Default)]
|
||||
pub(crate) struct Offset {
|
||||
pub(crate) negative: bool,
|
||||
pub(crate) hours: u32,
|
||||
pub(crate) minutes: u32,
|
||||
}
|
||||
|
||||
impl Offset {
|
||||
fn merge(self, offset: Offset) -> Offset {
|
||||
fn combine(a: u32, neg_a: bool, b: u32, neg_b: bool) -> (u32, bool) {
|
||||
if neg_a == neg_b {
|
||||
(a + b, neg_a)
|
||||
} else if a > b {
|
||||
(a - b, neg_a)
|
||||
} else {
|
||||
(b - a, neg_b)
|
||||
}
|
||||
}
|
||||
let (hours_minutes, negative) = combine(
|
||||
self.hours * 60 + self.minutes,
|
||||
self.negative,
|
||||
offset.hours * 60 + offset.minutes,
|
||||
offset.negative,
|
||||
);
|
||||
let hours = hours_minutes / 60;
|
||||
let minutes = hours_minutes % 60;
|
||||
|
||||
Offset {
|
||||
negative,
|
||||
hours,
|
||||
minutes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Offset> for chrono::FixedOffset {
|
||||
type Error = ParseDateTimeError;
|
||||
|
||||
fn try_from(
|
||||
Offset {
|
||||
negative,
|
||||
hours,
|
||||
minutes,
|
||||
}: Offset,
|
||||
) -> Result<Self, Self::Error> {
|
||||
let secs = hours * 3600 + minutes * 60;
|
||||
|
||||
let offset = if negative {
|
||||
FixedOffset::west_opt(
|
||||
secs.try_into()
|
||||
.map_err(|_| ParseDateTimeError::InvalidInput)?,
|
||||
)
|
||||
.ok_or(ParseDateTimeError::InvalidInput)?
|
||||
} else {
|
||||
FixedOffset::east_opt(
|
||||
secs.try_into()
|
||||
.map_err(|_| ParseDateTimeError::InvalidInput)?,
|
||||
)
|
||||
.ok_or(ParseDateTimeError::InvalidInput)?
|
||||
};
|
||||
|
||||
Ok(offset)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Offset {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
write!(
|
||||
fmt,
|
||||
"{}{:02}:{:02}",
|
||||
if self.negative { "-" } else { "+" },
|
||||
self.hours,
|
||||
self.minutes
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse(input: &mut &str) -> ModalResult<Offset> {
|
||||
timezone(input)
|
||||
}
|
||||
|
||||
fn timezone(input: &mut &str) -> ModalResult<Offset> {
|
||||
timezone_name_offset.parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse a timezone starting with `+` or `-`
|
||||
pub(super) fn timezone_num(input: &mut &str) -> ModalResult<Offset> {
|
||||
// Strings like "+8 years" are ambiguous, they can either be parsed as a
|
||||
// timezone offset "+8" and a relative time "years", or just a relative time
|
||||
// "+8 years". GNU date parses them the second way, so we do the same here.
|
||||
//
|
||||
// Return early if the input can be parsed as a relative time.
|
||||
if peek(relative::parse).parse_next(input).is_ok() {
|
||||
return Err(ErrMode::Backtrack(ContextError::new()));
|
||||
}
|
||||
|
||||
seq!(plus_or_minus, alt((timezone_colon, timezone_colonless)))
|
||||
.parse_next(input)
|
||||
.and_then(|(negative, (hours, minutes))| {
|
||||
if !(0..=12).contains(&hours) {
|
||||
return Err(ErrMode::Cut(ctx_err("timezone hour between 0 and 12")));
|
||||
}
|
||||
|
||||
if !(0..=60).contains(&minutes) {
|
||||
return Err(ErrMode::Cut(ctx_err("timezone minute between 0 and 60")));
|
||||
}
|
||||
|
||||
Ok(Offset {
|
||||
negative,
|
||||
hours,
|
||||
minutes,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse a timezone offset with a colon separating hours and minutes
|
||||
fn timezone_colon(input: &mut &str) -> ModalResult<(u32, u32)> {
|
||||
seq!(
|
||||
s(take_while(1..=2, AsChar::is_dec_digit)).try_map(|x: &str| x.parse()),
|
||||
_: colon,
|
||||
s(take_while(1..=2, AsChar::is_dec_digit)).try_map(|x: &str| x.parse()),
|
||||
)
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse a timezone offset without colon
|
||||
fn timezone_colonless(input: &mut &str) -> ModalResult<(u32, u32)> {
|
||||
s(take_while(0.., AsChar::is_dec_digit))
|
||||
.verify_map(|s: &str| {
|
||||
// GNU date supports number strings with leading zeroes, e.g.,
|
||||
// `UTC+000001100` is valid.
|
||||
let s = if s.len() > 4 {
|
||||
s.trim_start_matches('0')
|
||||
} else {
|
||||
s
|
||||
};
|
||||
Some(match s.len() {
|
||||
0 => (0, 0),
|
||||
1 | 2 => (s.parse().ok()?, 0),
|
||||
// The minutes are the last two characters here, for some reason.
|
||||
3 => (s[..1].parse().ok()?, s[1..].parse().ok()?),
|
||||
4 => (s[..2].parse().ok()?, s[2..].parse().ok()?),
|
||||
_ => return None,
|
||||
})
|
||||
})
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse a timezone by name
|
||||
fn timezone_name_offset(input: &mut &str) -> ModalResult<Offset> {
|
||||
/// I'm assuming there are no timezone abbreviations with more
|
||||
/// than 6 charactres
|
||||
const MAX_TZ_SIZE: usize = 6;
|
||||
let nextword = s(take_while(1..=MAX_TZ_SIZE, AsChar::is_alpha)).parse_next(input)?;
|
||||
let tz = tzname_to_offset(nextword)?;
|
||||
|
||||
// Strings like "UTC +8 years" are ambiguous, they can either be parsed as
|
||||
// "UTC+8" and "years", or "UTC" and "+8 years". GNU date parses them the
|
||||
// second way, so we do the same here.
|
||||
//
|
||||
// Only process if the input cannot be parsed as a relative time.
|
||||
if peek(relative::parse).parse_next(input).is_err() {
|
||||
if let Ok(other_tz) = timezone_num.parse_next(input) {
|
||||
let newtz = tz.merge(other_tz);
|
||||
|
||||
return Ok(newtz);
|
||||
};
|
||||
}
|
||||
|
||||
Ok(tz)
|
||||
}
|
||||
|
||||
/// Named timezone list.
|
||||
///
|
||||
/// The full list of timezones can be extracted from
|
||||
/// https://www.timeanddate.com/time/zones/. GNU date only supports a subset of
|
||||
/// these. We support the same subset as GNU date.
|
||||
fn tzname_to_offset(input: &str) -> ModalResult<Offset> {
|
||||
let mut offset_str = match input {
|
||||
"z" => Ok("+0"),
|
||||
"y" => Ok("-12"),
|
||||
"x" => Ok("-11"),
|
||||
"wet" => Ok("+0"),
|
||||
"west" => Ok("+1"),
|
||||
"wat" => Ok("+1"),
|
||||
"w" => Ok("-10"),
|
||||
"v" => Ok("-9"),
|
||||
"utc" => Ok("+0"),
|
||||
"u" => Ok("-8"),
|
||||
"t" => Ok("-7"),
|
||||
"sst" => Ok("-11"),
|
||||
"sgt" => Ok("+8"),
|
||||
"sast" => Ok("+2"),
|
||||
"s" => Ok("-6"),
|
||||
"r" => Ok("-5"),
|
||||
"q" => Ok("-4"),
|
||||
"pst" => Ok("-8"),
|
||||
"pdt" => Ok("-7"),
|
||||
"p" => Ok("-3"),
|
||||
"o" => Ok("-2"),
|
||||
"nzst" => Ok("+12"),
|
||||
"nzdt" => Ok("+13"),
|
||||
"nst" => Ok("-3:30"),
|
||||
"ndt" => Ok("-2:30"),
|
||||
"n" => Ok("-1"),
|
||||
"mst" => Ok("-7"),
|
||||
"msk" => Ok("+3"),
|
||||
"msd" => Ok("+4"),
|
||||
"mdt" => Ok("-6"),
|
||||
"m" => Ok("+12"),
|
||||
"l" => Ok("+11"),
|
||||
"k" => Ok("+10"),
|
||||
"jst" => Ok("+9"),
|
||||
"ist" => Ok("+5:30"),
|
||||
"i" => Ok("+9"),
|
||||
"hst" => Ok("-10"),
|
||||
"h" => Ok("+8"),
|
||||
"gst" => Ok("+4"),
|
||||
"gmt" => Ok("+0"),
|
||||
"g" => Ok("+7"),
|
||||
"f" => Ok("+6"),
|
||||
"est" => Ok("-5"),
|
||||
"eet" => Ok("+2"),
|
||||
"eest" => Ok("+3"),
|
||||
"edt" => Ok("-4"),
|
||||
"eat" => Ok("+3"),
|
||||
"e" => Ok("+5"),
|
||||
"d" => Ok("+4"),
|
||||
"cst" => Ok("-6"),
|
||||
"clt" => Ok("-4"),
|
||||
"clst" => Ok("-3"),
|
||||
"cet" => Ok("+1"),
|
||||
"cest" => Ok("+2"),
|
||||
"cdt" => Ok("-5"),
|
||||
"cat" => Ok("+2"),
|
||||
"c" => Ok("+3"),
|
||||
"bst" => Ok("+6"),
|
||||
"brt" => Ok("-3"),
|
||||
"brst" => Ok("-2"),
|
||||
"b" => Ok("+2"),
|
||||
"ast" => Ok("-3"),
|
||||
"art" => Ok("-3"),
|
||||
"akst" => Ok("-9"),
|
||||
"akdt" => Ok("-8"),
|
||||
"adt" => Ok("+4"),
|
||||
"a" => Ok("+1"),
|
||||
_ => Err(ErrMode::Backtrack(ContextError::new())),
|
||||
}?;
|
||||
|
||||
timezone_num(&mut offset_str)
|
||||
}
|
||||
|
||||
/// Parse the plus or minus character and return whether it was negative
|
||||
fn plus_or_minus(input: &mut &str) -> ModalResult<bool> {
|
||||
s(alt(("+".value(false), "-".value(true)))).parse_next(input)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_timezone_colonless() {
|
||||
fn aux(inp: &mut &str) -> String {
|
||||
format!("{:?}", timezone_colonless(inp).expect("timezone_colonless"))
|
||||
}
|
||||
|
||||
assert_eq!("(0, 0)", aux(&mut "0000"));
|
||||
assert_eq!("(12, 34)", aux(&mut "1234"));
|
||||
assert_eq!("(12, 34)", aux(&mut "00001234"));
|
||||
assert!(timezone_colonless(&mut "12345").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timezone() {
|
||||
let make_timezone = |input: &mut &str| {
|
||||
timezone(input)
|
||||
.map_err(|e| eprintln!("TEST FAILED AT:\n{e}"))
|
||||
.map(|offset| format!("{offset}"))
|
||||
.expect("expect tests to succeed")
|
||||
};
|
||||
|
||||
assert_eq!("+00:00", make_timezone(&mut "gmt"));
|
||||
assert_eq!("+01:00", make_timezone(&mut "a"));
|
||||
assert_eq!("+00:00", make_timezone(&mut "utc"));
|
||||
assert_eq!("-02:00", make_timezone(&mut "brst"));
|
||||
assert_eq!("-03:00", make_timezone(&mut "brt"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timezone_num() {
|
||||
let make_timezone = |input: &mut &str| {
|
||||
timezone_num(input)
|
||||
.map_err(|e| eprintln!("TEST FAILED AT:\n{e}"))
|
||||
.map(|offset| format!("{offset}"))
|
||||
.expect("expect tests to succeed")
|
||||
};
|
||||
|
||||
assert_eq!("+00:00", make_timezone(&mut "+00:00"));
|
||||
assert_eq!("+00:00", make_timezone(&mut "+0000"));
|
||||
assert_eq!("-00:00", make_timezone(&mut "-0000"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user