use chrono::naive::NaiveDate; use rust_decimal::Decimal; 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, //TODO ? } 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 { match &sort[0] { SortTarget::Amount => transactions.sort_by_key(|t| t.amount), SortTarget::Date => transactions.sort_by_key(|t| t.date), } for i in 1..sort.len() { inner_sort_by(&mut transactions, sort_by_func(&sort[i-1]), sort_by_func(&sort[i])); } } println!("{}", Table::new(transactions).with(Style::psql())); } } } fn sort_by_func(sort: &SortTarget) -> impl FnMut(&&Transaction, &&Transaction) -> std::cmp::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], mut outer_cmp: F, mut inner_cmp: F) where F: FnMut(&T, &T) -> std::cmp::Ordering, { // Early out if v.len() < 2 { return; } let mut lower = 0; // Lower bound of current equal range for i in 0..v.len() { if outer_cmp(&v[i], &v[lower]) != std::cmp::Ordering::Equal { let upper = i; if upper - lower > 1 { v[lower..upper].sort_by(&mut inner_cmp); } lower = i; } } }