1use std::{fmt, ops::Mul, str::FromStr};
47
48use anyhow::format_err;
49use rust_decimal::Decimal;
50use serde::{Deserialize, Serialize};
51
52use crate::{dec, ln::amount::Amount};
53
54#[macro_export]
60macro_rules! ppm {
61 ($whole:tt . $frac:tt %) => {
62 const { $crate::ppm::Ppm::const_from_percent($crate::dec!($whole . $frac)) }
63 };
64 ($whole:tt %) => {
65 const { $crate::ppm::Ppm::const_from_percent($crate::dec!($whole)) }
66 };
67 ($amount:expr) => {
68 const { $crate::ppm::Ppm::new($amount) }
69 }
70}
71
72#[derive(Debug, thiserror::Error)]
74pub enum Error {
75 #[error("Ppm value is negative")]
76 Negative,
77 #[error("Ppm value exceeds 1_000_000")]
78 TooLarge,
79}
80
81#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
86#[derive(Serialize, Deserialize)]
87#[serde(try_from = "i32", into = "i32")]
88pub struct Ppm(i32);
89
90impl Ppm {
91 pub const MAX: Self = Self(1_000_000);
93
94 pub const ZERO: Self = Self(0);
96
97 #[inline]
104 pub const fn new(value: i32) -> Self {
105 assert!(value >= 0, "Ppm value must be non-negative");
106 assert!(value <= Self::MAX.0, "Ppm value must be <= 1_000_000");
107 Self(value)
108 }
109
110 #[doc(hidden)]
118 pub const fn const_from_percent(pct: Decimal) -> Self {
119 let lo = pct.mantissa() as u32;
121 let mid = 0;
122 let hi = 0;
123 Self::const_from_decimal(Decimal::from_parts(
124 lo,
125 mid,
126 hi,
127 false,
128 pct.scale() + 2,
129 ))
130 }
131
132 #[doc(hidden)]
140 const fn const_from_decimal(dec: Decimal) -> Self {
141 let scale = dec.scale();
142 if scale <= 6 {
144 let exp = 6 - scale;
145 let base = 10_i128.pow(exp);
146 let ppm = dec.mantissa() * base;
147 Ppm::new(ppm as i32)
148 } else {
149 let exp = scale - 6;
152 let base = 10_i128.pow(exp);
153 let mantissa = dec.mantissa();
154
155 assert!(mantissa % base == 0);
158
159 let ppm = mantissa / base;
160 Ppm::new(ppm as i32)
161 }
162 }
163
164 pub fn try_from_decimal(rate: Decimal) -> Result<Self, Error> {
171 use rust_decimal::prelude::ToPrimitive;
172
173 let ppm_dec = (rate * dec!(1_000_000)).round();
174 let ppm_i32 = ppm_dec.to_i32().ok_or(Error::TooLarge)?;
175 Self::try_from_inner(ppm_i32)
176 }
177
178 pub fn try_from_percent(pct: Decimal) -> Result<Self, Error> {
185 Self::try_from_decimal(pct / dec!(100))
186 }
187
188 #[inline]
190 pub const fn to_i32(self) -> i32 {
191 self.0
192 }
193
194 #[inline]
196 pub const fn to_u32(self) -> u32 {
197 self.0 as u32
198 }
199
200 #[inline]
204 pub const fn to_decimal(self) -> Decimal {
205 let lo = self.to_u32();
208 let mid = 0;
209 let hi = 0;
210 let negative = false;
211 let scale = 6;
212 Decimal::from_parts(lo, mid, hi, negative, scale)
213 }
214
215 #[inline]
219 pub const fn to_percent(self) -> Decimal {
220 let lo = self.to_u32();
221 let mid = 0;
222 let hi = 0;
223 let negative = false;
224 let scale = 4;
225 Decimal::from_parts(lo, mid, hi, negative, scale)
226 }
227
228 #[inline]
230 fn try_from_inner(value: i32) -> Result<Self, Error> {
231 if value < 0 {
232 Err(Error::Negative)
233 } else if value > Self::MAX.0 {
234 Err(Error::TooLarge)
235 } else {
236 Ok(Self(value))
237 }
238 }
239}
240
241impl fmt::Display for Ppm {
242 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243 fmt::Display::fmt(&self.0, f)
244 }
245}
246
247impl FromStr for Ppm {
248 type Err = anyhow::Error;
249
250 fn from_str(s: &str) -> Result<Self, Self::Err> {
251 let value = s.parse::<i32>().map_err(|err| format_err!("{err}"))?;
252 Ok(Self::try_from_inner(value)?)
253 }
254}
255
256impl From<u16> for Ppm {
259 #[inline]
261 fn from(value: u16) -> Self {
262 Self(i32::from(value))
263 }
264}
265
266impl From<Ppm> for i32 {
267 #[inline]
268 fn from(ppm: Ppm) -> Self {
269 ppm.0
270 }
271}
272
273impl From<Ppm> for u32 {
274 #[inline]
275 fn from(ppm: Ppm) -> Self {
276 ppm.0 as u32
277 }
278}
279
280impl From<Ppm> for i64 {
281 #[inline]
282 fn from(ppm: Ppm) -> Self {
283 i64::from(ppm.0)
284 }
285}
286
287impl From<Ppm> for u64 {
288 #[inline]
289 fn from(ppm: Ppm) -> Self {
290 ppm.0 as u64
291 }
292}
293
294impl TryFrom<i32> for Ppm {
297 type Error = Error;
298
299 #[inline]
300 fn try_from(value: i32) -> Result<Self, Self::Error> {
301 Self::try_from_inner(value)
302 }
303}
304
305impl TryFrom<u32> for Ppm {
306 type Error = Error;
307
308 fn try_from(value: u32) -> Result<Self, Self::Error> {
309 let value_i32 = i32::try_from(value).map_err(|_| Error::TooLarge)?;
310 Self::try_from_inner(value_i32)
311 }
312}
313
314impl TryFrom<Decimal> for Ppm {
315 type Error = Error;
316
317 #[inline]
318 fn try_from(rate: Decimal) -> Result<Self, Self::Error> {
319 Self::try_from_decimal(rate)
320 }
321}
322
323impl Mul<Ppm> for Amount {
331 type Output = Self;
332
333 #[inline]
334 fn mul(self, rhs: Ppm) -> Self::Output {
335 self * rhs.to_decimal()
336 }
337}
338
339impl Mul<Amount> for Ppm {
341 type Output = Amount;
342
343 #[inline]
344 fn mul(self, rhs: Amount) -> Self::Output {
345 rhs * self.to_decimal()
346 }
347}
348
349#[cfg(any(test, feature = "test-utils"))]
352impl proptest::arbitrary::Arbitrary for Ppm {
353 type Parameters = ();
354 type Strategy = proptest::strategy::BoxedStrategy<Self>;
355
356 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
357 use proptest::strategy::Strategy;
358 (0i32..=Self::MAX.0).prop_map(Self).boxed()
359 }
360}
361
362#[cfg(test)]
365mod test {
366 use proptest::{arbitrary::any, prop_assert, prop_assert_eq, proptest};
367
368 use super::*;
369
370 #[test]
371 fn const_construction() {
372 const TEST_PPM: Ppm = Ppm::new(3000);
374
375 assert_eq!(TEST_PPM.to_i32(), 3000);
376 assert_eq!(Ppm::ZERO.to_i32(), 0);
377 assert_eq!(Ppm::MAX.to_i32(), 1_000_000);
378 }
379
380 #[test]
381 fn macros() {
382 assert_eq!(ppm!(0), Ppm::ZERO);
383 assert_eq!(ppm!(1230), Ppm::new(1230));
384 assert_eq!(ppm!(1_000_000), Ppm::new(1_000_000));
385
386 assert_eq!(ppm!(0%), Ppm::ZERO);
387 assert_eq!(ppm!(0.0%), Ppm::ZERO);
388 assert_eq!(ppm!(0.123%), Ppm::new(1230));
389 assert_eq!(ppm!(0.1230%), Ppm::new(1230));
390 assert_eq!(ppm!(0.12300%), Ppm::new(1230));
391 assert_eq!(ppm!(0.3%), Ppm::new(3000));
392 assert_eq!(ppm!(1%), Ppm::new(10_000));
393 assert_eq!(ppm!(1.0%), Ppm::new(10_000));
394 assert_eq!(ppm!(50%), Ppm::new(500_000));
395 assert_eq!(ppm!(100%), Ppm::MAX);
396 assert_eq!(ppm!(100.0%), Ppm::MAX);
397 assert_eq!(ppm!(0.0001%), Ppm::new(1));
398 }
399
400 #[test]
401 fn to_decimal() {
402 assert_eq!(Ppm::ZERO.to_decimal(), dec!(0));
403 assert_eq!(Ppm::new(1).to_decimal(), dec!(0.000001));
404 assert_eq!(Ppm::new(1000).to_decimal(), dec!(0.001));
405 assert_eq!(Ppm::new(10_000).to_decimal(), dec!(0.01));
406 assert_eq!(Ppm::new(100_000).to_decimal(), dec!(0.1));
407 assert_eq!(Ppm::MAX.to_decimal(), dec!(1));
408 }
409
410 #[test]
411 fn to_percent() {
412 assert_eq!(Ppm::ZERO.to_percent(), dec!(0));
413 assert_eq!(Ppm::new(1).to_percent(), dec!(0.0001));
414 assert_eq!(Ppm::new(1000).to_percent(), dec!(0.1));
415 assert_eq!(Ppm::new(3000).to_percent(), dec!(0.3));
416 assert_eq!(Ppm::new(10_000).to_percent(), dec!(1));
417 assert_eq!(Ppm::new(100_000).to_percent(), dec!(10));
418 assert_eq!(Ppm::MAX.to_percent(), dec!(100));
419 }
420
421 #[test]
422 fn try_from_decimal() {
423 assert_eq!(Ppm::try_from(dec!(0)).unwrap(), Ppm::ZERO);
425 assert_eq!(Ppm::try_from(dec!(0.005)).unwrap(), Ppm::new(5000));
426 assert_eq!(Ppm::try_from(dec!(0.1)).unwrap(), Ppm::new(100_000));
427 assert_eq!(Ppm::try_from(dec!(1)).unwrap(), Ppm::MAX);
428
429 assert_eq!(Ppm::try_from(dec!(0.0000014)).unwrap(), Ppm::new(1));
431 assert_eq!(Ppm::try_from(dec!(0.0000016)).unwrap(), Ppm::new(2));
432
433 assert!(matches!(Ppm::try_from(dec!(-0.001)), Err(Error::Negative)));
435 assert!(matches!(
436 Ppm::try_from(dec!(1.000001)),
437 Err(Error::TooLarge)
438 ));
439 }
440
441 #[test]
442 fn try_from_rejects_invalid() {
443 assert!(matches!(Ppm::try_from(-1i32), Err(Error::Negative)));
444 assert!(matches!(Ppm::try_from(1_000_001i32), Err(Error::TooLarge)));
445 assert!(matches!(Ppm::try_from(1_000_001u32), Err(Error::TooLarge)));
446 }
447
448 #[test]
449 fn from_str() {
450 assert_eq!("0".parse::<Ppm>().unwrap(), Ppm::ZERO);
451 assert_eq!("3000".parse::<Ppm>().unwrap(), Ppm::new(3000));
452 assert_eq!("1000000".parse::<Ppm>().unwrap(), Ppm::MAX);
453
454 assert!("-1".parse::<Ppm>().is_err());
455 assert!("1000001".parse::<Ppm>().is_err());
456 assert!("abc".parse::<Ppm>().is_err());
457 }
458
459 #[test]
461 fn serde_json_format() {
462 #[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
463 struct Foo {
464 ppm: Ppm,
465 }
466
467 let foo = Foo {
468 ppm: Ppm::new(3000),
469 };
470 let json = serde_json::to_string(&foo).unwrap();
471 assert_eq!(json, r#"{"ppm":3000}"#);
472 let roundtrip: Foo = serde_json::from_str(&json).unwrap();
473 assert_eq!(foo, roundtrip);
474
475 assert!(serde_json::from_str::<Ppm>("-1").is_err());
477 assert!(serde_json::from_str::<Ppm>("1000001").is_err());
478 }
479
480 #[test]
481 fn proptest_integer_conversions() {
482 proptest!(|(ppm in any::<Ppm>(), val in any::<u16>())| {
483 let i = ppm.to_i32();
484
485 prop_assert_eq!(i32::from(ppm), i);
487 prop_assert_eq!(u32::from(ppm), i as u32);
488 prop_assert_eq!(i64::from(ppm), i64::from(i));
489 prop_assert_eq!(u64::from(ppm), i as u64);
490
491 prop_assert_eq!(Ppm::try_from(i).unwrap(), ppm);
493 prop_assert_eq!(Ppm::try_from(i as u32).unwrap(), ppm);
494
495 let from_u16 = Ppm::from(val);
497 prop_assert_eq!(from_u16.to_i32(), i32::from(val));
498 });
499 }
500
501 #[test]
502 fn proptest_mul_amount() {
503 proptest!(|(amount in any::<Amount>(), ppm in any::<Ppm>())| {
504 prop_assert_eq!(amount * ppm, ppm * amount);
506
507 prop_assert_eq!(amount * ppm, amount * ppm.to_decimal());
509 });
510 }
511
512 #[test]
513 fn proptest_serde_roundtrip() {
514 proptest!(|(ppm in any::<Ppm>())| {
515 let json = serde_json::to_string(&ppm).unwrap();
516 let roundtrip: Ppm = serde_json::from_str(&json).unwrap();
517 prop_assert_eq!(ppm, roundtrip);
518 });
519 }
520
521 #[test]
522 fn proptest_decimal_roundtrip() {
523 proptest!(|(ppm in any::<Ppm>())| {
524 let dec = ppm.to_decimal();
525
526 prop_assert!(dec >= Decimal::ZERO);
528 prop_assert!(dec <= Decimal::ONE);
529
530 prop_assert_eq!(ppm, Ppm::try_from(dec).unwrap());
532 prop_assert_eq!(ppm, Ppm::try_from_decimal(dec).unwrap());
533 prop_assert_eq!(ppm, Ppm::const_from_decimal(dec));
534 });
535 }
536
537 #[test]
538 fn proptest_percent_roundtrip() {
539 proptest!(|(ppm in any::<Ppm>())| {
540 let pct = ppm.to_percent();
541
542 prop_assert!(pct >= Decimal::ZERO);
544 prop_assert!(pct <= dec!(100));
545
546 prop_assert_eq!(ppm, Ppm::try_from_percent(pct).unwrap());
548 prop_assert_eq!(ppm, Ppm::const_from_percent(pct));
549
550 prop_assert_eq!(pct, ppm.to_decimal() * dec!(100.0));
551 });
552 }
553}