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}; use crate::search::parse_filters; //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() { let filters = parse_filters(&filters.join(" ")); search = search.apply(filters); } 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(); transactions.sort_by(|t1, t2| { for sort in &mut sorts { let sort = sort(t1, t2); if sort != Ordering::Equal { return sort; } } return Ordering::Equal; }); } 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), } }