1use crate::components::config_provider::{Locale, use_config};
2use crate::components::floating::use_floating_close_handle;
3use crate::components::select_base::use_dropdown_layer;
4use dioxus::events::KeyboardEvent;
5use dioxus::prelude::*;
6use std::collections::HashMap;
7use std::rc::Rc;
8use time::Date;
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub struct DateRangeValue {
13 pub start: Option<DateValue>,
14 pub end: Option<DateValue>,
15}
16
17impl DateRangeValue {
18 pub fn empty() -> Self {
19 Self {
20 start: None,
21 end: None,
22 }
23 }
24}
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub struct DateValue {
33 pub inner: Date,
34}
35
36impl DateValue {
37 pub fn from_ymd(year: i32, month: u8, day: u8) -> Option<Self> {
39 let month_enum = time::Month::try_from(month).ok()?;
41 Date::from_calendar_date(year, month_enum, day)
42 .ok()
43 .map(|d| DateValue { inner: d })
44 }
45
46 pub fn to_ymd_string(&self) -> String {
48 format!(
49 "{:04}-{:02}-{:02}",
50 self.inner.year(),
51 self.inner.month() as u8,
52 self.inner.day()
53 )
54 }
55}
56
57fn days_in_month(year: i32, month: u8) -> u8 {
59 match month {
60 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
61 4 | 6 | 9 | 11 => 30,
62 2 => {
63 let is_leap = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
65 if is_leap { 29 } else { 28 }
66 }
67 _ => 30,
68 }
69}
70
71fn weekday_index_monday(year: i32, month: u8, day: u8) -> u8 {
73 let m = month as i32;
75 let d = day as i32;
76 let mut y = year;
77 let t = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4];
78 if m < 3 {
79 y -= 1;
80 }
81 let w = (y + y / 4 - y / 100 + y / 400 + t[(m - 1) as usize] + d) % 7;
82 let sunday_based = w as u8; (sunday_based + 6) % 7
85}
86
87#[derive(Props, Clone)]
89pub struct DatePickerProps {
90 #[props(optional)]
92 pub value: Option<DateValue>,
93 #[props(optional)]
95 pub default_value: Option<DateValue>,
96 #[props(optional)]
98 pub placeholder: Option<String>,
99 #[props(optional)]
101 pub format: Option<String>,
102 #[props(optional)]
104 pub disabled: Option<bool>,
105 #[props(optional)]
107 pub allow_clear: Option<bool>,
108 #[props(optional)]
110 pub class: Option<String>,
111 #[props(optional)]
113 pub style: Option<String>,
114 #[props(optional)]
116 pub on_change: Option<EventHandler<Option<DateValue>>>,
117 #[props(optional)]
119 pub show_time: Option<ShowTimeConfig>,
120 #[props(optional)]
122 pub ranges: Option<HashMap<String, (DateValue, DateValue)>>,
123 #[props(optional)]
125 pub disabled_date: Option<Rc<dyn Fn(DateValue) -> bool>>,
126 #[props(optional)]
128 pub disabled_time: Option<Rc<dyn Fn(DateValue) -> bool>>,
129 #[props(optional)]
131 pub render_extra_footer: Option<Rc<dyn Fn() -> Element>>,
132 #[props(optional)]
134 pub generate_config: Option<DateGenerateConfig>,
135}
136
137impl PartialEq for DatePickerProps {
138 fn eq(&self, other: &Self) -> bool {
139 self.value == other.value
141 && self.default_value == other.default_value
142 && self.placeholder == other.placeholder
143 && self.format == other.format
144 && self.disabled == other.disabled
145 && self.allow_clear == other.allow_clear
146 && self.class == other.class
147 && self.style == other.style
148 && self.show_time == other.show_time
149 && self.ranges == other.ranges
150 && self.generate_config == other.generate_config
151 }
153}
154
155#[derive(Clone, Debug, PartialEq)]
157pub struct ShowTimeConfig {
158 pub format: Option<String>,
160 pub default_value: Option<String>,
162 pub hour_step: Option<u8>,
164 pub minute_step: Option<u8>,
166 pub second_step: Option<u8>,
168}
169
170#[derive(Clone)]
172pub struct DateGenerateConfig {
173 pub from_ymd: Rc<dyn Fn(i32, u8, u8) -> Option<DateValue>>,
175 pub now: Rc<dyn Fn() -> DateValue>,
177 pub format: Rc<dyn Fn(DateValue, &str) -> String>,
179 pub parse: Rc<dyn Fn(&str, &str) -> Option<DateValue>>,
181}
182
183impl PartialEq for DateGenerateConfig {
184 fn eq(&self, _other: &Self) -> bool {
185 false
187 }
188}
189
190#[component]
194pub fn DatePicker(props: DatePickerProps) -> Element {
195 let DatePickerProps {
196 value,
197 default_value,
198 placeholder,
199 format: _format,
200 disabled,
201 allow_clear,
202 class,
203 style,
204 on_change,
205 ..
206 } = props;
207
208 let config = use_config();
209 let locale = config.locale;
210
211 let is_disabled = disabled.unwrap_or(false);
212 let allow_clear_flag = allow_clear.unwrap_or(false);
213
214 let selected_state: Signal<Option<DateValue>> = use_signal(|| default_value);
216 let current_value: Option<DateValue> = if let Some(v) = value {
217 Some(v)
218 } else {
219 *selected_state.read()
220 };
221
222 let display_text = current_value.map(|v| v.to_ymd_string()).unwrap_or_default();
223
224 let default_placeholder = match locale {
225 Locale::ZhCN => "请选择日期".to_string(),
226 Locale::EnUS => "Select date".to_string(),
227 };
228 let placeholder_str = placeholder.unwrap_or(default_placeholder);
229
230 let controlled = value.is_some();
231
232 let open_state: Signal<bool> = use_signal(|| false);
234 let open_flag = *open_state.read();
235
236 let close_handle = use_floating_close_handle(open_state);
239 let dropdown_layer = use_dropdown_layer(open_flag);
240 let current_z = *dropdown_layer.z_index.read();
241
242 let initial_year = current_value.map(|v| v.inner.year()).unwrap_or(2024);
246 let initial_month = current_value.map(|v| v.inner.month() as u8).unwrap_or(1);
247 let view_year: Signal<i32> = use_signal(|| initial_year);
248 let view_month: Signal<u8> = use_signal(|| initial_month);
249
250 let year_now = *view_year.read();
251 let month_now = *view_month.read();
252 let days_in_month_now = days_in_month(year_now, month_now);
253 let first_weekday = weekday_index_monday(year_now, month_now, 1) as usize;
254 let total_cells = first_weekday + days_in_month_now as usize;
255 let padded_cells = total_cells.div_ceil(7) * 7; let locale_for_header = locale;
258 let month_label = match locale_for_header {
259 Locale::ZhCN => format!("{year_now}年{month_now}月"),
260 Locale::EnUS => format!("{year_now}-{month_now:02}"),
261 };
262 let weekday_labels: [&str; 7] = match locale_for_header {
263 Locale::ZhCN => ["一", "二", "三", "四", "五", "六", "日"],
264 Locale::EnUS => ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"],
265 };
266
267 let mut control_classes = vec!["adui-date-picker".to_string()];
269 if is_disabled {
270 control_classes.push("adui-date-picker-disabled".to_string());
271 }
272 if let Some(extra) = class.clone() {
273 control_classes.push(extra);
274 }
275 let control_class_attr = control_classes.join(" ");
276 let style_attr = style.unwrap_or_default();
277
278 let selected_for_day_click = selected_state;
280 let on_change_cb = on_change;
281 let controlled_flag = controlled;
282 let open_for_toggle = open_state;
283 let open_for_keydown = open_for_toggle;
284
285 let has_value = current_value.is_some();
287
288 let mut date_cells: Vec<Element> = Vec::new();
290 for index in 0..padded_cells {
291 let cell_day =
292 if index < first_weekday || index >= first_weekday + days_in_month_now as usize {
293 None
294 } else {
295 Some((index - first_weekday + 1) as u8)
296 };
297
298 let is_outside = cell_day.is_none();
299 let is_selected = if let (Some(day), Some(current)) = (cell_day, current_value) {
300 current.inner.year() == year_now
301 && current.inner.month() as u8 == month_now
302 && current.inner.day() == day
303 } else {
304 false
305 };
306
307 let mut cell_classes = vec!["adui-date-picker-cell".to_string()];
308 if is_outside {
309 cell_classes.push("adui-date-picker-cell-empty".to_string());
310 } else {
311 cell_classes.push("adui-date-picker-cell-date".to_string());
312 }
313 if is_selected {
314 cell_classes.push("adui-date-picker-cell-selected".to_string());
315 }
316 let cell_class_attr = cell_classes.join(" ");
317
318 let selected_state_for_cell = selected_for_day_click;
319 let on_change_cb_for_cell = on_change_cb;
320 let controlled_flag_for_cell = controlled_flag;
321 let close_for_cell = close_handle;
322
323 let cell_node = rsx! {
324 span {
325 class: "{cell_class_attr}",
326 onclick: move |_| {
327 close_for_cell.mark_internal_click();
328 if let Some(day) = cell_day
329 && let Some(value) = DateValue::from_ymd(year_now, month_now, day)
330 {
331 if controlled_flag_for_cell {
332 if let Some(cb) = on_change_cb_for_cell {
333 cb.call(Some(value));
334 }
335 } else {
336 let mut state = selected_state_for_cell;
337 state.set(Some(value));
338 if let Some(cb) = on_change_cb_for_cell {
339 cb.call(Some(value));
340 }
341 }
342 close_for_cell.close();
344 }
345 },
346 match cell_day {
347 Some(day) => rsx!{ "{day}" },
348 None => rsx!{ "" },
349 }
350 }
351 };
352
353 date_cells.push(cell_node);
354 }
355
356 rsx! {
357 div {
358 class: "adui-date-picker-root",
359 style: "position: relative; display: inline-block;",
360 div {
361 class: "{control_class_attr}",
362 style: "{style_attr}",
363 role: "combobox",
364 tabindex: (!is_disabled).then_some(0),
365 "aria-expanded": open_flag,
366 "aria-disabled": is_disabled,
367 onclick: move |_| {
368 if is_disabled {
369 return;
370 }
371 close_handle.mark_internal_click();
372 let mut open_signal = open_for_toggle;
373 let next = !*open_signal.read();
374 open_signal.set(next);
375 },
376 onkeydown: move |evt: KeyboardEvent| {
377 if is_disabled {
378 return;
379 }
380 use dioxus::prelude::Key;
381 match evt.key() {
382 Key::Enter => {
383 evt.prevent_default();
384 let mut open_signal = open_for_keydown;
385 open_signal.set(true);
386 }
387 Key::Escape => {
388 close_handle.close();
389 }
390 _ => {}
391 }
392 },
393 input {
394 class: "adui-date-picker-input",
395 readonly: true,
396 disabled: is_disabled,
397 value: "{display_text}",
398 placeholder: "{placeholder_str}",
399 }
400 if allow_clear_flag && has_value && !is_disabled {
401 span {
402 class: "adui-date-picker-clear",
403 onclick: move |_| {
404 if controlled_flag {
405 if let Some(cb) = on_change_cb {
406 cb.call(None);
407 }
408 } else {
409 let mut state = selected_for_day_click;
410 state.set(None);
411 if let Some(cb) = on_change_cb {
412 cb.call(None);
413 }
414 }
415 },
416 "×"
417 }
418 }
419 }
420
421 if open_flag {
422 div {
424 class: "adui-date-picker-dropdown",
425 style: "position: absolute; top: 100%; left: 0; min-width: 100%; z-index: {current_z};",
426 div { class: "adui-date-picker-header",
428 button {
429 class: "adui-date-picker-nav-btn adui-date-picker-prev-month",
430 onclick: move |_| {
431 close_handle.mark_internal_click();
432 let mut year = *view_year.read();
433 let mut month = *view_month.read();
434 if month == 1 {
435 month = 12;
436 year -= 1;
437 } else {
438 month -= 1;
439 }
440 let mut y = view_year;
441 let mut m = view_month;
442 y.set(year);
443 m.set(month);
444 },
445 "<"
446 }
447 span { class: "adui-date-picker-header-view", "{month_label}" }
448 button {
449 class: "adui-date-picker-nav-btn adui-date-picker-next-month",
450 onclick: move |_| {
451 close_handle.mark_internal_click();
452 let mut year = *view_year.read();
453 let mut month = *view_month.read();
454 if month == 12 {
455 month = 1;
456 year += 1;
457 } else {
458 month += 1;
459 }
460 let mut y = view_year;
461 let mut m = view_month;
462 y.set(year);
463 m.set(month);
464 },
465 ">"
466 }
467 }
468
469 div { class: "adui-date-picker-week-row",
471 for label in weekday_labels {
472 span { class: "adui-date-picker-week-cell", "{label}" }
473 }
474 }
475
476 div { class: "adui-date-picker-body",
478 for cell in date_cells { {cell} }
479 }
480 }
481 }
482 }
483 }
484}
485
486#[derive(Props, Clone, PartialEq)]
488pub struct RangePickerProps {
489 #[props(optional)]
492 pub value: Option<DateRangeValue>,
493 #[props(optional)]
495 pub default_value: Option<DateRangeValue>,
496 #[props(optional)]
498 pub placeholder: Option<(String, String)>,
499 #[props(optional)]
501 pub format: Option<String>,
502 #[props(optional)]
504 pub disabled: Option<bool>,
505 #[props(optional)]
507 pub allow_clear: Option<bool>,
508 #[props(optional)]
510 pub class: Option<String>,
511 #[props(optional)]
513 pub style: Option<String>,
514 #[props(optional)]
516 pub on_change: Option<EventHandler<DateRangeValue>>,
517}
518
519#[component]
521pub fn RangePicker(props: RangePickerProps) -> Element {
522 let RangePickerProps {
523 value,
524 default_value,
525 placeholder,
526 format: _format,
527 disabled,
528 allow_clear,
529 class,
530 style,
531 on_change,
532 } = props;
533
534 let config = use_config();
535 let locale = config.locale;
536
537 let is_disabled = disabled.unwrap_or(false);
538 let allow_clear_flag = allow_clear.unwrap_or(false);
539
540 let initial_range = default_value.unwrap_or_else(DateRangeValue::empty);
541 let range_state: Signal<DateRangeValue> = use_signal(|| initial_range);
542 let range = value.unwrap_or_else(|| *range_state.read());
543
544 let start_text = range.start.map(|d| d.to_ymd_string()).unwrap_or_default();
545 let end_text = range.end.map(|d| d.to_ymd_string()).unwrap_or_default();
546
547 let default_placeholder = match locale {
548 Locale::ZhCN => ("开始日期".to_string(), "结束日期".to_string()),
549 Locale::EnUS => ("Start date".to_string(), "End date".to_string()),
550 };
551 let (start_ph, end_ph) = placeholder.unwrap_or(default_placeholder);
552
553 let controlled = value.is_some();
554
555 let open_state: Signal<bool> = use_signal(|| false);
556 let open_flag = *open_state.read();
557 let dropdown_layer = use_dropdown_layer(open_flag);
558 let current_z = *dropdown_layer.z_index.read();
559
560 let initial_year = range
563 .start
564 .or(range.end)
565 .map(|v| v.inner.year())
566 .unwrap_or(2024);
567 let initial_month = range
568 .start
569 .or(range.end)
570 .map(|v| v.inner.month() as u8)
571 .unwrap_or(1);
572
573 let view_year: Signal<i32> = use_signal(|| initial_year);
574 let view_month: Signal<u8> = use_signal(|| initial_month);
575
576 let year_now = *view_year.read();
577 let month_now = *view_month.read();
578 let days_in_month_now = days_in_month(year_now, month_now);
579 let first_weekday = weekday_index_monday(year_now, month_now, 1) as usize;
580 let total_cells = first_weekday + days_in_month_now as usize;
581 let padded_cells = total_cells.div_ceil(7) * 7;
582
583 let locale_for_header = locale;
584 let month_label = match locale_for_header {
585 Locale::ZhCN => format!("{year_now}年{month_now}月"),
586 Locale::EnUS => format!("{year_now}-{month_now:02}"),
587 };
588 let weekday_labels: [&str; 7] = match locale_for_header {
589 Locale::ZhCN => ["一", "二", "三", "四", "五", "六", "日"],
590 Locale::EnUS => ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"],
591 };
592
593 let mut control_classes = vec!["adui-date-picker adui-date-picker-range".to_string()];
594 if is_disabled {
595 control_classes.push("adui-date-picker-disabled".to_string());
596 }
597 if let Some(extra) = class.clone() {
598 control_classes.push(extra);
599 }
600 let control_class_attr = control_classes.join(" ");
601 let style_attr = style.unwrap_or_default();
602
603 let open_for_toggle = open_state;
604
605 let on_change_cb = on_change;
606 let range_for_click = range_state;
607
608 let close_handle = use_floating_close_handle(open_for_toggle);
610
611 let mut date_cells: Vec<Element> = Vec::new();
613 for index in 0..padded_cells {
614 let cell_day =
615 if index < first_weekday || index >= first_weekday + days_in_month_now as usize {
616 None
617 } else {
618 Some((index - first_weekday + 1) as u8)
619 };
620
621 let is_outside = cell_day.is_none();
622 let mut is_selected_start = false;
623 let mut is_selected_end = false;
624 let mut in_range = false;
625
626 if let Some(day) = cell_day {
627 if let Some(start) = range.start
628 && start.inner.year() == year_now
629 && start.inner.month() as u8 == month_now
630 && start.inner.day() == day
631 {
632 is_selected_start = true;
633 }
634 if let Some(end) = range.end
635 && end.inner.year() == year_now
636 && end.inner.month() as u8 == month_now
637 && end.inner.day() == day
638 {
639 is_selected_end = true;
640 }
641 if let (Some(start), Some(end)) = (range.start, range.end) {
642 let date = DateValue::from_ymd(year_now, month_now, day).unwrap();
643 if date.inner >= start.inner && date.inner <= end.inner {
644 in_range = true;
645 }
646 }
647 }
648
649 let mut cell_classes = vec!["adui-date-picker-cell".to_string()];
650 if is_outside {
651 cell_classes.push("adui-date-picker-cell-empty".to_string());
652 } else {
653 cell_classes.push("adui-date-picker-cell-date".to_string());
654 }
655 if in_range {
656 cell_classes.push("adui-date-picker-cell-in-range".to_string());
657 }
658 if is_selected_start {
659 cell_classes.push("adui-date-picker-cell-range-start".to_string());
660 }
661 if is_selected_end {
662 cell_classes.push("adui-date-picker-cell-range-end".to_string());
663 }
664 let cell_class_attr = cell_classes.join(" ");
665
666 let on_change_for_cell = on_change_cb;
667 let controlled_for_cell = controlled;
668 let close_for_cell = close_handle;
669
670 let cell_node = rsx! {
671 span {
672 class: "{cell_class_attr}",
673 onclick: move |_| {
674 if is_disabled {
675 return;
676 }
677 close_for_cell.mark_internal_click();
678 if let Some(day) = cell_day
679 && let Some(clicked) = DateValue::from_ymd(year_now, month_now, day)
680 {
681 let mut next = range;
682 match (next.start, next.end) {
683 (None, _) => {
684 next.start = Some(clicked);
685 next.end = None;
686 }
687 (Some(start), None) => {
688 if clicked.inner < start.inner {
689 next.start = Some(clicked);
690 next.end = Some(start);
691 } else {
692 next.end = Some(clicked);
693 }
694
695 close_for_cell.close();
696 }
697 (Some(_), Some(_)) => {
698 next.start = Some(clicked);
699 next.end = None;
700 }
701 }
702
703 if controlled_for_cell {
704 if let Some(cb) = on_change_for_cell {
705 cb.call(next);
706 }
707 } else {
708 let mut state = range_for_click;
709 state.set(next);
710 if let Some(cb) = on_change_for_cell {
711 cb.call(next);
712 }
713 }
714 }
715 },
716 match cell_day {
717 Some(day) => rsx!{ "{day}" },
718 None => rsx!{ "" },
719 }
720 }
721 };
722
723 date_cells.push(cell_node);
724 }
725
726 let has_any_value = range.start.is_some() || range.end.is_some();
727
728 rsx! {
729 div {
730 class: "adui-date-picker-root",
731 style: "position: relative; display: inline-block;",
732 div {
733 class: "{control_class_attr}",
734 style: "{style_attr}",
735 role: "group",
736 tabindex: (!is_disabled).then_some(0),
737 onclick: move |_| {
738 if is_disabled {
739 return;
740 }
741 close_handle.mark_internal_click();
742 let mut open_signal = open_for_toggle;
743 let next = !*open_signal.read();
744 open_signal.set(next);
745 },
746 onkeydown: move |evt: KeyboardEvent| {
747 if is_disabled {
748 return;
749 }
750 use dioxus::prelude::Key;
751 match evt.key() {
752 Key::Enter => {
753 evt.prevent_default();
754 let mut open_signal = open_for_toggle;
755 open_signal.set(true);
756 }
757 Key::Escape => {
758 close_handle.close();
759 }
760 _ => {}
761 }
762 },
763 input {
764 class: "adui-date-picker-input adui-date-picker-input-start",
765 readonly: true,
766 disabled: is_disabled,
767 value: "{start_text}",
768 placeholder: "{start_ph}",
769 }
770 span { class: "adui-date-picker-range-separator", " ~ " }
771 input {
772 class: "adui-date-picker-input adui-date-picker-input-end",
773 readonly: true,
774 disabled: is_disabled,
775 value: "{end_text}",
776 placeholder: "{end_ph}",
777 }
778 if allow_clear_flag && has_any_value && !is_disabled {
779 span {
780 class: "adui-date-picker-clear",
781 onclick: move |_| {
782 if controlled {
783 if let Some(cb) = on_change {
784 cb.call(DateRangeValue::empty());
785 }
786 } else {
787 let mut state = range_state;
788 state.set(DateRangeValue::empty());
789 if let Some(cb) = on_change {
790 cb.call(DateRangeValue::empty());
791 }
792 }
793 },
794 "×"
795 }
796 }
797 }
798
799 if open_flag {
800 div {
801 class: "adui-date-picker-dropdown",
802 style: "position: absolute; top: 100%; left: 0; min-width: 100%; z-index: {current_z};",
803 div { class: "adui-date-picker-header",
804 button {
805 class: "adui-date-picker-nav-btn adui-date-picker-prev-month",
806 onclick: move |_| {
807 close_handle.mark_internal_click();
808 let mut year = *view_year.read();
809 let mut month = *view_month.read();
810 if month == 1 {
811 month = 12;
812 year -= 1;
813 } else {
814 month -= 1;
815 }
816 let mut y = view_year;
817 let mut m = view_month;
818 y.set(year);
819 m.set(month);
820 },
821 "<"
822 }
823 span { class: "adui-date-picker-header-view", "{month_label}" }
824 button {
825 class: "adui-date-picker-nav-btn adui-date-picker-next-month",
826 onclick: move |_| {
827 close_handle.mark_internal_click();
828 let mut year = *view_year.read();
829 let mut month = *view_month.read();
830 if month == 12 {
831 month = 1;
832 year += 1;
833 } else {
834 month += 1;
835 }
836 let mut y = view_year;
837 let mut m = view_month;
838 y.set(year);
839 m.set(month);
840 },
841 ">"
842 }
843 }
844
845 div { class: "adui-date-picker-week-row",
846 for label in weekday_labels {
847 span { class: "adui-date-picker-week-cell", "{label}" }
848 }
849 }
850
851 div { class: "adui-date-picker-body",
852 for cell in date_cells { {cell} }
853 }
854 }
855 }
856 }
857 }
858}
859
860#[cfg(test)]
861mod tests {
862 use super::*;
863
864 #[test]
865 fn date_value_from_ymd_and_to_string_are_consistent() {
866 let value = DateValue::from_ymd(2024, 5, 17).expect("valid date");
867 assert_eq!(value.to_ymd_string(), "2024-05-17");
868 }
869
870 #[test]
871 fn date_value_from_ymd_valid_dates() {
872 assert!(DateValue::from_ymd(2024, 1, 1).is_some());
874 assert!(DateValue::from_ymd(2024, 12, 31).is_some());
875 assert!(DateValue::from_ymd(2000, 2, 29).is_some()); assert!(DateValue::from_ymd(2024, 2, 29).is_some()); }
878
879 #[test]
880 fn date_value_from_ymd_invalid_dates() {
881 assert!(DateValue::from_ymd(2024, 2, 30).is_none()); assert!(DateValue::from_ymd(2023, 2, 29).is_none()); assert!(DateValue::from_ymd(2024, 13, 1).is_none()); assert!(DateValue::from_ymd(2024, 0, 1).is_none()); assert!(DateValue::from_ymd(2024, 4, 31).is_none()); }
888
889 #[test]
890 fn date_value_to_ymd_string_formatting() {
891 let value1 = DateValue::from_ymd(2024, 1, 1).expect("valid date");
892 assert_eq!(value1.to_ymd_string(), "2024-01-01");
893
894 let value2 = DateValue::from_ymd(2024, 12, 31).expect("valid date");
895 assert_eq!(value2.to_ymd_string(), "2024-12-31");
896
897 let value3 = DateValue::from_ymd(2000, 2, 29).expect("valid date");
898 assert_eq!(value3.to_ymd_string(), "2000-02-29");
899
900 let value4 = DateValue::from_ymd(1999, 3, 15).expect("valid date");
901 assert_eq!(value4.to_ymd_string(), "1999-03-15");
902 }
903
904 #[test]
905 fn date_value_leap_year_handling() {
906 assert!(DateValue::from_ymd(2000, 2, 29).is_some()); assert!(DateValue::from_ymd(2004, 2, 29).is_some()); assert!(DateValue::from_ymd(1900, 2, 29).is_none()); assert!(DateValue::from_ymd(2024, 2, 29).is_some()); }
912
913 #[test]
914 fn days_in_month_regular_months() {
915 assert_eq!(days_in_month(2024, 1), 31); assert_eq!(days_in_month(2024, 3), 31); assert_eq!(days_in_month(2024, 4), 30); assert_eq!(days_in_month(2024, 5), 31); assert_eq!(days_in_month(2024, 6), 30); assert_eq!(days_in_month(2024, 7), 31); assert_eq!(days_in_month(2024, 8), 31); assert_eq!(days_in_month(2024, 9), 30); assert_eq!(days_in_month(2024, 10), 31); assert_eq!(days_in_month(2024, 11), 30); assert_eq!(days_in_month(2024, 12), 31); }
927
928 #[test]
929 fn days_in_month_february_leap_years() {
930 assert_eq!(days_in_month(2000, 2), 29); assert_eq!(days_in_month(2004, 2), 29); assert_eq!(days_in_month(2024, 2), 29); assert_eq!(days_in_month(2023, 2), 28); assert_eq!(days_in_month(1900, 2), 28); assert_eq!(days_in_month(2100, 2), 28); }
937
938 #[test]
939 fn weekday_index_monday_calculation() {
940 assert_eq!(weekday_index_monday(2024, 1, 1), 0);
943
944 assert_eq!(weekday_index_monday(2024, 1, 2), 1);
946
947 assert_eq!(weekday_index_monday(2024, 1, 7), 6);
949
950 assert_eq!(weekday_index_monday(2024, 5, 17), 4);
952
953 assert_eq!(weekday_index_monday(2000, 2, 29), 1);
955 }
956
957 #[test]
958 fn weekday_index_monday_consistency() {
959 let base = weekday_index_monday(2024, 1, 1);
961 assert_eq!(weekday_index_monday(2024, 1, 2), (base + 1) % 7);
962 assert_eq!(weekday_index_monday(2024, 1, 3), (base + 2) % 7);
963 assert_eq!(weekday_index_monday(2024, 1, 8), base); }
965
966 #[test]
967 fn date_range_value_empty() {
968 let range = DateRangeValue::empty();
969 assert_eq!(range.start, None);
970 assert_eq!(range.end, None);
971 }
972
973 #[test]
974 fn date_value_clone_and_copy() {
975 let value1 = DateValue::from_ymd(2024, 5, 17).expect("valid date");
976 let value2 = value1; assert_eq!(value1, value2);
978 assert_eq!(value1.to_ymd_string(), value2.to_ymd_string());
979 }
980
981 #[test]
982 fn date_value_debug() {
983 let value = DateValue::from_ymd(2024, 5, 17).expect("valid date");
984 let debug_str = format!("{:?}", value);
985 assert!(debug_str.contains("DateValue"));
986 }
987}