1use std::cmp::Ordering;
22
23use crate::error::{FraiseQLError, Result};
24
25fn parse_date(date_str: &str) -> Result<(u32, u32, u32)> {
27 let parts: Vec<&str> = date_str.split('-').collect();
28 if parts.len() != 3 {
29 return Err(FraiseQLError::Validation {
30 message: format!("Invalid date format: '{}'. Expected YYYY-MM-DD", date_str),
31 path: None,
32 });
33 }
34
35 let year = parts[0].parse::<u32>().map_err(|_| FraiseQLError::Validation {
36 message: format!("Invalid year: '{}'", parts[0]),
37 path: None,
38 })?;
39
40 let month = parts[1].parse::<u32>().map_err(|_| FraiseQLError::Validation {
41 message: format!("Invalid month: '{}'", parts[1]),
42 path: None,
43 })?;
44
45 let day = parts[2].parse::<u32>().map_err(|_| FraiseQLError::Validation {
46 message: format!("Invalid day: '{}'", parts[2]),
47 path: None,
48 })?;
49
50 if !(1..=12).contains(&month) {
51 return Err(FraiseQLError::Validation {
52 message: format!("Month must be between 1 and 12, got {}", month),
53 path: None,
54 });
55 }
56
57 let days_in_month = get_days_in_month(month, year);
58 if !(1..=days_in_month).contains(&day) {
59 return Err(FraiseQLError::Validation {
60 message: format!("Day must be between 1 and {}, got {}", days_in_month, day),
61 path: None,
62 });
63 }
64
65 Ok((year, month, day))
66}
67
68fn is_leap_year(year: u32) -> bool {
70 (year.is_multiple_of(4) && !year.is_multiple_of(100)) || year.is_multiple_of(400)
71}
72
73fn get_days_in_month(month: u32, year: u32) -> u32 {
75 match month {
76 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
77 4 | 6 | 9 | 11 => 30,
78 2 => {
79 if is_leap_year(year) {
80 29
81 } else {
82 28
83 }
84 },
85 _ => 0,
86 }
87}
88
89fn get_today() -> (u32, u32, u32) {
92 (2026, 2, 8)
95}
96
97fn compare_dates(left: (u32, u32, u32), right: (u32, u32, u32)) -> i32 {
99 match left.0.cmp(&right.0) {
100 Ordering::Less => -1,
101 Ordering::Greater => 1,
102 Ordering::Equal => match left.1.cmp(&right.1) {
103 Ordering::Less => -1,
104 Ordering::Greater => 1,
105 Ordering::Equal => match left.2.cmp(&right.2) {
106 Ordering::Less => -1,
107 Ordering::Greater => 1,
108 Ordering::Equal => 0,
109 },
110 },
111 }
112}
113
114fn days_between(left: (u32, u32, u32), right: (u32, u32, u32)) -> i64 {
116 let days_left = i64::from(left.0) * 365 + i64::from(left.1) * 31 + i64::from(left.2);
118 let days_right = i64::from(right.0) * 365 + i64::from(right.1) * 31 + i64::from(right.2);
119 days_left - days_right
120}
121
122pub fn validate_min_date(date_str: &str, min_date_str: &str) -> Result<()> {
124 let date = parse_date(date_str)?;
125 let min_date = parse_date(min_date_str)?;
126
127 if compare_dates(date, min_date) < 0 {
128 return Err(FraiseQLError::Validation {
129 message: format!("Date '{}' must be >= '{}'", date_str, min_date_str),
130 path: None,
131 });
132 }
133
134 Ok(())
135}
136
137pub fn validate_max_date(date_str: &str, max_date_str: &str) -> Result<()> {
139 let date = parse_date(date_str)?;
140 let max_date = parse_date(max_date_str)?;
141
142 if compare_dates(date, max_date) > 0 {
143 return Err(FraiseQLError::Validation {
144 message: format!("Date '{}' must be <= '{}'", date_str, max_date_str),
145 path: None,
146 });
147 }
148
149 Ok(())
150}
151
152pub fn validate_date_range(date_str: &str, min_date_str: &str, max_date_str: &str) -> Result<()> {
154 validate_min_date(date_str, min_date_str)?;
155 validate_max_date(date_str, max_date_str)?;
156 Ok(())
157}
158
159pub fn validate_min_age(date_str: &str, min_age: u32) -> Result<()> {
161 let birth_date = parse_date(date_str)?;
162 let today = get_today();
163
164 let mut age = today.0 - birth_date.0;
166 if (today.1, today.2) < (birth_date.1, birth_date.2) {
167 age -= 1;
168 }
169
170 if age < min_age {
171 return Err(FraiseQLError::Validation {
172 message: format!("Age must be at least {} years old, got {}", min_age, age),
173 path: None,
174 });
175 }
176
177 Ok(())
178}
179
180pub fn validate_max_age(date_str: &str, max_age: u32) -> Result<()> {
182 let birth_date = parse_date(date_str)?;
183 let today = get_today();
184
185 let mut age = today.0 - birth_date.0;
187 if (today.1, today.2) < (birth_date.1, birth_date.2) {
188 age -= 1;
189 }
190
191 if age > max_age {
192 return Err(FraiseQLError::Validation {
193 message: format!("Age must be at most {} years old, got {}", max_age, age),
194 path: None,
195 });
196 }
197
198 Ok(())
199}
200
201pub fn validate_max_days_in_future(date_str: &str, max_days: i64) -> Result<()> {
203 let date = parse_date(date_str)?;
204 let today = get_today();
205
206 let days_diff = days_between(date, today);
207 if days_diff > max_days {
208 return Err(FraiseQLError::Validation {
209 message: format!(
210 "Date '{}' cannot be more than {} days in the future",
211 date_str, max_days
212 ),
213 path: None,
214 });
215 }
216
217 Ok(())
218}
219
220pub fn validate_max_days_in_past(date_str: &str, max_days: i64) -> Result<()> {
222 let date = parse_date(date_str)?;
223 let today = get_today();
224
225 let days_diff = days_between(today, date);
226 if days_diff > max_days {
227 return Err(FraiseQLError::Validation {
228 message: format!(
229 "Date '{}' cannot be more than {} days in the past",
230 date_str, max_days
231 ),
232 path: None,
233 });
234 }
235
236 Ok(())
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 #[test]
244 fn test_parse_date_valid() {
245 let result = parse_date("2026-02-08");
246 assert!(result.is_ok());
247 assert_eq!(result.unwrap(), (2026, 2, 8));
248 }
249
250 #[test]
251 fn test_parse_date_invalid_format() {
252 assert!(parse_date("2026/02/08").is_err());
253 assert!(parse_date("02-08-2026").is_err());
254 }
255
256 #[test]
257 fn test_parse_date_invalid_month() {
258 assert!(parse_date("2026-13-01").is_err());
259 assert!(parse_date("2026-00-01").is_err());
260 }
261
262 #[test]
263 fn test_parse_date_invalid_day() {
264 assert!(parse_date("2026-02-30").is_err());
265 assert!(parse_date("2026-04-31").is_err());
266 }
267
268 #[test]
269 fn test_leap_year_detection() {
270 assert!(is_leap_year(2024));
271 assert!(is_leap_year(2000));
272 assert!(!is_leap_year(1900));
273 assert!(!is_leap_year(2025));
274 }
275
276 #[test]
277 fn test_days_in_month() {
278 assert_eq!(get_days_in_month(1, 2026), 31);
279 assert_eq!(get_days_in_month(2, 2024), 29); assert_eq!(get_days_in_month(2, 2026), 28); assert_eq!(get_days_in_month(4, 2026), 30);
282 }
283
284 #[test]
285 fn test_compare_dates() {
286 assert!(compare_dates((2026, 2, 8), (2026, 2, 7)) > 0);
287 assert!(compare_dates((2026, 2, 7), (2026, 2, 8)) < 0);
288 assert_eq!(compare_dates((2026, 2, 8), (2026, 2, 8)), 0);
289 assert!(compare_dates((2026, 3, 1), (2026, 2, 28)) > 0);
290 assert!(compare_dates((2027, 1, 1), (2026, 12, 31)) > 0);
291 }
292
293 #[test]
294 fn test_min_date_passes() {
295 assert!(validate_min_date("2026-02-08", "2026-02-01").is_ok());
296 assert!(validate_min_date("2026-02-08", "2026-02-08").is_ok());
297 }
298
299 #[test]
300 fn test_min_date_fails() {
301 assert!(validate_min_date("2026-02-08", "2026-02-09").is_err());
302 }
303
304 #[test]
305 fn test_max_date_passes() {
306 assert!(validate_max_date("2026-02-08", "2026-02-15").is_ok());
307 assert!(validate_max_date("2026-02-08", "2026-02-08").is_ok());
308 }
309
310 #[test]
311 fn test_max_date_fails() {
312 assert!(validate_max_date("2026-02-08", "2026-02-07").is_err());
313 }
314
315 #[test]
316 fn test_date_range_passes() {
317 assert!(validate_date_range("2026-02-08", "2026-01-01", "2026-12-31").is_ok());
318 }
319
320 #[test]
321 fn test_date_range_fails_below_min() {
322 assert!(validate_date_range("2025-12-31", "2026-01-01", "2026-12-31").is_err());
323 }
324
325 #[test]
326 fn test_date_range_fails_above_max() {
327 assert!(validate_date_range("2027-01-01", "2026-01-01", "2026-12-31").is_err());
328 }
329
330 #[test]
331 fn test_min_age_passes() {
332 assert!(validate_min_age("2000-01-01", 25).is_ok());
334 assert!(validate_min_age("2000-01-01", 26).is_ok());
335 }
336
337 #[test]
338 fn test_min_age_fails() {
339 assert!(validate_min_age("2010-03-15", 16).is_err());
341 }
342
343 #[test]
344 fn test_min_age_birthday_today() {
345 assert!(validate_min_age("2008-02-08", 18).is_ok());
347 }
348
349 #[test]
350 fn test_min_age_before_birthday_this_year() {
351 assert!(validate_min_age("2008-03-15", 18).is_err());
353 }
354
355 #[test]
356 fn test_max_age_passes() {
357 assert!(validate_max_age("2010-01-01", 17).is_ok());
359 assert!(validate_max_age("2010-01-01", 16).is_ok());
360 }
361
362 #[test]
363 fn test_max_age_fails() {
364 assert!(validate_max_age("1990-01-01", 35).is_err());
366 }
367
368 #[test]
369 fn test_max_days_in_future_passes() {
370 assert!(validate_max_days_in_future("2026-02-10", 30).is_ok());
372 }
373
374 #[test]
375 fn test_max_days_in_future_fails() {
376 assert!(validate_max_days_in_future("2026-03-15", 30).is_err());
378 }
379
380 #[test]
381 fn test_max_days_in_past_passes() {
382 assert!(validate_max_days_in_past("2026-02-01", 30).is_ok());
384 }
385
386 #[test]
387 fn test_max_days_in_past_fails() {
388 assert!(validate_max_days_in_past("2026-01-01", 30).is_err());
390 }
391
392 #[test]
393 fn test_days_between_same_date() {
394 assert_eq!(days_between((2026, 2, 8), (2026, 2, 8)), 0);
395 }
396
397 #[test]
398 fn test_days_between_year_difference() {
399 let diff = days_between((2027, 2, 8), (2026, 2, 8));
400 assert!(diff > 0);
401 }
402
403 #[test]
404 fn test_february_leap_year_edge_case() {
405 assert!(parse_date("2024-02-29").is_ok());
407 assert!(parse_date("2024-02-30").is_err());
408 }
409
410 #[test]
411 fn test_february_non_leap_year_edge_case() {
412 assert!(parse_date("2025-02-28").is_ok());
414 assert!(parse_date("2025-02-29").is_err());
415 }
416
417 #[test]
418 fn test_year_2000_leap_year() {
419 assert!(is_leap_year(2000));
421 assert!(parse_date("2000-02-29").is_ok());
422 }
423
424 #[test]
425 fn test_year_1900_not_leap_year() {
426 assert!(!is_leap_year(1900));
428 assert!(parse_date("1900-02-29").is_err());
429 }
430
431 #[test]
432 fn test_age_calculation_before_birthday() {
433 assert!(validate_min_age("2000-05-15", 26).is_err());
436 assert!(validate_min_age("2000-05-15", 25).is_ok());
437 }
438
439 #[test]
440 fn test_age_calculation_after_birthday() {
441 assert!(validate_min_age("2000-01-15", 26).is_ok());
444 }
445}