Skip to main content

dpm_model/calendar/
clock.rs

1//! Local times of day written `HH:MM`, and working periods between two of them.
2
3use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
4
5/// Minutes after local midnight, from `00:00` to `24:00` inclusive.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
7pub struct ClockTime(u16);
8
9/// Minutes in a day; `24:00` ends a period at the next midnight.
10const DAY_MINUTES: u16 = 24 * 60;
11
12impl ClockTime {
13    /// A whole hour, clamped to `24:00`.
14    #[must_use]
15    pub const fn hours(hours: u16) -> Self {
16        let minutes = hours.saturating_mul(60);
17        Self(if minutes > DAY_MINUTES {
18            DAY_MINUTES
19        } else {
20            minutes
21        })
22    }
23
24    /// Time from minutes after midnight, or `None` beyond `24:00`.
25    #[must_use]
26    pub fn from_minutes(minutes: u16) -> Option<Self> {
27        (minutes <= DAY_MINUTES).then_some(Self(minutes))
28    }
29
30    /// Minutes after midnight.
31    #[must_use]
32    pub fn minutes(self) -> u16 {
33        self.0
34    }
35}
36
37impl std::fmt::Display for ClockTime {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        write!(f, "{:02}:{:02}", self.0 / 60, self.0 % 60)
40    }
41}
42
43impl std::str::FromStr for ClockTime {
44    type Err = String;
45
46    fn from_str(text: &str) -> Result<Self, Self::Err> {
47        let invalid = || format!("time {text:?} must be HH:MM between 00:00 and 24:00");
48        let (hours, minutes) = text.split_once(':').ok_or_else(invalid)?;
49        if hours.len() != 2 || minutes.len() != 2 {
50            return Err(invalid());
51        }
52        let hours: u16 = hours.parse().map_err(|_| invalid())?;
53        let minutes: u16 = minutes.parse().map_err(|_| invalid())?;
54        if minutes >= 60 {
55            return Err(invalid());
56        }
57        Self::from_minutes(hours.saturating_mul(60).saturating_add(minutes)).ok_or_else(invalid)
58    }
59}
60
61impl Serialize for ClockTime {
62    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
63        serializer.collect_str(self)
64    }
65}
66
67struct ClockVisitor;
68
69impl de::Visitor<'_> for ClockVisitor {
70    type Value = ClockTime;
71
72    fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73        f.write_str("a local time HH:MM")
74    }
75
76    fn visit_str<E: de::Error>(self, text: &str) -> Result<ClockTime, E> {
77        text.parse().map_err(E::custom)
78    }
79}
80
81impl<'de> Deserialize<'de> for ClockTime {
82    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
83        deserializer.deserialize_str(ClockVisitor)
84    }
85}
86
87/// A local working period on one day, written `{"from": "08:00", "to": "12:00"}`; `to` may be
88/// `24:00`.
89#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
90#[serde(deny_unknown_fields)]
91pub struct WorkingPeriod {
92    /// Start of the period.
93    pub from: ClockTime,
94    /// End of the period, after its start.
95    pub to: ClockTime,
96}
97
98impl WorkingPeriod {
99    /// A period between two local times.
100    #[must_use]
101    pub fn new(from: ClockTime, to: ClockTime) -> Self {
102        Self { from, to }
103    }
104
105    /// Start of the period.
106    #[must_use]
107    pub fn start(self) -> ClockTime {
108        self.from
109    }
110
111    /// End of the period.
112    #[must_use]
113    pub fn end(self) -> ClockTime {
114        self.to
115    }
116}