Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
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
115 changes: 113 additions & 2 deletions src/uu/date/src/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<()> {
Expand Down Expand Up @@ -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 todays 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))
Expand Down Expand Up @@ -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
}
}
105 changes: 105 additions & 0 deletions tests/by-util/test_date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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