use crate::{Buffer, FieldType};
use std::convert::{TryFrom, TryInto};
const LEN_IN_BYTES: usize = 8;
const MAX_YEAR: u32 = 9999;
const MAX_MONTH: u32 = 12;
const MAX_DAY: u32 = 31;
const MIN_MONTH: u32 = 1;
const MIN_DAY: u32 = 1;
const ERR_NOT_ASCII_DIGITS: &str = "Invalid characters, expected ASCII digits.";
const ERR_LENGTH: &str = "Invalid length, expected 8 bytes (YYYYMMDD format).";
const ERR_BOUNDS: &str = "Values outside legal bounds.";
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Date {
year: u32,
month: u32,
day: u32,
}
impl Date {
pub fn new(year: u32, month: u32, day: u32) -> Option<Self> {
if year <= MAX_YEAR
&& (MIN_MONTH..=MAX_MONTH).contains(&month)
&& (MIN_DAY..=MAX_DAY).contains(&day)
{
Some(Self { year, month, day })
} else {
None
}
}
pub fn to_yyyymmdd(&self) -> [u8; LEN_IN_BYTES] {
fn digit_to_ascii(n: u32) -> u8 {
(n + b'0' as u32) as u8
}
[
digit_to_ascii(self.year() / 1000),
digit_to_ascii((self.year() / 100) % 10),
digit_to_ascii((self.year() / 10) % 10),
digit_to_ascii(self.year() % 10),
digit_to_ascii(self.month() / 10),
digit_to_ascii(self.month() % 10),
digit_to_ascii(self.day() / 10),
digit_to_ascii(self.day() % 10),
]
}
pub fn year(&self) -> u32 {
self.year
}
pub fn month(&self) -> u32 {
self.month
}
pub fn day(&self) -> u32 {
self.day
}
#[cfg(feature = "utils-chrono")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "utils-chrono")))]
pub fn to_chrono_utc(&self) -> Option<chrono::Date<chrono::Utc>> {
let naive = self.to_chrono_naive()?;
Some(chrono::Date::from_utc(naive, chrono::Utc))
}
#[cfg(feature = "utils-chrono")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "utils-chrono")))]
pub fn to_chrono_naive(&self) -> Option<chrono::NaiveDate> {
chrono::NaiveDate::from_ymd_opt(self.year() as i32, self.month(), self.day())
}
}
impl<'a> FieldType<'a> for Date {
type Error = &'static str;
type SerializeSettings = ();
fn serialize_with<B>(&self, buffer: &mut B, _settings: ()) -> usize
where
B: Buffer,
{
let bytes = self.to_yyyymmdd();
buffer.extend_from_slice(&bytes[..]);
bytes.len()
}
fn deserialize(data: &'a [u8]) -> Result<Self, Self::Error> {
if let Ok(bytes) = <[u8; LEN_IN_BYTES]>::try_from(data) {
for byte in bytes.iter().copied() {
if !is_digit(byte) {
return Err(ERR_NOT_ASCII_DIGITS);
}
}
deserialize(bytes)
} else {
Err(ERR_LENGTH)
}
}
fn deserialize_lossy(data: &'a [u8]) -> Result<Self, Self::Error> {
if let Ok(bytes) = data.try_into() {
deserialize(bytes)
} else {
Err(ERR_LENGTH)
}
}
}
fn deserialize(data: [u8; LEN_IN_BYTES]) -> Result<Date, &'static str> {
let year = ascii_digit_to_u32(data[0], 1000)
+ ascii_digit_to_u32(data[1], 100)
+ ascii_digit_to_u32(data[2], 10)
+ ascii_digit_to_u32(data[3], 1);
let month = ascii_digit_to_u32(data[4], 10) + ascii_digit_to_u32(data[5], 1);
let day = ascii_digit_to_u32(data[6], 10) + ascii_digit_to_u32(data[7], 1);
Date::new(year, month, day).ok_or(ERR_BOUNDS)
}
const fn is_digit(byte: u8) -> bool {
byte >= b'0' && byte <= b'9'
}
const fn ascii_digit_to_u32(digit: u8, multiplier: u32) -> u32 {
(digit as u32).wrapping_sub(b'0' as u32) * multiplier
}
#[cfg(test)]
mod test {
use super::*;
use quickcheck::{Arbitrary, Gen};
use quickcheck_macros::quickcheck;
impl Arbitrary for Date {
fn arbitrary(g: &mut Gen) -> Self {
let year = u32::arbitrary(g) % 10000;
let month = (u32::arbitrary(g) % 12) + 1;
let day = (u32::arbitrary(g) % 31) + 1;
Date::new(year, month, day).unwrap()
}
}
#[quickcheck]
fn verify_serialization_behavior(date: Date) -> bool {
crate::field_types::test_utility_verify_serialization_behavior(date)
}
const VALID_DATES: &[&[u8]] = &[
b"00000101",
b"00010101",
b"99991231",
b"99990101",
b"20191225",
b"20190231",
];
const INVALID_DATES: &[&[u8]] = &[
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", ];
#[quickcheck]
fn to_yyyymmdd_to_bytes_are_the_same(date: Date) -> bool {
date.to_yyyymmdd() == FieldType::to_bytes(&date)[..]
}
#[test]
fn lossy_and_lossless_are_equivalent() {
for bytes in VALID_DATES {
let date = Date::deserialize(bytes).unwrap();
let date_lossy = Date::deserialize_lossy(bytes).unwrap();
assert_eq!(date, date_lossy);
}
}
#[quickcheck]
fn new_via_getters(date: Date) -> bool {
let date_via_new = Date::new(date.year(), date.month(), date.day()).unwrap();
date == date_via_new
}
#[test]
fn lossless_deserialization_detects_errors() {
for bytes in INVALID_DATES {
assert!(Date::deserialize(bytes).is_err());
}
}
#[test]
fn serialize_and_deserialize_are_consistent_with_each_other() {
for bytes in VALID_DATES {
let date = Date::deserialize(bytes).unwrap();
let serialized = date.to_bytes();
assert_eq!(**bytes, serialized);
}
}
}