af_utilities/types/
from_str.rs

1// Copyright (c) 2023 The BigDecimal-rs Contributors
2//
3// Permission is hereby granted, free of charge, to any
4// person obtaining a copy of this software and associated
5// documentation files (the "Software"), to deal in the
6// Software without restriction, including without
7// limitation the rights to use, copy, modify, merge,
8// publish, distribute, sublicense, and/or sell copies of
9// the Software, and to permit persons to whom the Software
10// is furnished to do so, subject to the following
11// conditions:
12//
13// The above copyright notice and this permission notice
14// shall be included in all copies or substantial portions
15// of the Software.
16
17use 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    // split slice into base and exponent parts
46    let (base_part, exponent_value) = match s.find(exp_separator) {
47        // exponent defaults to 0 if (e|E) not found
48        None => (s, 0),
49
50        // split and parse exponent field
51        Some(loc) => {
52            // slice up to `loc` and 1 after to skip the 'e' char
53            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    // split decimal into a digit string and decimal-point offset
71    let (digits, decimal_offset) = match base_part.find('.') {
72        // No dot! pass directly to BigInt
73        None => (base_part, 0),
74        // dot at last digit, pass all preceding digits to BigInt
75        Some(loc) if loc == last_digit_loc => (&base_part[..last_digit_loc], 0),
76        // decimal point found - necessary copy into new string buffer
77        Some(loc) => {
78            // split into leading and trailing digits
79            let (lead, trail) = (&base_part[..loc], &base_part[loc + 1..]);
80
81            digit_buffer.reserve(lead.len() + trail.len());
82            // copy all leading characters into 'digits' string
83            digit_buffer.push_str(lead);
84            // copy all trailing characters after '.' into the digits string
85            digit_buffer.push_str(trail);
86
87            // count number of trailing digits
88            let trail_digits = trail.chars().filter(|c| *c != '_').count();
89
90            (digit_buffer.as_str(), trail_digits as i128)
91        }
92    };
93
94    // Calculate scale by subtracing the parsed exponential
95    // value from the number of decimal digits.
96    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        // If the scale is smaller than IFixed's, then we need more 0s for the underlying u256
103        digits.to_owned()
104            + &String::from_utf8(vec![b'0'; (IFIXED_SCALE - scale) as usize])
105                .expect("0s are valid utf8")
106    } else {
107        // In this case, the number has more decimals than IFixed supports, so we truncate.
108        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    //==============================================================================================
271    // Previous attempt at a 'lossless' conversion
272    //==============================================================================================
273
274    #[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    /// Demonstrating why the documentation recommends against converting from f64 directly.
300    ///
301    /// > It is not recommended to convert a floating point number to a decimal directly, as the
302    /// > floating point representation may be unexpected
303    #[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        // Pad with zeros if len < N
369        vec.resize(N, 0);
370        Ok(vec.try_into().expect("len == N"))
371    }
372}