cli_tables/
lib.rs

1use std::{
2    fmt,
3    error,
4    cmp::{
5        max,
6        min
7    }
8};
9use terminal_size::{
10    Width, 
11    terminal_size
12};
13
14#[derive(Debug, PartialEq)]
15pub struct TableError {
16    pub message: String,
17}
18
19impl fmt::Display for TableError {
20    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
21        write!(f, "{}", self.message)
22    }
23}
24
25impl error::Error for TableError {}
26
27#[derive(Debug)]
28pub struct Table {
29    table_vec: Vec<Vec<String>>,
30    num_records: usize,
31    num_fields: usize
32}
33
34impl fmt::Display for Table {
35    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36        write!(f, "({:?}, {}, {})", self.table_vec, self.num_records, self.num_fields)
37    }
38}
39
40impl Table {
41    pub fn new() -> Self {
42        Table {
43            table_vec: Vec::new(),
44            num_records: 0,
45            num_fields: 0
46        }
47    }
48
49    pub fn num_records(&self) -> usize {
50        return self.num_records;
51    }
52
53    pub fn num_fields(&self) -> usize {
54        return self.num_fields;
55    }
56
57    /// ### Description
58    /// Pushes a new record to the end of the `Table` object.
59    ///
60    /// ### Arguments
61    /// * `new_record: &Vec<&str>` - An immutable reference 
62    /// to string slices that represent the new record.
63    ///
64    /// ### Errors
65    /// Returns an error if the number of fields is invalid.
66    ///
67    /// ### Example
68    /// ```
69    /// use cli_tables::Table;
70    /// 
71    /// let mut table = Table::new();
72    ///
73    /// let header = vec!["Id", "Title", "Series", "Author"];
74    /// table.push_row(&header).unwrap();
75    /// 
76    /// let book = vec!["0", "Sword of Destiny", "The Witcher Series", "Andrzej Sapkowski"];
77    /// table.push_row(&book).unwrap();
78    /// ```
79    pub fn push_row(&mut self, new_record: &Vec<&str>) -> Result<(), TableError> {
80        let new_record: Vec<String> = new_record.iter().map(|&field| field.to_string()).collect();
81        let num_fields = new_record.len();
82
83        if num_fields == self.num_fields && self.num_records != 0 {
84            self.table_vec.push(new_record.to_vec());
85            self.num_records += 1;
86            Ok(())
87        }
88        else if self.num_records == 0 {
89            self.table_vec.push(new_record.to_vec());
90            self.num_records = 1;
91            self.num_fields = num_fields;
92            Ok(())
93        }
94        else {
95            let msg = format!("Invalid number of fields in record. Found {}, but expected {}.", num_fields, self.num_fields);
96            Err(TableError {
97                message: msg,
98            })
99        }
100    }
101
102    /// ### Description
103    /// Pushes a new record to the end of the `Table` object.
104    ///
105    /// ### Arguments
106    /// * `new_record: &Vec<String>` - An immutable reference
107    /// to Strings that represent the new record.
108    ///
109    /// ### Errors
110    /// Returns an error if the number of fields is invalid.
111    ///
112    /// ### Example
113    /// ```
114    /// use cli_tables::Table;
115    /// 
116    /// let mut table = Table::new();
117    ///
118    /// let header = vec![
119    ///     "Id".to_string(), 
120    ///     "Title".to_string(), 
121    ///     "Series".to_string(), 
122    ///     "Author".to_string()
123    /// ];
124    /// table.push_row_string(&header).unwrap();
125    /// 
126    /// let book = vec![
127    ///     "0".to_string(), 
128    ///     "Sword of Destiny".to_string(), 
129    ///     "The Witcher Series".to_string(), 
130    ///     "Andrzej Sapkowski".to_string()
131    /// ];
132    /// table.push_row_string(&book).unwrap();
133    /// ```
134    pub fn push_row_string(&mut self, new_record: &Vec<String>) -> Result<(), TableError> {
135        let num_fields = new_record.len();
136
137        if num_fields == self.num_fields && self.num_records != 0 {
138            self.table_vec.push(new_record.to_vec());
139            self.num_records += 1;
140            Ok(())
141        }
142        else if self.num_records == 0 {
143            self.table_vec.push(new_record.to_vec());
144            self.num_records = 1;
145            self.num_fields = num_fields;
146            Ok(())
147        }
148        else {
149            let msg = format!("Invalid number of fields in record. Found {}, but expected {}.", num_fields, self.num_fields);
150            Err(TableError {
151                message: msg,
152            })
153        }
154    }
155
156    /// ### Description
157    /// The `push_rows` function takes multiple records, and adds each one 
158    /// in order to the `Table` object.
159    ///
160    /// ### Arguments
161    /// * `new_records` - reference to a `Vec<Vec<&str>>` object that
162    /// represents multiple records.
163    ///
164    /// ### Errors
165    /// Returns a `TableError` if the number of fields is not equal
166    /// for each record or if the number of fields is not equal to
167    /// the existing number of fields in the `Table` object.
168    ///
169    /// ### Example
170    /// ```
171    /// use cli_tables::Table;
172    /// 
173    /// let mut table = Table::new();
174    ///
175    /// let table_vec = vec![
176    ///     vec!["Id", "Title", "Series", "Author"],
177    ///     vec!["0", "Sword of Destiny", "The Witcher Series", "Andrzej Sapkowski"],
178    ///     vec!["1", "The Last Wish", "The Witcher Series", "Andrzej Sapkowski"]
179    /// ];
180    /// table.push_rows(&table_vec).unwrap();
181    /// ```
182    pub fn push_rows(&mut self, new_records: &Vec<Vec<&str>>) -> Result<(), TableError> {
183        let num_records = new_records.len();
184        let num_fields = new_records[0].len();
185        for record in 1..num_records {
186            if new_records[record].len() != num_fields {
187                return Err(TableError {
188                    message: "Records have an unequal number of fields".to_string() 
189                })
190            }
191        }
192        if !self.table_vec.is_empty() {
193            if num_fields != self.num_fields {
194                return Err(TableError {
195                    message: format!(
196                        "Invalid number of fields in records. Found {}, but expected {}.", 
197                        num_fields, 
198                        self.num_fields
199                    )
200                })
201            }
202        } else {
203            self.num_records = num_records;
204            self.num_fields = num_fields;
205        }
206        for record in 0..self.num_records {
207            self.table_vec.push(Vec::new());
208            for field in 0..self.num_fields {
209                self.table_vec[record].push(new_records[record][field].to_string());
210            }
211        }
212        Ok(())
213    }
214
215    /// ### Description
216    /// Retrieves the desired record of the `Table` object and returns it.
217    ///
218    /// ### Arguments
219    /// * `id: usize` - represets the desired record id to be retrieved.
220    ///
221    /// ### Errors
222    /// Returns a `TableError` if no record with the given id is found.
223    ///
224    /// ### Example
225    /// ```
226    /// use cli_tables::Table;
227    /// 
228    /// let mut table = Table::new();
229    ///
230    /// let header = vec![
231    ///     "Id",
232    ///     "Title",
233    ///     "Series",
234    ///     "Author"
235    /// ];
236    /// table.push_row(&header).unwrap();
237    /// 
238    /// table.get_row(0).unwrap();
239    /// ```
240    pub fn get_row(&self, id: usize) -> Result<Vec<String>, TableError> {
241        for record in 0..self.num_records {
242            if record == id {
243                return Ok(self.table_vec[record].clone())
244            }
245        }
246        Err(TableError {
247            message: "No record with matching id".to_string()
248        })
249    }
250
251    /// ### Description
252    /// Deletes the desired record from the `Table` object.
253    ///
254    /// ### Arguments
255    /// * `id` - A `usize` value that represets the desired 
256    /// record id to be deleted.
257    ///
258    /// ### Errors
259    /// Returns a `TableError` if no record with the given id is found.
260    ///
261    /// ### Example
262    /// ```
263    /// use cli_tables::Table;
264    /// 
265    /// let mut table = Table::new();
266    ///
267    /// let header = vec![
268    ///     "Id",
269    ///     "Title",
270    ///     "Series",
271    ///     "Author"
272    /// ];
273    /// table.push_row(&header).unwrap();
274    /// 
275    /// table.delete_record(0).unwrap();
276    /// ```
277    pub fn delete_record(&mut self, id: usize) -> Result<(), TableError> {
278        for record in 0..self.num_records {
279            if record == id {
280                self.table_vec.remove(record);
281                self.num_records -= 1;
282                return Ok(())
283            }
284        }
285        Err(TableError {
286            message: "No record with matching id".to_string() 
287        })
288    }
289
290    /// ### Description
291    /// The `set` function takes multiple records, and adds each one 
292    /// in order to the `Table` object. If the `Table` object is not 
293    /// empty, it is reset with the given values.
294    ///
295    /// ### Arguments
296    /// * `new_table` - reference to a `Vec<Vec<&str>>` object that
297    /// represents multiple records.
298    ///
299    /// ### Errors
300    /// Returns a `TableError` if the number of fields is not equal
301    /// for each record.
302    ///
303    /// ### Example
304    /// ```
305    /// use cli_tables::Table;
306    /// 
307    /// let mut table = Table::new();
308    ///
309    /// let table_vec = vec![
310    ///     vec!["Id", "Title", "Series", "Author"],
311    ///     vec!["0", "Sword of Destiny", "The Witcher Series", "Andrzej Sapkowski"],
312    ///     vec!["1", "The Last Wish", "The Witcher Series", "Andrzej Sapkowski"]
313    /// ];
314    /// table.set(&table_vec).unwrap();
315    /// ```
316    pub fn set(&mut self, new_table: &Vec<Vec<&str>>) -> Result<(), TableError> {
317        if !self.table_vec.is_empty() {
318            self.table_vec = Vec::new();
319        }
320        self.num_records = new_table.len();
321        self.num_fields = new_table[0].len();
322        for record in 0..new_table.len() {
323            if self.num_fields == new_table[record].len() {
324                self.table_vec.push(Vec::new());
325                for field in 0..new_table[record].len() {
326                    self.table_vec[record].push(new_table[record][field].to_string());
327                }
328            } else {
329                return Err(TableError {
330                    message: "Records have an unequal number of fields".to_string()
331                })
332            }
333        }
334        Ok(())
335    }
336
337    pub fn to_string(&self) -> String {
338        if self.table_vec.is_empty() {
339            return "+----------------+\n| Table is empty |\n+----------------+".to_string();
340        }
341        
342        let mut table_str = String::new();
343        let mut field_width = vec![0; self.num_fields];
344        let mut field_length = vec![
345            vec![0; self.num_fields]; 
346            self.num_records
347        ];
348        let mut terminal_width = 0;
349        if let Some((Width(width), _)) = terminal_size() {
350            terminal_width = width.into();
351        }
352        let mut table_width = 0;
353        let padding_length: usize = 2;
354
355        // table characters
356        let newline = '\n';
357        let border = '|';
358        let padding = ' ';
359        let edge = '+';
360        let line = '-';
361
362        // determine field width for each field
363        for record in 0..self.num_records {
364            // for first record, set width
365            if record == 0 {
366                for field in 0..self.num_fields {
367                    field_length[record][field] = self.table_vec[record][field].len();
368                    field_width[field] = field_length[record][field];
369                }
370            } else {
371                for field in 0..self.num_fields {
372                    field_length[record][field] = self.table_vec[record][field].len();
373                    // otherwise, compare widths to find max
374                    field_width[field] = max(
375                        field_width[field],
376                        field_length[record][field]
377                    )
378                }
379            }
380        }
381        table_width += field_width.iter().sum::<usize>();
382
383        // account for formatting for the table width
384        let num_borders = self.num_fields + 1;
385        let num_spaces = self.num_fields * 2;
386        table_width += num_borders + num_spaces;
387
388        // determine field widths
389        while table_width > terminal_width {
390            let mut max_field_width = 0;
391            let mut widest_field = 0;
392            for field in 0..self.num_fields {
393                if field_width[field] > max_field_width {
394                    widest_field = field;
395                    max_field_width = field_width[field];
396                }
397            }
398            field_width[widest_field] -= 1;
399            table_width -= 1;
400        }
401
402        // determine record heights
403        let mut record_height = Vec::new();
404        let mut is_wrapped = vec![
405            vec![false; self.num_fields]; 
406            self.num_records
407        ];
408        for record in 0..self.num_records {
409            let mut max_record_height = 0;
410            for field in 0..self.num_fields {
411                if field_length[record][field] % field_width[field] != 0 {
412                    max_record_height = max(
413                        max_record_height,
414                        (field_length[record][field] / field_width[field]) + 1
415                    );
416                }
417                else {
418                    max_record_height = max(
419                        max_record_height,
420                        field_length[record][field] / field_width[field]
421                    );
422                }
423                is_wrapped[record][field] = field_length[record][field] > field_width[field];
424            }
425            record_height.push(max_record_height);
426        }
427        
428        // add top border
429        table_str.push(edge);
430        for field in 0..self.num_fields {
431            for _ in 0..field_width[field] + padding_length {
432                table_str.push(line);
433            }
434            table_str.push(edge);
435        }
436        table_str.push(newline);
437
438        // add column headers
439        if record_height[0] > 1 {
440            let mut remaining = "";
441            for line in 0..record_height[0] {
442                table_str.push(border);
443                table_str.push(padding);
444                for field in 0..self.num_fields {
445                    if is_wrapped[0][field] {
446                        let slice; // check other comment
447                        if line == 0 {
448                            slice = &self.table_vec[0][field][..field_width[field]];
449                            remaining = &self.table_vec[0][field][field_width[field]..];
450                        } else {
451                            slice = &remaining[..field_width[field]];
452                            remaining = &remaining[field_width[field]..];
453                        }
454                        table_str.push_str(slice);
455                    }
456                }
457                table_str.push(padding);
458                table_str.push(border);
459                table_str.push(newline);
460            }
461        } else {
462            table_str.push(border);
463            table_str.push(padding);
464            for field in 0..self.num_fields {
465                table_str.push_str(&self.table_vec[0][field]);
466                for _ in field_length[0][field]..field_width[field] {
467                    table_str.push(padding);
468                }
469                // add separators
470                if field != self.num_fields - 1 {
471                    table_str.push(padding);
472                    table_str.push(border);
473                    table_str.push(padding);
474                }
475            }
476            table_str.push(padding);
477            table_str.push(border);
478            table_str.push(newline);
479        }
480
481        // add middle line
482        table_str.push(edge);
483        for field in 0..self.num_fields {
484            for _ in 0..field_width[field] + padding_length {
485                table_str.push(line);
486            }
487            table_str.push(edge);
488        }
489        table_str.push(newline);
490
491        // add values
492        for record in 1..self.num_records {
493            let mut remaining = "";
494            if record_height[record] > 1 {
495                for line in 0..record_height[record] {
496                    table_str.push(border);
497                    table_str.push(padding);
498                    for field in 0..self.num_fields {
499                        // add truncated value and store remaining
500                        if is_wrapped[record][field] {
501                            let slice; // removed "" assignment and make not mutable
502                            let mut slice_length = field_width[field];
503                            if line == 0 {
504                                slice = &self.table_vec[record][field][..field_width[field]];
505                                remaining = &self.table_vec[record][field][field_width[field]..];
506                            } else {
507                                slice_length = min(field_width[field], remaining.len());
508                                slice = &remaining[..slice_length];
509                                remaining = &remaining[slice_length..];
510                            }
511                            table_str.push_str(slice);
512                            if slice_length < field_width[field] {
513                                for _ in slice_length..field_width[field] {
514                                    table_str.push(padding);
515                                }
516                            }
517                        } else {
518                            if line == 0 {
519                                table_str.push_str(&self.table_vec[record][field]);
520                                // add padding
521                                for _ in field_length[record][field]..field_width[field] {
522                                    table_str.push(padding);
523                                }
524                            } else {
525                                // add padding
526                                for _ in 0..field_width[field] {
527                                    table_str.push(padding);
528                                }
529                            }
530                        }
531                        // add separators
532                        if field != self.num_fields - 1 {
533                            table_str.push(padding);
534                            table_str.push(border);
535                            table_str.push(padding);
536                        }
537                    }
538                    table_str.push(padding);
539                    table_str.push(border);
540                    table_str.push(newline);
541                }
542            } else {
543                table_str.push(border);
544                table_str.push(padding);
545                for field in 0..self.num_fields {
546                    // add value
547                    table_str.push_str(&self.table_vec[record][field]);
548                    // add padding
549                    for _ in field_length[record][field]..field_width[field] {
550                        table_str.push(padding);
551                    }
552                    // add separators
553                    if field != self.num_fields - 1 {
554                        table_str.push(padding);
555                        table_str.push(border);
556                        table_str.push(padding);
557                    }
558                }
559                table_str.push(padding);
560                table_str.push(border);
561                table_str.push(newline);
562            }
563        }
564
565        // add bottom border
566        table_str.push(edge);
567        for field in 0..self.num_fields {
568            for _ in 0..field_width[field] + padding_length {
569                table_str.push(line);
570            }
571            table_str.push(edge);
572        }
573
574        table_str
575    }
576}