fc_dioxus/behavior/
date.rs1use std::fmt;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
12pub struct Date {
13 year: i32,
14 month: u8,
15 day: u8,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub struct InvalidDate;
21impl fmt::Display for InvalidDate {
22 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23 f.write_str("expected a real date as YYYY-MM-DD")
24 }
25}
26impl std::error::Error for InvalidDate {}
27
28const fn is_leap(year: i32) -> bool {
29 (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
30}
31
32#[must_use]
34pub const fn days_in_month(year: i32, month: u8) -> u8 {
35 match month {
36 2 if is_leap(year) => 29,
37 2 => 28,
38 4 | 6 | 9 | 11 => 30,
39 _ => 31,
40 }
41}
42
43impl Date {
44 pub fn new(year: i32, month: u8, day: u8) -> Result<Self, InvalidDate> {
50 if (1..=9999).contains(&year)
51 && (1..=12).contains(&month)
52 && day >= 1
53 && day <= days_in_month(year, month)
54 {
55 Ok(Self { year, month, day })
56 } else {
57 Err(InvalidDate)
58 }
59 }
60 pub fn parse(iso: &str) -> Result<Self, InvalidDate> {
65 let mut parts = iso.split('-');
66 let (Some(y), Some(m), Some(d), None) =
67 (parts.next(), parts.next(), parts.next(), parts.next())
68 else {
69 return Err(InvalidDate);
70 };
71 if y.len() != 4 || m.len() != 2 || d.len() != 2 {
72 return Err(InvalidDate);
73 }
74 let number = |s: &str| s.parse::<u16>().map_err(|_| InvalidDate);
75 let year = i32::from(number(y)?);
76 let month = u8::try_from(number(m)?).map_err(|_| InvalidDate)?;
77 let day = u8::try_from(number(d)?).map_err(|_| InvalidDate)?;
78 Self::new(year, month, day)
79 }
80 #[must_use]
82 pub const fn year(self) -> i32 {
83 self.year
84 }
85 #[must_use]
87 pub const fn month(self) -> u8 {
88 self.month
89 }
90 #[must_use]
92 pub const fn day(self) -> u8 {
93 self.day
94 }
95 #[must_use]
97 pub fn to_days(self) -> i64 {
98 let y = i64::from(self.year) - i64::from(self.month <= 2);
99 let era = y.div_euclid(400);
100 let yoe = y - era * 400;
101 let m = i64::from(self.month);
102 let doy = (153 * (m + if m > 2 { -3 } else { 9 }) + 2) / 5 + i64::from(self.day) - 1;
103 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
104 era * 146_097 + doe - 719_468
105 }
106 #[must_use]
108 pub fn from_days(days: i64) -> Option<Self> {
109 let z = days.checked_add(719_468)?;
110 let era = z.div_euclid(146_097);
111 let doe = z - era * 146_097;
112 let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
113 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
114 let mp = (5 * doy + 2) / 153;
115 let day = doy - (153 * mp + 2) / 5 + 1;
116 let month = if mp < 10 { mp + 3 } else { mp - 9 };
117 let year = yoe + era * 400 + i64::from(month <= 2);
118 Self::new(
119 i32::try_from(year).ok()?,
120 u8::try_from(month).ok()?,
121 u8::try_from(day).ok()?,
122 )
123 .ok()
124 }
125 #[must_use]
127 pub fn add_days(self, delta: i64) -> Option<Self> {
128 Self::from_days(self.to_days().checked_add(delta)?)
129 }
130 #[must_use]
132 pub fn weekday(self) -> u8 {
133 u8::try_from((self.to_days() + 4).rem_euclid(7)).unwrap_or(0)
135 }
136 #[must_use]
146 pub fn add_months(self, delta: i32) -> Option<Self> {
147 let index = self
148 .year
149 .checked_mul(12)?
150 .checked_add(i32::from(self.month) - 1)?
151 .checked_add(delta)?;
152 let year = index.div_euclid(12);
153 let month = u8::try_from(index.rem_euclid(12) + 1).ok()?;
154 let day = self.day.min(days_in_month(year, month));
155 Self::new(year, month, day).ok()
156 }
157 #[must_use]
159 pub fn first_of_month(self) -> Self {
160 Self { day: 1, ..self }
161 }
162}
163impl fmt::Display for Date {
164 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165 write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
166 }
167}
168
169#[must_use]
172pub fn leading_blanks(first_of_month: Date, first_day_of_week: u8) -> u8 {
173 (first_of_month.weekday() + 7 - first_day_of_week % 7) % 7
174}
175
176#[must_use]
180pub fn key_target(
181 key: &dioxus::prelude::Key,
182 shift: bool,
183 rtl: bool,
184 current: Date,
185 first_day_of_week: u8,
186) -> Option<Date> {
187 use dioxus::prelude::Key;
188 let weekday = i64::from((current.weekday() + 7 - first_day_of_week % 7) % 7);
189 match key {
190 Key::ArrowRight => current.add_days(if rtl { -1 } else { 1 }),
191 Key::ArrowLeft => current.add_days(if rtl { 1 } else { -1 }),
192 Key::ArrowDown => current.add_days(7),
193 Key::ArrowUp => current.add_days(-7),
194 Key::Home => current.add_days(-weekday),
195 Key::End => current.add_days(6 - weekday),
196 Key::PageDown => current.add_months(if shift { 12 } else { 1 }),
197 Key::PageUp => current.add_months(if shift { -12 } else { -1 }),
198 _ => None,
199 }
200}