use core::fmt;
use core::iter::Sum;
use core::ops::Neg;
use core::str::FromStr;
use rust_decimal::Decimal;
use crate::error::{AmountError, ParseAmountError};
const SCALE: u32 = 2;
const UNITS: i64 = 100;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(try_from = "String", into = "String"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct InvoiceAmount(i64);
impl InvoiceAmount {
pub const ZERO: Self = Self(0);
pub const MAX: Self = Self(i64::MAX);
pub const MIN: Self = Self(i64::MIN);
pub fn parse(s: &str) -> Result<Self, ParseAmountError> {
let t = s.trim();
if t.is_empty() {
return Err(ParseAmountError::Empty);
}
let (neg, digits) = match t.as_bytes()[0] {
b'-' => (true, &t[1..]),
b'+' => (false, &t[1..]),
_ => (false, t),
};
if digits.is_empty() {
return Err(ParseAmountError::Malformed {
input: s.to_owned(),
});
}
let (int_part, frac_part) = match digits.split_once('.') {
Some((i, f)) => (i, f),
None => (digits, ""),
};
if int_part.is_empty()
|| !int_part.bytes().all(|b| b.is_ascii_digit())
|| !frac_part.bytes().all(|b| b.is_ascii_digit())
{
return Err(ParseAmountError::Malformed {
input: s.to_owned(),
});
}
if frac_part.len() > SCALE as usize {
return Err(ParseAmountError::TooManyDecimals {
input: s.to_owned(),
max: SCALE as u8,
});
}
let overflow = || ParseAmountError::OutOfRange {
input: s.to_owned(),
};
let mut units: i64 = 0;
for b in int_part.bytes() {
units = units
.checked_mul(10)
.and_then(|u| u.checked_add(i64::from(b - b'0')))
.ok_or_else(overflow)?;
}
units = units.checked_mul(UNITS).ok_or_else(overflow)?;
let mut frac: i64 = 0;
for i in 0..SCALE as usize {
let d = frac_part
.as_bytes()
.get(i)
.map_or(0, |b| i64::from(b - b'0'));
frac = frac * 10 + d;
}
units = units.checked_add(frac).ok_or_else(overflow)?;
Ok(Self(if neg { -units } else { units }))
}
pub fn from_decimal_exact(d: Decimal) -> Result<Self, AmountError> {
let scaled = d
.checked_mul(Decimal::from(UNITS))
.ok_or(AmountError::Overflow)?;
if scaled.fract() != Decimal::ZERO {
return Err(AmountError::PrecisionLoss {
value: d.to_string(),
max: SCALE as u8,
});
}
let units = i64::try_from(scaled.trunc()).map_err(|_| AmountError::Overflow)?;
Ok(Self(units))
}
#[must_use]
pub fn into_decimal(self) -> Decimal {
Decimal::new(self.0, SCALE)
}
#[must_use]
pub fn to_minor_units(self) -> i64 {
self.0
}
#[must_use]
pub fn from_minor_units(units: i64) -> Self {
Self(units)
}
pub fn checked_add(self, rhs: Self) -> Result<Self, AmountError> {
self.0
.checked_add(rhs.0)
.map(Self)
.ok_or(AmountError::Overflow)
}
pub fn checked_sub(self, rhs: Self) -> Result<Self, AmountError> {
self.0
.checked_sub(rhs.0)
.map(Self)
.ok_or(AmountError::Overflow)
}
pub fn checked_neg(self) -> Result<Self, AmountError> {
self.0.checked_neg().map(Self).ok_or(AmountError::Overflow)
}
pub fn checked_abs(self) -> Result<Self, AmountError> {
self.0.checked_abs().map(Self).ok_or(AmountError::Overflow)
}
pub fn checked_sum<I: IntoIterator<Item = Self>>(iter: I) -> Result<Self, AmountError> {
let mut acc = Self::ZERO;
for a in iter {
acc = acc.checked_add(a)?;
}
Ok(acc)
}
#[must_use]
pub fn is_positive(self) -> bool {
self.0 > 0
}
#[must_use]
pub fn is_negative(self) -> bool {
self.0 < 0
}
#[must_use]
pub fn is_zero(self) -> bool {
self.0 == 0
}
}
impl fmt::Display for InvoiceAmount {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let sign = if self.0 < 0 { "-" } else { "" };
let abs = self.0.unsigned_abs();
let s = format!("{sign}{}.{:02}", abs / UNITS as u64, abs % UNITS as u64);
f.pad(&s)
}
}
impl FromStr for InvoiceAmount {
type Err = ParseAmountError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
impl Neg for InvoiceAmount {
type Output = Self;
fn neg(self) -> Self {
self.checked_neg().expect("negation of InvoiceAmount::MIN")
}
}
impl Sum for InvoiceAmount {
fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
Self::checked_sum(iter).expect("overflow summing InvoiceAmount")
}
}
impl TryFrom<Decimal> for InvoiceAmount {
type Error = AmountError;
fn try_from(d: Decimal) -> Result<Self, Self::Error> {
Self::from_decimal_exact(d)
}
}
impl From<InvoiceAmount> for Decimal {
fn from(a: InvoiceAmount) -> Self {
a.into_decimal()
}
}
#[cfg(feature = "serde")]
impl TryFrom<String> for InvoiceAmount {
type Error = ParseAmountError;
fn try_from(s: String) -> Result<Self, Self::Error> {
Self::parse(&s)
}
}
#[cfg(feature = "serde")]
impl From<InvoiceAmount> for String {
fn from(a: InvoiceAmount) -> Self {
a.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal::dec;
#[test]
fn parses_and_renders_the_interchange_spelling() {
for (input, rendered) in [
("1190.00", "1190.00"),
("0", "0.00"),
("12.5", "12.50"),
("-85.00", "-85.00"),
("+7.25", "7.25"),
(" 31.88 ", "31.88"),
("0.07", "0.07"),
] {
let a = InvoiceAmount::parse(input).unwrap_or_else(|e| panic!("{input}: {e}"));
assert_eq!(a.to_string(), rendered, "input {input}");
}
}
#[test]
fn a_third_decimal_is_refused_not_rounded() {
assert!(matches!(
InvoiceAmount::parse("0.005"),
Err(ParseAmountError::TooManyDecimals { .. })
));
assert!(matches!(
InvoiceAmount::from_decimal_exact(dec!(0.005)),
Err(AmountError::PrecisionLoss { .. })
));
}
#[test]
fn rejects_malformed_input() {
for bad in ["", " ", "abc", "1.2.3", "1,50", ".5", "-", "1e3", "1 000"] {
assert!(
InvoiceAmount::parse(bad).is_err(),
"{bad:?} should not parse"
);
}
}
#[test]
fn fraction_padding_is_positional() {
assert_eq!(InvoiceAmount::parse("1.5").unwrap().to_minor_units(), 150);
assert_eq!(InvoiceAmount::parse("1.05").unwrap().to_minor_units(), 105);
}
#[test]
fn arithmetic_is_checked_never_wrapping() {
assert!(
InvoiceAmount::MAX
.checked_add(InvoiceAmount::parse("0.01").unwrap())
.is_err()
);
assert!(InvoiceAmount::MIN.checked_neg().is_err());
assert!(InvoiceAmount::MIN.checked_abs().is_err());
assert!(
InvoiceAmount::MIN
.checked_sub(InvoiceAmount::parse("0.01").unwrap())
.is_err()
);
}
#[test]
fn decimal_round_trips_exactly() {
for s in ["1190.00", "-85.00", "0.07", "0.00"] {
let a = InvoiceAmount::parse(s).unwrap();
assert_eq!(
InvoiceAmount::from_decimal_exact(a.into_decimal()).unwrap(),
a
);
}
}
#[test]
fn annex_a_1_6_negative_invoice_line() {
let line1 = InvoiceAmount::parse("212.50").unwrap(); let line2 = InvoiceAmount::parse("-85.00").unwrap();
let bt_106 = InvoiceAmount::checked_sum([line1, line2]).unwrap();
assert_eq!(bt_106, InvoiceAmount::parse("127.50").unwrap());
let bt_117 = InvoiceAmount::parse("31.88").unwrap();
let exact = bt_106.into_decimal() * dec!(25) / dec!(100); assert_eq!(exact, dec!(31.875));
assert!(InvoiceAmount::from_decimal_exact(exact).is_err());
assert!((bt_117.into_decimal() - exact).abs() < dec!(1));
let bt_112 = bt_106.checked_add(bt_117).unwrap();
assert_eq!(bt_112, InvoiceAmount::parse("159.38").unwrap());
assert_eq!(bt_112.checked_sub(InvoiceAmount::ZERO).unwrap(), bt_112);
}
#[test]
fn annex_a_1_7_negative_amount_due() {
let bt_112 = InvoiceAmount::parse("137.50").unwrap();
let bt_113 = InvoiceAmount::parse("250.00").unwrap();
let bt_115 = bt_112.checked_sub(bt_113).unwrap(); assert_eq!(bt_115, InvoiceAmount::parse("-112.50").unwrap());
assert!(bt_115.is_negative(), "a refund is a lawful invoice");
}
#[test]
fn ordering_and_display_padding() {
let a = InvoiceAmount::parse("-1.00").unwrap();
let b = InvoiceAmount::parse("1.00").unwrap();
assert!(a < b);
assert_eq!(format!("{b:>10}"), " 1.00");
assert_eq!(format!("{a:<8}"), "-1.00 ");
}
#[cfg(feature = "serde")]
#[test]
fn serde_uses_a_decimal_string() {
let a = InvoiceAmount::parse("1190.00").unwrap();
let json = serde_json::to_string(&a).unwrap();
assert_eq!(json, r#""1190.00""#, "never a float, never a raw integer");
assert_eq!(serde_json::from_str::<InvoiceAmount>(&json).unwrap(), a);
assert!(serde_json::from_str::<InvoiceAmount>(r#""0.005""#).is_err());
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct UnitPriceAmount(Decimal);
impl UnitPriceAmount {
pub const ZERO: Self = Self(Decimal::ZERO);
#[must_use]
pub const fn new(value: Decimal) -> Self {
Self(value)
}
#[must_use]
pub const fn into_decimal(self) -> Decimal {
self.0
}
#[must_use]
pub fn is_negative(self) -> bool {
self.0 < Decimal::ZERO
}
#[must_use]
pub fn checked_sub(self, rhs: Self) -> Option<Self> {
self.0.checked_sub(rhs.0).map(Self)
}
}
impl fmt::Display for UnitPriceAmount {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.pad(&self.0.normalize().to_string())
}
}
impl From<Decimal> for UnitPriceAmount {
fn from(d: Decimal) -> Self {
Self(d)
}
}
#[cfg(test)]
mod unit_price_tests {
use super::*;
use rust_decimal::dec;
#[test]
fn a_unit_price_is_not_capped_at_two_decimals() {
let p = UnitPriceAmount::new(dec!(10000.1234));
assert_eq!(p.to_string(), "10000.1234");
assert!(InvoiceAmount::from_decimal_exact(dec!(10000.1234)).is_err());
}
#[test]
fn metering_precision_survives() {
assert_eq!(UnitPriceAmount::new(dec!(0.28901)).to_string(), "0.28901");
}
#[test]
fn r046_derives_the_net_price_exactly() {
let gross = UnitPriceAmount::new(dec!(9.50));
let discount = UnitPriceAmount::new(dec!(1.00));
assert_eq!(
gross.checked_sub(discount).unwrap(),
UnitPriceAmount::new(dec!(8.50))
);
}
#[test]
fn negative_is_representable_because_br_27_is_a_rule() {
let p = UnitPriceAmount::new(dec!(-0.005));
assert!(
p.is_negative(),
"reportable as a BR-27 finding, not a parse error"
);
}
}