validator_rs/
date.rs

1//! Date and time validation functions
2
3use regex::Regex;
4use std::sync::OnceLock;
5
6static DATE_REGEX: OnceLock<Regex> = OnceLock::new();
7static DATETIME_REGEX: OnceLock<Regex> = OnceLock::new();
8
9fn get_date_regex() -> &'static Regex {
10    DATE_REGEX.get_or_init(|| {
11        Regex::new(r"^\d{4}-\d{2}-\d{2}$")
12            .expect("Failed to compile date regex")
13    })
14}
15
16fn get_datetime_regex() -> &'static Regex {
17    DATETIME_REGEX.get_or_init(|| {
18        Regex::new(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$")
19            .expect("Failed to compile datetime regex")
20    })
21}
22
23/// Validates if a string is in ISO 8601 date format (YYYY-MM-DD)
24///
25/// # Examples
26///
27/// ```
28/// use validator_rs::date::is_valid_date;
29///
30/// assert!(is_valid_date("2023-12-31"));
31/// assert!(!is_valid_date("31-12-2023"));
32/// ```
33pub fn is_valid_date(date: &str) -> bool {
34    if !get_date_regex().is_match(date) {
35        return false;
36    }
37
38    let parts: Vec<&str> = date.split('-').collect();
39    if parts.len() != 3 {
40        return false;
41    }
42
43    let year = parts[0].parse::<i32>().ok();
44    let month = parts[1].parse::<u32>().ok();
45    let day = parts[2].parse::<u32>().ok();
46
47    match (year, month, day) {
48        (Some(y), Some(m), Some(d)) => is_valid_date_parts(y, m, d),
49        _ => false,
50    }
51}
52
53/// Validates if year, month, and day form a valid date
54fn is_valid_date_parts(year: i32, month: u32, day: u32) -> bool {
55    if !(1..=12).contains(&month) || day < 1 {
56        return false;
57    }
58
59    let max_day = match month {
60        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
61        4 | 6 | 9 | 11 => 30,
62        2 => {
63            if is_leap_year(year) {
64                29
65            } else {
66                28
67            }
68        }
69        _ => return false,
70    };
71
72    day <= max_day
73}
74
75/// Checks if a year is a leap year
76fn is_leap_year(year: i32) -> bool {
77    (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
78}
79
80/// Validates if a string is in ISO 8601 datetime format
81pub fn is_valid_datetime(datetime: &str) -> bool {
82    get_datetime_regex().is_match(datetime)
83}
84
85/// Validates if a string is a valid time in HH:MM:SS format
86pub fn is_valid_time(time: &str) -> bool {
87    let parts: Vec<&str> = time.split(':').collect();
88    if parts.len() != 3 {
89        return false;
90    }
91
92    let hour = parts[0].parse::<u32>().ok();
93    let minute = parts[1].parse::<u32>().ok();
94    let second = parts[2].parse::<u32>().ok();
95
96    match (hour, minute, second) {
97        (Some(h), Some(m), Some(s)) => h < 24 && m < 60 && s < 60,
98        _ => false,
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn test_valid_dates() {
108        assert!(is_valid_date("2023-12-31"));
109        assert!(is_valid_date("2024-02-29")); // Leap year
110        assert!(is_valid_date("2000-01-01"));
111    }
112
113    #[test]
114    fn test_invalid_dates() {
115        assert!(!is_valid_date(""));
116        assert!(!is_valid_date("2023-13-01")); // Invalid month
117        assert!(!is_valid_date("2023-02-30")); // Invalid day
118        assert!(!is_valid_date("2023-02-29")); // Not a leap year
119        assert!(!is_valid_date("31-12-2023")); // Wrong format
120        assert!(!is_valid_date("2023/12/31")); // Wrong separator
121    }
122
123    #[test]
124    fn test_leap_year() {
125        assert!(is_leap_year(2024));
126        assert!(is_leap_year(2000));
127        assert!(!is_leap_year(2023));
128        assert!(!is_leap_year(1900));
129    }
130
131    #[test]
132    fn test_valid_datetime() {
133        assert!(is_valid_datetime("2023-12-31T23:59:59Z"));
134        assert!(is_valid_datetime("2023-12-31T23:59:59.123Z"));
135        assert!(is_valid_datetime("2023-12-31T23:59:59+05:30"));
136    }
137
138    #[test]
139    fn test_invalid_datetime() {
140        assert!(!is_valid_datetime("2023-12-31"));
141        assert!(!is_valid_datetime("2023-12-31 23:59:59"));
142    }
143
144    #[test]
145    fn test_valid_time() {
146        assert!(is_valid_time("12:30:45"));
147        assert!(is_valid_time("00:00:00"));
148        assert!(is_valid_time("23:59:59"));
149    }
150
151    #[test]
152    fn test_invalid_time() {
153        assert!(!is_valid_time("24:00:00"));
154        assert!(!is_valid_time("12:60:00"));
155        assert!(!is_valid_time("12:30:60"));
156        assert!(!is_valid_time("12:30"));
157    }
158}
159