1use super::{pack_string, unpack_string};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
22pub struct DateTime {
23 pub year: u16,
25 pub month: u8,
27 pub day: u8,
29 pub hour: u8,
31 pub minute: u8,
33 pub second: u8,
35}
36
37impl DateTime {
38 #[must_use]
55 pub fn new(year: u16, month: u8, day: u8, hour: u8, minute: u8, second: u8) -> Option<Self> {
56 let dt = DateTime {
57 year,
58 month,
59 day,
60 hour,
61 minute,
62 second,
63 };
64 if dt.is_valid() {
65 Some(dt)
66 } else {
67 None
68 }
69 }
70
71 #[must_use]
84 pub fn is_valid(&self) -> bool {
85 self.year <= 9999
86 && (1..=12).contains(&self.month)
87 && (1..=31).contains(&self.day)
88 && self.hour <= 23
89 && self.minute <= 59
90 && self.second <= 59
91 }
92
93 #[must_use]
100 pub fn parse(s: &str) -> Option<Self> {
101 if s.len() < 15 {
103 return None;
104 }
105
106 if s.as_bytes().get(8) != Some(&b'T') {
108 return None;
109 }
110
111 let year: u16 = s.get(0..4)?.parse().ok()?;
113 let month: u8 = s.get(4..6)?.parse().ok()?;
114 let day: u8 = s.get(6..8)?.parse().ok()?;
115 let hour: u8 = s.get(9..11)?.parse().ok()?;
116 let minute: u8 = s.get(11..13)?.parse().ok()?;
117 let second: u8 = s.get(13..15)?.parse().ok()?;
118
119 Self::new(year, month, day, hour, minute, second)
121 }
122
123 #[must_use]
141 pub fn format(&self) -> Option<String> {
142 if !self.is_valid() {
143 return None;
144 }
145 Some(format!(
146 "{:04}{:02}{:02}T{:02}{:02}{:02}",
147 self.year, self.month, self.day, self.hour, self.minute, self.second
148 ))
149 }
150}
151
152pub fn pack_datetime(dt: &DateTime) -> Result<Vec<u8>, crate::Error> {
156 let formatted = dt.format().ok_or_else(|| {
157 crate::Error::invalid_data(format!(
158 "invalid DateTime: year={}, month={}, day={}, hour={}, minute={}, second={}",
159 dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second
160 ))
161 })?;
162 Ok(pack_string(&formatted))
163}
164
165pub fn unpack_datetime(buf: &[u8]) -> Result<(Option<DateTime>, usize), crate::Error> {
169 let (s, consumed) = unpack_string(buf)?;
170
171 if s.is_empty() {
172 return Ok((None, consumed));
173 }
174
175 let dt = DateTime::parse(&s)
176 .ok_or_else(|| crate::Error::invalid_data(format!("invalid datetime format: {}", s)))?;
177
178 Ok((Some(dt), consumed))
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184 use proptest::prelude::*;
185
186 #[test]
189 fn datetime_parse_basic() {
190 let dt = DateTime::parse("20240315T143022").unwrap();
191 assert_eq!(
192 (dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second),
193 (2024, 3, 15, 14, 30, 22)
194 );
195 }
196
197 #[test]
198 fn datetime_parse_with_timezone_z() {
199 assert!(DateTime::parse("20240315T143022Z").is_some());
200 }
201
202 #[test]
203 fn datetime_parse_with_timezone_positive() {
204 assert!(DateTime::parse("20240315T143022+0530").is_some());
205 }
206
207 #[test]
208 fn datetime_parse_with_timezone_negative() {
209 assert!(DateTime::parse("20240315T143022-0800").is_some());
210 }
211
212 #[test]
213 fn datetime_parse_invalid_too_short() {
214 for s in ["2024031", ""] {
215 assert!(DateTime::parse(s).is_none());
216 }
217 }
218
219 #[test]
220 fn datetime_parse_invalid_no_t_separator() {
221 for s in ["20240315 143022", "20240315143022"] {
222 assert!(DateTime::parse(s).is_none());
223 }
224 }
225
226 #[test]
227 fn datetime_parse_invalid_month() {
228 for s in ["20240015T143022", "20241315T143022"] {
229 assert!(DateTime::parse(s).is_none());
231 }
232 }
233
234 #[test]
235 fn datetime_parse_invalid_day() {
236 for s in ["20240100T143022", "20240132T143022"] {
237 assert!(DateTime::parse(s).is_none());
239 }
240 }
241
242 #[test]
243 fn datetime_parse_invalid_hour() {
244 assert!(DateTime::parse("20240315T243022").is_none()); }
246
247 #[test]
248 fn datetime_parse_invalid_minute() {
249 assert!(DateTime::parse("20240315T146022").is_none()); }
251
252 #[test]
253 fn datetime_parse_invalid_second() {
254 assert!(DateTime::parse("20240315T143060").is_none()); }
256
257 #[test]
260 fn datetime_format() {
261 assert_eq!(
262 DateTime::new(2024, 3, 15, 14, 30, 22).unwrap().format(),
263 Some("20240315T143022".into())
264 );
265 }
266
267 #[test]
268 fn datetime_format_with_leading_zeros() {
269 assert_eq!(
270 DateTime::new(2024, 1, 5, 9, 5, 3).unwrap().format(),
271 Some("20240105T090503".into())
272 );
273 }
274
275 #[test]
276 fn datetime_roundtrip() {
277 let original = DateTime::new(2024, 12, 31, 23, 59, 59).unwrap();
278 assert_eq!(
279 DateTime::parse(&original.format().unwrap()).unwrap(),
280 original
281 );
282 }
283
284 #[test]
285 fn datetime_format_invalid_returns_none() {
286 let invalid_cases = [
287 (2024, 13, 1, 0, 0, 0), (2024, 1, 1, 0, 60, 0), (10000, 1, 1, 0, 0, 0), ];
291 for (y, mo, d, h, mi, s) in invalid_cases {
292 let dt = DateTime {
293 year: y,
294 month: mo,
295 day: d,
296 hour: h,
297 minute: mi,
298 second: s,
299 };
300 assert_eq!(dt.format(), None);
301 }
302 }
303
304 #[test]
305 fn datetime_default() {
306 let dt = DateTime::default();
307 assert_eq!(
308 (dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second),
309 (0, 0, 0, 0, 0, 0)
310 );
311 }
312
313 #[test]
316 fn pack_datetime_basic() {
317 let packed = pack_datetime(&DateTime::new(2024, 3, 15, 14, 30, 22).unwrap()).unwrap();
318 assert_eq!(packed[0], 16); }
320
321 #[test]
322 fn pack_datetime_invalid_returns_error() {
323 let invalid = DateTime {
324 year: 2024,
325 month: 13,
326 day: 1,
327 hour: 0,
328 minute: 0,
329 second: 0,
330 };
331 assert!(pack_datetime(&invalid).is_err());
332 }
333
334 #[test]
335 fn unpack_datetime_basic() {
336 let dt = DateTime::new(2024, 3, 15, 14, 30, 22).unwrap();
337 let (unpacked, _) = unpack_datetime(&pack_datetime(&dt).unwrap()).unwrap();
338 assert_eq!(unpacked, Some(dt));
339 }
340
341 #[test]
342 fn unpack_datetime_empty_string() {
343 let (dt, consumed) = unpack_datetime(&[0x00]).unwrap();
344 assert_eq!((dt, consumed), (None, 1));
345 }
346
347 #[test]
348 fn unpack_datetime_invalid_format() {
349 assert!(unpack_datetime(&pack_string("not a date")).is_err());
350 }
351
352 #[test]
353 fn datetime_pack_unpack_roundtrip() {
354 for dt in [
355 DateTime::new(2024, 1, 1, 0, 0, 0).unwrap(),
356 DateTime::new(2024, 12, 31, 23, 59, 59).unwrap(),
357 DateTime::new(1999, 6, 15, 12, 30, 45).unwrap(),
358 ] {
359 let (unpacked, _) = unpack_datetime(&pack_datetime(&dt).unwrap()).unwrap();
360 assert_eq!(unpacked, Some(dt));
361 }
362 }
363
364 #[test]
367 fn datetime_boundary_day_31() {
368 let dt = DateTime {
369 year: 2024,
370 month: 1,
371 day: 31,
372 hour: 0,
373 minute: 0,
374 second: 0,
375 };
376 let parsed = DateTime::parse(&dt.format().unwrap()).unwrap();
377 assert_eq!(parsed.day, 31);
378 }
379
380 #[test]
381 fn datetime_boundary_year_0() {
382 let dt = DateTime {
383 year: 0,
384 month: 1,
385 day: 1,
386 hour: 0,
387 minute: 0,
388 second: 0,
389 };
390 if let Some(p) = DateTime::parse(&dt.format().unwrap()) {
391 assert_eq!(p.year, 0);
392 }
393 }
394
395 #[test]
396 fn datetime_boundary_year_9999() {
397 let dt = DateTime {
398 year: 9999,
399 month: 12,
400 day: 31,
401 hour: 23,
402 minute: 59,
403 second: 59,
404 };
405 assert_eq!(DateTime::parse(&dt.format().unwrap()).unwrap().year, 9999);
406 }
407
408 #[test]
409 fn datetime_boundary_year_10000() {
410 let dt = DateTime {
411 year: 10000,
412 month: 1,
413 day: 1,
414 hour: 0,
415 minute: 0,
416 second: 0,
417 };
418 assert!(dt.format().is_none());
419 assert!(pack_datetime(&dt).is_err());
420 }
421
422 fn valid_datetime() -> impl Strategy<Value = DateTime> {
425 (
426 1000u16..9999u16,
427 1u8..=12u8,
428 1u8..=28u8,
429 0u8..=23u8,
430 0u8..=59u8,
431 0u8..=59u8,
432 )
433 .prop_map(|(year, month, day, hour, minute, second)| DateTime {
434 year,
435 month,
436 day,
437 hour,
438 minute,
439 second,
440 })
441 }
442
443 proptest! {
444 #[test]
445 fn prop_datetime_format_parse_roundtrip(dt in valid_datetime()) {
446 let formatted = dt.format().unwrap();
447 prop_assert_eq!(DateTime::parse(&formatted).unwrap(), dt);
448 }
449
450 #[test]
451 fn prop_datetime_pack_unpack_roundtrip(dt in valid_datetime()) {
452 let packed = pack_datetime(&dt).unwrap();
453 let (unpacked, consumed) = unpack_datetime(&packed).unwrap();
454 prop_assert_eq!(unpacked, Some(dt));
455 prop_assert_eq!(consumed, packed.len());
456 }
457
458 #[test]
459 fn prop_datetime_format_length(dt in valid_datetime()) {
460 prop_assert_eq!(dt.format().unwrap().len(), 15);
461 }
462
463 #[test]
464 fn fuzz_datetime_invalid_month(
465 year in 1900u16..2100u16,
466 month in prop::sample::select(vec![0u8, 13, 14, 99, 255]),
467 day in 1u8..=28u8, hour in 0u8..=23u8, minute in 0u8..=59u8, second in 0u8..=59u8,
468 ) {
469 let dt = DateTime { year, month, day, hour, minute, second };
470 prop_assert!(dt.format().is_none());
471 prop_assert!(pack_datetime(&dt).is_err());
472 }
473
474 #[test]
475 fn fuzz_datetime_invalid_day(
476 year in 1900u16..2100u16, month in 1u8..=12u8,
477 day in prop::sample::select(vec![0u8, 32, 33, 99, 255]),
478 hour in 0u8..=23u8, minute in 0u8..=59u8, second in 0u8..=59u8,
479 ) {
480 let dt = DateTime { year, month, day, hour, minute, second };
481 prop_assert!(dt.format().is_none());
482 prop_assert!(pack_datetime(&dt).is_err());
483 }
484
485 #[test]
486 fn fuzz_datetime_invalid_hour(
487 year in 1900u16..2100u16, month in 1u8..=12u8, day in 1u8..=28u8,
488 hour in prop::sample::select(vec![24u8, 25, 99, 255]),
489 minute in 0u8..=59u8, second in 0u8..=59u8,
490 ) {
491 let dt = DateTime { year, month, day, hour, minute, second };
492 prop_assert!(dt.format().is_none());
493 prop_assert!(pack_datetime(&dt).is_err());
494 }
495
496 #[test]
497 fn fuzz_datetime_invalid_minute(
498 year in 1900u16..2100u16, month in 1u8..=12u8, day in 1u8..=28u8, hour in 0u8..=23u8,
499 minute in prop::sample::select(vec![60u8, 61, 99]),
500 second in 0u8..=59u8,
501 ) {
502 let dt = DateTime { year, month, day, hour, minute, second };
503 prop_assert!(dt.format().is_none());
504 prop_assert!(pack_datetime(&dt).is_err());
505 }
506
507 #[test]
508 fn fuzz_datetime_minute_overflow(
509 year in 1900u16..2100u16, month in 1u8..=12u8, day in 1u8..=28u8, hour in 0u8..=23u8,
510 minute in 100u8..=255u8, second in 0u8..=59u8,
511 ) {
512 let dt = DateTime { year, month, day, hour, minute, second };
513 prop_assert!(dt.format().is_none());
514 prop_assert!(pack_datetime(&dt).is_err());
515 }
516
517 #[test]
518 fn fuzz_datetime_invalid_second(
519 year in 1900u16..2100u16, month in 1u8..=12u8, day in 1u8..=28u8, hour in 0u8..=23u8, minute in 0u8..=59u8,
520 second in prop::sample::select(vec![60u8, 61, 99]),
521 ) {
522 let dt = DateTime { year, month, day, hour, minute, second };
523 prop_assert!(dt.format().is_none());
524 prop_assert!(pack_datetime(&dt).is_err());
525 }
526
527 #[test]
528 fn fuzz_datetime_second_overflow(
529 year in 1900u16..2100u16, month in 1u8..=12u8, day in 1u8..=28u8, hour in 0u8..=23u8, minute in 0u8..=59u8,
530 second in 100u8..=255u8,
531 ) {
532 let dt = DateTime { year, month, day, hour, minute, second };
533 prop_assert!(dt.format().is_none());
534 prop_assert!(pack_datetime(&dt).is_err());
535 }
536
537 #[test]
538 fn fuzz_datetime_parse_garbage(s in ".*") {
539 let _ = DateTime::parse(&s);
540 }
541
542 #[test]
543 fn fuzz_datetime_parse_malformed(prefix in "[0-9]{0,20}", suffix in "[^T]*") {
544 let _ = DateTime::parse(&format!("{}{}", prefix, suffix));
545 }
546 }
547
548 crate::fuzz_bytes_fn!(fuzz_unpack_datetime, unpack_datetime, 50);
549}