Browse Source

Use human friendly numbers by default (unless --precision is used)

pull/2/head
JuanLeon Lahoz 4 years ago
parent
commit
f77c695de4
  1. 17
      src/app.rs
  2. 158
      src/format/mod.rs
  3. 26
      src/main.rs
  4. 68
      src/plot/histogram.rs
  5. 47
      src/plot/xy.rs
  6. 36
      src/stats/mod.rs
  7. 58
      tests/integration_tests.rs

17
src/app.rs

@ -94,11 +94,24 @@ fn add_intervals(cmd: Command) -> Command {
)
}
fn add_precision(cmd: Command) -> Command {
cmd.arg(
Arg::new("precision")
.long("precision")
.short('p')
.help("Show that number of decimals (if omitted, 'human' units will be used)")
.default_value("-1")
.takes_value(true),
)
}
pub fn get_app() -> Command<'static> {
let mut hist = Command::new("hist")
.version(clap::crate_version!())
.about("Plot an histogram from input values");
hist = add_input(add_regex(add_width(add_min_max(add_intervals(hist)))));
hist = add_input(add_regex(add_width(add_min_max(add_precision(
add_intervals(hist),
)))));
let mut plot = Command::new("plot")
.version(clap::crate_version!())
@ -111,7 +124,7 @@ pub fn get_app() -> Command<'static> {
.default_value("40")
.takes_value(true),
);
plot = add_input(add_regex(add_width(add_min_max(plot))));
plot = add_input(add_regex(add_width(add_min_max(add_precision(plot)))));
let mut matches = Command::new("matches")
.version(clap::crate_version!())

158
src/format/mod.rs

