Skip to main content

textwrap/
columns.rs

1//! Functionality for wrapping text into columns.
2
3use alloc::string::String;
4use alloc::vec::Vec;
5
6use crate::core::display_width;
7use crate::{Options, wrap};
8
9/// Wrap text into columns with a given total width.
10///
11/// The `left_gap`, `middle_gap` and `right_gap` arguments specify the
12/// strings to insert before, between, and after the columns. The
13/// total width of all columns and all gaps is specified using the
14/// `total_width_or_options` argument. This argument can simply be an
15/// integer if you want to use default settings when wrapping, or it
16/// can be an [`Options`] value if you want to customize the wrapping.
17///
18/// If the columns are narrow, it is recommended to set
19/// [`Options::break_words`] to `true` to prevent words from
20/// protruding into the margins.
21///
22/// The per-column width is computed like this:
23///
24/// ```
25/// # let (left_gap, middle_gap, right_gap) = ("", "", "");
26/// # let columns = 2;
27/// # let options = textwrap::Options::new(80);
28/// let inner_width = options.width
29///     - textwrap::core::display_width(left_gap)
30///     - textwrap::core::display_width(right_gap)
31///     - textwrap::core::display_width(middle_gap) * (columns - 1);
32/// let column_width = inner_width / columns;
33/// ```
34///
35/// The `text` is wrapped using [`wrap()`] and the given `options`
36/// argument, but the width is overwritten with the computed
37/// `column_width`.
38///
39/// # Panics
40///
41/// Panics if `columns` is zero.
42///
43/// # Examples
44///
45/// ```
46/// use textwrap::wrap_columns;
47///
48/// let text = "\
49/// This is an example text, which is wrapped into three columns. \
50/// Notice how the final column can be shorter than the others.";
51///
52/// #[cfg(feature = "smawk")]
53/// assert_eq!(wrap_columns(text, 3, 50, "| ", " | ", " |"),
54///            vec!["| This is       | into three    | column can be  |",
55///                 "| an example    | columns.      | shorter than   |",
56///                 "| text, which   | Notice how    | the others.    |",
57///                 "| is wrapped    | the final     |                |"]);
58///
59/// // Without the `smawk` feature, the middle column is a little more uneven:
60/// #[cfg(not(feature = "smawk"))]
61/// assert_eq!(wrap_columns(text, 3, 50, "| ", " | ", " |"),
62///            vec!["| This is an    | three         | column can be  |",
63///                 "| example text, | columns.      | shorter than   |",
64///                 "| which is      | Notice how    | the others.    |",
65///                 "| wrapped into  | the final     |                |"]);
66pub fn wrap_columns<'a, Opt>(
67    text: &str,
68    columns: usize,
69    total_width_or_options: Opt,
70    left_gap: &str,
71    middle_gap: &str,
72    right_gap: &str,
73) -> Vec<String>
74where
75    Opt: Into<Options<'a>>,
76{
77    assert!(columns > 0);
78
79    let mut options: Options = total_width_or_options.into();
80
81    let inner_width = inner_width(options.width, columns, left_gap, middle_gap, right_gap);
82
83    let column_width = core::cmp::max(inner_width / columns, 1);
84    options.width = column_width;
85    let last_column_padding = " ".repeat(inner_width % column_width);
86    let wrapped_lines = wrap(text, options);
87    let lines_per_column =
88        wrapped_lines.len() / columns + usize::from(wrapped_lines.len() % columns > 0);
89    let mut lines = Vec::new();
90    for line_no in 0..lines_per_column {
91        let mut line = String::from(left_gap);
92        for column_no in 0..columns {
93            match wrapped_lines.get(line_no + column_no * lines_per_column) {
94                Some(column_line) => {
95                    line.push_str(column_line);
96                    line.push_str(&" ".repeat(column_width - display_width(column_line)));
97                }
98                None => {
99                    line.push_str(&" ".repeat(column_width));
100                }
101            }
102            if column_no == columns - 1 {
103                line.push_str(&last_column_padding);
104            } else {
105                line.push_str(middle_gap);
106            }
107        }
108        line.push_str(right_gap);
109        lines.push(line);
110    }
111
112    lines
113}
114
115/// Compute the width left for the columns themselves, i.e., the total
116/// width minus the width of the gaps.
117///
118/// The multiplication is saturating because `columns` is unbounded and
119/// the combined width of the middle gaps can thus exceed `usize::MAX`.
120/// Nothing is lost by saturating: such a product is larger than
121/// `width`, so the subtraction saturates to zero either way.
122fn inner_width(
123    width: usize,
124    columns: usize,
125    left_gap: &str,
126    middle_gap: &str,
127    right_gap: &str,
128) -> usize {
129    width
130        .saturating_sub(display_width(left_gap))
131        .saturating_sub(display_width(right_gap))
132        .saturating_sub(display_width(middle_gap).saturating_mul(columns - 1))
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use alloc::vec;
139
140    #[test]
141    fn wrap_columns_empty_text() {
142        assert_eq!(wrap_columns("", 1, 10, "| ", "", " |"), vec!["|        |"]);
143    }
144
145    #[test]
146    fn wrap_columns_single_column() {
147        assert_eq!(
148            wrap_columns("Foo", 3, 30, "| ", " | ", " |"),
149            vec!["| Foo    |        |          |"]
150        );
151    }
152
153    #[test]
154    fn wrap_columns_uneven_columns() {
155        // The gaps take up a total of 5 columns, so the columns are
156        // (21 - 5)/4 = 4 columns wide:
157        assert_eq!(
158            wrap_columns("Foo Bar Baz Quux", 4, 21, "|", "|", "|"),
159            vec!["|Foo |Bar |Baz |Quux|"]
160        );
161        // As the total width increases, the last column absorbs the
162        // excess width:
163        assert_eq!(
164            wrap_columns("Foo Bar Baz Quux", 4, 24, "|", "|", "|"),
165            vec!["|Foo |Bar |Baz |Quux   |"]
166        );
167        // Finally, when the width is 25, the columns can be resized
168        // to a width of (25 - 5)/4 = 5 columns:
169        assert_eq!(
170            wrap_columns("Foo Bar Baz Quux", 4, 25, "|", "|", "|"),
171            vec!["|Foo  |Bar  |Baz  |Quux |"]
172        );
173    }
174
175    #[test]
176    #[cfg(all(feature = "unicode-width", feature = "smawk"))]
177    fn wrap_columns_with_emojis() {
178        assert_eq!(
179            wrap_columns(
180                "Words and a few emojis 😍 wrapped in ⓶ columns",
181                2,
182                30,
183                "✨ ",
184                " ⚽ ",
185                " 👀"
186            ),
187            vec![
188                "✨ Words      ⚽ wrapped in 👀",
189                "✨ and a few  ⚽ ⓶ columns  👀",
190                "✨ emojis 😍  ⚽            👀"
191            ]
192        );
193    }
194
195    #[test]
196    #[cfg(all(feature = "unicode-width", not(feature = "smawk")))]
197    fn wrap_columns_with_emojis_without_smawk() {
198        assert_eq!(
199            wrap_columns(
200                "Words and a few emojis 😍 wrapped in ⓶ columns",
201                2,
202                30,
203                "✨ ",
204                " ⚽ ",
205                " 👀"
206            ),
207            vec![
208                "✨ Words and  ⚽ wrapped in 👀",
209                "✨ a few      ⚽ ⓶ columns  👀",
210                "✨ emojis 😍  ⚽            👀"
211            ]
212        );
213    }
214
215    #[test]
216    fn wrap_columns_big_gaps() {
217        // The column width shrinks to 1 because the gaps take up all
218        // the space.
219        assert_eq!(
220            wrap_columns("xyz", 2, 10, "----> ", " !!! ", " <----"),
221            vec![
222                "----> x !!! z <----", //
223                "----> y !!!   <----"
224            ]
225        );
226    }
227
228    #[test]
229    #[should_panic]
230    fn wrap_columns_panic_with_zero_columns() {
231        wrap_columns("", 0, 10, "", "", "");
232    }
233
234    #[test]
235    fn inner_width_with_normal_gaps() {
236        // The gaps take up a total of 5 columns.
237        assert_eq!(inner_width(21, 4, "|", "|", "|"), 16);
238    }
239
240    #[test]
241    fn inner_width_with_huge_number_of_columns() {
242        // The combined width of the middle gaps overflows a `usize`,
243        // which leaves no room at all for the columns. The number of
244        // columns is chosen so that a wrapping multiplication would
245        // wrap all the way around to zero.
246        assert_eq!(inner_width(80, usize::MAX / 2 + 2, "", "xx", ""), 0);
247    }
248}