Skip to main content

nautilus_model/types/
fixed.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Functions for handling fixed-point arithmetic.
17//!
18//! This module provides constants and functions that enforce a fixed-point precision strategy,
19//! ensuring consistent precision and scaling across various types and calculations.
20//!
21//! # Raw Value Requirements
22//!
23//! When constructing value types like [`Price`] or [`Quantity`] using `from_raw`, the raw value
24//! **must** be a valid multiple of the scale factor for the given precision. Valid raw values
25//! should ideally come from:
26//!
27//! - Calling the `raw()` accessor of an existing value (e.g., `price.raw()`)
28//! - Using the fixed-point conversion functions in this module
29//! - Values from Nautilus-produced Arrow data
30//!
31//! Raw values that are not valid multiples will cause a panic on construction in debug builds,
32//! and may result in incorrect values in release builds.
33//!
34//! # Legacy Catalog Data and Floating-Point Errors
35//!
36//! Data written to catalogs using V2 wranglers before 16th December 2025 may contain raw values with
37//! floating-point precision errors. This occurred because the wranglers used:
38//!
39//! ```text
40//! int(value * FIXED_SCALAR)  # Introduces floating-point errors
41//! ```
42//!
43//! instead of the correct precision-aware approach:
44//!
45//! ```text
46//! round(value * 10^precision) * scale  # Correct
47//! ```
48//!
49//! # Raw Value Correction
50//!
51//! To handle legacy data with floating-point errors, the Arrow decode path uses correction
52//! functions ([`correct_raw_i64`], [`correct_raw_i128`], etc.) to round raw values to the
53//! nearest valid multiple. This ensures backward compatibility with existing catalogs.
54//!
55//! **Note:** This correction adds a small amount of overhead during decoding. In a future
56//! version, once catalogs have been repaired or migrated, this correction will become opt-in.
57//!
58//! [`Price`]: crate::types::Price
59//! [`Quantity`]: crate::types::Quantity
60
61use std::{borrow::Cow, cmp::Ordering, fmt::Display};
62
63use nautilus_core::correctness::{
64    CorrectnessError, CorrectnessResult, CorrectnessResultExt, FAILED,
65};
66use rust_decimal::Decimal;
67
68use crate::types::{price::PriceRaw, quantity::QuantityRaw};
69
70/// Indicates if high-precision mode is enabled.
71///
72/// # Safety
73///
74/// This static variable is initialized at compile time and never mutated,
75/// making it safe to read from multiple threads without synchronization.
76/// The value is determined by the "high-precision" feature flag.
77#[unsafe(no_mangle)]
78#[allow(unsafe_code)]
79pub static HIGH_PRECISION_MODE: u8 = cfg!(feature = "high-precision") as u8;
80
81// -----------------------------------------------------------------------------
82// FIXED_PRECISION
83// -----------------------------------------------------------------------------
84
85#[cfg(feature = "high-precision")]
86/// The maximum fixed-point precision.
87pub const FIXED_PRECISION: u8 = 16;
88
89/// The maximum fixed-point precision used by standard-precision catalog data.
90pub const FIXED_PRECISION_STANDARD: u8 = 9;
91
92#[cfg(not(feature = "high-precision"))]
93/// The maximum fixed-point precision.
94pub const FIXED_PRECISION: u8 = FIXED_PRECISION_STANDARD;
95
96// -----------------------------------------------------------------------------
97// PRECISION_BYTES (size of integer backing the fixed-point values)
98// -----------------------------------------------------------------------------
99
100#[cfg(feature = "high-precision")]
101/// The width in bytes for fixed-point value types in high-precision mode (128-bit).
102pub const PRECISION_BYTES: i32 = 16;
103
104#[cfg(not(feature = "high-precision"))]
105/// The width in bytes for fixed-point value types in standard-precision mode (64-bit).
106pub const PRECISION_BYTES: i32 = 8;
107
108/// The Arrow data type name for fixed-point value types.
109pub const FIXED_DECIMAL: &str = "Decimal128(38, 16)";
110
111// -----------------------------------------------------------------------------
112// FIXED_SCALAR
113// -----------------------------------------------------------------------------
114
115#[cfg(feature = "high-precision")]
116pub(crate) const FIXED_SCALAR_RAW: QuantityRaw = 10_000_000_000_000_000;
117
118#[cfg(not(feature = "high-precision"))]
119pub(crate) const FIXED_SCALAR_RAW: QuantityRaw = 1_000_000_000;
120
121#[cfg(feature = "high-precision")]
122/// The scalar value corresponding to the maximum precision (10^16).
123pub const FIXED_SCALAR: f64 = 10_000_000_000_000_000.0;
124
125#[cfg(not(feature = "high-precision"))]
126/// The scalar value corresponding to the maximum precision (10^9).
127pub const FIXED_SCALAR: f64 = 1_000_000_000.0;
128
129// -----------------------------------------------------------------------------
130// PRECISION_DIFF_SCALAR
131// -----------------------------------------------------------------------------
132
133#[cfg(feature = "high-precision")]
134/// The scalar representing the difference between high-precision and standard-precision modes.
135pub const PRECISION_DIFF_SCALAR: f64 = 10_000_000.0; // 10^(16-9)
136
137#[cfg(not(feature = "high-precision"))]
138/// The scalar representing the difference between high-precision and standard-precision modes.
139pub const PRECISION_DIFF_SCALAR: f64 = 1.0;
140
141// -----------------------------------------------------------------------------
142// POWERS_OF_10 (lookup table for fast validation)
143// -----------------------------------------------------------------------------
144
145/// Precomputed powers of 10 for fast scale lookup.
146///
147/// Index i contains 10^i. Table covers 10^0 through 10^16 (sufficient for `FIXED_PRECISION`).
148/// Used by `check_fixed_raw_*` functions to avoid runtime exponentiation.
149const POWERS_OF_10: [u64; 17] = [
150    1,                      // 10^0
151    10,                     // 10^1
152    100,                    // 10^2
153    1_000,                  // 10^3
154    10_000,                 // 10^4
155    100_000,                // 10^5
156    1_000_000,              // 10^6
157    10_000_000,             // 10^7
158    100_000_000,            // 10^8
159    1_000_000_000,          // 10^9
160    10_000_000_000,         // 10^10
161    100_000_000_000,        // 10^11
162    1_000_000_000_000,      // 10^12
163    10_000_000_000_000,     // 10^13
164    100_000_000_000_000,    // 10^14
165    1_000_000_000_000_000,  // 10^15
166    10_000_000_000_000_000, // 10^16
167];
168
169// Compile-time verification that FIXED_PRECISION is within table bounds.
170// We index POWERS_OF_10[FIXED_PRECISION] when precision=0, so need strict `<`.
171const _: () = assert!(
172    (FIXED_PRECISION as usize) < POWERS_OF_10.len(),
173    "FIXED_PRECISION exceeds POWERS_OF_10 table size"
174);
175
176// -----------------------------------------------------------------------------
177
178/// The maximum precision that can be safely used with f64-based constructors.
179///
180/// This is a hard limit imposed by IEEE 754 double-precision floating-point representation,
181/// which has approximately 15-17 significant decimal digits. Beyond 16 decimal places,
182/// floating-point arithmetic becomes unreliable due to rounding errors.
183///
184/// For higher precision values (such as 18-decimal wei values in DeFi), specialized
185/// constructors that work with integer representations should be used instead.
186pub const MAX_FLOAT_PRECISION: u8 = 16;
187
188/// Checks if a given `precision` value is within the allowed fixed-point precision range.
189///
190/// # Errors
191///
192/// Returns an error if:
193/// - The `defi` feature is enabled and `precision` exceeds `WEI_PRECISION` (18).
194/// - The `defi` feature is disabled and `precision` exceeds [`FIXED_PRECISION`].
195pub fn check_fixed_precision(precision: u8) -> CorrectnessResult<()> {
196    #[cfg(feature = "defi")]
197    if precision > crate::defi::WEI_PRECISION {
198        return Err(CorrectnessError::PredicateViolation {
199            message: format!("`precision` exceeded maximum `WEI_PRECISION` (18), was {precision}"),
200        });
201    }
202
203    #[cfg(not(feature = "defi"))]
204    if precision > FIXED_PRECISION {
205        return Err(CorrectnessError::PredicateViolation {
206            message: format!(
207                "`precision` exceeded maximum `FIXED_PRECISION` ({FIXED_PRECISION}), was {precision}"
208            ),
209        });
210    }
211
212    Ok(())
213}
214
215/// Checks that a value with the given `precision` can be converted to `f64`.
216///
217/// # Errors
218///
219/// Returns an error if `precision` exceeds [`MAX_FLOAT_PRECISION`].
220pub fn check_float_precision(precision: u8) -> CorrectnessResult<()> {
221    if precision > MAX_FLOAT_PRECISION {
222        return Err(CorrectnessError::PredicateViolation {
223            message: format!(
224                "Fixed-point precision {precision} exceeds maximum float precision {MAX_FLOAT_PRECISION}"
225            ),
226        });
227    }
228
229    Ok(())
230}
231
232/// Returns `true` when two precisions encode their `raw` values at the same scale.
233///
234/// The effective scale for a given precision is `max(precision, FIXED_PRECISION)`:
235/// - Standard precisions (`<= FIXED_PRECISION`) all store raw at `FIXED_SCALAR` scale.
236/// - Defi precisions (`> FIXED_PRECISION`, e.g. 17 or 18) each store raw at their own
237///   native `10^precision` scale via constructors like `Price::from_wei` /
238///   `Quantity::from_u256`.
239///
240/// Two precisions match iff their effective scales are identical. Mixing different
241/// scales in raw arithmetic produces wrong results.
242#[inline]
243#[must_use]
244pub fn raw_scales_match(a: u8, b: u8) -> bool {
245    a == b || a.max(b) <= FIXED_PRECISION
246}
247
248/// Returns the effective integer scale for a raw fixed-point value.
249#[inline]
250#[must_use]
251pub(crate) fn raw_scale(precision: u8) -> u128 {
252    10_u128.pow(u32::from(precision.max(FIXED_PRECISION)))
253}
254
255// Removing only native-scale trailing zeros gives equal values identical hash inputs
256#[must_use]
257pub(crate) fn canonical_raw(raw: impl Into<u128>, precision: u8) -> (u128, u8) {
258    let mut raw = raw.into();
259    let mut precision = if raw == 0 {
260        FIXED_PRECISION
261    } else {
262        precision.max(FIXED_PRECISION)
263    };
264
265    while precision > FIXED_PRECISION && raw % 10 == 0 {
266        raw /= 10;
267        precision -= 1;
268    }
269
270    (raw, precision)
271}
272
273#[inline]
274#[must_use]
275pub(crate) fn compare_raw_signed(
276    lhs: PriceRaw,
277    lhs_precision: u8,
278    rhs: PriceRaw,
279    rhs_precision: u8,
280) -> Ordering {
281    if raw_scales_match(lhs_precision, rhs_precision) {
282        return lhs.cmp(&rhs);
283    }
284
285    lhs.signum().cmp(&rhs.signum()).then_with(|| {
286        let ordering = compare_raw(
287            lhs.unsigned_abs(),
288            lhs_precision,
289            rhs.unsigned_abs(),
290            rhs_precision,
291        );
292
293        if lhs < 0 {
294            ordering.reverse()
295        } else {
296            ordering
297        }
298    })
299}
300
301#[inline]
302#[must_use]
303pub(crate) fn compare_raw(
304    lhs: impl Into<u128>,
305    lhs_precision: u8,
306    rhs: impl Into<u128>,
307    rhs_precision: u8,
308) -> Ordering {
309    let lhs = lhs.into();
310    let rhs = rhs.into();
311
312    // The zero-valued ERROR_PRICE sentinel has precision 255, which is not a numeric scale
313    if (lhs == 0 && rhs == 0) || raw_scales_match(lhs_precision, rhs_precision) {
314        return lhs.cmp(&rhs);
315    }
316
317    let lhs_scale = raw_scale(lhs_precision);
318    let rhs_scale = raw_scale(rhs_precision);
319    let scale = lhs_scale.max(rhs_scale);
320
321    // Compare whole parts first so aligning fractional parts cannot overflow
322    (lhs / lhs_scale).cmp(&(rhs / rhs_scale)).then_with(|| {
323        let lhs_fraction = (lhs % lhs_scale) * (scale / lhs_scale);
324        let rhs_fraction = (rhs % rhs_scale) * (scale / rhs_scale);
325        lhs_fraction.cmp(&rhs_fraction)
326    })
327}
328
329/// Converts a raw value already rescaled to `10^precision` into a `Decimal`.
330///
331/// `Decimal` stores a 96-bit mantissa, so `Decimal::from_i128_with_scale` panics once the raw
332/// value exceeds `79_228_162_514_264_337_593_543_950_335`. Valid values reach that: under
333/// `high-precision` a precision-16 amount does so above roughly 7.92e12, and a `defi`
334/// precision-18 amount above roughly 7.92e10. Those values fall back to adding the whole and
335/// fractional parts, which keeps both operands small enough that `Decimal` drops scale rather
336/// than panicking. Every value the direct conversion accepts keeps its exact value and scale.
337#[must_use]
338pub(crate) fn scaled_raw_to_decimal(scaled_raw: i128, precision: u8) -> Decimal {
339    let scale = u32::from(precision);
340
341    Decimal::try_from_i128_with_scale(scaled_raw, scale).unwrap_or_else(|_| {
342        let divisor = 10_i128.pow(scale);
343
344        Decimal::from(scaled_raw / divisor)
345            + Decimal::from_i128_with_scale(scaled_raw % divisor, scale)
346    })
347}
348
349pub(crate) fn format_scaled_i128(raw: i128, precision: u8) -> String {
350    let sign = if raw < 0 { "-" } else { "" };
351    format!(
352        "{sign}{}",
353        format_scaled_u128(raw.unsigned_abs(), precision)
354    )
355}
356
357/// Parses a plain decimal string into its signed mantissa and fractional precision.
358pub(crate) fn parse_decimal_mantissa(value: &str) -> Result<(i128, u8), String> {
359    let (negative, unsigned) = value
360        .strip_prefix('-')
361        .map_or((false, value), |value| (true, value));
362    let unsigned = if negative {
363        unsigned
364    } else {
365        unsigned.strip_prefix('+').unwrap_or(unsigned)
366    };
367    let (whole, fraction) = unsigned.split_once('.').unwrap_or((unsigned, ""));
368    if fraction.contains('.') {
369        return Err(format!("Invalid decimal value '{value}'"));
370    }
371
372    let digits = whole.bytes().chain(fraction.bytes());
373    if (whole.is_empty() && fraction.is_empty())
374        || !digits.clone().all(|byte| byte.is_ascii_digit())
375    {
376        return Err(format!("Invalid decimal value '{value}'"));
377    }
378    let precision = u8::try_from(fraction.len())
379        .map_err(|_| format!("Decimal value '{value}' has too many fractional digits"))?;
380    let mut mantissa = 0_i128;
381    for digit in digits.map(|byte| i128::from(byte - b'0')) {
382        mantissa = if negative {
383            mantissa
384                .checked_mul(10)
385                .and_then(|value| value.checked_sub(digit))
386        } else {
387            mantissa
388                .checked_mul(10)
389                .and_then(|value| value.checked_add(digit))
390        }
391        .ok_or_else(|| format!("Decimal value '{value}' exceeds i128 range"))?;
392    }
393    Ok((mantissa, precision))
394}
395
396pub(super) fn remove_underscores(value: &str) -> Cow<'_, str> {
397    if value.contains('_') {
398        Cow::Owned(value.replace('_', ""))
399    } else {
400        Cow::Borrowed(value)
401    }
402}
403
404pub(crate) fn format_scaled_u128(raw: u128, precision: u8) -> String {
405    if precision == 0 {
406        return raw.to_string();
407    }
408
409    let scale = 10_u128.pow(u32::from(precision));
410    format!(
411        "{}.{:0>width$}",
412        raw / scale,
413        raw % scale,
414        width = usize::from(precision),
415    )
416}
417
418/// Returns `lhs * rhs / FIXED_SCALAR`, truncated toward zero.
419///
420/// Returns `None` only when the scaled result exceeds [`QuantityRaw::MAX`].
421#[must_use]
422pub(crate) fn checked_mul_div_fixed(lhs: QuantityRaw, rhs: QuantityRaw) -> Option<QuantityRaw> {
423    checked_mul_div_raw(lhs, rhs, FIXED_SCALAR_RAW)
424}
425
426// Splitting both operands avoids intermediate overflow, the remainder product fits
427// QuantityRaw for every supported fixed-point scale (up to 10^18 with defi).
428#[must_use]
429pub(crate) fn checked_mul_div_raw(
430    lhs: QuantityRaw,
431    rhs: QuantityRaw,
432    scalar: QuantityRaw,
433) -> Option<QuantityRaw> {
434    let lhs_whole = lhs / scalar;
435    let lhs_remainder = lhs % scalar;
436    let rhs_whole = rhs / scalar;
437    let rhs_remainder = rhs % scalar;
438
439    lhs_whole
440        .checked_mul(rhs)
441        .and_then(|whole| {
442            lhs_remainder
443                .checked_mul(rhs_whole)
444                .and_then(|mixed| whole.checked_add(mixed))
445        })
446        .and_then(|whole_and_mixed| {
447            lhs_remainder
448                .checked_mul(rhs_remainder)
449                .map(|fractional| fractional / scalar)
450                .and_then(|fractional| whole_and_mixed.checked_add(fractional))
451        })
452}
453
454const _: () = {
455    assert!(FIXED_SCALAR_RAW > 0);
456    assert!((FIXED_SCALAR_RAW as f64).to_bits() == FIXED_SCALAR.to_bits());
457    let max_remainder = FIXED_SCALAR_RAW - 1;
458    assert!(max_remainder.checked_mul(max_remainder).is_some());
459};
460
461// -----------------------------------------------------------------------------
462// Raw value validation
463// -----------------------------------------------------------------------------
464
465/// Returns `true` if validation should be skipped, `false` to proceed.
466///
467/// Validation is skipped when precision >= `FIXED_PRECISION` because every bit of the raw
468/// value is significant. For precision > `FIXED_PRECISION` without the defi feature,
469/// a debug assertion fires to surface potential misuse during development.
470#[inline(always)]
471fn should_skip_validation(precision: u8) -> bool {
472    #[cfg(not(feature = "defi"))]
473    debug_assert!(
474        precision <= FIXED_PRECISION,
475        "precision {precision} exceeds FIXED_PRECISION {FIXED_PRECISION}: \
476         raw value validation is not possible at this precision"
477    );
478
479    precision >= FIXED_PRECISION
480}
481
482/// Builds the error for invalid fixed-point raw values (cold path).
483#[cold]
484fn invalid_raw_error(
485    raw: impl Display,
486    precision: u8,
487    remainder: impl Display,
488    scale: impl Display,
489) -> anyhow::Error {
490    anyhow::anyhow!(
491        "Invalid fixed-point raw value {raw} for precision {precision}: \
492         remainder {remainder} when divided by scale {scale}. \
493         Raw value should be a multiple of {scale}. \
494         This indicates data corruption or incorrect precision/scaling upstream"
495    )
496}
497
498/// Checks that a raw unsigned fixed-point value has no spurious bits beyond the precision scale.
499///
500/// For a given precision P where P < `FIXED_PRECISION`, valid raw values must be exact
501/// multiples of `10^(FIXED_PRECISION` - P). Any non-zero remainder indicates data corruption
502/// or incorrect scaling upstream.
503///
504/// # Precision Limits
505///
506/// This check only validates when `precision < FIXED_PRECISION`:
507/// - When `precision == FIXED_PRECISION`, every bit of the raw value is significant and
508///   the check passes trivially (no "extra" bits to validate).
509/// - When `precision > FIXED_PRECISION` (possible with defi feature allowing up to 18dp),
510///   validation is not possible because the requested precision exceeds our internal
511///   representation. A debug assertion will fire to surface this during development.
512///
513/// **Important**: For defi 18dp values, this check provides NO protection against incorrectly scaled
514/// raw values. The inherent limitation is that we cannot detect if a 16dp raw is incorrectly
515/// labeled as 18dp, since both would appear valid at full internal precision.
516///
517/// # Example
518///
519/// With `FIXED_PRECISION=9` and precision=0:
520/// - Valid: `raw=120_000_000_000` (120 * 10^9, divisible by 10^9)
521/// - Invalid: `raw=119_582_001_968_421_736` (remainder `968_421_736` when divided by 10^9)
522///
523/// # Errors
524///
525/// Returns an error if the raw value has non-zero bits beyond the precision scale
526/// (only when `precision < FIXED_PRECISION`).
527#[inline(always)]
528pub fn check_fixed_raw_u128(raw: u128, precision: u8) -> anyhow::Result<()> {
529    if should_skip_validation(precision) {
530        return Ok(());
531    }
532
533    let exp = usize::from(FIXED_PRECISION - precision);
534    let scale = u128::from(POWERS_OF_10[exp]);
535    let remainder = raw % scale;
536
537    if remainder != 0 {
538        return Err(invalid_raw_error(raw, precision, remainder, scale));
539    }
540
541    Ok(())
542}
543
544/// Checks that a raw unsigned fixed-point value (64-bit) has no spurious bits.
545///
546/// Uses direct u64 arithmetic for better performance than widening to u128.
547/// See [`check_fixed_raw_u128`] for full documentation on precision limits and behavior.
548///
549/// # Errors
550///
551/// Returns an error if the raw value has non-zero bits beyond the precision scale.
552#[inline(always)]
553pub fn check_fixed_raw_u64(raw: u64, precision: u8) -> anyhow::Result<()> {
554    if should_skip_validation(precision) {
555        return Ok(());
556    }
557
558    let exp = usize::from(FIXED_PRECISION - precision);
559    let scale = POWERS_OF_10[exp];
560    let remainder = raw % scale;
561
562    if remainder != 0 {
563        return Err(invalid_raw_error(raw, precision, remainder, scale));
564    }
565
566    Ok(())
567}
568
569/// Checks that a raw signed fixed-point value has no spurious bits beyond the precision scale.
570///
571/// For a given precision P where P < `FIXED_PRECISION`, valid raw values must be exact
572/// multiples of `10^(FIXED_PRECISION` - P). Any non-zero remainder indicates data corruption
573/// or incorrect scaling upstream.
574///
575/// # Precision Limits
576///
577/// This check only validates when `precision < FIXED_PRECISION`:
578/// - When `precision == FIXED_PRECISION`, every bit of the raw value is significant and
579///   the check passes trivially (no "extra" bits to validate).
580/// - When `precision > FIXED_PRECISION` (possible with defi feature allowing up to 18dp),
581///   validation is not possible because the requested precision exceeds our internal
582///   representation. A debug assertion will fire to surface this during development.
583///
584/// **Important**: For defi 18dp values, this check provides NO protection against incorrectly scaled
585/// raw values. The inherent limitation is that we cannot detect if a 16dp raw is incorrectly
586/// labeled as 18dp, since both would appear valid at full internal precision.
587///
588/// # Example
589///
590/// With `FIXED_PRECISION=9` and precision=0:
591/// - Valid: `raw=120_000_000_000` (120 * 10^9, divisible by 10^9)
592/// - Invalid: `raw=119_582_001_968_421_736` (remainder `968_421_736` when divided by 10^9)
593///
594/// # Errors
595///
596/// Returns an error if the raw value has non-zero bits beyond the precision scale
597/// (only when `precision < FIXED_PRECISION`).
598#[inline(always)]
599pub fn check_fixed_raw_i128(raw: i128, precision: u8) -> anyhow::Result<()> {
600    if should_skip_validation(precision) {
601        return Ok(());
602    }
603
604    let exp = usize::from(FIXED_PRECISION - precision);
605    let scale = i128::from(POWERS_OF_10[exp]);
606    let remainder = raw % scale;
607
608    if remainder != 0 {
609        return Err(invalid_raw_error(raw, precision, remainder, scale));
610    }
611
612    Ok(())
613}
614
615/// Checks that a raw signed fixed-point value (64-bit) has no spurious bits.
616///
617/// Uses direct i64 arithmetic for better performance than widening to i128.
618/// See [`check_fixed_raw_i128`] for full documentation on precision limits and behavior.
619///
620/// # Errors
621///
622/// Returns an error if the raw value has non-zero bits beyond the precision scale.
623#[inline(always)]
624pub fn check_fixed_raw_i64(raw: i64, precision: u8) -> anyhow::Result<()> {
625    if should_skip_validation(precision) {
626        return Ok(());
627    }
628
629    let exp = usize::from(FIXED_PRECISION - precision);
630    let scale = POWERS_OF_10[exp].cast_signed();
631    let remainder = raw % scale;
632
633    if remainder != 0 {
634        return Err(invalid_raw_error(raw, precision, remainder, scale));
635    }
636
637    Ok(())
638}
639
640// -----------------------------------------------------------------------------
641// Raw value correction functions
642// -----------------------------------------------------------------------------
643// These functions round raw values to the nearest valid multiple of the scale
644// factor for a given precision. This is needed when reading data from catalogs
645// or other sources that may have been created with floating-point precision
646// errors (e.g., `int(value * FIXED_SCALAR)` instead of the correct
647// `round(value * 10^precision) * scale` approach).
648
649/// Rounds a raw `u128` value to the nearest valid multiple of the scale for the given precision.
650///
651/// This corrects raw values that have spurious bits beyond the precision scale, which can occur
652/// from floating-point conversion errors during data creation.
653///
654/// Rounds half away from zero; when rounding away would overflow the integer range,
655/// rounds toward zero instead.
656#[must_use]
657pub fn correct_raw_u128(raw: u128, precision: u8) -> u128 {
658    if precision >= FIXED_PRECISION {
659        return raw;
660    }
661    let exp = usize::from(FIXED_PRECISION - precision);
662    let scale = u128::from(POWERS_OF_10[exp]);
663    let half_scale = scale / 2;
664    let remainder = raw % scale;
665    if remainder == 0 {
666        raw
667    } else if remainder >= half_scale {
668        raw.checked_add(scale - remainder)
669            .unwrap_or(raw - remainder)
670    } else {
671        raw - remainder
672    }
673}
674
675/// Rounds a raw `u64` value to the nearest valid multiple of the scale for the given precision.
676///
677/// This corrects raw values that have spurious bits beyond the precision scale, which can occur
678/// from floating-point conversion errors during data creation.
679///
680/// Rounds half away from zero; when rounding away would overflow the integer range,
681/// rounds toward zero instead.
682#[must_use]
683pub fn correct_raw_u64(raw: u64, precision: u8) -> u64 {
684    if precision >= FIXED_PRECISION {
685        return raw;
686    }
687    let exp = usize::from(FIXED_PRECISION - precision);
688    let scale = POWERS_OF_10[exp];
689    let half_scale = scale / 2;
690    let remainder = raw % scale;
691    if remainder == 0 {
692        raw
693    } else if remainder >= half_scale {
694        raw.checked_add(scale - remainder)
695            .unwrap_or(raw - remainder)
696    } else {
697        raw - remainder
698    }
699}
700
701/// Rounds a raw `i128` value to the nearest valid multiple of the scale for the given precision.
702///
703/// This corrects raw values that have spurious bits beyond the precision scale, which can occur
704/// from floating-point conversion errors during data creation.
705///
706/// Rounds half away from zero; when rounding away would overflow the integer range,
707/// rounds toward zero instead.
708#[must_use]
709pub fn correct_raw_i128(raw: i128, precision: u8) -> i128 {
710    if precision >= FIXED_PRECISION {
711        return raw;
712    }
713    let exp = usize::from(FIXED_PRECISION - precision);
714    let scale = i128::from(POWERS_OF_10[exp]);
715    let half_scale = scale / 2;
716    let remainder = raw % scale;
717    if remainder == 0 {
718        raw
719    } else if raw >= 0 {
720        if remainder >= half_scale {
721            raw.checked_add(scale - remainder)
722                .unwrap_or(raw - remainder)
723        } else {
724            raw - remainder
725        }
726    } else {
727        // For negative values, remainder is negative
728        if remainder.abs() >= half_scale {
729            raw.checked_sub(scale + remainder)
730                .unwrap_or(raw - remainder)
731        } else {
732            raw - remainder
733        }
734    }
735}
736
737/// Rounds a raw `i64` value to the nearest valid multiple of the scale for the given precision.
738///
739/// This corrects raw values that have spurious bits beyond the precision scale, which can occur
740/// from floating-point conversion errors during data creation.
741///
742/// Rounds half away from zero; when rounding away would overflow the integer range,
743/// rounds toward zero instead.
744#[must_use]
745pub fn correct_raw_i64(raw: i64, precision: u8) -> i64 {
746    if precision >= FIXED_PRECISION {
747        return raw;
748    }
749    let exp = usize::from(FIXED_PRECISION - precision);
750    let scale = POWERS_OF_10[exp].cast_signed();
751    let half_scale = scale / 2;
752    let remainder = raw % scale;
753    if remainder == 0 {
754        raw
755    } else if raw >= 0 {
756        if remainder >= half_scale {
757            raw.checked_add(scale - remainder)
758                .unwrap_or(raw - remainder)
759        } else {
760            raw - remainder
761        }
762    } else {
763        // For negative values, remainder is negative
764        if remainder.abs() >= half_scale {
765            raw.checked_sub(scale + remainder)
766                .unwrap_or(raw - remainder)
767        } else {
768            raw - remainder
769        }
770    }
771}
772
773/// Rounds a raw price value to the nearest valid multiple of the scale for the given precision.
774///
775/// This is a type-aliased wrapper that calls the appropriate underlying function based on
776/// whether the `high-precision` feature is enabled. Use this when working with `PriceRaw` values
777/// to ensure consistent feature-flag handling.
778#[must_use]
779#[inline]
780pub fn correct_price_raw(raw: PriceRaw, precision: u8) -> PriceRaw {
781    #[cfg(feature = "high-precision")]
782    {
783        correct_raw_i128(raw, precision)
784    }
785    #[cfg(not(feature = "high-precision"))]
786    {
787        correct_raw_i64(raw, precision)
788    }
789}
790
791/// Rounds a raw quantity value to the nearest valid multiple of the scale for the given precision.
792///
793/// This is a type-aliased wrapper that calls the appropriate underlying function based on
794/// whether the `high-precision` feature is enabled. Use this when working with `QuantityRaw` values
795/// to ensure consistent feature-flag handling.
796#[must_use]
797#[inline]
798pub fn correct_quantity_raw(raw: QuantityRaw, precision: u8) -> QuantityRaw {
799    #[cfg(feature = "high-precision")]
800    {
801        correct_raw_u128(raw, precision)
802    }
803    #[cfg(not(feature = "high-precision"))]
804    {
805        correct_raw_u64(raw, precision)
806    }
807}
808
809/// Rounds a mantissa by removing `excess` decimal digits using banker's rounding (half to even).
810///
811/// Given a mantissa representing a number with `excess` extra decimal places beyond the desired
812/// precision, divides by `10^excess` and rounds the result using round-half-to-even semantics.
813#[must_use]
814#[inline]
815pub fn bankers_round(mantissa: i128, excess: u32) -> i128 {
816    if excess == 0 {
817        return mantissa;
818    }
819
820    // 10^39 overflows i128, and any i64-origin mantissa divided by 10^39+ is 0
821    if excess >= 39 {
822        return 0;
823    }
824
825    let divisor = 10i128.pow(excess);
826    let quotient = mantissa / divisor;
827    let remainder = mantissa % divisor;
828    let half = divisor / 2;
829
830    if remainder.abs() > half || (remainder.abs() == half && quotient % 2 != 0) {
831        quotient + mantissa.signum()
832    } else {
833        quotient
834    }
835}
836
837/// Converts a mantissa/exponent pair to a raw fixed-point `i128` value at the given precision.
838///
839/// The value is `mantissa * 10^exponent`. Uses pure integer arithmetic with banker's rounding
840/// when fractional digits exceed `precision`. The result is scaled to [`FIXED_PRECISION`].
841///
842/// This is the shared core for `from_decimal`, `from_decimal_dp`, and `from_mantissa_exponent`
843/// across Money, Price, and Quantity.
844///
845/// # Errors
846///
847/// Returns an error if:
848/// - `precision` exceeds the maximum allowed by [`check_fixed_precision`].
849/// - The scale factor exceeds `10^38` (i128 range).
850/// - Overflow occurs during multiplication.
851pub fn mantissa_exponent_to_fixed_i128(
852    mantissa: i128,
853    exponent: i8,
854    precision: u8,
855) -> CorrectnessResult<i128> {
856    check_fixed_precision(precision)?;
857
858    let precision_i16 = i16::from(precision);
859    let target_scale = i16::from(FIXED_PRECISION).max(precision_i16);
860    let frac_digits = -i16::from(exponent);
861
862    let mantissa = if frac_digits > precision_i16 {
863        let excess = u32::from((frac_digits - precision_i16).cast_unsigned());
864        bankers_round(mantissa, excess)
865    } else {
866        mantissa
867    };
868
869    let scale_after_rounding = frac_digits.min(precision_i16);
870    let scale_exp = target_scale - scale_after_rounding;
871    if scale_exp > 38 {
872        return Err(CorrectnessError::PredicateViolation {
873            message: format!(
874                "Exponent {exponent} produces scale factor 10^{scale_exp} which exceeds i128 range"
875            ),
876        });
877    }
878
879    if scale_exp >= 0 {
880        mantissa.checked_mul(10i128.pow(u32::from(scale_exp.cast_unsigned())))
881    } else {
882        Some(mantissa / 10i128.pow(u32::from((-scale_exp).cast_unsigned())))
883    }
884    .ok_or_else(|| CorrectnessError::PredicateViolation {
885        message: "Overflow when scaling mantissa to fixed precision".to_string(),
886    })
887}
888
889pub(crate) fn mantissa_exponent_to_raw_checked<R>(
890    mantissa: i128,
891    exponent: i8,
892    precision: u8,
893    context: &'static str,
894    raw_type_name: &'static str,
895    value_type_name: &'static str,
896) -> CorrectnessResult<R>
897where
898    R: TryFrom<i128>,
899{
900    check_fixed_precision(precision)?;
901
902    let raw_i128 = if mantissa == 0 {
903        0
904    } else {
905        mantissa_exponent_to_fixed_i128(mantissa, exponent, precision).map_err(|_| {
906            CorrectnessError::PredicateViolation {
907                message: format!(
908                    "Overflow in {context} (mantissa={mantissa}, exponent={exponent}, precision={precision})"
909                ),
910            }
911        })?
912    };
913
914    raw_i128
915        .try_into()
916        .map_err(|_| CorrectnessError::PredicateViolation {
917            message: format!(
918                "Raw value {raw_i128} exceeds {raw_type_name} range for {value_type_name}"
919            ),
920        })
921}
922
923/// Converts an `f64` value to a raw fixed-point `i64` representation with a specified precision.
924///
925/// # Precision and Rounding
926///
927/// This function performs IEEE 754 "round half to even" rounding at the specified precision
928/// before scaling to the fixed-point representation. The rounding is intentionally applied
929/// at the user-specified precision level to ensure values are correctly represented
930/// without accumulating floating-point errors during scaling.
931///
932/// Callers are expected to validate that `value` is finite and within range; non-finite
933/// values saturate at the integer bounds during the float-to-integer cast.
934///
935/// # Panics
936///
937/// Panics if `precision` exceeds [`FIXED_PRECISION`], or if scaling the rounded value
938/// overflows the raw integer range.
939#[must_use]
940#[expect(
941    clippy::cast_precision_loss,
942    clippy::cast_possible_truncation,
943    reason = "f64 to fixed-point conversion is inherently lossy; callers validate range and finiteness"
944)]
945pub fn f64_to_fixed_i64(value: f64, precision: u8) -> i64 {
946    check_fixed_precision(precision).expect_display(FAILED);
947    let pow1 = 10_i64.pow(u32::from(precision));
948    let pow2 = 10_i64.pow(u32::from(FIXED_PRECISION - precision));
949    let rounded = (value * pow1 as f64).round() as i64;
950    rounded
951        .checked_mul(pow2)
952        .expect("Overflow when scaling f64 to fixed-point i64")
953}
954
955/// Converts an `f64` value to a raw fixed-point `i128` representation with a specified precision.
956///
957/// Callers are expected to validate that `value` is finite and within range; non-finite
958/// values saturate at the integer bounds during the float-to-integer cast.
959///
960/// # Panics
961///
962/// Panics if `precision` exceeds [`FIXED_PRECISION`], or if scaling the rounded value
963/// overflows the raw integer range.
964#[must_use]
965#[expect(
966    clippy::cast_precision_loss,
967    clippy::cast_possible_truncation,
968    reason = "f64 to fixed-point conversion is inherently lossy; callers validate range and finiteness"
969)]
970pub fn f64_to_fixed_i128(value: f64, precision: u8) -> i128 {
971    check_fixed_precision(precision).expect_display(FAILED);
972    let pow1 = 10_i128.pow(u32::from(precision));
973    let pow2 = 10_i128.pow(u32::from(FIXED_PRECISION - precision));
974    let rounded = (value * pow1 as f64).round() as i128;
975    rounded
976        .checked_mul(pow2)
977        .expect("Overflow when scaling f64 to fixed-point i128")
978}
979
980/// Converts an `f64` value to a raw fixed-point `u64` representation with a specified precision.
981///
982/// Callers are expected to validate that `value` is finite and non-negative; non-finite
983/// and negative values saturate at the integer bounds during the float-to-integer cast.
984///
985/// # Panics
986///
987/// Panics if `precision` exceeds [`FIXED_PRECISION`], or if scaling the rounded value
988/// overflows the raw integer range.
989#[must_use]
990#[expect(
991    clippy::cast_precision_loss,
992    clippy::cast_possible_truncation,
993    clippy::cast_sign_loss,
994    reason = "f64 to fixed-point conversion is inherently lossy; callers validate range and finiteness"
995)]
996pub fn f64_to_fixed_u64(value: f64, precision: u8) -> u64 {
997    check_fixed_precision(precision).expect_display(FAILED);
998    let pow1 = 10_u64.pow(u32::from(precision));
999    let pow2 = 10_u64.pow(u32::from(FIXED_PRECISION - precision));
1000    let rounded = (value * pow1 as f64).round() as u64;
1001    rounded
1002        .checked_mul(pow2)
1003        .expect("Overflow when scaling f64 to fixed-point u64")
1004}
1005
1006/// Converts an `f64` value to a raw fixed-point `u128` representation with a specified precision.
1007///
1008/// Callers are expected to validate that `value` is finite and non-negative; non-finite
1009/// and negative values saturate at the integer bounds during the float-to-integer cast.
1010///
1011/// # Panics
1012///
1013/// Panics if `precision` exceeds [`FIXED_PRECISION`], or if scaling the rounded value
1014/// overflows the raw integer range.
1015#[must_use]
1016#[expect(
1017    clippy::cast_precision_loss,
1018    clippy::cast_possible_truncation,
1019    clippy::cast_sign_loss,
1020    reason = "f64 to fixed-point conversion is inherently lossy; callers validate range and finiteness"
1021)]
1022pub fn f64_to_fixed_u128(value: f64, precision: u8) -> u128 {
1023    check_fixed_precision(precision).expect_display(FAILED);
1024    let pow1 = 10_u128.pow(u32::from(precision));
1025    let pow2 = 10_u128.pow(u32::from(FIXED_PRECISION - precision));
1026    let rounded = (value * pow1 as f64).round() as u128;
1027    rounded
1028        .checked_mul(pow2)
1029        .expect("Overflow when scaling f64 to fixed-point u128")
1030}
1031
1032/// Converts a raw fixed-point `i64` value back to an `f64` value.
1033#[must_use]
1034#[expect(
1035    clippy::cast_precision_loss,
1036    reason = "i64 to f64 is inherently lossy above 2^53; accepted for float interop"
1037)]
1038pub fn fixed_i64_to_f64(value: i64) -> f64 {
1039    (value as f64) / FIXED_SCALAR
1040}
1041
1042/// Converts a raw fixed-point `i128` value back to an `f64` value.
1043#[must_use]
1044#[expect(
1045    clippy::cast_precision_loss,
1046    reason = "i128 to f64 is inherently lossy above 2^53; accepted for float interop"
1047)]
1048pub fn fixed_i128_to_f64(value: i128) -> f64 {
1049    (value as f64) / FIXED_SCALAR
1050}
1051
1052/// Converts a raw fixed-point `u64` value back to an `f64` value.
1053#[must_use]
1054#[expect(
1055    clippy::cast_precision_loss,
1056    reason = "u64 to f64 is inherently lossy above 2^53; accepted for float interop"
1057)]
1058pub fn fixed_u64_to_f64(value: u64) -> f64 {
1059    (value as f64) / FIXED_SCALAR
1060}
1061
1062/// Converts a raw fixed-point `u128` value back to an `f64` value.
1063#[must_use]
1064#[expect(
1065    clippy::cast_precision_loss,
1066    reason = "u128 to f64 is inherently lossy above 2^53; accepted for float interop"
1067)]
1068pub fn fixed_u128_to_f64(value: u128) -> f64 {
1069    (value as f64) / FIXED_SCALAR
1070}
1071
1072#[cfg(feature = "high-precision")]
1073#[cfg(test)]
1074mod tests {
1075    use nautilus_core::approx_eq;
1076    use rstest::rstest;
1077
1078    use super::*;
1079
1080    #[rstest]
1081    fn test_correct_raw_rounds_half_away_from_zero() {
1082        let precision = FIXED_PRECISION - 1;
1083
1084        assert_eq!(correct_raw_u128(20, precision), 20);
1085        assert_eq!(correct_raw_u128(14, precision), 10);
1086        assert_eq!(correct_raw_u128(15, precision), 20);
1087        assert_eq!(correct_raw_u64(14, precision), 10);
1088        assert_eq!(correct_raw_u64(15, precision), 20);
1089        assert_eq!(correct_raw_i128(15, precision), 20);
1090        assert_eq!(correct_raw_i128(-14, precision), -10);
1091        assert_eq!(correct_raw_i128(-15, precision), -20);
1092        assert_eq!(correct_raw_i64(15, precision), 20);
1093        assert_eq!(correct_raw_i64(-14, precision), -10);
1094        assert_eq!(correct_raw_i64(-15, precision), -20);
1095    }
1096
1097    #[rstest]
1098    fn test_f64_fixed_u128_round_trip() {
1099        let raw = f64_to_fixed_u128(1.5, 1);
1100
1101        assert_eq!(raw, 15 * 10_u128.pow(u32::from(FIXED_PRECISION - 1)));
1102        assert_eq!(fixed_u128_to_f64(raw), 1.5);
1103    }
1104
1105    #[rstest]
1106    fn test_mantissa_exponent_to_fixed_i128_allows_max_scale_factor() {
1107        let exponent = i8::try_from(38 - FIXED_PRECISION).unwrap();
1108
1109        assert_eq!(
1110            mantissa_exponent_to_fixed_i128(1, exponent, 0).unwrap(),
1111            10_i128.pow(38)
1112        );
1113        assert_eq!(
1114            mantissa_exponent_to_fixed_i128(1, exponent + 1, 0)
1115                .unwrap_err()
1116                .to_string(),
1117            format!(
1118                "Exponent {} produces scale factor 10^39 which exceeds i128 range",
1119                exponent + 1
1120            )
1121        );
1122    }
1123
1124    #[rstest]
1125    fn test_raw_scales_match_requires_equal_effective_scale() {
1126        assert!(raw_scales_match(0, FIXED_PRECISION));
1127        assert!(raw_scales_match(FIXED_PRECISION + 1, FIXED_PRECISION + 1));
1128        assert!(!raw_scales_match(FIXED_PRECISION, FIXED_PRECISION + 1));
1129    }
1130
1131    #[rstest]
1132    fn test_canonical_raw_trims_native_scale_trailing_zeros() {
1133        assert_eq!(
1134            canonical_raw(0_u128, FIXED_PRECISION + 2),
1135            (0, FIXED_PRECISION)
1136        );
1137        assert_eq!(
1138            canonical_raw(1_200_u128, FIXED_PRECISION + 2),
1139            (12, FIXED_PRECISION)
1140        );
1141        assert_eq!(
1142            canonical_raw(12_000_u128, FIXED_PRECISION + 2),
1143            (120, FIXED_PRECISION)
1144        );
1145        assert_eq!(
1146            canonical_raw(1_205_u128, FIXED_PRECISION + 2),
1147            (1_205, FIXED_PRECISION + 2)
1148        );
1149        assert_eq!(
1150            canonical_raw(5_u128, FIXED_PRECISION - 1),
1151            (5, FIXED_PRECISION)
1152        );
1153    }
1154
1155    #[rstest]
1156    fn test_compare_raw_zero_operands_are_equal_across_scales() {
1157        assert_eq!(compare_raw(0_u128, u8::MAX, 0_u128, 2), Ordering::Equal);
1158    }
1159
1160    #[rstest]
1161    #[case("")]
1162    #[case("0")]
1163    #[case("-12.3400")]
1164    #[case("+1e9")]
1165    #[case("not decimal")]
1166    fn test_remove_underscores_borrows_clean_values(#[case] input: &str) {
1167        assert!(matches!(remove_underscores(input), Cow::Borrowed(value) if value == input));
1168    }
1169
1170    #[rstest]
1171    #[case("_", "")]
1172    #[case("1_234.56", "1234.56")]
1173    #[case("_1__2_3.4_5_", "123.45")]
1174    #[case("1e_3", "1e3")]
1175    #[case("_-_1_0_._0_", "-10.0")]
1176    fn test_remove_underscores_removes_separators(#[case] input: &str, #[case] expected: &str) {
1177        assert!(matches!(remove_underscores(input), Cow::Owned(value) if value == expected));
1178    }
1179
1180    #[rstest]
1181    #[case("1.00", 100, 2)]
1182    #[case("+1.00", 100, 2)]
1183    #[case("-1.00", -100, 2)]
1184    #[case("-0.00", 0, 2)]
1185    fn test_parse_decimal_mantissa_sign(
1186        #[case] input: &str,
1187        #[case] mantissa: i128,
1188        #[case] precision: u8,
1189    ) {
1190        assert_eq!(parse_decimal_mantissa(input), Ok((mantissa, precision)));
1191    }
1192
1193    #[rstest]
1194    #[case("-+1.00")]
1195    #[case("+-1.00")]
1196    #[case("--1.00")]
1197    #[case("++1.00")]
1198    #[case("-+0.00")]
1199    fn test_parse_decimal_mantissa_rejects_multiple_signs(#[case] input: &str) {
1200        assert_eq!(
1201            parse_decimal_mantissa(input),
1202            Err(format!("Invalid decimal value '{input}'")),
1203        );
1204        assert!(input.parse::<crate::types::Price>().is_err());
1205        assert!(input.parse::<crate::types::Quantity>().is_err());
1206        assert!(
1207            format!("{input} USD")
1208                .parse::<crate::types::Money>()
1209                .is_err()
1210        );
1211    }
1212
1213    #[rstest]
1214    #[case(i128::MIN)]
1215    #[case(i128::MAX)]
1216    fn test_parse_decimal_mantissa_integer_limits(#[case] value: i128) {
1217        assert_eq!(parse_decimal_mantissa(&value.to_string()), Ok((value, 0)));
1218    }
1219
1220    #[rstest]
1221    #[case("170141183460469231731687303715884105728")]
1222    #[case("-170141183460469231731687303715884105729")]
1223    fn test_parse_decimal_mantissa_overflow(#[case] input: &str) {
1224        assert_eq!(
1225            parse_decimal_mantissa(input),
1226            Err(format!("Decimal value '{input}' exceeds i128 range")),
1227        );
1228    }
1229
1230    #[rstest]
1231    #[case(".5", 5, 1)]
1232    #[case("-.5", -5, 1)]
1233    #[case("1.", 1, 0)]
1234    #[case("0001.0200", 10200, 4)]
1235    fn test_parse_decimal_mantissa_syntax(
1236        #[case] input: &str,
1237        #[case] mantissa: i128,
1238        #[case] precision: u8,
1239    ) {
1240        assert_eq!(parse_decimal_mantissa(input), Ok((mantissa, precision)));
1241    }
1242
1243    #[rstest]
1244    #[case("")]
1245    #[case(".")]
1246    #[case("+")]
1247    #[case("-")]
1248    #[case("1.2.3")]
1249    #[case(" 1")]
1250    #[case("1 ")]
1251    #[case("1 2")]
1252    #[case("1")]
1253    fn test_parse_decimal_mantissa_invalid_syntax(#[case] input: &str) {
1254        assert_eq!(
1255            parse_decimal_mantissa(input),
1256            Err(format!("Invalid decimal value '{input}'")),
1257        );
1258    }
1259
1260    #[rstest]
1261    fn test_parse_decimal_mantissa_fraction_length_limit() {
1262        let accepted = format!("0.{}", "0".repeat(255));
1263        let rejected = format!("{accepted}0");
1264
1265        assert_eq!(parse_decimal_mantissa(&accepted), Ok((0, 255)));
1266        assert_eq!(
1267            parse_decimal_mantissa(&rejected),
1268            Err(format!(
1269                "Decimal value '{rejected}' has too many fractional digits"
1270            )),
1271        );
1272    }
1273
1274    #[rstest]
1275    fn test_decimal_string_domain_precision_limit() {
1276        use crate::types::{Price, Quantity};
1277
1278        #[cfg(feature = "defi")]
1279        let precision = crate::defi::WEI_PRECISION;
1280        #[cfg(not(feature = "defi"))]
1281        let precision = FIXED_PRECISION;
1282        let accepted = format!("0.{}1", "0".repeat(usize::from(precision - 1)));
1283        let rejected = format!("{accepted}0");
1284        let price = accepted.parse::<Price>().unwrap();
1285        let quantity = accepted.parse::<Quantity>().unwrap();
1286
1287        assert_eq!(price.raw, 1);
1288        assert_eq!(price.precision, precision);
1289        assert_eq!(quantity.raw, 1);
1290        assert_eq!(quantity.precision, precision);
1291        assert!(rejected.parse::<Price>().is_err());
1292        assert!(rejected.parse::<Quantity>().is_err());
1293    }
1294
1295    #[rstest]
1296    #[case(0, 0, "0")]
1297    #[case(125, 2, "1.25")]
1298    #[case(-1234, 2, "-12.34")]
1299    #[case(1, 16, "0.0000000000000001")]
1300    #[case(-1, 16, "-0.0000000000000001")]
1301    #[case(1_000_000_000_000_000_000, 18, "1.000000000000000000")]
1302    fn test_scaled_raw_to_decimal_matches_plain_conversion(
1303        #[case] raw: i128,
1304        #[case] precision: u8,
1305        #[case] expected: &str,
1306    ) {
1307        let plain = Decimal::from_i128_with_scale(raw, u32::from(precision));
1308        let result = scaled_raw_to_decimal(raw, precision);
1309
1310        assert_eq!(result, plain);
1311        assert_eq!(result.scale(), plain.scale());
1312        assert_eq!(result.to_string(), expected);
1313    }
1314
1315    #[rstest]
1316    #[case(80_000_000_000_000_000_000_000_000_000, 16, "8000000000000")]
1317    #[case(340_282_366_920_930_000_000_000_000_000, 16, "34028236692093")]
1318    #[case(170_141_183_460_460_000_000_000_000_000, 16, "17014118346046")]
1319    #[case(-170_141_183_460_460_000_000_000_000_000, 16, "-17014118346046")]
1320    // Non-zero remainders exercise the fractional addition, including sign composition across
1321    // the truncating division, and a precision beyond `FIXED_PRECISION`.
1322    #[case(
1323        80_000_000_000_000_005_000_000_000_000,
1324        16,
1325        "8000000000000.000500000000000"
1326    )]
1327    #[case(-80_000_000_000_000_005_000_000_000_000, 16, "-8000000000000.000500000000000")]
1328    #[case(
1329        80_000_000_000_000_000_000_000_000_001,
1330        16,
1331        "8000000000000.000000000000000"
1332    )]
1333    #[case(
1334        80_000_000_000_000_000_250_000_000_000,
1335        18,
1336        "80000000000.00000025000000000"
1337    )]
1338    #[case(-80_000_000_000_000_000_250_000_000_000, 18, "-80000000000.00000025000000000")]
1339    fn test_scaled_raw_to_decimal_beyond_mantissa_rounds_rather_than_panics(
1340        #[case] raw: i128,
1341        #[case] precision: u8,
1342        #[case] expected: &str,
1343    ) {
1344        // `Decimal::from_i128_with_scale` panics on each of these raw values. Splitting the whole
1345        // and fractional parts lets `Decimal` drop scale instead, which is the only representable
1346        // outcome once the value needs more than a 96-bit mantissa.
1347        assert_eq!(scaled_raw_to_decimal(raw, precision).to_string(), expected);
1348    }
1349
1350    #[cfg(not(feature = "defi"))]
1351    #[rstest]
1352    fn test_precision_boundaries() {
1353        assert!(check_fixed_precision(0).is_ok());
1354        assert!(check_fixed_precision(FIXED_PRECISION).is_ok());
1355        assert!(check_fixed_precision(FIXED_PRECISION + 1).is_err());
1356    }
1357
1358    #[cfg(feature = "defi")]
1359    #[rstest]
1360    fn test_precision_boundaries() {
1361        use crate::defi::WEI_PRECISION;
1362
1363        assert!(check_fixed_precision(0).is_ok());
1364        assert!(check_fixed_precision(WEI_PRECISION).is_ok());
1365        assert!(check_fixed_precision(WEI_PRECISION + 1).is_err());
1366    }
1367
1368    #[rstest]
1369    #[case(0.0)]
1370    #[case(1.0)]
1371    #[case(-1.0)]
1372    fn test_basic_roundtrip(#[case] value: f64) {
1373        for precision in 0..=FIXED_PRECISION {
1374            let fixed = f64_to_fixed_i128(value, precision);
1375            let result = fixed_i128_to_f64(fixed);
1376            assert!(approx_eq!(f64, value, result, epsilon = 0.001));
1377        }
1378    }
1379
1380    #[rstest]
1381    #[case(1_000_000.0)]
1382    #[case(-1_000_000.0)]
1383    fn test_large_value_roundtrip(#[case] value: f64) {
1384        for precision in 0..=FIXED_PRECISION {
1385            let fixed = f64_to_fixed_i128(value, precision);
1386            let result = fixed_i128_to_f64(fixed);
1387            assert!(approx_eq!(f64, value, result, epsilon = 0.000_1));
1388        }
1389    }
1390
1391    #[rstest]
1392    #[case(0, 123_456.0)]
1393    #[case(0, 123_456.7)]
1394    #[case(1, 123_456.7)]
1395    #[case(2, 123_456.78)]
1396    #[case(8, 123_456.123_456_78)]
1397    fn test_precision_specific_values_basic(#[case] precision: u8, #[case] value: f64) {
1398        let result = f64_to_fixed_i128(value, precision);
1399        let back_converted = fixed_i128_to_f64(result);
1400        // Round-trip should preserve the value up to the specified precision
1401        let scale = 10.0_f64.powi(i32::from(precision));
1402        let expected_rounded = (value * scale).round() / scale;
1403        assert!((back_converted - expected_rounded).abs() < 1e-10);
1404    }
1405
1406    #[rstest]
1407    fn test_max_precision_values() {
1408        // Test with maximum precision that the current feature set supports
1409        let test_value = 123_456.123_456_789;
1410        let result = f64_to_fixed_i128(test_value, FIXED_PRECISION);
1411        let back_converted = fixed_i128_to_f64(result);
1412        // For maximum precision, we expect some floating-point limitations
1413        assert!((back_converted - test_value).abs() < 1e-6);
1414    }
1415
1416    #[rstest]
1417    #[case(0.0)]
1418    #[case(1.0)]
1419    #[case(1_000_000.0)]
1420    fn test_unsigned_basic_roundtrip(#[case] value: f64) {
1421        for precision in 0..=FIXED_PRECISION {
1422            let fixed = f64_to_fixed_u128(value, precision);
1423            let result = fixed_u128_to_f64(fixed);
1424            assert!(approx_eq!(f64, value, result, epsilon = 0.001));
1425        }
1426    }
1427
1428    #[rstest]
1429    #[case(0)]
1430    #[case(FIXED_PRECISION)]
1431    fn test_valid_precision(#[case] precision: u8) {
1432        let result = check_fixed_precision(precision);
1433        assert!(result.is_ok());
1434    }
1435
1436    #[cfg(not(feature = "defi"))]
1437    #[rstest]
1438    fn test_invalid_precision() {
1439        let precision = FIXED_PRECISION + 1;
1440        let result = check_fixed_precision(precision);
1441        assert!(result.is_err());
1442    }
1443
1444    #[cfg(feature = "defi")]
1445    #[rstest]
1446    fn test_invalid_precision() {
1447        use crate::defi::WEI_PRECISION;
1448        let precision = WEI_PRECISION + 1;
1449        let result = check_fixed_precision(precision);
1450        assert!(result.is_err());
1451    }
1452
1453    #[rstest]
1454    fn test_check_float_precision_matches_supported_range() {
1455        for precision in 0..=u8::MAX {
1456            let expected = if precision <= MAX_FLOAT_PRECISION {
1457                Ok(())
1458            } else {
1459                Err(CorrectnessError::PredicateViolation {
1460                    message: format!(
1461                        "Fixed-point precision {precision} exceeds maximum float precision 16"
1462                    ),
1463                })
1464            };
1465
1466            assert_eq!(check_float_precision(precision), expected);
1467        }
1468    }
1469
1470    #[rstest]
1471    fn test_check_float_precision_error_display() {
1472        let error = check_float_precision(18).unwrap_err();
1473
1474        assert_eq!(
1475            error.to_string(),
1476            "Fixed-point precision 18 exceeds maximum float precision 16"
1477        );
1478    }
1479
1480    #[cfg(not(feature = "defi"))]
1481    #[rstest]
1482    fn test_check_fixed_precision_returns_typed_error_with_stable_display() {
1483        let error = check_fixed_precision(FIXED_PRECISION + 1).unwrap_err();
1484
1485        assert_eq!(
1486            error,
1487            CorrectnessError::PredicateViolation {
1488                message: format!(
1489                    "`precision` exceeded maximum `FIXED_PRECISION` ({FIXED_PRECISION}), was {}",
1490                    FIXED_PRECISION + 1
1491                ),
1492            }
1493        );
1494        assert_eq!(
1495            error.to_string(),
1496            format!(
1497                "`precision` exceeded maximum `FIXED_PRECISION` ({FIXED_PRECISION}), was {}",
1498                FIXED_PRECISION + 1
1499            )
1500        );
1501    }
1502
1503    #[cfg(feature = "defi")]
1504    #[rstest]
1505    fn test_check_fixed_precision_returns_typed_error_with_stable_display() {
1506        use crate::defi::WEI_PRECISION;
1507
1508        let error = check_fixed_precision(WEI_PRECISION + 1).unwrap_err();
1509
1510        assert_eq!(
1511            error,
1512            CorrectnessError::PredicateViolation {
1513                message: format!(
1514                    "`precision` exceeded maximum `WEI_PRECISION` (18), was {}",
1515                    WEI_PRECISION + 1
1516                ),
1517            }
1518        );
1519        assert_eq!(
1520            error.to_string(),
1521            format!(
1522                "`precision` exceeded maximum `WEI_PRECISION` (18), was {}",
1523                WEI_PRECISION + 1
1524            )
1525        );
1526    }
1527
1528    #[rstest]
1529    #[case(0, 0.0)]
1530    #[case(1, 1.0)]
1531    #[case(1, 1.1)]
1532    #[case(9, 0.000_000_001)]
1533    #[case(16, 0.000_000_000_000_000_1)]
1534    #[case(0, -0.0)]
1535    #[case(1, -1.0)]
1536    #[case(1, -1.1)]
1537    #[case(9, -0.000_000_001)]
1538    #[case(16, -0.000_000_000_000_000_1)]
1539    fn test_f64_to_fixed_i128_to_fixed(#[case] precision: u8, #[case] value: f64) {
1540        let fixed = f64_to_fixed_i128(value, precision);
1541        let result = fixed_i128_to_f64(fixed);
1542        assert_eq!(result, value);
1543    }
1544
1545    #[rstest]
1546    #[case(0, 0.0)]
1547    #[case(1, 1.0)]
1548    #[case(1, 1.1)]
1549    #[case(9, 0.000_000_001)]
1550    #[case(16, 0.000_000_000_000_000_1)]
1551    fn test_f64_to_fixed_u128_to_fixed(#[case] precision: u8, #[case] value: f64) {
1552        let fixed = f64_to_fixed_u128(value, precision);
1553        let result = fixed_u128_to_f64(fixed);
1554        assert_eq!(result, value);
1555    }
1556
1557    #[rstest]
1558    #[case(0, 123_456.0)]
1559    #[case(0, 123_456.7)]
1560    #[case(0, 123_456.4)]
1561    #[case(1, 123_456.0)]
1562    #[case(1, 123_456.7)]
1563    #[case(1, 123_456.4)]
1564    #[case(2, 123_456.0)]
1565    #[case(2, 123_456.7)]
1566    #[case(2, 123_456.4)]
1567    fn test_f64_to_fixed_i128_with_precision(#[case] precision: u8, #[case] value: f64) {
1568        let result = f64_to_fixed_i128(value, precision);
1569
1570        // Calculate expected value dynamically based on current FIXED_PRECISION
1571        let pow1 = 10_i128.pow(u32::from(precision));
1572        let pow2 = 10_i128.pow(u32::from(FIXED_PRECISION - precision));
1573        let rounded = (value * pow1 as f64).round() as i128;
1574        let expected = rounded * pow2;
1575
1576        assert_eq!(
1577            result, expected,
1578            "Failed for precision {precision}, value {value}: got {result}, expected {expected}"
1579        );
1580    }
1581
1582    #[rstest]
1583    #[case(0, 5.555_555_555_555_555)]
1584    #[case(1, 5.555_555_555_555_555)]
1585    #[case(2, 5.555_555_555_555_555)]
1586    #[case(3, 5.555_555_555_555_555)]
1587    #[case(4, 5.555_555_555_555_555)]
1588    #[case(5, 5.555_555_555_555_555)]
1589    #[case(6, 5.555_555_555_555_555)]
1590    #[case(7, 5.555_555_555_555_555)]
1591    #[case(8, 5.555_555_555_555_555)]
1592    #[case(9, 5.555_555_555_555_555)]
1593    #[case(10, 5.555_555_555_555_555)]
1594    #[case(11, 5.555_555_555_555_555)]
1595    #[case(12, 5.555_555_555_555_555)]
1596    #[case(13, 5.555_555_555_555_555)]
1597    #[case(14, 5.555_555_555_555_555)]
1598    #[case(15, 5.555_555_555_555_555)]
1599    #[case(0, -5.555_555_555_555_555)]
1600    #[case(1, -5.555_555_555_555_555)]
1601    #[case(2, -5.555_555_555_555_555)]
1602    #[case(3, -5.555_555_555_555_555)]
1603    #[case(4, -5.555_555_555_555_555)]
1604    #[case(5, -5.555_555_555_555_555)]
1605    #[case(6, -5.555_555_555_555_555)]
1606    #[case(7, -5.555_555_555_555_555)]
1607    #[case(8, -5.555_555_555_555_555)]
1608    #[case(9, -5.555_555_555_555_555)]
1609    #[case(10, -5.555_555_555_555_555)]
1610    #[case(11, -5.555_555_555_555_555)]
1611    #[case(12, -5.555_555_555_555_555)]
1612    #[case(13, -5.555_555_555_555_555)]
1613    #[case(14, -5.555_555_555_555_555)]
1614    #[case(15, -5.555_555_555_555_555)]
1615    fn test_f64_to_fixed_i128(#[case] precision: u8, #[case] value: f64) {
1616        // Only test up to the current FIXED_PRECISION
1617        if precision > FIXED_PRECISION {
1618            return;
1619        }
1620
1621        let result = f64_to_fixed_i128(value, precision);
1622
1623        // Calculate expected value dynamically based on current FIXED_PRECISION
1624        let pow1 = 10_i128.pow(u32::from(precision));
1625        let pow2 = 10_i128.pow(u32::from(FIXED_PRECISION - precision));
1626        let rounded = (value * pow1 as f64).round() as i128;
1627        let expected = rounded * pow2;
1628
1629        assert_eq!(
1630            result, expected,
1631            "Failed for precision {precision}, value {value}: got {result}, expected {expected}"
1632        );
1633    }
1634
1635    #[rstest]
1636    #[case(0, 5.555_555_555_555_555)]
1637    #[case(1, 5.555_555_555_555_555)]
1638    #[case(2, 5.555_555_555_555_555)]
1639    #[case(3, 5.555_555_555_555_555)]
1640    #[case(4, 5.555_555_555_555_555)]
1641    #[case(5, 5.555_555_555_555_555)]
1642    #[case(6, 5.555_555_555_555_555)]
1643    #[case(7, 5.555_555_555_555_555)]
1644    #[case(8, 5.555_555_555_555_555)]
1645    #[case(9, 5.555_555_555_555_555)]
1646    #[case(10, 5.555_555_555_555_555)]
1647    #[case(11, 5.555_555_555_555_555)]
1648    #[case(12, 5.555_555_555_555_555)]
1649    #[case(13, 5.555_555_555_555_555)]
1650    #[case(14, 5.555_555_555_555_555)]
1651    #[case(15, 5.555_555_555_555_555)]
1652    #[case(16, 5.555_555_555_555_555)]
1653    fn test_f64_to_fixed_u64(#[case] precision: u8, #[case] value: f64) {
1654        // Only test up to the current FIXED_PRECISION
1655        if precision > FIXED_PRECISION {
1656            return;
1657        }
1658
1659        let result = f64_to_fixed_u128(value, precision);
1660
1661        // Calculate expected value dynamically based on current FIXED_PRECISION
1662        let pow1 = 10_u128.pow(u32::from(precision));
1663        let pow2 = 10_u128.pow(u32::from(FIXED_PRECISION - precision));
1664        let rounded = (value * pow1 as f64).round() as u128;
1665        let expected = rounded * pow2;
1666
1667        assert_eq!(
1668            result, expected,
1669            "Failed for precision {precision}, value {value}: got {result}, expected {expected}"
1670        );
1671    }
1672
1673    #[rstest]
1674    fn test_fixed_i128_to_f64(
1675        #[values(1, -1, 2, -2, 10, -10, 100, -100, 1_000, -1_000, -10_000, -100_000)] value: i128,
1676    ) {
1677        assert_eq!(fixed_i128_to_f64(value), value as f64 / FIXED_SCALAR);
1678    }
1679
1680    #[rstest]
1681    fn test_fixed_u128_to_f64(
1682        #[values(
1683            0,
1684            1,
1685            2,
1686            3,
1687            10,
1688            100,
1689            1_000,
1690            10_000,
1691            100_000,
1692            1_000_000,
1693            10_000_000,
1694            100_000_000,
1695            1_000_000_000,
1696            10_000_000_000,
1697            100_000_000_000,
1698            1_000_000_000_000,
1699            10_000_000_000_000,
1700            100_000_000_000_000,
1701            1_000_000_000_000_000,
1702            10_000_000_000_000_000,
1703            100_000_000_000_000_000,
1704            1_000_000_000_000_000_000,
1705            10_000_000_000_000_000_000,
1706            100_000_000_000_000_000_000
1707        )]
1708        value: u128,
1709    ) {
1710        let result = fixed_u128_to_f64(value);
1711        assert_eq!(result, (value as f64) / FIXED_SCALAR);
1712    }
1713
1714    // -------------------------------------------------------------------------
1715    // Raw value validation tests (high-precision: FIXED_PRECISION = 16)
1716    // -------------------------------------------------------------------------
1717
1718    #[rstest]
1719    #[case(0, 0)] // Zero is always valid
1720    #[case(0, 10_000_000_000_000_000)] // 1 * 10^16 at precision 0
1721    #[case(0, 1_200_000_000_000_000_000)] // 120 * 10^16 at precision 0
1722    #[case(8, 12_345_678_900_000_000)] // 123456789 * 10^8 at precision 8
1723    #[case(15, 1_234_567_890_123_450)] // Multiple of 10 at precision 15
1724    fn test_check_fixed_raw_u128_valid(#[case] precision: u8, #[case] raw: u128) {
1725        assert!(check_fixed_raw_u128(raw, precision).is_ok());
1726    }
1727
1728    #[rstest]
1729    #[case(0, 1)] // Not multiple of 10^16
1730    #[case(0, 9_999_999_999_999_999)] // One less than scale
1731    #[case(0, 10_000_000_000_000_001)] // One more than 10^16
1732    #[case(8, 12_345_678_900_000_001)] // Not multiple of 10^8
1733    #[case(15, 1_234_567_890_123_451)] // Not multiple of 10
1734    fn test_check_fixed_raw_u128_invalid(#[case] precision: u8, #[case] raw: u128) {
1735        assert!(check_fixed_raw_u128(raw, precision).is_err());
1736    }
1737
1738    #[rstest]
1739    fn test_check_fixed_raw_u128_at_max_precision() {
1740        // At FIXED_PRECISION (16), validation is skipped
1741        assert!(check_fixed_raw_u128(0, FIXED_PRECISION).is_ok());
1742        assert!(check_fixed_raw_u128(1, FIXED_PRECISION).is_ok());
1743        assert!(check_fixed_raw_u128(123_456_789, FIXED_PRECISION).is_ok());
1744        assert!(check_fixed_raw_u128(u128::MAX, FIXED_PRECISION).is_ok());
1745    }
1746
1747    #[rstest]
1748    #[case(0, 0)]
1749    #[case(0, 10_000_000_000_000_000)]
1750    #[case(0, -10_000_000_000_000_000)]
1751    #[case(8, 12_345_678_900_000_000)]
1752    #[case(8, -12_345_678_900_000_000)]
1753    fn test_check_fixed_raw_i128_valid(#[case] precision: u8, #[case] raw: i128) {
1754        assert!(check_fixed_raw_i128(raw, precision).is_ok());
1755    }
1756
1757    #[rstest]
1758    #[case(0, 1)]
1759    #[case(0, -1)]
1760    #[case(0, 9_999_999_999_999_999)]
1761    #[case(0, -9_999_999_999_999_999)]
1762    fn test_check_fixed_raw_i128_invalid(#[case] precision: u8, #[case] raw: i128) {
1763        assert!(check_fixed_raw_i128(raw, precision).is_err());
1764    }
1765
1766    #[rstest]
1767    fn test_check_fixed_raw_i128_at_max_precision() {
1768        assert!(check_fixed_raw_i128(0, FIXED_PRECISION).is_ok());
1769        assert!(check_fixed_raw_i128(1, FIXED_PRECISION).is_ok());
1770        assert!(check_fixed_raw_i128(-1, FIXED_PRECISION).is_ok());
1771        assert!(check_fixed_raw_i128(i128::MAX, FIXED_PRECISION).is_ok());
1772        assert!(check_fixed_raw_i128(i128::MIN, FIXED_PRECISION).is_ok());
1773    }
1774
1775    #[rstest]
1776    #[should_panic(expected = "Overflow when scaling f64 to fixed-point i128")]
1777    fn test_f64_to_fixed_i128_overflow_panics() {
1778        let _ = f64_to_fixed_i128(1e30, 0);
1779    }
1780
1781    #[rstest]
1782    #[should_panic(expected = "Overflow when scaling f64 to fixed-point u128")]
1783    fn test_f64_to_fixed_u128_overflow_panics() {
1784        let _ = f64_to_fixed_u128(1e30, 0);
1785    }
1786}
1787
1788#[cfg(not(feature = "high-precision"))]
1789#[cfg(test)]
1790mod tests {
1791    use nautilus_core::approx_eq;
1792    use rstest::rstest;
1793
1794    use super::*;
1795
1796    #[rstest]
1797    fn test_correct_raw_rounds_half_away_from_zero() {
1798        let precision = FIXED_PRECISION - 1;
1799
1800        assert_eq!(correct_raw_u128(20, precision), 20);
1801        assert_eq!(correct_raw_u128(14, precision), 10);
1802        assert_eq!(correct_raw_u128(15, precision), 20);
1803        assert_eq!(correct_raw_u64(14, precision), 10);
1804        assert_eq!(correct_raw_u64(15, precision), 20);
1805        assert_eq!(correct_raw_i128(15, precision), 20);
1806        assert_eq!(correct_raw_i128(-14, precision), -10);
1807        assert_eq!(correct_raw_i128(-15, precision), -20);
1808        assert_eq!(correct_raw_i64(15, precision), 20);
1809        assert_eq!(correct_raw_i64(-14, precision), -10);
1810        assert_eq!(correct_raw_i64(-15, precision), -20);
1811    }
1812
1813    #[rstest]
1814    fn test_f64_fixed_u128_round_trip() {
1815        let raw = f64_to_fixed_u128(1.5, 1);
1816
1817        assert_eq!(raw, 15 * 10_u128.pow(u32::from(FIXED_PRECISION - 1)));
1818        assert_eq!(fixed_u128_to_f64(raw), 1.5);
1819    }
1820
1821    #[rstest]
1822    fn test_mantissa_exponent_to_fixed_i128_allows_max_scale_factor() {
1823        let exponent = i8::try_from(38 - FIXED_PRECISION).unwrap();
1824
1825        assert_eq!(
1826            mantissa_exponent_to_fixed_i128(1, exponent, 0).unwrap(),
1827            10_i128.pow(38)
1828        );
1829        assert_eq!(
1830            mantissa_exponent_to_fixed_i128(1, exponent + 1, 0)
1831                .unwrap_err()
1832                .to_string(),
1833            format!(
1834                "Exponent {} produces scale factor 10^39 which exceeds i128 range",
1835                exponent + 1
1836            )
1837        );
1838    }
1839
1840    #[rstest]
1841    fn test_raw_scales_match_requires_equal_effective_scale() {
1842        assert!(raw_scales_match(0, FIXED_PRECISION));
1843        assert!(raw_scales_match(FIXED_PRECISION + 1, FIXED_PRECISION + 1));
1844        assert!(!raw_scales_match(FIXED_PRECISION, FIXED_PRECISION + 1));
1845    }
1846
1847    #[rstest]
1848    fn test_canonical_raw_trims_native_scale_trailing_zeros() {
1849        assert_eq!(
1850            canonical_raw(0_u128, FIXED_PRECISION + 2),
1851            (0, FIXED_PRECISION)
1852        );
1853        assert_eq!(
1854            canonical_raw(1_200_u128, FIXED_PRECISION + 2),
1855            (12, FIXED_PRECISION)
1856        );
1857        assert_eq!(
1858            canonical_raw(12_000_u128, FIXED_PRECISION + 2),
1859            (120, FIXED_PRECISION)
1860        );
1861        assert_eq!(
1862            canonical_raw(1_205_u128, FIXED_PRECISION + 2),
1863            (1_205, FIXED_PRECISION + 2)
1864        );
1865        assert_eq!(
1866            canonical_raw(5_u128, FIXED_PRECISION - 1),
1867            (5, FIXED_PRECISION)
1868        );
1869    }
1870
1871    #[rstest]
1872    fn test_compare_raw_zero_operands_are_equal_across_scales() {
1873        assert_eq!(compare_raw(0_u128, u8::MAX, 0_u128, 2), Ordering::Equal);
1874    }
1875
1876    #[rstest]
1877    fn test_precision_boundaries() {
1878        assert!(check_fixed_precision(0).is_ok());
1879        assert!(check_fixed_precision(FIXED_PRECISION).is_ok());
1880        assert!(check_fixed_precision(FIXED_PRECISION + 1).is_err());
1881    }
1882
1883    #[rstest]
1884    #[case(0.0)]
1885    #[case(1.0)]
1886    #[case(-1.0)]
1887    fn test_basic_roundtrip(#[case] value: f64) {
1888        for precision in 0..=FIXED_PRECISION {
1889            let fixed = f64_to_fixed_i64(value, precision);
1890            let result = fixed_i64_to_f64(fixed);
1891            assert!(approx_eq!(f64, value, result, epsilon = 0.001));
1892        }
1893    }
1894
1895    #[rstest]
1896    #[case(1_000_000.0)]
1897    #[case(-1_000_000.0)]
1898    fn test_large_value_roundtrip(#[case] value: f64) {
1899        for precision in 0..=FIXED_PRECISION {
1900            let fixed = f64_to_fixed_i64(value, precision);
1901            let result = fixed_i64_to_f64(fixed);
1902            assert!(approx_eq!(f64, value, result, epsilon = 0.000_1));
1903        }
1904    }
1905
1906    #[rstest]
1907    #[case(0, 123_456.0, 123_456_000_000_000)]
1908    #[case(0, 123_456.7, 123_457_000_000_000)]
1909    #[case(1, 123_456.7, 123_456_700_000_000)]
1910    #[case(2, 123_456.78, 123_456_780_000_000)]
1911    #[case(8, 123_456.123_456_78, 123_456_123_456_780)]
1912    #[case(9, 123_456.123_456_789, 123_456_123_456_789)]
1913    fn test_precision_specific_values(
1914        #[case] precision: u8,
1915        #[case] value: f64,
1916        #[case] expected: i64,
1917    ) {
1918        assert_eq!(f64_to_fixed_i64(value, precision), expected);
1919    }
1920
1921    #[rstest]
1922    #[case(0.0)]
1923    #[case(1.0)]
1924    #[case(1_000_000.0)]
1925    fn test_unsigned_basic_roundtrip(#[case] value: f64) {
1926        for precision in 0..=FIXED_PRECISION {
1927            let fixed = f64_to_fixed_u64(value, precision);
1928            let result = fixed_u64_to_f64(fixed);
1929            assert!(approx_eq!(f64, value, result, epsilon = 0.001));
1930        }
1931    }
1932
1933    #[rstest]
1934    #[case(0, 1.4, 1.0)]
1935    #[case(0, 1.5, 2.0)]
1936    #[case(0, 1.6, 2.0)]
1937    #[case(1, 1.44, 1.4)]
1938    #[case(1, 1.45, 1.5)]
1939    #[case(1, 1.46, 1.5)]
1940    #[case(2, 1.444, 1.44)]
1941    #[case(2, 1.445, 1.45)]
1942    #[case(2, 1.446, 1.45)]
1943    fn test_rounding(#[case] precision: u8, #[case] input: f64, #[case] expected: f64) {
1944        let fixed = f64_to_fixed_i128(input, precision);
1945        assert!(approx_eq!(
1946            f64,
1947            fixed_i128_to_f64(fixed),
1948            expected,
1949            epsilon = 0.000_000_001
1950        ));
1951    }
1952
1953    #[rstest]
1954    fn test_special_values() {
1955        // Zero handling
1956        assert_eq!(f64_to_fixed_i128(0.0, FIXED_PRECISION), 0);
1957        assert_eq!(f64_to_fixed_i128(-0.0, FIXED_PRECISION), 0);
1958
1959        // Small values
1960        let smallest_positive = 1.0 / FIXED_SCALAR;
1961        let fixed_smallest = f64_to_fixed_i128(smallest_positive, FIXED_PRECISION);
1962        assert_eq!(fixed_smallest, 1);
1963
1964        // Large integers
1965        let large_int = 1_000_000_000.0;
1966        let fixed_large = f64_to_fixed_i128(large_int, 0);
1967        assert_eq!(fixed_i128_to_f64(fixed_large), large_int);
1968    }
1969
1970    #[rstest]
1971    #[case(0)]
1972    #[case(FIXED_PRECISION)]
1973    fn test_valid_precision(#[case] precision: u8) {
1974        let result = check_fixed_precision(precision);
1975        assert!(result.is_ok());
1976    }
1977
1978    #[rstest]
1979    fn test_invalid_precision() {
1980        let precision = FIXED_PRECISION + 1;
1981        let result = check_fixed_precision(precision);
1982        assert!(result.is_err());
1983    }
1984
1985    #[rstest]
1986    #[case(0, 0.0)]
1987    #[case(1, 1.0)]
1988    #[case(1, 1.1)]
1989    #[case(9, 0.000_000_001)]
1990    #[case(0, -0.0)]
1991    #[case(1, -1.0)]
1992    #[case(1, -1.1)]
1993    #[case(9, -0.000_000_001)]
1994    fn test_f64_to_fixed_i64_to_fixed(#[case] precision: u8, #[case] value: f64) {
1995        let fixed = f64_to_fixed_i64(value, precision);
1996        let result = fixed_i64_to_f64(fixed);
1997        assert_eq!(result, value);
1998    }
1999
2000    #[rstest]
2001    #[case(0, 0.0)]
2002    #[case(1, 1.0)]
2003    #[case(1, 1.1)]
2004    #[case(9, 0.000_000_001)]
2005    fn test_f64_to_fixed_u64_to_fixed(#[case] precision: u8, #[case] value: f64) {
2006        let fixed = f64_to_fixed_u64(value, precision);
2007        let result = fixed_u64_to_f64(fixed);
2008        assert_eq!(result, value);
2009    }
2010
2011    #[rstest]
2012    #[case(0, 123_456.0, 123_456_000_000_000)]
2013    #[case(0, 123_456.7, 123_457_000_000_000)]
2014    #[case(0, 123_456.4, 123_456_000_000_000)]
2015    #[case(1, 123_456.0, 123_456_000_000_000)]
2016    #[case(1, 123_456.7, 123_456_700_000_000)]
2017    #[case(1, 123_456.4, 123_456_400_000_000)]
2018    #[case(2, 123_456.0, 123_456_000_000_000)]
2019    #[case(2, 123_456.7, 123_456_700_000_000)]
2020    #[case(2, 123_456.4, 123_456_400_000_000)]
2021    fn test_f64_to_fixed_i64_with_precision(
2022        #[case] precision: u8,
2023        #[case] value: f64,
2024        #[case] expected: i64,
2025    ) {
2026        assert_eq!(f64_to_fixed_i64(value, precision), expected);
2027    }
2028
2029    #[rstest]
2030    #[case(0, 5.5, 6_000_000_000)]
2031    #[case(1, 5.55, 5_600_000_000)]
2032    #[case(2, 5.555, 5_560_000_000)]
2033    #[case(3, 5.5555, 5_556_000_000)]
2034    #[case(4, 5.55555, 5_555_600_000)]
2035    #[case(5, 5.555_555, 5_555_560_000)]
2036    #[case(6, 5.555_555_5, 5_555_556_000)]
2037    #[case(7, 5.555_555_55, 5_555_555_600)]
2038    #[case(8, 5.555_555_555, 5_555_555_560)]
2039    #[case(9, 5.555_555_555_5, 5_555_555_556)]
2040    #[case(0, -5.5, -6_000_000_000)]
2041    #[case(1, -5.55, -5_600_000_000)]
2042    #[case(2, -5.555, -5_560_000_000)]
2043    #[case(3, -5.5555, -5_556_000_000)]
2044    #[case(4, -5.55555, -5_555_600_000)]
2045    #[case(5, -5.555_555, -5_555_560_000)]
2046    #[case(6, -5.555_555_5, -5_555_556_000)]
2047    #[case(7, -5.555_555_55, -5_555_555_600)]
2048    #[case(8, -5.555_555_555, -5_555_555_560)]
2049    #[case(9, -5.555_555_555_5, -5_555_555_556)]
2050    fn test_f64_to_fixed_i64(#[case] precision: u8, #[case] value: f64, #[case] expected: i64) {
2051        assert_eq!(f64_to_fixed_i64(value, precision), expected);
2052    }
2053
2054    #[rstest]
2055    #[case(0, 5.5, 6_000_000_000)]
2056    #[case(1, 5.55, 5_600_000_000)]
2057    #[case(2, 5.555, 5_560_000_000)]
2058    #[case(3, 5.5555, 5_556_000_000)]
2059    #[case(4, 5.55555, 5_555_600_000)]
2060    #[case(5, 5.555_555, 5_555_560_000)]
2061    #[case(6, 5.555_555_5, 5_555_556_000)]
2062    #[case(7, 5.555_555_55, 5_555_555_600)]
2063    #[case(8, 5.555_555_555, 5_555_555_560)]
2064    #[case(9, 5.555_555_555_5, 5_555_555_556)]
2065    fn test_f64_to_fixed_u64(#[case] precision: u8, #[case] value: f64, #[case] expected: u64) {
2066        assert_eq!(f64_to_fixed_u64(value, precision), expected);
2067    }
2068
2069    #[rstest]
2070    fn test_fixed_i64_to_f64(
2071        #[values(1, -1, 2, -2, 10, -10, 100, -100, 1_000, -1_000)] value: i64,
2072    ) {
2073        assert_eq!(fixed_i64_to_f64(value), value as f64 / FIXED_SCALAR);
2074    }
2075
2076    #[rstest]
2077    fn test_fixed_u64_to_f64(
2078        #[values(
2079            0,
2080            1,
2081            2,
2082            3,
2083            10,
2084            100,
2085            1_000,
2086            10_000,
2087            100_000,
2088            1_000_000,
2089            10_000_000,
2090            100_000_000,
2091            1_000_000_000,
2092            10_000_000_000,
2093            100_000_000_000,
2094            1_000_000_000_000,
2095            10_000_000_000_000,
2096            100_000_000_000_000,
2097            1_000_000_000_000_000
2098        )]
2099        value: u64,
2100    ) {
2101        let result = fixed_u64_to_f64(value);
2102        assert_eq!(result, (value as f64) / FIXED_SCALAR);
2103    }
2104
2105    #[rstest]
2106    #[case(0, 0)] // Zero is always valid
2107    #[case(0, 1_000_000_000)] // 1 * 10^9 at precision 0
2108    #[case(0, 120_000_000_000)] // 120 * 10^9 at precision 0
2109    #[case(2, 123_450_000_000)] // 12345 * 10^7 at precision 2
2110    #[case(8, 1_234_567_890)] // 123456789 * 10 at precision 8
2111    fn test_check_fixed_raw_u64_valid(#[case] precision: u8, #[case] raw: u64) {
2112        assert!(check_fixed_raw_u64(raw, precision).is_ok());
2113    }
2114
2115    #[rstest]
2116    #[case(0, 1)] // Not multiple of 10^9
2117    #[case(0, 999_999_999)] // One less than scale
2118    #[case(0, 1_000_000_001)] // One more than 10^9
2119    #[case(0, 119_582_001_968_421_736)] // The original bug case
2120    #[case(2, 123_456_789_000)] // Not multiple of 10^7
2121    #[case(8, 1_234_567_891)] // Not multiple of 10
2122    fn test_check_fixed_raw_u64_invalid(#[case] precision: u8, #[case] raw: u64) {
2123        assert!(check_fixed_raw_u64(raw, precision).is_err());
2124    }
2125
2126    #[rstest]
2127    fn test_check_fixed_raw_u64_at_max_precision() {
2128        // At FIXED_PRECISION, validation is skipped - any value is valid
2129        assert!(check_fixed_raw_u64(0, FIXED_PRECISION).is_ok());
2130        assert!(check_fixed_raw_u64(1, FIXED_PRECISION).is_ok());
2131        assert!(check_fixed_raw_u64(123_456_789, FIXED_PRECISION).is_ok());
2132        assert!(check_fixed_raw_u64(u64::MAX, FIXED_PRECISION).is_ok());
2133    }
2134
2135    #[rstest]
2136    #[case(0, 0)]
2137    #[case(0, 1_000_000_000)]
2138    #[case(0, -1_000_000_000)]
2139    #[case(2, 123_450_000_000)]
2140    #[case(2, -123_450_000_000)]
2141    fn test_check_fixed_raw_i64_valid(#[case] precision: u8, #[case] raw: i64) {
2142        assert!(check_fixed_raw_i64(raw, precision).is_ok());
2143    }
2144
2145    #[rstest]
2146    #[case(0, 1)]
2147    #[case(0, -1)]
2148    #[case(0, 999_999_999)]
2149    #[case(0, -999_999_999)]
2150    fn test_check_fixed_raw_i64_invalid(#[case] precision: u8, #[case] raw: i64) {
2151        assert!(check_fixed_raw_i64(raw, precision).is_err());
2152    }
2153
2154    #[rstest]
2155    fn test_check_fixed_raw_i64_at_max_precision() {
2156        assert!(check_fixed_raw_i64(0, FIXED_PRECISION).is_ok());
2157        assert!(check_fixed_raw_i64(1, FIXED_PRECISION).is_ok());
2158        assert!(check_fixed_raw_i64(-1, FIXED_PRECISION).is_ok());
2159        assert!(check_fixed_raw_i64(i64::MAX, FIXED_PRECISION).is_ok());
2160        assert!(check_fixed_raw_i64(i64::MIN, FIXED_PRECISION).is_ok());
2161    }
2162
2163    #[rstest]
2164    #[should_panic(expected = "Overflow when scaling f64 to fixed-point i64")]
2165    fn test_f64_to_fixed_i64_overflow_panics() {
2166        let _ = f64_to_fixed_i64(2e18, 0);
2167    }
2168
2169    #[rstest]
2170    #[should_panic(expected = "Overflow when scaling f64 to fixed-point u64")]
2171    fn test_f64_to_fixed_u64_overflow_panics() {
2172        let _ = f64_to_fixed_u64(2e19, 0);
2173    }
2174}
2175
2176#[cfg(test)]
2177mod bankers_round_tests {
2178    use std::str::FromStr;
2179
2180    use rstest::rstest;
2181    use rust_decimal::{Decimal, RoundingStrategy};
2182
2183    use super::*;
2184
2185    #[rstest]
2186    // Excess=0: no rounding, identity
2187    #[case(0, 0, 0)]
2188    #[case(1, 0, 1)]
2189    #[case(5, 0, 5)]
2190    #[case(99, 0, 99)]
2191    #[case(-7, 0, -7)]
2192    // Excess >= 39: overflow guard returns 0
2193    #[case(12345, 39, 0)]
2194    #[case(i128::from(i64::MAX), 100, 0)]
2195    #[case(-99999, 50, 0)]
2196    // Excess=1: halfway cases (remainder == 5, half of 10)
2197    #[case(15, 1, 2)] // 1.5 -> 2 (round up to even)
2198    #[case(25, 1, 2)] // 2.5 -> 2 (round down to even)
2199    #[case(35, 1, 4)] // 3.5 -> 4 (round up to even)
2200    #[case(45, 1, 4)] // 4.5 -> 4 (round down to even)
2201    #[case(55, 1, 6)] // 5.5 -> 6 (round up to even)
2202    #[case(65, 1, 6)] // 6.5 -> 6 (round down to even)
2203    #[case(75, 1, 8)] // 7.5 -> 8 (round up to even)
2204    #[case(85, 1, 8)] // 8.5 -> 8 (round down to even)
2205    #[case(95, 1, 10)] // 9.5 -> 10 (round up to even)
2206    #[case(105, 1, 10)] // 10.5 -> 10 (round down to even)
2207    // Excess=1: non-halfway cases
2208    #[case(14, 1, 1)] // 1.4 -> 1 (truncate)
2209    #[case(16, 1, 2)] // 1.6 -> 2 (round up)
2210    #[case(24, 1, 2)] // 2.4 -> 2 (truncate)
2211    #[case(26, 1, 3)] // 2.6 -> 3 (round up)
2212    #[case(11, 1, 1)] // 1.1 -> 1 (truncate)
2213    #[case(19, 1, 2)] // 1.9 -> 2 (round up)
2214    // Excess=2: halfway cases (remainder == 50, half of 100)
2215    #[case(150, 2, 2)] // 1.50 -> 2 (round up to even)
2216    #[case(250, 2, 2)] // 2.50 -> 2 (round down to even)
2217    #[case(350, 2, 4)] // 3.50 -> 4 (round up to even)
2218    #[case(450, 2, 4)] // 4.50 -> 4 (round down to even)
2219    #[case(550, 2, 6)] // 5.50 -> 6 (round up to even)
2220    #[case(1050, 2, 10)] // 10.50 -> 10 (round down to even)
2221    #[case(1150, 2, 12)] // 11.50 -> 12 (round up to even)
2222    // Excess=2: non-halfway cases
2223    #[case(149, 2, 1)] // 1.49 -> 1 (truncate)
2224    #[case(151, 2, 2)] // 1.51 -> 2 (round up)
2225    #[case(199, 2, 2)] // 1.99 -> 2 (round up)
2226    #[case(101, 2, 1)] // 1.01 -> 1 (truncate)
2227    // Excess=3: halfway cases (remainder == 500, half of 1000)
2228    #[case(1500, 3, 2)] // 1.500 -> 2 (round up to even)
2229    #[case(2500, 3, 2)] // 2.500 -> 2 (round down to even)
2230    #[case(3500, 3, 4)] // 3.500 -> 4 (round up to even)
2231    #[case(10500, 3, 10)] // 10.500 -> 10 (round down to even)
2232    #[case(11500, 3, 12)] // 11.500 -> 12 (round up to even)
2233    // Excess=3: non-halfway cases
2234    #[case(1499, 3, 1)] // 1.499 -> 1 (truncate)
2235    #[case(1501, 3, 2)] // 1.501 -> 2 (round up)
2236    // Negative halfway cases
2237    #[case(-15, 1, -2)] // -1.5 -> -2 (round away from zero to even)
2238    #[case(-25, 1, -2)] // -2.5 -> -2 (round toward zero to even)
2239    #[case(-35, 1, -4)] // -3.5 -> -4 (round away from zero to even)
2240    #[case(-45, 1, -4)] // -4.5 -> -4 (round toward zero to even)
2241    #[case(-55, 1, -6)] // -5.5 -> -6 (round away from zero to even)
2242    #[case(-65, 1, -6)] // -6.5 -> -6 (round toward zero to even)
2243    #[case(-150, 2, -2)] // -1.50 -> -2 (round away from zero to even)
2244    #[case(-250, 2, -2)] // -2.50 -> -2 (round toward zero to even)
2245    #[case(-350, 2, -4)] // -3.50 -> -4 (round away from zero to even)
2246    // Negative non-halfway cases
2247    #[case(-14, 1, -1)] // -1.4 -> -1 (truncate toward zero)
2248    #[case(-16, 1, -2)] // -1.6 -> -2 (round away from zero)
2249    #[case(-24, 1, -2)] // -2.4 -> -2 (truncate toward zero)
2250    #[case(-26, 1, -3)] // -2.6 -> -3 (round away from zero)
2251    // Zero mantissa
2252    #[case(0, 1, 0)]
2253    #[case(0, 2, 0)]
2254    #[case(0, 5, 0)]
2255    // Large excess values
2256    #[case(123_456_789, 3, 123_457)] // 123456.789 -> 123457
2257    #[case(123_456_500, 3, 123_456)] // 123456.500 -> 123456 (half, even quotient)
2258    #[case(123_457_500, 3, 123_458)] // 123457.500 -> 123458 (half, odd quotient)
2259    #[case(100_005, 1, 10_000)] // 10000.5 -> 10000 (half, even quotient)
2260    #[case(100_015, 1, 10_002)] // 10001.5 -> 10002 (half, odd quotient)
2261    // Large mantissa values
2262    #[case(999_999_999_999_999_995, 1, 100_000_000_000_000_000)]
2263    #[case(1_000_000_000_000_000_005, 1, 100_000_000_000_000_000)]
2264    fn test_bankers_round(#[case] mantissa: i128, #[case] excess: u32, #[case] expected: i128) {
2265        assert_eq!(
2266            bankers_round(mantissa, excess),
2267            expected,
2268            "bankers_round({mantissa}, {excess}) expected {expected}"
2269        );
2270    }
2271
2272    // Symmetry: bankers_round(-x, e) == -bankers_round(x, e) for all positive x
2273    #[rstest]
2274    #[case(15, 1)]
2275    #[case(25, 1)]
2276    #[case(35, 1)]
2277    #[case(150, 2)]
2278    #[case(250, 2)]
2279    #[case(1500, 3)]
2280    #[case(2500, 3)]
2281    #[case(123_456_789, 3)]
2282    #[case(14, 1)]
2283    #[case(16, 1)]
2284    fn test_bankers_round_negative_symmetry(#[case] mantissa: i128, #[case] excess: u32) {
2285        assert_eq!(
2286            bankers_round(-mantissa, excess),
2287            -bankers_round(mantissa, excess),
2288            "Negative symmetry failed for mantissa={mantissa}, excess={excess}"
2289        );
2290    }
2291
2292    // Verify consistency with Rust Decimal's banker's rounding
2293    #[rstest]
2294    #[case("1.005", 2, "1.00")] // 0.005 remainder, even quotient -> truncate
2295    #[case("1.015", 2, "1.02")] // 0.005 remainder, odd quotient -> round up
2296    #[case("1.025", 2, "1.02")] // 0.005 remainder, even quotient -> truncate
2297    #[case("1.035", 2, "1.04")] // 0.005 remainder, odd quotient -> round up
2298    #[case("1.045", 2, "1.04")] // 0.005 remainder, even quotient -> truncate
2299    #[case("2.5", 0, "2")] // 0.5 remainder, even quotient -> truncate
2300    #[case("3.5", 0, "4")] // 0.5 remainder, odd quotient -> round up
2301    #[case("-2.5", 0, "-2")]
2302    #[case("-3.5", 0, "-4")]
2303    #[case("123.456", 2, "123.46")]
2304    #[case("123.455", 2, "123.46")] // Odd quotient at half
2305    #[case("123.445", 2, "123.44")] // Even quotient at half
2306    fn test_bankers_round_matches_decimal(
2307        #[case] input: &str,
2308        #[case] target_precision: u8,
2309        #[case] expected: &str,
2310    ) {
2311        let dec = Decimal::from_str(input).unwrap();
2312        let expected_dec = Decimal::from_str(expected).unwrap();
2313
2314        let decimal_rounded = dec.round_dp_with_strategy(
2315            u32::from(target_precision),
2316            RoundingStrategy::MidpointNearestEven,
2317        );
2318        assert_eq!(
2319            decimal_rounded, expected_dec,
2320            "Decimal rounding sanity check failed for {input}"
2321        );
2322
2323        let mantissa = dec.mantissa();
2324        let scale = dec.scale() as u8;
2325        let excess = u32::from(scale.saturating_sub(target_precision));
2326        if excess > 0 {
2327            let rounded = bankers_round(mantissa, excess);
2328
2329            // Reconstruct expected mantissa at target precision
2330            let expected_mantissa = expected_dec.mantissa();
2331            let expected_scale = expected_dec.scale() as u8;
2332            let scale_diff = u32::from(target_precision.saturating_sub(expected_scale));
2333            let normalized_expected = expected_mantissa * 10i128.pow(scale_diff);
2334
2335            assert_eq!(
2336                rounded, normalized_expected,
2337                "bankers_round disagrees with Decimal for {input} at precision {target_precision}"
2338            );
2339        }
2340    }
2341}
2342
2343#[cfg(test)]
2344mod correct_raw_tests {
2345    use rstest::rstest;
2346
2347    use super::*;
2348
2349    // All cases use precision = FIXED_PRECISION - 1 so the scale is 10 in both
2350    // standard-precision and high-precision modes.
2351
2352    #[rstest]
2353    #[case(0, 0)]
2354    #[case(10, 10)] // Already a multiple
2355    #[case(14, 10)] // Rounds down
2356    #[case(15, 20)] // Half rounds up
2357    #[case(16, 20)] // Rounds up
2358    #[case(u64::MAX, u64::MAX - 5)] // Rounding up would overflow; rounds down instead
2359    fn test_correct_raw_u64(#[case] raw: u64, #[case] expected: u64) {
2360        assert_eq!(correct_raw_u64(raw, FIXED_PRECISION - 1), expected);
2361    }
2362
2363    #[rstest]
2364    #[case(0, 0)]
2365    #[case(14, 10)]
2366    #[case(15, 20)]
2367    #[case(-14, -10)] // Rounds toward zero
2368    #[case(-15, -20)] // Half rounds away from zero
2369    #[case(-16, -20)] // Rounds away from zero
2370    #[case(i64::MAX, i64::MAX - 7)] // Rounding up would overflow; rounds down instead
2371    #[case(i64::MIN, i64::MIN + 8)] // Rounding down would overflow; rounds toward zero instead
2372    fn test_correct_raw_i64(#[case] raw: i64, #[case] expected: i64) {
2373        assert_eq!(correct_raw_i64(raw, FIXED_PRECISION - 1), expected);
2374    }
2375
2376    #[rstest]
2377    #[case(0, 0)]
2378    #[case(14, 10)]
2379    #[case(15, 20)]
2380    #[case(u128::MAX, u128::MAX - 5)] // Rounding up would overflow; rounds down instead
2381    fn test_correct_raw_u128(#[case] raw: u128, #[case] expected: u128) {
2382        assert_eq!(correct_raw_u128(raw, FIXED_PRECISION - 1), expected);
2383    }
2384
2385    #[rstest]
2386    #[case(0, 0)]
2387    #[case(14, 10)]
2388    #[case(15, 20)]
2389    #[case(-15, -20)]
2390    #[case(i128::MAX, i128::MAX - 7)] // Rounding up would overflow; rounds down instead
2391    #[case(i128::MIN, i128::MIN + 8)] // Rounding down would overflow; rounds toward zero instead
2392    fn test_correct_raw_i128(#[case] raw: i128, #[case] expected: i128) {
2393        assert_eq!(correct_raw_i128(raw, FIXED_PRECISION - 1), expected);
2394    }
2395
2396    #[rstest]
2397    fn test_correct_raw_identity_at_max_precision() {
2398        assert_eq!(correct_raw_u64(12_345, FIXED_PRECISION), 12_345);
2399        assert_eq!(correct_raw_i64(-12_345, FIXED_PRECISION), -12_345);
2400        assert_eq!(correct_raw_u128(12_345, FIXED_PRECISION), 12_345);
2401        assert_eq!(correct_raw_i128(-12_345, FIXED_PRECISION), -12_345);
2402    }
2403}
2404
2405#[cfg(test)]
2406mod checked_mul_div_tests {
2407    #[cfg(feature = "defi")]
2408    use alloy_primitives::U256;
2409    use proptest::{prelude::*, test_runner::Config as ProptestConfig};
2410    use rstest::rstest;
2411
2412    use super::{FIXED_SCALAR_RAW, checked_mul_div_fixed};
2413    use crate::types::quantity::QuantityRaw;
2414
2415    #[rstest]
2416    fn test_checked_mul_div_fixed_exact_boundaries() {
2417        let scalar = FIXED_SCALAR_RAW;
2418
2419        assert_eq!(checked_mul_div_fixed(0, QuantityRaw::MAX), Some(0));
2420        assert_eq!(checked_mul_div_fixed(QuantityRaw::MAX, 0), Some(0));
2421        assert_eq!(checked_mul_div_fixed(scalar, scalar), Some(scalar));
2422        assert_eq!(
2423            checked_mul_div_fixed(scalar - 1, scalar - 1),
2424            Some(scalar - 2)
2425        );
2426        assert_eq!(
2427            checked_mul_div_fixed(scalar + 1, scalar + 1),
2428            Some(scalar + 2)
2429        );
2430        assert_eq!(
2431            checked_mul_div_fixed(QuantityRaw::MAX, scalar),
2432            Some(QuantityRaw::MAX)
2433        );
2434        assert_eq!(
2435            checked_mul_div_fixed(scalar, QuantityRaw::MAX),
2436            Some(QuantityRaw::MAX)
2437        );
2438        assert_eq!(checked_mul_div_fixed(QuantityRaw::MAX, scalar + 1), None);
2439        assert_eq!(checked_mul_div_fixed(scalar + 1, QuantityRaw::MAX), None);
2440    }
2441
2442    #[cfg(not(feature = "high-precision"))]
2443    proptest! {
2444        #![proptest_config(ProptestConfig::with_cases(4_096))]
2445
2446        #[rstest]
2447        fn prop_checked_mul_div_fixed_matches_u128_full_range(
2448            lhs in any::<QuantityRaw>(),
2449            rhs in any::<QuantityRaw>(),
2450        ) {
2451            let expected = u128::from(lhs)
2452                .checked_mul(u128::from(rhs))
2453                .map(|product| product / u128::from(FIXED_SCALAR_RAW))
2454                .and_then(|result| QuantityRaw::try_from(result).ok());
2455
2456            prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), expected);
2457        }
2458
2459        #[rstest]
2460        fn prop_checked_mul_div_fixed_matches_u128_final_fit(
2461            (lhs, rhs) in standard_final_fit_strategy(),
2462        ) {
2463            let expected =
2464                u128::from(lhs) * u128::from(rhs) / u128::from(FIXED_SCALAR_RAW);
2465            let expected = QuantityRaw::try_from(expected).expect("strategy result fits u64");
2466
2467            prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), Some(expected));
2468        }
2469
2470        #[rstest]
2471        fn prop_checked_mul_div_fixed_avoids_u64_phantom_overflow(
2472            (lhs, rhs, expected) in standard_phantom_overflow_strategy(),
2473        ) {
2474            prop_assert!(lhs.checked_mul(rhs).is_none());
2475            prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), Some(expected));
2476        }
2477    }
2478
2479    #[cfg(feature = "high-precision")]
2480    proptest! {
2481        #![proptest_config(ProptestConfig::with_cases(4_096))]
2482
2483        #[rstest]
2484        fn prop_checked_mul_div_fixed_matches_u128_ordinary(
2485            (lhs, rhs) in high_precision_ordinary_strategy(),
2486        ) {
2487            let expected = lhs
2488                .checked_mul(rhs)
2489                .expect("ordinary strategy product fits u128")
2490                / FIXED_SCALAR_RAW;
2491
2492            prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), Some(expected));
2493        }
2494
2495        #[rstest]
2496        fn prop_checked_mul_div_fixed_avoids_u128_phantom_overflow(
2497            (lhs, rhs, expected) in high_precision_phantom_overflow_strategy(),
2498        ) {
2499            prop_assert!(lhs.checked_mul(rhs).is_none());
2500            prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), Some(expected));
2501        }
2502
2503        #[rstest]
2504        fn prop_checked_mul_div_fixed_handles_remainders_after_u128_overflow(
2505            rhs in high_precision_remainder_overflow_strategy(),
2506        ) {
2507            let lhs = 2 * FIXED_SCALAR_RAW - 1;
2508            let expected = 2 * rhs - rhs.div_ceil(FIXED_SCALAR_RAW);
2509
2510            prop_assert!(lhs.checked_mul(rhs).is_none());
2511            prop_assert_ne!(lhs % FIXED_SCALAR_RAW, 0);
2512            prop_assert_ne!(rhs % FIXED_SCALAR_RAW, 0);
2513            prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), Some(expected));
2514            prop_assert_eq!(checked_mul_div_fixed(rhs, lhs), Some(expected));
2515        }
2516
2517        #[rstest]
2518        fn prop_checked_mul_div_fixed_is_commutative(
2519            lhs in any::<QuantityRaw>(),
2520            rhs in any::<QuantityRaw>(),
2521        ) {
2522            prop_assert_eq!(
2523                checked_mul_div_fixed(lhs, rhs),
2524                checked_mul_div_fixed(rhs, lhs)
2525            );
2526        }
2527    }
2528
2529    #[cfg(feature = "defi")]
2530    proptest! {
2531        #![proptest_config(ProptestConfig::with_cases(4_096))]
2532
2533        #[rstest]
2534        fn prop_checked_mul_div_raw_matches_u256_full_range(
2535            lhs in any::<QuantityRaw>(),
2536            rhs in any::<QuantityRaw>(),
2537            precision in 16_u32..=18,
2538        ) {
2539            let scalar = 10_u128.pow(precision);
2540            let expected = U256::from(lhs) * U256::from(rhs) / U256::from(scalar);
2541            let expected = QuantityRaw::try_from(expected).ok();
2542            prop_assert_eq!(super::checked_mul_div_raw(lhs, rhs, scalar), expected);
2543        }
2544
2545        #[rstest]
2546        fn prop_checked_mul_div_fixed_matches_u256_full_range(
2547            lhs in any::<QuantityRaw>(),
2548            rhs in any::<QuantityRaw>(),
2549        ) {
2550            let expected = U256::from(lhs)
2551                .checked_mul(U256::from(rhs))
2552                .expect("u128 product fits U256")
2553                / U256::from(FIXED_SCALAR_RAW);
2554            let expected = QuantityRaw::try_from(expected).ok();
2555
2556            prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), expected);
2557        }
2558    }
2559
2560    #[cfg(not(feature = "high-precision"))]
2561    fn standard_final_fit_strategy() -> impl Strategy<Value = (QuantityRaw, QuantityRaw)> {
2562        let scalar = FIXED_SCALAR_RAW;
2563
2564        (0_u64..=1_000, 0_u64..=1_000, 0_u64..scalar, 0_u64..scalar).prop_map(
2565            move |(lhs_whole, rhs_whole, lhs_remainder, rhs_remainder)| {
2566                (
2567                    lhs_whole * scalar + lhs_remainder,
2568                    rhs_whole * scalar + rhs_remainder,
2569                )
2570            },
2571        )
2572    }
2573
2574    #[cfg(not(feature = "high-precision"))]
2575    fn standard_phantom_overflow_strategy()
2576    -> impl Strategy<Value = (QuantityRaw, QuantityRaw, QuantityRaw)> {
2577        let scalar = FIXED_SCALAR_RAW;
2578
2579        (8_000_000_000_u64..=9_000_000_000, 0_u64..scalar).prop_map(
2580            move |(lhs_whole, rhs_remainder)| {
2581                let lhs = lhs_whole * scalar;
2582                let rhs = scalar + rhs_remainder;
2583                (lhs, rhs, lhs_whole * rhs)
2584            },
2585        )
2586    }
2587
2588    #[cfg(feature = "high-precision")]
2589    fn high_precision_ordinary_strategy() -> impl Strategy<Value = (QuantityRaw, QuantityRaw)> {
2590        let scalar = FIXED_SCALAR_RAW;
2591
2592        (
2593            0_u128..=1_000,
2594            0_u128..=1_000,
2595            0_u128..scalar,
2596            0_u128..scalar,
2597        )
2598            .prop_map(
2599                move |(lhs_whole, rhs_whole, lhs_remainder, rhs_remainder)| {
2600                    (
2601                        lhs_whole * scalar + lhs_remainder,
2602                        rhs_whole * scalar + rhs_remainder,
2603                    )
2604                },
2605            )
2606    }
2607
2608    #[cfg(feature = "high-precision")]
2609    fn high_precision_phantom_overflow_strategy()
2610    -> impl Strategy<Value = (QuantityRaw, QuantityRaw, QuantityRaw)> {
2611        let scalar = FIXED_SCALAR_RAW;
2612
2613        (10_000_u128..=1_000_000, 1_000_u128..=10_000, 0_u128..scalar).prop_map(
2614            move |(lhs_whole, rhs_whole, rhs_remainder)| {
2615                let lhs = lhs_whole * scalar;
2616                let rhs = rhs_whole * scalar + rhs_remainder;
2617                (lhs, rhs, lhs_whole * rhs)
2618            },
2619        )
2620    }
2621
2622    #[cfg(feature = "high-precision")]
2623    fn high_precision_remainder_overflow_strategy() -> impl Strategy<Value = QuantityRaw> {
2624        let lhs = 2 * FIXED_SCALAR_RAW - 1;
2625        let min = QuantityRaw::MAX / lhs + 1;
2626
2627        (min..=QuantityRaw::MAX / 2).prop_filter("rhs remainder is nonzero", |rhs| {
2628            rhs % FIXED_SCALAR_RAW != 0
2629        })
2630    }
2631}