1use std::{fmt, ops::Mul, str::FromStr};
42
43use anyhow::format_err;
44use rust_decimal::Decimal;
45use serde::{Deserialize, Serialize};
46
47use crate::{dec, ln::amount::Amount};
48
49#[derive(Debug, thiserror::Error)]
51pub enum Error {
52 #[error("Ppm value is negative")]
53 Negative,
54 #[error("Ppm value exceeds 1_000_000")]
55 TooLarge,
56}
57
58#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
63#[derive(Serialize, Deserialize)]
64#[serde(try_from = "i32", into = "i32")]
65pub struct Ppm(i32);
66
67impl Ppm {
68 pub const MAX: Self = Self(1_000_000);
70
71 pub const ZERO: Self = Self(0);
73
74 #[inline]
81 pub const fn new(value: i32) -> Self {
82 assert!(value >= 0, "Ppm value must be non-negative");
83 assert!(value <= Self::MAX.0, "Ppm value must be <= 1_000_000");
84 Self(value)
85 }
86
87 #[inline]
89 pub const fn to_i32(self) -> i32 {
90 self.0
91 }
92
93 #[inline]
95 pub const fn to_u32(self) -> u32 {
96 self.0 as u32
97 }
98
99 #[inline]
103 pub fn to_decimal(self) -> Decimal {
104 Decimal::from(self.0) / dec!(1_000_000)
105 }
106
107 #[inline]
109 fn try_from_inner(value: i32) -> Result<Self, Error> {
110 if value < 0 {
111 Err(Error::Negative)
112 } else if value > Self::MAX.0 {
113 Err(Error::TooLarge)
114 } else {
115 Ok(Self(value))
116 }
117 }
118}
119
120impl fmt::Display for Ppm {
121 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122 fmt::Display::fmt(&self.0, f)
123 }
124}
125
126impl FromStr for Ppm {
127 type Err = anyhow::Error;
128
129 fn from_str(s: &str) -> Result<Self, Self::Err> {
130 let value = s.parse::<i32>().map_err(|err| format_err!("{err}"))?;
131 Ok(Self::try_from_inner(value)?)
132 }
133}
134
135impl From<u16> for Ppm {
138 #[inline]
140 fn from(value: u16) -> Self {
141 Self(i32::from(value))
142 }
143}
144
145impl From<Ppm> for i32 {
146 #[inline]
147 fn from(ppm: Ppm) -> Self {
148 ppm.0
149 }
150}
151
152impl From<Ppm> for u32 {
153 #[inline]
154 fn from(ppm: Ppm) -> Self {
155 ppm.0 as u32
156 }
157}
158
159impl From<Ppm> for i64 {
160 #[inline]
161 fn from(ppm: Ppm) -> Self {
162 i64::from(ppm.0)
163 }
164}
165
166impl From<Ppm> for u64 {
167 #[inline]
168 fn from(ppm: Ppm) -> Self {
169 ppm.0 as u64
170 }
171}
172
173impl TryFrom<i32> for Ppm {
176 type Error = Error;
177
178 fn try_from(value: i32) -> Result<Self, Self::Error> {
179 Self::try_from_inner(value)
180 }
181}
182
183impl TryFrom<u32> for Ppm {
184 type Error = Error;
185
186 fn try_from(value: u32) -> Result<Self, Self::Error> {
187 let value_i32 = i32::try_from(value).map_err(|_| Error::TooLarge)?;
188 Self::try_from_inner(value_i32)
189 }
190}
191
192impl TryFrom<Decimal> for Ppm {
193 type Error = Error;
194
195 fn try_from(rate: Decimal) -> Result<Self, Self::Error> {
202 use rust_decimal::prelude::ToPrimitive;
203
204 let ppm_dec = (rate * dec!(1_000_000)).round();
205 let ppm_i32 = ppm_dec.to_i32().ok_or(Error::TooLarge)?;
206 Self::try_from_inner(ppm_i32)
207 }
208}
209
210impl Mul<Ppm> for Amount {
218 type Output = Self;
219
220 #[inline]
221 fn mul(self, rhs: Ppm) -> Self::Output {
222 self * rhs.to_decimal()
223 }
224}
225
226impl Mul<Amount> for Ppm {
228 type Output = Amount;
229
230 #[inline]
231 fn mul(self, rhs: Amount) -> Self::Output {
232 rhs * self.to_decimal()
233 }
234}
235
236#[cfg(any(test, feature = "test-utils"))]
239impl proptest::arbitrary::Arbitrary for Ppm {
240 type Parameters = ();
241 type Strategy = proptest::strategy::BoxedStrategy<Self>;
242
243 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
244 use proptest::strategy::Strategy;
245 (0i32..=Self::MAX.0).prop_map(Self).boxed()
246 }
247}
248
249#[cfg(test)]
252mod test {
253 use proptest::{arbitrary::any, prop_assert, prop_assert_eq, proptest};
254
255 use super::*;
256
257 #[test]
258 fn const_construction() {
259 const TEST_PPM: Ppm = Ppm::new(3000);
261
262 assert_eq!(TEST_PPM.to_i32(), 3000);
263 assert_eq!(Ppm::ZERO.to_i32(), 0);
264 assert_eq!(Ppm::MAX.to_i32(), 1_000_000);
265 }
266
267 #[test]
268 fn to_decimal() {
269 assert_eq!(Ppm::ZERO.to_decimal(), dec!(0));
270 assert_eq!(Ppm::new(1).to_decimal(), dec!(0.000001));
271 assert_eq!(Ppm::new(1000).to_decimal(), dec!(0.001));
272 assert_eq!(Ppm::new(10_000).to_decimal(), dec!(0.01));
273 assert_eq!(Ppm::new(100_000).to_decimal(), dec!(0.1));
274 assert_eq!(Ppm::MAX.to_decimal(), dec!(1));
275 }
276
277 #[test]
278 fn try_from_decimal() {
279 assert_eq!(Ppm::try_from(dec!(0)).unwrap(), Ppm::ZERO);
281 assert_eq!(Ppm::try_from(dec!(0.005)).unwrap(), Ppm::new(5000));
282 assert_eq!(Ppm::try_from(dec!(0.1)).unwrap(), Ppm::new(100_000));
283 assert_eq!(Ppm::try_from(dec!(1)).unwrap(), Ppm::MAX);
284
285 assert_eq!(Ppm::try_from(dec!(0.0000014)).unwrap(), Ppm::new(1));
287 assert_eq!(Ppm::try_from(dec!(0.0000016)).unwrap(), Ppm::new(2));
288
289 assert!(matches!(Ppm::try_from(dec!(-0.001)), Err(Error::Negative)));
291 assert!(matches!(
292 Ppm::try_from(dec!(1.000001)),
293 Err(Error::TooLarge)
294 ));
295 }
296
297 #[test]
298 fn try_from_rejects_invalid() {
299 assert!(matches!(Ppm::try_from(-1i32), Err(Error::Negative)));
300 assert!(matches!(Ppm::try_from(1_000_001i32), Err(Error::TooLarge)));
301 assert!(matches!(Ppm::try_from(1_000_001u32), Err(Error::TooLarge)));
302 }
303
304 #[test]
305 fn from_str() {
306 assert_eq!("0".parse::<Ppm>().unwrap(), Ppm::ZERO);
307 assert_eq!("3000".parse::<Ppm>().unwrap(), Ppm::new(3000));
308 assert_eq!("1000000".parse::<Ppm>().unwrap(), Ppm::MAX);
309
310 assert!("-1".parse::<Ppm>().is_err());
311 assert!("1000001".parse::<Ppm>().is_err());
312 assert!("abc".parse::<Ppm>().is_err());
313 }
314
315 #[test]
317 fn serde_json_format() {
318 #[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
319 struct Foo {
320 ppm: Ppm,
321 }
322
323 let foo = Foo {
324 ppm: Ppm::new(3000),
325 };
326 let json = serde_json::to_string(&foo).unwrap();
327 assert_eq!(json, r#"{"ppm":3000}"#);
328 let roundtrip: Foo = serde_json::from_str(&json).unwrap();
329 assert_eq!(foo, roundtrip);
330
331 assert!(serde_json::from_str::<Ppm>("-1").is_err());
333 assert!(serde_json::from_str::<Ppm>("1000001").is_err());
334 }
335
336 #[test]
337 fn proptest_integer_conversions() {
338 proptest!(|(ppm in any::<Ppm>(), val in any::<u16>())| {
339 let i = ppm.to_i32();
340
341 prop_assert_eq!(i32::from(ppm), i);
343 prop_assert_eq!(u32::from(ppm), i as u32);
344 prop_assert_eq!(i64::from(ppm), i64::from(i));
345 prop_assert_eq!(u64::from(ppm), i as u64);
346
347 prop_assert_eq!(Ppm::try_from(i).unwrap(), ppm);
349 prop_assert_eq!(Ppm::try_from(i as u32).unwrap(), ppm);
350
351 let from_u16 = Ppm::from(val);
353 prop_assert_eq!(from_u16.to_i32(), i32::from(val));
354 });
355 }
356
357 #[test]
358 fn proptest_mul_amount() {
359 proptest!(|(amount in any::<Amount>(), ppm in any::<Ppm>())| {
360 prop_assert_eq!(amount * ppm, ppm * amount);
362
363 prop_assert_eq!(amount * ppm, amount * ppm.to_decimal());
365 });
366 }
367
368 #[test]
369 fn proptest_serde_roundtrip() {
370 proptest!(|(ppm in any::<Ppm>())| {
371 let json = serde_json::to_string(&ppm).unwrap();
372 let roundtrip: Ppm = serde_json::from_str(&json).unwrap();
373 prop_assert_eq!(ppm, roundtrip);
374 });
375 }
376
377 #[test]
378 fn proptest_decimal_roundtrip() {
379 proptest!(|(ppm in any::<Ppm>())| {
380 let dec = ppm.to_decimal();
381
382 prop_assert!(dec >= Decimal::ZERO);
384 prop_assert!(dec <= Decimal::ONE);
385
386 let roundtrip = Ppm::try_from(dec).unwrap();
388 prop_assert_eq!(ppm, roundtrip);
389 });
390 }
391}