diff --git a/src/impls/interval.rs b/src/impls/interval.rs index 784bf549d..a4b9c94a9 100644 --- a/src/impls/interval.rs +++ b/src/impls/interval.rs @@ -425,24 +425,28 @@ impl LiteralFields { /// /// Each count carries its own sign, so `1 mon -1 day` prints as written, /// and a count is plural whenever it is not exactly one, which is why - /// PostgreSQL answers `-1 days`. The time of day is printed only when it - /// is non-zero, except for an interval that is zero throughout, which - /// prints as a zero clock. + /// PostgreSQL answers `-1 days`. A positive count or clock right after a + /// negative count is marked with `+`, so `-1 day 2 hours` prints as + /// `-1 days +02:00:00`. The time of day is printed only when it is + /// non-zero, except for an interval that is zero throughout, which prints + /// as a zero clock. fn to_postgres_text(&self) -> String { let mut parts: Vec = Vec::new(); - let years = self.months / 12; - let months = self.months % 12; - if years != 0 { - parts.push(format!("{years} {}", if years == 1 { "year" } else { "years" })); - } - if months != 0 { - parts.push(format!("{months} {}", if months == 1 { "mon" } else { "mons" })); - } - if self.days != 0 { - parts.push(format!("{} {}", self.days, if self.days == 1 { "day" } else { "days" })); + let mut after_negative = false; + for (count, unit) in + [(self.months / 12, "year"), (self.months % 12, "mon"), (self.days, "day")] + { + if count == 0 { + continue; + } + let plus = if after_negative && count > 0 { "+" } else { "" }; + let plural = if count == 1 { "" } else { "s" }; + parts.push(format!("{plus}{count} {unit}{plural}")); + after_negative = count < 0; } if self.micros != 0 || parts.is_empty() { - parts.push(clock_text(self.micros)); + let plus = if after_negative && self.micros > 0 { "+" } else { "" }; + parts.push(format!("{plus}{}", clock_text(self.micros))); } parts.join(" ") } diff --git a/tests/test_type_fidelity.rs b/tests/test_type_fidelity.rs index ab3ac5714..078262ca6 100644 --- a/tests/test_type_fidelity.rs +++ b/tests/test_type_fidelity.rs @@ -1023,6 +1023,24 @@ fn interval_counts_keep_their_own_signs() { assert_eq!(interval_text("PT-1H"), vec![Some("-01:00:00".to_string())]); } +#[test] +fn a_positive_interval_count_after_a_negative_one_is_printed_with_a_plus() { + // What PostgreSQL 18 prints for each literal under the default + // IntervalStyle. + for (literal, printed) in [ + ("-1 day 2 hours", "-1 days +02:00:00"), + ("-1 day +2 hours", "-1 days +02:00:00"), + ("-1 mon 2 days", "-1 mons +2 days"), + ("-1 day -2 hours", "-1 days -02:00:00"), + ("1 year -1 day", "1 year -1 days"), + ("1 day 02:00:00", "1 day 02:00:00"), + ("-1 year 1 day 2 hours", "-1 years +1 day 02:00:00"), + ("1 mon -1 day 2 hours", "1 mon -1 days +02:00:00"), + ] { + assert_eq!(interval_text(literal), vec![Some(printed.to_string())], "{literal}"); + } +} + #[test] fn an_interval_second_keeps_its_microseconds() { assert_eq!(interval_text("1.000001 seconds"), vec![Some("00:00:01.000001".to_string())]);