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
1 change: 1 addition & 0 deletions Cargo.lock

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

4 changes: 2 additions & 2 deletions src/uu/date/src/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use uucore::display::Quotable;
#[cfg(not(any(target_os = "redox")))]
use uucore::error::FromIo;
use uucore::error::{UResult, USimpleError};
use uucore::parse_datetime::parse_datetime;
use uucore::{format_usage, help_about, help_usage, show};
#[cfg(windows)]
use windows_sys::Win32::{Foundation::SYSTEMTIME, System::SystemInformation::SetSystemTime};
Expand Down Expand Up @@ -398,8 +399,7 @@ fn make_format_string(settings: &Settings) -> &str {
fn parse_date<S: AsRef<str> + Clone>(
s: S,
) -> Result<DateTime<FixedOffset>, (String, chrono::format::ParseError)> {
// TODO: The GNU date command can parse a wide variety of inputs.
s.as_ref().parse().map_err(|e| (s.as_ref().into(), e))
parse_datetime(s.as_ref())
}

#[cfg(not(any(unix, windows)))]
Expand Down
1 change: 1 addition & 0 deletions src/uucore/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ path="src/lib/lib.rs"

[dependencies]
clap = { workspace=true }
chrono = { workspace=true }
uucore_procs = { workspace=true }
dns-lookup = { version="1.0.8", optional=true }
dunce = "1.0.4"
Expand Down
1 change: 1 addition & 0 deletions src/uucore/src/lib/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub use crate::mods::update_control;
pub use crate::mods::version_cmp;

// * string parsing modules
pub use crate::parser::parse_datetime;
pub use crate::parser::parse_glob;
pub use crate::parser::parse_size;
pub use crate::parser::parse_time;
Expand Down
1 change: 1 addition & 0 deletions src/uucore/src/lib/parser.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod parse_datetime;
pub mod parse_glob;
pub mod parse_size;
pub mod parse_time;
177 changes: 177 additions & 0 deletions src/uucore/src/lib/parser/parse_datetime.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, TimeZone};

/// Formats that parse input can take.
/// Taken from `touch` core util
mod format {
pub(crate) const ISO_8601: &str = "%Y-%m-%d";
pub(crate) const POSIX_LOCALE: &str = "%a %b %e %H:%M:%S %Y";
pub(crate) const YYYYMMDDHHMM_DOT_SS: &str = "%Y%m%d%H%M.%S";
pub(crate) const YYYYMMDDHHMMSS: &str = "%Y-%m-%d %H:%M:%S.%f";
pub(crate) const YYYYMMDDHHMMS: &str = "%Y-%m-%d %H:%M:%S";
pub(crate) const YYYY_MM_DD_HH_MM: &str = "%Y-%m-%d %H:%M";
pub(crate) const YYYYMMDDHHMM: &str = "%Y%m%d%H%M";
pub(crate) const YYYYMMDDHHMM_OFFSET: &str = "%Y%m%d%H%M %z";
pub(crate) const YYYYMMDDHHMM_UTC_OFFSET: &str = "%Y%m%d%H%MUTC%z";
pub(crate) const YYYYMMDDHHMM_ZULU_OFFSET: &str = "%Y%m%d%H%MZ%z";
pub(crate) const YYYYMMDDHHMM_HYPHENATED_OFFSET: &str = "%Y-%m-%d %H:%M %z";
pub(crate) const ISO_T_SEP: &str = "%Y-%m-%dT%H:%M:%S";
pub(crate) const UTC_OFFSET: &str = "UTC%#z";
pub(crate) const ZULU_OFFSET: &str = "Z%#z";
}

