jiff/fmt/temporal/
printer.rs

1use core::time::Duration;
2
3use crate::{
4    civil::{Date, DateTime, ISOWeekDate, Time},
5    error::{fmt::temporal::Error as E, Error},
6    fmt::{
7        buffer::{ArrayBuffer, BorrowedBuffer, BorrowedWriter},
8        temporal::{Pieces, PiecesOffset, TimeZoneAnnotationKind},
9        Write,
10    },
11    span::{Span, UnitSet},
12    tz::{Offset, TimeZone},
13    SignedDuration, Timestamp, Unit, Zoned,
14};
15
16/// Defines the "maximum" possible length (in bytes) of an RFC 9557 zoned
17/// datetime.
18///
19/// In practice, the actual maximal string possible is I believe
20/// `-009999-03-14T17:30:00.999999999-04:23[America/Argentina/ComodRivadavia]`,
21/// which is 72 bytes. Obviously, the length of the IANA tzdb identifier
22/// matters and can be highly variable.
23///
24/// So why is this called "reasonable" and not "maximum"? Well, it's the
25/// IANA tzdb identifiers. They can be arbitrarily long. There also aren't
26/// any rules about how long they can be. So in theory, IANA could allocate
27/// a new identifier longer than `America/Argentina/ComodRivadavia`. With
28/// that said, they do generally try to keep them succinct.
29///
30/// Separately from what IANA does, Jiff itself doesn't impose any restrictions
31/// on the length of identifiers. Callers can pass in arbitrarily long
32/// identifiers via `TimeZone::tzif` or by simply futzing with the names of
33/// files in `/usr/share/zoneinfo`. It's also possible to use an arbitrarily
34/// long identifier via the "pieces" `TimeZoneAnnotationName` API. Since we
35/// don't impose any restrictions, we really do want to at least try to handle
36/// arbitrarily long identifiers here.
37///
38/// Thus, we define a "reasonable" upper bound. When the RFC 9557 string we
39/// want to serialize is known to be under this bound, then we'll use a "fast"
40/// path with a fixed size buffer on the stack (or perhaps even write directly
41/// into the spare capacity of a caller providd `String`). But when it's above
42/// this bound, then we fall back to a slower path that uses a buffering
43/// mechanism to permit arbitrarily long IANA tzdb identifiers.
44///
45/// For the most part, doing this dance doesn't come with a runtime cost,
46/// primarily because we choose to sacrifice code size a bit by duplicating
47/// some functions. We could have our cake and eat it too if we enforce a
48/// maximum length on IANA tzdb identifiers. Then we could set a true
49/// `MAX_ZONED_LEN` and avoid the case where buffering is needed.
50const REASONABLE_ZONED_LEN: usize = 72;
51
52/// Defines the "maximum" possible length (in bytes) of an RFC 9557 zoned
53/// datetime via the "pieces" API.
54///
55/// This generally should be identical to `REASONABLE_ZONED_LEN`, except its
56/// expected maximum is one byte longer. Namely, the pieces API currently
57/// lets the caller roundtrip the "criticality" of the timestamp. i.e.,
58/// the `!` in the time zone annotation. So it's one extra byte longer
59/// than zoned datetimes.
60///
61/// Note that all the same considerations from variable length IANA tzdb
62/// identifiers apply to the pieces printing just as it does to zoned datetime
63/// printing.
64const REASONABLE_PIECES_LEN: usize = 73;
65
66/// Defines the maximum possible length (in bytes) of an RFC 3339 timestamp
67/// that is always in Zulu time.
68///
69/// The longest possible string is `-009999-03-14T21:53:08.999999999Z`.
70const MAX_TIMESTAMP_ZULU_LEN: usize = 33;
71
72/// Defines the maximum possible length (in bytes) of an RFC 3339 timestamp
73/// with an offset (up to minute precision).
74///
75/// The longest possible string is `-009999-03-15T23:53:07.999999999+25:59`.
76const MAX_TIMESTAMP_OFFSET_LEN: usize = 38;
77
78/// Defines the maximum possible length (in bytes) of an ISO 8601 datetime.
79///
80/// The longest possible string is `-009999-03-14T17:30:00.999999999`.
81const MAX_DATETIME_LEN: usize = 32;
82
83/// Defines the maximum possible length (in bytes) of an ISO 8601 date.
84///
85/// The longest possible string is `-009999-03-14`.
86const MAX_DATE_LEN: usize = 13;
87
88/// Defines the maximum possible length (in bytes) of an ISO 8601 time.
89///
90/// The longest possible string is `17:30:00.999999999`.
91const MAX_TIME_LEN: usize = 18;
92
93/// Defines the maximum possible length (in bytes) of an offset.
94///
95/// The longest possible string is `-25:59:59`.
96const MAX_OFFSET_LEN: usize = 9;
97
98/// Defines the maximum possible length (in bytes) of an ISO 8601 week date.
99///
100/// The longest possible string is `-009999-W11-3`.
101const MAX_ISO_WEEK_DATE_LEN: usize = 13;
102
103/// Defines the maximum possible length (in bytes) of a `Span` printed in the
104/// Temporal ISO 8601 format.
105///
106/// The way I computed this length was by using the default printer (since
107/// there's only one knob and it doesn't impact the length of the string)
108/// and using a negative `Span` with each unit set to
109/// its minimum value.
110const MAX_SPAN_LEN: usize = 78;
111
112/// Defines the maximum possible length (in bytes) of a duration printed in the
113/// Temporal ISO 8601 format.
114///
115/// This applies to both signed and unsigned durations. Unsigned durations have
116/// one more digit, but signed durations can have a negative sign.
117const MAX_DURATION_LEN: usize = 35;
118
119#[derive(Clone, Debug)]
120pub(super) struct DateTimePrinter {
121    lowercase: bool,
122    separator: u8,
123    precision: Option<u8>,
124}
125
126impl DateTimePrinter {
127    pub(super) const fn new() -> DateTimePrinter {
128        DateTimePrinter { lowercase: false, separator: b'T', precision: None }
129    }
130
131    pub(super) const fn lowercase(self, yes: bool) -> DateTimePrinter {
132        DateTimePrinter { lowercase: yes, ..self }
133    }
134
135    pub(super) const fn separator(self, ascii_char: u8) -> DateTimePrinter {
136        assert!(ascii_char.is_ascii(), "RFC3339 separator must be ASCII");
137        DateTimePrinter { separator: ascii_char, ..self }
138    }
139
140    pub(super) const fn precision(
141        self,
142        precision: Option<u8>,
143    ) -> DateTimePrinter {
144        DateTimePrinter { precision, ..self }
145    }
146
147    pub(super) fn print_zoned(
148        &self,
149        zdt: &Zoned,
150        wtr: &mut dyn Write,
151    ) -> Result<(), Error> {
152        /// The base size of an RFC 9557 zoned datetime string.
153        ///
154        /// 19 comes from the datetime component, e.g., `2025-01-01T00:00:00`
155        /// 6 comes from the offset, e.g., `+05:30`
156        /// 2 comes from the `[` and `]` for the time zone annotation
157        ///
158        /// Basically, we always need space for *at least* the above stuff.
159        /// The actual time zone annotation name, negative year and fractional
160        /// second component are all optional and can vary quite a bit in
161        /// length.
162        ///
163        /// We do this calculation to get a tighter bound on the spare capacity
164        /// needed when the caller provides a `&mut String` or a `&mut Vec<u8>`
165        /// to write into. That is, we want to try hard not to over-allocate.
166        ///
167        /// Note that memory safety does not depend on us getting this
168        /// calculation right. If we get it wrong, the printer will panic if
169        /// it tries to print a string that exceeds the calculated amount.
170        const BASE: usize = 19 + 6 + 2;
171
172        // An IANA tzdb identifier is variable length data, so add its length
173        // to the `BASE` for runtime allocation size. When there is no IANA
174        // identifier, we could write a fixed offset (that's always 6 bytes) or
175        // `Etc/Unknown` (11 bytes) for when the offset from UTC is not known.
176        // In the non-IANA case, we just use an upper bound of 11, so we will
177        // over-allocate a little in the fixed offset case.
178        let mut runtime_allocation = BASE
179            + zdt.time_zone().iana_name().map(|name| name.len()).unwrap_or(11);
180        // A datetime before year 0 means we add a `-00` prefix. e.g.,
181        // `-001234-01-01`.
182        if zdt.year() < 0 {
183            runtime_allocation += 3;
184        }
185        // If we're printing fractional seconds, then we need more room for
186        // that. This potentially overallocates because we don't do the extra
187        // work required to find a tighter bound.
188        if zdt.subsec_nanosecond() != 0 || self.precision.is_some() {
189            runtime_allocation += 10;
190        }
191
192        // The runtime allocation could be greater than what we assume is a
193        // "reasonable" upper bound on the length of an RFC 9557 string. This
194        // can only happen when an IANA tzdb identifier is very long. When
195        // we're under the limit, we use a fast path.
196        if runtime_allocation <= REASONABLE_ZONED_LEN {
197            return BorrowedBuffer::with_writer::<REASONABLE_ZONED_LEN>(
198                wtr,
199                runtime_allocation,
200                |bbuf| Ok(self.print_zoned_buf(zdt, bbuf)),
201            );
202        }
203
204        // ... otherwise, we use a path with a buffered writer that is slower
205        // but can deal with arbitrarily long IANA tzdb identifiers.
206        let mut buf = ArrayBuffer::<REASONABLE_ZONED_LEN>::default();
207        let mut bbuf = buf.as_borrowed();
208        let mut wtr = BorrowedWriter::new(&mut bbuf, wtr);
209        self.print_zoned_wtr(zdt, &mut wtr)?;
210        wtr.finish()
211    }
212
213    fn print_zoned_buf(&self, zdt: &Zoned, bbuf: &mut BorrowedBuffer<'_>) {
214        self.print_datetime_buf(&zdt.datetime(), bbuf);
215        let tz = zdt.time_zone();
216        if tz.is_unknown() {
217            bbuf.write_str("Z[Etc/Unknown]");
218        } else {
219            self.print_offset_rounded_buf(&zdt.offset(), bbuf);
220            self.print_time_zone_annotation_buf(&tz, &zdt.offset(), bbuf);
221        }
222    }
223
224    fn print_zoned_wtr(
225        &self,
226        zdt: &Zoned,
227        wtr: &mut BorrowedWriter<'_, '_, '_>,
228    ) -> Result<(), Error> {
229        self.print_datetime_wtr(&zdt.datetime(), wtr)?;
230        let tz = zdt.time_zone();
231        if tz.is_unknown() {
232            wtr.write_str("Z[Etc/Unknown]")?;
233        } else {
234            self.print_offset_rounded_wtr(&zdt.offset(), wtr)?;
235            self.print_time_zone_annotation_wtr(&tz, &zdt.offset(), wtr)?;
236        }
237        Ok(())
238    }
239
240    pub(super) fn print_timestamp(
241        &self,
242        timestamp: &Timestamp,
243        wtr: &mut dyn Write,
244    ) -> Result<(), Error> {
245        let mut runtime_allocation = MAX_TIMESTAMP_ZULU_LEN;
246        // Don't reserve room for fractional seconds if we don't use them.
247        if timestamp.subsec_nanosecond() == 0 && self.precision.is_none() {
248            runtime_allocation -= 10;
249        }
250        BorrowedBuffer::with_writer::<MAX_TIMESTAMP_ZULU_LEN>(
251            wtr,
252            runtime_allocation,
253            |bbuf| Ok(self.print_timestamp_buf(timestamp, bbuf)),
254        )
255    }
256
257    fn print_timestamp_buf(
258        &self,
259        timestamp: &Timestamp,
260        bbuf: &mut BorrowedBuffer<'_>,
261    ) {
262        let dt = Offset::UTC.to_datetime(*timestamp);
263        self.print_datetime_buf(&dt, bbuf);
264        self.print_zulu_buf(bbuf);
265    }
266
267    pub(super) fn print_timestamp_with_offset(
268        &self,
269        timestamp: &Timestamp,
270        offset: Offset,
271        wtr: &mut dyn Write,
272    ) -> Result<(), Error> {
273        let mut runtime_allocation = MAX_TIMESTAMP_OFFSET_LEN;
274        // Don't reserve room for fractional seconds if we don't use them.
275        if timestamp.subsec_nanosecond() == 0 && self.precision.is_none() {
276            runtime_allocation -= 10;
277        }
278        BorrowedBuffer::with_writer::<MAX_TIMESTAMP_OFFSET_LEN>(
279            wtr,
280            runtime_allocation,
281            |bbuf| {
282                Ok(self
283                    .print_timestamp_with_offset_buf(timestamp, offset, bbuf))
284            },
285        )
286    }
287
288    fn print_timestamp_with_offset_buf(
289        &self,
290        timestamp: &Timestamp,
291        offset: Offset,
292        bbuf: &mut BorrowedBuffer<'_>,
293    ) {
294        let dt = offset.to_datetime(*timestamp);
295        self.print_datetime_buf(&dt, bbuf);
296        self.print_offset_rounded_buf(&offset, bbuf);
297    }
298
299    pub(super) fn print_datetime(
300        &self,
301        dt: &DateTime,
302        wtr: &mut dyn Write,
303    ) -> Result<(), Error> {
304        let mut runtime_allocation = MAX_DATETIME_LEN;
305        // Don't reserve room for fractional seconds if we don't use them.
306        if dt.subsec_nanosecond() == 0 && self.precision.is_none() {
307            runtime_allocation -= 10;
308        }
309        BorrowedBuffer::with_writer::<MAX_DATETIME_LEN>(
310            wtr,
311            runtime_allocation,
312            |bbuf| Ok(self.print_datetime_buf(dt, bbuf)),
313        )
314    }
315
316    fn print_datetime_buf(
317        &self,
318        dt: &DateTime,
319        bbuf: &mut BorrowedBuffer<'_>,
320    ) {
321        self.print_date_buf(&dt.date(), bbuf);
322        bbuf.write_ascii_char(if self.lowercase {
323            self.separator.to_ascii_lowercase()
324        } else {
325            self.separator
326        });
327        self.print_time_buf(&dt.time(), bbuf);
328    }
329
330    fn print_datetime_wtr(
331        &self,
332        dt: &DateTime,
333        wtr: &mut BorrowedWriter<'_, '_, '_>,
334    ) -> Result<(), Error> {
335        self.print_date_wtr(&dt.date(), wtr)?;
336        wtr.write_ascii_char(if self.lowercase {
337            self.separator.to_ascii_lowercase()
338        } else {
339            self.separator
340        })?;
341        self.print_time_wtr(&dt.time(), wtr)?;
342        Ok(())
343    }
344
345    pub(super) fn print_date(
346        &self,
347        date: &Date,
348        wtr: &mut dyn Write,
349    ) -> Result<(), Error> {
350        BorrowedBuffer::with_writer::<MAX_DATE_LEN>(
351            wtr,
352            MAX_DATE_LEN,
353            |bbuf| Ok(self.print_date_buf(date, bbuf)),
354        )
355    }
356
357    fn print_date_buf(&self, date: &Date, bbuf: &mut BorrowedBuffer<'_>) {
358        let year = date.year();
359        if year < 0 {
360            bbuf.write_str("-00");
361        }
362        bbuf.write_int_pad4(year.unsigned_abs());
363        bbuf.write_ascii_char(b'-');
364        bbuf.write_int_pad2(date.month().unsigned_abs());
365        bbuf.write_ascii_char(b'-');
366        bbuf.write_int_pad2(date.day().unsigned_abs());
367    }
368
369    fn print_date_wtr(
370        &self,
371        date: &Date,
372        wtr: &mut BorrowedWriter<'_, '_, '_>,
373    ) -> Result<(), Error> {
374        let year = date.year();
375        if year < 0 {
376            wtr.write_str("-00")?;
377        }
378        wtr.write_int_pad4(year.unsigned_abs())?;
379        wtr.write_ascii_char(b'-')?;
380        wtr.write_int_pad2(date.month().unsigned_abs())?;
381        wtr.write_ascii_char(b'-')?;
382        wtr.write_int_pad2(date.day().unsigned_abs())?;
383        Ok(())
384    }
385
386    pub(super) fn print_time(
387        &self,
388        time: &Time,
389        wtr: &mut dyn Write,
390    ) -> Result<(), Error> {
391        let mut runtime_allocation = MAX_TIME_LEN;
392        // Don't reserve room for fractional seconds if we don't use them.
393        if time.subsec_nanosecond() == 0 && self.precision.is_none() {
394            runtime_allocation -= 10;
395        }
396        BorrowedBuffer::with_writer::<MAX_TIME_LEN>(
397            wtr,
398            runtime_allocation,
399            |bbuf| Ok(self.print_time_buf(time, bbuf)),
400        )
401    }
402
403    fn print_time_buf(&self, time: &Time, bbuf: &mut BorrowedBuffer<'_>) {
404        bbuf.write_int_pad2(time.hour().unsigned_abs());
405        bbuf.write_ascii_char(b':');
406        bbuf.write_int_pad2(time.minute().unsigned_abs());
407        bbuf.write_ascii_char(b':');
408        bbuf.write_int_pad2(time.second().unsigned_abs());
409        let fractional_nanosecond = time.subsec_nanosecond();
410        if self.precision.map_or(fractional_nanosecond != 0, |p| p > 0) {
411            bbuf.write_ascii_char(b'.');
412            bbuf.write_fraction(
413                self.precision,
414                fractional_nanosecond.unsigned_abs(),
415            );
416        }
417    }
418
419    fn print_time_wtr(
420        &self,
421        time: &Time,
422        wtr: &mut BorrowedWriter<'_, '_, '_>,
423    ) -> Result<(), Error> {
424        wtr.write_int_pad2(time.hour().unsigned_abs())?;
425        wtr.write_ascii_char(b':')?;
426        wtr.write_int_pad2(time.minute().unsigned_abs())?;
427        wtr.write_ascii_char(b':')?;
428        wtr.write_int_pad2(time.second().unsigned_abs())?;
429        let fractional_nanosecond = time.subsec_nanosecond();
430        if self.precision.map_or(fractional_nanosecond != 0, |p| p > 0) {
431            wtr.write_ascii_char(b'.')?;
432            wtr.write_fraction(
433                self.precision,
434                fractional_nanosecond.unsigned_abs(),
435            )?;
436        }
437        Ok(())
438    }
439
440    pub(super) fn print_time_zone<W: Write>(
441        &self,
442        tz: &TimeZone,
443        mut wtr: W,
444    ) -> Result<(), Error> {
445        // N.B. We use a `&mut dyn Write` here instead of an uninitialized
446        // buffer (as in the other routines for this printer) because this
447        // can emit a POSIX time zone string. We don't really have strong
448        // guarantees about how long this string can be (although all sensible
449        // values are pretty short). Since this API is not expected to be used
450        // much, we don't spend the time to try and optimize this.
451        //
452        // If and when we get a borrowed buffer writer abstraction (for truly
453        // variable length output), then we might consider using that here.
454        self.print_time_zone_wtr(tz, &mut wtr)
455    }
456
457    fn print_time_zone_wtr(
458        &self,
459        tz: &TimeZone,
460        wtr: &mut dyn Write,
461    ) -> Result<(), Error> {
462        if let Some(iana_name) = tz.iana_name() {
463            return wtr.write_str(iana_name);
464        }
465        if tz.is_unknown() {
466            return wtr.write_str("Etc/Unknown");
467        }
468        if let Ok(offset) = tz.to_fixed_offset() {
469            // Kind of unfortunate, but it's probably better than
470            // making `print_offset_full_precision` accept a `dyn Write`.
471            let mut buf = ArrayBuffer::<MAX_OFFSET_LEN>::default();
472            let mut bbuf = buf.as_borrowed();
473            self.print_offset_full_precision(&offset, &mut bbuf);
474            return wtr.write_str(bbuf.filled());
475        }
476        if let Some(posix_tz) = tz.posix_tz() {
477            use core::fmt::Write as _;
478
479            // This is rather circuitous, but I'm not sure how else to do it
480            // without allocating an intermediate string. Or writing another
481            // printing API for `PosixTimeZone`. (Which might actually not be
482            // a bad idea. Perhaps using uninit buffers. But... who gives a
483            // fuck about printing POSIX time zone strings?)
484            return write!(crate::fmt::StdFmtWrite(wtr), "{posix_tz}")
485                .map_err(|_| {
486                    Error::from(crate::error::fmt::Error::StdFmtWriteAdapter)
487                });
488        }
489        // Ideally this never actually happens, but it can, and there
490        // are likely system configurations out there in which it does.
491        // I can imagine "lightweight" installations that just have a
492        // `/etc/localtime` as a TZif file that doesn't point to any IANA time
493        // zone. In which case, serializing a time zone probably doesn't make
494        // much sense.
495        //
496        // Anyway, if you're seeing this error and think there should be a
497        // different behavior, please file an issue.
498        Err(Error::from(E::PrintTimeZoneFailure))
499    }
500
501    pub(super) fn print_pieces<W: Write>(
502        &self,
503        pieces: &Pieces,
504        mut wtr: W,
505    ) -> Result<(), Error> {
506        // N.B. We don't bother with writing into the spare capacity of a
507        // `&mut String` here because with `Pieces` it's a little more
508        // complicated to find a more precise upper bound on the length.
509        // Plus, I don't think this API is commonly used, so it's not clear
510        // that it's worth optimizing. But I'm open to a PR with benchmarks
511        // if there's a good use case. ---AG
512        let mut buf = ArrayBuffer::<REASONABLE_PIECES_LEN>::default();
513        let mut bbuf = buf.as_borrowed();
514        let mut wtr = BorrowedWriter::new(&mut bbuf, &mut wtr);
515        self.print_pieces_wtr(pieces, &mut wtr)?;
516        wtr.finish()
517    }
518
519    fn print_pieces_wtr(
520        &self,
521        pieces: &Pieces,
522        wtr: &mut BorrowedWriter<'_, '_, '_>,
523    ) -> Result<(), Error> {
524        if let Some(time) = pieces.time() {
525            let dt = DateTime::from_parts(pieces.date(), time);
526            self.print_datetime_wtr(&dt, wtr)?;
527            if let Some(poffset) = pieces.offset() {
528                self.print_pieces_offset(&poffset, wtr)?;
529            }
530        } else if let Some(poffset) = pieces.offset() {
531            // In this case, we have an offset but no time component. Since
532            // `2025-01-02-05:00` isn't valid, we forcefully write out the
533            // default time (which is what would be assumed anyway).
534            let dt = DateTime::from_parts(pieces.date(), Time::midnight());
535            self.print_datetime_wtr(&dt, wtr)?;
536            self.print_pieces_offset(&poffset, wtr)?;
537        } else {
538            // We have no time and no offset, so we can just write the date.
539            // It's okay to write this followed by an annotation, e.g.,
540            // `2025-01-02[America/New_York]` or even `2025-01-02[-05:00]`.
541            self.print_date_wtr(&pieces.date(), wtr)?;
542        }
543        // For the time zone annotation, a `Pieces` gives us the annotation
544        // name or offset directly, where as with `Zoned`, we have a
545        // `TimeZone`. So we hand-roll our own formatter directly from the
546        // annotation.
547        if let Some(ann) = pieces.time_zone_annotation() {
548            wtr.write_ascii_char(b'[')?;
549            if ann.is_critical() {
550                wtr.write_ascii_char(b'!')?;
551            }
552            match *ann.kind() {
553                TimeZoneAnnotationKind::Named(ref name) => {
554                    wtr.write_str(name.as_str())?;
555                }
556                TimeZoneAnnotationKind::Offset(offset) => {
557                    self.print_offset_rounded_wtr(&offset, wtr)?;
558                }
559            }
560            wtr.write_ascii_char(b']')?;
561        }
562        Ok(())
563    }
564
565    pub(super) fn print_iso_week_date(
566        &self,
567        iso_week_date: &ISOWeekDate,
568        wtr: &mut dyn Write,
569    ) -> Result<(), Error> {
570        BorrowedBuffer::with_writer::<MAX_ISO_WEEK_DATE_LEN>(
571            wtr,
572            MAX_ISO_WEEK_DATE_LEN,
573            |bbuf| Ok(self.print_iso_week_date_buf(iso_week_date, bbuf)),
574        )
575    }
576
577    fn print_iso_week_date_buf(
578        &self,
579        iso_week_date: &ISOWeekDate,
580        bbuf: &mut BorrowedBuffer<'_>,
581    ) {
582        let year = iso_week_date.year();
583        if year < 0 {
584            bbuf.write_str("-00");
585        }
586        bbuf.write_int_pad4(year.unsigned_abs());
587        bbuf.write_ascii_char(b'-');
588        bbuf.write_ascii_char(if self.lowercase { b'w' } else { b'W' });
589        bbuf.write_int_pad2(iso_week_date.week().unsigned_abs());
590        bbuf.write_ascii_char(b'-');
591        bbuf.write_int1(
592            iso_week_date.weekday().to_monday_one_offset().unsigned_abs(),
593        );
594    }
595
596    fn print_pieces_offset(
597        &self,
598        poffset: &PiecesOffset,
599        wtr: &mut BorrowedWriter<'_, '_, '_>,
600    ) -> Result<(), Error> {
601        match *poffset {
602            PiecesOffset::Zulu => self.print_zulu_wtr(wtr),
603            PiecesOffset::Numeric(ref noffset) => {
604                if noffset.offset().is_zero() && noffset.is_negative() {
605                    wtr.write_str("-00:00")
606                } else {
607                    self.print_offset_rounded_wtr(&noffset.offset(), wtr)
608                }
609            }
610        }
611    }
612
613    /// Formats the given offset into the writer given.
614    ///
615    /// If the given offset has non-zero seconds, then they are rounded to
616    /// the nearest minute.
617    fn print_offset_rounded_buf(
618        &self,
619        offset: &Offset,
620        bbuf: &mut BorrowedBuffer<'_>,
621    ) {
622        bbuf.write_ascii_char(if offset.is_negative() { b'-' } else { b'+' });
623        let (offset_hours, offset_minutes) = offset.round_to_nearest_minute();
624        bbuf.write_int_pad2(offset_hours);
625        bbuf.write_ascii_char(b':');
626        bbuf.write_int_pad2(offset_minutes);
627    }
628
629    /// Formats the given offset into the writer given.
630    ///
631    /// If the given offset has non-zero seconds, then they are rounded to
632    /// the nearest minute.
633    fn print_offset_rounded_wtr(
634        &self,
635        offset: &Offset,
636        wtr: &mut BorrowedWriter<'_, '_, '_>,
637    ) -> Result<(), Error> {
638        wtr.write_ascii_char(if offset.is_negative() { b'-' } else { b'+' })?;
639        let (offset_hours, offset_minutes) = offset.round_to_nearest_minute();
640        wtr.write_int_pad2(offset_hours)?;
641        wtr.write_ascii_char(b':')?;
642        wtr.write_int_pad2(offset_minutes)?;
643        Ok(())
644    }
645
646    /// Formats the given offset into the writer given.
647    ///
648    /// If the given offset has non-zero seconds, then they are emitted as a
649    /// third `:`-delimited component of the offset. If seconds are zero, then
650    /// only the hours and minute components are emitted.
651    fn print_offset_full_precision(
652        &self,
653        offset: &Offset,
654        bbuf: &mut BorrowedBuffer<'_>,
655    ) {
656        bbuf.write_ascii_char(if offset.is_negative() { b'-' } else { b'+' });
657        let hours = offset.part_hours().unsigned_abs();
658        let minutes = offset.part_minutes().unsigned_abs();
659        let seconds = offset.part_seconds().unsigned_abs();
660        bbuf.write_int_pad2(hours);
661        bbuf.write_ascii_char(b':');
662        bbuf.write_int_pad2(minutes);
663        if seconds > 0 {
664            bbuf.write_ascii_char(b':');
665            bbuf.write_int_pad2(seconds);
666        }
667    }
668
669    /// Prints the "zulu" indicator.
670    ///
671    /// This should only be used when the offset is not known. For example,
672    /// when printing a `Timestamp`.
673    fn print_zulu_buf(&self, bbuf: &mut BorrowedBuffer<'_>) {
674        bbuf.write_ascii_char(if self.lowercase { b'z' } else { b'Z' });
675    }
676
677    /// Prints the "zulu" indicator.
678    ///
679    /// This should only be used when the offset is not known. For example,
680    /// when printing a `Timestamp`.
681    fn print_zulu_wtr(
682        &self,
683        wtr: &mut BorrowedWriter<'_, '_, '_>,
684    ) -> Result<(), Error> {
685        wtr.write_ascii_char(if self.lowercase { b'z' } else { b'Z' })
686    }
687
688    /// Formats the given time zone name into the writer given as an RFC 9557
689    /// time zone annotation.
690    ///
691    /// When the given time zone is not an IANA time zone name, then the offset
692    /// is printed instead. (This means the offset will be printed twice, which
693    /// is indeed an intended behavior of RFC 9557 for cases where a time zone
694    /// name is not used or unavailable.)
695    fn print_time_zone_annotation_buf(
696        &self,
697        time_zone: &TimeZone,
698        offset: &Offset,
699        bbuf: &mut BorrowedBuffer<'_>,
700    ) {
701        bbuf.write_ascii_char(b'[');
702        if let Some(iana_name) = time_zone.iana_name() {
703            bbuf.write_str(iana_name);
704        } else {
705            self.print_offset_rounded_buf(offset, bbuf);
706        }
707        bbuf.write_ascii_char(b']');
708    }
709
710    /// Formats the given time zone name into the writer given as an RFC 9557
711    /// time zone annotation.
712    ///
713    /// When the given time zone is not an IANA time zone name, then the offset
714    /// is printed instead. (This means the offset will be printed twice, which
715    /// is indeed an intended behavior of RFC 9557 for cases where a time zone
716    /// name is not used or unavailable.)
717    fn print_time_zone_annotation_wtr(
718        &self,
719        time_zone: &TimeZone,
720        offset: &Offset,
721        wtr: &mut BorrowedWriter<'_, '_, '_>,
722    ) -> Result<(), Error> {
723        wtr.write_ascii_char(b'[')?;
724        if let Some(iana_name) = time_zone.iana_name() {
725            wtr.write_str(iana_name)?;
726        } else {
727            self.print_offset_rounded_wtr(offset, wtr)?;
728        }
729        wtr.write_ascii_char(b']')?;
730        Ok(())
731    }
732}
733
734impl Default for DateTimePrinter {
735    fn default() -> DateTimePrinter {
736        DateTimePrinter::new()
737    }
738}
739
740/// A printer for Temporal spans.
741///
742/// Note that in Temporal, a "span" is called a "duration."
743#[derive(Debug)]
744pub(super) struct SpanPrinter {
745    /// The designators to use.
746    designators: &'static Designators,
747}
748
749impl SpanPrinter {
750    /// Create a new Temporal span printer with the default configuration.
751    pub(super) const fn new() -> SpanPrinter {
752        SpanPrinter { designators: DESIGNATORS_UPPERCASE }
753    }
754
755    /// Use lowercase for unit designator labels.
756    ///
757    /// By default, unit designator labels are written in uppercase.
758    pub(super) const fn lowercase(self, yes: bool) -> SpanPrinter {
759        SpanPrinter {
760            designators: if yes {
761                DESIGNATORS_LOWERCASE
762            } else {
763                DESIGNATORS_UPPERCASE
764            },
765        }
766    }
767
768    /// Print the given span to the writer given.
769    ///
770    /// This only returns an error when the given writer returns an error.
771    pub(super) fn print_span<W: Write>(
772        &self,
773        span: &Span,
774        mut wtr: W,
775    ) -> Result<(), Error> {
776        let mut buf = ArrayBuffer::<MAX_SPAN_LEN>::default();
777        let mut bbuf = buf.as_borrowed();
778        self.print_span_impl(span, &mut bbuf);
779        wtr.write_str(bbuf.filled())
780    }
781
782    fn print_span_impl(&self, span: &Span, bbuf: &mut BorrowedBuffer<'_>) {
783        static SUBSECOND: UnitSet = UnitSet::from_slice(&[
784            Unit::Millisecond,
785            Unit::Microsecond,
786            Unit::Nanosecond,
787        ]);
788
789        if span.is_negative() {
790            bbuf.write_ascii_char(b'-');
791        }
792        bbuf.write_ascii_char(b'P');
793
794        let units = span.units();
795        if units.contains(Unit::Year) {
796            bbuf.write_int(span.get_years_unsigned());
797            bbuf.write_ascii_char(self.label(Unit::Year));
798        }
799        if units.contains(Unit::Month) {
800            bbuf.write_int(span.get_months_unsigned());
801            bbuf.write_ascii_char(self.label(Unit::Month));
802        }
803        if units.contains(Unit::Week) {
804            bbuf.write_int(span.get_weeks_unsigned());
805            bbuf.write_ascii_char(self.label(Unit::Week));
806        }
807        if units.contains(Unit::Day) {
808            bbuf.write_int(span.get_days_unsigned());
809            bbuf.write_ascii_char(self.label(Unit::Day));
810        }
811
812        if units.only_time().is_empty() {
813            if units.only_calendar().is_empty() {
814                bbuf.write_ascii_char(b'T');
815                bbuf.write_ascii_char(b'0');
816                bbuf.write_ascii_char(self.label(Unit::Second));
817            }
818            return;
819        }
820
821        bbuf.write_ascii_char(b'T');
822
823        if units.contains(Unit::Hour) {
824            bbuf.write_int(span.get_hours_unsigned());
825            bbuf.write_ascii_char(self.label(Unit::Hour));
826        }
827        if units.contains(Unit::Minute) {
828            bbuf.write_int(span.get_minutes_unsigned());
829            bbuf.write_ascii_char(self.label(Unit::Minute));
830        }
831
832        // ISO 8601 (and Temporal) don't support writing out milliseconds,
833        // microseconds or nanoseconds as separate components like for all
834        // the other units. Instead, they must be incorporated as fractional
835        // seconds. But we only want to do that work if we need to.
836        let has_subsecond = !units.intersection(SUBSECOND).is_empty();
837        if units.contains(Unit::Second) && !has_subsecond {
838            bbuf.write_int(span.get_seconds_unsigned());
839            bbuf.write_ascii_char(self.label(Unit::Second));
840        } else if has_subsecond {
841            // We want to combine our seconds, milliseconds, microseconds and
842            // nanoseconds into one single value in terms of nanoseconds. Then
843            // we can "balance" that out so that we have a number of seconds
844            // and a number of nanoseconds not greater than 1 second. (Which is
845            // our fraction.)
846            let (seconds, millis, micros, nanos) = (
847                Duration::from_secs(span.get_seconds_unsigned()),
848                Duration::from_millis(span.get_milliseconds_unsigned()),
849                Duration::from_micros(span.get_microseconds_unsigned()),
850                Duration::from_nanos(span.get_nanoseconds_unsigned()),
851            );
852            // OK because the maximums for a span's seconds, millis, micros
853            // and nanos combined all fit into a 96-bit integer. (This is
854            // guaranteed by `Span::to_duration_invariant`.)
855            let total = seconds + millis + micros + nanos;
856            let (secs, subsecs) = (total.as_secs(), total.subsec_nanos());
857            bbuf.write_int(secs);
858            if subsecs != 0 {
859                bbuf.write_ascii_char(b'.');
860                bbuf.write_fraction(None, subsecs);
861            }
862            bbuf.write_ascii_char(self.label(Unit::Second));
863        }
864    }
865
866    /// Print the given signed duration to the writer given.
867    ///
868    /// This only returns an error when the given writer returns an error.
869    pub(super) fn print_signed_duration<W: Write>(
870        &self,
871        dur: &SignedDuration,
872        mut wtr: W,
873    ) -> Result<(), Error> {
874        let mut buf = ArrayBuffer::<MAX_DURATION_LEN>::default();
875        let mut bbuf = buf.as_borrowed();
876        if dur.is_negative() {
877            bbuf.write_ascii_char(b'-');
878        }
879        self.print_unsigned_duration_impl(&dur.unsigned_abs(), &mut bbuf);
880        wtr.write_str(bbuf.filled())
881    }
882
883    /// Print the given unsigned duration to the writer given.
884    ///
885    /// This only returns an error when the given writer returns an error.
886    pub(super) fn print_unsigned_duration<W: Write>(
887        &self,
888        dur: &Duration,
889        mut wtr: W,
890    ) -> Result<(), Error> {
891        let mut buf = ArrayBuffer::<MAX_DURATION_LEN>::default();
892        let mut bbuf = buf.as_borrowed();
893        self.print_unsigned_duration_impl(dur, &mut bbuf);
894        wtr.write_str(bbuf.filled())
895    }
896
897    fn print_unsigned_duration_impl(
898        &self,
899        dur: &Duration,
900        bbuf: &mut BorrowedBuffer<'_>,
901    ) {
902        bbuf.write_ascii_char(b'P');
903        bbuf.write_ascii_char(b'T');
904
905        let (mut secs, nanos) = (dur.as_secs(), dur.subsec_nanos());
906        let non_zero_greater_than_second = secs >= 60;
907        if non_zero_greater_than_second {
908            let hours = secs / (60 * 60);
909            secs %= 60 * 60;
910            let minutes = secs / 60;
911            secs = secs % 60;
912            if hours != 0 {
913                bbuf.write_int(hours);
914                bbuf.write_ascii_char(self.label(Unit::Hour));
915            }
916            if minutes != 0 {
917                bbuf.write_int(minutes);
918                bbuf.write_ascii_char(self.label(Unit::Minute));
919            }
920        }
921        if !non_zero_greater_than_second || secs != 0 || nanos != 0 {
922            bbuf.write_int(secs);
923            if nanos != 0 {
924                bbuf.write_ascii_char(b'.');
925                bbuf.write_fraction(None, nanos);
926            }
927            bbuf.write_ascii_char(self.label(Unit::Second));
928        }
929    }
930
931    /// Converts the ASCII uppercase unit designator label to lowercase if this
932    /// printer is configured to use lowercase. Otherwise the label is returned
933    /// unchanged.
934    fn label(&self, unit: Unit) -> u8 {
935        self.designators.designator(unit)
936    }
937}
938
939#[derive(Clone, Debug)]
940struct Designators {
941    // Indexed by `Unit as usize`
942    map: [u8; 10],
943}
944
945const DESIGNATORS_UPPERCASE: &'static Designators = &Designators {
946    // N.B. ISO 8601 duration format doesn't have unit
947    // designators for sub-second units.
948    map: [0, 0, 0, b'S', b'M', b'H', b'D', b'W', b'M', b'Y'],
949};
950
951const DESIGNATORS_LOWERCASE: &'static Designators = &Designators {
952    // N.B. ISO 8601 duration format doesn't have unit
953    // designators for sub-second units.
954    map: [0, 0, 0, b's', b'm', b'h', b'd', b'w', b'm', b'y'],
955};
956
957impl Designators {
958    /// Returns the designator for the given unit.
959    fn designator(&self, unit: Unit) -> u8 {
960        self.map[unit as usize]
961    }
962}
963
964#[cfg(feature = "alloc")]
965#[cfg(test)]
966mod tests {
967    use alloc::string::{String, ToString};
968
969    use crate::{
970        civil::{date, time, Weekday},
971        fmt::StdFmtWrite,
972        span::ToSpan,
973        util::b,
974    };
975
976    use super::*;
977
978    #[test]
979    fn print_zoned() {
980        if crate::tz::db().is_definitively_empty() {
981            return;
982        }
983
984        let p = || DateTimePrinter::new();
985        let via_io = |dtp: DateTimePrinter, zdt| {
986            let mut buf = String::new();
987            dtp.print_zoned(&zdt, &mut StdFmtWrite(&mut buf)).unwrap();
988            buf
989        };
990        let to_string = |dtp: DateTimePrinter, zdt| {
991            let mut buf = String::new();
992            dtp.print_zoned(&zdt, &mut buf).unwrap();
993            let got_via_io = via_io(dtp, zdt);
994            assert_eq!(
995                buf, got_via_io,
996                "expected writes to `&mut String` to match `&mut StdFmtWrite`"
997            );
998            buf
999        };
1000
1001        let dt = date(2024, 3, 10).at(5, 34, 45, 0);
1002        let zdt = dt.in_tz("America/New_York").unwrap();
1003        let got = to_string(p(), zdt);
1004        assert_eq!(got, "2024-03-10T05:34:45-04:00[America/New_York]");
1005
1006        let dt = date(2024, 3, 10).at(5, 34, 45, 0);
1007        let zdt = dt.in_tz("America/New_York").unwrap();
1008        let zdt = zdt.with_time_zone(TimeZone::UTC);
1009        let got = to_string(p(), zdt);
1010        assert_eq!(got, "2024-03-10T09:34:45+00:00[UTC]");
1011
1012        let dt = date(2024, 3, 10).at(5, 34, 45, 0);
1013        let zdt = dt.to_zoned(TimeZone::fixed(Offset::MIN)).unwrap();
1014        let got = to_string(p(), zdt);
1015        assert_eq!(got, "2024-03-10T05:34:45-25:59[-25:59]");
1016
1017        let dt = date(2024, 3, 10).at(5, 34, 45, 0);
1018        let zdt = dt.to_zoned(TimeZone::fixed(Offset::MAX)).unwrap();
1019        let got = to_string(p(), zdt);
1020        assert_eq!(got, "2024-03-10T05:34:45+25:59[+25:59]");
1021
1022        let dt = date(2024, 3, 10).at(5, 34, 45, 123_456_789);
1023        let zdt = dt.in_tz("America/New_York").unwrap();
1024        let got = to_string(p(), zdt);
1025        assert_eq!(
1026            got,
1027            "2024-03-10T05:34:45.123456789-04:00[America/New_York]"
1028        );
1029
1030        let dt = date(2024, 3, 10).at(5, 34, 45, 0);
1031        let zdt = dt.in_tz("America/New_York").unwrap();
1032        let got = to_string(p().precision(Some(9)), zdt);
1033        assert_eq!(
1034            got,
1035            "2024-03-10T05:34:45.000000000-04:00[America/New_York]"
1036        );
1037
1038        let dt = date(-9999, 3, 1).at(23, 59, 59, 999_999_999);
1039        let zdt = dt.in_tz("America/Argentina/ComodRivadavia").unwrap();
1040        let got = to_string(p().precision(Some(9)), zdt);
1041        assert_eq!(
1042            got,
1043            "-009999-03-01T23:59:59.999999999-04:23[America/Argentina/ComodRivadavia]",
1044        );
1045
1046        // Inject a very long IANA tzdb identifier to ensure it's handled
1047        // properly.
1048        let tz = TimeZone::tzif(
1049            "Abc/Def/Ghi/Jkl/Mno/Pqr/Stu/Vwx/Yzz/Abc/Def/Ghi/Jkl/Mno/Pqr/Stu/Vwx/Yzz/Abc/Def/Ghi/Jkl/Mno/Pqr/Stu/Vwx/Yzz",
1050            crate::tz::testdata::TzifTestFile::get("America/New_York").data,
1051        ).unwrap();
1052        let dt = date(-9999, 3, 1).at(23, 59, 59, 999_999_999);
1053        let zdt = dt.to_zoned(tz).unwrap();
1054        let got = to_string(p().precision(Some(9)), zdt);
1055        assert_eq!(
1056            got,
1057            "-009999-03-01T23:59:59.999999999-04:56[Abc/Def/Ghi/Jkl/Mno/Pqr/Stu/Vwx/Yzz/Abc/Def/Ghi/Jkl/Mno/Pqr/Stu/Vwx/Yzz/Abc/Def/Ghi/Jkl/Mno/Pqr/Stu/Vwx/Yzz]",
1058        );
1059    }
1060
1061    #[test]
1062    fn print_timestamp_zulu() {
1063        let p = || DateTimePrinter::new();
1064        let via_io = |dtp: DateTimePrinter, ts| {
1065            let mut buf = String::new();
1066            dtp.print_timestamp(&ts, &mut StdFmtWrite(&mut buf)).unwrap();
1067            buf
1068        };
1069        let to_string = |dtp: DateTimePrinter, ts| {
1070            let mut buf = String::new();
1071            dtp.print_timestamp(&ts, &mut buf).unwrap();
1072            let got_via_io = via_io(dtp, ts);
1073            assert_eq!(
1074                buf, got_via_io,
1075                "expected writes to `&mut String` to match `&mut StdFmtWrite`"
1076            );
1077            buf
1078        };
1079
1080        let tz = TimeZone::fixed(-Offset::constant(4));
1081
1082        let dt = date(2024, 3, 10).at(5, 34, 45, 0);
1083        let zoned: Zoned = dt.to_zoned(tz.clone()).unwrap();
1084        let got = to_string(p(), zoned.timestamp());
1085        assert_eq!(got, "2024-03-10T09:34:45Z");
1086
1087        let dt = date(-2024, 3, 10).at(5, 34, 45, 0);
1088        let zoned: Zoned = dt.to_zoned(tz.clone()).unwrap();
1089        let got = to_string(p(), zoned.timestamp());
1090        assert_eq!(got, "-002024-03-10T09:34:45Z");
1091
1092        let dt = date(2024, 3, 10).at(5, 34, 45, 123_456_789);
1093        let zoned: Zoned = dt.to_zoned(tz.clone()).unwrap();
1094        let got = to_string(p(), zoned.timestamp());
1095        assert_eq!(got, "2024-03-10T09:34:45.123456789Z");
1096
1097        let dt = date(2024, 3, 10).at(5, 34, 45, 0);
1098        let zoned: Zoned = dt.to_zoned(tz.clone()).unwrap();
1099        let got = to_string(p().precision(Some(9)), zoned.timestamp());
1100        assert_eq!(got, "2024-03-10T09:34:45.000000000Z");
1101    }
1102
1103    #[test]
1104    fn print_timestamp_with_offset() {
1105        let p = || DateTimePrinter::new();
1106        let via_io = |dtp: DateTimePrinter, ts, offset| {
1107            let mut buf = String::new();
1108            dtp.print_timestamp_with_offset(
1109                &ts,
1110                offset,
1111                &mut StdFmtWrite(&mut buf),
1112            )
1113            .unwrap();
1114            buf
1115        };
1116        let to_string = |dtp: DateTimePrinter, ts, offset| {
1117            let mut buf = String::new();
1118            dtp.print_timestamp_with_offset(&ts, offset, &mut buf).unwrap();
1119            let got_via_io = via_io(dtp, ts, offset);
1120            assert_eq!(
1121                buf, got_via_io,
1122                "expected writes to `&mut String` to match `&mut StdFmtWrite`"
1123            );
1124            buf
1125        };
1126
1127        let tz = TimeZone::fixed(-Offset::constant(4));
1128
1129        let dt = date(2024, 3, 10).at(5, 34, 45, 0);
1130        let zoned: Zoned = dt.to_zoned(tz.clone()).unwrap();
1131        let got = to_string(p(), zoned.timestamp(), zoned.offset());
1132        assert_eq!(got, "2024-03-10T05:34:45-04:00");
1133
1134        let dt = date(-2024, 3, 10).at(5, 34, 45, 0);
1135        let zoned: Zoned = dt.to_zoned(tz.clone()).unwrap();
1136        let got = to_string(p(), zoned.timestamp(), zoned.offset());
1137        assert_eq!(got, "-002024-03-10T05:34:45-04:00");
1138
1139        let dt = date(2024, 3, 10).at(5, 34, 45, 123_456_789);
1140        let zoned: Zoned = dt.to_zoned(tz.clone()).unwrap();
1141        let got = to_string(p(), zoned.timestamp(), zoned.offset());
1142        assert_eq!(got, "2024-03-10T05:34:45.123456789-04:00");
1143
1144        let dt = date(2024, 3, 10).at(5, 34, 45, 0);
1145        let zoned: Zoned = dt.to_zoned(tz.clone()).unwrap();
1146        let got = to_string(
1147            p().precision(Some(9)),
1148            zoned.timestamp(),
1149            zoned.offset(),
1150        );
1151        assert_eq!(got, "2024-03-10T05:34:45.000000000-04:00");
1152
1153        let dt = DateTime::MIN;
1154        let zoned: Zoned = dt.to_zoned(TimeZone::fixed(Offset::MIN)).unwrap();
1155        let got = to_string(
1156            p().precision(Some(9)),
1157            zoned.timestamp(),
1158            zoned.offset(),
1159        );
1160        assert_eq!(got, "-009999-01-01T00:00:00.000000000-25:59");
1161    }
1162
1163    #[test]
1164    fn print_datetime() {
1165        let p = || DateTimePrinter::new();
1166        let via_io = |dtp: DateTimePrinter, dt| {
1167            let mut buf = String::new();
1168            dtp.print_datetime(&dt, &mut StdFmtWrite(&mut buf)).unwrap();
1169            buf
1170        };
1171        let to_string = |dtp: DateTimePrinter, dt| {
1172            let mut buf = String::new();
1173            dtp.print_datetime(&dt, &mut buf).unwrap();
1174            let got_via_io = via_io(dtp, dt);
1175            assert_eq!(
1176                buf, got_via_io,
1177                "expected writes to `&mut String` to match `&mut StdFmtWrite`"
1178            );
1179            buf
1180        };
1181
1182        let dt = date(2024, 3, 10).at(5, 34, 45, 0);
1183        let got = to_string(p(), dt);
1184        assert_eq!(got, "2024-03-10T05:34:45");
1185
1186        let dt = date(-2024, 3, 10).at(5, 34, 45, 0);
1187        let got = to_string(p(), dt);
1188        assert_eq!(got, "-002024-03-10T05:34:45");
1189
1190        let dt = date(2024, 3, 10).at(5, 34, 45, 123_456_789);
1191        let got = to_string(p(), dt);
1192        assert_eq!(got, "2024-03-10T05:34:45.123456789");
1193
1194        let dt = date(2024, 3, 10).at(5, 34, 45, 0);
1195        let got = to_string(p().precision(Some(9)), dt);
1196        assert_eq!(got, "2024-03-10T05:34:45.000000000");
1197
1198        let dt = DateTime::MIN;
1199        let got = to_string(p().precision(Some(9)), dt);
1200        assert_eq!(got, "-009999-01-01T00:00:00.000000000");
1201    }
1202
1203    #[test]
1204    fn print_date() {
1205        let p = || DateTimePrinter::new();
1206        let via_io = |dtp: DateTimePrinter, date| {
1207            let mut buf = String::new();
1208            dtp.print_date(&date, &mut StdFmtWrite(&mut buf)).unwrap();
1209            buf
1210        };
1211        let to_string = |dtp: DateTimePrinter, date| {
1212            let mut buf = String::new();
1213            dtp.print_date(&date, &mut buf).unwrap();
1214            let got_via_io = via_io(dtp, date);
1215            assert_eq!(
1216                buf, got_via_io,
1217                "expected writes to `&mut String` to match `&mut StdFmtWrite`"
1218            );
1219            buf
1220        };
1221
1222        let d = date(2024, 3, 10);
1223        let got = to_string(p(), d);
1224        assert_eq!(got, "2024-03-10");
1225
1226        let d = date(-2024, 3, 10);
1227        let got = to_string(p(), d);
1228        assert_eq!(got, "-002024-03-10");
1229
1230        let d = date(2024, 3, 10);
1231        let got = to_string(p(), d);
1232        assert_eq!(got, "2024-03-10");
1233
1234        let d = Date::MIN;
1235        let got = to_string(p().precision(Some(9)), d);
1236        assert_eq!(got, "-009999-01-01");
1237    }
1238
1239    #[test]
1240    fn print_time() {
1241        let p = || DateTimePrinter::new();
1242        let via_io = |dtp: DateTimePrinter, time| {
1243            let mut buf = String::new();
1244            dtp.print_time(&time, &mut StdFmtWrite(&mut buf)).unwrap();
1245            buf
1246        };
1247        let to_string = |dtp: DateTimePrinter, time| {
1248            let mut buf = String::new();
1249            dtp.print_time(&time, &mut buf).unwrap();
1250            let got_via_io = via_io(dtp, time);
1251            assert_eq!(
1252                buf, got_via_io,
1253                "expected writes to `&mut String` to match `&mut StdFmtWrite`"
1254            );
1255            buf
1256        };
1257
1258        let t = time(5, 34, 45, 0);
1259        let got = to_string(p(), t);
1260        assert_eq!(got, "05:34:45");
1261
1262        let t = time(5, 34, 45, 0);
1263        let got = to_string(p(), t);
1264        assert_eq!(got, "05:34:45");
1265
1266        let t = time(5, 34, 45, 123_456_789);
1267        let got = to_string(p(), t);
1268        assert_eq!(got, "05:34:45.123456789");
1269
1270        let t = time(5, 34, 45, 0);
1271        let got = to_string(p().precision(Some(9)), t);
1272        assert_eq!(got, "05:34:45.000000000");
1273
1274        let t = Time::MIN;
1275        let got = to_string(p().precision(Some(9)), t);
1276        assert_eq!(got, "00:00:00.000000000");
1277
1278        let t = Time::MAX;
1279        let got = to_string(p().precision(Some(9)), t);
1280        assert_eq!(got, "23:59:59.999999999");
1281    }
1282
1283    #[test]
1284    fn print_iso_week_date() {
1285        let p = || DateTimePrinter::new();
1286        let via_io = |dtp: DateTimePrinter, date| {
1287            let mut buf = String::new();
1288            dtp.print_iso_week_date(&date, &mut StdFmtWrite(&mut buf))
1289                .unwrap();
1290            buf
1291        };
1292        let to_string = |dtp: DateTimePrinter, date| {
1293            let mut buf = String::new();
1294            dtp.print_iso_week_date(&date, &mut buf).unwrap();
1295            let got_via_io = via_io(dtp, date);
1296            assert_eq!(
1297                buf, got_via_io,
1298                "expected writes to `&mut String` to match `&mut StdFmtWrite`"
1299            );
1300            buf
1301        };
1302
1303        let d = ISOWeekDate::new(2024, 52, Weekday::Monday).unwrap();
1304        let got = to_string(p(), d);
1305        assert_eq!(got, "2024-W52-1");
1306
1307        let d = ISOWeekDate::new(2004, 1, Weekday::Sunday).unwrap();
1308        let got = to_string(p(), d);
1309        assert_eq!(got, "2004-W01-7");
1310
1311        let d = ISOWeekDate::MIN;
1312        let got = to_string(p(), d);
1313        assert_eq!(got, "-009999-W01-1");
1314
1315        let d = ISOWeekDate::MAX;
1316        let got = to_string(p(), d);
1317        assert_eq!(got, "9999-W52-5");
1318    }
1319
1320    #[test]
1321    fn print_pieces() {
1322        let p = || DateTimePrinter::new();
1323        let via_io = |dtp: DateTimePrinter, pieces| {
1324            let mut buf = String::new();
1325            dtp.print_pieces(&pieces, &mut StdFmtWrite(&mut buf)).unwrap();
1326            buf
1327        };
1328        let to_string = |dtp: DateTimePrinter, pieces| {
1329            let mut buf = String::new();
1330            dtp.print_pieces(&pieces, &mut buf).unwrap();
1331            let got_via_io = via_io(dtp, pieces);
1332            assert_eq!(
1333                buf, got_via_io,
1334                "expected writes to `&mut String` to match `&mut StdFmtWrite`"
1335            );
1336            buf
1337        };
1338
1339        let pieces = Pieces::from(date(2024, 3, 10).at(5, 34, 45, 0))
1340            .with_offset(Offset::constant(-4))
1341            .with_time_zone_name("America/New_York");
1342        let got = to_string(p(), pieces);
1343        assert_eq!(got, "2024-03-10T05:34:45-04:00[America/New_York]");
1344
1345        let pieces = Pieces::from(date(2024, 3, 10).at(5, 34, 45, 0))
1346            .with_offset(Offset::UTC)
1347            .with_time_zone_name("UTC");
1348        let got = to_string(p(), pieces);
1349        assert_eq!(got, "2024-03-10T05:34:45+00:00[UTC]");
1350
1351        let pieces = Pieces::from(date(2024, 3, 10).at(5, 34, 45, 0))
1352            .with_offset(Offset::MIN)
1353            .with_time_zone_offset(Offset::MIN);
1354        let got = to_string(p(), pieces);
1355        assert_eq!(got, "2024-03-10T05:34:45-25:59[-25:59]");
1356
1357        let pieces = Pieces::from(date(2024, 3, 10).at(5, 34, 45, 0))
1358            .with_offset(Offset::MAX)
1359            .with_time_zone_offset(Offset::MAX);
1360        let got = to_string(p(), pieces);
1361        assert_eq!(got, "2024-03-10T05:34:45+25:59[+25:59]");
1362
1363        let pieces =
1364            Pieces::from(date(2024, 3, 10).at(5, 34, 45, 123_456_789))
1365                .with_offset(Offset::constant(-4))
1366                .with_time_zone_name("America/New_York");
1367        let got = to_string(p(), pieces);
1368        assert_eq!(
1369            got,
1370            "2024-03-10T05:34:45.123456789-04:00[America/New_York]"
1371        );
1372
1373        let pieces = Pieces::from(date(2024, 3, 10).at(5, 34, 45, 0))
1374            .with_offset(Offset::constant(-4))
1375            .with_time_zone_name("America/New_York");
1376        let got = to_string(p().precision(Some(9)), pieces);
1377        assert_eq!(
1378            got,
1379            "2024-03-10T05:34:45.000000000-04:00[America/New_York]"
1380        );
1381
1382        let pieces =
1383            Pieces::from(date(-9999, 3, 1).at(23, 59, 59, 999_999_999))
1384                .with_offset(Offset::constant(-4))
1385                .with_time_zone_name("America/Argentina/ComodRivadavia");
1386        let got = to_string(p().precision(Some(9)), pieces);
1387        assert_eq!(
1388            got,
1389            "-009999-03-01T23:59:59.999999999-04:00[America/Argentina/ComodRivadavia]",
1390        );
1391
1392        let pieces =
1393            Pieces::parse(
1394                "-009999-03-01T23:59:59.999999999-04:00[!America/Argentina/ComodRivadavia]",
1395            ).unwrap();
1396        let got = to_string(p().precision(Some(9)), pieces);
1397        assert_eq!(
1398            got,
1399            "-009999-03-01T23:59:59.999999999-04:00[!America/Argentina/ComodRivadavia]",
1400        );
1401
1402        // Inject a very long IANA tzdb identifier to ensure it's handled
1403        let name = "Abc/Def/Ghi/Jkl/Mno/Pqr/Stu/Vwx/Yzz/Abc/Def/Ghi/Jkl/Mno/Pqr/Stu/Vwx/Yzz/Abc/Def/Ghi/Jkl/Mno/Pqr/Stu/Vwx/Yzz";
1404        let pieces =
1405            Pieces::from(date(-9999, 3, 1).at(23, 59, 59, 999_999_999))
1406                .with_offset(Offset::constant(-4))
1407                .with_time_zone_name(name);
1408        let got = to_string(p().precision(Some(9)), pieces);
1409        assert_eq!(
1410            got,
1411            "-009999-03-01T23:59:59.999999999-04:00[Abc/Def/Ghi/Jkl/Mno/Pqr/Stu/Vwx/Yzz/Abc/Def/Ghi/Jkl/Mno/Pqr/Stu/Vwx/Yzz/Abc/Def/Ghi/Jkl/Mno/Pqr/Stu/Vwx/Yzz]",
1412        );
1413    }
1414
1415    #[test]
1416    fn print_time_zone() {
1417        let p = || DateTimePrinter::new();
1418        let via_io = |dtp: DateTimePrinter, tz| {
1419            let mut buf = String::new();
1420            dtp.print_time_zone(&tz, &mut StdFmtWrite(&mut buf)).unwrap();
1421            buf
1422        };
1423        let to_string = |dtp: DateTimePrinter, tz| {
1424            let mut buf = String::new();
1425            dtp.print_time_zone(&tz, &mut buf).unwrap();
1426            let got_via_io = via_io(dtp, tz);
1427            assert_eq!(
1428                buf, got_via_io,
1429                "expected writes to `&mut String` to match `&mut StdFmtWrite`"
1430            );
1431            buf
1432        };
1433
1434        let tztest =
1435            crate::tz::testdata::TzifTestFile::get("America/New_York");
1436        let tz = TimeZone::tzif(tztest.name, tztest.data).unwrap();
1437        let got = to_string(p(), tz);
1438        assert_eq!(got, "America/New_York");
1439
1440        // Inject a very long IANA tzdb identifier to ensure it's handled
1441        // properly.
1442        let tz = TimeZone::tzif(
1443            "Abc/Def/Ghi/Jkl/Mno/Pqr/Stu/Vwx/Yzz/Abc/Def/Ghi/Jkl/Mno/Pqr/Stu/Vwx/Yzz/Abc/Def/Ghi/Jkl/Mno/Pqr/Stu/Vwx/Yzz",
1444            tztest.data,
1445        ).unwrap();
1446        let got = to_string(p(), tz);
1447        assert_eq!(
1448            got,
1449            "Abc/Def/Ghi/Jkl/Mno/Pqr/Stu/Vwx/Yzz/Abc/Def/Ghi/Jkl/Mno/Pqr/Stu/Vwx/Yzz/Abc/Def/Ghi/Jkl/Mno/Pqr/Stu/Vwx/Yzz",
1450        );
1451
1452        let tz = TimeZone::UTC;
1453        let got = to_string(p(), tz);
1454        assert_eq!(got, "UTC");
1455
1456        let tz = TimeZone::unknown();
1457        let got = to_string(p(), tz);
1458        assert_eq!(got, "Etc/Unknown");
1459
1460        let tz = TimeZone::fixed(Offset::MIN);
1461        let got = to_string(p(), tz);
1462        assert_eq!(got, "-25:59:59");
1463
1464        let tz = TimeZone::fixed(Offset::MAX);
1465        let got = to_string(p(), tz);
1466        assert_eq!(got, "+25:59:59");
1467
1468        let tz = TimeZone::posix("EST5EDT,M3.2.0,M11.1.0").unwrap();
1469        let got = to_string(p(), tz);
1470        assert_eq!(got, "EST5EDT,M3.2.0,M11.1.0");
1471
1472        let tz = TimeZone::posix(
1473            "ABCDEFGHIJKLMNOPQRSTUVWXY5ABCDEFGHIJKLMNOPQRSTUVWXYT,M3.2.0,M11.1.0",
1474        ).unwrap();
1475        let got = to_string(p(), tz);
1476        assert_eq!(
1477            got,
1478            "ABCDEFGHIJKLMNOPQRSTUVWXY5ABCDEFGHIJKLMNOPQRSTUVWXYT,M3.2.0,M11.1.0",
1479        );
1480
1481        // This isn't a public API, but this lets us test the error
1482        // case: a valid time zone but without a succinct name.
1483        #[cfg(feature = "tz-system")]
1484        {
1485            let tz = TimeZone::tzif_system(tztest.data).unwrap();
1486            let mut buf = String::new();
1487            let err = p().print_time_zone(&tz, &mut buf).unwrap_err();
1488            assert_eq!(
1489                err.to_string(),
1490                "time zones without IANA identifiers that aren't \
1491                 either fixed offsets or a POSIX time zone can't be \
1492                 serialized (this typically occurs when this is a \
1493                 system time zone derived from `/etc/localtime` on \
1494                 Unix systems that isn't symlinked to an entry in \
1495                 `/usr/share/zoneinfo`)",
1496            );
1497        }
1498    }
1499
1500    #[cfg(not(miri))]
1501    #[test]
1502    fn print_span_basic() {
1503        let p = |span: Span| -> String {
1504            let mut buf = String::new();
1505            SpanPrinter::new().print_span(&span, &mut buf).unwrap();
1506            buf
1507        };
1508
1509        insta::assert_snapshot!(p(Span::new()), @"PT0S");
1510        insta::assert_snapshot!(p(1.second()), @"PT1S");
1511        insta::assert_snapshot!(p(-1.second()), @"-PT1S");
1512        insta::assert_snapshot!(p(
1513            1.second().milliseconds(1).microseconds(1).nanoseconds(1),
1514        ), @"PT1.001001001S");
1515        insta::assert_snapshot!(p(
1516            0.second().milliseconds(999).microseconds(999).nanoseconds(999),
1517        ), @"PT0.999999999S");
1518        insta::assert_snapshot!(p(
1519            1.year().months(1).weeks(1).days(1)
1520            .hours(1).minutes(1).seconds(1)
1521            .milliseconds(1).microseconds(1).nanoseconds(1),
1522        ), @"P1Y1M1W1DT1H1M1.001001001S");
1523        insta::assert_snapshot!(p(
1524            -1.year().months(1).weeks(1).days(1)
1525            .hours(1).minutes(1).seconds(1)
1526            .milliseconds(1).microseconds(1).nanoseconds(1),
1527        ), @"-P1Y1M1W1DT1H1M1.001001001S");
1528    }
1529
1530    #[cfg(not(miri))]
1531    #[test]
1532    fn print_span_subsecond_positive() {
1533        let p = |span: Span| -> String {
1534            let mut buf = String::new();
1535            SpanPrinter::new().print_span(&span, &mut buf).unwrap();
1536            buf
1537        };
1538
1539        // These are all sub-second trickery tests.
1540        insta::assert_snapshot!(p(
1541            0.second().milliseconds(1000).microseconds(1000).nanoseconds(1000),
1542        ), @"PT1.001001S");
1543        insta::assert_snapshot!(p(
1544            1.second().milliseconds(1000).microseconds(1000).nanoseconds(1000),
1545        ), @"PT2.001001S");
1546        insta::assert_snapshot!(p(
1547            0.second()
1548            .milliseconds(b::SpanMilliseconds::MAX),
1549        ), @"PT631107417600S");
1550        insta::assert_snapshot!(p(
1551            0.second()
1552            .microseconds(b::SpanMicroseconds::MAX),
1553        ), @"PT631107417600S");
1554        insta::assert_snapshot!(p(
1555            0.second()
1556            .nanoseconds(b::SpanNanoseconds::MAX),
1557        ), @"PT9223372036.854775807S");
1558
1559        insta::assert_snapshot!(p(
1560            0.second()
1561            .milliseconds(b::SpanMilliseconds::MAX)
1562            .microseconds(999_999),
1563        ), @"PT631107417600.999999S");
1564        // This is 1 microsecond more than the maximum number of seconds
1565        // representable in a span.
1566        insta::assert_snapshot!(p(
1567            0.second()
1568            .milliseconds(b::SpanMilliseconds::MAX)
1569            .microseconds(1_000_000),
1570        ), @"PT631107417601S");
1571        insta::assert_snapshot!(p(
1572            0.second()
1573            .milliseconds(b::SpanMilliseconds::MAX)
1574            .microseconds(1_000_001),
1575        ), @"PT631107417601.000001S");
1576        // This is 1 nanosecond more than the maximum number of seconds
1577        // representable in a span.
1578        insta::assert_snapshot!(p(
1579            0.second()
1580            .milliseconds(b::SpanMilliseconds::MAX)
1581            .nanoseconds(1_000_000_000),
1582        ), @"PT631107417601S");
1583        insta::assert_snapshot!(p(
1584            0.second()
1585            .milliseconds(b::SpanMilliseconds::MAX)
1586            .nanoseconds(1_000_000_001),
1587        ), @"PT631107417601.000000001S");
1588
1589        // The max millis, micros and nanos, combined.
1590        insta::assert_snapshot!(p(
1591            0.second()
1592            .milliseconds(b::SpanMilliseconds::MAX)
1593            .microseconds(b::SpanMicroseconds::MAX)
1594            .nanoseconds(b::SpanNanoseconds::MAX),
1595        ), @"PT1271438207236.854775807S");
1596        // The max seconds, millis, micros and nanos, combined.
1597        insta::assert_snapshot!(p(
1598            Span::new()
1599            .seconds(b::SpanSeconds::MAX)
1600            .milliseconds(b::SpanMilliseconds::MAX)
1601            .microseconds(b::SpanMicroseconds::MAX)
1602            .nanoseconds(b::SpanNanoseconds::MAX),
1603        ), @"PT1902545624836.854775807S");
1604    }
1605
1606    #[cfg(not(miri))]
1607    #[test]
1608    fn print_span_subsecond_negative() {
1609        let p = |span: Span| -> String {
1610            let mut buf = String::new();
1611            SpanPrinter::new().print_span(&span, &mut buf).unwrap();
1612            buf
1613        };
1614
1615        // These are all sub-second trickery tests.
1616        insta::assert_snapshot!(p(
1617            -0.second().milliseconds(1000).microseconds(1000).nanoseconds(1000),
1618        ), @"-PT1.001001S");
1619        insta::assert_snapshot!(p(
1620            -1.second().milliseconds(1000).microseconds(1000).nanoseconds(1000),
1621        ), @"-PT2.001001S");
1622        insta::assert_snapshot!(p(
1623            0.second()
1624            .milliseconds(b::SpanMilliseconds::MIN),
1625        ), @"-PT631107417600S");
1626        insta::assert_snapshot!(p(
1627            0.second()
1628            .microseconds(b::SpanMicroseconds::MIN),
1629        ), @"-PT631107417600S");
1630        insta::assert_snapshot!(p(
1631            0.second()
1632            .nanoseconds(b::SpanNanoseconds::MIN),
1633        ), @"-PT9223372036.854775807S");
1634
1635        insta::assert_snapshot!(p(
1636            0.second()
1637            .milliseconds(b::SpanMilliseconds::MIN)
1638            .microseconds(999_999),
1639        ), @"-PT631107417600.999999S");
1640        // This is 1 microsecond more than the maximum number of seconds
1641        // representable in a span.
1642        insta::assert_snapshot!(p(
1643            0.second()
1644            .milliseconds(b::SpanMilliseconds::MIN)
1645            .microseconds(1_000_000),
1646        ), @"-PT631107417601S");
1647        insta::assert_snapshot!(p(
1648            0.second()
1649            .milliseconds(b::SpanMilliseconds::MIN)
1650            .microseconds(1_000_001),
1651        ), @"-PT631107417601.000001S");
1652        // This is 1 nanosecond more than the maximum number of seconds
1653        // representable in a span.
1654        insta::assert_snapshot!(p(
1655            0.second()
1656            .milliseconds(b::SpanMilliseconds::MIN)
1657            .nanoseconds(1_000_000_000),
1658        ), @"-PT631107417601S");
1659        insta::assert_snapshot!(p(
1660            0.second()
1661            .milliseconds(b::SpanMilliseconds::MIN)
1662            .nanoseconds(1_000_000_001),
1663        ), @"-PT631107417601.000000001S");
1664
1665        // The max millis, micros and nanos, combined.
1666        insta::assert_snapshot!(p(
1667            0.second()
1668            .milliseconds(b::SpanMilliseconds::MIN)
1669            .microseconds(b::SpanMicroseconds::MIN)
1670            .nanoseconds(b::SpanNanoseconds::MIN),
1671        ), @"-PT1271438207236.854775807S");
1672        // The max seconds, millis, micros and nanos, combined.
1673        insta::assert_snapshot!(p(
1674            Span::new()
1675            .seconds(b::SpanSeconds::MIN)
1676            .milliseconds(b::SpanMilliseconds::MIN)
1677            .microseconds(b::SpanMicroseconds::MIN)
1678            .nanoseconds(b::SpanNanoseconds::MIN),
1679        ), @"-PT1902545624836.854775807S");
1680    }
1681
1682    #[cfg(not(miri))]
1683    #[test]
1684    fn print_signed_duration() {
1685        let p = |secs, nanos| -> String {
1686            let dur = SignedDuration::new(secs, nanos);
1687            let mut buf = String::new();
1688            SpanPrinter::new().print_signed_duration(&dur, &mut buf).unwrap();
1689            buf
1690        };
1691
1692        insta::assert_snapshot!(p(0, 0), @"PT0S");
1693        insta::assert_snapshot!(p(0, 1), @"PT0.000000001S");
1694        insta::assert_snapshot!(p(1, 0), @"PT1S");
1695        insta::assert_snapshot!(p(59, 0), @"PT59S");
1696        insta::assert_snapshot!(p(60, 0), @"PT1M");
1697        insta::assert_snapshot!(p(60, 1), @"PT1M0.000000001S");
1698        insta::assert_snapshot!(p(61, 1), @"PT1M1.000000001S");
1699        insta::assert_snapshot!(p(3_600, 0), @"PT1H");
1700        insta::assert_snapshot!(p(3_600, 1), @"PT1H0.000000001S");
1701        insta::assert_snapshot!(p(3_660, 0), @"PT1H1M");
1702        insta::assert_snapshot!(p(3_660, 1), @"PT1H1M0.000000001S");
1703        insta::assert_snapshot!(p(3_661, 0), @"PT1H1M1S");
1704        insta::assert_snapshot!(p(3_661, 1), @"PT1H1M1.000000001S");
1705
1706        insta::assert_snapshot!(p(0, -1), @"-PT0.000000001S");
1707        insta::assert_snapshot!(p(-1, 0), @"-PT1S");
1708        insta::assert_snapshot!(p(-59, 0), @"-PT59S");
1709        insta::assert_snapshot!(p(-60, 0), @"-PT1M");
1710        insta::assert_snapshot!(p(-60, -1), @"-PT1M0.000000001S");
1711        insta::assert_snapshot!(p(-61, -1), @"-PT1M1.000000001S");
1712        insta::assert_snapshot!(p(-3_600, 0), @"-PT1H");
1713        insta::assert_snapshot!(p(-3_600, -1), @"-PT1H0.000000001S");
1714        insta::assert_snapshot!(p(-3_660, 0), @"-PT1H1M");
1715        insta::assert_snapshot!(p(-3_660, -1), @"-PT1H1M0.000000001S");
1716        insta::assert_snapshot!(p(-3_661, 0), @"-PT1H1M1S");
1717        insta::assert_snapshot!(p(-3_661, -1), @"-PT1H1M1.000000001S");
1718
1719        insta::assert_snapshot!(
1720            p(i64::MIN, -999_999_999),
1721            @"-PT2562047788015215H30M8.999999999S",
1722        );
1723        insta::assert_snapshot!(
1724            p(i64::MAX, 999_999_999),
1725            @"PT2562047788015215H30M7.999999999S",
1726        );
1727    }
1728
1729    #[cfg(not(miri))]
1730    #[test]
1731    fn print_unsigned_duration() {
1732        let p = |secs, nanos| -> String {
1733            let dur = Duration::new(secs, nanos);
1734            let mut buf = String::new();
1735            SpanPrinter::new()
1736                .print_unsigned_duration(&dur, &mut buf)
1737                .unwrap();
1738            buf
1739        };
1740
1741        insta::assert_snapshot!(p(0, 0), @"PT0S");
1742        insta::assert_snapshot!(p(0, 1), @"PT0.000000001S");
1743        insta::assert_snapshot!(p(1, 0), @"PT1S");
1744        insta::assert_snapshot!(p(59, 0), @"PT59S");
1745        insta::assert_snapshot!(p(60, 0), @"PT1M");
1746        insta::assert_snapshot!(p(60, 1), @"PT1M0.000000001S");
1747        insta::assert_snapshot!(p(61, 1), @"PT1M1.000000001S");
1748        insta::assert_snapshot!(p(3_600, 0), @"PT1H");
1749        insta::assert_snapshot!(p(3_600, 1), @"PT1H0.000000001S");
1750        insta::assert_snapshot!(p(3_660, 0), @"PT1H1M");
1751        insta::assert_snapshot!(p(3_660, 1), @"PT1H1M0.000000001S");
1752        insta::assert_snapshot!(p(3_661, 0), @"PT1H1M1S");
1753        insta::assert_snapshot!(p(3_661, 1), @"PT1H1M1.000000001S");
1754
1755        insta::assert_snapshot!(
1756            p(u64::MAX, 999_999_999),
1757            @"PT5124095576030431H15.999999999S",
1758        );
1759    }
1760}