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, compare_values, is_truthy, to_value_number, value_to_display_string, value_to_f64,
11};
12
13use super::types::BinaryOp;
14
15/// Coerce a `Value` to `Decimal` for precise arithmetic.
16///
17/// - `Decimal`: identity
18/// - `Integer`: exact conversion via `Decimal::from`
19/// - `Float`: best-effort via `Decimal::try_from` (preserves fractional digits
20///   rather than compounding f64 ULP error through further f64 arithmetic)
21/// - Other types: `None`
22fn value_to_decimal(v: &Value) -> Option<Decimal> {
23    match v {
24        Value::Decimal(d) => Some(*d),
25        Value::Integer(i) => Some(Decimal::from(*i)),
26        Value::Float(f) => Decimal::try_from(*f).ok(),
27        _ => None,
28    }
29}
30
31/// Apply a Decimal arithmetic operation, returning `Value::Null` on overflow/div-zero.
32fn decimal_arith(a: Decimal, op: BinaryOp, b: Decimal) -> Value {
33    let result = match op {
34        BinaryOp::Add => a.checked_add(b),
35        BinaryOp::Sub => a.checked_sub(b),
36        BinaryOp::Mul => a.checked_mul(b),
37        BinaryOp::Div => a.checked_div(b),
38        BinaryOp::Mod => a.checked_rem(b),
39        _ => return Value::Null,
40    };
41    result.map(Value::Decimal).unwrap_or(Value::Null)
42}
43
44pub(super) fn eval_binary_op(left: &Value, op: BinaryOp, right: &Value) -> Value {
45    match op {
46        // Arithmetic: prefer Decimal when either operand is Decimal to avoid f64 drift.
47        BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Mod => {
48            let left_is_decimal = matches!(left, Value::Decimal(_));
49            let right_is_decimal = matches!(right, Value::Decimal(_));
50            if left_is_decimal || right_is_decimal {
51                match (value_to_decimal(left), value_to_decimal(right)) {
52                    (Some(a), Some(b)) => return decimal_arith(a, op, b),
53                    _ => return Value::Null,
54                }
55            }
56            // Both operands are non-Decimal: use f64 path.
57            match op {
58                BinaryOp::Add => match (value_to_f64(left, true), value_to_f64(right, true)) {
59                    (Some(a), Some(b)) => to_value_number(a + b),
60                    _ => Value::Null,
61                },
62                BinaryOp::Sub => match (value_to_f64(left, true), value_to_f64(right, true)) {
63                    (Some(a), Some(b)) => to_value_number(a - b),
64                    _ => Value::Null,
65                },
66                BinaryOp::Mul => match (value_to_f64(left, true), value_to_f64(right, true)) {
67                    (Some(a), Some(b)) => to_value_number(a * b),
68                    _ => Value::Null,
69                },
70                BinaryOp::Div => match (value_to_f64(left, true), value_to_f64(right, true)) {
71                    (Some(a), Some(b)) => {
72                        if b == 0.0 {
73                            Value::Null
74                        } else {
75                            to_value_number(a / b)
76                        }
77                    }
78                    _ => Value::Null,
79                },
80                BinaryOp::Mod => match (value_to_f64(left, true), value_to_f64(right, true)) {
81                    (Some(a), Some(b)) => {
82                        if b == 0.0 {
83                            Value::Null
84                        } else {
85                            to_value_number(a % b)
86                        }
87                    }
88                    _ => Value::Null,
89                },
90                _ => Value::Null,
91            }
92        }
93        BinaryOp::Concat => {
94            let ls = value_to_display_string(left);
95            let rs = value_to_display_string(right);
96            Value::String(format!("{ls}{rs}"))
97        }
98        BinaryOp::Eq => Value::Bool(coerced_eq(left, right)),
99        BinaryOp::NotEq => Value::Bool(!coerced_eq(left, right)),
100        BinaryOp::Gt => Value::Bool(compare_values(left, right) == std::cmp::Ordering::Greater),
101        BinaryOp::GtEq => {
102            let c = compare_values(left, right);
103            Value::Bool(c == std::cmp::Ordering::Greater || c == std::cmp::Ordering::Equal)
104        }
105        BinaryOp::Lt => Value::Bool(compare_values(left, right) == std::cmp::Ordering::Less),
106        BinaryOp::LtEq => {
107            let c = compare_values(left, right);
108            Value::Bool(c == std::cmp::Ordering::Less || c == std::cmp::Ordering::Equal)
109        }
110        BinaryOp::And => Value::Bool(is_truthy(left) && is_truthy(right)),
111        BinaryOp::Or => Value::Bool(is_truthy(left) || is_truthy(right)),
112    }
113}