use af_sui_types::U256;
use super::IFixed;
use crate::I256;
use crate::types::onchain::max_i256;
const IFIXED_SCALE: i64 = 18;
const RADIX: u32 = 10;
#[derive(thiserror::Error, Debug)]
#[error("Parsing string '{string}': {error}")]
pub struct Error {
pub string: String,
pub error: String,
}
pub(crate) fn ifixed_from_str(s: &str) -> Result<IFixed, Error> {
convert(s).map_err(|error| Error {
string: s.to_owned(),
error,
})
}
fn convert(s: &str) -> Result<IFixed, String> {
use std::str::FromStr as _;
let exp_separator: &[_] = &['e', 'E'];
let (base_part, exponent_value) = match s.find(exp_separator) {
None => (s, 0),
Some(loc) => {
let (base, e_exp) = s.split_at(loc);
(
base,
i128::from_str(&e_exp[1..])
.map_err(|e| format!("Couldn't convert exponent to i128: {e:?}"))?,
)
}
};
if base_part.is_empty() {
return Err("Missing base part of the number".into());
}
let mut digit_buffer = String::new();
let last_digit_loc = base_part.len() - 1;
let (digits, decimal_offset) = match base_part.find('.') {
None => (base_part, 0),
Some(loc) if loc == last_digit_loc => (&base_part[..last_digit_loc], 0),
Some(loc) => {
let (lead, trail) = (&base_part[..loc], &base_part[loc + 1..]);
digit_buffer.reserve(lead.len() + trail.len());
digit_buffer.push_str(lead);
digit_buffer.push_str(trail);
let trail_digits = trail.chars().filter(|c| *c != '_').count();
(digit_buffer.as_str(), trail_digits as i128)
}
};
let scale = decimal_offset
.checked_sub(exponent_value)
.and_then(|scale| i64::try_from(scale).ok())
.ok_or_else(|| format!("Exponent overflow when parsing '{}'", s))?;
let digits = if scale < IFIXED_SCALE {
digits.to_owned()
+ &String::from_utf8(vec![b'0'; (IFIXED_SCALE - scale) as usize])
.expect("0s are valid utf8")
} else {
digits[0..(digits.len() - (scale - IFIXED_SCALE) as usize)].to_owned()
};
let is_neg = digits.starts_with('-');
let u256_str = if is_neg { &digits[1..] } else { &digits };
let inner =
U256::from_str_radix(u256_str, RADIX).map_err(|e| format!("Parsing inner u256: {e:?}"))?;
if inner > max_i256() {
return Err(format!("Inner digits exceed maximum '{}'", max_i256()));
}
let unsigned = IFixed::from_inner(I256::from_inner(inner));
Ok(if is_neg { -unsigned } else { unsigned })
}
#[cfg(test)]
mod tests {
use bigdecimal::BigDecimal;
use super::*;
use crate::types::Fixed;
impl IFixed {
fn from_f64_faulty(value: f64) -> Self {
let max_i256 = U256::max_value() >> 1;
let unsigned_inner = Fixed::from(value.abs()).into_inner().min(max_i256);
let unsigned_inner = I256::from_inner(unsigned_inner);
Self::from_inner(if value.is_sign_negative() {
-unsigned_inner
} else {
unsigned_inner
})
}
}
#[test]
fn original_conversion() {
let mut float = 0.001_f64;
let ifixed = IFixed::from_f64_faulty(float);
insta::assert_snapshot!(ifixed, @"0.001");
float = 0.009;
let ifixed = IFixed::from_f64_faulty(float);
insta::assert_snapshot!(ifixed, @"0.008999999999999999");
float = 0.003;
let ifixed = IFixed::from_f64_faulty(float);
insta::assert_snapshot!(ifixed, @"0.003");
float = 1e-18;
let ifixed = IFixed::from_f64_faulty(float);
insta::assert_snapshot!(ifixed, @"0.000000000000000001");
float = 2.2238;
let ifixed = IFixed::from_f64_faulty(float);
insta::assert_snapshot!(ifixed, @"2.223800000000000256");
}
fn ifixed_from_f64(v: f64) -> std::result::Result<IFixed, super::Error> {
ifixed_from_str(&v.to_string())
}
#[test]
fn new_conversion() {
let mut float = 0.001_f64;
let ifixed = ifixed_from_f64(float).unwrap();
insta::assert_snapshot!(ifixed, @"0.001");
float = 0.009;
insta::assert_snapshot!(float, @"0.009");
let ifixed = ifixed_from_f64(float).unwrap();
insta::assert_snapshot!(ifixed, @"0.009");
float = 0.003;
let ifixed = ifixed_from_f64(float).unwrap();
insta::assert_snapshot!(ifixed, @"0.003");
float = 1e-18;
insta::assert_snapshot!(float, @"0.000000000000000001");
let ifixed = ifixed_from_f64(float).unwrap();
insta::assert_snapshot!(ifixed, @"0.000000000000000001");
float = 2.2238;
let ifixed = ifixed_from_f64(float).unwrap();
insta::assert_snapshot!(ifixed, @"2.2238");
float = 2.3e+10;
insta::assert_snapshot!(float, @"23000000000");
let ifixed = ifixed_from_f64(float).unwrap();
insta::assert_snapshot!(ifixed, @"23000000000.0");
float = 1.234567e+20;
insta::assert_snapshot!(float, @"123456700000000000000");
let ifixed = ifixed_from_f64(float).unwrap();
insta::assert_snapshot!(ifixed, @"123456700000000000000.0");
let ifixed = ifixed_from_str("2.3e+10").unwrap();
insta::assert_snapshot!(ifixed, @"23000000000.0");
let ifixed = ifixed_from_str("1.234567e+20").unwrap();
insta::assert_snapshot!(ifixed, @"123456700000000000000.0");
float = -2.2238;
let ifixed = ifixed_from_f64(float).unwrap();
insta::assert_snapshot!(ifixed, @"-2.2238");
let ifixed = ifixed_from_str("-1.234567e+20").unwrap();
insta::assert_snapshot!(ifixed, @"-123456700000000000000.0");
let ifixed = ifixed_from_str(
"57896044618658097711785492504343953926634992332820282019728.792003956564819967",
)
.unwrap();
insta::assert_snapshot!(ifixed, @"57896044618658097711785492504343953926634992332820282019728.792003956564819967");
let ifixed = ifixed_from_str(
"-57896044618658097711785492504343953926634992332820282019728.792003956564819967",
)
.unwrap();
insta::assert_snapshot!(ifixed, @"-57896044618658097711785492504343953926634992332820282019728.792003956564819967");
let err = ifixed_from_str(
"57896044618658097711785492504343953926634992332820282019728.792003956564819968",
)
.unwrap_err();
insta::assert_debug_snapshot!(err, @r###"
Error {
string: "57896044618658097711785492504343953926634992332820282019728.792003956564819968",
error: "Inner digits exceed maximum '57896044618658097711785492504343953926634992332820282019728792003956564819967'",
}
"###);
float = f64::INFINITY;
let err = ifixed_from_f64(float).unwrap_err();
insta::assert_debug_snapshot!(err, @r###"
Error {
string: "inf",
error: "Parsing inner u256: U256FromStrError(FromStrRadixErr { kind: InvalidCharacter, source: Some(Dec(InvalidCharacter)) })",
}
"###);
float = f64::NEG_INFINITY;
let err = ifixed_from_f64(float).unwrap_err();
insta::assert_debug_snapshot!(err, @r###"
Error {
string: "-inf",
error: "Parsing inner u256: U256FromStrError(FromStrRadixErr { kind: InvalidCharacter, source: Some(Dec(InvalidCharacter)) })",
}
"###);
float = f64::NAN;
let err = ifixed_from_f64(float).unwrap_err();
insta::assert_debug_snapshot!(err, @r###"
Error {
string: "NaN",
error: "Parsing inner u256: U256FromStrError(FromStrRadixErr { kind: InvalidCharacter, source: Some(Dec(InvalidCharacter)) })",
}
"###);
}
#[test]
fn conversion_via_bigdecimal() {
let mut float = 0.001_f64;
let ifixed = IFixed::from_f64(float).unwrap();
insta::assert_snapshot!(ifixed, @"0.001");
float = 0.009;
insta::assert_snapshot!(float, @"0.009");
let ifixed = IFixed::from_f64(float).unwrap();
insta::assert_snapshot!(ifixed, @"0.009");
float = 0.003;
let ifixed = IFixed::from_f64(float).unwrap();
insta::assert_snapshot!(ifixed, @"0.003");
float = 1e-18;
let ifixed = IFixed::from_f64(float).unwrap();
insta::assert_snapshot!(ifixed, @"0.000000000000000001");
float = 2.2238;
let ifixed = IFixed::from_f64(float).unwrap();
insta::assert_snapshot!(ifixed, @"2.2238");
}
#[test]
fn bigdecimal_native_conversion() {
let float = 0.009;
insta::assert_snapshot!(float, @"0.009");
let bigd: BigDecimal = "0.009".parse().unwrap();
insta::assert_snapshot!(bigd, @"0.009");
let bigd: BigDecimal = float.try_into().unwrap();
insta::assert_snapshot!(bigd, @"0.00899999999999999931998839741709161899052560329437255859375");
let bigd: BigDecimal = float.to_string().parse().unwrap();
insta::assert_snapshot!(bigd, @"0.009");
let float = 2.2238;
insta::assert_snapshot!(float, @"2.2238");
let bigd: BigDecimal = "2.2238".parse().unwrap();
insta::assert_snapshot!(bigd, @"2.2238");
let bigd: BigDecimal = float.try_into().unwrap();
insta::assert_snapshot!(bigd, @"2.223800000000000220978790821391157805919647216796875");
let bigd: BigDecimal = float.to_string().parse().unwrap();
insta::assert_snapshot!(bigd, @"2.2238");
}
type Result<T> = std::result::Result<T, Error>;
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
enum Error {
#[error("Couldn't convert from {value}: {error}")]
FromF64 { value: f64, error: String },
}
trait FromF64: Sized {
fn from_f64(value: f64) -> Result<Self>;
}
impl FromF64 for IFixed {
fn from_f64(value: f64) -> Result<Self> {
let decimal: BigDecimal = value.to_string().parse().map_err(|e| Error::FromF64 {
value,
error: format!("Parsing string representation: {e:?}"),
})?;
let bytes = decimal
.with_scale(IFIXED_SCALE)
.into_bigint_and_scale()
.0
.to_signed_bytes_le();
let u256_bytes = get_bytes_padded(bytes).map_err(|vec| Error::FromF64 {
value,
error: format!("BigDecimal has too many bytes; {} > 32", vec.len()),
})?;
Ok(Self::from_inner(I256::from_inner(U256::from_le_bytes(
&u256_bytes,
))))
}
}
fn get_bytes_padded<const N: usize>(mut vec: Vec<u8>) -> std::result::Result<[u8; N], Vec<u8>> {
if vec.len() > N {
return Err(vec);
}
vec.resize(N, 0);
Ok(vec.try_into().expect("len == N"))
}
}