Skip to main content

rupg_types/datetime/
mod.rs

1//! `date`, `time`, `timetz`, `timestamp`, `timestamptz` and `interval`: the binary forms and the text output.
2//!
3//! The values are the integers of PostgreSQL. A date is days and a timestamp is microseconds since 2000-01-01, a time is microseconds since midnight, and a `timetz` zone is seconds west of UTC. The engine counts from 1970-01-01, so the row encoder adds [`UNIX_TO_POSTGRES_DAYS`] or [`UNIX_TO_POSTGRES_USECS`] after it has tested for infinity.
4//!
5//! The output functions are ports of `EncodeDateOnly`, `EncodeTimeOnly`, `EncodeDateTime` and `EncodeInterval` in `src/backend/utils/adt/datetime.c`, with `DateStyle` and `IntervalStyle` as arguments. A `timestamptz` prints in the session time zone, which is a [`TimeZone`]: the caller gives the offset and the abbreviation at each instant. The receive functions are ports of the functions in `date.c` and `timestamp.c`, with the same range checks and the same rounding to the typmod.
6//!
7//! Lifted from `crates/rudb-pgtypes/src/datetime/mod.rs` of tamnd/rudb at f5f7065a (spec/04 section 4.9).
8
9mod decode;
10
11use rupg_common::SqlState;
12
13pub use decode::{
14    Abbrev, DateTimeInput, DateUnit, NoZones, ZoneAbbrevs, ZoneLookup, ZoneName, date_in,
15    decode_numeric_timezone, decode_timezone_abbrev_prefix, decode_timezone_name, interval_in,
16    local_offset, time_in, timestamp_in, timestamptz_in, timetz_in,
17};
18
19use crate::binary::Recv;
20use crate::error::TypeError;
21use crate::number::u64_out;
22use crate::typmod::{
23    INTERVAL_FULL_PRECISION, INTERVAL_FULL_RANGE, IntervalField, MAX_TIME_PRECISION,
24};
25
26/// The Julian day of 2000-01-01, the epoch of PostgreSQL.
27pub const POSTGRES_EPOCH_JDATE: i32 = 2451545;
28/// The Julian day of 1970-01-01, the epoch of the engine.
29pub const UNIX_EPOCH_JDATE: i32 = 2440588;
30/// Add this to days since 1970-01-01 to get days since 2000-01-01.
31pub const UNIX_TO_POSTGRES_DAYS: i32 = UNIX_EPOCH_JDATE - POSTGRES_EPOCH_JDATE;
32pub const USECS_PER_SEC: i64 = 1_000_000;
33pub const USECS_PER_DAY: i64 = 86_400 * USECS_PER_SEC;
34/// Add this to microseconds since 1970-01-01 to get microseconds since 2000-01-01.
35pub const UNIX_TO_POSTGRES_USECS: i64 = UNIX_TO_POSTGRES_DAYS as i64 * USECS_PER_DAY;
36
37/// `DATEVAL_NOBEGIN` and `DATEVAL_NOEND`, the dates `-infinity` and `infinity`.
38pub const DATE_NEGATIVE_INFINITY: i32 = i32::MIN;
39pub const DATE_INFINITY: i32 = i32::MAX;
40/// `DT_NOBEGIN` and `DT_NOEND`, the timestamps `-infinity` and `infinity`.
41pub const TIMESTAMP_NEGATIVE_INFINITY: i64 = i64::MIN;
42pub const TIMESTAMP_INFINITY: i64 = i64::MAX;
43
44/// `DATE_END_JULIAN`, the first Julian day after the last valid date, 5874898-01-01.
45const DATE_END_JULIAN: i32 = 2147483494;
46/// `MIN_TIMESTAMP` and `END_TIMESTAMP`: 4714-11-24 BC and 294277-01-01.
47const MIN_TIMESTAMP: i64 = -211813488000000000;
48const END_TIMESTAMP: i64 = 9223371331200000000;
49/// `TZDISP_LIMIT`, the limit of the zone of a `timetz`, in seconds.
50const TZDISP_LIMIT: i32 = 16 * 3600;
51/// `MAXTZLEN`, the most bytes of an abbreviation that the output shows.
52const MAX_TZ_LEN: usize = 10;
53
54const DAYS: [&str; 7] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
55const MONTHS: [&str; 12] =
56    ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
57
58/// The output part of `DateStyle`.
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum DateStyle {
61    Iso,
62    Sql,
63    Postgres,
64    German,
65    /// `USE_XSD_DATES`: the ISO style with `T` between the date and the time and the minutes of the zone always shown. The functions of `json` use it, and `DateStyle` does not take it.
66    Xsd,
67}
68
69/// The field order part of `DateStyle`. The output uses it only to put the day before the month.
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum DateOrder {
72    Mdy,
73    Dmy,
74    Ymd,
75}
76
77/// The `DateStyle` setting. The default of `initdb` is `ISO, MDY`.
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub struct DateFormat {
80    pub style: DateStyle,
81    pub order: DateOrder,
82}
83
84impl DateFormat {
85    pub const ISO_MDY: DateFormat = DateFormat { style: DateStyle::Iso, order: DateOrder::Mdy };
86
87    fn day_first(self) -> bool {
88        self.order == DateOrder::Dmy
89    }
90}
91
92/// The `IntervalStyle` setting. The default is `Postgres`.
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum IntervalStyle {
95    Postgres,
96    PostgresVerbose,
97    SqlStandard,
98    Iso8601,
99}
100
101/// The rules of a time zone: the offset east of UTC in seconds and the abbreviation, at an instant in seconds since 1970-01-01 UTC, as `pg_localtime` gives them.
102pub trait TimeZone {
103    fn at(&self, unix_seconds: i64) -> (i32, &str);
104
105    /// The offset east of UTC before the first change of the rules after an instant, and the instant and the offset of that change, as `pg_next_dst_boundary` gives them.
106    fn next_change(&self, unix_seconds: i64) -> (i32, Option<(i64, i32)>) {
107        (self.at(unix_seconds).0, None)
108    }
109
110    /// The meaning of an abbreviation in upper case in this zone over all of its history, as `pg_timezone_abbrev_is_known` gives it.
111    fn abbrev_meaning(&self, _abbrev: &str) -> Option<AbbrevMeaning> {
112        None
113    }
114
115    /// The offset east of UTC and the daylight saving flag of an abbreviation in upper case at an instant, as `pg_interpret_timezone_abbrev` gives them.
116    fn abbrev_at(&self, _abbrev: &str, _unix_seconds: i64) -> Option<(i32, bool)> {
117        None
118    }
119
120    /// The offset east of UTC if the zone has one offset at all instants, as `pg_get_timezone_offset` gives it.
121    fn fixed_offset(&self) -> Option<i32> {
122        None
123    }
124
125    /// The abbreviations that the zone uses, in the order of `pg_get_next_timezone_abbrev`.
126    fn abbrevs(&self) -> Vec<&str> {
127        Vec::new()
128    }
129}
130
131impl<T: TimeZone + ?Sized> TimeZone for std::sync::Arc<T> {
132    fn at(&self, unix_seconds: i64) -> (i32, &str) {
133        (**self).at(unix_seconds)
134    }
135
136    fn next_change(&self, unix_seconds: i64) -> (i32, Option<(i64, i32)>) {
137        (**self).next_change(unix_seconds)
138    }
139
140    fn abbrev_meaning(&self, abbrev: &str) -> Option<AbbrevMeaning> {
141        (**self).abbrev_meaning(abbrev)
142    }
143
144    fn abbrev_at(&self, abbrev: &str, unix_seconds: i64) -> Option<(i32, bool)> {
145        (**self).abbrev_at(abbrev, unix_seconds)
146    }
147
148    fn fixed_offset(&self) -> Option<i32> {
149        (**self).fixed_offset()
150    }
151
152    fn abbrevs(&self) -> Vec<&str> {
153        (**self).abbrevs()
154    }
155}
156
157/// The meaning of a time zone abbreviation in the zone that uses it.
158#[derive(Debug, Clone, Copy, PartialEq, Eq)]
159pub enum AbbrevMeaning {
160    /// One offset east of UTC at all instants, and whether it is daylight saving time.
161    Fixed { offset: i32, dst: bool },
162    /// The offset changed over the history of the zone.
163    Varies,
164}
165
166/// A zone with one offset at all instants, such as `UTC`.
167#[derive(Debug, Clone, PartialEq, Eq)]
168pub struct FixedZone {
169    /// Seconds east of UTC.
170    pub offset: i32,
171    pub abbrev: String,
172}
173
174impl FixedZone {
175    pub fn utc() -> FixedZone {
176        FixedZone { offset: 0, abbrev: "UTC".to_string() }
177    }
178}
179
180impl TimeZone for FixedZone {
181    fn at(&self, _: i64) -> (i32, &str) {
182        (self.offset, &self.abbrev)
183    }
184
185    fn abbrev_meaning(&self, abbrev: &str) -> Option<AbbrevMeaning> {
186        (!self.abbrev.is_empty() && abbrev == self.abbrev)
187            .then_some(AbbrevMeaning::Fixed { offset: self.offset, dst: false })
188    }
189
190    fn abbrev_at(&self, abbrev: &str, _: i64) -> Option<(i32, bool)> {
191        (!self.abbrev.is_empty() && abbrev == self.abbrev).then_some((self.offset, false))
192    }
193
194    fn fixed_offset(&self) -> Option<i32> {
195        Some(self.offset)
196    }
197
198    fn abbrevs(&self) -> Vec<&str> {
199        vec![&self.abbrev]
200    }
201}
202
203/// An `interval`: months, days and microseconds, which do not convert into each other.
204#[derive(Debug, Clone, Copy, PartialEq, Eq)]
205pub struct Interval {
206    pub time: i64,
207    pub day: i32,
208    pub month: i32,
209}
210
211impl Interval {
212    pub const NEGATIVE_INFINITY: Interval =
213        Interval { time: i64::MIN, day: i32::MIN, month: i32::MIN };
214    pub const INFINITY: Interval = Interval { time: i64::MAX, day: i32::MAX, month: i32::MAX };
215}
216
217/// A date of the engine as PostgreSQL holds it. The engine counts days from 1970-01-01 and has `i32::MAX` and `-i32::MAX` for the infinities. PostgreSQL counts days from 2000-01-01 and has [`DATE_INFINITY`] and [`DATE_NEGATIVE_INFINITY`]. A date that PostgreSQL cannot hold is an error.
218pub fn date_from_unix(days: i32) -> Result<i32, TypeError> {
219    match days {
220        i32::MAX => Ok(DATE_INFINITY),
221        days if days == -i32::MAX => Ok(DATE_NEGATIVE_INFINITY),
222        days => {
223            let date = i64::from(days) + i64::from(UNIX_TO_POSTGRES_DAYS);
224            let valid =
225                i64::from(-POSTGRES_EPOCH_JDATE)..i64::from(DATE_END_JULIAN - POSTGRES_EPOCH_JDATE);
226            if valid.contains(&date) { Ok(date as i32) } else { Err(out_of_range("date")) }
227        }
228    }
229}
230
231/// A `timestamp` or a `timestamptz` of the engine as PostgreSQL holds it. The engine counts microseconds from 1970-01-01 and has `i64::MAX` and `-i64::MAX` for the infinities. A value that PostgreSQL cannot hold is an error.
232pub fn timestamp_from_unix(micros: i64) -> Result<i64, TypeError> {
233    match micros {
234        i64::MAX => Ok(TIMESTAMP_INFINITY),
235        micros if micros == -i64::MAX => Ok(TIMESTAMP_NEGATIVE_INFINITY),
236        micros => micros
237            .checked_add(UNIX_TO_POSTGRES_USECS)
238            .filter(|ts| (MIN_TIMESTAMP..END_TIMESTAMP).contains(ts))
239            .ok_or_else(|| out_of_range("timestamp")),
240    }
241}
242
243fn out_of_range(what: &str) -> TypeError {
244    TypeError::new(SqlState::DATETIME_VALUE_OUT_OF_RANGE, format!("{what} out of range"))
245}
246
247/// `date2j`: the Julian day of a date in the proleptic Gregorian calendar. The year before 1 is 0.
248///
249/// The arithmetic wraps as the C code does on a value far out of range. The callers check the range before they use the result.
250pub fn date2j(year: i32, month: i32, day: i32) -> i32 {
251    let (month, year) = if month > 2 {
252        (month.wrapping_add(1), year.wrapping_add(4800))
253    } else {
254        (month.wrapping_add(13), year.wrapping_add(4799))
255    };
256    let century = year / 100;
257    let julian = year.wrapping_mul(365).wrapping_sub(32167);
258    let julian = julian.wrapping_add(year / 4 - century + century / 4);
259    julian.wrapping_add(7834i32.wrapping_mul(month) / 256).wrapping_add(day)
260}
261
262/// `j2date`: the year, the month and the day of a Julian day.
263pub fn j2date(jd: i32) -> (i32, u32, u32) {
264    let mut julian = (jd as u32).wrapping_add(32044);
265    let mut quad = julian / 146097;
266    let extra = (julian - quad * 146097) * 4 + 3;
267    julian = julian.wrapping_add(60 + quad * 3 + extra / 146097);
268    quad = julian / 1461;
269    julian -= quad * 1461;
270    let mut y = julian * 4 / 1461;
271    julian = if y != 0 { (julian + 305) % 365 } else { (julian + 306) % 366 } + 123;
272    y += quad * 4;
273    let quad = julian * 2141 / 65536;
274    (y as i32 - 4800, (quad + 10) % 12 + 1, julian - 7834 * quad / 256)
275}
276
277/// The fields of a date and a time, as `struct pg_tm` and `fsec_t` hold them.
278struct Fields {
279    year: i32,
280    month: u32,
281    day: u32,
282    hour: u32,
283    minute: u32,
284    second: u32,
285    usec: u32,
286}
287
288impl Fields {
289    /// `timestamp2tm` with no zone. `None` when the Julian day does not fit in an `int`.
290    fn of_timestamp(ts: i64) -> Option<Fields> {
291        let date = ts.div_euclid(USECS_PER_DAY) + i64::from(POSTGRES_EPOCH_JDATE);
292        let jd = i32::try_from(date).ok().filter(|&jd| jd >= 0)?;
293        let time = ts.rem_euclid(USECS_PER_DAY);
294        let (year, month, day) = j2date(jd);
295        let (hour, minute, second, usec) = split_time(time);
296        Some(Fields { year, month, day, hour, minute, second, usec })
297    }
298}
299
300/// `dt2time`: the hours, the minutes, the seconds and the microseconds of a time of day.
301fn split_time(time: i64) -> (u32, u32, u32, u32) {
302    let usec = (time % USECS_PER_SEC) as u32;
303    let seconds = time / USECS_PER_SEC;
304    ((seconds / 3600) as u32, (seconds / 60 % 60) as u32, (seconds % 60) as u32, usec)
305}
306
307/// `pg_ultostr_zeropad`: at least `width` digits.
308fn zeropad(n: u64, width: usize, out: &mut Vec<u8>) {
309    let start = out.len();
310    u64_out(n, out);
311    let len = out.len() - start;
312    if len < width {
313        out.splice(start..start, std::iter::repeat_n(b'0', width - len));
314    }
315}
316
317/// Two digits of a field that is less than 100.
318fn two(n: u32, out: &mut Vec<u8>) {
319    debug_assert!(n < 100);
320    out.extend_from_slice(&[b'0' + (n / 10) as u8, b'0' + (n % 10) as u8]);
321}
322
323/// The year as PostgreSQL shows it: four digits at least, and a positive number for a year BC.
324fn year(year: i32, out: &mut Vec<u8>) {
325    let shown = if year > 0 { year } else { 1 - year };
326    zeropad(shown as u64, 4, out);
327}
328
329/// `AppendSeconds`: the seconds, and the microseconds with no zeros at the end. The sign is dropped.
330fn seconds(sec: i64, usec: i64, fill: bool, out: &mut Vec<u8>) {
331    if fill {
332        zeropad(sec.unsigned_abs(), 2, out);
333    } else {
334        u64_out(sec.unsigned_abs(), out);
335    }
336    if usec != 0 {
337        out.push(b'.');
338        let mut usec = usec.unsigned_abs();
339        let mut digits = 6;
340        while usec.is_multiple_of(10) {
341            usec /= 10;
342            digits -= 1;
343        }
344        zeropad(usec, digits, out);
345    }
346}
347
348/// `EncodeTimezone`: the offset east of UTC, with minutes and seconds only when they are not zero.
349fn zone_offset(offset: i32, minutes: bool, out: &mut Vec<u8>) {
350    out.push(if offset >= 0 { b'+' } else { b'-' });
351    let total = offset.unsigned_abs();
352    let (hour, min, sec) = (total / 3600, total / 60 % 60, total % 60);
353    zeropad(u64::from(hour), 2, out);
354    if min != 0 || sec != 0 || minutes {
355        out.push(b':');
356        two(min, out);
357    }
358    if sec != 0 {
359        out.push(b':');
360        two(sec, out);
361    }
362}
363
364/// The date part in the order and with the separator of the style.
365fn date_part(f: &Fields, format: DateFormat, out: &mut Vec<u8>) {
366    let (first, second, sep) = match (format.style, format.day_first()) {
367        (DateStyle::Iso | DateStyle::Xsd, _) => {
368            year(f.year, out);
369            out.push(b'-');
370            two(f.month, out);
371            out.push(b'-');
372            two(f.day, out);
373            return;
374        }
375        (DateStyle::German, _) => (f.day, f.month, b'.'),
376        (DateStyle::Sql, true) => (f.day, f.month, b'/'),
377        (DateStyle::Sql, false) => (f.month, f.day, b'/'),
378        (DateStyle::Postgres, true) => (f.day, f.month, b'-'),
379        (DateStyle::Postgres, false) => (f.month, f.day, b'-'),
380    };
381    two(first, out);
382    out.push(sep);
383    two(second, out);
384    out.push(sep);
385    year(f.year, out);
386}
387
388fn time_part(hour: u32, minute: u32, second: u32, usec: u32, out: &mut Vec<u8>) {
389    zeropad(u64::from(hour), 2, out);
390    out.push(b':');
391    two(minute, out);
392    out.push(b':');
393    seconds(i64::from(second), i64::from(usec), true, out);
394}
395
396fn bc(year: i32, out: &mut Vec<u8>) {
397    if year <= 0 {
398        out.extend_from_slice(b" BC");
399    }
400}
401
402/// The text output of `date`.
403pub fn date_out(date: i32, format: DateFormat, out: &mut Vec<u8>) {
404    match date {
405        DATE_NEGATIVE_INFINITY => return out.extend_from_slice(b"-infinity"),
406        DATE_INFINITY => return out.extend_from_slice(b"infinity"),
407        _ => {}
408    }
409    let (year, month, day) = j2date(date.wrapping_add(POSTGRES_EPOCH_JDATE));
410    let fields = Fields { year, month, day, hour: 0, minute: 0, second: 0, usec: 0 };
411    date_part(&fields, format, out);
412    bc(year, out);
413}
414
415/// `date_recv`: the range is the range that `date_in` takes, and the infinities.
416pub fn date_recv(recv: &mut Recv<'_>) -> Result<i32, TypeError> {
417    let date = recv.i32()?;
418    let valid = -POSTGRES_EPOCH_JDATE..DATE_END_JULIAN - POSTGRES_EPOCH_JDATE;
419    if date == DATE_NEGATIVE_INFINITY || date == DATE_INFINITY || valid.contains(&date) {
420        Ok(date)
421    } else {
422        Err(out_of_range("date"))
423    }
424}
425
426/// The text output of `time`, microseconds since midnight. `24:00:00` is a valid time.
427pub fn time_out(time: i64, out: &mut Vec<u8>) {
428    let (hour, minute, second, usec) = split_time(time);
429    time_part(hour, minute, second, usec, out);
430}
431
432/// The text output of `timetz`. The zone is in seconds west of UTC, as PostgreSQL stores it.
433pub fn timetz_out(time: i64, zone: i32, out: &mut Vec<u8>) {
434    time_out(time, out);
435    zone_offset(-zone, false, out);
436}
437
438/// `AdjustTimeForTypmod` and the rounding of `AdjustTimestampForTypmod`: rounds half away from zero to `precision` digits after the point.
439fn round_usecs(value: i64, precision: i32) -> i64 {
440    let scale = 10i64.pow((MAX_TIME_PRECISION - precision) as u32);
441    let half = scale / 2;
442    if value >= 0 { (value + half) / scale * scale } else { -((-value + half) / scale * scale) }
443}
444
445fn time_value(recv: &mut Recv<'_>) -> Result<i64, TypeError> {
446    let time = recv.i64()?;
447    if !(0..=USECS_PER_DAY).contains(&time) {
448        return Err(out_of_range("time"));
449    }
450    Ok(time)
451}
452
453/// `AdjustTimeForTypmod`: the time rounded to the precision of the typmod.
454pub fn adjust_time(time: i64, typmod: i32) -> i64 {
455    if (0..=MAX_TIME_PRECISION).contains(&typmod) { round_usecs(time, typmod) } else { time }
456}
457
458/// `time_recv`.
459pub fn time_recv(recv: &mut Recv<'_>, typmod: i32) -> Result<i64, TypeError> {
460    Ok(adjust_time(time_value(recv)?, typmod))
461}
462
463/// `timetz_recv`: the time and the zone in seconds west of UTC.
464pub fn timetz_recv(recv: &mut Recv<'_>, typmod: i32) -> Result<(i64, i32), TypeError> {
465    let time = time_value(recv)?;
466    let zone = recv.i32()?;
467    if zone <= -TZDISP_LIMIT || zone >= TZDISP_LIMIT {
468        return Err(TypeError::new(
469            SqlState::INVALID_TIME_ZONE_DISPLACEMENT_VALUE,
470            "time zone displacement out of range".to_string(),
471        ));
472    }
473    Ok((adjust_time(time, typmod), zone))
474}
475
476/// `EncodeDateTime`. `zone` is the offset east of UTC and the abbreviation, for `timestamptz`.
477fn date_time(f: &Fields, zone: Option<(i32, &str)>, format: DateFormat, out: &mut Vec<u8>) {
478    let abbrev = |abbrev: &str, out: &mut Vec<u8>| {
479        out.push(b' ');
480        out.extend_from_slice(&abbrev.as_bytes()[..abbrev.len().min(MAX_TZ_LEN)]);
481    };
482    match format.style {
483        DateStyle::Iso | DateStyle::Xsd => {
484            let xsd = format.style == DateStyle::Xsd;
485            date_part(f, format, out);
486            out.push(if xsd { b'T' } else { b' ' });
487            time_part(f.hour, f.minute, f.second, f.usec, out);
488            if let Some((offset, _)) = zone {
489                zone_offset(offset, xsd, out);
490            }
491        }
492        DateStyle::Sql | DateStyle::German => {
493            date_part(f, format, out);
494            out.push(b' ');
495            time_part(f.hour, f.minute, f.second, f.usec, out);
496            if let Some((_, name)) = zone {
497                abbrev(name, out);
498            }
499        }
500        DateStyle::Postgres => {
501            let weekday = (date2j(f.year, f.month as i32, f.day as i32) + 1).rem_euclid(7);
502            out.extend_from_slice(DAYS[weekday as usize].as_bytes());
503            out.push(b' ');
504            let month = MONTHS[f.month as usize - 1].as_bytes();
505            if format.day_first() {
506                two(f.day, out);
507                out.push(b' ');
508                out.extend_from_slice(month);
509            } else {
510                out.extend_from_slice(month);
511                out.push(b' ');
512                two(f.day, out);
513            }
514            out.push(b' ');
515            time_part(f.hour, f.minute, f.second, f.usec, out);
516            out.push(b' ');
517            year(f.year, out);
518            if let Some((_, name)) = zone {
519                abbrev(name, out);
520            }
521        }
522    }
523    bc(f.year, out);
524}
525
526/// The text output of `timestamp`. A value before 4714-11-24 BC has no Julian day and is an error, as in PostgreSQL.
527pub fn timestamp_out(ts: i64, format: DateFormat, out: &mut Vec<u8>) -> Result<(), TypeError> {
528    match ts {
529        TIMESTAMP_NEGATIVE_INFINITY => out.extend_from_slice(b"-infinity"),
530        TIMESTAMP_INFINITY => out.extend_from_slice(b"infinity"),
531        _ => date_time(
532            &Fields::of_timestamp(ts).ok_or_else(|| out_of_range("timestamp"))?,
533            None,
534            format,
535            out,
536        ),
537    }
538    Ok(())
539}
540
541/// The text output of `timestamptz` in the session time zone.
542pub fn timestamptz_out(
543    ts: i64,
544    format: DateFormat,
545    zone: &(impl TimeZone + ?Sized),
546    out: &mut Vec<u8>,
547) -> Result<(), TypeError> {
548    let infinity: &[u8] = match ts {
549        TIMESTAMP_NEGATIVE_INFINITY => b"-infinity",
550        TIMESTAMP_INFINITY => b"infinity",
551        _ => b"",
552    };
553    if !infinity.is_empty() {
554        out.extend_from_slice(infinity);
555        return Ok(());
556    }
557    // The UTC fields only check the range, as `timestamp2tm` does before it asks the zone.
558    let utc = Fields::of_timestamp(ts).ok_or_else(|| out_of_range("timestamp"))?;
559    let unix = ts.div_euclid(USECS_PER_SEC) - UNIX_TO_POSTGRES_USECS / USECS_PER_SEC;
560    let (offset, name) = zone.at(unix);
561    let local = unix + i64::from(offset);
562    let (year, month, day) = j2date((local.div_euclid(86400) + i64::from(UNIX_EPOCH_JDATE)) as i32);
563    let (hour, minute, second, _) = split_time(local.rem_euclid(86400) * USECS_PER_SEC);
564    let fields = Fields { year, month, day, hour, minute, second, usec: utc.usec };
565    date_time(&fields, Some((offset, name)), format, out);
566    Ok(())
567}
568
569/// `timestamp_recv` and `timestamptz_recv`: the range is the range that the input takes, and the value is rounded to the precision of the typmod.
570pub fn timestamp_recv(recv: &mut Recv<'_>, typmod: i32) -> Result<i64, TypeError> {
571    let ts = recv.i64()?;
572    if ts == TIMESTAMP_NEGATIVE_INFINITY || ts == TIMESTAMP_INFINITY {
573        return Ok(ts);
574    }
575    if Fields::of_timestamp(ts).is_none() || !(MIN_TIMESTAMP..END_TIMESTAMP).contains(&ts) {
576        return Err(out_of_range("timestamp"));
577    }
578    adjust_timestamp(ts, typmod)
579}
580
581/// `AdjustTimestampForTypmod`: the timestamp rounded to the precision of the typmod.
582pub fn adjust_timestamp(ts: i64, typmod: i32) -> Result<i64, TypeError> {
583    if ts == TIMESTAMP_NEGATIVE_INFINITY
584        || ts == TIMESTAMP_INFINITY
585        || typmod == -1
586        || typmod == MAX_TIME_PRECISION
587    {
588        return Ok(ts);
589    }
590    if !(0..=MAX_TIME_PRECISION).contains(&typmod) {
591        return Err(TypeError::new(
592            SqlState::INVALID_PARAMETER_VALUE,
593            format!("timestamp({typmod}) precision must be between 0 and {MAX_TIME_PRECISION}"),
594        ));
595    }
596    Ok(round_usecs(ts, typmod))
597}
598
599/// `AddPostgresIntPart` and `AddVerboseIntPart` need to know if a field came before and if the last field was negative.
600struct Parts {
601    zero: bool,
602    before: bool,
603}
604
605impl Parts {
606    /// `AddPostgresIntPart`: `-1 years` has an `s`, and a positive field after a negative one has a `+`.
607    fn postgres(&mut self, value: i64, unit: &str, out: &mut Vec<u8>) {
608        if value == 0 {
609            return;
610        }
611        if !self.zero {
612            out.push(b' ');
613        }
614        if self.before && value > 0 {
615            out.push(b'+');
616        }
617        out.extend_from_slice(format!("{value} {unit}").as_bytes());
618        if value != 1 {
619            out.push(b's');
620        }
621        self.before = value < 0;
622        self.zero = false;
623    }
624
625    /// `AddVerboseIntPart`: the first field sets the sign, and the text has `ago` at the end in place of a minus sign.
626    fn verbose(&mut self, mut value: i64, unit: &str, out: &mut Vec<u8>) {
627        if value == 0 {
628            return;
629        }
630        if self.zero {
631            self.before = value < 0;
632            value = value.abs();
633        } else if self.before {
634            value = -value;
635        }
636        out.extend_from_slice(format!(" {value} {unit}").as_bytes());
637        if value != 1 {
638            out.push(b's');
639        }
640        self.zero = false;
641    }
642}
643
644/// The text output of `interval`.
645pub fn interval_out(period: &Interval, style: IntervalStyle, out: &mut Vec<u8>) {
646    if *period == Interval::NEGATIVE_INFINITY {
647        return out.extend_from_slice(b"-infinity");
648    }
649    if *period == Interval::INFINITY {
650        return out.extend_from_slice(b"infinity");
651    }
652    // `interval2itm`. Each field has the sign of its source, as C division gives it.
653    let mut year = i64::from(period.month / 12);
654    let mut mon = i64::from(period.month % 12);
655    let mut mday = i64::from(period.day);
656    let mut hour = period.time / 3_600_000_000;
657    let mut min = period.time / 60_000_000 % 60;
658    let mut sec = period.time / USECS_PER_SEC % 60;
659    let mut usec = period.time % USECS_PER_SEC;
660    let time_negative = hour < 0 || min < 0 || sec < 0 || usec < 0;
661    let time_nonzero = hour != 0 || min != 0 || sec != 0 || usec != 0;
662    let mut parts = Parts { zero: true, before: false };
663    match style {
664        IntervalStyle::SqlStandard => {
665            let negative = year < 0 || mon < 0 || mday < 0 || time_negative;
666            let positive =
667                year > 0 || mon > 0 || mday > 0 || hour > 0 || min > 0 || sec > 0 || usec > 0;
668            let year_month = year != 0 || mon != 0;
669            let day_time = mday != 0 || time_nonzero;
670            let standard = !(negative && positive) && !(year_month && day_time);
671            if negative && standard {
672                out.push(b'-');
673                (year, mon, mday, hour, min, sec, usec) =
674                    (-year, -mon, -mday, -hour, -min, -sec, -usec);
675            }
676            if !negative && !positive {
677                out.push(b'0');
678            } else if !standard {
679                let sign = |negative: bool| if negative { '-' } else { '+' };
680                out.extend_from_slice(
681                    format!(
682                        "{}{}-{} {}{} {}{}:{:02}:",
683                        sign(year < 0 || mon < 0),
684                        year.abs(),
685                        mon.abs(),
686                        sign(mday < 0),
687                        mday.abs(),
688                        sign(time_negative),
689                        hour.abs(),
690                        min.abs()
691                    )
692                    .as_bytes(),
693                );
694                seconds(sec, usec, true, out);
695            } else if year_month {
696                out.extend_from_slice(format!("{year}-{mon}").as_bytes());
697            } else {
698                if mday != 0 {
699                    out.extend_from_slice(format!("{mday} ").as_bytes());
700                }
701                out.extend_from_slice(format!("{hour}:{min:02}:").as_bytes());
702                seconds(sec, usec, true, out);
703            }
704        }
705        IntervalStyle::Iso8601 => {
706            if year == 0 && mon == 0 && mday == 0 && !time_nonzero {
707                return out.extend_from_slice(b"PT0S");
708            }
709            out.push(b'P');
710            let part = |value: i64, unit: char, out: &mut Vec<u8>| {
711                if value != 0 {
712                    out.extend_from_slice(format!("{value}{unit}").as_bytes());
713                }
714            };
715            part(year, 'Y', out);
716            part(mon, 'M', out);
717            part(mday, 'D', out);
718            if time_nonzero {
719                out.push(b'T');
720            }
721            part(hour, 'H', out);
722            part(min, 'M', out);
723            if sec != 0 || usec != 0 {
724                if sec < 0 || usec < 0 {
725                    out.push(b'-');
726                }
727                seconds(sec, usec, false, out);
728                out.push(b'S');
729            }
730        }
731        IntervalStyle::Postgres => {
732            parts.postgres(year, "year", out);
733            parts.postgres(mon, "mon", out);
734            parts.postgres(mday, "day", out);
735            if parts.zero || time_nonzero {
736                if !parts.zero {
737                    out.push(b' ');
738                }
739                if time_negative {
740                    out.push(b'-');
741                } else if parts.before {
742                    out.push(b'+');
743                }
744                zeropad(hour.unsigned_abs(), 2, out);
745                out.push(b':');
746                zeropad(min.unsigned_abs(), 2, out);
747                out.push(b':');
748                seconds(sec, usec, true, out);
749            }
750        }
751        IntervalStyle::PostgresVerbose => {
752            out.push(b'@');
753            parts.verbose(year, "year", out);
754            parts.verbose(mon, "mon", out);
755            parts.verbose(mday, "day", out);
756            parts.verbose(hour, "hour", out);
757            parts.verbose(min, "min", out);
758            if sec != 0 || usec != 0 {
759                out.push(b' ');
760                if sec < 0 || (sec == 0 && usec < 0) {
761                    if parts.zero {
762                        parts.before = true;
763                    } else if !parts.before {
764                        out.push(b'-');
765                    }
766                } else if parts.before {
767                    out.push(b'-');
768                }
769                seconds(sec, usec, false, out);
770                out.extend_from_slice(if sec.abs() != 1 || usec != 0 { b" secs" } else { b" sec" });
771                parts.zero = false;
772            }
773            if parts.zero {
774                out.extend_from_slice(b" 0");
775            }
776            if parts.before {
777                out.extend_from_slice(b" ago");
778            }
779        }
780    }
781}
782
783/// `interval_recv`: the fields after the last field of the typmod are cleared, and the microseconds are rounded to the precision.
784pub fn interval_recv(recv: &mut Recv<'_>, typmod: i32) -> Result<Interval, TypeError> {
785    let period = Interval { time: recv.i64()?, day: recv.i32()?, month: recv.i32()? };
786    adjust_interval(period, typmod)
787}
788
789/// `AdjustIntervalForTypmod`: the interval with the fields and the precision of the typmod.
790pub fn adjust_interval(mut period: Interval, typmod: i32) -> Result<Interval, TypeError> {
791    if typmod < 0 || period == Interval::INFINITY || period == Interval::NEGATIVE_INFINITY {
792        return Ok(period);
793    }
794    let (range, precision) = ((typmod >> 16) & 0x7fff, typmod & 0xffff);
795    let mask = |fields: &[IntervalField]| crate::typmod::interval_range(fields);
796    use IntervalField::*;
797    const HOUR_USECS: i64 = 3_600_000_000;
798    const MINUTE_USECS: i64 = 60_000_000;
799    let hours = |time: i64| time / HOUR_USECS * HOUR_USECS;
800    let minutes = |time: i64| time / MINUTE_USECS * MINUTE_USECS;
801    match range {
802        r if r == INTERVAL_FULL_RANGE
803            || r == mask(&[Second])
804            || r == mask(&[Day, Hour, Minute, Second])
805            || r == mask(&[Hour, Minute, Second])
806            || r == mask(&[Minute, Second]) => {}
807        r if r == mask(&[Year]) => {
808            period = Interval { time: 0, day: 0, month: period.month / 12 * 12 }
809        }
810        r if r == mask(&[Month]) || r == mask(&[Year, Month]) => (period.day, period.time) = (0, 0),
811        r if r == mask(&[Day]) => period.time = 0,
812        r if r == mask(&[Hour]) || r == mask(&[Day, Hour]) => period.time = hours(period.time),
813        r if r == mask(&[Minute])
814            || r == mask(&[Day, Hour, Minute])
815            || r == mask(&[Hour, Minute]) =>
816        {
817            period.time = minutes(period.time)
818        }
819        _ => {
820            return Err(TypeError::new(
821                SqlState::INTERNAL_ERROR,
822                format!("unrecognized interval typmod: {typmod}"),
823            ));
824        }
825    }
826    if precision != INTERVAL_FULL_PRECISION {
827        if !(0..=MAX_TIME_PRECISION).contains(&precision) {
828            return Err(TypeError::new(
829                SqlState::INVALID_PARAMETER_VALUE,
830                format!(
831                    "interval({precision}) precision must be between 0 and {MAX_TIME_PRECISION}"
832                ),
833            ));
834        }
835        let scale = 10i64.pow((MAX_TIME_PRECISION - precision) as u32);
836        let half = scale / 2;
837        let time = if period.time >= 0 {
838            period.time.checked_add(half)
839        } else {
840            period.time.checked_sub(half)
841        };
842        let time = time.ok_or_else(|| out_of_range("interval"))?;
843        period.time = time - time % scale;
844    }
845    Ok(period)
846}
847
848/// The binary output of `interval`.
849pub fn interval_send(period: &Interval, out: &mut Vec<u8>) {
850    out.extend_from_slice(&period.time.to_be_bytes());
851    out.extend_from_slice(&period.day.to_be_bytes());
852    out.extend_from_slice(&period.month.to_be_bytes());
853}
854
855#[cfg(test)]
856mod tests {
857    use super::*;
858
859    fn text(f: impl FnOnce(&mut Vec<u8>)) -> String {
860        let mut out = Vec::new();
861        f(&mut out);
862        String::from_utf8(out).unwrap()
863    }
864
865    #[test]
866    fn the_julian_day_goes_both_ways() {
867        assert_eq!(date2j(2000, 1, 1), POSTGRES_EPOCH_JDATE);
868        assert_eq!(date2j(1970, 1, 1), UNIX_EPOCH_JDATE);
869        assert_eq!(date2j(5874898, 1, 1), DATE_END_JULIAN);
870        assert_eq!(date2j(-4713, 11, 24), 0);
871        for jd in [0, 1, 1721425, 1721426, 2451545, 2460954, 109203528, DATE_END_JULIAN - 1] {
872            let (y, m, d) = j2date(jd);
873            assert_eq!(date2j(y, m as i32, d as i32), jd);
874        }
875        assert_eq!(UNIX_TO_POSTGRES_USECS, -946_684_800_000_000);
876    }
877
878    #[test]
879    fn each_style_writes_its_layout() {
880        let ts =
881            (date2j(2026, 10, 5) - POSTGRES_EPOCH_JDATE) as i64 * USECS_PER_DAY + 45_296_789_000;
882        let at =
883            |style, order| text(|out| timestamp_out(ts, DateFormat { style, order }, out).unwrap());
884        assert_eq!(at(DateStyle::Iso, DateOrder::Mdy), "2026-10-05 12:34:56.789");
885        assert_eq!(at(DateStyle::Sql, DateOrder::Mdy), "10/05/2026 12:34:56.789");
886        assert_eq!(at(DateStyle::Sql, DateOrder::Dmy), "05/10/2026 12:34:56.789");
887        assert_eq!(at(DateStyle::German, DateOrder::Ymd), "05.10.2026 12:34:56.789");
888        assert_eq!(at(DateStyle::Postgres, DateOrder::Mdy), "Mon Oct 05 12:34:56.789 2026");
889        assert_eq!(at(DateStyle::Postgres, DateOrder::Dmy), "Mon 05 Oct 12:34:56.789 2026");
890        let zone = FixedZone { offset: 19800, abbrev: "IST".to_string() };
891        let tz = |style| {
892            text(|out| {
893                timestamptz_out(ts, DateFormat { style, order: DateOrder::Mdy }, &zone, out)
894                    .unwrap()
895            })
896        };
897        assert_eq!(tz(DateStyle::Iso), "2026-10-05 18:04:56.789+05:30");
898        assert_eq!(tz(DateStyle::Postgres), "Mon Oct 05 18:04:56.789 2026 IST");
899        let bc = (date2j(-43, 3, 15) - POSTGRES_EPOCH_JDATE) as i64 * USECS_PER_DAY;
900        assert_eq!(
901            text(|out| timestamp_out(bc, DateFormat::ISO_MDY, out).unwrap()),
902            "0044-03-15 00:00:00 BC"
903        );
904        assert_eq!(text(|out| timetz_out(USECS_PER_DAY, -20130, out)), "24:00:00+05:35:30");
905        assert_eq!(text(|out| date_out(DATE_INFINITY, DateFormat::ISO_MDY, out)), "infinity");
906        let error = timestamp_out(i64::MIN + 1, DateFormat::ISO_MDY, &mut Vec::new()).unwrap_err();
907        assert_eq!(error.message, "timestamp out of range");
908    }
909
910    #[test]
911    fn the_receive_functions_check_the_range() {
912        let date = |d: i32| date_recv(&mut Recv::new(&d.to_be_bytes()));
913        assert_eq!(date(i32::MIN), Ok(i32::MIN));
914        assert_eq!(date(-POSTGRES_EPOCH_JDATE), Ok(-POSTGRES_EPOCH_JDATE));
915        assert_eq!(date(-POSTGRES_EPOCH_JDATE - 1).unwrap_err().message, "date out of range");
916        let time = |t: i64, typmod| time_recv(&mut Recv::new(&t.to_be_bytes()), typmod);
917        assert_eq!(time(USECS_PER_DAY, -1), Ok(USECS_PER_DAY));
918        assert_eq!(time(USECS_PER_DAY + 1, -1).unwrap_err().message, "time out of range");
919        assert_eq!(time(1_500_000, 0), Ok(2_000_000));
920        let ts = |t: i64, typmod| timestamp_recv(&mut Recv::new(&t.to_be_bytes()), typmod);
921        assert_eq!(ts(-1_500_000, 0), Ok(-2_000_000));
922        assert_eq!(ts(END_TIMESTAMP, -1).unwrap_err().message, "timestamp out of range");
923        assert_eq!(ts(0, 7).unwrap_err().message, "timestamp(7) precision must be between 0 and 6");
924        let zone = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xe1, 0];
925        assert_eq!(timetz_recv(&mut Recv::new(&zone), -1).unwrap_err().sqlstate.as_str(), "22009");
926    }
927
928    #[test]
929    fn an_interval_typmod_clears_the_small_fields() {
930        let period = Interval { time: 90_061_500_000, day: 3, month: 14 };
931        let mut bytes = Vec::new();
932        interval_send(&period, &mut bytes);
933        let recv = |typmod| interval_recv(&mut Recv::new(&bytes), typmod).unwrap();
934        let typmod = |fields: &[IntervalField], precision| {
935            crate::typmod::interval_typmod(precision, crate::typmod::interval_range(fields))
936        };
937        use IntervalField::*;
938        assert_eq!(recv(-1), period);
939        assert_eq!(
940            recv(typmod(&[Year], INTERVAL_FULL_PRECISION)),
941            Interval { time: 0, day: 0, month: 12 }
942        );
943        assert_eq!(recv(typmod(&[Day, Hour], INTERVAL_FULL_PRECISION)).time, 90_000_000_000);
944        assert_eq!(recv(typmod(&[Day, Hour, Minute, Second], 0)).time, 90_062_000_000);
945        let error = interval_recv(&mut Recv::new(&bytes), typmod(&[Second], 7)).unwrap_err();
946        assert_eq!(error.message, "interval(7) precision must be between 0 and 6");
947        let error = interval_recv(&mut Recv::new(&bytes), typmod(&[Year, Day], 0)).unwrap_err();
948        assert_eq!(error.sqlstate.as_str(), "XX000");
949    }
950}