1use crate::time::tz::TimezoneSource;
15use chrono::{DateTime, Utc};
16
17pub const DEFAULT_FORMAT: &str = "%b %d %H:%M:%S";
19
20pub const DEFAULT_FRACTIONAL_DIGITS: usize = 6;
23
24pub fn format_default(now: DateTime<Utc>, tz: &TimezoneSource) -> String {
26 format_with(DEFAULT_FORMAT, now, tz)
27}
28
29pub fn format_with(spec: &str, now: DateTime<Utc>, tz: &TimezoneSource) -> String {
32 if !spec.contains("%.S") && !spec.contains("%.s") {
33 return tz.render(now, spec);
34 }
35
36 let micros = now.timestamp_subsec_micros();
42 let epoch = now.timestamp();
43 let frac_seconds = format!("{micros:06}");
44 let frac_epoch = format!("{epoch}.{micros:06}");
45
46 let mut out = String::with_capacity(spec.len() + 16);
47 let mut remaining = spec;
48
49 loop {
50 let pos_big = remaining.find("%.S");
52 let pos_small = remaining.find("%.s");
53
54 let (pos, is_big) = match (pos_big, pos_small) {
55 (Some(a), Some(b)) if a < b => (a, true),
56 (Some(_), Some(b)) => (b, false),
57 (Some(a), None) => (a, true),
58 (None, Some(b)) => (b, false),
59 (None, None) => {
60 out.push_str(&tz.render(now, remaining));
62 break;
63 }
64 };
65
66 if pos > 0 {
68 out.push_str(&tz.render(now, &remaining[..pos]));
69 }
70
71 if is_big {
72 let seconds = tz.render(now, "%S");
76 out.push_str(&seconds);
77 out.push('.');
78 out.push_str(&frac_seconds);
79 } else {
80 out.push_str(&frac_epoch);
83 }
84
85 remaining = &remaining[pos + 3..];
86 }
87
88 out
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94 use chrono::{TimeZone, Timelike};
95
96 fn fixture_instant() -> DateTime<Utc> {
97 Utc.with_ymd_and_hms(2026, 5, 22, 14, 30, 45)
100 .unwrap()
101 .with_nanosecond(123_456_000)
102 .unwrap()
103 }
104
105 #[test]
106 fn default_format_matches_moreutils_default_string() {
107 assert_eq!(DEFAULT_FORMAT, "%b %d %H:%M:%S");
108 }
109
110 #[test]
111 fn default_format_under_utc_is_deterministic() {
112 let rendered = format_default(fixture_instant(), &TimezoneSource::Utc);
113 assert_eq!(rendered, "May 22 14:30:45");
114 }
115
116 #[test]
117 fn custom_format_renders_tokens() {
118 let rendered = format_with("%Y-%m-%d %H:%M:%S", fixture_instant(), &TimezoneSource::Utc);
119 assert_eq!(rendered, "2026-05-22 14:30:45");
120 }
121
122 #[test]
123 fn literal_brackets_are_preserved() {
124 let rendered = format_with("[%H:%M:%S]", fixture_instant(), &TimezoneSource::Utc);
125 assert_eq!(rendered, "[14:30:45]");
126 }
127
128 #[test]
129 fn fractional_seconds_token_expands() {
130 let rendered = format_with("%H:%M:%.S", fixture_instant(), &TimezoneSource::Utc);
131 assert_eq!(rendered, "14:30:45.123456");
132 }
133
134 #[test]
135 fn fractional_epoch_token_expands() {
136 let rendered = format_with("%.s", fixture_instant(), &TimezoneSource::Utc);
137 let expected_epoch: i64 = fixture_instant().timestamp();
139 assert_eq!(rendered, format!("{expected_epoch}.123456"));
140 }
141
142 #[test]
143 fn both_fractional_tokens_in_one_string() {
144 let rendered = format_with(
145 "%H:%M:%.S epoch=%.s",
146 fixture_instant(),
147 &TimezoneSource::Utc,
148 );
149 let expected_epoch: i64 = fixture_instant().timestamp();
150 assert_eq!(
151 rendered,
152 format!("14:30:45.123456 epoch={expected_epoch}.123456"),
153 );
154 }
155}