icydb_core/value/ops/
numeric.rs1use 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
19enum NumericRepr {
28 Decimal(Decimal),
29 F64(f64),
30 None,
31}
32
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
42#[cfg(any(test, feature = "query"))]
43pub(crate) enum NumericArithmeticError {
44 Overflow,
45 NotRepresentable,
46}
47
48fn numeric_repr(value: &Value) -> NumericRepr {
49 if !semantics::supports_numeric_coercion(value) {
51 return NumericRepr::None;
52 }
53
54 if let Some(decimal) = to_decimal(value) {
55 return NumericRepr::Decimal(decimal);
56 }
57 if let Some(float) = to_f64_lossless(value) {
58 return NumericRepr::F64(float);
59 }
60 NumericRepr::None
61}
62
63fn to_decimal(value: &Value) -> Option<Decimal> {
64 match value {
65 Value::Decimal(value) => value.try_to_decimal(),
66 Value::Duration(value) => value.try_to_decimal(),
67 Value::Float64(value) => value.try_to_decimal(),
68 Value::Float32(value) => value.try_to_decimal(),
69 Value::Int64(value) => value.try_to_decimal(),
70 Value::Int128(value) => value.try_to_decimal(),
71 Value::IntBig(value) => value.try_to_decimal(),
72 Value::Timestamp(value) => value.try_to_decimal(),
73 Value::Nat64(value) => value.try_to_decimal(),
74 Value::Nat128(value) => value.try_to_decimal(),
75 Value::NatBig(value) => value.try_to_decimal(),
76
77 _ => None,
78 }
79}
80
81pub(crate) fn to_numeric_decimal(value: &Value) -> Option<Decimal> {
83 to_decimal(value)
84}
85
86#[expect(clippy::cast_precision_loss)]
89fn to_f64_lossless(value: &Value) -> Option<f64> {
90 match value {
91 Value::Duration(value) if value.repr() <= F64_SAFE_U64 => Some(value.repr() as f64),
92 Value::Float64(value) => Some(value.get()),
93 Value::Float32(value) => Some(f64::from(value.get())),
94 Value::Int64(value) if (-F64_SAFE_I64..=F64_SAFE_I64).contains(value) => {
95 Some(*value as f64)
96 }
97 Value::Int128(value) if (-F64_SAFE_I128..=F64_SAFE_I128).contains(value) => {
98 Some(*value as f64)
99 }
100 Value::IntBig(value) => value.to_i128().and_then(|integer| {
101 (-F64_SAFE_I128..=F64_SAFE_I128)
102 .contains(&integer)
103 .then_some(integer as f64)
104 }),
105 Value::Timestamp(value) if (-F64_SAFE_I64..=F64_SAFE_I64).contains(&value.repr()) => {
106 Some(value.repr() as f64)
107 }
108 Value::Nat64(value) if *value <= F64_SAFE_U64 => Some(*value as f64),
109 Value::Nat128(value) if *value <= F64_SAFE_U128 => Some(*value as f64),
110 Value::NatBig(value) => value
111 .to_u128()
112 .and_then(|integer| (integer <= F64_SAFE_U128).then_some(integer as f64)),
113
114 _ => None,
115 }
116}
117
118#[must_use]
120fn cmp_numeric(left: &Value, right: &Value) -> Option<Ordering> {
121 if !semantics::supports_numeric_coercion(left) || !semantics::supports_numeric_coercion(right) {
122 return None;
123 }
124
125 match (numeric_repr(left), numeric_repr(right)) {
126 (NumericRepr::Decimal(left), NumericRepr::Decimal(right)) => left.partial_cmp(&right),
127 (NumericRepr::F64(left), NumericRepr::F64(right)) => left.partial_cmp(&right),
128 _ => None,
129 }
130}
131
132#[must_use]
134#[cfg(any(test, feature = "query"))]
135pub(crate) fn compare_decimal_order(left: &Value, right: &Value) -> Option<Ordering> {
136 if !semantics::supports_numeric_coercion(left) || !semantics::supports_numeric_coercion(right) {
137 return None;
138 }
139
140 let left = to_decimal(left)?;
141 let right = to_decimal(right)?;
142
143 left.partial_cmp(&right)
144}
145
146#[cfg(any(test, feature = "query"))]
148pub(crate) fn add(left: &Value, right: &Value) -> Result<Option<Decimal>, NumericArithmeticError> {
149 apply_decimal_arithmetic(left, right, Decimal::checked_add, false)
150}
151
152#[cfg(any(test, feature = "query"))]
154pub(crate) fn sub(left: &Value, right: &Value) -> Result<Option<Decimal>, NumericArithmeticError> {
155 apply_decimal_arithmetic(left, right, Decimal::checked_sub, false)
156}
157
158#[cfg(any(test, feature = "query"))]
160pub(crate) fn mul(left: &Value, right: &Value) -> Result<Option<Decimal>, NumericArithmeticError> {
161 apply_decimal_arithmetic(left, right, Decimal::checked_mul, false)
162}
163
164#[cfg(any(test, feature = "query"))]
166pub(crate) fn div(left: &Value, right: &Value) -> Result<Option<Decimal>, NumericArithmeticError> {
167 apply_decimal_arithmetic(left, right, Decimal::checked_div, true)
168}
169
170#[cfg(any(test, feature = "query"))]
171fn apply_decimal_arithmetic(
172 left: &Value,
173 right: &Value,
174 apply: impl FnOnce(Decimal, Decimal) -> Option<Decimal>,
175 division: bool,
176) -> Result<Option<Decimal>, NumericArithmeticError> {
177 if !semantics::supports_numeric_coercion(left) || !semantics::supports_numeric_coercion(right) {
178 return Ok(None);
179 }
180
181 let Some(left) = to_decimal(left) else {
182 return Ok(None);
183 };
184 let Some(right) = to_decimal(right) else {
185 return Ok(None);
186 };
187 if division && right.is_zero() {
188 return Err(NumericArithmeticError::NotRepresentable);
189 }
190
191 apply(left, right)
192 .map(Some)
193 .ok_or(NumericArithmeticError::Overflow)
194}
195
196impl Value {
197 pub(crate) fn to_numeric_decimal(&self) -> Option<Decimal> {
199 to_numeric_decimal(self)
200 }
201
202 #[must_use]
208 pub fn cmp_numeric(&self, other: &Self) -> Option<Ordering> {
209 cmp_numeric(self, other)
210 }
211}