use chrono::{DateTime, Local, TimeZone}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum Clock { #[default] FirstIn, FirstOut, SecondIn, SecondOut, } // unsafe impl Send for Clock {} // unsafe impl Sync for Clock {} #[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Record { pub employee_id: u32, pub clock: Clock, pub datetime: DateTime, } // unsafe impl Send for Record {} // unsafe impl Sync for Record {} impl TryFrom<&[u8]> for Record { type Error = &'static str; fn try_from(record_bytes: &[u8]) -> Result { // Return an error if the slice length is less than 12 if record_bytes.len() != 12 { return Err("Slice must be of length 12 to be converted into Record"); } // Extract the employee ID from the central 4 bytes of the record in // little endian let mut employee_id = [0u8; 4]; employee_id.clone_from_slice(&record_bytes[4..8]); let employee_id = u32::from_le_bytes(employee_id); // Extract the DateTime from the last 4 bytes of the record in little // endian let mut datetime = [0u8; 4]; datetime.clone_from_slice(&record_bytes[8..12]); let datetime = u32::from_le_bytes(datetime); // Return a new Record #[allow(clippy::cast_possible_wrap)] Ok(Self { employee_id, clock: match record_bytes[1] >> 6 { 0 => Clock::FirstIn, 1 => Clock::FirstOut, 2 => Clock::SecondIn, 3 => Clock::SecondOut, _ => unreachable!("Math has broken"), }, datetime: Local .with_ymd_and_hms( // DateTime data is in this format: // // mmmmmmhh hhhddddd MMMMyyyy yyyyyyyy // // Using some bitshifts and some bitwise operations we can // extract the data and put it into a DateTime struct (datetime & 0x0fff) as i32, (datetime >> 12) & 0x0f, (datetime >> 16) & 0x1f, (datetime >> 21) & 0x1f, (datetime >> 26) & 0x3f, 0, ) .single() .ok_or("DateTime conversion error")?, }) } } #[cfg(test)] mod tests { use super::*; #[test] fn valid_record_conversion() { let record_bytes: &[u8] = &[0x10, 0x23, 0x0b, 0x1d, 0x01, 0, 0, 0, 0xb2, 0x17, 0x01, 0]; assert_eq!( record_bytes.try_into(), Ok(Record { employee_id: 1, clock: Clock::FirstIn, datetime: Local .with_ymd_and_hms(1970, 1, 1, 0, 0, 0) .single() .expect("Datetime is not unique!"), }) ); } #[test] fn invalid_length() { let record_bytes: &[u8] = &[0u8; 11]; assert_eq!( record_bytes.try_into(), Err::("Slice must be of length 12 to be converted into Record") ); } #[test] fn invalid_datetime() { let record_bytes: &[u8] = &[0x10, 0x23, 0x0b, 0x1d, 0x01, 0, 0, 0, 0xb2, 0x17, 0x20, 0]; assert_eq!( record_bytes.try_into(), Err::("DateTime conversion error") ); } }