Skip to main content

hotfix_encoding/field_types/
date.rs

1use 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/// Representation for `LocalMktDate` and and `UTCDateOnly` in `YYYYMMDD` format.
18///
19/// # Examples
20///
21/// - `19411208`
22/// - `16201211`
23/// - `18630101`
24/// - `20170526`
25#[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    /// Creates a new date from its components. It returns `None` if any of the
34    /// three components is outside the legal range.
35    ///
36    /// # Examples
37    ///
38    /// ```
39    /// use hotfix_encoding::field_types::Date;
40    ///
41    /// assert!(Date::new(2021, 4, 16).is_some());
42    /// assert!(Date::new(2021, 13, 32).is_none());
43    ///
44    /// // Support from January 1, year zero (which doesn't actually exist) to
45    /// // December 31, 9999.
46    /// assert!(Date::new(0, 1, 1).is_some());
47    /// assert!(Date::new(9999, 12, 31).is_some());
48    ///
49    /// // We don't check month-aware day boundaries, i.e. go ahead and assume
50    /// // every month has 31 days.
51    /// assert!(Date::new(2021, 2, 31).is_some());
52    /// ```
53    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    /// Converts `self` to `YYYYMMDD` format.
65    ///
66    /// # Examples
67    ///
68    /// ```
69    /// use hotfix_encoding::field_types::Date;
70    ///
71    /// assert_eq!(&Date::new(2021, 01, 01).unwrap().to_yyyymmdd(), b"20210101");
72    /// ```
73    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    /// Returns the `year` of `self`.
90    pub fn year(&self) -> u32 {
91        self.year
92    }
93
94    /// Returns the `month` of `self` (1-indexing, i.e. 1-12).
95    pub fn month(&self) -> u32 {
96        self.month
97    }
98
99    /// Returns the `day` of `self` (1-indexing, i.e. 1-31).
100    pub fn day(&self) -> u32 {
101        self.day
102    }
103
104    /// Converts `self` to a [`chrono`] UTC date. [`chrono`] might impose
105    /// additional constraints and checks on date components (e.g. leap year,
106    /// day 31 in 30-day months); this function will return `None` for invalid
107    /// dates.
108    #[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    /// Converts `self` to [`chrono::NaiveDate`]. [`chrono`] might impose
116    /// additional constraints and checks on date components (e.g. leap year,
117    /// day 31 in 30-day months); this function will return `None` for invalid
118    /// dates.
119    #[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"",          // Empty string.
210        b"2013011",   // String too short.
211        b"201301120", // String too long.
212        b"00000001",  // Invalid month.
213        b"00000100",  // Invalid day.
214        b"19801301",  // Invalid month.
215        b"19800001",  // Invalid month.
216        b"19801233",  // Invalid day.
217        b"19801232",  // Invalid day.
218        b"-9801232",  // Invalid character.
219        b"29801232",  // Invalid day.
220        b"1980010a",  // Invalid character.
221        b"1980010:",  // Invalid character.
222        b"19800:01",  // Invalid character.
223        b"19800:00",  // Invalid character and invalid day.
224    ];
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        // Lossy and losseless deserialization can only be meaningfully compared
234        // on legal inputs.
235        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}