Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 2de96bd

Browse filesBrowse files
committed
date: improve timezone handling and UTC mode support
- Add proper UTC mode handling with -u flag - Improve TZ environment variable support - Add more TZ tests FIXES #7497 FIXES #7498
1 parent 5a0988c commit 2de96bd
Copy full SHA for 2de96bd

File tree

3 files changed

+156
-13
lines changed
Filter options

3 files changed

+156
-13
lines changed

‎src/uu/date/src/date.rs

Copy file name to clipboardExpand all lines: src/uu/date/src/date.rs
+51-10Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
// spell-checker:ignore (chrono) Datelike Timelike ; (format) DATEFILE MMDDhhmm ; (vars) datetime datetimes
77

88
use chrono::format::{Item, StrftimeItems};
9-
use chrono::{DateTime, FixedOffset, Local, Offset, TimeDelta, Utc};
9+
use chrono::{DateTime, FixedOffset, Local, TimeDelta, Utc};
1010
#[cfg(windows)]
1111
use chrono::{Datelike, Timelike};
1212
use clap::{Arg, ArgAction, Command};
@@ -136,6 +136,27 @@ impl From<&str> for Rfc3339Format {
136136
}
137137
}
138138

139+
/// Check if we should use UTC time based on the utc flag and TZ environment variable
140+
fn should_use_utc(utc: bool) -> bool {
141+
// Either the -u flag is set or the TZ environment variable is empty
142+
utc || match std::env::var("TZ") {
143+
Ok(tz) if tz.is_empty() => true,
144+
_ => false,
145+
}
146+
}
147+
148+
/// Get the current time, considering the utc flag and TZ environment variable
149+
fn get_current_time(utc: bool) -> DateTime<Local> {
150+
if should_use_utc(utc) {
151+
// When -u flag is used or TZ is empty, we should use UTC time
152+
// Simply convert UTC to Local without adjusting the time value
153+
Utc::now().into()
154+
} else {
155+
// Use local time when neither condition is met
156+
Local::now()
157+
}
158+
}
159+
139160
#[uucore::main]
140161
#[allow(clippy::cognitive_complexity)]
141162
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
@@ -167,7 +188,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
167188
};
168189