/// Parse a `String` into a `DateTime`.
/// If it fails, return a tuple of the `String` along with its `ParseError`.
///
/// The purpose of this function is to provide a basic loose DateTime parser.
pub fn parse_datetime<S: AsRef<str> + Clone>(
s: S,
) -> Result<DateTime<FixedOffset>, (String, chrono::format::ParseError)> {
// TODO: Replace with a proper customiseable parsing solution using `nom`, `grmtools`, or
// similar

// Formats with offsets don't require NaiveDateTime workaround
for fmt in [
format::YYYYMMDDHHMM_OFFSET,
format::YYYYMMDDHHMM_HYPHENATED_OFFSET,
format::YYYYMMDDHHMM_UTC_OFFSET,
format::YYYYMMDDHHMM_ZULU_OFFSET,
] {
if let Ok(parsed) = DateTime::parse_from_str(s.as_ref(), fmt) {
return Ok(parsed);
}
}

// Parse formats with no offset, assume local time
for fmt in [
format::ISO_T_SEP,
format::YYYYMMDDHHMM,
format::YYYYMMDDHHMMS,
format::YYYYMMDDHHMMSS,
format::YYYY_MM_DD_HH_MM,
format::YYYYMMDDHHMM_DOT_SS,
format::POSIX_LOCALE,
] {
if let Ok(parsed) = NaiveDateTime::parse_from_str(s.as_ref(), fmt) {
return Ok(naive_dt_to_fixed_offset(parsed));
}
}

// Parse epoch seconds
if s.as_ref().bytes().next() == Some(b'@') {
if let Ok(parsed) = NaiveDateTime::parse_from_str(&s.as_ref()[1..], "%s") {
return Ok(naive_dt_to_fixed_offset(parsed));
}
}

let ts = s.as_ref().to_owned() + "0000";
// Parse date only formats - assume midnight local timezone
for fmt in [format::ISO_8601] {
let f = fmt.to_owned() + "%H%M";
if let Ok(parsed) = NaiveDateTime::parse_from_str(&ts, &f) {
return Ok(naive_dt_to_fixed_offset(parsed));
}
}

// Parse offsets. chrono doesn't provide any functionality to parse
// offsets, so instead we replicate parse_date behaviour by getting
// the current date with local, and create a date time string at midnight,
// before trying offset suffixes
let local = Local::now();
let ts = format!("{}", local.format("%Y%m%d")) + "0000" + s.as_ref();
for fmt in [format::UTC_OFFSET, format::ZULU_OFFSET] {
let f = format::YYYYMMDDHHMM.to_owned() + fmt;
if let Ok(parsed) = DateTime::parse_from_str(&ts, &f) {
return Ok(parsed);
}
}

// Default parse and failure
s.as_ref().parse().map_err(|e| (s.as_ref().into(), e))
}

// Convert NaiveDateTime to DateTime<FixedOffset> by assuming the offset
// is local time
fn naive_dt_to_fixed_offset(dt: NaiveDateTime) -> DateTime<FixedOffset> {
let now = Local::now();
now.with_timezone(now.offset());
now.offset().from_local_datetime(&dt).unwrap().into()
}

#[cfg(test)]
mod tests {
static TEST_TIME: i64 = 1613371067;

#[cfg(test)]
mod iso_8601 {
use std::env;

use crate::{parse_datetime::parse_datetime, parse_datetime::tests::TEST_TIME};

#[test]
fn test_t_sep() {
env::set_var("TZ", "UTC");
let dt = "2021-02-15T06:37:47";
let actual = parse_datetime(dt);
assert_eq!(actual.unwrap().timestamp(), TEST_TIME);
}

#[test]
fn test_space_sep() {
env::set_var("TZ", "UTC");
let dt = "2021-02-15 06:37:47";
let actual = parse_datetime(dt);
assert_eq!(actual.unwrap().timestamp(), TEST_TIME);
}

#[test]
fn test_space_sep_offset() {
env::set_var("TZ", "UTC");
let dt = "2021-02-14 22:37:47 -0800";
let actual = parse_datetime(dt);
assert_eq!(actual.unwrap().timestamp(), TEST_TIME);
}

#[test]
fn test_t_sep_offset() {
env::set_var("TZ", "UTC");
let dt = "2021-02-14T22:37:47 -0800";
let actual = parse_datetime(dt);
assert_eq!(actual.unwrap().timestamp(), TEST_TIME);
}
}

#[cfg(test)]
mod offsets {
use chrono::Local;

use crate::parse_datetime::parse_datetime;

#[test]
fn test_positive_offsets() {
let offsets = vec![
"UTC+07:00",
"UTC+0700",
"UTC+07",
"Z+07:00",
"Z+0700",
"Z+07",
];

let expected = format!("{}{}", Local::now().format("%Y%m%d"), "0000+0700");
for offset in offsets {
let actual = parse_datetime(offset).unwrap();
assert_eq!(expected, format!("{}", actual.format("%Y%m%d%H%M%z")));
}
}

#[test]
fn test_partial_offset() {
let offsets = vec!["UTC+00:15", "UTC+0015", "Z+00:15", "Z+0015"];
let expected = format!("{}{}", Local::now().format("%Y%m%d"), "0000+0015");
for offset in offsets {
let actual = parse_datetime(offset).unwrap();
assert_eq!(expected, format!("{}", actual.format("%Y%m%d%H%M%z")));
}
}
}
}
23 changes: 23 additions & 0 deletions tests/by-util/test_date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -395,3 +395,26 @@ fn test_invalid_date_string() {
.no_stdout()
.stderr_contains("invalid date");
}

#[test]
fn test_date_parse_iso8601() {
let dates = vec![
"2023-03-27 08:30:00",
"2023-04-01 12:00:00",
"2023-04-15 18:30:00",
];
for date in dates {
new_ucmd!().arg("-d").arg(date).succeeds();
}
}

#[test]
fn test_date_parse_epoch() {
let date = "@2147483647";
new_ucmd!()
.arg("-u")
.arg("-d")
.arg(date)
.succeeds()
.stdout_is("Tue Jan 19 03:14:07 2038\n");
}