add cli
This commit is contained in:
parent
c163d4f8d3
commit
d7bc50c267
15
Cargo.toml
15
Cargo.toml
@ -1,22 +1,31 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "accounting-rust"
|
name = "accounting-rust"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
chrono = "0.4.39"
|
chrono = "0.4.39"
|
||||||
|
clap = { version = "4.5.53", features = ["derive"] }
|
||||||
|
crossterm = "0.29.0"
|
||||||
|
csv = "1.4.0"
|
||||||
|
inquire = "0.9.1"
|
||||||
nom = "7.1.3"
|
nom = "7.1.3"
|
||||||
nom_locate = "4.2.0"
|
nom_locate = "4.2.0"
|
||||||
|
nucleo-matcher = "0.3.1"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
ratatui = "0.29.0"
|
ratatui = "0.29.0"
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
|
rt-format = "0.3.1"
|
||||||
rust_decimal = "1.36.0"
|
rust_decimal = "1.36.0"
|
||||||
rust_decimal_macros = "1.36.0"
|
rust_decimal_macros = "1.36.0"
|
||||||
|
serde = { version="1.0", features = ["derive"] }
|
||||||
|
strsim = "0.11.1"
|
||||||
|
toml = "0.9.10"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
debug = 1
|
debug = 1
|
||||||
|
|
||||||
[rust]
|
# [rust]
|
||||||
debuginfo-level = 1
|
# debuginfo-level = 1
|
||||||
29
src/cli/balance.rs
Normal file
29
src/cli/balance.rs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
use super::commands::Balance;
|
||||||
|
use accounting_rust::{core::Ledger, output::cli::format_balance, parser, query::{self, PostingField}};
|
||||||
|
use chrono::Utc;
|
||||||
|
|
||||||
|
pub fn balance(ledger: &Ledger, b: Balance) {
|
||||||
|
|
||||||
|
let balance_query = if b.accounts.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let balance_query = b
|
||||||
|
.accounts
|
||||||
|
.iter()
|
||||||
|
.map(|a| format!("account.name ~ '{}'", a))
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(" OR ");
|
||||||
|
|
||||||
|
let parsed_query = parser::query::<PostingField>(&balance_query).unwrap();
|
||||||
|
if parsed_query.0.trim().len() != 0 {
|
||||||
|
panic!("Full string not consumed")
|
||||||
|
}
|
||||||
|
Some(parsed_query.1)
|
||||||
|
};
|
||||||
|
|
||||||
|
let current_date = Utc::now().date_naive();
|
||||||
|
|
||||||
|
let balance = query::balance(&ledger, balance_query, Some(("$", current_date)));
|
||||||
|
|
||||||
|
format_balance(&ledger, &balance);
|
||||||
|
}
|
||||||
35
src/cli/commands.rs
Normal file
35
src/cli/commands.rs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
use clap::{Args, Parser, Subcommand};
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(version, about, long_about = None)]
|
||||||
|
pub struct Cli {
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub check: bool,
|
||||||
|
|
||||||
|
#[command(subcommand)]
|
||||||
|
pub command: Commands,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
pub struct Balance {
|
||||||
|
/// Accounts to query
|
||||||
|
pub accounts: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
pub struct Import {
|
||||||
|
/// Import config name
|
||||||
|
pub config: String,
|
||||||
|
/// File path to import
|
||||||
|
pub file_path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand, Debug)]
|
||||||
|
pub enum Commands {
|
||||||
|
/// Shows balance of accounts
|
||||||
|
Balance(Balance),
|
||||||
|
/// Check transactions and balances
|
||||||
|
Check,
|
||||||
|
/// Import CSV
|
||||||
|
Import(Import),
|
||||||
|
}
|
||||||
62
src/cli/config.rs
Normal file
62
src/cli/config.rs
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
use accounting_rust::core::CoreError;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::{collections::HashMap, path::Path};
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct Config {
|
||||||
|
pub ledger: LedgerConfig,
|
||||||
|
pub importers: HashMap<String, ImporterConfig>,
|
||||||
|
pub variables: Option<HashMap<String, String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct LedgerConfig {
|
||||||
|
pub path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct ImporterConfig {
|
||||||
|
pub template: String,
|
||||||
|
pub column_date: Option<u32>,
|
||||||
|
pub column_description: Option<u32>,
|
||||||
|
pub column_amount: Option<i32>,
|
||||||
|
pub column_unit: Option<u32>,
|
||||||
|
pub column_price_total: Option<i32>,
|
||||||
|
pub first_row: Option<u32>,
|
||||||
|
pub reverse_order: Option<bool>,
|
||||||
|
pub unit: Option<String>,
|
||||||
|
pub date_format: Option<String>,
|
||||||
|
pub mapping_file: Option<String>,
|
||||||
|
pub output_file: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_config(file_path: &Path) -> Result<Config, CoreError> {
|
||||||
|
let config_file_data =
|
||||||
|
std::fs::read_to_string(file_path).map_err(|e| CoreError::from(e.to_string()))?;
|
||||||
|
let mut config: Result<Config, CoreError> =
|
||||||
|
toml::from_str(&config_file_data).map_err(|e| e.to_string().into());
|
||||||
|
|
||||||
|
if let Ok(config) = &mut config
|
||||||
|
&& let Some(variables) = &config.variables
|
||||||
|
{
|
||||||
|
println!("{variables:?}");
|
||||||
|
for variable in variables {
|
||||||
|
config.ledger.path = config
|
||||||
|
.ledger
|
||||||
|
.path
|
||||||
|
.replace(&format!("{{{}}}", variable.0), &variable.1);
|
||||||
|
for importer in &mut config.importers {
|
||||||
|
importer.1.mapping_file = importer
|
||||||
|
.1
|
||||||
|
.mapping_file
|
||||||
|
.as_ref()
|
||||||
|
.map(|v| v.replace(&format!("{{{}}}", variable.0), &variable.1));
|
||||||
|
importer.1.output_file = importer
|
||||||
|
.1
|
||||||
|
.output_file
|
||||||
|
.replace(&format!("{{{}}}", variable.0), &variable.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
config
|
||||||
|
}
|
||||||
98
src/cli/fmt.rs
Normal file
98
src/cli/fmt.rs
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
use core::fmt;
|
||||||
|
|
||||||
|
use chrono::NaiveDate;
|
||||||
|
use rt_format::{Format, FormatArgument, Specifier};
|
||||||
|
use rust_decimal::Decimal;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub enum Variant {
|
||||||
|
Int(i32),
|
||||||
|
Float(f64),
|
||||||
|
String(String),
|
||||||
|
Decimal(Decimal),
|
||||||
|
Date(NaiveDate),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FormatArgument for Variant {
|
||||||
|
fn supports_format(&self, spec: &Specifier) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Int(_) => true,
|
||||||
|
Self::Float(_) | Self::Decimal(_) => match spec.format {
|
||||||
|
Format::Display | Format::Debug | Format::LowerExp | Format::UpperExp => true,
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
Self::String(_) | Self::Date(_) => match spec.format {
|
||||||
|
Format::Display | Format::Debug => true,
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fmt_display(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Int(val) => fmt::Display::fmt(&val, f),
|
||||||
|
Self::Float(val) => fmt::Display::fmt(&val, f),
|
||||||
|
Self::String(val) => fmt::Display::fmt(&val, f),
|
||||||
|
Self::Decimal(val) => fmt::Display::fmt(&val, f),
|
||||||
|
Self::Date(val) => fmt::Display::fmt(&val, f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fmt_debug(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
fmt::Debug::fmt(self, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fmt_octal(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Int(val) => fmt::Octal::fmt(&val, f),
|
||||||
|
_ => Err(fmt::Error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fmt_lower_hex(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Int(val) => fmt::LowerHex::fmt(&val, f),
|
||||||
|
_ => Err(fmt::Error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fmt_upper_hex(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Int(val) => fmt::UpperHex::fmt(&val, f),
|
||||||
|
_ => Err(fmt::Error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fmt_binary(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Int(val) => fmt::Binary::fmt(&val, f),
|
||||||
|
_ => Err(fmt::Error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fmt_lower_exp(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Int(val) => fmt::LowerExp::fmt(&val, f),
|
||||||
|
Self::Float(val) => fmt::LowerExp::fmt(&val, f),
|
||||||
|
Self::Decimal(val) => fmt::LowerExp::fmt(&val, f),
|
||||||
|
_ => Err(fmt::Error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fmt_upper_exp(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Int(val) => fmt::UpperExp::fmt(&val, f),
|
||||||
|
Self::Float(val) => fmt::UpperExp::fmt(&val, f),
|
||||||
|
Self::Decimal(val) => fmt::UpperExp::fmt(&val, f),
|
||||||
|
_ => Err(fmt::Error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_usize(&self) -> Result<usize, ()> {
|
||||||
|
match self {
|
||||||
|
Variant::Int(val) => (*val).try_into().map_err(|_| ()),
|
||||||
|
_ => Err(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
620
src/cli/importer.rs
Normal file
620
src/cli/importer.rs
Normal file
@ -0,0 +1,620 @@
|
|||||||
|
use std::{
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
fmt::format,
|
||||||
|
fs::{File, OpenOptions},
|
||||||
|
io::{Write, stdout},
|
||||||
|
path::Path,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::cli::{config::ImporterConfig, fmt::Variant};
|
||||||
|
|
||||||
|
use super::{commands::Import, config::Config};
|
||||||
|
use accounting_rust::{
|
||||||
|
core::{Amount, CoreError, Ledger},
|
||||||
|
parser::decimal,
|
||||||
|
};
|
||||||
|
use chrono::NaiveDate;
|
||||||
|
use crossterm::{ExecutableCommand, cursor::MoveToPreviousLine, terminal};
|
||||||
|
use inquire::{
|
||||||
|
Autocomplete, Confirm, InquireError, Text, required, validator::{ErrorMessage, Validation}
|
||||||
|
};
|
||||||
|
use rt_format::ParsedFormat;
|
||||||
|
use rust_decimal::Decimal;
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
struct Mapping {
|
||||||
|
original_description: String,
|
||||||
|
account: Option<String>,
|
||||||
|
payee: Option<String>,
|
||||||
|
narration: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Accounts {
|
||||||
|
accounts: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Payees {
|
||||||
|
payees: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Narrations {
|
||||||
|
narrations: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn import(ledger: &Ledger, config: &Config, args: Import) -> Result<(), CoreError> {
|
||||||
|
let import_config = config
|
||||||
|
.importers
|
||||||
|
.get(&args.config)
|
||||||
|
.ok_or(CoreError::from("Config not found"))?;
|
||||||
|
|
||||||
|
let mut mappings = get_mappings(&import_config);
|
||||||
|
|
||||||
|
let file_to_read = Path::new(&args.file_path);
|
||||||
|
let mut reader = csv::ReaderBuilder::new()
|
||||||
|
.flexible(true)
|
||||||
|
.has_headers(false)
|
||||||
|
.from_path(file_to_read)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let output_path = Path::new(&import_config.output_file);
|
||||||
|
let mut file: File = File::options()
|
||||||
|
.append(true)
|
||||||
|
.create(true)
|
||||||
|
.open(output_path)
|
||||||
|
.unwrap();
|
||||||
|
writeln!(&mut file, "\n").unwrap();
|
||||||
|
|
||||||
|
// Needed to add bottom space for prompts
|
||||||
|
// Extra line needed because enter key after prompt
|
||||||
|
print!("\n\n\n\n");
|
||||||
|
clear_lines(4);
|
||||||
|
|
||||||
|
let mut records: Vec<Result<csv::StringRecord, csv::Error>> = reader
|
||||||
|
.records()
|
||||||
|
.skip((import_config.first_row.unwrap_or(1) - 1) as usize)
|
||||||
|
.collect();
|
||||||
|
if import_config.reverse_order.unwrap_or(false) {
|
||||||
|
records.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
for record in records {
|
||||||
|
let transaction = parse_record(
|
||||||
|
record.map_err(|e| CoreError::from(e.to_string()))?,
|
||||||
|
ledger,
|
||||||
|
import_config,
|
||||||
|
&mut mappings,
|
||||||
|
);
|
||||||
|
if let Some(transaction) = transaction {
|
||||||
|
let mut file: File = File::options()
|
||||||
|
.append(true)
|
||||||
|
.create(true)
|
||||||
|
.open(output_path)
|
||||||
|
.unwrap();
|
||||||
|
write!(&mut file, "{}", transaction).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_lines(count: u16) {
|
||||||
|
let mut stdout = stdout();
|
||||||
|
stdout.execute(MoveToPreviousLine(count)).unwrap();
|
||||||
|
stdout
|
||||||
|
.execute(terminal::Clear(terminal::ClearType::FromCursorDown))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_record(
|
||||||
|
record: csv::StringRecord,
|
||||||
|
ledger: &Ledger,
|
||||||
|
import_config: &ImporterConfig,
|
||||||
|
mappings: &mut Vec<Mapping>,
|
||||||
|
) -> Option<String> {
|
||||||
|
let mut lines_to_clear = 0;
|
||||||
|
|
||||||
|
let date = import_config
|
||||||
|
.column_date
|
||||||
|
.and_then(|col| record.get((col - 1) as usize))
|
||||||
|
.filter(|val| val.trim().len() != 0);
|
||||||
|
let description = import_config
|
||||||
|
.column_description
|
||||||
|
.and_then(|col| record.get((col - 1) as usize))
|
||||||
|
.unwrap_or("");
|
||||||
|
let amount = import_config
|
||||||
|
.column_amount
|
||||||
|
.and_then(|col| record.get((col.abs() - 1) as usize))
|
||||||
|
.filter(|val| val.trim().len() != 0);
|
||||||
|
let mut unit_symbol = import_config
|
||||||
|
.column_unit
|
||||||
|
.and_then(|col| record.get((col - 1) as usize).map(|c| c.to_string()))
|
||||||
|
.filter(|val| val.trim().len() != 0);
|
||||||
|
let mut price_total = import_config
|
||||||
|
.column_price_total
|
||||||
|
.and_then(|col| record.get((col.abs() - 1) as usize))
|
||||||
|
.filter(|val| val.trim().len() != 0)
|
||||||
|
.and_then(|val| {
|
||||||
|
decimal(val).ok().map(|val| {
|
||||||
|
if let Some(column_price_total) = import_config.column_price_total
|
||||||
|
&& column_price_total < 0
|
||||||
|
{
|
||||||
|
val.1 * Decimal::NEGATIVE_ONE
|
||||||
|
} else {
|
||||||
|
val.1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let top_match = find_top_mapping(&description, mappings);
|
||||||
|
let match_to_use = top_match.filter(|m| m.1 >= 0.8).map(|m| m.0);
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"{} {:?} {}{}{}",
|
||||||
|
date.unwrap_or(""),
|
||||||
|
description,
|
||||||
|
amount.unwrap_or(""),
|
||||||
|
unit_symbol
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|s| Some(format!(" ({s})")))
|
||||||
|
.unwrap_or("".to_string()),
|
||||||
|
price_total
|
||||||
|
.and_then(|t| Some(format!(" @@ {t}")))
|
||||||
|
.unwrap_or("".to_string()),
|
||||||
|
);
|
||||||
|
lines_to_clear += 1;
|
||||||
|
|
||||||
|
let Some(date) = date else {
|
||||||
|
Confirm::new("Skip row without date?")
|
||||||
|
.with_default(true)
|
||||||
|
.prompt()
|
||||||
|
.unwrap();
|
||||||
|
lines_to_clear += 1;
|
||||||
|
clear_lines(lines_to_clear);
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let Ok(date) = NaiveDate::parse_from_str(
|
||||||
|
date,
|
||||||
|
import_config
|
||||||
|
.date_format
|
||||||
|
.as_ref()
|
||||||
|
.map(|v| v.as_str())
|
||||||
|
.unwrap_or("%Y-%m-%d"),
|
||||||
|
) else {
|
||||||
|
Confirm::new("Skip row with invalid date?")
|
||||||
|
.with_default(true)
|
||||||
|
.prompt()
|
||||||
|
.unwrap();
|
||||||
|
lines_to_clear += 1;
|
||||||
|
clear_lines(lines_to_clear);
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(amount) = amount else {
|
||||||
|
Confirm::new("Skip row without amount?")
|
||||||
|
.with_default(true)
|
||||||
|
.prompt()
|
||||||
|
.unwrap();
|
||||||
|
lines_to_clear += 1;
|
||||||
|
clear_lines(lines_to_clear);
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let Ok(amount) = decimal(amount) else {
|
||||||
|
Confirm::new("Skip with invalid amount?")
|
||||||
|
.with_default(true)
|
||||||
|
.prompt()
|
||||||
|
.unwrap();
|
||||||
|
lines_to_clear += 1;
|
||||||
|
clear_lines(lines_to_clear);
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let mut amount = amount.1;
|
||||||
|
if let Some(column_amount) = import_config.column_amount
|
||||||
|
&& column_amount < 0
|
||||||
|
{
|
||||||
|
amount *= Decimal::NEGATIVE_ONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(price_total_unwrap) = price_total {
|
||||||
|
if amount.is_zero() {
|
||||||
|
amount = price_total_unwrap;
|
||||||
|
unit_symbol = import_config.unit.clone();
|
||||||
|
price_total = None;
|
||||||
|
} else {
|
||||||
|
price_total = Some(price_total_unwrap.abs());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(match_to_use) = match_to_use {
|
||||||
|
if let Some(match_account) = &match_to_use.account {
|
||||||
|
let skip = loop {
|
||||||
|
let skip = Confirm::new(&format!("Enter {:.0}% match?", top_match.unwrap().1 * 100.))
|
||||||
|
.with_help_message(&format!(
|
||||||
|
"Account: {}\n Payee: {}\n Narration: {} ",
|
||||||
|
match_account,
|
||||||
|
match_to_use.payee.as_ref().unwrap_or(&"".to_string()),
|
||||||
|
match_to_use.narration.as_ref().unwrap_or(&"".to_string())
|
||||||
|
))
|
||||||
|
.with_default(true)
|
||||||
|
.prompt();
|
||||||
|
match skip {
|
||||||
|
Ok(s) => break s,
|
||||||
|
Err(InquireError::OperationCanceled) => {},
|
||||||
|
_ => panic!()
|
||||||
|
}
|
||||||
|
clear_lines(1);
|
||||||
|
};
|
||||||
|
// .unwrap_or(true);
|
||||||
|
lines_to_clear += 1;
|
||||||
|
if skip {
|
||||||
|
clear_lines(lines_to_clear);
|
||||||
|
return Some(
|
||||||
|
format_transaction(
|
||||||
|
ledger,
|
||||||
|
&import_config,
|
||||||
|
date,
|
||||||
|
amount,
|
||||||
|
unit_symbol,
|
||||||
|
price_total,
|
||||||
|
match_account.clone(),
|
||||||
|
match_to_use.payee.clone(),
|
||||||
|
match_to_use.narration.clone(),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let skip = loop {
|
||||||
|
let skip = Confirm::new(&format!("Skip {:.1}% match?", top_match.unwrap().1 * 100.))
|
||||||
|
.with_default(true)
|
||||||
|
.prompt();
|
||||||
|
match skip {
|
||||||
|
Ok(s) => break s,
|
||||||
|
Err(InquireError::OperationCanceled) => {},
|
||||||
|
_ => panic!()
|
||||||
|
}
|
||||||
|
clear_lines(1);
|
||||||
|
};
|
||||||
|
lines_to_clear += 1;
|
||||||
|
if skip {
|
||||||
|
clear_lines(lines_to_clear);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut account = Text::new("Account?");
|
||||||
|
if let Some(match_to_use) = match_to_use
|
||||||
|
&& let Some(match_account) = &match_to_use.account
|
||||||
|
{
|
||||||
|
account = account.with_default(&match_account);
|
||||||
|
}
|
||||||
|
let account = account
|
||||||
|
.with_autocomplete(Accounts::new(ledger))
|
||||||
|
.with_validator(|v: &str| {
|
||||||
|
if v.trim().len() == 0 {
|
||||||
|
Ok(Validation::Invalid(ErrorMessage::Custom(
|
||||||
|
"Account cannot be empty, press escape to skip row".into(),
|
||||||
|
)))
|
||||||
|
} else if v.contains(' ') {
|
||||||
|
Ok(Validation::Invalid(ErrorMessage::Custom(
|
||||||
|
"Account cannot contain spaces".into(),
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
Ok(Validation::Valid)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.prompt_skippable()
|
||||||
|
.unwrap();
|
||||||
|
lines_to_clear += 1;
|
||||||
|
|
||||||
|
let Some(account) = account else {
|
||||||
|
clear_lines(lines_to_clear);
|
||||||
|
|
||||||
|
if let Some(top_match) = top_match
|
||||||
|
&& top_match.1 == 1.
|
||||||
|
{
|
||||||
|
} else {
|
||||||
|
add_mapping(
|
||||||
|
Mapping {
|
||||||
|
original_description: description.to_string(),
|
||||||
|
account: None,
|
||||||
|
payee: None,
|
||||||
|
narration: None,
|
||||||
|
},
|
||||||
|
mappings,
|
||||||
|
import_config,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut payee = Text::new("Payee?");
|
||||||
|
if let Some(match_to_use) = match_to_use
|
||||||
|
&& let Some(match_payee) = &match_to_use.payee
|
||||||
|
{
|
||||||
|
payee = payee.with_default(&match_payee);
|
||||||
|
}
|
||||||
|
let payee = payee
|
||||||
|
.with_autocomplete(Payees::new(ledger))
|
||||||
|
.with_validator(|p: &str| {
|
||||||
|
if p.contains('|') {
|
||||||
|
Ok(Validation::Invalid(ErrorMessage::Custom(
|
||||||
|
"Payee cannot contain '|'".into(),
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
Ok(Validation::Valid)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.prompt_skippable()
|
||||||
|
.unwrap();
|
||||||
|
lines_to_clear += 1;
|
||||||
|
let payee = payee.filter(|v| v.trim().len() != 0);
|
||||||
|
|
||||||
|
let narration = Text::new("Narration?")
|
||||||
|
.with_autocomplete(Narrations::new(ledger))
|
||||||
|
.with_validator(|p: &str| {
|
||||||
|
if p.contains('|') {
|
||||||
|
Ok(Validation::Invalid(ErrorMessage::Custom(
|
||||||
|
"Payee cannot contain '|'".into(),
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
Ok(Validation::Valid)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.prompt_skippable()
|
||||||
|
.unwrap();
|
||||||
|
lines_to_clear += 1;
|
||||||
|
let narration = narration.filter(|v| v.trim().len() != 0);
|
||||||
|
|
||||||
|
clear_lines(lines_to_clear);
|
||||||
|
|
||||||
|
if let Some(top_match) = top_match
|
||||||
|
&& top_match.1 == 1.
|
||||||
|
{
|
||||||
|
} else {
|
||||||
|
add_mapping(
|
||||||
|
Mapping {
|
||||||
|
original_description: description.to_string(),
|
||||||
|
account: Some(account.clone()),
|
||||||
|
payee: payee.clone(),
|
||||||
|
narration: narration.clone(),
|
||||||
|
},
|
||||||
|
mappings,
|
||||||
|
import_config,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(
|
||||||
|
format_transaction(
|
||||||
|
ledger,
|
||||||
|
&import_config,
|
||||||
|
date,
|
||||||
|
amount,
|
||||||
|
unit_symbol,
|
||||||
|
price_total,
|
||||||
|
account,
|
||||||
|
payee,
|
||||||
|
narration,
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_transaction(
|
||||||
|
ledger: &Ledger,
|
||||||
|
import_config: &ImporterConfig,
|
||||||
|
date: NaiveDate,
|
||||||
|
amount: Decimal,
|
||||||
|
unit_symbol: Option<String>,
|
||||||
|
price_total: Option<Decimal>,
|
||||||
|
account: String,
|
||||||
|
payee: Option<String>,
|
||||||
|
narration: Option<String>,
|
||||||
|
) -> Result<String, ()> {
|
||||||
|
let unit_symbol = unit_symbol.or(import_config.unit.clone()).ok_or(())?;
|
||||||
|
|
||||||
|
let unit = ledger.get_unit_by_symbol(&unit_symbol);
|
||||||
|
let amount = if let Some(unit) = unit {
|
||||||
|
// TODO: pad to decimal point
|
||||||
|
ledger
|
||||||
|
.format_amount(&Amount { unit_id: unit.get_id(), value: amount })
|
||||||
|
.unwrap()
|
||||||
|
.0
|
||||||
|
} else {
|
||||||
|
format!("{} {}", amount, unit_symbol)
|
||||||
|
};
|
||||||
|
let price_total = if let Some(price_total) = price_total {
|
||||||
|
let unit = ledger
|
||||||
|
.get_unit_by_symbol(import_config.unit.as_ref().unwrap())
|
||||||
|
.unwrap();
|
||||||
|
let price_total_str = ledger
|
||||||
|
.format_amount(&Amount { unit_id: unit.get_id(), value: price_total })
|
||||||
|
.unwrap()
|
||||||
|
.0;
|
||||||
|
format!("@@ {}", price_total_str)
|
||||||
|
} else {
|
||||||
|
"".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let args = HashMap::from([
|
||||||
|
("date", Variant::Date(date)),
|
||||||
|
("amount", Variant::String(amount)),
|
||||||
|
("price_total", Variant::String(price_total)),
|
||||||
|
("account", Variant::String(account)),
|
||||||
|
("payee", Variant::String(payee.unwrap_or("".into()))),
|
||||||
|
(
|
||||||
|
"narration",
|
||||||
|
Variant::String(narration.map(|v| format!("| {v}")).unwrap_or("".into())),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Ok(ParsedFormat::parse(&import_config.template, &[], &args)
|
||||||
|
.or(Err(()))?
|
||||||
|
.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_mappings(import_config: &ImporterConfig) -> Vec<Mapping> {
|
||||||
|
let file_to_read = Path::new(import_config.mapping_file.as_ref().unwrap());
|
||||||
|
let mut mappings_reader = csv::ReaderBuilder::new()
|
||||||
|
.flexible(true)
|
||||||
|
.has_headers(false)
|
||||||
|
.from_path(file_to_read)
|
||||||
|
.unwrap();
|
||||||
|
let mut mappings: Vec<_> = mappings_reader
|
||||||
|
.deserialize::<Mapping>()
|
||||||
|
.map(|v| v.unwrap())
|
||||||
|
.collect();
|
||||||
|
// Reverse to check newest first
|
||||||
|
mappings.reverse();
|
||||||
|
mappings
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_mapping(new_mapping: Mapping, mappings: &mut Vec<Mapping>, import_config: &ImporterConfig) {
|
||||||
|
let file_to_append = OpenOptions::new()
|
||||||
|
.append(true)
|
||||||
|
.create(true)
|
||||||
|
.open(Path::new(import_config.mapping_file.as_ref().unwrap()))
|
||||||
|
.unwrap();
|
||||||
|
let mut mappings_writer: csv::Writer<File> = csv::WriterBuilder::new()
|
||||||
|
.has_headers(false)
|
||||||
|
.from_writer(file_to_append);
|
||||||
|
mappings_writer.serialize(&new_mapping).unwrap();
|
||||||
|
// mappings_writer.write();
|
||||||
|
mappings.push(new_mapping);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_top_mapping<'a>(
|
||||||
|
description: &str,
|
||||||
|
mappings: &'a Vec<Mapping>,
|
||||||
|
) -> Option<(&'a Mapping, f64)> {
|
||||||
|
let description = description.to_lowercase();
|
||||||
|
let mut best_mapping: Option<(&'a Mapping, f64)> = None;
|
||||||
|
for mapping in mappings {
|
||||||
|
let score =
|
||||||
|
strsim::jaro_winkler(&description, &mapping.original_description.to_lowercase());
|
||||||
|
if let Some(best_mapping) = best_mapping
|
||||||
|
&& best_mapping.1 >= score
|
||||||
|
{
|
||||||
|
} else {
|
||||||
|
best_mapping = Some((mapping, score));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
best_mapping
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Accounts {
|
||||||
|
fn new(ledger: &Ledger) -> Self {
|
||||||
|
let mut accounts: Vec<_> = ledger
|
||||||
|
.get_accounts()
|
||||||
|
.iter()
|
||||||
|
.map(|a| a.get_name().clone())
|
||||||
|
.collect();
|
||||||
|
accounts.sort();
|
||||||
|
Self { accounts }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Autocomplete for Accounts {
|
||||||
|
fn get_suggestions(&mut self, input: &str) -> Result<Vec<String>, inquire::CustomUserError> {
|
||||||
|
let input = input.to_lowercase();
|
||||||
|
Ok(self
|
||||||
|
.accounts
|
||||||
|
.iter()
|
||||||
|
.filter(|a| a.to_lowercase().find(&input).is_some())
|
||||||
|
.map(|t| t.clone())
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_completion(
|
||||||
|
&mut self,
|
||||||
|
input: &str,
|
||||||
|
highlighted_suggestion: Option<String>,
|
||||||
|
) -> Result<inquire::autocompletion::Replacement, inquire::CustomUserError> {
|
||||||
|
let suggestions = self.get_suggestions(input);
|
||||||
|
if highlighted_suggestion.is_none() {
|
||||||
|
Ok(suggestions?.get(0).cloned())
|
||||||
|
} else {
|
||||||
|
Ok(highlighted_suggestion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Payees {
|
||||||
|
fn new(ledger: &Ledger) -> Self {
|
||||||
|
let payees: HashSet<_> = ledger
|
||||||
|
.get_transactions()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|t| t.get_payee().clone())
|
||||||
|
.collect();
|
||||||
|
let mut payees: Vec<_> = payees.into_iter().collect();
|
||||||
|
payees.sort();
|
||||||
|
Self { payees }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Autocomplete for Payees {
|
||||||
|
fn get_suggestions(&mut self, input: &str) -> Result<Vec<String>, inquire::CustomUserError> {
|
||||||
|
let input = input.to_lowercase();
|
||||||
|
Ok(self
|
||||||
|
.payees
|
||||||
|
.iter()
|
||||||
|
.filter(|t| t.to_lowercase().find(&input).is_some())
|
||||||
|
.map(|t| t.clone())
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_completion(
|
||||||
|
&mut self,
|
||||||
|
input: &str,
|
||||||
|
highlighted_suggestion: Option<String>,
|
||||||
|
) -> Result<inquire::autocompletion::Replacement, inquire::CustomUserError> {
|
||||||
|
let suggestions = self.get_suggestions(input);
|
||||||
|
if highlighted_suggestion.is_none() {
|
||||||
|
Ok(suggestions?.get(0).cloned())
|
||||||
|
} else {
|
||||||
|
Ok(highlighted_suggestion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Narrations {
|
||||||
|
fn new(ledger: &Ledger) -> Self {
|
||||||
|
let narrations: HashSet<_> = ledger
|
||||||
|
.get_transactions()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|t| t.get_narration().clone())
|
||||||
|
.collect();
|
||||||
|
let mut narrations: Vec<_> = narrations.into_iter().collect();
|
||||||
|
narrations.sort();
|
||||||
|
Self { narrations }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Autocomplete for Narrations {
|
||||||
|
fn get_suggestions(&mut self, input: &str) -> Result<Vec<String>, inquire::CustomUserError> {
|
||||||
|
let input = input.to_lowercase();
|
||||||
|
Ok(self
|
||||||
|
.narrations
|
||||||
|
.iter()
|
||||||
|
.filter(|t| t.to_lowercase().find(&input).is_some())
|
||||||
|
.map(|t| t.clone())
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_completion(
|
||||||
|
&mut self,
|
||||||
|
input: &str,
|
||||||
|
highlighted_suggestion: Option<String>,
|
||||||
|
) -> Result<inquire::autocompletion::Replacement, inquire::CustomUserError> {
|
||||||
|
let suggestions = self.get_suggestions(input);
|
||||||
|
if highlighted_suggestion.is_none() {
|
||||||
|
Ok(suggestions?.get(0).cloned())
|
||||||
|
} else {
|
||||||
|
Ok(highlighted_suggestion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/cli/mod.rs
Normal file
37
src/cli/mod.rs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
use crate::cli::{balance::balance, commands::Commands, importer::import};
|
||||||
|
use accounting_rust::{core::CoreError, document::Document};
|
||||||
|
|
||||||
|
mod balance;
|
||||||
|
mod commands;
|
||||||
|
pub mod config;
|
||||||
|
mod importer;
|
||||||
|
pub mod fmt;
|
||||||
|
|
||||||
|
pub fn run() -> Result<(), CoreError> {
|
||||||
|
let cli = commands::Cli::parse();
|
||||||
|
|
||||||
|
let config_file =
|
||||||
|
Path::new("/Users/petersonev/Documents/Projects/accounting/ledger-data/config.toml");
|
||||||
|
let config = config::read_config(config_file)?;
|
||||||
|
|
||||||
|
let document = Document::new(Path::new(&config.ledger.path))?;
|
||||||
|
let balance_check = cli.check || matches!(cli.command, Commands::Check);
|
||||||
|
let ledger = document.generate_ledger(balance_check)?;
|
||||||
|
|
||||||
|
match cli.command {
|
||||||
|
Commands::Balance(b) => balance(&ledger, b),
|
||||||
|
Commands::Check => {
|
||||||
|
println!("{} transactions", ledger.get_transactions().len());
|
||||||
|
println!("{} prices", ledger.get_prices().len());
|
||||||
|
println!("{} balances checked", document.get_balance_count());
|
||||||
|
}
|
||||||
|
Commands::Import(args) => {
|
||||||
|
import(&ledger, &config, args)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@ -77,7 +77,8 @@ impl Unit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_scale(&self) -> Option<u32> {
|
pub fn get_scale(&self) -> Option<u32> {
|
||||||
self.scale
|
// TODO: probably don't hard code 8 here
|
||||||
|
self.scale.or(Some(8))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_symbols(&self) -> &Vec<UnitSymbol> {
|
pub fn get_symbols(&self) -> &Vec<UnitSymbol> {
|
||||||
|
|||||||
@ -60,6 +60,10 @@ impl Ledger {
|
|||||||
&self.transactions
|
&self.transactions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_prices(&self) -> &Vec<Price> {
|
||||||
|
&self.prices
|
||||||
|
}
|
||||||
|
|
||||||
// Assume prices are sorted by date already
|
// Assume prices are sorted by date already
|
||||||
// For now only trivial conversions, not multiple conversions
|
// For now only trivial conversions, not multiple conversions
|
||||||
pub fn get_price_on_date(
|
pub fn get_price_on_date(
|
||||||
@ -166,7 +170,6 @@ impl Ledger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn sort_prices(&mut self) {
|
pub fn sort_prices(&mut self) {
|
||||||
println!("price length = {:?}", self.prices.len());
|
|
||||||
self.prices.sort_by(|a, b| a.date.cmp(&b.date));
|
self.prices.sort_by(|a, b| a.date.cmp(&b.date));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -98,7 +98,7 @@ impl Transaction {
|
|||||||
}
|
}
|
||||||
amounts
|
amounts
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(_, &value)| value != dec!(0))
|
.filter(|(_, value)| **value != dec!(0))
|
||||||
.map(|(&unit_id, &value)| Amount { value, unit_id })
|
.map(|(&unit_id, &value)| Amount { value, unit_id })
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -90,7 +90,7 @@ pub fn check_balance2(ledger: &Ledger, balance: &BalanceDirective) -> Result<(),
|
|||||||
|
|
||||||
let t2 = Instant::now();
|
let t2 = Instant::now();
|
||||||
|
|
||||||
let accounts = query::balance(&ledger, Some(&balance.query), None);
|
let accounts = query::balance(&ledger, Some(balance.query.clone()), None);
|
||||||
|
|
||||||
let t3 = Instant::now();
|
let t3 = Instant::now();
|
||||||
|
|
||||||
@ -233,6 +233,7 @@ pub fn check_balance2(ledger: &Ledger, balance: &BalanceDirective) -> Result<(),
|
|||||||
// Ok(())
|
// Ok(())
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
struct IncompletePosting {
|
struct IncompletePosting {
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
amount: Option<Amount>,
|
amount: Option<Amount>,
|
||||||
@ -300,7 +301,7 @@ fn complete_incomplete_postings(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(account_id) = incomplete_posting_account_id {
|
if let Some(account_id) = incomplete_posting_account_id {
|
||||||
for (&unit_id, &value) in amounts.iter().filter(|(_, &v)| v != dec!(0)) {
|
for (&unit_id, &value) in amounts.iter().filter(|(_, v)| **v != dec!(0)) {
|
||||||
let mut value = -value;
|
let mut value = -value;
|
||||||
if let Some(scale) = ledger.get_unit(unit_id).and_then(|u| u.get_scale()) {
|
if let Some(scale) = ledger.get_unit(unit_id).and_then(|u| u.get_scale()) {
|
||||||
value = value.round_dp(scale);
|
value = value.round_dp(scale);
|
||||||
|
|||||||
@ -4,7 +4,11 @@ mod ledger;
|
|||||||
pub use directives::*;
|
pub use directives::*;
|
||||||
use ledger::{add_transaction, check_balance2};
|
use ledger::{add_transaction, check_balance2};
|
||||||
|
|
||||||
use crate::{core::{CoreError, Ledger, Unit}, document::ledger::add_price, parser::parse_directives};
|
use crate::{
|
||||||
|
core::{CoreError, Ledger, Unit},
|
||||||
|
document::ledger::add_price,
|
||||||
|
parser::parse_directives,
|
||||||
|
};
|
||||||
use std::{path::Path, time::Instant};
|
use std::{path::Path, time::Instant};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -26,7 +30,11 @@ impl Document {
|
|||||||
Ok(document)
|
Ok(document)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_ledger(&self) -> Result<Ledger, CoreError> {
|
pub fn get_balance_count(&self) -> usize {
|
||||||
|
self.directives.balances.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_ledger(&self, check_balance: bool) -> Result<Ledger, CoreError> {
|
||||||
let mut ledger = Ledger::new();
|
let mut ledger = Ledger::new();
|
||||||
|
|
||||||
for commodity in &self.directives.commodities {
|
for commodity in &self.directives.commodities {
|
||||||
@ -45,11 +53,13 @@ impl Document {
|
|||||||
ledger.sort_prices();
|
ledger.sort_prices();
|
||||||
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
|
if check_balance {
|
||||||
for balance in &self.directives.balances {
|
for balance in &self.directives.balances {
|
||||||
check_balance2(&ledger, &balance)?;
|
check_balance2(&ledger, &balance)?;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
let end = Instant::now();
|
let end = Instant::now();
|
||||||
println!("time to calculate balance: {:?}", end - start);
|
// println!("time to calculate balance: {:?}", end - start);
|
||||||
|
|
||||||
Ok(ledger)
|
Ok(ledger)
|
||||||
// for balance in self.directives.balances {
|
// for balance in self.directives.balances {
|
||||||
|
|||||||
187
src/main.backup.rs
Normal file
187
src/main.backup.rs
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
use std::{
|
||||||
|
io::{self, Write},
|
||||||
|
path::Path,
|
||||||
|
time::Instant,
|
||||||
|
};
|
||||||
|
|
||||||
|
use accounting_rust::{
|
||||||
|
document::Document,
|
||||||
|
output::cli::{format_balance, tui_to_ansi::text_to_ansi},
|
||||||
|
parser::{self, query},
|
||||||
|
query::{self, PostingField},
|
||||||
|
// queries::{self, base::{self, DataValue, Query}, functions::{ComparisonFunction, LogicalFunction, SubAccountFunction}, transaction::{AccountField, PostingField, TransactionField}},
|
||||||
|
};
|
||||||
|
use chrono::{NaiveDate, Utc};
|
||||||
|
use ratatui::{
|
||||||
|
crossterm::{self, style::PrintStyledContent},
|
||||||
|
layout::Rect,
|
||||||
|
prelude::CrosstermBackend,
|
||||||
|
style::{Color, Style, Stylize},
|
||||||
|
text::{Line, Span, Text},
|
||||||
|
widgets::Widget,
|
||||||
|
Frame, Terminal,
|
||||||
|
};
|
||||||
|
|
||||||
|
// use accounting_rust::{create_ledger::create_ledger, document::{SingleDocument, WholeDocument}, parser::parse_directives};
|
||||||
|
|
||||||
|
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// let file_data = fs::read_to_string("data/2020-bofa-checking.ledger").unwrap();
|
||||||
|
// let file_data = fs::read_to_string("data/2020-fidelity-401k.ledger").unwrap();
|
||||||
|
// let directives = parse_directives(file_data.as_str()).unwrap();
|
||||||
|
// println!("{:?}", directives.balances);
|
||||||
|
|
||||||
|
// let document = Document::new(Path::new("data/2020-fidelity-401k.ledger")).unwrap();
|
||||||
|
|
||||||
|
// let f = Frame::
|
||||||
|
|
||||||
|
// println!(
|
||||||
|
// "{}{}",
|
||||||
|
// span_to_ansi(&Span::raw("Hello ")),
|
||||||
|
// span_to_ansi(&Span::styled(
|
||||||
|
// "World",
|
||||||
|
// Style::new().fg(Color::Green).bg(Color::White)
|
||||||
|
// ))
|
||||||
|
// );
|
||||||
|
|
||||||
|
// let stdout = io::stdout();
|
||||||
|
// let backend = CrosstermBackend::new(stdout);
|
||||||
|
// let mut terminal = Terminal::new(backend)?;
|
||||||
|
|
||||||
|
// let line = Line::from(vec![
|
||||||
|
// Span::raw("Hello "),
|
||||||
|
// Span::styled("Hello ", Style::new().fg(Color::Rgb(100, 200, 150))),
|
||||||
|
// Span::styled("World", Style::new().fg(Color::Green).bg(Color::White)),
|
||||||
|
// ])
|
||||||
|
// .centered();
|
||||||
|
// let text = Text::from(line);
|
||||||
|
|
||||||
|
// println!("{}", text_to_ansi(&text));
|
||||||
|
|
||||||
|
// // println!("{:?}", line.to_string());
|
||||||
|
|
||||||
|
// // terminal.dra
|
||||||
|
|
||||||
|
// // crossterm::terminal::enable_raw_mode()?;
|
||||||
|
|
||||||
|
// terminal.draw(|f| {
|
||||||
|
// let area = f.area();
|
||||||
|
// f.render_widget(text, area);
|
||||||
|
// })?;
|
||||||
|
|
||||||
|
// PrintStyledContent
|
||||||
|
|
||||||
|
// let styled_text = Text::from(vec![
|
||||||
|
// Line::from(vec![
|
||||||
|
// Span::styled("Hello, ", Style::default().fg(Color::Green)),
|
||||||
|
// Span::styled("world! ", Style::default().fg(Color::Blue)),
|
||||||
|
// ]),
|
||||||
|
// Line::from(vec![
|
||||||
|
// Span::styled("This is ", Style::default().fg(Color::Yellow)),
|
||||||
|
// Span::styled("styled text", Style::default().fg(Color::Red)),
|
||||||
|
// Span::raw("."),
|
||||||
|
// ]),
|
||||||
|
// ]);
|
||||||
|
|
||||||
|
// // Convert the styled text to an ANSI-escaped string
|
||||||
|
// let ansi_string = styled_text.to_string();
|
||||||
|
|
||||||
|
// // Print the ANSI-styled string
|
||||||
|
// println!("{}", ansi_string);
|
||||||
|
|
||||||
|
// println!("{}", text.render(area, buf););
|
||||||
|
|
||||||
|
// return Ok(());
|
||||||
|
|
||||||
|
let t1 = Instant::now();
|
||||||
|
|
||||||
|
let document = Document::new(Path::new("/Users/petersonev/Documents/Projects/accounting/ledger-data/main.ledger")).unwrap();
|
||||||
|
|
||||||
|
let t2 = Instant::now();
|
||||||
|
|
||||||
|
let ledger = document.generate_ledger().unwrap();
|
||||||
|
|
||||||
|
let t3 = Instant::now();
|
||||||
|
|
||||||
|
// let balance = queries::balance(
|
||||||
|
// &ledger,
|
||||||
|
// &[],
|
||||||
|
// );
|
||||||
|
|
||||||
|
// let balance = queries::balance2(
|
||||||
|
// &ledger,
|
||||||
|
// NaiveDate::from_ymd_opt(2100, 01, 01).unwrap(),
|
||||||
|
// Some("$")
|
||||||
|
// );
|
||||||
|
|
||||||
|
// let balance_query = "transaction.date < 2100-01-01";
|
||||||
|
let balance_query = "account.name ~ 'Assets' OR account.name ~ 'Liabilities'";
|
||||||
|
|
||||||
|
let parsed_query = parser::query::<PostingField>(balance_query).unwrap();
|
||||||
|
if parsed_query.0.trim().len() != 0 {
|
||||||
|
panic!("Full string not consumed")
|
||||||
|
}
|
||||||
|
let balance_query = parsed_query.1;
|
||||||
|
|
||||||
|
let current_date = Utc::now().date_naive();
|
||||||
|
|
||||||
|
let balance = query::balance(&ledger, Some(&balance_query), Some(("$", current_date)));
|
||||||
|
|
||||||
|
// let date_query = ComparisonFunction::new(
|
||||||
|
// "<=",
|
||||||
|
// Query::from_field(PostingField::Transaction(TransactionField::Date)),
|
||||||
|
// Query::from(DataValue::from(NaiveDate::from_ymd_opt(2100, 01, 01).unwrap())),
|
||||||
|
// ).unwrap();
|
||||||
|
// let account_query = SubAccountFunction::new(
|
||||||
|
// "Assets".into(),
|
||||||
|
// base::Query::from_field(PostingField::Account(AccountField::Name)),
|
||||||
|
// );
|
||||||
|
|
||||||
|
// let total_query = LogicalFunction::new(
|
||||||
|
// "and",
|
||||||
|
// base::Query::from_fn(date_query),
|
||||||
|
// base::Query::from_fn(account_query),
|
||||||
|
// ).unwrap();
|
||||||
|
|
||||||
|
// let balance = queries::balance3(
|
||||||
|
// &ledger,
|
||||||
|
// &base::Query::from_fn(total_query),
|
||||||
|
// );
|
||||||
|
|
||||||
|
|
||||||
|
let t4 = Instant::now();
|
||||||
|
|
||||||
|
format_balance(&ledger, &balance);
|
||||||
|
|
||||||
|
let t5 = Instant::now();
|
||||||
|
|
||||||
|
println!("{:?} - {:?} - {:?} - {:?}", t2-t1, t3-t2, t4-t3, t5-t4);
|
||||||
|
|
||||||
|
// return;
|
||||||
|
|
||||||
|
// for (account_id, amounts) in balance {
|
||||||
|
// let account = ledger.get_account(account_id).unwrap();
|
||||||
|
// if !account.get_name().starts_with("Assets") {
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
// print!("{} : ", account.get_name());
|
||||||
|
// for (i, amount) in amounts.iter().enumerate() {
|
||||||
|
// if i != 0 {
|
||||||
|
// print!(", ");
|
||||||
|
// }
|
||||||
|
// print!("{}", ledger.format_amount(amount));
|
||||||
|
// // let unit = ledger.get_unit(amount.unit_id).unwrap();
|
||||||
|
// // let symbol = unit.default_symbol();
|
||||||
|
// // if symbol.is_prefix {
|
||||||
|
// // print!("{}{}", symbol.symbol, amount.value);
|
||||||
|
// // } else {
|
||||||
|
// // print!("{} {}", amount.value, symbol.symbol);
|
||||||
|
// // }
|
||||||
|
// }
|
||||||
|
// println!();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let ledger = create_ledger(&file_data).unwrap();
|
||||||
|
// println!("{:?}", val);
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
227
src/main.rs
227
src/main.rs
@ -1,187 +1,70 @@
|
|||||||
use std::{
|
// use std::path::PathBuf;
|
||||||
io::{self, Write},
|
|
||||||
path::Path,
|
use crossterm::{
|
||||||
time::Instant,
|
ExecutableCommand, QueueableCommand,
|
||||||
|
cursor::{self, MoveToPreviousLine},
|
||||||
|
style::{self, Stylize},
|
||||||
|
terminal,
|
||||||
};
|
};
|
||||||
|
use std::{collections::HashMap, default, io::stdout, path::Path};
|
||||||
|
|
||||||
use accounting_rust::{
|
use chrono::{Datelike, NaiveDate};
|
||||||
document::Document,
|
use inquire::{CustomType, Text};
|
||||||
output::cli::{format_balance, tui_to_ansi::text_to_ansi},
|
use rt_format::ParsedFormat;
|
||||||
parser::{self, query},
|
|
||||||
query::{self, PostingField},
|
|
||||||
// queries::{self, base::{self, DataValue, Query}, functions::{ComparisonFunction, LogicalFunction, SubAccountFunction}, transaction::{AccountField, PostingField, TransactionField}},
|
|
||||||
};
|
|
||||||
use chrono::{NaiveDate, Utc};
|
|
||||||
use ratatui::{
|
|
||||||
crossterm::{self, style::PrintStyledContent},
|
|
||||||
layout::Rect,
|
|
||||||
prelude::CrosstermBackend,
|
|
||||||
style::{Color, Style, Stylize},
|
|
||||||
text::{Line, Span, Text},
|
|
||||||
widgets::Widget,
|
|
||||||
Frame, Terminal,
|
|
||||||
};
|
|
||||||
|
|
||||||
// use accounting_rust::{create_ledger::create_ledger, document::{SingleDocument, WholeDocument}, parser::parse_directives};
|
use crate::cli::fmt::Variant;
|
||||||
|
|
||||||
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
mod cli;
|
||||||
// let file_data = fs::read_to_string("data/2020-bofa-checking.ledger").unwrap();
|
|
||||||
// let file_data = fs::read_to_string("data/2020-fidelity-401k.ledger").unwrap();
|
|
||||||
// let directives = parse_directives(file_data.as_str()).unwrap();
|
|
||||||
// println!("{:?}", directives.balances);
|
|
||||||
|
|
||||||
// let document = Document::new(Path::new("data/2020-fidelity-401k.ledger")).unwrap();
|
fn main() {
|
||||||
|
cli::run().unwrap();
|
||||||
|
|
||||||
// let f = Frame::
|
// let skip = inquire::Confirm::new("Test?")
|
||||||
|
// .with_help_message("Account: abc\n Payee: def\n Narration: sdfsdf")
|
||||||
|
// .with_default(true)
|
||||||
|
// .prompt()
|
||||||
|
// .unwrap();
|
||||||
|
|
||||||
// println!(
|
// let score = strsim::jaro_winkler(
|
||||||
// "{}{}",
|
// &"KEYSIGHT TECHNOL DES:DIRECT DEP ID:XXXXX4194068UTV INDN:PETERSON,EVAN J CO ID:XXXXX11101 PPD".to_lowercase(),
|
||||||
// span_to_ansi(&Span::raw("Hello ")),
|
// &"KEYSIGHT TECHNOL DES:DIRECT DEP ID:XXXXX6001478UTV INDN:PETERSON,EVAN J CO ID:XXXXX11101 PPD".to_lowercase(),
|
||||||
// span_to_ansi(&Span::styled(
|
|
||||||
// "World",
|
|
||||||
// Style::new().fg(Color::Green).bg(Color::White)
|
|
||||||
// ))
|
|
||||||
// );
|
// );
|
||||||
|
// // // let score = strsim::levenshtein("abc", "");
|
||||||
|
// println!("{score}");
|
||||||
|
|
||||||
// let stdout = io::stdout();
|
// // nucleo::Nucleo::new(nucleo::Config::DEFAULT, notify, num_threads, columns);
|
||||||
// let backend = CrosstermBackend::new(stdout);
|
// let mut matcher = nucleo_matcher::Matcher::new(nucleo_matcher::Config::DEFAULT);
|
||||||
// let mut terminal = Terminal::new(backend)?;
|
|
||||||
|
|
||||||
// let line = Line::from(vec![
|
// let pattern = nucleo_matcher::pattern::Pattern::new(
|
||||||
// Span::raw("Hello "),
|
// "Daniel Realty DES:RENTAL ID:XXXXX2645",
|
||||||
// Span::styled("Hello ", Style::new().fg(Color::Rgb(100, 200, 150))),
|
// nucleo_matcher::pattern::CaseMatching::Ignore, // Ignore case for matching
|
||||||
// Span::styled("World", Style::new().fg(Color::Green).bg(Color::White)),
|
// nucleo_matcher::pattern::Normalization::Smart, // Use smart normalization
|
||||||
// ])
|
// nucleo_matcher::pattern::AtomKind::Fuzzy // Use the fuzzy matching algorithm
|
||||||
// .centered();
|
|
||||||
// let text = Text::from(line);
|
|
||||||
|
|
||||||
// println!("{}", text_to_ansi(&text));
|
|
||||||
|
|
||||||
// // println!("{:?}", line.to_string());
|
|
||||||
|
|
||||||
// // terminal.dra
|
|
||||||
|
|
||||||
// // crossterm::terminal::enable_raw_mode()?;
|
|
||||||
|
|
||||||
// terminal.draw(|f| {
|
|
||||||
// let area = f.area();
|
|
||||||
// f.render_widget(text, area);
|
|
||||||
// })?;
|
|
||||||
|
|
||||||
// PrintStyledContent
|
|
||||||
|
|
||||||
// let styled_text = Text::from(vec![
|
|
||||||
// Line::from(vec![
|
|
||||||
// Span::styled("Hello, ", Style::default().fg(Color::Green)),
|
|
||||||
// Span::styled("world! ", Style::default().fg(Color::Blue)),
|
|
||||||
// ]),
|
|
||||||
// Line::from(vec![
|
|
||||||
// Span::styled("This is ", Style::default().fg(Color::Yellow)),
|
|
||||||
// Span::styled("styled text", Style::default().fg(Color::Red)),
|
|
||||||
// Span::raw("."),
|
|
||||||
// ]),
|
|
||||||
// ]);
|
|
||||||
|
|
||||||
// // Convert the styled text to an ANSI-escaped string
|
|
||||||
// let ansi_string = styled_text.to_string();
|
|
||||||
|
|
||||||
// // Print the ANSI-styled string
|
|
||||||
// println!("{}", ansi_string);
|
|
||||||
|
|
||||||
// println!("{}", text.render(area, buf););
|
|
||||||
|
|
||||||
// return Ok(());
|
|
||||||
|
|
||||||
let t1 = Instant::now();
|
|
||||||
|
|
||||||
let document = Document::new(Path::new("data/full/main.ledger")).unwrap();
|
|
||||||
|
|
||||||
let t2 = Instant::now();
|
|
||||||
|
|
||||||
let ledger = document.generate_ledger().unwrap();
|
|
||||||
|
|
||||||
let t3 = Instant::now();
|
|
||||||
|
|
||||||
// let balance = queries::balance(
|
|
||||||
// &ledger,
|
|
||||||
// &[],
|
|
||||||
// );
|
// );
|
||||||
|
// let mut buf = Vec::new();
|
||||||
|
// let score = pattern.score(nucleo_matcher::Utf32Str::new("Daniel Realty DES:RENTAL ID:XXXXX8868", &mut buf), &mut matcher);
|
||||||
|
// println!("{score:?}");
|
||||||
|
|
||||||
// let balance = queries::balance2(
|
// let matches = pattern.match_list(&items_to_match, &mut matcher);
|
||||||
// &ledger,
|
// matcher
|
||||||
// NaiveDate::from_ymd_opt(2100, 01, 01).unwrap(),
|
|
||||||
// Some("$")
|
|
||||||
// );
|
|
||||||
|
|
||||||
// let balance_query = "transaction.date < 2100-01-01";
|
// let args = HashMap::from([("abc", Variant::String("aaa".into()))]);
|
||||||
let balance_query = "account.name ~ 'Assets' OR account.name ~ 'Liabilities'";
|
// let out = ParsedFormat::parse("{abc:>8} def", &[], &args).unwrap().to_string();
|
||||||
|
// println!("{out}");
|
||||||
|
|
||||||
let parsed_query = parser::query::<PostingField>(balance_query).unwrap();
|
// let a = "{abc} def".format(args);
|
||||||
if parsed_query.0.trim().len() != 0 {
|
|
||||||
panic!("Full string not consumed")
|
// let mut stdout = stdout();
|
||||||
}
|
|
||||||
let balance_query = parsed_query.1;
|
// let date: NaiveDate = CustomType::new("Date:")
|
||||||
|
// .with_error_message("Please type a valid date")
|
||||||
let current_date = Utc::now().date_naive();
|
// .with_default(NaiveDate::from_ymd_opt(2000, 01, 01).unwrap())
|
||||||
|
// .prompt()
|
||||||
let balance = query::balance(&ledger, Some(&balance_query), Some(("$", current_date)));
|
// .unwrap();
|
||||||
|
|
||||||
// let date_query = ComparisonFunction::new(
|
// stdout.execute(MoveToPreviousLine(1)).unwrap();
|
||||||
// "<=",
|
// stdout.execute(terminal::Clear(terminal::ClearType::FromCursorDown)).unwrap();
|
||||||
// Query::from_field(PostingField::Transaction(TransactionField::Date)),
|
|
||||||
// Query::from(DataValue::from(NaiveDate::from_ymd_opt(2100, 01, 01).unwrap())),
|
// // let name = Text::new("What is your name?").with_help_message("Help message").prompt();
|
||||||
// ).unwrap();
|
// println!("{:?}", date.year());
|
||||||
// let account_query = SubAccountFunction::new(
|
|
||||||
// "Assets".into(),
|
|
||||||
// base::Query::from_field(PostingField::Account(AccountField::Name)),
|
|
||||||
// );
|
|
||||||
|
|
||||||
// let total_query = LogicalFunction::new(
|
|
||||||
// "and",
|
|
||||||
// base::Query::from_fn(date_query),
|
|
||||||
// base::Query::from_fn(account_query),
|
|
||||||
// ).unwrap();
|
|
||||||
|
|
||||||
// let balance = queries::balance3(
|
|
||||||
// &ledger,
|
|
||||||
// &base::Query::from_fn(total_query),
|
|
||||||
// );
|
|
||||||
|
|
||||||
|
|
||||||
let t4 = Instant::now();
|
|
||||||
|
|
||||||
format_balance(&ledger, &balance);
|
|
||||||
|
|
||||||
let t5 = Instant::now();
|
|
||||||
|
|
||||||
println!("{:?} - {:?} - {:?} - {:?}", t2-t1, t3-t2, t4-t3, t5-t4);
|
|
||||||
|
|
||||||
// return;
|
|
||||||
|
|
||||||
// for (account_id, amounts) in balance {
|
|
||||||
// let account = ledger.get_account(account_id).unwrap();
|
|
||||||
// if !account.get_name().starts_with("Assets") {
|
|
||||||
// continue;
|
|
||||||
// }
|
|
||||||
// print!("{} : ", account.get_name());
|
|
||||||
// for (i, amount) in amounts.iter().enumerate() {
|
|
||||||
// if i != 0 {
|
|
||||||
// print!(", ");
|
|
||||||
// }
|
|
||||||
// print!("{}", ledger.format_amount(amount));
|
|
||||||
// // let unit = ledger.get_unit(amount.unit_id).unwrap();
|
|
||||||
// // let symbol = unit.default_symbol();
|
|
||||||
// // if symbol.is_prefix {
|
|
||||||
// // print!("{}{}", symbol.symbol, amount.value);
|
|
||||||
// // } else {
|
|
||||||
// // print!("{} {}", amount.value, symbol.symbol);
|
|
||||||
// // }
|
|
||||||
// }
|
|
||||||
// println!();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// let ledger = create_ledger(&file_data).unwrap();
|
|
||||||
// println!("{:?}", val);
|
|
||||||
|
|
||||||
return Ok(());
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,8 +2,8 @@ use crate::core::{Amount, Ledger};
|
|||||||
|
|
||||||
impl Ledger {
|
impl Ledger {
|
||||||
/// Returns formatted string and position of decimal point in string
|
/// Returns formatted string and position of decimal point in string
|
||||||
pub fn format_amount(&self, amount: &Amount) -> (String, usize) {
|
pub fn format_amount(&self, amount: &Amount) -> Result<(String, usize), ()> {
|
||||||
let unit = self.get_unit(amount.unit_id).unwrap();
|
let unit = self.get_unit(amount.unit_id).ok_or(())?;
|
||||||
let default_symbol = unit.default_symbol();
|
let default_symbol = unit.default_symbol();
|
||||||
let amount = self.round_amount(&amount);
|
let amount = self.round_amount(&amount);
|
||||||
|
|
||||||
@ -16,13 +16,13 @@ impl Ledger {
|
|||||||
let value = amount.value.abs().to_string();
|
let value = amount.value.abs().to_string();
|
||||||
let mut split = value.split(".");
|
let mut split = value.split(".");
|
||||||
|
|
||||||
let mut value = split.next().unwrap()
|
let mut value = split.next().ok_or(())?
|
||||||
.as_bytes()
|
.as_bytes()
|
||||||
.rchunks(3)
|
.rchunks(3)
|
||||||
.rev()
|
.rev()
|
||||||
.map(std::str::from_utf8)
|
.map(std::str::from_utf8)
|
||||||
.collect::<Result<Vec<&str>, _>>()
|
.collect::<Result<Vec<&str>, _>>()
|
||||||
.unwrap()
|
.or(Err(()))?
|
||||||
.join(",");
|
.join(",");
|
||||||
let value_decimal_pos = value.len();
|
let value_decimal_pos = value.len();
|
||||||
|
|
||||||
@ -33,13 +33,13 @@ impl Ledger {
|
|||||||
|
|
||||||
if default_symbol.is_prefix {
|
if default_symbol.is_prefix {
|
||||||
let decimal_pos = sign.len() + default_symbol.symbol.len() + value_decimal_pos;
|
let decimal_pos = sign.len() + default_symbol.symbol.len() + value_decimal_pos;
|
||||||
(format!("{}{}{}", sign, default_symbol.symbol, value), decimal_pos)
|
Ok((format!("{}{}{}", sign, default_symbol.symbol, value), decimal_pos))
|
||||||
} else {
|
} else {
|
||||||
let decimal_pos = sign.len() + value_decimal_pos;
|
let decimal_pos = sign.len() + value_decimal_pos;
|
||||||
if default_symbol.symbol.len() == 1 {
|
if default_symbol.symbol.len() == 1 {
|
||||||
(format!("{}{}{}", sign, value, default_symbol.symbol), decimal_pos)
|
Ok((format!("{}{}{}", sign, value, default_symbol.symbol), decimal_pos))
|
||||||
} else {
|
} else {
|
||||||
(format!("{}{} {}", sign, value, default_symbol.symbol), decimal_pos)
|
Ok((format!("{}{} {}", sign, value, default_symbol.symbol), decimal_pos))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -231,7 +231,7 @@ fn tree_to_text(tree: &BalanceTreeStr, ledger: &Ledger, base_amount_pos: usize,
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
fn balance_tree_to_str_tree(tree: BalanceTree, ledger: &Ledger) -> BalanceTreeStr {
|
fn balance_tree_to_str_tree(tree: BalanceTree, ledger: &Ledger) -> BalanceTreeStr {
|
||||||
let amounts = tree.amounts.map(|v| v.iter().map(|a| ledger.format_amount(a)).collect());
|
let amounts = tree.amounts.map(|v| v.iter().map(|a| ledger.format_amount(a).unwrap()).collect());
|
||||||
let children = tree.children.into_iter().map(|c| balance_tree_to_str_tree(c, ledger)).collect();
|
let children = tree.children.into_iter().map(|c| balance_tree_to_str_tree(c, ledger)).collect();
|
||||||
|
|
||||||
BalanceTreeStr{amounts, name: tree.name, children}
|
BalanceTreeStr{amounts, name: tree.name, children}
|
||||||
|
|||||||
@ -1,21 +1,26 @@
|
|||||||
use nom::{
|
use nom::{
|
||||||
|
IResult, Parser,
|
||||||
branch::alt,
|
branch::alt,
|
||||||
character::complete::{none_of, one_of, space0},
|
character::complete::{none_of, one_of, space0},
|
||||||
combinator::{opt, recognize},
|
combinator::{opt, recognize},
|
||||||
multi::many1,
|
multi::many1,
|
||||||
sequence::{preceded, tuple}, IResult, Parser,
|
sequence::{preceded, tuple},
|
||||||
};
|
};
|
||||||
use rust_decimal_macros::dec;
|
use rust_decimal_macros::dec;
|
||||||
|
|
||||||
|
use super::{decimal, quoted_string};
|
||||||
use crate::core::RawAmount;
|
use crate::core::RawAmount;
|
||||||
use super::decimal;
|
|
||||||
|
|
||||||
pub fn amount(input: &str) -> IResult<&str, RawAmount> {
|
pub fn amount(input: &str) -> IResult<&str, RawAmount> {
|
||||||
alt((suffix_amount, prefix_amount)).parse(input)
|
alt((suffix_amount, prefix_amount)).parse(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unit(input: &str) -> IResult<&str, &str> {
|
pub fn unit(input: &str) -> IResult<&str, &str> {
|
||||||
recognize(many1(none_of("0123456789,+-_()*/.{} \t"))).parse(input)
|
alt((
|
||||||
|
quoted_string,
|
||||||
|
recognize(many1(none_of("0123456789,+-_()*/.{} \t"))),
|
||||||
|
))
|
||||||
|
.parse(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////
|
///////////////
|
||||||
|
|||||||
@ -114,26 +114,38 @@ fn posting(input: &str) -> IResult<&str, DirectivePosting> {
|
|||||||
amount = Some(v.0);
|
amount = Some(v.0);
|
||||||
if let Some(c) = v.1.0 {
|
if let Some(c) = v.1.0 {
|
||||||
if c.1 {
|
if c.1 {
|
||||||
|
let amount = amount.as_ref().unwrap().value.abs();
|
||||||
|
if amount.is_zero() {
|
||||||
|
// TODO: this isn't really right
|
||||||
|
cost = None;
|
||||||
|
} else {
|
||||||
cost = Some(RawCost {
|
cost = Some(RawCost {
|
||||||
amount: RawAmount {
|
amount: RawAmount {
|
||||||
value: c.0.amount.value / amount.as_ref().unwrap().value.abs(),
|
value: c.0.amount.value / amount,
|
||||||
unit_symbol: c.0.amount.unit_symbol,
|
unit_symbol: c.0.amount.unit_symbol,
|
||||||
is_unit_prefix: c.0.amount.is_unit_prefix,
|
is_unit_prefix: c.0.amount.is_unit_prefix,
|
||||||
},
|
},
|
||||||
date: c.0.date,
|
date: c.0.date,
|
||||||
label: c.0.label,
|
label: c.0.label,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
cost = Some(c.0);
|
cost = Some(c.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(p) = v.1.1 {
|
if let Some(p) = v.1.1 {
|
||||||
if p.1 {
|
if p.1 {
|
||||||
|
let amount = amount.as_ref().unwrap().value.abs();
|
||||||
|
if amount.is_zero() {
|
||||||
|
// TODO: this isn't really right
|
||||||
|
price = None;
|
||||||
|
} else {
|
||||||
price = Some(RawAmount {
|
price = Some(RawAmount {
|
||||||
value: p.0.value / amount.as_ref().unwrap().value.abs(),
|
value: p.0.value / amount,
|
||||||
unit_symbol: p.0.unit_symbol,
|
unit_symbol: p.0.unit_symbol,
|
||||||
is_unit_prefix: p.0.is_unit_prefix,
|
is_unit_prefix: p.0.is_unit_prefix,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
price = Some(p.0);
|
price = Some(p.0);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ use crate::core::{Amount, Ledger};
|
|||||||
|
|
||||||
pub fn balance(
|
pub fn balance(
|
||||||
ledger: &Ledger,
|
ledger: &Ledger,
|
||||||
filter: Option<&Query<PostingField>>,
|
filter: Option<Query<PostingField>>,
|
||||||
convert_to_unit: Option<(&str, NaiveDate)>,
|
convert_to_unit: Option<(&str, NaiveDate)>,
|
||||||
) -> HashMap<u32, Vec<Amount>> {
|
) -> HashMap<u32, Vec<Amount>> {
|
||||||
let convert_to_unit = convert_to_unit.map(|u| (ledger.get_unit_by_symbol(u.0).unwrap(), u.1));
|
let convert_to_unit = convert_to_unit.map(|u| (ledger.get_unit_by_symbol(u.0).unwrap(), u.1));
|
||||||
@ -29,7 +29,7 @@ pub fn balance(
|
|||||||
|
|
||||||
let filter = match filter {
|
let filter = match filter {
|
||||||
Some(filter) => filter,
|
Some(filter) => filter,
|
||||||
None => &Query::Value(true.into()),
|
None => Query::Value(true.into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let filtered_postings = postings.filter(|data| {
|
let filtered_postings = postings.filter(|data| {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user