Skip to main content

icydb_core/value/ops/
numeric.rs

1//! Module: value::ops::numeric
2//!
3//! Responsibility: representation-local numeric conversion and comparison.
4//! Does not own: predicate-level numeric policy or planner coercion legality.
5//! Boundary: low-level helpers consumed by database numeric semantics.
6
7use crate::{
8    traits::Repr,
9    types::{Decimal, NumericValue},
10    value::{Value, semantics},
11};
12use std::cmp::Ordering;
13
14const F64_SAFE_I64: i64 = 1i64 << 53;
15const F64_SAFE_U64: u64 = 1u64 << 53;
16const F64_SAFE_I128: i128 = 1i128 << 53;
17const F64_SAFE_U128: u128 = 1u128 << 53;
18
19///
20/// NumericRepr
21///
22/// Represents the comparable numeric form available for one `Value`. Decimal
23/// is preferred when exact conversion is available; otherwise a lossless `f64`
24/// is used only for values inside the well-defined integer safety envelope.
25///
26
27enum NumericRepr {
28    Decimal(Decimal),
29    F64(f64),
30    None,
31}
32
33fn numeric_repr(value: &Value) -> NumericRepr {
34    // Numeric comparison eligibility is registry-authoritative.
35    if !semantics::supports_numeric_coercion(value) {
36        return NumericRepr::None;
37    }
38
39    if let Some(decimal) = to_decimal(value) {
40        return NumericRepr::Decimal(decimal);
41    }
42    if let Some(float) = to_f64_lossless(value) {
43        return NumericRepr::F64(float);
44    }
45    NumericRepr::None
46}
47
48fn to_decimal(value: &Value) -> Option<Decimal> {
49    match value {
50        Value::Decimal(value) => value.try_to_decimal(),
51        Value::Duration(value) => value.try_to_decimal(),
52        Value::Float64(value) => value.try_to_decimal(),
53        Value::Float32(value) => value.try_to_decimal(),
54        Value::Int64(value) => value.try_to_decimal(),
55        Value::Int128(value) => value.try_to_decimal(),
56        Value::IntBig(value) => value.try_to_decimal(),
57        Value::Timestamp(value) => value.try_to_decimal(),
58        Value::Nat64(value) => value.try_to_decimal(),
59        Value::Nat128(value) => value.try_to_decimal(),
60        Value::NatBig(value) => value.try_to_decimal(),
61
62        _ => None,
63    }
64}
65
66// Internal numeric coercion helper for aggregate arithmetic.
67pub(crate) fn to_numeric_decimal(value: &Value) -> Option<Decimal> {
68    to_decimal(value)
69}
70
71// This helper only returns `Some` inside the integer range exactly representable
72// by `f64`, or for finite float wrappers that already own their precision.
73#[expect(clippy::cast_precision_loss)]
74fn to_f64_lossless(value: &Value) -> Option<f64> {
75    match value {
76        Value::Duration(value) if value.repr() <= F64_SAFE_U64 => Some(value.repr() as f64),
77        Value::Float64(value) => Some(value.get()),
78        Value::Float32(value) => Some(f64::from(value.get())),
79        Value::Int64(value) if (-F64_SAFE_I64..=F64_SAFE_I64).contains(value) => {
80            Some(*value as f64)
81        }
82        Value::Int128(value) if (-F64_SAFE_I128..=F64_SAFE_I128).contains(value) => {
83            Some(*value as f64)
84        }
85        Value::IntBig(value) => value.to_i128().and_then(|integer| {
86            (-F64_SAFE_I128..=F64_SAFE_I128)
87                .contains(&integer)
88                .then_some(integer as f64)
89        }),
90        Value::Timestamp(value) if (-F64_SAFE_I64..=F64_SAFE_I64).contains(&value.repr()) => {
91            Some(value.repr() as f64)
92        }
93        Value::Nat64(value) if *value <= F64_SAFE_U64 => Some(*value as f64),
94        Value::Nat128(value) if *value <= F64_SAFE_U128 => Some(*value as f64),
95        Value::NatBig(value) => value
96            .to_u128()
97            .and_then(|integer| (integer <= F64_SAFE_U128).then_some(integer as f64)),
98
99        _ => None,
100    }
101}
102
103/// Compare two runtime values under value-local numeric coercion semantics.
104#[must_use]
105fn cmp_numeric(left: &Value, right: &Value) -> Option<Ordering> {
106    if !semantics::supports_numeric_coercion(left) || !semantics::supports_numeric_coercion(right) {
107        return None;
108    }
109
110    match (numeric_repr(left), numeric_repr(right)) {
111        (NumericRepr::Decimal(left), NumericRepr::Decimal(right)) => left.partial_cmp(&right),
112        (NumericRepr::F64(left), NumericRepr::F64(right)) => left.partial_cmp(&right),
113        _ => None,
114    }
115}
116
117/// Compare two values after exact decimal numeric coercion.
118#[must_use]
119pub(crate) fn compare_decimal_order(left: &Value, right: &Value) -> Option<Ordering> {
120    if !semantics::supports_numeric_coercion(left) || !semantics::supports_numeric_coercion(right) {
121        return None;
122    }
123
124    let left = to_decimal(left)?;
125    let right = to_decimal(right)?;
126
127    left.partial_cmp(&right)
128}
129
130impl Value {
131    // Internal numeric coercion helper for aggregate arithmetic.
132    pub(crate) fn to_numeric_decimal(&self) -> Option<Decimal> {
133        to_numeric_decimal(self)
134    }
135
136    /// Compare two runtime values under value-local numeric coercion semantics.
137    ///
138    /// Database execution code should use `db::numeric` helpers as the
139    /// canonical runtime boundary; this method remains the representation-local
140    /// comparison primitive that those higher-level helpers are tested against.
141    #[must_use]
142    pub fn cmp_numeric(&self, other: &Self) -> Option<Ordering> {
143        cmp_numeric(self, other)
144    }
145}