Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_datime/src/validation/validator.rs

14.1 KiB, 67 runs

created by r1870400018:6405, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! Validation of CalClock, CalendarDate and ClockTime values.
2//!
3//! A validator carries a set of rules and a strictness setting, and reports
4//! every failure it finds rather than stopping at the first.
5//!
6//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
7//! Anthropic Claude
8
9use crate::{
10 calendar::CalendarDate,
11 clock::ClockTime,
12 time::CalClock,
13 validation::ValidationRule,
14};
15
16use oxedyne_fe2o3_core::prelude::*;
17
18use std::fmt;
19
20#[derive(Clone, Debug)]
21pub struct ValidationError {
22 pub rule: String, // which rule rejected it
23 pub message: String,
24 pub field: Option<String>,
25 pub value: Option<String>, // what was given
26 pub expected: Option<String>, // what would have been accepted
27}
28
29impl ValidationError {
30 pub fn new<S: Into<String>>(rule: S, message: S) -> Self {
31 Self {
32 rule: rule.into(),
33 message: message.into(),
34 field: None,
35 value: None,
36 expected: None,
37 }
38 }
39
40 pub fn field<S: Into<String>>(mut self, field: S) -> Self {
41 self.field = Some(field.into());
42 self
43 }
44
45 pub fn value<S: Into<String>>(mut self, value: S) -> Self {
46 self.value = Some(value.into());
47 self
48 }
49
50 pub fn expected<S: Into<String>>(mut self, expected: S) -> Self {
51 self.expected = Some(expected.into());
52 self
53 }
54}
55
56impl fmt::Display for ValidationError {
57 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58 ok!(write!(f, "[{}] {}", self.rule, self.message));
59
60 if let Some(ref field) = self.field {
61 ok!(write!(f, " (field: {})", field));
62 }
63
64 if let Some(ref value) = self.value {
65 ok!(write!(f, " (value: {})", value));
66 }
67
68 if let Some(ref expected) = self.expected {
69 ok!(write!(f, " (expected: {})", expected));
70 }
71
72 Ok(())
73 }
74}
75
76pub type ValidationResult = Result<(), Vec<ValidationError>>;
77
78/// # Examples
79///
80/// ```ignore
81/// use oxedyne_fe2o3_datime::{
82/// validation::CalClockValidator,
83/// time::CalClock,
84/// };
85///
86/// let validator = CalClockValidator::new();
87/// let calclock = res!(CalClock::now_utc());
88///
89/// // Quick validation
90/// if validator.is_valid_calclock(&calclock) {
91/// println!("CalClock is valid");
92/// }
93///
94/// // Detailed validation with error messages
95/// match validator.validate_calclock(&calclock) {
96/// Ok(_) => println!("CalClock is valid"),
97/// Err(errors) => {
98/// for error in errors {
99/// println!("Validation error: {}", error);
100/// }
101/// }
102/// }
103/// ```
104#[derive(Debug)]
105pub struct CalClockValidator {
106 rules: Vec<ValidationRule>,
107 // Strict adds two checks the lenient mode allows: a year outside
108 // -9999 to 9999, and a leap second.
109 strict: bool,
110}
111
112impl CalClockValidator {
113 pub fn new() -> Self {
114 Self {
115 rules: Vec::new(),
116 strict: false,
117 }
118 }
119
120 pub fn strict() -> Self {
121 Self {
122 rules: Vec::new(),
123 strict: true,
124 }
125 }
126
127 pub fn add_rule(&mut self, rule: ValidationRule) {
128 self.rules.push(rule);
129 }
130
131 pub fn set_strict(&mut self, strict: bool) {
132 self.strict = strict;
133 }
134
135 // ========================================================================
136 // CalClock Validation
137 // ========================================================================
138
139 pub fn validate_calclock(&self, calclock: &CalClock) -> ValidationResult {
140 let mut errors = Vec::new();
141
142 // Validate date component
143 if let Err(mut date_errors) = self.validate_date(calclock.date()) {
144 errors.append(&mut date_errors);
145 }
146
147 // Validate time component
148 if let Err(mut time_errors) = self.validate_time(calclock.time()) {
149 errors.append(&mut time_errors);
150 }
151
152 // Validate timezone consistency
153 if calclock.date().zone() != calclock.time().zone() {
154 errors.push(ValidationError::new(
155 "timezone_consistency",
156 "Date and time components must use the same timezone"
157 ).field("timezone"));
158 }
159
160 // Apply custom rules
161 for rule in &self.rules {
162 if let Err(mut rule_errors) = rule.validate_calclock(calclock) {
163 errors.append(&mut rule_errors);
164 }
165 }
166
167 if errors.is_empty() {
168 Ok(())
169 } else {
170 Err(errors)
171 }
172 }
173
174 pub fn is_valid_calclock(&self, calclock: &CalClock) -> bool {
175 self.validate_calclock(calclock).is_ok()
176 }
177
178 // ========================================================================
179 // CalendarDate Validation
180 // ========================================================================
181
182 pub fn validate_date(&self, date: &CalendarDate) -> ValidationResult {
183 let mut errors = Vec::new();
184
185 // Validate year range
186 if self.strict && (date.year() < -9999 || date.year() > 9999) {
187 errors.push(ValidationError::new(
188 "year_range",
189 "Year must be between -9999 and 9999 in strict mode"
190 ).field("year")
191 .value(date.year().to_string())
192 .expected("-9999 to 9999"));
193 }
194
195 // Validate month range
196 if date.month() < 1 || date.month() > 12 {
197 errors.push(ValidationError::new(
198 "month_range",
199 "Month must be between 1 and 12"
200 ).field("month")
201 .value(date.month().to_string())
202 .expected("1 to 12"));
203 }
204
205 // Validate day range
206 if date.day() < 1 {
207 errors.push(ValidationError::new(
208 "day_range",
209 "Day must be at least 1"
210 ).field("day")
211 .value(date.day().to_string())
212 .expected("1 or greater"));
213 }
214
215 // Validate day against month length
216 if date.month() >= 1 && date.month() <= 12 {
217 let days_in_month = date.month_of_year().days_in_month(date.year());
218 if date.day() > days_in_month {
219 errors.push(ValidationError::new(
220 "day_in_month",
221 "Day does not exist in this month"
222 ).field("day")
223 .value(date.day().to_string())
224 .expected(format!("1 to {}", days_in_month)));
225 }
226 }
227
228 // Validate leap year February 29
229 if date.month() == 2 && date.day() == 29 && !date.is_leap_year() {
230 errors.push(ValidationError::new(
231 "leap_year",
232 "February 29 does not exist in non-leap year"
233 ).field("day")
234 .value("29".to_string())
235 .expected("1 to 28 (non-leap year)"));
236 }
237
238 // Apply custom rules
239 for rule in &self.rules {
240 if let Err(mut rule_errors) = rule.validate_date(date) {
241 errors.append(&mut rule_errors);
242 }
243 }
244
245 if errors.is_empty() {
246 Ok(())
247 } else {
248 Err(errors)
249 }
250 }
251
252 pub fn is_valid_date(&self, date: &CalendarDate) -> bool {
253 self.validate_date(date).is_ok()
254 }
255
256 // ========================================================================
257 // ClockTime Validation
258 // ========================================================================
259
260 pub fn validate_time(&self, time: &ClockTime) -> ValidationResult {
261 let mut errors = Vec::new();
262
263 // Validate hour range
264 if time.hour().of() > 23 {
265 errors.push(ValidationError::new(
266 "hour_range",
267 "Hour must be between 0 and 23"
268 ).field("hour")
269 .value(time.hour().of().to_string())
270 .expected("0 to 23"));
271 }
272
273 // Validate minute range
274 if time.minute().of() > 59 {
275 errors.push(ValidationError::new(
276 "minute_range",
277 "Minute must be between 0 and 59"
278 ).field("minute")
279 .value(time.minute().of().to_string())
280 .expected("0 to 59"));
281 }
282
283 // Validate second range
284 if time.second().of() > 59 {
285 errors.push(ValidationError::new(
286 "second_range",
287 "Second must be between 0 and 59"
288 ).field("second")
289 .value(time.second().of().to_string())
290 .expected("0 to 59"));
291 }
292
293 // Validate nanosecond range
294 if time.nanosecond().of() >= 1_000_000_000 {
295 errors.push(ValidationError::new(
296 "nanosecond_range",
297 "Nanosecond must be between 0 and 999,999,999"
298 ).field("nanosecond")
299 .value(time.nanosecond().of().to_string())
300 .expected("0 to 999,999,999"));
301 }
302
303 // Strict validation for leap seconds
304 if self.strict && time.second().of() == 60 {
305 errors.push(ValidationError::new(
306 "leap_second",
307 "Leap seconds (second = 60) are not supported in strict mode"
308 ).field("second")
309 .value("60".to_string())
310 .expected("0 to 59"));
311 }
312
313 // Apply custom rules
314 for rule in &self.rules {
315 if let Err(mut rule_errors) = rule.validate_time(time) {
316 errors.append(&mut rule_errors);
317 }
318 }
319
320 if errors.is_empty() {
321 Ok(())
322 } else {
323 Err(errors)
324 }
325 }
326
327 pub fn is_valid_time(&self, time: &ClockTime) -> bool {
328 self.validate_time(time).is_ok()
329 }
330
331 // ========================================================================
332 // Batch Validation
333 // ========================================================================
334
335 pub fn validate_many_calclocks(&self, calclocks: &[CalClock]) -> ValidationResult {
336 let mut all_errors = Vec::new();
337
338 for (index, calclock) in calclocks.iter().enumerate() {
339 if let Err(mut errors) = self.validate_calclock(calclock) {
340 // Add index information to errors
341 for error in &mut errors {
342 error.field = Some(format!("item[{}].{}", index,
343 error.field.as_deref().unwrap_or("unknown")));
344 }
345 all_errors.append(&mut errors);
346 }
347 }
348
349 if all_errors.is_empty() {
350 Ok(())
351 } else {
352 Err(all_errors)
353 }
354 }
355
356 pub fn count_valid_calclocks(&self, calclocks: &[CalClock]) -> usize {
357 calclocks.iter()
358 .filter(|calclock| self.is_valid_calclock(calclock))
359 .count()
360 }
361
362 pub fn filter_valid_calclocks(&self, calclocks: Vec<CalClock>) -> Vec<CalClock> {
363 calclocks.into_iter()
364 .filter(|calclock| self.is_valid_calclock(calclock))
365 .collect()
366 }
367
368 // ========================================================================
369 // Range Validation
370 // ========================================================================
371
372 pub fn validate_date_range(
373 &self,
374 date: &CalendarDate,
375 min_date: &CalendarDate,
376 max_date: &CalendarDate,
377 ) -> ValidationResult {
378 let mut errors = Vec::new();
379
380 if date < min_date {
381 errors.push(ValidationError::new(
382 "date_range_min",
383 "Date is before minimum allowed date"
384 ).field("date")
385 .value(date.to_string())
386 .expected("minimum date or later"));
387 }
388
389 if date > max_date {
390 errors.push(ValidationError::new(
391 "date_range_max",
392 "Date is after maximum allowed date"
393 ).field("date")
394 .value(date.to_string())
395 .expected("maximum date or earlier"));
396 }
397
398 if errors.is_empty() {
399 Ok(())
400 } else {
401 Err(errors)
402 }
403 }
404
405 pub fn validate_time_range(
406 &self,
407 time: &ClockTime,
408 min_time: &ClockTime,
409 max_time: &ClockTime,
410 ) -> ValidationResult {
411 let mut errors = Vec::new();
412
413 if time < min_time {
414 errors.push(ValidationError::new(
415 "time_range_min",
416 "Time is before minimum allowed time"
417 ).field("time")
418 .value(time.to_string())
419 .expected("minimum time or later"));
420 }
421
422 if time > max_time {
423 errors.push(ValidationError::new(
424 "time_range_max",
425 "Time is after maximum allowed time"
426 ).field("time")
427 .value(time.to_string())
428 .expected("maximum time or earlier"));
429 }
430
431 if errors.is_empty() {
432 Ok(())
433 } else {
434 Err(errors)
435 }
436 }
437}
438
439impl Default for CalClockValidator {
440 fn default() -> Self {
441 Self::new()
442 }
443}
444
445// ========================================================================
446// Convenience Functions
447// ========================================================================
448
449pub fn validate_calclock(calclock: &CalClock) -> ValidationResult {
450 CalClockValidator::new().validate_calclock(calclock)
451}
452
453pub fn validate_date(date: &CalendarDate) -> ValidationResult {
454 CalClockValidator::new().validate_date(date)
455}
456
457pub fn validate_time(time: &ClockTime) -> ValidationResult {
458 CalClockValidator::new().validate_time(time)
459}
460
461pub fn validate_calclock_strict(calclock: &CalClock) -> ValidationResult {
462 CalClockValidator::strict().validate_calclock(calclock)
463}
464
465pub fn is_valid_calclock(calclock: &CalClock) -> bool {
466 validate_calclock(calclock).is_ok()
467}
468
469pub fn is_valid_date(date: &CalendarDate) -> bool {
470 validate_date(date).is_ok()
471}
472
473pub fn is_valid_time(time: &ClockTime) -> bool {
474 validate_time(time).is_ok()
475}