use crate::commands::WholeStreamCommand; use crate::prelude::*; use nu_errors::ShellError; use nu_protocol::{ ColumnPath, ReturnSuccess, Signature, SyntaxShape, TaggedDictBuilder, UntaggedValue, Value, }; use nu_source::Tagged; pub struct Histogram; #[async_trait] impl WholeStreamCommand for Histogram { fn name(&self) -> &str { "histogram" } fn signature(&self) -> Signature { Signature::build("histogram") .named( "use", SyntaxShape::ColumnPath, "Use data at the column path given as valuator", None, ) .rest( SyntaxShape::ColumnPath, "column name to give the histogram's frequency column", ) } fn usage(&self) -> &str { "Creates a new table with a histogram based on the column name passed in." } async fn run( &self, args: CommandArgs, registry: &CommandRegistry, ) -> Result { histogram(args, registry).await } fn examples(&self) -> Vec { vec![ Example { description: "Get a histogram for the types of files", example: "ls | histogram type", result: None, }, Example { description: "Get a histogram for the types of files, with frequency column named percentage", example: "ls | histogram type percentage", result: None, }, Example { description: "Get a histogram for a list of numbers", example: "echo [1 2 3 1 1 1 2 2 1 1] | histogram", result: None, }, ] } } pub async fn histogram( args: CommandArgs, registry: &CommandRegistry, ) -> Result { let registry = registry.clone(); let name = args.call_info.name_tag.clone(); let (input, args) = args.evaluate_once(®istry).await?.parts(); let values: Vec = input.collect().await; let mut columns = args .positional_iter() .map(|c| c.as_column_path()) .filter_map(Result::ok) .collect::>(); let evaluate_with = if let Some(path) = args.get("use") { Some(evaluator(path.as_column_path()?.item)) } else { None }; let column_grouper = if !columns.is_empty() { match columns.remove(0).split_last() { Some((key, _)) => Some(key.as_string().tagged(&name)), None => None, } } else { None }; let frequency_column_name = if columns.is_empty() { "frequency".to_string() } else if let Some((key, _)) = columns[0].split_last() { key.as_string() } else { "frecuency".to_string() }; let column = if let Some(ref column) = column_grouper { column.clone() } else { "value".to_string().tagged(&name) }; let results = crate::utils::data::report( &UntaggedValue::table(&values).into_value(&name), crate::utils::data::Operation { grouper: Some(Box::new(move |_, _| Ok(String::from("frequencies")))), splitter: Some(splitter(column_grouper)), format: None, eval: &evaluate_with, }, &name, )?; let labels = results.labels.y.clone(); let mut idx = 0; Ok(futures::stream::iter( results .percentages .table_entries() .map(move |value| { let values = value.table_entries().cloned().collect::>(); let occurrences = values.len(); (occurrences, values[occurrences - 1].clone()) }) .collect::>() .into_iter() .map(move |(occurrences, value)| { let mut fact = TaggedDictBuilder::new(&name); let column_value = labels .get(idx) .ok_or_else(|| { ShellError::labeled_error( "Unable to load group labels", "unabled to load group labels", &name, ) })? .clone(); fact.insert_value(&column.item, column_value); fact.insert_untagged("occurrences", UntaggedValue::int(occurrences)); let percentage = format!( "{}%", // Some(2) < the number of digits // true < group the digits crate::commands::str_::from::action(&value, &name, Some(2), true)? .as_string()? ); fact.insert_untagged("percentage", UntaggedValue::string(percentage)); let string = std::iter::repeat("*") .take(value.as_u64().map_err(|_| { ShellError::labeled_error("expected a number", "expected a number", &name) })? as usize) .collect::(); fact.insert_untagged(&frequency_column_name, UntaggedValue::string(string)); idx += 1; ReturnSuccess::value(fact.into_value()) }), ) .to_output_stream()) } fn evaluator(by: ColumnPath) -> Box Result + Send> { Box::new(move |_: usize, value: &Value| { let path = by.clone(); let eval = nu_value_ext::get_data_by_column_path( value, &path, Box::new(move |(_, _, error)| error), ); match eval { Ok(with_value) => Ok(with_value), Err(reason) => Err(reason), } }) } fn splitter( by: Option>, ) -> Box Result + Send> { match by { Some(column) => Box::new(move |_, row: &Value| { let key = &column; match row.get_data_by_key(key.borrow_spanned()) { Some(key) => nu_value_ext::as_string(&key), None => Err(ShellError::labeled_error( "unknown column", "unknown column", key.tag(), )), } }), None => Box::new(move |_, row: &Value| nu_value_ext::as_string(&row)), } } #[cfg(test)] mod tests { use super::Histogram; #[test] fn examples_work_as_expected() { use crate::examples::test as test_examples; test_examples(Histogram {}) } }