123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246 |
- use std::collections::HashSet;
- use itertools::Itertools;
- // use crate::{check::CheckLevel, data, import::import_from, io, show};
- use crate::prelude::*;
- mod comb;
- mod infer;
- mod txnmatch;
- #[derive(clap::Parser)]
- pub struct Invocation {
- /// path to treasure file
- #[clap(long, short, env = "HOARD_ROOT")]
- pub file: std::path::PathBuf,
- /// verbosity of output messages
- #[clap(long, short, action = clap::ArgAction::Count)]
- pub verbose: u8,
- #[clap(subcommand)]
- pub cmd: Command,
- }
- fn load_data(
- fsdata: &mut io::FilesystemData,
- invocation: &Invocation,
- level: check::CheckLevel,
- ) -> anyhow::Result<data::Hoard> {
- let path = std::fs::canonicalize(&invocation.file)?;
- match data::Hoard::load(fsdata, &path, level) {
- Ok(data) => Ok(data),
- Err(DataError::IOError(ioerror)) => Err(ioerror.into()),
- Err(DataError::Report(report)) => {
- report.eprint(fsdata)?;
- Err(anyhow::anyhow!("Error reported"))
- }
- }
- }
- #[derive(clap::Subcommand)]
- pub enum Command {
- Summarize,
- Ledger {
- account: String,
- #[clap(long, short)]
- raw: bool
- },
- Reformat {
- #[clap(long)]
- /// do not add ID annotations to transactions lacking them
- skip_ids: bool,
- },
- Import {
- account: String,
- from: std::path::PathBuf,
- target: Option<std::path::PathBuf>,
- },
- Infer,
- Match {
- account: Option<data::AccountName>,
- },
- Comb {
- account: Option<data::AccountName>,
- },
- }
- fn summarize(data: &data::Hoard) {
- let positive = console::Style::new().green();
- let negative = console::Style::new().red();
- let neutral = console::Style::new();
- let Some(maxlen) = data.account_names().map(|v| v.len()).max() else {
- return;
- };
- for shortname in data.account_names().sorted_by_key(|an| an.as_str()) {
- if shortname.as_ref() == "initial" {
- continue;
- }
- if let Some(mut balances) = data.balance(shortname) {
- // insert a default balance of 0 if no balance is present
- if let Some(unit) = data.account_spec(shortname).unwrap().unit {
- balances.entry(unit).or_default();
- }
- let balances = balances
- .into_iter()
- .filter(|(_, b)| !b.is_zero())
- .sorted()
- .map(|(u, b)| {
- (
- u,
- if b.is_sign_positive() {
- positive.apply_to(b).to_string()
- } else if b.is_sign_negative() {
- negative.apply_to(b).to_string()
- } else {
- neutral.apply_to("0").to_string()
- },
- )
- });
- println!(
- "{shortname:maxlen$} {}",
- balances.map(|(u, b)| format!("{b:8} {u:5}")).join(" ")
- );
- }
- }
- }
- impl Command {
- pub fn run(&self, inv: &Invocation) -> anyhow::Result<()> {
- let mut fsdata = io::FilesystemData::default();
- match self {
- Self::Summarize => {
- let data = load_data(&mut fsdata, inv, Default::default())?;
- summarize(&data);
- }
- Self::Ledger { account, raw } => {
- let data = load_data(&mut fsdata, inv, check::CheckLevel::WellFormed)?;
- let aname = data::AccountName::new(account.as_str());
- if let Some(ld) = data.ledger_data_for(aname) {
- if *raw {
- for txn in ld {
- show::show_transaction(Some(&data), &txn.borrow());
- }
- } else {
- let tt = show::TransactionTable::default();
- tt.show_refs(Some(&data), aname, ld.iter());
- }
- } else {
- log::error!("account not found!");
- }
- }
- Self::Reformat { skip_ids } => {
- let mut data = load_data(&mut fsdata, inv, check::CheckLevel::WellFormed)?;
- if !skip_ids {
- let mut used_ids = HashSet::new();
- for le in data.all_raw_ledger_data() {
- let Some(txn) = le.as_transaction() else {
- continue;
- };
- let Some(id) = txn.get_annotation("id") else {
- continue;
- };
- used_ids.insert(id.to_owned());
- }
- // assign IDs if to transactions lacking them
- for le in data.all_raw_ledger_data_mut() {
- let Some(txn) = le.as_transaction_mut() else {
- continue;
- };
- if txn.get_annotation("id").is_none() {
- // generated unique ID
- let mut id = nanoid::nanoid!(10);
- while used_ids.contains(&id) {
- id = nanoid::nanoid!(10);
- }
- // assign ID
- txn.annotations.insert("id".to_string(), id);
- }
- }
- }
- data::format_ledger(&mut fsdata, &data, data.all_raw_ledger_data().iter())?;
- }
- Self::Import {
- account,
- from,
- target,
- } => {
- let data = load_data(&mut fsdata, inv, Default::default())?;
- let aname = account.into();
- let Some(aspec) = data.account_spec(aname) else {
- log::error!("Account {aname} does not exist!");
- return Ok(());
- };
- let default_destination = data.spec_root().placeholder_account;
- let imported =
- import::import_from(aspec, aname, default_destination, from.as_path()).unwrap();
- log::info!("Imported {} transaction(s)", imported.len());
- if let Some(target) = target {
- let new_source = std::fs::canonicalize(target.as_os_str())?
- .into_os_string()
- .into();
- let source_span = io::Span::null_for_file(new_source);
- let new_data = imported
- .into_iter()
- .map(|mut txn| {
- txn.source = Some(source_span);
- data::RawLedgerEntry::Transaction(txn)
- })
- .collect_vec();
- data::format_ledger(
- &mut fsdata,
- &data,
- data.raw_ledger_data_from(new_source).chain(new_data.iter()),
- )?;
- } else {
- log::info!("No target specified, showing new data on stdout.");
- let tt = show::TransactionTable::default();
- tt.show(Some(&data), aname, imported.iter());
- }
- }
- Self::Infer => {
- let mut data = load_data(&mut fsdata, inv, check::CheckLevel::WellFormed)?;
- if infer::do_inference(&mut data)? {
- data::format_entire_ledger(&mut fsdata, &data)?;
- }
- }
- Self::Match { account } => {
- let mut data = load_data(&mut fsdata, inv, check::CheckLevel::WellFormed)?;
- let account = account.unwrap_or(data.spec_root().placeholder_account);
- if txnmatch::do_match(&mut data, account)? {
- data::format_entire_ledger(&mut fsdata, &data)?;
- }
- }
- Self::Comb { account } => {
- let data = load_data(&mut fsdata, inv, check::CheckLevel::Consistent)?;
- let account = account.unwrap_or(data.spec_root().placeholder_account);
- if comb::do_comb(&data, account)? {
- data::format_entire_ledger(&mut fsdata, &data)?;
- }
- }
- }
- Ok(())
- }
- }
|