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