add groupby
This commit is contained in:
parent
99fe4ecee1
commit
9fa39fc1c2
3 changed files with 112 additions and 30 deletions
|
@ -24,6 +24,7 @@ pub fn get_args() -> ArgMatches {
|
||||||
.default_value("file.title:Title"),
|
.default_value("file.title:Title"),
|
||||||
)
|
)
|
||||||
.arg(arg!(-s --sortby <KEY> "Sort results based on specified key").required(false))
|
.arg(arg!(-s --sortby <KEY> "Sort results based on specified key").required(false))
|
||||||
|
.arg(arg!(-g --groupby <KEY> "Group results based on specified key").required(false))
|
||||||
.arg(arg!(-r --reverse "Reverse the results").required(false))
|
.arg(arg!(-r --reverse "Reverse the results").required(false))
|
||||||
.get_matches()
|
.get_matches()
|
||||||
}
|
}
|
||||||
|
|
42
src/lib.rs
42
src/lib.rs
|
@ -1,3 +1,5 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use txd::DataType;
|
use txd::DataType;
|
||||||
|
|
||||||
/// get frontmatter from markdown document
|
/// get frontmatter from markdown document
|
||||||
|
@ -79,16 +81,7 @@ impl Index {
|
||||||
|
|
||||||
/// Build a table with specified columns from index within specified scope
|
/// Build a table with specified columns from index within specified scope
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn select_columns(
|
pub fn apply(&self, limit: usize, offset: usize, sort: Option<String>, reverse: bool) -> Self {
|
||||||
&self,
|
|
||||||
col: &[String],
|
|
||||||
limit: usize,
|
|
||||||
offset: usize,
|
|
||||||
sort: Option<String>,
|
|
||||||
reverse: bool,
|
|
||||||
) -> Table {
|
|
||||||
let mut rows = vec![];
|
|
||||||
|
|
||||||
let mut scope = self.documents.clone();
|
let mut scope = self.documents.clone();
|
||||||
|
|
||||||
if let Some(sort) = sort {
|
if let Some(sort) = sort {
|
||||||
|
@ -112,7 +105,28 @@ impl Index {
|
||||||
scope.into_iter().take(limit).collect()
|
scope.into_iter().take(limit).collect()
|
||||||
};
|
};
|
||||||
|
|
||||||
for doc in scope {
|
Self { documents: scope }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn group_by(&self, key: &str) -> HashMap<String, Self> {
|
||||||
|
let mut grouped_items: HashMap<String, Vec<Document>> = 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![];
|
let mut rcol = vec![];
|
||||||
for c in col {
|
for c in col {
|
||||||
rcol.push(doc.get_key(c));
|
rcol.push(doc.get_key(c));
|
||||||
|
@ -138,7 +152,11 @@ impl Index {
|
||||||
let mut a = txd::parse(&a_str);
|
let mut a = txd::parse(&a_str);
|
||||||
let b = txd::parse(&f.2);
|
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() {
|
if a_str.is_empty() {
|
||||||
// TODO : Maybe add explicit null instead of empty string
|
// TODO : Maybe add explicit null instead of empty string
|
||||||
|
|
99
src/main.rs
99
src/main.rs
|
@ -1,10 +1,9 @@
|
||||||
use std::io::IsTerminal;
|
use std::{collections::HashMap, io::IsTerminal};
|
||||||
|
|
||||||
use mdq::Index;
|
use mdq::Index;
|
||||||
|
|
||||||
mod args;
|
mod args;
|
||||||
|
|
||||||
// TODO : Add debug logging
|
|
||||||
// TODO : Add documentation comments
|
// TODO : Add documentation comments
|
||||||
// TODO : Add tests
|
// TODO : Add tests
|
||||||
// TODO : Add GROUP BY Function
|
// TODO : Add GROUP BY Function
|
||||||
|
@ -21,7 +20,13 @@ fn main() {
|
||||||
|
|
||||||
let offset: usize = args.get_one::<String>("offset").unwrap().parse().unwrap();
|
let offset: usize = args.get_one::<String>("offset").unwrap().parse().unwrap();
|
||||||
|
|
||||||
let sort_by = args.get_one::<String>("sortby").map(|x| x.to_owned());
|
let sort_by = args
|
||||||
|
.get_one::<String>("sortby")
|
||||||
|
.map(std::borrow::ToOwned::to_owned);
|
||||||
|
|
||||||
|
let group_by = args
|
||||||
|
.get_one::<String>("groupby")
|
||||||
|
.map(std::borrow::ToOwned::to_owned);
|
||||||
|
|
||||||
let reversed = args.get_flag("reverse");
|
let reversed = args.get_flag("reverse");
|
||||||
|
|
||||||
|
@ -30,7 +35,7 @@ fn main() {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect();
|
.collect();
|
||||||
log::info!("selected columns: {columns:?}");
|
log::debug!("columns: {columns:?}");
|
||||||
|
|
||||||
let (columns, headers): (Vec<_>, Vec<_>) = columns
|
let (columns, headers): (Vec<_>, Vec<_>) = columns
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -41,21 +46,71 @@ fn main() {
|
||||||
})
|
})
|
||||||
.unzip();
|
.unzip();
|
||||||
|
|
||||||
|
if columns != headers {
|
||||||
|
log::debug!("renamed headers: {headers:?}");
|
||||||
|
}
|
||||||
|
|
||||||
let filters = args
|
let filters = args
|
||||||
.get_many::<String>("filter")
|
.get_many::<String>("filter")
|
||||||
.map_or_else(std::vec::Vec::new, std::iter::Iterator::collect);
|
.map_or_else(std::vec::Vec::new, std::iter::Iterator::collect);
|
||||||
|
|
||||||
|
log::debug!("raw filters: {filters:?}");
|
||||||
let filters: Vec<_> = filters
|
let filters: Vec<_> = filters
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|x| txd::filter::parse_condition(x).expect("failed to parse filter"))
|
.map(|x| txd::filter::parse_condition(x).expect("failed to parse filter"))
|
||||||
.collect();
|
.collect();
|
||||||
|
log::debug!("parsed filters: {filters:?}");
|
||||||
|
|
||||||
let mut i = Index::new(root_dir);
|
let mut i = Index::new(root_dir);
|
||||||
if !filters.is_empty() {
|
if !filters.is_empty() {
|
||||||
i = i.filter_documents(&filters);
|
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 {
|
if output_json {
|
||||||
let mut data = serde_json::json!(
|
let mut data = serde_json::json!(
|
||||||
|
@ -72,24 +127,18 @@ fn main() {
|
||||||
println!("{}", serde_json::to_string(&data).unwrap());
|
println!("{}", serde_json::to_string(&data).unwrap());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if std::io::stdout().is_terminal() {
|
||||||
|
print_result(data, &headers);
|
||||||
|
} else {
|
||||||
|
print_csv(data, Some(&headers));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_result(data: Vec<Vec<String>>, headers: &[String]) {
|
||||||
if data.is_empty() {
|
if data.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut table = comfy_table::Table::new();
|
let mut table = comfy_table::Table::new();
|
||||||
|
|
||||||
table.set_header(headers);
|
table.set_header(headers);
|
||||||
|
@ -98,3 +147,17 @@ fn main() {
|
||||||
|
|
||||||
println!("{table}");
|
println!("{table}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn print_csv(data: Vec<Vec<String>>, 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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue