fix(date): align pure-digit -d parsing with GNU semantics

Implement GNU 'Pure numbers in date strings' for time-of-day tokens:
- 1–2 digits => HH:00 today
- 3–4 digits => HHMM today
- Validate ranges; reject invalid times (e.g., 2400, 2360)

Also:
- Add tests for -d0, -d7, -d0700 under TZ=UTC0 and invalid numeric inputs
- Reference GNU manual section for pure numbers
This commit is contained in:
naoNao89 2025-10-14 11:33:21 +07:00
parent 8d59e08097
commit 0047c7e66f
2 changed files with 99 additions and 1 deletions

View file

@ -203,7 +203,48 @@ 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::Human(ref input) => {
let date = parse_date(input);
// GNU compatibility (Pure numbers in date strings):
// - Manual: https://www.gnu.org/software/coreutils/manual/html_node/Pure-numbers-in-date-strings.html
// - Semantics: a pure decimal number denotes todays time-of-day (HH or HHMM).
// Examples: "0"/"00" => 00:00 today; "7"/"07" => 07:00 today; "0700" => 07:00 today.
// For all other forms, fall back to the general parser.
let is_pure_digits =
!input.is_empty() && input.len() <= 4 && input.chars().all(|c| c.is_ascii_digit());
let date = if is_pure_digits {
// Derive HH and MM from the input
let (hh_opt, mm_opt) = if input.len() <= 2 {
(input.parse::<u32>().ok(), Some(0u32))
} else {
let (h, m) = input.split_at(input.len() - 2);
(h.parse::<u32>().ok(), m.parse::<u32>().ok())
};
if let (Some(hh), Some(mm)) = (hh_opt, mm_opt) {
// Compose a concrete datetime string for today with zone offset.
// Use the already-determined 'now' and settings.utc to select offset.
let date_part =
strtime::format("%F", &now).unwrap_or_else(|_| String::from("1970-01-01"));
// If -u, force +00:00; otherwise use the local offset of 'now'.
let offset = if settings.utc {
String::from("+00:00")
} else {
strtime::format("%:z", &now).unwrap_or_default()
};
let composed = if offset.is_empty() {
format!("{date_part} {hh:02}:{mm:02}")
} else {
format!("{date_part} {hh:02}:{mm:02} {offset}")
};
parse_date(composed)
} else {
// Fallback on parse failure of digits
parse_date(input)
}
} else {
parse_date(input)
};
let iter = std::iter::once(date);
Box::new(iter)
}

View file

@ -778,3 +778,60 @@ fn test_date_resolution_no_combine() {
.arg("2025-01-01")
.fails();
}
#[test]
fn test_date_numeric_d_basic_utc() {
// Verify GNU-compatible pure-digit parsing for -d STRING under UTC
// 0/00 -> today at 00:00; 7/07 -> today at 07:00; 0700 -> today at 07:00
let today = Utc::now().date_naive();
let yyyy = today.year();
let mm = today.month();
let dd = today.day();
let mk =
|h: u32, m: u32| -> String { format!("{yyyy:04}-{mm:02}-{dd:02} {h:02}:{m:02}:00 UTC\n") };
new_ucmd!()
.env("TZ", "UTC0")
.arg("-d")
.arg("0")
.arg("+%F %T %Z")
.succeeds()
.stdout_only(mk(0, 0));
new_ucmd!()
.env("TZ", "UTC0")
.arg("-d")
.arg("7")
.arg("+%F %T %Z")
.succeeds()
.stdout_only(mk(7, 0));
new_ucmd!()
.env("TZ", "UTC0")
.arg("-d")
.arg("0700")
.arg("+%F %T %Z")
.succeeds()
.stdout_only(mk(7, 0));
}
#[test]
fn test_date_numeric_d_invalid_numbers() {
// Ensure invalid HHMM values are rejected (GNU-compatible)
new_ucmd!()
.env("TZ", "UTC0")
.arg("-d")
.arg("2400")
.arg("+%F %T %Z")
.fails()
.stderr_contains("invalid date");
new_ucmd!()
.env("TZ", "UTC0")
.arg("-d")
.arg("2360")
.arg("+%F %T %Z")
.fails()
.stderr_contains("invalid date");
}