jiff/fmt/temporal/
printer.rs

1use 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)]
19pub(super) struct DateTimePrinter {
20    lowercase: bool,
21    separator: u8,
22    rfc9557: bool,
23    precision: Option<u8>,
24}
25
26impl 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
372impl 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)]
382pub(super) struct SpanPrinter {
383    /// Whether to use lowercase unit designators.
384    lowercase: bool,
385}
386
387impl 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)]
575mod 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}