1use std::fmt::Write as _;
7
8use crate::calendar::MonthGrid;
9use crate::persian::{
10 self, MONTH_NAMES, MONTH_NAMES_ROMANIZED, WEEKDAY_NAMES_EN, WEEKDAY_NAMES_FA,
11};
12
13const MONTH_WIDTH: usize = 20; const MONTH_WIDTH_JULIAN: usize = 27; const MONTHS_PER_ROW: usize = 3;
17const GAP: &str = " ";
18
19#[must_use]
22pub fn format_month(grid: &MonthGrid, julian: bool, persian: bool) -> Vec<String> {
23 let cell_width = if julian { 4 } else { 3 };
24 let width = if julian {
25 MONTH_WIDTH_JULIAN
26 } else {
27 MONTH_WIDTH
28 };
29
30 let weekday_names = if persian {
31 &WEEKDAY_NAMES_FA
32 } else {
33 &WEEKDAY_NAMES_EN
34 };
35
36 let mut lines = Vec::new();
37
38 let month_name = if persian {
40 MONTH_NAMES[grid.month as usize - 1].to_string()
41 } else {
42 capitalize(MONTH_NAMES_ROMANIZED[grid.month as usize - 1])
43 };
44 let year_str = if persian {
45 persian::to_persian_digits(&grid.year.to_string())
46 } else {
47 grid.year.to_string()
48 };
49 let title = format!("{month_name} {year_str}");
50 lines.push(center_persian(&title, width));
51
52 let header: String = weekday_names
54 .iter()
55 .enumerate()
56 .map(|(i, name)| {
57 if i == 0 {
58 format!("{:>width$}", name, width = cell_width - 1)
59 } else {
60 format!("{name:>cell_width$}")
61 }
62 })
63 .collect::<String>();
64 lines.push(header);
65
66 let rows = grid.row_count();
68 for r in 0..rows {
69 let mut row_str = String::new();
70 for c in 0..7 {
71 let cell = match grid.grid[r][c] {
72 Some(day) => {
73 let num = if julian { grid.day_of_year(day) } else { day };
74 let s = num.to_string();
75 if persian {
76 persian::to_persian_digits(&s)
77 } else {
78 s
79 }
80 }
81 None => String::new(),
82 };
83 if c == 0 {
84 write!(row_str, "{:>width$}", cell, width = cell_width - 1).unwrap();
85 } else {
86 write!(row_str, "{cell:>cell_width$}").unwrap();
87 }
88 }
89 lines.push(row_str);
90 }
91
92 while lines.len() < 8 {
94 lines.push(String::new());
96 }
97
98 for line in &mut lines {
100 let current_len = display_width(line);
101 if current_len < width {
102 line.push_str(&" ".repeat(width - current_len));
103 }
104 }
105
106 lines
107}
108
109#[must_use]
111pub fn format_months(grids: &[MonthGrid], julian: bool, persian: bool) -> Vec<String> {
112 let mut output = Vec::new();
113
114 for chunk in grids.chunks(MONTHS_PER_ROW) {
115 let formatted: Vec<Vec<String>> = chunk
116 .iter()
117 .map(|g| format_month(g, julian, persian))
118 .collect();
119
120 let max_lines = formatted.iter().map(|f| f.len()).max().unwrap_or(0);
121
122 let width = if julian {
123 MONTH_WIDTH_JULIAN
124 } else {
125 MONTH_WIDTH
126 };
127
128 for line_idx in 0..max_lines {
129 let mut combined = String::new();
130 for (i, month_lines) in formatted.iter().enumerate() {
131 if i > 0 {
132 combined.push_str(GAP);
133 }
134 if line_idx < month_lines.len() {
135 combined.push_str(&month_lines[line_idx]);
136 } else {
137 combined.push_str(&" ".repeat(width));
138 }
139 }
140 output.push(combined.trim_end().to_string());
141 }
142
143 output.push(String::new());
145 }
146
147 if output.last().is_some_and(|l| l.is_empty()) {
149 output.pop();
150 }
151
152 output
153}
154
155#[must_use]
157pub fn format_year(year: i32, julian: bool, persian: bool) -> Vec<String> {
158 let width = if julian {
159 MONTH_WIDTH_JULIAN
160 } else {
161 MONTH_WIDTH
162 };
163 let total_width = width * MONTHS_PER_ROW + GAP.len() * (MONTHS_PER_ROW - 1);
164
165 let mut output = Vec::new();
166
167 let year_str = if persian {
169 persian::to_persian_digits(&year.to_string())
170 } else {
171 year.to_string()
172 };
173 output.push(center_str(&year_str, total_width));
174 output.push(String::new());
175
176 let grids: Vec<MonthGrid> = (1..=12).map(|m| MonthGrid::new(year, m)).collect();
177
178 output.extend(format_months(&grids, julian, persian));
179 output
180}
181
182fn capitalize(s: &str) -> String {
184 let mut chars = s.chars();
185 match chars.next() {
186 None => String::new(),
187 Some(c) => c.to_uppercase().to_string() + chars.as_str(),
188 }
189}
190
191fn center_str(s: &str, width: usize) -> String {
193 let len = s.len();
194 if len >= width {
195 return s.to_string();
196 }
197 let padding = (width - len) / 2;
198 format!("{}{}", " ".repeat(padding), s)
199}
200
201fn center_persian(s: &str, width: usize) -> String {
204 let dw = display_width(s);
205 if dw >= width {
206 return s.to_string();
207 }
208 let padding = (width - dw) / 2;
209 format!("{}{}", " ".repeat(padding), s)
210}
211
212fn display_width(s: &str) -> usize {
214 s.chars()
215 .filter(|c| !matches!(c, '\u{200e}' | '\u{200f}'))
216 .map(|_| 1)
217 .sum()
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn test_format_month_default() {
226 let grid = MonthGrid::new(2584, 1);
227 let lines = format_month(&grid, false, false);
228 assert_eq!(lines.len(), 8);
229 assert!(lines[0].contains("Farvardin"));
231 assert!(lines[0].contains("2584"));
232 assert!(lines[1].contains("Sa"));
234 }
235
236 #[test]
237 fn test_format_month_persian() {
238 let grid = MonthGrid::new(2584, 1);
239 let lines = format_month(&grid, false, true);
240 assert!(lines[0].contains("فروردین"));
241 assert!(lines[0].contains("۲۵۸۴"));
242 }
243
244 #[test]
245 fn test_format_year() {
246 let lines = format_year(2584, false, false);
247 assert!(lines[0].contains("2584"));
248 for name in MONTH_NAMES_ROMANIZED {
250 assert!(lines.iter().any(|l| l.to_lowercase().contains(name)));
251 }
252 }
253}