diff --git a/src/args.rs b/src/args.rs index 22c52ef..e971d87 100644 --- a/src/args.rs +++ b/src/args.rs @@ -24,6 +24,7 @@ pub fn get_args() -> ArgMatches { .default_value("file.title:Title"), ) .arg(arg!(-s --sortby "Sort results based on specified key").required(false)) + .arg(arg!(-g --groupby "Group results based on specified key").required(false)) .arg(arg!(-r --reverse "Reverse the results").required(false)) .get_matches() } diff --git a/src/lib.rs b/src/lib.rs index f6edef3..7ba4ae9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use txd::DataType; /// get frontmatter from markdown document @@ -79,16 +81,7 @@ impl Index { /// Build a table with specified columns from index within specified scope #[must_use] - pub fn select_columns( - &self, - col: &[String], - limit: usize, - offset: usize, - sort: Option, - reverse: bool, - ) -> Table { - let mut rows = vec![]; - + pub fn apply(&self, limit: usize, offset: usize, sort: Option, reverse: bool) -> Self { let mut scope = self.documents.clone(); if let Some(sort) = sort { @@ -112,7 +105,28 @@ impl Index { scope.into_iter().take(limit).collect() }; - for doc in scope { + Self { documents: scope } + } + + #[must_use] + pub fn group_by(&self, key: &str) -> HashMap { + let mut grouped_items: HashMap> = HashMap::new(); + + for doc in self.documents.clone() { + grouped_items.entry(doc.get_key(key)).or_default().push(doc); + } + + grouped_items + .into_iter() + .map(|(key, item)| (key, Index { documents: item })) + .collect() + } + + #[must_use] + pub fn create_table_data(&self, col: &[String]) -> Table { + let mut rows = vec![]; + + for doc in &self.documents { let mut rcol = vec![]; for c in col { rcol.push(doc.get_key(c)); @@ -138,7 +152,11 @@ impl Index { let mut a = txd::parse(&a_str); let b = txd::parse(&f.2); - log::debug!("Trying to compare {a:?} and {b:?} with {:?}", f.1); + log::debug!( + "Trying to compare '{}' = {a:?} and {b:?} with {:?}", + f.0, + f.1 + ); if a_str.is_empty() { // TODO : Maybe add explicit null instead of empty string diff --git a/src/main.rs b/src/main.rs index 27a2a4f..1c5f055 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,9 @@ -use std::io::IsTerminal; +use std::{collections::HashMap, io::IsTerminal}; use mdq::Index; mod args; -// TODO : Add debug logging // TODO : Add documentation comments // TODO : Add tests // TODO : Add GROUP BY Function @@ -21,7 +20,13 @@ fn main() { let offset: usize = args.get_one::("offset").unwrap().parse().unwrap(); - let sort_by = args.get_one::("sortby").map(|x| x.to_owned()); + let sort_by = args + .get_one::("sortby") + .map(std::borrow::ToOwned::to_owned); + + let group_by = args + .get_one::("groupby") + .map(std::borrow::ToOwned::to_owned); let reversed = args.get_flag("reverse"); @@ -30,7 +35,7 @@ fn main() { .unwrap() .cloned() .collect(); - log::info!("selected columns: {columns:?}"); + log::debug!("columns: {columns:?}"); let (columns, headers): (Vec<_>, Vec<_>) = columns .into_iter() @@ -41,21 +46,71 @@ fn main() { }) .unzip(); + if columns != headers { + log::debug!("renamed headers: {headers:?}"); + } + let filters = args .get_many::("filter") .map_or_else(std::vec::Vec::new, std::iter::Iterator::collect); + log::debug!("raw filters: {filters:?}"); let filters: Vec<_> = filters .into_iter() .map(|x| txd::filter::parse_condition(x).expect("failed to parse filter")) .collect(); + log::debug!("parsed filters: {filters:?}"); let mut i = Index::new(root_dir); if !filters.is_empty() { i = i.filter_documents(&filters); } - let data = i.select_columns(&columns, limit, offset, sort_by, reversed); + i = i.apply(limit, offset, sort_by, reversed); + + if group_by.is_some() { + let grouped = i.group_by(&group_by.unwrap()); + let grouped: HashMap<_, _> = grouped + .into_iter() + .map(|(key, val)| (key, val.create_table_data(&columns))) + .collect(); + + if output_json { + let mut data = serde_json::json!( + { + "columns": columns, + "results": grouped + } + ); + if columns != headers { + data.as_object_mut() + .unwrap() + .insert("headers".into(), headers.into()); + } + println!("{}", serde_json::to_string(&data).unwrap()); + return; + } + + if std::io::stdout().is_terminal() { + for (group, val) in grouped { + println!("# {group}"); + print_result(val, &headers); + } + } else { + let mut first = true; + for (_, val) in grouped { + if first { + print_csv(val, Some(&headers)); + first = false; + continue; + } + print_csv(val, None); + } + } + return; + } + + let data = i.create_table_data(&columns); if output_json { let mut data = serde_json::json!( @@ -72,21 +127,15 @@ fn main() { println!("{}", serde_json::to_string(&data).unwrap()); return; } - - if data.is_empty() { - return; + if std::io::stdout().is_terminal() { + print_result(data, &headers); + } else { + print_csv(data, Some(&headers)); } +} - if !std::io::stdout().is_terminal() { - let mut writer = csv::WriterBuilder::new().from_writer(vec![]); - writer.write_record(headers).unwrap(); - for e in data { - writer.write_record(e).unwrap(); - } - print!( - "{}", - String::from_utf8(writer.into_inner().unwrap()).unwrap() - ); +fn print_result(data: Vec>, headers: &[String]) { + if data.is_empty() { return; } @@ -98,3 +147,17 @@ fn main() { println!("{table}"); } + +fn print_csv(data: Vec>, headers: Option<&[String]>) { + let mut writer = csv::WriterBuilder::new().from_writer(vec![]); + if let Some(headers) = headers { + writer.write_record(headers).unwrap(); + } + for e in data { + writer.write_record(e).unwrap(); + } + print!( + "{}", + String::from_utf8(writer.into_inner().unwrap()).unwrap() + ); +}