1 | use crate::{ |
2 | error::{err, ErrorContext}, |
3 | fmt::{ |
4 | strtime::{ |
5 | month_name_abbrev, month_name_full, weekday_name_abbrev, |
6 | weekday_name_full, BrokenDownTime, Extension, Flag, |
7 | }, |
8 | util::{DecimalFormatter, FractionalFormatter}, |
9 | Write, WriteExt, |
10 | }, |
11 | tz::Offset, |
12 | util::{escape, t::C, utf8}, |
13 | Error, |
14 | }; |
15 | |
16 | pub(super) struct Formatter<'f, 't, 'w, W> { |
17 | pub(super) fmt: &'f [u8], |
18 | pub(super) tm: &'t BrokenDownTime, |
19 | pub(super) wtr: &'w mut W, |
20 | } |
21 | |
22 | impl<'f, 't, 'w, W: Write> Formatter<'f, 't, 'w, W> { |
23 | pub(super) fn format(&mut self) -> Result<(), Error> { |
24 | while !self.fmt.is_empty() { |
25 | if self.f() != b'%' { |
26 | if self.f().is_ascii() { |
27 | self.wtr.write_char(char::from(self.f()))?; |
28 | self.bump_fmt(); |
29 | } else { |
30 | let ch = self.utf8_decode_and_bump()?; |
31 | self.wtr.write_char(ch)?; |
32 | } |
33 | continue; |
34 | } |
35 | if !self.bump_fmt() { |
36 | return Err(err!( |
37 | "invalid format string, expected byte after '%', \ |
38 | but found end of format string" , |
39 | )); |
40 | } |
41 | // Parse extensions like padding/case options and padding width. |
42 | let ext = self.parse_extension()?; |
43 | match self.f() { |
44 | b'%' => self.wtr.write_str("%" ).context("%% failed" )?, |
45 | b'A' => self.fmt_weekday_full(ext).context("%A failed" )?, |
46 | b'a' => self.fmt_weekday_abbrev(ext).context("%a failed" )?, |
47 | b'B' => self.fmt_month_full(ext).context("%B failed" )?, |
48 | b'b' => self.fmt_month_abbrev(ext).context("%b failed" )?, |
49 | b'C' => self.fmt_century(ext).context("%C failed" )?, |
50 | b'D' => self.fmt_american_date(ext).context("%D failed" )?, |
51 | b'd' => self.fmt_day_zero(ext).context("%d failed" )?, |
52 | b'e' => self.fmt_day_space(ext).context("%e failed" )?, |
53 | b'F' => self.fmt_iso_date(ext).context("%F failed" )?, |
54 | b'f' => self.fmt_fractional(ext).context("%f failed" )?, |
55 | b'G' => self.fmt_iso_week_year(ext).context("%G failed" )?, |
56 | b'g' => self.fmt_iso_week_year2(ext).context("%g failed" )?, |
57 | b'H' => self.fmt_hour24_zero(ext).context("%H failed" )?, |
58 | b'h' => self.fmt_month_abbrev(ext).context("%b failed" )?, |
59 | b'I' => self.fmt_hour12_zero(ext).context("%H failed" )?, |
60 | b'j' => self.fmt_day_of_year(ext).context("%j failed" )?, |
61 | b'k' => self.fmt_hour24_space(ext).context("%k failed" )?, |
62 | b'l' => self.fmt_hour12_space(ext).context("%l failed" )?, |
63 | b'M' => self.fmt_minute(ext).context("%M failed" )?, |
64 | b'm' => self.fmt_month(ext).context("%m failed" )?, |
65 | b'n' => self.fmt_literal(" \n" ).context("%n failed" )?, |
66 | b'P' => self.fmt_ampm_lower(ext).context("%P failed" )?, |
67 | b'p' => self.fmt_ampm_upper(ext).context("%p failed" )?, |
68 | b'Q' => self.fmt_iana_nocolon().context("%Q failed" )?, |
69 | b'R' => self.fmt_clock_nosecs(ext).context("%R failed" )?, |
70 | b'S' => self.fmt_second(ext).context("%S failed" )?, |
71 | b's' => self.fmt_timestamp(ext).context("%s failed" )?, |
72 | b'T' => self.fmt_clock_secs(ext).context("%T failed" )?, |
73 | b't' => self.fmt_literal(" \t" ).context("%t failed" )?, |
74 | b'U' => self.fmt_week_sun(ext).context("%U failed" )?, |
75 | b'u' => self.fmt_weekday_mon(ext).context("%u failed" )?, |
76 | b'V' => self.fmt_week_iso(ext).context("%V failed" )?, |
77 | b'W' => self.fmt_week_mon(ext).context("%W failed" )?, |
78 | b'w' => self.fmt_weekday_sun(ext).context("%w failed" )?, |
79 | b'Y' => self.fmt_year(ext).context("%Y failed" )?, |
80 | b'y' => self.fmt_year2(ext).context("%y failed" )?, |
81 | b'Z' => self.fmt_tzabbrev(ext).context("%Z failed" )?, |
82 | b'z' => self.fmt_offset_nocolon().context("%z failed" )?, |
83 | b':' => { |
84 | if !self.bump_fmt() { |
85 | return Err(err!( |
86 | "invalid format string, expected directive \ |
87 | after '%:'" , |
88 | )); |
89 | } |
90 | match self.f() { |
91 | b'Q' => self.fmt_iana_colon().context("%:Q failed" )?, |
92 | b'z' => { |
93 | self.fmt_offset_colon().context("%:z failed" )? |
94 | } |
95 | unk => { |
96 | return Err(err!( |
97 | "found unrecognized directive % {unk} \ |
98 | following %:" , |
99 | unk = escape::Byte(unk), |
100 | )); |
101 | } |
102 | } |
103 | } |
104 | b'.' => { |
105 | if !self.bump_fmt() { |
106 | return Err(err!( |
107 | "invalid format string, expected directive \ |
108 | after '%.'" , |
109 | )); |
110 | } |
111 | // Parse precision settings after the `.`, effectively |
112 | // overriding any digits that came before it. |
113 | let ext = Extension { width: self.parse_width()?, ..ext }; |
114 | match self.f() { |
115 | b'f' => self |
116 | .fmt_dot_fractional(ext) |
117 | .context("%.f failed" )?, |
118 | unk => { |
119 | return Err(err!( |
120 | "found unrecognized directive % {unk} \ |
121 | following %." , |
122 | unk = escape::Byte(unk), |
123 | )); |
124 | } |
125 | } |
126 | } |
127 | unk => { |
128 | return Err(err!( |
129 | "found unrecognized specifier directive % {unk}" , |
130 | unk = escape::Byte(unk), |
131 | )); |
132 | } |
133 | } |
134 | self.bump_fmt(); |
135 | } |
136 | Ok(()) |
137 | } |
138 | |
139 | /// Returns the byte at the current position of the format string. |
140 | /// |
141 | /// # Panics |
142 | /// |
143 | /// This panics when the entire format string has been consumed. |
144 | fn f(&self) -> u8 { |
145 | self.fmt[0] |
146 | } |
147 | |
148 | /// Bumps the position of the format string. |
149 | /// |
150 | /// This returns true in precisely the cases where `self.f()` will not |
151 | /// panic. i.e., When the end of the format string hasn't been reached yet. |
152 | fn bump_fmt(&mut self) -> bool { |
153 | self.fmt = &self.fmt[1..]; |
154 | !self.fmt.is_empty() |
155 | } |
156 | |
157 | /// Decodes a Unicode scalar value from the beginning of `fmt` and advances |
158 | /// the parser accordingly. |
159 | /// |
160 | /// If a Unicode scalar value could not be decoded, then an error is |
161 | /// returned. |
162 | /// |
163 | /// It would be nice to just pass through bytes as-is instead of doing |
164 | /// actual UTF-8 decoding, but since the `Write` trait only represents |
165 | /// Unicode-accepting buffers, we need to actually do decoding here. |
166 | /// |
167 | /// # Panics |
168 | /// |
169 | /// When `self.fmt` is empty. i.e., Only call this when you know there is |
170 | /// some remaining bytes to parse. |
171 | #[inline (never)] |
172 | fn utf8_decode_and_bump(&mut self) -> Result<char, Error> { |
173 | match utf8::decode(self.fmt).expect("non-empty fmt" ) { |
174 | Ok(ch) => { |
175 | self.fmt = &self.fmt[ch.len_utf8()..]; |
176 | return Ok(ch); |
177 | } |
178 | Err(invalid) => Err(err!( |
179 | "found invalid UTF-8 byte {byte:?} in format \ |
180 | string (format strings must be valid UTF-8)" , |
181 | byte = escape::Byte(invalid), |
182 | )), |
183 | } |
184 | } |
185 | |
186 | /// Parses optional extensions before a specifier directive. That is, right |
187 | /// after the `%`. If any extensions are parsed, the parser is bumped |
188 | /// to the next byte. (If no next byte exists, then an error is returned.) |
189 | fn parse_extension(&mut self) -> Result<Extension, Error> { |
190 | let flag = self.parse_flag()?; |
191 | let width = self.parse_width()?; |
192 | Ok(Extension { flag, width }) |
193 | } |
194 | |
195 | /// Parses an optional flag. And if one is parsed, the parser is bumped |
196 | /// to the next byte. (If no next byte exists, then an error is returned.) |
197 | fn parse_flag(&mut self) -> Result<Option<Flag>, Error> { |
198 | let (flag, fmt) = Extension::parse_flag(self.fmt)?; |
199 | self.fmt = fmt; |
200 | Ok(flag) |
201 | } |
202 | |
203 | /// Parses an optional width that comes after a (possibly absent) flag and |
204 | /// before the specifier directive itself. And if a width is parsed, the |
205 | /// parser is bumped to the next byte. (If no next byte exists, then an |
206 | /// error is returned.) |
207 | /// |
208 | /// Note that this is also used to parse precision settings for `%f` and |
209 | /// `%.f`. In the former case, the width is just re-interpreted as a |
210 | /// precision setting. In the latter case, something like `%5.9f` is |
211 | /// technically valid, but the `5` is ignored. |
212 | fn parse_width(&mut self) -> Result<Option<u8>, Error> { |
213 | let (width, fmt) = Extension::parse_width(self.fmt)?; |
214 | self.fmt = fmt; |
215 | Ok(width) |
216 | } |
217 | |
218 | // These are the formatting functions. They are pretty much responsible |
219 | // for getting what they need for the broken down time and reporting a |
220 | // decent failure mode if what they need couldn't be found. And then, |
221 | // of course, doing the actual formatting. |
222 | |
223 | /// %P |
224 | fn fmt_ampm_lower(&mut self, ext: Extension) -> Result<(), Error> { |
225 | let hour = self |
226 | .tm |
227 | .hour |
228 | .ok_or_else(|| err!("requires time to format AM/PM" ))? |
229 | .get(); |
230 | ext.write_str( |
231 | Case::AsIs, |
232 | if hour < 12 { "am" } else { "pm" }, |
233 | self.wtr, |
234 | ) |
235 | } |
236 | |
237 | /// %p |
238 | fn fmt_ampm_upper(&mut self, ext: Extension) -> Result<(), Error> { |
239 | let hour = self |
240 | .tm |
241 | .hour |
242 | .ok_or_else(|| err!("requires time to format AM/PM" ))? |
243 | .get(); |
244 | ext.write_str( |
245 | Case::Upper, |
246 | if hour < 12 { "AM" } else { "PM" }, |
247 | self.wtr, |
248 | ) |
249 | } |
250 | |
251 | /// %D |
252 | fn fmt_american_date(&mut self, ext: Extension) -> Result<(), Error> { |
253 | self.fmt_month(ext)?; |
254 | self.wtr.write_char('/' )?; |
255 | self.fmt_day_zero(ext)?; |
256 | self.wtr.write_char('/' )?; |
257 | self.fmt_year2(ext)?; |
258 | Ok(()) |
259 | } |
260 | |
261 | /// %R |
262 | fn fmt_clock_nosecs(&mut self, ext: Extension) -> Result<(), Error> { |
263 | self.fmt_hour24_zero(ext)?; |
264 | self.wtr.write_char(':' )?; |
265 | self.fmt_minute(ext)?; |
266 | Ok(()) |
267 | } |
268 | |
269 | /// %T |
270 | fn fmt_clock_secs(&mut self, ext: Extension) -> Result<(), Error> { |
271 | self.fmt_hour24_zero(ext)?; |
272 | self.wtr.write_char(':' )?; |
273 | self.fmt_minute(ext)?; |
274 | self.wtr.write_char(':' )?; |
275 | self.fmt_second(ext)?; |
276 | Ok(()) |
277 | } |
278 | |
279 | /// %d |
280 | fn fmt_day_zero(&mut self, ext: Extension) -> Result<(), Error> { |
281 | let day = self |
282 | .tm |
283 | .day |
284 | .or_else(|| self.tm.to_date().ok().map(|d| d.day_ranged())) |
285 | .ok_or_else(|| err!("requires date to format day" ))? |
286 | .get(); |
287 | ext.write_int(b'0' , Some(2), day, self.wtr) |
288 | } |
289 | |
290 | /// %e |
291 | fn fmt_day_space(&mut self, ext: Extension) -> Result<(), Error> { |
292 | let day = self |
293 | .tm |
294 | .day |
295 | .or_else(|| self.tm.to_date().ok().map(|d| d.day_ranged())) |
296 | .ok_or_else(|| err!("requires date to format day" ))? |
297 | .get(); |
298 | ext.write_int(b' ' , Some(2), day, self.wtr) |
299 | } |
300 | |
301 | /// %I |
302 | fn fmt_hour12_zero(&mut self, ext: Extension) -> Result<(), Error> { |
303 | let mut hour = self |
304 | .tm |
305 | .hour |
306 | .ok_or_else(|| err!("requires time to format hour" ))? |
307 | .get(); |
308 | if hour == 0 { |
309 | hour = 12; |
310 | } else if hour > 12 { |
311 | hour -= 12; |
312 | } |
313 | ext.write_int(b'0' , Some(2), hour, self.wtr) |
314 | } |
315 | |
316 | /// %H |
317 | fn fmt_hour24_zero(&mut self, ext: Extension) -> Result<(), Error> { |
318 | let hour = self |
319 | .tm |
320 | .hour |
321 | .ok_or_else(|| err!("requires time to format hour" ))? |
322 | .get(); |
323 | ext.write_int(b'0' , Some(2), hour, self.wtr) |
324 | } |
325 | |
326 | /// %l |
327 | fn fmt_hour12_space(&mut self, ext: Extension) -> Result<(), Error> { |
328 | let mut hour = self |
329 | .tm |
330 | .hour |
331 | .ok_or_else(|| err!("requires time to format hour" ))? |
332 | .get(); |
333 | if hour == 0 { |
334 | hour = 12; |
335 | } else if hour > 12 { |
336 | hour -= 12; |
337 | } |
338 | ext.write_int(b' ' , Some(2), hour, self.wtr) |
339 | } |
340 | |
341 | /// %k |
342 | fn fmt_hour24_space(&mut self, ext: Extension) -> Result<(), Error> { |
343 | let hour = self |
344 | .tm |
345 | .hour |
346 | .ok_or_else(|| err!("requires time to format hour" ))? |
347 | .get(); |
348 | ext.write_int(b' ' , Some(2), hour, self.wtr) |
349 | } |
350 | |
351 | /// %F |
352 | fn fmt_iso_date(&mut self, ext: Extension) -> Result<(), Error> { |
353 | self.fmt_year(ext)?; |
354 | self.wtr.write_char('-' )?; |
355 | self.fmt_month(ext)?; |
356 | self.wtr.write_char('-' )?; |
357 | self.fmt_day_zero(ext)?; |
358 | Ok(()) |
359 | } |
360 | |
361 | /// %M |
362 | fn fmt_minute(&mut self, ext: Extension) -> Result<(), Error> { |
363 | let minute = self |
364 | .tm |
365 | .minute |
366 | .ok_or_else(|| err!("requires time to format minute" ))? |
367 | .get(); |
368 | ext.write_int(b'0' , Some(2), minute, self.wtr) |
369 | } |
370 | |
371 | /// %m |
372 | fn fmt_month(&mut self, ext: Extension) -> Result<(), Error> { |
373 | let month = self |
374 | .tm |
375 | .month |
376 | .or_else(|| self.tm.to_date().ok().map(|d| d.month_ranged())) |
377 | .ok_or_else(|| err!("requires date to format month" ))? |
378 | .get(); |
379 | ext.write_int(b'0' , Some(2), month, self.wtr) |
380 | } |
381 | |
382 | /// %B |
383 | fn fmt_month_full(&mut self, ext: Extension) -> Result<(), Error> { |
384 | let month = self |
385 | .tm |
386 | .month |
387 | .or_else(|| self.tm.to_date().ok().map(|d| d.month_ranged())) |
388 | .ok_or_else(|| err!("requires date to format month" ))?; |
389 | ext.write_str(Case::AsIs, month_name_full(month), self.wtr) |
390 | } |
391 | |
392 | /// %b, %h |
393 | fn fmt_month_abbrev(&mut self, ext: Extension) -> Result<(), Error> { |
394 | let month = self |
395 | .tm |
396 | .month |
397 | .or_else(|| self.tm.to_date().ok().map(|d| d.month_ranged())) |
398 | .ok_or_else(|| err!("requires date to format month" ))?; |
399 | ext.write_str(Case::AsIs, month_name_abbrev(month), self.wtr) |
400 | } |
401 | |
402 | /// %Q |
403 | fn fmt_iana_nocolon(&mut self) -> Result<(), Error> { |
404 | let Some(iana) = self.tm.iana_time_zone() else { |
405 | let offset = self.tm.offset.ok_or_else(|| { |
406 | err!( |
407 | "requires IANA time zone identifier or time \ |
408 | zone offset, but none were present" |
409 | ) |
410 | })?; |
411 | return write_offset(offset, false, &mut self.wtr); |
412 | }; |
413 | self.wtr.write_str(iana)?; |
414 | Ok(()) |
415 | } |
416 | |
417 | /// %:Q |
418 | fn fmt_iana_colon(&mut self) -> Result<(), Error> { |
419 | let Some(iana) = self.tm.iana_time_zone() else { |
420 | let offset = self.tm.offset.ok_or_else(|| { |
421 | err!( |
422 | "requires IANA time zone identifier or time \ |
423 | zone offset, but none were present" |
424 | ) |
425 | })?; |
426 | return write_offset(offset, true, &mut self.wtr); |
427 | }; |
428 | self.wtr.write_str(iana)?; |
429 | Ok(()) |
430 | } |
431 | |
432 | /// %z |
433 | fn fmt_offset_nocolon(&mut self) -> Result<(), Error> { |
434 | let offset = self.tm.offset.ok_or_else(|| { |
435 | err!("requires offset to format time zone offset" ) |
436 | })?; |
437 | write_offset(offset, false, self.wtr) |
438 | } |
439 | |
440 | /// %:z |
441 | fn fmt_offset_colon(&mut self) -> Result<(), Error> { |
442 | let offset = self.tm.offset.ok_or_else(|| { |
443 | err!("requires offset to format time zone offset" ) |
444 | })?; |
445 | write_offset(offset, true, self.wtr) |
446 | } |
447 | |
448 | /// %S |
449 | fn fmt_second(&mut self, ext: Extension) -> Result<(), Error> { |
450 | let second = self |
451 | .tm |
452 | .second |
453 | .ok_or_else(|| err!("requires time to format second" ))? |
454 | .get(); |
455 | ext.write_int(b'0' , Some(2), second, self.wtr) |
456 | } |
457 | |
458 | /// %s |
459 | fn fmt_timestamp(&mut self, ext: Extension) -> Result<(), Error> { |
460 | let timestamp = self.tm.to_timestamp().map_err(|_| { |
461 | err!( |
462 | "requires instant (a date, time and offset) \ |
463 | to format Unix timestamp" , |
464 | ) |
465 | })?; |
466 | ext.write_int(b' ' , None, timestamp.as_second(), self.wtr) |
467 | } |
468 | |
469 | /// %f |
470 | fn fmt_fractional(&mut self, ext: Extension) -> Result<(), Error> { |
471 | let subsec = self.tm.subsec.ok_or_else(|| { |
472 | err!("requires time to format subsecond nanoseconds" ) |
473 | })?; |
474 | // For %f, we always want to emit at least one digit. The only way we |
475 | // wouldn't is if our fractional component is zero. One exception to |
476 | // this is when the width is `0` (which looks like `%00f`), in which |
477 | // case, we emit an error. We could allow it to emit an empty string, |
478 | // but this seems very odd. And an empty string cannot be parsed by |
479 | // `%f`. |
480 | if ext.width == Some(0) { |
481 | return Err(err!("zero precision with %f is not allowed" )); |
482 | } |
483 | if subsec == C(0) && ext.width.is_none() { |
484 | self.wtr.write_str("0" )?; |
485 | return Ok(()); |
486 | } |
487 | ext.write_fractional_seconds(subsec, self.wtr)?; |
488 | Ok(()) |
489 | } |
490 | |
491 | /// %.f |
492 | fn fmt_dot_fractional(&mut self, ext: Extension) -> Result<(), Error> { |
493 | let Some(subsec) = self.tm.subsec else { return Ok(()) }; |
494 | if subsec == C(0) && ext.width.is_none() || ext.width == Some(0) { |
495 | return Ok(()); |
496 | } |
497 | ext.write_str(Case::AsIs, "." , self.wtr)?; |
498 | ext.write_fractional_seconds(subsec, self.wtr)?; |
499 | Ok(()) |
500 | } |
501 | |
502 | /// %Z |
503 | fn fmt_tzabbrev(&mut self, ext: Extension) -> Result<(), Error> { |
504 | let tzabbrev = self.tm.tzabbrev.as_ref().ok_or_else(|| { |
505 | err!("requires time zone abbreviation in broken down time" ) |
506 | })?; |
507 | ext.write_str(Case::Upper, tzabbrev.as_str(), self.wtr) |
508 | } |
509 | |
510 | /// %A |
511 | fn fmt_weekday_full(&mut self, ext: Extension) -> Result<(), Error> { |
512 | let weekday = self |
513 | .tm |
514 | .weekday |
515 | .or_else(|| self.tm.to_date().ok().map(|d| d.weekday())) |
516 | .ok_or_else(|| err!("requires date to format weekday" ))?; |
517 | ext.write_str(Case::AsIs, weekday_name_full(weekday), self.wtr) |
518 | } |
519 | |
520 | /// %a |
521 | fn fmt_weekday_abbrev(&mut self, ext: Extension) -> Result<(), Error> { |
522 | let weekday = self |
523 | .tm |
524 | .weekday |
525 | .or_else(|| self.tm.to_date().ok().map(|d| d.weekday())) |
526 | .ok_or_else(|| err!("requires date to format weekday" ))?; |
527 | ext.write_str(Case::AsIs, weekday_name_abbrev(weekday), self.wtr) |
528 | } |
529 | |
530 | /// %u |
531 | fn fmt_weekday_mon(&mut self, ext: Extension) -> Result<(), Error> { |
532 | let weekday = self |
533 | .tm |
534 | .weekday |
535 | .or_else(|| self.tm.to_date().ok().map(|d| d.weekday())) |
536 | .ok_or_else(|| err!("requires date to format weekday number" ))?; |
537 | ext.write_int(b' ' , None, weekday.to_monday_one_offset(), self.wtr) |
538 | } |
539 | |
540 | /// %w |
541 | fn fmt_weekday_sun(&mut self, ext: Extension) -> Result<(), Error> { |
542 | let weekday = self |
543 | .tm |
544 | .weekday |
545 | .or_else(|| self.tm.to_date().ok().map(|d| d.weekday())) |
546 | .ok_or_else(|| err!("requires date to format weekday number" ))?; |
547 | ext.write_int(b' ' , None, weekday.to_sunday_zero_offset(), self.wtr) |
548 | } |
549 | |
550 | /// %U |
551 | fn fmt_week_sun(&mut self, ext: Extension) -> Result<(), Error> { |
552 | // Short circuit if the week number was explicitly set. |
553 | if let Some(weeknum) = self.tm.week_sun { |
554 | return ext.write_int(b'0' , Some(2), weeknum, self.wtr); |
555 | } |
556 | let day = self |
557 | .tm |
558 | .day_of_year |
559 | .map(|day| day.get()) |
560 | .or_else(|| self.tm.to_date().ok().map(|d| d.day_of_year())) |
561 | .ok_or_else(|| { |
562 | err!("requires date to format Sunday-based week number" ) |
563 | })?; |
564 | let weekday = self |
565 | .tm |
566 | .weekday |
567 | .or_else(|| self.tm.to_date().ok().map(|d| d.weekday())) |
568 | .ok_or_else(|| { |
569 | err!("requires date to format Sunday-based week number" ) |
570 | })? |
571 | .to_sunday_zero_offset(); |
572 | // Example: 2025-01-05 is the first Sunday in 2025, and thus the start |
573 | // of week 1. This means that 2025-01-04 (Saturday) is in week 0. |
574 | // |
575 | // So for 2025-01-05, day=5 and weekday=0. Thus we get 11/7 = 1. |
576 | // For 2025-01-04, day=4 and weekday=6. Thus we get 4/7 = 0. |
577 | let weeknum = (day + 6 - i16::from(weekday)) / 7; |
578 | ext.write_int(b'0' , Some(2), weeknum, self.wtr) |
579 | } |
580 | |
581 | /// %V |
582 | fn fmt_week_iso(&mut self, ext: Extension) -> Result<(), Error> { |
583 | let weeknum = self |
584 | .tm |
585 | .iso_week |
586 | .or_else(|| { |
587 | self.tm.to_date().ok().map(|d| d.iso_week_date().week_ranged()) |
588 | }) |
589 | .ok_or_else(|| { |
590 | err!("requires date to format ISO 8601 week number" ) |
591 | })?; |
592 | ext.write_int(b'0' , Some(2), weeknum, self.wtr) |
593 | } |
594 | |
595 | /// %W |
596 | fn fmt_week_mon(&mut self, ext: Extension) -> Result<(), Error> { |
597 | // Short circuit if the week number was explicitly set. |
598 | if let Some(weeknum) = self.tm.week_mon { |
599 | return ext.write_int(b'0' , Some(2), weeknum, self.wtr); |
600 | } |
601 | let day = self |
602 | .tm |
603 | .day_of_year |
604 | .map(|day| day.get()) |
605 | .or_else(|| self.tm.to_date().ok().map(|d| d.day_of_year())) |
606 | .ok_or_else(|| { |
607 | err!("requires date to format Monday-based week number" ) |
608 | })?; |
609 | let weekday = self |
610 | .tm |
611 | .weekday |
612 | .or_else(|| self.tm.to_date().ok().map(|d| d.weekday())) |
613 | .ok_or_else(|| { |
614 | err!("requires date to format Monday-based week number" ) |
615 | })? |
616 | .to_sunday_zero_offset(); |
617 | // Example: 2025-01-06 is the first Monday in 2025, and thus the start |
618 | // of week 1. This means that 2025-01-05 (Sunday) is in week 0. |
619 | // |
620 | // So for 2025-01-06, day=6 and weekday=1. Thus we get 12/7 = 1. |
621 | // For 2025-01-05, day=5 and weekday=7. Thus we get 5/7 = 0. |
622 | let weeknum = (day + 6 - ((i16::from(weekday) + 6) % 7)) / 7; |
623 | ext.write_int(b'0' , Some(2), weeknum, self.wtr) |
624 | } |
625 | |
626 | /// %Y |
627 | fn fmt_year(&mut self, ext: Extension) -> Result<(), Error> { |
628 | let year = self |
629 | .tm |
630 | .year |
631 | .or_else(|| self.tm.to_date().ok().map(|d| d.year_ranged())) |
632 | .ok_or_else(|| err!("requires date to format year" ))? |
633 | .get(); |
634 | ext.write_int(b'0' , Some(4), year, self.wtr) |
635 | } |
636 | |
637 | /// %y |
638 | fn fmt_year2(&mut self, ext: Extension) -> Result<(), Error> { |
639 | let year = self |
640 | .tm |
641 | .year |
642 | .or_else(|| self.tm.to_date().ok().map(|d| d.year_ranged())) |
643 | .ok_or_else(|| err!("requires date to format year (2-digit)" ))? |
644 | .get(); |
645 | if !(1969 <= year && year <= 2068) { |
646 | return Err(err!( |
647 | "formatting a 2-digit year requires that it be in \ |
648 | the inclusive range 1969 to 2068, but got {year}" , |
649 | )); |
650 | } |
651 | let year = year % 100; |
652 | ext.write_int(b'0' , Some(2), year, self.wtr) |
653 | } |
654 | |
655 | /// %C |
656 | fn fmt_century(&mut self, ext: Extension) -> Result<(), Error> { |
657 | let year = self |
658 | .tm |
659 | .year |
660 | .or_else(|| self.tm.to_date().ok().map(|d| d.year_ranged())) |
661 | .ok_or_else(|| err!("requires date to format century (2-digit)" ))? |
662 | .get(); |
663 | let century = year / 100; |
664 | ext.write_int(b' ' , None, century, self.wtr) |
665 | } |
666 | |
667 | /// %G |
668 | fn fmt_iso_week_year(&mut self, ext: Extension) -> Result<(), Error> { |
669 | let year = self |
670 | .tm |
671 | .iso_week_year |
672 | .or_else(|| { |
673 | self.tm.to_date().ok().map(|d| d.iso_week_date().year_ranged()) |
674 | }) |
675 | .ok_or_else(|| { |
676 | err!("requires date to format ISO 8601 week-based year" ) |
677 | })? |
678 | .get(); |
679 | ext.write_int(b'0' , Some(4), year, self.wtr) |
680 | } |
681 | |
682 | /// %g |
683 | fn fmt_iso_week_year2(&mut self, ext: Extension) -> Result<(), Error> { |
684 | let year = self |
685 | .tm |
686 | .iso_week_year |
687 | .or_else(|| { |
688 | self.tm.to_date().ok().map(|d| d.iso_week_date().year_ranged()) |
689 | }) |
690 | .ok_or_else(|| { |
691 | err!( |
692 | "requires date to format \ |
693 | ISO 8601 week-based year (2-digit)" |
694 | ) |
695 | })? |
696 | .get(); |
697 | if !(1969 <= year && year <= 2068) { |
698 | return Err(err!( |
699 | "formatting a 2-digit ISO 8601 week-based year \ |
700 | requires that it be in \ |
701 | the inclusive range 1969 to 2068, but got {year}" , |
702 | )); |
703 | } |
704 | let year = year % 100; |
705 | ext.write_int(b'0' , Some(2), year, self.wtr) |
706 | } |
707 | |
708 | /// %j |
709 | fn fmt_day_of_year(&mut self, ext: Extension) -> Result<(), Error> { |
710 | let day = self |
711 | .tm |
712 | .day_of_year |
713 | .map(|day| day.get()) |
714 | .or_else(|| self.tm.to_date().ok().map(|d| d.day_of_year())) |
715 | .ok_or_else(|| err!("requires date to format day of year" ))?; |
716 | ext.write_int(b'0' , Some(3), day, self.wtr) |
717 | } |
718 | |
719 | /// %n, %t |
720 | fn fmt_literal(&mut self, literal: &str) -> Result<(), Error> { |
721 | self.wtr.write_str(literal) |
722 | } |
723 | } |
724 | |
725 | /// Writes the given time zone offset to the writer. |
726 | /// |
727 | /// When `colon` is true, the hour, minute and optional second components are |
728 | /// delimited by a colon. Otherwise, no delimiter is used. |
729 | fn write_offset<W: Write>( |
730 | offset: Offset, |
731 | colon: bool, |
732 | wtr: &mut W, |
733 | ) -> Result<(), Error> { |
734 | static FMT_TWO: DecimalFormatter = DecimalFormatter::new().padding(digits:2); |
735 | |
736 | let hours: i8 = offset.part_hours_ranged().abs().get(); |
737 | let minutes: i8 = offset.part_minutes_ranged().abs().get(); |
738 | let seconds: i8 = offset.part_seconds_ranged().abs().get(); |
739 | |
740 | wtr.write_str(string:if offset.is_negative() { "-" } else { "+" })?; |
741 | wtr.write_int(&FMT_TWO, n:hours)?; |
742 | if colon { |
743 | wtr.write_str(string:":" )?; |
744 | } |
745 | wtr.write_int(&FMT_TWO, n:minutes)?; |
746 | if seconds != 0 { |
747 | if colon { |
748 | wtr.write_str(string:":" )?; |
749 | } |
750 | wtr.write_int(&FMT_TWO, n:seconds)?; |
751 | } |
752 | Ok(()) |
753 | } |
754 | |
755 | impl Extension { |
756 | /// Writes the given string using the default case rule provided, unless |
757 | /// an option in this extension config overrides the default case. |
758 | fn write_str<W: Write>( |
759 | self, |
760 | default: Case, |
761 | string: &str, |
762 | wtr: &mut W, |
763 | ) -> Result<(), Error> { |
764 | let case = match self.flag { |
765 | Some(Flag::Uppercase) => Case::Upper, |
766 | Some(Flag::Swapcase) => default.swap(), |
767 | _ => default, |
768 | }; |
769 | match case { |
770 | Case::AsIs => { |
771 | wtr.write_str(string)?; |
772 | } |
773 | Case::Upper => { |
774 | for ch in string.chars() { |
775 | for ch in ch.to_uppercase() { |
776 | wtr.write_char(ch)?; |
777 | } |
778 | } |
779 | } |
780 | Case::Lower => { |
781 | for ch in string.chars() { |
782 | for ch in ch.to_lowercase() { |
783 | wtr.write_char(ch)?; |
784 | } |
785 | } |
786 | } |
787 | } |
788 | Ok(()) |
789 | } |
790 | |
791 | /// Writes the given integer using the given padding width and byte, unless |
792 | /// an option in this extension config overrides a default setting. |
793 | fn write_int<W: Write>( |
794 | self, |
795 | pad_byte: u8, |
796 | pad_width: Option<u8>, |
797 | number: impl Into<i64>, |
798 | wtr: &mut W, |
799 | ) -> Result<(), Error> { |
800 | let number = number.into(); |
801 | let pad_byte = match self.flag { |
802 | Some(Flag::PadZero) => b'0' , |
803 | Some(Flag::PadSpace) => b' ' , |
804 | _ => pad_byte, |
805 | }; |
806 | let pad_width = if matches!(self.flag, Some(Flag::NoPad)) { |
807 | None |
808 | } else { |
809 | self.width.or(pad_width) |
810 | }; |
811 | |
812 | let mut formatter = DecimalFormatter::new().padding_byte(pad_byte); |
813 | if let Some(width) = pad_width { |
814 | formatter = formatter.padding(width); |
815 | } |
816 | wtr.write_int(&formatter, number) |
817 | } |
818 | |
819 | /// Writes the given number of nanoseconds as a fractional component of |
820 | /// a second. This does not include the leading `.`. |
821 | /// |
822 | /// The `width` setting on `Extension` is treated as a precision setting. |
823 | fn write_fractional_seconds<W: Write>( |
824 | self, |
825 | number: impl Into<i64>, |
826 | wtr: &mut W, |
827 | ) -> Result<(), Error> { |
828 | let number = number.into(); |
829 | |
830 | let formatter = FractionalFormatter::new().precision(self.width); |
831 | wtr.write_fraction(&formatter, number) |
832 | } |
833 | } |
834 | |
835 | /// The case to use when printing a string like weekday or TZ abbreviation. |
836 | #[derive (Clone, Copy, Debug)] |
837 | enum Case { |
838 | AsIs, |
839 | Upper, |
840 | Lower, |
841 | } |
842 | |
843 | impl Case { |
844 | /// Swap upper to lowercase, and lower to uppercase. |
845 | fn swap(self) -> Case { |
846 | match self { |
847 | Case::AsIs => Case::AsIs, |
848 | Case::Upper => Case::Lower, |
849 | Case::Lower => Case::Upper, |
850 | } |
851 | } |
852 | } |
853 | |
854 | #[cfg (feature = "alloc" )] |
855 | #[cfg (test)] |
856 | mod tests { |
857 | use crate::{ |
858 | civil::{date, time, Date, DateTime, Time}, |
859 | fmt::strtime::format, |
860 | Timestamp, Zoned, |
861 | }; |
862 | |
863 | #[test ] |
864 | fn ok_format_american_date() { |
865 | let f = |fmt: &str, date: Date| format(fmt, date).unwrap(); |
866 | |
867 | insta::assert_snapshot!(f("%D" , date(2024, 7, 9)), @"07/09/24" ); |
868 | insta::assert_snapshot!(f("%-D" , date(2024, 7, 9)), @"7/9/24" ); |
869 | insta::assert_snapshot!(f("%3D" , date(2024, 7, 9)), @"007/009/024" ); |
870 | insta::assert_snapshot!(f("%03D" , date(2024, 7, 9)), @"007/009/024" ); |
871 | } |
872 | |
873 | #[test ] |
874 | fn ok_format_ampm() { |
875 | let f = |fmt: &str, time: Time| format(fmt, time).unwrap(); |
876 | |
877 | insta::assert_snapshot!(f("%H%P" , time(9, 0, 0, 0)), @"09am" ); |
878 | insta::assert_snapshot!(f("%H%P" , time(11, 0, 0, 0)), @"11am" ); |
879 | insta::assert_snapshot!(f("%H%P" , time(23, 0, 0, 0)), @"23pm" ); |
880 | insta::assert_snapshot!(f("%H%P" , time(0, 0, 0, 0)), @"00am" ); |
881 | |
882 | insta::assert_snapshot!(f("%H%p" , time(9, 0, 0, 0)), @"09AM" ); |
883 | insta::assert_snapshot!(f("%H%p" , time(11, 0, 0, 0)), @"11AM" ); |
884 | insta::assert_snapshot!(f("%H%p" , time(23, 0, 0, 0)), @"23PM" ); |
885 | insta::assert_snapshot!(f("%H%p" , time(0, 0, 0, 0)), @"00AM" ); |
886 | |
887 | insta::assert_snapshot!(f("%H%#p" , time(9, 0, 0, 0)), @"09am" ); |
888 | } |
889 | |
890 | #[test ] |
891 | fn ok_format_clock() { |
892 | let f = |fmt: &str, time: Time| format(fmt, time).unwrap(); |
893 | |
894 | insta::assert_snapshot!(f("%R" , time(23, 59, 8, 0)), @"23:59" ); |
895 | insta::assert_snapshot!(f("%T" , time(23, 59, 8, 0)), @"23:59:08" ); |
896 | } |
897 | |
898 | #[test ] |
899 | fn ok_format_day() { |
900 | let f = |fmt: &str, date: Date| format(fmt, date).unwrap(); |
901 | |
902 | insta::assert_snapshot!(f("%d" , date(2024, 7, 9)), @"09" ); |
903 | insta::assert_snapshot!(f("%0d" , date(2024, 7, 9)), @"09" ); |
904 | insta::assert_snapshot!(f("%-d" , date(2024, 7, 9)), @"9" ); |
905 | insta::assert_snapshot!(f("%_d" , date(2024, 7, 9)), @" 9" ); |
906 | |
907 | insta::assert_snapshot!(f("%e" , date(2024, 7, 9)), @" 9" ); |
908 | insta::assert_snapshot!(f("%0e" , date(2024, 7, 9)), @"09" ); |
909 | insta::assert_snapshot!(f("%-e" , date(2024, 7, 9)), @"9" ); |
910 | insta::assert_snapshot!(f("%_e" , date(2024, 7, 9)), @" 9" ); |
911 | } |
912 | |
913 | #[test ] |
914 | fn ok_format_iso_date() { |
915 | let f = |fmt: &str, date: Date| format(fmt, date).unwrap(); |
916 | |
917 | insta::assert_snapshot!(f("%F" , date(2024, 7, 9)), @"2024-07-09" ); |
918 | insta::assert_snapshot!(f("%-F" , date(2024, 7, 9)), @"2024-7-9" ); |
919 | insta::assert_snapshot!(f("%3F" , date(2024, 7, 9)), @"2024-007-009" ); |
920 | insta::assert_snapshot!(f("%03F" , date(2024, 7, 9)), @"2024-007-009" ); |
921 | } |
922 | |
923 | #[test ] |
924 | fn ok_format_hour() { |
925 | let f = |fmt: &str, time: Time| format(fmt, time).unwrap(); |
926 | |
927 | insta::assert_snapshot!(f("%H" , time(9, 0, 0, 0)), @"09" ); |
928 | insta::assert_snapshot!(f("%H" , time(11, 0, 0, 0)), @"11" ); |
929 | insta::assert_snapshot!(f("%H" , time(23, 0, 0, 0)), @"23" ); |
930 | insta::assert_snapshot!(f("%H" , time(0, 0, 0, 0)), @"00" ); |
931 | |
932 | insta::assert_snapshot!(f("%I" , time(9, 0, 0, 0)), @"09" ); |
933 | insta::assert_snapshot!(f("%I" , time(11, 0, 0, 0)), @"11" ); |
934 | insta::assert_snapshot!(f("%I" , time(23, 0, 0, 0)), @"11" ); |
935 | insta::assert_snapshot!(f("%I" , time(0, 0, 0, 0)), @"12" ); |
936 | |
937 | insta::assert_snapshot!(f("%k" , time(9, 0, 0, 0)), @" 9" ); |
938 | insta::assert_snapshot!(f("%k" , time(11, 0, 0, 0)), @"11" ); |
939 | insta::assert_snapshot!(f("%k" , time(23, 0, 0, 0)), @"23" ); |
940 | insta::assert_snapshot!(f("%k" , time(0, 0, 0, 0)), @" 0" ); |
941 | |
942 | insta::assert_snapshot!(f("%l" , time(9, 0, 0, 0)), @" 9" ); |
943 | insta::assert_snapshot!(f("%l" , time(11, 0, 0, 0)), @"11" ); |
944 | insta::assert_snapshot!(f("%l" , time(23, 0, 0, 0)), @"11" ); |
945 | insta::assert_snapshot!(f("%l" , time(0, 0, 0, 0)), @"12" ); |
946 | } |
947 | |
948 | #[test ] |
949 | fn ok_format_minute() { |
950 | let f = |fmt: &str, time: Time| format(fmt, time).unwrap(); |
951 | |
952 | insta::assert_snapshot!(f("%M" , time(0, 9, 0, 0)), @"09" ); |
953 | insta::assert_snapshot!(f("%M" , time(0, 11, 0, 0)), @"11" ); |
954 | insta::assert_snapshot!(f("%M" , time(0, 23, 0, 0)), @"23" ); |
955 | insta::assert_snapshot!(f("%M" , time(0, 0, 0, 0)), @"00" ); |
956 | } |
957 | |
958 | #[test ] |
959 | fn ok_format_month() { |
960 | let f = |fmt: &str, date: Date| format(fmt, date).unwrap(); |
961 | |
962 | insta::assert_snapshot!(f("%m" , date(2024, 7, 14)), @"07" ); |
963 | insta::assert_snapshot!(f("%m" , date(2024, 12, 14)), @"12" ); |
964 | insta::assert_snapshot!(f("%0m" , date(2024, 7, 14)), @"07" ); |
965 | insta::assert_snapshot!(f("%0m" , date(2024, 12, 14)), @"12" ); |
966 | insta::assert_snapshot!(f("%-m" , date(2024, 7, 14)), @"7" ); |
967 | insta::assert_snapshot!(f("%-m" , date(2024, 12, 14)), @"12" ); |
968 | insta::assert_snapshot!(f("%_m" , date(2024, 7, 14)), @" 7" ); |
969 | insta::assert_snapshot!(f("%_m" , date(2024, 12, 14)), @"12" ); |
970 | } |
971 | |
972 | #[test ] |
973 | fn ok_format_month_name() { |
974 | let f = |fmt: &str, date: Date| format(fmt, date).unwrap(); |
975 | |
976 | insta::assert_snapshot!(f("%B" , date(2024, 7, 14)), @"July" ); |
977 | insta::assert_snapshot!(f("%b" , date(2024, 7, 14)), @"Jul" ); |
978 | insta::assert_snapshot!(f("%h" , date(2024, 7, 14)), @"Jul" ); |
979 | |
980 | insta::assert_snapshot!(f("%#B" , date(2024, 7, 14)), @"July" ); |
981 | insta::assert_snapshot!(f("%^B" , date(2024, 7, 14)), @"JULY" ); |
982 | } |
983 | |
984 | #[test ] |
985 | fn ok_format_offset() { |
986 | if crate::tz::db().is_definitively_empty() { |
987 | return; |
988 | } |
989 | |
990 | let f = |fmt: &str, zdt: &Zoned| format(fmt, zdt).unwrap(); |
991 | |
992 | let zdt = date(2024, 7, 14) |
993 | .at(22, 24, 0, 0) |
994 | .in_tz("America/New_York" ) |
995 | .unwrap(); |
996 | insta::assert_snapshot!(f("%z" , &zdt), @"-0400" ); |
997 | insta::assert_snapshot!(f("%:z" , &zdt), @"-04:00" ); |
998 | |
999 | let zdt = zdt.checked_add(crate::Span::new().months(5)).unwrap(); |
1000 | insta::assert_snapshot!(f("%z" , &zdt), @"-0500" ); |
1001 | insta::assert_snapshot!(f("%:z" , &zdt), @"-05:00" ); |
1002 | } |
1003 | |
1004 | #[test ] |
1005 | fn ok_format_second() { |
1006 | let f = |fmt: &str, time: Time| format(fmt, time).unwrap(); |
1007 | |
1008 | insta::assert_snapshot!(f("%S" , time(0, 0, 9, 0)), @"09" ); |
1009 | insta::assert_snapshot!(f("%S" , time(0, 0, 11, 0)), @"11" ); |
1010 | insta::assert_snapshot!(f("%S" , time(0, 0, 23, 0)), @"23" ); |
1011 | insta::assert_snapshot!(f("%S" , time(0, 0, 0, 0)), @"00" ); |
1012 | } |
1013 | |
1014 | #[test ] |
1015 | fn ok_format_subsec_nanosecond() { |
1016 | let f = |fmt: &str, time: Time| format(fmt, time).unwrap(); |
1017 | let mk = |subsec| time(0, 0, 0, subsec); |
1018 | |
1019 | insta::assert_snapshot!(f("%f" , mk(123_000_000)), @"123" ); |
1020 | insta::assert_snapshot!(f("%f" , mk(0)), @"0" ); |
1021 | insta::assert_snapshot!(f("%3f" , mk(0)), @"000" ); |
1022 | insta::assert_snapshot!(f("%3f" , mk(123_000_000)), @"123" ); |
1023 | insta::assert_snapshot!(f("%6f" , mk(123_000_000)), @"123000" ); |
1024 | insta::assert_snapshot!(f("%9f" , mk(123_000_000)), @"123000000" ); |
1025 | insta::assert_snapshot!(f("%255f" , mk(123_000_000)), @"123000000" ); |
1026 | |
1027 | insta::assert_snapshot!(f("%.f" , mk(123_000_000)), @".123" ); |
1028 | insta::assert_snapshot!(f("%.f" , mk(0)), @"" ); |
1029 | insta::assert_snapshot!(f("%3.f" , mk(0)), @"" ); |
1030 | insta::assert_snapshot!(f("%.3f" , mk(0)), @".000" ); |
1031 | insta::assert_snapshot!(f("%.3f" , mk(123_000_000)), @".123" ); |
1032 | insta::assert_snapshot!(f("%.6f" , mk(123_000_000)), @".123000" ); |
1033 | insta::assert_snapshot!(f("%.9f" , mk(123_000_000)), @".123000000" ); |
1034 | insta::assert_snapshot!(f("%.255f" , mk(123_000_000)), @".123000000" ); |
1035 | |
1036 | insta::assert_snapshot!(f("%3f" , mk(123_456_789)), @"123" ); |
1037 | insta::assert_snapshot!(f("%6f" , mk(123_456_789)), @"123456" ); |
1038 | insta::assert_snapshot!(f("%9f" , mk(123_456_789)), @"123456789" ); |
1039 | |
1040 | insta::assert_snapshot!(f("%.0f" , mk(123_456_789)), @"" ); |
1041 | insta::assert_snapshot!(f("%.3f" , mk(123_456_789)), @".123" ); |
1042 | insta::assert_snapshot!(f("%.6f" , mk(123_456_789)), @".123456" ); |
1043 | insta::assert_snapshot!(f("%.9f" , mk(123_456_789)), @".123456789" ); |
1044 | } |
1045 | |
1046 | #[test ] |
1047 | fn ok_format_tzabbrev() { |
1048 | if crate::tz::db().is_definitively_empty() { |
1049 | return; |
1050 | } |
1051 | |
1052 | let f = |fmt: &str, zdt: &Zoned| format(fmt, zdt).unwrap(); |
1053 | |
1054 | let zdt = date(2024, 7, 14) |
1055 | .at(22, 24, 0, 0) |
1056 | .in_tz("America/New_York" ) |
1057 | .unwrap(); |
1058 | insta::assert_snapshot!(f("%Z" , &zdt), @"EDT" ); |
1059 | insta::assert_snapshot!(f("%^Z" , &zdt), @"EDT" ); |
1060 | insta::assert_snapshot!(f("%#Z" , &zdt), @"edt" ); |
1061 | |
1062 | let zdt = zdt.checked_add(crate::Span::new().months(5)).unwrap(); |
1063 | insta::assert_snapshot!(f("%Z" , &zdt), @"EST" ); |
1064 | } |
1065 | |
1066 | #[test ] |
1067 | fn ok_format_iana() { |
1068 | if crate::tz::db().is_definitively_empty() { |
1069 | return; |
1070 | } |
1071 | |
1072 | let f = |fmt: &str, zdt: &Zoned| format(fmt, zdt).unwrap(); |
1073 | |
1074 | let zdt = date(2024, 7, 14) |
1075 | .at(22, 24, 0, 0) |
1076 | .in_tz("America/New_York" ) |
1077 | .unwrap(); |
1078 | insta::assert_snapshot!(f("%Q" , &zdt), @"America/New_York" ); |
1079 | insta::assert_snapshot!(f("%:Q" , &zdt), @"America/New_York" ); |
1080 | |
1081 | let zdt = date(2024, 7, 14) |
1082 | .at(22, 24, 0, 0) |
1083 | .to_zoned(crate::tz::offset(-4).to_time_zone()) |
1084 | .unwrap(); |
1085 | insta::assert_snapshot!(f("%Q" , &zdt), @"-0400" ); |
1086 | insta::assert_snapshot!(f("%:Q" , &zdt), @"-04:00" ); |
1087 | |
1088 | let zdt = date(2024, 7, 14) |
1089 | .at(22, 24, 0, 0) |
1090 | .to_zoned(crate::tz::TimeZone::UTC) |
1091 | .unwrap(); |
1092 | insta::assert_snapshot!(f("%Q" , &zdt), @"UTC" ); |
1093 | insta::assert_snapshot!(f("%:Q" , &zdt), @"UTC" ); |
1094 | } |
1095 | |
1096 | #[test ] |
1097 | fn ok_format_weekday_name() { |
1098 | let f = |fmt: &str, date: Date| format(fmt, date).unwrap(); |
1099 | |
1100 | insta::assert_snapshot!(f("%A" , date(2024, 7, 14)), @"Sunday" ); |
1101 | insta::assert_snapshot!(f("%a" , date(2024, 7, 14)), @"Sun" ); |
1102 | |
1103 | insta::assert_snapshot!(f("%#A" , date(2024, 7, 14)), @"Sunday" ); |
1104 | insta::assert_snapshot!(f("%^A" , date(2024, 7, 14)), @"SUNDAY" ); |
1105 | |
1106 | insta::assert_snapshot!(f("%u" , date(2024, 7, 14)), @"7" ); |
1107 | insta::assert_snapshot!(f("%w" , date(2024, 7, 14)), @"0" ); |
1108 | } |
1109 | |
1110 | #[test ] |
1111 | fn ok_format_year() { |
1112 | let f = |fmt: &str, date: Date| format(fmt, date).unwrap(); |
1113 | |
1114 | insta::assert_snapshot!(f("%Y" , date(2024, 7, 14)), @"2024" ); |
1115 | insta::assert_snapshot!(f("%Y" , date(24, 7, 14)), @"0024" ); |
1116 | insta::assert_snapshot!(f("%Y" , date(-24, 7, 14)), @"-0024" ); |
1117 | |
1118 | insta::assert_snapshot!(f("%C" , date(2024, 7, 14)), @"20" ); |
1119 | insta::assert_snapshot!(f("%C" , date(1815, 7, 14)), @"18" ); |
1120 | insta::assert_snapshot!(f("%C" , date(915, 7, 14)), @"9" ); |
1121 | insta::assert_snapshot!(f("%C" , date(1, 7, 14)), @"0" ); |
1122 | insta::assert_snapshot!(f("%C" , date(0, 7, 14)), @"0" ); |
1123 | insta::assert_snapshot!(f("%C" , date(-1, 7, 14)), @"0" ); |
1124 | insta::assert_snapshot!(f("%C" , date(-2024, 7, 14)), @"-20" ); |
1125 | insta::assert_snapshot!(f("%C" , date(-1815, 7, 14)), @"-18" ); |
1126 | insta::assert_snapshot!(f("%C" , date(-915, 7, 14)), @"-9" ); |
1127 | } |
1128 | |
1129 | #[test ] |
1130 | fn ok_format_year_2digit() { |
1131 | let f = |fmt: &str, date: Date| format(fmt, date).unwrap(); |
1132 | |
1133 | insta::assert_snapshot!(f("%y" , date(2024, 7, 14)), @"24" ); |
1134 | insta::assert_snapshot!(f("%y" , date(2001, 7, 14)), @"01" ); |
1135 | insta::assert_snapshot!(f("%-y" , date(2001, 7, 14)), @"1" ); |
1136 | insta::assert_snapshot!(f("%5y" , date(2001, 7, 14)), @"00001" ); |
1137 | insta::assert_snapshot!(f("%-5y" , date(2001, 7, 14)), @"1" ); |
1138 | insta::assert_snapshot!(f("%05y" , date(2001, 7, 14)), @"00001" ); |
1139 | insta::assert_snapshot!(f("%_y" , date(2001, 7, 14)), @" 1" ); |
1140 | insta::assert_snapshot!(f("%_5y" , date(2001, 7, 14)), @" 1" ); |
1141 | } |
1142 | |
1143 | #[test ] |
1144 | fn ok_format_iso_week_year() { |
1145 | let f = |fmt: &str, date: Date| format(fmt, date).unwrap(); |
1146 | |
1147 | insta::assert_snapshot!(f("%G" , date(2019, 11, 30)), @"2019" ); |
1148 | insta::assert_snapshot!(f("%G" , date(19, 11, 30)), @"0019" ); |
1149 | insta::assert_snapshot!(f("%G" , date(-19, 11, 30)), @"-0019" ); |
1150 | |
1151 | // tricksy |
1152 | insta::assert_snapshot!(f("%G" , date(2019, 12, 30)), @"2020" ); |
1153 | } |
1154 | |
1155 | #[test ] |
1156 | fn ok_format_week_num() { |
1157 | let f = |fmt: &str, date: Date| format(fmt, date).unwrap(); |
1158 | |
1159 | insta::assert_snapshot!(f("%U" , date(2025, 1, 4)), @"00" ); |
1160 | insta::assert_snapshot!(f("%U" , date(2025, 1, 5)), @"01" ); |
1161 | |
1162 | insta::assert_snapshot!(f("%W" , date(2025, 1, 5)), @"00" ); |
1163 | insta::assert_snapshot!(f("%W" , date(2025, 1, 6)), @"01" ); |
1164 | } |
1165 | |
1166 | #[test ] |
1167 | fn ok_format_timestamp() { |
1168 | let f = |fmt: &str, ts: Timestamp| format(fmt, ts).unwrap(); |
1169 | |
1170 | let ts = "1970-01-01T00:00Z" .parse().unwrap(); |
1171 | insta::assert_snapshot!(f("%s" , ts), @"0" ); |
1172 | insta::assert_snapshot!(f("%3s" , ts), @" 0" ); |
1173 | insta::assert_snapshot!(f("%03s" , ts), @"000" ); |
1174 | |
1175 | let ts = "2025-01-20T13:09-05[US/Eastern]" .parse().unwrap(); |
1176 | insta::assert_snapshot!(f("%s" , ts), @"1737396540" ); |
1177 | } |
1178 | |
1179 | #[test ] |
1180 | fn err_format_subsec_nanosecond() { |
1181 | let f = |fmt: &str, time: Time| format(fmt, time).unwrap_err(); |
1182 | let mk = |subsec| time(0, 0, 0, subsec); |
1183 | |
1184 | insta::assert_snapshot!( |
1185 | f("%00f" , mk(123_456_789)), |
1186 | @"strftime formatting failed: %f failed: zero precision with %f is not allowed" , |
1187 | ); |
1188 | } |
1189 | |
1190 | #[test ] |
1191 | fn err_format_timestamp() { |
1192 | let f = |fmt: &str, dt: DateTime| format(fmt, dt).unwrap_err(); |
1193 | |
1194 | let dt = date(2025, 1, 20).at(13, 9, 0, 0); |
1195 | insta::assert_snapshot!( |
1196 | f("%s" , dt), |
1197 | @"strftime formatting failed: %s failed: requires instant (a date, time and offset) to format Unix timestamp" , |
1198 | ); |
1199 | } |
1200 | } |
1201 | |