use rust_decimal::RoundingStrategy;
use crate::types::Number;
#[cfg(test)]
use crate::types::Validate as _;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct PricingPolicy {
pub component_decimals: u32,
pub currency_decimals: u32,
pub rounding: RoundingStrategy,
pub quantity_decimals: u32,
pub quantisation: Quantisation,
}
impl Default for PricingPolicy {
fn default() -> Self {
Self {
component_decimals: 4,
currency_decimals: 2,
quantity_decimals: 6,
rounding: RoundingStrategy::MidpointAwayFromZero,
quantisation: Quantisation::StepSize,
}
}
}
impl PricingPolicy {
#[must_use]
pub fn zero_decimal_currency() -> Self {
Self { currency_decimals: 0, ..Self::default() }
}
#[must_use]
pub fn without_step_size(mut self) -> Self {
self.quantisation = Quantisation::None;
self
}
#[must_use]
pub fn round_component(&self, value: Number) -> Number {
Number::new(value.get().round_dp_with_strategy(self.component_decimals, self.rounding))
}
#[must_use]
pub fn round_currency(&self, value: Number) -> Number {
Number::new(value.get().round_dp_with_strategy(self.currency_decimals, self.rounding))
}
#[must_use]
pub fn round_quantity(&self, value: Number) -> Number {
Number::new(value.get().round_dp_with_strategy(self.quantity_decimals, self.rounding))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Quantisation {
StepSize,
None,
}
impl Quantisation {
#[must_use]
pub fn apply(self, quantity: Number, step_size: u32, unit_scale: u32) -> Number {
if self == Self::None || step_size == 0 {
return quantity;
}
let scale = Number::from(unit_scale);
let step = Number::from(step_size);
let in_units = quantity * scale;
let blocks = (in_units / step).get().ceil();
Number::new(blocks) * step / scale
}
}
#[cfg(test)]
mod tests {
use super::*;
fn n(s: &str) -> Number {
s.parse().unwrap()
}
#[test]
fn energy_is_quantised_in_watt_hours() {
assert_eq!(Quantisation::StepSize.apply(n("0.1152"), 1, 1000), n("0.116"));
assert_eq!(Quantisation::StepSize.apply(n("0.1152"), 25, 1000), n("0.125"));
assert_eq!(Quantisation::StepSize.apply(n("0.1152"), 500, 1000), n("0.5"));
}
#[test]
fn time_is_quantised_in_seconds() {
let eight_minutes = n("8") / n("60");
let billed = Quantisation::StepSize.apply(eight_minutes, 300, 3600);
assert_eq!(billed, n("10") / n("60"));
assert_eq!(Quantisation::StepSize.apply(n("5.4"), 500, 1000), n("5.5"));
}
#[test]
fn an_exact_multiple_is_left_alone() {
assert_eq!(Quantisation::StepSize.apply(n("5.5"), 500, 1000), n("5.5"));
assert_eq!(Quantisation::StepSize.apply(n("2"), 1, 1000), n("2"));
}
#[test]
fn a_step_size_of_zero_quantises_nothing_but_a_step_size_of_one_is_real() {
assert_eq!(Quantisation::StepSize.apply(n("0.1152"), 0, 1000), n("0.1152"));
assert_eq!(Quantisation::StepSize.apply(n("0.1152"), 1, 1000), n("0.116"));
}
#[test]
fn disabling_quantisation_bills_the_measured_amount() {
assert_eq!(Quantisation::None.apply(n("5.4"), 500, 1000), n("5.4"));
let policy = PricingPolicy::default().without_step_size();
assert_eq!(policy.quantisation, Quantisation::None);
}
#[test]
fn a_reported_quantity_survives_being_written_down() {
let p = PricingPolicy::default();
let eight_minutes = n("8") / n("60");
assert!(!eight_minutes.json_round_trips(), "the raw quantity does not");
let reported = p.round_quantity(eight_minutes);
assert_eq!(reported, n("0.133333"));
assert!(reported.json_round_trips());
assert!(reported.validate().is_ok());
}
#[test]
fn rounding_half_goes_away_from_zero_by_default() {
let p = PricingPolicy::default();
assert_eq!(p.round_currency(n("2.005")), n("2.01"));
assert_eq!(p.round_currency(n("-2.005")), n("-2.01"));
assert_eq!(p.round_component(n("0.00005")), n("0.0001"));
assert_eq!(PricingPolicy::zero_decimal_currency().round_currency(n("2.5")), n("3"));
}
}