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
95 changes: 51 additions & 44 deletions src/uu/date/src/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<String>(OPT_DATE) {
DateSource::Human(date.into())
} else if let Some(file) = matches.get_one::<String>(OPT_FILE) {
Expand All @@ -284,7 +293,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
DateSource::Now
};

let set_to = match matches.get_one::<String>(OPT_SET).map(parse_date) {
let set_to = match matches
.get_one::<String>(OPT_SET)
.map(|s| parse_date(s, &now))
{
None => None,
Some(Err((input, _err))) => {
return Err(USimpleError::new(
Expand All @@ -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,
Expand All @@ -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<dyn Iterator<Item = _>> = match settings.date_source {
DateSource::Human(ref input) => {
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -413,21 +418,21 @@ 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);
Box::new(iter)
}
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) => {
Expand All @@ -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) => {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -756,15 +768,12 @@ fn try_parse_with_abbreviation<S: AsRef<str>>(date_str: S) -> Option<Zoned> {
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);
}
}
}
}
Expand All @@ -786,19 +795,17 @@ fn try_parse_with_abbreviation<S: AsRef<str>>(date_str: S) -> Option<Zoned> {
/// "12345.123456789 seconds ago" which failed in 0.11 but works in 0.13).
fn parse_date<S: AsRef<str> + Clone>(
s: S,
now: &Zoned,
) -> Result<Zoned, (String, parse_datetime::ParseDateTimeError)> {
// 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)),
}
}
Expand Down
71 changes: 71 additions & 0 deletions tests/by-util/test_date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!());
Expand Down
Loading