use super::uint::{Uint, Wide};
use super::{AmountError, AmountQuantum, AmountRounding, DecimalAmount, finish, round_count};
use crate::error::NonFinite;
const FROM_EXACT: &str = "from_binary64_exact";
const FROM_ROUNDED: &str = "from_binary64_rounded";
const TO_EXACT: &str = "to_binary64_exact";
type Conversion = Wide<19>;
struct Parts {
negative: bool,
significand: u64,
exponent: i32,
}
fn decompose(value: f64, operation: &'static str) -> Result<Parts, AmountError> {
if let Some(class) = NonFinite::classify(value) {
return Err(AmountError::NonFinite {
operation,
value: class,
});
}
let bits = value.to_bits();
let field = (bits >> 52) & 0x7ff;
let fraction = bits & ((1 << 52) - 1);
let field = field as i32;
let (significand, exponent) = if field == 0 {
(fraction, -1074)
} else {
(fraction | (1 << 52), field - 1075)
};
Ok(Parts {
negative: bits >> 63 != 0,
significand,
exponent,
})
}
const ZERO: DecimalAmount = DecimalAmount {
coefficient: 0,
scale: 0,
};
impl DecimalAmount {
pub fn from_binary64_exact(value: f64) -> Result<Self, AmountError> {
let Parts {
negative,
significand,
exponent,
} = decompose(value, FROM_EXACT)?;
if significand == 0 {
return Ok(ZERO);
}
let zeros = significand.trailing_zeros();
let odd = significand >> zeros;
let exponent = exponent.saturating_add_unsigned(zeros);
let range = || AmountError::OutOfRange {
operation: FROM_EXACT,
negative,
};
if exponent >= 0 {
let magnitude = Uint::from_u128(u128::from(odd))
.checked_shl(exponent.unsigned_abs())
.ok_or_else(range)?;
return finish(magnitude, negative, 0, FROM_EXACT);
}
let places = exponent.unsigned_abs();
if places > u32::from(Self::MAX_SCALE) {
#[expect(
clippy::cast_possible_truncation,
reason = "a binary64 needs at most 1074 fractional places"
)]
let scale = places as u16;
return Err(AmountError::ResultScale {
operation: FROM_EXACT,
scale,
maximum: Self::MAX_SCALE,
});
}
let power = 5_u128.checked_pow(places).ok_or_else(range)?;
let magnitude = Uint::from_u128(u128::from(odd))
.checked_mul(Uint::from_u128(power))
.ok_or_else(range)?;
#[expect(
clippy::cast_possible_truncation,
reason = "places is at most 38 in this branch"
)]
let scale = places as u16;
finish(magnitude, negative, scale, FROM_EXACT)
}
pub fn from_binary64_rounded(
value: f64,
quantum: AmountQuantum,
mode: AmountRounding,
) -> Result<Self, AmountError> {
let Parts {
negative,
significand,
exponent,
} = decompose(value, FROM_ROUNDED)?;
if significand == 0 {
return Ok(ZERO);
}
let capacity = |result: Option<Conversion>| {
result.ok_or(AmountError::OutOfRange {
operation: FROM_ROUNDED,
negative,
})
};
let scale = quantum.amount.scale;
let decimal = capacity(Conversion::pow10(u16::from(scale)))?;
let units = Conversion::from_u128(quantum.amount.coefficient.unsigned_abs());
let significand = Conversion::from_u128(u128::from(significand));
let shift = exponent.unsigned_abs();
let (numerator, denominator) = if exponent >= 0 {
let shifted = capacity(significand.checked_shl(shift))?;
(capacity(shifted.checked_mul(decimal))?, units)
} else {
(
capacity(significand.checked_mul(decimal))?,
capacity(units.checked_shl(shift))?,
)
};
let count = round_count(numerator, denominator, negative, mode, FROM_ROUNDED)?;
let product = capacity(count.checked_mul(units))?;
let product = product.resize::<8>().ok_or(AmountError::OutOfRange {
operation: FROM_ROUNDED,
negative,
})?;
finish(product, negative, u16::from(scale), FROM_ROUNDED)
}
pub fn to_binary64_exact(self) -> Result<f64, AmountError> {
match self.binary64_parts() {
Some((bits, true)) => Ok(f64::from_bits(bits)),
_ => Err(AmountError::Inexact {
operation: TO_EXACT,
}),
}
}
#[must_use]
pub fn to_binary64_nearest(self) -> f64 {
self.binary64_parts()
.map_or(f64::NAN, |(bits, _)| f64::from_bits(bits))
}
fn binary64_parts(self) -> Option<(u64, bool)> {
if self.coefficient == 0 {
return Some((0, true));
}
let sign = if self.coefficient < 0 { 1_u64 << 63 } else { 0 };
let magnitude = Uint::from_u128(self.coefficient.unsigned_abs());
let base = Uint::from_u128(10_u128.pow(u32::from(self.scale)));
#[expect(clippy::cast_possible_wrap, reason = "both bit counts are at most 128")]
let width = magnitude.bits() as i32 - base.bits() as i32;
let mut exponent = width - 53;
let mut quotient = Self::scaled(magnitude, base, exponent)?;
if quotient.0.bits() > 53 {
exponent += 1;
quotient = Self::scaled(magnitude, base, exponent)?;
}
let (mut significand, remainder, denominator) = quotient;
let exact = remainder.is_zero();
if !exact {
let complement = denominator.checked_sub(remainder)?;
if remainder > complement || (remainder == complement && significand.is_odd()) {
significand = significand.checked_add(Uint::from_u128(1))?;
if significand.bits() > 53 {
significand = Uint::from_u128(1 << 52);
exponent += 1;
}
}
}
let significand = u64::try_from(significand.to_u128()?).ok()?;
let biased = u64::try_from(exponent + 1075).ok()?;
Some((sign | (biased << 52) | (significand - (1 << 52)), exact))
}
fn scaled(magnitude: Uint, base: Uint, exponent: i32) -> Option<(Uint, Uint, Uint)> {
let shift = exponent.unsigned_abs();
let (numerator, denominator) = if exponent <= 0 {
(magnitude.checked_shl(shift)?, base)
} else {
(magnitude, base.checked_shl(shift)?)
};
let (quotient, remainder) = numerator.div_rem(denominator)?;
Some((quotient, remainder, denominator))
}
}