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