Skip to main content

vibesql_types/temporal/
date.rs

1//! SQL DATE type implementation
2
3use std::{cmp::Ordering, fmt, str::FromStr};
4
5/// SQL DATE type - represents a date without time
6///
7/// Format: YYYY-MM-DD (e.g., '2024-01-01')
8/// Stored as year, month, day components for correct comparison
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub struct Date {
11    pub year: i32,
12    pub month: u8, // 1-12
13    pub day: u8,   // 1-31
14}
15
16impl Date {
17    /// Create a new Date (validation is basic)
18    pub fn new(year: i32, month: u8, day: u8) -> Result<Self, String> {
19        if !(1..=12).contains(&month) {
20            return Err(format!("Invalid month: {}", month));
21        }
22        if !(1..=31).contains(&day) {
23            return Err(format!("Invalid day: {}", day));
24        }
25        Ok(Date { year, month, day })
26    }
27}
28
29impl FromStr for Date {
30    type Err = String;
31
32    fn from_str(s: &str) -> Result<Self, Self::Err> {
33        // Parse format: YYYY-MM-DD
34        let parts: Vec<&str> = s.split('-').collect();
35        if parts.len() != 3 {
36            return Err(format!("Invalid date format: '{}' (expected YYYY-MM-DD)", s));
37        }
38
39        let year = parts[0].parse::<i32>().map_err(|_| format!("Invalid year: '{}'", parts[0]))?;
40        let month = parts[1].parse::<u8>().map_err(|_| format!("Invalid month: '{}'", parts[1]))?;
41        let day = parts[2].parse::<u8>().map_err(|_| format!("Invalid day: '{}'", parts[2]))?;
42
43        Date::new(year, month, day)
44    }
45}
46
47impl fmt::Display for Date {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        // Zero-pad the year to 4 digits excluding the sign: SQLite renders
50        // negative (astronomical) years as e.g. '-0900-02-28', but Rust's
51        // `{:04}` counts the '-' toward the width (giving '-900').
52        if self.year < 0 {
53            write!(f, "-{:04}-{:02}-{:02}", -self.year, self.month, self.day)
54        } else {
55            write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
56        }
57    }
58}
59
60impl PartialOrd for Date {
61    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
62        Some(self.cmp(other))
63    }
64}
65
66impl Ord for Date {
67    fn cmp(&self, other: &Self) -> Ordering {
68        self.year
69            .cmp(&other.year)
70            .then_with(|| self.month.cmp(&other.month))
71            .then_with(|| self.day.cmp(&other.day))
72    }
73}