Skip to main content

pg_srv/values/
interval.rs

1//! Interval value representation for PostgreSQL protocol
2
3use crate::{ProtocolError, ToProtocolValue};
4use bytes::{BufMut, BytesMut};
5use std::fmt::{Display, Formatter};
6
7#[derive(Debug, Clone, Default)]
8pub struct IntervalValue {
9    pub months: i32,
10    pub days: i32,
11    pub hours: i32,
12    pub mins: i32,
13    pub secs: i32,
14    pub usecs: i32,
15}
16
17impl IntervalValue {
18    pub fn new(months: i32, days: i32, hours: i32, mins: i32, secs: i32, usecs: i32) -> Self {
19        Self {
20            months,
21            days,
22            hours,
23            mins,
24            secs,
25            usecs,
26        }
27    }
28
29    pub fn is_zeroed(&self) -> bool {
30        self.months == 0
31            && self.days == 0
32            && self.hours == 0
33            && self.mins == 0
34            && self.secs == 0
35            && self.usecs == 0
36    }
37
38    pub fn extract_years_month(&self) -> (i32, i32) {
39        let years = self.months / 12;
40        let month = self.months % 12;
41
42        (years, month)
43    }
44
45    pub fn as_iso_str(&self) -> String {
46        if self.is_zeroed() {
47            return "00:00:00".to_owned();
48        }
49
50        let mut res = "".to_owned();
51        let (years, months) = self.extract_years_month();
52
53        if years != 0 {
54            if years == 1 {
55                res.push_str(&format!("{:#?} year ", years))
56            } else {
57                res.push_str(&format!("{:#?} years ", years))
58            }
59        }
60
61        if months != 0 {
62            if months == 1 {
63                res.push_str(&format!("{:#?} mon ", months));
64            } else {
65                res.push_str(&format!("{:#?} mons ", months));
66            }
67        }
68
69        if self.days != 0 {
70            if self.days == 1 {
71                res.push_str(&format!("{:#?} day ", self.days));
72            } else {
73                res.push_str(&format!("{:#?} days ", self.days));
74            }
75        }
76
77        if self.hours != 0 || self.mins != 0 || self.secs != 0 || self.usecs != 0 {
78            if self.hours < 0 || self.mins < 0 || self.secs < 0 || self.usecs < 0 {
79                res.push('-')
80            };
81
82            res.push_str(&format!(
83                "{:02}:{:02}:{:02}",
84                self.hours.abs(),
85                self.mins.abs(),
86                self.secs.abs()
87            ));
88
89            if self.usecs != 0 {
90                res.push_str(&format!(".{:06}", self.usecs.abs()))
91            }
92        }
93
94        res.trim().to_string()
95    }
96
97    pub fn as_postgresql_str(&self) -> String {
98        let (years, months) = self.extract_years_month();
99
100        // We manually format sign for the case where self.secs == 0, self.usecs < 0.
101        // We follow assumptions about consistency of hours/mins/secs/usecs signs as in
102        // as_iso_str here.
103        format!(
104            "{} years {} mons {} days {} hours {} mins {}{}.{} secs",
105            years,
106            months,
107            self.days,
108            self.hours,
109            self.mins,
110            if self.secs < 0 || self.usecs < 0 {
111                "-"
112            } else {
113                ""
114            },
115            self.secs.abs(),
116            if self.usecs == 0 {
117                "00".to_string()
118            } else {
119                format!("{:06}", self.usecs.abs())
120            }
121        )
122    }
123}
124
125impl Display for IntervalValue {
126    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
127        // TODO lift formatter higher, to as_postgresql_str
128        // https://github.com/postgres/postgres/blob/REL_14_4/src/interfaces/ecpg/pgtypeslib/interval.c#L763
129        f.write_str(&self.as_postgresql_str())
130    }
131}
132
133impl ToProtocolValue for IntervalValue {
134    // https://github.com/postgres/postgres/blob/REL_14_4/src/backend/utils/adt/timestamp.c#L958
135    fn to_text(&self, buf: &mut BytesMut) -> Result<(), ProtocolError> {
136        self.to_string().to_text(buf)
137    }
138
139    // https://github.com/postgres/postgres/blob/REL_14_4/src/backend/utils/adt/timestamp.c#L1005
140    fn to_binary(&self, buf: &mut BytesMut) -> Result<(), ProtocolError> {
141        let usecs = self.hours as i64 * 60 * 60 * 1_000_000
142            + self.mins as i64 * 60 * 1_000_000
143            + self.secs as i64 * 1_000_000
144            + self.usecs as i64;
145
146        buf.put_i32(16);
147        buf.put_i64(usecs);
148        buf.put_i32(self.days);
149        buf.put_i32(self.months);
150
151        Ok(())
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158    use crate::ProtocolError;
159
160    #[test]
161    fn test_interval_to_iso() -> Result<(), ProtocolError> {
162        assert_eq!(
163            IntervalValue::new(1, 0, 0, 0, 0, 0).as_iso_str(),
164            "1 mon".to_string()
165        );
166        assert_eq!(
167            IntervalValue::new(14, 0, 0, 0, 0, 0).as_iso_str(),
168            "1 year 2 mons".to_string()
169        );
170        assert_eq!(
171            IntervalValue::new(0, 1, 1, 1, 1, 1).as_iso_str(),
172            "1 day 01:01:01.000001".to_string()
173        );
174        assert_eq!(
175            IntervalValue::new(0, 0, -1, -1, -1, -1).as_iso_str(),
176            "-01:01:01.000001".to_string()
177        );
178        assert_eq!(
179            IntervalValue::new(0, 0, 0, 0, 0, 0).as_iso_str(),
180            "00:00:00".to_string()
181        );
182
183        Ok(())
184    }
185
186    #[test]
187    fn test_interval_to_postgres() -> Result<(), ProtocolError> {
188        assert_eq!(
189            IntervalValue::new(0, 0, 0, 0, 0, 0).to_string(),
190            "0 years 0 mons 0 days 0 hours 0 mins 0.00 secs".to_string()
191        );
192
193        assert_eq!(
194            IntervalValue::new(0, 0, 0, 0, 1, 23).to_string(),
195            "0 years 0 mons 0 days 0 hours 0 mins 1.000023 secs".to_string()
196        );
197
198        assert_eq!(
199            IntervalValue::new(0, 0, 0, 0, -1, -23).to_string(),
200            "0 years 0 mons 0 days 0 hours 0 mins -1.000023 secs".to_string()
201        );
202
203        assert_eq!(
204            IntervalValue::new(0, 0, 0, 0, -1, 0).to_string(),
205            "0 years 0 mons 0 days 0 hours 0 mins -1.00 secs".to_string()
206        );
207
208        assert_eq!(
209            IntervalValue::new(0, 0, -14, -5, -1, 0).to_string(),
210            "0 years 0 mons 0 days -14 hours -5 mins -1.00 secs".to_string()
211        );
212
213        Ok(())
214    }
215}