orbital_date_pickers/shared/
range_calendar_logic.rs1use chrono::{NaiveDate, Utc};
2use orbital_base_components::{DatetimeTimezone, OrbitalDateTime};
3
4#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
6pub struct RangeSelection {
7 pub start: Option<NaiveDate>,
8 pub end: Option<NaiveDate>,
9}
10
11impl RangeSelection {
12 pub fn is_complete(self) -> bool {
13 self.start.is_some() && self.end.is_some()
14 }
15}
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub enum DayRangeRole {
20 None,
21 RangeStart,
22 RangeEnd,
23 InRange,
24 PreviewRange,
25}
26
27pub fn apply_range_click(selection: RangeSelection, day: NaiveDate) -> RangeSelection {
29 if selection.end.is_some() {
30 return RangeSelection {
31 start: Some(day),
32 end: None,
33 };
34 }
35 if let Some(start) = selection.start {
36 let (start, end) = if day < start {
37 (day, start)
38 } else {
39 (start, day)
40 };
41 RangeSelection {
42 start: Some(start),
43 end: Some(end),
44 }
45 } else {
46 RangeSelection {
47 start: Some(day),
48 end: None,
49 }
50 }
51}
52
53pub fn day_range_role(
55 day: NaiveDate,
56 selection: RangeSelection,
57 hover: Option<NaiveDate>,
58) -> DayRangeRole {
59 if let (Some(start), Some(end)) = (selection.start, selection.end) {
60 if day == start {
61 return DayRangeRole::RangeStart;
62 }
63 if day == end {
64 return DayRangeRole::RangeEnd;
65 }
66 if day > start && day < end {
67 return DayRangeRole::InRange;
68 }
69 return DayRangeRole::None;
70 }
71
72 if let Some(start) = selection.start {
73 if day == start {
74 return DayRangeRole::RangeStart;
75 }
76 if let Some(hover_day) = hover {
77 let (preview_start, preview_end) = if hover_day < start {
78 (hover_day, start)
79 } else {
80 (start, hover_day)
81 };
82 if day == preview_start && day != start {
83 return DayRangeRole::PreviewRange;
84 }
85 if day == preview_end && day != start {
86 return DayRangeRole::PreviewRange;
87 }
88 if day > preview_start && day < preview_end {
89 return DayRangeRole::PreviewRange;
90 }
91 }
92 }
93
94 DayRangeRole::None
95}
96
97pub fn is_day_disabled(
98 day: NaiveDate,
99 min_date: Option<OrbitalDateTime>,
100 max_date: Option<OrbitalDateTime>,
101) -> bool {
102 if let Some(min) = min_date.and_then(|dt| dt.wall_date()) {
103 if day < min {
104 return true;
105 }
106 }
107 if let Some(max) = max_date.and_then(|dt| dt.wall_date()) {
108 if day > max {
109 return true;
110 }
111 }
112 false
113}
114
115pub fn orbital_from_date(date: NaiveDate, timezone: DatetimeTimezone) -> Option<OrbitalDateTime> {
116 OrbitalDateTime::from_naive_date(date, timezone)
117}
118
119pub fn today_for_timezone(timezone: DatetimeTimezone) -> NaiveDate {
120 use chrono::{FixedOffset, Local};
121 match timezone {
122 DatetimeTimezone::Local => Local::now().date_naive(),
123 DatetimeTimezone::Utc => Utc::now().date_naive(),
124 DatetimeTimezone::FixedOffset(offset_secs) => FixedOffset::east_opt(offset_secs)
125 .map(|offset| Utc::now().with_timezone(&offset).date_naive())
126 .unwrap_or_else(|| Utc::now().date_naive()),
127 }
128}
129
130pub fn selection_to_range(
131 selection: RangeSelection,
132 timezone: DatetimeTimezone,
133) -> Option<crate::DateTimeRange> {
134 let (start, end) = (selection.start?, selection.end?);
135 let start_dt = orbital_from_date(start, timezone)?;
136 let end_dt = orbital_from_date(end, timezone)?;
137 Some(crate::DateTimeRange::new(start_dt, end_dt).normalized())
138}
139
140pub fn range_to_selection(range: Option<crate::DateTimeRange>) -> RangeSelection {
141 match range {
142 Some(range) => RangeSelection {
143 start: range.start.wall_date(),
144 end: range.end.wall_date(),
145 },
146 None => RangeSelection::default(),
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use chrono::NaiveDate;
154
155 #[test]
156 fn first_click_sets_start_only() {
157 let day = NaiveDate::from_ymd_opt(2025, 6, 10).expect("date");
158 let next = apply_range_click(RangeSelection::default(), day);
159 assert_eq!(next.start, Some(day));
160 assert_eq!(next.end, None);
161 }
162
163 #[test]
164 fn second_click_completes_range() {
165 let start = NaiveDate::from_ymd_opt(2025, 6, 10).expect("date");
166 let end = NaiveDate::from_ymd_opt(2025, 6, 15).expect("date");
167 let partial = RangeSelection {
168 start: Some(start),
169 end: None,
170 };
171 let complete = apply_range_click(partial, end);
172 assert_eq!(complete.start, Some(start));
173 assert_eq!(complete.end, Some(end));
174 }
175
176 #[test]
177 fn click_while_complete_restarts() {
178 let old_start = NaiveDate::from_ymd_opt(2025, 6, 1).expect("date");
179 let old_end = NaiveDate::from_ymd_opt(2025, 6, 20).expect("date");
180 let complete = RangeSelection {
181 start: Some(old_start),
182 end: Some(old_end),
183 };
184 let new_day = NaiveDate::from_ymd_opt(2025, 7, 4).expect("date");
185 let restarted = apply_range_click(complete, new_day);
186 assert_eq!(restarted.start, Some(new_day));
187 assert_eq!(restarted.end, None);
188 }
189}