1 | use crate::{ |
2 | civil::{Date, DateTime, Time}, |
3 | error::{err, Error}, |
4 | fmt::{ |
5 | temporal::{Pieces, PiecesOffset, TimeZoneAnnotationKind}, |
6 | util::{DecimalFormatter, FractionalFormatter}, |
7 | Write, WriteExt, |
8 | }, |
9 | span::Span, |
10 | tz::{Offset, TimeZone}, |
11 | util::{ |
12 | rangeint::RFrom, |
13 | t::{self, C}, |
14 | }, |
15 | SignedDuration, Timestamp, Zoned, |
16 | }; |
17 | |
18 | #[derive (Clone, Debug)] |
19 | pub(super) struct DateTimePrinter { |
20 | lowercase: bool, |
21 | separator: u8, |
22 | rfc9557: bool, |
23 | precision: Option<u8>, |
24 | } |
25 | |
26 | impl DateTimePrinter { |
27 | pub(super) const fn new() -> DateTimePrinter { |
28 | DateTimePrinter { |
29 | lowercase: false, |
30 | separator: b'T' , |
31 | rfc9557: true, |
32 | precision: None, |
33 | } |
34 | } |
35 | |
36 | pub(super) const fn lowercase(self, yes: bool) -> DateTimePrinter { |
37 | DateTimePrinter { lowercase: yes, ..self } |
38 | } |
39 | |
40 | pub(super) const fn separator(self, ascii_char: u8) -> DateTimePrinter { |
41 | assert!(ascii_char.is_ascii(), "RFC3339 separator must be ASCII" ); |
42 | DateTimePrinter { separator: ascii_char, ..self } |
43 | } |
44 | |
45 | pub(super) const fn precision( |
46 | self, |
47 | precision: Option<u8>, |
48 | ) -> DateTimePrinter { |
49 | DateTimePrinter { precision, ..self } |
50 | } |
51 | |
52 | pub(super) fn print_zoned<W: Write>( |
53 | &self, |
54 | zdt: &Zoned, |
55 | mut wtr: W, |
56 | ) -> Result<(), Error> { |
57 | let timestamp = zdt.timestamp(); |
58 | let tz = zdt.time_zone(); |
59 | let offset = tz.to_offset(timestamp); |
60 | let dt = offset.to_datetime(timestamp); |
61 | self.print_datetime(&dt, &mut wtr)?; |
62 | if tz.is_unknown() { |
63 | wtr.write_str("Z[Etc/Unknown]" )?; |
64 | } else { |
65 | self.print_offset_rounded(&offset, &mut wtr)?; |
66 | self.print_time_zone_annotation(&tz, &offset, &mut wtr)?; |
67 | } |
68 | Ok(()) |
69 | } |
70 | |
71 | pub(super) fn print_timestamp<W: Write>( |
72 | &self, |
73 | timestamp: &Timestamp, |
74 | offset: Option<Offset>, |
75 | mut wtr: W, |
76 | ) -> Result<(), Error> { |
77 | let Some(offset) = offset else { |
78 | let dt = TimeZone::UTC.to_datetime(*timestamp); |
79 | self.print_datetime(&dt, &mut wtr)?; |
80 | self.print_zulu(&mut wtr)?; |
81 | return Ok(()); |
82 | }; |
83 | let dt = offset.to_datetime(*timestamp); |
84 | self.print_datetime(&dt, &mut wtr)?; |
85 | self.print_offset_rounded(&offset, &mut wtr)?; |
86 | Ok(()) |
87 | } |
88 | |
89 | /// Formats the given datetime into the writer given. |
90 | pub(super) fn print_datetime<W: Write>( |
91 | &self, |
92 | dt: &DateTime, |
93 | mut wtr: W, |
94 | ) -> Result<(), Error> { |
95 | self.print_date(&dt.date(), &mut wtr)?; |
96 | wtr.write_char(char::from(if self.lowercase { |
97 | self.separator.to_ascii_lowercase() |
98 | } else { |
99 | self.separator |
100 | }))?; |
101 | self.print_time(&dt.time(), &mut wtr)?; |
102 | Ok(()) |
103 | } |
104 | |
105 | /// Formats the given date into the writer given. |
106 | pub(super) fn print_date<W: Write>( |
107 | &self, |
108 | date: &Date, |
109 | mut wtr: W, |
110 | ) -> Result<(), Error> { |
111 | static FMT_YEAR_POSITIVE: DecimalFormatter = |
112 | DecimalFormatter::new().padding(4); |
113 | static FMT_YEAR_NEGATIVE: DecimalFormatter = |
114 | DecimalFormatter::new().padding(6); |
115 | static FMT_TWO: DecimalFormatter = DecimalFormatter::new().padding(2); |
116 | |
117 | if date.year() >= 0 { |
118 | wtr.write_int(&FMT_YEAR_POSITIVE, date.year())?; |
119 | } else { |
120 | wtr.write_int(&FMT_YEAR_NEGATIVE, date.year())?; |
121 | } |
122 | wtr.write_str("-" )?; |
123 | wtr.write_int(&FMT_TWO, date.month())?; |
124 | wtr.write_str("-" )?; |
125 | wtr.write_int(&FMT_TWO, date.day())?; |
126 | Ok(()) |
127 | } |
128 | |
129 | /// Formats the given time into the writer given. |
130 | pub(super) fn print_time<W: Write>( |
131 | &self, |
132 | time: &Time, |
133 | mut wtr: W, |
134 | ) -> Result<(), Error> { |
135 | static FMT_TWO: DecimalFormatter = DecimalFormatter::new().padding(2); |
136 | static FMT_FRACTION: FractionalFormatter = FractionalFormatter::new(); |
137 | |
138 | wtr.write_int(&FMT_TWO, time.hour())?; |
139 | wtr.write_str(":" )?; |
140 | wtr.write_int(&FMT_TWO, time.minute())?; |
141 | wtr.write_str(":" )?; |
142 | wtr.write_int(&FMT_TWO, time.second())?; |
143 | let fractional_nanosecond = time.subsec_nanosecond(); |
144 | if self.precision.map_or(fractional_nanosecond != 0, |p| p > 0) { |
145 | wtr.write_str("." )?; |
146 | wtr.write_fraction( |
147 | &FMT_FRACTION.precision(self.precision), |
148 | fractional_nanosecond, |
149 | )?; |
150 | } |
151 | Ok(()) |
152 | } |
153 | |
154 | /// Formats the given time zone into the writer given. |
155 | pub(super) fn print_time_zone<W: Write>( |
156 | &self, |
157 | tz: &TimeZone, |
158 | mut wtr: W, |
159 | ) -> Result<(), Error> { |
160 | if let Some(iana_name) = tz.iana_name() { |
161 | return wtr.write_str(iana_name); |
162 | } |
163 | if tz.is_unknown() { |
164 | return wtr.write_str("Etc/Unknown" ); |
165 | } |
166 | if let Ok(offset) = tz.to_fixed_offset() { |
167 | return self.print_offset_full_precision(&offset, wtr); |
168 | } |
169 | // We get this on `alloc` because we format the POSIX time zone into a |
170 | // `String` first. See the note below. |
171 | // |
172 | // This is generally okay because there is no current (2025-02-28) way |
173 | // to create a `TimeZone` that is *only* a POSIX time zone in core-only |
174 | // environments. (All you can do is create a TZif time zone, which may |
175 | // contain a POSIX time zone, but `tz.posix_tz()` would still return |
176 | // `None` in that case.) |
177 | #[cfg (feature = "alloc" )] |
178 | { |
179 | if let Some(posix_tz) = tz.posix_tz() { |
180 | // This is pretty unfortunate, but at time of writing, I |
181 | // didn't see an easy way to make the `Display` impl for |
182 | // `PosixTimeZone` automatically work with |
183 | // `jiff::fmt::Write` without allocating a new string. As |
184 | // far as I can see, I either have to duplicate the code or |
185 | // make it generic in some way. I judged neither to be worth |
186 | // doing for such a rare case. ---AG |
187 | let s = alloc::string::ToString::to_string(posix_tz); |
188 | return wtr.write_str(&s); |
189 | } |
190 | } |
191 | // Ideally this never actually happens, but it can, and there |
192 | // are likely system configurations out there in which it does. |
193 | // I can imagine "lightweight" installations that just have a |
194 | // `/etc/localtime` as a TZif file that doesn't point to any IANA time |
195 | // zone. In which case, serializing a time zone probably doesn't make |
196 | // much sense. |
197 | // |
198 | // Anyway, if you're seeing this error and think there should be a |
199 | // different behavior, please file an issue. |
200 | Err(err!( |
201 | "time zones without IANA identifiers that aren't either \ |
202 | fixed offsets or a POSIX time zone can't be serialized \ |
203 | (this typically occurs when this is a system time zone \ |
204 | derived from `/etc/localtime` on Unix systems that \ |
205 | isn't symlinked to an entry in `/usr/share/zoneinfo`)" , |
206 | )) |
207 | } |
208 | |
209 | pub(super) fn print_pieces<W: Write>( |
210 | &self, |
211 | pieces: &Pieces, |
212 | mut wtr: W, |
213 | ) -> Result<(), Error> { |
214 | if let Some(time) = pieces.time() { |
215 | let dt = DateTime::from_parts(pieces.date(), time); |
216 | self.print_datetime(&dt, &mut wtr)?; |
217 | if let Some(poffset) = pieces.offset() { |
218 | self.print_pieces_offset(&poffset, &mut wtr)?; |
219 | } |
220 | } else if let Some(poffset) = pieces.offset() { |
221 | // In this case, we have an offset but no time component. Since |
222 | // `2025-01-02-05:00` isn't valid, we forcefully write out the |
223 | // default time (which is what would be assumed anyway). |
224 | let dt = DateTime::from_parts(pieces.date(), Time::midnight()); |
225 | self.print_datetime(&dt, &mut wtr)?; |
226 | self.print_pieces_offset(&poffset, &mut wtr)?; |
227 | } else { |
228 | // We have no time and no offset, so we can just write the date. |
229 | // It's okay to write this followed by an annotation, e.g., |
230 | // `2025-01-02[America/New_York]` or even `2025-01-02[-05:00]`. |
231 | self.print_date(&pieces.date(), &mut wtr)?; |
232 | } |
233 | // For the time zone annotation, a `Pieces` gives us the annotation |
234 | // name or offset directly, where as with `Zoned`, we have a |
235 | // `TimeZone`. So we hand-roll our own formatter directly from the |
236 | // annotation. |
237 | if let Some(ann) = pieces.time_zone_annotation() { |
238 | // Note that we explicitly ignore `self.rfc9557` here, since with |
239 | // `Pieces`, the annotation has been explicitly provided. Also, |
240 | // at time of writing, `self.rfc9557` is always enabled anyway. |
241 | wtr.write_str("[" )?; |
242 | if ann.is_critical() { |
243 | wtr.write_str("!" )?; |
244 | } |
245 | match *ann.kind() { |
246 | TimeZoneAnnotationKind::Named(ref name) => { |
247 | wtr.write_str(name.as_str())? |
248 | } |
249 | TimeZoneAnnotationKind::Offset(offset) => { |
250 | self.print_offset_rounded(&offset, &mut wtr)? |
251 | } |
252 | } |
253 | wtr.write_str("]" )?; |
254 | } |
255 | Ok(()) |
256 | } |
257 | |
258 | /// Formats the given "pieces" offset into the writer given. |
259 | fn print_pieces_offset<W: Write>( |
260 | &self, |
261 | poffset: &PiecesOffset, |
262 | mut wtr: W, |
263 | ) -> Result<(), Error> { |
264 | match *poffset { |
265 | PiecesOffset::Zulu => self.print_zulu(wtr), |
266 | PiecesOffset::Numeric(ref noffset) => { |
267 | if noffset.offset().is_zero() && noffset.is_negative() { |
268 | wtr.write_str("-00:00" ) |
269 | } else { |
270 | self.print_offset_rounded(&noffset.offset(), wtr) |
271 | } |
272 | } |
273 | } |
274 | } |
275 | |
276 | /// Formats the given offset into the writer given. |
277 | /// |
278 | /// If the given offset has non-zero seconds, then they are rounded to |
279 | /// the nearest minute. |
280 | fn print_offset_rounded<W: Write>( |
281 | &self, |
282 | offset: &Offset, |
283 | mut wtr: W, |
284 | ) -> Result<(), Error> { |
285 | static FMT_TWO: DecimalFormatter = DecimalFormatter::new().padding(2); |
286 | |
287 | wtr.write_str(if offset.is_negative() { "-" } else { "+" })?; |
288 | let mut hours = offset.part_hours_ranged().abs().get(); |
289 | let mut minutes = offset.part_minutes_ranged().abs().get(); |
290 | // RFC 3339 requires that time zone offsets are an integral number |
291 | // of minutes. While rounding based on seconds doesn't seem clearly |
292 | // indicated, the `1937-01-01T12:00:27.87+00:20` example seems |
293 | // to suggest that the number of minutes should be "as close as |
294 | // possible" to the actual offset. So we just do basic rounding |
295 | // here. |
296 | if offset.part_seconds_ranged().abs() >= C(30) { |
297 | if minutes == 59 { |
298 | hours = hours.saturating_add(1); |
299 | minutes = 0; |
300 | } else { |
301 | minutes = minutes.saturating_add(1); |
302 | } |
303 | } |
304 | wtr.write_int(&FMT_TWO, hours)?; |
305 | wtr.write_str(":" )?; |
306 | wtr.write_int(&FMT_TWO, minutes)?; |
307 | Ok(()) |
308 | } |
309 | |
310 | /// Formats the given offset into the writer given. |
311 | /// |
312 | /// If the given offset has non-zero seconds, then they are emitted as a |
313 | /// third `:`-delimited component of the offset. If seconds are zero, then |
314 | /// only the hours and minute components are emitted. |
315 | fn print_offset_full_precision<W: Write>( |
316 | &self, |
317 | offset: &Offset, |
318 | mut wtr: W, |
319 | ) -> Result<(), Error> { |
320 | static FMT_TWO: DecimalFormatter = DecimalFormatter::new().padding(2); |
321 | |
322 | wtr.write_str(if offset.is_negative() { "-" } else { "+" })?; |
323 | let hours = offset.part_hours_ranged().abs().get(); |
324 | let minutes = offset.part_minutes_ranged().abs().get(); |
325 | let seconds = offset.part_seconds_ranged().abs().get(); |
326 | wtr.write_int(&FMT_TWO, hours)?; |
327 | wtr.write_str(":" )?; |
328 | wtr.write_int(&FMT_TWO, minutes)?; |
329 | if seconds > 0 { |
330 | wtr.write_str(":" )?; |
331 | wtr.write_int(&FMT_TWO, seconds)?; |
332 | } |
333 | Ok(()) |
334 | } |
335 | |
336 | /// Prints the "zulu" indicator. |
337 | /// |
338 | /// This should only be used when the offset is not known. For example, |
339 | /// when printing a `Timestamp`. |
340 | fn print_zulu<W: Write>(&self, mut wtr: W) -> Result<(), Error> { |
341 | wtr.write_str(if self.lowercase { "z" } else { "Z" }) |
342 | } |
343 | |
344 | /// Formats the given time zone name into the writer given as an RFC 9557 |
345 | /// time zone annotation. |
346 | /// |
347 | /// This is a no-op when RFC 9557 support isn't enabled. And when the given |
348 | /// time zone is not an IANA time zone name, then the offset is printed |
349 | /// instead. (This means the offset will be printed twice, which is indeed |
350 | /// an intended behavior of RFC 9557 for cases where a time zone name is |
351 | /// not used or unavailable.) |
352 | fn print_time_zone_annotation<W: Write>( |
353 | &self, |
354 | time_zone: &TimeZone, |
355 | offset: &Offset, |
356 | mut wtr: W, |
357 | ) -> Result<(), Error> { |
358 | if !self.rfc9557 { |
359 | return Ok(()); |
360 | } |
361 | wtr.write_str("[" )?; |
362 | if let Some(iana_name) = time_zone.iana_name() { |
363 | wtr.write_str(iana_name)?; |
364 | } else { |
365 | self.print_offset_rounded(offset, &mut wtr)?; |
366 | } |
367 | wtr.write_str("]" )?; |
368 | Ok(()) |
369 | } |
370 | } |
371 | |
372 | impl Default for DateTimePrinter { |
373 | fn default() -> DateTimePrinter { |
374 | DateTimePrinter::new() |
375 | } |
376 | } |
377 | |
378 | /// A printer for Temporal spans. |
379 | /// |
380 | /// Note that in Temporal, a "span" is called a "duration." |
381 | #[derive (Debug)] |
382 | pub(super) struct SpanPrinter { |
383 | /// Whether to use lowercase unit designators. |
384 | lowercase: bool, |
385 | } |
386 | |
387 | impl SpanPrinter { |
388 | /// Create a new Temporal span printer with the default configuration. |
389 | pub(super) const fn new() -> SpanPrinter { |
390 | SpanPrinter { lowercase: false } |
391 | } |
392 | |
393 | /// Use lowercase for unit designator labels. |
394 | /// |
395 | /// By default, unit designator labels are written in uppercase. |
396 | pub(super) const fn lowercase(self, yes: bool) -> SpanPrinter { |
397 | SpanPrinter { lowercase: yes } |
398 | } |
399 | |
400 | /// Print the given span to the writer given. |
401 | /// |
402 | /// This only returns an error when the given writer returns an error. |
403 | pub(super) fn print_span<W: Write>( |
404 | &self, |
405 | span: &Span, |
406 | mut wtr: W, |
407 | ) -> Result<(), Error> { |
408 | static FMT_INT: DecimalFormatter = DecimalFormatter::new(); |
409 | static FMT_FRACTION: FractionalFormatter = FractionalFormatter::new(); |
410 | |
411 | if span.is_negative() { |
412 | wtr.write_str("-" )?; |
413 | } |
414 | wtr.write_str("P" )?; |
415 | |
416 | let mut non_zero_greater_than_second = false; |
417 | if span.get_years_ranged() != C(0) { |
418 | wtr.write_int(&FMT_INT, span.get_years_ranged().get().abs())?; |
419 | wtr.write_char(self.label('Y' ))?; |
420 | non_zero_greater_than_second = true; |
421 | } |
422 | if span.get_months_ranged() != C(0) { |
423 | wtr.write_int(&FMT_INT, span.get_months_ranged().get().abs())?; |
424 | wtr.write_char(self.label('M' ))?; |
425 | non_zero_greater_than_second = true; |
426 | } |
427 | if span.get_weeks_ranged() != C(0) { |
428 | wtr.write_int(&FMT_INT, span.get_weeks_ranged().get().abs())?; |
429 | wtr.write_char(self.label('W' ))?; |
430 | non_zero_greater_than_second = true; |
431 | } |
432 | if span.get_days_ranged() != C(0) { |
433 | wtr.write_int(&FMT_INT, span.get_days_ranged().get().abs())?; |
434 | wtr.write_char(self.label('D' ))?; |
435 | non_zero_greater_than_second = true; |
436 | } |
437 | |
438 | let mut printed_time_prefix = false; |
439 | if span.get_hours_ranged() != C(0) { |
440 | if !printed_time_prefix { |
441 | wtr.write_str("T" )?; |
442 | printed_time_prefix = true; |
443 | } |
444 | wtr.write_int(&FMT_INT, span.get_hours_ranged().get().abs())?; |
445 | wtr.write_char(self.label('H' ))?; |
446 | non_zero_greater_than_second = true; |
447 | } |
448 | if span.get_minutes_ranged() != C(0) { |
449 | if !printed_time_prefix { |
450 | wtr.write_str("T" )?; |
451 | printed_time_prefix = true; |
452 | } |
453 | wtr.write_int(&FMT_INT, span.get_minutes_ranged().get().abs())?; |
454 | wtr.write_char(self.label('M' ))?; |
455 | non_zero_greater_than_second = true; |
456 | } |
457 | |
458 | // ISO 8601 (and Temporal) don't support writing out milliseconds, |
459 | // microseconds or nanoseconds as separate components like for all |
460 | // the other units. Instead, they must be incorporated as fractional |
461 | // seconds. But we only want to do that work if we need to. |
462 | let (seconds, millis, micros, nanos) = ( |
463 | span.get_seconds_ranged().abs(), |
464 | span.get_milliseconds_ranged().abs(), |
465 | span.get_microseconds_ranged().abs(), |
466 | span.get_nanoseconds_ranged().abs(), |
467 | ); |
468 | if (seconds != C(0) || !non_zero_greater_than_second) |
469 | && millis == C(0) |
470 | && micros == C(0) |
471 | && nanos == C(0) |
472 | { |
473 | if !printed_time_prefix { |
474 | wtr.write_str("T" )?; |
475 | } |
476 | wtr.write_int(&FMT_INT, seconds.get())?; |
477 | wtr.write_char(self.label('S' ))?; |
478 | } else if millis != C(0) || micros != C(0) || nanos != C(0) { |
479 | if !printed_time_prefix { |
480 | wtr.write_str("T" )?; |
481 | } |
482 | // We want to combine our seconds, milliseconds, microseconds and |
483 | // nanoseconds into one single value in terms of nanoseconds. Then |
484 | // we can "balance" that out so that we have a number of seconds |
485 | // and a number of nanoseconds not greater than 1 second. (Which is |
486 | // our fraction.) |
487 | let combined_as_nanos = |
488 | t::SpanSecondsOrLowerNanoseconds::rfrom(nanos) |
489 | + (t::SpanSecondsOrLowerNanoseconds::rfrom(micros) |
490 | * t::NANOS_PER_MICRO) |
491 | + (t::SpanSecondsOrLowerNanoseconds::rfrom(millis) |
492 | * t::NANOS_PER_MILLI) |
493 | + (t::SpanSecondsOrLowerNanoseconds::rfrom(seconds) |
494 | * t::NANOS_PER_SECOND); |
495 | let fraction_second = t::SpanSecondsOrLower::rfrom( |
496 | combined_as_nanos / t::NANOS_PER_SECOND, |
497 | ); |
498 | let fraction_nano = t::SubsecNanosecond::rfrom( |
499 | combined_as_nanos % t::NANOS_PER_SECOND, |
500 | ); |
501 | wtr.write_int(&FMT_INT, fraction_second.get())?; |
502 | if fraction_nano != C(0) { |
503 | wtr.write_str("." )?; |
504 | wtr.write_fraction(&FMT_FRACTION, fraction_nano.get())?; |
505 | } |
506 | wtr.write_char(self.label('S' ))?; |
507 | } |
508 | Ok(()) |
509 | } |
510 | |
511 | /// Print the given signed duration to the writer given. |
512 | /// |
513 | /// This only returns an error when the given writer returns an error. |
514 | pub(super) fn print_duration<W: Write>( |
515 | &self, |
516 | dur: &SignedDuration, |
517 | mut wtr: W, |
518 | ) -> Result<(), Error> { |
519 | static FMT_INT: DecimalFormatter = DecimalFormatter::new(); |
520 | static FMT_FRACTION: FractionalFormatter = FractionalFormatter::new(); |
521 | |
522 | let mut non_zero_greater_than_second = false; |
523 | if dur.is_negative() { |
524 | wtr.write_str("-" )?; |
525 | } |
526 | wtr.write_str("PT" )?; |
527 | |
528 | let mut secs = dur.as_secs(); |
529 | // OK because subsec_nanos -999_999_999<=nanos<=999_999_999. |
530 | let nanos = dur.subsec_nanos().abs(); |
531 | // OK because guaranteed to be bigger than i64::MIN. |
532 | let hours = (secs / (60 * 60)).abs(); |
533 | secs %= 60 * 60; |
534 | // OK because guaranteed to be bigger than i64::MIN. |
535 | let minutes = (secs / 60).abs(); |
536 | // OK because guaranteed to be bigger than i64::MIN. |
537 | secs = (secs % 60).abs(); |
538 | if hours != 0 { |
539 | wtr.write_int(&FMT_INT, hours)?; |
540 | wtr.write_char(self.label('H' ))?; |
541 | non_zero_greater_than_second = true; |
542 | } |
543 | if minutes != 0 { |
544 | wtr.write_int(&FMT_INT, minutes)?; |
545 | wtr.write_char(self.label('M' ))?; |
546 | non_zero_greater_than_second = true; |
547 | } |
548 | if (secs != 0 || !non_zero_greater_than_second) && nanos == 0 { |
549 | wtr.write_int(&FMT_INT, secs)?; |
550 | wtr.write_char(self.label('S' ))?; |
551 | } else if nanos != 0 { |
552 | wtr.write_int(&FMT_INT, secs)?; |
553 | wtr.write_str("." )?; |
554 | wtr.write_fraction(&FMT_FRACTION, nanos)?; |
555 | wtr.write_char(self.label('S' ))?; |
556 | } |
557 | Ok(()) |
558 | } |
559 | |
560 | /// Converts the uppercase unit designator label to lowercase if this |
561 | /// printer is configured to use lowercase. Otherwise the label is returned |
562 | /// unchanged. |
563 | fn label(&self, upper: char) -> char { |
564 | debug_assert!(upper.is_ascii()); |
565 | if self.lowercase { |
566 | upper.to_ascii_lowercase() |
567 | } else { |
568 | upper |
569 | } |
570 | } |
571 | } |
572 | |
573 | #[cfg (feature = "alloc" )] |
574 | #[cfg (test)] |
575 | mod tests { |
576 | use alloc::string::String; |
577 | |
578 | use crate::{civil::date, span::ToSpan}; |
579 | |
580 | use super::*; |
581 | |
582 | #[test ] |
583 | fn print_zoned() { |
584 | if crate::tz::db().is_definitively_empty() { |
585 | return; |
586 | } |
587 | |
588 | let dt = date(2024, 3, 10).at(5, 34, 45, 0); |
589 | let zoned: Zoned = dt.in_tz("America/New_York" ).unwrap(); |
590 | let mut buf = String::new(); |
591 | DateTimePrinter::new().print_zoned(&zoned, &mut buf).unwrap(); |
592 | assert_eq!(buf, "2024-03-10T05:34:45-04:00[America/New_York]" ); |
593 | |
594 | let dt = date(2024, 3, 10).at(5, 34, 45, 0); |
595 | let zoned: Zoned = dt.in_tz("America/New_York" ).unwrap(); |
596 | let zoned = zoned.with_time_zone(TimeZone::UTC); |
597 | let mut buf = String::new(); |
598 | DateTimePrinter::new().print_zoned(&zoned, &mut buf).unwrap(); |
599 | assert_eq!(buf, "2024-03-10T09:34:45+00:00[UTC]" ); |
600 | } |
601 | |
602 | #[test ] |
603 | fn print_timestamp() { |
604 | if crate::tz::db().is_definitively_empty() { |
605 | return; |
606 | } |
607 | |
608 | let dt = date(2024, 3, 10).at(5, 34, 45, 0); |
609 | let zoned: Zoned = dt.in_tz("America/New_York" ).unwrap(); |
610 | let mut buf = String::new(); |
611 | DateTimePrinter::new() |
612 | .print_timestamp(&zoned.timestamp(), None, &mut buf) |
613 | .unwrap(); |
614 | assert_eq!(buf, "2024-03-10T09:34:45Z" ); |
615 | |
616 | let dt = date(-2024, 3, 10).at(5, 34, 45, 0); |
617 | let zoned: Zoned = dt.in_tz("America/New_York" ).unwrap(); |
618 | let mut buf = String::new(); |
619 | DateTimePrinter::new() |
620 | .print_timestamp(&zoned.timestamp(), None, &mut buf) |
621 | .unwrap(); |
622 | assert_eq!(buf, "-002024-03-10T10:30:47Z" ); |
623 | } |
624 | |
625 | #[test ] |
626 | fn print_span_basic() { |
627 | let p = |span: Span| -> String { |
628 | let mut buf = String::new(); |
629 | SpanPrinter::new().print_span(&span, &mut buf).unwrap(); |
630 | buf |
631 | }; |
632 | |
633 | insta::assert_snapshot!(p(Span::new()), @"PT0S" ); |
634 | insta::assert_snapshot!(p(1.second()), @"PT1S" ); |
635 | insta::assert_snapshot!(p(-1.second()), @"-PT1S" ); |
636 | insta::assert_snapshot!(p( |
637 | 1.second().milliseconds(1).microseconds(1).nanoseconds(1), |
638 | ), @"PT1.001001001S" ); |
639 | insta::assert_snapshot!(p( |
640 | 0.second().milliseconds(999).microseconds(999).nanoseconds(999), |
641 | ), @"PT0.999999999S" ); |
642 | insta::assert_snapshot!(p( |
643 | 1.year().months(1).weeks(1).days(1) |
644 | .hours(1).minutes(1).seconds(1) |
645 | .milliseconds(1).microseconds(1).nanoseconds(1), |
646 | ), @"P1Y1M1W1DT1H1M1.001001001S" ); |
647 | insta::assert_snapshot!(p( |
648 | -1.year().months(1).weeks(1).days(1) |
649 | .hours(1).minutes(1).seconds(1) |
650 | .milliseconds(1).microseconds(1).nanoseconds(1), |
651 | ), @"-P1Y1M1W1DT1H1M1.001001001S" ); |
652 | } |
653 | |
654 | #[test ] |
655 | fn print_span_subsecond_positive() { |
656 | let p = |span: Span| -> String { |
657 | let mut buf = String::new(); |
658 | SpanPrinter::new().print_span(&span, &mut buf).unwrap(); |
659 | buf |
660 | }; |
661 | |
662 | // These are all sub-second trickery tests. |
663 | insta::assert_snapshot!(p( |
664 | 0.second().milliseconds(1000).microseconds(1000).nanoseconds(1000), |
665 | ), @"PT1.001001S" ); |
666 | insta::assert_snapshot!(p( |
667 | 1.second().milliseconds(1000).microseconds(1000).nanoseconds(1000), |
668 | ), @"PT2.001001S" ); |
669 | insta::assert_snapshot!(p( |
670 | 0.second() |
671 | .milliseconds(t::SpanMilliseconds::MAX_REPR), |
672 | ), @"PT631107417600S" ); |
673 | insta::assert_snapshot!(p( |
674 | 0.second() |
675 | .microseconds(t::SpanMicroseconds::MAX_REPR), |
676 | ), @"PT631107417600S" ); |
677 | insta::assert_snapshot!(p( |
678 | 0.second() |
679 | .nanoseconds(t::SpanNanoseconds::MAX_REPR), |
680 | ), @"PT9223372036.854775807S" ); |
681 | |
682 | insta::assert_snapshot!(p( |
683 | 0.second() |
684 | .milliseconds(t::SpanMilliseconds::MAX_REPR) |
685 | .microseconds(999_999), |
686 | ), @"PT631107417600.999999S" ); |
687 | // This is 1 microsecond more than the maximum number of seconds |
688 | // representable in a span. |
689 | insta::assert_snapshot!(p( |
690 | 0.second() |
691 | .milliseconds(t::SpanMilliseconds::MAX_REPR) |
692 | .microseconds(1_000_000), |
693 | ), @"PT631107417601S" ); |
694 | insta::assert_snapshot!(p( |
695 | 0.second() |
696 | .milliseconds(t::SpanMilliseconds::MAX_REPR) |
697 | .microseconds(1_000_001), |
698 | ), @"PT631107417601.000001S" ); |
699 | // This is 1 nanosecond more than the maximum number of seconds |
700 | // representable in a span. |
701 | insta::assert_snapshot!(p( |
702 | 0.second() |
703 | .milliseconds(t::SpanMilliseconds::MAX_REPR) |
704 | .nanoseconds(1_000_000_000), |
705 | ), @"PT631107417601S" ); |
706 | insta::assert_snapshot!(p( |
707 | 0.second() |
708 | .milliseconds(t::SpanMilliseconds::MAX_REPR) |
709 | .nanoseconds(1_000_000_001), |
710 | ), @"PT631107417601.000000001S" ); |
711 | |
712 | // The max millis, micros and nanos, combined. |
713 | insta::assert_snapshot!(p( |
714 | 0.second() |
715 | .milliseconds(t::SpanMilliseconds::MAX_REPR) |
716 | .microseconds(t::SpanMicroseconds::MAX_REPR) |
717 | .nanoseconds(t::SpanNanoseconds::MAX_REPR), |
718 | ), @"PT1271438207236.854775807S" ); |
719 | // The max seconds, millis, micros and nanos, combined. |
720 | insta::assert_snapshot!(p( |
721 | Span::new() |
722 | .seconds(t::SpanSeconds::MAX_REPR) |
723 | .milliseconds(t::SpanMilliseconds::MAX_REPR) |
724 | .microseconds(t::SpanMicroseconds::MAX_REPR) |
725 | .nanoseconds(t::SpanNanoseconds::MAX_REPR), |
726 | ), @"PT1902545624836.854775807S" ); |
727 | } |
728 | |
729 | #[test ] |
730 | fn print_span_subsecond_negative() { |
731 | let p = |span: Span| -> String { |
732 | let mut buf = String::new(); |
733 | SpanPrinter::new().print_span(&span, &mut buf).unwrap(); |
734 | buf |
735 | }; |
736 | |
737 | // These are all sub-second trickery tests. |
738 | insta::assert_snapshot!(p( |
739 | -0.second().milliseconds(1000).microseconds(1000).nanoseconds(1000), |
740 | ), @"-PT1.001001S" ); |
741 | insta::assert_snapshot!(p( |
742 | -1.second().milliseconds(1000).microseconds(1000).nanoseconds(1000), |
743 | ), @"-PT2.001001S" ); |
744 | insta::assert_snapshot!(p( |
745 | 0.second() |
746 | .milliseconds(t::SpanMilliseconds::MIN_REPR), |
747 | ), @"-PT631107417600S" ); |
748 | insta::assert_snapshot!(p( |
749 | 0.second() |
750 | .microseconds(t::SpanMicroseconds::MIN_REPR), |
751 | ), @"-PT631107417600S" ); |
752 | insta::assert_snapshot!(p( |
753 | 0.second() |
754 | .nanoseconds(t::SpanNanoseconds::MIN_REPR), |
755 | ), @"-PT9223372036.854775807S" ); |
756 | |
757 | insta::assert_snapshot!(p( |
758 | 0.second() |
759 | .milliseconds(t::SpanMilliseconds::MIN_REPR) |
760 | .microseconds(999_999), |
761 | ), @"-PT631107417600.999999S" ); |
762 | // This is 1 microsecond more than the maximum number of seconds |
763 | // representable in a span. |
764 | insta::assert_snapshot!(p( |
765 | 0.second() |
766 | .milliseconds(t::SpanMilliseconds::MIN_REPR) |
767 | .microseconds(1_000_000), |
768 | ), @"-PT631107417601S" ); |
769 | insta::assert_snapshot!(p( |
770 | 0.second() |
771 | .milliseconds(t::SpanMilliseconds::MIN_REPR) |
772 | .microseconds(1_000_001), |
773 | ), @"-PT631107417601.000001S" ); |
774 | // This is 1 nanosecond more than the maximum number of seconds |
775 | // representable in a span. |
776 | insta::assert_snapshot!(p( |
777 | 0.second() |
778 | .milliseconds(t::SpanMilliseconds::MIN_REPR) |
779 | .nanoseconds(1_000_000_000), |
780 | ), @"-PT631107417601S" ); |
781 | insta::assert_snapshot!(p( |
782 | 0.second() |
783 | .milliseconds(t::SpanMilliseconds::MIN_REPR) |
784 | .nanoseconds(1_000_000_001), |
785 | ), @"-PT631107417601.000000001S" ); |
786 | |
787 | // The max millis, micros and nanos, combined. |
788 | insta::assert_snapshot!(p( |
789 | 0.second() |
790 | .milliseconds(t::SpanMilliseconds::MIN_REPR) |
791 | .microseconds(t::SpanMicroseconds::MIN_REPR) |
792 | .nanoseconds(t::SpanNanoseconds::MIN_REPR), |
793 | ), @"-PT1271438207236.854775807S" ); |
794 | // The max seconds, millis, micros and nanos, combined. |
795 | insta::assert_snapshot!(p( |
796 | Span::new() |
797 | .seconds(t::SpanSeconds::MIN_REPR) |
798 | .milliseconds(t::SpanMilliseconds::MIN_REPR) |
799 | .microseconds(t::SpanMicroseconds::MIN_REPR) |
800 | .nanoseconds(t::SpanNanoseconds::MIN_REPR), |
801 | ), @"-PT1902545624836.854775807S" ); |
802 | } |
803 | |
804 | #[test ] |
805 | fn print_duration() { |
806 | let p = |secs, nanos| -> String { |
807 | let dur = SignedDuration::new(secs, nanos); |
808 | let mut buf = String::new(); |
809 | SpanPrinter::new().print_duration(&dur, &mut buf).unwrap(); |
810 | buf |
811 | }; |
812 | |
813 | insta::assert_snapshot!(p(0, 0), @"PT0S" ); |
814 | insta::assert_snapshot!(p(0, 1), @"PT0.000000001S" ); |
815 | insta::assert_snapshot!(p(1, 0), @"PT1S" ); |
816 | insta::assert_snapshot!(p(59, 0), @"PT59S" ); |
817 | insta::assert_snapshot!(p(60, 0), @"PT1M" ); |
818 | insta::assert_snapshot!(p(60, 1), @"PT1M0.000000001S" ); |
819 | insta::assert_snapshot!(p(61, 1), @"PT1M1.000000001S" ); |
820 | insta::assert_snapshot!(p(3_600, 0), @"PT1H" ); |
821 | insta::assert_snapshot!(p(3_600, 1), @"PT1H0.000000001S" ); |
822 | insta::assert_snapshot!(p(3_660, 0), @"PT1H1M" ); |
823 | insta::assert_snapshot!(p(3_660, 1), @"PT1H1M0.000000001S" ); |
824 | insta::assert_snapshot!(p(3_661, 0), @"PT1H1M1S" ); |
825 | insta::assert_snapshot!(p(3_661, 1), @"PT1H1M1.000000001S" ); |
826 | |
827 | insta::assert_snapshot!(p(0, -1), @"-PT0.000000001S" ); |
828 | insta::assert_snapshot!(p(-1, 0), @"-PT1S" ); |
829 | insta::assert_snapshot!(p(-59, 0), @"-PT59S" ); |
830 | insta::assert_snapshot!(p(-60, 0), @"-PT1M" ); |
831 | insta::assert_snapshot!(p(-60, -1), @"-PT1M0.000000001S" ); |
832 | insta::assert_snapshot!(p(-61, -1), @"-PT1M1.000000001S" ); |
833 | insta::assert_snapshot!(p(-3_600, 0), @"-PT1H" ); |
834 | insta::assert_snapshot!(p(-3_600, -1), @"-PT1H0.000000001S" ); |
835 | insta::assert_snapshot!(p(-3_660, 0), @"-PT1H1M" ); |
836 | insta::assert_snapshot!(p(-3_660, -1), @"-PT1H1M0.000000001S" ); |
837 | insta::assert_snapshot!(p(-3_661, 0), @"-PT1H1M1S" ); |
838 | insta::assert_snapshot!(p(-3_661, -1), @"-PT1H1M1.000000001S" ); |
839 | |
840 | insta::assert_snapshot!( |
841 | p(i64::MIN, -999_999_999), |
842 | @"-PT2562047788015215H30M8.999999999S" , |
843 | ); |
844 | insta::assert_snapshot!( |
845 | p(i64::MAX, 999_999_999), |
846 | @"PT2562047788015215H30M7.999999999S" , |
847 | ); |
848 | } |
849 | } |
850 | |