Skip to content
Draft
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
31 changes: 10 additions & 21 deletions Cargo.lock

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

5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@ num-bigint = "0.4.4"
num-prime = "0.5.0"
num-traits = "0.2.19"
onig = { version = "~6.5.1", default-features = false }
parse_datetime = "0.14.0"
parse_datetime = "0.15.0"
phf = "0.13.1"
phf_codegen = "0.13.1"
platform-info = "2.0.3"
Expand Down Expand Up @@ -769,3 +769,6 @@ debug = true

[lints]
workspace = true

[patch.crates-io]
parse_datetime = { path = "../parse_datetime" }
19 changes: 15 additions & 4 deletions src/uu/date/src/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -854,7 +854,10 @@ fn try_parse_with_abbreviation<S: AsRef<str>>(date_str: S, now: &Zoned) -> Optio
if let Some(tz) = tz {
let date_part = s.trim_end_matches(last_word).trim();
// Parse in the target timezone so "10:30 EDT" means 10:30 in EDT.
if let Ok(parsed) = parse_datetime::parse_datetime_at_date(now.clone(), date_part) {
if let Some(parsed) = parse_datetime::parse_datetime_at_date(now.clone(), date_part)
.ok()
.and_then(|p| p.into_zoned())
{
let dt = parsed.datetime();
if let Ok(zoned) = dt.to_zoned(tz) {
// The trailing abbreviation only describes the *input*
Expand Down Expand Up @@ -932,9 +935,17 @@ fn parse_date<S: AsRef<str> + Clone>(
}

match parse_datetime::parse_datetime_at_date(now.clone(), input_str) {
// Convert to system timezone for display
// (parse_datetime 0.13 returns Zoned in the input's timezone)
Ok(date) => {
// Convert to system timezone for display.
// parse_datetime returns the value in the input's timezone; the
// `Extended` variant covers years outside jiff's range, which `date`
// cannot represent, so treat it as an invalid input.
Ok(parsed) => {
let Some(date) = parsed.into_zoned() else {
return Err((
input_str.into(),
parse_datetime::ParseDateTimeError::InvalidInput,
));
};
let result = date.timestamp().to_zoned(now.time_zone().clone());
if dbg_opts.debug {
// Show final parsed date and time
Expand Down
5 changes: 4 additions & 1 deletion src/uu/touch/src/touch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -714,7 +714,10 @@ fn parse_date(ref_zoned: Zoned, s: &str) -> Result<FileTime, TouchError> {
}
}

if let Ok(zoned) = parse_datetime::parse_datetime_at_date(ref_zoned, s) {
if let Some(zoned) = parse_datetime::parse_datetime_at_date(ref_zoned, s)
.ok()
.and_then(|parsed| parsed.into_zoned())
{
return Ok(timestamp_to_filetime(zoned.timestamp()));
}

Expand Down
15 changes: 15 additions & 0 deletions tests/by-util/test_date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,21 @@ fn test_date_utc_vs_local() {
}
}

#[test]
fn test_date_utc_keyword_plus_relative_seconds_across_dst() {
// Regression test for #12555. The "UTC" keyword fixes the offset and must
// anchor the instant before the relative "N seconds" is applied. In a
// DST-observing zone, the epoch base is in winter (CET, +1) while the result
// lands in summer (CEST, +2); a naive implementation drifts by one hour.
// "1970-01-01 UTC + N seconds" must always be exactly N seconds past the epoch.
let seconds = 1_780_318_971; // lands in summer 2026
new_ucmd!()
.env("TZ", "Europe/Berlin")
.args(&["-d", &format!("1970/01/01 UTC {seconds} seconds"), "+%s"])
.succeeds()
.stdout_only(format!("{seconds}\n"));
}

#[test]
fn test_date_utc_output_formats() {
let cases = [
Expand Down
Loading