1use std::sync::Arc;
2
3use time::{Date, Duration, Month, Weekday};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub struct DateRange {
7 pub start: Date,
8 pub end: Date,
9}
10
11impl DateRange {
12 pub fn new(a: Date, b: Date) -> Self {
13 if a <= b {
14 Self { start: a, end: b }
15 } else {
16 Self { start: b, end: a }
17 }
18 }
19
20 pub fn contains(&self, date: Date) -> bool {
21 self.start <= date && date <= self.end
22 }
23}
24
25#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
27pub struct DateRangeSelection {
28 pub from: Option<Date>,
29 pub to: Option<Date>,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub enum SelectionUpdate<T> {
34 NoChange,
35 Set(T),
36}
37
38impl<T> SelectionUpdate<T> {
39 pub fn is_change(&self) -> bool {
40 matches!(self, Self::Set(_))
41 }
42}
43
44#[derive(Clone)]
48pub enum DayMatcher {
49 Bool(bool),
50 Predicate(Arc<dyn Fn(Date) -> bool + Send + Sync + 'static>),
51 Date(Date),
52 Dates(Arc<[Date]>),
53 DateRange(DateRangeSelection),
54 DateBefore { before: Date },
55 DateAfter { after: Date },
56 DateInterval { before: Date, after: Date },
57 DayOfWeek(Arc<[Weekday]>),
58}
59
60impl std::fmt::Debug for DayMatcher {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 match self {
63 Self::Bool(v) => f.debug_tuple("Bool").field(v).finish(),
64 Self::Predicate(_) => f.debug_tuple("Predicate").field(&"<fn>").finish(),
65 Self::Date(d) => f.debug_tuple("Date").field(d).finish(),
66 Self::Dates(ds) => f.debug_tuple("Dates").field(ds).finish(),
67 Self::DateRange(r) => f.debug_tuple("DateRange").field(r).finish(),
68 Self::DateBefore { before } => f
69 .debug_struct("DateBefore")
70 .field("before", before)
71 .finish(),
72 Self::DateAfter { after } => f.debug_struct("DateAfter").field("after", after).finish(),
73 Self::DateInterval { before, after } => f
74 .debug_struct("DateInterval")
75 .field("before", before)
76 .field("after", after)
77 .finish(),
78 Self::DayOfWeek(days) => f.debug_tuple("DayOfWeek").field(days).finish(),
79 }
80 }
81}
82
83impl DayMatcher {
84 pub fn day_of_week(day: Weekday) -> Self {
85 Self::DayOfWeek(Arc::from([day]))
86 }
87
88 pub fn day_of_week_any(days: impl Into<Arc<[Weekday]>>) -> Self {
89 Self::DayOfWeek(days.into())
90 }
91
92 pub fn dates(dates: impl Into<Arc<[Date]>>) -> Self {
93 Self::Dates(dates.into())
94 }
95
96 pub fn is_match(&self, date: Date) -> bool {
97 match self {
99 Self::Bool(v) => *v,
100 Self::Predicate(p) => p(date),
101 Self::Date(d) => *d == date,
102 Self::Dates(ds) => ds.contains(&date),
103 Self::DateRange(r) => range_includes_date(*r, date, false),
104 Self::DayOfWeek(days) => days.iter().any(|d| *d == date.weekday()),
105 Self::DateInterval { before, after } => {
106 let diff_before = (*before - date).whole_days();
107 let diff_after = (*after - date).whole_days();
108 let is_day_before = diff_before > 0;
109 let is_day_after = diff_after < 0;
110 let is_closed_interval = *before > *after;
111 if is_closed_interval {
112 is_day_after && is_day_before
113 } else {
114 is_day_before || is_day_after
115 }
116 }
117 Self::DateAfter { after } => (date - *after).whole_days() > 0,
118 Self::DateBefore { before } => (*before - date).whole_days() > 0,
119 }
120 }
121}
122
123pub fn range_includes_date(range: DateRangeSelection, date: Date, exclude_ends: bool) -> bool {
125 let mut from = range.from;
126 let mut to = range.to;
127 if let (Some(f), Some(t)) = (from, to)
128 && t < f
129 {
130 (from, to) = (Some(t), Some(f));
131 }
132
133 match (from, to) {
134 (Some(f), Some(t)) => {
135 let left = (date - f).whole_days();
136 let right = (t - date).whole_days();
137 let min = if exclude_ends { 1 } else { 0 };
138 left >= min && right >= min
139 }
140 (None, Some(t)) if !exclude_ends => t == date,
141 (Some(f), None) if !exclude_ends => f == date,
142 _ => false,
143 }
144}
145
146#[derive(Debug, Default, Clone)]
149pub struct DayPickerModifiers {
150 pub disabled: Vec<DayMatcher>,
151 pub hidden: Vec<DayMatcher>,
152}
153
154impl DayPickerModifiers {
155 pub fn disabled_by(mut self, matcher: DayMatcher) -> Self {
156 self.disabled.push(matcher);
157 self
158 }
159
160 pub fn hidden_by(mut self, matcher: DayMatcher) -> Self {
161 self.hidden.push(matcher);
162 self
163 }
164}
165
166#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
167pub struct DayPickerDayModifiers {
168 pub outside: bool,
169 pub disabled: bool,
170 pub hidden: bool,
171}
172
173pub fn day_picker_day_modifiers(
174 day: CalendarDay,
175 show_outside_days: bool,
176 modifiers: &DayPickerModifiers,
177) -> DayPickerDayModifiers {
178 let outside = !day.in_month;
179 let hidden =
180 (!show_outside_days && outside) || modifiers.hidden.iter().any(|m| m.is_match(day.date));
181 let disabled = modifiers.disabled.iter().any(|m| m.is_match(day.date));
182 DayPickerDayModifiers {
183 outside,
184 disabled,
185 hidden,
186 }
187}
188
189#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
199pub struct DayPickerCellState {
200 pub hidden: bool,
201 pub disabled: bool,
202}
203
204pub fn day_picker_cell_state(
205 day: CalendarDay,
206 show_outside_days: bool,
207 disable_outside_days: bool,
208 in_bounds: bool,
209 modifiers: &DayPickerModifiers,
210) -> DayPickerCellState {
211 let base = day_picker_day_modifiers(day, show_outside_days, modifiers);
212 let hidden = base.hidden || (!in_bounds && !day.in_month);
216
217 let mut disabled = base.disabled || (!day.in_month && disable_outside_days) || !in_bounds;
218 if hidden {
219 disabled = true;
220 }
221
222 DayPickerCellState { hidden, disabled }
223}
224
225pub fn day_grid_step_target_skipping_disabled(
226 current: usize,
227 len: usize,
228 step: i32,
229 disabled: &[bool],
230) -> usize {
231 if len == 0 || step == 0 || current >= len {
232 return current;
233 }
234
235 let mut idx = (current as i32 + step).clamp(0, (len.saturating_sub(1)) as i32) as usize;
236 if idx == current {
237 return current;
238 }
239
240 while idx != current && disabled.get(idx).copied().unwrap_or(true) {
241 let next = idx as i32 + step;
242 if next < 0 || next >= len as i32 {
243 break;
244 }
245 idx = next as usize;
246 }
247
248 if idx != current && !disabled.get(idx).copied().unwrap_or(true) {
249 idx
250 } else {
251 current
252 }
253}
254
255pub fn day_grid_row_edge_target_skipping_disabled(
256 current: usize,
257 len: usize,
258 target: usize,
259 disabled: &[bool],
260) -> usize {
261 if len == 0 || current >= len {
262 return current;
263 }
264
265 let row_start = (current / 7) * 7;
266 let row_end = (row_start + 6).min(len.saturating_sub(1));
267 let target = target.clamp(row_start, row_end);
268
269 if !disabled.get(target).copied().unwrap_or(true) {
270 return target;
271 }
272
273 if target == row_start {
274 for idx in row_start..=row_end {
275 if !disabled.get(idx).copied().unwrap_or(true) {
276 return idx;
277 }
278 }
279 } else if target == row_end {
280 for idx in (row_start..=row_end).rev() {
281 if !disabled.get(idx).copied().unwrap_or(true) {
282 return idx;
283 }
284 }
285 }
286
287 current
288}
289
290#[derive(Debug, Clone, Copy, PartialEq, Eq)]
291pub enum DayPickerGridLayout {
292 Compact,
293 FixedWeeks,
294}
295
296#[derive(Debug, Clone, Copy, PartialEq, Eq)]
297pub struct DayPickerGridOptions {
298 pub week_start: Weekday,
299 pub layout: DayPickerGridLayout,
300}
301
302impl Default for DayPickerGridOptions {
303 fn default() -> Self {
304 Self {
305 week_start: Weekday::Monday,
306 layout: DayPickerGridLayout::Compact,
307 }
308 }
309}
310
311pub fn day_picker_month_grid(
316 month: CalendarMonth,
317 options: DayPickerGridOptions,
318) -> Vec<CalendarDay> {
319 match options.layout {
320 DayPickerGridLayout::Compact => month_grid_compact(month, options.week_start),
321 DayPickerGridLayout::FixedWeeks => month_grid(month, options.week_start).to_vec(),
322 }
323}
324
325pub fn day_picker_select_single(
327 trigger: Date,
328 current: Option<Date>,
329 required: bool,
330) -> SelectionUpdate<Option<Date>> {
331 let mut next = Some(trigger);
332 if !required && current.is_some_and(|d| d == trigger) {
333 next = None;
334 }
335
336 if next == current {
337 SelectionUpdate::NoChange
338 } else {
339 SelectionUpdate::Set(next)
340 }
341}
342
343pub fn day_picker_select_multi(
345 trigger: Date,
346 current: &[Date],
347 required: bool,
348 min: Option<usize>,
349 max: Option<usize>,
350) -> SelectionUpdate<Vec<Date>> {
351 let min = min.unwrap_or(0);
352 let max = max.unwrap_or(0);
353
354 let is_selected = current.contains(&trigger);
355
356 if is_selected {
357 if current.len() == min {
358 return SelectionUpdate::NoChange;
359 }
360 if required && current.len() == 1 {
361 return SelectionUpdate::NoChange;
362 }
363 let next = current
364 .iter()
365 .copied()
366 .filter(|d| *d != trigger)
367 .collect::<Vec<_>>();
368 if next == current {
369 SelectionUpdate::NoChange
370 } else {
371 SelectionUpdate::Set(next)
372 }
373 } else {
374 let next = if max > 0 && current.len() == max {
375 vec![trigger]
376 } else {
377 let mut next = current.to_vec();
378 next.push(trigger);
379 next
380 };
381 if next == current {
382 SelectionUpdate::NoChange
383 } else {
384 SelectionUpdate::Set(next)
385 }
386 }
387}
388
389pub fn day_picker_add_to_range(
399 trigger: Date,
400 current: DateRangeSelection,
401 min_days: i64,
402 max_days: i64,
403 required: bool,
404 exclude_disabled: bool,
405 disabled_predicate: Option<&dyn Fn(Date) -> bool>,
406) -> SelectionUpdate<DateRangeSelection> {
407 let mut from = current.from;
408 let mut to = current.to;
409
410 if from.is_none() && to.is_some() {
412 from = None;
413 to = None;
414 }
415
416 let mut next: Option<DateRangeSelection> = match (from, to) {
417 (None, None) => Some(DateRangeSelection {
418 from: Some(trigger),
419 to: if min_days > 0 { None } else { Some(trigger) },
420 }),
421 (Some(f), None) => {
422 if f == trigger {
423 if min_days == 0 {
424 Some(DateRangeSelection {
425 from: Some(f),
426 to: Some(trigger),
427 })
428 } else if required {
429 Some(DateRangeSelection {
430 from: Some(f),
431 to: None,
432 })
433 } else {
434 None
435 }
436 } else if trigger < f {
437 Some(DateRangeSelection {
438 from: Some(trigger),
439 to: Some(f),
440 })
441 } else {
442 Some(DateRangeSelection {
443 from: Some(f),
444 to: Some(trigger),
445 })
446 }
447 }
448 (Some(f), Some(t)) => {
449 if f == trigger && t == trigger {
450 if required {
451 Some(DateRangeSelection {
452 from: Some(f),
453 to: Some(t),
454 })
455 } else {
456 None
457 }
458 } else if f == trigger {
459 Some(DateRangeSelection {
460 from: Some(f),
461 to: if min_days > 0 { None } else { Some(trigger) },
462 })
463 } else if t == trigger {
464 Some(DateRangeSelection {
465 from: Some(trigger),
466 to: if min_days > 0 { None } else { Some(trigger) },
467 })
468 } else if trigger < f {
469 Some(DateRangeSelection {
470 from: Some(trigger),
471 to: Some(t),
472 })
473 } else if trigger > f {
474 Some(DateRangeSelection {
475 from: Some(f),
476 to: Some(trigger),
477 })
478 } else {
479 Some(DateRangeSelection {
481 from: Some(f),
482 to: Some(t),
483 })
484 }
485 }
486 (None, Some(_)) => Some(DateRangeSelection {
487 from: Some(trigger),
488 to: if min_days > 0 { None } else { Some(trigger) },
489 }),
490 };
491
492 if let Some(r) = next.as_mut()
494 && let (Some(f), Some(t)) = (r.from, r.to)
495 {
496 let diff_days = (t - f).whole_days();
497 if (max_days > 0 && diff_days > max_days) || (min_days > 1 && diff_days < min_days) {
498 *r = DateRangeSelection {
499 from: Some(trigger),
500 to: None,
501 };
502 }
503 }
504
505 if exclude_disabled
507 && let Some(pred) = disabled_predicate
508 && let Some(r) = next.as_mut()
509 && let (Some(f), Some(t)) = (r.from, r.to)
510 {
511 let diff_days = (t - f).whole_days();
512 if diff_days >= 0 {
513 for i in 0..=diff_days {
514 let d = f + Duration::days(i);
515 if pred(d) {
516 *r = DateRangeSelection {
517 from: Some(trigger),
518 to: None,
519 };
520 break;
521 }
522 }
523 }
524 }
525
526 let next = next.unwrap_or_default();
527 if next == current {
528 SelectionUpdate::NoChange
529 } else {
530 SelectionUpdate::Set(next)
531 }
532}
533
534impl DateRangeSelection {
535 pub fn clear(&mut self) {
536 self.from = None;
537 self.to = None;
538 }
539
540 pub fn is_complete(&self) -> bool {
541 self.from.is_some() && self.to.is_some()
542 }
543
544 pub fn normalized_range(&self) -> Option<DateRange> {
545 Some(DateRange::new(self.from?, self.to?))
546 }
547
548 pub fn is_start(&self, date: Date) -> bool {
549 self.from.is_some_and(|d| d == date)
550 || self.to.is_some_and(|d| d == date && self.from.is_none())
551 }
552
553 pub fn is_end(&self, date: Date) -> bool {
554 self.to.is_some_and(|d| d == date) && self.from.is_some()
555 }
556
557 pub fn contains(&self, date: Date) -> bool {
558 self.normalized_range()
559 .is_some_and(|range| range.contains(date))
560 }
561
562 pub fn apply_click(&mut self, date: Date) {
571 let current = *self;
572 if let SelectionUpdate::Set(next) =
573 day_picker_add_to_range(date, current, 0, 0, false, false, None)
574 {
575 *self = next;
576 }
577 }
578
579 pub fn apply_click_with(
580 &mut self,
581 date: Date,
582 min_days: i64,
583 max_days: i64,
584 required: bool,
585 exclude_disabled: bool,
586 disabled_predicate: Option<&dyn Fn(Date) -> bool>,
587 ) {
588 let current = *self;
589 if let SelectionUpdate::Set(next) = day_picker_add_to_range(
590 date,
591 current,
592 min_days,
593 max_days,
594 required,
595 exclude_disabled,
596 disabled_predicate,
597 ) {
598 *self = next;
599 }
600 }
601}
602
603#[derive(Debug, Clone, Copy, PartialEq, Eq)]
604pub struct CalendarMonth {
605 pub year: i32,
606 pub month: Month,
607}
608
609impl CalendarMonth {
610 pub fn new(year: i32, month: Month) -> Self {
611 Self { year, month }
612 }
613
614 pub fn from_date(date: Date) -> Self {
615 Self {
616 year: date.year(),
617 month: date.month(),
618 }
619 }
620
621 pub fn first_day(&self) -> Date {
622 Date::from_calendar_date(self.year, self.month, 1).expect("valid month")
623 }
624
625 pub fn next_month(&self) -> Self {
626 let (year, month) = if self.month == Month::December {
627 (self.year + 1, Month::January)
628 } else {
629 (self.year, self.month.next())
630 };
631 Self { year, month }
632 }
633
634 pub fn prev_month(&self) -> Self {
635 let (year, month) = if self.month == Month::January {
636 (self.year - 1, Month::December)
637 } else {
638 (self.year, self.month.previous())
639 };
640 Self { year, month }
641 }
642}
643
644#[derive(Debug, Clone, Copy, PartialEq, Eq)]
645pub struct CalendarDay {
646 pub date: Date,
647 pub in_month: bool,
648}
649
650fn weekday_index_from_monday(weekday: Weekday) -> u8 {
651 weekday.number_from_monday()
652}
653
654fn offset_to_week_start(day: Weekday, week_start: Weekday) -> u8 {
655 let a = weekday_index_from_monday(day) as i16;
656 let b = weekday_index_from_monday(week_start) as i16;
657 let diff = a - b;
658 ((diff % 7 + 7) % 7) as u8
659}
660
661pub fn month_grid(month: CalendarMonth, week_start: Weekday) -> [CalendarDay; 42] {
667 let first = month.first_day();
668 let start_offset = offset_to_week_start(first.weekday(), week_start) as i64;
669 let grid_start = first - Duration::days(start_offset);
670
671 std::array::from_fn(|i| {
672 let date = grid_start + Duration::days(i as i64);
673 CalendarDay {
674 date,
675 in_month: date.year() == month.year && date.month() == month.month,
676 }
677 })
678}
679
680pub fn month_grid_compact(month: CalendarMonth, week_start: Weekday) -> Vec<CalendarDay> {
687 let first = month.first_day();
688 let next_first = month.next_month().first_day();
689 let last = next_first - Duration::days(1);
690
691 let start_offset = offset_to_week_start(first.weekday(), week_start) as i64;
692 let grid_start = first - Duration::days(start_offset);
693
694 let week_start_idx = weekday_index_from_monday(week_start) as i16;
695 let week_end_idx = (week_start_idx + 6) % 7;
696 let last_idx = weekday_index_from_monday(last.weekday()) as i16;
697 let end_offset = ((week_end_idx - last_idx) % 7 + 7) % 7;
698 let grid_end = last + Duration::days(end_offset as i64);
699
700 let days = (grid_end - grid_start).whole_days() + 1;
701 debug_assert!(days > 0 && days % 7 == 0);
702
703 (0..days)
704 .map(|i| {
705 let date = grid_start + Duration::days(i);
706 CalendarDay {
707 date,
708 in_month: date.year() == month.year && date.month() == month.month,
709 }
710 })
711 .collect()
712}
713
714fn start_of_week(date: Date, week_start: Weekday) -> Date {
715 let offset = offset_to_week_start(date.weekday(), week_start) as i64;
716 date - Duration::days(offset)
717}
718
719pub fn week_number(date: Date, week_start: Weekday) -> u32 {
724 let week_start_date = start_of_week(date, week_start);
725 let week_end_date = week_start_date + Duration::days(6);
726 let week_year = week_end_date.year();
727
728 let jan1 = Date::from_calendar_date(week_year, Month::January, 1).expect("valid year");
729 let week1_start = start_of_week(jan1, week_start);
730
731 let diff_days = (week_start_date - week1_start).whole_days();
732 let weeks = diff_days.div_euclid(7).max(0);
733 (weeks as u32) + 1
734}
735#[cfg(test)]
736mod tests {
737 use super::*;
738
739 #[test]
740 fn day_picker_cell_state_out_of_bounds_is_disabled_not_hidden() {
741 let d = Date::from_calendar_date(2026, Month::January, 1).unwrap();
742 let day = CalendarDay {
743 date: d,
744 in_month: true,
745 };
746
747 let st = day_picker_cell_state(day, true, false, false, &DayPickerModifiers::default());
748 assert!(!st.hidden);
749 assert!(st.disabled);
750 }
751
752 #[test]
753 fn day_picker_cell_state_outside_day_out_of_bounds_is_hidden() {
754 let d = Date::from_calendar_date(2026, Month::February, 1).unwrap();
755 let day = CalendarDay {
756 date: d,
757 in_month: false,
758 };
759
760 let st = day_picker_cell_state(day, true, false, false, &DayPickerModifiers::default());
761 assert!(st.hidden);
762 assert!(st.disabled);
763 }
764
765 #[test]
766 fn month_grid_is_stable_42_days() {
767 let m = CalendarMonth::new(2026, Month::January);
768 let grid = month_grid(m, Weekday::Monday);
769 assert_eq!(grid.len(), 42);
770 }
771
772 #[test]
773 fn month_grid_includes_first_day_and_marks_in_month() {
774 let m = CalendarMonth::new(2026, Month::January);
775 let grid = month_grid(m, Weekday::Monday);
776 let jan1 = Date::from_calendar_date(2026, Month::January, 1).unwrap();
777 assert!(grid.iter().any(|d| d.date == jan1 && d.in_month));
778 }
779
780 #[test]
781 fn month_nav_rolls_year_boundaries() {
782 let dec = CalendarMonth::new(2025, Month::December);
783 assert_eq!(dec.next_month(), CalendarMonth::new(2026, Month::January));
784
785 let jan = CalendarMonth::new(2026, Month::January);
786 assert_eq!(jan.prev_month(), CalendarMonth::new(2025, Month::December));
787 }
788
789 #[test]
790 fn date_range_new_orders_start_end() {
791 let a = Date::from_calendar_date(2026, Month::January, 5).unwrap();
792 let b = Date::from_calendar_date(2026, Month::January, 2).unwrap();
793 let range = DateRange::new(a, b);
794 assert_eq!(range.start, b);
795 assert_eq!(range.end, a);
796 assert!(range.contains(a));
797 assert!(range.contains(b));
798 }
799
800 #[test]
801 fn date_range_selection_click_sequence_matches_daypicker_expectations() {
802 let d1 = Date::from_calendar_date(2026, Month::January, 2).unwrap();
803 let d2 = Date::from_calendar_date(2026, Month::January, 5).unwrap();
804 let d3 = Date::from_calendar_date(2026, Month::January, 8).unwrap();
805
806 let mut sel = DateRangeSelection::default();
807 sel.apply_click(d2);
808 assert_eq!(sel.from, Some(d2));
809 assert_eq!(sel.to, Some(d2));
810 assert!(sel.is_complete());
811
812 sel.apply_click(d1);
813 assert_eq!(sel.from, Some(d1));
814 assert_eq!(sel.to, Some(d2));
815 assert!(sel.is_complete());
816 assert!(sel.contains(d1));
817 assert!(sel.contains(d2));
818
819 sel.apply_click(d3);
820 assert_eq!(sel.from, Some(d1));
821 assert_eq!(sel.to, Some(d3));
822 assert!(sel.is_complete());
823 }
824
825 #[test]
826 fn day_picker_single_optional_toggles_off_on_same_day() {
827 let d1 = Date::from_calendar_date(2026, Month::January, 2).unwrap();
828 assert_eq!(
829 day_picker_select_single(d1, Some(d1), false),
830 SelectionUpdate::Set(None)
831 );
832 assert_eq!(
833 day_picker_select_single(d1, Some(d1), true),
834 SelectionUpdate::NoChange
835 );
836 }
837
838 #[test]
839 fn day_picker_multi_resets_when_max_reached() {
840 let d1 = Date::from_calendar_date(2026, Month::January, 2).unwrap();
841 let d2 = Date::from_calendar_date(2026, Month::January, 3).unwrap();
842 let d3 = Date::from_calendar_date(2026, Month::January, 4).unwrap();
843
844 let cur = vec![d1, d2];
845 assert_eq!(
846 day_picker_select_multi(d3, &cur, false, None, Some(2)),
847 SelectionUpdate::Set(vec![d3])
848 );
849 }
850
851 #[test]
852 fn day_picker_range_min_and_exclude_disabled_match_upstream_intent() {
853 let d1 = Date::from_calendar_date(2026, Month::January, 1).unwrap();
854 let d2 = Date::from_calendar_date(2026, Month::January, 2).unwrap();
855 let d10 = Date::from_calendar_date(2026, Month::January, 10).unwrap();
856
857 let mut sel = DateRangeSelection::default();
859 sel.apply_click_with(d1, 1, 0, false, false, None);
860 assert_eq!(
861 sel,
862 DateRangeSelection {
863 from: Some(d1),
864 to: None
865 }
866 );
867
868 let disabled = |d: Date| d == d2;
870 let mut sel = DateRangeSelection::default();
871 sel.apply_click_with(d1, 0, 0, false, false, None);
872 sel.apply_click_with(d10, 0, 0, false, true, Some(&disabled));
873 assert_eq!(
874 sel,
875 DateRangeSelection {
876 from: Some(d10),
877 to: None
878 }
879 );
880 }
881
882 #[test]
883 fn day_picker_cell_state_composes_hidden_disabled_outside_and_bounds() {
884 let date = Date::from_calendar_date(2026, Month::January, 2).unwrap();
885
886 let in_month = CalendarDay {
887 date,
888 in_month: true,
889 };
890 let outside = CalendarDay {
891 date,
892 in_month: false,
893 };
894
895 let mut modifiers = DayPickerModifiers::default();
896 modifiers.hidden.push(DayMatcher::Date(date));
897
898 let st = day_picker_cell_state(in_month, true, false, true, &modifiers);
900 assert_eq!(
901 st,
902 DayPickerCellState {
903 hidden: true,
904 disabled: true
905 }
906 );
907
908 let st = day_picker_cell_state(outside, true, true, true, &DayPickerModifiers::default());
910 assert_eq!(
911 st,
912 DayPickerCellState {
913 hidden: false,
914 disabled: true
915 }
916 );
917
918 let st =
920 day_picker_cell_state(in_month, true, false, false, &DayPickerModifiers::default());
921 assert_eq!(
922 st,
923 DayPickerCellState {
924 hidden: false,
925 disabled: true
926 }
927 );
928 }
929
930 #[test]
931 fn day_grid_navigation_skips_disabled() {
932 let len = 14;
933 let mut disabled = vec![true; len];
934 for idx in [1usize, 6, 8, 13] {
935 disabled[idx] = false;
936 }
937
938 assert_eq!(
939 day_grid_step_target_skipping_disabled(6, len, 1, &disabled),
940 8
941 );
942 assert_eq!(
943 day_grid_step_target_skipping_disabled(8, len, -1, &disabled),
944 6
945 );
946 assert_eq!(
947 day_grid_step_target_skipping_disabled(1, len, 7, &disabled),
948 8
949 );
950 assert_eq!(
951 day_grid_step_target_skipping_disabled(13, len, -7, &disabled),
952 6
953 );
954
955 let current = 10;
956 let row_start = (current / 7) * 7;
957 let row_end = (row_start + 6).min(len - 1);
958 assert_eq!(
959 day_grid_row_edge_target_skipping_disabled(current, len, row_start, &disabled),
960 8
961 );
962 assert_eq!(
963 day_grid_row_edge_target_skipping_disabled(current, len, row_end, &disabled),
964 13
965 );
966 }
967
968 #[test]
969 fn range_includes_date_matches_endpoints_only_when_open_ended() {
970 let d1 = Date::from_calendar_date(2026, Month::January, 2).unwrap();
971 let d2 = Date::from_calendar_date(2026, Month::January, 3).unwrap();
972 let only_from = DateRangeSelection {
973 from: Some(d1),
974 to: None,
975 };
976 let only_to = DateRangeSelection {
977 from: None,
978 to: Some(d1),
979 };
980
981 assert!(range_includes_date(only_from, d1, false));
982 assert!(!range_includes_date(only_from, d2, false));
983 assert!(range_includes_date(only_to, d1, false));
984 assert!(!range_includes_date(only_to, d2, false));
985 }
986
987 #[test]
988 fn day_matcher_date_interval_excludes_ends() {
989 let after = Date::from_calendar_date(2026, Month::January, 2).unwrap();
990 let before = Date::from_calendar_date(2026, Month::January, 5).unwrap();
991 let mid = Date::from_calendar_date(2026, Month::January, 3).unwrap();
992 let m = DayMatcher::DateInterval { before, after };
993
994 assert!(!m.is_match(after));
995 assert!(m.is_match(mid));
996 assert!(!m.is_match(before));
997 }
998
999 #[test]
1000 fn day_matcher_day_of_week_matches_any() {
1001 let monday = Date::from_calendar_date(2026, Month::January, 5).unwrap();
1002 assert_eq!(monday.weekday(), Weekday::Monday);
1003 let m = DayMatcher::day_of_week_any(Arc::from([Weekday::Sunday, Weekday::Monday]));
1004 assert!(m.is_match(monday));
1005 }
1006
1007 #[test]
1008 fn day_picker_month_grid_fixed_weeks_is_42_days() {
1009 let m = CalendarMonth::new(2026, Month::January);
1010 let grid = day_picker_month_grid(
1011 m,
1012 DayPickerGridOptions {
1013 week_start: Weekday::Monday,
1014 layout: DayPickerGridLayout::FixedWeeks,
1015 },
1016 );
1017 assert_eq!(grid.len(), 42);
1018 }
1019}