7 changed files with 594 additions and 0 deletions
@ -0,0 +1,15 @@ |
|||||||
|
[package] |
||||||
|
name = "lowcharts" |
||||||
|
version = "0.1.0" |
||||||
|
authors = ["JuanLeon Lahoz <juanleon.lahoz@gmail.com>"] |
||||||
|
edition = "2018" |
||||||
|
|
||||||
|
[dependencies] |
||||||
|
clap = "3.0.0-beta.2" |
||||||
|
yansi = "0.5.0" |
||||||
|
isatty = "0.1" |
||||||
|
derive_builder = "0.10.0" |
||||||
|
regex = "1.4.5" |
||||||
|
|
||||||
|
[dev-dependencies] |
||||||
|
float_eq = "0.5.0" |
||||||
@ -0,0 +1,139 @@ |
|||||||
|
use std::fmt; |
||||||
|
use std::ops::Range; |
||||||
|
|
||||||
|
use yansi::Color::{Red, Blue, Green}; |
||||||
|
|
||||||
|
use crate::stats::Stats; |
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug)] |
||||||
|
pub struct Bucket { |
||||||
|
range: Range<f64>, |
||||||
|
count: usize, |
||||||
|
} |
||||||
|
|
||||||
|
impl Bucket { |
||||||
|
fn new(range: Range<f64>) -> Bucket { |
||||||
|
Bucket { |
||||||
|
range, |
||||||
|
count: 0, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fn inc(&mut self) { |
||||||
|
self.count += 1; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Debug)] |
||||||
|
pub struct Histogram { |
||||||
|
vec: Vec<Bucket>, |
||||||
|
max: f64, |
||||||
|
step: f64, |
||||||
|
top: usize, |
||||||
|
last: usize, |
||||||
|
stats: Stats, |
||||||
|
} |
||||||
|
|
||||||
|
impl Histogram { |
||||||
|
pub fn new(size: usize, step: f64, stats: Stats) -> Histogram { |
||||||
|
let mut b = Histogram { |
||||||
|
vec: Vec::with_capacity(size), |
||||||
|
max: stats.min + (step * size as f64), |
||||||
|
step, |
||||||
|
top: 0, |
||||||
|
last: size - 1, |
||||||
|
stats, |
||||||
|
}; |
||||||
|
let mut lower = b.stats.min; |
||||||
|
for _ in 0..size { |
||||||
|
b.vec.push(Bucket::new(lower..lower + step)); |
||||||
|
lower += step; |
||||||
|
} |
||||||
|
b |
||||||
|
} |
||||||
|
|
||||||
|
pub fn load(&mut self, vec: &[f64]) { |
||||||
|
for x in vec { |
||||||
|
self.add(*x); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub fn add(&mut self, n: f64) { |
||||||
|
if let Some(slot) = self.find_slot(n) { |
||||||
|
self.vec[slot].inc(); |
||||||
|
self.top = self.top.max(self.vec[slot].count); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fn find_slot(&self, n: f64) -> Option<usize> { |
||||||
|
if n < self.stats.min || n > self.max { |
||||||
|
None |
||||||
|
} else { |
||||||
|
Some((((n - self.stats.min) / self.step) as usize).min(self.last)) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl fmt::Display for Histogram { |
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
||||||
|
write!(f, "{}", self.stats)?; |
||||||
|
let writer = HistWriter {width: f.width().unwrap_or(110)}; |
||||||
|
writer.write(f, &self) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
struct HistWriter { |
||||||
|
width: usize, |
||||||
|
} |
||||||
|
|
||||||
|
impl HistWriter { |
||||||
|
|
||||||
|
pub fn write(&self, f: &mut fmt::Formatter, hist: &Histogram) -> fmt::Result { |
||||||
|
let width_range = Self::get_width(hist); |
||||||
|
let width_count = ((hist.top as f64).log10().ceil() as usize).max(1); |
||||||
|
let divisor = 1.max(hist.top / self.get_max_bar_len(width_range + width_count)); |
||||||
|
writeln!( |
||||||
|
f, |
||||||
|
"each {} represents a count of {}", |
||||||
|
Red.paint("∎"), |
||||||
|
Blue.paint(divisor.to_string()), |
||||||
|
)?; |
||||||
|
for x in hist.vec.iter() { |
||||||
|
self.write_bucket(f, x, divisor, width_range, width_count)?; |
||||||
|
} |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
fn write_bucket(&self, f: &mut fmt::Formatter, bucket: &Bucket, divisor: usize, width: usize, width_count: usize) -> fmt::Result { |
||||||
|
let bar = Red.paint(format!("{:∎<width$}", "", width=bucket.count / divisor)); |
||||||
|
writeln!( |
||||||
|
f, |
||||||
|
"[{range}] [{count}] {bar}", |
||||||
|
range=Blue.paint( |
||||||
|
format!( |
||||||
|
"{:width$.3} .. {:width$.3}", |
||||||
|
bucket.range.start, |
||||||
|
bucket.range.end, |
||||||
|
width = width, |
||||||
|
) |
||||||
|
), |
||||||
|
count=Green.paint(format!("{:width$}", bucket.count, width=width_count)), |
||||||
|
bar=bar |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
fn get_width(hist: &Histogram) -> usize { |
||||||
|
format!("{:.3}", hist.stats.min).len().max(format!("{:.3}", hist.max).len()) |
||||||
|
} |
||||||
|
|
||||||
|
fn get_max_bar_len(&self, fixed_width: usize) -> usize { |
||||||
|
const EXTRA_CHARS: usize = 10; |
||||||
|
if self.width < fixed_width + EXTRA_CHARS { |
||||||
|
75 |
||||||
|
} else { |
||||||
|
self.width - fixed_width - EXTRA_CHARS |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,145 @@ |
|||||||
|
use std::env; |
||||||
|
|
||||||
|
use yansi::Paint; |
||||||
|
use yansi::Color::Red; |
||||||
|
use yansi::Color::Yellow; |
||||||
|
use clap::{AppSettings, Clap}; |
||||||
|
use isatty::stdout_isatty; |
||||||
|
use regex::Regex; |
||||||
|
|
||||||
|
#[macro_use] |
||||||
|
extern crate derive_builder; |
||||||
|
|
||||||
|
|
||||||
|
mod stats; |
||||||
|
mod histogram; |
||||||
|
mod reader; |
||||||
|
mod plot; |
||||||
|
|
||||||
|
fn disable_color_if_needed(option: &str) { |
||||||
|
match option { |
||||||
|
"no" => Paint::disable(), |
||||||
|
"auto" => { |
||||||
|
match env::var("TERM") { |
||||||
|
Ok(value) if value == "dumb" => Paint::disable(), |
||||||
|
_ => { |
||||||
|
if !stdout_isatty() { |
||||||
|
Paint::disable(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
_ => () |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/// Tool to draw low-resolution graphs in terminal
|
||||||
|
#[derive(Clap)] |
||||||
|
#[clap(setting = AppSettings::ColoredHelp)] |
||||||
|
struct Opts { |
||||||
|
/// Input file. If not present or a single dash, standard input will be used.
|
||||||
|
#[clap(default_value = "-")] |
||||||
|
input: String, |
||||||
|
/// Filter out values bigger than this
|
||||||
|
#[clap(long)] |
||||||
|
max: Option<f64>, |
||||||
|
/// Filter out values smaller than this
|
||||||
|
#[clap(long)] |
||||||
|
min: Option<f64>, |
||||||
|
/// Use colors in the output. Auto means "yes if tty with TERM != dumb and
|
||||||
|
/// no redirects".
|
||||||
|
#[clap(short, long, default_value = "auto", possible_values = &["auto", "no", "yes"])] |
||||||
|
color: String, |
||||||
|
/// Use this many characters as terminal width
|
||||||
|
#[clap(long, default_value = "110")] |
||||||
|
width: usize, |
||||||
|
/// Use a regex to capture input values. By default this will use a capture
|
||||||
|
/// group named "value". If not present, it will use first capture group.
|
||||||
|
/// If not present, a number per line is expected. Examples of regex are '
|
||||||
|
/// 200 \d+ ([0-9.]+)' (1 anonymous capture group) or 'a(a)?
|
||||||
|
/// (?P<value>[0-9.]+)' (a named capture group).
|
||||||
|
#[clap(long)] |
||||||
|
regex: Option<String>, |
||||||
|
#[clap(long)] |
||||||
|
/// Be more verbose
|
||||||
|
verbose: bool, |
||||||
|
#[clap(subcommand)] |
||||||
|
subcmd: SubCommand, |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
#[derive(Clap)] |
||||||
|
enum SubCommand { |
||||||
|
/// Plot an histogram from input values
|
||||||
|
Hist(Hist), |
||||||
|
/// Plot an 2d plot where y-values are averages of input values (as many
|
||||||
|
/// averages as wide is the plot)
|
||||||
|
Plot(Plot), |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Clap)] |
||||||
|
#[clap(setting = AppSettings::ColoredHelp)] |
||||||
|
struct Hist { |
||||||
|
/// Use that many intervals
|
||||||
|
#[clap(long, default_value = "20")] |
||||||
|
intervals: usize, |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Clap)] |
||||||
|
#[clap(setting = AppSettings::ColoredHelp)] |
||||||
|
struct Plot { |
||||||
|
/// Use that many rows for the plot
|
||||||
|
#[clap(long, default_value = "40")] |
||||||
|
height: usize, |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
fn main() { |
||||||
|
let opts: Opts = Opts::parse(); |
||||||
|
disable_color_if_needed(&opts.color); |
||||||
|
let mut builder = reader::DataReaderBuilder::default(); |
||||||
|
builder.verbose(opts.verbose); |
||||||
|
if opts.min.is_some() || opts.max.is_some() { |
||||||
|
builder.range( |
||||||
|
opts.min.unwrap_or(f64::NEG_INFINITY)..opts.max.unwrap_or(f64::INFINITY) |
||||||
|
); |
||||||
|
} |
||||||
|
if let Some(string) = opts.regex { |
||||||
|
match Regex::new(&string) { |
||||||
|
Ok(re) => { |
||||||
|
builder.regex(re); |
||||||
|
}, |
||||||
|
_ => eprintln!("[{}]: Failed to parse regex {}", Red.paint("ERROR"), string) |
||||||
|
}; |
||||||
|
} |
||||||
|
let reader = builder.build().unwrap(); |
||||||
|
|
||||||
|
let vec = reader.read(opts.input); |
||||||
|
if vec.is_empty() { |
||||||
|
eprintln!("[{}]: No data", Yellow.paint("WARN")); |
||||||
|
std::process::exit(0); |
||||||
|
} |
||||||
|
|
||||||
|
let stats = stats::Stats::new(&vec); |
||||||
|
match opts.subcmd { |
||||||
|
SubCommand::Hist(o) => { |
||||||
|
let mut histogram = histogram::Histogram::new( |
||||||
|
o.intervals, |
||||||
|
(stats.max - stats.min) / o.intervals as f64, |
||||||
|
stats |
||||||
|
); |
||||||
|
histogram.load(&vec); |
||||||
|
println!("{:width$}", histogram, width=opts.width); |
||||||
|
}, |
||||||
|
SubCommand::Plot(o) => { |
||||||
|
let mut plot = plot::Plot::new( |
||||||
|
opts.width, |
||||||
|
o.height, |
||||||
|
stats |
||||||
|
); |
||||||
|
plot.load(&vec); |
||||||
|
print!("{}", plot); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,109 @@ |
|||||||
|
use std::fmt; |
||||||
|
use std::ops::Range; |
||||||
|
|
||||||
|
use yansi::Color::{Red, Blue}; |
||||||
|
|
||||||
|
use crate::stats::Stats; |
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug)] |
||||||
|
pub struct Plot { |
||||||
|
x_axis: Vec<f64>, |
||||||
|
y_axis: Vec<f64>, |
||||||
|
width: usize, |
||||||
|
height: usize, |
||||||
|
stats: Stats, |
||||||
|
} |
||||||
|
|
||||||
|
impl Plot { |
||||||
|
pub fn new(width: usize, height: usize, stats: Stats) -> Plot { |
||||||
|
Plot { |
||||||
|
x_axis: Vec::with_capacity(width), |
||||||
|
y_axis: Vec::with_capacity(height), |
||||||
|
width, |
||||||
|
height, |
||||||
|
stats, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub fn load(&mut self, vec: &[f64]) { |
||||||
|
self.width = self.width.min(vec.len()); |
||||||
|
let num_chunks = vec.len() / self.width; |
||||||
|
let iter = vec.chunks(num_chunks); |
||||||
|
for x in iter { |
||||||
|
let sum: f64 = x.iter().sum(); |
||||||
|
self.x_axis.push(sum / x.len() as f64); |
||||||
|
} |
||||||
|
let step = (self.stats.max - self.stats.min) / self.height as f64; |
||||||
|
for y in 0..self.height { |
||||||
|
self.y_axis.push(self.stats.min + step * y as f64); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl fmt::Display for Plot { |
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
||||||
|
write!(f, "{}", self.stats)?; |
||||||
|
let _step = (self.stats.max - self.stats.min) / self.height as f64; |
||||||
|
let y_width = format!("{:.3}", self.stats.max).len(); |
||||||
|
let mut newvec = self.y_axis.to_vec(); |
||||||
|
newvec.reverse(); |
||||||
|
print_line(f, &self.x_axis, newvec[0]..f64::INFINITY, y_width)?; |
||||||
|
for y in newvec.windows(2) { |
||||||
|
print_line(f, &self.x_axis, y[1]..y[0], y_width)?; |
||||||
|
} |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fn print_line(f: &mut fmt::Formatter, x_axis: &[f64], range: Range<f64>, y_width: usize) -> fmt::Result { |
||||||
|
let mut row = format!("{: <width$}", "", width=x_axis.len()); |
||||||
|
// The reverse in the enumeration is to avoid breaking char boundaries
|
||||||
|
// because of unicode char ● having more bytes than ascii chars.
|
||||||
|
for (x, value) in x_axis.iter().enumerate().rev() { |
||||||
|
if range.contains(value) { |
||||||
|
row.replace_range(x..x+1, "●".as_ref()); |
||||||
|
} |
||||||
|
} |
||||||
|
writeln!( |
||||||
|
f, |
||||||
|
"[{}] {}", |
||||||
|
Blue.paint(format!("{y:.*}", y_width, y=range.start.to_string())), |
||||||
|
Red.paint(row), |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
#[cfg(test)] |
||||||
|
mod tests { |
||||||
|
use super::*; |
||||||
|
use float_eq::assert_float_eq; |
||||||
|
use yansi::Paint; |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn basic_test() { |
||||||
|
let stats = Stats::new(&[-1.0, 4.0]); |
||||||
|
let mut plot = Plot::new(3, 5, stats); |
||||||
|
plot.load(&[-1.0, 0.0, 1.0, 2.0, 3.0, 4.0, -1.0]); |
||||||
|
assert_float_eq!(plot.x_axis[0], -0.5, rmax <= f64::EPSILON); |
||||||
|
assert_float_eq!(plot.x_axis[1], 1.5, rmax <= f64::EPSILON); |
||||||
|
assert_float_eq!(plot.x_axis[2], 3.5, rmax <= f64::EPSILON); |
||||||
|
assert_float_eq!(plot.x_axis[3], -1.0, rmax <= f64::EPSILON); |
||||||
|
|
||||||
|
assert_float_eq!(plot.y_axis[0], -1.0, rmax <= f64::EPSILON); |
||||||
|
assert_float_eq!(plot.y_axis[4], 3.0, rmax <= f64::EPSILON); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn display_test() { |
||||||
|
let stats = Stats::new(&[-1.0, 4.0]); |
||||||
|
let mut plot = Plot::new(3, 5, stats); |
||||||
|
plot.load(&[-1.0, 0.0, 1.0, 2.0, 3.0, 4.0, -1.0]); |
||||||
|
Paint::disable(); |
||||||
|
let display = format!("{}", plot); |
||||||
|
assert!(display.find("[3] ● ").is_some()); |
||||||
|
assert!(display.find("[2] ").is_some()); |
||||||
|
assert!(display.find("[1] ● ").is_some()); |
||||||
|
assert!(display.find("[-1] ● ●").is_some()); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,102 @@ |
|||||||
|
use std::io::{self, BufRead}; |
||||||
|
use std::fs::File; |
||||||
|
use std::ops::Range; |
||||||
|
|
||||||
|
use regex::Regex; |
||||||
|
use yansi::Color::{Red, Magenta}; |
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Builder)] |
||||||
|
pub struct DataReader { |
||||||
|
#[builder(setter(strip_option), default)] |
||||||
|
range: Option<Range<f64>>, |
||||||
|
#[builder(setter(strip_option), default)] |
||||||
|
regex: Option<Regex>, |
||||||
|
#[builder(default)] |
||||||
|
verbose: bool, |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
impl DataReader { |
||||||
|
|
||||||
|
pub fn read(&self, path: String) -> Vec<f64> { |
||||||
|
let mut vec: Vec<f64> = vec![]; |
||||||
|
match path.as_str() { |
||||||
|
"-" => { |
||||||
|
vec = self.read_data(io::stdin().lock().lines()); |
||||||
|
} |
||||||
|
_ => { |
||||||
|
let file = File::open(path); |
||||||
|
match file { |
||||||
|
Ok(fd) => { |
||||||
|
vec = self.read_data(io::BufReader::new(fd).lines()); |
||||||
|
} |
||||||
|
Err(error) => eprintln!("[{}]: {}", Red.paint("ERROR"), error), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
vec |
||||||
|
} |
||||||
|
|
||||||
|
fn read_data<T: BufRead>(&self, lines: std::io::Lines<T>) -> Vec<f64> { |
||||||
|
let mut vec: Vec<f64> = Vec::new(); |
||||||
|
let line_parser = match self.regex { |
||||||
|
Some(_) => Self::parse_regex, |
||||||
|
None => Self::parse_float |
||||||
|
}; |
||||||
|
for line in lines { |
||||||
|
match line { |
||||||
|
Ok(as_string) => if let Some(n) = line_parser(&self, &as_string) { |
||||||
|
match &self.range { |
||||||
|
Some(range) => if range.contains(&n) { |
||||||
|
vec.push(n); |
||||||
|
}, |
||||||
|
_ => vec.push(n) |
||||||
|
} |
||||||
|
}, |
||||||
|
Err(error) => eprintln!("[{}]: {}", Red.paint("ERROR"), error), |
||||||
|
} |
||||||
|
} |
||||||
|
vec |
||||||
|
} |
||||||
|
|
||||||
|
fn parse_float(&self, line: &str) -> Option<f64> { |
||||||
|
match line.parse::<f64>() { |
||||||
|
Ok(n) => Some(n), |
||||||
|
Err(parse_error) => { |
||||||
|
eprintln!( |
||||||
|
"[{}] Cannot parse float ({}) at '{}'", |
||||||
|
Red.paint("ERROR"), |
||||||
|
parse_error, |
||||||
|
line |
||||||
|
); |
||||||
|
None |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fn parse_regex(&self, line: &str) -> Option<f64> { |
||||||
|
match self.regex.as_ref().unwrap().captures(line) { |
||||||
|
Some(cap) => { |
||||||
|
if let Some(name) = cap.name("value") { |
||||||
|
self.parse_float(&name.as_str()) |
||||||
|
} else if let Some(capture) = cap.get(1) { |
||||||
|
self.parse_float(&capture.as_str()) |
||||||
|
} else { |
||||||
|
None |
||||||
|
} |
||||||
|
}, |
||||||
|
None => { |
||||||
|
if self.verbose { |
||||||
|
eprintln!( |
||||||
|
"[{}] Regex does not match '{}'", |
||||||
|
Magenta.paint("DEBUG"), |
||||||
|
line |
||||||
|
); |
||||||
|
} |
||||||
|
None |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,83 @@ |
|||||||
|
use std::fmt; |
||||||
|
|
||||||
|
use yansi::Color::Blue; |
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug)] |
||||||
|
pub struct Stats { |
||||||
|
pub min: f64, |
||||||
|
pub max: f64, |
||||||
|
pub avg: f64, |
||||||
|
pub std: f64, |
||||||
|
pub var: f64, |
||||||
|
pub sum: f64, |
||||||
|
pub samples: usize, |
||||||
|
} |
||||||
|
|
||||||
|
impl Stats { |
||||||
|
|
||||||
|
pub fn new(vec: &[f64]) -> Stats { |
||||||
|
let mut max = vec[0]; |
||||||
|
let mut min = max; |
||||||
|
let mut temp: f64 = 0.0; |
||||||
|
let sum = vec.iter().sum::<f64>(); |
||||||
|
let avg = sum / vec.len() as f64; |
||||||
|
for val in vec.iter() { |
||||||
|
max = max.max(*val); |
||||||
|
min = min.min(*val); |
||||||
|
temp += (avg - *val).powi(2) |
||||||
|
} |
||||||
|
let var = temp / vec.len() as f64; |
||||||
|
let std = var.sqrt(); |
||||||
|
Stats { min, max, avg, std, var, sum, samples: vec.len() } |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl fmt::Display for Stats { |
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
||||||
|
writeln!( |
||||||
|
f, |
||||||
|
"Samples = {len:.5}; Min = {min:.5}; Max = {max:.5}", |
||||||
|
len=Blue.paint(self.samples.to_string()), |
||||||
|
min=Blue.paint(self.min.to_string()), |
||||||
|
max=Blue.paint(self.max.to_string()), |
||||||
|
)?; |
||||||
|
writeln!( |
||||||
|
f, |
||||||
|
"Average = {avg:.5}; Variance = {var:.5}; STD = {std:.5}", |
||||||
|
avg=Blue.paint(self.avg.to_string()), |
||||||
|
var=Blue.paint(self.var.to_string()), |
||||||
|
std=Blue.paint(self.std.to_string()) |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[cfg(test)] |
||||||
|
mod tests { |
||||||
|
use super::*; |
||||||
|
use float_eq::assert_float_eq; |
||||||
|
use yansi::Paint; |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn basic_test() { |
||||||
|
let stats = Stats::new(&[1.1, 3.3, 2.2]); |
||||||
|
assert_eq!(3_usize, stats.samples); |
||||||
|
assert_float_eq!(stats.sum, 6.6, rmax <= f64::EPSILON); |
||||||
|
assert_float_eq!(stats.avg, 2.2, rmax <= f64::EPSILON); |
||||||
|
assert_float_eq!(stats.min, 1.1, rmax <= f64::EPSILON); |
||||||
|
assert_float_eq!(stats.max, 3.3, rmax <= f64::EPSILON); |
||||||
|
assert_float_eq!(stats.var, 0.8066, abs <= 0.0001); |
||||||
|
assert_float_eq!(stats.std, 0.8981, abs <= 0.0001); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn test_display() { |
||||||
|
let stats = Stats::new(&[1.1, 3.3, 2.2]); |
||||||
|
Paint::disable(); |
||||||
|
let display = format!("{}", stats); |
||||||
|
assert!(display.find("Samples = 3").is_some()); |
||||||
|
assert!(display.find("Min = 1.1").is_some()); |
||||||
|
assert!(display.find("Max = 3.3").is_some()); |
||||||
|
assert!(display.find("Average = 2.2").is_some()); |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue