chrono/format/strftime.rs
1// This is a part of Chrono.
2// See README.md and LICENSE.txt for details.
3
4/*!
5`strftime`/`strptime`-inspired date and time formatting syntax.
6
7## Specifiers
8
9The following specifiers are available both to formatting and parsing.
10
11| Spec. | Example | Description |
12|-------|----------|----------------------------------------------------------------------------|
13| | | **DATE SPECIFIERS:** |
14| `%Y` | `2001` | The full proleptic Gregorian year, zero-padded to 4 digits. [^1] |
15| `%C` | `20` | The proleptic Gregorian year divided by 100, zero-padded to 2 digits. [^2] |
16| `%y` | `01` | The proleptic Gregorian year modulo 100, zero-padded to 2 digits. [^2] |
17| | | |
18| `%m` | `07` | Month number (01--12), zero-padded to 2 digits. |
19| `%b` | `Jul` | Abbreviated month name. Always 3 letters. |
20| `%B` | `July` | Full month name. Also accepts corresponding abbreviation in parsing. |
21| `%h` | `Jul` | Same to `%b`. |
22| | | |
23| `%d` | `08` | Day number (01--31), zero-padded to 2 digits. |
24| `%e` | ` 8` | Same to `%d` but space-padded. Same to `%_d`. |
25| | | |
26| `%a` | `Sun` | Abbreviated weekday name. Always 3 letters. |
27| `%A` | `Sunday` | Full weekday name. Also accepts corresponding abbreviation in parsing. |
28| `%w` | `0` | Sunday = 0, Monday = 1, ..., Saturday = 6. |
29| `%u` | `7` | Monday = 1, Tuesday = 2, ..., Sunday = 7. (ISO 8601) |
30| | | |
31| `%U` | `28` | Week number starting with Sunday (00--53), zero-padded to 2 digits. [^3] |
32| `%W` | `27` | Same to `%U`, but week 1 starts with the first Monday in that year instead.|
33| | | |
34| `%G` | `2001` | Same to `%Y` but uses the year number in ISO 8601 week date. [^4] |
35| `%g` | `01` | Same to `%y` but uses the year number in ISO 8601 week date. [^4] |
36| `%V` | `27` | Same to `%U` but uses the week number in ISO 8601 week date (01--53). [^4] |
37| | | |
38| `%j` | `189` | Day of the year (001--366), zero-padded to 3 digits. |
39| | | |
40| `%D` | `07/08/01` | Month-day-year format. Same to `%m/%d/%y`. |
41| `%x` | `07/08/01` | Same to `%D`. |
42| `%F` | `2001-07-08` | Year-month-day format (ISO 8601). Same to `%Y-%m-%d`. |
43| `%v` | ` 8-Jul-2001` | Day-month-year format. Same to `%e-%b-%Y`. |
44| | | |
45| | | **TIME SPECIFIERS:** |
46| `%H` | `00` | Hour number (00--23), zero-padded to 2 digits. |
47| `%k` | ` 0` | Same to `%H` but space-padded. Same to `%_H`. |
48| `%I` | `12` | Hour number in 12-hour clocks (01--12), zero-padded to 2 digits. |
49| `%l` | `12` | Same to `%I` but space-padded. Same to `%_I`. |
50| | | |
51| `%P` | `am` | `am` or `pm` in 12-hour clocks. |
52| `%p` | `AM` | `AM` or `PM` in 12-hour clocks. |
53| | | |
54| `%M` | `34` | Minute number (00--59), zero-padded to 2 digits. |
55| `%S` | `60` | Second number (00--60), zero-padded to 2 digits. [^5] |
56| `%f` | `026490000` | The fractional seconds (in nanoseconds) since last whole second. [^8] |
57| `%.f` | `.026490`| Similar to `.%f` but left-aligned. These all consume the leading dot. [^8] |
58| `%.3f`| `.026` | Similar to `.%f` but left-aligned but fixed to a length of 3. [^8] |
59| `%.6f`| `.026490` | Similar to `.%f` but left-aligned but fixed to a length of 6. [^8] |
60| `%.9f`| `.026490000` | Similar to `.%f` but left-aligned but fixed to a length of 9. [^8] |
61| `%3f` | `026` | Similar to `%.3f` but without the leading dot. [^8] |
62| `%6f` | `026490` | Similar to `%.6f` but without the leading dot. [^8] |
63| `%9f` | `026490000` | Similar to `%.9f` but without the leading dot. [^8] |
64| | | |
65| `%R` | `00:34` | Hour-minute format. Same to `%H:%M`. |
66| `%T` | `00:34:60` | Hour-minute-second format. Same to `%H:%M:%S`. |
67| `%X` | `00:34:60` | Same to `%T`. |
68| `%r` | `12:34:60 AM` | Hour-minute-second format in 12-hour clocks. Same to `%I:%M:%S %p`. |
69| | | |
70| | | **TIME ZONE SPECIFIERS:** |
71| `%Z` | `ACST` | *Formatting only:* Local time zone name. |
72| `%z` | `+0930` | Offset from the local time to UTC (with UTC being `+0000`). |
73| `%:z` | `+09:30` | Same to `%z` but with a colon. |
74| `%#z` | `+09` | *Parsing only:* Same to `%z` but allows minutes to be missing or present. |
75| | | |
76| | | **DATE & TIME SPECIFIERS:** |
77|`%c`|`Sun Jul 8 00:34:60 2001`|`ctime` date & time format. Same to `%a %b %e %T %Y` sans `\n`.|
78| `%+` | `2001-07-08T00:34:60.026490+09:30` | ISO 8601 / RFC 3339 date & time format. [^6] |
79| | | |
80| `%s` | `994518299` | UNIX timestamp, the number of seconds since 1970-01-01 00:00 UTC. [^7]|
81| | | |
82| | | **SPECIAL SPECIFIERS:** |
83| `%t` | | Literal tab (`\t`). |
84| `%n` | | Literal newline (`\n`). |
85| `%%` | | Literal percent sign. |
86
87It is possible to override the default padding behavior of numeric specifiers `%?`.
88This is not allowed for other specifiers and will result in the `BAD_FORMAT` error.
89
90Modifier | Description
91-------- | -----------
92`%-?` | Suppresses any padding including spaces and zeroes. (e.g. `%j` = `012`, `%-j` = `12`)
93`%_?` | Uses spaces as a padding. (e.g. `%j` = `012`, `%_j` = ` 12`)
94`%0?` | Uses zeroes as a padding. (e.g. `%e` = ` 9`, `%0e` = `09`)
95
96Notes:
97
98[^1]: `%Y`:
99 Negative years are allowed in formatting but not in parsing.
100
101[^2]: `%C`, `%y`:
102 This is floor division, so 100 BCE (year number -99) will print `-1` and `99` respectively.
103
104[^3]: `%U`:
105 Week 1 starts with the first Sunday in that year.
106 It is possible to have week 0 for days before the first Sunday.
107
108[^4]: `%G`, `%g`, `%V`:
109 Week 1 is the first week with at least 4 days in that year.
110 Week 0 does not exist, so this should be used with `%G` or `%g`.
111
112[^5]: `%S`:
113 It accounts for leap seconds, so `60` is possible.
114
115[^6]: `%+`: Same as `%Y-%m-%dT%H:%M:%S%.f%:z`, i.e. 0, 3, 6 or 9 fractional
116 digits for seconds and colons in the time zone offset.
117 <br>
118 <br>
119 The typical `strftime` implementations have different (and locale-dependent)
120 formats for this specifier. While Chrono's format for `%+` is far more
121 stable, it is best to avoid this specifier if you want to control the exact
122 output.
123
124[^7]: `%s`:
125 This is not padded and can be negative.
126 For the purpose of Chrono, it only accounts for non-leap seconds
127 so it slightly differs from ISO C `strftime` behavior.
128
129[^8]: `%f`, `%.f`, `%.3f`, `%.6f`, `%.9f`, `%3f`, `%6f`, `%9f`:
130 <br>
131 The default `%f` is right-aligned and always zero-padded to 9 digits
132 for the compatibility with glibc and others,
133 so it always counts the number of nanoseconds since the last whole second.
134 E.g. 7ms after the last second will print `007000000`,
135 and parsing `7000000` will yield the same.
136 <br>
137 <br>
138 The variant `%.f` is left-aligned and print 0, 3, 6 or 9 fractional digits
139 according to the precision.
140 E.g. 70ms after the last second under `%.f` will print `.070` (note: not `.07`),
141 and parsing `.07`, `.070000` etc. will yield the same.
142 Note that they can print or read nothing if the fractional part is zero or
143 the next character is not `.`.
144 <br>
145 <br>
146 The variant `%.3f`, `%.6f` and `%.9f` are left-aligned and print 3, 6 or 9 fractional digits
147 according to the number preceding `f`.
148 E.g. 70ms after the last second under `%.3f` will print `.070` (note: not `.07`),
149 and parsing `.07`, `.070000` etc. will yield the same.
150 Note that they can read nothing if the fractional part is zero or
151 the next character is not `.` however will print with the specified length.
152 <br>
153 <br>
154 The variant `%3f`, `%6f` and `%9f` are left-aligned and print 3, 6 or 9 fractional digits
155 according to the number preceding `f`, but without the leading dot.
156 E.g. 70ms after the last second under `%3f` will print `070` (note: not `07`),
157 and parsing `07`, `070000` etc. will yield the same.
158 Note that they can read nothing if the fractional part is zero.
159
160*/
161
162use super::{Item, Numeric, Fixed, InternalFixed, InternalInternal, Pad};
163
164/// Parsing iterator for `strftime`-like format strings.
165#[derive(Clone, Debug)]
166pub struct StrftimeItems<'a> {
167 /// Remaining portion of the string.
168 remainder: &'a str,
169 /// If the current specifier is composed of multiple formatting items (e.g. `%+`),
170 /// parser refers to the statically reconstructed slice of them.
171 /// If `recons` is not empty they have to be returned earlier than the `remainder`.
172 recons: &'static [Item<'static>],
173}
174
175impl<'a> StrftimeItems<'a> {
176 /// Creates a new parsing iterator from the `strftime`-like format string.
177 pub fn new(s: &'a str) -> StrftimeItems<'a> {
178 static FMT_NONE: [Item<'static>; 0] = [];
179 StrftimeItems { remainder: s, recons: &FMT_NONE }
180 }
181}
182
183const HAVE_ALTERNATES: &'static str = "z";
184
185impl<'a> Iterator for StrftimeItems<'a> {
186 type Item = Item<'a>;
187
188 fn next(&mut self) -> Option<Item<'a>> {
189 // we have some reconstructed items to return
190 if !self.recons.is_empty() {
191 let item = self.recons[0].clone();
192 self.recons = &self.recons[1..];
193 return Some(item);
194 }
195
196 match self.remainder.chars().next() {
197 // we are done
198 None => None,
199
200 // the next item is a specifier
201 Some('%') => {
202 self.remainder = &self.remainder[1..];
203
204 macro_rules! next {
205 () => (
206 match self.remainder.chars().next() {
207 Some(x) => {
208 self.remainder = &self.remainder[x.len_utf8()..];
209 x
210 },
211 None => return Some(Item::Error), // premature end of string
212 }
213 )
214 }
215
216 let spec = next!();
217 let pad_override = match spec {
218 '-' => Some(Pad::None),
219 '0' => Some(Pad::Zero),
220 '_' => Some(Pad::Space),
221 _ => None,
222 };
223 let is_alternate = spec == '#';
224 let spec = if pad_override.is_some() || is_alternate { next!() } else { spec };
225 if is_alternate && !HAVE_ALTERNATES.contains(spec) {
226 return Some(Item::Error);
227 }
228
229 macro_rules! recons {
230 [$head:expr, $($tail:expr),+] => ({
231 const RECONS: &'static [Item<'static>] = &[$($tail),+];
232 self.recons = RECONS;
233 $head
234 })
235 }
236
237 let item = match spec {
238 'A' => fix!(LongWeekdayName),
239 'B' => fix!(LongMonthName),
240 'C' => num0!(YearDiv100),
241 'D' => recons![num0!(Month), lit!("/"), num0!(Day), lit!("/"),
242 num0!(YearMod100)],
243 'F' => recons![num0!(Year), lit!("-"), num0!(Month), lit!("-"), num0!(Day)],
244 'G' => num0!(IsoYear),
245 'H' => num0!(Hour),
246 'I' => num0!(Hour12),
247 'M' => num0!(Minute),
248 'P' => fix!(LowerAmPm),
249 'R' => recons![num0!(Hour), lit!(":"), num0!(Minute)],
250 'S' => num0!(Second),
251 'T' => recons![num0!(Hour), lit!(":"), num0!(Minute), lit!(":"), num0!(Second)],
252 'U' => num0!(WeekFromSun),
253 'V' => num0!(IsoWeek),
254 'W' => num0!(WeekFromMon),
255 'X' => recons![num0!(Hour), lit!(":"), num0!(Minute), lit!(":"), num0!(Second)],
256 'Y' => num0!(Year),
257 'Z' => fix!(TimezoneName),
258 'a' => fix!(ShortWeekdayName),
259 'b' | 'h' => fix!(ShortMonthName),
260 'c' => recons![fix!(ShortWeekdayName), sp!(" "), fix!(ShortMonthName),
261 sp!(" "), nums!(Day), sp!(" "), num0!(Hour), lit!(":"),
262 num0!(Minute), lit!(":"), num0!(Second), sp!(" "), num0!(Year)],
263 'd' => num0!(Day),
264 'e' => nums!(Day),
265 'f' => num0!(Nanosecond),
266 'g' => num0!(IsoYearMod100),
267 'j' => num0!(Ordinal),
268 'k' => nums!(Hour),
269 'l' => nums!(Hour12),
270 'm' => num0!(Month),
271 'n' => sp!("\n"),
272 'p' => fix!(UpperAmPm),
273 'r' => recons![num0!(Hour12), lit!(":"), num0!(Minute), lit!(":"),
274 num0!(Second), sp!(" "), fix!(UpperAmPm)],
275 's' => num!(Timestamp),
276 't' => sp!("\t"),
277 'u' => num!(WeekdayFromMon),
278 'v' => recons![nums!(Day), lit!("-"), fix!(ShortMonthName), lit!("-"),
279 num0!(Year)],
280 'w' => num!(NumDaysFromSun),
281 'x' => recons![num0!(Month), lit!("/"), num0!(Day), lit!("/"),
282 num0!(YearMod100)],
283 'y' => num0!(YearMod100),
284 'z' => if is_alternate {
285 internal_fix!(TimezoneOffsetPermissive)
286 } else {
287 fix!(TimezoneOffset)
288 },
289 '+' => fix!(RFC3339),
290 ':' => match next!() {
291 'z' => fix!(TimezoneOffsetColon),
292 _ => Item::Error,
293 },
294 '.' => match next!() {
295 '3' => match next!() {
296 'f' => fix!(Nanosecond3),
297 _ => Item::Error,
298 },
299 '6' => match next!() {
300 'f' => fix!(Nanosecond6),
301 _ => Item::Error,
302 },
303 '9' => match next!() {
304 'f' => fix!(Nanosecond9),
305 _ => Item::Error,
306 },
307 'f' => fix!(Nanosecond),
308 _ => Item::Error,
309 },
310 '3' => match next!() {
311 'f' => internal_fix!(Nanosecond3NoDot),
312 _ => Item::Error,
313 },
314 '6' => match next!() {
315 'f' => internal_fix!(Nanosecond6NoDot),
316 _ => Item::Error,
317 },
318 '9' => match next!() {
319 'f' => internal_fix!(Nanosecond9NoDot),
320 _ => Item::Error,
321 },
322 '%' => lit!("%"),
323 _ => Item::Error, // no such specifier
324 };
325
326 // adjust `item` if we have any padding modifier
327 if let Some(new_pad) = pad_override {
328 match item {
329 Item::Numeric(ref kind, _pad) if self.recons.is_empty() =>
330 Some(Item::Numeric(kind.clone(), new_pad)),
331 _ => Some(Item::Error), // no reconstructed or non-numeric item allowed
332 }
333 } else {
334 Some(item)
335 }
336 },
337
338 // the next item is space
339 Some(c) if c.is_whitespace() => {
340 // `%` is not a whitespace, so `c != '%'` is redundant
341 let nextspec = self.remainder.find(|c: char| !c.is_whitespace())
342 .unwrap_or_else(|| self.remainder.len());
343 assert!(nextspec > 0);
344 let item = sp!(&self.remainder[..nextspec]);
345 self.remainder = &self.remainder[nextspec..];
346 Some(item)
347 },
348
349 // the next item is literal
350 _ => {
351 let nextspec = self.remainder.find(|c: char| c.is_whitespace() || c == '%')
352 .unwrap_or_else(|| self.remainder.len());
353 assert!(nextspec > 0);
354 let item = lit!(&self.remainder[..nextspec]);
355 self.remainder = &self.remainder[nextspec..];
356 Some(item)
357 },
358 }
359 }
360}
361
362#[cfg(test)]
363#[test]
364fn test_strftime_items() {
365 fn parse_and_collect<'a>(s: &'a str) -> Vec<Item<'a>> {
366 // map any error into `[Item::Error]`. useful for easy testing.
367 let items = StrftimeItems::new(s);
368 let items = items.map(|spec| if spec == Item::Error {None} else {Some(spec)});
369 items.collect::<Option<Vec<_>>>().unwrap_or(vec![Item::Error])
370 }
371
372 assert_eq!(parse_and_collect(""), []);
373 assert_eq!(parse_and_collect(" \t\n\r "), [sp!(" \t\n\r ")]);
374 assert_eq!(parse_and_collect("hello?"), [lit!("hello?")]);
375 assert_eq!(parse_and_collect("a b\t\nc"), [lit!("a"), sp!(" "), lit!("b"), sp!("\t\n"),
376 lit!("c")]);
377 assert_eq!(parse_and_collect("100%%"), [lit!("100"), lit!("%")]);
378 assert_eq!(parse_and_collect("100%% ok"), [lit!("100"), lit!("%"), sp!(" "), lit!("ok")]);
379 assert_eq!(parse_and_collect("%%PDF-1.0"), [lit!("%"), lit!("PDF-1.0")]);
380 assert_eq!(parse_and_collect("%Y-%m-%d"), [num0!(Year), lit!("-"), num0!(Month), lit!("-"),
381 num0!(Day)]);
382 assert_eq!(parse_and_collect("[%F]"), parse_and_collect("[%Y-%m-%d]"));
383 assert_eq!(parse_and_collect("%m %d"), [num0!(Month), sp!(" "), num0!(Day)]);
384 assert_eq!(parse_and_collect("%"), [Item::Error]);
385 assert_eq!(parse_and_collect("%%"), [lit!("%")]);
386 assert_eq!(parse_and_collect("%%%"), [Item::Error]);
387 assert_eq!(parse_and_collect("%%%%"), [lit!("%"), lit!("%")]);
388 assert_eq!(parse_and_collect("foo%?"), [Item::Error]);
389 assert_eq!(parse_and_collect("bar%42"), [Item::Error]);
390 assert_eq!(parse_and_collect("quux% +"), [Item::Error]);
391 assert_eq!(parse_and_collect("%.Z"), [Item::Error]);
392 assert_eq!(parse_and_collect("%:Z"), [Item::Error]);
393 assert_eq!(parse_and_collect("%-Z"), [Item::Error]);
394 assert_eq!(parse_and_collect("%0Z"), [Item::Error]);
395 assert_eq!(parse_and_collect("%_Z"), [Item::Error]);
396 assert_eq!(parse_and_collect("%.j"), [Item::Error]);
397 assert_eq!(parse_and_collect("%:j"), [Item::Error]);
398 assert_eq!(parse_and_collect("%-j"), [num!(Ordinal)]);
399 assert_eq!(parse_and_collect("%0j"), [num0!(Ordinal)]);
400 assert_eq!(parse_and_collect("%_j"), [nums!(Ordinal)]);
401 assert_eq!(parse_and_collect("%.e"), [Item::Error]);
402 assert_eq!(parse_and_collect("%:e"), [Item::Error]);
403 assert_eq!(parse_and_collect("%-e"), [num!(Day)]);
404 assert_eq!(parse_and_collect("%0e"), [num0!(Day)]);
405 assert_eq!(parse_and_collect("%_e"), [nums!(Day)]);
406 assert_eq!(parse_and_collect("%z"), [fix!(TimezoneOffset)]);
407 assert_eq!(parse_and_collect("%#z"), [internal_fix!(TimezoneOffsetPermissive)]);
408 assert_eq!(parse_and_collect("%#m"), [Item::Error]);
409}
410
411#[cfg(test)]
412#[test]
413fn test_strftime_docs() {
414 use {FixedOffset, TimeZone, Timelike};
415
416 let dt = FixedOffset::east(34200).ymd(2001, 7, 8).and_hms_nano(0, 34, 59, 1_026_490_708);
417
418 // date specifiers
419 assert_eq!(dt.format("%Y").to_string(), "2001");
420 assert_eq!(dt.format("%C").to_string(), "20");
421 assert_eq!(dt.format("%y").to_string(), "01");
422 assert_eq!(dt.format("%m").to_string(), "07");
423 assert_eq!(dt.format("%b").to_string(), "Jul");
424 assert_eq!(dt.format("%B").to_string(), "July");
425 assert_eq!(dt.format("%h").to_string(), "Jul");
426 assert_eq!(dt.format("%d").to_string(), "08");
427 assert_eq!(dt.format("%e").to_string(), " 8");
428 assert_eq!(dt.format("%e").to_string(), dt.format("%_d").to_string());
429 assert_eq!(dt.format("%a").to_string(), "Sun");
430 assert_eq!(dt.format("%A").to_string(), "Sunday");
431 assert_eq!(dt.format("%w").to_string(), "0");
432 assert_eq!(dt.format("%u").to_string(), "7");
433 assert_eq!(dt.format("%U").to_string(), "28");
434 assert_eq!(dt.format("%W").to_string(), "27");
435 assert_eq!(dt.format("%G").to_string(), "2001");
436 assert_eq!(dt.format("%g").to_string(), "01");
437 assert_eq!(dt.format("%V").to_string(), "27");
438 assert_eq!(dt.format("%j").to_string(), "189");
439 assert_eq!(dt.format("%D").to_string(), "07/08/01");
440 assert_eq!(dt.format("%x").to_string(), "07/08/01");
441 assert_eq!(dt.format("%F").to_string(), "2001-07-08");
442 assert_eq!(dt.format("%v").to_string(), " 8-Jul-2001");
443
444 // time specifiers
445 assert_eq!(dt.format("%H").to_string(), "00");
446 assert_eq!(dt.format("%k").to_string(), " 0");
447 assert_eq!(dt.format("%k").to_string(), dt.format("%_H").to_string());
448 assert_eq!(dt.format("%I").to_string(), "12");
449 assert_eq!(dt.format("%l").to_string(), "12");
450 assert_eq!(dt.format("%l").to_string(), dt.format("%_I").to_string());
451 assert_eq!(dt.format("%P").to_string(), "am");
452 assert_eq!(dt.format("%p").to_string(), "AM");
453 assert_eq!(dt.format("%M").to_string(), "34");
454 assert_eq!(dt.format("%S").to_string(), "60");
455 assert_eq!(dt.format("%f").to_string(), "026490708");
456 assert_eq!(dt.format("%.f").to_string(), ".026490708");
457 assert_eq!(dt.with_nanosecond(1_026_490_000).unwrap().format("%.f").to_string(),
458 ".026490");
459 assert_eq!(dt.format("%.3f").to_string(), ".026");
460 assert_eq!(dt.format("%.6f").to_string(), ".026490");
461 assert_eq!(dt.format("%.9f").to_string(), ".026490708");
462 assert_eq!(dt.format("%3f").to_string(), "026");
463 assert_eq!(dt.format("%6f").to_string(), "026490");
464 assert_eq!(dt.format("%9f").to_string(), "026490708");
465 assert_eq!(dt.format("%R").to_string(), "00:34");
466 assert_eq!(dt.format("%T").to_string(), "00:34:60");
467 assert_eq!(dt.format("%X").to_string(), "00:34:60");
468 assert_eq!(dt.format("%r").to_string(), "12:34:60 AM");
469
470 // time zone specifiers
471 //assert_eq!(dt.format("%Z").to_string(), "ACST");
472 assert_eq!(dt.format("%z").to_string(), "+0930");
473 assert_eq!(dt.format("%:z").to_string(), "+09:30");
474
475 // date & time specifiers
476 assert_eq!(dt.format("%c").to_string(), "Sun Jul 8 00:34:60 2001");
477 assert_eq!(dt.format("%+").to_string(), "2001-07-08T00:34:60.026490708+09:30");
478 assert_eq!(dt.with_nanosecond(1_026_490_000).unwrap().format("%+").to_string(),
479 "2001-07-08T00:34:60.026490+09:30");
480 assert_eq!(dt.format("%s").to_string(), "994518299");
481
482 // special specifiers
483 assert_eq!(dt.format("%t").to_string(), "\t");
484 assert_eq!(dt.format("%n").to_string(), "\n");
485 assert_eq!(dt.format("%%").to_string(), "%");
486}