Skip to content

Commit b5e0192

Browse files
Copilotjgarzik
andcommitted
Improve %Z handling: fix DST transitions and escaped %% support
Co-authored-by: jgarzik <494411+jgarzik@users.noreply.github.com>
1 parent 9d8acd1 commit b5e0192

File tree

1 file changed

+67
-17
lines changed

1 file changed

+67
-17
lines changed

datetime/date.rs

Lines changed: 67 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ fn get_timezone_abbreviation(dt: &DateTime<Local>) -> String {
3131
// Try to parse it as a chrono-tz timezone
3232
if let Ok(tz) = tz_str.parse::<Tz>() {
3333
// Convert the local datetime to the specified timezone
34-
if let Some(dt_tz) = tz.from_local_datetime(&dt.naive_local()).single() {
34+
// Use earliest() to handle DST transitions consistently
35+
let dt_tz = tz.from_local_datetime(&dt.naive_local()).earliest()
36+
.or_else(|| tz.from_local_datetime(&dt.naive_local()).latest());
37+
if let Some(dt_tz) = dt_tz {
3538
return dt_tz.format("%Z").to_string();
3639
}
3740
}
@@ -49,15 +52,69 @@ fn get_timezone_abbreviation(dt: &DateTime<Local>) -> String {
4952
}
5053

5154
/// Format a datetime string, replacing %Z with proper timezone abbreviation
52-
fn format_with_timezone(formatstr: &str, dt: &DateTime<Local>) -> String {
55+
fn format_with_timezone_local(formatstr: &str, dt: &DateTime<Local>) -> String {
5356
if formatstr.contains("%Z") {
5457
let tz_abbr = get_timezone_abbreviation(dt);
55-
let formatted = dt.format(formatstr).to_string();
56-
// Replace the offset (like +01:00) with the timezone abbreviation
57-
// chrono uses %Z for offset, so we need to replace it
58-
// The offset format from chrono with %Z is like "+01:00" or "+00:00"
59-
let offset_pattern = dt.format("%Z").to_string();
60-
formatted.replace(&offset_pattern, &tz_abbr)
58+
// Process the format string character by character to handle %Z properly
59+
let mut result = String::new();
60+
let mut chars = formatstr.chars().peekable();
61+
62+
while let Some(ch) = chars.next() {
63+
if ch == '%' {
64+
if let Some(&next_ch) = chars.peek() {
65+
if next_ch == '%' {
66+
// %% should become % in the output - let chrono handle this
67+
result.push('%');
68+
result.push('%');
69+
chars.next(); // consume the second %
70+
continue;
71+
} else if next_ch == 'Z' {
72+
// Replace %Z with the timezone abbreviation
73+
result.push_str(&tz_abbr);
74+
chars.next(); // consume 'Z'
75+
continue;
76+
}
77+
}
78+
}
79+
result.push(ch);
80+
}
81+
82+
// Format the modified format string
83+
dt.format(&result).to_string()
84+
} else {
85+
dt.format(formatstr).to_string()
86+
}
87+
}
88+
89+
/// Format a datetime string for UTC, replacing %Z with "UTC"
90+
fn format_with_timezone_utc(formatstr: &str, dt: &DateTime<Utc>) -> String {
91+
if formatstr.contains("%Z") {
92+
// Process the format string character by character to handle %Z properly
93+
let mut result = String::new();
94+
let mut chars = formatstr.chars().peekable();
95+
96+
while let Some(ch) = chars.next() {
97+
if ch == '%' {
98+
if let Some(&next_ch) = chars.peek() {
99+
if next_ch == '%' {
100+
// %% should become % in the output - let chrono handle this
101+
result.push('%');
102+
result.push('%');
103+
chars.next(); // consume the second %
104+
continue;
105+
} else if next_ch == 'Z' {
106+
// Replace %Z with "UTC"
107+
result.push_str("UTC");
108+
chars.next(); // consume 'Z'
109+
continue;
110+
}
111+
}
112+
}
113+
result.push(ch);
114+
}
115+
116+
// Format the modified format string
117+
dt.format(&result).to_string()
61118
} else {
62119
dt.format(formatstr).to_string()
63120
}
@@ -86,19 +143,12 @@ struct Args {
86143

87144
fn show_time_local(formatstr: &str) -> String {
88145
let now = chrono::Local::now();
89-
format_with_timezone(formatstr, &now)
146+
format_with_timezone_local(formatstr, &now)
90147
}
91148

92149
fn show_time_utc(formatstr: &str) -> String {
93150
let now = chrono::Utc::now();
94-
// For UTC, %Z should always be "UTC"
95-
if formatstr.contains("%Z") {
96-
let formatted = now.format(formatstr).to_string();
97-
let offset_pattern = now.format("%Z").to_string();
98-
formatted.replace(&offset_pattern, "UTC")
99-
} else {
100-
now.format(formatstr).to_string()
101-
}
151+
format_with_timezone_utc(formatstr, &now)
102152
}
103153

104154
fn show_time(utc: bool, formatstr: &str) {

0 commit comments

Comments
 (0)