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 papergrid::{config::Position, records::PeekableRecords};
9
10use crate::grid::records::{ExactRecords, Records, Resizable};
11
12use super::TableOption;
13
14#[derive(Debug, Clone, Copy)]
15enum Direction {
16 Column,
17 Row,
18}
19
20#[derive(Debug, Clone, Copy)]
21enum Behavior {
22 Concat,
23 Zip,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq)]
27enum Display {
28 Clean,
29 Retain,
30}
31
32/// Returns a new [`Table`] formatted with several optional parameters.
33///
34/// The required index parameter determines how many columns/rows a table will be redistributed into.
35///
36/// - index
37/// - direction
38/// - behavior
39/// - display
40///
41/// # Example
42///
43/// ```rust,no_run
44/// use std::iter::FromIterator;
45/// use tabled::{
46/// settings::split::Split,
47/// Table,
48/// };
49///
50/// let mut table = Table::from_iter(['a'..='z']);
51/// let table = table.with(Split::column(4)).to_string();
52///
53/// assert_eq!(table, "+---+---+---+---+\n\
54/// | a | b | c | d |\n\
55/// +---+---+---+---+\n\
56/// | e | f | g | h |\n\
57/// +---+---+---+---+\n\
58/// | i | j | k | l |\n\
59/// +---+---+---+---+\n\
60/// | m | n | o | p |\n\
61/// +---+---+---+---+\n\
62/// | q | r | s | t |\n\
63/// +---+---+---+---+\n\
64/// | u | v | w | x |\n\
65/// +---+---+---+---+\n\
66/// | y | z | | |\n\
67/// +---+---+---+---+")
68/// ```
69///
70/// [`Table`]: crate::Table
71#[derive(Debug, Clone, Copy)]
72pub struct Split {
73 direction: Direction,
74 behavior: Behavior,
75 display: Display,
76 index: usize,
77}
78
79impl Split {
80 /// Returns a new [`Table`] split on the column at the provided index.
81 ///
82 /// The column found at that index becomes the new right-most column in the returned table.
83 /// Columns found beyond the index are redistributed into the table based on other defined
84 /// parameters.
85 ///
86 /// ```rust,no_run
87 /// # use tabled::settings::split::Split;
88 /// Split::column(4);
89 /// ```
90 ///
91 /// [`Table`]: crate::Table
92 pub fn column(index: usize) -> Self {
93 Split {
94 direction: Direction::Column,
95 behavior: Behavior::Zip,
96 display: Display::Clean,
97 index,
98 }
99 }
100
101 /// Returns a new [`Table`] split on the row at the provided index.
102 ///
103 /// The row found at that index becomes the new bottom row in the returned table.
104 /// Rows found beyond the index are redistributed into the table based on other defined
105 /// parameters.
106 ///
107 /// ```rust,no_run
108 /// # use tabled::settings::split::Split;
109 /// Split::row(4);
110 /// ```
111 ///
112 /// [`Table`]: crate::Table
113 pub fn row(index: usize) -> Self {
114 Split {
115 direction: Direction::Row,
116 behavior: Behavior::Zip,
117 display: Display::Clean,
118 index,
119 }
120 }
121
122 /// Returns a split [`Table`] with the redistributed cells pushed to the back of the new shape.
123 ///
124 /// ```text
125 /// +---+---+
126 /// | a | b |
127 /// +---+---+
128 /// +---+---+---+---+---+ | f | g |
129 /// | a | b | c | d | e | Split::column(2).concat() +---+---+
130 /// +---+---+---+---+---+ => | c | d |
131 /// | f | g | h | i | j | +---+---+
132 /// +---+---+---+---+---+ | h | i |
133 /// +---+---+
134 /// | e | |
135 /// +---+---+
136 /// | j | |
137 /// +---+---+
138 /// ```
139 ///
140 /// [`Table`]: crate::Table
141 pub fn concat(self) -> Self {
142 Self {
143 behavior: Behavior::Concat,
144 ..self
145 }
146 }
147
148 /// Returns a split [`Table`] with the redistributed cells inserted behind
149 /// the first correlating column/row one after another.
150 ///
151 /// ```text
152 /// +---+---+
153 /// | a | b |
154 /// +---+---+
155 /// +---+---+---+---+---+ | c | d |
156 /// | a | b | c | d | e | Split::column(2).zip() +---+---+
157 /// +---+---+---+---+---+ => | e | |
158 /// | f | g | h | i | j | +---+---+
159 /// +---+---+---+---+---+ | f | g |
160 /// +---+---+
161 /// | h | i |
162 /// +---+---+
163 /// | j | |
164 /// +---+---+
165 /// ```
166 ///
167 /// [`Table`]: crate::Table
168 pub fn zip(self) -> Self {
169 Self {
170 behavior: Behavior::Zip,
171 ..self
172 }
173 }
174
175 /// Returns a split [`Table`] with the empty columns/rows filtered out.
176 ///
177 /// ```text
178 ///
179 ///
180 /// +---+---+---+
181 /// +---+---+---+---+---+ | a | b | c |
182 /// | a | b | c | d | e | Split::column(3).clean() +---+---+---+
183 /// +---+---+---+---+---+ => | d | e | |
184 /// | f | g | h | | | +---+---+---+
185 /// +---+---+---+---+---+ | f | g | h |
186 /// ^ ^ +---+---+---+
187 /// these cells are filtered
188 /// from the resulting table
189 /// ```
190 ///
191 /// ## Notes
192 ///
193 /// This is apart of the default configuration for Split.
194 ///
195 /// See [`retain`] for an alternative display option.
196 ///
197 /// [`Table`]: crate::Table
198 /// [`retain`]: crate::settings::split::Split::retain
199 pub fn clean(self) -> Self {
200 Self {
201 display: Display::Clean,
202 ..self
203 }
204 }
205
206 /// Returns a split [`Table`] with all cells retained.
207 ///
208 /// ```text
209 /// +---+---+---+
210 /// | a | b | c |
211 /// +---+---+---+
212 /// +---+---+---+---+---+ | d | e | |
213 /// | a | b | c | d | e | Split::column(3).retain() +---+---+---+
214 /// +---+---+---+---+---+ => | f | g | h |
215 /// | f | g | h | | | +---+---+---+
216 /// +---+---+---+---+---+ |-----------> | | | |
217 /// ^ ^ | +---+---+---+
218 /// |___|_____cells are kept!
219 /// ```
220 ///
221 /// ## Notes
222 ///
223 /// See [`clean`] for an alternative display option.
224 ///
225 /// [`Table`]: crate::Table
226 /// [`clean`]: crate::settings::split::Split::clean
227 pub fn retain(self) -> Self {
228 Self {
229 display: Display::Retain,
230 ..self
231 }
232 }
233}
234
235impl<R, D, Cfg> TableOption<R, D, Cfg> for Split
236where
237 R: Records + ExactRecords + Resizable + PeekableRecords,
238{
239 fn change(self, records: &mut R, _: &mut Cfg, _: &mut D) {
240 // variables
241 let Split {
242 direction,
243 behavior,
244 display,
245 index: section_length,
246 } = self;
247 let mut filtered_sections = 0;
248
249 // early return check
250 if records.count_columns() == 0 || records.count_rows() == 0 || section_length == 0 {
251 return;
252 }
253
254 // computed variables
255 let (primary_length, secondary_length) = compute_length_arrangement(records, direction);
256 let sections_per_direction = ceil_div(primary_length, section_length);
257 let (outer_range, inner_range) =
258 compute_range_order(secondary_length, sections_per_direction, behavior);
259
260 // work
261 for outer_index in outer_range {
262 let from_secondary_index = outer_index * sections_per_direction - filtered_sections;
263 for inner_index in inner_range.clone() {
264 let (section_index, from_secondary_index, to_secondary_index) =
265 compute_range_variables(
266 outer_index,
267 inner_index,
268 secondary_length,
269 from_secondary_index,
270 sections_per_direction,
271 filtered_sections,
272 behavior,
273 );
274
275 match (direction, behavior) {
276 (Direction::Column, Behavior::Concat) => records.push_row(),
277 (Direction::Column, Behavior::Zip) => records.insert_row(to_secondary_index),
278 (Direction::Row, Behavior::Concat) => records.push_column(),
279 (Direction::Row, Behavior::Zip) => records.insert_column(to_secondary_index),
280 }
281
282 let section_is_empty = copy_section(
283 records,
284 section_length,
285 section_index,
286 primary_length,
287 from_secondary_index,
288 to_secondary_index,
289 direction,
290 );
291
292 if section_is_empty && display == Display::Clean {
293 delete(records, to_secondary_index, direction);
294 filtered_sections += 1;
295 }
296 }
297 }
298
299 cleanup(records, section_length, primary_length, direction);
300 }
301}
302
303/// Determine which direction should be considered the primary, and which the secondary based on direction
304fn compute_length_arrangement<R>(records: &mut R, direction: Direction) -> (usize, usize)
305where
306 R: Records + ExactRecords,
307{
308 match direction {
309 Direction::Column => (records.count_columns(), records.count_rows()),
310 Direction::Row => (records.count_rows(), records.count_columns()),
311 }
312}
313
314/// reduce the table size to the length of the index in the specified direction
315fn cleanup<R>(records: &mut R, section_length: usize, primary_length: usize, direction: Direction)
316where
317 R: Resizable,
318{
319 for segment: usize in (section_length..primary_length).rev() {
320 match direction {
321 Direction::Column => records.remove_column(segment),
322 Direction::Row => records.remove_row(segment),
323 }
324 }
325}
326
327/// Delete target index row or column
328fn delete<R>(records: &mut R, target_index: usize, direction: Direction)
329where
330 R: Resizable,
331{
332 match direction {
333 Direction::Column => records.remove_row(target_index),
334 Direction::Row => records.remove_column(target_index),
335 }
336}
337
338/// copy cells to new location
339///
340/// returns if the copied section was entirely blank
341fn copy_section<R>(
342 records: &mut R,
343 section_length: usize,
344 section_index: usize,
345 primary_length: usize,
346 from_secondary_index: usize,
347 to_secondary_index: usize,
348 direction: Direction,
349) -> bool
350where
351 R: ExactRecords + Resizable + PeekableRecords,
352{
353 let mut section_is_empty: bool = true;
354 for to_primary_index: usize in 0..section_length {
355 let from_primary_index: usize = to_primary_index + section_index * section_length;
356
357 if from_primary_index < primary_length {
358 let from_position: (usize, usize) =
359 format_position(direction, from_primary_index, from_secondary_index);
360 if records.get_text(pos:from_position) != "" {
361 section_is_empty = false;
362 }
363 records.swap(
364 lhs:from_position,
365 rhs:format_position(direction, to_primary_index, to_secondary_index),
366 );
367 }
368 }
369 section_is_empty
370}
371
372/// determine section over direction or vice versa based on behavior
373fn compute_range_order(
374 direction_length: usize,
375 sections_per_direction: usize,
376 behavior: Behavior,
377) -> (Range<usize>, Range<usize>) {
378 match behavior {
379 Behavior::Concat => (1..sections_per_direction, 0..direction_length),
380 Behavior::Zip => (0..direction_length, 1..sections_per_direction),
381 }
382}
383
384/// helper function for shimming both behaviors to work within a single nested loop
385fn compute_range_variables(
386 outer_index: usize,
387 inner_index: usize,
388 direction_length: usize,
389 from_secondary_index: usize,
390 sections_per_direction: usize,
391 filtered_sections: usize,
392 behavior: Behavior,
393) -> (usize, usize, usize) {
394 match behavior {
395 Behavior::Concat => (
396 outer_index,
397 inner_index,
398 inner_index + outer_index * direction_length - filtered_sections,
399 ),
400 Behavior::Zip => (
401 inner_index,
402 from_secondary_index,
403 outer_index * sections_per_direction + inner_index - filtered_sections,
404 ),
405 }
406}
407
408/// utility for arguments of a position easily
409fn format_position(direction: Direction, primary_index: usize, secondary_index: usize) -> Position {
410 match direction {
411 Direction::Column => (secondary_index, primary_index),
412 Direction::Row => (primary_index, secondary_index),
413 }
414}
415
416/// ceil division utility because the std lib ceil_div isn't stable yet
417fn ceil_div(x: usize, y: usize) -> usize {
418 debug_assert!(x != 0);
419 1 + ((x - 1) / y)
420}
421