agglayer_config/
multiplier.rs

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