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