Repository: mgeisler/textwrap (language: rust)

Issue:
Support for columns
Hi, I would love to be able to wrap a text into mutiple columns. 

Something like this quick draft could be the API for it.
```
pub fn columns (s: &str, columns: usize, width: usize, outer_gap: usize, inner_gap: usize );
```


Discussion / hints:
Hi @wucke13! Oh, that's a cool idea!

I guess the function should return a `String`? A string with the text already formatted into the columns? So if your text above is wrapped into 12 columns, it should return a string looking like this:

```
  Hi, I would   │  Something
  love to be    │  like this
  able to wrap  │  quick draft
  a text into   │  could be the
  mutiple       │  API for it.
  columns.      │
```
~Yes!~ No, your example is what I would expect from two columns. 

I think the interspace between the columns should be an `&str` Argument. That users can easily decide what type of seperation they desire (just spaces, pipes, UTF-8 magic, you name it). The space before the first column and after the last one maybe should be configurable as well? Other than that, I think only the maximum width and the ammount of columns must be configurable.

So, new API proposal

```Rust
fn columns(
    text: &str,
    columns: usize,
    total_width: usize,
    left_gap: &str,
    middle_gaps: &str,
    right_gap: &str,
)->String;
```

Is it clear what I mean?

Also: I already wrote code for a specific use case of this: Formatting a HashMap<String, String> to multiple columns. The code is close to what we need for this, but not quite the same. Consider the following a release to the public domain:

```Rust
let max_key_length = mapping
    .iter()
    .map(|(k, _)| k.to_string().len())
    .max()
    .unwrap_or(0);
let max_value_length = mapping.iter().map(|(_, v)| v.len()).max().unwrap_or(0);
let joint = " = ";
let cols = width / (max_key_length + max_value_length + joint.len() + 1);
let mut rows = mapping.len() / cols;
if mapping.len() % cols != 0 {
    rows += 1;
}
                                                                                
(0..rows)
    .map(|initial_offset| {
        mapping
            .iter()
            .skip(initial_offset)
            .step_by(rows)
            .enumerate()
            .map(|(i, (k, v))| {
                format!(
                    "{}{:k_width$}{}{:^v_width$}",
                    if i % cols == 0 { '\n' } else { ' ' },
                    k,
                    joint,
                    v,
                    k_width = max_key_length,
                    v_width = max_value_length
                )
            })
    })
    .flatten()
    .collect()
```
Hi @wucke13 — I'm sorry, I forgot to reply back to you!

Yes, I think it's pretty clear what you mean and I think your function signature is great! I like the general `&str` arguments for the gaps. I would call it `wrap_columns` to clearly relate it to the existing `wrap` function.

The way I generated the example above was to wrap the text at 12 columns and then simply copy-paste the wrapped lines from the second half after the first half.

So I think it should be possible to implement this by first wrapping the text using the column with given (so `total_width: usize` should actually be [`options: WrapOptions` now, just like `wrap`][1]). Then collect the wrapped lines into a `Vec<Cow<'_, str>>`, count the number of lines finally rearrange the lines. I don't know if your `HashMap<String, String>` code above does something similar?

[1]: https://github.com/mgeisler/textwrap/blob/e49a2a3366ff776877d4454e5637f60b7ff6d6bb/src/lib.rs#L695

Here is a sketch of what I have in mind:

```rust
let wrapped_lines = wrap(text, options).collect::<Vec<_>>();
let lines_per_column = wrapped_lines.len() / columns; // Need to round this up 
let columns = Vec::new();
for line_no in 0..lines_per_column {
    let mut line = String::from(left_gap);
    for column_no in 0..columns {
        line.push_str(lines[line_no + column_no * lines_per_column]);
        if column_no < columns - 1 {
            line.push_str(mid_gap);
        }
    }
    line.push_str(right_gap);
    columns.push(line);
}
```

There are certainly rounding errors in this :smile:  Also, it just occured to me that the lines would need padding, just like you do above with `format!`...

Would you be up for implementing this? It would need some documentation, tests, etc...
Hi, currently I've got a somewhat hefty workload, so I don't want to make any promises. But I just put in on my todo list, let's see whether I will come up with results in the next couple of weeks.
That's perfectly fine, no promises needed! I'll simply let the issue be open in case you find time :smile:

Fix patch:
diff --git a/src/lib.rs b/src/lib.rs
index 44a38b38..6aa38770 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -804,6 +804,117 @@ where
     lines
 }
 
