Skip to content

Commit e2e5c76

Browse files
authored
Merge pull request uutils#8907 from naoNao89/fix/date-numeric-d-parsing
date: follow GNU pure-number -d semantics
2 parents 58f3264 + 0047c7e commit e2e5c76

File tree

2 files changed

+99
-1
lines changed

2 files changed

+99
-1
lines changed

src/uu/date/src/date.rs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,48 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
203203
// Iterate over all dates - whether it's a single date or a file.
204204
let dates: Box<dyn Iterator<Item = _>> = match settings.date_source {
205205
DateSource::Human(ref input) => {
206-
let date = parse_date(input);
206+
// GNU compatibility (Pure numbers in date strings):
207+
// - Manual: https://www.gnu.org/software/coreutils/manual/html_node/Pure-numbers-in-date-strings.html
208+
// - Semantics: a pure decimal number denotes today’s time-of-day (HH or HHMM).
209+
// Examples: "0"/"00" => 00:00 today; "7"/"07" => 07:00 today; "0700" => 07:00 today.
210+
// For all other forms, fall back to the general parser.
211+
let is_pure_digits =
212+
!input.is_empty() && input.len() <= 4 && input.chars().all(|c| c.is_ascii_digit());
213+
214+
let date = if is_pure_digits {
215+
// Derive HH and MM from the input
216+
let (hh_opt, mm_opt) = if input.len() <= 2 {
217+
(input.parse::<u32>().ok(), Some(0u32))
218+
} else {
219+
let (h, m) = input.split_at(input.len() - 2);
220+
(h.parse::<u32>().ok(), m.parse::<u32>().ok())
221+
};
222+
223+
if let (Some(hh), Some(mm)) = (hh_opt, mm_opt) {
224+
// Compose a concrete datetime string for today with zone offset.
225+
// Use the already-determined 'now' and settings.utc to select offset.
226+
let date_part =
227+
strtime::format("%F", &now).unwrap_or_else(|_| String::from("1970-01-01"));
228+
// If -u, force +00:00; otherwise use the local offset of 'now'.
229+
let offset = if settings.utc {
230+
String::from("+00:00")
231+
} else {
232+
strtime::format("%:z", &now).unwrap_or_default()
233+
};
234+
let composed = if offset.is_empty() {
235+
format!("{date_part} {hh:02}:{mm:02}")
236+
} else {
237+
format!("{date_part} {hh:02}:{mm:02} {offset}")
238+
};
239+
parse_date(composed)
240+
} else {
241+
// Fallback on parse failure of digits
242+
parse_date(input)
243+
}
244+
} else {
245+
parse_date(input)
246+
};
247+
207248
let iter = std::iter::once(date);
208249
Box::new(iter)
209250
}

tests/by-util/test_date.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,3 +778,60 @@ fn test_date_resolution_no_combine() {
778778
.arg("2025-01-01")
779779
.fails();
780780
}
781+
782+
#[test]
783+
fn test_date_numeric_d_basic_utc() {
784+
// Verify GNU-compatible pure-digit parsing for -d STRING under UTC
785+
// 0/00 -> today at 00:00; 7/07 -> today at 07:00; 0700 -> today at 07:00
786+
let today = Utc::now().date_naive();
787+
let yyyy = today.year();
788+
let mm = today.month();
789+
let dd = today.day();
790+
791+
let mk =
792+
|h: u32, m: u32| -> String { format!("{yyyy:04}-{mm:02}-{dd:02} {h:02}:{m:02}:00 UTC\n") };
793+
794+
new_ucmd!()
795+
.env("TZ", "UTC0")
796+
.arg("-d")
797+
.arg("0")
798+
.arg("+%F %T %Z")
799+
.succeeds()
800+
.stdout_only(mk(0, 0));
801+
802+
new_ucmd!()
803+
.env("TZ", "UTC0")
804+
.arg("-d")
805+
.arg("7")
806+
.arg("+%F %T %Z")
807+
.succeeds()
808+
.stdout_only(mk(7, 0));
809+
810+
new_ucmd!()
811+
.env("TZ", "UTC0")
812+
.arg("-d")
813+
.arg("0700")
814+
.arg("+%F %T %Z")
815+
.succeeds()
816+
.stdout_only(mk(7, 0));
817+
}
818+
819+
#[test]
820+
fn test_date_numeric_d_invalid_numbers() {
821+
// Ensure invalid HHMM values are rejected (GNU-compatible)
822+
new_ucmd!()
823+
.env("TZ", "UTC0")
824+
.arg("-d")
825+
.arg("2400")
826+
.arg("+%F %T %Z")
827+
.fails()
828+
.stderr_contains("invalid date");
829+
830+
new_ucmd!()
831+
.env("TZ", "UTC0")
832+
.arg("-d")
833+
.arg("2360")
834+
.arg("+%F %T %Z")
835+
.fails()
836+
.stderr_contains("invalid date");
837+
}

0 commit comments

Comments
 (0)