Skip to main content

open_timeline_core/
date.rs

1// SPDX-License-Identifier: MIT
2
3//!
4//! The OpenTimeline date type
5//!
6
7use serde::{Deserialize, Deserializer, Serialize};
8use std::cmp::Ordering;
9use thiserror::Error;
10use time::OffsetDateTime;
11
12/// The minimum year allowed in the OpenTimeline system
13pub const MIN_YEAR: i64 = -50000;
14
15/// The maximum year allowed in the OpenTimeline system
16pub const MAX_YEAR: i64 = 10000;
17
18/// Errors that can arise in relation to a [`Date`]
19#[derive(Error, Debug, Clone)]
20pub enum DateError {
21    /// The day number is not allowed (must be 1 <= day <= 31)
22    #[error("Day `{0}` is not allowed")]
23    InvalidDay(i64),
24
25    /// The month number is not allowed (must be 1 <= day <= 12)
26    #[error("Month `{0}` is not allowed")]
27    InvalidMonth(i64),
28
29    /// The day number is not allowed (must be [`MIN_YEAR`] <= day <= [`MAX_YEAR`])
30    #[error("Month `{0}` is not allowed")]
31    InvalidYear(i64),
32
33    /// Invalid field initialisation.  e.g. the day has been set without the
34    /// month also being set
35    #[error("e.g. can't set day without setting month")]
36    InvalidFields,
37}
38
39/// The OpenTimeline date type
40///
41/// The year field must be set but the day and month fields are optional.  If
42/// the day field is set the month field must be set, but if the month field is
43/// set, the day field is optional.
44#[derive(Serialize, PartialEq, Eq, Clone, Copy, Debug, Hash)]
45pub struct Date {
46    day: Option<Day>,
47    month: Option<Month>,
48    year: Year,
49}
50
51/// The OpenTimeline day type
52#[rustfmt::skip]
53#[derive(derive_more::Display, Serialize, Eq, PartialEq, Clone, Copy, Debug, Hash, PartialOrd, Ord)]
54#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
55#[cfg_attr(feature = "sqlx", sqlx(transparent))]
56pub struct Day(u8);
57
58/// The OpenTimeline month type
59#[rustfmt::skip]
60#[derive(derive_more::Display, Serialize, Eq, PartialEq, Clone, Copy, Debug, Hash, PartialOrd, Ord)]
61#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
62#[cfg_attr(feature = "sqlx", sqlx(transparent))]
63pub struct Month(u8);
64
65/// The OpenTimeline year type
66/// 
67/// The minimum year allowed is [`MIN_YEAR`].  The maximum year allowed is
68/// [`MAX_YEAR`]
69#[rustfmt::skip]
70#[derive(derive_more::Display, Serialize, Eq, PartialEq, Clone, Copy, Debug, Hash, PartialOrd, Ord)]
71#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
72#[cfg_attr(feature = "sqlx", sqlx(transparent))]
73pub struct Year(i32);
74
75impl Day {
76    pub fn value(&self) -> u8 {
77        self.0
78    }
79
80    pub fn current() -> Self {
81        Date::today().day().unwrap()
82    }
83}
84
85impl Month {
86    pub fn value(&self) -> u8 {
87        self.0
88    }
89
90    pub fn current() -> Self {
91        Date::today().month().unwrap()
92    }
93}
94
95impl Year {
96    pub fn value(&self) -> i32 {
97        self.0
98    }
99
100    pub fn min() -> Self {
101        Year(MIN_YEAR as i32)
102    }
103
104    pub fn max() -> Self {
105        Year(MAX_YEAR as i32)
106    }
107
108    pub fn current() -> Self {
109        Date::today().year()
110    }
111}
112
113impl TryFrom<i64> for Day {
114    type Error = DateError;
115    fn try_from(value: i64) -> Result<Self, Self::Error> {
116        if (1..=31).contains(&value) {
117            Ok(Day(value as u8))
118        } else {
119            Err(DateError::InvalidDay(value))
120        }
121    }
122}
123
124impl TryFrom<i64> for Month {
125    type Error = DateError;
126    fn try_from(value: i64) -> Result<Self, Self::Error> {
127        if (1..=12).contains(&value) {
128            Ok(Month(value as u8))
129        } else {
130            Err(DateError::InvalidMonth(value))
131        }
132    }
133}
134
135impl TryFrom<i64> for Year {
136    type Error = DateError;
137    fn try_from(value: i64) -> Result<Self, Self::Error> {
138        if (MIN_YEAR..=MAX_YEAR).contains(&value) {
139            Ok(Year(value as i32))
140        } else {
141            Err(DateError::InvalidYear(value))
142        }
143    }
144}
145
146impl From<time::Month> for Month {
147    fn from(month: time::Month) -> Self {
148        Self(month.into())
149    }
150}
151
152impl From<Month> for time::Month {
153    fn from(month: Month) -> Self {
154        Self::try_from(month.value()).unwrap()
155    }
156}
157
158// TODO: add visitor so that can deserialise from strings as well?
159impl<'de> Deserialize<'de> for Day {
160    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
161    where
162        D: Deserializer<'de>,
163    {
164        let value = i64::deserialize(deserializer)?;
165        Day::try_from(value).map_err(|e| serde::de::Error::custom(format!("{:?}", e)))
166    }
167}
168
169// TODO: add visitor so that can deserialise from strings as well?
170impl<'de> Deserialize<'de> for Month {
171    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
172    where
173        D: Deserializer<'de>,
174    {
175        let value = i64::deserialize(deserializer)?;
176        Month::try_from(value).map_err(|e| serde::de::Error::custom(format!("{:?}", e)))
177    }
178}
179
180// TODO: add visitor so that can deserialise from strings as well?
181impl<'de> Deserialize<'de> for Year {
182    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
183    where
184        D: Deserializer<'de>,
185    {
186        let value = i64::deserialize(deserializer)?;
187        Year::try_from(value).map_err(|e| serde::de::Error::custom(format!("{:?}", e)))
188    }
189}
190
191impl Date {
192    /// Today's date
193    pub fn today() -> Self {
194        let today = OffsetDateTime::now_utc();
195        let month: Month = today.month().into();
196        Self::from(
197            Some(today.day().into()),
198            Some(month.value().into()),
199            today.year().into(),
200        )
201        .unwrap()
202    }
203
204    /// Create a new [`Date`] if the result will be valid
205    pub fn from(day: Option<i64>, month: Option<i64>, year: i64) -> Result<Date, DateError> {
206        let mut date = Date {
207            day: None,
208            month: None,
209            year: Year(0),
210        };
211        date.set_year(year)?;
212        date.set_month(month)?;
213        date.set_day(day)?;
214        Ok(date)
215    }
216
217    /// e.g. 1st Jan 2025 format
218    pub fn as_long_date_format(&self) -> String {
219        // Day
220        let day = match self.day() {
221            Some(day) => format!("{day}"),
222            None => String::new(),
223        };
224
225        // Month
226        let month = match self.month() {
227            Some(month) => match month.value() {
228                1 => "Jan",
229                2 => "Feb",
230                3 => "Mar",
231                4 => "Apr",
232                5 => "May",
233                6 => "Jun",
234                7 => "Jul",
235                8 => "Aug",
236                9 => "Sep",
237                10 => "Oct",
238                11 => "Nov",
239                12 => "Dec",
240                _ => panic!("Month value must be 1 <= x <= 12"),
241            },
242            None => "",
243        };
244
245        // Year
246        let year = self.year();
247
248        format!("{day} {month} {year}").trim().to_string()
249    }
250
251    /// dd/mm/yyyy format
252    pub fn as_short_date_format(&self) -> String {
253        // Day
254        let day = match self.day() {
255            Some(day) => format!("{day}"),
256            None => String::from("-"),
257        };
258
259        // Month
260        let month = match self.month() {
261            Some(month) => format!("{month}"),
262            None => String::from("-"),
263        };
264
265        // Year
266        let year = format!("{}", self.year());
267
268        // Return
269        format!("{day} / {month} / {year}")
270    }
271
272    // TODO: pass in Option<Day>?
273    /// Update an existing [`Date`]'s `day` if the result will be valid
274    pub fn set_day(&mut self, day: Option<i64>) -> Result<(), DateError> {
275        match day {
276            None => self.day = None,
277            Some(day) => {
278                // Ensure the new Date will be valid
279                let mut new_date = *self;
280                new_date.day = Some(Day::try_from(day)?);
281                new_date.is_valid()?;
282                *self = new_date;
283            }
284        }
285        Ok(())
286    }
287
288    // TODO: pass in Option<Month>?
289    // TODO: this is wrong - can end up with a day but no month
290    /// Update an existing [`Date`]'s `month` if the result will be valid
291    pub fn set_month(&mut self, month: Option<i64>) -> Result<(), DateError> {
292        match month {
293            None => self.month = None,
294            Some(month) => {
295                // Ensure the new Date will be valid
296                let mut new_date = *self;
297                new_date.month = Some(Month::try_from(month)?);
298                new_date.is_valid()?;
299                *self = new_date;
300            }
301        }
302        Ok(())
303    }
304
305    // TODO: pass in Option<Year>?
306    /// Update an existing [`Date`]'s `year` if the result will be valid
307    pub fn set_year(&mut self, year: i64) -> Result<(), DateError> {
308        // Ensure the new Date will be valid
309        let mut new_date = *self;
310        new_date.year = Year::try_from(year)?;
311        new_date.is_valid()?;
312        *self = new_date;
313
314        Ok(())
315    }
316
317    /// Get the [`Date`]'s day
318    pub fn day(&self) -> Option<Day> {
319        self.day
320    }
321
322    /// Get the [`Date`]'s month
323    pub fn month(&self) -> Option<Month> {
324        self.month
325    }
326
327    /// Get the [`Date`]'s year
328    pub fn year(&self) -> Year {
329        self.year
330    }
331
332    /// Check if the [`Date`] is valid
333    fn is_valid(&self) -> Result<(), DateError> {
334        match (self.day, self.month, self.year) {
335            // Valid
336            (None, None, _) => Ok(()),
337            (None, Some(_), _) => Ok(()),
338            (Some(_), Some(_), _) => Ok(()),
339
340            // Not valid
341            _ => Err(DateError::InvalidFields),
342        }
343    }
344}
345
346impl PartialOrd for Date {
347    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
348        match self.year.cmp(&other.year) {
349            Ordering::Less => return Some(Ordering::Less),
350            Ordering::Greater => return Some(Ordering::Greater),
351            Ordering::Equal => (),
352        };
353        if let (Some(this_month), Some(other_month)) = (self.month, other.month) {
354            match this_month.cmp(&other_month) {
355                Ordering::Less => return Some(Ordering::Less),
356                Ordering::Greater => return Some(Ordering::Greater),
357                Ordering::Equal => (),
358            };
359        } else {
360            return None;
361        }
362        if let (Some(this_day), Some(other_day)) = (self.day, other.day) {
363            match this_day.cmp(&other_day) {
364                Ordering::Less => Some(Ordering::Less),
365                Ordering::Greater => Some(Ordering::Greater),
366                Ordering::Equal => Some(Ordering::Equal),
367            }
368        } else {
369            None
370        }
371    }
372}
373
374// Beware!
375impl Ord for Date {
376    fn cmp(&self, other: &Self) -> Ordering {
377        let this_month = self.month().map(|m| m.value()).unwrap_or(1);
378        let other_month = other.month().map(|m| m.value()).unwrap_or(1);
379
380        let this_day = self.day().map(|d| d.value()).unwrap_or(1);
381        let other_day = other.day().map(|d| d.value()).unwrap_or(1);
382
383        (self.year, this_month, this_day).cmp(&(other.year, other_month, other_day))
384    }
385}
386
387#[derive(Deserialize)]
388struct RawDate {
389    day: Option<i64>,
390    month: Option<i64>,
391    year: i64,
392}
393
394impl<'de> Deserialize<'de> for Date {
395    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
396    where
397        D: Deserializer<'de>,
398    {
399        // TODO: look into serde Visitors & doing without RawDate type
400        let raw_date = RawDate::deserialize(deserializer)?;
401        let date = Date::from(raw_date.day, raw_date.month, raw_date.year);
402        match date {
403            Ok(date) => Ok(date),
404            Err(error) => Err(serde::de::Error::custom(error)),
405        }
406    }
407}
408
409#[cfg(test)]
410mod test {
411    use super::Date;
412
413    #[test]
414    fn from() {
415        // Should return error
416        assert!(Date::from(Some(1), None, 234).is_err());
417        assert!(Date::from(None, None, 999_999).is_err());
418        assert!(Date::from(None, None, -999_999).is_err());
419        assert!(Date::from(Some(0), Some(0), 1234).is_err());
420        assert!(Date::from(Some(32), Some(13), 1234).is_err());
421
422        // Should be ok
423        assert!(Date::from(Some(1), Some(1), 1).is_ok());
424    }
425
426    #[test]
427    fn cmp() {
428        // Year only
429        let date_1 = Date::from(None, None, 234).unwrap();
430        let date_2 = Date::from(None, None, 4321).unwrap();
431        assert!(date_2 > date_1);
432        assert!(date_1 < date_2);
433        assert!(date_1 == date_1);
434        assert!(date_1 != date_2);
435
436        // Difference of 1 day
437        let date_1 = Date::from(Some(1), Some(1), 234).unwrap();
438        let date_2 = Date::from(Some(2), Some(1), 234).unwrap();
439        assert!(date_2 > date_1);
440    }
441
442    #[test]
443    fn today() {
444        let today = Date::today();
445        println!("today = {}", today.as_long_date_format());
446        println!("today = {}", today.as_short_date_format());
447    }
448}