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.unsigned_abs(),
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(
505                    &FMT_FRACTION,
506                    i32::from(fraction_nano).unsigned_abs(),
507                )?;
508            }
509            wtr.write_char(self.label('S'))?;
510        }
511        Ok(())
512    }
513
514    /// Print the given signed duration to the writer given.
515    ///
516    /// This only returns an error when the given writer returns an error.
517    pub(super) fn print_signed_duration<W: Write>(
518        &self,
519        dur: &SignedDuration,
520        mut wtr: W,
521    ) -> Result<(), Error> {
522        static FMT_INT: DecimalFormatter = DecimalFormatter::new();
523        static FMT_FRACTION: FractionalFormatter = FractionalFormatter::new();
524
525        let mut non_zero_greater_than_second = false;
526        if dur.is_negative() {
527            wtr.write_str("-")?;
528        }
529        wtr.write_str("PT")?;
530
531        let mut secs = dur.as_secs();
532        // OK because subsec_nanos -999_999_999<=nanos<=999_999_999.
533        let nanos = dur.subsec_nanos().abs();
534        // OK because guaranteed to be bigger than i64::MIN.
535        let hours = (secs / (60 * 60)).abs();
536        secs %= 60 * 60;
537        // OK because guaranteed to be bigger than i64::MIN.
538        let minutes = (secs / 60).abs();
539        // OK because guaranteed to be bigger than i64::MIN.
540        secs = (secs % 60).abs();
541        if hours != 0 {
542            wtr.write_int(&FMT_INT, hours)?;
543            wtr.write_char(self.label('H'))?;
544            non_zero_greater_than_second = true;
545        }
546        if minutes != 0 {
547            wtr.write_int(&FMT_INT, minutes)?;
548            wtr.write_char(self.label('M'))?;
549            non_zero_greater_than_second = true;
550        }
551        if (secs != 0 || !non_zero_greater_than_second) && nanos == 0 {
552            wtr.write_int(&FMT_INT, secs)?;
553            wtr.write_char(self.label('S'))?;
554        } else if nanos != 0 {
555            wtr.write_int(&FMT_INT, secs)?;
556            wtr.write_str(".")?;
557            wtr.write_fraction(&FMT_FRACTION, nanos.unsigned_abs())?;
558            wtr.write_char(self.label('S'))?;
559        }
560        Ok(())
561    }
562
563    /// Print the given unsigned duration to the writer given.
564    ///
565    /// This only returns an error when the given writer returns an error.
566    pub(super) fn print_unsigned_duration<W: Write>(
567        &self,
568        dur: &core::time::Duration,
569        mut wtr: W,
570    ) -> Result<(), Error> {
571        static FMT_INT: DecimalFormatter = DecimalFormatter::new();
572        static FMT_FRACTION: FractionalFormatter = FractionalFormatter::new();
573
574        let mut non_zero_greater_than_second = false;
575        wtr.write_str("PT")?;
576
577        let mut secs = dur.as_secs();
578        let nanos = dur.subsec_nanos();
579        let hours = secs / (60 * 60);
580        secs %= 60 * 60;
581        let minutes = secs / 60;
582        secs = secs % 60;
583        if hours != 0 {
584            wtr.write_uint(&FMT_INT, hours)?;
585            wtr.write_char(self.label('H'))?;
586            non_zero_greater_than_second = true;
587        }
588        if minutes != 0 {
589            wtr.write_uint(&FMT_INT, minutes)?;
590            wtr.write_char(self.label('M'))?;
591            non_zero_greater_than_second = true;
592        }
593        if (secs != 0 || !non_zero_greater_than_second) && nanos == 0 {
594            wtr.write_uint(&FMT_INT, secs)?;
595            wtr.write_char(self.label('S'))?;
596        } else if nanos != 0 {
597            wtr.write_uint(&FMT_INT, secs)?;
598            wtr.write_str(".")?;
599            wtr.write_fraction(&FMT_FRACTION, nanos)?;
600            wtr.write_char(self.label('S'))?;
601        }
602        Ok(())
603    }
604
605    /// Converts the uppercase unit designator label to lowercase if this
606    /// printer is configured to use lowercase. Otherwise the label is returned
607    /// unchanged.
608    fn label(&self, upper: char) -> char {
609        debug_assert!(upper.is_ascii());
610        if self.lowercase {
611            upper.to_ascii_lowercase()
612        } else {
613            upper
614        }
615    }
616}
617
618#[cfg(feature = "alloc")]
619#[cfg(test)]
620mod tests {
621    use alloc::string::String;
622
623    use crate::{civil::date, span::ToSpan};
624
625    use super::*;
626
627    #[test]
628    fn print_zoned() {
629        if crate::tz::db().is_definitively_empty() {
630            return;
631        }
632
633        let dt = date(2024, 3, 10).at(5, 34, 45, 0);
634        let zoned: Zoned = dt.in_tz("America/New_York").unwrap();
635        let mut buf = String::new();
636        DateTimePrinter::new().print_zoned(&zoned, &mut buf).unwrap();
637        assert_eq!(buf, "2024-03-10T05:34:45-04:00[America/New_York]");
638
639        let dt = date(2024, 3, 10).at(5, 34, 45, 0);
640        let zoned: Zoned = dt.in_tz("America/New_York").unwrap();
641        let zoned = zoned.with_time_zone(TimeZone::UTC);
642        let mut buf = String::new();
643        DateTimePrinter::new().print_zoned(&zoned, &mut buf).unwrap();
644        assert_eq!(buf, "2024-03-10T09:34:45+00:00[UTC]");
645    }
646
647    #[test]
648    fn print_timestamp() {
649        if crate::tz::db().is_definitively_empty() {
650            return;
651        }
652
653        let dt = date(2024, 3, 10).at(5, 34, 45, 0);
654        let zoned: Zoned = dt.in_tz("America/New_York").unwrap();
655        let mut buf = String::new();
656        DateTimePrinter::new()
657            .print_timestamp(&zoned.timestamp(), None, &mut buf)
658            .unwrap();
659        assert_eq!(buf, "2024-03-10T09:34:45Z");
660
661        let dt = date(-2024, 3, 10).at(5, 34, 45, 0);
662        let zoned: Zoned = dt.in_tz("America/New_York").unwrap();
663        let mut buf = String::new();
664        DateTimePrinter::new()
665            .print_timestamp(&zoned.timestamp(), None, &mut buf)
666            .unwrap();
667        assert_eq!(buf, "-002024-03-10T10:30:47Z");
668    }
669
670    #[test]
671    fn print_span_basic() {
672        let p = |span: Span| -> String {
673            let mut buf = String::new();
674            SpanPrinter::new().print_span(&span, &mut buf).unwrap();
675            buf
676        };
677
678        insta::assert_snapshot!(p(Span::new()), @"PT0S");
679        insta::assert_snapshot!(p(1.second()), @"PT1S");
680        insta::assert_snapshot!(p(-1.second()), @"-PT1S");
681        insta::assert_snapshot!(p(
682            1.second().milliseconds(1).microseconds(1).nanoseconds(1),
683        ), @"PT1.001001001S");
684        insta::assert_snapshot!(p(
685            0.second().milliseconds(999).microseconds(999).nanoseconds(999),
686        ), @"PT0.999999999S");
687        insta::assert_snapshot!(p(
688            1.year().months(1).weeks(1).days(1)
689            .hours(1).minutes(1).seconds(1)
690            .milliseconds(1).microseconds(1).nanoseconds(1),
691        ), @"P1Y1M1W1DT1H1M1.001001001S");
692        insta::assert_snapshot!(p(
693            -1.year().months(1).weeks(1).days(1)
694            .hours(1).minutes(1).seconds(1)
695            .milliseconds(1).microseconds(1).nanoseconds(1),
696        ), @"-P1Y1M1W1DT1H1M1.001001001S");
697    }
698
699    #[test]
700    fn print_span_subsecond_positive() {
701        let p = |span: Span| -> String {
702            let mut buf = String::new();
703            SpanPrinter::new().print_span(&span, &mut buf).unwrap();
704            buf
705        };
706
707        // These are all sub-second trickery tests.
708        insta::assert_snapshot!(p(
709            0.second().milliseconds(1000).microseconds(1000).nanoseconds(1000),
710        ), @"PT1.001001S");
711        insta::assert_snapshot!(p(
712            1.second().milliseconds(1000).microseconds(1000).nanoseconds(1000),
713        ), @"PT2.001001S");
714        insta::assert_snapshot!(p(
715            0.second()
716            .milliseconds(t::SpanMilliseconds::MAX_REPR),
717        ), @"PT631107417600S");
718        insta::assert_snapshot!(p(
719            0.second()
720            .microseconds(t::SpanMicroseconds::MAX_REPR),
721        ), @"PT631107417600S");
722        insta::assert_snapshot!(p(
723            0.second()
724            .nanoseconds(t::SpanNanoseconds::MAX_REPR),
725        ), @"PT9223372036.854775807S");
726
727        insta::assert_snapshot!(p(
728            0.second()
729            .milliseconds(t::SpanMilliseconds::MAX_REPR)
730            .microseconds(999_999),
731        ), @"PT631107417600.999999S");
732        // This is 1 microsecond more than the maximum number of seconds
733        // representable in a span.
734        insta::assert_snapshot!(p(
735            0.second()
736            .milliseconds(t::SpanMilliseconds::MAX_REPR)
737            .microseconds(1_000_000),
738        ), @"PT631107417601S");
739        insta::assert_snapshot!(p(
740            0.second()
741            .milliseconds(t::SpanMilliseconds::MAX_REPR)
742            .microseconds(1_000_001),
743        ), @"PT631107417601.000001S");
744        // This is 1 nanosecond more than the maximum number of seconds
745        // representable in a span.
746        insta::assert_snapshot!(p(
747            0.second()
748            .milliseconds(t::SpanMilliseconds::MAX_REPR)
749            .nanoseconds(1_000_000_000),
750        ), @"PT631107417601S");
751        insta::assert_snapshot!(p(
752            0.second()
753            .milliseconds(t::SpanMilliseconds::MAX_REPR)
754            .nanoseconds(1_000_000_001),
755        ), @"PT631107417601.000000001S");
756
757        // The max millis, micros and nanos, combined.
758        insta::assert_snapshot!(p(
759            0.second()
760            .milliseconds(t::SpanMilliseconds::MAX_REPR)
761            .microseconds(t::SpanMicroseconds::MAX_REPR)
762            .nanoseconds(t::SpanNanoseconds::MAX_REPR),
763        ), @"PT1271438207236.854775807S");
764        // The max seconds, millis, micros and nanos, combined.
765        insta::assert_snapshot!(p(
766            Span::new()
767            .seconds(t::SpanSeconds::MAX_REPR)
768            .milliseconds(t::SpanMilliseconds::MAX_REPR)
769            .microseconds(t::SpanMicroseconds::MAX_REPR)
770            .nanoseconds(t::SpanNanoseconds::MAX_REPR),
771        ), @"PT1902545624836.854775807S");
772    }
773
774    #[test]
775    fn print_span_subsecond_negative() {
776        let p = |span: Span| -> String {
777            let mut buf = String::new();
778            SpanPrinter::new().print_span(&span, &mut buf).unwrap();
779            buf
780        };
781
782        // These are all sub-second trickery tests.
783        insta::assert_snapshot!(p(
784            -0.second().milliseconds(1000).microseconds(1000).nanoseconds(1000),
785        ), @"-PT1.001001S");
786        insta::assert_snapshot!(p(
787            -1.second().milliseconds(1000).microseconds(1000).nanoseconds(1000),
788        ), @"-PT2.001001S");
789        insta::assert_snapshot!(p(
790            0.second()
791            .milliseconds(t::SpanMilliseconds::MIN_REPR),
792        ), @"-PT631107417600S");
793        insta::assert_snapshot!(p(
794            0.second()
795            .microseconds(t::SpanMicroseconds::MIN_REPR),
796        ), @"-PT631107417600S");
797        insta::assert_snapshot!(p(
798            0.second()
799            .nanoseconds(t::SpanNanoseconds::MIN_REPR),
800        ), @"-PT9223372036.854775807S");
801
802        insta::assert_snapshot!(p(
803            0.second()
804            .milliseconds(t::SpanMilliseconds::MIN_REPR)
805            .microseconds(999_999),
806        ), @"-PT631107417600.999999S");
807        // This is 1 microsecond more than the maximum number of seconds
808        // representable in a span.
809        insta::assert_snapshot!(p(
810            0.second()
811            .milliseconds(t::SpanMilliseconds::MIN_REPR)
812            .microseconds(1_000_000),
813        ), @"-PT631107417601S");
814        insta::assert_snapshot!(p(
815            0.second()
816            .milliseconds(t::SpanMilliseconds::MIN_REPR)
817            .microseconds(1_000_001),
818        ), @"-PT631107417601.000001S");
819        // This is 1 nanosecond more than the maximum number of seconds
820        // representable in a span.
821        insta::assert_snapshot!(p(
822            0.second()
823            .milliseconds(t::SpanMilliseconds::MIN_REPR)
824            .nanoseconds(1_000_000_000),
825        ), @"-PT631107417601S");
826        insta::assert_snapshot!(p(
827            0.second()
828            .milliseconds(t::SpanMilliseconds::MIN_REPR)
829            .nanoseconds(1_000_000_001),
830        ), @"-PT631107417601.000000001S");
831
832        // The max millis, micros and nanos, combined.
833        insta::assert_snapshot!(p(
834            0.second()
835            .milliseconds(t::SpanMilliseconds::MIN_REPR)
836            .microseconds(t::SpanMicroseconds::MIN_REPR)
837            .nanoseconds(t::SpanNanoseconds::MIN_REPR),
838        ), @"-PT1271438207236.854775807S");
839        // The max seconds, millis, micros and nanos, combined.
840        insta::assert_snapshot!(p(
841            Span::new()
842            .seconds(t::SpanSeconds::MIN_REPR)
843            .milliseconds(t::SpanMilliseconds::MIN_REPR)
844            .microseconds(t::SpanMicroseconds::MIN_REPR)
845            .nanoseconds(t::SpanNanoseconds::MIN_REPR),
846        ), @"-PT1902545624836.854775807S");
847    }
848
849    #[test]
850    fn print_signed_duration() {
851        let p = |secs, nanos| -> String {
852            let dur = SignedDuration::new(secs, nanos);
853            let mut buf = String::new();
854            SpanPrinter::new().print_signed_duration(&dur, &mut buf).unwrap();
855            buf
856        };
857
858        insta::assert_snapshot!(p(0, 0), @"PT0S");
859        insta::assert_snapshot!(p(0, 1), @"PT0.000000001S");
860        insta::assert_snapshot!(p(1, 0), @"PT1S");
861        insta::assert_snapshot!(p(59, 0), @"PT59S");
862        insta::assert_snapshot!(p(60, 0), @"PT1M");
863        insta::assert_snapshot!(p(60, 1), @"PT1M0.000000001S");
864        insta::assert_snapshot!(p(61, 1), @"PT1M1.000000001S");
865        insta::assert_snapshot!(p(3_600, 0), @"PT1H");
866        insta::assert_snapshot!(p(3_600, 1), @"PT1H0.000000001S");
867        insta::assert_snapshot!(p(3_660, 0), @"PT1H1M");
868        insta::assert_snapshot!(p(3_660, 1), @"PT1H1M0.000000001S");
869        insta::assert_snapshot!(p(3_661, 0), @"PT1H1M1S");
870        insta::assert_snapshot!(p(3_661, 1), @"PT1H1M1.000000001S");
871
872        insta::assert_snapshot!(p(0, -1), @"-PT0.000000001S");
873        insta::assert_snapshot!(p(-1, 0), @"-PT1S");
874        insta::assert_snapshot!(p(-59, 0), @"-PT59S");
875        insta::assert_snapshot!(p(-60, 0), @"-PT1M");
876        insta::assert_snapshot!(p(-60, -1), @"-PT1M0.000000001S");
877        insta::assert_snapshot!(p(-61, -1), @"-PT1M1.000000001S");
878        insta::assert_snapshot!(p(-3_600, 0), @"-PT1H");
879        insta::assert_snapshot!(p(-3_600, -1), @"-PT1H0.000000001S");
880        insta::assert_snapshot!(p(-3_660, 0), @"-PT1H1M");
881        insta::assert_snapshot!(p(-3_660, -1), @"-PT1H1M0.000000001S");
882        insta::assert_snapshot!(p(-3_661, 0), @"-PT1H1M1S");
883        insta::assert_snapshot!(p(-3_661, -1), @"-PT1H1M1.000000001S");
884
885        insta::assert_snapshot!(
886            p(i64::MIN, -999_999_999),
887            @"-PT2562047788015215H30M8.999999999S",
888        );
889        insta::assert_snapshot!(
890            p(i64::MAX, 999_999_999),
891            @"PT2562047788015215H30M7.999999999S",
892        );
893    }
894
895    #[test]
896    fn print_unsigned_duration() {
897        let p = |secs, nanos| -> String {
898            let dur = core::time::Duration::new(secs, nanos);
899            let mut buf = String::new();
900            SpanPrinter::new()
901                .print_unsigned_duration(&dur, &mut buf)
902                .unwrap();
903            buf
904        };
905
906        insta::assert_snapshot!(p(0, 0), @"PT0S");
907        insta::assert_snapshot!(p(0, 1), @"PT0.000000001S");
908        insta::assert_snapshot!(p(1, 0), @"PT1S");
909        insta::assert_snapshot!(p(59, 0), @"PT59S");
910        insta::assert_snapshot!(p(60, 0), @"PT1M");
911        insta::assert_snapshot!(p(60, 1), @"PT1M0.000000001S");
912        insta::assert_snapshot!(p(61, 1), @"PT1M1.000000001S");
913        insta::assert_snapshot!(p(3_600, 0), @"PT1H");
914        insta::assert_snapshot!(p(3_600, 1), @"PT1H0.000000001S");
915        insta::assert_snapshot!(p(3_660, 0), @"PT1H1M");
916        insta::assert_snapshot!(p(3_660, 1), @"PT1H1M0.000000001S");
917        insta::assert_snapshot!(p(3_661, 0), @"PT1H1M1S");
918        insta::assert_snapshot!(p(3_661, 1), @"PT1H1M1.000000001S");
919
920        insta::assert_snapshot!(
921            p(u64::MAX, 999_999_999),
922            @"PT5124095576030431H15.999999999S",
923        );
924    }
925}