agglayer_config/
multiplier.rs

1/// Multiplier is a quantity specifying a scaling factor of some sort.
2///
3/// It is internally implemented as a `u64` fixed point scaled by 1000.
4/// It defaults to scaling by 1.0.
5#[derive(
6    Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, serde::Serialize, serde::Deserialize,
7)]
8#[serde(try_from = "f64", into = "f64")]
9pub struct Multiplier(u64);
10
11#[derive(PartialEq, Eq, Debug, Clone, thiserror::Error)]
12pub enum FromF64Error {
13    #[error("Multiplier out of range")]
14    OutOfRange,
15
16    #[error("Multiplier supports up to 3 decimal places.")]
17    Imprecise,
18}
19
20impl Multiplier {
21    pub const ONE: Self = Self(Self::SCALE);
22    pub const ZERO: Self = Self(0);
23    pub const MAX: Self = Self(u64::MAX);
24    pub const DECIMALS: usize = 3;
25
26    const FROM_F64_TOLERANCE: f64 = 1e-6;
27    const FROM_F64_MAX: u64 = ((1_u64 << f64::MANTISSA_DIGITS) as f64).next_down() as u64;
28    const SCALE: u64 = 1000;
29
30    pub const fn from_u64_per_1000(x: u64) -> Self {
31        Self(x)
32    }
33
34    /// Creates a multiplier from `f64`, requiring max 3 decimals.
35    ///
36    /// Fails if the value has more than 3 decimal places or is out of range.
37    pub fn try_from_f64_strict(x: f64) -> Result<Self, FromF64Error> {
38        // We first get the rounded conversion, check the delta against the original
39        // value and fail if there is too much precision loss, indicating there were
40        // too many decimals in the original floating point number.
41        let r = Self::try_from_f64_lossy(x)?;
42        let delta = r.as_u64_per_1000() as f64 - Self::scale_f64(x);
43
44        // We still allow some tolerance to account for the fact that floating point
45        // cannot represent base-10 decimals (such as 1.2) exactly.
46        (delta.abs() <= Self::FROM_F64_TOLERANCE)
47            .then_some(r)
48            .ok_or(FromF64Error::Imprecise)
49    }
50
51    /// Creates a multiplier from `f64`, rounding to 3 decimal places if needed.
52    ///
53    /// Fails only if the value is out of range.
54    pub fn try_from_f64_lossy(x: f64) -> Result<Self, FromF64Error> {
55        let x = Self::scale_f64(x).round();
56        (0.0..=Self::FROM_F64_MAX as f64)
57            .contains(&x)
58            .then_some(Self(x as u64))
59            .ok_or(FromF64Error::OutOfRange)
60    }
61
62    pub const fn as_u64_per_1000(self) -> u64 {
63        self.0
64    }
65
66    pub fn as_f64(self) -> f64 {
67        self.0 as f64 / Self::SCALE as f64
68    }
69
70    fn scale_f64(x: f64) -> f64 {
71        x * Self::SCALE as f64
72    }
73}
74
75impl Default for Multiplier {
76    fn default() -> Self {
77        Self::ONE
78    }
79}
80
81impl std::fmt::Display for Multiplier {
82    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83        // A quick, dirty and inefficient implementation that does not use f64
84        // to print the decimal number since it could result in imprecision.
85        let width = Self::DECIMALS + 1;
86        let s = format!("{:0width$}", self.0);
87
88        // Split the integral and the decimal part.
89        // Not worth panicking over a printout (should never happen anyway).
90        let (n, d) = s
91            .split_at_checked(s.len() - Self::DECIMALS)
92            .unwrap_or(("?", "???"));
93
94        write!(f, "{n}.{d}")
95    }
96}
97
98impl TryFrom<f64> for Multiplier {
99    type Error = FromF64Error;
100
101    fn try_from(value: f64) -> Result<Self, Self::Error> {
102        Self::try_from_f64_strict(value)
103    }
104}
105
106impl From<Multiplier> for f64 {
107    fn from(value: Multiplier) -> Self {
108        value.as_f64()
109    }
110}
111
112#[cfg(test)]
113mod test {
114    use rstest::rstest;
115
116    use super::*;
117
118    #[rstest]
119    #[case(0.0, 0)]
120    #[case(1.0, 1000)]
121    #[case(1.5, 1500)]
122    #[case(2.0, 2000)]
123    #[case(0.001, 1)]
124    #[case(0.123, 123)]
125    #[case(10.5, 10500)]
126    fn try_from_f64_strict_valid_values(#[case] input: f64, #[case] expected: u64) {
127        let result = Multiplier::try_from_f64_strict(input).unwrap();
128        assert_eq!(result, Multiplier::from_u64_per_1000(expected));
129    }
130
131    #[rstest]
132    #[case(-1.0)]
133    #[case(-0.001)]
134    #[case(-100.0)]
135    #[case((1u64 << f64::MANTISSA_DIGITS) as f64)]
136    #[case(1.001 * u64::MAX as f64)]
137    fn try_from_f64_out_of_range(#[case] input: f64) {
138        assert_eq!(
139            Multiplier::try_from_f64_strict(input).unwrap_err(),
140            FromF64Error::OutOfRange
141        );
142        assert_eq!(
143            Multiplier::try_from_f64_lossy(input).unwrap_err(),
144            FromF64Error::OutOfRange
145        );
146    }
147
148    #[rstest]
149    #[case(1.2345)]
150    #[case(0.0001)]
151    #[case(2.12345)]
152    fn try_from_f64_strict_imprecise(#[case] input: f64) {
153        assert_eq!(
154            Multiplier::try_from_f64_strict(input).unwrap_err(),
155            FromF64Error::Imprecise
156        );
157    }
158
159    #[rstest]
160    #[case(0.0, 0)]
161    #[case(1.0, 1000)]
162    #[case(1.5, 1500)]
163    #[case(2.0, 2000)]
164    #[case(1.2345, 1235)]
165    #[case(1.2344, 1234)]
166    #[case(0.0001, 0)]
167    #[case(0.0006, 1)]
168    fn try_from_f64_lossy_valid_values(#[case] input: f64, #[case] expected: u64) {
169        let result = Multiplier::try_from_f64_lossy(input).unwrap();
170        assert_eq!(result, Multiplier::from_u64_per_1000(expected));
171    }
172
173    #[rstest]
174    #[case(1000)]
175    #[case(1500)]
176    #[case(2000)]
177    #[case(123)]
178    fn roundtrip(#[case] value: u64) {
179        let original = Multiplier::from_u64_per_1000(value);
180        let as_f64 = original.as_f64();
181        let back = Multiplier::try_from_f64_strict(as_f64).unwrap();
182        assert_eq!(original, back);
183    }
184
185    #[rstest]
186    #[case(0, "0.000")]
187    #[case(1, "0.001")]
188    #[case(50, "0.050")]
189    #[case(700, "0.700")]
190    #[case(1000, "1.000")]
191    #[case(1001, "1.001")]
192    #[case(10000, "10.000")]
193    #[case(12345, "12.345")]
194    #[case(u64::MAX, "18446744073709551.615")]
195    fn display(#[case] value: u64, #[case] expected: &str) {
196        assert_eq!(Multiplier(value).to_string(), expected);
197    }
198}