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
| 1 | use oxedyne_fe2o3_core::prelude::*; |
| 2 | |
| 3 | #[derive(Clone, Copy)] |
| 4 | pub 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 | |
| 18 | impl 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)] |
| 63 | pub enum Alignment { |
| 64 | Left, |
| 65 | Right, |
| 66 | Centre, |
| 67 | Center, |
| 68 | } |
| 69 | |
| 70 | #[derive(Clone)] |
| 71 | enum AlignmentRule { |
| 72 | Column(usize, Alignment), |
| 73 | Row(usize, Alignment), |
| 74 | All(Alignment), |
| 75 | } |
| 76 | |
| 77 | pub struct Tabular { |
| 78 | rows: Vec<Vec<String>>, |
| 79 | separator: &'static str, |
| 80 | box_style: Option<BoxStyle>, |
| 81 | alignment_rules: Vec<AlignmentRule>, |
| 82 | } |
| 83 | |
| 84 | impl 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(); |