kmp_plugin_api/domain/
calendar_date.rs1use 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}