papergrid/estimation/
width.rs

1//! The module contains a [`WidthEstimator`] for [`Grid`] width estimation.
2//!
3//! [`Grid`]: crate::Grid
4
5use std::cmp::Ordering;
6
7use crate::{records::Records, GridConfig, Position};
8
9use super::Estimate;
10
11pub use super::width_func::{CfgWidthFunction, WidthFunc};
12
13/// A [`Estimate`]or of a width for a [`Grid`].
14///
15/// [`Grid`]: crate::Grid
16#[derive(Debug, Default, Clone, PartialEq, Eq)]
17pub struct WidthEstimator {
18    widths: Vec<usize>,
19}
20
21impl<R> Estimate<R> for WidthEstimator
22where
23    R: Records,
24{
25    fn estimate(&mut self, records: R, cfg: &GridConfig) {
26        let width_ctrl = CfgWidthFunction::from_cfg(cfg);
27        self.widths = build_widths(&records, cfg, &width_ctrl);
28    }
29
30    fn get(&self, column: usize) -> Option<usize> {
31        self.widths.get(column).copied()
32    }
33
34    fn total(&self) -> usize {
35        self.widths.iter().sum()
36    }
37}
38
39impl From<Vec<usize>> for WidthEstimator {
40    fn from(widths: Vec<usize>) -> Self {
41        Self { widths }
42    }
43}
44
45impl From<WidthEstimator> for Vec<usize> {
46    fn from(val: WidthEstimator) -> Self {
47        val.widths
48    }
49}
50
51fn build_widths<R>(records: &R, cfg: &GridConfig, width_ctrl: &CfgWidthFunction) -> Vec<usize>
52where
53    R: Records,
54{
55    let shape = (records.count_rows(), records.count_columns());
56    let mut widths = vec![0; records.count_columns()];
57    for (col, column) in widths.iter_mut().enumerate() {
58        let max = (0..records.count_rows())
59            .filter(|&row| is_simple_cell(cfg, (row, col), shape))
60            .map(|row| get_cell_width(cfg, records, (row, col), width_ctrl))
61            .max()
62            .unwrap_or(0);
63
64        *column = max;
65    }
66
67    adjust_spans(cfg, width_ctrl, records, &mut widths);
68
69    widths
70}
71
72fn adjust_spans<R>(
73    cfg: &GridConfig,
74    width_ctrl: &CfgWidthFunction,
75    records: &R,
76    widths: &mut [usize],
77) where
78    R: Records,
79{
80    if !cfg.has_column_spans() {
81        return;
82    }
83
84    // The overall width disctribution will be different depend on the order.
85    //
86    // We sort spans in order to prioritize the smaller spans first.
87    let mut spans = cfg
88        .iter_column_spans((records.count_rows(), records.count_columns()))
89        .collect::<Vec<_>>();
90    spans.sort_unstable_by(|a, b| match a.1.cmp(&b.1) {
91        Ordering::Equal => a.0.cmp(&b.0),
92        o => o,
93    });
94
95    // todo: the order is matter here; we need to figure out what is correct.
96    for ((row, col), span) in spans {
97        adjust_range(cfg, width_ctrl, records, row, col, col + span, widths);
98    }
99}
100
101fn adjust_range<R>(
102    cfg: &GridConfig,
103    width_ctrl: &CfgWidthFunction,
104    records: &R,
105    row: usize,
106    start: usize,
107    end: usize,
108    widths: &mut [usize],
109) where
110    R: Records,
111{
112    let max_span_width = get_cell_width(cfg, records, (row, start), width_ctrl);
113    let range_width = range_width(cfg, start, end, widths);
114
115    if range_width >= max_span_width {
116        return;
117    }
118
119    inc_range_width(widths, max_span_width - range_width, start, end);
120}
121
122fn inc_range_width(widths: &mut [usize], size: usize, start: usize, end: usize) {
123    if widths.is_empty() {
124        return;
125    }
126
127    let span = end - start;
128    let one = size / span;
129    let rest = size - span * one;
130
131    let mut i = start;
132    while i < end {
133        if i == start {
134            widths[i] += one + rest;
135        } else {
136            widths[i] += one;
137        }
138
139        i += 1;
140    }
141}
142
143fn is_simple_cell(cfg: &GridConfig, pos: Position, shape: (usize, usize)) -> bool {
144    cfg.is_cell_visible(pos, shape) && matches!(cfg.get_column_span(pos, shape), None | Some(1))
145}
146
147fn get_cell_width<R>(
148    cfg: &GridConfig,
149    records: &R,
150    pos: Position,
151    width_ctrl: &CfgWidthFunction,
152) -> usize
153where
154    R: Records,
155{
156    let width = records.get_width(pos, width_ctrl);
157    let padding = get_cell_padding(cfg, pos);
158    width + padding
159}
160
161fn get_cell_padding(cfg: &GridConfig, pos: Position) -> usize {
162    let padding = cfg.get_padding(pos.into());
163    padding.left.size + padding.right.size
164}
165
166fn range_width(grid: &GridConfig, start: usize, end: usize, widths: &[usize]) -> usize {
167    let count_borders = count_borders_in_range(grid, start, end, widths.len());
168    let range_width = widths[start..end].iter().sum::<usize>();
169    count_borders + range_width
170}
171
172fn count_borders_in_range(
173    cfg: &GridConfig,
174    start: usize,
175    end: usize,
176    count_columns: usize,
177) -> usize {
178    (start..end)
179        .skip(1)
180        .filter(|&i| cfg.has_vertical(i, count_columns))
181        .count()
182}