cargo_cargofmt/formatting/
datetime.rs

1use std::borrow::Cow;
2
3use crate::toml::ScalarKind;
4use crate::toml::TokenKind;
5
6/// Normalizes datetime separators to use `T` instead of space or lowercase `t`.
7///
8/// TOML allows `2025-12-26T10:30:00`, `2025-12-26t10:30:00`, and `2025-12-26 10:30:00`.
9/// This function normalizes to the uppercase `T` form for consistency.
10#[tracing::instrument]
11pub fn normalize_datetime_separators(tokens: &mut crate::toml::TomlTokens<'_>) {
12    // YYYY-MM-DD is 10 characters, so the separator is at index 10
13    const DATE_LEN: usize = 10;
14
15    for i in tokens.indices() {
16        let token = &mut tokens.tokens[i];
17        if token.kind == TokenKind::Scalar && token.scalar == Some(ScalarKind::DateTime) {
18            let raw_bytes = token.raw.as_bytes();
19            if raw_bytes.len() > DATE_LEN && matches!(raw_bytes[DATE_LEN], b' ' | b't') {
20                let mut new_raw = token.raw.to_string();
21                new_raw.replace_range(DATE_LEN..=DATE_LEN, "T");
22                token.raw = Cow::Owned(new_raw);
23            }
24        }
25    }
26}
27
28#[cfg(test)]
29mod test {
30    use snapbox::assert_data_eq;
31    use snapbox::str;
32    use snapbox::IntoData;
33
34    #[track_caller]
35    fn valid(input: &str, expected: impl IntoData) {
36        let mut tokens = crate::toml::TomlTokens::parse(input);
37        super::normalize_datetime_separators(&mut tokens);
38        let actual = tokens.to_string();
39
40        assert_data_eq!(&actual, expected);
41
42        let (_, errors) = toml::de::DeTable::parse_recoverable(&actual);
43        if !errors.is_empty() {
44            use std::fmt::Write as _;
45            let mut result = String::new();
46            writeln!(&mut result, "---").unwrap();
47            for error in errors {
48                writeln!(&mut result, "{error}").unwrap();
49                writeln!(&mut result, "---").unwrap();
50            }
51            panic!("failed to parse\n---\n{actual}\n{result}");
52        }
53    }
54
55    #[test]
56    fn empty() {
57        valid("", str![]);
58    }
59
60    #[test]
61    fn no_datetime() {
62        valid(
63            r#"
64name = "test"
65version = 1
66"#,
67            str![[r#"
68
69name = "test"
70version = 1
71
72"#]],
73        );
74    }
75
76    #[test]
77    fn datetime_with_t_unchanged() {
78        valid(
79            r#"
80created = 2025-12-26T10:30:00
81"#,
82            str![[r#"
83
84created = 2025-12-26T10:30:00
85
86"#]],
87        );
88    }
89
90    #[test]
91    fn datetime_with_space_normalized() {
92        valid(
93            r#"
94created = 2025-12-26 10:30:00
95"#,
96            str![[r#"
97
98created = 2025-12-26T10:30:00
99
100"#]],
101        );
102    }
103
104    #[test]
105    fn local_date_only_unchanged() {
106        valid(
107            r#"
108date = 2025-12-26
109"#,
110            str![[r#"
111
112date = 2025-12-26
113
114"#]],
115        );
116    }
117
118    #[test]
119    fn local_time_only_unchanged() {
120        valid(
121            r#"
122time = 10:30:00
123"#,
124            str![[r#"
125
126time = 10:30:00
127
128"#]],
129        );
130    }
131
132    #[test]
133    fn offset_datetime_with_space_normalized() {
134        valid(
135            r#"
136utc = 2025-12-26 10:30:00Z
137"#,
138            str![[r#"
139
140utc = 2025-12-26T10:30:00Z
141
142"#]],
143        );
144    }
145
146    #[test]
147    fn offset_datetime_with_timezone_normalized() {
148        valid(
149            r#"
150eastern = 2025-12-26 10:30:00-05:00
151"#,
152            str![[r#"
153
154eastern = 2025-12-26T10:30:00-05:00
155
156"#]],
157        );
158    }
159
160    #[test]
161    fn datetime_in_array() {
162        valid(
163            r#"
164dates = [
165    2025-12-26 10:30:00,
166    2025-12-27 11:00:00,
167]
168"#,
169            str![[r#"
170
171dates = [
172    2025-12-26T10:30:00,
173    2025-12-27T11:00:00,
174]
175
176"#]],
177        );
178    }
179
180    #[test]
181    fn datetime_in_inline_table() {
182        valid(
183            r#"
184event = { start = 2025-12-26 10:30:00, end = 2025-12-26 12:00:00 }
185"#,
186            str![[r#"
187
188event = { start = 2025-12-26T10:30:00, end = 2025-12-26T12:00:00 }
189
190"#]],
191        );
192    }
193
194    #[test]
195    fn mixed_datetime_formats() {
196        valid(
197            r#"
198with_t = 2025-12-26T10:30:00
199with_space = 2025-12-26 10:30:00
200date_only = 2025-12-26
201time_only = 10:30:00
202"#,
203            str![[r#"
204
205with_t = 2025-12-26T10:30:00
206with_space = 2025-12-26T10:30:00
207date_only = 2025-12-26
208time_only = 10:30:00
209
210"#]],
211        );
212    }
213
214    #[test]
215    fn datetime_with_fractional_seconds() {
216        valid(
217            r#"
218precise = 2025-12-26 10:30:00.123456
219"#,
220            str![[r#"
221
222precise = 2025-12-26T10:30:00.123456
223
224"#]],
225        );
226    }
227
228    #[test]
229    fn datetime_with_lowercase_t_normalized() {
230        // RFC 3339 allows lowercase 't' as separator
231        // We normalize to uppercase 'T' for consistency
232        valid(
233            r#"
234created = 2025-12-26t10:30:00
235"#,
236            str![[r#"
237
238created = 2025-12-26T10:30:00
239
240"#]],
241        );
242    }
243
244    #[test]
245    fn offset_datetime_with_positive_timezone() {
246        valid(
247            r#"
248tokyo = 2025-12-26 10:30:00+09:00
249"#,
250            str![[r#"
251
252tokyo = 2025-12-26T10:30:00+09:00
253
254"#]],
255        );
256    }
257}