vibesql_types/temporal/
interval.rs1use std::{cmp::Ordering, fmt, str::FromStr};
9
10#[derive(Debug, Clone)]
23pub struct Interval {
24 pub value: String,
26 months: i32,
28 days: i32,
30 microseconds: i64,
32}
33
34impl Interval {
35 pub fn new(value: String) -> Self {
37 let (months, days, microseconds) = Self::parse_interval(&value);
39 Interval { value, months, days, microseconds }
40 }
41
42 fn parse_interval(s: &str) -> (i32, i32, i64) {
47 let mut months = 0;
48 let mut days = 0;
49 let mut microseconds = 0i64;
50
51 let parts: Vec<&str> = s.split_whitespace().collect();
54
55 if parts.is_empty() {
56 return (0, 0, 0);
57 }
58
59 if let Some(to_pos) = parts.iter().position(|&p| p.eq_ignore_ascii_case("TO")) {
61 if to_pos >= 2 {
63 let value_part = parts[0];
64 let from_unit = parts[to_pos - 1];
65 let to_unit = parts[to_pos + 1];
66
67 if from_unit.eq_ignore_ascii_case("YEAR") && to_unit.eq_ignore_ascii_case("MONTH") {
69 if let Some(dash_pos) = value_part.find('-') {
70 let years: i32 = value_part[..dash_pos].parse().unwrap_or(0);
71 let month_part: i32 = value_part[dash_pos + 1..].parse().unwrap_or(0);
72 months = years * 12 + month_part;
73 } else {
74 let years: i32 = value_part.parse().unwrap_or(0);
75 months = years * 12;
76 }
77 }
78 else if from_unit.eq_ignore_ascii_case("DAY") {
80 if let Some(space_pos) = value_part.find(' ') {
81 days = value_part[..space_pos].parse().unwrap_or(0);
82 let time_part = value_part[space_pos + 1..].trim();
84 microseconds = Self::parse_time_to_microseconds(time_part);
85 } else {
86 days = value_part.parse().unwrap_or(0);
87 }
88 }
89 else if from_unit.eq_ignore_ascii_case("HOUR")
91 || from_unit.eq_ignore_ascii_case("MINUTE")
92 || from_unit.eq_ignore_ascii_case("SECOND")
93 {
94 microseconds = Self::parse_time_to_microseconds(value_part);
95 }
96 }
97 } else {
98 if parts.len() >= 2 {
100 let value_part = parts[0];
101 let unit = parts[1];
102
103 match unit.to_uppercase().as_str() {
104 "YEAR" | "YEARS" => {
105 let years: i32 = value_part.parse().unwrap_or(0);
106 months = years * 12;
107 }
108 "MONTH" | "MONTHS" => {
109 months = value_part.parse().unwrap_or(0);
110 }
111 "DAY" | "DAYS" => {
112 days = value_part.parse().unwrap_or(0);
113 }
114 "HOUR" | "HOURS" => {
115 let hours: i64 = value_part.parse().unwrap_or(0);
116 microseconds = hours * 3600 * 1_000_000;
117 }
118 "MINUTE" | "MINUTES" => {
119 let minutes: i64 = value_part.parse().unwrap_or(0);
120 microseconds = minutes * 60 * 1_000_000;
121 }
122 "SECOND" | "SECONDS" => {
123 microseconds = Self::parse_seconds_to_microseconds(value_part);
124 }
125 _ => {}
126 }
127 }
128 }
129
130 (months, days, microseconds)
131 }
132
133 fn parse_time_to_microseconds(s: &str) -> i64 {
135 let parts: Vec<&str> = s.split(':').collect();
136 let mut total_microseconds = 0i64;
137
138 if !parts.is_empty() {
139 if let Ok(hours) = parts[0].parse::<i64>() {
141 total_microseconds += hours * 3600 * 1_000_000;
142 }
143 }
144
145 if parts.len() > 1 {
146 if let Ok(minutes) = parts[1].parse::<i64>() {
148 total_microseconds += minutes * 60 * 1_000_000;
149 }
150 }
151
152 if parts.len() > 2 {
153 total_microseconds += Self::parse_seconds_to_microseconds(parts[2]);
155 }
156
157 total_microseconds
158 }
159
160 fn parse_seconds_to_microseconds(s: &str) -> i64 {
162 if let Some(dot_pos) = s.find('.') {
163 let whole: i64 = s[..dot_pos].parse().unwrap_or(0);
164 let frac_str = &s[dot_pos + 1..];
165 let frac_str_padded = format!("{:0<6}", frac_str);
167 let frac: i64 = frac_str_padded[..6].parse().unwrap_or(0);
168 whole * 1_000_000 + frac
169 } else {
170 s.parse::<i64>().unwrap_or(0) * 1_000_000
171 }
172 }
173
174 fn cmp_value(&self) -> i128 {
185 let total_days = (self.months as i64) * 30 + (self.days as i64);
187
188 let days_in_microseconds = total_days as i128 * 86_400_000_000i128;
190
191 days_in_microseconds + (self.microseconds as i128)
193 }
194}
195
196impl FromStr for Interval {
197 type Err = String;
198
199 fn from_str(s: &str) -> Result<Self, Self::Err> {
200 Ok(Interval::new(s.to_string()))
203 }
204}
205
206impl fmt::Display for Interval {
207 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
208 write!(f, "{}", self.value)
209 }
210}
211
212impl PartialOrd for Interval {
213 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
214 Some(self.cmp(other))
215 }
216}
217
218impl Ord for Interval {
219 fn cmp(&self, other: &Self) -> Ordering {
220 self.cmp_value().cmp(&other.cmp_value())
221 }
222}
223
224impl PartialEq for Interval {
225 fn eq(&self, other: &Self) -> bool {
226 self.months == other.months
229 && self.days == other.days
230 && self.microseconds == other.microseconds
231 }
232}
233
234impl Eq for Interval {}
235
236impl std::hash::Hash for Interval {
237 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
238 self.months.hash(state);
241 self.days.hash(state);
242 self.microseconds.hash(state);
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249
250 #[test]
251 fn test_interval_comparison_simple_years() {
252 let i1 = Interval::new("1 YEAR".to_string());
253 let i2 = Interval::new("2 YEAR".to_string());
254 assert!(i1 < i2, "1 YEAR should be less than 2 YEAR");
255 }
256
257 #[test]
258 fn test_interval_comparison_simple_months() {
259 let i1 = Interval::new("1 MONTH".to_string());
260 let i2 = Interval::new("6 MONTH".to_string());
261 assert!(i1 < i2, "1 MONTH should be less than 6 MONTH");
262 }
263
264 #[test]
265 fn test_interval_comparison_simple_days() {
266 let i1 = Interval::new("1 DAY".to_string());
267 let i2 = Interval::new("30 DAY".to_string());
268 assert!(i1 < i2, "1 DAY should be less than 30 DAY");
269 }
270
271 #[test]
272 fn test_interval_comparison_month_vs_days() {
273 let i1 = Interval::new("1 MONTH".to_string());
277 let i2 = Interval::new("30 DAY".to_string());
278 assert_ne!(i1, i2, "1 MONTH and 30 DAY have different representations");
279 assert_eq!(
280 i1.cmp(&i2),
281 Ordering::Equal,
282 "1 MONTH should compare equal to 30 DAY (approximation)"
283 );
284 }
285
286 #[test]
287 fn test_interval_comparison_year_vs_months() {
288 let i1 = Interval::new("1 YEAR".to_string());
290 let i2 = Interval::new("12 MONTH".to_string());
291 assert_eq!(i1, i2, "1 YEAR should equal 12 MONTH");
292 }
293
294 #[test]
295 fn test_interval_comparison_year_vs_days() {
296 let i1 = Interval::new("1 YEAR".to_string());
300 let i2 = Interval::new("360 DAY".to_string());
301 assert_ne!(i1, i2, "1 YEAR and 360 DAY have different representations");
302 assert_eq!(
303 i1.cmp(&i2),
304 Ordering::Equal,
305 "1 YEAR should compare equal to 360 DAY (approximation)"
306 );
307 }
308
309 #[test]
310 fn test_interval_comparison_hours() {
311 let i1 = Interval::new("1 HOUR".to_string());
312 let i2 = Interval::new("2 HOUR".to_string());
313 assert!(i1 < i2, "1 HOUR should be less than 2 HOUR");
314 }
315
316 #[test]
317 fn test_interval_comparison_minutes() {
318 let i1 = Interval::new("30 MINUTE".to_string());
319 let i2 = Interval::new("90 MINUTE".to_string());
320 assert!(i1 < i2, "30 MINUTE should be less than 90 MINUTE");
321 }
322
323 #[test]
324 fn test_interval_comparison_seconds() {
325 let i1 = Interval::new("45 SECOND".to_string());
326 let i2 = Interval::new("90 SECOND".to_string());
327 assert!(i1 < i2, "45 SECOND should be less than 90 SECOND");
328 }
329
330 #[test]
331 fn test_interval_comparison_year_to_month() {
332 let i1 = Interval::new("1-6 YEAR TO MONTH".to_string());
334 let i2 = Interval::new("18 MONTH".to_string());
335 assert_eq!(i1, i2, "1-6 YEAR TO MONTH should equal 18 MONTH");
336 }
337
338 #[test]
339 fn test_interval_comparison_year_to_month_ordering() {
340 let i1 = Interval::new("1-0 YEAR TO MONTH".to_string());
341 let i2 = Interval::new("1-6 YEAR TO MONTH".to_string());
342 let i3 = Interval::new("2-0 YEAR TO MONTH".to_string());
343 assert!(i1 < i2, "1-0 should be less than 1-6");
344 assert!(i2 < i3, "1-6 should be less than 2-0");
345 }
346
347 #[test]
348 fn test_interval_comparison_cross_type_month_vs_day() {
349 let i1 = Interval::new("1 MONTH".to_string());
351 let i2 = Interval::new("31 DAY".to_string());
352 assert!(i1 < i2, "1 MONTH should be less than 31 DAY");
353 }
354
355 #[test]
356 fn test_interval_comparison_cross_type_month_vs_day_greater() {
357 let i1 = Interval::new("1 MONTH".to_string());
359 let i2 = Interval::new("29 DAY".to_string());
360 assert!(i1 > i2, "1 MONTH should be greater than 29 DAY");
361 }
362
363 #[test]
364 fn test_interval_comparison_equality() {
365 let i1 = Interval::new("5 YEAR".to_string());
366 let i2 = Interval::new("5 YEAR".to_string());
367 assert_eq!(i1, i2, "Same intervals should be equal");
368 }
369
370 #[test]
371 fn test_interval_parsing_zero() {
372 let i = Interval::new("0 DAY".to_string());
373 assert_eq!(i.days, 0);
374 assert_eq!(i.months, 0);
375 assert_eq!(i.microseconds, 0);
376 }
377
378 #[test]
379 fn test_interval_ordering_total() {
380 let mut intervals = [
381 Interval::new("2 YEAR".to_string()),
382 Interval::new("1 MONTH".to_string()),
383 Interval::new("1 DAY".to_string()),
384 Interval::new("1 YEAR".to_string()),
385 Interval::new("30 DAY".to_string()),
386 ];
387
388 intervals.sort();
389
390 assert_eq!(intervals[0].value, "1 DAY");
392 assert!(
394 (intervals[1].value == "1 MONTH" && intervals[2].value == "30 DAY")
395 || (intervals[1].value == "30 DAY" && intervals[2].value == "1 MONTH")
396 );
397 assert_eq!(intervals[3].value, "1 YEAR");
398 assert_eq!(intervals[4].value, "2 YEAR");
399 }
400
401 #[test]
402 fn test_interval_cmp_value_calculation() {
403 let i1 = Interval::new("1 YEAR".to_string());
405 let expected = 360i128 * 86_400_000_000i128;
407 assert_eq!(i1.cmp_value(), expected);
408
409 let i2 = Interval::new("1 DAY".to_string());
410 let expected = 86_400_000_000i128;
412 assert_eq!(i2.cmp_value(), expected);
413
414 let i3 = Interval::new("1 HOUR".to_string());
415 let expected = 3_600_000_000i128;
417 assert_eq!(i3.cmp_value(), expected);
418 }
419
420 #[test]
421 fn test_interval_from_str() {
422 let i: Interval = "5 YEAR".parse().unwrap();
423 assert_eq!(i.value, "5 YEAR");
424 assert_eq!(i.months, 60); }
426
427 #[test]
428 fn test_interval_display() {
429 let i = Interval::new("5 YEAR".to_string());
430 assert_eq!(format!("{}", i), "5 YEAR");
431 }
432
433 #[test]
434 fn test_interval_parsing_fractional_seconds() {
435 let i = Interval::new("1.5 SECOND".to_string());
436 assert_eq!(i.microseconds, 1_500_000); }
438
439 #[test]
440 fn test_interval_comparison_with_fractional_seconds() {
441 let i1 = Interval::new("1.5 SECOND".to_string());
442 let i2 = Interval::new("2 SECOND".to_string());
443 assert!(i1 < i2, "1.5 SECOND should be less than 2 SECOND");
444 }
445
446 #[test]
447 fn test_interval_hash_consistency() {
448 use std::{
449 collections::hash_map::DefaultHasher,
450 hash::{Hash, Hasher},
451 };
452
453 fn calculate_hash<T: Hash>(t: &T) -> u64 {
455 let mut s = DefaultHasher::new();
456 t.hash(&mut s);
457 s.finish()
458 }
459
460 let i1 = Interval::new("1 YEAR".to_string());
462 let i2 = Interval::new("12 MONTH".to_string());
463
464 assert_eq!(i1, i2, "1 YEAR should equal 12 MONTH");
466
467 assert_eq!(
469 calculate_hash(&i1),
470 calculate_hash(&i2),
471 "Equal intervals must have equal hash values"
472 );
473
474 let i3 = Interval::new("1-6 YEAR TO MONTH".to_string());
476 let i4 = Interval::new("18 MONTH".to_string());
477
478 assert_eq!(i3, i4, "1-6 YEAR TO MONTH should equal 18 MONTH");
479 assert_eq!(
480 calculate_hash(&i3),
481 calculate_hash(&i4),
482 "Equal intervals must have equal hash values"
483 );
484
485 let i5 = Interval::new("1 YEAR".to_string());
487 let i6 = Interval::new("2 YEAR".to_string());
488
489 assert_ne!(i5, i6, "1 YEAR should not equal 2 YEAR");
490 }
493}