+/// Wrap text into columns with a given total width.
+///
+/// The `left_gap`, `mid_gap` and `right_gap` arguments specify the
+/// strings to insert before, between, and after the columns. The
+/// total width of all columns and all gaps is specified using the
+/// `options` argument. This argument can simply be an integer if you
+/// want to use default settings when wrapping, or it can be a
+/// [`Options`] value if you want to customize the wrapping.
+///
+/// If the columns are narrow, it is recommended to set
+/// [`Options::break_words`] to `true` to prevent words from
+/// protruding into the margins.
+///
+/// The per-column width is computed like this:
+///
+/// ```
+/// # let (left_gap, middle_gap, right_gap) = ("", "", "");
+/// # let columns = 2;
+/// # let options = textwrap::Options::new(80);
+/// let inner_width = options.width
+///     - textwrap::core::display_width(left_gap)
+///     - textwrap::core::display_width(right_gap)
+///     - textwrap::core::display_width(middle_gap) * (columns - 1);
+/// let column_width = inner_width / columns;
+/// ```
+///
+/// The `text` is wrapped using [`wrap`] and the given `options`
+/// argument, but the width is overwritten to the computed
+/// `column_width`.
+///
+/// # Panics
+///
+/// Panics if `columns` is zero.
+///
+/// # Examples
+///
+/// ```
+/// use textwrap::wrap_columns;
+///
+/// let text = "\
+/// This is an example text, which is wrapped into three columns. \
+/// Notice how the final column can be shorter than the others.";
+///
+/// #[cfg(feature = "smawk")]
+/// assert_eq!(wrap_columns(text, 3, 50, "| ", " | ", " |"),
+///            vec!["| This is       | into three    | column can be  |",
+///                 "| an example    | columns.      | shorter than   |",
+///                 "| text, which   | Notice how    | the others.    |",
+///                 "| is wrapped    | the final     |                |"]);
+///
+/// // Without the `smawk` feature, the middle column is a little more uneven:
+/// #[cfg(not(feature = "smawk"))]
+/// assert_eq!(wrap_columns(text, 3, 50, "| ", " | ", " |"),
+///            vec!["| This is an    | three         | column can be  |",
+///                 "| example text, | columns.      | shorter than   |",
+///                 "| which is      | Notice how    | the others.    |",
+///                 "| wrapped into  | the final     |                |"]);
+pub fn wrap_columns<'a, S, Opt>(
+    text: &str,
+    columns: usize,
+    options: Opt,
+    left_gap: &str,
+    mid_gap: &str,
+    right_gap: &str,
+) -> Vec<String>
+where
+    S: WordSplitter,
+    Opt: Into<Options<'a, S>>,
+{
+    assert!(columns > 0);
+
+    let mut options = options.into();
+
+    let inner_width = options
+        .width
+        .saturating_sub(core::display_width(left_gap))
+        .saturating_sub(core::display_width(right_gap))
+        .saturating_sub(core::display_width(mid_gap) * (columns - 1));
+
+    let column_width = std::cmp::max(inner_width / columns, 1);
+    options.width = column_width;
+    let last_column_padding = " ".repeat(inner_width % column_width);
+    let wrapped_lines = wrap(text, options);
+    let lines_per_column =
+        wrapped_lines.len() / columns + usize::from(wrapped_lines.len() % columns > 0);
+    let mut lines = Vec::new();
+    for line_no in 0..lines_per_column {
+        let mut line = String::from(left_gap);
+        for column_no in 0..columns {
+            match wrapped_lines.get(line_no + column_no * lines_per_column) {
+                Some(column_line) => {
+                    line.push_str(&column_line);
+                    line.push_str(&" ".repeat(column_width - core::display_width(&column_line)));
+                }
+                None => {
+                    line.push_str(&" ".repeat(column_width));
+                }
+            }
+            if column_no == columns - 1 {
+                line.push_str(&last_column_padding);
+            } else {
+                line.push_str(mid_gap);
+            }
+        }
+        line.push_str(right_gap);
+        lines.push(line);
+    }
+
+    lines
+}
+
 /// Fill `text` in-place without reallocating the input string.
 ///
 /// This function works by modifying the input string: some `' '`
@@ -1473,4 +1584,76 @@ mod tests {
             vec!["foo", "bar", "baz"]
         );
     }
+
+    #[test]
+    fn wrap_columns_empty_text() {
+        assert_eq!(wrap_columns("", 1, 10, "| ", "", " |"), vec!["|        |"]);
+    }
+
+    #[test]
+    fn wrap_columns_single_column() {
+        assert_eq!(
+            wrap_columns("Foo", 3, 30, "| ", " | ", " |"),
+            vec!["| Foo    |        |          |"]
+        );
+    }
+
+    #[test]
+    fn wrap_columns_uneven_columns() {
+        // The gaps take up a total of 5 columns, so the columns are
+        // (21 - 5)/4 = 4 columns wide:
+        assert_eq!(
+            wrap_columns("Foo Bar Baz Quux", 4, 21, "|", "|", "|"),
+            vec!["|Foo |Bar |Baz |Quux|"]
+        );
+        // As the total width increases, the last column absorbs the
+        // excess width:
+        assert_eq!(
+            wrap_columns("Foo Bar Baz Quux", 4, 24, "|", "|", "|"),
+            vec!["|Foo |Bar |Baz |Quux   |"]
+        );
+        // Finally, when the width is 25, the columns can be resized
+        // to a width of (25 - 5)/4 = 5 columns:
+        assert_eq!(
+            wrap_columns("Foo Bar Baz Quux", 4, 25, "|", "|", "|"),
+            vec!["|Foo  |Bar  |Baz  |Quux |"]
+        );
+    }
+
+    #[test]
+    #[cfg(feature = "unicode-width")]
+    fn wrap_columns_with_emojis() {
+        assert_eq!(
+            wrap_columns(
+                "Words and a few emojis 😍 wrapped in ⓶ columns",
+                2,
+                30,
+                "✨ ",
+                " ⚽ ",
+                " 👀"
+            ),
+            vec![
+                "✨ Words      ⚽ wrapped in 👀",
+                "✨ and a few  ⚽ ⓶ columns  👀",
+                "✨ emojis 😍  ⚽            👀"
+            ]
+        );
+    }
+
+
+    #[test]
+    fn wrap_columns_big_gaps() {
+        // The column width shrinks to 1 because the gaps take up all
+        // the space.
+        assert_eq!(wrap_columns("xyz", 2, 10, "----> ", " !!! ", " <----"),
+                   vec!["----> x !!! z <----",
+                        "----> y !!!   <----"]);
+    }
+
+
+    #[test]
+    #[should_panic]
+    fn wrap_columns_panic_with_zero_columns() {
+        wrap_columns("", 0, 10, "", "", "");
+    }
 }


Test patch: