use chrono::naive::NaiveDate; use rust_decimal::Decimal; use std::cmp::Ordering; use std::path::PathBuf; use std::str::FromStr; use structopt::clap::AppSettings; use structopt::StructOpt; use tabled::{Style, Table}; mod search; mod store; mod transaction; use search::Search; use store::Store; use transaction::{Transaction, TransactionKind}; //TODO relative ("yesterday", "-2d", etc) fn parse_date(s: &str) -> Result { NaiveDate::parse_from_str(s, "%Y-%m-%d").map_err(|e| e.to_string()) } #[derive(Debug)] #[derive(StructOpt)] enum Command { Insert { kind: TransactionKind, #[structopt(long)] account: String, //TODO multiple #[structopt(long)] category: String, #[structopt(long, parse(try_from_str = Decimal::from_str))] amount: Decimal, description: String, #[structopt(long, parse(try_from_str = parse_date))] date: Option, }, List { target: ListTarget, }, #[structopt(setting = AppSettings::AllowLeadingHyphen)] Show { #[structopt(long, multiple = true, number_of_values = 1)] sort: Vec, filters: Vec, }, } #[derive(Debug)] #[derive(StructOpt)] enum ListTarget { Categories, } impl std::str::FromStr for ListTarget { type Err = String; fn from_str(s: &str) -> Result { match s { "categories" => Ok(ListTarget::Categories), _ => Err(format!("Unknown listable: {:?}", s)), } } } #[derive(Debug)] #[derive(StructOpt)] enum SortTarget { Amount, Date, } impl std::str::FromStr for SortTarget { type Err = String; fn from_str(s: &str) -> Result { match s { "amount" => Ok(SortTarget::Amount), "date" => Ok(SortTarget::Date), _ => Err(format!("Unknown sort target: {:?}", s)), } } } #[derive(Debug)] #[derive(StructOpt)] struct Mn { #[structopt(subcommand)] command: Command, } fn main() { let mut store = Store::open(PathBuf::from("store")).unwrap(); let args = Mn::from_args(); eprintln!("{:?}", args); match args.command { Command::Insert { kind, account, category, amount, description, date, } => { let transaction = Transaction { kind, to: account, from: "Default".to_string(), category, amount, description, date: match date { Some(date) => date, None => chrono::offset::Local::today().naive_utc(), }, }; eprintln!("{:?}", transaction); println!("{}", transaction.id()); store.push(transaction); store.write().unwrap(); } Command::List { target: ListTarget::Categories } => { println!("{}", store.categories().join("\n")); } Command::Show { sort, filters, } => { let mut search = Search::new(store.transactions()); if !filters.is_empty() { search = search.parse(filters.join(" ")); } let mut transactions = search.get(); if sort.is_empty() { transactions.sort_by_key(|t| t.date); } else { let mut sorts: Vec<_> = sort.iter().map(sort_by_func).collect(); inner_sort_by(&mut transactions, &mut sorts); } println!("{}", Table::new(transactions).with(Style::psql())); } } } fn sort_by_func(sort: &SortTarget) -> impl FnMut(&&Transaction, &&Transaction) -> Ordering { match sort { SortTarget::Amount => |t1: &&Transaction, t2: &&Transaction| t1.amount.cmp(&t2.amount), SortTarget::Date => |t1: &&Transaction, t2: &&Transaction| t1.date.cmp(&t2.date), } } fn inner_sort_by(v: &mut [T], cmps: &mut [F]) where F: FnMut(&T, &T) -> Ordering, { if v.is_empty() || cmps.is_empty() { return; } // Sort the slice using the first comparison. v.sort_by(&mut cmps[0]); // Find ranges of consecutive equal items according to the first comparison. let mut ranges = Vec::new(); let mut lower = 0; // Lower bound of current equal range. for i in 0..v.len() { if cmps[0](&v[i], &v[lower]) != Ordering::Equal { ranges.push(lower..i); lower = i; } } // If we still have comparisons to use, sort the ranges of consecutive items recursively. // The recursion ensures that ranges of equal items aren't mixed between comparisons further down. if cmps.len() != 1 { for range in ranges { inner_sort_by(&mut v[range], &mut cmps[1..]); } } }