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}