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 | |
6 | use core::ops::Range; |
7 | |
8 | use papergrid::{config::Position, records::PeekableRecords}; |
9 | |
10 | use crate::grid::records::{ExactRecords, Records, Resizable}; |
11 | |
12 | use super::TableOption; |
13 | |
14 | #[derive (Debug, Clone, Copy)] |
15 | enum Direction { |
16 | Column, |
17 | Row, |
18 | } |
19 | |
20 | #[derive (Debug, Clone, Copy)] |
21 | enum Behavior { |
22 | Concat, |
23 | Zip, |
24 | } |
25 | |
26 | #[derive (Debug, Clone, Copy, PartialEq)] |
27 | enum 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)] |
72 | pub struct Split { |
73 | direction: Direction, |
74 | behavior: Behavior, |
75 | display: Display, |
76 | index: usize, |
77 | } |
78 | |
79 | impl 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 | |
235 | impl<R, D, Cfg> TableOption<R, D, Cfg> for Split |
236 | where |
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 |
304 | fn compute_length_arrangement<R>(records: &mut R, direction: Direction) -> (usize, usize) |
305 | where |
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 |
315 | fn cleanup<R>(records: &mut R, section_length: usize, primary_length: usize, direction: Direction) |
316 | where |
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 |
328 | fn delete<R>(records: &mut R, target_index: usize, direction: Direction) |
329 | where |
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 |
341 | fn 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 |
350 | where |
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 |
373 | fn 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 |
385 | fn 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 |
409 | fn 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 |
417 | fn ceil_div(x: usize, y: usize) -> usize { |
418 | debug_assert!(x != 0); |
419 | 1 + ((x - 1) / y) |
420 | } |
421 | |