1use std::fmt;
9
10use serde::{Deserialize, Deserializer, Serialize, Serializer};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
15pub struct Timestamp(pub jiff::Timestamp);
16
17impl Timestamp {
18 pub fn as_millisecond(&self) -> i64 {
19 self.0.as_millisecond()
20 }
21 pub fn from_millisecond(ms: i64) -> Self {
22 Self(jiff::Timestamp::from_millisecond(ms).unwrap_or(jiff::Timestamp::UNIX_EPOCH))
23 }
24}
25
26impl Serialize for Timestamp {
27 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
28 s.serialize_i64(self.as_millisecond())
29 }
30}
31
32impl<'de> Deserialize<'de> for Timestamp {
33 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
34 Ok(Self::from_millisecond(i64::deserialize(d)?))
35 }
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
42#[serde(transparent)]
43pub struct Date(pub jiff::civil::Date);
44
45impl Date {
46 pub fn parse(s: &str) -> Result<Self, jiff::Error> {
47 s.parse::<jiff::civil::Date>().map(Date)
48 }
49}
50
51impl fmt::Display for Date {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 write!(f, "{}", self.0)
54 }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
60pub struct Time(pub jiff::civil::Time);
61
62impl Time {
63 pub fn parse(s: &str) -> Result<Self, String> {
65 let s = s.trim();
66 let (h, m) = s
67 .split_once(':')
68 .ok_or_else(|| format!("expected \"HH:MM\", got {s:?}"))?;
69 let h: i8 = h.parse().map_err(|_| format!("bad hour in {s:?}"))?;
70 let m: i8 = m.parse().map_err(|_| format!("bad minute in {s:?}"))?;
71 jiff::civil::Time::new(h, m, 0, 0)
72 .map(Time)
73 .map_err(|e| format!("bad time {s:?}: {e}"))
74 }
75}
76
77impl fmt::Display for Time {
78 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79 write!(f, "{:02}:{:02}", self.0.hour(), self.0.minute())
80 }
81}
82
83impl Serialize for Time {
89 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
90 s.serialize_str(&self.to_string())
91 }
92}
93
94impl<'de> Deserialize<'de> for Time {
95 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
96 let s = String::deserialize(d)?;
97 Time::parse(&s).map_err(serde::de::Error::custom)
98 }
99}
100#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
107pub struct Due {
108 pub date: Date,
109 pub time: Option<Time>,
110}
111
112impl Due {
113 pub fn parse(s: &str) -> Result<Self, String> {
115 let s = s.trim();
116 let (date_part, time_part) = match s.split_once([' ', 'T']) {
117 Some((d, t)) => (d, Some(t)),
118 None => (s, None),
119 };
120 let date = Date::parse(date_part).map_err(|e| format!("bad date {date_part:?}: {e}"))?;
121 let time = time_part.map(Time::parse).transpose()?;
122 Ok(Due { date, time })
123 }
124}
125
126impl From<Date> for Due {
127 fn from(date: Date) -> Self {
128 Due { date, time: None }
129 }
130}
131
132impl fmt::Display for Due {
133 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134 match self.time {
135 Some(t) => write!(f, "{} {t}", self.date),
136 None => write!(f, "{}", self.date),
137 }
138 }
139}
140
141impl Serialize for Due {
144 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
145 s.serialize_str(&self.to_string())
146 }
147}
148
149impl<'de> Deserialize<'de> for Due {
150 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
151 let s = String::deserialize(d)?;
152 Due::parse(&s).map_err(serde::de::Error::custom)
153 }
154}
155#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
161pub struct Duration(pub std::time::Duration);
162
163impl Duration {
164 pub const ZERO: Self = Self(std::time::Duration::ZERO);
165
166 pub fn from_minutes(m: u32) -> Self {
167 Self(std::time::Duration::from_secs(u64::from(m) * 60))
168 }
169
170 pub fn as_minutes(&self) -> u32 {
171 u32::try_from(self.0.as_secs() / 60).unwrap_or(u32::MAX)
172 }
173}
174
175impl std::ops::Add for Duration {
176 type Output = Duration;
177 fn add(self, rhs: Self) -> Duration {
178 Duration(self.0 + rhs.0)
179 }
180}
181
182impl std::ops::AddAssign for Duration {
183 fn add_assign(&mut self, rhs: Self) {
184 self.0 += rhs.0;
185 }
186}
187
188impl std::ops::Sub for Duration {
189 type Output = Duration;
190 fn sub(self, rhs: Self) -> Duration {
191 Duration(self.0.saturating_sub(rhs.0))
192 }
193}
194
195impl std::iter::Sum for Duration {
196 fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
197 iter.fold(Duration::ZERO, std::ops::Add::add)
198 }
199}
200
201impl fmt::Display for Duration {
202 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203 let total = self.as_minutes();
204 match (total / 60, total % 60) {
205 (0, 0) => write!(f, "0m"),
206 (0, m) => write!(f, "{m}m"),
207 (h, 0) => write!(f, "{h}h"),
208 (h, m) => write!(f, "{h}h{m}m"),
209 }
210 }
211}
212
213impl Serialize for Duration {
214 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
215 s.serialize_u32(self.as_minutes())
216 }
217}
218
219impl<'de> Deserialize<'de> for Duration {
220 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
221 Ok(Self::from_minutes(u32::deserialize(d)?))
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn duration_display_is_human_readable() {
231 assert_eq!(Duration::ZERO.to_string(), "0m");
232 assert_eq!(Duration::from_minutes(45).to_string(), "45m");
233 assert_eq!(Duration::from_minutes(60).to_string(), "1h");
234 assert_eq!(Duration::from_minutes(90).to_string(), "1h30m");
235 }
236
237 #[test]
238 fn duration_json_roundtrips_as_integer_minutes() {
239 let d = Duration::from_minutes(30);
240 let json = serde_json::to_string(&d).unwrap();
241 assert_eq!(json, "30");
242 assert_eq!(serde_json::from_str::<Duration>(&json).unwrap(), d);
243 }
244
245 #[test]
246 fn date_json_roundtrips_as_iso_string() {
247 let d = Date::parse("2026-07-01").unwrap();
248 let json = serde_json::to_string(&d).unwrap();
249 assert_eq!(json, "\"2026-07-01\"");
250 assert_eq!(serde_json::from_str::<Date>(&json).unwrap(), d);
251 }
252
253 #[test]
254 fn due_parses_date_only_and_date_time() {
255 let date_only = Due::parse("2026-07-01").unwrap();
256 assert_eq!(date_only.time, None);
257 assert_eq!(date_only.to_string(), "2026-07-01");
258
259 let with_time = Due::parse("2026-07-01 14:30").unwrap();
260 assert_eq!(with_time.date, date_only.date);
261 assert_eq!(with_time.to_string(), "2026-07-01 14:30");
262
263 assert_eq!(Due::parse("2026-07-01T14:30").unwrap(), with_time);
265 }
266
267 #[test]
268 fn due_json_roundtrips_and_orders_date_before_time() {
269 let with_time = Due::parse("2026-07-01 09:00").unwrap();
270 let json = serde_json::to_string(&with_time).unwrap();
271 assert_eq!(json, "\"2026-07-01 09:00\"");
272 assert_eq!(serde_json::from_str::<Due>(&json).unwrap(), with_time);
273
274 let date_only: Due = Date::parse("2026-07-01").unwrap().into();
275 assert!(
276 date_only < with_time,
277 "a bare due date sorts before a same-day rendez-vous time"
278 );
279 }
280}