use core::fmt;
use core::str::FromStr;
use rust_decimal::Decimal;
#[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 Percentage(Decimal);
impl Percentage {
pub const ZERO: Self = Self(Decimal::ZERO);
#[must_use]
pub const fn new(percent: Decimal) -> Self {
Self(percent)
}
#[must_use]
pub fn from_fraction(fraction: Decimal) -> Option<Self> {
fraction
.checked_mul(Decimal::ONE_HUNDRED)
.map(|d| Self(d.normalize()))
}
#[must_use]
pub const fn into_decimal(self) -> Decimal {
self.0
}
#[must_use]
pub fn as_fraction(self) -> Decimal {
self.0 / Decimal::ONE_HUNDRED
}
#[must_use]
pub fn is_zero(self) -> bool {
self.0.is_zero()
}
#[must_use]
pub fn is_positive(self) -> bool {
self.0 > Decimal::ZERO
}
#[must_use]
pub fn is_negative(self) -> bool {
self.0 < Decimal::ZERO
}
#[must_use]
pub fn normalized(self) -> Self {
Self(self.0.normalize())
}
}
impl fmt::Display for Percentage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.pad(&self.0.normalize().to_string())
}
}
impl From<Decimal> for Percentage {
fn from(d: Decimal) -> Self {
Self(d)
}
}
impl FromStr for Percentage {
type Err = rust_decimal::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Decimal::from_str_exact(s).map(Self)
}
}
#[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 Quantity(Decimal);
impl Quantity {
pub const ZERO: Self = Self(Decimal::ZERO);
pub const ONE: Self = Self(Decimal::ONE);
#[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 is_positive(self) -> bool {
self.0 > Decimal::ZERO
}
#[must_use]
pub fn is_zero(self) -> bool {
self.0.is_zero()
}
#[must_use]
pub fn checked_neg(self) -> Option<Self> {
Decimal::ZERO.checked_sub(self.0).map(Self)
}
}
impl fmt::Display for Quantity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.pad(&self.0.normalize().to_string())
}
}
impl From<Decimal> for Quantity {
fn from(d: Decimal) -> Self {
Self(d)
}
}
impl FromStr for Quantity {
type Err = rust_decimal::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Decimal::from_str_exact(s).map(Self)
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal::dec;
#[test]
fn percentage_is_per_cent_not_a_fraction() {
let p = Percentage::new(dec!(34.78));
assert_eq!(p.to_string(), "34.78");
assert_eq!(p.as_fraction(), dec!(0.3478));
assert_eq!(
Percentage::from_fraction(dec!(0.19)).unwrap(),
Percentage::new(dec!(19))
);
}
#[test]
fn percentage_display_strips_trailing_zeros() {
assert_eq!(Percentage::new(dec!(19.00)).to_string(), "19");
assert_eq!(Percentage::new(dec!(7.50)).to_string(), "7.5");
assert_eq!(Percentage::ZERO.to_string(), "0");
}
#[test]
fn a_rates_scale_is_not_part_of_its_identity() {
use std::collections::HashMap;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let a = Percentage::new(dec!(19));
let b = Percentage::new(dec!(19.00));
assert_eq!(a, b, "Peppol: trailing zeros are not significant");
assert_eq!(a.cmp(&b), core::cmp::Ordering::Equal);
let hash = |p: &Percentage| {
let mut h = DefaultHasher::new();
p.hash(&mut h);
h.finish()
};
assert_eq!(
hash(&a),
hash(&b),
"Hash must agree with Eq or grouping breaks"
);
let mut groups: HashMap<(super::super::VatCategory, Percentage), u32> = HashMap::new();
*groups
.entry((super::super::VatCategory::Standard, a))
.or_default() += 1;
*groups
.entry((super::super::VatCategory::Standard, b))
.or_default() += 1;
assert_eq!(groups.len(), 1, "19 and 19.00 are one VAT breakdown group");
assert_eq!(b.normalized().to_string(), "19");
}
#[test]
fn rate_predicates_match_the_category_rules() {
assert!(Percentage::new(dec!(19)).is_positive()); assert!(Percentage::ZERO.is_zero()); assert!(!Percentage::ZERO.is_positive());
assert!(Percentage::new(dec!(-1)).is_negative()); }
#[test]
fn quantity_may_be_negative() {
let q = Quantity::new(dec!(-10));
assert!(q.is_negative());
assert_eq!(q.to_string(), "-10");
assert_eq!(Quantity::new(dec!(10)).checked_neg().unwrap(), q);
}
#[test]
fn quantity_one_is_exact_for_a_flat_charge() {
assert_eq!(Quantity::ONE.into_decimal(), Decimal::ONE);
assert_eq!(Quantity::ONE.into_decimal() * dec!(8.50), dec!(8.50));
}
#[test]
fn quantity_keeps_metering_precision() {
let q = Quantity::new(dec!(1234.567));
assert_eq!(q.to_string(), "1234.567");
}
}