blob: 32f820aefb567c7c59fe27a463f6b599b96ef290 [file] [log] [blame]
//! This module contains a [`Split`] setting which is used to
//! format the cells of a [`Table`] by a provided index, direction, behavior, and display preference.
//!
//! [`Table`]: crate::Table
use core::ops::Range;
use papergrid::{config::Position, records::PeekableRecords};
use crate::grid::records::{ExactRecords, Records, Resizable};
use super::TableOption;
#[derive(Debug, Clone, Copy)]
enum Direction {
Column,
Row,
}
#[derive(Debug, Clone, Copy)]
enum Behavior {
Concat,
Zip,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum Display {
Clean,
Retain,
}
/// Returns a new [`Table`] formatted with several optional parameters.
///
/// The required index parameter determines how many columns/rows a table will be redistributed into.
///
/// - index
/// - direction
/// - behavior
/// - display
///
/// # Example
///
/// ```rust,no_run
/// use std::iter::FromIterator;
/// use tabled::{
/// settings::split::Split,
/// Table,
/// };
///
/// let mut table = Table::from_iter(['a'..='z']);
/// let table = table.with(Split::column(4)).to_string();
///
/// assert_eq!(table, "+---+---+---+---+\n\
/// | a | b | c | d |\n\
/// +---+---+---+---+\n\
/// | e | f | g | h |\n\
/// +---+---+---+---+\n\
/// | i | j | k | l |\n\
/// +---+---+---+---+\n\
/// | m | n | o | p |\n\
/// +---+---+---+---+\n\
/// | q | r | s | t |\n\
/// +---+---+---+---+\n\
/// | u | v | w | x |\n\
/// +---+---+---+---+\n\
/// | y | z | | |\n\
/// +---+---+---+---+")
/// ```
///
/// [`Table`]: crate::Table
#[derive(Debug, Clone, Copy)]
pub struct Split {
direction: Direction,
behavior: Behavior,
display: Display,
index: usize,
}
impl Split {
/// Returns a new [`Table`] split on the column at the provided index.
///
/// The column found at that index becomes the new right-most column in the returned table.
/// Columns found beyond the index are redistributed into the table based on other defined
/// parameters.
///
/// ```rust,no_run
/// # use tabled::settings::split::Split;
/// Split::column(4);
/// ```
///
/// [`Table`]: crate::Table
pub fn column(index: usize) -> Self {
Split {
direction: Direction::Column,
behavior: Behavior::Zip,
display: Display::Clean,
index,
}
}
/// Returns a new [`Table`] split on the row at the provided index.
///
/// The row found at that index becomes the new bottom row in the returned table.
/// Rows found beyond the index are redistributed into the table based on other defined
/// parameters.
///
/// ```rust,no_run
/// # use tabled::settings::split::Split;
/// Split::row(4);
/// ```
///
/// [`Table`]: crate::Table
pub fn row(index: usize) -> Self {
Split {
direction: Direction::Row,
behavior: Behavior::Zip,
display: Display::Clean,
index,
}
}
/// Returns a split [`Table`] with the redistributed cells pushed to the back of the new shape.
///
/// ```text
/// +---+---+
/// | a | b |
/// +---+---+
/// +---+---+---+---+---+ | f | g |
/// | a | b | c | d | e | Split::column(2).concat() +---+---+
/// +---+---+---+---+---+ => | c | d |
/// | f | g | h | i | j | +---+---+
/// +---+---+---+---+---+ | h | i |
/// +---+---+
/// | e | |
/// +---+---+
/// | j | |
/// +---+---+
/// ```
///
/// [`Table`]: crate::Table
pub fn concat(self) -> Self {
Self {
behavior: Behavior::Concat,
..self
}
}
/// Returns a split [`Table`] with the redistributed cells inserted behind
/// the first correlating column/row one after another.
///
/// ```text
/// +---+---+
/// | a | b |
/// +---+---+
/// +---+---+---+---+---+ | c | d |
/// | a | b | c | d | e | Split::column(2).zip() +---+---+
/// +---+---+---+---+---+ => | e | |
/// | f | g | h | i | j | +---+---+
/// +---+---+---+---+---+ | f | g |
/// +---+---+
/// | h | i |
/// +---+---+
/// | j | |
/// +---+---+
/// ```
///
/// [`Table`]: crate::Table
pub fn zip(self) -> Self {
Self {
behavior: Behavior::Zip,
..self
}
}
/// Returns a split [`Table`] with the empty columns/rows filtered out.
///
/// ```text
///
///
/// +---+---+---+
/// +---+---+---+---+---+ | a | b | c |
/// | a | b | c | d | e | Split::column(3).clean() +---+---+---+
/// +---+---+---+---+---+ => | d | e | |
/// | f | g | h | | | +---+---+---+
/// +---+---+---+---+---+ | f | g | h |
/// ^ ^ +---+---+---+
/// these cells are filtered
/// from the resulting table
/// ```
///
/// ## Notes
///
/// This is apart of the default configuration for Split.
///
/// See [`retain`] for an alternative display option.
///
/// [`Table`]: crate::Table
/// [`retain`]: crate::settings::split::Split::retain
pub fn clean(self) -> Self {
Self {
display: Display::Clean,
..self
}
}
/// Returns a split [`Table`] with all cells retained.
///
/// ```text
/// +---+---+---+
/// | a | b | c |
/// +---+---+---+
/// +---+---+---+---+---+ | d | e | |
/// | a | b | c | d | e | Split::column(3).retain() +---+---+---+
/// +---+---+---+---+---+ => | f | g | h |
/// | f | g | h | | | +---+---+---+
/// +---+---+---+---+---+ |-----------> | | | |
/// ^ ^ | +---+---+---+
/// |___|_____cells are kept!
/// ```
///
/// ## Notes
///
/// See [`clean`] for an alternative display option.
///
/// [`Table`]: crate::Table
/// [`clean`]: crate::settings::split::Split::clean
pub fn retain(self) -> Self {
Self {
display: Display::Retain,
..self
}
}
}
impl<R, D, Cfg> TableOption<R, D, Cfg> for Split
where
R: Records + ExactRecords + Resizable + PeekableRecords,
{
fn change(self, records: &mut R, _: &mut Cfg, _: &mut D) {
// variables
let Split {
direction,
behavior,
display,
index: section_length,
} = self;
let mut filtered_sections = 0;
// early return check
if records.count_columns() == 0 || records.count_rows() == 0 || section_length == 0 {
return;
}
// computed variables
let (primary_length, secondary_length) = compute_length_arrangement(records, direction);
let sections_per_direction = ceil_div(primary_length, section_length);
let (outer_range, inner_range) =
compute_range_order(secondary_length, sections_per_direction, behavior);
// work
for outer_index in outer_range {
let from_secondary_index = outer_index * sections_per_direction - filtered_sections;
for inner_index in inner_range.clone() {
let (section_index, from_secondary_index, to_secondary_index) =
compute_range_variables(
outer_index,
inner_index,
secondary_length,
from_secondary_index,
sections_per_direction,
filtered_sections,
behavior,
);
match (direction, behavior) {
(Direction::Column, Behavior::Concat) => records.push_row(),
(Direction::Column, Behavior::Zip) => records.insert_row(to_secondary_index),
(Direction::Row, Behavior::Concat) => records.push_column(),
(Direction::Row, Behavior::Zip) => records.insert_column(to_secondary_index),
}
let section_is_empty = copy_section(
records,
section_length,
section_index,
primary_length,
from_secondary_index,
to_secondary_index,
direction,
);
if section_is_empty && display == Display::Clean {
delete(records, to_secondary_index, direction);
filtered_sections += 1;
}
}
}
cleanup(records, section_length, primary_length, direction);
}
}
/// Determine which direction should be considered the primary, and which the secondary based on direction
fn compute_length_arrangement<R>(records: &mut R, direction: Direction) -> (usize, usize)
where
R: Records + ExactRecords,
{
match direction {
Direction::Column => (records.count_columns(), records.count_rows()),
Direction::Row => (records.count_rows(), records.count_columns()),
}
}
/// reduce the table size to the length of the index in the specified direction
fn cleanup<R>(records: &mut R, section_length: usize, primary_length: usize, direction: Direction)
where
R: Resizable,
{
for segment in (section_length..primary_length).rev() {
match direction {
Direction::Column => records.remove_column(segment),
Direction::Row => records.remove_row(segment),
}
}
}
/// Delete target index row or column
fn delete<R>(records: &mut R, target_index: usize, direction: Direction)
where
R: Resizable,
{
match direction {
Direction::Column => records.remove_row(target_index),
Direction::Row => records.remove_column(target_index),
}
}
/// copy cells to new location
///
/// returns if the copied section was entirely blank
fn copy_section<R>(
records: &mut R,
section_length: usize,
section_index: usize,
primary_length: usize,
from_secondary_index: usize,
to_secondary_index: usize,
direction: Direction,
) -> bool
where
R: ExactRecords + Resizable + PeekableRecords,
{
let mut section_is_empty = true;
for to_primary_index in 0..section_length {
let from_primary_index = to_primary_index + section_index * section_length;
if from_primary_index < primary_length {
let from_position =
format_position(direction, from_primary_index, from_secondary_index);
if records.get_text(from_position) != "" {
section_is_empty = false;
}
records.swap(
from_position,
format_position(direction, to_primary_index, to_secondary_index),
);
}
}
section_is_empty
}
/// determine section over direction or vice versa based on behavior
fn compute_range_order(
direction_length: usize,
sections_per_direction: usize,
behavior: Behavior,
) -> (Range<usize>, Range<usize>) {
match behavior {
Behavior::Concat => (1..sections_per_direction, 0..direction_length),
Behavior::Zip => (0..direction_length, 1..sections_per_direction),
}
}
/// helper function for shimming both behaviors to work within a single nested loop
fn compute_range_variables(
outer_index: usize,
inner_index: usize,
direction_length: usize,
from_secondary_index: usize,
sections_per_direction: usize,
filtered_sections: usize,
behavior: Behavior,
) -> (usize, usize, usize) {
match behavior {
Behavior::Concat => (
outer_index,
inner_index,
inner_index + outer_index * direction_length - filtered_sections,
),
Behavior::Zip => (
inner_index,
from_secondary_index,
outer_index * sections_per_direction + inner_index - filtered_sections,
),
}
}
/// utility for arguments of a position easily
fn format_position(direction: Direction, primary_index: usize, secondary_index: usize) -> Position {
match direction {
Direction::Column => (secondary_index, primary_index),
Direction::Row => (primary_index, secondary_index),
}
}
/// ceil division utility because the std lib ceil_div isn't stable yet
fn ceil_div(x: usize, y: usize) -> usize {
debug_assert!(x != 0);
1 + ((x - 1) / y)
}