1use af_sui_types::U256;
18
19use super::IFixed;
20use crate::I256;
21use crate::types::onchain::max_i256;
22
23const IFIXED_SCALE: i64 = 18;
24const RADIX: u32 = 10;
25
26#[derive(thiserror::Error, Debug)]
27#[error("Parsing string '{string}': {error}")]
28pub struct Error {
29 pub string: String,
30 pub error: String,
31}
32
33pub(crate) fn ifixed_from_str(s: &str) -> Result<IFixed, Error> {
34 convert(s).map_err(|error| Error {
35 string: s.to_owned(),
36 error,
37 })
38}
39
40fn convert(s: &str) -> Result<IFixed, String> {
41 use std::str::FromStr as _;
42
43 let exp_separator: &[_] = &['e', 'E'];
44
45 let (base_part, exponent_value) = match s.find(exp_separator) {
47 None => (s, 0),
49
50 Some(loc) => {
52 let (base, e_exp) = s.split_at(loc);
54 (
55 base,
56 i128::from_str(&e_exp[1..])
57 .map_err(|e| format!("Couldn't convert exponent to i128: {e:?}"))?,
58 )
59 }
60 };
61
62 if base_part.is_empty() {
63 return Err("Missing base part of the number".into());
64 }
65
66 let mut digit_buffer = String::new();
67
68 let last_digit_loc = base_part.len() - 1;
69
70 let (digits, decimal_offset) = match base_part.find('.') {
72 None => (base_part, 0),
74 Some(loc) if loc == last_digit_loc => (&base_part[..last_digit_loc], 0),
76 Some(loc) => {
78 let (lead, trail) = (&base_part[..loc], &base_part[loc + 1..]);
80
81 digit_buffer.reserve(lead.len() + trail.len());
82 digit_buffer.push_str(lead);
84 digit_buffer.push_str(trail);
86
87 let trail_digits = trail.chars().filter(|c| *c != '_').count();
89
90 (digit_buffer.as_str(), trail_digits as i128)
91 }
92 };
93
94 let scale = decimal_offset
97 .checked_sub(exponent_value)
98 .and_then(|scale| i64::try_from(scale).ok())
99 .ok_or_else(|| format!("Exponent overflow when parsing '{}'", s))?;
100
101 let digits = if scale < IFIXED_SCALE {
102 digits.to_owned()
104 + &String::from_utf8(vec![b'0'; (IFIXED_SCALE - scale) as usize])
105 .expect("0s are valid utf8")
106 } else {
107 digits[0..(digits.len() - (scale - IFIXED_SCALE) as usize)].to_owned()
109 };
110 let is_neg = digits.starts_with('-');
111
112 let u256_str = if is_neg { &digits[1..] } else { &digits };
113 let inner =
114 U256::from_str_radix(u256_str, RADIX).map_err(|e| format!("Parsing inner u256: {e:?}"))?;
115
116 if inner > max_i256() {
117 return Err(format!("Inner digits exceed maximum '{}'", max_i256()));
118 }
119
120 let unsigned = IFixed::from_inner(I256::from_inner(inner));
121 Ok(if is_neg { -unsigned } else { unsigned })
122}
123
124#[cfg(test)]
125mod tests {
126 use bigdecimal::BigDecimal;
127
128 use super::*;
129 use crate::types::Fixed;
130
131 impl IFixed {
132 fn from_f64_faulty(value: f64) -> Self {
133 let max_i256 = U256::max_value() >> 1;
134 let unsigned_inner = Fixed::from(value.abs()).into_inner().min(max_i256);
135 let unsigned_inner = I256::from_inner(unsigned_inner);
136 Self::from_inner(if value.is_sign_negative() {
137 -unsigned_inner
138 } else {
139 unsigned_inner
140 })
141 }
142 }
143
144 #[test]
145 fn original_conversion() {
146 let mut float = 0.001_f64;
147 let ifixed = IFixed::from_f64_faulty(float);
148 insta::assert_snapshot!(ifixed, @"0.001");
149
150 float = 0.009;
151 let ifixed = IFixed::from_f64_faulty(float);
152 insta::assert_snapshot!(ifixed, @"0.008999999999999999");
153
154 float = 0.003;
155 let ifixed = IFixed::from_f64_faulty(float);
156 insta::assert_snapshot!(ifixed, @"0.003");
157
158 float = 1e-18;
159 let ifixed = IFixed::from_f64_faulty(float);
160 insta::assert_snapshot!(ifixed, @"0.000000000000000001");
161
162 float = 2.2238;
163 let ifixed = IFixed::from_f64_faulty(float);
164 insta::assert_snapshot!(ifixed, @"2.223800000000000256");
165 }
166
167 fn ifixed_from_f64(v: f64) -> std::result::Result<IFixed, super::Error> {
168 ifixed_from_str(&v.to_string())
169 }
170
171 #[test]
172 fn new_conversion() {
173 let mut float = 0.001_f64;
174 let ifixed = ifixed_from_f64(float).unwrap();
175 insta::assert_snapshot!(ifixed, @"0.001");
176
177 float = 0.009;
178 insta::assert_snapshot!(float, @"0.009");
179
180 let ifixed = ifixed_from_f64(float).unwrap();
181 insta::assert_snapshot!(ifixed, @"0.009");
182
183 float = 0.003;
184 let ifixed = ifixed_from_f64(float).unwrap();
185 insta::assert_snapshot!(ifixed, @"0.003");
186
187 float = 1e-18;
188 insta::assert_snapshot!(float, @"0.000000000000000001");
189 let ifixed = ifixed_from_f64(float).unwrap();
190 insta::assert_snapshot!(ifixed, @"0.000000000000000001");
191
192 float = 2.2238;
193 let ifixed = ifixed_from_f64(float).unwrap();
194 insta::assert_snapshot!(ifixed, @"2.2238");
195
196 float = 2.3e+10;
197 insta::assert_snapshot!(float, @"23000000000");
198 let ifixed = ifixed_from_f64(float).unwrap();
199 insta::assert_snapshot!(ifixed, @"23000000000.0");
200
201 float = 1.234567e+20;
202 insta::assert_snapshot!(float, @"123456700000000000000");
203 let ifixed = ifixed_from_f64(float).unwrap();
204 insta::assert_snapshot!(ifixed, @"123456700000000000000.0");
205
206 let ifixed = ifixed_from_str("2.3e+10").unwrap();
207 insta::assert_snapshot!(ifixed, @"23000000000.0");
208
209 let ifixed = ifixed_from_str("1.234567e+20").unwrap();
210 insta::assert_snapshot!(ifixed, @"123456700000000000000.0");
211
212 float = -2.2238;
213 let ifixed = ifixed_from_f64(float).unwrap();
214 insta::assert_snapshot!(ifixed, @"-2.2238");
215
216 let ifixed = ifixed_from_str("-1.234567e+20").unwrap();
217 insta::assert_snapshot!(ifixed, @"-123456700000000000000.0");
218
219 let ifixed = ifixed_from_str(
220 "57896044618658097711785492504343953926634992332820282019728.792003956564819967",
221 )
222 .unwrap();
223 insta::assert_snapshot!(ifixed, @"57896044618658097711785492504343953926634992332820282019728.792003956564819967");
224
225 let ifixed = ifixed_from_str(
226 "-57896044618658097711785492504343953926634992332820282019728.792003956564819967",
227 )
228 .unwrap();
229 insta::assert_snapshot!(ifixed, @"-57896044618658097711785492504343953926634992332820282019728.792003956564819967");
230
231 let err = ifixed_from_str(
232 "57896044618658097711785492504343953926634992332820282019728.792003956564819968",
233 )
234 .unwrap_err();
235 insta::assert_debug_snapshot!(err, @r###"
236 Error {
237 string: "57896044618658097711785492504343953926634992332820282019728.792003956564819968",
238 error: "Inner digits exceed maximum '57896044618658097711785492504343953926634992332820282019728792003956564819967'",
239 }
240 "###);
241
242 float = f64::INFINITY;
243 let err = ifixed_from_f64(float).unwrap_err();
244 insta::assert_debug_snapshot!(err, @r###"
245 Error {
246 string: "inf",
247 error: "Parsing inner u256: U256FromStrError(FromStrRadixErr { kind: InvalidCharacter, source: Some(Dec(InvalidCharacter)) })",
248 }
249 "###);
250
251 float = f64::NEG_INFINITY;
252 let err = ifixed_from_f64(float).unwrap_err();
253 insta::assert_debug_snapshot!(err, @r###"
254 Error {
255 string: "-inf",
256 error: "Parsing inner u256: U256FromStrError(FromStrRadixErr { kind: InvalidCharacter, source: Some(Dec(InvalidCharacter)) })",
257 }
258 "###);
259
260 float = f64::NAN;
261 let err = ifixed_from_f64(float).unwrap_err();
262 insta::assert_debug_snapshot!(err, @r###"
263 Error {
264 string: "NaN",
265 error: "Parsing inner u256: U256FromStrError(FromStrRadixErr { kind: InvalidCharacter, source: Some(Dec(InvalidCharacter)) })",
266 }
267 "###);
268 }
269
270 #[test]
275 fn conversion_via_bigdecimal() {
276 let mut float = 0.001_f64;
277 let ifixed = IFixed::from_f64(float).unwrap();
278 insta::assert_snapshot!(ifixed, @"0.001");
279
280 float = 0.009;
281 insta::assert_snapshot!(float, @"0.009");
282
283 let ifixed = IFixed::from_f64(float).unwrap();
284 insta::assert_snapshot!(ifixed, @"0.009");
285
286 float = 0.003;
287 let ifixed = IFixed::from_f64(float).unwrap();
288 insta::assert_snapshot!(ifixed, @"0.003");
289
290 float = 1e-18;
291 let ifixed = IFixed::from_f64(float).unwrap();
292 insta::assert_snapshot!(ifixed, @"0.000000000000000001");
293
294 float = 2.2238;
295 let ifixed = IFixed::from_f64(float).unwrap();
296 insta::assert_snapshot!(ifixed, @"2.2238");
297 }
298
299 #[test]
304 fn bigdecimal_native_conversion() {
305 let float = 0.009;
306 insta::assert_snapshot!(float, @"0.009");
307
308 let bigd: BigDecimal = "0.009".parse().unwrap();
309 insta::assert_snapshot!(bigd, @"0.009");
310
311 let bigd: BigDecimal = float.try_into().unwrap();
312 insta::assert_snapshot!(bigd, @"0.00899999999999999931998839741709161899052560329437255859375");
313
314 let bigd: BigDecimal = float.to_string().parse().unwrap();
315 insta::assert_snapshot!(bigd, @"0.009");
316
317 let float = 2.2238;
318 insta::assert_snapshot!(float, @"2.2238");
319
320 let bigd: BigDecimal = "2.2238".parse().unwrap();
321 insta::assert_snapshot!(bigd, @"2.2238");
322
323 let bigd: BigDecimal = float.try_into().unwrap();
324 insta::assert_snapshot!(bigd, @"2.223800000000000220978790821391157805919647216796875");
325
326 let bigd: BigDecimal = float.to_string().parse().unwrap();
327 insta::assert_snapshot!(bigd, @"2.2238");
328 }
329
330 type Result<T> = std::result::Result<T, Error>;
331
332 #[derive(thiserror::Error, Debug)]
333 #[non_exhaustive]
334 enum Error {
335 #[error("Couldn't convert from {value}: {error}")]
336 FromF64 { value: f64, error: String },
337 }
338
339 trait FromF64: Sized {
340 fn from_f64(value: f64) -> Result<Self>;
341 }
342
343 impl FromF64 for IFixed {
344 fn from_f64(value: f64) -> Result<Self> {
345 let decimal: BigDecimal = value.to_string().parse().map_err(|e| Error::FromF64 {
346 value,
347 error: format!("Parsing string representation: {e:?}"),
348 })?;
349 let bytes = decimal
350 .with_scale(IFIXED_SCALE)
351 .into_bigint_and_scale()
352 .0
353 .to_signed_bytes_le();
354 let u256_bytes = get_bytes_padded(bytes).map_err(|vec| Error::FromF64 {
355 value,
356 error: format!("BigDecimal has too many bytes; {} > 32", vec.len()),
357 })?;
358 Ok(Self::from_inner(I256::from_inner(U256::from_le_bytes(
359 &u256_bytes,
360 ))))
361 }
362 }
363
364 fn get_bytes_padded<const N: usize>(mut vec: Vec<u8>) -> std::result::Result<[u8; N], Vec<u8>> {
365 if vec.len() > N {
366 return Err(vec);
367 }
368 vec.resize(N, 0);
370 Ok(vec.try_into().expect("len == N"))
371 }
372}