commit
6cdcdcbb8d
@ -0,0 +1 @@
|
||||
/target
|
@ -0,0 +1,153 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||
|
||||
[[package]]
|
||||
name = "beancount-parser"
|
||||
version = "1.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1e30aaa9270127e9201b1db9644001e13b5218178afced4c6abc7e1969ac938"
|
||||
dependencies = [
|
||||
"nom",
|
||||
"rust_decimal",
|
||||
"rustc_version",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "beancount-rs"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"beancount-parser",
|
||||
"rust_decimal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust_decimal"
|
||||
version = "1.26.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee9164faf726e4f3ece4978b25ca877ddc6802fa77f38cdccb32c7f805ecd70c"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"num-traits",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
|
||||
dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.145"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fcd952facd492f9be3ef0d0b7032a6e442ee9b361d4acc2b1d0c4aaa5f613a1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"
|
@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "beancount-rs"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
beancount-parser = { version = "1.8.5", features = ["rust_decimal"] }
|
||||
#beancount-parser = { version = "1.0.0", features = ["rust_decimal"] }
|
||||
rust_decimal = "1.26"
|
@ -0,0 +1,21 @@
|
||||
# `beancount-rs`
|
||||
|
||||
This tool is extremely similar to the `beancount` utilities you're used to.
|
||||
The only difference, is that they're written in Rust.
|
||||
I plan to use this daily, so I should find any issues, but feel free to put issues in the Git for additional information.
|
||||
|
||||
All code is licensed under the GPLv3, with the exception of the web portion, which is provided under the AGPLv3.
|
||||
|
||||
## Limitations
|
||||
|
||||
* The largest value the parser can understand is around: 79228162514264340000000000000 (That's 79 × 10^27).
|
||||
* Pretty sure nobody has this much money, even theoretically. But who knows with inflation these days.
|
||||
|
||||
## Restrictions
|
||||
|
||||
* All transactions must either:
|
||||
* Balance to zero, OR
|
||||
* Have a blank posting in the account to fill in.
|
||||
* You may only have zero or one blank postings per transaction.
|
||||
* All transactions are processed in order of date, regardless of the order that the file contents.
|
||||
|
@ -0,0 +1,125 @@
|
||||
use std::{
|
||||
io::Read,
|
||||
fs::File,
|
||||
env::args,
|
||||
process::exit,
|
||||
};
|
||||
|
||||
use rust_decimal::Decimal;
|
||||
use beancount_parser::{
|
||||
Parser,
|
||||
Directive,
|
||||
Transaction,
|
||||
Account,
|
||||
Open,
|
||||
Close,
|
||||
Date,
|
||||
};
|
||||
|
||||
fn last_posting_is_none(transaction: &Transaction<'_>) -> bool {
|
||||
transaction.postings()
|
||||
.last()
|
||||
.expect("There are no transactions in this posting")
|
||||
.amount()
|
||||
.is_none()
|
||||
}
|
||||
fn balance(transaction: &Transaction<'_>) -> Decimal {
|
||||
transaction.postings()
|
||||
.iter()
|
||||
.filter_map(|p| Some::<Decimal>(p.amount()?
|
||||
.expression()
|
||||
.evaluate()
|
||||
.into())
|
||||
)
|
||||
.sum()
|
||||
}
|
||||
fn account_exists(acct: &Account, opens: &Vec<Open<'_>>) -> bool {
|
||||
opens.iter()
|
||||
.filter(|a| a.account() == acct)
|
||||
.count() == 1
|
||||
}
|
||||
fn accounts_exist(transaction: &Transaction, opens: &Vec<Open<'_>>) -> bool {
|
||||
transaction.postings()
|
||||
.iter()
|
||||
.all(|post| account_exists(post.account(), opens))
|
||||
}
|
||||
fn account_open(account: &Account, date: Date, opens: &Vec<Open<'_>>) -> bool {
|
||||
opens.iter()
|
||||
.any(|o| o.date() <= date && account == o.account())
|
||||
}
|
||||
fn accounts_open(transaction: &Transaction, opens: &Vec<Open<'_>>) -> bool {
|
||||
transaction.postings()
|
||||
.iter()
|
||||
.all(|post| account_open(post.account(), transaction.date(), opens))
|
||||
}
|
||||
fn account_closed(account: &Account, date: Date, closes: &Vec<Close<'_>>) -> bool {
|
||||
closes.iter()
|
||||
.any(|c| c.date() < date && account == c.account())
|
||||
}
|
||||
fn accounts_closed(transaction: &Transaction, closes: &Vec<Close<'_>>) -> bool {
|
||||
transaction.postings()
|
||||
.iter()
|
||||
.all(|post| account_closed(post.account(), transaction.date(), closes))
|
||||
}
|
||||
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Copy, Hash)]
|
||||
enum TransactionValidationError {
|
||||
AccountDoesNotExist,
|
||||
AccountNotOpenYet,
|
||||
AccountPreviouslyClosed,
|
||||
DoesNotBalanceToZero,
|
||||
}
|
||||
|
||||
fn is_valid_transaction(transaction: &Transaction<'_>, opens: &Vec<Open<'_>>, closes: &Vec<Close<'_>>) -> Result<bool, TransactionValidationError> {
|
||||
if balance(transaction) != Decimal::ZERO && !last_posting_is_none(transaction) {
|
||||
return Err(TransactionValidationError::DoesNotBalanceToZero);
|
||||
}
|
||||
if !accounts_exist(transaction, opens) {
|
||||
return Err(TransactionValidationError::AccountDoesNotExist);
|
||||
}
|
||||
if !accounts_open(transaction, opens) {
|
||||
return Err(TransactionValidationError::AccountNotOpenYet);
|
||||
}
|
||||
if accounts_closed(transaction, closes) {
|
||||
return Err(TransactionValidationError::AccountPreviouslyClosed);
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn main() -> Result<(), beancount_parser::Error> {
|
||||
let bean_filename = args().nth(1).expect("A file must be specified");
|
||||
let mut bean_file = File::open(bean_filename).expect("Can not open file");
|
||||
let mut bean_contents = String::new();
|
||||
bean_file.read_to_string(&mut bean_contents).expect("Can not read file");
|
||||
let directives: Vec<Directive<'_>> = Parser::new(&bean_contents)
|
||||
.collect::<Result<_, _>>()
|
||||
.expect("Can not load directives; invalid syntax.");
|
||||
let transactions: Vec<Transaction> = directives.iter()
|
||||
.filter_map(|d| match d {
|
||||
Directive::Transaction(t) => Some(t.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let opens: Vec<Open<'_>> = directives.iter()
|
||||
.filter_map(|d| match d {
|
||||
Directive::Open(o) => Some(o.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let closes: Vec<Close<'_>> = directives.iter()
|
||||
.filter_map(|d| match d {
|
||||
Directive::Close(c) => Some(c.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
for transaction in &transactions {
|
||||
if let Err(e) = is_valid_transaction(&transaction, &opens, &closes) {
|
||||
println!("Invalid transaction: {:#?}", e);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
println!("Accounts: {}", opens.len());
|
||||
println!("Transactions: {}", transactions.len());
|
||||
Ok(())
|
||||
}
|
Loading…
Reference in new issue