tabulate_rs/
alignment.rs

1use crate::width::visible_width;
2
3/// Text alignment options supported by the tabulator.
4#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5pub enum Alignment {
6    /// Align text to the left (pad on the right).
7    Left,
8    /// Align text to the right (pad on the left).
9    Right,
10    /// Align text to the centre.
11    Center,
12    /// Align text so that decimal separators line up.
13    Decimal,
14}
15
16impl Alignment {
17    /// Parse an alignment specifier compatible with `python-tabulate`.
18    pub fn parse(value: &str) -> Option<Self> {
19        match value {
20            "left" => Some(Self::Left),
21            "right" => Some(Self::Right),
22            "center" => Some(Self::Center),
23            "decimal" => Some(Self::Decimal),
24            _ => None,
25        }
26    }
27}
28
29/// Layout information for decimal alignment.
30#[derive(Clone, Copy, Debug)]
31pub struct DecimalLayout {
32    pub integer: usize,
33    pub fraction: usize,
34}
35
36/// Align `cell` so that the visible width equals `width`.
37#[allow(clippy::too_many_arguments)]
38pub fn align_cell(
39    cell: &str,
40    width: usize,
41    alignment: Alignment,
42    decimal_marker: char,
43    decimal_layout: Option<DecimalLayout>,
44    per_line: bool,
45    enforce_left_alignment: bool,
46    enable_widechars: bool,
47) -> String {
48    if per_line {
49        let lines: Vec<&str> = cell.split('\n').collect();
50        let aligned_lines: Vec<String> = lines
51            .into_iter()
52            .map(|line| {
53                align_line(
54                    line,
55                    width,
56                    alignment,
57                    decimal_marker,
58                    decimal_layout,
59                    enable_widechars,
60                )
61            })
62            .collect();
63        aligned_lines.join("\n")
64    } else {
65        let mut aligned = if matches!(alignment, Alignment::Left) && !enforce_left_alignment {
66            cell.to_string()
67        } else {
68            align_line(
69                cell,
70                width,
71                alignment,
72                decimal_marker,
73                decimal_layout,
74                enable_widechars,
75            )
76        };
77        if matches!(alignment, Alignment::Left) && aligned.contains('\n') {
78            let mut lines: Vec<String> = aligned.split('\n').map(|line| line.to_string()).collect();
79            let last = lines.len().saturating_sub(1);
80            if last > 0 {
81                for line in lines.iter_mut().take(last) {
82                    let trimmed = line.trim_end();
83                    line.truncate(trimmed.len());
84                }
85            }
86            if let Some(last_line) = lines.last_mut() {
87                let trimmed = last_line.trim_end();
88                last_line.truncate(trimmed.len());
89            }
90            aligned = lines.join("\n");
91        }
92        aligned
93    }
94}
95
96fn align_line(
97    line: &str,
98    width: usize,
99    alignment: Alignment,
100    decimal_marker: char,
101    decimal_layout: Option<DecimalLayout>,
102    enable_widechars: bool,
103) -> String {
104    match alignment {
105        Alignment::Left => align_left_line(line, width, enable_widechars),
106        Alignment::Right => align_right_line(line, width, enable_widechars),
107        Alignment::Center => align_center_line(line, width, enable_widechars),
108        Alignment::Decimal => align_decimal_line(
109            line,
110            width,
111            decimal_marker,
112            decimal_layout,
113            enable_widechars,
114        ),
115    }
116}
117
118fn align_left_line(line: &str, width: usize, enable_widechars: bool) -> String {
119    let cell_width = visible_width(line, enable_widechars);
120    if cell_width >= width {
121        line.to_string()
122    } else {
123        format!("{line}{:padding$}", "", padding = width - cell_width)
124    }
125}
126
127fn align_right_line(line: &str, width: usize, enable_widechars: bool) -> String {
128    let cell_width = visible_width(line, enable_widechars);
129    if cell_width >= width {
130        line.to_string()
131    } else {
132        format!("{:padding$}{line}", "", padding = width - cell_width)
133    }
134}
135
136fn align_center_line(line: &str, width: usize, enable_widechars: bool) -> String {
137    let cell_width = visible_width(line, enable_widechars);
138    if cell_width >= width {
139        line.to_string()
140    } else {
141        let padding = width - cell_width;
142        let left = padding / 2;
143        let right = padding - left;
144        format!(
145            "{:left$}{line}{:right$}",
146            "",
147            "",
148            left = left,
149            right = right
150        )
151    }
152}
153
154fn align_decimal_line(
155    line: &str,
156    width: usize,
157    decimal_marker: char,
158    decimal_layout: Option<DecimalLayout>,
159    enable_widechars: bool,
160) -> String {
161    let layout = decimal_layout.unwrap_or(DecimalLayout {
162        integer: width,
163        fraction: 0,
164    });
165    let split_pos = line.find(decimal_marker);
166
167    let (integer_part, fractional_part) = match split_pos {
168        Some(pos) => (&line[..pos], Some(&line[pos + decimal_marker.len_utf8()..])),
169        None => (line, None),
170    };
171
172    let integer_width = visible_width(integer_part, enable_widechars);
173    let mut result = String::new();
174    if integer_width < layout.integer {
175        result.push_str(&" ".repeat(layout.integer - integer_width));
176    }
177    result.push_str(integer_part);
178    let mut fraction_width = 0usize;
179    if let Some(fractional) = fractional_part {
180        result.push(decimal_marker);
181        result.push_str(fractional);
182        fraction_width = visible_width(fractional, enable_widechars);
183    }
184
185    if layout.fraction > 0 {
186        if fractional_part.is_some() {
187            if fraction_width < layout.fraction {
188                result.push_str(&" ".repeat(layout.fraction - fraction_width));
189            }
190        } else {
191            result.push_str(&" ".repeat(layout.fraction));
192        }
193    }
194
195    let current_width = visible_width(&result, enable_widechars);
196    if current_width < width {
197        let mut padded = String::with_capacity(width);
198        padded.push_str(&" ".repeat(width - current_width));
199        padded.push_str(&result);
200        return padded;
201    }
202
203    result
204}