1use chrono::{Datelike, NaiveDate};
2
3#[derive(Debug, Clone, Copy)]
4pub struct MonthInfo {
5 pub month: u32,
6 pub name: &'static str,
7 pub short_name: &'static str,
8 pub days: u32,
9}
10
11impl MonthInfo {
12 pub fn from_month(month: u32) -> Self {
13 let (name, short_name, days) = match month {
14 1 => ("January", "Jan", 31),
15 2 => ("February", "Feb", 28),
16 3 => ("March", "Mar", 31),
17 4 => ("April", "Apr", 30),
18 5 => ("May", "May", 31),
19 6 => ("June", "Jun", 30),
20 7 => ("July", "Jul", 31),
21 8 => ("August", "Aug", 31),
22 9 => ("September", "Sep", 30),
23 10 => ("October", "Oct", 31),
24 11 => ("November", "Nov", 30),
25 12 => ("December", "Dec", 31),
26 _ => ("", "", 0),
27 };
28 MonthInfo {
29 month,
30 name,
31 short_name,
32 days,
33 }
34 }
35
36 pub fn from_date(date: NaiveDate) -> Self {
37 Self::from_month(date.month())
38 }
39
40 pub fn is_leap_year(year: i32) -> bool {
41 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
42 }
43
44 pub fn days_in_month(month: u32, year: i32) -> u32 {
45 if month == 2 && Self::is_leap_year(year) {
46 29
47 } else {
48 Self::from_month(month).days
49 }
50 }
51}
52
53const DAYS_IN_WEEK: i64 = 7;
54
55#[derive(Debug, Clone)]
56pub struct WeekLayout {
57 pub dates: Vec<NaiveDate>,
58 pub month_start_idx: Option<(usize, u32)>,
59 pub month_end_idx: Option<(usize, u32)>,
60 pub year_boundary_idx: Option<usize>,
61}
62
63impl WeekLayout {
64 pub fn new(start_date: NaiveDate) -> Self {
65 let dates: Vec<NaiveDate> = (0..DAYS_IN_WEEK)
66 .map(|day_offset| {
67 start_date
68 .checked_add_signed(chrono::Duration::days(day_offset))
69 .unwrap()
70 })
71 .collect();
72
73 let month_start_idx = dates
74 .iter()
75 .enumerate()
76 .find(|(_, date)| date.day() == 1)
77 .map(|(idx, date)| (idx, date.month()));
78
79 let month_end_idx = Self::find_month_end(&dates);
80 let year_boundary_idx = Self::find_year_boundary(&dates);
81
82 WeekLayout {
83 dates,
84 month_start_idx,
85 month_end_idx,
86 year_boundary_idx,
87 }
88 }
89
90 fn find_month_end(dates: &[NaiveDate]) -> Option<(usize, u32)> {
91 for (idx, &date) in dates.iter().enumerate() {
92 if idx < dates.len() - 1 {
93 let next_date = dates[idx + 1];
94 if date.month() != next_date.month() || date.year() != next_date.year() {
95 return Some((idx, date.month()));
96 }
97 }
98 }
99 None
100 }
101
102 fn find_year_boundary(dates: &[NaiveDate]) -> Option<usize> {
103 for (idx, &date) in dates.iter().enumerate() {
104 if idx > 0 {
105 let prev_date = dates[idx - 1];
106 if date.year() != prev_date.year() {
107 return Some(idx);
108 }
109 }
110 }
111 None
112 }
113
114 pub fn get_date(&self, idx: usize) -> Option<NaiveDate> {
115 self.dates.get(idx).copied()
116 }
117
118 pub fn get_first_date(&self) -> NaiveDate {
119 self.dates[0]
120 }
121
122 pub fn get_last_date(&self) -> NaiveDate {
123 self.dates[self.dates.len() - 1]
124 }
125
126 pub fn contains_month_start(&self) -> bool {
127 self.month_start_idx.is_some()
128 }
129
130 pub fn contains_month_end(&self) -> bool {
131 self.month_end_idx.is_some()
132 }
133
134 pub fn is_in_current_month(&self, idx: usize, year: i32, current_month: Option<u32>) -> bool {
135 if let Some(date) = self.get_date(idx) {
136 date.year() == year && Some(date.month()) == current_month
137 } else {
138 false
139 }
140 }
141
142 pub fn was_prev_in_month(&self, idx: usize, year: i32, current_month: Option<u32>) -> bool {
143 if idx > 0 {
144 if let Some(prev_date) = self.get_date(idx - 1) {
145 return prev_date.year() == year && Some(prev_date.month()) == current_month;
146 }
147 }
148 false
149 }
150
151 pub fn will_next_be_in_month(&self, idx: usize, year: i32, current_month: Option<u32>) -> bool {
152 if idx < 6 {
153 if let Some(next_date) = self.get_date(idx + 1) {
154 return next_date.year() == year && Some(next_date.month()) == current_month;
155 }
156 }
157 false
158 }
159
160 pub fn count_days_in_month(&self, month: u32) -> usize {
161 self.dates.iter().filter(|d| d.month() == month).count()
162 }
163}
164
165#[derive(Debug, Clone, Copy)]
166pub struct SpacingConfig {
167 pub idx: usize,
168 pub in_month: bool,
169 pub prev_in_month: bool,
170 pub next_in_month: bool,
171 pub first_out_of_month: bool,
172}
173
174impl SpacingConfig {
175 pub fn new(
176 idx: usize,
177 in_month: bool,
178 prev_in_month: bool,
179 next_in_month: bool,
180 first_out_of_month: bool,
181 ) -> Self {
182 Self {
183 idx,
184 in_month,
185 prev_in_month,
186 next_in_month,
187 first_out_of_month,
188 }
189 }
190
191 pub fn is_last_in_week(&self) -> bool {
192 self.idx >= 6
193 }
194
195 pub fn is_first_in_week(&self) -> bool {
196 self.idx == 0
197 }
198}
199
200#[derive(Debug, Clone, Copy)]
201pub struct SpacingCalculator;
202
203impl SpacingCalculator {
204 pub fn date_spacing(config: SpacingConfig) -> &'static str {
205 match (
206 config.is_last_in_week(),
207 config.in_month,
208 config.prev_in_month,
209 config.next_in_month,
210 config.first_out_of_month,
211 ) {
212 (true, true, _, _, _) => " ",
213 (true, false, _, _, _) => "",
214 (false, true, false, _, _) if config.is_first_in_week() => " ",
215 (false, true, _, _, _) => " ",
216 (false, false, _, true, _) => " ",
217 (false, false, _, false, true) => " ",
218 _ => " ",
219 }
220 }
221
222 pub fn date_spacing_legacy(
223 idx: usize,
224 in_month: bool,
225 prev_in_month: bool,
226 next_in_month: bool,
227 first_out_of_month: bool,
228 ) -> &'static str {
229 let config = SpacingConfig::new(
230 idx,
231 in_month,
232 prev_in_month,
233 next_in_month,
234 first_out_of_month,
235 );
236 Self::date_spacing(config)
237 }
238
239 pub fn border_width_before(bar_idx: usize) -> usize {
240 if bar_idx == 0 {
241 0
242 } else if bar_idx == 1 {
243 5
244 } else {
245 6 + (bar_idx - 2) * 5 + 4
246 }
247 }
248
249 pub fn border_width_after(bar_idx: usize) -> usize {
250 (7 - bar_idx) * 5
251 }
252}
253
254#[derive(Debug, Clone)]
255pub struct BorderState {
256 pub before_width: usize,
257 pub after_width: usize,
258 pub has_boundary: bool,
259 pub boundary_position: Option<usize>,
260}
261
262impl BorderState {
263 pub fn new(boundary_position: Option<usize>) -> Self {
264 let (before_width, after_width, has_boundary) = if let Some(pos) = boundary_position {
265 (
266 SpacingCalculator::border_width_before(pos),
267 SpacingCalculator::border_width_after(pos),
268 true,
269 )
270 } else {
271 (0, 0, false)
272 };
273
274 Self {
275 before_width,
276 after_width,
277 has_boundary,
278 boundary_position,
279 }
280 }
281
282 pub fn total_width(&self) -> usize {
283 self.before_width + self.after_width
284 }
285}