Jason Lee 2024-08-08 23:29:54 +08:00 committed by GitHub
parent 2c531fa3ef
commit 5d55c5c31e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 413 additions and 118 deletions

View file

@ -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.

View file

@ -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<DatePicker>,
date_picker_large: View<DatePicker>,
date_picker_value: Option<String>,
date_range_picker: View<DatePicker>,
}
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())
}
}

View file

@ -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<NaiveDate>),
Range(Option<NaiveDate>, Option<NaiveDate>),
}
impl From<NaiveDate> 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<NaiveDate> {
match self {
Self::Single(Some(date)) => Some(*date),
Self::Range(Some(start), _) => Some(*start),
_ => None,
}
}
pub fn end(&self) -> Option<NaiveDate> {
match self {
Self::Range(_, Some(end)) => Some(*end),
_ => None,
}
}
/// Return formatted date string.
pub fn format(&self, format: &str) -> Option<SharedString> {
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<NaiveDate>,
mode: Mode,
date: Date,
view_mode: ViewMode,
current_year: i32,
current_month: u8,
years: Vec<Vec<i32>>,
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<NaiveDate>, cx: &mut ViewContext<Self>) {
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<Date>, cx: &mut ViewContext<Self>) {
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<NaiveDate> {
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>) {
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<Vec<NaiveDate>> {
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<ElementId>,
label: impl Into<SharedString>,
active: bool,
secondary_active: bool,
muted: bool,
cx: &mut ViewContext<Self>,
) -> 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<Self>,
) -> 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>) {
self.mode = mode;
fn set_view_mode(&mut self, mode: ViewMode, cx: &mut ViewContext<Self>) {
self.view_mode = mode;
cx.notify();
}
@ -259,7 +449,8 @@ impl Calendar {
fn render_header(&self, cx: &mut ViewContext<Self>) -> 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<Self>) -> 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))
}),
)
}
}

View file

@ -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<NaiveDate>),
Change(Date),
}
pub struct DatePicker {
id: ElementId,
focus_handle: FocusHandle,
date: Option<NaiveDate>,
date: Date,
cleanable: bool,
placeholder: Option<SharedString>,
open: bool,
@ -35,6 +34,7 @@ pub struct DatePicker {
width: Length,
date_format: SharedString,
calendar: View<Calendar>,
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<NaiveDate> {
pub fn date(&self) -> Date {
self.date
}
/// Set the date of the date picker.
pub fn set_date(&mut self, date: Option<NaiveDate>, cx: &mut ViewContext<Self>) {
self.update_date(date, cx);
pub fn set_date(&mut self, date: impl Into<Date>, cx: &mut ViewContext<Self>) {
self.update_date(date.into(), cx);
}
fn update_date(&mut self, date: Option<NaiveDate>, cx: &mut ViewContext<Self>) {
fn update_date(&mut self, date: Date, cx: &mut ViewContext<Self>) {
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>) {
self.update_date(None, cx);
self.update_date(Date::Single(None), cx);
}
fn toggle_calendar(&mut self, _: &gpui::ClickEvent, cx: &mut ViewContext<Self>) {
@ -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,

View file

@ -28,19 +28,47 @@ impl NaiveDateExt for chrono::NaiveDate {
}
}
pub(crate) fn days_in_month(year: i32, month: u32) -> Vec<Vec<NaiveDate>> {
pub(crate) fn days_in_month(year: i32, month: u32, number_of_months: u32) -> Vec<Vec<NaiveDate>> {
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",
],
);
}
}