Skip to main content

klirr_core/models/
decimal.rs

1use crate::prelude::*;
2use derive_more::FromStr;
3
4/// A wrapper around `rust_decimal::Decimal` which serializes to and from `f64`.
5///
6/// We have a specific need for f64 as underlying type when serialized into JSON,
7/// since we bridge to Typst dictionary and we must be able to perform arithmetic
8/// with values as numbers in Typst - which we cannot do with Strings.
9///
10/// We don't need the full precision of `rust_decimal::Decimal` in this context,
11/// so we use `f64` for serialization and deserialization, which allows us to easily
12/// bridge to Typst.
13#[derive(
14    Clone,
15    Copy,
16    Display,
17    PartialEq,
18    Eq,
19    PartialOrd,
20    Ord,
21    Default,
22    Hash,
23    Debug,
24    From,
25    FromStr,
26    Deref,
27    derive_more::Mul,
28    derive_more::Add,
29    derive_more::Sub,
30    derive_more::AddAssign,
31)]
32#[from(rust_decimal::Decimal, u8, i32)]
33pub struct Decimal(rust_decimal::Decimal);
34
35impl Decimal {
36    pub const ZERO: Self = Self(rust_decimal::Decimal::ZERO);
37    pub const ONE: Self = Self(rust_decimal::Decimal::ONE);
38    pub const TWO: Self = Self(rust_decimal::Decimal::TWO);
39    pub const EIGHT: Self = Self(rust_decimal::Decimal::from_parts(8, 0, 0, false, 0));
40}
41
42use rust_decimal::prelude::{FromPrimitive, ToPrimitive};
43
44impl TryFrom<Decimal> for f64 {
45    type Error = crate::Error;
46    fn try_from(value: Decimal) -> Result<Self> {
47        value
48            .0
49            .to_f64()
50            .ok_or_else(|| Error::InvalidDecimalToF64Conversion {
51                value: value.to_string(),
52            })
53    }
54}
55impl TryFrom<f64> for Decimal {
56    type Error = crate::Error;
57    fn try_from(value: f64) -> Result<Self> {
58        rust_decimal::Decimal::from_f64(value)
59            .ok_or(Error::InvalidDecimalFromF64Conversion { value })
60            .map(Decimal)
61    }
62}
63impl Serialize for Decimal {
64    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
65    where
66        S: serde::Serializer,
67    {
68        f64::try_from(*self)
69            .map_err(serde::ser::Error::custom)
70            .and_then(|f| f.serialize(serializer))
71    }
72}
73
74impl<'de> Deserialize<'de> for Decimal {
75    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
76    where
77        D: serde::Deserializer<'de>,
78    {
79        f64::deserialize(deserializer)
80            .and_then(|f| Decimal::try_from(f).map_err(serde::de::Error::custom))
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87    use insta::{assert_ron_snapshot, assert_snapshot};
88    use test_log::test;
89
90    type Sut = Decimal;
91
92    #[test]
93    fn test_display() {
94        assert_snapshot!(Sut::EIGHT)
95    }
96
97    #[test]
98    fn test_serde() {
99        assert_ron_snapshot!(Sut::from(dec!(3.14159265)));
100    }
101
102    #[test]
103    fn test_decimal_from_f64_nan() {
104        let result = Sut::try_from(f64::NAN);
105        assert!(result.is_err());
106    }
107}