tabled/settings/split/mod.rs
1//! This module contains a [`Split`] setting which is used to
2//! format the cells of a [`Table`] by a provided index, direction, behavior, and display preference.
3//!
4//! [`Table`]: crate::Table
5
6use core::ops::Range;
7
8use crate::grid::{
9 config::Position,
10 records::{ExactRecords, PeekableRecords, Records, Resizable},
11};
12
13use super::TableOption;
14
15#[derive(Debug, Clone, Copy)]
16enum Direction {
17 Column,
18 Row,
19}
20
21#[derive(Debug, Clone, Copy)]
22enum Behavior {
23 Concat,
24 Zip,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq)]
28enum Display {
29 Clean,
30 Retain,
31}
32
33/// Returns a new [`Table`] formatted with several optional parameters.
34///
35/// The required index parameter determines how many columns/rows a table will be redistributed into.
36///
37/// - index
38/// - direction
39/// - behavior
40/// - display
41///
42/// #### Directions
43///
44/// Direction functions are the entry point for the `Split` setting.
45///
46/// There are two directions available: `column` and `row`.
47///
48/// ```rust
49/// use std::iter::FromIterator;
50/// use tabled::{Table, settings::split::Split};
51///
52/// let mut table = Table::from_iter(['a'..='z']);
53/// table.with(Split::column(12));
54/// table.with(Split::row(2));
55/// ```
56///
57/// ```text
58/// ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
59/// │ a │ b │ c │ d │ e │ f │ g │ h │ i │ j │ k │ l │ m │ n │ o │ p │ q │ r │ s │ t │ u │ v │ w │ x │ y │ z │
60/// └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
61/// ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
62/// │ a │ b │ c │ d │ e │ f │ g │ h │ i │ j │ k │ l │
63/// ├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
64/// │ m │ n │ o │ p │ q │ r │ s │ t │ u │ v │ w │ x │
65/// ├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
66/// │ y │ z │ │ │ │ │ │ │ │ │ │ │<- y and z act as anchors to new empty cells
67/// └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘ to conform to the new shape
68/// ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
69/// │ a │ y │ b │ z │ c │ d │ e │ f │ g │ h │ i │ j │ k │ l │
70/// ├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤<- Display::Clean removes empty cells that would be anchors otherwise
71/// │ m │ │ n │ │ o │ p │ q │ r │ s │ t │ u │ v │ w │ x │
72/// └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
73/// ^anchors^
74/// ```
75///
76///
77/// #### Behaviors
78///
79/// Behaviors determine how cells attempt to conform to the new tables shape.
80///
81/// There are two behaviors available: `zip` and `concat`.
82///
83/// `zip` is the default behavior.
84///
85/// ```rust
86/// use std::iter::FromIterator;
87/// use tabled::{Table, settings::split::Split};
88///
89/// let mut table = Table::from_iter(['a'..='z']);
90/// table.with(Split::column(2).concat());
91/// table.with(Split::column(2).zip());
92/// ```
93///
94/// ```text
95/// +---+---+
96/// | a | b |
97/// +---+---+
98/// +---+---+---+---+---+ | f | g |
99/// | a | b | c | d | e | Split::column(2).concat() +---+---+
100/// +---+---+---+---+---+ => | c | d |
101/// | f | g | h | i | j | +---+---+
102/// +---+---+---+---+---+ | h | i |
103/// +---+---+
104/// | e | |
105/// +---+---+
106/// | j | |
107/// +---+---+
108///
109/// sect 3 +---+---+
110/// sect 1 sect 2 (anchors) | a | b |
111/// / \ / \ / \ +---+---+
112/// +---+---+---+---+---+ | c | d |
113/// | a | b | c | d | e | Split::column(2).zip() +---+---+
114/// +---+---+---+---+---+ => | e | |
115/// | f | g | h | i | j | +---+---+
116/// +---+---+---+---+---+ | f | g |
117/// +---+---+
118/// | h | i |
119/// +---+---+
120/// | j | |
121/// +---+---+
122/// ```
123///
124/// #### Displays
125///
126/// Display functions give the user the choice to `retain` or `clean` empty sections in a `Split` table result.
127///
128/// - `retain` does not filter any existing or newly added cells when conforming to a new shape.
129///
130/// - `clean` filters out empty columns/rows from the output and prevents empty cells from acting as anchors to newly inserted cells.
131///
132/// `clean` is the default `Display`.
133///
134/// ```rust
135/// use std::iter::FromIterator;
136/// use tabled::{
137/// settings::{split::Split, style::Style},
138/// Table,
139/// };
140/// let mut table = Table::from_iter(['a'..='z']);
141/// table.with(Split::column(25)).with(Style::modern());
142/// table.clone().with(Split::column(1).concat().retain());
143/// table.clone().with(Split::column(1).concat()); // .clean() is not necessary as it is the default display property
144/// ```
145///
146/// ```text
147/// ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
148/// │ a │ b │ c │ d │ e │ f │ g │ h │ i │ j │ k │ l │ m │ n │ o │ p │ q │ r │ s │ t │ u │ v │ w │ x │ y │
149/// ├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
150/// │ z │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │<- lots of extra cells generated
151/// └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
152/// ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
153/// │ a │ b │ c │ d │ e │ f │ g │ h │ i │ j │ k │ l │ m │ n │ o │ p │ q │ r │ s │ t │ u │ v │ w │ x │ y │ z │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
154/// └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
155/// ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐ ^ cells retained during concatenation
156/// │ a │ b │ c │ d │ e │ f │ g │ h │ i │ j │ k │ l │ m │ n │ o │ p │ q │ r │ s │ t │ u │ v │ w │ x │ y │ z │
157/// └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘<- cells removed during concatenation
158/// ```
159///
160///
161/// # Example
162///
163/// ```rust
164/// use std::iter::FromIterator;
165/// use tabled::{
166/// settings::split::Split,
167/// Table,
168/// };
169///
170/// let mut table = Table::from_iter(['a'..='z']);
171/// let table = table.with(Split::column(4)).to_string();
172///
173/// assert_eq!(table, "+---+---+---+---+\n\
174/// | a | b | c | d |\n\
175/// +---+---+---+---+\n\
176/// | e | f | g | h |\n\
177/// +---+---+---+---+\n\
178/// | i | j | k | l |\n\
179/// +---+---+---+---+\n\
180/// | m | n | o | p |\n\
181/// +---+---+---+---+\n\
182/// | q | r | s | t |\n\
183/// +---+---+---+---+\n\
184/// | u | v | w | x |\n\
185/// +---+---+---+---+\n\
186/// | y | z | | |\n\
187/// +---+---+---+---+")
188/// ```
189///
190/// [`Table`]: crate::Table
191#[derive(Debug, Clone, Copy)]
192pub struct Split {
193 direction: Direction,
194 behavior: Behavior,
195 display: Display,
196 index: usize,
197}
198
199impl Split {
200 /// Returns a new [`Table`] split on the column at the provided index.
201 ///
202 /// The column found at that index becomes the new right-most column in the returned table.
203 /// Columns found beyond the index are redistributed into the table based on other defined
204 /// parameters.
205 ///
206 /// ```rust,no_run
207 /// # use tabled::settings::split::Split;
208 /// Split::column(4);
209 /// ```
210 ///
211 /// [`Table`]: crate::Table
212 pub fn column(index: usize) -> Self {
213 Split {
214 direction: Direction::Column,
215 behavior: Behavior::Zip,
216 display: Display::Clean,
217 index,
218 }
219 }
220
221 /// Returns a new [`Table`] split on the row at the provided index.
222 ///
223 /// The row found at that index becomes the new bottom row in the returned table.
224 /// Rows found beyond the index are redistributed into the table based on other defined
225 /// parameters.
226 ///
227 /// ```rust,no_run
228 /// # use tabled::settings::split::Split;
229 /// Split::row(4);
230 /// ```
231 ///
232 /// [`Table`]: crate::Table
233 pub fn row(index: usize) -> Self {
234 Split {
235 direction: Direction::Row,
236 behavior: Behavior::Zip,
237 display: Display::Clean,
238 index,
239 }
240 }
241
242 /// Returns a split [`Table`] with the redistributed cells pushed to the back of the new shape.
243 ///
244 /// ```text
245 /// +---+---+
246 /// | a | b |
247 /// +---+---+
248 /// +---+---+---+---+---+ | f | g |
249 /// | a | b | c | d | e | Split::column(2).concat() +---+---+
250 /// +---+---+---+---+---+ => | c | d |
251 /// | f | g | h | i | j | +---+---+
252 /// +---+---+---+---+---+ | h | i |
253 /// +---+---+
254 /// | e | |
255 /// +---+---+
256 /// | j | |
257 /// +---+---+
258 /// ```
259 ///
260 /// [`Table`]: crate::Table
261 pub fn concat(self) -> Self {
262 Self {
263 behavior: Behavior::Concat,
264 ..self
265 }
266 }
267
268 /// Returns a split [`Table`] with the redistributed cells inserted behind
269 /// the first correlating column/row one after another.
270 ///
271 /// ```text
272 /// +---+---+
273 /// | a | b |
274 /// +---+---+
275 /// +---+---+---+---+---+ | c | d |
276 /// | a | b | c | d | e | Split::column(2).zip() +---+---+
277 /// +---+---+---+---+---+ => | e | |
278 /// | f | g | h | i | j | +---+---+
279 /// +---+---+---+---+---+ | f | g |
280 /// +---+---+
281 /// | h | i |
282 /// +---+---+
283 /// | j | |
284 /// +---+---+
285 /// ```
286 ///
287 /// [`Table`]: crate::Table
288 pub fn zip(self) -> Self {
289 Self {
290 behavior: Behavior::Zip,
291 ..self
292 }
293 }
294
295 /// Returns a split [`Table`] with the empty columns/rows filtered out.
296 ///
297 /// ```text
298 ///
299 ///
300 /// +---+---+---+
301 /// +---+---+---+---+---+ | a | b | c |
302 /// | a | b | c | d | e | Split::column(3).clean() +---+---+---+
303 /// +---+---+---+---+---+ => | d | e | |
304 /// | f | g | h | | | +---+---+---+
305 /// +---+---+---+---+---+ | f | g | h |
306 /// ^ ^ +---+---+---+
307 /// these cells are filtered
308 /// from the resulting table
309 /// ```
310 ///
311 /// ## Notes
312 ///
313 /// This is apart of the default configuration for Split.
314 ///
315 /// See [`retain`] for an alternative display option.
316 ///
317 /// [`Table`]: crate::Table
318 /// [`retain`]: crate::settings::split::Split::retain
319 pub fn clean(self) -> Self {
320 Self {
321 display: Display::Clean,
322 ..self
323 }
324 }
325
326 /// Returns a split [`Table`] with all cells retained.
327 ///
328 /// ```text
329 /// +---+---+---+
330 /// | a | b | c |
331 /// +---+---+---+
332 /// +---+---+---+---+---+ | d | e | |
333 /// | a | b | c | d | e | Split::column(3).retain() +---+---+---+
334 /// +---+---+---+---+---+ => | f | g | h |
335 /// | f | g | h | | | +---+---+---+
336 /// +---+---+---+---+---+ |-----------> | | | |
337 /// ^ ^ | +---+---+---+
338 /// |___|_____cells are kept!
339 /// ```
340 ///
341 /// ## Notes
342 ///
343 /// See [`clean`] for an alternative display option.
344 ///
345 /// [`Table`]: crate::Table
346 /// [`clean`]: crate::settings::split::Split::clean
347 pub fn retain(self) -> Self {
348 Self {
349 display: Display::Retain,
350 ..self
351 }
352 }
353}
354
355impl<R, D, C> TableOption<R, C, D> for Split
356where
357 R: Records + ExactRecords + Resizable + PeekableRecords,
358{
359 fn change(self, records: &mut R, _: &mut C, _: &mut D) {
360 // variables
361 let Split {
362 direction,
363 behavior,
364 display,
365 index: section_length,
366 } = self;
367 let mut filtered_sections = 0;
368
369 // early return check
370 if records.count_columns() == 0 || records.count_rows() == 0 || section_length == 0 {
371 return;
372 }
373
374 // computed variables
375 let (primary_length, secondary_length) = compute_length_arrangement(records, direction);
376 let sections_per_direction = ceil_div(primary_length, section_length);
377 let (outer_range, inner_range) =
378 compute_range_order(secondary_length, sections_per_direction, behavior);
379
380 // work
381 for outer_index in outer_range {
382 let from_secondary_index = outer_index * sections_per_direction - filtered_sections;
383 for inner_index in inner_range.clone() {
384 let (section_index, from_secondary_index, to_secondary_index) =
385 compute_range_variables(
386 outer_index,
387 inner_index,
388 secondary_length,
389 from_secondary_index,
390 sections_per_direction,
391 filtered_sections,
392 behavior,
393 );
394
395 match (direction, behavior) {
396 (Direction::Column, Behavior::Concat) => records.push_row(),
397 (Direction::Column, Behavior::Zip) => records.insert_row(to_secondary_index),
398 (Direction::Row, Behavior::Concat) => records.push_column(),
399 (Direction::Row, Behavior::Zip) => records.insert_column(to_secondary_index),
400 }
401
402 let section_is_empty = copy_section(
403 records,
404 section_length,
405 section_index,
406 primary_length,
407 from_secondary_index,
408 to_secondary_index,
409 direction,
410 );
411
412 if section_is_empty && display == Display::Clean {
413 delete(records, to_secondary_index, direction);
414 filtered_sections += 1;
415 }
416 }
417 }
418
419 cleanup(records, section_length, primary_length, direction);
420 }
421}
422
423/// Determine which direction should be considered the primary, and which the secondary based on direction
424fn compute_length_arrangement<R>(records: &mut R, direction: Direction) -> (usize, usize)
425where
426 R: Records + ExactRecords,
427{
428 match direction {
429 Direction::Column => (records.count_columns(), records.count_rows()),
430 Direction::Row => (records.count_rows(), records.count_columns()),
431 }
432}
433
434/// reduce the table size to the length of the index in the specified direction
435fn cleanup<R>(records: &mut R, section_length: usize, primary_length: usize, direction: Direction)
436where
437 R: Resizable,
438{
439 for segment in (section_length..primary_length).rev() {
440 match direction {
441 Direction::Column => records.remove_column(segment),
442 Direction::Row => records.remove_row(segment),
443 }
444 }
445}
446
447/// Delete target index row or column
448fn delete<R>(records: &mut R, target_index: usize, direction: Direction)
449where
450 R: Resizable,
451{
452 match direction {
453 Direction::Column => records.remove_row(target_index),
454 Direction::Row => records.remove_column(target_index),
455 }
456}
457
458/// copy cells to new location
459///
460/// returns if the copied section was entirely blank
461fn copy_section<R>(
462 records: &mut R,
463 section_length: usize,
464 section_index: usize,
465 primary_length: usize,
466 from_secondary_index: usize,
467 to_secondary_index: usize,
468 direction: Direction,
469) -> bool
470where
471 R: ExactRecords + Resizable + PeekableRecords,
472{
473 let mut section_is_empty = true;
474 for to_primary_index in 0..section_length {
475 let from_primary_index = to_primary_index + section_index * section_length;
476
477 if from_primary_index < primary_length {
478 let from_position =
479 format_position(direction, from_primary_index, from_secondary_index);
480 if !records.get_text(from_position).is_empty() {
481 section_is_empty = false;
482 }
483 records.swap(
484 from_position,
485 format_position(direction, to_primary_index, to_secondary_index),
486 );
487 }
488 }
489 section_is_empty
490}
491
492/// determine section over direction or vice versa based on behavior
493fn compute_range_order(
494 direction_length: usize,
495 sections_per_direction: usize,
496 behavior: Behavior,
497) -> (Range<usize>, Range<usize>) {
498 match behavior {
499 Behavior::Concat => (1..sections_per_direction, 0..direction_length),
500 Behavior::Zip => (0..direction_length, 1..sections_per_direction),
501 }
502}
503
504/// helper function for shimming both behaviors to work within a single nested loop
505fn compute_range_variables(
506 outer_index: usize,
507 inner_index: usize,
508 direction_length: usize,
509 from_secondary_index: usize,
510 sections_per_direction: usize,
511 filtered_sections: usize,
512 behavior: Behavior,
513) -> (usize, usize, usize) {
514 match behavior {
515 Behavior::Concat => (
516 outer_index,
517 inner_index,
518 inner_index + outer_index * direction_length - filtered_sections,
519 ),
520 Behavior::Zip => (
521 inner_index,
522 from_secondary_index,
523 outer_index * sections_per_direction + inner_index - filtered_sections,
524 ),
525 }
526}
527
528/// utility for arguments of a position easily
529fn format_position(direction: Direction, primary_index: usize, secondary_index: usize) -> Position {
530 match direction {
531 Direction::Column => (secondary_index, primary_index).into(),
532 Direction::Row => (primary_index, secondary_index).into(),
533 }
534}
535
536/// ceil division utility because the std lib ceil_div isn't stable yet
537fn ceil_div(x: usize, y: usize) -> usize {
538 debug_assert!(x != 0);
539 1 + ((x - 1) / y)
540}