From c8edd3de1ec1e666ef959c4e27eb186943ff5c67 Mon Sep 17 00:00:00 2001 From: JuanLeon Lahoz Date: Thu, 22 Apr 2021 15:29:36 +0200 Subject: [PATCH] Implement the matches sub-command for bar charts --- README.md | 12 +++- resources/matches-example.png | Bin 0 -> 10106 bytes src/app.rs | 129 +++++++++++++++++++++------------- src/histogram.rs | 27 ++++--- src/main.rs | 108 ++++++++++++++++------------ src/matchbar.rs | 107 ++++++++++++++++++++++++++++ src/plot.rs | 8 +-- src/reader.rs | 86 +++++++++++++++++------ src/stats.rs | 8 +-- 9 files changed, 347 insertions(+), 138 deletions(-) create mode 100644 resources/matches-example.png create mode 100644 src/matchbar.rs diff --git a/README.md b/README.md index 67877e0..975ff96 100644 --- a/README.md +++ b/README.md @@ -20,15 +20,21 @@ terminal. Type `lowcharts --help`, or `lowcharts PLOT-TYPE --help` for a complete list of options. -[![Sample histogram with lowcharts](resources/histogram-example.png)](resources/histogram-example.png) +Currently three basic types of plots are supported: + +#### Bar chart for matches in the input + +Since `grep -c` does not aggregate counts per pattern, this is maybe my most frequent use case. -Currently two basic types of plots are supported: +This chart is generated using `lowcharts matches database.log SELECT UPDATE DELETE INSERT DROP`: + +[![Simple bar chart with lowcharts](resources/matches-example.png)](resources/matches-example.png) #### Histogram This chart is generated using `python3 -c 'import random; [print(random.normalvariate(5, 5)) for _ in range(100000)]' | lowcharts hist`: - +[![Sample histogram with lowcharts](resources/histogram-example.png)](resources/histogram-example.png) This was inspired by [data-hacks](https://github.com/bitly/data_hacks). However, for some big log files I found that project was slower of what I would diff --git a/resources/matches-example.png b/resources/matches-example.png new file mode 100644 index 0000000000000000000000000000000000000000..8fd5ee97611f3bdb619529104b9795af61bfa433 GIT binary patch literal 10106 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 App { +use clap::{self, App, AppSettings, Arg}; +fn add_common_options(app: App) -> App { const LONG_RE_ABOUT: &str = "\ A regular expression used for capturing the values to be plotted inside input lines. @@ -17,48 +16,45 @@ group) and 'a(a)? (?P[0-9.]+)' (where there are two capture groups, and the named one will be used). "; - app - .arg( - Arg::new("max") - .long("max") - .short('M') - .about("Filter out values bigger than this") - .takes_value(true) - ) - .arg( - Arg::new("min") - .long("min") - .short('m') - .about("Filter out values smaller than this") - .takes_value(true) - ) - .arg( - Arg::new("width") - .long("width") - .short('w') - .about("Use this many characters as terminal width") - .default_value("110") - .takes_value(true) - ) - .arg( - Arg::new("regex") - .long("regex") - .short('R') - .about("Use a regex to capture input values") - .long_about(LONG_RE_ABOUT) - .takes_value(true) - ) - .arg( - Arg::new("input") - .about("Input file") - .default_value("-") - .long_about("If not present or a single dash, standard input will be used") - ) - + app.arg( + Arg::new("max") + .long("max") + .short('M') + .about("Filter out values bigger than this") + .takes_value(true), + ) + .arg( + Arg::new("min") + .long("min") + .short('m') + .about("Filter out values smaller than this") + .takes_value(true), + ) + .arg( + Arg::new("width") + .long("width") + .short('w') + .about("Use this many characters as terminal width") + .default_value("110") + .takes_value(true), + ) + .arg( + Arg::new("regex") + .long("regex") + .short('R') + .about("Use a regex to capture input values") + .long_about(LONG_RE_ABOUT) + .takes_value(true), + ) + .arg( + Arg::new("input") + .about("Input file") + .default_value("-") + .long_about("If not present or a single dash, standard input will be used"), + ) } pub fn get_app() -> App<'static> { - let mut hist = App::new("hist") .version(clap::crate_version!()) .setting(AppSettings::ColoredHelp) @@ -69,7 +65,7 @@ pub fn get_app() -> App<'static> { .short('i') .about("Use no more than this amount of buckets to classify data") .default_value("20") - .takes_value(true) + .takes_value(true), ); hist = add_common_options(hist); @@ -77,17 +73,44 @@ pub fn get_app() -> App<'static> { let mut plot = App::new("plot") .version(clap::crate_version!()) .setting(AppSettings::ColoredHelp) - .about("Plot an 2d plot where y-values are averages of input values") + .about("Plot an 2d x-y graph where y-values are averages of input values") .arg( Arg::new("height") .long("height") .short('h') .about("Use that many `rows` for the plot") .default_value("40") - .takes_value(true) + .takes_value(true), ); plot = add_common_options(plot); + let matches = App::new("matches") + .version(clap::crate_version!()) + .setting(AppSettings::ColoredHelp) + .setting(AppSettings::AllowMissingPositional) + .about("Plot barchar with counts of occurences of matches params") + .arg( + Arg::new("width") + .long("width") + .short('w') + .about("Use this many characters as terminal width") + .default_value("110") + .takes_value(true), + ) + .arg( + Arg::new("input") + .about("Input file") + .required(true) + .long_about("If not present or a single dash, standard input will be used"), + ) + .arg( + Arg::new("match") + .about("Count maches for those strings") + .required(true) + .takes_value(true) + .multiple(true), + ); + App::new("lowcharts") .author(clap::crate_authors!()) .version(clap::crate_version!()) @@ -101,17 +124,18 @@ pub fn get_app() -> App<'static> { .long("color") .about("Use colors in the output") .possible_values(&["auto", "no", "yes"]) - .takes_value(true) + .takes_value(true), ) .arg( Arg::new("verbose") .short('v') .long("verbose") .about("Be more verbose") - .takes_value(false) + .takes_value(false), ) .subcommand(hist) .subcommand(plot) + .subcommand(matches) } #[cfg(test)] @@ -138,7 +162,16 @@ mod tests { #[test] fn plot_subcommand_arg_parsing() { - let arg_vec = vec!["lowcharts", "plot", "--max", "1.1", "-m", "0.9", "--height", "11"]; + let arg_vec = vec![ + "lowcharts", + "plot", + "--max", + "1.1", + "-m", + "0.9", + "--height", + "11", + ]; let m = get_app().get_matches_from(arg_vec); assert!(!m.is_present("verbose")); if let Some(sub_m) = m.subcommand_matches("plot") { diff --git a/src/histogram.rs b/src/histogram.rs index 10cc677..c5e521c 100644 --- a/src/histogram.rs +++ b/src/histogram.rs @@ -6,7 +6,7 @@ use yansi::Color::{Blue, Green, Red}; use crate::stats::Stats; #[derive(Debug)] -pub struct Bucket { +struct Bucket { range: Range, count: usize, } @@ -33,20 +33,20 @@ pub struct Histogram { impl Histogram { pub fn new(size: usize, step: f64, stats: Stats) -> Histogram { - let mut b = Histogram { - vec: Vec::with_capacity(size), + let mut vec = Vec::::with_capacity(size); + let mut lower = stats.min; + for _ in 0..size { + vec.push(Bucket::new(lower..lower + step)); + lower += step; + } + Histogram { + vec, 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]) { @@ -110,7 +110,6 @@ impl HistWriter { width: usize, width_count: usize, ) -> fmt::Result { - let bar = Red.paint(format!("{:∎ reader::DataReader { let min = matches.value_of_t("min").unwrap_or(f64::NEG_INFINITY); let max = matches.value_of_t("max").unwrap_or(f64::INFINITY); if min > max { - eprintln!("[{}] Minimum should be smaller than maximum", Red.paint("ERROR")); + eprintln!( + "[{}] Minimum should be smaller than maximum", + Red.paint("ERROR") + ); std::process::exit(1); } builder.range(min..max); @@ -57,52 +60,69 @@ fn get_reader(matches: &ArgMatches, verbose: bool) -> reader::DataReader { builder.build().unwrap() } +fn histogram(matches: &ArgMatches, verbose: bool) { + let reader = get_reader(&matches, verbose); + let vec = reader.read(matches.value_of("input").unwrap()); + if vec.is_empty() { + eprintln!("[{}] No data to process", Yellow.paint("WARN")); + std::process::exit(0); + } + let stats = stats::Stats::new(&vec); + 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 = + histogram::Histogram::new(intervals, (stats.max - stats.min) / intervals as f64, stats); + histogram.load(&vec); + println!("{:width$}", histogram, width = width); +} + +fn plot(matches: &ArgMatches, verbose: bool) { + let reader = get_reader(&matches, verbose); + let vec = reader.read(matches.value_of("input").unwrap()); + if vec.is_empty() { + eprintln!("[{}] No data to process", Yellow.paint("WARN")); + std::process::exit(0); + } + let mut plot = plot::Plot::new( + matches.value_of_t("width").unwrap(), + matches.value_of_t("height").unwrap(), + stats::Stats::new(&vec), + ); + plot.load(&vec); + print!("{}", plot); +} + +fn matchbar(matches: &ArgMatches) { + let reader = reader::DataReader::default(); + let width = matches.value_of_t("width").unwrap(); + print!( + "{:width$}", + reader.read_matches( + matches.value_of("input").unwrap(), + matches.values_of("match").unwrap().collect() + ), + width = width + ); +} fn main() { let matches = app::get_app().get_matches(); - + let verbose = matches.is_present("verbose"); if let Some(c) = matches.value_of("color") { disable_color_if_needed(c); } - - let sub_matches = match matches.subcommand_name() { - Some("hist") => { - matches.subcommand_matches("hist").unwrap() - }, - Some("plot") => { - matches.subcommand_matches("plot").unwrap() - }, - _ => { - eprintln!("[{}] Invalid subcommand", Red.paint("ERROR")); - std::process::exit(1); + match matches.subcommand() { + Some(("hist", subcommand_matches)) => { + histogram(subcommand_matches, verbose); } - }; - let reader = get_reader(&sub_matches, matches.is_present("verbose")); - - let vec = reader.read(sub_matches.value_of("input").unwrap_or("-")); - if vec.is_empty() { - eprintln!("[{}]: No data to process", Yellow.paint("WARN")); - std::process::exit(0); - } - let stats = stats::Stats::new(&vec); - let width = sub_matches.value_of_t("width").unwrap(); - match matches.subcommand_name() { - Some("hist") => { - let mut intervals: usize = sub_matches.value_of_t("intervals").unwrap(); - intervals = intervals.min(vec.len()); - let mut histogram = histogram::Histogram::new( - intervals, - (stats.max - stats.min) / intervals as f64, - stats, - ); - histogram.load(&vec); - println!("{:width$}", histogram, width = width); - }, - Some("plot") => { - let mut plot = plot::Plot::new(width, sub_matches.value_of_t("height").unwrap(), stats); - plot.load(&vec); - print!("{}", plot); - }, - _ => () + Some(("plot", subcommand_matches)) => { + plot(subcommand_matches, verbose); + } + Some(("matches", subcommand_matches)) => { + matchbar(subcommand_matches); + } + _ => unreachable!("Invalid subcommand"), }; } diff --git a/src/matchbar.rs b/src/matchbar.rs new file mode 100644 index 0000000..73863df --- /dev/null +++ b/src/matchbar.rs @@ -0,0 +1,107 @@ +use std::fmt; + +use yansi::Color::{Blue, Green, Red}; + +#[derive(Debug)] +pub struct MatchBarRow { + pub label: String, + pub count: usize, +} + +impl MatchBarRow { + pub fn new(string: &str) -> MatchBarRow { + MatchBarRow { + label: string.to_string(), + count: 0, + } + } + + pub fn inc_if_matches(&mut self, line: &str) { + if line.contains(&self.label) { + self.count += 1; + } + } +} + +#[derive(Debug)] +pub struct MatchBar { + pub vec: Vec, + top_values: usize, + top_lenght: usize, +} + +impl MatchBar { + pub fn new(vec: Vec) -> MatchBar { + let mut top_lenght: usize = 0; + let mut top_values: usize = 0; + for row in vec.iter() { + top_lenght = top_lenght.max(row.label.len()); + top_values = top_values.max(row.count); + } + MatchBar { + vec, + top_lenght, + top_values, + } + } +} + +impl fmt::Display for MatchBar { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let width = f.width().unwrap_or(100); + let divisor = 1.max(self.top_values / width); + let width_count = format!("{}", self.top_values).len(); + writeln!( + f, + "Matches: {}.", + Blue.paint(format!( + "{}", + self.vec.iter().map(|r| r.count).sum::() + )), + )?; + writeln!( + f, + "Each {} represents a count of {}", + Red.paint("∎"), + Blue.paint(divisor.to_string()), + )?; + for row in self.vec.iter() { + writeln!( + f, + "[{label}] [{count}] {bar}", + label = Blue.paint(format!("{:width$}", row.label, width = self.top_lenght)), + count = Green.paint(format!("{:width$}", row.count, width = width_count)), + bar = Red.paint(format!("{:∎ Vec { - let mut vec: Vec = vec![]; - match path { - "-" => { - 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(&self, lines: std::io::Lines) -> Vec { let mut vec: Vec = Vec::new(); let line_parser = match self.regex { Some(_) => Self::parse_regex, None => Self::parse_float, }; - for line in lines { + for line in open_file(path).lines() { match line { Ok(as_string) => { if let Some(n) = line_parser(&self, &as_string) { @@ -101,6 +84,42 @@ impl DataReader { } } } + + pub fn read_matches(&self, path: &str, strings: Vec<&str>) -> MatchBar { + let mut rows = Vec::::with_capacity(strings.len()); + for s in strings { + rows.push(MatchBarRow::new(s)); + } + for line in open_file(path).lines() { + match line { + Ok(as_string) => { + for row in rows.iter_mut() { + row.inc_if_matches(&as_string); + } + } + Err(error) => eprintln!("[{}]: {}", Red.paint("ERROR"), error), + } + } + MatchBar::new(rows) + } +} + +fn open_file(path: &str) -> Box { + match path { + "-" => Box::new(BufReader::new(io::stdin())), + _ => match File::open(path) { + Ok(fd) => Box::new(io::BufReader::new(fd)), + Err(error) => { + eprintln!( + "[{}] Could not open {}: {}", + Red.paint("ERROR"), + path, + error + ); + std::process::exit(0); + } + }, + } } #[cfg(test)] @@ -194,4 +213,29 @@ mod tests { Err(_) => assert!(false, "Could not create temp file"), } } + + #[test] + fn basic_match_reader() { + let reader = DataReader::default(); + match NamedTempFile::new() { + Ok(ref mut file) => { + writeln!(file, "foobar").unwrap(); + writeln!(file, "data data foobar").unwrap(); + writeln!(file, "data data").unwrap(); + writeln!(file, "foobar").unwrap(); + writeln!(file, "none").unwrap(); + let mb = reader.read_matches( + file.path().to_str().unwrap(), + vec!["random", "foobar", "data"], + ); + assert_eq!(mb.vec[0].label, "random"); + assert_eq!(mb.vec[0].count, 0); + assert_eq!(mb.vec[1].label, "foobar"); + assert_eq!(mb.vec[1].count, 3); + assert_eq!(mb.vec[2].label, "data"); + assert_eq!(mb.vec[2].count, 2); + } + Err(_) => assert!(false, "Could not create temp file"), + } + } } diff --git a/src/stats.rs b/src/stats.rs index 2b4d5b0..0c7d781 100644 --- a/src/stats.rs +++ b/src/stats.rs @@ -81,9 +81,9 @@ mod tests { 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()); + assert!(display.contains("Samples = 3")); + assert!(display.contains("Min = 1.1")); + assert!(display.contains("Max = 3.3")); + assert!(display.contains("Average = 2.2")); } }