use crate::{
civil::{Date, DateTime, Time},
error::{err, Error},
fmt::{
temporal::{Pieces, PiecesOffset, TimeZoneAnnotationKind},
util::{DecimalFormatter, FractionalFormatter},
Write, WriteExt,
},
span::Span,
tz::{Offset, TimeZone},
util::{
rangeint::RFrom,
t::{self, C},
},
SignedDuration, Timestamp, Zoned,
};
#[derive(Clone, Debug)]
pub(super) struct DateTimePrinter {
lowercase: bool,
separator: u8,
rfc9557: bool,
precision: Option<u8>,
}
impl DateTimePrinter {
pub(super) const fn new() -> DateTimePrinter {
DateTimePrinter {
lowercase: false,
separator: b'T',
rfc9557: true,
precision: None,
}
}
pub(super) const fn lowercase(self, yes: bool) -> DateTimePrinter {
DateTimePrinter { lowercase: yes, ..self }
}
pub(super) const fn separator(self, ascii_char: u8) -> DateTimePrinter {
assert!(ascii_char.is_ascii(), "RFC3339 separator must be ASCII");
DateTimePrinter { separator: ascii_char, ..self }
}
pub(super) const fn precision(
self,
precision: Option<u8>,
) -> DateTimePrinter {
DateTimePrinter { precision, ..self }
}
pub(super) fn print_zoned<W: Write>(
&self,
zdt: &Zoned,
mut wtr: W,
) -> Result<(), Error> {
let timestamp = zdt.timestamp();
let tz = zdt.time_zone();
let offset = tz.to_offset(timestamp);
let dt = offset.to_datetime(timestamp);
self.print_datetime(&dt, &mut wtr)?;
if tz.is_unknown() {
wtr.write_str("Z[Etc/Unknown]")?;
} else {
self.print_offset_rounded(&offset, &mut wtr)?;
self.print_time_zone_annotation(&tz, &offset, &mut wtr)?;
}
Ok(())
}
pub(super) fn print_timestamp<W: Write>(
&self,
timestamp: &Timestamp,
offset: Option<Offset>,
mut wtr: W,
) -> Result<(), Error> {
let Some(offset) = offset else {
let dt = TimeZone::UTC.to_datetime(*timestamp);
self.print_datetime(&dt, &mut wtr)?;
self.print_zulu(&mut wtr)?;
return Ok(());
};
let dt = offset.to_datetime(*timestamp);
self.print_datetime(&dt, &mut wtr)?;
self.print_offset_rounded(&offset, &mut wtr)?;
Ok(())
}
pub(super) fn print_datetime<W: Write>(
&self,
dt: &DateTime,
mut wtr: W,
) -> Result<(), Error> {
self.print_date(&dt.date(), &mut wtr)?;
wtr.write_char(char::from(if self.lowercase {
self.separator.to_ascii_lowercase()
} else {
self.separator
}))?;
self.print_time(&dt.time(), &mut wtr)?;
Ok(())
}
pub(super) fn print_date<W: Write>(
&self,
date: &Date,
mut wtr: W,
) -> Result<(), Error> {
static FMT_YEAR_POSITIVE: DecimalFormatter =
DecimalFormatter::new().padding(4);
static FMT_YEAR_NEGATIVE: DecimalFormatter =
DecimalFormatter::new().padding(6);
static FMT_TWO: DecimalFormatter = DecimalFormatter::new().padding(2);
if date.year() >= 0 {
wtr.write_int(&FMT_YEAR_POSITIVE, date.year())?;
} else {
wtr.write_int(&FMT_YEAR_NEGATIVE, date.year())?;
}
wtr.write_str("-")?;
wtr.write_int(&FMT_TWO, date.month())?;
wtr.write_str("-")?;
wtr.write_int(&FMT_TWO, date.day())?;
Ok(())
}
pub(super) fn print_time<W: Write>(
&self,
time: &Time,
mut wtr: W,
) -> Result<(), Error> {
static FMT_TWO: DecimalFormatter = DecimalFormatter::new().padding(2);
static FMT_FRACTION: FractionalFormatter = FractionalFormatter::new();
wtr.write_int(&FMT_TWO, time.hour())?;
wtr.write_str(":")?;
wtr.write_int(&FMT_TWO, time.minute())?;
wtr.write_str(":")?;
wtr.write_int(&FMT_TWO, time.second())?;
let fractional_nanosecond = time.subsec_nanosecond();
if self.precision.map_or(fractional_nanosecond != 0, |p| p > 0) {
wtr.write_str(".")?;
wtr.write_fraction(
&FMT_FRACTION.precision(self.precision),
fractional_nanosecond,
)?;
}
Ok(())
}
pub(super) fn print_time_zone<W: Write>(
&self,
tz: &TimeZone,
mut wtr: W,
) -> Result<(), Error> {
if let Some(iana_name) = tz.iana_name() {
return wtr.write_str(iana_name);
}
if tz.is_unknown() {
return wtr.write_str("Etc/Unknown");
}
if let Ok(offset) = tz.to_fixed_offset() {
return self.print_offset_full_precision(&offset, wtr);
}
#[cfg(feature = "alloc")]
{
if let Some(posix_tz) = tz.posix_tz() {
let s = alloc::string::ToString::to_string(posix_tz);
return wtr.write_str(&s);
}
}
Err(err!(
"time zones without IANA identifiers that aren't either \
fixed offsets or a POSIX time zone can't be serialized \
(this typically occurs when this is a system time zone \
derived from `/etc/localtime` on Unix systems that \
isn't symlinked to an entry in `/usr/share/zoneinfo`)",
))
}
pub(super) fn print_pieces<W: Write>(
&self,
pieces: &Pieces,
mut wtr: W,
) -> Result<(), Error> {
if let Some(time) = pieces.time() {
let dt = DateTime::from_parts(pieces.date(), time);
self.print_datetime(&dt, &mut wtr)?;
if let Some(poffset) = pieces.offset() {
self.print_pieces_offset(&poffset, &mut wtr)?;
}
} else if let Some(poffset) = pieces.offset() {
let dt = DateTime::from_parts(pieces.date(), Time::midnight());
self.print_datetime(&dt, &mut wtr)?;
self.print_pieces_offset(&poffset, &mut wtr)?;
} else {
self.print_date(&pieces.date(), &mut wtr)?;
}
if let Some(ann) = pieces.time_zone_annotation() {
wtr.write_str("[")?;
if ann.is_critical() {
wtr.write_str("!")?;
}
match *ann.kind() {
TimeZoneAnnotationKind::Named(ref name) => {
wtr.write_str(name.as_str())?
}
TimeZoneAnnotationKind::Offset(offset) => {
self.print_offset_rounded(&offset, &mut wtr)?
}
}
wtr.write_str("]")?;
}
Ok(())
}
fn print_pieces_offset<W: Write>(
&self,
poffset: &PiecesOffset,
mut wtr: W,
) -> Result<(), Error> {
match *poffset {
PiecesOffset::Zulu => self.print_zulu(wtr),
PiecesOffset::Numeric(ref noffset) => {
if noffset.offset().is_zero() && noffset.is_negative() {
wtr.write_str("-00:00")
} else {
self.print_offset_rounded(&noffset.offset(), wtr)
}
}
}
}
fn print_offset_rounded<W: Write>(
&self,
offset: &Offset,
mut wtr: W,
) -> Result<(), Error> {
static FMT_TWO: DecimalFormatter = DecimalFormatter::new().padding(2);
wtr.write_str(if offset.is_negative() { "-" } else { "+" })?;
let mut hours = offset.part_hours_ranged().abs().get();
let mut minutes = offset.part_minutes_ranged().abs().get();
if offset.part_seconds_ranged().abs() >= C(30) {
if minutes == 59 {
hours = hours.saturating_add(1);
minutes = 0;
} else {
minutes = minutes.saturating_add(1);
}
}
wtr.write_int(&FMT_TWO, hours)?;
wtr.write_str(":")?;
wtr.write_int(&FMT_TWO, minutes)?;
Ok(())
}
fn print_offset_full_precision<W: Write>(
&self,
offset: &Offset,
mut wtr: W,
) -> Result<(), Error> {
static FMT_TWO: DecimalFormatter = DecimalFormatter::new().padding(2);
wtr.write_str(if offset.is_negative() { "-" } else { "+" })?;
let hours = offset.part_hours_ranged().abs().get();
let minutes = offset.part_minutes_ranged().abs().get();
let seconds = offset.part_seconds_ranged().abs().get();
wtr.write_int(&FMT_TWO, hours)?;
wtr.write_str(":")?;
wtr.write_int(&FMT_TWO, minutes)?;
if seconds > 0 {
wtr.write_str(":")?;
wtr.write_int(&FMT_TWO, seconds)?;
}
Ok(())
}
fn print_zulu<W: Write>(&self, mut wtr: W) -> Result<(), Error> {
wtr.write_str(if self.lowercase { "z" } else { "Z" })
}
fn print_time_zone_annotation<W: Write>(
&self,
time_zone: &TimeZone,
offset: &Offset,
mut wtr: W,
) -> Result<(), Error> {
if !self.rfc9557 {
return Ok(());
}
wtr.write_str("[")?;
if let Some(iana_name) = time_zone.iana_name() {
wtr.write_str(iana_name)?;
} else {
self.print_offset_rounded(offset, &mut wtr)?;
}
wtr.write_str("]")?;
Ok(())
}
}
impl Default for DateTimePrinter {
fn default() -> DateTimePrinter {
DateTimePrinter::new()
}
}
#[derive(Debug)]
pub(super) struct SpanPrinter {
lowercase: bool,
}
impl SpanPrinter {
pub(super) const fn new() -> SpanPrinter {
SpanPrinter { lowercase: false }
}
pub(super) const fn lowercase(self, yes: bool) -> SpanPrinter {
SpanPrinter { lowercase: yes }
}
pub(super) fn print_span<W: Write>(
&self,
span: &Span,
mut wtr: W,
) -> Result<(), Error> {
static FMT_INT: DecimalFormatter = DecimalFormatter::new();
static FMT_FRACTION: FractionalFormatter = FractionalFormatter::new();
if span.is_negative() {
wtr.write_str("-")?;
}
wtr.write_str("P")?;
let mut non_zero_greater_than_second = false;
if span.get_years_ranged() != C(0) {
wtr.write_int(&FMT_INT, span.get_years_ranged().get().abs())?;
wtr.write_char(self.label('Y'))?;
non_zero_greater_than_second = true;
}
if span.get_months_ranged() != C(0) {
wtr.write_int(&FMT_INT, span.get_months_ranged().get().abs())?;
wtr.write_char(self.label('M'))?;
non_zero_greater_than_second = true;
}
if span.get_weeks_ranged() != C(0) {
wtr.write_int(&FMT_INT, span.get_weeks_ranged().get().abs())?;
wtr.write_char(self.label('W'))?;
non_zero_greater_than_second = true;
}
if span.get_days_ranged() != C(0) {
wtr.write_int(&FMT_INT, span.get_days_ranged().get().abs())?;
wtr.write_char(self.label('D'))?;
non_zero_greater_than_second = true;
}
let mut printed_time_prefix = false;
if span.get_hours_ranged() != C(0) {
if !printed_time_prefix {
wtr.write_str("T")?;
printed_time_prefix = true;
}
wtr.write_int(&FMT_INT, span.get_hours_ranged().get().abs())?;
wtr.write_char(self.label('H'))?;
non_zero_greater_than_second = true;
}
if span.get_minutes_ranged() != C(0) {
if !printed_time_prefix {
wtr.write_str("T")?;
printed_time_prefix = true;
}
wtr.write_int(&FMT_INT, span.get_minutes_ranged().get().abs())?;
wtr.write_char(self.label('M'))?;
non_zero_greater_than_second = true;
}
let (seconds, millis, micros, nanos) = (
span.get_seconds_ranged().abs(),
span.get_milliseconds_ranged().abs(),
span.get_microseconds_ranged().abs(),
span.get_nanoseconds_ranged().abs(),
);
if (seconds != C(0) || !non_zero_greater_than_second)
&& millis == C(0)
&& micros == C(0)
&& nanos == C(0)
{
if !printed_time_prefix {
wtr.write_str("T")?;
}
wtr.write_int(&FMT_INT, seconds.get())?;
wtr.write_char(self.label('S'))?;
} else if millis != C(0) || micros != C(0) || nanos != C(0) {
if !printed_time_prefix {
wtr.write_str("T")?;
}
let combined_as_nanos =
t::SpanSecondsOrLowerNanoseconds::rfrom(nanos)
+ (t::SpanSecondsOrLowerNanoseconds::rfrom(micros)
* t::NANOS_PER_MICRO)
+ (t::SpanSecondsOrLowerNanoseconds::rfrom(millis)
* t::NANOS_PER_MILLI)
+ (t::SpanSecondsOrLowerNanoseconds::rfrom(seconds)
* t::NANOS_PER_SECOND);
let fraction_second = t::SpanSecondsOrLower::rfrom(
combined_as_nanos / t::NANOS_PER_SECOND,
);
let fraction_nano = t::SubsecNanosecond::rfrom(
combined_as_nanos % t::NANOS_PER_SECOND,
);
wtr.write_int(&FMT_INT, fraction_second.get())?;
if fraction_nano != C(0) {
wtr.write_str(".")?;
wtr.write_fraction(&FMT_FRACTION, fraction_nano.get())?;
}
wtr.write_char(self.label('S'))?;
}
Ok(())
}
pub(super) fn print_duration<W: Write>(
&self,
dur: &SignedDuration,
mut wtr: W,
) -> Result<(), Error> {
static FMT_INT: DecimalFormatter = DecimalFormatter::new();
static FMT_FRACTION: FractionalFormatter = FractionalFormatter::new();
let mut non_zero_greater_than_second = false;
if dur.is_negative() {
wtr.write_str("-")?;
}
wtr.write_str("PT")?;
let mut secs = dur.as_secs();
let nanos = dur.subsec_nanos().abs();
let hours = (secs / (60 * 60)).abs();
secs %= 60 * 60;
let minutes = (secs / 60).abs();
secs = (secs % 60).abs();
if hours != 0 {
wtr.write_int(&FMT_INT, hours)?;
wtr.write_char(self.label('H'))?;
non_zero_greater_than_second = true;
}
if minutes != 0 {
wtr.write_int(&FMT_INT, minutes)?;
wtr.write_char(self.label('M'))?;
non_zero_greater_than_second = true;
}
if (secs != 0 || !non_zero_greater_than_second) && nanos == 0 {
wtr.write_int(&FMT_INT, secs)?;
wtr.write_char(self.label('S'))?;
} else if nanos != 0 {
wtr.write_int(&FMT_INT, secs)?;
wtr.write_str(".")?;
wtr.write_fraction(&FMT_FRACTION, nanos)?;
wtr.write_char(self.label('S'))?;
}
Ok(())
}
fn label(&self, upper: char) -> char {
debug_assert!(upper.is_ascii());
if self.lowercase {
upper.to_ascii_lowercase()
} else {
upper
}
}
}
#[cfg(feature = "alloc")]
#[cfg(test)]
mod tests {
use alloc::string::String;
use crate::{civil::date, span::ToSpan};
use super::*;
#[test]
fn print_zoned() {
if crate::tz::db().is_definitively_empty() {
return;
}
let dt = date(2024, 3, 10).at(5, 34, 45, 0);
let zoned: Zoned = dt.in_tz("America/New_York").unwrap();
let mut buf = String::new();
DateTimePrinter::new().print_zoned(&zoned, &mut buf).unwrap();
assert_eq!(buf, "2024-03-10T05:34:45-04:00[America/New_York]");
let dt = date(2024, 3, 10).at(5, 34, 45, 0);
let zoned: Zoned = dt.in_tz("America/New_York").unwrap();
let zoned = zoned.with_time_zone(TimeZone::UTC);
let mut buf = String::new();
DateTimePrinter::new().print_zoned(&zoned, &mut buf).unwrap();
assert_eq!(buf, "2024-03-10T09:34:45+00:00[UTC]");
}
#[test]
fn print_timestamp() {
if crate::tz::db().is_definitively_empty() {
return;
}
let dt = date(2024, 3, 10).at(5, 34, 45, 0);
let zoned: Zoned = dt.in_tz("America/New_York").unwrap();
let mut buf = String::new();
DateTimePrinter::new()
.print_timestamp(&zoned.timestamp(), None, &mut buf)
.unwrap();
assert_eq!(buf, "2024-03-10T09:34:45Z");
let dt = date(-2024, 3, 10).at(5, 34, 45, 0);
let zoned: Zoned = dt.in_tz("America/New_York").unwrap();
let mut buf = String::new();
DateTimePrinter::new()
.print_timestamp(&zoned.timestamp(), None, &mut buf)
.unwrap();
assert_eq!(buf, "-002024-03-10T10:30:47Z");
}
#[test]
fn print_span_basic() {
let p = |span: Span| -> String {
let mut buf = String::new();
SpanPrinter::new().print_span(&span, &mut buf).unwrap();
buf
};
insta::assert_snapshot!(p(Span::new()), @"PT0S");
insta::assert_snapshot!(p(1.second()), @"PT1S");
insta::assert_snapshot!(p(-1.second()), @"-PT1S");
insta::assert_snapshot!(p(
1.second().milliseconds(1).microseconds(1).nanoseconds(1),
), @"PT1.001001001S");
insta::assert_snapshot!(p(
0.second().milliseconds(999).microseconds(999).nanoseconds(999),
), @"PT0.999999999S");
insta::assert_snapshot!(p(
1.year().months(1).weeks(1).days(1)
.hours(1).minutes(1).seconds(1)
.milliseconds(1).microseconds(1).nanoseconds(1),
), @"P1Y1M1W1DT1H1M1.001001001S");
insta::assert_snapshot!(p(
-1.year().months(1).weeks(1).days(1)
.hours(1).minutes(1).seconds(1)
.milliseconds(1).microseconds(1).nanoseconds(1),
), @"-P1Y1M1W1DT1H1M1.001001001S");
}
#[test]
fn print_span_subsecond_positive() {
let p = |span: Span| -> String {
let mut buf = String::new();
SpanPrinter::new().print_span(&span, &mut buf).unwrap();
buf
};
insta::assert_snapshot!(p(
0.second().milliseconds(1000).microseconds(1000).nanoseconds(1000),
), @"PT1.001001S");
insta::assert_snapshot!(p(
1.second().milliseconds(1000).microseconds(1000).nanoseconds(1000),
), @"PT2.001001S");
insta::assert_snapshot!(p(
0.second()
.milliseconds(t::SpanMilliseconds::MAX_REPR),
), @"PT631107417600S");
insta::assert_snapshot!(p(
0.second()
.microseconds(t::SpanMicroseconds::MAX_REPR),
), @"PT631107417600S");
insta::assert_snapshot!(p(
0.second()
.nanoseconds(t::SpanNanoseconds::MAX_REPR),
), @"PT9223372036.854775807S");
insta::assert_snapshot!(p(
0.second()
.milliseconds(t::SpanMilliseconds::MAX_REPR)
.microseconds(999_999),
), @"PT631107417600.999999S");
insta::assert_snapshot!(p(
0.second()
.milliseconds(t::SpanMilliseconds::MAX_REPR)
.microseconds(1_000_000),
), @"PT631107417601S");
insta::assert_snapshot!(p(
0.second()
.milliseconds(t::SpanMilliseconds::MAX_REPR)
.microseconds(1_000_001),
), @"PT631107417601.000001S");
insta::assert_snapshot!(p(
0.second()
.milliseconds(t::SpanMilliseconds::MAX_REPR)
.nanoseconds(1_000_000_000),
), @"PT631107417601S");
insta::assert_snapshot!(p(
0.second()
.milliseconds(t::SpanMilliseconds::MAX_REPR)
.nanoseconds(1_000_000_001),
), @"PT631107417601.000000001S");
insta::assert_snapshot!(p(
0.second()
.milliseconds(t::SpanMilliseconds::MAX_REPR)
.microseconds(t::SpanMicroseconds::MAX_REPR)
.nanoseconds(t::SpanNanoseconds::MAX_REPR),
), @"PT1271438207236.854775807S");
insta::assert_snapshot!(p(
Span::new()
.seconds(t::SpanSeconds::MAX_REPR)
.milliseconds(t::SpanMilliseconds::MAX_REPR)
.microseconds(t::SpanMicroseconds::MAX_REPR)
.nanoseconds(t::SpanNanoseconds::MAX_REPR),
), @"PT1902545624836.854775807S");
}
#[test]
fn print_span_subsecond_negative() {
let p = |span: Span| -> String {
let mut buf = String::new();
SpanPrinter::new().print_span(&span, &mut buf).unwrap();
buf
};
insta::assert_snapshot!(p(
-0.second().milliseconds(1000).microseconds(1000).nanoseconds(1000),
), @"-PT1.001001S");
insta::assert_snapshot!(p(
-1.second().milliseconds(1000).microseconds(1000).nanoseconds(1000),
), @"-PT2.001001S");
insta::assert_snapshot!(p(
0.second()
.milliseconds(t::SpanMilliseconds::MIN_REPR),
), @"-PT631107417600S");
insta::assert_snapshot!(p(
0.second()
.microseconds(t::SpanMicroseconds::MIN_REPR),
), @"-PT631107417600S");
insta::assert_snapshot!(p(
0.second()
.nanoseconds(t::SpanNanoseconds::MIN_REPR),
), @"-PT9223372036.854775807S");
insta::assert_snapshot!(p(
0.second()
.milliseconds(t::SpanMilliseconds::MIN_REPR)
.microseconds(999_999),
), @"-PT631107417600.999999S");
insta::assert_snapshot!(p(
0.second()
.milliseconds(t::SpanMilliseconds::MIN_REPR)
.microseconds(1_000_000),
), @"-PT631107417601S");
insta::assert_snapshot!(p(
0.second()
.milliseconds(t::SpanMilliseconds::MIN_REPR)
.microseconds(1_000_001),
), @"-PT631107417601.000001S");
insta::assert_snapshot!(p(
0.second()
.milliseconds(t::SpanMilliseconds::MIN_REPR)
.nanoseconds(1_000_000_000),
), @"-PT631107417601S");
insta::assert_snapshot!(p(
0.second()
.milliseconds(t::SpanMilliseconds::MIN_REPR)
.nanoseconds(1_000_000_001),
), @"-PT631107417601.000000001S");
insta::assert_snapshot!(p(
0.second()
.milliseconds(t::SpanMilliseconds::MIN_REPR)
.microseconds(t::SpanMicroseconds::MIN_REPR)
.nanoseconds(t::SpanNanoseconds::MIN_REPR),
), @"-PT1271438207236.854775807S");
insta::assert_snapshot!(p(
Span::new()
.seconds(t::SpanSeconds::MIN_REPR)
.milliseconds(t::SpanMilliseconds::MIN_REPR)
.microseconds(t::SpanMicroseconds::MIN_REPR)
.nanoseconds(t::SpanNanoseconds::MIN_REPR),
), @"-PT1902545624836.854775807S");
}
#[test]
fn print_duration() {
let p = |secs, nanos| -> String {
let dur = SignedDuration::new(secs, nanos);
let mut buf = String::new();
SpanPrinter::new().print_duration(&dur, &mut buf).unwrap();
buf
};
insta::assert_snapshot!(p(0, 0), @"PT0S");
insta::assert_snapshot!(p(0, 1), @"PT0.000000001S");
insta::assert_snapshot!(p(1, 0), @"PT1S");
insta::assert_snapshot!(p(59, 0), @"PT59S");
insta::assert_snapshot!(p(60, 0), @"PT1M");
insta::assert_snapshot!(p(60, 1), @"PT1M0.000000001S");
insta::assert_snapshot!(p(61, 1), @"PT1M1.000000001S");
insta::assert_snapshot!(p(3_600, 0), @"PT1H");
insta::assert_snapshot!(p(3_600, 1), @"PT1H0.000000001S");
insta::assert_snapshot!(p(3_660, 0), @"PT1H1M");
insta::assert_snapshot!(p(3_660, 1), @"PT1H1M0.000000001S");
insta::assert_snapshot!(p(3_661, 0), @"PT1H1M1S");
insta::assert_snapshot!(p(3_661, 1), @"PT1H1M1.000000001S");
insta::assert_snapshot!(p(0, -1), @"-PT0.000000001S");
insta::assert_snapshot!(p(-1, 0), @"-PT1S");
insta::assert_snapshot!(p(-59, 0), @"-PT59S");
insta::assert_snapshot!(p(-60, 0), @"-PT1M");
insta::assert_snapshot!(p(-60, -1), @"-PT1M0.000000001S");
insta::assert_snapshot!(p(-61, -1), @"-PT1M1.000000001S");
insta::assert_snapshot!(p(-3_600, 0), @"-PT1H");
insta::assert_snapshot!(p(-3_600, -1), @"-PT1H0.000000001S");
insta::assert_snapshot!(p(-3_660, 0), @"-PT1H1M");
insta::assert_snapshot!(p(-3_660, -1), @"-PT1H1M0.000000001S");
insta::assert_snapshot!(p(-3_661, 0), @"-PT1H1M1S");
insta::assert_snapshot!(p(-3_661, -1), @"-PT1H1M1.000000001S");
insta::assert_snapshot!(
p(i64::MIN, -999_999_999),
@"-PT2562047788015215H30M8.999999999S",
);
insta::assert_snapshot!(
p(i64::MAX, 999_999_999),
@"PT2562047788015215H30M7.999999999S",
);
}
}