sem-reg 0.1.1

Handle certain Windows registry binary values semantically. Extensible.
Documentation
use std::{num::ParseIntError, str::FromStr};

use map_self::MapSelf;
use serde::Serialize;
use thiserror::Error;

use crate::data_conversion::byte_seq::{ByteSeq, ParseError};

#[derive(Clone, Copy, PartialEq, Serialize, Debug)]
pub struct ClockTimeFrame {
    pub start: ClockTime,
    pub end: ClockTime,
}

impl ClockTimeFrame {
    pub const MIDNIGHT_TO_MIDNIGHT: Self = Self {
        start: ClockTime::MIDNIGHT,
        end: ClockTime::MIDNIGHT,
    };

    pub fn format(&self, use_12_hour_clock: bool) -> String {
        format!(
            "{}-{}",
            self.start.format(use_12_hour_clock),
            self.end.format(use_12_hour_clock)
        )
    }
}

impl FromStr for ClockTimeFrame {
    type Err = ClockTimeOrFrameFromStrError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let mut clock_time_iter = s.splitn(2, '-');
        if let (Some(start), Some(end)) = (clock_time_iter.next(), clock_time_iter.next()) {
            Ok(Self {
                start: start.parse()?,
                end: end.parse()?,
            })
        } else {
            Err(ClockTimeOrFrameFromStrError)
        }
    }
}

#[derive(Clone, Copy, PartialEq, Serialize, Debug)]
pub struct ClockTime {
    pub(super) hour: u8,
    pub(super) minute: u8,
}

impl ClockTime {
    pub const MIDNIGHT: Self = Self { hour: 0, minute: 0 };

    pub fn from_h_min(hour: u8, minute: u8) -> Option<Self> {
        if hour <= 23 && minute <= 59 {
            Some(Self { hour, minute })
        } else {
            None
        }
    }

    pub fn from_h_min_with_meridiem(mut hour: u8, minute: u8, meridiem: Meridiem) -> Option<Self> {
        if hour > 12 {
            return None;
        }

        match (meridiem, &mut hour) {
            (Meridiem::Am, hour) if *hour == 12 => *hour = 0,
            (Meridiem::Pm, hour) if *hour < 12 => *hour += 12,
            _ => {}
        };

        Self::from_h_min(hour, minute)
    }

    pub fn hour(&self) -> u8 {
        self.hour
    }

    pub fn hour_meridiem(&self) -> (u8, Meridiem) {
        if self.hour == 0 {
            (12, Meridiem::Am)
        } else if self.hour == 12 {
            (12, Meridiem::Pm)
        } else if self.hour > 12 {
            (self.hour - 12, Meridiem::Pm)
        } else {
            (self.hour, Meridiem::Am)
        }
    }

    pub fn minute(&self) -> u8 {
        self.minute
    }

    pub fn is_midnight(&self) -> bool {
        self.hour == 0 && self.minute == 0
    }

    pub fn format(&self, use_12_hour_clock: bool) -> String {
        let (hour, meridiem) = if use_12_hour_clock {
            self.hour_meridiem()
                .map_self(|(hour, meridiem)| (hour, Some(meridiem)))
        } else {
            (self.hour, None)
        };

        let mut string = format!("{:02}:{:02}", hour, self.minute,);

        if let Some(meridiem) = meridiem {
            string.push_str(meridiem.as_str());
        }

        string
    }
}

impl FromStr for ClockTime {
    type Err = ClockTimeOrFrameFromStrError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let lowercase_string = s.to_ascii_lowercase();
        let mut number_iter = lowercase_string.splitn(2, ':');

