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::money_add::add_money; ← from money.add ^1.0.0 · built alongside by fune
use super::money_amount::{money, money_from_value, money_to_value}; ← from money.amount ^1.0.0 · built alongside by fune
/// 1-20 ASCII letters and digits, in groups joined by a single - . or /
fn is_account_code(account: &str) -> bool {
let bytes = account.as_bytes();
if bytes.is_empty() || bytes.len() > 20 {
return false;
}
let mut previous_was_alnum = false;
for &b in bytes {
if b.is_ascii_alphanumeric() {
previous_was_alnum = true;
} else if (b == b'-' || b == b'.' || b == b'/') && previous_was_alnum {
previous_was_alnum = false;
} else {
return false;
}
}
previous_was_alnum
}
/// Check a journal before it is posted, reporting every problem at once.
///
/// A line in the wrong currency or with a negative amount is left out of the
/// totals, since adding it would make them meaningless; the other problems
/// leave the line's amounts in.
///
/// # Panics
/// Panics only if `currency` is not an ISO 4217 code.
pub fn validate_journal(lines: &[JournalLine], currency: &str) -> JournalCheck {
let mut debit_total = money(0, currency);
let mut credit_total = money(0, currency);
let mut problems: Vec<JournalProblem> = Vec::new();
let mut report = |line: Option<i64>, code: &str, message: String| {
problems.push(JournalProblem {
line,
code: code.to_string(),
message,
});
};
if lines.is_empty() {
report(None, "empty", "the journal has no lines".to_string());
}
for (index, entry) in lines.iter().enumerate() {
let n = index as i64 + 1;
if !is_account_code(&entry.account) {
report(
Some(n),
"invalid-account",
format!("line {}: account \"{}\" is not a valid account code", n, entry.account),
);
}
let foreign = if entry.debit.currency != currency {
Some(entry.debit.currency.clone())
} else if entry.credit.currency != currency {
Some(entry.credit.currency.clone())
} else {
None
};
if let Some(foreign) = foreign {
report(
Some(n),
"currency-mismatch",
format!("line {}: amount is in {}, not the journal currency {}", n, foreign, currency),
);
continue;
}
if entry.debit.minor < 0 || entry.credit.minor < 0 {
report(
Some(n),
"negative-amount",
format!("line {}: debit and credit must not be negative", n),
);
continue;
}
if entry.debit.minor == 0 && entry.credit.minor == 0 {
report(Some(n), "zero-line", format!("line {}: debit and credit are both zero", n));
} else if entry.debit.minor != 0 && entry.credit.minor != 0 {
report(Some(n), "both-sides", format!("line {}: has both a debit and a credit", n));
}
debit_total = add_money(&debit_total, &entry.debit);
credit_total = add_money(&credit_total, &entry.credit);
}
let difference = money(debit_total.minor - credit_total.minor, currency);
if difference.minor != 0 {
report(None, "unbalanced", "debits do not equal credits".to_string());
}
JournalCheck {
valid: problems.is_empty(),
debit_total,
credit_total,
difference,
problems,
}
}
pub fn journal_line_from_value(v: &Value) -> JournalLine {
JournalLine {
account: v.get("account").as_str().to_string(),
debit: money_from_value(v.get("debit")),
credit: money_from_value(v.get("credit")),
}
}
pub fn journal_check_to_value(check: &JournalCheck) -> Value {
Value::obj(vec![
("valid", Value::Bool(check.valid)),
("debitTotal", money_to_value(&check.debit_total)),
("creditTotal", money_to_value(&check.credit_total)),
("difference", money_to_value(&check.difference)),
(
"problems",
Value::Arr(
check
.problems
.iter()
.map(|p| {
Value::obj(vec![
(
"line",
match p.line {
Some(n) => Value::Int(n),
None => Value::Null,
},
),
("code", Value::str(&p.code)),
("message", Value::str(&p.message)),
])
})
.collect(),
),
),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
let lines: Vec<JournalLine> = args[0].as_arr().iter().map(journal_line_from_value).collect();
journal_check_to_value(&validate_journal(&lines, args[1].as_str()))
}