funst 0.1.1

A tiny command-line tool to calculate fundamental statistics of numbers given via stdin
use anyhow::Context;
use ordered_float::OrderedFloat;
use std::io::BufRead;
use structopt::StructOpt;

#[derive(Debug, StructOpt)]
#[structopt(about = "Calculate fundamental statistics of numbers given via the standard input.")]
struct Opt {}

fn main() -> anyhow::Result<()> {
    let _opt = Opt::from_args();
    let mut values = Vec::new();

    for (i, line) in std::io::stdin().lock().lines().enumerate() {
        let line = line.with_context(|| format!("Failed to read the line {}", i + 1))?;
        let value: f64 = line
            .parse()
            .with_context(|| format!("Line {} is not a number: {:?}", i + 1, line))?;
        anyhow::ensure!(
            value.is_finite(),
            "Line {} is not a finite number: {}",
            i + 1,
            value
        );
        values.push(value);
    }
    anyhow::ensure!(!values.is_empty(), "Empty standard input");

    values.sort_by_key(|v| OrderedFloat(*v));
    let n = values.len();
    let stat = Stat {
        count: n,
        mean: mean(&values),
        stddev: stddev(&values),
        min: values[0],
        median: if n % 2 == 1 {
            values[n / 2]
        } else {
            (values[n / 2 - 1] + values[n / 2]) / 2.0
        },
        max: values[n - 1],
    };
    serde_json::to_writer_pretty(std::io::stdout().lock(), &stat)?;
    println!();

    Ok(())
}

#[derive(Debug, serde::Serialize)]
struct Stat {
    count: usize,
    mean: f64,
    stddev: f64,
    min: f64,
    median: f64,
    max: f64,
}

fn mean(vs: &[f64]) -> f64 {
    vs.iter().sum::<f64>() / vs.len() as f64
}

fn stddev(vs: &[f64]) -> f64 {
    let n = vs.len() as f64;
    let m = mean(vs);
    (vs.iter().map(|&v| (v - m).powi(2)).sum::<f64>() / n).sqrt()
}