termimad/fit/
tbl_fit.rs

1use {
2    crate::InsufficientWidthError,
3    std::cmp,
4};
5
6/// A fitter, accumulating data about the table which must fit into
7/// a given width, then computing the best column widths.
8pub struct TblFit {
9    cols: Vec<ColData>,
10    available_sum_width: usize,
11}
12
13/// Information observed with calls to `see_cell`
14#[derive(Debug, Clone, Copy)]
15struct ColData {
16    sum_widths: usize,
17    width: usize,
18    count: usize,
19}
20
21impl Default for ColData {
22    fn default() -> Self {
23        Self {
24            sum_widths: 0,
25            width: 3, // minimal col width
26            count: 0, // number of observations on which we summed
27        }
28    }
29}
30
31impl ColData {
32    const fn avg_width(self) -> usize {
33        if self.count == 0 {
34            0
35        } else {
36            div_ceil(self.sum_widths, self.count)
37        }
38    }
39    pub fn see_cell(&mut self, cell_width: usize) {
40        self.sum_widths += cell_width;
41        self.width = self.width.max(cell_width);
42        self.count += 1;
43    }
44}
45
46/// Result of the fitting operation (always a success)
47pub struct TblFitResult {
48    /// whether some cells will have to be wrapped
49    pub reduced: bool,
50
51    /// the widths of all columns, so that they're guaranteed to fit
52    /// into the `available_width` (taking the borders into account)
53    pub col_widths: Vec<usize>,
54}
55
56impl TblFit {
57    /// Build a new fitter, or return an error if the width isn't enough
58    /// for the given number of columns.
59    ///
60    /// `available_width`: total available width, including external borders
61    pub fn new(cols_count: usize, available_width: usize) -> Result<Self, InsufficientWidthError> {
62        if available_width < cols_count * 4 + 1 {
63            return Err(InsufficientWidthError { available_width });
64        }
65        let cols = vec![ColData::default(); cols_count];
66        let available_sum_width = available_width - 1 - cols_count;
67        Ok(Self {
68            cols,
69            available_sum_width,
70        })
71    }
72    pub fn see_cell(&mut self, col_idx: usize, cell_width: usize) {
73        if let Some(col) = self.cols.get_mut(col_idx) {
74            col.see_cell(cell_width);
75        }
76    }
77    /// compute the fitting
78    pub fn fit(&self) -> TblFitResult {
79        let sum_widths: usize = self.cols.iter().map(|c| c.width).sum();
80        if sum_widths <= self.available_sum_width {
81            return TblFitResult {
82                reduced: false,
83                col_widths: self.cols.iter().map(|c| c.width).collect(),
84            };
85        }
86        if self.cols.is_empty() {
87            return TblFitResult {
88                reduced: false,
89                col_widths: Vec::new(),
90            };
91        }
92        if self.cols.len() == 1 {
93            return TblFitResult {
94                reduced: false,
95                col_widths: vec![self.available_sum_width],
96            };
97        }
98        #[derive(Debug)]
99        struct ColFit {
100            idx: usize,       // index of the col
101            std_width: usize, // col internal max width
102            avg_width: usize, // col internal average width
103            width: usize,     // final width
104        }
105        let mut fits: Vec<ColFit> = self
106            .cols
107            .iter()
108            .enumerate()
109            .map(|(idx, c)| ColFit {
110                idx,
111                std_width: c.width,
112                avg_width: c.avg_width(),
113                width: c.width,
114            })
115            .collect();
116
117        if self.available_sum_width >= sum_widths {
118            return TblFitResult {
119                reduced: false,
120                col_widths: self.cols.iter().map(|c| c.width).collect(),
121            };
122        }
123
124        let mut excess = sum_widths - self.available_sum_width;
125        fits.sort_by_key(|c| cmp::Reverse(c.width));
126
127        // Step 1
128        // We do a first reduction, if possible, on columns wider
129        // than 5, and trying to keep above the average width
130        let potential_uncut_gain_1 = fits
131            .iter()
132            .filter(|c| c.width > 4 && c.width > c.avg_width + 1)
133            .map(|c| (c.width - c.avg_width).min(4))
134            .sum::<usize>();
135        let potential_cut_gain_1 = potential_uncut_gain_1.min(excess);
136        if potential_cut_gain_1 > 0 {
137            for c in fits.iter_mut() {
138                if c.std_width > 4 && c.std_width > c.avg_width {
139                    let gain_1 = div_ceil(
140                        (c.width - c.avg_width) * potential_cut_gain_1,
141                        potential_uncut_gain_1,
142                    );
143                    let gain_1 = gain_1.min(excess).min(c.width - 4);
144                    c.width -= gain_1;
145                    excess -= gain_1;
146                    if excess == 0 {
147                        break;
148                    }
149                }
150            }
151        }
152
153        // Step 2
154        // We remove excess proportionnally
155        if excess > 0 {
156            let potential_total_gain_2 =
157                fits.iter().map(|c| c.width - 3).sum::<usize>().min(excess);
158            let excess_before_2 = excess;
159            for c in fits.iter_mut() {
160                let gain_2 = div_ceil((c.width - 3) * excess_before_2, potential_total_gain_2);
161                let gain_2 = gain_2.min(excess).min(c.width - 3);
162                c.width -= gain_2;
163                excess -= gain_2;
164                if excess == 0 {
165                    break;
166                }
167            }
168        }
169
170        // it should be OK now
171
172        fits.sort_by_key(|c| c.idx);
173        TblFitResult {
174            reduced: true,
175            col_widths: fits.iter().map(|c| c.width).collect(),
176        }
177    }
178}
179
180/// divide, rounding to the top
181const fn div_ceil(q: usize, r: usize) -> usize {
182    let mut res = q / r;
183    if q % r != 0 {
184        res += 1;
185    }
186    res
187}