Skip to content

Commit 07d49c4

Browse files
authored
update width calculations (#48)
* optimize padding size calc * remove separator duplication to reduce memory usage * rework dimensions calculations * add max possible width test * simplify the min_columns calculation and add edge cases tests * fix some comments and remove leftovers * small improvements to variable names and comments
1 parent c8db34b commit 07d49c4

File tree

2 files changed

+127
-86
lines changed

2 files changed

+127
-86
lines changed

src/lib.rs

Lines changed: 50 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ pub struct GridOptions {
7979
#[derive(PartialEq, Eq, Debug)]
8080
struct Dimensions {
8181
/// The number of lines in the grid.
82-
num_lines: usize,
82+
num_rows: usize,
8383

8484
/// The width of each column in the grid. The length of this vector serves
8585
/// as the number of columns.
@@ -113,23 +113,21 @@ impl<T: AsRef<str>> Grid<T> {
113113
pub fn new(cells: Vec<T>, options: GridOptions) -> Self {
114114
let widths: Vec<usize> = cells.iter().map(|c| ansi_width(c.as_ref())).collect();
115115
let widest_cell_width = widths.iter().copied().max().unwrap_or(0);
116-
let width = options.width;
117116

118117
let mut grid = Self {
119118
options,
120119
cells,
121120
widths,
122121
widest_cell_width,
123122
dimensions: Dimensions {
124-
num_lines: 0,
123+
num_rows: 0,
125124
widths: Vec::new(),
126125
},
127126
};
128127

129-
grid.dimensions = grid.width_dimensions(width).unwrap_or(Dimensions {
130-
num_lines: grid.cells.len(),
131-
widths: vec![widest_cell_width],
132-
});
128+
if !grid.cells.is_empty() {
129+
grid.dimensions = grid.width_dimensions();
130+
}
133131

134132
grid
135133
}
@@ -142,7 +140,7 @@ impl<T: AsRef<str>> Grid<T> {
142140

143141
/// The number of rows this display takes up.
144142
pub fn row_count(&self) -> usize {
145-
self.dimensions.num_lines
143+
self.dimensions.num_rows
146144
}
147145

148146
/// The width of each column
@@ -174,99 +172,65 @@ impl<T: AsRef<str>> Grid<T> {
174172
}
175173

176174
Dimensions {
177-
num_lines,
175+
num_rows: num_lines,
178176
widths: column_widths,
179177
}
180178
}
181179

182-
fn theoretical_max_num_lines(&self, maximum_width: usize) -> usize {
183-
// TODO: Make code readable / efficient.
184-
let mut widths = self.widths.clone();
185-
186-
// Sort widths in reverse order
187-
widths.sort_unstable_by(|a, b| b.cmp(a));
188-
189-
let mut col_total_width_so_far = 0;
190-
for (i, &width) in widths.iter().enumerate() {
191-
let adjusted_width = if i == 0 {
192-
width
193-
} else {
194-
width + self.options.filling.width()
180+
fn width_dimensions(&mut self) -> Dimensions {
181+
if self.cells.len() == 1 {
182+
let cell_widths = self.widths[0];
183+
return Dimensions {
184+
num_rows: 1,
185+
widths: vec![cell_widths],
195186
};
196-
if col_total_width_so_far + adjusted_width <= maximum_width {
197-
col_total_width_so_far += adjusted_width;
198-
} else {
199-
return div_ceil(self.cells.len(), i);
200-
}
201187
}
202188

203-
// If we make it to this point, we have exhausted all cells before
204-
// reaching the maximum width; the theoretical max number of lines
205-
// needed to display all cells is 1.
206-
1
207-
}
208-
209-
fn width_dimensions(&self, maximum_width: usize) -> Option<Dimensions> {
210-
if self.widest_cell_width > maximum_width {
211-
// Largest cell is wider than maximum width; it is impossible to fit.
212-
return None;
189+
// Calculate widest column size with separator.
190+
let widest_column = self.widest_cell_width + self.options.filling.width();
191+
// If it exceeds terminal's width, return, since it is impossible to fit.
192+
if widest_column > self.options.width {
193+
return Dimensions {
194+
num_rows: self.cells.len(),
195+
widths: vec![self.widest_cell_width],
196+
};
213197
}
214198

215-
if self.cells.is_empty() {
216-
return Some(Dimensions {
217-
num_lines: 0,
218-
widths: Vec::new(),
219-
});
220-
}
199+
// Calculate the number of columns if all columns had the size of the largest
200+
// column. This is a lower bound on the number of columns.
201+
let min_columns = self
202+
.cells
203+
.len()
204+
.min((self.options.width + self.options.filling.width()) / widest_column);
221205

222-
if self.cells.len() == 1 {
223-
let cell_widths = self.widths[0];
224-
return Some(Dimensions {
225-
num_lines: 1,
226-
widths: vec![cell_widths],
227-
});
228-
}
206+
// Calculate maximum number of lines and columns.
207+
let max_rows = div_ceil(self.cells.len(), min_columns);
229208

230-
let theoretical_max_num_lines = self.theoretical_max_num_lines(maximum_width);
231-
if theoretical_max_num_lines == 1 {
232-
// This if—statement is necessary for the function to work correctly
233-
// for small inputs.
234-
return Some(Dimensions {
235-
num_lines: 1,
236-
widths: self.widths.clone(),
237-
});
238-
}
239-
// Instead of numbers of columns, try to find the fewest number of *lines*
240-
// that the output will fit in.
241-
let mut smallest_dimensions_yet = None;
242-
for num_lines in (1..=theoretical_max_num_lines).rev() {
243-
// The number of columns is the number of cells divided by the number
244-
// of lines, *rounded up*.
245-
let num_columns = div_ceil(self.cells.len(), num_lines);
246-
247-
// Early abort: if there are so many columns that the width of the
248-
// *column separators* is bigger than the width of the screen, then
249-
// don’t even try to tabulate it.
250-
// This is actually a necessary check, because the width is stored as
251-
// a usize, and making it go negative makes it huge instead, but it
252-
// also serves as a speed-up.
253-
let total_separator_width = (num_columns - 1) * self.options.filling.width();
254-
if maximum_width < total_separator_width {
255-
continue;
256-
}
209+
// This is a potential dimension, which can definitely fit all of the cells.
210+
let mut potential_dimension = self.compute_dimensions(max_rows, min_columns);
257211

258-
// Remove the separator width from the available space.
259-
let adjusted_width = maximum_width - total_separator_width;
212+
// If all of the cells can be fit on one line, return immediately.
213+
if max_rows == 1 {
214+
return potential_dimension;
215+
}
260216

261-
let potential_dimensions = self.compute_dimensions(num_lines, num_columns);
262-
if potential_dimensions.widths.iter().sum::<usize>() <= adjusted_width {
263-
smallest_dimensions_yet = Some(potential_dimensions);
264-
} else {
217+
// Try to increase number of columns, to see if new dimension can still fit.
218+
for num_columns in min_columns + 1..self.cells.len() {
219+
let Some(adjusted_width) = self
220+
.options
221+
.width
222+
.checked_sub((num_columns - 1) * self.options.filling.width())
223+
else {
265224
break;
225+
};
226+
let num_rows = div_ceil(self.cells.len(), num_columns);
227+
let new_dimension = self.compute_dimensions(num_rows, num_columns);
228+
if new_dimension.widths.iter().sum::<usize>() <= adjusted_width {
229+
potential_dimension = new_dimension;
266230
}
267231
}
268232

269-
smallest_dimensions_yet
233+
potential_dimension
270234
}
271235
}
272236

@@ -293,7 +257,7 @@ impl<T: AsRef<str>> fmt::Display for Grid<T> {
293257
// get exactly right.
294258
let padding = " ".repeat(self.widest_cell_width + self.options.filling.width());
295259

296-
for y in 0..self.dimensions.num_lines {
260+
for y in 0..self.dimensions.num_rows {
297261
// Current position on the line.
298262
let mut cursor: usize = 0;
299263
for x in 0..self.dimensions.widths.len() {
@@ -302,7 +266,7 @@ impl<T: AsRef<str>> fmt::Display for Grid<T> {
302266
let (current, offset) = match self.options.direction {
303267
Direction::LeftToRight => (y * self.dimensions.widths.len() + x, 1),
304268
Direction::TopToBottom => {
305-
(y + self.dimensions.num_lines * x, self.dimensions.num_lines)
269+
(y + self.dimensions.num_rows * x, self.dimensions.num_rows)
306270
}
307271
};
308272

tests/test.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,83 @@ fn different_size_separator_with_tabs() {
325325
assert_eq!(grid.to_string(), bits);
326326
}
327327

328+
#[test]
329+
fn use_max_possible_width() {
330+
let grid = Grid::new(
331+
vec![
332+
"test1", "test2", "test3", "test4", "test5", "test6", "test7", "test8", "test9",
333+
"test10", "test11",
334+
],
335+
GridOptions {
336+
filling: Filling::Text("||".to_string()),
337+
direction: Direction::LeftToRight,
338+
width: 69,
339+
},
340+
);
341+
342+
let bits = "test1 ||test2 ||test3||test4||test5||test6||test7||test8||test9\ntest10||test11\n";
343+
344+
assert_eq!(grid.to_string(), bits);
345+
assert_eq!(grid.row_count(), 2);
346+
}
347+
348+
#[test]
349+
fn dont_use_max_possible_width() {
350+
let grid = Grid::new(
351+
vec![
352+
"test1", "test2", "test3", "test4", "test5", "test6", "test7", "test8", "test9",
353+
"test10", "test11",
354+
],
355+
GridOptions {
356+
filling: Filling::Text("||".to_string()),
357+
direction: Direction::TopToBottom,
358+
width: 69,
359+
},
360+
);
361+
362+
let bits = "test1||test3||test5||test7||test9 ||test11\ntest2||test4||test6||test8||test10\n";
363+
364+
assert_eq!(grid.to_string(), bits);
365+
assert_eq!(grid.row_count(), 2);
366+
}
367+
368+
#[test]
369+
fn use_minimal_optimal_lines() {
370+
let grid = Grid::new(
371+
vec!["a", "b", "ccc", "ddd"],
372+
GridOptions {
373+
direction: Direction::TopToBottom,
374+
filling: Filling::Spaces(2),
375+
width: 6,
376+
},
377+
);
378+
379+
let expected = "a ccc\nb ddd\n";
380+
assert_eq!(grid.to_string(), expected);
381+
}
382+
383+
#[test]
384+
fn weird_column_edge_case() {
385+
// Here, 5 columns fit while fewer columns don't. So if we exit too early
386+
// while increasing columns, we don't find the optimal solution.
387+
let grid = Grid::new(
388+
vec!["0", "1", "222222222", "333333333", "4", "5", "6", "7", "8"],
389+
GridOptions {
390+
direction: Direction::TopToBottom,
391+
filling: Filling::Spaces(2),
392+
width: 21,
393+
},
394+
);
395+
396+
let expected = "\
397+
0 222222222 4 6 8\n\
398+
1 333333333 5 7\n\
399+
";
400+
401+
println!("{grid}");
402+
assert_eq!(grid.to_string(), expected);
403+
}
404+
328405
// These test are based on the tests in uutils ls, to ensure we won't break
329406
// it while editing this library.
330407
mod uutils_ls {

0 commit comments

Comments
 (0)