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 use crate::ppm;
370
371 #[test]
372 fn const_construction() {
373 const TEST_PPM: Ppm = Ppm::new(3000);
375
376 assert_eq!(TEST_PPM.to_i32(), 3000);
377 assert_eq!(Ppm::ZERO.to_i32(), 0);
378 assert_eq!(Ppm::MAX.to_i32(), 1_000_000);
379 }
380
381 #[test]
382 fn macros() {
383 assert_eq!(ppm!(0), Ppm::ZERO);
384 assert_eq!(ppm!(1230), Ppm::new(1230));
385 assert_eq!(ppm!(1_000_000), Ppm::new(1_000_000));
386
387 assert_eq!(ppm!(0%), Ppm::ZERO);
388 assert_eq!(ppm!(0.0%), Ppm::ZERO);
389 assert_eq!(ppm!(0.123%), Ppm::new(1230));
390 assert_eq!(ppm!(0.1230%), Ppm::new(1230));
391 assert_eq!(ppm!(0.12300%), Ppm::new(1230));
392 assert_eq!(ppm!(0.3%), Ppm::new(3000));
393 assert_eq!(ppm!(1%), Ppm::new(10_000));
394 assert_eq!(ppm!(1.0%), Ppm::new(10_000));
395 assert_eq!(ppm!(50%), Ppm::new(500_000));
396 assert_eq!(ppm!(100%), Ppm::MAX);
397 assert_eq!(ppm!(100.0%), Ppm::MAX);
398 assert_eq!(ppm!(0.0001%), Ppm::new(1));
399 }
400
401 #[test]
402 fn to_decimal() {
403 assert_eq!(Ppm::ZERO.to_decimal(), dec!(0));
404 assert_eq!(Ppm::new(1).to_decimal(), dec!(0.000001));
405 assert_eq!(Ppm::new(1000).to_decimal(), dec!(0.001));
406 assert_eq!(Ppm::new(10_000).to_decimal(), dec!(0.01));
407 assert_eq!(Ppm::new(100_000).to_decimal(), dec!(0.1));
408 assert_eq!(Ppm::MAX.to_decimal(), dec!(1));
409 }
410
411 #[test]
412 fn to_percent() {
413 assert_eq!(Ppm::ZERO.to_percent(), dec!(0));
414 assert_eq!(Ppm::new(1).to_percent(), dec!(0.0001));
415 assert_eq!(Ppm::new(1000).to_percent(), dec!(0.1));
416 assert_eq!(Ppm::new(3000).to_percent(), dec!(0.3));
417 assert_eq!(Ppm::new(10_000).to_percent(), dec!(1));
418 assert_eq!(Ppm::new(100_000).to_percent(), dec!(10));
419 assert_eq!(Ppm::MAX.to_percent(), dec!(100));
420 }
421
422 #[test]
423 fn try_from_decimal() {
424 assert_eq!(Ppm::try_from(dec!(0)).unwrap(), Ppm::ZERO);
426 assert_eq!(Ppm::try_from(dec!(0.005)).unwrap(), Ppm::new(5000));
427 assert_eq!(Ppm::try_from(dec!(0.1)).unwrap(), Ppm::new(100_000));
428 assert_eq!(Ppm::try_from(dec!(1)).unwrap(), Ppm::MAX);
429
430 assert_eq!(Ppm::try_from(dec!(0.0000014)).unwrap(), Ppm::new(1));
432 assert_eq!(Ppm::try_from(dec!(0.0000016)).unwrap(), Ppm::new(2));
433
434 assert!(matches!(Ppm::try_from(dec!(-0.001)), Err(Error::Negative)));
436 assert!(matches!(
437 Ppm::try_from(dec!(1.000001)),
438 Err(Error::TooLarge)
439 ));
440 }
441
442 #[test]
443 fn try_from_rejects_invalid() {
444 assert!(matches!(Ppm::try_from(-1i32), Err(Error::Negative)));
445 assert!(matches!(Ppm::try_from(1_000_001i32), Err(Error::TooLarge)));
446 assert!(matches!(Ppm::try_from(1_000_001u32), Err(Error::TooLarge)));
447 }
448
449 #[test]
450 fn from_str() {
451 assert_eq!("0".parse::<Ppm>().unwrap(), Ppm::ZERO);
452 assert_eq!("3000".parse::<Ppm>().unwrap(), Ppm::new(3000));
453 assert_eq!("1000000".parse::<Ppm>().unwrap(), Ppm::MAX);
454
455 assert!("-1".parse::<Ppm>().is_err());
456 assert!("1000001".parse::<Ppm>().is_err());
457 assert!("abc".parse::<Ppm>().is_err());
458 }
459
460 #[test]
462 fn serde_json_format() {
463 #[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
464 struct Foo {
465 ppm: Ppm,
466 }
467
468 let foo = Foo {
469 ppm: Ppm::new(3000),
470 };
471 let json = serde_json::to_string(&foo).unwrap();
472 assert_eq!(json, r#"{"ppm":3000}"#);
473 let roundtrip: Foo = serde_json::from_str(&json).unwrap();
474 assert_eq!(foo, roundtrip);
475
476 assert!(serde_json::from_str::<Ppm>("-1").is_err());
478 assert!(serde_json::from_str::<Ppm>("1000001").is_err());
479 }
480
481 #[test]
482 fn proptest_integer_conversions() {
483 proptest!(|(ppm in any::<Ppm>(), val in any::<u16>())| {
484 let i = ppm.to_i32();
485
486 prop_assert_eq!(i32::from(ppm), i);
488 prop_assert_eq!(u32::from(ppm), i as u32);
489 prop_assert_eq!(i64::from(ppm), i64::from(i));
490 prop_assert_eq!(u64::from(ppm), i as u64);
491
492 prop_assert_eq!(Ppm::try_from(i).unwrap(), ppm);
494 prop_assert_eq!(Ppm::try_from(i as u32).unwrap(), ppm);
495
496 let from_u16 = Ppm::from(val);
498 prop_assert_eq!(from_u16.to_i32(), i32::from(val));
499 });
500 }
501
502 #[test]
503 fn proptest_mul_amount() {
504 proptest!(|(amount in any::<Amount>(), ppm in any::<Ppm>())| {
505 prop_assert_eq!(amount * ppm, ppm * amount);
507
508 prop_assert_eq!(amount * ppm, amount * ppm.to_decimal());
510 });
511 }
512
513 #[test]
514 fn proptest_serde_roundtrip() {
515 proptest!(|(ppm in any::<Ppm>())| {
516 let json = serde_json::to_string(&ppm).unwrap();
517 let roundtrip: Ppm = serde_json::from_str(&json).unwrap();
518 prop_assert_eq!(ppm, roundtrip);
519 });
520 }
521
522 #[test]
523 fn proptest_decimal_roundtrip() {
524 proptest!(|(ppm in any::<Ppm>())| {
525 let dec = ppm.to_decimal();
526
527 prop_assert!(dec >= Decimal::ZERO);
529 prop_assert!(dec <= Decimal::ONE);
530
531 prop_assert_eq!(ppm, Ppm::try_from(dec).unwrap());
533 prop_assert_eq!(ppm, Ppm::try_from_decimal(dec).unwrap());
534 prop_assert_eq!(ppm, Ppm::const_from_decimal(dec));
535 });
536 }
537
538 #[test]
539 fn proptest_percent_roundtrip() {
540 proptest!(|(ppm in any::<Ppm>())| {
541 let pct = ppm.to_percent();
542
543 prop_assert!(pct >= Decimal::ZERO);
545 prop_assert!(pct <= dec!(100));
546
547 prop_assert_eq!(ppm, Ppm::try_from_percent(pct).unwrap());
549 prop_assert_eq!(ppm, Ppm::const_from_percent(pct));
550
551 prop_assert_eq!(pct, ppm.to_decimal() * dec!(100.0));
552 });
553 }
554}