diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index cc507c78176..5e84e5d6ce5 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -269,6 +269,15 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { Format::Default }; + let utc = matches.get_flag(OPT_UNIVERSAL); + + // Get the current time, either in the local time zone or UTC. + let now = if utc { + Timestamp::now().to_zoned(TimeZone::UTC) + } else { + Zoned::now() + }; + let date_source = if let Some(date) = matches.get_one::(OPT_DATE) { DateSource::Human(date.into()) } else if let Some(file) = matches.get_one::(OPT_FILE) { @@ -284,7 +293,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { DateSource::Now }; - let set_to = match matches.get_one::(OPT_SET).map(parse_date) { + let set_to = match matches + .get_one::(OPT_SET) + .map(|s| parse_date(s, &now)) + { None => None, Some(Err((input, _err))) => { return Err(USimpleError::new( @@ -296,7 +308,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { }; let settings = Settings { - utc: matches.get_flag(OPT_UNIVERSAL), + utc, format, date_source, set_to, @@ -315,13 +327,6 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { return set_system_datetime(date); } - // Get the current time, either in the local time zone or UTC. - let now = if settings.utc { - Timestamp::now().to_zoned(TimeZone::UTC) - } else { - Zoned::now() - }; - // Iterate over all dates - whether it's a single date or a file. let dates: Box> = match settings.date_source { DateSource::Human(ref input) => { @@ -367,7 +372,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } else { format!("{date_part} 00:00 {offset}") }; - parse_date(composed) + parse_date(composed, &now) } else if let Some((total_hours, day_delta)) = military_tz_with_offset { // Military timezone with optional hour offset // Convert to UTC time: midnight + military_tz_offset + additional_hours @@ -382,12 +387,12 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .unwrap_or_else(|_| String::from("1970-01-01")) }; let date_part = match day_delta { - DayDelta::Same => format_date_with_epoch_fallback(Ok(now)), + DayDelta::Same => format_date_with_epoch_fallback(Ok(now.clone())), DayDelta::Next => format_date_with_epoch_fallback(now.tomorrow()), DayDelta::Previous => format_date_with_epoch_fallback(now.yesterday()), }; let composed = format!("{date_part} {total_hours:02}:00:00 +00:00"); - parse_date(composed) + parse_date(composed, &now) } else if is_pure_digits { // Derive HH and MM from the input let (hh_opt, mm_opt) = if input.len() <= 2 { @@ -413,13 +418,13 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } else { format!("{date_part} {hh:02}:{mm:02} {offset}") }; - parse_date(composed) + parse_date(composed, &now) } else { // Fallback on parse failure of digits - parse_date(input) + parse_date(input, &now) } } else { - parse_date(input) + parse_date(input, &now) }; let iter = std::iter::once(date); @@ -427,7 +432,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } DateSource::Stdin => { let lines = BufReader::new(std::io::stdin()).lines(); - let iter = lines.map_while(Result::ok).map(parse_date); + let iter = lines.map_while(Result::ok).map(|s| parse_date(s, &now)); Box::new(iter) } DateSource::File(ref path) => { @@ -440,7 +445,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let file = File::open(path).map_err_context(|| path.as_os_str().maybe_quote().to_string())?; let lines = BufReader::new(file).lines(); - let iter = lines.map_while(Result::ok).map(parse_date); + let iter = lines.map_while(Result::ok).map(|s| parse_date(s, &now)); Box::new(iter) } DateSource::FileMtime(ref path) => { @@ -476,18 +481,25 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let config = Config::new().custom(PosixCustom::new()).lenient(true); for date in dates { match date { - Ok(date) => match format_date_with_locale_aware_months(&date, format_string, &config) { - Ok(s) => writeln!(stdout, "{s}").map_err(|e| { - USimpleError::new(1, translate!("date-error-write", "error" => e)) - })?, - Err(e) => { - let _ = stdout.flush(); - return Err(USimpleError::new( - 1, - translate!("date-error-invalid-format", "format" => format_string, "error" => e), - )); + Ok(date) => { + let date = if settings.utc { + date.with_time_zone(TimeZone::UTC) + } else { + date + }; + match format_date_with_locale_aware_months(&date, format_string, &config) { + Ok(s) => writeln!(stdout, "{s}").map_err(|e| { + USimpleError::new(1, translate!("date-error-write", "error" => e)) + })?, + Err(e) => { + let _ = stdout.flush(); + return Err(USimpleError::new( + 1, + translate!("date-error-invalid-format", "format" => format_string, "error" => e), + )); + } } - }, + } Err((input, _err)) => { let _ = stdout.flush(); show!(USimpleError::new( @@ -756,15 +768,12 @@ fn try_parse_with_abbreviation>(date_str: S) -> Option { if let Ok(tz) = TimeZone::get(iana_name) { // Parse the date part (everything before the TZ abbreviation) let date_part = s.trim_end_matches(last_word).trim(); - - // Try to parse the date with UTC first to get timestamp - let date_with_utc = format!("{date_part} +00:00"); - if let Ok(parsed) = parse_datetime::parse_datetime(&date_with_utc) { - // Get timestamp from parsed date (which is already a Zoned) - let ts = parsed.timestamp(); - - // Get the offset for this specific timestamp in the target timezone - return Some(ts.to_zoned(tz)); + // Parse in the target timezone so "10:30 EDT" means 10:30 in EDT + if let Ok(parsed) = parse_datetime::parse_datetime(date_part) { + let dt = parsed.datetime(); + if let Ok(zoned) = dt.to_zoned(tz) { + return Some(zoned); + } } } } @@ -786,19 +795,17 @@ fn try_parse_with_abbreviation>(date_str: S) -> Option { /// "12345.123456789 seconds ago" which failed in 0.11 but works in 0.13). fn parse_date + Clone>( s: S, + now: &Zoned, ) -> Result { // First, try to parse any timezone abbreviations if let Some(zoned) = try_parse_with_abbreviation(s.as_ref()) { return Ok(zoned); } - match parse_datetime::parse_datetime(s.as_ref()) { - Ok(date) => { - // Convert to system timezone for display - // (parse_datetime 0.13 returns Zoned in the input's timezone) - let timestamp = date.timestamp(); - Ok(timestamp.to_zoned(TimeZone::try_system().unwrap_or(TimeZone::UTC))) - } + match parse_datetime::parse_datetime_at_date(now.clone(), s.as_ref()) { + // Convert to system timezone for display + // (parse_datetime 0.13 returns Zoned in the input's timezone) + Ok(date) => Ok(date.timestamp().to_zoned(now.time_zone().clone())), Err(e) => Err((s.as_ref().into(), e)), } } diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 08b72ce2550..923922f2b04 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -221,6 +221,77 @@ fn test_date_utc_issue_6495() { .stdout_is("Thu Jan 1 00:00:00 UTC 1970\n"); } +#[test] +fn test_date_utc_with_d_flag() { + let cases = [ + ("2024-01-01 12:00", "+%H:%M %Z", "12:00 UTC\n"), + ("2024-06-15 10:30", "+%H:%M %Z", "10:30 UTC\n"), + ("2024-12-31 23:59:59", "+%H:%M:%S %Z", "23:59:59 UTC\n"), + ("@0", "+%Y-%m-%d %H:%M:%S %Z", "1970-01-01 00:00:00 UTC\n"), + ("@3600", "+%H:%M:%S %Z", "01:00:00 UTC\n"), + ("@86400", "+%Y-%m-%d %Z", "1970-01-02 UTC\n"), + ("2024-06-15 10:30 EDT", "+%H:%M %Z", "14:30 UTC\n"), + ("2024-01-15 10:30 EST", "+%H:%M %Z", "15:30 UTC\n"), + ("2024-06-15 12:00 PDT", "+%H:%M %Z", "19:00 UTC\n"), + ("2024-01-15 12:00 PST", "+%H:%M %Z", "20:00 UTC\n"), + ("2024-01-01 12:00 +0000", "+%H:%M %Z", "12:00 UTC\n"), + ("2024-01-01 12:00 +0530", "+%H:%M %Z", "06:30 UTC\n"), + ("2024-01-01 12:00 -0500", "+%H:%M %Z", "17:00 UTC\n"), + ]; + for (input, fmt, expected) in cases { + new_ucmd!() + .env("TZ", "America/New_York") + .args(&["-u", "-d", input, fmt]) + .succeeds() + .stdout_is(expected); + } +} + +#[test] +fn test_date_utc_vs_local() { + let cases = [ + ("-d", "2024-01-01 12:00", "+%H:%M %Z", "12:00 EST\n"), + ("-ud", "2024-01-01 12:00", "+%H:%M %Z", "12:00 UTC\n"), + ("-d", "2024-06-15 12:00", "+%H:%M %Z", "12:00 EDT\n"), + ("-ud", "2024-06-15 12:00", "+%H:%M %Z", "12:00 UTC\n"), + ("-d", "@0", "+%H:%M %Z", "19:00 EST\n"), + ("-ud", "@0", "+%H:%M %Z", "00:00 UTC\n"), + ]; + for (flag, date, fmt, expected) in cases { + new_ucmd!() + .env("TZ", "America/New_York") + .args(&[flag, date, fmt]) + .succeeds() + .stdout_is(expected); + } +} + +#[test] +fn test_date_utc_output_formats() { + let cases = [ + ("-I", "2024-06-15"), + ("--rfc-3339=seconds", "+00:00"), + ("-R", "+0000"), + ]; + for (fmt_flag, expected) in cases { + new_ucmd!() + .env("TZ", "America/New_York") + .args(&["-u", "-d", "2024-06-15 12:00", fmt_flag]) + .succeeds() + .stdout_contains(expected); + } +} + +#[test] +fn test_date_utc_stdin() { + new_ucmd!() + .env("TZ", "America/New_York") + .args(&["-u", "-f", "-", "+%H:%M %Z"]) + .pipe_in("2024-01-01 12:00\n2024-06-15 18:30\n") + .succeeds() + .stdout_is("12:00 UTC\n18:30 UTC\n"); +} + #[test] fn test_date_format_y() { let scene = TestScenario::new(util_name!());