Skip to main content

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}