aki-stats 0.2.1

output the statistics of text, like a wc of linux command.
Documentation
use crate::conf::CmdOptConf;
use crate::util::err::BrokenPipeError;
use runnel::RunnelIoe;
use std::fmt::Write as FmtWrite;

pub fn run(sioe: &RunnelIoe, conf: &CmdOptConf) -> anyhow::Result<()> {
    let r = run_0(sioe, conf);
    if r.is_broken_pipe() {
        return Ok(());
    }
    r
}

#[derive(Default)]
struct Stats {
    byte_count: u64,
    char_count: u64,
    line_count: u64,
    word_count: u64,
    max_line_bytes: u64,
}

struct StatsAscii {
    ascii: [u64; 128],
}
impl StatsAscii {
    fn new() -> StatsAscii {
        Self { ascii: [0; 128] }
    }
    fn count_up(&mut self, b: u8) {
        if b < 128 {
            self.ascii[b as usize] += 1;
        }
    }
    fn get_count(&self, idx: usize) -> u64 {
        if idx < 128 {
            self.ascii[idx]
        } else {
            0
        }
    }
    fn max(&self) -> u64 {
        *self.ascii.iter().max().unwrap_or(&0)
    }
}
impl std::default::Default for StatsAscii {
    fn default() -> Self {
        Self::new()
    }
}

fn run_0(sioe: &RunnelIoe, conf: &CmdOptConf) -> anyhow::Result<()> {
    let mut stats = Stats::default();
    let mut map_ascii = StatsAscii::default();
    // input
    for line in sioe.pg_in().lines() {
        let line_s = line?;
        let line_ss = line_s.as_str();
        //
        run_00(conf, line_ss, &mut stats, &mut map_ascii)?;
    }
    // output
    {
        let out_s = make_out_s_from_stats(conf, &stats)?;
        sioe.pg_out().write_line(out_s)?;
        sioe.pg_out().flush_line()?;
    }
    if conf.flg_map_ascii {
        if conf.is_opt_uc_x_map_ascii_rust_src() {
            let out_s = make_out_s_from_map_ascii_1(&map_ascii)?;
            sioe.pg_out().write_line(out_s)?;
        } else {
            let mut vec = make_out_s_from_map_ascii_2(&map_ascii)?;
            vec.reverse();
            while let Some(v) = vec.pop() {
                sioe.pg_out().write_line(v)?;
            }
        }
        sioe.pg_out().flush_line()?;
    }
    //
    Ok(())
}

fn run_00(
    conf: &CmdOptConf,
    line_ss: &str,
    stats: &mut Stats,
    map_ascii: &mut StatsAscii,
) -> anyhow::Result<()> {
    let line_len: usize = line_ss.len();
    //
    stats.line_count += 1;
    //
    let line_bytes = line_len as u64;
    if conf.flg_bytes {
        stats.byte_count += line_bytes;
    }
    if conf.flg_max_line_bytes {
        stats.max_line_bytes = stats.max_line_bytes.max(line_bytes);
    }
    if conf.flg_chars || conf.flg_words {
        let mut prev_c: char = ' ';
        for c in line_ss.chars() {
            if conf.flg_chars {
                stats.char_count += 1;
            }
            if conf.flg_words {
                if prev_c.is_ascii_whitespace() && !c.is_ascii_whitespace() {
                    stats.word_count += 1;
                }
                prev_c = c;
            }
            if conf.flg_map_ascii && c.is_ascii() {
                map_ascii.count_up(c as u8);
            }
        }
    } else if conf.flg_map_ascii {
        for b in line_ss.as_bytes() {
            map_ascii.count_up(*b);
        }
    }
    Ok(())
}

fn make_out_s_from_stats(conf: &CmdOptConf, stats: &Stats) -> anyhow::Result<String> {
    let mut vec: Vec<String> = Vec::new();
    if conf.flg_lines {
        vec.push(my_formatted(conf, "lines", stats.line_count)?);
    }
    if conf.flg_bytes {
        vec.push(my_formatted(conf, "bytes", stats.byte_count)?);
    }
    if conf.flg_chars {
        vec.push(my_formatted(conf, "chars", stats.char_count)?);
    }
    if conf.flg_words {
        vec.push(my_formatted(conf, "words", stats.word_count)?);
    }
    if conf.flg_max_line_bytes {
        vec.push(my_formatted(conf, "max", stats.max_line_bytes)?);
    }
    Ok(vec.join(", "))
}

fn my_formatted(conf: &CmdOptConf, label: &str, num: u64) -> anyhow::Result<String> {
    let mut s = String::new();
    s.write_fmt(format_args!(
        "{}:\"{}\"",
        label,
        conf.opt_locale.formatted_string(num)
    ))?;
    Ok(s)
}

fn make_out_s_from_map_ascii_1(map_ascii: &StatsAscii) -> anyhow::Result<String> {
    let mut vec: Vec<String> = Vec::new();
    let max_val = map_ascii.max();
    for i in 0x00..0x80 {
        let val = map_ascii.get_count(i);
        let val = val * 255 / max_val;
        vec.push(format!("{}", val as u8));
    }
    //
    Ok(format!(
        "const ASCII_STOCHAS: [u8;128] = [{}];",
        vec.join(", ")
    ))
}

fn make_out_s_from_map_ascii_2(map_ascii: &StatsAscii) -> anyhow::Result<Vec<String>> {
    let mut vec: Vec<String> = Vec::new();
    let mut ascii_ctrl: u64 = 0;
    let mut ascii_ctrl_ht: u64 = 0;
    let mut ascii_ctrl_vt: u64 = 0;
    //let mut ascii_ctrl_lf: u64 = 0;
    //let mut ascii_ctrl_cr: u64 = 0;
    for i in 0..0x1F {
        let val = map_ascii.get_count(i);
        match i {
            0x09 => ascii_ctrl_ht = val,
            0x0B => ascii_ctrl_vt = val,
            //0x0A => ascii_ctrl_lf = val,
            //0x0D => ascii_ctrl_cr = val,
            _ => ascii_ctrl += val,
        }
    }
    ascii_ctrl += map_ascii.get_count(0x7F);
    vec.push(format!("ctrl: --: {ascii_ctrl}"));
    //vec.push(format!("ctrl: lf: {ascii_ctrl_lf}"));
    //vec.push(format!("ctrl: cr: {ascii_ctrl_cr}"));
    vec.push(format!("ctrl: ht: {ascii_ctrl_ht}"));
    vec.push(format!("ctrl: vt: {ascii_ctrl_vt}"));
    //
    vec.push(format!("0x20: SP: {}", map_ascii.get_count(0x20)));
    for i in 0x21..0x7F {
        vec.push(format!(
            "0x{:02x}:  {}: {}",
            i,
            i as u8 as char,
            map_ascii.get_count(i)
        ));
    }
    Ok(vec)
}