| 1 | //! This module contains an [`ExtendedTable`] structure which is useful in cases where |
| 2 | //! a structure has a lot of fields. |
| 3 | //! |
| 4 | #![cfg_attr (feature = "derive" , doc = "```" )] |
| 5 | #![cfg_attr (not(feature = "derive" ), doc = "```ignore" )] |
| 6 | //! use tabled::{Tabled, tables::ExtendedTable}; |
| 7 | //! |
| 8 | //! #[derive(Tabled)] |
| 9 | //! struct Language { |
| 10 | //! name: &'static str, |
| 11 | //! designed_by: &'static str, |
| 12 | //! invented_year: usize, |
| 13 | //! } |
| 14 | //! |
| 15 | //! let languages = vec![ |
| 16 | //! Language{ |
| 17 | //! name: "C" , |
| 18 | //! designed_by: "Dennis Ritchie" , |
| 19 | //! invented_year: 1972 |
| 20 | //! }, |
| 21 | //! Language{ |
| 22 | //! name: "Rust" , |
| 23 | //! designed_by: "Graydon Hoare" , |
| 24 | //! invented_year: 2010 |
| 25 | //! }, |
| 26 | //! Language{ |
| 27 | //! name: "Go" , |
| 28 | //! designed_by: "Rob Pike" , |
| 29 | //! invented_year: 2009 |
| 30 | //! }, |
| 31 | //! ]; |
| 32 | //! |
| 33 | //! let table = ExtendedTable::new(languages).to_string(); |
| 34 | //! |
| 35 | //! let expected = "-[ RECORD 0 ]-+--------------- \n\ |
| 36 | //! name | C \n\ |
| 37 | //! designed_by | Dennis Ritchie \n\ |
| 38 | //! invented_year | 1972 \n\ |
| 39 | //! -[ RECORD 1 ]-+--------------- \n\ |
| 40 | //! name | Rust \n\ |
| 41 | //! designed_by | Graydon Hoare \n\ |
| 42 | //! invented_year | 2010 \n\ |
| 43 | //! -[ RECORD 2 ]-+--------------- \n\ |
| 44 | //! name | Go \n\ |
| 45 | //! designed_by | Rob Pike \n\ |
| 46 | //! invented_year | 2009" ; |
| 47 | //! |
| 48 | //! assert_eq!(table, expected); |
| 49 | //! ``` |
| 50 | |
| 51 | use std::fmt::{self, Display}; |
| 52 | |
| 53 | use crate::grid::util::string::string_width; |
| 54 | use crate::Tabled; |
| 55 | |
| 56 | /// `ExtendedTable` display data in a 'expanded display mode' from postgresql. |
| 57 | /// It may be useful for a large data sets with a lot of fields. |
| 58 | /// |
| 59 | /// See 'Examples' in <https://www.postgresql.org/docs/current/app-psql.html>. |
| 60 | /// |
| 61 | /// It escapes strings to resolve a multi-line ones. |
| 62 | /// Because of that ANSI sequences will be not be rendered too so colores will not be showed. |
| 63 | /// |
| 64 | /// ``` |
| 65 | /// use tabled::tables::ExtendedTable; |
| 66 | /// |
| 67 | /// let data = vec!["Hello" , "2021" ]; |
| 68 | /// let table = ExtendedTable::new(&data).to_string(); |
| 69 | /// |
| 70 | /// assert_eq!( |
| 71 | /// table, |
| 72 | /// concat!( |
| 73 | /// "-[ RECORD 0 ]- \n" , |
| 74 | /// "&str | Hello \n" , |
| 75 | /// "-[ RECORD 1 ]- \n" , |
| 76 | /// "&str | 2021" , |
| 77 | /// ) |
| 78 | /// ); |
| 79 | /// ``` |
| 80 | #[derive (Debug, Clone)] |
| 81 | pub struct ExtendedTable { |
| 82 | fields: Vec<String>, |
| 83 | records: Vec<Vec<String>>, |
| 84 | } |
| 85 | |
| 86 | impl ExtendedTable { |
| 87 | /// Creates a new instance of `ExtendedTable` |
| 88 | pub fn new<T>(iter: impl IntoIterator<Item = T>) -> Self |
| 89 | where |
| 90 | T: Tabled, |
| 91 | { |
| 92 | let data = iter |
| 93 | .into_iter() |
| 94 | .map(|i| { |
| 95 | i.fields() |
| 96 | .into_iter() |
| 97 | .map(|s| s.escape_debug().to_string()) |
| 98 | .collect() |
| 99 | }) |
| 100 | .collect(); |
| 101 | let header = T::headers() |
| 102 | .into_iter() |
| 103 | .map(|s| s.escape_debug().to_string()) |
| 104 | .collect(); |
| 105 | |
| 106 | Self { |
| 107 | records: data, |
| 108 | fields: header, |
| 109 | } |
| 110 | } |
| 111 | |
| 112 | /// Truncates table to a set width value for a table. |
| 113 | /// It returns a success inticator, where `false` means it's not possible to set the table width, |
| 114 | /// because of the given arguments. |
| 115 | /// |
| 116 | /// It tries to not affect fields, but if there's no enough space all records will be deleted and fields will be cut. |
| 117 | /// |
| 118 | /// The minimum width is 14. |
| 119 | pub fn truncate(&mut self, max: usize, suffix: &str) -> bool { |
| 120 | // -[ RECORD 0 ]- |
| 121 | let teplate_width = self.records.len().to_string().len() + 13; |
| 122 | let min_width = teplate_width; |
| 123 | if max < min_width { |
| 124 | return false; |
| 125 | } |
| 126 | |
| 127 | let suffix_width = string_width(suffix); |
| 128 | if max < suffix_width { |
| 129 | return false; |
| 130 | } |
| 131 | |
| 132 | let max = max - suffix_width; |
| 133 | |
| 134 | let fields_max_width = self |
| 135 | .fields |
| 136 | .iter() |
| 137 | .map(|s| string_width(s)) |
| 138 | .max() |
| 139 | .unwrap_or_default(); |
| 140 | |
| 141 | // 3 is a space for ' | ' |
| 142 | let fields_affected = max < fields_max_width + 3; |
| 143 | if fields_affected { |
| 144 | if max < 3 { |
| 145 | return false; |
| 146 | } |
| 147 | |
| 148 | let max = max - 3; |
| 149 | |
| 150 | if max < suffix_width { |
| 151 | return false; |
| 152 | } |
| 153 | |
| 154 | let max = max - suffix_width; |
| 155 | |
| 156 | truncate_fields(&mut self.fields, max, suffix); |
| 157 | truncate_records(&mut self.records, 0, suffix); |
| 158 | } else { |
| 159 | let max = max - fields_max_width - 3 - suffix_width; |
| 160 | truncate_records(&mut self.records, max, suffix); |
| 161 | } |
| 162 | |
| 163 | true |
| 164 | } |
| 165 | } |
| 166 | |
| 167 | impl From<Vec<Vec<String>>> for ExtendedTable { |
| 168 | fn from(mut data: Vec<Vec<String>>) -> Self { |
| 169 | if data.is_empty() { |
| 170 | return Self { |
| 171 | fields: vec![], |
| 172 | records: vec![], |
| 173 | }; |
| 174 | } |
| 175 | |
| 176 | let fields: Vec = data.remove(index:0); |
| 177 | |
| 178 | Self { |
| 179 | fields, |
| 180 | records: data, |
| 181 | } |
| 182 | } |
| 183 | } |
| 184 | |
| 185 | impl Display for ExtendedTable { |
| 186 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| 187 | if self.records.is_empty() { |
| 188 | return Ok(()); |
| 189 | } |
| 190 | |
| 191 | // It's possible that field|header can be a multiline string so |
| 192 | // we escape it and trim \" chars. |
| 193 | let fields = self.fields.iter().collect::<Vec<_>>(); |
| 194 | |
| 195 | let max_field_width = fields |
| 196 | .iter() |
| 197 | .map(|s| string_width(s)) |
| 198 | .max() |
| 199 | .unwrap_or_default(); |
| 200 | |
| 201 | let max_values_length = self |
| 202 | .records |
| 203 | .iter() |
| 204 | .map(|record| record.iter().map(|s| string_width(s)).max()) |
| 205 | .max() |
| 206 | .unwrap_or_default() |
| 207 | .unwrap_or_default(); |
| 208 | |
| 209 | for (i, records) in self.records.iter().enumerate() { |
| 210 | write_header_template(f, i, max_field_width, max_values_length)?; |
| 211 | |
| 212 | for (value, field) in records.iter().zip(fields.iter()) { |
| 213 | writeln!(f)?; |
| 214 | write_record(f, field, value, max_field_width)?; |
| 215 | } |
| 216 | |
| 217 | let is_last_record = i + 1 == self.records.len(); |
| 218 | if !is_last_record { |
| 219 | writeln!(f)?; |
| 220 | } |
| 221 | } |
| 222 | |
| 223 | Ok(()) |
| 224 | } |
| 225 | } |
| 226 | |
| 227 | fn truncate_records(records: &mut Vec<Vec<String>>, max_width: usize, suffix: &str) { |
| 228 | for fields: &mut Vec in records { |
| 229 | truncate_fields(records:fields, max_width, suffix); |
| 230 | } |
| 231 | } |
| 232 | |
| 233 | fn truncate_fields(records: &mut Vec<String>, max_width: usize, suffix: &str) { |
| 234 | for text: &mut String in records { |
| 235 | truncate(text, max_width, suffix); |
| 236 | } |
| 237 | } |
| 238 | |
| 239 | fn write_header_template( |
| 240 | f: &mut fmt::Formatter<'_>, |
| 241 | index: usize, |
| 242 | max_field_width: usize, |
| 243 | max_values_length: usize, |
| 244 | ) -> fmt::Result { |
| 245 | let mut template = format!("-[ RECORD {index} ]-" ); |
| 246 | let default_template_length = template.len(); |
| 247 | |
| 248 | // 3 - is responsible for ' | ' formatting |
| 249 | let max_line_width = std::cmp::max( |
| 250 | max_field_width + 3 + max_values_length, |
| 251 | default_template_length, |
| 252 | ); |
| 253 | let rest_to_print = max_line_width - default_template_length; |
| 254 | if rest_to_print > 0 { |
| 255 | // + 1 is a space after field name and we get a next pos so its +2 |
| 256 | if max_field_width + 2 > default_template_length { |
| 257 | let part1 = (max_field_width + 1) - default_template_length; |
| 258 | let part2 = rest_to_print - part1 - 1; |
| 259 | |
| 260 | template.extend( |
| 261 | std::iter::repeat('-' ) |
| 262 | .take(part1) |
| 263 | .chain(std::iter::once('+' )) |
| 264 | .chain(std::iter::repeat('-' ).take(part2)), |
| 265 | ); |
| 266 | } else { |
| 267 | template.extend(std::iter::repeat('-' ).take(rest_to_print)); |
| 268 | } |
| 269 | } |
| 270 | |
| 271 | write!(f, " {template}" )?; |
| 272 | |
| 273 | Ok(()) |
| 274 | } |
| 275 | |
| 276 | fn write_record( |
| 277 | f: &mut fmt::Formatter<'_>, |
| 278 | field: &str, |
| 279 | value: &str, |
| 280 | max_field_width: usize, |
| 281 | ) -> fmt::Result { |
| 282 | write!(f, " {field:max_field_width$} | {value}" ) |
| 283 | } |
| 284 | |
| 285 | fn truncate(text: &mut String, max: usize, suffix: &str) { |
| 286 | let original_len: usize = text.len(); |
| 287 | |
| 288 | if max == 0 || text.is_empty() { |
| 289 | *text = String::new(); |
| 290 | } else { |
| 291 | *text = crate::util::string::cut_str2(text, width:max).into_owned(); |
| 292 | } |
| 293 | |
| 294 | let cut_was_done: bool = text.len() < original_len; |
| 295 | if !suffix.is_empty() && cut_was_done { |
| 296 | text.push_str(string:suffix); |
| 297 | } |
| 298 | } |
| 299 | |