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}