use crate::commands::WholeStreamCommand; use crate::prelude::*; use nu_errors::ShellError; use nu_protocol::{ReturnSuccess, Signature, SyntaxShape, Value}; use nu_source::Tagged; pub struct GroupBy; #[derive(Deserialize)] pub struct GroupByArgs { column_name: Tagged, date: Tagged, format: Option>, } impl WholeStreamCommand for GroupBy { fn name(&self) -> &str { "group-by" } fn signature(&self) -> Signature { Signature::build("group-by") .required( "column_name", SyntaxShape::String, "the name of the column to group by", ) .named( "format", SyntaxShape::String, "Specify date and time formatting", Some('f'), ) .switch("date", "by date", Some('d')) } fn usage(&self) -> &str { "Creates a new table with the data from the table rows grouped by the column given." } fn run( &self, args: CommandArgs, registry: &CommandRegistry, ) -> Result { args.process(registry, group_by)?.run() } fn examples(&self) -> &[Example] { &[Example { description: "Group files by type", example: "ls | group-by type", }] } } enum Grouper { Default, ByDate(Option), } pub fn group_by( GroupByArgs { column_name, date, format, }: GroupByArgs, RunnableContext { input, name, .. }: RunnableContext, ) -> Result { let stream = async_stream! { let values: Vec = input.collect().await; if values.is_empty() { yield Err(ShellError::labeled_error( "Expected table from pipeline", "requires a table input", column_name.span() )) } else { let grouper = if let Tagged { item: true, tag } = date { if let Some(Tagged { item: fmt, tag }) = format { Grouper::ByDate(Some(fmt)) } else { Grouper::ByDate(None) } } else { Grouper::Default }; match grouper { Grouper::Default => { match crate::utils::data::group(column_name, &values, None, &name) { Ok(grouped) => yield ReturnSuccess::value(grouped), Err(err) => yield Err(err), } } Grouper::ByDate(None) => { match crate::utils::data::group(column_name, &values, Some(Box::new(|row: &Value| row.format("%Y-%b-%d"))), &name) { Ok(grouped) => yield ReturnSuccess::value(grouped), Err(err) => yield Err(err), } } Grouper::ByDate(Some(fmt)) => { match crate::utils::data::group(column_name, &values, Some(Box::new(move |row: &Value| { row.format(&fmt) })), &name) { Ok(grouped) => yield ReturnSuccess::value(grouped), Err(err) => yield Err(err), } } } } }; Ok(stream.to_output_stream()) } pub fn group( column_name: &Tagged, values: Vec, tag: impl Into, ) -> Result { crate::utils::data::group(column_name.clone(), &values, None, tag) } #[cfg(test)] mod tests { use crate::commands::group_by::group; use indexmap::IndexMap; use nu_errors::ShellError; use nu_protocol::{UntaggedValue, Value}; use nu_source::*; fn string(input: impl Into) -> Value { UntaggedValue::string(input.into()).into_untagged_value() } fn row(entries: IndexMap) -> Value { UntaggedValue::row(entries).into_untagged_value() } fn table(list: &[Value]) -> Value { UntaggedValue::table(list).into_untagged_value() } fn nu_releases_commiters() -> Vec { vec![ row( indexmap! {"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("August 23-2019")}, ), row( indexmap! {"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("August 23-2019")}, ), row( indexmap! {"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("October 10-2019")}, ), row( indexmap! {"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("Sept 24-2019")}, ), row( indexmap! {"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("October 10-2019")}, ), row( indexmap! {"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("Sept 24-2019")}, ), row( indexmap! {"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("October 10-2019")}, ), row( indexmap! {"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("Sept 24-2019")}, ), row( indexmap! {"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("August 23-2019")}, ), ] } #[test] fn groups_table_by_date_column() -> Result<(), ShellError> { let for_key = String::from("date").tagged_unknown(); assert_eq!( group(&for_key, nu_releases_commiters(), Tag::unknown())?, row(indexmap! { "August 23-2019".into() => table(&[ row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("August 23-2019")}), row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("August 23-2019")}), row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("August 23-2019")}) ]), "October 10-2019".into() => table(&[ row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("October 10-2019")}), row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("October 10-2019")}), row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("October 10-2019")}) ]), "Sept 24-2019".into() => table(&[ row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("Sept 24-2019")}), row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("Sept 24-2019")}), row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("Sept 24-2019")}) ]), }) ); Ok(()) } #[test] fn groups_table_by_country_column() -> Result<(), ShellError> { let for_key = String::from("country").tagged_unknown(); assert_eq!( group(&for_key, nu_releases_commiters(), Tag::unknown())?, row(indexmap! { "EC".into() => table(&[ row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("August 23-2019")}), row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("Sept 24-2019")}), row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("October 10-2019")}) ]), "NZ".into() => table(&[ row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("August 23-2019")}), row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("October 10-2019")}), row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("Sept 24-2019")}) ]), "US".into() => table(&[ row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("October 10-2019")}), row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("Sept 24-2019")}), row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("August 23-2019")}), ]), }) ); Ok(()) } }