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}