compact_calendar_cli/
formatting.rs

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}