Skip to main content

kmp_plugin_api/domain/
calendar_date.rs

1use std::fmt;
2
3use serde::{Deserialize, Serialize};
4
5use super::interpretation_error::InterpretationError;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8pub struct CalendarDate {
9    pub year: i32,
10    pub month: u8,
11    pub day: u8,
12}
13
14impl CalendarDate {
15    pub fn new(year: i32, month: u8, day: u8) -> Result<Self, InterpretationError> {
16        if !(1..=12).contains(&month) {
17            return Err(InterpretationError::new(format!(
18                "invalid calendar month `{month}`"
19            )));
20        }
21        let max_day = days_in_month(year, month);
22        if day == 0 || day > max_day {
23            return Err(InterpretationError::new(format!(
24                "invalid calendar day `{day}` for {year:04}-{month:02}"
25            )));
26        }
27        Ok(Self { year, month, day })
28    }
29
30    pub fn ordinal_days(&self) -> i64 {
31        days_from_civil(self.year, self.month, self.day)
32    }
33}
34
35impl fmt::Display for CalendarDate {
36    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
37        write!(
38            formatter,
39            "{:04}-{:02}-{:02}",
40            self.year, self.month, self.day
41        )
42    }
43}
44
45fn days_in_month(year: i32, month: u8) -> u8 {
46    match month {
47        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
48        4 | 6 | 9 | 11 => 30,
49        2 if is_leap_year(year) => 29,
50        2 => 28,
51        _ => 0,
52    }
53}
54
55fn is_leap_year(year: i32) -> bool {
56    (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
57}
58
59fn days_from_civil(year: i32, month: u8, day: u8) -> i64 {
60    let adjusted_year = year - i32::from(month <= 2);
61    let era = if adjusted_year >= 0 {
62        adjusted_year
63    } else {
64        adjusted_year - 399
65    } / 400;
66    let year_of_era = adjusted_year - era * 400;
67    let month_i32 = i32::from(month);
68    let day_i32 = i32::from(day);
69    let day_of_year =
70        (153 * (month_i32 + if month_i32 > 2 { -3 } else { 9 }) + 2) / 5 + day_i32 - 1;
71    let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
72    i64::from(era * 146_097 + day_of_era)
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn rejects_invalid_day_for_month() {
81        assert_eq!(
82            CalendarDate::new(2026, 2, 29)
83                .expect_err("invalid")
84                .to_string(),
85            "invalid calendar day `29` for 2026-02"
86        );
87    }
88}