Skip to main content

nodedb_query/expr/
binary.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Binary-operator evaluation on `Value` operands.
4
5use rust_decimal::Decimal;
6
7use nodedb_types::Value;
8
9use crate::value_ops::{
10    coerced_eq, is_truthy, partial_compare_values, to_value_number, value_to_display_string,
11    value_to_f64,
12};
13
14use super::eval::EvalError;
15use super::types::BinaryOp;
16
17/// Coerce a `Value` to `Decimal` for precise arithmetic.
18///
19/// - `Decimal`: identity
20/// - `Integer`: exact conversion via `Decimal::from`
21/// - `Float`: best-effort via `Decimal::try_from` (preserves fractional digits
22///   rather than compounding f64 ULP error through further f64 arithmetic)
23/// - Other types: `None`
24fn value_to_decimal(v: &Value) -> Option<Decimal> {
25    match v {
26        Value::Decimal(d) => Some(*d),
27        Value::Integer(i) => Some(Decimal::from(*i)),
28        Value::Float(f) => Decimal::try_from(*f).ok(),
29        _ => None,
30    }
31}
32
33/// Apply a Decimal arithmetic operation, returning `Value::Null` on overflow
34/// and `Err(EvalError::DivisionByZero)` when `/` or `%` see a zero divisor.
35/// The zero check happens before `checked_div`/
36/// `checked_rem` are called because those methods return `None` for both
37/// div-by-zero *and* overflow, and only the former is a `22012` error —
38/// overflow keeps folding to `Value::Null` exactly as before.
39fn decimal_arith(a: Decimal, op: BinaryOp, b: Decimal) -> Result<Value, EvalError> {
40    let result = match op {
41        BinaryOp::Add => a.checked_add(b),
42        BinaryOp::Sub => a.checked_sub(b),
43        BinaryOp::Mul => a.checked_mul(b),
44        BinaryOp::Div => {
45            if b.is_zero() {
46                return Err(EvalError::DivisionByZero);
47            }
48            a.checked_div(b)
49        }
50        BinaryOp::Mod => {
51            if b.is_zero() {
52                return Err(EvalError::DivisionByZero);
53            }
54            a.checked_rem(b)
55        }
56        _ => return Ok(Value::Null),
57    };
58    Ok(result.map(Value::Decimal).unwrap_or(Value::Null))
59}
60
61pub(super) fn eval_binary_op(
62    left: &Value,
63    op: BinaryOp,
64    right: &Value,
65) -> Result<Value, EvalError> {
66    match op {
67        // Arithmetic: prefer Decimal when either operand is Decimal to avoid f64 drift.
68        BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Mod => {
69            let left_is_decimal = matches!(left, Value::Decimal(_));
70            let right_is_decimal = matches!(right, Value::Decimal(_));
71            if left_is_decimal || right_is_decimal {
72                return match (value_to_decimal(left), value_to_decimal(right)) {
73                    (Some(a), Some(b)) => decimal_arith(a, op, b),
74                    _ => Ok(Value::Null),
75                };
76            }
77            // Both operands are non-Decimal: use f64 path.
78            match op {
79                BinaryOp::Add => Ok(
80                    match (value_to_f64(left, true), value_to_f64(right, true)) {
81                        (Some(a), Some(b)) => to_value_number(a + b),
82                        _ => Value::Null,
83                    },
84                ),
85                BinaryOp::Sub => Ok(
86                    match (value_to_f64(left, true), value_to_f64(right, true)) {
87                        (Some(a), Some(b)) => to_value_number(a - b),
88                        _ => Value::Null,
89                    },
90                ),
91                BinaryOp::Mul => Ok(
92                    match (value_to_f64(left, true), value_to_f64(right, true)) {
93                        (Some(a), Some(b)) => to_value_number(a * b),
94                        _ => Value::Null,
95                    },
96                ),
97                BinaryOp::Div => match (value_to_f64(left, true), value_to_f64(right, true)) {
98                    (Some(a), Some(b)) => {
99                        if b == 0.0 {
100                            Err(EvalError::DivisionByZero)
101                        } else {
102                            Ok(to_value_number(a / b))
103                        }
104                    }
105                    _ => Ok(Value::Null),
106                },
107                BinaryOp::Mod => match (value_to_f64(left, true), value_to_f64(right, true)) {
108                    (Some(a), Some(b)) => {
109                        if b == 0.0 {
110                            Err(EvalError::DivisionByZero)
111                        } else {
112                            Ok(to_value_number(a % b))
113                        }
114                    }
115                    _ => Ok(Value::Null),
116                },
117                _ => Ok(Value::Null),
118            }
119        }
120        BinaryOp::Concat => {
121            let ls = value_to_display_string(left);
122            let rs = value_to_display_string(right);
123            Ok(Value::String(format!("{ls}{rs}")))
124        }
125        BinaryOp::Eq => Ok(Value::Bool(coerced_eq(left, right))),
126        BinaryOp::NotEq => Ok(Value::Bool(!coerced_eq(left, right))),
127        BinaryOp::Gt => Ok(Value::Bool(matches!(
128            partial_compare_values(left, right),
129            Some(std::cmp::Ordering::Greater)
130        ))),
131        BinaryOp::GtEq => Ok(Value::Bool(matches!(
132            partial_compare_values(left, right),
133            Some(std::cmp::Ordering::Greater | std::cmp::Ordering::Equal)
134        ))),
135        BinaryOp::Lt => Ok(Value::Bool(matches!(
136            partial_compare_values(left, right),
137            Some(std::cmp::Ordering::Less)
138        ))),
139        BinaryOp::LtEq => Ok(Value::Bool(matches!(
140            partial_compare_values(left, right),
141            Some(std::cmp::Ordering::Less | std::cmp::Ordering::Equal)
142        ))),
143        BinaryOp::And => Ok(Value::Bool(is_truthy(left) && is_truthy(right))),
144        BinaryOp::Or => Ok(Value::Bool(is_truthy(left) || is_truthy(right))),
145    }
146}