agglayer_config/
multiplier.rs1#[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 pub fn try_from_f64_strict(x: f64) -> Result<Self, FromF64Error> {
38 let r = Self::try_from_f64_lossy(x)?;
42 let delta = r.as_u64_per_1000() as f64 - Self::scale_f64(x);
43
44 (delta.abs() <= Self::FROM_F64_TOLERANCE)
47 .then_some(r)
48 .ok_or(FromF64Error::Imprecise)
49 }
50
51 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 let width = Self::DECIMALS + 1;
86 let s = format!("{:0width$}", self.0);
87
88 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}