Skip to main content

qubit_argument/argument/
numeric_argument.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//! Ownership-preserving validation for primitive numeric arguments.
9
10use std::ops::{
11    Bound,
12    RangeBounds,
13};
14
15use crate::argument::{
16    ArgumentBound,
17    ArgumentError,
18    ArgumentErrorKind,
19    ArgumentResult,
20    ArgumentValue,
21    ComparisonConstraint,
22    RangeConstraint,
23    sealed::Sealed,
24};
25
26/// Restricts numeric validation to supported primitive numeric values.
27///
28/// Implementations provide the type's zero value, an exact structured error
29/// representation, and NaN detection. The trait is private so arbitrary
30/// partially ordered caller types cannot opt into numeric validation.
31trait NumericValue: Sealed + Copy + PartialOrd {
32    /// Returns the zero value for this primitive numeric type.
33    fn zero() -> Self;
34
35    /// Captures this value without losing integer magnitude or floating bits.
36    fn to_argument_value(self) -> ArgumentValue;
37
38    /// Returns whether this value is a floating-point NaN.
39    ///
40    /// Integer implementations always return `false`.
41    fn is_nan(self) -> bool;
42}
43
44/// Implements primitive numeric conversion and non-NaN behavior for integers.
45macro_rules! impl_numeric_value_for_integer {
46    ($($numeric_type:ty),+ $(,)?) => {
47        $(
48            impl NumericValue for $numeric_type {
49                /// Returns integer zero.
50                #[inline]
51                fn zero() -> Self {
52                    0
53                }
54
55                /// Captures the integer without losing its value.
56                #[inline]
57                fn to_argument_value(self) -> ArgumentValue {
58                    ArgumentValue::from(self)
59                }
60
61                /// Reports that an integer can never be NaN.
62                #[inline]
63                fn is_nan(self) -> bool {
64                    false
65                }
66            }
67        )+
68    };
69}
70
71impl_numeric_value_for_integer!(i8, i16, i32, i64, i128, isize);
72impl_numeric_value_for_integer!(u8, u16, u32, u64, u128, usize);
73
74impl NumericValue for f32 {
75    /// Returns positive floating-point zero.
76    #[inline]
77    fn zero() -> Self {
78        0.0
79    }
80
81    /// Captures the exact IEEE 754 bit pattern of this value.
82    #[inline]
83    fn to_argument_value(self) -> ArgumentValue {
84        ArgumentValue::from(self)
85    }
86
87    /// Returns whether this value is NaN.
88    #[inline]
89    fn is_nan(self) -> bool {
90        self.is_nan()
91    }
92}
93
94impl NumericValue for f64 {
95    /// Returns positive floating-point zero.
96    #[inline]
97    fn zero() -> Self {
98        0.0
99    }
100
101    /// Captures the exact IEEE 754 bit pattern of this value.
102    #[inline]
103    fn to_argument_value(self) -> ArgumentValue {
104        ArgumentValue::from(self)
105    }
106
107    /// Returns whether this value is NaN.
108    #[inline]
109    fn is_nan(self) -> bool {
110        self.is_nan()
111    }
112}
113
114/// Validates primitive numeric arguments while preserving their values.
115///
116/// Every successful method returns the original value without conversion or
117/// normalization. Failures contain structured comparison or range data, and
118/// every method rejects floating-point NaN values with
119/// [`ArgumentErrorKind::NotANumber`].
120///
121/// The trait is sealed and implemented only for primitive numeric types.
122pub trait NumericArgument: Sealed + Sized {
123    /// Requires this value to equal zero.
124    ///
125    /// Success returns the original value without cloning. A nonzero value
126    /// returns [`ArgumentErrorKind::Comparison`] with an `EqualTo(0)`
127    /// constraint at `path`; NaN returns [`ArgumentErrorKind::NotANumber`].
128    fn require_zero(self, path: &str) -> ArgumentResult<Self>;
129
130    /// Requires this value not to equal zero.
131    ///
132    /// Success returns the original value without cloning. Zero returns
133    /// [`ArgumentErrorKind::Comparison`] with a `NotEqualTo(0)` constraint at
134    /// `path`; NaN returns [`ArgumentErrorKind::NotANumber`].
135    fn require_non_zero(self, path: &str) -> ArgumentResult<Self>;
136
137    /// Requires this value to be strictly greater than zero.
138    ///
139    /// Success returns the original value without cloning. A value that is not
140    /// positive returns [`ArgumentErrorKind::Comparison`] with a
141    /// `GreaterThan(0)` constraint at `path`; NaN returns
142    /// [`ArgumentErrorKind::NotANumber`].
143    fn require_positive(self, path: &str) -> ArgumentResult<Self>;
144
145    /// Requires this value to be greater than or equal to zero.
146    ///
147    /// Success returns the original value without cloning. A negative value
148    /// returns [`ArgumentErrorKind::Comparison`] with an `AtLeast(0)`
149    /// constraint at `path`; NaN returns [`ArgumentErrorKind::NotANumber`].
150    fn require_non_negative(self, path: &str) -> ArgumentResult<Self>;
151
152    /// Requires this value to be strictly less than zero.
153    ///
154    /// Success returns the original value without cloning. A value that is not
155    /// negative returns [`ArgumentErrorKind::Comparison`] with a `LessThan(0)`
156    /// constraint at `path`; NaN returns [`ArgumentErrorKind::NotANumber`].
157    fn require_negative(self, path: &str) -> ArgumentResult<Self>;
158
159    /// Requires this value to be less than or equal to zero.
160    ///
161    /// Success returns the original value without cloning. A positive value
162    /// returns [`ArgumentErrorKind::Comparison`] with an `AtMost(0)`
163    /// constraint at `path`; NaN returns [`ArgumentErrorKind::NotANumber`].
164    fn require_non_positive(self, path: &str) -> ArgumentResult<Self>;
165
166    /// Requires this value to be strictly less than `bound`.
167    ///
168    /// Success returns the original value without cloning. An unsatisfied
169    /// comparison returns [`ArgumentErrorKind::Comparison`] with a `LessThan`
170    /// constraint at `path`; a NaN value or bound returns
171    /// [`ArgumentErrorKind::NotANumber`].
172    fn require_less_than(self, path: &str, bound: Self)
173    -> ArgumentResult<Self>;
174
175    /// Requires this value to be less than or equal to `bound`.
176    ///
177    /// Success returns the original value without cloning. An unsatisfied
178    /// comparison returns [`ArgumentErrorKind::Comparison`] with an `AtMost`
179    /// constraint at `path`; a NaN value or bound returns
180    /// [`ArgumentErrorKind::NotANumber`].
181    fn require_at_most(self, path: &str, bound: Self) -> ArgumentResult<Self>;
182
183    /// Requires this value to be strictly greater than `bound`.
184    ///
185    /// Success returns the original value without cloning. An unsatisfied
186    /// comparison returns [`ArgumentErrorKind::Comparison`] with a
187    /// `GreaterThan` constraint at `path`; a NaN value or bound returns
188    /// [`ArgumentErrorKind::NotANumber`].
189    fn require_greater_than(
190        self,
191        path: &str,
192        bound: Self,
193    ) -> ArgumentResult<Self>;
194
195    /// Requires this value to be greater than or equal to `bound`.
196    ///
197    /// Success returns the original value without cloning. An unsatisfied
198    /// comparison returns [`ArgumentErrorKind::Comparison`] with an `AtLeast`
199    /// constraint at `path`; a NaN value or bound returns
200    /// [`ArgumentErrorKind::NotANumber`].
201    fn require_at_least(self, path: &str, bound: Self) -> ArgumentResult<Self>;
202
203    /// Requires this value to lie within `range`.
204    ///
205    /// Standard inclusive, exclusive, and unbounded [`RangeBounds`] are
206    /// supported. The range structure is validated before this value: reversed
207    /// endpoints and equal endpoints with either endpoint excluded return
208    /// [`ArgumentErrorKind::InvalidRangeConstraint`]. NaN endpoints or values
209    /// return [`ArgumentErrorKind::NotANumber`].
210    /// An otherwise out-of-range value returns [`ArgumentErrorKind::Range`].
211    /// The range's start and end accessors are each called exactly once, and
212    /// all checks use that owned endpoint snapshot.
213    /// Successful validation returns the original value without cloning.
214    fn require_in_range<R>(self, path: &str, range: R) -> ArgumentResult<Self>
215    where
216        R: RangeBounds<Self>;
217}
218
219impl<T> NumericArgument for T
220where
221    T: NumericValue,
222{
223    /// Requires equality with zero and preserves the original value.
224    #[inline]
225    fn require_zero(self, path: &str) -> ArgumentResult<Self> {
226        let zero = T::zero();
227        validate_comparison(
228            self,
229            path,
230            zero,
231            ComparisonConstraint::EqualTo(zero.to_argument_value()),
232            |actual, bound| actual == bound,
233        )
234    }
235
236    /// Requires inequality with zero and preserves the original value.
237    #[inline]
238    fn require_non_zero(self, path: &str) -> ArgumentResult<Self> {
239        let zero = T::zero();
240        validate_comparison(
241            self,
242            path,
243            zero,
244            ComparisonConstraint::NotEqualTo(zero.to_argument_value()),
245            |actual, bound| actual != bound,
246        )
247    }
248
249    /// Requires a value strictly greater than zero.
250    #[inline]
251    fn require_positive(self, path: &str) -> ArgumentResult<Self> {
252        let zero = T::zero();
253        validate_comparison(
254            self,
255            path,
256            zero,
257            ComparisonConstraint::GreaterThan(zero.to_argument_value()),
258            |actual, bound| actual > bound,
259        )
260    }
261
262    /// Requires a value greater than or equal to zero.
263    #[inline]
264    fn require_non_negative(self, path: &str) -> ArgumentResult<Self> {
265        let zero = T::zero();
266        validate_comparison(
267            self,
268            path,
269            zero,
270            ComparisonConstraint::AtLeast(zero.to_argument_value()),
271            |actual, bound| actual >= bound,
272        )
273    }
274
275    /// Requires a value strictly less than zero.
276    #[inline]
277    fn require_negative(self, path: &str) -> ArgumentResult<Self> {
278        let zero = T::zero();
279        validate_comparison(
280            self,
281            path,
282            zero,
283            ComparisonConstraint::LessThan(zero.to_argument_value()),
284            |actual, bound| actual < bound,
285        )
286    }
287
288    /// Requires a value less than or equal to zero.
289    #[inline]
290    fn require_non_positive(self, path: &str) -> ArgumentResult<Self> {
291        let zero = T::zero();
292        validate_comparison(
293            self,
294            path,
295            zero,
296            ComparisonConstraint::AtMost(zero.to_argument_value()),
297            |actual, bound| actual <= bound,
298        )
299    }
300
301    /// Requires a value strictly less than the supplied bound.
302    #[inline]
303    fn require_less_than(
304        self,
305        path: &str,
306        bound: Self,
307    ) -> ArgumentResult<Self> {
308        validate_comparison(
309            self,
310            path,
311            bound,
312            ComparisonConstraint::LessThan(bound.to_argument_value()),
313            |actual, bound| actual < bound,
314        )
315    }
316
317    /// Requires a value less than or equal to the supplied bound.
318    #[inline]
319    fn require_at_most(self, path: &str, bound: Self) -> ArgumentResult<Self> {
320        validate_comparison(
321            self,
322            path,
323            bound,
324            ComparisonConstraint::AtMost(bound.to_argument_value()),
325            |actual, bound| actual <= bound,
326        )
327    }
328
329    /// Requires a value strictly greater than the supplied bound.
330    #[inline]
331    fn require_greater_than(
332        self,
333        path: &str,
334        bound: Self,
335    ) -> ArgumentResult<Self> {
336        validate_comparison(
337            self,
338            path,
339            bound,
340            ComparisonConstraint::GreaterThan(bound.to_argument_value()),
341            |actual, bound| actual > bound,
342        )
343    }
344
345    /// Requires a value greater than or equal to the supplied bound.
346    #[inline]
347    fn require_at_least(self, path: &str, bound: Self) -> ArgumentResult<Self> {
348        validate_comparison(
349            self,
350            path,
351            bound,
352            ComparisonConstraint::AtLeast(bound.to_argument_value()),
353            |actual, bound| actual >= bound,
354        )
355    }
356
357    /// Validates the range structure before checking and returning this value.
358    #[inline]
359    fn require_in_range<R>(self, path: &str, range: R) -> ArgumentResult<Self>
360    where
361        R: RangeBounds<Self>,
362    {
363        let (lower_bound, upper_bound) = snapshot_range_bounds(&range);
364        let constraint = capture_range_constraint(lower_bound, upper_bound);
365        validate_range_structure(path, lower_bound, upper_bound, &constraint)?;
366        validate_not_nan(path, self)?;
367        if range_contains(lower_bound, upper_bound, self) {
368            Ok(self)
369        } else {
370            Err(ArgumentError::new(
371                path,
372                ArgumentErrorKind::Range {
373                    actual: self.to_argument_value(),
374                    constraint,
375                },
376            ))
377        }
378    }
379}
380
381/// Rejects a NaN numeric value or constraint at the supplied argument path.
382///
383/// `value` is inspected without normalization. Integer values always succeed;
384/// floating-point NaN values return `ArgumentErrorKind::NotANumber` at `path`.
385fn validate_not_nan<T>(path: &str, value: T) -> ArgumentResult<()>
386where
387    T: NumericValue,
388{
389    if value.is_nan() {
390        Err(ArgumentError::new(path, ArgumentErrorKind::NotANumber))
391    } else {
392        Ok(())
393    }
394}
395
396/// Applies one comparison and returns the unchanged numeric value on success.
397///
398/// `actual` and `bound` are checked for NaN before `predicate` is evaluated.
399/// If the predicate returns `false`, the error records `actual` and the exact
400/// supplied `constraint` at `path`.
401fn validate_comparison<T, F>(
402    actual: T,
403    path: &str,
404    bound: T,
405    constraint: ComparisonConstraint,
406    predicate: F,
407) -> ArgumentResult<T>
408where
409    T: NumericValue,
410    F: FnOnce(T, T) -> bool,
411{
412    validate_not_nan(path, actual)?;
413    validate_not_nan(path, bound)?;
414    if predicate(actual, bound) {
415        Ok(actual)
416    } else {
417        Err(ArgumentError::new(
418            path,
419            ArgumentErrorKind::Comparison {
420                actual: actual.to_argument_value(),
421                constraint,
422            },
423        ))
424    }
425}
426
427/// Copies a borrowed standard-library bound into an owned bound.
428///
429/// Included and excluded endpoints are copied exactly; an unbounded endpoint
430/// remains unbounded.
431fn copy_range_bound<T>(bound: Bound<&T>) -> Bound<T>
432where
433    T: NumericValue,
434{
435    match bound {
436        Bound::Unbounded => Bound::Unbounded,
437        Bound::Included(value) => Bound::Included(*value),
438        Bound::Excluded(value) => Bound::Excluded(*value),
439    }
440}
441
442/// Reads each endpoint from `range` once and returns an owned snapshot.
443///
444/// The lower and upper endpoint accessors are each invoked exactly once. The
445/// returned values are reused for constraint construction, validation, and
446/// membership checks.
447fn snapshot_range_bounds<T, R>(range: &R) -> (Bound<T>, Bound<T>)
448where
449    T: NumericValue,
450    R: RangeBounds<T>,
451{
452    let lower_bound = copy_range_bound(range.start_bound());
453    let upper_bound = copy_range_bound(range.end_bound());
454    (lower_bound, upper_bound)
455}
456
457/// Captures an owned standard-library bound as a structured argument bound.
458///
459/// Included and excluded endpoints are converted without losing numeric bits;
460/// an unbounded endpoint remains unbounded.
461fn capture_argument_bound<T>(bound: Bound<T>) -> ArgumentBound
462where
463    T: NumericValue,
464{
465    match bound {
466        Bound::Unbounded => ArgumentBound::Unbounded,
467        Bound::Included(value) => {
468            ArgumentBound::Included(value.to_argument_value())
469        }
470        Bound::Excluded(value) => {
471            ArgumentBound::Excluded(value.to_argument_value())
472        }
473    }
474}
475
476/// Captures both owned endpoints without validating their relationship.
477///
478/// The returned constraint preserves inclusive, exclusive, unbounded, and
479/// floating-point bit-pattern details exactly.
480fn capture_range_constraint<T>(
481    lower_bound: Bound<T>,
482    upper_bound: Bound<T>,
483) -> RangeConstraint
484where
485    T: NumericValue,
486{
487    RangeConstraint::new(
488        capture_argument_bound(lower_bound),
489        capture_argument_bound(upper_bound),
490    )
491}
492
493/// Validates that two snapshotted bounds form a non-empty numeric interval.
494///
495/// Endpoint NaNs return `NotANumber`. Reversed endpoints, or equal endpoints
496/// where either bound is excluded, return `InvalidRangeConstraint` containing
497/// a clone of `constraint`. Unbounded sides require no ordering comparison.
498fn validate_range_structure<T>(
499    path: &str,
500    lower_bound: Bound<T>,
501    upper_bound: Bound<T>,
502    constraint: &RangeConstraint,
503) -> ArgumentResult<()>
504where
505    T: NumericValue,
506{
507    validate_range_bound_not_nan(path, lower_bound)?;
508    validate_range_bound_not_nan(path, upper_bound)?;
509
510    let is_valid = match (lower_bound, upper_bound) {
511        (Bound::Unbounded, _) | (_, Bound::Unbounded) => return Ok(()),
512        (Bound::Included(lower), Bound::Included(upper)) => lower <= upper,
513        (Bound::Included(lower), Bound::Excluded(upper))
514        | (Bound::Excluded(lower), Bound::Included(upper))
515        | (Bound::Excluded(lower), Bound::Excluded(upper)) => lower < upper,
516    };
517    if is_valid {
518        Ok(())
519    } else {
520        Err(ArgumentError::new(
521            path,
522            ArgumentErrorKind::InvalidRangeConstraint {
523                constraint: constraint.clone(),
524            },
525        ))
526    }
527}
528
529/// Rejects a NaN endpoint while accepting unbounded and ordinary endpoints.
530///
531/// A NaN included or excluded endpoint returns `NotANumber` at `path`.
532fn validate_range_bound_not_nan<T>(
533    path: &str,
534    bound: Bound<T>,
535) -> ArgumentResult<()>
536where
537    T: NumericValue,
538{
539    match bound {
540        Bound::Included(value) | Bound::Excluded(value) => {
541            validate_not_nan(path, value)
542        }
543        Bound::Unbounded => Ok(()),
544    }
545}
546
547/// Returns whether `actual` satisfies two snapshotted bounds.
548///
549/// This helper assumes the bounds and all values were already checked for NaN.
550/// It uses comparisons only and performs no endpoint arithmetic.
551fn range_contains<T>(
552    lower_bound: Bound<T>,
553    upper_bound: Bound<T>,
554    actual: T,
555) -> bool
556where
557    T: NumericValue,
558{
559    let satisfies_lower = match lower_bound {
560        Bound::Unbounded => true,
561        Bound::Included(lower) => actual >= lower,
562        Bound::Excluded(lower) => actual > lower,
563    };
564    let satisfies_upper = match upper_bound {
565        Bound::Unbounded => true,
566        Bound::Included(upper) => actual <= upper,
567        Bound::Excluded(upper) => actual < upper,
568    };
569    satisfies_lower && satisfies_upper
570}