mirror of
https://github.com/uutils/coreutils.git
synced 2025-12-23 08:47:37 +00:00
Merge pull request #9022 from sylvestre/test-ignored-date
date: improve compat with GNU
This commit is contained in:
commit
870bc29cb4
2 changed files with 218 additions and 2 deletions
|
|
@ -117,6 +117,57 @@ impl From<&str> for Rfc3339Format {
|
|||
}
|
||||
}
|
||||
|
||||
/// Parse military timezone with optional hour offset.
|
||||
/// Pattern: single letter (a-z except j) optionally followed by 1-2 digits.
|
||||
/// Returns Some(total_hours_in_utc) or None if pattern doesn't match.
|
||||
///
|
||||
/// Military timezone mappings:
|
||||
/// - A-I: UTC+1 to UTC+9 (J is skipped for local time)
|
||||
/// - K-M: UTC+10 to UTC+12
|
||||
/// - N-Y: UTC-1 to UTC-12
|
||||
/// - Z: UTC+0
|
||||
///
|
||||
/// The hour offset from digits is added to the base military timezone offset.
|
||||
/// Examples: "m" -> 12 (noon UTC), "m9" -> 21 (9pm UTC), "a5" -> 4 (4am UTC next day)
|
||||
fn parse_military_timezone_with_offset(s: &str) -> Option<i32> {
|
||||
if s.is_empty() || s.len() > 3 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut chars = s.chars();
|
||||
let letter = chars.next()?.to_ascii_lowercase();
|
||||
|
||||
// Check if first character is a letter (a-z, except j which is handled separately)
|
||||
if !letter.is_ascii_lowercase() || letter == 'j' {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Parse optional digits (1-2 digits for hour offset)
|
||||
let additional_hours: i32 = if let Some(rest) = chars.as_str().chars().next() {
|
||||
if !rest.is_ascii_digit() {
|
||||
return None;
|
||||
}
|
||||
chars.as_str().parse().ok()?
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
// Map military timezone letter to UTC offset
|
||||
let tz_offset = match letter {
|
||||
'a'..='i' => (letter as i32 - 'a' as i32) + 1, // A=+1, B=+2, ..., I=+9
|
||||
'k'..='m' => (letter as i32 - 'k' as i32) + 10, // K=+10, L=+11, M=+12
|
||||
'n'..='y' => -((letter as i32 - 'n' as i32) + 1), // N=-1, O=-2, ..., Y=-12
|
||||
'z' => 0, // Z=+0
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
// Calculate total hours: midnight (0) + tz_offset + additional_hours
|
||||
// Midnight in timezone X converted to UTC
|
||||
let total_hours = (0 - tz_offset + additional_hours).rem_euclid(24);
|
||||
|
||||
Some(total_hours)
|
||||
}
|
||||
|
||||
#[uucore::main]
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
|
|
@ -205,15 +256,54 @@ 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 input = input.trim();
|
||||
// GNU compatibility (Empty string):
|
||||
// An empty string (or whitespace-only) should be treated as midnight today.
|
||||
let is_empty_or_whitespace = input.is_empty();
|
||||
|
||||
// GNU compatibility (Military timezone 'J'):
|
||||
// 'J' is reserved for local time in military timezones.
|
||||
// GNU date accepts it and treats it as midnight today (00:00:00).
|
||||
let is_military_j = input.eq_ignore_ascii_case("j");
|
||||
|
||||
// GNU compatibility (Military timezone with optional hour offset):
|
||||
// Single letter (a-z except j) optionally followed by 1-2 digits.
|
||||
// Letter represents midnight in that military timezone (UTC offset).
|
||||
// Digits represent additional hours to add.
|
||||
// Examples: "m" -> noon UTC (12:00); "m9" -> 21:00 UTC; "a5" -> 04:00 UTC
|
||||
let military_tz_with_offset = parse_military_timezone_with_offset(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 today’s time-of-day (HH or HHMM).
|
||||
// - Semantics: a pure decimal number denotes today's 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 {
|
||||
let date = if is_empty_or_whitespace || is_military_j {
|
||||
// Treat empty string or 'J' as midnight today (00:00:00) in local time
|
||||
let date_part =
|
||||
strtime::format("%F", &now).unwrap_or_else(|_| String::from("1970-01-01"));
|
||||
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} 00:00")
|
||||
} else {
|
||||
format!("{date_part} 00:00 {offset}")
|
||||
};
|
||||
parse_date(composed)
|
||||
} else if let Some(total_hours) = military_tz_with_offset {
|
||||
// Military timezone with optional hour offset
|
||||
// Convert to UTC time: midnight + military_tz_offset + additional_hours
|
||||
let date_part =
|
||||
strtime::format("%F", &now).unwrap_or_else(|_| String::from("1970-01-01"));
|
||||
let composed = format!("{date_part} {total_hours:02}:00:00 +00:00");
|
||||
parse_date(composed)
|
||||
} else 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))
|
||||
|
|
@ -717,3 +807,24 @@ fn set_system_datetime(date: Zoned) -> UResult<()> {
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_military_timezone_with_offset() {
|
||||
// Valid cases: letter only, letter + digit, uppercase
|
||||
assert_eq!(parse_military_timezone_with_offset("m"), Some(12)); // UTC+12 -> 12:00 UTC
|
||||
assert_eq!(parse_military_timezone_with_offset("m9"), Some(21)); // 12 + 9 = 21
|
||||
assert_eq!(parse_military_timezone_with_offset("a5"), Some(4)); // 23 + 5 = 28 % 24 = 4
|
||||
assert_eq!(parse_military_timezone_with_offset("z"), Some(0)); // UTC+0 -> 00:00 UTC
|
||||
assert_eq!(parse_military_timezone_with_offset("M9"), Some(21)); // Uppercase works
|
||||
|
||||
// Invalid cases: 'j' reserved, empty, too long, starts with digit
|
||||
assert_eq!(parse_military_timezone_with_offset("j"), None); // Reserved for local time
|
||||
assert_eq!(parse_military_timezone_with_offset(""), None); // Empty
|
||||
assert_eq!(parse_military_timezone_with_offset("m999"), None); // Too long
|
||||
assert_eq!(parse_military_timezone_with_offset("9m"), None); // Starts with digit
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -944,3 +944,108 @@ fn test_date_tz_abbreviation_unknown() {
|
|||
.fails()
|
||||
.stderr_contains("invalid date");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_date_military_timezone_j_variations() {
|
||||
// Test multiple variations of 'J' input (case insensitive, with whitespace)
|
||||
// All should produce midnight (00:00:00)
|
||||
let test_cases = vec!["J", "j", " J ", " j ", "\tJ\t"];
|
||||
|
||||
for input in test_cases {
|
||||
new_ucmd!()
|
||||
.env("TZ", "UTC")
|
||||
.arg("-d")
|
||||
.arg(input)
|
||||
.arg("+%T")
|
||||
.succeeds()
|
||||
.stdout_is("00:00:00\n");
|
||||
}
|
||||
|
||||
// Test with -u flag to verify UTC behavior
|
||||
new_ucmd!()
|
||||
.arg("-u")
|
||||
.arg("-d")
|
||||
.arg("J")
|
||||
.arg("+%T %Z")
|
||||
.succeeds()
|
||||
.stdout_contains("00:00:00")
|
||||
.stdout_contains("UTC");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_date_empty_string() {
|
||||
// Empty string should be treated as midnight today
|
||||
new_ucmd!()
|
||||
.env("TZ", "UTC+1")
|
||||
.arg("-d")
|
||||
.arg("")
|
||||
.succeeds()
|
||||
.stdout_contains("00:00:00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_date_empty_string_variations() {
|
||||
// Test multiple variations of empty/whitespace strings
|
||||
// All should produce midnight (00:00:00)
|
||||
let test_cases = vec!["", " ", " ", "\t", "\n", " \t ", "\t\n\t"];
|
||||
|
||||
for input in test_cases {
|
||||
new_ucmd!()
|
||||
.env("TZ", "UTC")
|
||||
.arg("-d")
|
||||
.arg(input)
|
||||
.arg("+%T")
|
||||
.succeeds()
|
||||
.stdout_is("00:00:00\n");
|
||||
}
|
||||
|
||||
// Test with -u flag to verify UTC behavior
|
||||
new_ucmd!()
|
||||
.arg("-u")
|
||||
.arg("-d")
|
||||
.arg("")
|
||||
.arg("+%T %Z")
|
||||
.succeeds()
|
||||
.stdout_contains("00:00:00")
|
||||
.stdout_contains("UTC");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_date_relative_m9() {
|
||||
// Military timezone "m9" should be parsed as noon + 9 hours = 21:00 UTC
|
||||
// When displayed in TZ=UTC+9 (which is UTC-9), this shows as 12:00 local time
|
||||
new_ucmd!()
|
||||
.env("TZ", "UTC+9")
|
||||
.arg("-d")
|
||||
.arg("m9")
|
||||
.succeeds()
|
||||
.stdout_contains("12:00:00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_date_military_timezone_with_offset_variations() {
|
||||
// Test various military timezone + offset combinations
|
||||
// Format: single letter (a-z except j) optionally followed by 1-2 digits
|
||||
|
||||
// Test cases: (input, expected_time_utc)
|
||||
let test_cases = vec![
|
||||
("a", "23:00:00"), // A = UTC+1, midnight in UTC+1 = 23:00 UTC
|
||||
("m", "12:00:00"), // M = UTC+12, midnight in UTC+12 = 12:00 UTC
|
||||
("z", "00:00:00"), // Z = UTC+0, midnight in UTC+0 = 00:00 UTC
|
||||
("m9", "21:00:00"), // M + 9 hours = 12 + 9 = 21:00 UTC
|
||||
("a5", "04:00:00"), // A + 5 hours = 23 + 5 = 04:00 UTC (next day)
|
||||
("z3", "03:00:00"), // Z + 3 hours = 00 + 3 = 03:00 UTC
|
||||
("M", "12:00:00"), // Uppercase should work too
|
||||
("A5", "04:00:00"), // Uppercase with offset
|
||||
];
|
||||
|
||||
for (input, expected) in test_cases {
|
||||
new_ucmd!()
|
||||
.env("TZ", "UTC")
|
||||
.arg("-d")
|
||||
.arg(input)
|
||||
.arg("+%T")
|
||||
.succeeds()
|
||||
.stdout_is(format!("{expected}\n"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue