use rust_decimal::Decimal;
pub const VAT_TOLERANCE: Decimal = Decimal::ONE;
const HALF: Decimal = Decimal::from_parts(5, 0, 0, false, 1);
pub fn xpath_round(x: Decimal) -> Decimal {
x.checked_add(HALF).map_or(x, |shifted| shifted.floor())
}
pub fn derived_vat(base: Decimal, rate: Decimal) -> Option<Decimal> {
base.abs()
.checked_mul(rate)
.map(|product| xpath_round(product) / Decimal::ONE_HUNDRED)
}
pub fn within_vat_tolerance(stated: Decimal, expected: Decimal) -> bool {
(stated - expected).abs() < VAT_TOLERANCE
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal::Decimal;
use std::str::FromStr;
fn d(s: &str) -> Decimal {
Decimal::from_str(s).unwrap()
}
#[test]
fn ties_go_towards_positive_infinity() {
assert_eq!(xpath_round(d("0.5")), d("1"));
assert_eq!(xpath_round(d("2.5")), d("3"));
assert_eq!(xpath_round(d("-0.5")), d("0"));
assert_eq!(xpath_round(d("-1.5")), d("-1"));
assert_eq!(xpath_round(d("0.4")), d("0"));
assert_eq!(xpath_round(d("0.6")), d("1"));
assert_eq!(xpath_round(d("-0.6")), d("-1"));
assert_eq!(xpath_round(d("19")), d("19"));
}
#[test]
fn a_rate_of_half_a_per_cent_is_not_a_zero_rate() {
assert_ne!(xpath_round(d("0.5")), Decimal::ZERO);
assert_eq!(derived_vat(d("1000.00"), d("0.5")), Some(d("5")));
}
#[test]
fn the_derivation_is_taken_on_absolute_values() {
assert_eq!(
derived_vat(d("-1000.00"), d("19")),
derived_vat(d("1000.00"), d("19"))
);
}
#[test]
fn a_full_currency_unit_of_slack_excludes_its_own_boundary() {
assert!(within_vat_tolerance(d("190.99"), d("190.00")));
assert!(!within_vat_tolerance(d("191.00"), d("190.00")));
assert!(!within_vat_tolerance(d("189.00"), d("190.00")));
}
}