use crate::commands::WholeStreamCommand; use crate::data::base::property_get::get_data_by_key; use crate::data::{value, TaggedDictBuilder, TaggedListBuilder}; use crate::prelude::*; use chrono::{DateTime, NaiveDate, Utc}; use nu_protocol::{ Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value, }; use nu_errors::ShellError; use nu_source::Tagged; pub struct TSortBy; #[derive(Deserialize)] pub struct TSortByArgs { #[serde(rename(deserialize = "show-columns"))] show_columns: bool, group_by: Option>, #[allow(unused)] split_by: Option, } impl WholeStreamCommand for TSortBy { fn name(&self) -> &str { "t-sort-by" } fn signature(&self) -> Signature { Signature::build("t-sort-by") .switch("show-columns", "Displays the column names sorted") .named( "group_by", SyntaxShape::String, "the name of the column to group by", ) .named( "split_by", SyntaxShape::String, "the name of the column within the grouped by table to split by", ) } fn usage(&self) -> &str { "Sort by the given columns." } fn run( &self, args: CommandArgs, registry: &CommandRegistry, ) -> Result { args.process(registry, t_sort_by)?.run() } } fn t_sort_by( TSortByArgs { show_columns, group_by, .. }: TSortByArgs, RunnableContext { input, name, .. }: RunnableContext, ) -> Result { Ok(OutputStream::new(async_stream! { let values: Vec = input.values.collect().await; let column_grouped_by_name = if let Some(grouped_by) = group_by { Some(grouped_by.item().clone()) } else { None }; if show_columns { for label in columns_sorted(column_grouped_by_name, &values[0], &name).into_iter() { yield ReturnSuccess::value(value::string(label.item).into_value(label.tag)); } } else { match t_sort(column_grouped_by_name, None, &values[0], name) { Ok(sorted) => yield ReturnSuccess::value(sorted), Err(err) => yield Err(err) } } })) } pub fn columns_sorted( _group_by_name: Option, value: &Value, tag: impl Into, ) -> Vec> { let origin_tag = tag.into(); match value { Value { value: UntaggedValue::Row(rows), .. } => { let mut keys: Vec = rows .entries .keys() .map(|s| s.as_ref()) .map(|k: &str| { let date = NaiveDate::parse_from_str(k, "%B %d-%Y"); let date = match date { Ok(parsed) => UntaggedValue::Primitive(Primitive::Date( DateTime::::from_utc(parsed.and_hms(12, 34, 56), Utc), )), Err(_) => value::string(k), }; date.into_untagged_value() }) .collect(); keys.sort(); let keys: Vec = keys .into_iter() .map(|k| match k { Value { value: UntaggedValue::Primitive(Primitive::Date(d)), .. } => format!("{}", d.format("%B %d-%Y")), _ => k.as_string().unwrap().to_string(), }) .collect(); keys.into_iter().map(|k| k.tagged(&origin_tag)).collect() } _ => vec![format!("default").tagged(&origin_tag)], } } pub fn t_sort( group_by_name: Option, split_by_name: Option, value: &Value, tag: impl Into, ) -> Result { let origin_tag = tag.into(); match group_by_name { Some(column_name) => { let sorted_labels: Vec> = columns_sorted(Some(column_name), value, &origin_tag); match split_by_name { None => { let mut dataset = TaggedDictBuilder::new(&origin_tag); dataset.insert_value("default", value.clone()); let dataset = dataset.into_value(); let split_labels: Vec> = match &dataset { Value { value: UntaggedValue::Row(rows), .. } => { let mut keys: Vec> = rows .entries .keys() .map(|k| k.clone().tagged_unknown()) .collect(); keys.sort(); keys } _ => vec![], }; let results: Vec> = split_labels .iter() .map(|split| { let groups = get_data_by_key(&dataset, split.borrow_spanned()); sorted_labels .clone() .into_iter() .map(|label| match &groups { Some(Value { value: UntaggedValue::Row(dict), .. }) => dict .get_data_by_key(label.borrow_spanned()) .unwrap() .clone(), _ => UntaggedValue::Table(vec![]).into_value(&origin_tag), }) .collect() }) .collect(); let mut outer = TaggedListBuilder::new(&origin_tag); for i in results { outer.push_value(UntaggedValue::Table(i).into_value(&origin_tag)); } return Ok(UntaggedValue::Table(outer.list).into_value(&origin_tag)); } Some(_) => return Ok(value::nothing().into_value(&origin_tag)), } } None => return Ok(value::nothing().into_value(&origin_tag)), } } #[cfg(test)] mod tests { use crate::commands::group_by::group; use crate::commands::t_sort_by::{columns_sorted, t_sort}; use crate::data::value; use indexmap::IndexMap; use nu_protocol::{UntaggedValue, Value}; use nu_source::*; fn string(input: impl Into) -> Value { value::string(input.into()).into_untagged_value() } fn row(entries: IndexMap) -> Value { value::row(entries).into_untagged_value() } fn table(list: &Vec) -> Value { value::table(list).into_untagged_value() } fn nu_releases_grouped_by_date() -> Value { let key = String::from("date").tagged_unknown(); group(&key, nu_releases_commiters(), Tag::unknown()).unwrap() } 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("September 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("September 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("September 24-2019")}, ), row( indexmap! {"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("August 23-2019")}, ), ] } #[test] fn show_columns_sorted_given_a_column_to_sort_by() { let by_column = String::from("date"); assert_eq!( columns_sorted( Some(by_column), &nu_releases_grouped_by_date(), Tag::unknown() ), vec![ format!("August 23-2019").tagged_unknown(), format!("September 24-2019").tagged_unknown(), format!("October 10-2019").tagged_unknown() ] ) } #[test] fn sorts_the_tables() { let group_by = String::from("date"); assert_eq!( t_sort( Some(group_by), None, &nu_releases_grouped_by_date(), Tag::unknown() ) .unwrap(), table(&vec![table(&vec![ table(&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("August 23-2019")} ) ]), table(&vec![ row( indexmap! {"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("September 24-2019")} ), row( indexmap! {"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("September 24-2019")} ), row( indexmap! {"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("September 24-2019")} ) ]), table(&vec![ 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")} ) ]), ]),]) ); } }