Initial commit

master
Tait Hoyem 1 year ago
commit 6cdcdcbb8d

1
.gitignore vendored

@ -0,0 +1 @@
/target

153
Cargo.lock generated

@ -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…
Cancel
Save