#![allow(clippy::items_after_statements)]
mod accounts;
mod balances;
mod balsheet;
mod commodities;
mod holdings;
mod income;
mod journal;
mod networth;
mod prices;
mod stats;
use crate::cmd::completions::ShellType;
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use rustledger_core::NaiveDate;
use rustledger_loader::{LoadOptions, load};
use std::io;
use std::path::PathBuf;
#[derive(Parser, Debug)]
#[command(name = "report")]
#[command(author, version, about, long_about = None)]
pub struct Args {
#[arg(long, value_name = "SHELL", hide = true)]
generate_completions: Option<ShellType>,
#[arg(value_name = "FILE")]
pub file: Option<PathBuf>,
#[command(subcommand)]
pub report: Option<Report>,
#[arg(short, long, global = true)]
pub verbose: bool,
#[arg(short = 'f', long, global = true)]
pub format: Option<OutputFormat>,
#[arg(long, global = true)]
pub no_pager: bool,
}
#[derive(Clone, Debug, Default, clap::ValueEnum)]
pub enum OutputFormat {
#[default]
Text,
Csv,
Json,
}
impl OutputFormat {
#[must_use]
pub fn from_str_config(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"text" => Some(Self::Text),
"csv" => Some(Self::Csv),
"json" => Some(Self::Json),
_ => None,
}
}
}
#[derive(Subcommand, Debug)]
pub enum Report {
Balances {
#[arg(short, long)]
account: Option<String>,
},
#[command(alias = "bal")]
Balsheet,
#[command(alias = "is")]
Income,
#[command(alias = "register")]
Journal {
#[arg(short, long)]
account: Option<String>,
#[arg(short, long)]
limit: Option<usize>,
},
Holdings {
#[arg(short, long)]
account: Option<String>,
},
Networth {
#[arg(short, long, default_value = "monthly")]
period: String,
#[arg(short, long)]
currency: Option<String>,
#[arg(short, long)]
account: Option<String>,
#[arg(long)]
no_zero: bool,
},
Accounts,
Commodities,
Stats,
Prices {
#[arg(short, long)]
commodity: Option<String>,
},
}
pub fn run(
file: &PathBuf,
report: &Report,
verbose: bool,
format: &OutputFormat,
no_pager: bool,
) -> Result<()> {
if !file.exists() {
anyhow::bail!("file not found: {}", file.display());
}
if verbose {
eprintln!("Loading {}...", file.display());
}
let options = LoadOptions {
validate: false, ..Default::default()
};
let ledger =
load(file, &options).with_context(|| format!("failed to load {}", file.display()))?;
for err in &ledger.errors {
eprintln!("{}: {}", err.code, err.message);
}
let directives: Vec<_> = ledger.directives.into_iter().map(|s| s.value).collect();
let use_pager = !no_pager && matches!(format, OutputFormat::Text);
let pager_cmd = if use_pager {
crate::config::Config::load()
.ok()
.and_then(|l| l.config.output.pager)
} else {
None
};
let mut writer = if use_pager {
crate::pager::create_pager(pager_cmd.as_deref())
} else {
crate::pager::PagerWriter::Stdout(io::stdout().lock())
};
match report {
Report::Balances { account } => {
balances::report_balances(&directives, account.as_deref(), format, &mut writer)?;
}
Report::Balsheet => {
balsheet::report_balsheet(&directives, format, &mut writer)?;
}
Report::Income => {
income::report_income(&directives, format, &mut writer)?;
}
Report::Journal { account, limit } => {
journal::report_journal(&directives, account.as_deref(), *limit, format, &mut writer)?;
}
Report::Holdings { account } => {
holdings::report_holdings(&directives, account.as_deref(), format, &mut writer)?;
}
Report::Networth {
period,
currency,
account,
no_zero,
} => {
networth::report_networth(
&directives,
period,
currency.as_deref(),
account.as_deref(),
*no_zero,
format,
&mut writer,
)?;
}
Report::Accounts => {
accounts::report_accounts(&directives, format, &mut writer)?;
}
Report::Commodities => {
commodities::report_commodities(&directives, format, &mut writer)?;
}
Report::Stats => {
stats::report_stats(&directives, file, &mut writer)?;
}
Report::Prices { commodity } => {
prices::report_prices(&directives, commodity.as_deref(), format, &mut writer)?;
}
}
writer.finish();
Ok(())
}
pub(super) fn csv_escape(s: &str) -> String {
if s.contains(',') || s.contains('"') || s.contains('\n') {
format!("\"{}\"", s.replace('"', "\"\""))
} else {
s.to_string()
}
}
pub(super) fn json_escape(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
}
#[derive(Default)]
pub(super) struct LedgerStats {
pub transactions: usize,
pub postings: usize,
pub accounts: usize,
pub commodities: usize,
pub balance_assertions: usize,
pub prices: usize,
pub pads: usize,
pub events: usize,
pub notes: usize,
pub documents: usize,
pub queries: usize,
pub custom: usize,
pub first_date: Option<NaiveDate>,
pub last_date: Option<NaiveDate>,
}