        if let (Some(hour), Some(minute_meridiem)) = (number_iter.next(), number_iter.next()) {
            let hour = hour.parse()?;

            let (minute, meridiem) = if let Some(minute) = minute_meridiem.strip_suffix("am") {
                (minute, Some(Meridiem::Am))
            } else if let Some(minute) = minute_meridiem.strip_suffix("pm") {
                (minute, Some(Meridiem::Pm))
            } else {
                (minute_meridiem, None)
            };
            let minute = minute.parse()?;

            Ok(if let Some(meridiem) = meridiem {
                Self::from_h_min_with_meridiem(hour, minute, meridiem)
            } else {
                Self::from_h_min(hour, minute)
            }
            .ok_or(ClockTimeOrFrameFromStrError)?)
        } else {
            Err(ClockTimeOrFrameFromStrError)
        }
    }
}

#[derive(Clone, Copy, PartialEq)]
pub enum Meridiem {
    Am,
    Pm,
}

impl Meridiem {
    fn as_str(&self) -> &'static str {
        match self {
            Meridiem::Am => "am",
            Meridiem::Pm => "pm",
        }
    }
}

pub(super) trait BinConvertClockTime {
    fn read_clock_time(&mut self) -> Result<ClockTime, ParseError>;
    fn push_clock_time(&mut self, clock_time: ClockTime);
}

impl BinConvertClockTime for ByteSeq {
    fn read_clock_time(&mut self) -> Result<ClockTime, ParseError> {
        let hour = if self.assert_const(&[0x0e]).is_ok() {
            self.read_int()?
        } else {
            0
        };

        let minute = if self.assert_const(&[0x2e]).is_ok() {
            self.read_int()?
        } else {
            0
        };

        Ok(ClockTime::from_h_min(hour, minute).ok_or(ParseError::ValueNotInRange)?)
    }

    fn push_clock_time(&mut self, clock_time: ClockTime) {
        if clock_time.hour != 0 {
            self.push_const(&[0x0e]);
            self.push_int(clock_time.hour);
        }
        if clock_time.minute != 0 {
            self.push_const(&[0x2e]);
            self.push_int(clock_time.minute);
        }
    }
}

#[derive(Error, PartialEq, Debug)]
#[error("couldn't parse `ClockTime` or `ClockTimeFrame`")]
pub struct ClockTimeOrFrameFromStrError;

impl From<ParseIntError> for ClockTimeOrFrameFromStrError {
    fn from(_error: ParseIntError) -> Self {
        ClockTimeOrFrameFromStrError
    }
}

#[cfg(test)]
mod tests {
    use crate::cloud_store::night_light::{ClockTime, ClockTimeFrame, Meridiem};

    #[test]
    fn clock_time_frame_from_str() {
        assert_eq!(
            "20:21-6:00".parse::<ClockTimeFrame>(),
            Ok(ClockTimeFrame {
                start: ClockTime::from_h_min_with_meridiem(8, 21, Meridiem::Pm).unwrap(),
                end: ClockTime::from_h_min_with_meridiem(6, 0, Meridiem::Am).unwrap()
            })
        );
        assert_eq!(
            "08:00pm-05:45AM".parse::<ClockTimeFrame>(),
            Ok(ClockTimeFrame {
                start: ClockTime::from_h_min(20, 00).unwrap(),
                end: ClockTime::from_h_min(5, 45).unwrap()
            })
        );
        assert_eq!(
            "9:59-9:59am".parse::<ClockTimeFrame>(),
            Ok(ClockTimeFrame {
                start: ClockTime::from_h_min_with_meridiem(9, 59, Meridiem::Am).unwrap(),
                end: ClockTime::from_h_min(9, 59).unwrap()
            })
        );

        assert!("9:59-9:59am-".parse::<ClockTimeFrame>().is_err());
        assert!("10:00 - 10:00".parse::<ClockTimeFrame>().is_err());
        assert!("10.00-10.00".parse::<ClockTimeFrame>().is_err());
        assert!("10:00-10:60".parse::<ClockTimeFrame>().is_err());
        assert!("10:00-24:00".parse::<ClockTimeFrame>().is_err());
    }
}