Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/uu/date/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ chrono = { workspace = true }
clap = { workspace = true }
uucore = { workspace = true }
parse_datetime = { workspace = true }
regex = { workspace = true }
lazy_static = "1.5.0"

[target.'cfg(unix)'.dependencies]
libc = { workspace = true }
Expand Down
6 changes: 5 additions & 1 deletion src/uu/date/src/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ use windows_sys::Win32::{Foundation::SYSTEMTIME, System::SystemInformation::SetS

use uucore::shortcut_value_parser::ShortcutValueParser;

mod parser;

// Options
const DATE: &str = "date";
const HOURS: &str = "hours";
Expand Down Expand Up @@ -222,7 +224,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
// Iterate over all dates - whether it's a single date or a file.
let dates: Box<dyn Iterator<Item = _>> = match settings.date_source {
DateSource::Custom(ref input) => {
let date = parse_date(input.clone());
let date = parse_date(input.clone())
// fallback to parser::parse_fb if parse_date fails
.or_else(|_| parser::parse_fb(input, now).map_err(|(i, e)| (i.to_string(), e)));
let iter = std::iter::once(date);
Box::new(iter)
}
Expand Down
206 changes: 206 additions & 0 deletions src/uu/date/src/parser.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
use std::str::FromStr;

use chrono::offset::TimeZone;
use chrono::{DateTime, Datelike, FixedOffset, Local, TimeDelta, Timelike};

use lazy_static::lazy_static;
use regex::{Captures, Regex};

#[derive(Debug)]
enum Token {
Ymd(u32, u32, u32),
Hms(u32, u32, u32),
Ymdhms(u32, u32, u32, u32, u32, u32),
}

trait RegexUtils {
fn unwrap_group<T>(&self, name: &str) -> T
where
T: FromStr<Err: std::fmt::Debug>;
}

impl RegexUtils for Captures<'_> {
fn unwrap_group<T>(&self, name: &str) -> T
where
T: FromStr<Err: std::fmt::Debug>,
{
self.name(name).unwrap().as_str().parse::<T>().unwrap()
}
}

impl Token {
fn parse_ymd(token: &str) -> Option<Self> {
lazy_static! {
static ref ymd_regex: Regex =
Regex::new(r"(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})").unwrap();
}
ymd_regex.captures(token).map(|m| {
let y = m.unwrap_group("year");
let mo = m.unwrap_group("month");
let d = m.unwrap_group("day");
Self::Ymd(y, mo, d)
})
}

fn parse_choices(token: &str, choices: &'static str) -> Option<String> {
let regex = Regex::new(choices).unwrap();
regex
.captures(token)
.map(|m| m.get(1).unwrap().as_str().to_string())
}

fn parse_month_name(token: &str) -> Option<i32> {
let choices =
Self::parse_choices(token, r"(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dez)")?;
let month = match choices.as_str() {
"Jan" => 1,
"Feb" => 2,
"Mar" => 3,
"Apr" => 4,
"May" => 5,
"Jun" => 6,
"Jul" => 7,
"Aug" => 8,
"Sep" => 9,
"Oct" => 10,
"Nov" => 11,
"Dez" => 12,
_ => unreachable!(),
};
Some(month)
}

fn parse_hm(token: &str) -> Option<Self> {
lazy_static! {
static ref hm_regex: Regex = Regex::new(r"(?<hour>\d{2}):(?<minute>\d{2})").unwrap();
}
hm_regex.captures(token).map(|m| {
let h = m.unwrap_group("hour");
let mi = m.unwrap_group("minute");
Self::Hms(h, mi, 0)
})
}

fn parse_hms(token: &str) -> Option<Self> {
lazy_static! {
static ref hms_regex: Regex =
Regex::new(r"(?<hour>\d{2}):(?<minute>\d{2}):(?<second>\d{2})").unwrap();
}
hms_regex
.captures(token)
.map(|m| {
let h = m.unwrap_group("hour");
let mi = m.unwrap_group("minute");
let s = m.unwrap_group("second");
Self::Hms(h, mi, s)
})
.or_else(|| Self::parse_hm(token))
}

fn parse_dateunit(token: &str) -> Option<String> {
Self::parse_choices(
token,
r"(?<dateunit>second|minute|hour|day|week|month|year)s?",
)
}

fn parse_number_i32(token: &str) -> Option<i32> {
lazy_static! {
static ref number_regex: Regex = Regex::new(r"\+?(\d{1,9})$").unwrap();
}
number_regex
.captures(token)
.and_then(|m| m.get(1).unwrap().as_str().parse::<i32>().ok())
}

// Parses date like
// "Jul 18 06:14:49 2024 GMT" +%s"
fn parse_with_month(input: &str, d: &DateTime<FixedOffset>) -> Option<Self> {
let mut tokens = input.split_whitespace();
let month = Self::parse_month_name(tokens.next()?)?;
let day = Self::parse_number_i32(tokens.next()?)?;
let hms = Self::parse_hms(tokens.next()?)?;
let year = Self::parse_number_i32(tokens.next()?).unwrap_or(d.year());
// @TODO: Parse the timezone
if let Self::Hms(hour, minute, second) = hms {
// Return the value
Some(Self::Ymdhms(
year as u32,
month as u32,
day as u32,
hour,
minute,
second,
))
} else {
None
}
}

fn parse(input: &str, mut d: DateTime<FixedOffset>) -> Result<DateTime<FixedOffset>, String> {
// Parsing "Jul 18 06:14:49 2024 GMT" like dates
if let Some(Self::Ymdhms(year, mo, day, h, m, s)) = Self::parse_with_month(input, &d) {
d = Local
.with_ymd_and_hms(year as i32, mo, day, h, m, s)
.unwrap()
.into();
return Ok(d);
}

let mut tokens = input.split_whitespace().peekable();
while let Some(token) = tokens.next() {
// Parse YMD
if let Some(Self::Ymd(year, mo, day)) = Self::parse_ymd(token) {
d = Local
.with_ymd_and_hms(year as i32, mo, day, d.hour(), d.minute(), d.second())
.unwrap()
.into();
continue;
}
// Parse HMS
else if let Some(Self::Hms(h, mi, s)) = Self::parse_hms(token) {
d = Local
.with_ymd_and_hms(d.year(), d.month(), d.day(), h, mi, s)
.unwrap()
.into();
continue;
}
// Parse a number
else if let Some(number) = Self::parse_number_i32(token) {
let number: i64 = number.into();
// Followed by a dateunit
let dateunit = tokens
.peek()
.and_then(|x| Self::parse_dateunit(x))
.unwrap_or("hour".to_string());
match dateunit.as_str() {
"second" => d += TimeDelta::seconds(number),
"minute" => d += TimeDelta::minutes(number),
"hour" => d += TimeDelta::hours(number),
"day" => d += TimeDelta::days(number),
"week" => d += TimeDelta::weeks(number),
"month" => d += TimeDelta::days(30),
"year" => d += TimeDelta::days(365),
_ => unreachable!(),
};
tokens.next(); // consume the token
continue;
}
// Don't know how to parse this
else {
return Err(format!("Error parsing date, unexpected token {token}"));
}
}

Ok(d)
}
}

// Parse fallback for dates. It tries to parse `input` and update
// `d` accordingly.
pub fn parse_fb(
input: &str,
d: DateTime<FixedOffset>,
) -> Result<DateTime<FixedOffset>, (&str, parse_datetime::ParseDateTimeError)> {
Token::parse(input, d).map_err(|_| (input, parse_datetime::ParseDateTimeError::InvalidInput))
}
33 changes: 33 additions & 0 deletions tests/by-util/test_date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,39 @@ fn test_invalid_date_string() {
.stderr_contains("invalid date");
}

#[test]
fn test_invalid_date_fallback() {
new_ucmd!()
.arg("-u")
.arg("-d")
.arg("11111111")
.succeeds()
// how coreutils outputs
//.stdout_contains("Sat Nov 11 12:00:00 AM UTC 1111");
.stdout_contains("Sat Nov 11 00:00:00 1111");

new_ucmd!()
.arg("-u")
.arg("-d")
.arg("2024-01-01 +351 day 00:00")
.succeeds()
.stdout_contains("Tue Dec 17 00:00:00 2024");

new_ucmd!()
.arg("-u")
.arg("-d")
.arg("2024-01-01 00:00 1 day 1 hour 1 minute 3 second")
.succeeds()
.stdout_contains("Tue Jan 2 01:01:03 2024");

new_ucmd!()
.arg("-d")
.arg("Jul 18 06:14:49 2024 GMT")
.succeeds()
//.stdout_contains("Jul 18 06:14:49 2024 GMT");
.stdout_contains("Jul 18 06:14:49 2024");
}

#[test]
fn test_date_one_digit_date() {
new_ucmd!()
Expand Down