use rust_decimal::Decimal;
#[must_use]
pub fn extract_per_unit_price<T>(
units_number: Decimal,
annotation: Option<(bool, Decimal, T)>,
cost: Option<(Option<crate::CostNumber>, T)>,
) -> Option<(Decimal, T)> {
if let Some((is_total, amount, currency)) = annotation {
if is_total {
if !units_number.is_zero() {
return Some((amount / units_number.abs(), currency));
}
} else {
return Some((amount, currency));
}
}
if let Some((number, currency)) = cost {
match number {
Some(crate::CostNumber::PerUnit { value: per }) => {
return Some((per, currency));
}
Some(crate::CostNumber::PerUnitFromTotal(b)) => {
return Some((b.per_unit, currency));
}
Some(crate::CostNumber::Total { value: total }) if !units_number.is_zero() => {
return Some((total / units_number.abs(), currency));
}
Some(crate::CostNumber::Total { value: _ }) | None => {}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn unit_annotation_returns_amount_directly() {
let p = extract_per_unit_price(dec!(5), Some((false, dec!(1.40), "EUR")), None);
assert_eq!(p, Some((dec!(1.40), "EUR")));
}
#[test]
fn total_annotation_divides_by_unit_count() {
let p = extract_per_unit_price(dec!(10), Some((true, dec!(1500), "USD")), None);
assert_eq!(p, Some((dec!(150), "USD")));
}
#[test]
fn total_annotation_uses_abs_unit_count() {
let p = extract_per_unit_price(dec!(-27204.53), Some((true, dec!(15152.07), "EUR")), None);
let expected = dec!(15152.07) / dec!(27204.53);
assert_eq!(p, Some((expected, "EUR")));
assert!(p.unwrap().0 > dec!(0.55) && p.unwrap().0 < dec!(0.56));
}
#[test]
fn total_annotation_with_zero_units_falls_through_to_cost() {
let p = extract_per_unit_price(
dec!(0),
Some((true, dec!(100), "EUR")),
Some((Some(crate::CostNumber::PerUnit { value: dec!(50) }), "USD")),
);
assert_eq!(p, Some((dec!(50), "USD")));
}
#[test]
fn total_annotation_with_zero_units_and_no_cost_returns_none() {
let p = extract_per_unit_price::<&str>(dec!(0), Some((true, dec!(100), "EUR")), None);
assert_eq!(p, None);
}
#[test]
fn cost_per_unit_used_when_no_annotation() {
let p = extract_per_unit_price(
dec!(10),
None,
Some((
Some(crate::CostNumber::PerUnit { value: dec!(50.00) }),
"USD",
)),
);
assert_eq!(p, Some((dec!(50.00), "USD")));
}
#[test]
fn cost_total_divides_by_unit_count() {
let p = extract_per_unit_price(
dec!(10),
None,
Some((Some(crate::CostNumber::Total { value: dec!(500) }), "USD")),
);
assert_eq!(p, Some((dec!(50), "USD")));
}
#[test]
fn cost_total_with_zero_units_returns_none() {
let p = extract_per_unit_price::<&str>(
dec!(0),
None,
Some((Some(crate::CostNumber::Total { value: dec!(500) }), "USD")),
);
assert_eq!(p, None);
}
#[test]
fn annotation_wins_over_cost_when_both_present() {
let p = extract_per_unit_price(
dec!(5),
Some((false, dec!(1.40), "EUR")),
Some((
Some(crate::CostNumber::PerUnit { value: dec!(1.25) }),
"EUR",
)),
);
assert_eq!(p, Some((dec!(1.40), "EUR")));
}
#[test]
fn total_annotation_wins_over_cost_per_unit() {
let p = extract_per_unit_price(
dec!(-10),
Some((true, dec!(14), "EUR")),
Some((
Some(crate::CostNumber::PerUnit { value: dec!(1.25) }),
"EUR",
)),
);
assert_eq!(p, Some((dec!(1.4), "EUR")));
}
#[test]
fn no_inputs_returns_none() {
let p = extract_per_unit_price::<&str>(dec!(10), None, None);
assert_eq!(p, None);
}
#[test]
fn annotation_without_amount_falls_through_to_cost() {
let p = extract_per_unit_price(
dec!(10),
None,
Some((Some(crate::CostNumber::PerUnit { value: dec!(7) }), "USD")),
);
assert_eq!(p, Some((dec!(7), "USD")));
}
}