agglayer_config/
multiplier.rs1use std::time::Duration;
2
3#[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 pub fn try_from_f64_strict(x: f64) -> Result<Self, FromF64Error> {
40 let r = Self::try_from_f64_lossy(x)?;
44 let delta = r.as_u64_per_1000() as f64 - Self::scale_f64(x);
45
46 (delta.abs() <= Self::FROM_F64_TOLERANCE)
49 .then_some(r)
50 .ok_or(FromF64Error::Imprecise)
51 }
52
53 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 let width = Self::DECIMALS + 1;
99 let s = format!("{:0width$}", self.0);
100
101 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}