1#[cfg(test)]
6pub mod test;
7
8#[cfg(test)]
9mod test_approx_eq;
10
11#[cfg(test)]
12mod test_round_to_ocpi;
13
14#[cfg(test)]
15mod test_string_encoded;
16
17#[cfg(test)]
18mod test_from_schema;
19
20use std::{fmt, num::IntErrorKind};
21
22use rust_decimal::Decimal;
23
24use crate::{
25 json, schema,
26 warning::{self, GatherWarnings as _, IntoCaveat as _},
27 FromSchema,
28};
29
30pub const SCALE: u32 = 4;
34
35#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
37pub enum Warning {
38 ContainsEscapeCodes,
40
41 Decimal(String),
43
44 Decode(json::decode::Warning),
46
47 ExceedsMaximumPossibleValue,
49
50 ExcessivePrecision,
52
53 LessThanMinimumPossibleValue,
55
56 Underflow,
58}
59
60impl From<json::decode::Warning> for Warning {
61 fn from(warning: json::decode::Warning) -> Self {
62 Self::Decode(warning)
63 }
64}
65
66impl fmt::Display for Warning {
67 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 match self {
69 Self::ContainsEscapeCodes => f.write_str("The value contains escape codes but it does not need them"),
70 Self::Decimal(msg) => write!(f, "{msg}"),
71 Self::Decode(warning) => fmt::Display::fmt(warning, f),
72 Self::ExcessivePrecision => f.write_str("The number given has more than the four decimal precision required by the OCPI spec."),
73 Self::ExceedsMaximumPossibleValue => {
74 f.write_str("The value provided exceeds `79,228,162,514,264,337,593,543,950,335`.")
75 }
76 Self::LessThanMinimumPossibleValue => f.write_str("The value provided is less than `-79,228,162,514,264,337,593,543,950,335`."),
77 Self::Underflow => f.write_str("An underflow is when there are more than 28 fractional digits"),
78 }
79 }
80}
81
82impl crate::Warning for Warning {
83 fn id(&self) -> warning::Id {
84 match self {
85 Self::ContainsEscapeCodes => warning::Id::from_static("contains_escape_codes"),
86 Self::Decimal(_) => warning::Id::from_static("decimal"),
87 Self::Decode(warning) => warning.id(),
88 Self::ExcessivePrecision => warning::Id::from_static("excessive_precision"),
89 Self::ExceedsMaximumPossibleValue => {
90 warning::Id::from_static("exceeds_maximum_possible_value")
91 }
92 Self::LessThanMinimumPossibleValue => {
93 warning::Id::from_static("less_than_minimum_possible_value")
94 }
95 Self::Underflow => warning::Id::from_static("underflow"),
96 }
97 }
98}
99
100pub(crate) fn int_error_kind_as_str(kind: IntErrorKind) -> &'static str {
101 match kind {
102 IntErrorKind::Empty => "empty",
103 IntErrorKind::InvalidDigit => "invalid digit",
104 IntErrorKind::PosOverflow => "positive overflow",
105 IntErrorKind::NegOverflow => "negative overflow",
106 IntErrorKind::Zero => "zero",
107 _ => "unknown",
108 }
109}
110
111impl<'buf> FromSchema<'buf, schema::Number<'buf>> for Decimal {
112 type Warning = Warning;
113
114 fn from_schema(source: &schema::Number<'buf>) -> crate::Verdict<Self, Self::Warning> {
115 let mut warnings = warning::Set::new();
116
117 let (elem, digits) = match source {
121 schema::Number::Number { elem, digits } => (elem, *digits),
122 schema::Number::StringEncoded { elem, value } => {
123 let pending_str = value.has_escapes(elem).gather_warnings_into(&mut warnings);
124
125 match pending_str {
126 json::PendingStr::NoEscapes(s) => (elem, s),
127 json::PendingStr::HasEscapes(_) => {
128 return warnings.bail(elem, Warning::ContainsEscapeCodes);
129 }
130 }
131 }
132 };
133
134 let decimal = match Decimal::from_str_exact(digits) {
135 Ok(v) => v,
136 Err(err) => {
137 let kind = match err {
138 rust_decimal::Error::ExceedsMaximumPossibleValue => {
139 Warning::ExceedsMaximumPossibleValue
140 }
141 rust_decimal::Error::LessThanMinimumPossibleValue => {
142 Warning::LessThanMinimumPossibleValue
143 }
144 rust_decimal::Error::Underflow => Warning::Underflow,
145 rust_decimal::Error::ConversionTo(_) => {
146 unreachable!("This is only triggered when converting to numerical types")
147 }
148 rust_decimal::Error::ErrorString(msg) => Warning::Decimal(msg),
149 rust_decimal::Error::ScaleExceedsMaximumPrecision(_) => {
150 unreachable!("`Decimal::from_str_exact` uses a scale of zero")
151 }
152 };
153
154 return warnings.bail(elem, kind);
155 }
156 };
157
158 if decimal.scale() > SCALE {
159 warnings.insert(elem, Warning::ExcessivePrecision);
160 }
161
162 Ok(decimal.into_caveat(warnings))
163 }
164}
165
166pub(crate) trait FromDecimal {
167 fn from_decimal(d: Decimal) -> Self;
168}
169
170impl FromDecimal for Decimal {
174 fn from_decimal(mut d: Decimal) -> Self {
175 d.rescale(SCALE);
176 d
177 }
178}
179
180pub trait RoundDecimal {
184 #[must_use]
188 fn round_to_ocpi_scale(self) -> Self;
189}
190
191impl RoundDecimal for Decimal {
192 fn round_to_ocpi_scale(self) -> Self {
193 self.round_dp_with_strategy(SCALE, rust_decimal::RoundingStrategy::MidpointNearestEven)
194 }
195}
196
197impl<T: RoundDecimal> RoundDecimal for Option<T> {
198 fn round_to_ocpi_scale(self) -> Self {
199 self.map(RoundDecimal::round_to_ocpi_scale)
200 }
201}
202
203#[allow(
208 dead_code,
209 reason = "Kept for the tariff linter being rebuilt on the schema IR; see `docs/lint-catalogue.md`"
210)]
211pub(crate) trait IsZero {
212 fn is_zero(&self) -> bool;
214}
215
216#[allow(
218 dead_code,
219 reason = "Kept for the tariff linter being rebuilt on the schema IR; see `docs/lint-catalogue.md`"
220)]
221pub(crate) fn approx_eq_dec(a: &Decimal, b: &Decimal, tolerance: Decimal) -> bool {
222 let Some(diff) = a.checked_sub(*b) else {
225 return false;
226 };
227 diff.abs() <= tolerance
229}
230
231#[doc(hidden)]
238#[macro_export]
239macro_rules! impl_dec_newtype {
240 ($kind:ident, $unit:literal) => {
241 impl $kind {
242 #[must_use]
244 pub fn rescale(mut self) -> Self {
245 self.0.rescale(number::SCALE);
246 Self(self.0)
247 }
248
249 #[must_use]
250 pub fn round_dp(self, digits: u32) -> Self {
251 Self(self.0.round_dp(digits))
252 }
253 }
254
255 impl $crate::number::FromDecimal for $kind {
256 fn from_decimal(d: Decimal) -> Self {
257 Self(d)
258 }
259 }
260
261 impl $crate::number::RoundDecimal for $kind {
262 fn round_to_ocpi_scale(self) -> Self {
263 Self(self.0.round_to_ocpi_scale())
264 }
265 }
266
267 impl std::fmt::Display for $kind {
268 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
269 if self.0.is_zero() {
271 if f.alternate() {
272 write!(f, "0")
273 } else {
274 write!(f, "0{}", $unit)
275 }
276 } else {
277 if f.alternate() {
278 write!(f, "{:.4}", self.0)
279 } else {
280 write!(f, "{:.4}{}", self.0, $unit)
281 }
282 }
283 }
284 }
285
286 impl From<$kind> for rust_decimal::Decimal {
289 fn from(value: $kind) -> Self {
290 value.0
291 }
292 }
293
294 #[cfg(test)]
295 impl From<u64> for $kind {
296 fn from(value: u64) -> Self {
297 Self(value.into())
298 }
299 }
300
301 #[cfg(test)]
302 impl From<f64> for $kind {
303 fn from(value: f64) -> Self {
304 Self(Decimal::from_f64_retain(value).unwrap())
305 }
306 }
307
308 #[cfg(test)]
309 impl From<rust_decimal::Decimal> for $kind {
310 fn from(value: rust_decimal::Decimal) -> Self {
311 Self(value)
312 }
313 }
314
315 impl $crate::SaturatingAdd for $kind {
316 fn saturating_add(self, other: Self) -> Self {
317 Self(self.0.saturating_add(other.0))
318 }
319 }
320
321 impl $crate::SaturatingSub for $kind {
322 fn saturating_sub(self, other: Self) -> Self {
323 Self(self.0.saturating_sub(other.0))
324 }
325 }
326
327 impl<'buf> $crate::FromSchema<'buf, $crate::schema::Number<'buf>> for $kind {
328 type Warning = $crate::number::Warning;
329
330 fn from_schema(
331 source: &$crate::schema::Number<'buf>,
332 ) -> $crate::Verdict<Self, Self::Warning> {
333 let decimal: $crate::Verdict<rust_decimal::Decimal, $crate::number::Warning> =
334 $crate::FromSchema::from_schema(source);
335 decimal.map(|v| v.map(Self))
336 }
337 }
338
339 #[cfg(test)]
340 impl $crate::test::ApproxEq for $kind {
341 type Tolerance = Decimal;
342
343 fn default_tolerance() -> Self::Tolerance {
344 rust_decimal_macros::dec!(0.1)
345 }
346
347 fn approx_eq_tolerance(&self, other: &Self, tolerance: Decimal) -> bool {
348 $crate::number::approx_eq_dec(&self.0, &other.0, tolerance)
349 }
350 }
351 };
352}