-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
date: improve timezone handling and UTC mode support #7597
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,7 +6,7 @@ | |
// spell-checker:ignore (chrono) Datelike Timelike ; (format) DATEFILE MMDDhhmm ; (vars) datetime datetimes | ||
|
||
use chrono::format::{Item, StrftimeItems}; | ||
use chrono::{DateTime, FixedOffset, Local, Offset, TimeDelta, Utc}; | ||
use chrono::{DateTime, FixedOffset, Local, TimeDelta, Utc}; | ||
#[cfg(windows)] | ||
use chrono::{Datelike, Timelike}; | ||
use clap::{Arg, ArgAction, Command}; | ||
|
@@ -136,6 +136,24 @@ impl From<&str> for Rfc3339Format { | |
} | ||
} | ||
|
||
/// Check if we should use UTC time based on the utc flag and TZ environment variable | ||
fn should_use_utc(utc: bool) -> bool { | ||
// Either the -u flag is set or the TZ environment variable is empty | ||
utc || matches!(std::env::var("TZ"), Ok(tz) if tz.is_empty()) | ||
} | ||
|
||
/// Get the current time, considering the utc flag and TZ environment variable | ||
fn get_current_time(utc: bool) -> DateTime<Local> { | ||
if should_use_utc(utc) { | ||
// When -u flag is used or TZ is empty, we should use UTC time | ||
// Simply convert UTC to Local without adjusting the time value | ||
Utc::now().into() | ||
} else { | ||
// Use local time when neither condition is met | ||
Local::now() | ||
} | ||
} | ||
|
||
#[uucore::main] | ||
#[allow(clippy::cognitive_complexity)] | ||
pub fn uumain(args: impl uucore::Args) -> UResult<()> { | ||
|
@@ -167,7 +185,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { | |
}; | ||
|
||
let date_source = if let Some(date) = matches.get_one::<String>(OPT_DATE) { | ||
let ref_time = Local::now(); | ||
let ref_time = get_current_time(matches.get_flag(OPT_UNIVERSAL)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd have a mild preference if you did this:
And used utc here and below when initializing |
||
if let Ok(new_time) = parse_datetime::parse_datetime_at_date(ref_time, date.as_str()) { | ||
let duration = new_time.signed_duration_since(ref_time); | ||
DateSource::Human(duration) | ||
|
@@ -212,11 +230,14 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { | |
return set_system_datetime(date); | ||
} else { | ||
// Get the current time, either in the local time zone or UTC. | ||
let now: DateTime<FixedOffset> = if settings.utc { | ||
let now = Utc::now(); | ||
now.with_timezone(&now.offset().fix()) | ||
let now = get_current_time(settings.utc); | ||
// Convert to FixedOffset for consistent timezone handling | ||
let now_fixed = if should_use_utc(settings.utc) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why aren't you moving the |
||
// When -u flag is used or TZ is empty, use actual UTC time with zero offset | ||
let utc_now = Utc::now(); | ||
utc_now.with_timezone(&FixedOffset::east_opt(0).unwrap()) | ||
} else { | ||
let now = Local::now(); | ||
// Otherwise use the local timezone's offset | ||
now.with_timezone(now.offset()) | ||
}; | ||
|
||
|
@@ -230,7 +251,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { | |
DateSource::Human(relative_time) => { | ||
// Double check the result is overflow or not of the current_time + relative_time | ||
// it may cause a panic of chrono::datetime::DateTime add | ||
match now.checked_add_signed(relative_time) { | ||
match now_fixed.checked_add_signed(relative_time) { | ||
Some(date) => { | ||
let iter = std::iter::once(Ok(date)); | ||
Box::new(iter) | ||
|
@@ -262,7 +283,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { | |
Box::new(iter) | ||
} | ||
DateSource::Now => { | ||
let iter = std::iter::once(Ok(now)); | ||
let iter = std::iter::once(Ok(now_fixed)); | ||
Box::new(iter) | ||
} | ||
}; | ||
|
@@ -273,7 +294,13 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { | |
for date in dates { | ||
match date { | ||
Ok(date) => { | ||
let format_string = custom_time_format(format_string); | ||
let format_string = if should_use_utc(settings.utc) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wouldn't it be better to move that logic to |
||
// When -u flag is used or TZ is empty, replace %Z with UTC directly | ||
format_string.replace("%Z", "UTC") | ||
} else { | ||
custom_time_format(format_string) | ||
}; | ||
|
||
// Refuse to pass this string to chrono as it is crashing in this crate | ||
if format_string.contains("%#z") { | ||
return Err(USimpleError::new( | ||
|
@@ -290,10 +317,21 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { | |
format!("invalid format {}", format_string.replace("%f", "%N")), | ||
)); | ||
} | ||
let formatted = date | ||
|
||
// When -u flag is used or TZ is empty, ensure we format using UTC time | ||
let date_to_format = if should_use_utc(settings.utc) { | ||
// Convert the date to UTC to ensure correct time display | ||
date.with_timezone(&Utc) | ||
.with_timezone(&FixedOffset::east_opt(0).unwrap()) | ||
} else { | ||
date | ||
}; | ||
|
||
let formatted = date_to_format | ||
.format_with_items(format_items) | ||
.to_string() | ||
.replace("%f", "%N"); | ||
|
||
println!("{formatted}"); | ||
} | ||
Err((input, _err)) => show!(USimpleError::new( | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -492,3 +492,113 @@ fn test_date_empty_tz() { | |
.succeeds() | ||
.stdout_only("UTC\n"); | ||
} | ||
|
||
#[test] | ||
fn test_date_tz_utc() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you do a blank TZ too? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the comments. there was one test empty TZ and I add another one with |
||
new_ucmd!() | ||
.env("TZ", "UTC0") | ||
.arg("+%Z") | ||
.succeeds() | ||
.stdout_only("UTC\n"); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, it'd be nice to have tests that actually test that the output time is shifted properly (since that's the issue in #7498). You should pass a fixed datetime as a parameter to Note that adding a full test for #7498 might be tricky, as it sort of relies on the localtime not being UTC (not sure how our CI runners are configured...) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dear @drinkcat , I've added a test for this but it was not working correctly on CI/CD. Its kind of complicated. Will try again tomorrow. |
||
|
||
#[test] | ||
fn test_date_tz_berlin() { | ||
new_ucmd!() | ||
.env("TZ", "Europe/Berlin") | ||
.arg("+%Z") | ||
.succeeds() | ||
.stdout_matches(&Regex::new(r"^(CET|CEST)\n$").unwrap()); | ||
} | ||
|
||
#[test] | ||
fn test_date_tz_vancouver() { | ||
new_ucmd!() | ||
.env("TZ", "America/Vancouver") | ||
.arg("+%Z") | ||
.succeeds() | ||
.stdout_matches(&Regex::new(r"^(PDT|PST)\n$").unwrap()); | ||
} | ||
|
||
#[test] | ||
fn test_date_tz_invalid() { | ||
new_ucmd!() | ||
.env("TZ", "Invalid/Timezone") | ||
.arg("+%Z") | ||
.succeeds() | ||
.stdout_only("UTC\n"); | ||
} | ||
|
||
#[test] | ||
fn test_date_tz_with_format() { | ||
new_ucmd!() | ||
.env("TZ", "Europe/Berlin") | ||
.arg("+%Y-%m-%d %H:%M:%S %Z") | ||
.succeeds() | ||
.stdout_matches( | ||
&Regex::new(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} (CET|CEST)\n$").unwrap(), | ||
); | ||
} | ||
|
||
#[test] | ||
fn test_date_tz_with_utc_flag() { | ||
new_ucmd!() | ||
.env("TZ", "Europe/Berlin") | ||
.arg("-u") | ||
.arg("+%Z") | ||
.succeeds() | ||
.stdout_only("UTC\n"); | ||
} | ||
|
||
#[test] | ||
fn test_date_tz_with_date_string() { | ||
new_ucmd!() | ||
.env("TZ", "Asia/Tokyo") | ||
.arg("-d") | ||
.arg("2024-01-01 12:00:00") | ||
.arg("+%Y-%m-%d %H:%M:%S %Z") | ||
.succeeds() | ||
.stdout_only("2024-01-01 12:00:00 JST\n"); | ||
} | ||
|
||
#[test] | ||
fn test_date_tz_with_relative_time() { | ||
new_ucmd!() | ||
.env("TZ", "America/Vancouver") | ||
.arg("-d") | ||
.arg("1 hour ago") | ||
.arg("+%Y-%m-%d %H:%M:%S %Z") | ||
.succeeds() | ||
.stdout_matches(&Regex::new(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} PDT\n$").unwrap()); | ||
} | ||
|
||
#[test] | ||
fn test_date_utc_time() { | ||
// Test that -u flag shows correct UTC time | ||
new_ucmd!().arg("-u").arg("+%H:%M").succeeds(); | ||
|
||
// Test that -u flag shows UTC timezone | ||
new_ucmd!() | ||
.arg("-u") | ||
.arg("+%Z") | ||
.succeeds() | ||
.stdout_only("UTC\n"); | ||
|
||
// Test that -u flag with specific timestamp shows correct UTC time | ||
new_ucmd!() | ||
.arg("-u") | ||
.arg("-d") | ||
.arg("@0") | ||
.succeeds() | ||
.stdout_only("Thu Jan 1 00:00:00 UTC 1970\n"); | ||
} | ||
|
||
#[test] | ||
fn test_date_empty_tz_time() { | ||
new_ucmd!() | ||
.env("TZ", "") | ||
.arg("-d") | ||
.arg("@0") | ||
.succeeds() | ||
.stdout_only("Thu Jan 1 00:00:00 UTC 1970\n"); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this is slightly nicer:
std::env::var("TZ").is_ok_and(|tz| tz.is_empty())