xwc 0.5.0

A small wc-style command line tool
Documentation
use bytesize::ByteSize;

use crate::{Config, Counts};

pub fn render_rows<'a>(
    config: &Config,
    rows: impl IntoIterator<Item = (Counts, Option<&'a str>)>,
) -> Vec<Vec<String>> {
    let rows = rows.into_iter().collect::<Vec<_>>();
    let has_labels = rows.iter().any(|(_, label)| label.is_some());
    let mut rendered_rows = Vec::new();

    if config.show_headings {
        rendered_rows.push(headings(config, has_labels));
    }

    for (counts, label) in rows {
        rendered_rows.push(fields(config, counts, label));
    }

    rendered_rows
}

pub fn column_widths(rows: &[Vec<String>]) -> Vec<usize> {
    let column_count = rows.iter().map(Vec::len).max().unwrap_or(0);
    let mut widths = vec![0; column_count];

    for row in rows {
        for (width, field) in widths.iter_mut().zip(row) {
            *width = (*width).max(field.len());
        }
    }

    widths
}

pub fn format_byte_count(bytes: u64, human_readable: bool) -> String {
    if human_readable {
        ByteSize::b(bytes).display().iec_short().to_string()
    } else {
        bytes.to_string()
    }
}

fn headings(config: &Config, has_labels: bool) -> Vec<String> {
    let mut fields = Vec::new();

    if config.show_lines {
        fields.push("lines".to_owned());
    }

    if config.show_words {
        fields.push("words".to_owned());
    }

    if config.show_chars {
        fields.push("chars".to_owned());
    }

    if config.show_max_line_length {
        fields.push("max-line".to_owned());
    }

    if config.show_bytes {
        fields.push(byte_heading(config).to_owned());
    }

    if has_labels {
        fields.push("file".to_owned());
    }

    fields
}

fn fields(config: &Config, counts: Counts, label: Option<&str>) -> Vec<String> {
    let mut fields = Vec::new();

    if config.show_lines {
        fields.push(counts.lines.to_string());
    }

    if config.show_words {
        fields.push(counts.words.to_string());
    }

    if config.show_chars {
        fields.push(counts.chars.to_string());
    }

    if config.show_max_line_length {
        fields.push(counts.max_line_length.to_string());
    }

    if config.show_bytes {
        fields.push(format_byte_count(counts.bytes, config.human_readable));
    }

    if let Some(label) = label {
        fields.push(label.to_owned());
    }

    fields
}

fn byte_heading(config: &Config) -> &'static str {
    if config.human_readable {
        "size"
    } else {
        "bytes"
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn default_config() -> Config {
        Config {
            show_lines: true,
            show_words: false,
            show_chars: false,
            show_bytes: true,
            show_max_line_length: false,
            show_headings: true,
            human_readable: false,
            jobs: None,
            globs: Vec::new(),
            files: Vec::new(),
        }
    }

    #[test]
    fn renders_default_headings_and_fields() {
        let rendered = render_rows(
            &default_config(),
            [(
                Counts {
                    lines: 2,
                    words: 0,
                    chars: 0,
                    bytes: 14,
                    max_line_length: 0,
                },
                None,
            )],
        );

        assert_eq!(
            rendered,
            vec![
                vec!["lines".to_owned(), "bytes".to_owned()],
                vec!["2".to_owned(), "14".to_owned()],
            ]
        );
    }

    #[test]
    fn renders_label_heading_when_any_row_has_label() {
        let rendered = render_rows(
            &default_config(),
            [(
                Counts {
                    lines: 2,
                    words: 0,
                    chars: 0,
                    bytes: 14,
                    max_line_length: 0,
                },
                Some("a.txt"),
            )],
        );

        assert_eq!(
            rendered,
            vec![
                vec!["lines".to_owned(), "bytes".to_owned(), "file".to_owned()],
                vec!["2".to_owned(), "14".to_owned(), "a.txt".to_owned()],
            ]
        );
    }

    #[test]
    fn formats_human_readable_bytes() {
        assert_eq!(format_byte_count(1024, true), "1.0K");
        assert_eq!(format_byte_count(1024, false), "1024");
    }
}