169190
let date_source = if let Some(date) = matches.get_one::<String>(OPT_DATE) {
170-
let ref_time = Local::now();
191+
let ref_time = get_current_time(matches.get_flag(OPT_UNIVERSAL));
171192
if let Ok(new_time) = parse_datetime::parse_datetime_at_date(ref_time, date.as_str()) {
172193
let duration = new_time.signed_duration_since(ref_time);
173194
DateSource::Human(duration)
@@ -212,11 +233,14 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
212233
return set_system_datetime(date);
213234
} else {
214235
// Get the current time, either in the local time zone or UTC.
215-
let now: DateTime<FixedOffset> = if settings.utc {
216-
let now = Utc::now();
217-
now.with_timezone(&now.offset().fix())
236+
let now = get_current_time(settings.utc);
237+
// Convert to FixedOffset for consistent timezone handling
238+
let now_fixed = if should_use_utc(settings.utc) {
239+
// When -u flag is used or TZ is empty, use actual UTC time with zero offset
240+
let utc_now = Utc::now();
241+
utc_now.with_timezone(&FixedOffset::east_opt(0).unwrap())
218242
} else {
219-
let now = Local::now();
243+
// Otherwise use the local timezone's offset
220244
now.with_timezone(now.offset())
221245
};
222246

@@ -230,7 +254,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
230254
DateSource::Human(relative_time) => {
231255
// Double check the result is overflow or not of the current_time + relative_time
232256
// it may cause a panic of chrono::datetime::DateTime add
233-
match now.checked_add_signed(relative_time) {
257+
match now_fixed.checked_add_signed(relative_time) {
234258
Some(date) => {
235259
let iter = std::iter::once(Ok(date));
236260
Box::new(iter)
@@ -262,7 +286,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
262286
Box::new(iter)
263287
}
264288
DateSource::Now => {
265-
let iter = std::iter::once(Ok(now));
289+
let iter = std::iter::once(Ok(now_fixed));
266290
Box::new(iter)
267291
}
268292
};
@@ -273,7 +297,13 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
273297
for date in dates {
274298
match date {
275299
Ok(date) => {
276-
let format_string = custom_time_format(format_string);
300+
let format_string = if should_use_utc(settings.utc) {
301+
// When -u flag is used or TZ is empty, replace %Z with UTC directly
302+
format_string.replace("%Z", "UTC")
303+
} else {
304+
custom_time_format(format_string)
305+
};
306+
277307
// Refuse to pass this string to chrono as it is crashing in this crate
278308
if format_string.contains("%#z") {
279309
return Err(USimpleError::new(
@@ -290,10 +320,21 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
290320
format!("invalid format {}", format_string.replace("%f", "%N")),
291321
));
292322
}
293-
let formatted = date
323+
324+
// When -u flag is used or TZ is empty, ensure we format using UTC time
325+
let date_to_format = if should_use_utc(settings.utc) {
326+
// Convert the date to UTC to ensure correct time display
327+
date.with_timezone(&Utc)
328+
.with_timezone(&FixedOffset::east_opt(0).unwrap())
329+
} else {
330+
date
331+
};
332+
333+
let formatted = date_to_format
294334
.format_with_items(format_items)
295335
.to_string()
296336
.replace("%f", "%N");
337+
297338
println!("{formatted}");
298339
}
299340
Err((input, _err)) => show!(USimpleError::new(

‎src/uucore/src/lib/features/custom_tz_fmt.rs

Copy file name to clipboardExpand all lines: src/uucore/src/lib/features/custom_tz_fmt.rs
+7-3Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,15 @@ use iana_time_zone::get_timezone;
1111
///
1212
/// For example, "UTC" or "CET" or "PDT"
1313
fn timezone_abbreviation() -> String {
14+
// Check if we're in UTC mode (either through TZ=UTC0 or -u flag)
1415
let tz = match std::env::var("TZ") {
15-
// TODO Support other time zones...
16+
// If TZ is set to UTC0 or empty, use UTC
1617
Ok(s) if s == "UTC0" || s.is_empty() => Tz::Etc__UTC,
17-
_ => match get_timezone() {
18-
Ok(tz_str) => tz_str.parse().unwrap(),
18+
// If TZ is set to any other value, try to parse it
19+
Ok(tz_str) => tz_str.parse().unwrap_or(Tz::Etc__UTC),
20+
// If TZ is not set, try to get the system timezone
21+
Err(_) => match get_timezone() {
22+
Ok(tz_str) => tz_str.parse().unwrap_or(Tz::Etc__UTC),
1923
Err(_) => Tz::Etc__UTC,
2024
},
2125
};

‎tests/by-util/test_date.rs

Copy file name to clipboardExpand all lines: tests/by-util/test_date.rs
+98Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,3 +492,101 @@ fn test_date_empty_tz() {
492492
.succeeds()
493493
.stdout_only("UTC\n");
494494
}
495+
496+
#[test]
497+
fn test_date_tz_utc() {
498+
new_ucmd!()
499+
.env("TZ", "UTC0")
500+
.arg("+%Z")
501+
.succeeds()
502+
.stdout_only("UTC\n");
503+
}
504+
505+
#[test]
506+
fn test_date_tz_berlin() {
507+
new_ucmd!()
508+
.env("TZ", "Europe/Berlin")
509+
.arg("+%Z")
510+
.succeeds()
511+
.stdout_only("CET\n");
512+
}
513+
514+
#[test]
515+
fn test_date_tz_vancouver() {
516+
new_ucmd!()
517+
.env("TZ", "America/Vancouver")
518+
.arg("+%Z")
519+
.succeeds()
520+
.stdout_only("PDT\n");
521+
}
522+
523+
#[test]
524+
fn test_date_tz_invalid() {
525+
new_ucmd!()
526+
.env("TZ", "Invalid/Timezone")
527+
.arg("+%Z")
528+
.succeeds()
529+
.stdout_only("UTC\n");
530+
}
531+
532+
#[test]
533+
fn test_date_tz_with_format() {
534+
new_ucmd!()
535+
.env("TZ", "Europe/Berlin")
536+
.arg("+%Y-%m-%d %H:%M:%S %Z")
537+
.succeeds()
538+
.stdout_matches(&Regex::new(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} CET\n$").unwrap());
539+
}
540+
541+
#[test]
542+
fn test_date_tz_with_utc_flag() {
543+
new_ucmd!()
544+
.env("TZ", "Europe/Berlin")
545+
.arg("-u")
546+
.arg("+%Z")
547+
.succeeds()
548+
.stdout_only("UTC\n");
549+
}
550+
551+
#[test]
552+
fn test_date_tz_with_date_string() {
553+
new_ucmd!()
554+
.env("TZ", "Asia/Tokyo")
555+
.arg("-d")
556+
.arg("2024-01-01 12:00:00")
557+
.arg("+%Y-%m-%d %H:%M:%S %Z")
558+
.succeeds()
559+
.stdout_only("2024-01-01 12:00:00 JST\n");
560+
}
561+
562+
#[test]
563+
fn test_date_tz_with_relative_time() {
564+
new_ucmd!()
565+
.env("TZ", "America/Vancouver")
566+
.arg("-d")
567+
.arg("1 hour ago")
568+
.arg("+%Y-%m-%d %H:%M:%S %Z")
569+
.succeeds()
570+
.stdout_matches(&Regex::new(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} PDT\n$").unwrap());
571+
}
572+
573+
#[test]
574+
fn test_date_utc_time() {
575+
// Test that -u flag shows correct UTC time
576+
new_ucmd!().arg("-u").arg("+%H:%M").succeeds();
577+
578+
// Test that -u flag shows UTC timezone
579+
new_ucmd!()
580+
.arg("-u")
581+
.arg("+%Z")
582+
.succeeds()
583+
.stdout_only("UTC\n");
584+
585+
// Test that -u flag with specific timestamp shows correct UTC time
586+
new_ucmd!()
587+
.arg("-u")
588+
.arg("-d")
589+
.arg("@0")
590+
.succeeds()
591+
.stdout_only("Thu Jan 1 00:00:00 UTC 1970\n");
592+
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.