meansd-cli 1.4.0

calculate mean and standard deviation (CLI)
#![forbid(unsafe_code)]
#![deny(clippy::all)]
#![warn(clippy::pedantic, clippy::nursery, clippy::cargo)]

mod cli;
mod config;
mod log;

use std::io::{self, IsTerminal};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;

use prettytable::format::FormatBuilder;
use prettytable::{cell, Row, Table};
use smooth::Smooth;

use meansd::binned::{self, Bin, Binned};
use meansd::MeanSD;

use crate::config::Config;
use crate::config::ErrorHandler;
use crate::config::StandardDeviationMode;

fn main() {
    let args = cli::build().get_matches();
    let config = Config::from_args(&args);

    if io::stdin().is_terminal() {
        log::warn("input is read from the terminal");
        log::warn("pipe data into this app");
        log::warn("press CTRL-D to exit");
    };

    let numbers = io::stdin()
        .lines()
        .filter_map(|line| line_to_number(line, &config));

    if let Some(breaks) = &config.bin_breaks {
        let mut binned = binned::Breaks::new(breaks);
        binned.update_all(numbers, &config);
        binned.print(&config);
    } else if let Some(width) = &config.bin_width {
        let mut binned = binned::Width::new(*width);
        binned.update_all(numbers, &config);
        binned.print(&config);
    } else {
        meansd(numbers, &config);
    };
}

fn line_to_number(line: io::Result<String>, config: &Config) -> Option<f64> {
    let line = line.unwrap();
    let line = line.trim();

    match line.parse::<f64>() {
        Ok(v) => Some(v),

        Err(e) => match config.error_handler {
            ErrorHandler::Panic => {
                log::error(format!("{e}: {line}"));
                std::process::exit(1);
            }

            ErrorHandler::Skip => None,

            ErrorHandler::Warn => {
                log::warn(format!("{e}: {line}"));
                None
            }
        },
    }
}

fn meansd<I>(numbers: I, config: &Config)
where
    I: Iterator<Item = f64>,
{
    let mut meansd = MeanSD::default();

    let interrupt = Arc::new(AtomicBool::new(false));
    let interrupt_setter = interrupt.clone();

    ctrlc::set_handler(move || {
        interrupt_setter.store(true, Ordering::SeqCst);
    })
    .expect("error setting interrupt handler");

    for x in numbers {
        meansd.update(x);

        if interrupt.load(Ordering::SeqCst) {
            println!();
            break;
        }

        if config.progress.map_or(false, |p| meansd.size() % p == 0.0) {
            log::progress(
                meansd.size(),
                meansd.formatted(StandardDeviationMode::Sample),
            );
        }
    }

    println!("{}", meansd.formatted(config.sd_mode));
}

trait MeanSDExt {
    fn formatted(&self, sd_mode: StandardDeviationMode) -> String;
    fn stdev(&self, sd_mode: StandardDeviationMode) -> f64;
}

impl MeanSDExt for MeanSD {
    fn formatted(&self, sd_mode: StandardDeviationMode) -> String {
        format!(
            "n={} \u{2205} {} \u{b1} {}",
            self.size(),
            self.mean().smooth(),
            self.stdev(sd_mode).smooth(),
        )
    }

    fn stdev(&self, sd_mode: StandardDeviationMode) -> f64 {
        match sd_mode {
            StandardDeviationMode::Population => self.pstdev(),
            StandardDeviationMode::Sample => self.sstdev(),
        }
    }
}

trait BinExt {
    fn bottom_str(&self) -> String;
    fn top_str(&self) -> String;
}

impl BinExt for Bin {
    fn bottom_str(&self) -> String {
        if self.bottom() == f64::NEG_INFINITY {
            "-\u{221e}".to_string()
        } else {
            format!("{}", self.bottom())
        }
    }

    fn top_str(&self) -> String {
        if self.top() == f64::INFINITY {
            "\u{221e}".to_string()
        } else {
            format!("{}", self.top())
        }
    }
}

trait BinnedExt: Binned {
    fn update_all<I>(&mut self, numbers: I, config: &Config)
    where
        I: Iterator<Item = f64>,
    {
        let interrupt = Arc::new(AtomicBool::new(false));
        let interrupt_setter = interrupt.clone();

        ctrlc::set_handler(move || {
            interrupt_setter.store(true, Ordering::SeqCst);
        })
        .expect("error setting interrupt handler");

        for x in numbers {
            self.update(x);

            if interrupt.load(Ordering::SeqCst) {
                println!();
                break;
            }

            if config
                .progress
                .map_or(false, |p| self.all().size() % p == 0.0)
            {
                for (bin, meansd) in self.data() {
                    let size = self.all().size();

                    log::progress(
                        size,
                        format!(
                            "{} - {} \u{2192} {}",
                            bin.bottom_str(),
                            bin.top_str(),
                            meansd.formatted(StandardDeviationMode::Sample),
                        ),
                    );
                }
            }
        }
    }

    fn print(&self, config: &Config) {
        let mut table = Table::new();
        let format = FormatBuilder::new().column_separator(' ').build();
        table.set_format(format);

        let mut titles = Row::empty();

        titles.add_cell(cell!(bu->"From"));
        titles.add_cell(cell!(bur->"To"));
        titles.add_cell(cell!(bur->"Size"));
        titles.add_cell(cell!(bur->"Mean"));
        titles.add_cell(cell!(bur->"SD"));

        table.set_titles(titles);

        for (bin, meansd) in self.data() {
            let size = meansd.size();
            let mean = meansd.mean().smooth();
            let stdev = meansd.stdev(config.sd_mode).smooth();

            let mut row = Row::empty();
            row.add_cell(cell!(r->bin.bottom_str()));
            row.add_cell(cell!(r->bin.top_str()));
            row.add_cell(cell!(r->size));
            row.add_cell(cell!(r->mean));
            row.add_cell(cell!(r->stdev));

            table.add_row(row);
        }

        println!();
        table.printstd();
        println!();
        println!("{}", self.all().formatted(config.sd_mode));
    }
}

impl BinnedExt for meansd::binned::Breaks {}
impl BinnedExt for meansd::binned::Width {}