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}