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