@ -0,0 +1,158 @@
use std::ops::Range;
// Units-based suffixes for human formatting.
const UNITS: &[&str] = &["", " K", " M", " G", " T", " P", " E", " Z", " Y"];
#[derive(Debug)]
pub struct F64Formatter {
/// Decimals digits to be used
decimals: usize,
/// Number of times the value will be divided by 1000
divisor: u8,
/// Suffix (typycally units) to be printed after number
suffix: String,
}
impl F64Formatter {
/// Initializes a new `HumanF64Formatter` with default values.
pub fn new(decimals: usize) -> F64Formatter {
F64Formatter {
decimals,
divisor: 0,
suffix: "".to_owned(),
}
}
/// Initializes a new `HumanF64Formatter` for formatting numbers in the
/// provided range.
pub fn new_with_range(range: Range<f64>) -> F64Formatter {
// Range
let mut decimals = 3;
let mut divisor = 0_u8;
let mut suffix = UNITS[0].to_owned();
let difference = range.end - range.start;
if difference == 0.0 {
return F64Formatter {
decimals,
divisor,
suffix,
};
}
let log = difference.abs().log10() as i64;
if log <= 0 {
decimals = (-log as usize).min(8) + 3;
} else {
decimals = log.rem_euclid(3) as usize;
divisor = ((log - 1) / 3).min(5) as u8;
}
suffix = UNITS[divisor as usize].to_owned();
F64Formatter {
decimals,
divisor,
suffix,
}
}
pub fn format(&self, number: f64) -> String {
format!(
"{:.*}{}",
self.decimals,
number / 1000_usize.pow(self.divisor.into()) as f64,
self.suffix
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_format() {
assert_eq!(F64Formatter::new(0).format(1000.0), "1000");
assert_eq!(F64Formatter::new(3).format(1000.0), "1000.000");
assert_eq!(F64Formatter::new(1).format(12345.299), "12345.3");
assert_eq!(F64Formatter::new(10).format(3.0), "3.0000000000");
}
#[test]
fn test_human_format_from_zero() {
assert_eq!(F64Formatter::new_with_range(0.0..2.0).format(1.12), "1.120");
assert_eq!(
F64Formatter::new_with_range(0.0..200.0).format(234.12),
"234.12"
);
assert_eq!(
F64Formatter::new_with_range(0.0..1000.0).format(234.1234),
"234"
);
assert_eq!(
F64Formatter::new_with_range(0.0..10000.0).format(234.1234),
"0.2 K"
);
assert_eq!(
F64Formatter::new_with_range(0.0..100000.0).format(234.1234),
"0.23 K"
);
assert_eq!(
F64Formatter::new_with_range(0.0..1000000.0).format(234.1234),
"0 K"
);
assert_eq!(
F64Formatter::new_with_range(0.0..100000000.0).format(1234.1234),
"0.00 M"
);
assert_eq!(
F64Formatter::new_with_range(0.0..1000000.0).format(234000.1234),
"234 K"
);
assert_eq!(
F64Formatter::new_with_range(0.0..100000000.0).format(1234000.1234),
"1.23 M"
);
assert_eq!(
F64Formatter::new_with_range(0.0..100000000.0).format(12340000.1234),
"12.34 M"
);
}
#[test]
fn test_human_format_small_numbers() {
assert_eq!(
F64Formatter::new_with_range(0.0..0.0002).format(0.0000043),
"0.000004"
);
assert_eq!(
F64Formatter::new_with_range(0.0..0.00002).format(0.0000043),
"0.0000043"
);
assert_eq!(
F64Formatter::new_with_range(20000.0..20000.00002).format(20000.0000043),
"20000.0000043"
);
}
#[test]
fn test_human_format_bignum_small_interval() {
assert_eq!(
F64Formatter::new_with_range(100000000.0..100000001.0).format(100000000.12341234),
"100000000.123"
);
}
#[test]
fn test_human_format_negative_start() {
assert_eq!(
F64Formatter::new_with_range(-4.0..2.0).format(1.12),
"1.120"
);
assert_eq!(
F64Formatter::new_with_range(-4.0..-2.0).format(-3.12),
"-3.120"
);
assert_eq!(
F64Formatter::new_with_range(-10000000.0..10.0).format(-3.12),
"-0.0 M"
);
}
}

26
src/main.rs

@ -1,4 +1,5 @@
mod app;
mod format;
mod plot;
mod read;
mod stats;
@ -108,13 +109,23 @@ fn histogram(matches: &ArgMatches) -> i32 {
if !assert_data(&vec, 1) {
return 1;
}
let stats = stats::Stats::new(&vec);
let precision_arg: i32 = matches.value_of_t("precision").unwrap();
let precision = if precision_arg < 0 {
None
} else {
Some(precision_arg as usize)
};
let stats = stats::Stats::new(&vec, precision);
let width = matches.value_of_t("width").unwrap();
let mut intervals: usize = matches.value_of_t("intervals").unwrap();
intervals = intervals.min(vec.len());
let mut histogram =
plot::Histogram::new(intervals, (stats.max - stats.min) / intervals as f64, stats);
let mut histogram = plot::Histogram::new(
intervals,
(stats.max - stats.min) / intervals as f64,
stats,
precision,
);
histogram.load(&vec);
print!("{:width$}", histogram, width = width);
0
@ -130,10 +141,17 @@ fn plot(matches: &ArgMatches) -> i32 {
if !assert_data(&vec, 1) {
return 1;
}
let precision_arg: i32 = matches.value_of_t("precision").unwrap();
let precision = if precision_arg < 0 {
None
} else {
Some(precision_arg as usize)
};
let mut plot = plot::XyPlot::new(
matches.value_of_t("width").unwrap(),
matches.value_of_t("height").unwrap(),
stats::Stats::new(&vec),
stats::Stats::new(&vec, precision),
precision,
);
plot.load(&vec);
print!("{}", plot);

68
src/plot/histogram.rs

@ -3,6 +3,7 @@ use std::ops::Range;
use yansi::Color::{Blue, Green, Red};
use crate::format::F64Formatter;
use crate::stats::Stats;
#[derive(Debug)]
@ -29,10 +30,11 @@ pub struct Histogram {
top: usize,
last: usize,
stats: Stats,
precision: Option<usize>, // If None, then human friendly display will be used
}
impl Histogram {
pub fn new(size: usize, step: f64, stats: Stats) -> Histogram {
pub fn new(size: usize, step: f64, stats: Stats, precision: Option<usize>) -> Histogram {
let mut vec = Vec::<Bucket>::with_capacity(size);
let mut lower = stats.min;
for _ in 0..size {
@ -46,6 +48,7 @@ impl Histogram {
top: 0,
last: size - 1,
stats,
precision,
}
}
@ -74,8 +77,13 @@ impl Histogram {
impl fmt::Display for Histogram {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.stats)?;
let formatter = match self.precision {
None => F64Formatter::new_with_range(self.stats.min..self.stats.max),
Some(n) => F64Formatter::new(n),
};
let writer = HistWriter {
width: f.width().unwrap_or(110),
formatter,
};
writer.write(f, self)
}
@ -83,11 +91,12 @@ impl fmt::Display for Histogram {
struct HistWriter {
width: usize,
formatter: F64Formatter,
}
impl HistWriter {
pub fn write(&self, f: &mut fmt::Formatter, hist: &Histogram) -> fmt::Result {
let width_range = Self::get_width(hist);
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!(
@ -114,9 +123,9 @@ impl HistWriter {
f,
"[{range}] [{count}] {bar}",
range = Blue.paint(format!(
"{:width$.3} .. {:width$.3}",
bucket.range.start,
bucket.range.end,
"{:>width$} .. {:>width$}",
self.formatter.format(bucket.range.start),
self.formatter.format(bucket.range.end),
width = width,
)),
count = Green.paint(format!("{:width$}", bucket.count, width = width_count)),
@ -124,10 +133,11 @@ impl HistWriter {
)
}
fn get_width(hist: &Histogram) -> usize {
format!("{:.3}", hist.stats.min)
fn get_width(&self, hist: &Histogram) -> usize {
self.formatter
.format(hist.stats.min)
.len()
.max(format!("{:.3}", hist.max).len())
.max(self.formatter.format(hist.max).len())
}
fn get_max_bar_len(&self, fixed_width: usize) -> usize {
@ -147,8 +157,8 @@ mod tests {
#[test]
fn test_buckets() {
let stats = Stats::new(&[-2.0, 14.0]);
let mut hist = Histogram::new(8, 2.5, stats);
let stats = Stats::new(&[-2.0, 14.0], None);
let mut hist = Histogram::new(8, 2.5, stats, None);
hist.load(&[
-1.0, -1.1, 2.0, 2.0, 2.1, -0.9, 11.0, 11.2, 1.9, 1.99, 1.98, 1.97, 1.96,
]);
@ -164,15 +174,15 @@ mod tests {
#[test]
fn test_buckets_bad_stats() {
let mut hist = Histogram::new(6, 1.0, Stats::new(&[-2.0, 4.0]));
let mut hist = Histogram::new(6, 1.0, Stats::new(&[-2.0, 4.0], None), None);
hist.load(&[-1.0, 2.0, -1.0, 2.0, 10.0, 10.0, 10.0, -10.0]);
assert_eq!(hist.top, 2);
}
#[test]
fn display_test() {
let stats = Stats::new(&[-2.0, 14.0]);
let mut hist = Histogram::new(8, 2.5, stats);
let stats = Stats::new(&[-2.0, 14.0], None);
let mut hist = Histogram::new(8, 2.5, stats, Some(3));
hist.load(&[
-1.0, -1.1, 2.0, 2.0, 2.1, -0.9, 11.0, 11.2, 1.9, 1.99, 1.98, 1.97, 1.96,
]);
@ -185,7 +195,7 @@ mod tests {
#[test]
fn display_test_bad_width() {
let mut hist = Histogram::new(8, 2.5, Stats::new(&[-2.0, 14.0]));
let mut hist = Histogram::new(8, 2.5, Stats::new(&[-2.0, 14.0], None), Some(3));
hist.load(&[
-1.0, -1.1, 2.0, 2.0, 2.1, -0.9, 11.0, 11.2, 1.9, 1.99, 1.98, 1.97, 1.96,
]);
@ -193,4 +203,34 @@ mod tests {
let display = format!("{:2}", hist);
assert!(display.contains("[-2.000 .. 0.500] [3] ∎∎∎\n"));
}
#[test]
fn display_test_human_units() {
let vector = &[
-1.0,
-12000000.0,
-12000001.0,
-12000002.0,
-12000003.0,
-2000000.0,
500000.0,
500000.0,
];
let intervals = vector.len();
let stats = Stats::new(vector, None);
let mut hist = Histogram::new(
intervals,
(stats.max - stats.min) / intervals as f64,
stats,
None,
);
hist.load(vector);
Paint::disable();
let display = format!("{}", hist);
assert!(display.contains("[-12.0 M .. -10.4 M] [4] ∎∎∎∎\n"));
assert!(display.contains("[ -2.6 M .. -1.1 M] [1] ∎\n"));
assert!(display.contains("[ -1.1 M .. 0.5 M] [3] ∎∎∎\n"));
assert!(display.contains("Samples = 8; Min = -12.0 M; Max = 0.5 M"));
assert!(display.contains("Average = -6.1 M;"));
}
}

47
src/plot/xy.rs

@ -3,6 +3,7 @@ use std::ops::Range;
use yansi::Color::{Blue, Red};
use crate::format::F64Formatter;
use crate::stats::Stats;
#[derive(Debug)]
@ -12,16 +13,18 @@ pub struct XyPlot {
width: usize,
height: usize,
stats: Stats,
precision: Option<usize>,
}
impl XyPlot {
pub fn new(width: usize, height: usize, stats: Stats) -> XyPlot {
pub fn new(width: usize, height: usize, stats: Stats, precision: Option<usize>) -> XyPlot {
XyPlot {
x_axis: Vec::with_capacity(width),
y_axis: Vec::with_capacity(height),
width,
height,
stats,
precision,
}
}
@ -44,17 +47,21 @@ impl fmt::Display for XyPlot {
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 f64fmt = match self.precision {
None => F64Formatter::new_with_range(self.stats.min..self.stats.max),
Some(n) => F64Formatter::new(n),
};
let y_width = self
.y_axis
.iter()
.map(|v| format!("{:.3}", v).len())
.map(|v| f64fmt.format(*v).len())
.max()
.unwrap();
let mut newvec = self.y_axis.to_vec();
newvec.reverse();
print_line(f, &self.x_axis, newvec[0]..f64::INFINITY, y_width)?;
print_line(f, &self.x_axis, newvec[0]..f64::INFINITY, y_width, &f64fmt)?;
for y in newvec.windows(2) {
print_line(f, &self.x_axis, y[1]..y[0], y_width)?;
print_line(f, &self.x_axis, y[1]..y[0], y_width, &f64fmt)?;
}
Ok(())
}
@ -65,6 +72,7 @@ fn print_line(
x_axis: &[f64],
range: Range<f64>,
y_width: usize,
f64fmt: &F64Formatter,
) -> fmt::Result {
let mut row = format!("{: <width$}", "", width = x_axis.len());
// The reverse in the enumeration is to avoid breaking char boundaries
@ -77,7 +85,11 @@ fn print_line(
writeln!(
f,
"[{}] {}",
Blue.paint(format!("{:>width$.3}", range.start, width = y_width)),
Blue.paint(format!(
"{:>width$}",
f64fmt.format(range.start),
width = y_width
)),
Red.paint(row),
)
}
@ -90,8 +102,8 @@ mod tests {
#[test]
fn basic_test() {
let stats = Stats::new(&[-1.0, 4.0]);
let mut plot = XyPlot::new(3, 5, stats);
let stats = Stats::new(&[-1.0, 4.0], None);
let mut plot = XyPlot::new(3, 5, stats, Some(3));
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);
@ -104,8 +116,8 @@ mod tests {
#[test]
fn display_test() {
let stats = Stats::new(&[-1.0, 4.0]);
let mut plot = XyPlot::new(3, 5, stats);
let stats = Stats::new(&[-1.0, 4.0], None);
let mut plot = XyPlot::new(3, 5, stats, Some(3));
plot.load(&[-1.0, 0.0, 1.0, 2.0, 3.0, 4.0, -1.0]);
Paint::disable();
let display = format!("{}", plot);
@ -114,4 +126,21 @@ mod tests {
assert!(display.contains("[ 1.000] ● "));
assert!(display.contains("[-1.000] ● ●"));
}
#[test]
fn display_test_human_units() {
let vector = &[1000000.0, -1000000.0, -2000000.0, -4000000.0];
let stats = Stats::new(vector, None);
let mut plot = XyPlot::new(3, 5, stats, None);
plot.load(vector);
Paint::disable();
let display = format!("{}", plot);
assert!(display.contains("[ 0 K] ● "));
assert!(display.contains("[-1000 K] ● "));
assert!(display.contains("[-2000 K] ● "));
assert!(display.contains("[-3000 K] "));
assert!(display.contains("[-4000 K] ●"));
assert!(display.contains("Samples = 4; Min = -4000 K; Max = 1000 K"));
assert!(display.contains("Average = -1500 K;"));
}
}

36
src/stats/mod.rs

@ -2,6 +2,8 @@ use std::fmt;
use yansi::Color::Blue;
use crate::format::F64Formatter;
#[derive(Debug)]
pub struct Stats {
pub min: f64,
@ -11,10 +13,11 @@ pub struct Stats {
pub var: f64,
pub sum: f64,
pub samples: usize,
pub precision: Option<usize>, // If None, then human friendly display will be used
}
impl Stats {
pub fn new(vec: &[f64]) -> Stats {
pub fn new(vec: &[f64], precision: Option<usize>) -> Stats {
let mut max = vec[0];
let mut min = max;
let mut temp: f64 = 0.0;
@ -35,25 +38,30 @@ impl Stats {
var,
sum,
samples: vec.len(),
precision,
}
}
}
impl fmt::Display for Stats {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let formatter = match self.precision {
None => F64Formatter::new_with_range(self.min..self.max),
Some(n) => F64Formatter::new(n),
};
writeln!(
f,
"Samples = {len}; Min = {min}; Max = {max}",
len = Blue.paint(self.samples.to_string()),
min = Blue.paint(self.min.to_string()),
max = Blue.paint(self.max.to_string()),
min = Blue.paint(formatter.format(self.min)),
max = Blue.paint(formatter.format(self.max)),
)?;
writeln!(
f,
"Average = {avg}; Variance = {var}; STD = {std}",
avg = Blue.paint(self.avg.to_string()),
var = Blue.paint(self.var.to_string()),
std = Blue.paint(self.std.to_string())
avg = Blue.paint(formatter.format(self.avg)),
var = Blue.paint(format!("{:.3}", self.var)),
std = Blue.paint(format!("{:.3}", self.std)),
)
}
}
@ -66,7 +74,7 @@ mod tests {
#[test]
fn basic_test() {
let stats = Stats::new(&[1.1, 3.3, 2.2]);
let stats = Stats::new(&[1.1, 3.3, 2.2], Some(3));
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);
@ -78,22 +86,22 @@ mod tests {
#[test]
fn test_display() {
let stats = Stats::new(&[1.1, 3.3, 2.2]);
let stats = Stats::new(&[1.1, 3.3, 2.2], Some(3));
Paint::disable();
let display = format!("{}", stats);
assert!(display.contains("Samples = 3"));
assert!(display.contains("Min = 1.1"));
assert!(display.contains("Max = 3.3"));
assert!(display.contains("Average = 2.2"));
assert!(display.contains("Min = 1.100"));
assert!(display.contains("Max = 3.300"));
assert!(display.contains("Average = 2.200"));
}
#[test]
fn test_big_num() {
let stats = Stats::new(&[123456789.1234, 123456788.1234]);
let stats = Stats::new(&[123456789.1234, 123456788.1234], None);
Paint::disable();
let display = format!("{}", stats);
assert!(display.contains("Samples = 2"));
assert!(display.contains("Min = 123456788.1234"));
assert!(display.contains("Max = 123456789.1234"));
assert!(display.contains("Min = 123456788.123"));
assert!(display.contains("Max = 123456789.123"));
}
}

58
tests/integration_tests.rs

@ -83,10 +83,26 @@ fn test_hist() {
.assert()
.success()
.stdout(predicate::str::contains(
"Samples = 2; Min = 2.4; Max = 4.2",
"Samples = 2; Min = 2.400; Max = 4.200",
));
}
#[test]
fn test_hist_human() {
let mut cmd = Command::cargo_bin("lowcharts").unwrap();
cmd.arg("hist")
.arg("--min")
.arg("1")
.write_stdin("42000000\n24000000\n")
.assert()
.success()
.stdout(predicate::str::contains(
"Samples = 2; Min = 24.0 M; Max = 42.0 M",
))
.stdout(predicate::str::contains("\n[24.0 M .. 33.0 M] [1] ∎\n"))
.stdout(predicate::str::contains("\n[33.0 M .. 42.0 M] [1] ∎\n"));
}
#[test]
fn test_matchbar() {
let mut cmd = Command::cargo_bin("lowcharts").unwrap();
@ -156,7 +172,9 @@ fn test_plot() {
.arg("4")
.assert()
.success()
.stdout(predicate::str::contains("Samples = 4; Min = 1; Max = 4\n"))
.stdout(predicate::str::contains(
"Samples = 4; Min = 1.000; Max = 4.000\n",
))
.stdout(predicate::str::contains("\n[3.250] ●"))
.stdout(predicate::str::contains("\n[2.500] ●"))
.stdout(predicate::str::contains("\n[1.750] ●"))
@ -167,6 +185,40 @@ fn test_plot() {
}
}
#[test]
fn test_plot_precision() {
let mut cmd = Command::cargo_bin("lowcharts").unwrap();
match NamedTempFile::new() {
Ok(ref mut file) => {
writeln!(file, "1").unwrap();
writeln!(file, "2").unwrap();
writeln!(file, "3").unwrap();
writeln!(file, "4").unwrap();
writeln!(file, "none").unwrap();
cmd.arg("--verbose")
.arg("--color")
.arg("no")
.arg("plot")
.arg(file.path().to_str().unwrap())
.arg("--height")
.arg("4")
.arg("--precision")
.arg("1")
.assert()
.success()
.stdout(predicate::str::contains(
"Samples = 4; Min = 1.0; Max = 4.0\n",
))
.stdout(predicate::str::contains("\n[3.2] ●"))
.stdout(predicate::str::contains("\n[2.5] ●"))
.stdout(predicate::str::contains("\n[1.8] ●"))
.stdout(predicate::str::contains("\n[1.0] ●"))
.stderr(predicate::str::contains("[DEBUG] Cannot parse float"));
}
Err(_) => assert!(false, "Could not create temp file"),
}
}
#[test]
fn test_hist_negative_min() {
let mut cmd = Command::cargo_bin("lowcharts").unwrap();
@ -179,7 +231,7 @@ fn test_hist_negative_min() {
.assert()
.success()
.stdout(predicate::str::contains(
"Samples = 2; Min = 2.4; Max = 4.2",
"Samples = 2; Min = 2.400; Max = 4.200",
));
}

Loading…
Cancel
Save