hotfix_encoding/field_types/
date.rs1use crate::{Buffer, FieldType};
2use std::convert::{TryFrom, TryInto};
3
4const LEN_IN_BYTES: usize = 8;
5
6const MAX_YEAR: u32 = 9999;
7const MAX_MONTH: u32 = 12;
8const MAX_DAY: u32 = 31;
9
10const MIN_MONTH: u32 = 1;
11const MIN_DAY: u32 = 1;
12
13const ERR_NOT_ASCII_DIGITS: &str = "Invalid characters, expected ASCII digits.";
14const ERR_LENGTH: &str = "Invalid length, expected 8 bytes (YYYYMMDD format).";
15const ERR_BOUNDS: &str = "Values outside legal bounds.";
16
17#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
26pub struct Date {
27 year: u32,
28 month: u32,
29 day: u32,
30}
31
32impl Date {
33 pub fn new(year: u32, month: u32, day: u32) -> Option<Self> {
54 if year <= MAX_YEAR
55 && (MIN_MONTH..=MAX_MONTH).contains(&month)
56 && (MIN_DAY..=MAX_DAY).contains(&day)
57 {
58 Some(Self { year, month, day })
59 } else {
60 None
61 }
62 }
63
64 pub fn to_yyyymmdd(&self) -> [u8; LEN_IN_BYTES] {
74 fn digit_to_ascii(n: u32) -> u8 {
75 (n + b'0' as u32) as u8
76 }
77 [
78 digit_to_ascii(self.year() / 1000),
79 digit_to_ascii((self.year() / 100) % 10),
80 digit_to_ascii((self.year() / 10) % 10),
81 digit_to_ascii(self.year() % 10),
82 digit_to_ascii(self.month() / 10),
83 digit_to_ascii(self.month() % 10),
84 digit_to_ascii(self.day() / 10),
85 digit_to_ascii(self.day() % 10),
86 ]
87 }
88
89 pub fn year(&self) -> u32 {
91 self.year
92 }
93
94 pub fn month(&self) -> u32 {
96 self.month
97 }
98
99 pub fn day(&self) -> u32 {
101 self.day
102 }
103
104 #[cfg(feature = "utils-chrono")]
109 #[cfg_attr(doc_cfg, doc(cfg(feature = "utils-chrono")))]
110 pub fn to_chrono_utc(&self) -> Option<chrono::Date<chrono::Utc>> {
111 let naive = self.to_chrono_naive()?;
112 Some(chrono::Date::from_utc(naive, chrono::Utc))
113 }
114
115 #[cfg(feature = "utils-chrono")]
120 #[cfg_attr(doc_cfg, doc(cfg(feature = "utils-chrono")))]
121 pub fn to_chrono_naive(&self) -> Option<chrono::NaiveDate> {
122 chrono::NaiveDate::from_ymd_opt(self.year() as i32, self.month(), self.day())
123 }
124}
125
126impl<'a> FieldType<'a> for Date {
127 type Error = &'static str;
128 type SerializeSettings = ();
129
130 fn serialize_with<B>(&self, buffer: &mut B, _settings: ()) -> usize
131 where
132 B: Buffer,
133 {
134 let bytes = self.to_yyyymmdd();
135 buffer.extend_from_slice(&bytes[..]);
136 bytes.len()
137 }
138
139 fn deserialize(data: &'a [u8]) -> Result<Self, Self::Error> {
140 if let Ok(bytes) = <[u8; LEN_IN_BYTES]>::try_from(data) {
141 for byte in bytes.iter().copied() {
142 if !is_digit(byte) {
143 return Err(ERR_NOT_ASCII_DIGITS);
144 }
145 }
146 deserialize(bytes)
147 } else {
148 Err(ERR_LENGTH)
149 }
150 }
151
152 fn deserialize_lossy(data: &'a [u8]) -> Result<Self, Self::Error> {
153 if let Ok(bytes) = data.try_into() {
154 deserialize(bytes)
155 } else {
156 Err(ERR_LENGTH)
157 }
158 }
159}
160
161fn deserialize(data: [u8; LEN_IN_BYTES]) -> Result<Date, &'static str> {
162 let year = ascii_digit_to_u32(data[0], 1000)
163 + ascii_digit_to_u32(data[1], 100)
164 + ascii_digit_to_u32(data[2], 10)
165 + ascii_digit_to_u32(data[3], 1);
166 let month = ascii_digit_to_u32(data[4], 10) + ascii_digit_to_u32(data[5], 1);
167 let day = ascii_digit_to_u32(data[6], 10) + ascii_digit_to_u32(data[7], 1);
168 Date::new(year, month, day).ok_or(ERR_BOUNDS)
169}
170
171const fn is_digit(byte: u8) -> bool {
172 byte >= b'0' && byte <= b'9'
173}
174
175const fn ascii_digit_to_u32(digit: u8, multiplier: u32) -> u32 {
176 (digit as u32).wrapping_sub(b'0' as u32) * multiplier
177}
178
179#[cfg(test)]
180mod test {
181 use super::*;
182 use quickcheck::{Arbitrary, Gen};
183 use quickcheck_macros::quickcheck;
184
185 impl Arbitrary for Date {
186 fn arbitrary(g: &mut Gen) -> Self {
187 let year = u32::arbitrary(g) % 10000;
188 let month = (u32::arbitrary(g) % 12) + 1;
189 let day = (u32::arbitrary(g) % 31) + 1;
190 Date::new(year, month, day).unwrap()
191 }
192 }
193
194 #[quickcheck]
195 fn verify_serialization_behavior(date: Date) -> bool {
196 crate::field_types::test_utility_verify_serialization_behavior(date)
197 }
198
199 const VALID_DATES: &[&[u8]] = &[
200 b"00000101",
201 b"00010101",
202 b"99991231",
203 b"99990101",
204 b"20191225",
205 b"20190231",
206 ];
207
208 const INVALID_DATES: &[&[u8]] = &[
209 b"", b"2013011", b"201301120", b"00000001", b"00000100", b"19801301", b"19800001", b"19801233", b"19801232", b"-9801232", b"29801232", b"1980010a", b"1980010:", b"19800:01", b"19800:00", ];
225
226 #[quickcheck]
227 fn to_yyyymmdd_to_bytes_are_the_same(date: Date) -> bool {
228 date.to_yyyymmdd() == FieldType::to_bytes(&date)[..]
229 }
230
231 #[test]
232 fn lossy_and_lossless_are_equivalent() {
233 for bytes in VALID_DATES {
236 let date = Date::deserialize(bytes).unwrap();
237 let date_lossy = Date::deserialize_lossy(bytes).unwrap();
238 assert_eq!(date, date_lossy);
239 }
240 }
241
242 #[quickcheck]
243 fn new_via_getters(date: Date) -> bool {
244 let date_via_new = Date::new(date.year(), date.month(), date.day()).unwrap();
245 date == date_via_new
246 }
247
248 #[test]
249 fn lossless_deserialization_detects_errors() {
250 for bytes in INVALID_DATES {
251 assert!(Date::deserialize(bytes).is_err());
252 }
253 }
254
255 #[test]
256 fn serialize_and_deserialize_are_consistent_with_each_other() {
257 for bytes in VALID_DATES {
258 let date = Date::deserialize(bytes).unwrap();
259 let serialized = date.to_bytes();
260 assert_eq!(**bytes, serialized);
261 }
262 }
263}