Functional Weave
Code in TypeScript

finance.ledger.trial-balance@1.0.0

impl/rust.rs

3,521 bytes · the Rust implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::finance_ledger_journal_validate::{journal_line_from_value, validate_journal, JournalLine};  ← from finance.ledger.journal-validate ^1.0.0 · built alongside by fune
use super::money_add::add_money;  ← from money.add ^1.0.0 · built alongside by fune
use super::money_amount::{money, money_to_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune

const FATAL: [&str; 3] = ["invalid-account", "currency-mismatch", "negative-amount"];

/// A trial balance: each account's balance on its own side, and whether the
/// debit column equals the credit column.
///
/// # Panics
/// Panics, with journal-validate's message, on a line that cannot be totalled:
/// another currency, a negative amount or a malformed account code. An
/// unbalanced ledger is an answer, not a panic.
pub fn trial_balance(lines: &[JournalLine], currency: &str) -> TrialBalance {
    let check = validate_journal(lines, currency);
    for problem in &check.problems {
        if FATAL.contains(&problem.code.as_str()) {
            panic!("{}", problem.message);
        }
    }

    let zero = money(0, currency);
    let mut totals: Vec<(String, Money, Money)> = Vec::new();
    for line in lines {
        if line.debit.minor == 0 && line.credit.minor == 0 {
            continue;
        }
        match totals.iter_mut().find(|(account, _, _)| *account == line.account) {
            Some(entry) => {
                entry.1 = add_money(&entry.1, &line.debit);
                entry.2 = add_money(&entry.2, &line.credit);
            }
            None => totals.push((line.account.clone(), line.debit.clone(), line.credit.clone())),
        }
    }
    totals.sort_by(|a, b| a.0.cmp(&b.0));

    let mut rows: Vec<TrialBalanceRow> = Vec::with_capacity(totals.len());
    let mut total_debit = zero.clone();
    let mut total_credit = zero;
    for (account, debits, credits) in totals {
        let net = debits.minor - credits.minor;
        let debit_balance = money(if net > 0 { net } else { 0 }, currency);
        let credit_balance = money(if net < 0 { -net } else { 0 }, currency);
        total_debit = add_money(&total_debit, &debit_balance);
        total_credit = add_money(&total_credit, &credit_balance);
        rows.push(TrialBalanceRow {
            account,
            debits,
            credits,
            debit_balance,
            credit_balance,
        });
    }
    let balanced = total_debit.minor == total_credit.minor;
    TrialBalance {
        rows,
        total_debit,
        total_credit,
        balanced,
    }
}

pub fn trial_balance_to_value(tb: &TrialBalance) -> Value {
    Value::obj(vec![
        (
            "rows",
            Value::Arr(
                tb.rows
                    .iter()
                    .map(|r| {
                        Value::obj(vec![
                            ("account", Value::str(&r.account)),
                            ("debits", money_to_value(&r.debits)),
                            ("credits", money_to_value(&r.credits)),
                            ("debitBalance", money_to_value(&r.debit_balance)),
                            ("creditBalance", money_to_value(&r.credit_balance)),
                        ])
                    })
                    .collect(),
            ),
        ),
        ("totalDebit", money_to_value(&tb.total_debit)),
        ("totalCredit", money_to_value(&tb.total_credit)),
        ("balanced", Value::Bool(tb.balanced)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let lines: Vec<JournalLine> = args[0].as_arr().iter().map(journal_line_from_value).collect();
    trial_balance_to_value(&trial_balance(&lines, args[1].as_str()))
}