use std::fmt::Display;
use nautilus_core::correctness::FAILED;
use crate::types::{price::PriceRaw, quantity::QuantityRaw};
#[unsafe(no_mangle)]
#[allow(unsafe_code)]
pub static HIGH_PRECISION_MODE: u8 = cfg!(feature = "high-precision") as u8;
#[cfg(feature = "high-precision")]
pub const FIXED_PRECISION: u8 = 16;
#[cfg(not(feature = "high-precision"))]
pub const FIXED_PRECISION: u8 = 9;
#[cfg(feature = "high-precision")]
pub const PRECISION_BYTES: i32 = 16;
#[cfg(not(feature = "high-precision"))]
pub const PRECISION_BYTES: i32 = 8;
#[cfg(feature = "high-precision")]
pub const FIXED_SIZE_BINARY: &str = "FixedSizeBinary(16)";
#[cfg(not(feature = "high-precision"))]
pub const FIXED_SIZE_BINARY: &str = "FixedSizeBinary(8)";
#[cfg(feature = "high-precision")]
pub const FIXED_SCALAR: f64 = 10_000_000_000_000_000.0;
#[cfg(not(feature = "high-precision"))]
pub const FIXED_SCALAR: f64 = 1_000_000_000.0;
#[cfg(feature = "high-precision")]
pub const PRECISION_DIFF_SCALAR: f64 = 10_000_000.0;
#[cfg(not(feature = "high-precision"))]
pub const PRECISION_DIFF_SCALAR: f64 = 1.0;
const POWERS_OF_10: [u64; 17] = [
1, 10, 100, 1_000, 10_000, 100_000, 1_000_000, 10_000_000, 100_000_000, 1_000_000_000, 10_000_000_000, 100_000_000_000, 1_000_000_000_000, 10_000_000_000_000, 100_000_000_000_000, 1_000_000_000_000_000, 10_000_000_000_000_000, ];
const _: () = assert!(
(FIXED_PRECISION as usize) < POWERS_OF_10.len(),
"FIXED_PRECISION exceeds POWERS_OF_10 table size"
);
pub const MAX_FLOAT_PRECISION: u8 = 16;
pub fn check_fixed_precision(precision: u8) -> anyhow::Result<()> {
#[cfg(feature = "defi")]
if precision > crate::defi::WEI_PRECISION {
anyhow::bail!("`precision` exceeded maximum `WEI_PRECISION` (18), was {precision}")
}
#[cfg(not(feature = "defi"))]
if precision > FIXED_PRECISION {
anyhow::bail!(
"`precision` exceeded maximum `FIXED_PRECISION` ({FIXED_PRECISION}), was {precision}"
)
}
Ok(())
}
#[inline(always)]
fn should_skip_validation(precision: u8) -> bool {
if precision == FIXED_PRECISION {
return true;
}
if precision > FIXED_PRECISION {
#[cfg(not(feature = "defi"))]
debug_assert!(
false,
"precision {precision} exceeds FIXED_PRECISION {FIXED_PRECISION}: \
raw value validation is not possible at this precision"
);
return true;
}
false
}
#[cold]
fn invalid_raw_error(
raw: impl Display,
precision: u8,
remainder: impl Display,
scale: impl Display,
) -> anyhow::Error {
anyhow::anyhow!(
"Invalid fixed-point raw value {raw} for precision {precision}: \
remainder {remainder} when divided by scale {scale}. \
Raw value should be a multiple of {scale}. \
This indicates data corruption or incorrect precision/scaling upstream"
)
}
#[inline(always)]
pub fn check_fixed_raw_u128(raw: u128, precision: u8) -> anyhow::Result<()> {
if should_skip_validation(precision) {
return Ok(());
}
let exp = usize::from(FIXED_PRECISION - precision);
let scale = u128::from(POWERS_OF_10[exp]);
let remainder = raw % scale;
if remainder != 0 {
return Err(invalid_raw_error(raw, precision, remainder, scale));
}
Ok(())
}
#[inline(always)]
pub fn check_fixed_raw_u64(raw: u64, precision: u8) -> anyhow::Result<()> {
if should_skip_validation(precision) {
return Ok(());
}
let exp = usize::from(FIXED_PRECISION - precision);
let scale = POWERS_OF_10[exp];
let remainder = raw % scale;
if remainder != 0 {
return Err(invalid_raw_error(raw, precision, remainder, scale));
}
Ok(())
}
#[inline(always)]
pub fn check_fixed_raw_i128(raw: i128, precision: u8) -> anyhow::Result<()> {
if should_skip_validation(precision) {
return Ok(());
}
let exp = usize::from(FIXED_PRECISION - precision);
let scale = i128::from(POWERS_OF_10[exp]);
let remainder = raw % scale;
if remainder != 0 {
return Err(invalid_raw_error(raw, precision, remainder, scale));
}
Ok(())
}
#[inline(always)]
pub fn check_fixed_raw_i64(raw: i64, precision: u8) -> anyhow::Result<()> {
if should_skip_validation(precision) {
return Ok(());
}
let exp = usize::from(FIXED_PRECISION - precision);
let scale = POWERS_OF_10[exp] as i64;
let remainder = raw % scale;
if remainder != 0 {
return Err(invalid_raw_error(raw, precision, remainder, scale));
}
Ok(())
}
#[must_use]
pub fn correct_raw_u128(raw: u128, precision: u8) -> u128 {
if precision >= FIXED_PRECISION {
return raw;
}
let exp = usize::from(FIXED_PRECISION - precision);
let scale = u128::from(POWERS_OF_10[exp]);
let half_scale = scale / 2;
let remainder = raw % scale;
if remainder == 0 {
raw
} else if remainder >= half_scale {
raw + (scale - remainder)
} else {
raw - remainder
}
}
#[must_use]
pub fn correct_raw_u64(raw: u64, precision: u8) -> u64 {
if precision >= FIXED_PRECISION {
return raw;
}
let exp = usize::from(FIXED_PRECISION - precision);
let scale = POWERS_OF_10[exp];
let half_scale = scale / 2;
let remainder = raw % scale;
if remainder == 0 {
raw
} else if remainder >= half_scale {
raw + (scale - remainder)
} else {
raw - remainder
}
}
#[must_use]
pub fn correct_raw_i128(raw: i128, precision: u8) -> i128 {
if precision >= FIXED_PRECISION {
return raw;
}
let exp = usize::from(FIXED_PRECISION - precision);
let scale = i128::from(POWERS_OF_10[exp]);
let half_scale = scale / 2;
let remainder = raw % scale;
if remainder == 0 {
raw
} else if raw >= 0 {
if remainder >= half_scale {
raw + (scale - remainder)
} else {
raw - remainder
}
} else {
if remainder.abs() >= half_scale {
raw - (scale + remainder)
} else {
raw - remainder
}
}
}
#[must_use]
pub fn correct_raw_i64(raw: i64, precision: u8) -> i64 {
if precision >= FIXED_PRECISION {
return raw;
}
let exp = usize::from(FIXED_PRECISION - precision);
let scale = POWERS_OF_10[exp] as i64;
let half_scale = scale / 2;
let remainder = raw % scale;
if remainder == 0 {
raw
} else if raw >= 0 {
if remainder >= half_scale {
raw + (scale - remainder)
} else {
raw - remainder
}
} else {
if remainder.abs() >= half_scale {
raw - (scale + remainder)
} else {
raw - remainder
}
}
}
#[must_use]
#[inline]
pub fn correct_price_raw(raw: PriceRaw, precision: u8) -> PriceRaw {
#[cfg(feature = "high-precision")]
{
correct_raw_i128(raw, precision)
}
#[cfg(not(feature = "high-precision"))]
{
correct_raw_i64(raw, precision)
}
}
#[must_use]
#[inline]
pub fn correct_quantity_raw(raw: QuantityRaw, precision: u8) -> QuantityRaw {
#[cfg(feature = "high-precision")]
{
correct_raw_u128(raw, precision)
}
#[cfg(not(feature = "high-precision"))]
{
correct_raw_u64(raw, precision)
}
}
#[must_use]
#[inline]
pub fn bankers_round(mantissa: i128, excess: u32) -> i128 {
if excess == 0 {
return mantissa;
}
if excess >= 39 {
return 0;
}
let divisor = 10i128.pow(excess);
let quotient = mantissa / divisor;
let remainder = mantissa % divisor;
let half = divisor / 2;
if remainder.abs() > half || (remainder.abs() == half && quotient % 2 != 0) {
quotient + mantissa.signum()
} else {
quotient
}
}
pub fn mantissa_exponent_to_fixed_i128(
mantissa: i128,
exponent: i8,
precision: u8,
) -> anyhow::Result<i128> {
check_fixed_precision(precision)?;
let precision_i16 = precision as i16;
let target_scale = (FIXED_PRECISION as i16).max(precision_i16);
let frac_digits = -(exponent as i16);
let mantissa = if frac_digits > precision_i16 {
let excess = (frac_digits - precision_i16) as u32;
bankers_round(mantissa, excess)
} else {
mantissa
};
let scale_after_rounding = frac_digits.min(precision_i16);
let scale_exp = target_scale - scale_after_rounding;
anyhow::ensure!(
scale_exp <= 38,
"Exponent {exponent} produces scale factor 10^{scale_exp} which exceeds i128 range"
);
if scale_exp >= 0 {
mantissa.checked_mul(10i128.pow(scale_exp as u32))
} else {
Some(mantissa / 10i128.pow((-scale_exp) as u32))
}
.ok_or_else(|| anyhow::anyhow!("Overflow when scaling mantissa to fixed precision"))
}
#[must_use]
pub fn f64_to_fixed_i64(value: f64, precision: u8) -> i64 {
check_fixed_precision(precision).expect(FAILED);
let pow1 = 10_i64.pow(u32::from(precision));
let pow2 = 10_i64.pow(u32::from(FIXED_PRECISION - precision));
let rounded = (value * pow1 as f64).round() as i64;
rounded * pow2
}
pub fn f64_to_fixed_i128(value: f64, precision: u8) -> i128 {
check_fixed_precision(precision).expect(FAILED);
let pow1 = 10_i128.pow(u32::from(precision));
let pow2 = 10_i128.pow(u32::from(FIXED_PRECISION - precision));
let rounded = (value * pow1 as f64).round() as i128;
rounded * pow2
}
#[must_use]
pub fn f64_to_fixed_u64(value: f64, precision: u8) -> u64 {
check_fixed_precision(precision).expect(FAILED);
let pow1 = 10_u64.pow(u32::from(precision));
let pow2 = 10_u64.pow(u32::from(FIXED_PRECISION - precision));
let rounded = (value * pow1 as f64).round() as u64;
rounded * pow2
}
#[must_use]
pub fn f64_to_fixed_u128(value: f64, precision: u8) -> u128 {
check_fixed_precision(precision).expect(FAILED);
let pow1 = 10_u128.pow(u32::from(precision));
let pow2 = 10_u128.pow(u32::from(FIXED_PRECISION - precision));
let rounded = (value * pow1 as f64).round() as u128;
rounded * pow2
}
#[must_use]
pub fn fixed_i64_to_f64(value: i64) -> f64 {
(value as f64) / FIXED_SCALAR
}
#[must_use]
pub fn fixed_i128_to_f64(value: i128) -> f64 {
(value as f64) / FIXED_SCALAR
}
#[must_use]
pub fn fixed_u64_to_f64(value: u64) -> f64 {
(value as f64) / FIXED_SCALAR
}
#[must_use]
pub fn fixed_u128_to_f64(value: u128) -> f64 {
(value as f64) / FIXED_SCALAR
}
#[cfg(feature = "high-precision")]
#[cfg(test)]
mod tests {
use nautilus_core::approx_eq;
use rstest::rstest;
use super::*;
#[cfg(not(feature = "high-precision"))]
#[rstest]
fn test_precision_boundaries() {
assert!(check_fixed_precision(0).is_ok());
assert!(check_fixed_precision(FIXED_PRECISION).is_ok());
assert!(check_fixed_precision(FIXED_PRECISION + 1).is_err());
}
#[cfg(feature = "defi")]
#[rstest]
fn test_precision_boundaries() {
use crate::defi::WEI_PRECISION;
assert!(check_fixed_precision(0).is_ok());
assert!(check_fixed_precision(WEI_PRECISION).is_ok());
assert!(check_fixed_precision(WEI_PRECISION + 1).is_err());
}
#[rstest]
#[case(0.0)]
#[case(1.0)]
#[case(-1.0)]
fn test_basic_roundtrip(#[case] value: f64) {
for precision in 0..=FIXED_PRECISION {
let fixed = f64_to_fixed_i128(value, precision);
let result = fixed_i128_to_f64(fixed);
assert!(approx_eq!(f64, value, result, epsilon = 0.001));
}
}
#[rstest]
#[case(1000000.0)]
#[case(-1000000.0)]
fn test_large_value_roundtrip(#[case] value: f64) {
for precision in 0..=FIXED_PRECISION {
let fixed = f64_to_fixed_i128(value, precision);
let result = fixed_i128_to_f64(fixed);
assert!(approx_eq!(f64, value, result, epsilon = 0.000_1));
}
}
#[rstest]
#[case(0, 123456.0)]
#[case(0, 123456.7)]
#[case(1, 123456.7)]
#[case(2, 123456.78)]
#[case(8, 123456.12345678)]
fn test_precision_specific_values_basic(#[case] precision: u8, #[case] value: f64) {
let result = f64_to_fixed_i128(value, precision);
let back_converted = fixed_i128_to_f64(result);
let scale = 10.0_f64.powi(precision as i32);
let expected_rounded = (value * scale).round() / scale;
assert!((back_converted - expected_rounded).abs() < 1e-10);
}
#[rstest]
fn test_max_precision_values() {
let test_value = 123456.123456789;
let result = f64_to_fixed_i128(test_value, FIXED_PRECISION);
let back_converted = fixed_i128_to_f64(result);
assert!((back_converted - test_value).abs() < 1e-6);
}
#[rstest]
#[case(0.0)]
#[case(1.0)]
#[case(1000000.0)]
fn test_unsigned_basic_roundtrip(#[case] value: f64) {
for precision in 0..=FIXED_PRECISION {
let fixed = f64_to_fixed_u128(value, precision);
let result = fixed_u128_to_f64(fixed);
assert!(approx_eq!(f64, value, result, epsilon = 0.001));
}
}
#[rstest]
#[case(0)]
#[case(FIXED_PRECISION)]
fn test_valid_precision(#[case] precision: u8) {
let result = check_fixed_precision(precision);
assert!(result.is_ok());
}
#[cfg(not(feature = "defi"))]
#[rstest]
fn test_invalid_precision() {
let precision = FIXED_PRECISION + 1;
let result = check_fixed_precision(precision);
assert!(result.is_err());
}
#[cfg(feature = "defi")]
#[rstest]
fn test_invalid_precision() {
use crate::defi::WEI_PRECISION;
let precision = WEI_PRECISION + 1;
let result = check_fixed_precision(precision);
assert!(result.is_err());
}
#[rstest]
#[case(0, 0.0)]
#[case(1, 1.0)]
#[case(1, 1.1)]
#[case(9, 0.000_000_001)]
#[case(16, 0.000_000_000_000_000_1)]
#[case(0, -0.0)]
#[case(1, -1.0)]
#[case(1, -1.1)]
#[case(9, -0.000_000_001)]
#[case(16, -0.000_000_000_000_000_1)]
fn test_f64_to_fixed_i128_to_fixed(#[case] precision: u8, #[case] value: f64) {
let fixed = f64_to_fixed_i128(value, precision);
let result = fixed_i128_to_f64(fixed);
assert_eq!(result, value);
}
#[rstest]
#[case(0, 0.0)]
#[case(1, 1.0)]
#[case(1, 1.1)]
#[case(9, 0.000_000_001)]
#[case(16, 0.000_000_000_000_000_1)]
fn test_f64_to_fixed_u128_to_fixed(#[case] precision: u8, #[case] value: f64) {
let fixed = f64_to_fixed_u128(value, precision);
let result = fixed_u128_to_f64(fixed);
assert_eq!(result, value);
}
#[rstest]
#[case(0, 123_456.0)]
#[case(0, 123_456.7)]
#[case(0, 123_456.4)]
#[case(1, 123_456.0)]
#[case(1, 123_456.7)]
#[case(1, 123_456.4)]
#[case(2, 123_456.0)]
#[case(2, 123_456.7)]
#[case(2, 123_456.4)]
fn test_f64_to_fixed_i128_with_precision(#[case] precision: u8, #[case] value: f64) {
let result = f64_to_fixed_i128(value, precision);
let pow1 = 10_i128.pow(u32::from(precision));
let pow2 = 10_i128.pow(u32::from(FIXED_PRECISION - precision));
let rounded = (value * pow1 as f64).round() as i128;
let expected = rounded * pow2;
assert_eq!(
result, expected,
"Failed for precision {precision}, value {value}: got {result}, expected {expected}"
);
}
#[rstest]
#[case(0, 5.555555555555555)]
#[case(1, 5.555555555555555)]
#[case(2, 5.555555555555555)]
#[case(3, 5.555555555555555)]
#[case(4, 5.555555555555555)]
#[case(5, 5.555555555555555)]
#[case(6, 5.555555555555555)]
#[case(7, 5.555555555555555)]
#[case(8, 5.555555555555555)]
#[case(9, 5.555555555555555)]
#[case(10, 5.555555555555555)]
#[case(11, 5.555555555555555)]
#[case(12, 5.555555555555555)]
#[case(13, 5.555555555555555)]
#[case(14, 5.555555555555555)]
#[case(15, 5.555555555555555)]
#[case(0, -5.555555555555555)]
#[case(1, -5.555555555555555)]
#[case(2, -5.555555555555555)]
#[case(3, -5.555555555555555)]
#[case(4, -5.555555555555555)]
#[case(5, -5.555555555555555)]
#[case(6, -5.555555555555555)]
#[case(7, -5.555555555555555)]
#[case(8, -5.555555555555555)]
#[case(9, -5.555555555555555)]
#[case(10, -5.555555555555555)]
#[case(11, -5.555555555555555)]
#[case(12, -5.555555555555555)]
#[case(13, -5.555555555555555)]
#[case(14, -5.555555555555555)]
#[case(15, -5.555555555555555)]
fn test_f64_to_fixed_i128(#[case] precision: u8, #[case] value: f64) {
if precision > FIXED_PRECISION {
return;
}
let result = f64_to_fixed_i128(value, precision);
let pow1 = 10_i128.pow(u32::from(precision));
let pow2 = 10_i128.pow(u32::from(FIXED_PRECISION - precision));
let rounded = (value * pow1 as f64).round() as i128;
let expected = rounded * pow2;
assert_eq!(
result, expected,
"Failed for precision {precision}, value {value}: got {result}, expected {expected}"
);
}
#[rstest]
#[case(0, 5.555555555555555)]
#[case(1, 5.555555555555555)]
#[case(2, 5.555555555555555)]
#[case(3, 5.555555555555555)]
#[case(4, 5.555555555555555)]
#[case(5, 5.555555555555555)]
#[case(6, 5.555555555555555)]
#[case(7, 5.555555555555555)]
#[case(8, 5.555555555555555)]
#[case(9, 5.555555555555555)]
#[case(10, 5.555555555555555)]
#[case(11, 5.555555555555555)]
#[case(12, 5.555555555555555)]
#[case(13, 5.555555555555555)]
#[case(14, 5.555555555555555)]
#[case(15, 5.555555555555555)]
#[case(16, 5.555555555555555)]
fn test_f64_to_fixed_u64(#[case] precision: u8, #[case] value: f64) {
if precision > FIXED_PRECISION {
return;
}
let result = f64_to_fixed_u128(value, precision);
let pow1 = 10_u128.pow(u32::from(precision));
let pow2 = 10_u128.pow(u32::from(FIXED_PRECISION - precision));
let rounded = (value * pow1 as f64).round() as u128;
let expected = rounded * pow2;
assert_eq!(
result, expected,
"Failed for precision {precision}, value {value}: got {result}, expected {expected}"
);
}
#[rstest]
fn test_fixed_i128_to_f64(
#[values(1, -1, 2, -2, 10, -10, 100, -100, 1_000, -1_000, -10_000, -100_000)] value: i128,
) {
assert_eq!(fixed_i128_to_f64(value), value as f64 / FIXED_SCALAR);
}
#[rstest]
fn test_fixed_u128_to_f64(
#[values(
0,
1,
2,
3,
10,
100,
1_000,
10_000,
100_000,
1_000_000,
10_000_000,
100_000_000,
1_000_000_000,
10_000_000_000,
100_000_000_000,
1_000_000_000_000,
10_000_000_000_000,
100_000_000_000_000,
1_000_000_000_000_000,
10_000_000_000_000_000,
100_000_000_000_000_000,
1_000_000_000_000_000_000,
10_000_000_000_000_000_000,
100_000_000_000_000_000_000
)]
value: u128,
) {
let result = fixed_u128_to_f64(value);
assert_eq!(result, (value as f64) / FIXED_SCALAR);
}
#[rstest]
#[case(0, 0)] #[case(0, 10_000_000_000_000_000)] #[case(0, 1_200_000_000_000_000_000)] #[case(8, 12_345_678_900_000_000)] #[case(15, 1_234_567_890_123_450)] fn test_check_fixed_raw_u128_valid(#[case] precision: u8, #[case] raw: u128) {
assert!(check_fixed_raw_u128(raw, precision).is_ok());
}
#[rstest]
#[case(0, 1)] #[case(0, 9_999_999_999_999_999)] #[case(0, 10_000_000_000_000_001)] #[case(8, 12_345_678_900_000_001)] #[case(15, 1_234_567_890_123_451)] fn test_check_fixed_raw_u128_invalid(#[case] precision: u8, #[case] raw: u128) {
assert!(check_fixed_raw_u128(raw, precision).is_err());
}
#[rstest]
fn test_check_fixed_raw_u128_at_max_precision() {
assert!(check_fixed_raw_u128(0, FIXED_PRECISION).is_ok());
assert!(check_fixed_raw_u128(1, FIXED_PRECISION).is_ok());
assert!(check_fixed_raw_u128(123_456_789, FIXED_PRECISION).is_ok());
assert!(check_fixed_raw_u128(u128::MAX, FIXED_PRECISION).is_ok());
}
#[rstest]
#[case(0, 0)]
#[case(0, 10_000_000_000_000_000)]
#[case(0, -10_000_000_000_000_000)]
#[case(8, 12_345_678_900_000_000)]
#[case(8, -12_345_678_900_000_000)]
fn test_check_fixed_raw_i128_valid(#[case] precision: u8, #[case] raw: i128) {
assert!(check_fixed_raw_i128(raw, precision).is_ok());
}
#[rstest]
#[case(0, 1)]
#[case(0, -1)]
#[case(0, 9_999_999_999_999_999)]
#[case(0, -9_999_999_999_999_999)]
fn test_check_fixed_raw_i128_invalid(#[case] precision: u8, #[case] raw: i128) {
assert!(check_fixed_raw_i128(raw, precision).is_err());
}
#[rstest]
fn test_check_fixed_raw_i128_at_max_precision() {
assert!(check_fixed_raw_i128(0, FIXED_PRECISION).is_ok());
assert!(check_fixed_raw_i128(1, FIXED_PRECISION).is_ok());
assert!(check_fixed_raw_i128(-1, FIXED_PRECISION).is_ok());
assert!(check_fixed_raw_i128(i128::MAX, FIXED_PRECISION).is_ok());
assert!(check_fixed_raw_i128(i128::MIN, FIXED_PRECISION).is_ok());
}
}
#[cfg(not(feature = "high-precision"))]
#[cfg(test)]
mod tests {
use nautilus_core::approx_eq;
use rstest::rstest;
use super::*;
#[rstest]
fn test_precision_boundaries() {
assert!(check_fixed_precision(0).is_ok());
assert!(check_fixed_precision(FIXED_PRECISION).is_ok());
assert!(check_fixed_precision(FIXED_PRECISION + 1).is_err());
}
#[rstest]
#[case(0.0)]
#[case(1.0)]
#[case(-1.0)]
fn test_basic_roundtrip(#[case] value: f64) {
for precision in 0..=FIXED_PRECISION {
let fixed = f64_to_fixed_i64(value, precision);
let result = fixed_i64_to_f64(fixed);
assert!(approx_eq!(f64, value, result, epsilon = 0.001));
}
}
#[rstest]
#[case(1000000.0)]
#[case(-1000000.0)]
fn test_large_value_roundtrip(#[case] value: f64) {
for precision in 0..=FIXED_PRECISION {
let fixed = f64_to_fixed_i64(value, precision);
let result = fixed_i64_to_f64(fixed);
assert!(approx_eq!(f64, value, result, epsilon = 0.000_1));
}
}
#[rstest]
#[case(0, 123456.0, 123456_000000000)]
#[case(0, 123456.7, 123457_000000000)]
#[case(1, 123456.7, 123456_700000000)]
#[case(2, 123456.78, 123456_780000000)]
#[case(8, 123456.12345678, 123456_123456780)]
#[case(9, 123456.123456789, 123456_123456789)]
fn test_precision_specific_values(
#[case] precision: u8,
#[case] value: f64,
#[case] expected: i64,
) {
assert_eq!(f64_to_fixed_i64(value, precision), expected);
}
#[rstest]
#[case(0.0)]
#[case(1.0)]
#[case(1000000.0)]
fn test_unsigned_basic_roundtrip(#[case] value: f64) {
for precision in 0..=FIXED_PRECISION {
let fixed = f64_to_fixed_u64(value, precision);
let result = fixed_u64_to_f64(fixed);
assert!(approx_eq!(f64, value, result, epsilon = 0.001));
}
}
#[rstest]
#[case(0, 1.4, 1.0)]
#[case(0, 1.5, 2.0)]
#[case(0, 1.6, 2.0)]
#[case(1, 1.44, 1.4)]
#[case(1, 1.45, 1.5)]
#[case(1, 1.46, 1.5)]
#[case(2, 1.444, 1.44)]
#[case(2, 1.445, 1.45)]
#[case(2, 1.446, 1.45)]
fn test_rounding(#[case] precision: u8, #[case] input: f64, #[case] expected: f64) {
let fixed = f64_to_fixed_i128(input, precision);
assert!(approx_eq!(
f64,
fixed_i128_to_f64(fixed),
expected,
epsilon = 0.000_000_001
));
}
#[rstest]
fn test_special_values() {
assert_eq!(f64_to_fixed_i128(0.0, FIXED_PRECISION), 0);
assert_eq!(f64_to_fixed_i128(-0.0, FIXED_PRECISION), 0);
let smallest_positive = 1.0 / FIXED_SCALAR;
let fixed_smallest = f64_to_fixed_i128(smallest_positive, FIXED_PRECISION);
assert_eq!(fixed_smallest, 1);
let large_int = 1_000_000_000.0;
let fixed_large = f64_to_fixed_i128(large_int, 0);
assert_eq!(fixed_i128_to_f64(fixed_large), large_int);
}
#[rstest]
#[case(0)]
#[case(FIXED_PRECISION)]
fn test_valid_precision(#[case] precision: u8) {
let result = check_fixed_precision(precision);
assert!(result.is_ok());
}
#[rstest]
fn test_invalid_precision() {
let precision = FIXED_PRECISION + 1;
let result = check_fixed_precision(precision);
assert!(result.is_err());
}
#[rstest]
#[case(0, 0.0)]
#[case(1, 1.0)]
#[case(1, 1.1)]
#[case(9, 0.000_000_001)]
#[case(0, -0.0)]
#[case(1, -1.0)]
#[case(1, -1.1)]
#[case(9, -0.000_000_001)]
fn test_f64_to_fixed_i64_to_fixed(#[case] precision: u8, #[case] value: f64) {
let fixed = f64_to_fixed_i64(value, precision);
let result = fixed_i64_to_f64(fixed);
assert_eq!(result, value);
}
#[rstest]
#[case(0, 0.0)]
#[case(1, 1.0)]
#[case(1, 1.1)]
#[case(9, 0.000_000_001)]
fn test_f64_to_fixed_u64_to_fixed(#[case] precision: u8, #[case] value: f64) {
let fixed = f64_to_fixed_u64(value, precision);
let result = fixed_u64_to_f64(fixed);
assert_eq!(result, value);
}
#[rstest]
#[case(0, 123_456.0, 123_456_000_000_000)]
#[case(0, 123_456.7, 123_457_000_000_000)]
#[case(0, 123_456.4, 123_456_000_000_000)]
#[case(1, 123_456.0, 123_456_000_000_000)]
#[case(1, 123_456.7, 123_456_700_000_000)]
#[case(1, 123_456.4, 123_456_400_000_000)]
#[case(2, 123_456.0, 123_456_000_000_000)]
#[case(2, 123_456.7, 123_456_700_000_000)]
#[case(2, 123_456.4, 123_456_400_000_000)]
fn test_f64_to_fixed_i64_with_precision(
#[case] precision: u8,
#[case] value: f64,
#[case] expected: i64,
) {
assert_eq!(f64_to_fixed_i64(value, precision), expected);
}
#[rstest]
#[case(0, 5.5, 6_000_000_000)]
#[case(1, 5.55, 5_600_000_000)]
#[case(2, 5.555, 5_560_000_000)]
#[case(3, 5.5555, 5_556_000_000)]
#[case(4, 5.55555, 5_555_600_000)]
#[case(5, 5.555_555, 5_555_560_000)]
#[case(6, 5.555_555_5, 5_555_556_000)]
#[case(7, 5.555_555_55, 5_555_555_600)]
#[case(8, 5.555_555_555, 5_555_555_560)]
#[case(9, 5.555_555_555_5, 5_555_555_556)]
#[case(0, -5.5, -6_000_000_000)]
#[case(1, -5.55, -5_600_000_000)]
#[case(2, -5.555, -5_560_000_000)]
#[case(3, -5.5555, -5_556_000_000)]
#[case(4, -5.55555, -5_555_600_000)]
#[case(5, -5.555_555, -5_555_560_000)]
#[case(6, -5.555_555_5, -5_555_556_000)]
#[case(7, -5.555_555_55, -5_555_555_600)]
#[case(8, -5.555_555_555, -5_555_555_560)]
#[case(9, -5.555_555_555_5, -5_555_555_556)]
fn test_f64_to_fixed_i64(#[case] precision: u8, #[case] value: f64, #[case] expected: i64) {
assert_eq!(f64_to_fixed_i64(value, precision), expected);
}
#[rstest]
#[case(0, 5.5, 6_000_000_000)]
#[case(1, 5.55, 5_600_000_000)]
#[case(2, 5.555, 5_560_000_000)]
#[case(3, 5.5555, 5_556_000_000)]
#[case(4, 5.55555, 5_555_600_000)]
#[case(5, 5.555_555, 5_555_560_000)]
#[case(6, 5.555_555_5, 5_555_556_000)]
#[case(7, 5.555_555_55, 5_555_555_600)]
#[case(8, 5.555_555_555, 5_555_555_560)]
#[case(9, 5.555_555_555_5, 5_555_555_556)]
fn test_f64_to_fixed_u64(#[case] precision: u8, #[case] value: f64, #[case] expected: u64) {
assert_eq!(f64_to_fixed_u64(value, precision), expected);
}
#[rstest]
fn test_fixed_i64_to_f64(
#[values(1, -1, 2, -2, 10, -10, 100, -100, 1_000, -1_000)] value: i64,
) {
assert_eq!(fixed_i64_to_f64(value), value as f64 / FIXED_SCALAR);
}
#[rstest]
fn test_fixed_u64_to_f64(
#[values(
0,
1,
2,
3,
10,
100,
1_000,
10_000,
100_000,
1_000_000,
10_000_000,
100_000_000,
1_000_000_000,
10_000_000_000,
100_000_000_000,
1_000_000_000_000,
10_000_000_000_000,
100_000_000_000_000,
1_000_000_000_000_000
)]
value: u64,
) {
let result = fixed_u64_to_f64(value);
assert_eq!(result, (value as f64) / FIXED_SCALAR);
}
#[rstest]
#[case(0, 0)] #[case(0, 1_000_000_000)] #[case(0, 120_000_000_000)] #[case(2, 123_450_000_000)] #[case(8, 1_234_567_890)] fn test_check_fixed_raw_u64_valid(#[case] precision: u8, #[case] raw: u64) {
assert!(check_fixed_raw_u64(raw, precision).is_ok());
}
#[rstest]
#[case(0, 1)] #[case(0, 999_999_999)] #[case(0, 1_000_000_001)] #[case(0, 119_582_001_968_421_736)] #[case(2, 123_456_789_000)] #[case(8, 1_234_567_891)] fn test_check_fixed_raw_u64_invalid(#[case] precision: u8, #[case] raw: u64) {
assert!(check_fixed_raw_u64(raw, precision).is_err());
}
#[rstest]
fn test_check_fixed_raw_u64_at_max_precision() {
assert!(check_fixed_raw_u64(0, FIXED_PRECISION).is_ok());
assert!(check_fixed_raw_u64(1, FIXED_PRECISION).is_ok());
assert!(check_fixed_raw_u64(123_456_789, FIXED_PRECISION).is_ok());
assert!(check_fixed_raw_u64(u64::MAX, FIXED_PRECISION).is_ok());
}
#[rstest]
#[case(0, 0)]
#[case(0, 1_000_000_000)]
#[case(0, -1_000_000_000)]
#[case(2, 123_450_000_000)]
#[case(2, -123_450_000_000)]
fn test_check_fixed_raw_i64_valid(#[case] precision: u8, #[case] raw: i64) {
assert!(check_fixed_raw_i64(raw, precision).is_ok());
}
#[rstest]
#[case(0, 1)]
#[case(0, -1)]
#[case(0, 999_999_999)]
#[case(0, -999_999_999)]
fn test_check_fixed_raw_i64_invalid(#[case] precision: u8, #[case] raw: i64) {
assert!(check_fixed_raw_i64(raw, precision).is_err());
}
#[rstest]
fn test_check_fixed_raw_i64_at_max_precision() {
assert!(check_fixed_raw_i64(0, FIXED_PRECISION).is_ok());
assert!(check_fixed_raw_i64(1, FIXED_PRECISION).is_ok());
assert!(check_fixed_raw_i64(-1, FIXED_PRECISION).is_ok());
assert!(check_fixed_raw_i64(i64::MAX, FIXED_PRECISION).is_ok());
assert!(check_fixed_raw_i64(i64::MIN, FIXED_PRECISION).is_ok());
}
}
#[cfg(test)]
mod bankers_round_tests {
use std::str::FromStr;
use rstest::rstest;
use rust_decimal::{Decimal, RoundingStrategy};
use super::*;
#[rstest]
#[case(0, 0, 0)]
#[case(1, 0, 1)]
#[case(5, 0, 5)]
#[case(99, 0, 99)]
#[case(-7, 0, -7)]
#[case(12345, 39, 0)]
#[case(i64::MAX as i128, 100, 0)]
#[case(-99999, 50, 0)]
#[case(15, 1, 2)] #[case(25, 1, 2)] #[case(35, 1, 4)] #[case(45, 1, 4)] #[case(55, 1, 6)] #[case(65, 1, 6)] #[case(75, 1, 8)] #[case(85, 1, 8)] #[case(95, 1, 10)] #[case(105, 1, 10)] #[case(14, 1, 1)] #[case(16, 1, 2)] #[case(24, 1, 2)] #[case(26, 1, 3)] #[case(11, 1, 1)] #[case(19, 1, 2)] #[case(150, 2, 2)] #[case(250, 2, 2)] #[case(350, 2, 4)] #[case(450, 2, 4)] #[case(550, 2, 6)] #[case(1050, 2, 10)] #[case(1150, 2, 12)] #[case(149, 2, 1)] #[case(151, 2, 2)] #[case(199, 2, 2)] #[case(101, 2, 1)] #[case(1500, 3, 2)] #[case(2500, 3, 2)] #[case(3500, 3, 4)] #[case(10500, 3, 10)] #[case(11500, 3, 12)] #[case(1499, 3, 1)] #[case(1501, 3, 2)] #[case(-15, 1, -2)] #[case(-25, 1, -2)] #[case(-35, 1, -4)] #[case(-45, 1, -4)] #[case(-55, 1, -6)] #[case(-65, 1, -6)] #[case(-150, 2, -2)] #[case(-250, 2, -2)] #[case(-350, 2, -4)] #[case(-14, 1, -1)] #[case(-16, 1, -2)] #[case(-24, 1, -2)] #[case(-26, 1, -3)] #[case(0, 1, 0)]
#[case(0, 2, 0)]
#[case(0, 5, 0)]
#[case(123_456_789, 3, 123_457)] #[case(123_456_500, 3, 123_456)] #[case(123_457_500, 3, 123_458)] #[case(100_005, 1, 10_000)] #[case(100_015, 1, 10_002)] #[case(999_999_999_999_999_995, 1, 100_000_000_000_000_000)]
#[case(1_000_000_000_000_000_005, 1, 100_000_000_000_000_000)]
fn test_bankers_round(#[case] mantissa: i128, #[case] excess: u32, #[case] expected: i128) {
assert_eq!(
bankers_round(mantissa, excess),
expected,
"bankers_round({mantissa}, {excess}) expected {expected}"
);
}
#[rstest]
#[case(15, 1)]
#[case(25, 1)]
#[case(35, 1)]
#[case(150, 2)]
#[case(250, 2)]
#[case(1500, 3)]
#[case(2500, 3)]
#[case(123_456_789, 3)]
#[case(14, 1)]
#[case(16, 1)]
fn test_bankers_round_negative_symmetry(#[case] mantissa: i128, #[case] excess: u32) {
assert_eq!(
bankers_round(-mantissa, excess),
-bankers_round(mantissa, excess),
"Negative symmetry failed for mantissa={mantissa}, excess={excess}"
);
}
#[rstest]
#[case("1.005", 2, "1.00")] #[case("1.015", 2, "1.02")] #[case("1.025", 2, "1.02")] #[case("1.035", 2, "1.04")] #[case("1.045", 2, "1.04")] #[case("2.5", 0, "2")] #[case("3.5", 0, "4")] #[case("-2.5", 0, "-2")]
#[case("-3.5", 0, "-4")]
#[case("123.456", 2, "123.46")]
#[case("123.455", 2, "123.46")] #[case("123.445", 2, "123.44")] fn test_bankers_round_matches_decimal(
#[case] input: &str,
#[case] target_precision: u8,
#[case] expected: &str,
) {
let dec = Decimal::from_str(input).unwrap();
let expected_dec = Decimal::from_str(expected).unwrap();
let decimal_rounded = dec.round_dp_with_strategy(
u32::from(target_precision),
RoundingStrategy::MidpointNearestEven,
);
assert_eq!(
decimal_rounded, expected_dec,
"Decimal rounding sanity check failed for {input}"
);
let mantissa = dec.mantissa();
let scale = dec.scale() as u8;
let excess = scale.saturating_sub(target_precision) as u32;
if excess > 0 {
let rounded = bankers_round(mantissa, excess);
let expected_mantissa = expected_dec.mantissa();
let expected_scale = expected_dec.scale() as u8;
let scale_diff = target_precision.saturating_sub(expected_scale) as u32;
let normalized_expected = expected_mantissa * 10i128.pow(scale_diff);
assert_eq!(
rounded, normalized_expected,
"bankers_round disagrees with Decimal for {input} at precision {target_precision}"
);
}
}
}