gluesql_core/data/interval/
string.rs

1use {
2    super::{DAY, HOUR, Interval, IntervalError, MINUTE, SECOND},
3    crate::{
4        ast::{Expr, ToSql},
5        parse_sql::parse_interval,
6        result::Result,
7        translate::{NO_PARAMS, translate_expr},
8    },
9};
10
11impl Interval {
12    pub fn parse(s: &str) -> Result<Self> {
13        let parsed = parse_interval(s)?;
14
15        match translate_expr(&parsed, NO_PARAMS)? {
16            Expr::Interval {
17                expr,
18                leading_field,
19                last_field,
20            } => {
21                let literal = match expr.as_ref() {
22                    Expr::Literal(literal) => literal.to_sql(),
23                    _ => {
24                        return Err(IntervalError::ParseSupportedOnlyLiteral {
25                            expr: Box::new(Expr::Interval {
26                                expr,
27                                leading_field,
28                                last_field,
29                            }),
30                        }
31                        .into());
32                    }
33                };
34
35                Interval::try_from_str(&literal, leading_field, last_field)
36            }
37            _ => Err(IntervalError::Unreachable.into()),
38        }
39    }
40
41    pub fn to_sql_str(&self) -> String {
42        match self {
43            Interval::Month(v) => {
44                let v = *v;
45                let (sign, v) = if v < 0 { ("-", -v) } else { ("", v) };
46
47                let year = v / 12;
48                let month = v % 12;
49
50                match (year, month) {
51                    (_, 0) if year != 0 => format!("'{sign}{year}' YEAR"),
52                    (0, _) => format!("'{sign}{month}' MONTH"),
53                    _ => format!("'{sign}{year}-{month}' YEAR TO MONTH"),
54                }
55            }
56            Interval::Microsecond(v) => {
57                let v = *v;
58                let (sign, v) = if v < 0 { ("-", -v) } else { ("", v) };
59
60                let day = v / DAY;
61                let hour = (v % DAY) / HOUR;
62                let minute = (v % HOUR) / MINUTE;
63                let second = (v % MINUTE) / SECOND;
64                let microsecond = v % SECOND;
65
66                macro_rules! micro {
67                    () => {
68                        format!("{:06}", microsecond).trim_end_matches('0')
69                    };
70                }
71
72                macro_rules! f {
73                    ($template: literal; $( $value: expr )*; $from_to: literal) => {
74                        format!("'{}{}' {}", sign, format!($template, $( $value ),*), $from_to)
75                    };
76
77                    (DAY $template: literal) => {
78                        f!($template; day; "DAY")
79                    };
80                    (HOUR $template: literal) => {
81                        f!($template; hour; "HOUR")
82                    };
83                    (MINUTE $template: literal) => {
84                        f!($template; minute; "MINUTE")
85                    };
86                    (SECOND $template: literal) => {
87                        f!($template; second; "SECOND")
88                    };
89                    (MICRO $template: literal) => {
90                        f!($template; second micro!(); "SECOND")
91                    };
92
93                    // MINUTE TO ..
94                    (MINUTE TO SECOND $template: literal) => {
95                        f!($template; minute second; "MINUTE TO SECOND")
96                    };
97                    (MINUTE TO MICRO $template: literal) => {
98                        f!($template; minute second micro!(); "MINUTE TO SECOND")
99                    };
100
101                    // HOUR TO ..
102                    (HOUR TO MINUTE $template: literal) => {
103                        f!($template; hour minute; "HOUR TO MINUTE")
104                    };
105                    (HOUR TO SECOND $template: literal) => {
106                        f!($template; hour minute second; "HOUR TO SECOND")
107                    };
108                    (HOUR TO MICRO $template: literal) => {
109                        f!($template; hour minute second micro!(); "HOUR TO SECOND")
110                    };
111
112                    // DAY TO ..
113                    (DAY TO HOUR $template: literal) => {
114                        f!($template; day hour; "DAY TO HOUR")
115                    };
116                    (DAY TO MINUTE $template: literal) => {
117                        f!($template; day hour minute; "DAY TO MINUTE")
118                    };
119                    (DAY TO SECOND $template: literal) => {
120                        f!($template; day hour minute second; "DAY TO SECOND")
121                    };
122                    (DAY TO MICRO $template: literal) => {
123                        f!($template; day hour minute second micro!(); "DAY TO SECOND")
124                    };
125                }
126
127                match (day, hour, minute, second, microsecond) {
128                    (_, 0, 0, 0, 0) if day != 0 => f!(DAY "{}"),
129                    (0, _, 0, 0, 0) if hour != 0 => f!(HOUR "{}"),
130                    (0, 0, _, 0, 0) if minute != 0 => f!(MINUTE "{}"),
131                    (0, 0, 0, _, 0) if second != 0 => f!(SECOND "{}"),
132                    (0, 0, 0, _, _) if microsecond != 0 => f!(MICRO "{}.{}"),
133
134                    // MINUTE TO ..
135                    (0, 0, _, _, 0) => f!(MINUTE TO SECOND "{:02}:{:02}"),
136                    (0, 0, _, _, _) => f!(MINUTE TO MICRO  "{:02}:{:02}.{}"),
137
138                    // HOUR TO ..
139                    (0, _, _, 0, 0) => f!(HOUR TO MINUTE "{:02}:{:02}"),
140                    (0, _, _, _, 0) => f!(HOUR TO SECOND "{:02}:{:02}:{:02}"),
141                    (0, _, _, _, _) => f!(HOUR TO MICRO  "{:02}:{:02}:{:02}.{}"),
142
143                    // DAY TO ..
144                    (_, _, 0, 0, 0) => f!(DAY TO HOUR   "{} {}"),
145                    (_, _, _, 0, 0) => f!(DAY TO MINUTE "{} {:02}:{:02}"),
146                    (_, _, _, _, 0) => f!(DAY TO SECOND "{} {:02}:{:02}:{:02}"),
147                    (_, _, _, _, _) => f!(DAY TO MICRO  "{} {:02}:{:02}:{:02}.{}"),
148                }
149            }
150        }
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use {
157        super::{Interval, IntervalError},
158        crate::{
159            parse_sql::parse_interval,
160            translate::{NO_PARAMS, translate_expr},
161        },
162    };
163
164    #[test]
165    fn parse() {
166        macro_rules! test {
167            ($( $value: literal $duration: ident ),* => $result: literal $from_to: tt) => {
168                let interval = interval!($( $value $duration ),*);
169                let interval_str = format!("'{}' {}", $result, stringify!($from_to));
170
171                let expected = Interval::parse(interval_str.as_str());
172                assert_eq!(Ok(interval), expected);
173                assert_eq!(interval.to_sql_str(), interval_str);
174            };
175            ($( $value: literal $duration: ident ),* => $result: literal $from: tt TO $to: tt) => {
176                let interval = interval!($( $value $duration ),*);
177                let interval_str = format!(
178                    "'{}' {} TO {}",
179                    $result,
180                    stringify!($from),
181                    stringify!($to),
182                );
183
184                let expected = Interval::parse(interval_str.as_str());
185                assert_eq!(Ok(interval), expected);
186                assert_eq!(interval.to_sql_str(), interval_str);
187            };
188        }
189
190        macro_rules! interval {
191            ($value: literal $duration:ident) => {
192                Interval::$duration($value)
193            };
194            ($interval: expr ; $value: literal $duration:ident) => {
195                $interval.add(&Interval::$duration($value)).unwrap()
196            };
197            ($v0: literal $d0: ident, $( $v1: literal $d1: ident ),*) => {
198                interval!(
199                    Interval::$d0($v0) ;
200                    $( $v1 $d1 ),*
201                )
202            };
203            ($interval: expr ; $v0: literal $d0: ident, $( $v1: literal $d1: ident ),*) => {
204                interval!(
205                    $interval.add(&Interval::$d0($v0)).unwrap() ;
206                    $( $v1 $d1 ),*
207                )
208            };
209        }
210
211        // YEAR and MONTH
212        test!(33 years            => "33" YEAR);
213        test!(-33 years           => "-33" YEAR);
214        test!(14 months           => "1-2" YEAR TO MONTH);
215        test!(-33 months          => "-2-9" YEAR TO MONTH);
216        test!(12 months           => "1" YEAR);
217        test!(11 months           => "11" MONTH);
218        test!(-3 months           => "-3" MONTH);
219        test!(33 years, 11 months => "33-11" YEAR TO MONTH);
220        test!(1 years, 100 months => "9-4" YEAR TO MONTH);
221
222        // DAY, HOUR, MINUTE and SECOND
223        test!(102 days          => "102" DAY);
224        test!(-20 days          => "-20" DAY);
225        test!(30 hours          => "1 6" DAY TO HOUR);
226        test!(24 hours          => "1" DAY);
227        test!(3 hours           => "3" HOUR);
228        test!(-5 hours          => "-5" HOUR);
229        test!(350 minutes       => "05:50" HOUR TO MINUTE);
230        test!(600 minutes       => "10" HOUR);
231        test!(59 minutes        => "59" MINUTE);
232        test!(3 minutes         => "3" MINUTE);
233        test!(-9 minutes        => "-9" MINUTE);
234        test!(7298 seconds      => "02:01:38" HOUR TO SECOND);
235        test!(98 seconds        => "01:38" MINUTE TO SECOND);
236        test!(120 seconds       => "2" MINUTE);
237        test!(30 seconds        => "30" SECOND);
238        test!(3 seconds         => "3" SECOND);
239        test!(-37 seconds       => "-37" SECOND);
240        test!(1234 milliseconds => "1.234" SECOND);
241        test!(-234 milliseconds  => "-0.234" SECOND);
242        test!(1234 microseconds => "0.001234" SECOND);
243
244        // MINUTE TO ..
245        test!(11 minutes, 30 seconds                     => "11:30" MINUTE TO SECOND);
246        test!(-11 minutes, -39 seconds                   => "-11:39" MINUTE TO SECOND);
247        test!(11 minutes, 36540 microseconds             => "11:00.03654" MINUTE TO SECOND);
248        test!(-30 minutes, -9876 microseconds            => "-30:00.009876" MINUTE TO SECOND);
249        test!(11 minutes, 30 seconds, 36540 microseconds => "11:30.03654" MINUTE TO SECOND);
250
251        // HOUR TO ..
252        test!(11 hours, 50 minutes                   => "11:50" HOUR TO MINUTE);
253        test!(-20 hours, -39 minutes                 => "-20:39" HOUR TO MINUTE);
254        test!(23 hours, 59 seconds                   => "23:00:59" HOUR TO SECOND);
255        test!(-23 hours, -59 seconds                 => "-23:00:59" HOUR TO SECOND);
256        test!(7 hours, 30040 microseconds            => "07:00:00.03004" HOUR TO SECOND);
257        test!(-7 hours, -30040 microseconds          => "-07:00:00.03004" HOUR TO SECOND);
258        test!(23 hours, 12 minutes, 59 seconds       => "23:12:59" HOUR TO SECOND);
259        test!(23 hours, 12 minutes, 300 microseconds => "23:12:00.0003" HOUR TO SECOND);
260        test!(
261            23 hours, 12 minutes, 9 seconds, 300 microseconds =>
262            "23:12:09.0003" HOUR TO SECOND
263        );
264
265        // DAY TO ..
266        test!(1 days, 1 hours                    => "1 1" DAY TO HOUR);
267        test!(-1 days, -7 hours                  => "-1 7" DAY TO HOUR);
268        test!(30 days, 5 minutes                 => "30 00:05" DAY TO MINUTE);
269        test!(-1 days, -130 minutes              => "-1 02:10" DAY TO MINUTE);
270        test!(30 days, 100 seconds               => "30 00:01:40" DAY TO SECOND);
271        test!(-5 days, -3800 seconds             => "-5 01:03:20" DAY TO SECOND);
272        test!(1 days, 1 microseconds             => "1 00:00:00.000001" DAY TO SECOND);
273        test!(-1 days, -1 microseconds           => "-1 00:00:00.000001" DAY TO SECOND);
274        test!(100 days, 2 hours, 3 minutes       => "100 02:03" DAY TO MINUTE);
275        test!(2 days, 20 hours, 3 seconds        => "2 20:00:03" DAY TO SECOND);
276        test!(2 days, 20 hours, 234 milliseconds => "2 20:00:00.234" DAY TO SECOND);
277        test!(
278            30 days, 30 hours, 100 seconds, 3 microseconds =>
279            "31 06:01:40.000003" DAY TO SECOND
280        );
281    }
282
283    #[test]
284    fn parse_rejects_non_literal_expr() {
285        let error = Interval::parse("INTERVAL value DAY").unwrap_err();
286
287        let parsed = parse_interval("INTERVAL value DAY").unwrap();
288        let expected_expr = translate_expr(&parsed, NO_PARAMS).unwrap();
289
290        assert_eq!(
291            error,
292            IntervalError::ParseSupportedOnlyLiteral {
293                expr: Box::new(expected_expr),
294            }
295            .into(),
296        );
297    }
298}