Skip to main content

reifydb_value/value/number/
arithmetic.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use crate::value::{
5	Value,
6	decimal::Decimal,
7	is::IsNumber,
8	number::{
9		promote::Promote,
10		safe::{add::SafeAdd, div::SafeDiv, mul::SafeMul, remainder::SafeRemainder, sub::SafeSub},
11	},
12	to_value::ToValue,
13	value_type::ValueType,
14};
15
16fn arith_type(v: &Value) -> ValueType {
17	match v {
18		Value::None {
19			inner,
20		} => inner.clone(),
21		other => other.get_type(),
22	}
23}
24
25fn none_inner(l: &Value, r: &Value) -> Option<ValueType> {
26	let l_none = matches!(l, Value::None { .. });
27	let r_none = matches!(r, Value::None { .. });
28	if !l_none && !r_none {
29		return None;
30	}
31	Some(ValueType::promote(arith_type(l), arith_type(r)))
32}
33
34fn value_is_zero(v: &Value) -> bool {
35	match v {
36		Value::Int1(x) => SafeDiv::is_zero(x),
37		Value::Int2(x) => SafeDiv::is_zero(x),
38		Value::Int4(x) => SafeDiv::is_zero(x),
39		Value::Int8(x) => SafeDiv::is_zero(x),
40		Value::Int16(x) => SafeDiv::is_zero(x),
41		Value::Uint1(x) => SafeDiv::is_zero(x),
42		Value::Uint2(x) => SafeDiv::is_zero(x),
43		Value::Uint4(x) => SafeDiv::is_zero(x),
44		Value::Uint8(x) => SafeDiv::is_zero(x),
45		Value::Uint16(x) => SafeDiv::is_zero(x),
46		Value::Float4(x) => SafeDiv::is_zero(&x.value()),
47		Value::Float8(x) => SafeDiv::is_zero(&x.value()),
48		Value::Decimal(x) => SafeDiv::is_zero(x),
49		_ => false,
50	}
51}
52
53macro_rules! gen_helpers {
54	($checked:ident, $sat:ident, $wrap:ident, $trait:ident, $cm:ident, $sm:ident, $wm:ident) => {
55		fn $checked<L, R>(l: &L, r: &R) -> Option<Value>
56		where
57			L: Promote<R>,
58			R: IsNumber,
59			<L as Promote<R>>::Output: $trait,
60		{
61			let (a, b) = l.checked_promote(r)?;
62			a.$cm(&b).map(|o| o.to_value())
63		}
64
65		fn $sat<L, R>(l: &L, r: &R) -> Value
66		where
67			L: Promote<R>,
68			R: IsNumber,
69			<L as Promote<R>>::Output: $trait,
70		{
71			let (a, b) = l.saturating_promote(r);
72			a.$sm(&b).to_value()
73		}
74
75		fn $wrap<L, R>(l: &L, r: &R) -> Value
76		where
77			L: Promote<R>,
78			R: IsNumber,
79			<L as Promote<R>>::Output: $trait,
80		{
81			let (a, b) = l.wrapping_promote(r);
82			a.$wm(&b).to_value()
83		}
84	};
85}
86
87gen_helpers!(v_checked_add, v_sat_add, v_wrap_add, SafeAdd, checked_add, saturating_add, wrapping_add);
88gen_helpers!(v_checked_sub, v_sat_sub, v_wrap_sub, SafeSub, checked_sub, saturating_sub, wrapping_sub);
89gen_helpers!(v_checked_mul, v_sat_mul, v_wrap_mul, SafeMul, checked_mul, saturating_mul, wrapping_mul);
90gen_helpers!(v_checked_rem, v_sat_rem, v_wrap_rem, SafeRemainder, checked_rem, saturating_rem, wrapping_rem);
91
92trait DivToValue: Sized {
93	fn checked_div_to_value(&self, r: &Self) -> Option<Value>;
94	fn saturating_div_to_value(&self, r: &Self) -> Value;
95	fn wrapping_div_to_value(&self, r: &Self) -> Value;
96}
97
98macro_rules! impl_div_to_value_via_decimal {
99	($($t:ty),*) => {
100		$(
101			impl DivToValue for $t {
102				fn checked_div_to_value(&self, r: &Self) -> Option<Value> {
103					Decimal::from(self.clone()).checked_div(&Decimal::from(r.clone())).map(Value::Decimal)
104				}
105				fn saturating_div_to_value(&self, r: &Self) -> Value {
106					Value::Decimal(Decimal::from(self.clone()).saturating_div(&Decimal::from(r.clone())))
107				}
108				fn wrapping_div_to_value(&self, r: &Self) -> Value {
109					Value::Decimal(Decimal::from(self.clone()).wrapping_div(&Decimal::from(r.clone())))
110				}
111			}
112		)*
113	};
114}
115
116impl_div_to_value_via_decimal!(i128, u128);
117
118impl DivToValue for f64 {
119	fn checked_div_to_value(&self, r: &Self) -> Option<Value> {
120		self.checked_div(r).map(|o| o.to_value())
121	}
122	fn saturating_div_to_value(&self, r: &Self) -> Value {
123		self.saturating_div(r).to_value()
124	}
125	fn wrapping_div_to_value(&self, r: &Self) -> Value {
126		self.wrapping_div(r).to_value()
127	}
128}
129
130impl DivToValue for Decimal {
131	fn checked_div_to_value(&self, r: &Self) -> Option<Value> {
132		self.checked_div(r).map(Value::Decimal)
133	}
134	fn saturating_div_to_value(&self, r: &Self) -> Value {
135		Value::Decimal(self.saturating_div(r))
136	}
137	fn wrapping_div_to_value(&self, r: &Self) -> Value {
138		Value::Decimal(self.wrapping_div(r))
139	}
140}
141
142fn v_checked_div<L, R>(l: &L, r: &R) -> Option<Value>
143where
144	L: Promote<R>,
145	R: IsNumber,
146	<L as Promote<R>>::Output: DivToValue,
147{
148	let (a, b) = l.checked_promote(r)?;
149	a.checked_div_to_value(&b)
150}
151
152fn v_sat_div<L, R>(l: &L, r: &R) -> Value
153where
154	L: Promote<R>,
155	R: IsNumber,
156	<L as Promote<R>>::Output: DivToValue,
157{
158	let (a, b) = l.saturating_promote(r);
159	a.saturating_div_to_value(&b)
160}
161
162fn v_wrap_div<L, R>(l: &L, r: &R) -> Value
163where
164	L: Promote<R>,
165	R: IsNumber,
166	<L as Promote<R>>::Output: DivToValue,
167{
168	let (a, b) = l.wrapping_promote(r);
169	a.wrapping_div_to_value(&b)
170}
171
172macro_rules! right_arms {
173	($r:expr, $la:expr, $op:ident, $fallback:expr) => {
174		match $r {
175			Value::Int1(b) => $op($la, b),
176			Value::Int2(b) => $op($la, b),
177			Value::Int4(b) => $op($la, b),
178			Value::Int8(b) => $op($la, b),
179			Value::Int16(b) => $op($la, b),
180			Value::Uint1(b) => $op($la, b),
181			Value::Uint2(b) => $op($la, b),
182			Value::Uint4(b) => $op($la, b),
183			Value::Uint8(b) => $op($la, b),
184			Value::Uint16(b) => $op($la, b),
185			Value::Decimal(b) => $op($la, b),
186			Value::Float4(b) => $op($la, &b.value()),
187			Value::Float8(b) => $op($la, &b.value()),
188			_ => $fallback,
189		}
190	};
191}
192
193macro_rules! value_arith_dispatch {
194	($l:expr, $r:expr, $op:ident, $fallback:expr) => {
195		match $l {
196			Value::Int1(a) => right_arms!($r, a, $op, $fallback),
197			Value::Int2(a) => right_arms!($r, a, $op, $fallback),
198			Value::Int4(a) => right_arms!($r, a, $op, $fallback),
199			Value::Int8(a) => right_arms!($r, a, $op, $fallback),
200			Value::Int16(a) => right_arms!($r, a, $op, $fallback),
201			Value::Uint1(a) => right_arms!($r, a, $op, $fallback),
202			Value::Uint2(a) => right_arms!($r, a, $op, $fallback),
203			Value::Uint4(a) => right_arms!($r, a, $op, $fallback),
204			Value::Uint8(a) => right_arms!($r, a, $op, $fallback),
205			Value::Uint16(a) => right_arms!($r, a, $op, $fallback),
206			Value::Decimal(a) => right_arms!($r, a, $op, $fallback),
207			Value::Float4(a) => right_arms!($r, &a.value(), $op, $fallback),
208			Value::Float8(a) => right_arms!($r, &a.value(), $op, $fallback),
209			_ => $fallback,
210		}
211	};
212}
213
214macro_rules! impl_value_safe {
215	($trait:ident, $checked:ident, $sat:ident, $wrap:ident, $hc:ident, $hs:ident, $hw:ident) => {
216		impl $trait for Value {
217			fn $checked(&self, r: &Self) -> Option<Self> {
218				if let Some(inner) = none_inner(self, r) {
219					return Some(Value::None {
220						inner,
221					});
222				}
223				value_arith_dispatch!(self, r, $hc, None)
224			}
225
226			fn $sat(&self, r: &Self) -> Self {
227				if let Some(inner) = none_inner(self, r) {
228					return Value::None {
229						inner,
230					};
231				}
232				value_arith_dispatch!(
233					self,
234					r,
235					$hs,
236					Value::None {
237						inner: ValueType::Any
238					}
239				)
240			}
241
242			fn $wrap(&self, r: &Self) -> Self {
243				if let Some(inner) = none_inner(self, r) {
244					return Value::None {
245						inner,
246					};
247				}
248				value_arith_dispatch!(
249					self,
250					r,
251					$hw,
252					Value::None {
253						inner: ValueType::Any
254					}
255				)
256			}
257		}
258	};
259}
260
261impl_value_safe!(SafeAdd, checked_add, saturating_add, wrapping_add, v_checked_add, v_sat_add, v_wrap_add);
262impl_value_safe!(SafeSub, checked_sub, saturating_sub, wrapping_sub, v_checked_sub, v_sat_sub, v_wrap_sub);
263impl_value_safe!(SafeMul, checked_mul, saturating_mul, wrapping_mul, v_checked_mul, v_sat_mul, v_wrap_mul);
264
265impl SafeDiv for Value {
266	fn checked_div(&self, r: &Self) -> Option<Self> {
267		if let Some(inner) = none_inner(self, r) {
268			return Some(Value::None {
269				inner,
270			});
271		}
272		value_arith_dispatch!(self, r, v_checked_div, None)
273	}
274
275	fn saturating_div(&self, r: &Self) -> Self {
276		if let Some(inner) = none_inner(self, r) {
277			return Value::None {
278				inner,
279			};
280		}
281		value_arith_dispatch!(
282			self,
283			r,
284			v_sat_div,
285			Value::None {
286				inner: ValueType::Any
287			}
288		)
289	}
290
291	fn wrapping_div(&self, r: &Self) -> Self {
292		if let Some(inner) = none_inner(self, r) {
293			return Value::None {
294				inner,
295			};
296		}
297		value_arith_dispatch!(
298			self,
299			r,
300			v_wrap_div,
301			Value::None {
302				inner: ValueType::Any
303			}
304		)
305	}
306
307	fn is_zero(&self) -> bool {
308		value_is_zero(self)
309	}
310}
311
312impl SafeRemainder for Value {
313	fn checked_rem(&self, r: &Self) -> Option<Self> {
314		if let Some(inner) = none_inner(self, r) {
315			return Some(Value::None {
316				inner,
317			});
318		}
319		value_arith_dispatch!(self, r, v_checked_rem, None)
320	}
321
322	fn saturating_rem(&self, r: &Self) -> Self {
323		if let Some(inner) = none_inner(self, r) {
324			return Value::None {
325				inner,
326			};
327		}
328		value_arith_dispatch!(
329			self,
330			r,
331			v_sat_rem,
332			Value::None {
333				inner: ValueType::Any
334			}
335		)
336	}
337
338	fn wrapping_rem(&self, r: &Self) -> Self {
339		if let Some(inner) = none_inner(self, r) {
340			return Value::None {
341				inner,
342			};
343		}
344		value_arith_dispatch!(
345			self,
346			r,
347			v_wrap_rem,
348			Value::None {
349				inner: ValueType::Any
350			}
351		)
352	}
353
354	fn is_zero(&self) -> bool {
355		value_is_zero(self)
356	}
357}
358
359#[cfg(test)]
360mod tests {
361	use super::*;
362	use crate::value::{ordered_f32::OrderedF32, ordered_f64::OrderedF64};
363
364	fn int4(v: i32) -> Value {
365		Value::Int4(v)
366	}
367	fn int2(v: i16) -> Value {
368		Value::Int2(v)
369	}
370	fn uint4(v: u32) -> Value {
371		Value::Uint4(v)
372	}
373	fn dec(v: i64) -> Value {
374		Value::Decimal(Decimal::from(v))
375	}
376
377	#[test]
378	fn add_int_pair_promotes_to_int16() {
379		// Integer add/sub/mul promote to the widest fixed type so the operation cannot overflow
380		// the operand width, whatever the declared widths were.
381		assert_eq!(int2(3).checked_add(&int4(4)), Some(Value::Int16(7)));
382		assert_eq!(int4(3).checked_add(&int4(4)), Some(Value::Int16(7)));
383	}
384
385	#[test]
386	fn add_uint_pair_promotes_to_uint16() {
387		assert_eq!(uint4(3).checked_add(&uint4(4)), Some(Value::Uint16(7)));
388	}
389
390	#[test]
391	fn add_float_pair_is_float8() {
392		let out = Value::Float8(OrderedF64::try_from(1.5).unwrap())
393			.checked_add(&Value::Float4(OrderedF32::try_from(2.5f32).unwrap()));
394		assert_eq!(out, Some(Value::Float8(OrderedF64::try_from(4.0).unwrap())));
395	}
396
397	#[test]
398	fn add_decimal_pair_is_decimal() {
399		assert_eq!(dec(3).checked_add(&dec(4)), Some(Value::Decimal(Decimal::from(7i64))));
400	}
401
402	#[test]
403	fn mixed_int_decimal_is_decimal() {
404		assert_eq!(int4(3).checked_add(&dec(4)), Some(Value::Decimal(Decimal::from(7i64))));
405	}
406
407	#[test]
408	fn int_div_is_decimal_exact() {
409		// Integer division promotes to Decimal so 3/2 is 1.5, not a truncated 1.
410		let got = int4(3).checked_div(&int4(2)).unwrap();
411		assert_eq!(got, Value::Decimal(Decimal::from(3i64).checked_div(&Decimal::from(2i64)).unwrap()));
412		assert_ne!(got, Value::Decimal(Decimal::from(1i64)));
413	}
414
415	#[test]
416	fn float_div_stays_float8() {
417		let three = Value::Float8(OrderedF64::try_from(3.0).unwrap());
418		let two = Value::Float8(OrderedF64::try_from(2.0).unwrap());
419		assert_eq!(three.checked_div(&two), Some(Value::Float8(OrderedF64::try_from(1.5).unwrap())));
420	}
421
422	#[test]
423	fn none_propagates_with_promoted_inner() {
424		// A none operand yields Some(Value::None), which is a defined result distinct from the
425		// Option::None used for overflow, and it carries the promoted type.
426		let got = Value::none_of(ValueType::Int4).checked_add(&int4(5));
427		assert_eq!(
428			got,
429			Some(Value::None {
430				inner: ValueType::promote(ValueType::Int4, ValueType::Int4)
431			})
432		);
433		assert!(matches!(got, Some(Value::None { .. })));
434		assert!(matches!(int4(5).checked_add(&Value::none_of(ValueType::Int4)), Some(Value::None { .. })));
435		assert!(matches!(
436			Value::none_of(ValueType::Int4).checked_add(&Value::none_of(ValueType::Int8)),
437			Some(Value::None { .. })
438		));
439	}
440
441	#[test]
442	fn overflow_checked_saturating_wrapping() {
443		// Promotion moves the overflow point out to the i128 limit; past it the three
444		// strategies must differ, or a caller cannot choose its failure mode.
445		let max = Value::Int16(i128::MAX);
446		let one = Value::Int16(1);
447		assert_eq!(max.checked_add(&one), None);
448		assert_eq!(max.saturating_add(&one), Value::Int16(i128::MAX));
449		assert_eq!(max.wrapping_add(&one), Value::Int16(i128::MIN));
450	}
451
452	#[test]
453	fn div_by_zero_is_none() {
454		assert_eq!(int4(3).checked_div(&int4(0)), None);
455		assert_eq!(dec(3).checked_div(&dec(0)), None);
456		let one = Value::Float8(OrderedF64::try_from(1.0).unwrap());
457		let zero = Value::Float8(OrderedF64::try_from(0.0).unwrap());
458		assert_eq!(one.checked_div(&zero), None);
459	}
460
461	#[test]
462	fn is_zero_per_variant() {
463		assert!(SafeDiv::is_zero(&int4(0)));
464		assert!(!SafeDiv::is_zero(&int4(1)));
465		assert!(SafeDiv::is_zero(&dec(0)));
466		assert!(!SafeDiv::is_zero(&dec(5)));
467		assert!(!SafeDiv::is_zero(&Value::none_of(ValueType::Int4)));
468		assert!(!SafeDiv::is_zero(&Value::Boolean(true)));
469	}
470
471	#[test]
472	fn non_numeric_operand_is_none() {
473		assert_eq!(int4(3).checked_add(&Value::Boolean(true)), None);
474		assert_eq!(Value::Utf8("x".into()).checked_add(&int4(3)), None);
475	}
476
477	#[test]
478	fn retraction_invariant_exact_for_integers() {
479		// Window and aggregate accumulators retract by subtracting what they added, so for
480		// non-float operands checked_sub must invert checked_add exactly.
481		let cases = [(int4(100), int4(7)), (int2(30), int4(9)), (uint4(50), uint4(8)), (dec(1000), dec(123))];
482		for (r, x) in cases {
483			let added = r.checked_add(&x).unwrap();
484			let restored = added.checked_sub(&x).unwrap();
485			// Promotion changes the declared width, so the comparison is made after a
486			// second add rather than against the original value.
487			let twice = restored.checked_add(&x).unwrap();
488			assert_eq!(added, twice, "add/sub must invert for {r:?} - {x:?}");
489		}
490	}
491
492	#[test]
493	fn retraction_float_within_tolerance() {
494		// IEEE addition is not exactly invertible, so float retraction is bounded by a
495		// tolerance instead of the exact equality the integer case gets.
496		let r = Value::Float8(OrderedF64::try_from(0.1).unwrap());
497		let x = Value::Float8(OrderedF64::try_from(0.2).unwrap());
498		let restored = r.checked_add(&x).unwrap().checked_sub(&x).unwrap();
499		if let Value::Float8(v) = restored {
500			assert!((v.value() - 0.1).abs() < 1e-9, "float retraction drift too large: {}", v.value());
501		} else {
502			panic!("expected Float8, got {restored:?}");
503		}
504	}
505
506	#[test]
507	fn nan_result_is_none_float8() {
508		// f64::MAX squared overflows to infinity, which checked_mul reports as None rather
509		// than storing a non-finite float.
510		let inf = Value::Float8(OrderedF64::try_from(f64::MAX).unwrap());
511		assert_eq!(inf.checked_mul(&inf), None);
512	}
513}