qubit_value/value/value_numeric_comparison.rs
1// =============================================================================
2// Copyright (c) 2025 - 2026 Haixing Hu.
3//
4// SPDX-License-Identifier: Apache-2.0
5//
6// Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Policy-driven numeric comparison for [`super::Value`].
9
10use std::cmp::Ordering;
11
12use qubit_datatype::{
13 NumberRef,
14 NumericComparisonPolicy,
15};
16
17use super::{
18 Value,
19 ValueRepr,
20};
21use crate::NumericComparisonError;
22
23/// Projects one stored value according to its type-table numeric strategy.
24macro_rules! project_number_ref {
25 (number_copy, $value:expr) => {
26 Some(NumberRef::from(*$value))
27 };
28 (number_ref, $value:expr) => {
29 Some(NumberRef::from($value))
30 };
31 (not_number, $value:expr) => {{
32 let _ = $value;
33 None
34 }};
35}
36
37/// Generates the exhaustive numeric projection from the value type table.
38macro_rules! value_number_ref_match {
39 ($value:expr; $(([$($cfg:meta),*], $variant:ident, $type:ty, $data_type:expr, $materialization:ident, $json_class:ident, $number_projection:ident, $value_doc:literal, $multi_doc:literal)),+ $(,)?) => {
40 match &$value.repr {
41 ValueRepr::Unset(_) => None,
42 $(
43 $(#[$cfg])*
44 ValueRepr::$variant(value) => {
45 project_number_ref!($number_projection, value)
46 }
47 )+
48 }
49 };
50}
51
52impl Value {
53 /// Tests whether this value is a concrete floating-point NaN.
54 ///
55 /// Non-floating-point values and unset values return `false`.
56 ///
57 /// # Returns
58 ///
59 /// `true` only for concrete `Float32` or `Float64` NaN values.
60 #[inline(always)]
61 #[must_use]
62 pub fn is_nan(&self) -> bool {
63 self.as_number_ref().is_some_and(|value| value.is_nan())
64 }
65
66 /// Compares concrete numeric values across representation variants.
67 ///
68 /// This operation is separate from [`PartialEq`]: equality preserves enum
69 /// representation identity, while numeric comparison compares mathematical
70 /// values under an explicit policy.
71 ///
72 /// [`NumericComparisonPolicy::Approximate`] orders primitive infinities
73 /// separately. When a finite primitive float participates, it attempts to
74 /// project both operands to finite `f64` values; if either operand cannot
75 /// be projected that way, comparison falls back to the exact path.
76 /// Projected comparison is pair-dependent and not transitive across
77 /// mixed representations. Do not use it to implement [`Ord`], sort or
78 /// group values, or construct ordered-map or ordered-set keys. Use
79 /// [`NumericComparisonPolicy::Exact`] for deterministic ordering.
80 ///
81 /// Validation is deterministic: missing operands are checked from left to
82 /// right, followed by concrete operand types from left to right, and then
83 /// NaN positions.
84 ///
85 /// # Parameters
86 ///
87 /// * `other` - Right numeric operand.
88 /// * `policy` - Exact or approximate numeric comparison policy.
89 ///
90 /// # Returns
91 ///
92 /// The mathematical ordering of the two concrete, non-NaN numeric
93 /// operands.
94 ///
95 /// # Errors
96 ///
97 /// Returns [`NumericComparisonError::LeftMissing`] or
98 /// [`NumericComparisonError::RightMissing`] when the corresponding operand
99 /// is unset. Returns [`NumericComparisonError::LeftNotNumeric`] or
100 /// [`NumericComparisonError::RightNotNumeric`] when the corresponding
101 /// concrete operand is not numeric. Returns
102 /// [`NumericComparisonError::LeftNaN`],
103 /// [`NumericComparisonError::RightNaN`], or
104 /// [`NumericComparisonError::BothNaN`] according to the position of NaN
105 /// operands. Missing operands are checked left-to-right, then concrete
106 /// operand types are checked left-to-right, and finally NaN positions are
107 /// classified. After these checks the lower-level comparator must be able
108 /// to order the remaining numeric operands.
109 pub fn numeric_cmp(
110 &self,
111 other: &Self,
112 policy: NumericComparisonPolicy,
113 ) -> Result<Ordering, NumericComparisonError> {
114 if let ValueRepr::Unset(declared) = &self.repr {
115 return Err(NumericComparisonError::LeftMissing {
116 declared: *declared,
117 });
118 }
119 if let ValueRepr::Unset(declared) = &other.repr {
120 return Err(NumericComparisonError::RightMissing {
121 declared: *declared,
122 });
123 }
124
125 let left = self.as_number_ref().ok_or_else(|| {
126 NumericComparisonError::LeftNotNumeric {
127 actual: self.data_type(),
128 }
129 })?;
130 let right = other.as_number_ref().ok_or_else(|| {
131 NumericComparisonError::RightNotNumeric {
132 actual: other.data_type(),
133 }
134 })?;
135
136 match (left.is_nan(), right.is_nan()) {
137 (true, true) => return Err(NumericComparisonError::BothNaN),
138 (true, false) => return Err(NumericComparisonError::LeftNaN),
139 (false, true) => return Err(NumericComparisonError::RightNaN),
140 (false, false) => {}
141 }
142
143 match left.compare(right, policy) {
144 Some(ordering) => Ok(ordering),
145 None => unreachable!(
146 "validated non-NaN numeric values must be orderable"
147 ),
148 }
149 }
150
151 /// Borrows this value as a lower-level numeric representation.
152 ///
153 /// # Returns
154 ///
155 /// A borrowed numeric representation for every concrete numeric variant,
156 /// or `None` for unset and non-numeric variants.
157 fn as_number_ref(&self) -> Option<NumberRef<'_>> {
158 for_each_value_type!(value_number_ref_match, self)
159 }
160}