tabulate_rs/
options.rs

1use crate::{alignment::Alignment, format::TableFormat};
2
3/// Header specification for the table.
4#[derive(Clone, Debug, Default)]
5pub enum Headers {
6    /// No headers.
7    #[default]
8    None,
9    /// Use the first row of the data as a header row.
10    FirstRow,
11    /// Use the keys/index positions detected in the data source.
12    Keys,
13    /// Use explicitly provided header labels.
14    Explicit(Vec<String>),
15    /// Map column keys to header labels for dict-like rows.
16    Mapping(Vec<(String, String)>),
17}
18
19/// Format specifier for numeric columns.
20#[derive(Clone, Debug, Default)]
21pub enum FormatSpec {
22    /// Use the default formatting rules from `python-tabulate`.
23    #[default]
24    Default,
25    /// Reuse the same format string for all columns.
26    Fixed(String),
27    /// Set the format on a per-column basis.
28    PerColumn(Vec<String>),
29}
30
31/// Vertical alignment applied when rendering multi-line rows.
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub enum RowAlignment {
34    /// Align content to the top (extra blank lines at the bottom).
35    Top,
36    /// Align content to the centre.
37    Center,
38    /// Align content to the bottom (extra blank lines at the top).
39    Bottom,
40}
41
42/// Header alignment spec, including the python-tabulate "same" sentinel.
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
44pub enum HeaderAlignment {
45    /// Explicit alignment choice.
46    Align(Alignment),
47    /// Reuse the column alignment (`"same"` in python-tabulate).
48    SameAsColumn,
49}
50
51impl From<Alignment> for HeaderAlignment {
52    fn from(value: Alignment) -> Self {
53        HeaderAlignment::Align(value)
54    }
55}
56
57/// Placeholder values for missing entries.
58#[derive(Clone, Debug)]
59pub enum MissingValues {
60    /// Use the same placeholder for each column.
61    Single(String),
62    /// Provide per-column placeholders. Missing entries are extended by repeating the last value.
63    PerColumn(Vec<String>),
64}
65
66impl Default for MissingValues {
67    fn default() -> Self {
68        Self::Single(String::new())
69    }
70}
71
72/// Controls whether an index column is shown.
73#[derive(Clone, Debug, Default)]
74pub enum ShowIndex {
75    /// Follow the behaviour of `python-tabulate`: only show an index for data sources that
76    /// naturally provide one (e.g. pandas DataFrame).
77    #[default]
78    Default,
79    /// Always show an index column using the implicit row number.
80    Always,
81    /// Never show an index column.
82    Never,
83    /// Show an index column with the provided labels.
84    Values(Vec<String>),
85}
86
87/// Builder-style configuration for [`tabulate`](crate::tabulate).
88#[derive(Clone, Debug)]
89pub struct TabulateOptions {
90    /// Header configuration.
91    pub headers: Headers,
92    /// Table format selection (either a named format or a custom specification).
93    pub table_format: TableFormatChoice,
94    /// Floating point number formatting.
95    pub float_format: FormatSpec,
96    /// Integer number formatting.
97    pub int_format: FormatSpec,
98    /// Alignment for numeric columns.
99    pub num_align: Option<Alignment>,
100    /// Alignment for string columns.
101    pub str_align: Option<Alignment>,
102    /// Replacement value for `None`/missing data.
103    pub missing_values: MissingValues,
104    /// Index column behaviour.
105    pub show_index: ShowIndex,
106    /// Disable automatic detection of numeric values.
107    pub disable_numparse: bool,
108    /// Disable numeric parsing for specific columns (0-indexed).
109    pub disable_numparse_columns: Option<Vec<usize>>,
110    /// Preserve the original whitespace provided in the data.
111    pub preserve_whitespace: bool,
112    /// Treat East Asian wide characters using double-width measurements.
113    pub enable_widechars: bool,
114    /// Maximum column widths (if any).
115    pub max_col_widths: Option<Vec<Option<usize>>>,
116    /// Maximum header column widths (if any).
117    pub max_header_col_widths: Option<Vec<Option<usize>>>,
118    /// Alignment override applied to every column.
119    pub col_global_align: Option<Alignment>,
120    /// Alignment overrides per column.
121    pub col_align: Vec<Option<Alignment>>,
122    /// Alignment override for header row.
123    pub headers_global_align: Option<Alignment>,
124    /// Alignment overrides per-header cell.
125    pub headers_align: Vec<Option<HeaderAlignment>>,
126    /// Alignment override applied to rows.
127    pub row_align: Vec<Option<RowAlignment>>,
128    /// Alignment override applied to all rows.
129    pub row_global_align: Option<RowAlignment>,
130    /// Control how long words are wrapped.
131    pub break_long_words: bool,
132    /// Control whether words can be wrapped on hyphens.
133    pub break_on_hyphens: bool,
134}
135
136impl Default for TabulateOptions {
137    fn default() -> Self {
138        Self {
139            headers: Headers::default(),
140            table_format: TableFormatChoice::Name("simple".to_string()),
141            float_format: FormatSpec::Default,
142            int_format: FormatSpec::Default,
143            num_align: None,
144            str_align: None,
145            missing_values: MissingValues::default(),
146            show_index: ShowIndex::default(),
147            disable_numparse: false,
148            disable_numparse_columns: None,
149            preserve_whitespace: false,
150            enable_widechars: false,
151            max_col_widths: None,
152            max_header_col_widths: None,
153            col_global_align: None,
154            col_align: Vec::new(),
155            headers_global_align: None,
156            headers_align: Vec::new(),
157            row_align: Vec::new(),
158            row_global_align: None,
159            break_long_words: true,
160            break_on_hyphens: true,
161        }
162    }
163}
164
165impl TabulateOptions {
166    /// Construct a new options builder with default configuration.
167    pub fn new() -> Self {
168        Self::default()
169    }
170
171    /// Set explicit headers.
172    pub fn headers(mut self, headers: Headers) -> Self {
173        self.headers = headers;
174        self
175    }
176
177    /// Set table format by name.
178    pub fn table_format<S: Into<String>>(mut self, format: S) -> Self {
179        self.table_format = TableFormatChoice::Name(format.into());
180        self
181    }
182
183    /// Supply a custom [`TableFormat`].
184    pub fn table_format_custom(mut self, format: TableFormat) -> Self {
185        self.table_format = TableFormatChoice::Custom(Box::new(format));
186        self
187    }
188
189    /// Set floating point format.
190    pub fn float_format(mut self, format: FormatSpec) -> Self {
191        self.float_format = format;
192        self
193    }
194
195    /// Set integer format.
196    pub fn int_format(mut self, format: FormatSpec) -> Self {
197        self.int_format = format;
198        self
199    }
200
201    /// Set numeric column alignment.
202    pub fn num_align(mut self, align: Alignment) -> Self {
203        self.num_align = Some(align);
204        self
205    }
206
207    /// Override alignment for all columns.
208    pub fn col_global_align(mut self, align: Alignment) -> Self {
209        self.col_global_align = Some(align);
210        self
211    }
212
213    /// Set string column alignment.
214    pub fn str_align(mut self, align: Alignment) -> Self {
215        self.str_align = Some(align);
216        self
217    }
218
219    /// Set the placeholder for missing values.
220    pub fn missing_value<S: Into<String>>(mut self, value: S) -> Self {
221        self.missing_values = MissingValues::Single(value.into());
222        self
223    }
224
225    /// Set per-column placeholders for missing values.
226    pub fn missing_values<I, S>(mut self, values: I) -> Self
227    where
228        I: IntoIterator<Item = S>,
229        S: Into<String>,
230    {
231        let collected = values.into_iter().map(Into::into).collect();
232        self.missing_values = MissingValues::PerColumn(collected);
233        self
234    }
235
236    /// Control whether tabulate should attempt to parse numeric values.
237    pub fn disable_numparse(mut self, disable: bool) -> Self {
238        self.disable_numparse = disable;
239        self
240    }
241
242    /// Disable numeric parsing for specific columns (0-indexed).
243    pub fn disable_numparse_columns<I>(mut self, columns: I) -> Self
244    where
245        I: IntoIterator<Item = usize>,
246    {
247        let collected = columns.into_iter().collect::<Vec<_>>();
248        self.disable_numparse_columns = Some(collected);
249        self
250    }
251
252    /// Returns true if numeric parsing is disabled for the provided column.
253    pub fn is_numparse_disabled(&self, column: Option<usize>) -> bool {
254        if self.disable_numparse {
255            return true;
256        }
257        match (column, &self.disable_numparse_columns) {
258            (Some(idx), Some(columns)) => columns.contains(&idx),
259            _ => false,
260        }
261    }
262
263    /// Control whitespace preservation.
264    pub fn preserve_whitespace(mut self, preserve: bool) -> Self {
265        self.preserve_whitespace = preserve;
266        self
267    }
268
269    /// Enable width calculations that treat East Asian wide characters as double width.
270    pub fn enable_widechars(mut self, enable: bool) -> Self {
271        self.enable_widechars = enable;
272        self
273    }
274
275    /// Configure how the index column is displayed.
276    pub fn show_index(mut self, show_index: ShowIndex) -> Self {
277        self.show_index = show_index;
278        self
279    }
280
281    /// Set per-column maximum widths. Use `None` for columns without limits.
282    pub fn max_col_widths(mut self, widths: Vec<Option<usize>>) -> Self {
283        self.max_col_widths = Some(widths);
284        self
285    }
286
287    /// Set the same maximum width for all columns.
288    pub fn max_col_width(mut self, width: usize) -> Self {
289        self.max_col_widths = Some(vec![Some(width)]);
290        self
291    }
292
293    /// Set per-column maximum header widths. Use `None` for unlimited columns.
294    pub fn max_header_col_widths(mut self, widths: Vec<Option<usize>>) -> Self {
295        self.max_header_col_widths = Some(widths);
296        self
297    }
298
299    /// Set the same maximum width for all header columns.
300    pub fn max_header_col_width(mut self, width: usize) -> Self {
301        self.max_header_col_widths = Some(vec![Some(width)]);
302        self
303    }
304
305    /// Provide a mapping from column keys to display headers.
306    pub fn headers_mapping<I, K, V>(mut self, mapping: I) -> Self
307    where
308        I: IntoIterator<Item = (K, V)>,
309        K: Into<String>,
310        V: Into<String>,
311    {
312        let collected = mapping
313            .into_iter()
314            .map(|(key, value)| (key.into(), value.into()))
315            .collect();
316        self.headers = Headers::Mapping(collected);
317        self
318    }
319
320    /// Provide per-column alignment overrides.
321    pub fn col_alignments<I>(mut self, aligns: I) -> Self
322    where
323        I: IntoIterator<Item = Option<Alignment>>,
324    {
325        self.col_align = aligns.into_iter().collect();
326        self
327    }
328
329    /// Override alignment for all headers.
330    pub fn headers_global_align(mut self, align: Alignment) -> Self {
331        self.headers_global_align = Some(align);
332        self
333    }
334
335    /// Provide per-header alignment overrides.
336    pub fn headers_alignments<I>(mut self, aligns: I) -> Self
337    where
338        I: IntoIterator<Item = Option<HeaderAlignment>>,
339    {
340        self.headers_align = aligns.into_iter().collect();
341        self
342    }
343
344    /// Override the default alignment for all data rows.
345    pub fn row_global_align(mut self, align: RowAlignment) -> Self {
346        self.row_global_align = Some(align);
347        self
348    }
349
350    /// Provide per-row alignment overrides.
351    pub fn row_alignments<I>(mut self, aligns: I) -> Self
352    where
353        I: IntoIterator<Item = Option<RowAlignment>>,
354    {
355        self.row_align = aligns.into_iter().collect();
356        self
357    }
358
359    pub(crate) fn table_format_name(&self) -> Option<&str> {
360        match &self.table_format {
361            TableFormatChoice::Name(name) => Some(name.as_str()),
362            TableFormatChoice::Custom(_) => None,
363        }
364    }
365}
366
367/// Specifies whether to use a named or custom table format.
368#[derive(Clone, Debug)]
369pub enum TableFormatChoice {
370    /// Use one of the built-in formats by name.
371    Name(String),
372    /// Use a bespoke [`TableFormat`] instance.
373    Custom(Box<TableFormat>),
374}