Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_text/src/table.rs

9.2 KiB, 13 runs

created by r1870400018:8826, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1use oxedyne_fe2o3_core::prelude::*;
2
3#[derive(Clone, Copy)]
4pub struct BoxStyle {
5 horizontal: char,
6 vertical: char,
7 top_left: char,
8 top_right: char,
9 bottom_left: char,
10 bottom_right: char,
11 cross: char,
12 top_join: char,
13 bottom_join: char,
14 left_join: char,
15 right_join: char,
16}
17
18impl BoxStyle {
19 pub const SINGLE: Self = Self {
20 horizontal: '─',
21 vertical: '│',
22 top_left: '┌',
23 top_right: '┐',
24 bottom_left: '└',
25 bottom_right: '┘',
26 cross: '┼',
27 top_join: '┬',
28 bottom_join: '┴',
29 left_join: '├',
30 right_join: '┤',
31 };
32
33 pub const DOUBLE: Self = Self {
34 horizontal: '═',
35 vertical: '║',
36 top_left: '╔',
37 top_right: '╗',
38 bottom_left: '╚',
39 bottom_right: '╝',
40 cross: '╬',
41 top_join: '╦',
42 bottom_join: '╩',
43 left_join: '╠',
44 right_join: '╣',
45 };
46
47 pub const ROUNDED: Self = Self {
48 horizontal: '─',
49 vertical: '│',
50 top_left: '╭',
51 top_right: '╮',
52 bottom_left: '╰',
53 bottom_right: '╯',
54 cross: '┼',
55 top_join: '┬',
56 bottom_join: '┴',
57 left_join: '├',
58 right_join: '┤',
59 };
60}
61
62#[derive(Clone, Copy, PartialEq)]
63pub enum Alignment {
64 Left,
65 Right,
66 Centre,
67 Center,
68}
69
70#[derive(Clone)]
71enum AlignmentRule {
72 Column(usize, Alignment),
73 Row(usize, Alignment),
74 All(Alignment),
75}
76
77pub struct Tabular {
78 rows: Vec<Vec<String>>,
79 separator: &'static str,
80 box_style: Option<BoxStyle>,
81 alignment_rules: Vec<AlignmentRule>,
82}
83
84impl Tabular {
85 pub fn new() -> Self {
86 Self {
87 rows: vec![],
88 separator: " ",
89 box_style: None,
90 alignment_rules: vec![],
91 }
92 }
93
94 pub fn separator(mut self, sep: &'static str) -> Self {
95 self.separator = sep;
96 self
97 }
98
99 pub fn boxed(mut self, style: BoxStyle) -> Self {
100 self.box_style = Some(style);
101 self
102 }
103
104 pub fn align_col(mut self, col: usize, alignment: Alignment) -> Self {
105 self.alignment_rules.push(AlignmentRule::Column(col, alignment));
106 self
107 }
108
109 pub fn align_cols(mut self, alignments: &[(usize, Alignment)]) -> Self {
110 for &(col, align) in alignments {
111 self.alignment_rules.push(AlignmentRule::Column(col, align));
112 }
113 self
114 }
115
116 pub fn align_row(mut self, row: usize, alignment: Alignment) -> Self {
117 self.alignment_rules.push(AlignmentRule::Row(row, alignment));
118 self
119 }
120
121 pub fn align_rows(mut self, alignments: &[(usize, Alignment)]) -> Self {
122 for &(row, align) in alignments {
123 self.alignment_rules.push(AlignmentRule::Row(row, align));
124 }
125 self
126 }
127
128 pub fn align_all(mut self, alignment: Alignment) -> Self {
129 self.alignment_rules.push(AlignmentRule::All(alignment));
130 self
131 }
132
133 fn get_alignment(&self, row: usize, col: usize) -> Alignment {
134 let mut alignment = Alignment::Left; // default
135
136 // Apply rules in order - later rules override earlier ones.
137 for rule in &self.alignment_rules {
138 match rule {
139 AlignmentRule::All(align) => alignment = *align,
140 AlignmentRule::Column(c, align) if *c == col => alignment = *align,
141 AlignmentRule::Row(r, align) if *r == row => alignment = *align,
142 _ => {}
143 }
144 }
145
146 alignment
147 }
148
149 pub fn row<const N: usize>(mut self, cells: [&str; N]) -> Self {
150 self.rows.push(cells.iter().map(|&s| s.to_string()).collect());
151 self
152 }
153
154 pub fn row_owned(mut self, cells: Vec<String>) -> Self {
155 self.rows.push(cells);
156 self
157 }
158
159 pub fn rows_from_iter(mut self, iter: impl Iterator<Item = Vec<String>>) -> Self {
160 self.rows.extend(iter);
161 self
162 }
163
164 fn format_cell(
165 &self,
166 cell: &str,
167 width: usize,
168 row: usize,
169 col: usize,
170 )
171 -> String
172 {
173 let alignment = self.get_alignment(row, col);
174 match alignment {
175 Alignment::Left => format!("{:width$}", cell, width = width),
176 Alignment::Right => format!("{:>width$}", cell, width = width),
177 Alignment::Centre | Alignment::Center => format!("{:^width$}", cell, width = width),
178 }
179 }
180
181 fn print_horizontal_line(
182 &self,
183 widths: &[usize],
184 style: &BoxStyle,
185 left: char,
186 mid: char,
187 right: char,
188 ) {
189 print!("{}", left);
190 for (i, &width) in widths.iter().enumerate() {
191 print!("{}", style.horizontal.to_string().repeat(width + 2));
192 if i < widths.len() - 1 {
193 print!("{}", mid);
194 }
195 }
196 println!("{}", right);
197 }
198
199 pub fn print(&self) {
200
201 if self.rows.is_empty() { return; }
202
203 // Calculate max width for each column.
204 let widths: Vec<usize> = (0..self.rows[0].len())
205 .map(|col| self.rows.iter()
206 .map(|row| row.get(col).map_or(0, |s| s.len()))
207 .max()
208 .unwrap_or(0))
209 .collect();
210
211 match self.box_style {
212 Some(style) => {
213 // Top border.
214 self.print_horizontal_line(
215 &widths,
216 &style,
217 style.top_left,
218 style.top_join,
219 style.top_right,
220 );
221
222 // Rows with borders.
223 for (row_idx, row) in self.rows.iter().enumerate() {
224 print!("{}", style.vertical);
225 for (col_idx, (cell, &width)) in row.iter().zip(&widths).enumerate() {
226 let formatted = self.format_cell(cell, width, row_idx, col_idx);
227 print!(" {} ", formatted);
228 if col_idx < row.len() - 1 {
229 print!("{}", style.vertical);
230 }
231 }
232 println!("{}", style.vertical);
233
234 // Separator after header row.
235 if row_idx == 0 && self.rows.len() > 1 {
236 self.print_horizontal_line(
237 &widths,
238 &style,
239 style.left_join,
240 style.cross,
241 style.right_join,
242 );
243 }
244 }
245
246 // Bottom border.
247 self.print_horizontal_line(
248 &widths,
249 &style,
250 style.bottom_left,
251 style.bottom_join,
252 style.bottom_right,
253 );
254 }
255 None => {
256 // Original behaviour without boxing.
257 for (row_idx, row) in self.rows.iter().enumerate() {
258 let formatted: Vec<String> = row.iter()
259 .zip(&widths)
260 .enumerate()
261 .map(|(col_idx, (cell, &width))| self.format_cell(
262 cell,
263 width,
264 row_idx,
265 col_idx,
266 ))
267 .collect();
268 println!("{}", formatted.join(self.separator));
269 }
270 }
271 }
272 }
273}
274
275//// Usage examples:
276//
277//// Right-align numeric columns
278//Tabular::new()
279// .boxed(BoxStyle::SINGLE)
280// .align_col(1, Alignment::Right) // Revenue column
281// .align_col(2, Alignment::Right) // Expenses column
282// .align_col(3, Alignment::Right) // Profit column
283// .row(["Month", "Revenue", "Expenses", "Profit"])
284// .row(["January", "$100,000", "$80,000", "$20,000"])
285// .row(["February", "$1,234,567", "$987,654", "$246,913"])
286// .print();
287//
288//// Or set multiple alignments at once
289//Tabular::new()
290// .boxed(BoxStyle::SINGLE)
291// .align_cols(&[
292// (1, Alignment::Right),
293// (2, Alignment::Right),
294// (3, Alignment::Right),
295// ])
296// .row(["Month", "Revenue", "Expenses", "Profit"])
297// .row(["January", "$100,000", "$80,000", "$20,000"])
298// .print();
299//
300//// For your monthly table, right-align all numeric columns:
301//Tabular::new()
302// .align_cols(&[
303// (3, Alignment::Right), // Perth
304// (4, Alignment::Right), // Rest AU
305// (5, Alignment::Right), // Rest World
306// (6, Alignment::Right), // Total Users
307// (7, Alignment::Right), // Revenue
308// (8, Alignment::Right), // Balance
309// (9, Alignment::Right), // Rate A
310// (10, Alignment::Right), // Rate B
311// (11, Alignment::Right), // Rate C
312// (12, Alignment::Right), // Expenses
313// ])
314// .rows_from_iter(rows.into_iter())
315// .print();