#[cfg(test)]
mod test;
#[cfg(test)]
mod test_round_to_ocpi;
#[cfg(test)]
mod test_parse_string;
use std::{fmt, num::IntErrorKind};
use rust_decimal::Decimal;
use crate::{
into_caveat, json,
warning::{self, GatherWarnings, IntoCaveat},
};
pub const SCALE: u32 = 4;
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum Warning {
ContainsEscapeCodes,
Decimal(String),
Decode(json::decode::Warning),
ExceedsMaximumPossibleValue,
ExcessivePrecision,
InvalidType { type_found: json::ValueKind },
LessThanMinimumPossibleValue,
Underflow,
}
impl Warning {
fn invalid_type(elem: &json::Element<'_>) -> Self {
Self::InvalidType {
type_found: elem.value().kind(),
}
}
}
impl From<json::decode::Warning> for Warning {
fn from(warning: json::decode::Warning) -> Self {
Self::Decode(warning)
}
}
impl fmt::Display for Warning {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ContainsEscapeCodes => f.write_str("The value contains escape codes but it does not need them"),
Self::Decimal(msg) => write!(f, "{msg}"),
Self::Decode(warning) => fmt::Display::fmt(warning, f),
Self::ExcessivePrecision => f.write_str("The number given has more than the four decimal precision required by the OCPI spec."),
Self::InvalidType { type_found } => {
write!(f, "The value should be a number but is `{type_found}`.")
}
Self::ExceedsMaximumPossibleValue => {
f.write_str("The value provided exceeds `79,228,162,514,264,337,593,543,950,335`.")
}
Self::LessThanMinimumPossibleValue => f.write_str("The value provided is less than `-79,228,162,514,264,337,593,543,950,335`."),
Self::Underflow => f.write_str("An underflow is when there are more than 28 fractional digits"),
}
}
}
impl crate::Warning for Warning {
fn id(&self) -> warning::Id {
match self {
Self::ContainsEscapeCodes => warning::Id::from_static("contains_escape_codes"),
Self::Decimal(_) => warning::Id::from_static("decimal"),
Self::Decode(warning) => warning.id(),
Self::ExcessivePrecision => warning::Id::from_static("excessive_precision"),
Self::InvalidType { .. } => warning::Id::from_static("invalid_type"),
Self::ExceedsMaximumPossibleValue => {
warning::Id::from_static("exceeds_maximum_possible_value")
}
Self::LessThanMinimumPossibleValue => {
warning::Id::from_static("less_than_minimum_possible_value")
}
Self::Underflow => warning::Id::from_static("underflow"),
}
}
}
pub(crate) fn int_error_kind_as_str(kind: IntErrorKind) -> &'static str {
match kind {
IntErrorKind::Empty => "empty",
IntErrorKind::InvalidDigit => "invalid digit",
IntErrorKind::PosOverflow => "positive overflow",
IntErrorKind::NegOverflow => "negative overflow",
IntErrorKind::Zero => "zero",
_ => "unknown",
}
}
into_caveat!(Decimal);
impl json::FromJson<'_, '_> for Decimal {
type Warning = Warning;
fn from_json(elem: &json::Element<'_>) -> crate::Verdict<Self, Self::Warning> {
let mut warnings = warning::Set::new();
let value = elem.as_value();
let s = if let Some(s) = value.as_number() {
s
} else {
let Some(raw_str) = value.as_raw_str() else {
return warnings.bail(Warning::invalid_type(elem), elem);
};
let pending_str = raw_str
.has_escapes(elem)
.gather_warnings_into(&mut warnings);
match pending_str {
json::decode::PendingStr::NoEscapes(s) => s,
json::decode::PendingStr::HasEscapes(_) => {
return warnings.bail(Warning::ContainsEscapeCodes, elem);
}
}
};
let mut decimal = match Decimal::from_str_exact(s) {
Ok(v) => v,
Err(err) => {
let kind = match err {
rust_decimal::Error::ExceedsMaximumPossibleValue => {
Warning::ExceedsMaximumPossibleValue
}
rust_decimal::Error::LessThanMinimumPossibleValue => {
Warning::LessThanMinimumPossibleValue
}
rust_decimal::Error::Underflow => Warning::Underflow,
rust_decimal::Error::ConversionTo(_) => {
unreachable!("This is only triggered when converting to numerical types")
}
rust_decimal::Error::ErrorString(msg) => Warning::Decimal(msg),
rust_decimal::Error::ScaleExceedsMaximumPrecision(_) => {
unreachable!("`Decimal::from_str_exact` uses a scale of zero")
}
};
return warnings.bail(kind, elem);
}
};
if decimal.scale() > SCALE {
warnings.insert(Warning::ExcessivePrecision, elem);
}
decimal.rescale(SCALE);
Ok(decimal.into_caveat(warnings))
}
}
pub(crate) trait FromDecimal {
fn from_decimal(d: Decimal) -> Self;
}
impl FromDecimal for Decimal {
fn from_decimal(mut d: Decimal) -> Self {
d.rescale(SCALE);
d
}
}
pub trait RoundDecimal {
#[must_use]
fn round_to_ocpi_scale(self) -> Self;
}
impl RoundDecimal for Decimal {
fn round_to_ocpi_scale(self) -> Self {
self.round_dp_with_strategy(SCALE, rust_decimal::RoundingStrategy::MidpointNearestEven)
}
}
impl<T: RoundDecimal> RoundDecimal for Option<T> {
fn round_to_ocpi_scale(self) -> Self {
self.map(RoundDecimal::round_to_ocpi_scale)
}
}
#[doc(hidden)]
#[macro_export]
macro_rules! impl_dec_newtype {
($kind:ident, $unit:literal) => {
impl $kind {
#[must_use]
pub fn zero() -> Self {
use num_traits::Zero as _;
Self(Decimal::zero())
}
pub fn is_zero(&self) -> bool {
self.0.is_zero()
}
#[must_use]
pub fn rescale(mut self) -> Self {
self.0.rescale(number::SCALE);
Self(self.0)
}
#[must_use]
pub fn round_dp(self, digits: u32) -> Self {
Self(self.0.round_dp(digits))
}
}
impl $crate::number::FromDecimal for $kind {
fn from_decimal(mut d: Decimal) -> Self {
d.rescale($crate::number::SCALE);
Self(d)
}
}
impl $crate::number::RoundDecimal for $kind {
fn round_to_ocpi_scale(self) -> Self {
Self(self.0.round_to_ocpi_scale())
}
}
impl std::fmt::Display for $kind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.0.is_zero() {
if f.alternate() {
write!(f, "0")
} else {
write!(f, "0{}", $unit)
}
} else {
if f.alternate() {
write!(f, "{:.4}", self.0)
} else {
write!(f, "{:.4}{}", self.0, $unit)
}
}
}
}
impl From<$kind> for rust_decimal::Decimal {
fn from(value: $kind) -> Self {
value.0
}
}
#[cfg(test)]
impl From<u64> for $kind {
fn from(value: u64) -> Self {
Self(value.into())
}
}
#[cfg(test)]
impl From<f64> for $kind {
fn from(value: f64) -> Self {
Self(Decimal::from_f64_retain(value).unwrap())
}
}
#[cfg(test)]
impl From<rust_decimal::Decimal> for $kind {
fn from(value: rust_decimal::Decimal) -> Self {
Self(value)
}
}
impl $crate::SaturatingAdd for $kind {
fn saturating_add(self, other: Self) -> Self {
Self(self.0.saturating_add(other.0))
}
}
impl $crate::SaturatingSub for $kind {
fn saturating_sub(self, other: Self) -> Self {
Self(self.0.saturating_sub(other.0))
}
}
impl $crate::json::FromJson<'_, '_> for $kind {
type Warning = $crate::number::Warning;
fn from_json(elem: &json::Element<'_>) -> $crate::Verdict<Self, Self::Warning> {
rust_decimal::Decimal::from_json(elem).map(|v| v.map(Self))
}
}
#[cfg(test)]
impl $crate::test::ApproxEq for $kind {
type Tolerance = Decimal;
fn default_tolerance() -> Self::Tolerance {
rust_decimal_macros::dec!(0.1)
}
fn approx_eq_tolerance(&self, other: &Self, tolerance: Decimal) -> bool {
const PRECISION: u32 = 2;
$crate::test::approx_eq_dec(&self.0, &other.0, tolerance, PRECISION)
}
}
};
}