use std::{fmt, str::FromStr};
use chrono::{Datelike, Local, NaiveDate, NaiveDateTime, Timelike};
#[cfg(feature = "schemars")]
use schemars::JsonSchema;
use serde::{
de::{Deserialize, Deserializer},
ser::{Serialize, Serializer},
};
use crate::Error;
#[inline]
fn bcd_to_dec(byte: u8) -> u8 {
byte / 16 * 10 + byte % 16
}
#[inline]
fn dec_to_bcd(dec: u8) -> u8 {
dec / 10 * 16 + dec % 10
}
#[derive(Clone, Copy, PartialEq)]
pub struct Date(pub(crate) NaiveDate);
impl Date {
pub fn new(year: u16, month: u8, day: u8) -> Option<Self> {
Some(Self(NaiveDate::from_ymd_opt(year.into(), month.into(), day.into())?))
}
pub fn from_bytes(bytes: &[u8; 8]) -> Result<Self, Error> {
let year = u16::from(bcd_to_dec(bytes[0])) * 100 + u16::from(bcd_to_dec(bytes[1]));
let month = bcd_to_dec(bytes[2]);
let day = bcd_to_dec(bytes[3]);
if let Some(date) = NaiveDate::from_ymd_opt(year.into(), month.into(), day.into()) {
Ok(Self(date))
} else {
Err(Error::InvalidFormat(format!("invalid date: {year:04}-{month:02}-{day:02}")))
}
}
pub fn to_bytes(&self) -> [u8; 8] {
[
dec_to_bcd((self.year() / 100) as u8),
dec_to_bcd((self.year() % 100) as u8),
dec_to_bcd(self.month()),
dec_to_bcd(self.day()),
self.weekday(),
0,
0,
0,
]
}
#[inline(always)]
pub fn year(&self) -> u16 {
self.0.year() as u16
}
#[inline(always)]
pub fn month(&self) -> u8 {
self.0.month() as u8
}
#[inline(always)]
pub fn day(&self) -> u8 {
self.0.day() as u8
}
#[inline(always)]
pub fn weekday(&self) -> u8 {
self.0.weekday().num_days_from_monday() as u8
}
}
impl FromStr for Date {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
NaiveDate::parse_from_str(s, "%Y-%m-%d").map(Self).map_err(|_| ())
}
}
impl<'de> Deserialize<'de> for Date {
fn deserialize<D>(deserializer: D) -> Result<Date, D::Error>
where
D: Deserializer<'de>,
{
NaiveDate::deserialize(deserializer).map(Self)
}
}
#[cfg(feature = "schemars")]
impl JsonSchema for Date {
fn schema_name() -> String {
"Date".into()
}
fn json_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
let mut schema = generator.subschema_for::<String>().into_object();
schema.format = Some("date".into());
schema.into()
}
}
impl Serialize for Date {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_string())
}
}
impl fmt::Display for Date {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:04}-{:02}-{:02}", self.0.year(), self.0.month(), self.0.day())
}
}
impl fmt::Debug for Date {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Date({})", self)
}
}
#[derive(Clone, Copy, PartialEq)]
pub struct DateTime(pub(crate) NaiveDateTime);
impl DateTime {
pub fn new(year: u16, month: u8, day: u8, hour: u8, minute: u8, second: u8) -> Result<Self, Error> {
if let Some(datetime) = NaiveDate::from_ymd_opt(year.into(), month.into(), day.into())
.and_then(|date| date.and_hms_opt(hour.into(), minute.into(), second.into()))
{
Ok(Self(datetime))
} else {
Err(Error::InvalidFormat(format!(
"invalid date-time: {year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}"
)))
}
}
pub fn from_unix_timestamp(timestamp: u32) -> Self {
Self(chrono::DateTime::from_timestamp_secs(timestamp.into()).unwrap().with_timezone(&Local).naive_local())
}
pub fn unix_timestamp(&self) -> u32 {
self.0.and_local_timezone(Local).unwrap().timestamp() as u32
}
pub fn year(&self) -> u16 {
self.0.year() as u16
}
pub fn month(&self) -> u8 {
self.0.month() as u8
}
pub fn day(&self) -> u8 {
self.0.day() as u8
}
pub fn weekday(&self) -> u8 {
self.0.weekday().num_days_from_monday() as u8
}
#[inline(always)]
pub fn hour(&self) -> u8 {
self.0.hour() as u8
}
#[inline(always)]
pub fn minute(&self) -> u8 {
self.0.minute() as u8
}
#[inline(always)]
pub fn second(&self) -> u8 {
self.0.second() as u8
}
pub fn from_bytes(bytes: &[u8; 8]) -> Result<Self, Error> {
let year = u16::from(bcd_to_dec(bytes[0])) * 100 + u16::from(bcd_to_dec(bytes[1]));
let month = bcd_to_dec(bytes[2]);
let day = bcd_to_dec(bytes[3]);
let hour = bcd_to_dec(bytes[5]);
let minute = bcd_to_dec(bytes[6]);
let second = bcd_to_dec(bytes[7]);
Self::new(year, month, day, hour, minute, second)
}
pub fn to_bytes(&self) -> [u8; 8] {
[
dec_to_bcd((self.year() / 100) as u8),
dec_to_bcd((self.year() % 100) as u8),
dec_to_bcd(self.month()),
dec_to_bcd(self.day()),
self.weekday(),
dec_to_bcd(self.hour()),
dec_to_bcd(self.minute()),
dec_to_bcd(self.second()),
]
}
}
impl FromStr for DateTime {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S").map(Self).map_err(|_| ())
}
}
impl<'de> Deserialize<'de> for DateTime {
fn deserialize<D>(deserializer: D) -> Result<DateTime, D::Error>
where
D: Deserializer<'de>,
{
NaiveDateTime::deserialize(deserializer).map(Self)
}
}
#[cfg(feature = "schemars")]
impl JsonSchema for DateTime {
fn schema_name() -> String {
"DateTime".into()
}
fn json_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
let mut schema = generator.subschema_for::<String>().into_object();
schema.format = Some("date-time".into());
schema.into()
}
}
impl Serialize for DateTime {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_string())
}
}
impl fmt::Display for DateTime {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
self.year(),
self.month(),
self.day(),
self.hour(),
self.minute(),
self.second(),
)
}
}
impl fmt::Debug for DateTime {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "DateTime({})", self)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bcd_to_dec() {
for x in 0..=99 {
let n = u8::from_str_radix(dbg!(&format!("{x:02}")), 16).unwrap();
assert_eq!(bcd_to_dec(n), x);
}
}
#[test]
fn test_dec_to_bcd() {
for n in 0..=99 {
assert_eq!(format!("{:#04x}", dec_to_bcd(n)), format!("0x{n:02}"));
}
}
#[test]
fn new() {
let time = DateTime::new(2018, 12, 23, 17, 49, 31).unwrap();
assert_eq!(time.year(), 2018);
assert_eq!(time.month(), 12);
assert_eq!(time.day(), 23);
assert_eq!(time.weekday(), 6);
assert_eq!(time.hour(), 17);
assert_eq!(time.minute(), 49);
assert_eq!(time.second(), 31);
}
#[test]
fn from_str() {
let time = DateTime::from_str("2018-12-23T17:49:31").unwrap();
assert_eq!(time.year(), 2018);
assert_eq!(time.month(), 12);
assert_eq!(time.day(), 23);
assert_eq!(time.weekday(), 6);
assert_eq!(time.hour(), 17);
assert_eq!(time.minute(), 49);
assert_eq!(time.second(), 31);
}
#[test]
fn from_bytes() {
let time = DateTime::from_bytes(&[0x20, 0x25, 0x12, 0x17, 0x02, 0x23, 0x31, 0x14]).unwrap();
assert_eq!(time.year(), 2025);
assert_eq!(time.month(), 12);
assert_eq!(time.day(), 17);
assert_eq!(time.weekday(), 2);
assert_eq!(time.hour(), 23);
assert_eq!(time.minute(), 31);
assert_eq!(time.second(), 14);
}
#[test]
fn to_bytes() {
let time = DateTime::new(2018, 12, 23, 17, 49, 31).unwrap();
assert_eq!(time.to_bytes(), [0x20, 0x18, 0x12, 0x23, 0x06, 0x17, 0x49, 0x31]);
}
}