1use core::fmt;
34
35pub const MAX_RFC3339_LEN: usize = 40;
39
40#[derive(Debug, Clone, Copy)]
45pub struct OcppTimestamp {
46 secs: i64,
48 nanos: u32,
50 offset_minutes: i16,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
56pub enum TimestampError {
57 Malformed,
60 OutOfRange,
63}
64
65impl fmt::Display for TimestampError {
66 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67 f.write_str(match self {
68 Self::Malformed => "not an RFC 3339 date-time",
69 Self::OutOfRange => "RFC 3339 date-time with an out-of-range field",
70 })
71 }
72}
73
74impl core::error::Error for TimestampError {}
75
76const fn days_from_civil(year: i64, month: u32, day: u32) -> i64 {
80 let year = year - if month <= 2 { 1 } else { 0 };
81 let era = if year >= 0 { year } else { year - 399 } / 400;
82 let year_of_era = year - era * 400;
83 let month = month as i64;
84 let day_of_year = (153 * (month + if month > 2 { -3 } else { 9 }) + 2) / 5 + day as i64 - 1;
85 let day_of_era =
86 year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
87
88 era * 146097 + day_of_era - 719468
89}
90
91const fn civil_from_days(days: i64) -> (i64, u32, u32) {
93 let days = days + 719468;
94 let era = if days >= 0 { days } else { days - 146096 } / 146097;
95 let day_of_era = days - era * 146097;
96 let year_of_era = (day_of_era - day_of_era / 1460 + day_of_era / 36524
97 - day_of_era / 146096)
98 / 365;
99 let year = year_of_era + era * 400;
100 let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
101 let mp = (5 * day_of_year + 2) / 153;
102 let day = (day_of_year - (153 * mp + 2) / 5 + 1) as u32;
103 let month = (mp + if mp < 10 { 3 } else { -9 }) as u32;
104
105 (year + if month <= 2 { 1 } else { 0 }, month, day)
106}
107
108const fn is_leap(year: i64) -> bool {
109 (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
110}
111
112const fn days_in_month(year: i64, month: u32) -> u32 {
113 match month {
114 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
115 4 | 6 | 9 | 11 => 30,
116 2 if is_leap(year) => 29,
117 2 => 28,
118 _ => 0,
119 }
120}
121
122impl OcppTimestamp {
123 pub const UNIX_EPOCH: Self = Self {
125 secs: 0,
126 nanos: 0,
127 offset_minutes: 0,
128 };
129
130 pub const fn from_unix(secs: i64, nanos: u32) -> Result<Self, TimestampError> {
135 if nanos >= 1_000_000_000 {
136 return Err(TimestampError::OutOfRange);
137 }
138
139 Ok(Self {
140 secs,
141 nanos,
142 offset_minutes: 0,
143 })
144 }
145
146 pub const fn unix_seconds(&self) -> i64 {
148 self.secs
149 }
150
151 pub const fn subsec_nanos(&self) -> u32 {
153 self.nanos
154 }
155
156 pub const fn utc_offset_minutes(&self) -> i16 {
159 self.offset_minutes
160 }
161
162 pub const fn with_utc_offset_minutes(self, offset_minutes: i16) -> Result<Self, TimestampError> {
166 if offset_minutes <= -1440 || offset_minutes >= 1440 {
167 return Err(TimestampError::OutOfRange);
168 }
169
170 Ok(Self {
171 offset_minutes,
172 ..self
173 })
174 }
175
176 pub fn parse_rfc3339(text: &str) -> Result<Self, TimestampError> {
179 let bytes = text.as_bytes();
180
181 if bytes.len() < 20 {
184 return Err(TimestampError::Malformed);
185 }
186
187 let year = parse_number(&bytes[0..4])? as i64;
188 expect(bytes[4], b'-')?;
189 let month = parse_number(&bytes[5..7])?;
190 expect(bytes[7], b'-')?;
191 let day = parse_number(&bytes[8..10])?;
192
193 match bytes[10] {
194 b'T' | b't' | b' ' => {}
195 _ => return Err(TimestampError::Malformed),
196 }
197
198 let hour = parse_number(&bytes[11..13])?;
199 expect(bytes[13], b':')?;
200 let minute = parse_number(&bytes[14..16])?;
201 expect(bytes[16], b':')?;
202 let second = parse_number(&bytes[17..19])?;
203
204 let mut cursor = 19;
205 let mut nanos = 0u32;
206
207 if bytes[cursor] == b'.' || bytes[cursor] == b',' {
208 cursor += 1;
209 let start = cursor;
210
211 while cursor < bytes.len() && bytes[cursor].is_ascii_digit() {
212 if cursor - start < 9 {
215 nanos = nanos * 10 + u32::from(bytes[cursor] - b'0');
216 }
217 cursor += 1;
218 }
219
220 let digits = cursor - start;
221
222 if digits == 0 {
223 return Err(TimestampError::Malformed);
224 }
225
226 for _ in digits.min(9)..9 {
228 nanos *= 10;
229 }
230 }
231
232 if cursor >= bytes.len() {
233 return Err(TimestampError::Malformed);
234 }
235
236 let offset_minutes = match bytes[cursor] {
237 b'Z' | b'z' if cursor + 1 == bytes.len() => 0i32,
238 b'+' | b'-' => {
239 if cursor + 6 != bytes.len() {
240 return Err(TimestampError::Malformed);
241 }
242
243 let sign = if bytes[cursor] == b'-' { -1i32 } else { 1 };
244 let offset_hour = parse_number(&bytes[cursor + 1..cursor + 3])? as i32;
245 expect(bytes[cursor + 3], b':')?;
246 let offset_minute = parse_number(&bytes[cursor + 4..cursor + 6])? as i32;
247
248 if offset_hour > 23 || offset_minute > 59 {
249 return Err(TimestampError::OutOfRange);
250 }
251
252 sign * (offset_hour * 60 + offset_minute)
253 }
254 _ => return Err(TimestampError::Malformed),
255 };
256
257 if !(1..=12).contains(&month) || day < 1 || day > days_in_month(year, month) {
258 return Err(TimestampError::OutOfRange);
259 }
260
261 if hour > 23 || minute > 59 || second > 60 {
265 return Err(TimestampError::OutOfRange);
266 }
267
268 let second = second.min(59);
269 let days = days_from_civil(year, month, day);
270 let secs = days * 86_400
271 + i64::from(hour) * 3600
272 + i64::from(minute) * 60
273 + i64::from(second)
274 - i64::from(offset_minutes) * 60;
275
276 Ok(Self {
277 secs,
278 nanos,
279 offset_minutes: offset_minutes as i16,
280 })
281 }
282
283 pub fn to_rfc3339<'buf>(&self, buf: &'buf mut [u8]) -> Option<&'buf str> {
289 if buf.len() < MAX_RFC3339_LEN {
290 return None;
291 }
292
293 let local = self.secs + i64::from(self.offset_minutes) * 60;
295 let days = local.div_euclid(86_400);
296 let time_of_day = local.rem_euclid(86_400);
297 let (year, month, day) = civil_from_days(days);
298
299 let mut at = 0;
300
301 write_year(buf, &mut at, year);
302 buf[at] = b'-';
303 at += 1;
304 write_two(buf, &mut at, month as u64);
305 buf[at] = b'-';
306 at += 1;
307 write_two(buf, &mut at, day as u64);
308 buf[at] = b'T';
309 at += 1;
310 write_two(buf, &mut at, (time_of_day / 3600) as u64);
311 buf[at] = b':';
312 at += 1;
313 write_two(buf, &mut at, (time_of_day % 3600 / 60) as u64);
314 buf[at] = b':';
315 at += 1;
316 write_two(buf, &mut at, (time_of_day % 60) as u64);
317
318 if self.nanos != 0 {
319 buf[at] = b'.';
320 at += 1;
321
322 if self.nanos.is_multiple_of(1_000_000) {
325 write_padded(buf, &mut at, u64::from(self.nanos / 1_000_000), 3);
326 } else {
327 write_padded(buf, &mut at, u64::from(self.nanos), 9);
328 }
329 }
330
331 if self.offset_minutes == 0 {
332 buf[at] = b'Z';
333 at += 1;
334 } else {
335 let (sign, magnitude) = if self.offset_minutes < 0 {
336 (b'-', -i32::from(self.offset_minutes))
337 } else {
338 (b'+', i32::from(self.offset_minutes))
339 };
340
341 buf[at] = sign;
342 at += 1;
343 write_two(buf, &mut at, (magnitude / 60) as u64);
344 buf[at] = b':';
345 at += 1;
346 write_two(buf, &mut at, (magnitude % 60) as u64);
347 }
348
349 core::str::from_utf8(&buf[..at]).ok()
350 }
351}
352
353fn expect(actual: u8, expected: u8) -> Result<(), TimestampError> {
354 if actual == expected {
355 Ok(())
356 } else {
357 Err(TimestampError::Malformed)
358 }
359}
360
361fn parse_number(bytes: &[u8]) -> Result<u32, TimestampError> {
362 let mut value = 0u32;
363
364 for &byte in bytes {
365 if !byte.is_ascii_digit() {
366 return Err(TimestampError::Malformed);
367 }
368
369 value = value * 10 + u32::from(byte - b'0');
370 }
371
372 Ok(value)
373}
374
375fn write_two(buf: &mut [u8], at: &mut usize, value: u64) {
376 write_padded(buf, at, value, 2);
377}
378
379fn write_padded(buf: &mut [u8], at: &mut usize, value: u64, width: usize) {
380 let mut digits = [0u8; 20];
381 let mut count = 0;
382 let mut value = value;
383
384 while value > 0 {
385 digits[count] = b'0' + (value % 10) as u8;
386 value /= 10;
387 count += 1;
388 }
389
390 for _ in count..width {
391 buf[*at] = b'0';
392 *at += 1;
393 }
394
395 for index in (0..count).rev() {
396 buf[*at] = digits[index];
397 *at += 1;
398 }
399}
400
401fn write_year(buf: &mut [u8], at: &mut usize, year: i64) {
402 if year < 0 {
403 buf[*at] = b'-';
404 *at += 1;
405 write_padded(buf, at, year.unsigned_abs(), 4);
406 } else {
407 write_padded(buf, at, year as u64, 4);
408 }
409}
410
411impl fmt::Display for OcppTimestamp {
412 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
413 let mut buf = [0u8; MAX_RFC3339_LEN];
414
415 match self.to_rfc3339(&mut buf) {
416 Some(text) => f.write_str(text),
417 None => Err(fmt::Error),
418 }
419 }
420}
421
422impl core::str::FromStr for OcppTimestamp {
423 type Err = TimestampError;
424
425 fn from_str(text: &str) -> Result<Self, Self::Err> {
426 Self::parse_rfc3339(text)
427 }
428}
429
430impl TryFrom<&str> for OcppTimestamp {
431 type Error = TimestampError;
432
433 fn try_from(text: &str) -> Result<Self, Self::Error> {
434 Self::parse_rfc3339(text)
435 }
436}
437
438impl PartialEq for OcppTimestamp {
442 fn eq(&self, other: &Self) -> bool {
443 self.secs == other.secs && self.nanos == other.nanos
444 }
445}
446
447impl Eq for OcppTimestamp {}
448
449impl core::hash::Hash for OcppTimestamp {
450 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
451 self.secs.hash(state);
452 self.nanos.hash(state);
453 }
454}
455
456impl PartialOrd for OcppTimestamp {
457 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
458 Some(self.cmp(other))
459 }
460}
461
462impl Ord for OcppTimestamp {
463 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
464 self.secs
465 .cmp(&other.secs)
466 .then(self.nanos.cmp(&other.nanos))
467 }
468}
469
470#[cfg(feature = "serde")]
471impl serde::Serialize for OcppTimestamp {
472 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
473 let mut buf = [0u8; MAX_RFC3339_LEN];
474 let text = self
475 .to_rfc3339(&mut buf)
476 .ok_or_else(|| serde::ser::Error::custom("timestamp is not representable"))?;
477
478 serializer.serialize_str(text)
479 }
480}
481
482#[cfg(feature = "serde")]
483impl<'de> serde::Deserialize<'de> for OcppTimestamp {
484 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
485 struct Visitor;
486
487 impl<'v> serde::de::Visitor<'v> for Visitor {
488 type Value = OcppTimestamp;
489
490 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
491 f.write_str("an RFC 3339 date-time")
492 }
493
494 fn visit_str<E: serde::de::Error>(self, text: &str) -> Result<Self::Value, E> {
495 OcppTimestamp::parse_rfc3339(text).map_err(serde::de::Error::custom)
496 }
497 }
498
499 deserializer.deserialize_str(Visitor)
500 }
501}
502
503#[cfg(feature = "chrono")]
511mod chrono_interop {
512 use super::OcppTimestamp;
513 use chrono::{DateTime, FixedOffset, TimeZone, Utc};
514
515 impl From<OcppTimestamp> for DateTime<Utc> {
516 fn from(value: OcppTimestamp) -> Self {
517 Utc.timestamp_opt(value.unix_seconds(), value.subsec_nanos())
518 .single()
519 .expect("an OcppTimestamp always names exactly one UTC instant")
520 }
521 }
522
523 impl From<DateTime<Utc>> for OcppTimestamp {
524 fn from(value: DateTime<Utc>) -> Self {
525 OcppTimestamp::from_unix(value.timestamp(), value.timestamp_subsec_nanos())
526 .expect("chrono keeps subsec nanos below one second")
527 }
528 }
529
530 impl From<DateTime<FixedOffset>> for OcppTimestamp {
531 fn from(value: DateTime<FixedOffset>) -> Self {
532 let offset_minutes = (value.offset().local_minus_utc() / 60) as i16;
533
534 OcppTimestamp::from_unix(value.timestamp(), value.timestamp_subsec_nanos())
535 .expect("chrono keeps subsec nanos below one second")
536 .with_utc_offset_minutes(offset_minutes)
537 .expect("chrono offsets are within +/- 24 hours")
538 }
539 }
540
541 impl From<OcppTimestamp> for DateTime<FixedOffset> {
542 fn from(value: OcppTimestamp) -> Self {
543 let offset = FixedOffset::east_opt(i32::from(value.utc_offset_minutes()) * 60)
544 .expect("an OcppTimestamp offset is within +/- 24 hours");
545
546 DateTime::<Utc>::from(value).with_timezone(&offset)
547 }
548 }
549}
550
551#[cfg(test)]
552mod tests {
553 use super::*;
554
555 fn rendered(text: &str) -> heapless::String<MAX_RFC3339_LEN> {
556 let parsed = OcppTimestamp::parse_rfc3339(text).expect(text);
557 let mut buf = [0u8; MAX_RFC3339_LEN];
558
559 heapless::String::try_from(parsed.to_rfc3339(&mut buf).unwrap()).unwrap()
560 }
561
562 #[test]
565 fn parses_known_instants_to_their_known_unix_time() {
566 for (text, secs) in [
567 ("1970-01-01T00:00:00Z", 0),
568 ("2000-03-01T00:00:00Z", 951_868_800),
569 ("2024-02-29T12:00:00Z", 1_709_208_000),
570 ("2038-01-19T03:14:07Z", 2_147_483_647),
571 ("1969-12-31T23:59:59Z", -1),
572 ("1900-01-01T00:00:00Z", -2_208_988_800),
573 ] {
574 let parsed = OcppTimestamp::parse_rfc3339(text).expect(text);
575 assert_eq!(parsed.unix_seconds(), secs, "for {text}");
576 }
577 }
578
579 #[test]
580 fn round_trips_every_shape_the_spec_allows() {
581 for text in [
582 "2024-01-01T00:00:00Z",
583 "2024-06-15T12:34:56Z",
584 "2024-06-15T12:34:56.789Z",
585 "1970-01-01T00:00:00Z",
586 "2038-01-19T03:14:07Z",
587 ] {
588 assert_eq!(rendered(text).as_str(), text);
589 }
590 }
591
592 #[test]
593 fn accepts_the_separators_and_cases_rfc3339_permits() {
594 let canonical = OcppTimestamp::parse_rfc3339("2024-01-01T00:00:00Z").unwrap();
595
596 for text in [
597 "2024-01-01t00:00:00Z",
598 "2024-01-01 00:00:00Z",
599 "2024-01-01T00:00:00z",
600 ] {
601 assert_eq!(OcppTimestamp::parse_rfc3339(text).unwrap(), canonical, "for {text}");
602 }
603 }
604
605 #[test]
606 fn an_offset_names_the_same_instant_as_its_utc_form() {
607 let with_offset = OcppTimestamp::parse_rfc3339("2024-01-01T01:00:00+01:00").unwrap();
608 let utc = OcppTimestamp::parse_rfc3339("2024-01-01T00:00:00Z").unwrap();
609
610 assert_eq!(with_offset, utc);
611 assert_eq!(with_offset.unix_seconds(), utc.unix_seconds());
612 assert_eq!(with_offset.utc_offset_minutes(), 60);
614 assert_eq!(rendered("2024-01-01T01:00:00+01:00").as_str(), "2024-01-01T01:00:00+01:00");
615 assert_eq!(rendered("2023-12-31T19:00:00-05:00").as_str(), "2023-12-31T19:00:00-05:00");
616 assert_eq!(rendered("2024-01-01T00:30:00+05:30").as_str(), "2024-01-01T00:30:00+05:30");
617 }
618
619 #[test]
622 fn scales_fractional_seconds_by_their_length() {
623 for (text, nanos) in [
624 ("2024-01-01T00:00:00.5Z", 500_000_000),
625 ("2024-01-01T00:00:00.05Z", 50_000_000),
626 ("2024-01-01T00:00:00.123Z", 123_000_000),
627 ("2024-01-01T00:00:00.000001Z", 1_000),
628 ("2024-01-01T00:00:00.123456789Z", 123_456_789),
629 ] {
630 assert_eq!(
631 OcppTimestamp::parse_rfc3339(text).unwrap().subsec_nanos(),
632 nanos,
633 "for {text}"
634 );
635 }
636
637 let over = OcppTimestamp::parse_rfc3339("2024-01-01T00:00:00.1234567891Z").unwrap();
640 assert_eq!(over.subsec_nanos(), 123_456_789);
641 }
642
643 #[test]
644 fn writes_milliseconds_when_they_divide_evenly_and_nanoseconds_otherwise() {
645 assert_eq!(rendered("2024-01-01T00:00:00.250Z").as_str(), "2024-01-01T00:00:00.250Z");
646 assert_eq!(
647 rendered("2024-01-01T00:00:00.123456789Z").as_str(),
648 "2024-01-01T00:00:00.123456789Z"
649 );
650 assert_eq!(rendered("2024-01-01T00:00:00.000Z").as_str(), "2024-01-01T00:00:00Z");
652 }
653
654 #[test]
655 fn rejects_dates_that_do_not_exist() {
656 for text in [
657 "2023-02-29T00:00:00Z", "1900-02-29T00:00:00Z", "2024-13-01T00:00:00Z",
660 "2024-04-31T00:00:00Z",
661 "2024-01-01T24:00:00Z",
662 "2024-01-01T00:60:00Z",
663 "2024-01-01T00:00:00+24:00",
664 ] {
665 assert_eq!(
666 OcppTimestamp::parse_rfc3339(text),
667 Err(TimestampError::OutOfRange),
668 "for {text}"
669 );
670 }
671
672 assert!(OcppTimestamp::parse_rfc3339("2000-02-29T00:00:00Z").is_ok());
674 assert!(OcppTimestamp::parse_rfc3339("2024-02-29T00:00:00Z").is_ok());
675 }
676
677 #[test]
678 fn rejects_strings_that_are_not_rfc3339() {
679 for text in [
680 "",
681 "2024-01-01",
682 "2024-01-01T00:00:00", "2024/01/01T00:00:00Z",
684 "2024-01-01T00:00:00.Z", "2024-01-01T00:00:00+0100", "2024-01-01T00:00:00Zextra",
687 "not-a-date-at-all!!!",
688 ] {
689 assert_eq!(
690 OcppTimestamp::parse_rfc3339(text),
691 Err(TimestampError::Malformed),
692 "for {text}"
693 );
694 }
695 }
696
697 #[test]
700 fn clamps_a_leap_second_rather_than_rejecting_it() {
701 let parsed = OcppTimestamp::parse_rfc3339("2016-12-31T23:59:60Z").unwrap();
702
703 assert_eq!(
704 parsed,
705 OcppTimestamp::parse_rfc3339("2016-12-31T23:59:59Z").unwrap()
706 );
707 }
708
709 #[test]
710 fn orders_by_instant_regardless_of_written_offset() {
711 let earlier = OcppTimestamp::parse_rfc3339("2024-01-01T00:00:00Z").unwrap();
712 let later = OcppTimestamp::parse_rfc3339("2024-01-01T00:00:01Z").unwrap();
713 let same_instant_other_offset =
714 OcppTimestamp::parse_rfc3339("2024-01-01T01:00:00+01:00").unwrap();
715
716 assert!(earlier < later);
717 assert_eq!(earlier.cmp(&same_instant_other_offset), core::cmp::Ordering::Equal);
718 }
719
720 #[test]
721 fn round_trips_across_a_wide_span_of_days() {
722 let mut day = -36_500i64;
725
726 while day < 36_500 {
727 let stamp = OcppTimestamp::from_unix(day * 86_400 + 3661, 0).unwrap();
728 let mut buf = [0u8; MAX_RFC3339_LEN];
729 let text = stamp.to_rfc3339(&mut buf).unwrap();
730
731 assert_eq!(
732 OcppTimestamp::parse_rfc3339(text).unwrap(),
733 stamp,
734 "failed to round-trip {text}"
735 );
736
737 day += 37;
738 }
739 }
740
741 #[test]
742 fn to_rfc3339_refuses_a_buffer_it_could_overflow() {
743 let stamp = OcppTimestamp::UNIX_EPOCH;
744 let mut small = [0u8; MAX_RFC3339_LEN - 1];
745
746 assert_eq!(stamp.to_rfc3339(&mut small), None);
747 }
748
749 #[test]
750 fn is_sixteen_bytes() {
751 assert_eq!(core::mem::size_of::<OcppTimestamp>(), 16);
753 }
754
755 fn rendered_civil(value: &impl fmt::Display) -> heapless::String<16> {
759 let mut out = heapless::String::new();
760 core::fmt::Write::write_fmt(&mut out, format_args!("{value}")).unwrap();
761 out
762 }
763
764 #[test]
765 fn the_civil_types_are_as_small_as_their_fields_allow() {
766 assert_eq!(core::mem::size_of::<OcppTimeOfDay>(), 2);
769 assert_eq!(core::mem::size_of::<OcppDate>(), 4);
770 }
771
772 #[test]
773 fn time_of_day_round_trips_the_specs_format() {
774 for text in ["00:00", "09:05", "13:45", "23:59"] {
775 let parsed = OcppTimeOfDay::parse(text).expect(text);
776 assert_eq!(rendered_civil(&parsed).as_str(), text);
777 }
778 }
779
780 #[test]
781 fn time_of_day_rejects_impossible_and_malformed_values() {
782 for text in ["24:00", "23:60", "99:99"] {
783 assert_eq!(OcppTimeOfDay::parse(text), Err(TimestampError::OutOfRange), "{text}");
784 }
785
786 for text in ["", "9:05", "09:5", "09:05:00", "0905", "ab:cd", "09-05"] {
788 assert_eq!(OcppTimeOfDay::parse(text), Err(TimestampError::Malformed), "{text}");
789 }
790 }
791
792 #[test]
793 fn time_of_day_orders_chronologically() {
794 let early = OcppTimeOfDay::parse("09:05").unwrap();
795 let late = OcppTimeOfDay::parse("09:30").unwrap();
796
797 assert!(early < late);
798 assert!(OcppTimeOfDay::MIDNIGHT < early);
799 assert_eq!(late.minutes_since_midnight(), 570);
800 }
801
802 #[test]
803 fn date_round_trips_the_specs_format() {
804 for text in ["1970-01-01", "2015-12-24", "2024-02-29", "9999-12-31"] {
805 let parsed = OcppDate::parse(text).expect(text);
806 assert_eq!(rendered_civil(&parsed).as_str(), text);
807 }
808 }
809
810 #[test]
811 fn date_rejects_days_the_month_does_not_have() {
812 for text in ["2023-02-29", "1900-02-29", "2024-04-31", "2024-13-01", "2024-00-10"] {
813 assert_eq!(OcppDate::parse(text), Err(TimestampError::OutOfRange), "{text}");
814 }
815
816 assert!(OcppDate::parse("2000-02-29").is_ok());
817 assert!(OcppDate::parse("2024-02-29").is_ok());
818 }
819
820 #[test]
821 fn date_orders_chronologically_and_converts_to_epoch_days() {
822 let earlier = OcppDate::parse("2024-01-31").unwrap();
823 let later = OcppDate::parse("2024-02-01").unwrap();
824
825 assert!(earlier < later);
826 assert_eq!(OcppDate::parse("1970-01-01").unwrap().days_from_epoch(), 0);
827 assert_eq!(later.days_from_epoch() - earlier.days_from_epoch(), 1);
828 }
829
830}
831
832#[cfg(all(test, feature = "chrono"))]
833mod chrono_tests {
834 use super::*;
835 use chrono::{DateTime, FixedOffset, Utc};
836
837 #[test]
838 fn round_trips_through_chrono_utc() {
839 let ours = OcppTimestamp::parse_rfc3339("2024-06-15T12:34:56.789Z").unwrap();
840 let theirs: DateTime<Utc> = ours.into();
841
842 assert_eq!(theirs.timestamp(), ours.unix_seconds());
843 assert_eq!(theirs.timestamp_subsec_nanos(), ours.subsec_nanos());
844 assert_eq!(OcppTimestamp::from(theirs), ours);
845 }
846
847 #[test]
851 fn a_fixed_offset_survives_the_round_trip() {
852 let ours = OcppTimestamp::parse_rfc3339("2024-01-01T01:00:00+01:00").unwrap();
853 let theirs: DateTime<FixedOffset> = ours.into();
854
855 assert_eq!(theirs.offset().local_minus_utc(), 3600);
856
857 let back = OcppTimestamp::from(theirs);
858 assert_eq!(back, ours);
859 assert_eq!(back.utc_offset_minutes(), 60);
860 }
861
862 #[test]
863 fn chrono_agrees_with_our_parser_on_a_span_of_instants() {
864 let mut day = -20_000i64;
865
866 while day < 20_000 {
867 let ours = OcppTimestamp::from_unix(day * 86_400 + 7261, 0).unwrap();
868 let mut buf = [0u8; MAX_RFC3339_LEN];
869 let text = ours.to_rfc3339(&mut buf).unwrap();
870
871 let theirs = DateTime::parse_from_rfc3339(text)
872 .unwrap_or_else(|e| panic!("chrono rejected our output {text}: {e}"));
873
874 assert_eq!(theirs.timestamp(), ours.unix_seconds(), "for {text}");
875 day += 61;
876 }
877 }
878}
879
880#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
893pub struct OcppTimeOfDay {
894 hour: u8,
895 minute: u8,
896}
897
898impl OcppTimeOfDay {
899 pub const MIDNIGHT: Self = Self { hour: 0, minute: 0 };
901
902 pub const fn new(hour: u8, minute: u8) -> Result<Self, TimestampError> {
904 if hour > 23 || minute > 59 {
905 return Err(TimestampError::OutOfRange);
906 }
907
908 Ok(Self { hour, minute })
909 }
910
911 pub const fn hour(&self) -> u8 {
912 self.hour
913 }
914
915 pub const fn minute(&self) -> u8 {
916 self.minute
917 }
918
919 pub const fn minutes_since_midnight(&self) -> u16 {
921 self.hour as u16 * 60 + self.minute as u16
922 }
923
924 pub fn parse(text: &str) -> Result<Self, TimestampError> {
927 let bytes = text.as_bytes();
928
929 if bytes.len() != 5 {
930 return Err(TimestampError::Malformed);
931 }
932
933 let hour = parse_number(&bytes[0..2])?;
934 expect(bytes[2], b':')?;
935 let minute = parse_number(&bytes[3..5])?;
936
937 Self::new(hour as u8, minute as u8)
938 }
939}
940
941impl fmt::Display for OcppTimeOfDay {
942 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
943 write!(f, "{:02}:{:02}", self.hour, self.minute)
944 }
945}
946
947impl core::str::FromStr for OcppTimeOfDay {
948 type Err = TimestampError;
949
950 fn from_str(text: &str) -> Result<Self, Self::Err> {
951 Self::parse(text)
952 }
953}
954
955impl TryFrom<&str> for OcppTimeOfDay {
956 type Error = TimestampError;
957
958 fn try_from(text: &str) -> Result<Self, Self::Error> {
959 Self::parse(text)
960 }
961}
962
963#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
971pub struct OcppDate {
972 year: u16,
973 month: u8,
974 day: u8,
975}
976
977impl OcppDate {
978 pub const fn new(year: u16, month: u8, day: u8) -> Result<Self, TimestampError> {
981 if month < 1 || month > 12 || day < 1 || day > days_in_month(year as i64, month as u32) as u8
982 {
983 return Err(TimestampError::OutOfRange);
984 }
985
986 Ok(Self { year, month, day })
987 }
988
989 pub const fn year(&self) -> u16 {
990 self.year
991 }
992
993 pub const fn month(&self) -> u8 {
994 self.month
995 }
996
997 pub const fn day(&self) -> u8 {
998 self.day
999 }
1000
1001 pub const fn days_from_epoch(&self) -> i64 {
1003 days_from_civil(self.year as i64, self.month as u32, self.day as u32)
1004 }
1005
1006 pub fn parse(text: &str) -> Result<Self, TimestampError> {
1008 let bytes = text.as_bytes();
1009
1010 if bytes.len() != 10 {
1011 return Err(TimestampError::Malformed);
1012 }
1013
1014 let year = parse_number(&bytes[0..4])?;
1015 expect(bytes[4], b'-')?;
1016 let month = parse_number(&bytes[5..7])?;
1017 expect(bytes[7], b'-')?;
1018 let day = parse_number(&bytes[8..10])?;
1019
1020 Self::new(year as u16, month as u8, day as u8)
1021 }
1022}
1023
1024impl fmt::Display for OcppDate {
1025 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1026 write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
1027 }
1028}
1029
1030impl core::str::FromStr for OcppDate {
1031 type Err = TimestampError;
1032
1033 fn from_str(text: &str) -> Result<Self, Self::Err> {
1034 Self::parse(text)
1035 }
1036}
1037
1038impl TryFrom<&str> for OcppDate {
1039 type Error = TimestampError;
1040
1041 fn try_from(text: &str) -> Result<Self, Self::Error> {
1042 Self::parse(text)
1043 }
1044}
1045
1046#[cfg(feature = "serde")]
1049macro_rules! serde_via_display {
1050 ($ty:ty, $expecting:literal) => {
1051 #[cfg(feature = "serde")]
1052 impl serde::Serialize for $ty {
1053 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1054 serializer.collect_str(self)
1055 }
1056 }
1057
1058 #[cfg(feature = "serde")]
1059 impl<'de> serde::Deserialize<'de> for $ty {
1060 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1061 struct Visitor;
1062
1063 impl<'v> serde::de::Visitor<'v> for Visitor {
1064 type Value = $ty;
1065
1066 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1067 f.write_str($expecting)
1068 }
1069
1070 fn visit_str<E: serde::de::Error>(self, text: &str) -> Result<Self::Value, E> {
1071 <$ty>::parse(text).map_err(serde::de::Error::custom)
1072 }
1073 }
1074
1075 deserializer.deserialize_str(Visitor)
1076 }
1077 }
1078 };
1079}
1080
1081#[cfg(feature = "serde")]
1082serde_via_display!(OcppTimeOfDay, "a time of day as HH:MM");
1083#[cfg(feature = "serde")]
1084serde_via_display!(OcppDate, "a date as YYYY-MM-DD");