You've already forked parse_datetime
mirror of
https://github.com/uutils/parse_datetime.git
synced 2026-06-10 16:13:15 -07:00
code cleanup
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,208 +0,0 @@
|
||||
use chrono::{DateTime, FixedOffset, Local, NaiveTime, TimeZone};
|
||||
use regex::Regex;
|
||||
|
||||
mod time_only_formats {
|
||||
pub const HH_MM: &str = "%R";
|
||||
pub const HH_MM_SS: &str = "%T";
|
||||
pub const TWELVE_HOUR: &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::TWELVE_HOUR,
|
||||
] {
|
||||
let Ok(parsed) = NaiveTime::parse_from_str(s, fmt) else {
|
||||
continue;
|
||||
};
|
||||
let parsed_dt = date.date_naive().and_time(parsed);
|
||||
if let Some(dt) = offset.from_local_datetime(&parsed_dt).single() {
|
||||
return Some(dt);
|
||||
}
|
||||
}
|
||||
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;
|
||||
match captures.name("m") {
|
||||
Some(minutes) if !(minutes.as_str().is_empty()) => {
|
||||
offset_in_sec += minutes.as_str().parse::<i32>().unwrap() * 60;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
offset_in_sec *= if &captures["sign"] == "-" { -1 } else { 1 };
|
||||
FixedOffset::east_opt(offset_in_sec)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::parse_time_only_str::parse_time_only;
|
||||
use chrono::{DateTime, Local, TimeZone};
|
||||
use std::env;
|
||||
|
||||
fn get_test_date() -> DateTime<Local> {
|
||||
Local.with_ymd_and_hms(2024, 3, 3, 0, 0, 0).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_time_only() {
|
||||
env::set_var("TZ", "UTC");
|
||||
let parsed_time = parse_time_only(get_test_date(), "21:04")
|
||||
.unwrap()
|
||||
.timestamp();
|
||||
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");
|
||||
let parsed_time = parse_time_only(get_test_date(), "21:04 +0530")
|
||||
.unwrap()
|
||||
.timestamp();
|
||||
assert_eq!(parsed_time, 1709480040);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_time_with_hour_only_offset() {
|
||||
env::set_var("TZ", "UTC");
|
||||
let parsed_time = parse_time_only(get_test_date(), "22:04 +01")
|
||||
.unwrap()
|
||||
.timestamp();
|
||||
assert_eq!(parsed_time, 1709499840);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_time_with_hour_only_neg_offset() {
|
||||
env::set_var("TZ", "UTC");
|
||||
let parsed_time = parse_time_only(get_test_date(), "17:04 -04")
|
||||
.unwrap()
|
||||
.timestamp();
|
||||
assert_eq!(parsed_time, 1709499840);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_time_with_seconds() {
|
||||
env::set_var("TZ", "UTC");
|
||||
let parsed_time = parse_time_only(get_test_date(), "21:04:30")
|
||||
.unwrap()
|
||||
.timestamp();
|
||||
assert_eq!(parsed_time, 1709499870);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_time_with_seconds_with_offset() {
|
||||
env::set_var("TZ", "UTC");
|
||||
let parsed_time = parse_time_only(get_test_date(), "21:04:30 +0530")
|
||||
.unwrap()
|
||||
.timestamp();
|
||||
assert_eq!(parsed_time, 1709480070);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_twelve_hour_time() {
|
||||
env::set_var("TZ", "UTC");
|
||||
let parsed_time = parse_time_only(get_test_date(), "9:04:00 PM")
|
||||
.unwrap()
|
||||
.timestamp();
|
||||
assert_eq!(parsed_time, 1709499840);
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
use core::fmt;
|
||||
use std::error::Error;
|
||||
use std::fmt::Display;
|
||||
use std::num::ParseIntError;
|
||||
|
||||
use nom::branch::alt;
|
||||
use nom::character::complete::{char, digit1};
|
||||
use nom::combinator::all_consuming;
|
||||
use nom::multi::fold_many0;
|
||||
use nom::sequence::preceded;
|
||||
use nom::{self, IResult, Parser};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum ParseTimestampError {
|
||||
InvalidNumber(ParseIntError),
|
||||
InvalidInput,
|
||||
}
|
||||
|
||||
impl Display for ParseTimestampError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::InvalidInput => {
|
||||
write!(f, "Invalid input string: cannot be parsed as a timestamp")
|
||||
}
|
||||
Self::InvalidNumber(err) => {
|
||||
write!(f, "Invalid timestamp number: {err}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for ParseTimestampError {}
|
||||
|
||||
// TODO is this necessary
|
||||
impl From<ParseIntError> for ParseTimestampError {
|
||||
fn from(err: ParseIntError) -> Self {
|
||||
Self::InvalidNumber(err)
|
||||
}
|
||||
}
|
||||
|
||||
type NomError<'a> = nom::Err<nom::error::Error<&'a str>>;
|
||||
|
||||
impl<'a> From<NomError<'a>> for ParseTimestampError {
|
||||
fn from(_err: NomError<'a>) -> Self {
|
||||
Self::InvalidInput
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_timestamp(s: &str) -> Result<i64, ParseTimestampError> {
|
||||
let s = s.trim().to_lowercase();
|
||||
let s = s.as_str();
|
||||
|
||||
let res: IResult<&str, (char, &str)> = all_consuming(preceded(
|
||||
char('@'),
|
||||
(
|
||||
// Note: to stay compatible with gnu date this code allows
|
||||
// multiple + and - and only considers the last one
|
||||
fold_many0(
|
||||
// parse either + or -
|
||||
alt((char('+'), char('-'))),
|
||||
// start with a +
|
||||
|| '+',
|
||||
// whatever we get (+ or -), update the accumulator to that value
|
||||
|_, c| c,
|
||||
),
|
||||
digit1,
|
||||
),
|
||||
))
|
||||
.parse(s);
|
||||
|
||||
let (_, (sign, number_str)) = res?;
|
||||
|
||||
let mut number = number_str.parse::<i64>()?;
|
||||
|
||||
if sign == '-' {
|
||||
number *= -1;
|
||||
}
|
||||
|
||||
Ok(number)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use crate::parse_timestamp::parse_timestamp;
|
||||
|
||||
#[test]
|
||||
fn test_valid_timestamp() {
|
||||
assert_eq!(parse_timestamp("@1234"), Ok(1234));
|
||||
assert_eq!(parse_timestamp("@99999"), Ok(99999));
|
||||
assert_eq!(parse_timestamp("@-4"), Ok(-4));
|
||||
assert_eq!(parse_timestamp("@-99999"), Ok(-99999));
|
||||
assert_eq!(parse_timestamp("@+4"), Ok(4));
|
||||
assert_eq!(parse_timestamp("@0"), Ok(0));
|
||||
|
||||
// gnu date accepts numbers signs and uses the last sign
|
||||
assert_eq!(parse_timestamp("@---+12"), Ok(12));
|
||||
assert_eq!(parse_timestamp("@+++-12"), Ok(-12));
|
||||
assert_eq!(parse_timestamp("@+----+12"), Ok(12));
|
||||
assert_eq!(parse_timestamp("@++++-123"), Ok(-123));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_timestamp() {
|
||||
assert!(parse_timestamp("@").is_err());
|
||||
assert!(parse_timestamp("@+--+").is_err());
|
||||
assert!(parse_timestamp("@+1ab2").is_err());
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
use chrono::Weekday;
|
||||
use nom::branch::alt;
|
||||
use nom::bytes::complete::tag;
|
||||
use nom::combinator::value;
|
||||
use nom::{self, IResult, Parser};
|
||||
|
||||
// Helper macro to simplify tag matching
|
||||
macro_rules! tag_match {
|
||||
($day:expr, $($pattern:expr),+) => {
|
||||
value($day, alt(($(tag($pattern)),+)))
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn parse_weekday(s: &str) -> Option<Weekday> {
|
||||
let s = s.trim().to_lowercase();
|
||||
let s = s.as_str();
|
||||
|
||||
let parse_result: IResult<&str, Weekday> = nom::combinator::all_consuming(alt((
|
||||
tag_match!(Weekday::Mon, "monday", "mon"),
|
||||
tag_match!(Weekday::Tue, "tuesday", "tues", "tue"),
|
||||
tag_match!(Weekday::Wed, "wednesday", "wednes", "wed"),
|
||||
tag_match!(Weekday::Thu, "thursday", "thurs", "thur", "thu"),
|
||||
tag_match!(Weekday::Fri, "friday", "fri"),
|
||||
tag_match!(Weekday::Sat, "saturday", "sat"),
|
||||
tag_match!(Weekday::Sun, "sunday", "sun"),
|
||||
)))
|
||||
.parse(s);
|
||||
|
||||
match parse_result {
|
||||
Ok((_, weekday)) => Some(weekday),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use chrono::Weekday::*;
|
||||
|
||||
use crate::parse_weekday::parse_weekday;
|
||||
|
||||
#[test]
|
||||
fn test_valid_weekdays() {
|
||||
let days = [
|
||||
("mon", Mon),
|
||||
("monday", Mon),
|
||||
("tue", Tue),
|
||||
("tues", Tue),
|
||||
("tuesday", Tue),
|
||||
("wed", Wed),
|
||||
("wednes", Wed),
|
||||
("wednesday", Wed),
|
||||
("thu", Thu),
|
||||
("thursday", Thu),
|
||||
("fri", Fri),
|
||||
("friday", Fri),
|
||||
("sat", Sat),
|
||||
("saturday", Sat),
|
||||
("sun", Sun),
|
||||
("sunday", Sun),
|
||||
];
|
||||
|
||||
for (name, weekday) in days {
|
||||
assert_eq!(parse_weekday(name), Some(weekday));
|
||||
assert_eq!(parse_weekday(&format!(" {name}")), Some(weekday));
|
||||
assert_eq!(parse_weekday(&format!(" {name} ")), Some(weekday));
|
||||
assert_eq!(parse_weekday(&format!("{name} ")), Some(weekday));
|
||||
|
||||
let (left, right) = name.split_at(1);
|
||||
let (test_str1, test_str2) = (
|
||||
format!("{}{}", left.to_uppercase(), right.to_lowercase()),
|
||||
format!("{}{}", left.to_lowercase(), right.to_uppercase()),
|
||||
);
|
||||
|
||||
assert_eq!(parse_weekday(&test_str1), Some(weekday));
|
||||
assert_eq!(parse_weekday(&test_str2), Some(weekday));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_weekdays() {
|
||||
let days = [
|
||||
"mond",
|
||||
"tuesda",
|
||||
"we",
|
||||
"th",
|
||||
"fr",
|
||||
"sa",
|
||||
"su",
|
||||
"garbageday",
|
||||
"tomorrow",
|
||||
"yesterday",
|
||||
];
|
||||
for day in days {
|
||||
assert!(parse_weekday(day).is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user