cmd.rs 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. use std::collections::HashSet;
  2. use itertools::Itertools;
  3. // use crate::{check::CheckLevel, data, import::import_from, io, show};
  4. use crate::prelude::*;
  5. mod comb;
  6. mod infer;
  7. mod txnmatch;
  8. #[derive(clap::Parser)]
  9. pub struct Invocation {
  10. /// path to treasure file
  11. #[clap(long, short, env = "HOARD_ROOT")]
  12. pub file: std::path::PathBuf,
  13. /// verbosity of output messages
  14. #[clap(long, short, action = clap::ArgAction::Count)]
  15. pub verbose: u8,
  16. #[clap(subcommand)]
  17. pub cmd: Command,
  18. }
  19. fn load_data(
  20. fsdata: &mut io::FilesystemData,
  21. invocation: &Invocation,
  22. level: check::CheckLevel,
  23. ) -> anyhow::Result<data::Hoard> {
  24. let path = std::fs::canonicalize(&invocation.file)?;
  25. match data::Hoard::load(fsdata, &path, level) {
  26. Ok(data) => Ok(data),
  27. Err(DataError::IOError(ioerror)) => Err(ioerror.into()),
  28. Err(DataError::Report(report)) => {
  29. report.eprint(fsdata)?;
  30. Err(anyhow::anyhow!("Error reported"))
  31. }
  32. }
  33. }
  34. #[derive(clap::Subcommand)]
  35. pub enum Command {
  36. Summarize,
  37. Ledger {
  38. account: String,
  39. #[clap(long, short)]
  40. raw: bool
  41. },
  42. Reformat {
  43. #[clap(long)]
  44. /// do not add ID annotations to transactions lacking them
  45. skip_ids: bool,
  46. },
  47. Import {
  48. account: String,
  49. from: std::path::PathBuf,
  50. target: Option<std::path::PathBuf>,
  51. },
  52. Infer,
  53. Match {
  54. account: Option<data::AccountName>,
  55. },
  56. Comb {
  57. account: Option<data::AccountName>,
  58. },
  59. }
  60. fn summarize(data: &data::Hoard) {
  61. let positive = console::Style::new().green();
  62. let negative = console::Style::new().red();
  63. let neutral = console::Style::new();
  64. let Some(maxlen) = data.account_names().map(|v| v.len()).max() else {
  65. return;
  66. };
  67. for shortname in data.account_names().sorted_by_key(|an| an.as_str()) {
  68. if shortname.as_ref() == "initial" {
  69. continue;
  70. }
  71. if let Some(mut balances) = data.balance(shortname) {
  72. // insert a default balance of 0 if no balance is present
  73. if let Some(unit) = data.account_spec(shortname).unwrap().unit {
  74. balances.entry(unit).or_default();
  75. }
  76. let balances = balances
  77. .into_iter()
  78. .filter(|(_, b)| !b.is_zero())
  79. .sorted()
  80. .map(|(u, b)| {
  81. (
  82. u,
  83. if b.is_sign_positive() {
  84. positive.apply_to(b).to_string()
  85. } else if b.is_sign_negative() {
  86. negative.apply_to(b).to_string()
  87. } else {
  88. neutral.apply_to("0").to_string()
  89. },
  90. )
  91. });
  92. println!(
  93. "{shortname:maxlen$} {}",
  94. balances.map(|(u, b)| format!("{b:8} {u:5}")).join(" ")
  95. );
  96. }
  97. }
  98. }
  99. impl Command {
  100. pub fn run(&self, inv: &Invocation) -> anyhow::Result<()> {
  101. let mut fsdata = io::FilesystemData::default();
  102. match self {
  103. Self::Summarize => {
  104. let data = load_data(&mut fsdata, inv, Default::default())?;
  105. summarize(&data);
  106. }
  107. Self::Ledger { account, raw } => {
  108. let data = load_data(&mut fsdata, inv, check::CheckLevel::WellFormed)?;
  109. let aname = data::AccountName::new(account.as_str());
  110. if let Some(ld) = data.ledger_data_for(aname) {
  111. if *raw {
  112. for txn in ld {
  113. show::show_transaction(Some(&data), &txn.borrow());
  114. }
  115. } else {
  116. let tt = show::TransactionTable::default();
  117. tt.show_refs(Some(&data), aname, ld.iter());
  118. }
  119. } else {
  120. log::error!("account not found!");
  121. }
  122. }
  123. Self::Reformat { skip_ids } => {
  124. let mut data = load_data(&mut fsdata, inv, check::CheckLevel::WellFormed)?;
  125. if !skip_ids {
  126. let mut used_ids = HashSet::new();
  127. for le in data.all_raw_ledger_data() {
  128. let Some(txn) = le.as_transaction() else {
  129. continue;
  130. };
  131. let Some(id) = txn.get_annotation("id") else {
  132. continue;
  133. };
  134. used_ids.insert(id.to_owned());
  135. }
  136. // assign IDs if to transactions lacking them
  137. for le in data.all_raw_ledger_data_mut() {
  138. let Some(txn) = le.as_transaction_mut() else {
  139. continue;
  140. };
  141. if txn.get_annotation("id").is_none() {
  142. // generated unique ID
  143. let mut id = nanoid::nanoid!(10);
  144. while used_ids.contains(&id) {
  145. id = nanoid::nanoid!(10);
  146. }
  147. // assign ID
  148. txn.annotations.insert("id".to_string(), id);
  149. }
  150. }
  151. }
  152. data::format_ledger(&mut fsdata, &data, data.all_raw_ledger_data().iter())?;
  153. }
  154. Self::Import {
  155. account,
  156. from,
  157. target,
  158. } => {
  159. let data = load_data(&mut fsdata, inv, Default::default())?;
  160. let aname = account.into();
  161. let Some(aspec) = data.account_spec(aname) else {
  162. log::error!("Account {aname} does not exist!");
  163. return Ok(());
  164. };
  165. let default_destination = data.spec_root().placeholder_account;
  166. let imported =
  167. import::import_from(aspec, aname, default_destination, from.as_path()).unwrap();
  168. log::info!("Imported {} transaction(s)", imported.len());
  169. if let Some(target) = target {
  170. let new_source = std::fs::canonicalize(target.as_os_str())?
  171. .into_os_string()
  172. .into();
  173. let source_span = io::Span::null_for_file(new_source);
  174. let new_data = imported
  175. .into_iter()
  176. .map(|mut txn| {
  177. txn.source = Some(source_span);
  178. data::RawLedgerEntry::Transaction(txn)
  179. })
  180. .collect_vec();
  181. data::format_ledger(
  182. &mut fsdata,
  183. &data,
  184. data.raw_ledger_data_from(new_source).chain(new_data.iter()),
  185. )?;
  186. } else {
  187. log::info!("No target specified, showing new data on stdout.");
  188. let tt = show::TransactionTable::default();
  189. tt.show(Some(&data), aname, imported.iter());
  190. }
  191. }
  192. Self::Infer => {
  193. let mut data = load_data(&mut fsdata, inv, check::CheckLevel::WellFormed)?;
  194. if infer::do_inference(&mut data)? {
  195. data::format_entire_ledger(&mut fsdata, &data)?;
  196. }
  197. }
  198. Self::Match { account } => {
  199. let mut data = load_data(&mut fsdata, inv, check::CheckLevel::WellFormed)?;
  200. let account = account.unwrap_or(data.spec_root().placeholder_account);
  201. if txnmatch::do_match(&mut data, account)? {
  202. data::format_entire_ledger(&mut fsdata, &data)?;
  203. }
  204. }
  205. Self::Comb { account } => {
  206. let data = load_data(&mut fsdata, inv, check::CheckLevel::Consistent)?;
  207. let account = account.unwrap_or(data.spec_root().placeholder_account);
  208. if comb::do_comb(&data, account)? {
  209. data::format_entire_ledger(&mut fsdata, &data)?;
  210. }
  211. }
  212. }
  213. Ok(())
  214. }
  215. }