Skip to main content

qubit_budget/value/
big_integer_limits.rs

1// =============================================================================
2//    Copyright (c) 2026 Haixing Hu.
3//
4//    SPDX-License-Identifier: Apache-2.0
5//
6//    Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Bounded magnitude and significant decimal digit checks for `BigInt`.
9
10use num_bigint::BigInt;
11
12use super::BigIntegerLimitsBuilder;
13use crate::resource::LimitExceededError;
14use crate::resource::MeasuredBudgetError;
15use crate::resource::Observation;
16use crate::resource::ResourceLimit;
17use crate::resource::ResourceQuantity;
18
19/// Optional point limits for one arbitrary-precision integer.
20///
21/// # Type Parameters
22///
23/// * `R` - Caller-defined resource identity retained by limits and errors.
24/// * `Q` - Exact unsigned quantity used for measurements and accounting.
25///
26/// # Examples
27///
28/// ```
29/// use num_bigint::BigInt;
30/// use qubit_budget::BigIntegerLimits;
31/// use qubit_budget::ResourceLimit;
32///
33/// let limits = BigIntegerLimits::builder()
34///     .magnitude_bits_limit(ResourceLimit::new("integer bits", 8_u64))
35///     .build();
36/// limits.check(&BigInt::from(255_u16)).expect("255 needs eight bits");
37/// ```
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
39pub struct BigIntegerLimits<R, Q = u64>
40where
41    Q: ResourceQuantity,
42{
43    /// Optional inclusive maximum for the unsigned magnitude bit length.
44    max_magnitude_bits: Option<ResourceLimit<R, Q>>,
45    /// Optional inclusive maximum for significant decimal digits.
46    max_significant_decimal_digits: Option<ResourceLimit<R, Q>>,
47}
48
49impl<R, Q> BigIntegerLimits<R, Q>
50where
51    Q: ResourceQuantity,
52{
53    /// Creates limits with no configured integer bounds.
54    ///
55    /// # Returns
56    ///
57    /// Creates limits with no configured integer bounds.
58    #[inline]
59    #[must_use = "the integer limit check result must be handled"]
60    pub const fn new() -> Self {
61        Self {
62            max_magnitude_bits: None,
63            max_significant_decimal_digits: None,
64        }
65    }
66
67    /// Creates a builder for integer limits.
68    ///
69    /// # Returns
70    ///
71    /// Creates a builder for integer limits.
72    #[inline]
73    #[must_use]
74    pub const fn builder() -> BigIntegerLimitsBuilder<R, Q> {
75        BigIntegerLimitsBuilder::new()
76    }
77
78    /// Converts these limits into a builder for further configuration.
79    ///
80    /// # Returns
81    ///
82    /// Converts these limits into a builder for further configuration.
83    #[inline]
84    #[must_use]
85    pub const fn into_builder(self) -> BigIntegerLimitsBuilder<R, Q> {
86        BigIntegerLimitsBuilder::from_limits(self)
87    }
88
89    /// Returns the configured magnitude bit-length limit, if any.
90    ///
91    /// # Returns
92    ///
93    /// Returns the configured magnitude bit-length limit, if any.
94    ///
95    /// `None` indicates that the corresponding limit or budget dimension is
96    /// unconfigured.
97    #[must_use]
98    #[inline(always)]
99    pub const fn magnitude_bits_limit(&self) -> Option<&ResourceLimit<R, Q>> {
100        self.max_magnitude_bits.as_ref()
101    }
102
103    /// Returns the configured significant decimal digit limit, if any.
104    ///
105    /// # Returns
106    ///
107    /// Returns the configured significant decimal digit limit, if any.
108    ///
109    /// `None` indicates that the corresponding limit or budget dimension is
110    /// unconfigured.
111    #[must_use]
112    #[inline(always)]
113    pub const fn significant_decimal_digits_limit(&self) -> Option<&ResourceLimit<R, Q>> {
114        self.max_significant_decimal_digits.as_ref()
115    }
116
117    /// Checks one integer without formatting clearly oversized values.
118    ///
119    /// Values near the decimal boundary are formatted once. Clearly oversized
120    /// values report a conservative lower bound instead of allocating a
121    /// decimal string proportional to the input magnitude.
122    ///
123    /// # Parameters
124    ///
125    /// * `value` - Arbitrary-precision integer whose magnitude and decimal
126    ///   digit counts are compared with the configured limits.
127    ///
128    /// # Returns
129    ///
130    /// `Ok(())` when the operation completes successfully.
131    ///
132    /// # Errors
133    ///
134    /// Returns [`MeasuredBudgetError`] when a native measurement cannot fit `Q`
135    /// or a configured limit rejects it.
136    #[inline]
137    #[must_use = "the integer limit check result must be handled"]
138    pub fn check(&self, value: &BigInt) -> Result<(), MeasuredBudgetError<R, Q>>
139    where
140        R: Clone,
141    {
142        if let Some(limit) = self.max_magnitude_bits.as_ref() {
143            let bits = Q::try_from_u64(value.bits())
144                .map_err(|source| MeasuredBudgetError::quantity(limit.resource().clone(), source))?;
145            limit.check(bits).map_err(MeasuredBudgetError::from)?;
146        }
147        if let Some(limit) = self.max_significant_decimal_digits.as_ref() {
148            check_decimal_digits(limit, value)?;
149        }
150        Ok(())
151    }
152
153    /// Replaces the magnitude-bit limit during builder composition.
154    ///
155    /// # Parameters
156    ///
157    /// * `limit` - Resource-bound magnitude bit-length limit to install.
158    #[inline(always)]
159    pub(super) fn set_magnitude_bits_limit(&mut self, limit: ResourceLimit<R, Q>) {
160        self.max_magnitude_bits = Some(limit);
161    }
162
163    /// Replaces the decimal-digit limit during builder composition.
164    ///
165    /// # Parameters
166    ///
167    /// * `limit` - Resource-bound significant decimal-digit limit to install.
168    #[inline(always)]
169    pub(super) fn set_significant_decimal_digits_limit(&mut self, limit: ResourceLimit<R, Q>) {
170        self.max_significant_decimal_digits = Some(limit);
171    }
172}
173
174impl<R, Q> Default for BigIntegerLimits<R, Q>
175where
176    Q: ResourceQuantity,
177{
178    /// Creates unconfigured integer limits.
179    ///
180    /// # Returns
181    ///
182    /// Creates unconfigured integer limits.
183    #[inline]
184    fn default() -> Self {
185        Self::new()
186    }
187}
188
189/// Counts and validates significant decimal digits without allocating text.
190///
191/// # Type Parameters
192///
193/// * `R` - Caller-defined resource identity retained by limits and errors.
194/// * `Q` - Exact unsigned quantity used for measurements and accounting.
195///
196/// # Parameters
197///
198/// * `limit` - Resource-bound inclusive decimal-digit maximum.
199/// * `value` - Integer whose unsigned magnitude is measured.
200///
201/// # Returns
202///
203/// `Ok(())` when the significant digit count fits the configured maximum.
204///
205/// # Errors
206///
207/// Returns [`MeasuredBudgetError`] when the exact digit count cannot fit `Q`
208/// or exceeds `limit`.
209fn check_decimal_digits<R, Q>(limit: &ResourceLimit<R, Q>, value: &BigInt) -> Result<(), MeasuredBudgetError<R, Q>>
210where
211    R: Clone,
212    Q: ResourceQuantity,
213{
214    let bits = value.bits();
215    if bits == 0 {
216        return Ok(());
217    }
218
219    let maximum = limit.maximum();
220    let bits =
221        Q::try_from_u64(bits).map_err(|source| MeasuredBudgetError::quantity(limit.resource().clone(), source))?;
222    let low_bits = maximum
223        .checked_add(maximum)
224        .and_then(|value| value.checked_add(maximum));
225    if low_bits.is_some_and(|low_bits| bits <= low_bits) {
226        return Ok(());
227    }
228    let high_bits = low_bits.and_then(|value| value.checked_add(maximum));
229    if high_bits.is_some_and(|high_bits| bits > high_bits) {
230        let Some(observed) = maximum.checked_add(Q::ONE) else {
231            return Ok(());
232        };
233        return Err(LimitExceededError {
234            resource: limit.resource().clone(),
235            observed: Observation::AtLeast(observed),
236            maximum,
237        }
238        .into());
239    }
240
241    let text = value.to_str_radix(10);
242    let digits = text.strip_prefix('-').unwrap_or(&text).len();
243    let digits =
244        Q::try_from_usize(digits).map_err(|source| MeasuredBudgetError::quantity(limit.resource().clone(), source))?;
245    if digits > maximum {
246        Err(LimitExceededError {
247            resource: limit.resource().clone(),
248            observed: Observation::Exact(digits),
249            maximum,
250        }
251        .into())
252    } else {
253        Ok(())
254    }
255}