tinytable/
lib.rs

1// Copyright © 2025 Joaquim Monteiro
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4#![forbid(unsafe_code)]
5#![warn(clippy::pedantic)]
6#![allow(clippy::items_after_statements)]
7#![allow(clippy::missing_panics_doc)]
8#![allow(clippy::uninlined_format_args)]
9#![cfg_attr(docsrs, feature(doc_auto_cfg))]
10#![cfg_attr(docsrs, feature(doc_cfg))]
11
12//! A tiny text table drawing library.
13//!
14//! Features:
15//!
16//! * Small code size (< 250 lines of code, excluding docs and tests)
17//! * Minimal dependencies (not zero, because Unicode is hard)
18//! * Iterator support (you don't need to collect all the data to display at once, it can be streamed)
19//!     * Optional support for the [`fallible-iterator`](https://crates.io/crates/fallible-iterator) crate
20//! * Unicode support
21//! * Nothing more!
22//!
23//! See [`write_table`] for examples and usage details.
24
25use std::fmt::Write as FmtWrite;
26use std::fmt::{self, Display};
27use std::io::{self, BufWriter, Write};
28use std::num::NonZeroUsize;
29
30#[cfg(feature = "fallible-iterator")]
31use std::fmt::Debug;
32
33use unicode_segmentation::UnicodeSegmentation;
34use unicode_width::UnicodeWidthStr;
35
36#[cfg(feature = "fallible-iterator")]
37use fallible_iterator::FallibleIterator;
38
39const HORIZONTAL_LINE: &str = "─";
40const VERTICAL_LINE: &str = "│";
41const TOP_LEFT: &str = "╭";
42const TOP_RIGHT: &str = "╮";
43const BOTTOM_LEFT: &str = "╰";
44const BOTTOM_RIGHT: &str = "╯";
45const INTERSECTION: &str = "┼";
46const TOP_INTERSECTION: &str = "┬";
47const BOTTOM_INTERSECTION: &str = "┴";
48const LEFT_INTERSECTION: &str = "├";
49const RIGHT_INTERSECTION: &str = "┤";
50
51/// Render a table.
52///
53/// Writes a table containing data from `iter`, an [`Iterator`] over rows implementing [`IntoIterator`], which, in turn,
54/// yields values that implement [`Display`], into the `to` writer (which can be [`stdout`], a [`Vec<u8>`], etc.).
55///
56/// The width of each column is fixed (as specified by `column_widths`).
57///
58/// (If the type you want to display does not implement [`Display`] (or you want to use a different format
59/// than the one provided by [`Display`]), use [`write_table_with_fmt`] instead.)
60///
61/// [`stdout`]: std::io::Stdout
62///
63/// # Examples
64///
65/// ```
66/// # use std::num::NonZeroUsize;
67/// # use tinytable::write_table;
68/// let columns = ["x", "x²"];
69/// let column_widths = [3, 4].map(|n| NonZeroUsize::new(n).expect("non zero"));
70/// let data = [[1, 1], [2, 4], [3, 9], [4, 16]];
71/// # let stdout = std::io::stdout();
72///
73/// write_table(stdout.lock(), data.iter(), &columns, &column_widths)?;
74/// # Ok::<(), std::io::Error>(())
75/// ```
76///
77/// ```non_rust
78/// ╭───┬────╮
79/// │ x │ x² │
80/// ├───┼────┤
81/// │ 1 │ 1  │
82/// │ 2 │ 4  │
83/// │ 3 │ 9  │
84/// │ 4 │ 16 │
85/// ╰───┴────╯
86/// ```
87///
88/// Non-trivial iterators are supported:
89///
90/// ```
91/// # use std::num::NonZeroUsize;
92/// # use tinytable::write_table;
93/// let columns = ["x", "x²"];
94/// let column_widths = [3, 4].map(|n| NonZeroUsize::new(n).expect("non zero"));
95/// let iter = (1..).take(4).map(|n| [n, n * n]);
96/// # let stdout = std::io::stdout();
97///
98/// write_table(stdout.lock(), iter, &columns, &column_widths)?;
99/// # Ok::<(), std::io::Error>(())
100/// ```
101///
102/// ```non_rust
103/// ╭───┬────╮
104/// │ x │ x² │
105/// ├───┼────┤
106/// │ 1 │ 1  │
107/// │ 2 │ 4  │
108/// │ 3 │ 9  │
109/// │ 4 │ 16 │
110/// ╰───┴────╯
111/// ```
112///
113/// # Errors
114///
115/// If an I/O error is encountered while writing to the `to` writer, that error will be returned.
116pub fn write_table<Cell: Display, Row: IntoIterator<Item = Cell>, const COLUMN_COUNT: usize>(
117    to: impl Write,
118    iter: impl Iterator<Item = Row>,
119    column_names: &[&str; COLUMN_COUNT],
120    column_widths: &[NonZeroUsize; COLUMN_COUNT],
121) -> io::Result<()> {
122    let mut writer = write_table_start(to, column_names, column_widths)?;
123
124    let mut value = String::new();
125    for row in iter {
126        writer.write_all(VERTICAL_LINE.as_bytes())?;
127
128        let mut row_iter = row.into_iter();
129        for space in column_widths.iter().copied().map(NonZeroUsize::get) {
130            if let Some(col) = row_iter.next() {
131                write!(&mut value, "{}", col).expect("formatting to a string shouldn't fail");
132            }
133            draw_cell(&mut writer, &value, space)?;
134            value.clear();
135        }
136
137        writer.write_all("\n".as_bytes())?;
138    }
139
140    write_table_end(writer, column_widths)
141}
142
143/// Render a table using custom formatters.
144///
145/// Variant of [`write_table`] that converts values to text using the provided `formatters` instead of the [`Display`]
146/// trait. Besides being able to use a different representation than the one provided by a type's [`Display`]
147/// implementation, it is also useful for displaying values that do not implement [`Display`].
148///
149/// # Examples
150///
151/// ```
152/// # use std::net::Ipv4Addr;
153/// # use std::num::NonZeroUsize;
154/// # use tinytable::write_table_with_fmt;
155/// # let stdout = std::io::stdout();
156/// use std::fmt::Write;
157///
158/// let addrs = [
159///     Ipv4Addr::new(192, 168, 0, 1),
160///     Ipv4Addr::new(1, 1, 1, 1),
161///     Ipv4Addr::new(255, 127, 63, 31),
162/// ];
163/// let column_names = ["Full address", "BE bits", "Private"];
164/// let column_widths = [17, 12, 7].map(|n| NonZeroUsize::new(n).expect("non zero"));
165///
166/// let formatters: [fn(&Ipv4Addr, &mut String) -> std::fmt::Result; 3] = [
167///     |addr, f| write!(f, "{}", addr),
168///     |addr, f| write!(f, "0x{:x}", addr.to_bits().to_be()),
169///     |addr, f| write!(f, "{}", if addr.is_private() { "yes" } else { "no" }),
170/// ];
171///
172/// write_table_with_fmt(stdout.lock(), addrs.iter().copied(), &formatters, &column_names, &column_widths)?;
173/// # Ok::<(), std::io::Error>(())
174/// ```
175///
176/// ```non_rust
177/// ╭─────────────────┬────────────┬───────╮
178/// │ Full address    │ BE bits    │Private│
179/// ├─────────────────┼────────────┼───────┤
180/// │ 192.168.0.1     │ 0x100a8c0  │ yes   │
181/// │ 1.1.1.1         │ 0x1010101  │ no    │
182/// │ 255.127.63.31   │ 0x1f3f7fff │ no    │
183/// ╰─────────────────┴────────────┴───────╯
184/// ```
185///
186/// # Errors
187///
188/// If an I/O error is encountered while writing to the `to` writer, that error will be returned.
189pub fn write_table_with_fmt<Row, const COLUMN_COUNT: usize>(
190    to: impl Write,
191    iter: impl Iterator<Item = Row>,
192    formatters: &[impl Fn(&Row, &mut String) -> fmt::Result; COLUMN_COUNT],
193    column_names: &[&str; COLUMN_COUNT],
194    column_widths: &[NonZeroUsize; COLUMN_COUNT],
195) -> io::Result<()> {
196    let mut writer = write_table_start(to, column_names, column_widths)?;
197
198    let mut value = String::new();
199    for row in iter {
200        writer.write_all(VERTICAL_LINE.as_bytes())?;
201
202        let mut formatters = formatters.iter();
203        for space in column_widths.iter().copied().map(NonZeroUsize::get) {
204            if let Some(formatter) = formatters.next() {
205                formatter(&row, &mut value).expect("formatting to a string shouldn't fail");
206            }
207            draw_cell(&mut writer, &value, space)?;
208            value.clear();
209        }
210
211        writer.write_all("\n".as_bytes())?;
212    }
213
214    write_table_end(writer, column_widths)
215}
216
217fn write_table_start<W: Write, const COLUMN_COUNT: usize>(
218    to: W,
219    column_names: &[&str; COLUMN_COUNT],
220    column_widths: &[NonZeroUsize; COLUMN_COUNT],
221) -> Result<BufWriter<W>, io::Error> {
222    let _: () = const { assert!(COLUMN_COUNT > 0, "table must have columns") };
223
224    let mut writer = BufWriter::new(to);
225    draw_horizontal_line(&mut writer, column_widths, TOP_LEFT, TOP_RIGHT, TOP_INTERSECTION)?;
226
227    writer.write_all(VERTICAL_LINE.as_bytes())?;
228    for (space, name) in column_widths.iter().copied().map(NonZeroUsize::get).zip(column_names) {
229        draw_cell(&mut writer, name, space)?;
230    }
231    writer.write_all("\n".as_bytes())?;
232
233    draw_horizontal_line(
234        &mut writer,
235        column_widths,
236        LEFT_INTERSECTION,
237        RIGHT_INTERSECTION,
238        INTERSECTION,
239    )?;
240
241    Ok(writer)
242}
243
244fn write_table_end<W: Write, const COLUMN_COUNT: usize>(
245    mut writer: BufWriter<W>,
246    column_widths: &[NonZeroUsize; COLUMN_COUNT],
247) -> Result<(), io::Error> {
248    draw_horizontal_line(
249        &mut writer,
250        column_widths,
251        BOTTOM_LEFT,
252        BOTTOM_RIGHT,
253        BOTTOM_INTERSECTION,
254    )?;
255    writer.flush()
256}
257
258fn draw_horizontal_line<const COLUMN_COUNT: usize, W: Write>(
259    writer: &mut BufWriter<W>,
260    column_widths: &[NonZeroUsize; COLUMN_COUNT],
261    left: &str,
262    right: &str,
263    intersection: &str,
264) -> io::Result<()> {
265    writer.write_all(left.as_bytes())?;
266    for (i, width) in column_widths.iter().enumerate() {
267        for _ in 0..width.get() {
268            writer.write_all(HORIZONTAL_LINE.as_bytes())?;
269        }
270        writer.write_all((if i == COLUMN_COUNT - 1 { right } else { intersection }).as_bytes())?;
271    }
272    writer.write_all("\n".as_bytes())
273}
274
275fn draw_cell<W: Write>(writer: &mut BufWriter<W>, value: &str, space: usize) -> io::Result<()> {
276    let value_width = value.width();
277    let padding = if unlikely(value_width > space) {
278        let mut remaining = space - 1;
279        for grapheme in value.graphemes(true) {
280            remaining = match remaining.checked_sub(grapheme.width()) {
281                Some(r) => r,
282                None => break,
283            };
284            writer.write_all(grapheme.as_bytes())?;
285        }
286        writer.write_all("…".as_bytes())?;
287        remaining
288    } else {
289        if value_width < space {
290            writer.write_all(" ".as_bytes())?;
291        }
292        writer.write_all(value.as_bytes())?;
293        (space - value_width).saturating_sub(1)
294    };
295    for _ in 0..padding {
296        writer.write_all(" ".as_bytes())?;
297    }
298    writer.write_all(VERTICAL_LINE.as_bytes())
299}
300
301/// Render a table from a fallible iterator.
302///
303/// It differs from [`write_table`] in that `iter` is a [`FallibleIterator`] from the [`fallible-iterator`] crate.
304///
305/// [`FallibleIterator`]: FallibleIterator
306/// [`fallible-iterator`]: fallible_iterator
307///
308/// # Errors
309///
310/// If an I/O error is encountered while writing to the `to` writer, [`FallibleIteratorTableWriteError::Io`]
311/// is returned. If the iterator produces an error when getting the next row,
312/// [`FallibleIteratorTableWriteError::Iterator`] is returned.
313#[cfg(feature = "fallible-iterator")]
314pub fn write_table_fallible<Cell: Display, Row: IntoIterator<Item = Cell>, IteratorError, const COLUMN_COUNT: usize>(
315    to: impl Write,
316    mut iter: impl FallibleIterator<Item = Row, Error = IteratorError>,
317    column_names: &[&str; COLUMN_COUNT],
318    column_widths: &[NonZeroUsize; COLUMN_COUNT],
319) -> Result<(), FallibleIteratorTableWriteError<IteratorError>> {
320    let mut writer = write_table_start(to, column_names, column_widths)?;
321
322    let mut value = String::new();
323    let ret = loop {
324        match iter.next() {
325            Ok(Some(row)) => {
326                writer.write_all(VERTICAL_LINE.as_bytes())?;
327
328                let mut row_iter = row.into_iter();
329                for space in column_widths.iter().copied().map(NonZeroUsize::get) {
330                    if let Some(col) = row_iter.next() {
331                        write!(&mut value, "{}", col).expect("formatting to a string shouldn't fail");
332                    }
333                    draw_cell(&mut writer, &value, space)?;
334                    value.clear();
335                }
336
337                writer.write_all("\n".as_bytes())?;
338            }
339            Ok(None) => break Ok(()),
340            Err(err) => break Err(FallibleIteratorTableWriteError::Iterator(err)),
341        }
342    };
343
344    write_table_end(writer, column_widths)?;
345    ret
346}
347
348/// Render a table from a fallible iterator using custom formatters.
349///
350/// It differs from [`write_table_with_fmt`] in that `iter` is a [`FallibleIterator`] from the [`fallible-iterator`]
351/// crate.
352///
353/// [`FallibleIterator`]: FallibleIterator
354/// [`fallible-iterator`]: fallible_iterator
355///
356/// # Errors
357///
358/// If an I/O error is encountered while writing to the `to` writer, [`FallibleIteratorTableWriteError::Io`]
359/// is returned. If the iterator produces an error when getting the next row,
360/// [`FallibleIteratorTableWriteError::Iterator`] is returned.
361#[cfg(feature = "fallible-iterator")]
362pub fn write_table_with_fmt_fallible<Row, IteratorError, const COLUMN_COUNT: usize>(
363    to: impl Write,
364    mut iter: impl FallibleIterator<Item = Row, Error = IteratorError>,
365    formatters: &[impl Fn(&Row, &mut String) -> fmt::Result; COLUMN_COUNT],
366    column_names: &[&str; COLUMN_COUNT],
367    column_widths: &[NonZeroUsize; COLUMN_COUNT],
368) -> Result<(), FallibleIteratorTableWriteError<IteratorError>> {
369    let mut writer = write_table_start(to, column_names, column_widths)?;
370
371    let mut value = String::new();
372    let ret = loop {
373        match iter.next() {
374            Ok(Some(row)) => {
375                writer.write_all(VERTICAL_LINE.as_bytes())?;
376
377                let mut formatters = formatters.iter();
378                for space in column_widths.iter().copied().map(NonZeroUsize::get) {
379                    if let Some(formatter) = formatters.next() {
380                        formatter(&row, &mut value).expect("formatting to a string shouldn't fail");
381                    }
382                    draw_cell(&mut writer, &value, space)?;
383                    value.clear();
384                }
385
386                writer.write_all("\n".as_bytes())?;
387            }
388            Ok(None) => break Ok(()),
389            Err(err) => break Err(FallibleIteratorTableWriteError::Iterator(err)),
390        }
391    };
392
393    write_table_end(writer, column_widths)?;
394    ret
395}
396
397/// Error type of [`write_table_fallible`].
398#[cfg(feature = "fallible-iterator")]
399#[derive(Debug)]
400pub enum FallibleIteratorTableWriteError<IteratorError> {
401    Io(io::Error),
402    Iterator(IteratorError),
403}
404
405#[cfg(feature = "fallible-iterator")]
406impl<E> From<io::Error> for FallibleIteratorTableWriteError<E> {
407    fn from(error: io::Error) -> Self {
408        Self::Io(error)
409    }
410}
411
412#[cfg(feature = "fallible-iterator")]
413impl<E: Display> Display for FallibleIteratorTableWriteError<E> {
414    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
415        match self {
416            FallibleIteratorTableWriteError::Io(err) => write!(f, "failed to write table: {}", err),
417            FallibleIteratorTableWriteError::Iterator(err) => write!(f, "failed to get next table row: {}", err),
418        }
419    }
420}
421
422#[cfg(feature = "fallible-iterator")]
423impl<E: Debug + Display> std::error::Error for FallibleIteratorTableWriteError<E> {}
424
425#[allow(clippy::inline_always)]
426#[inline(always)]
427const fn unlikely(b: bool) -> bool {
428    if b {
429        cold();
430    }
431    b
432}
433
434#[inline(always)]
435#[cold]
436const fn cold() {}
437
438#[cfg(test)]
439mod tests {
440    use super::*;
441    use unicode_width::UnicodeWidthStr;
442
443    macro_rules! nz {
444        ($val:expr) => {
445            ::core::num::NonZeroUsize::new($val).unwrap()
446        };
447    }
448
449    fn assert_consistent_width(output: &str) {
450        let mut width = None;
451        for line in output.lines() {
452            if let Some(width) = width {
453                assert_eq!(line.width(), width);
454            } else {
455                width = Some(line.width());
456            }
457        }
458    }
459
460    #[test]
461    fn simple() {
462        let data = [["q3rrq", "qfqh843f9", "qa"], ["123", "", "aaaaaa"]];
463        let mut output = Vec::new();
464        write_table(&mut output, data.iter(), &["A", "B", "C"], &[nz!(5), nz!(10), nz!(4)])
465            .expect("write_table failed");
466        let output = String::from_utf8(output).expect("valid UTF-8");
467        assert_eq!(
468            output,
469            "╭─────┬──────────┬────╮
470│ A   │ B        │ C  │
471├─────┼──────────┼────┤
472│q3rrq│ qfqh843f9│ qa │
473│ 123 │          │aaa…│
474╰─────┴──────────┴────╯
475"
476        );
477        assert_consistent_width(&output);
478    }
479
480    #[test]
481    fn iter() {
482        use std::borrow::ToOwned;
483        use std::io::{BufRead, BufReader};
484
485        let data = "asdf eraf r34r23r
486awhfde 93ry af3f98
487awefz 234 23
4883442342 1 4";
489
490        let mut output = Vec::new();
491        let file = BufReader::new(data.as_bytes());
492        write_table(
493            &mut output,
494            file.lines()
495                .map(|line| line.unwrap().split(' ').map(ToOwned::to_owned).collect::<Vec<_>>()),
496            &["col1", "col2", "col3"],
497            &[nz!(5), nz!(7), nz!(10)],
498        )
499        .expect("write_table failed");
500
501        let output = String::from_utf8(output).expect("valid UTF-8");
502        assert_eq!(
503            output,
504            "╭─────┬───────┬──────────╮
505│ col1│ col2  │ col3     │
506├─────┼───────┼──────────┤
507│ asdf│ eraf  │ r34r23r  │
508│awhf…│ 93ry  │ af3f98   │
509│awefz│ 234   │ 23       │
510│3442…│ 1     │ 4        │
511╰─────┴───────┴──────────╯
512"
513        );
514        assert_consistent_width(&output);
515    }
516
517    #[test]
518    fn empty() {
519        let data: [[&str; 0]; 0] = [];
520        let mut output = Vec::new();
521        write_table(&mut output, data.iter(), &["A", "B"], &[nz!(1), nz!(1)]).expect("write_table failed");
522
523        let output = String::from_utf8(output).expect("valid UTF-8");
524        assert_eq!(
525            output,
526            "╭─┬─╮
527│A│B│
528├─┼─┤
529╰─┴─╯
530"
531        );
532        assert_consistent_width(&output);
533    }
534
535    #[test]
536    fn not_enough_data() {
537        let data = [["A"], ["B"], ["C"]];
538        let mut output = Vec::new();
539        write_table(&mut output, data.iter(), &["1", "2"], &[nz!(3), nz!(5)]).expect("write_table failed");
540
541        let output = String::from_utf8(output).expect("valid UTF-8");
542        assert_eq!(
543            output,
544            "╭───┬─────╮
545│ 1 │ 2   │
546├───┼─────┤
547│ A │     │
548│ B │     │
549│ C │     │
550╰───┴─────╯
551"
552        );
553        assert_consistent_width(&output);
554    }
555
556    #[test]
557    fn too_much_data() {
558        let data = [["A", "B", "C"], ["D", "E", "F"], ["G", "H", "I"]];
559        let mut output = Vec::new();
560        write_table(&mut output, data.iter(), &["1", "2"], &[nz!(3), nz!(3)]).expect("write_table failed");
561
562        let output = String::from_utf8(output).expect("valid UTF-8");
563        assert_eq!(
564            output,
565            "╭───┬───╮
566│ 1 │ 2 │
567├───┼───┤
568│ A │ B │
569│ D │ E │
570│ G │ H │
571╰───┴───╯
572"
573        );
574        assert_consistent_width(&output);
575    }
576
577    #[test]
578    fn unicode() {
579        let data = [["あいうえお", "スペース"], ["🦀🦀🦀🦀🦀🦀", "🗿🗿🗿"]];
580        let mut output = Vec::new();
581        write_table(&mut output, data.iter(), &["A", "B"], &[nz!(12), nz!(7)]).expect("write_table failed");
582
583        let output = String::from_utf8(output).expect("valid UTF-8");
584        assert_eq!(
585            output,
586            "╭────────────┬───────╮
587│ A          │ B     │
588├────────────┼───────┤
589│ あいうえお │スペー…│
590│🦀🦀🦀🦀🦀🦀│ 🗿🗿🗿│
591╰────────────┴───────╯
592"
593        );
594        assert_consistent_width(&output);
595    }
596
597    mod custom_fmt {
598        use super::*;
599        use std::net::Ipv4Addr;
600
601        #[test]
602        fn addr() {
603            let addrs = [
604                Ipv4Addr::new(192, 168, 0, 1),
605                Ipv4Addr::new(1, 1, 1, 1),
606                Ipv4Addr::new(255, 127, 63, 31),
607            ];
608            let column_names = ["Full address", "BE bits", "Private"];
609            let column_widths = [nz!(17), nz!(12), nz!(7)];
610
611            let formatters: [fn(&Ipv4Addr, &mut String) -> fmt::Result; 3] = [
612                |a, f| write!(f, "{}", a),
613                |a, f| write!(f, "0x{:x}", a.to_bits().to_be()),
614                |a, f| write!(f, "{}", if a.is_private() { "yes" } else { "no" }),
615            ];
616
617            let mut output = Vec::new();
618            write_table_with_fmt(
619                &mut output,
620                addrs.iter().copied(),
621                &formatters,
622                &column_names,
623                &column_widths,
624            )
625            .expect("write_table failed");
626
627            let output = String::from_utf8(output).expect("valid UTF-8");
628            assert_eq!(
629                output,
630                "╭─────────────────┬────────────┬───────╮
631│ Full address    │ BE bits    │Private│
632├─────────────────┼────────────┼───────┤
633│ 192.168.0.1     │ 0x100a8c0  │ yes   │
634│ 1.1.1.1         │ 0x1010101  │ no    │
635│ 255.127.63.31   │ 0x1f3f7fff │ no    │
636╰─────────────────┴────────────┴───────╯
637"
638            );
639            assert_consistent_width(&output);
640        }
641
642        #[test]
643        fn uppercase() {
644            let data = [["aaa", "bbb", "ccc", "ddd", "eee"], ["fff", "ggg", "hhh", "iii", "jjj"]];
645            let write_upper =
646                |index: usize| move |row: &&[&str; 5], f: &mut String| write!(f, "{}", row[index].to_ascii_uppercase());
647
648            let mut output = Vec::new();
649            write_table_with_fmt(
650                &mut output,
651                data.iter(),
652                &[
653                    write_upper(0),
654                    write_upper(1),
655                    write_upper(2),
656                    write_upper(3),
657                    write_upper(4),
658                ],
659                &["1", "2", "3", "4", "5"],
660                &[nz!(3); 5],
661            )
662            .expect("write_table failed");
663
664            let output = String::from_utf8(output).expect("valid UTF-8");
665            assert_eq!(
666                output,
667                "╭───┬───┬───┬───┬───╮
668│ 1 │ 2 │ 3 │ 4 │ 5 │
669├───┼───┼───┼───┼───┤
670│AAA│BBB│CCC│DDD│EEE│
671│FFF│GGG│HHH│III│JJJ│
672╰───┴───┴───┴───┴───╯
673"
674            );
675            assert_consistent_width(&output);
676        }
677    }
678
679    #[cfg(feature = "fallible-iterator")]
680    mod fallible_iterator {
681        use super::*;
682        use ::fallible_iterator::FallibleIterator;
683
684        #[test]
685        fn fallible_ok() {
686            let data = [["q3rrq", "qfqh843f9", "qa"], ["123", "", "aaaaaa"]];
687            let mut output = Vec::new();
688            write_table_fallible(
689                &mut output,
690                ::fallible_iterator::convert(data.iter().map(Ok::<_, ()>)),
691                &["A", "B", "C"],
692                &[nz!(5), nz!(10), nz!(4)],
693            )
694            .expect("write_table failed");
695
696            let output = String::from_utf8(output).expect("valid UTF-8");
697            assert_eq!(
698                output,
699                "╭─────┬──────────┬────╮
700│ A   │ B        │ C  │
701├─────┼──────────┼────┤
702│q3rrq│ qfqh843f9│ qa │
703│ 123 │          │aaa…│
704╰─────┴──────────┴────╯
705"
706            );
707            assert_consistent_width(&output);
708        }
709
710        #[test]
711        fn fallible_err() {
712            let data = [["q3rrq", "qfqh843f9", "qa"], ["123", "", "aaaaaa"]];
713            let mut output = Vec::new();
714            let result = write_table_fallible(
715                &mut output,
716                ::fallible_iterator::convert(data.iter().map(Ok::<_, &str>))
717                    .take(1)
718                    .chain(::fallible_iterator::once_err("error")),
719                &["A", "B", "C"],
720                &[nz!(5), nz!(10), nz!(4)],
721            );
722            assert!(matches!(result, Err(FallibleIteratorTableWriteError::Iterator(_))));
723
724            let output = String::from_utf8(output).expect("valid UTF-8");
725            assert_eq!(
726                output,
727                "╭─────┬──────────┬────╮
728│ A   │ B        │ C  │
729├─────┼──────────┼────┤
730│q3rrq│ qfqh843f9│ qa │
731╰─────┴──────────┴────╯
732"
733            );
734            assert_consistent_width(&output);
735        }
736
737        #[test]
738        fn fallible_fmt() {
739            let data = [["q3rrq", "qfqh843f9", "qa"], ["123", "", "aaaaaa"]];
740            let len = |index: usize| move |row: &&[&str; 3], f: &mut String| write!(f, "{}", row[index].len());
741
742            let mut output = Vec::new();
743            write_table_with_fmt_fallible(
744                &mut output,
745                ::fallible_iterator::convert(data.iter().map(Ok::<_, ()>)),
746                &[len(0), len(1), len(2)],
747                &["A", "B", "C"],
748                &[nz!(3); 3],
749            )
750            .expect("write_table failed");
751
752            let output = String::from_utf8(output).expect("valid UTF-8");
753            assert_eq!(
754                output,
755                "╭───┬───┬───╮
756│ A │ B │ C │
757├───┼───┼───┤
758│ 5 │ 9 │ 2 │
759│ 3 │ 0 │ 6 │
760╰───┴───┴───╯
761"
762            );
763            assert_consistent_width(&output);
764        }
765    }
766}
767
768/// ```compile_fail
769/// let data: [[&str; 0]; 0] = [];
770/// tinytable::write_table(::std::io::stdout(), data.iter(), &[], &[]).unwrap();
771/// ```
772#[cfg(doctest)]
773fn no_columns() {}