From 5d55c5c31e395d384ac416d2d4c6fbc906f34570 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Thu, 8 Aug 2024 23:29:54 +0800 Subject: [PATCH] Add DateRangePicker (#123) https://github.com/user-attachments/assets/cc934401-78c5-42d6-afc8-a365fea2129b --- README.md | 2 +- crates/story/src/calendar_story.rs | 24 +- crates/ui/src/time/calendar.rs | 406 ++++++++++++++++++++++------- crates/ui/src/time/date_picker.rs | 42 ++- crates/ui/src/time/utils.rs | 57 +++- 5 files changed, 413 insertions(+), 118 deletions(-) diff --git a/README.md b/README.md index 8f012932..4bc8a7bc 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ A UI components for building desktop application by using [GPUI](https://gpui.rs - [x] DatePicker - [x] Calendar - [ ] TimePicker - - [ ] DateRangePicker + - [x] DateRangePicker - [ ] ColorPicker - [x] List - [x] A complex List example. diff --git a/crates/story/src/calendar_story.rs b/crates/story/src/calendar_story.rs index a07d15a2..95aaf2a6 100644 --- a/crates/story/src/calendar_story.rs +++ b/crates/story/src/calendar_story.rs @@ -1,3 +1,4 @@ +use chrono::Days; use gpui::{ px, IntoElement, ParentElement as _, Render, Styled as _, View, ViewContext, VisualContext as _, WindowContext, @@ -12,6 +13,7 @@ pub struct CalendarStory { date_picker_small: View, date_picker_large: View, date_picker_value: Option, + date_range_picker: View, } impl CalendarStory { @@ -25,7 +27,7 @@ impl CalendarStory { let mut picker = DatePicker::new("date_picker_medium", cx) .cleanable(true) .width(px(220.)); - picker.set_date(Some(now), cx); + picker.set_date(now, cx); picker }); let date_picker_large = cx.new_view(|cx| { @@ -38,13 +40,27 @@ impl CalendarStory { let mut picker = DatePicker::new("date_picker_small", cx) .small() .width(px(180.)); - picker.set_date(Some(now), cx); + picker.set_date(now, cx); + picker + }); + let date_range_picker = cx.new_view(|cx| { + let mut picker = DatePicker::new("date_range_picker", cx) + .width(px(300.)) + .number_of_months(2) + .cleanable(true); + picker.set_date((now, now.checked_add_days(Days::new(4)).unwrap()), cx); picker }); cx.subscribe(&date_picker, |this, _, ev, _| match ev { DatePickerEvent::Change(date) => { - this.date_picker_value = date.map(|d| d.to_string()); + this.date_picker_value = date.format("%Y-%m-%d").map(|s| s.to_string()); + } + }) + .detach(); + cx.subscribe(&date_range_picker, |this, _, ev, _| match ev { + DatePickerEvent::Change(date) => { + this.date_picker_value = date.format("%Y-%m-%d").map(|s| s.to_string()); } }) .detach(); @@ -53,6 +69,7 @@ impl CalendarStory { date_picker, date_picker_large, date_picker_small, + date_range_picker, date_picker_value: None, } } @@ -65,6 +82,7 @@ impl Render for CalendarStory { .child(self.date_picker.clone()) .child(self.date_picker_small.clone()) .child(self.date_picker_large.clone()) + .child(self.date_range_picker.clone()) .child(format!("Date picker value: {:?}", self.date_picker_value).into_element()) } } diff --git a/crates/ui/src/time/calendar.rs b/crates/ui/src/time/calendar.rs index 5be0e132..dfacecf5 100644 --- a/crates/ui/src/time/calendar.rs +++ b/crates/ui/src/time/calendar.rs @@ -19,24 +19,133 @@ use super::utils::days_in_month; pub enum CalendarEvent { /// The user selected a date. - Selected(NaiveDate), + Selected(Date), +} + +/// The date of the calendar. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Date { + Single(Option), + Range(Option, Option), +} + +impl From for Date { + fn from(date: NaiveDate) -> Self { + Self::Single(Some(date)) + } +} + +impl From<(NaiveDate, NaiveDate)> for Date { + fn from((start, end): (NaiveDate, NaiveDate)) -> Self { + Self::Range(Some(start), Some(end)) + } +} + +impl Date { + fn is_active(&self, v: &NaiveDate) -> bool { + let v = *v; + match self { + Self::Single(d) => Some(v) == *d, + Self::Range(start, end) => Some(v) == *start || Some(v) == *end, + } + } + + fn is_single(&self) -> bool { + matches!(self, Self::Single(_)) + } + + fn is_in_range(&self, v: &NaiveDate) -> bool { + let v = *v; + match self { + Self::Range(start, end) => { + if let Some(start) = start { + if let Some(end) = end { + v >= *start && v <= *end + } else { + false + } + } else { + false + } + } + _ => false, + } + } + + pub fn is_some(&self) -> bool { + match self { + Self::Single(Some(_)) | Self::Range(Some(_), _) => true, + _ => false, + } + } + + /// Check if the date is complete. + pub fn is_complete(&self) -> bool { + match self { + Self::Range(Some(_), Some(_)) => true, + Self::Single(Some(_)) => true, + _ => false, + } + } + + pub fn start(&self) -> Option { + match self { + Self::Single(Some(date)) => Some(*date), + Self::Range(Some(start), _) => Some(*start), + _ => None, + } + } + + pub fn end(&self) -> Option { + match self { + Self::Range(_, Some(end)) => Some(*end), + _ => None, + } + } + + /// Return formatted date string. + pub fn format(&self, format: &str) -> Option { + match self { + Self::Single(Some(date)) => Some(date.format(format).to_string().into()), + Self::Range(Some(start), Some(end)) => { + Some(format!("{} - {}", start.format(format), end.format(format)).into()) + } + _ => None, + } + } } #[derive(Debug, PartialEq, Eq)] -enum Mode { +enum ViewMode { Day, Month, Year, } +impl ViewMode { + fn is_day(&self) -> bool { + matches!(self, Self::Day) + } + + fn is_month(&self) -> bool { + matches!(self, Self::Month) + } + + fn is_year(&self) -> bool { + matches!(self, Self::Year) + } +} + pub struct Calendar { focus_handle: FocusHandle, - date: Option, - mode: Mode, + date: Date, + view_mode: ViewMode, current_year: i32, current_month: u8, years: Vec>, year_page: i32, + /// Number of the months view to show. + number_of_months: usize, } impl Calendar { @@ -44,31 +153,54 @@ impl Calendar { let today = Local::now().naive_local().date(); Self { focus_handle: cx.focus_handle(), - mode: Mode::Day, - date: None, + view_mode: ViewMode::Day, + date: Date::Single(None), current_month: today.month() as u8, current_year: today.year(), years: vec![], year_page: 0, + number_of_months: 1, } .year_range((today.year() - 50, today.year() + 50)) } /// Set the date of the calendar. - pub fn set_date(&mut self, date: Option, cx: &mut ViewContext) { - self.date = date; - if let Some(date) = date { - self.current_month = date.month() as u8; - self.current_year = date.year(); + /// + /// When you set a range date, the mode will be automatically set to `Mode::Range`. + pub fn set_date(&mut self, date: impl Into, cx: &mut ViewContext) { + self.date = date.into(); + + match self.date { + Date::Single(Some(date)) => { + self.current_month = date.month() as u8; + self.current_year = date.year(); + } + Date::Range(Some(start), _) => { + self.current_month = start.month() as u8; + self.current_year = start.year(); + } + _ => {} } + cx.notify() } /// Get the date of the calendar. - pub fn date(&self) -> Option { + pub fn date(&self) -> Date { self.date } + /// Set number of months to show, default is 1. + pub fn number_of_months(mut self, number_of_months: usize) -> Self { + self.number_of_months = number_of_months; + self + } + + pub fn set_number_of_months(&mut self, number_of_months: usize, cx: &mut ViewContext) { + self.number_of_months = number_of_months; + cx.notify(); + } + /// Set the year range of the calendar, default is 50 years before and after the current year. /// /// Each year page contains 20 years, so the range will be divided into chunks of 20 years is better. @@ -86,9 +218,29 @@ impl Calendar { self } + /// Get year and month by offset month. + fn offset_year_month(&self, offset_month: usize) -> (i32, u32) { + let mut month = self.current_month as i32 + offset_month as i32; + let mut year = self.current_year; + while month < 1 { + month += 12; + year -= 1; + } + while month > 12 { + month -= 12; + year += 1; + } + + (year, month as u32) + } + /// Returns the days of the month in a 2D vector to render on calendar. fn days(&self) -> Vec> { - days_in_month(self.current_year, self.current_month as u32) + days_in_month( + self.current_year, + self.current_month as u32, + self.number_of_months as u32, + ) } fn has_prev_year_page(&self) -> bool { @@ -145,8 +297,9 @@ impl Calendar { cx.notify() } - fn month_name(&self) -> SharedString { - match self.current_month { + fn month_name(&self, offset_month: usize) -> SharedString { + let (_, month) = self.offset_year_month(offset_month); + match month { 1 => t!("Calendar.month.January"), 2 => t!("Calendar.month.February"), 3 => t!("Calendar.month.March"), @@ -184,6 +337,7 @@ impl Calendar { id: impl Into, label: impl Into, active: bool, + secondary_active: bool, muted: bool, cx: &mut ViewContext, ) -> impl IntoElement + Styled + StatefulInteractiveElement { @@ -197,6 +351,14 @@ impl Calendar { .when(muted, |this| { this.text_color(cx.theme().muted_foreground.opacity(0.3)) }) + .when(secondary_active, |this| { + this.bg(if muted { + cx.theme().accent.opacity(0.3) + } else { + cx.theme().accent + }) + .text_color(cx.theme().accent_foreground) + }) .when(!active, |this| { this.hover(|this| { this.bg(cx.theme().accent) @@ -210,30 +372,58 @@ impl Calendar { .child(label.into()) } - fn render_item( + fn render_day( &self, ix: usize, d: &NaiveDate, + offset_month: usize, cx: &mut ViewContext, ) -> impl IntoElement { + let (_, month) = self.offset_year_month(offset_month); let day = d.day(); - let is_current_month = d.month() == self.current_month as u32; - let is_active = match self.date { - Some(date) => date == *d, - None => false, - }; + let is_current_month = d.month() == month; + let is_active = self.date.is_active(d); + let is_in_range = self.date.is_in_range(d); let date = *d; - self.item_button(ix, day.to_string(), is_active, !is_current_month, cx) - .on_click(cx.listener(move |view, _: &ClickEvent, cx| { - view.set_date(Some(date), cx); - cx.emit(CalendarEvent::Selected(date)); - })) + self.item_button( + ix, + day.to_string(), + is_active, + is_in_range, + !is_current_month, + cx, + ) + .on_click(cx.listener(move |view, _: &ClickEvent, cx| { + if view.date.is_single() { + view.set_date(date, cx); + cx.emit(CalendarEvent::Selected(view.date())); + } else { + let start = view.date.start(); + let end = view.date.end(); + + if start.is_none() && end.is_none() { + view.set_date(Date::Range(Some(date), None), cx); + } else if start.is_some() && end.is_none() { + if date < start.unwrap() { + view.set_date(Date::Range(Some(date), None), cx); + } else { + view.set_date(Date::Range(Some(start.unwrap()), Some(date)), cx); + } + } else { + view.set_date(Date::Range(Some(date), None), cx); + } + + if view.date.is_complete() { + cx.emit(CalendarEvent::Selected(view.date())); + } + } + })) } - fn set_mode(&mut self, mode: Mode, cx: &mut ViewContext) { - self.mode = mode; + fn set_view_mode(&mut self, mode: ViewMode, cx: &mut ViewContext) { + self.view_mode = mode; cx.notify(); } @@ -259,7 +449,8 @@ impl Calendar { fn render_header(&self, cx: &mut ViewContext) -> impl IntoElement { let current_year = self.current_year; - let disabled = self.mode == Mode::Month; + let disabled = self.view_mode.is_month(); + let multiple_months = self.number_of_months > 1; h_flex() .gap_0p5() @@ -270,58 +461,71 @@ impl Calendar { .icon(IconName::ArrowLeft) .ghost() .disabled(disabled) - .when(self.mode == Mode::Day, |this| { + .when(self.view_mode.is_day(), |this| { this.on_click(cx.listener(Self::prev_month)) }) - .when(self.mode == Mode::Year, |this| { + .when(self.view_mode.is_year(), |this| { this.when(!self.has_prev_year_page(), |this| this.disabled(true)) .on_click(cx.listener(Self::prev_year_page)) }), ) - .child( - h_flex() - .justify_center() - .gap_3() - .child( - Button::new("month", cx) - .ghost() - .label(self.month_name()) - .selected(self.mode == Mode::Month) - .compact() - .on_click(cx.listener(|view, _, cx| { - if view.mode == Mode::Month { - view.set_mode(Mode::Day, cx); - } else { - view.set_mode(Mode::Month, cx); - } - cx.notify(); - })), - ) - .child( - Button::new("year", cx) - .ghost() - .label(current_year.to_string()) - .compact() - .selected(self.mode == Mode::Year) - .on_click(cx.listener(|view, _, cx| { - if view.mode == Mode::Year { - view.set_mode(Mode::Day, cx); - } else { - view.set_mode(Mode::Year, cx); - } - cx.notify(); - })), - ), - ) + .when(!multiple_months, |this| { + this.child( + h_flex() + .justify_center() + .gap_3() + .child( + Button::new("month", cx) + .ghost() + .label(self.month_name(0)) + .selected(self.view_mode.is_month()) + .compact() + .on_click(cx.listener(|view, _, cx| { + if view.view_mode.is_month() { + view.set_view_mode(ViewMode::Day, cx); + } else { + view.set_view_mode(ViewMode::Month, cx); + } + cx.notify(); + })), + ) + .child( + Button::new("year", cx) + .ghost() + .label(current_year.to_string()) + .compact() + .selected(self.view_mode.is_year()) + .on_click(cx.listener(|view, _, cx| { + if view.view_mode.is_year() { + view.set_view_mode(ViewMode::Day, cx); + } else { + view.set_view_mode(ViewMode::Year, cx); + } + cx.notify(); + })), + ), + ) + }) + .when(multiple_months, |this| { + this.child(h_flex().flex_1().justify_around().children( + (0..self.number_of_months).into_iter().map(|n| { + h_flex() + .justify_center() + .gap_3() + .child(self.month_name(n)) + .child(current_year.to_string()) + }), + )) + }) .child( Button::new("next", cx) .icon(IconName::ArrowRight) .ghost() .disabled(disabled) - .when(self.mode == Mode::Day, |this| { + .when(self.view_mode.is_day(), |this| { this.on_click(cx.listener(Self::next_month)) }) - .when(self.mode == Mode::Year, |this| { + .when(self.view_mode.is_year(), |this| { this.when(!self.has_next_year_page(), |this| this.disabled(true)) .on_click(cx.listener(Self::next_year_page)) }), @@ -339,20 +543,28 @@ impl Calendar { t!("Calendar.week.6"), ]; - v_flex() - .child( - h_flex() - .gap_0p5() - .justify_between() - .children(weeks.iter().map(|week| self.render_week(week.clone(), cx))), - ) - .children(self.days().iter().map(|week| { - h_flex().gap_0p5().justify_between().children( - week.iter() - .enumerate() - .map(|(ix, d)| self.render_item(ix, d, cx)), - ) - })) + h_flex().gap_4().justify_between().text_sm().children( + self.days() + .chunks(5) + .into_iter() + .enumerate() + .map(|(offset_month, days)| { + v_flex() + .gap_0p5() + .child( + h_flex().gap_0p5().justify_between().children( + weeks.iter().map(|week| self.render_week(week.clone(), cx)), + ), + ) + .children(days.iter().map(|week| { + h_flex().gap_0p5().justify_between().children( + week.iter() + .enumerate() + .map(|(ix, d)| self.render_day(ix, d, offset_month, cx)), + ) + })) + }), + ) } fn render_months(&mut self, cx: &mut ViewContext) -> impl IntoElement { @@ -371,11 +583,11 @@ impl Calendar { .map(|(ix, month)| { let active = (ix + 1) as u8 == self.current_month; - self.item_button(ix, month.to_string(), active, false, cx) + self.item_button(ix, month.to_string(), active, false, false, cx) .w(relative(0.3)) .on_click(cx.listener(move |view, _, cx| { view.current_month = (ix + 1) as u8; - view.set_mode(Mode::Day, cx); + view.set_view_mode(ViewMode::Day, cx); cx.notify(); })) }) @@ -401,11 +613,11 @@ impl Calendar { let year = *year; let active = year == self.current_year; - self.item_button(ix, year.to_string(), active, false, cx) + self.item_button(ix, year.to_string(), active, false, false, cx) .w(relative(0.2)) .on_click(cx.listener(move |view, _, cx| { view.current_year = year; - view.set_mode(Mode::Day, cx); + view.set_view_mode(ViewMode::Day, cx); cx.notify(); })) }) @@ -421,16 +633,18 @@ impl Render for Calendar { v_flex() .track_focus(&self.focus_handle) .gap_0p5() - .text_sm() .child(self.render_header(cx)) - .when(self.mode == Mode::Day, |this| { - this.child(self.render_days(cx)) - }) - .when(self.mode == Mode::Month, |this| { - this.child(self.render_months(cx)) - }) - .when(self.mode == Mode::Year, |this| { - this.child(self.render_years(cx)) - }) + .child( + v_flex() + .when(self.view_mode.is_day(), |this| { + this.child(self.render_days(cx)) + }) + .when(self.view_mode.is_month(), |this| { + this.child(self.render_months(cx)) + }) + .when(self.view_mode.is_year(), |this| { + this.child(self.render_years(cx)) + }), + ) } } diff --git a/crates/ui/src/time/date_picker.rs b/crates/ui/src/time/date_picker.rs index 4ffca55c..16631633 100644 --- a/crates/ui/src/time/date_picker.rs +++ b/crates/ui/src/time/date_picker.rs @@ -1,4 +1,3 @@ -use chrono::NaiveDate; use gpui::{ deferred, div, prelude::FluentBuilder as _, px, AppContext, ElementId, EventEmitter, FocusHandle, FocusableView, InteractiveElement as _, KeyBinding, Length, MouseButton, @@ -12,7 +11,7 @@ use crate::{ theme::ActiveTheme as _, Clickable, Icon, IconName, Sizable, Size, StyledExt as _, }; -use super::calendar::{Calendar, CalendarEvent}; +use super::calendar::{Calendar, CalendarEvent, Date}; pub fn init(cx: &mut AppContext) { let context = Some("DatePicker"); @@ -21,13 +20,13 @@ pub fn init(cx: &mut AppContext) { #[derive(Clone)] pub enum DatePickerEvent { - Change(Option), + Change(Date), } pub struct DatePicker { id: ElementId, focus_handle: FocusHandle, - date: Option, + date: Date, cleanable: bool, placeholder: Option, open: bool, @@ -35,6 +34,7 @@ pub struct DatePicker { width: Length, date_format: SharedString, calendar: View, + number_of_months: usize, } impl DatePicker { @@ -43,7 +43,7 @@ impl DatePicker { cx.subscribe(&calendar, |this, _, ev: &CalendarEvent, cx| match ev { CalendarEvent::Selected(date) => { - this.update_date(Some(*date), cx); + this.update_date(*date, cx); } }) .detach(); @@ -51,13 +51,14 @@ impl DatePicker { Self { id: id.into(), focus_handle: cx.focus_handle(), - date: None, + date: Date::Single(None), calendar, open: false, size: Size::default(), width: Length::Auto, date_format: "%Y/%m/%d".into(), cleanable: false, + number_of_months: 1, placeholder: None, } } @@ -86,17 +87,23 @@ impl DatePicker { self } + /// Set the number of months calendar view to display, default is 1. + pub fn number_of_months(mut self, number_of_months: usize) -> Self { + self.number_of_months = number_of_months; + self + } + /// Get the date of the date picker. - pub fn date(&self) -> Option { + pub fn date(&self) -> Date { self.date } /// Set the date of the date picker. - pub fn set_date(&mut self, date: Option, cx: &mut ViewContext) { - self.update_date(date, cx); + pub fn set_date(&mut self, date: impl Into, cx: &mut ViewContext) { + self.update_date(date.into(), cx); } - fn update_date(&mut self, date: Option, cx: &mut ViewContext) { + fn update_date(&mut self, date: Date, cx: &mut ViewContext) { self.date = date; self.calendar.update(cx, |view, cx| { view.set_date(date, cx); @@ -112,7 +119,7 @@ impl DatePicker { } fn clean(&mut self, _: &gpui::ClickEvent, cx: &mut ViewContext) { - self.update_date(None, cx); + self.update_date(Date::Single(None), cx); } fn toggle_calendar(&mut self, _: &gpui::ClickEvent, cx: &mut ViewContext) { @@ -144,8 +151,15 @@ impl Render for DatePicker { .unwrap_or_else(|| t!("DatePicker.placeholder").into()); let display_title = self .date - .map(|date| date.format(&self.date_format).to_string()) - .unwrap_or(placeholder.to_string()); + .format(&self.date_format) + .unwrap_or(placeholder.clone()); + + self.calendar.update(cx, |view, cx| { + view.set_number_of_months(self.number_of_months, cx); + }); + + let popover_width = + 285.0 * self.number_of_months as f32 + (self.number_of_months - 1) as f32 * 16.0; div() .id(self.id.clone()) @@ -208,7 +222,7 @@ impl Render for DatePicker { .overflow_hidden() .rounded_lg() .p_3() - .w(px(300.)) + .w(px(popover_width)) .elevation_2(cx) .on_mouse_up_out( MouseButton::Left, diff --git a/crates/ui/src/time/utils.rs b/crates/ui/src/time/utils.rs index d32c5313..e991c8a9 100644 --- a/crates/ui/src/time/utils.rs +++ b/crates/ui/src/time/utils.rs @@ -28,19 +28,47 @@ impl NaiveDateExt for chrono::NaiveDate { } } -pub(crate) fn days_in_month(year: i32, month: u32) -> Vec> { +pub(crate) fn days_in_month(year: i32, month: u32, number_of_months: u32) -> Vec> { + let mut year = year; + let mut month = month; + if month > 12 { + year += 1; + month = 1; + } + if month < 1 { + year -= 1; + month = 12; + } + let date = NaiveDate::from_ymd_opt(year, month, 1).unwrap(); let num_days = date.days_in_month(); let start_weekday = date.weekday().num_days_from_sunday(); + let mut total_groups = number_of_months * 5; + if total_groups == 0 { + total_groups = 5; + } + // Get the days in the month, 2023-02 will returns // "29|30|31| 1| 2| 3| 4", // " 5| 6| 7| 8| 9|10|11", // "12|13|14|15|16|17|18", // "19|20|21|22|23|24|25", // "26|27|28| 1| 2| 3| 4", + // + // If the number_of_months is 2, then it will return + // "29|30|31| 1| 2| 3| 4", + // " 5| 6| 7| 8| 9|10|11", + // "12|13|14|15|16|17|18", + // "19|20|21|22|23|24|25", + // "26|27|28| 1| 2| 3| 4", + // " 5| 6| 7| 8| 9|10|11", + // "12|13|14|15|16|17|18", + // "19|20|21|22|23|24|25", + // "26|27|28| 1| 2| 3| 4", + // " 5| 6| 7| 8| 9|10|11", let mut days = vec![]; - for n in 0..5 { + for n in 0..total_groups as i32 { let mut week_days = vec![]; for weekday in 0..7 { let (mut y, mut m) = (year, month); @@ -103,8 +131,8 @@ mod tests { #[test] fn test_days() { #[track_caller] - fn assert_case(date: NaiveDate, expected: Vec<&str>) { - let out = days_in_month(date.year(), date.month()) + fn assert_case(date: NaiveDate, number_of_months: u32, expected: Vec<&str>) { + let out = days_in_month(date.year(), date.month(), number_of_months) .iter() .map(|week| { week.iter() @@ -127,6 +155,7 @@ mod tests { assert_case( NaiveDate::from_ymd_opt(2024, 8, 1).unwrap(), + 1, vec![ "7-28|7-29|7-30|7-31| 1| 2| 3", " 4| 5| 6| 7| 8| 9|10", @@ -137,6 +166,7 @@ mod tests { ); assert_case( NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(), + 1, vec![ "2024-12-29|2024-12-30|2024-12-31| 1| 2| 3| 4", " 5| 6| 7| 8| 9|10|11", @@ -148,6 +178,7 @@ mod tests { assert_case( NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(), + 1, vec![ "1-28|1-29|1-30|1-31| 1| 2| 3", " 4| 5| 6| 7| 8| 9|10", @@ -158,6 +189,7 @@ mod tests { ); assert_case( NaiveDate::from_ymd_opt(2023, 2, 20).unwrap(), + 1, vec![ "1-29|1-30|1-31| 1| 2| 3| 4", " 5| 6| 7| 8| 9|10|11", @@ -166,5 +198,22 @@ mod tests { "26|27|28|3-1|3-2|3-3|3-4", ], ); + + assert_case( + NaiveDate::from_ymd_opt(2023, 2, 20).unwrap(), + 2, + vec![ + "1-29|1-30|1-31| 1| 2| 3| 4", + " 5| 6| 7| 8| 9|10|11", + "12|13|14|15|16|17|18", + "19|20|21|22|23|24|25", + "26|27|28|3-1|3-2|3-3|3-4", + " 5| 6| 7| 8| 9|10|11", + "12|13|14|15|16|17|18", + "19|20|21|22|23|24|25", + "26|27|28|4-1|4-2|4-3|4-4", + " 5| 6| 7| 8| 9|10|11", + ], + ); } }