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