1#[inline]
20#[must_use]
21#[expect(
22 clippy::cast_possible_truncation,
23 reason = "Intentional for parsing, value range validated"
24)]
25fn clamp_precision_with_log(len: usize, context: &str, input: &str) -> u8 {
26 if len > u8::MAX as usize {
27 log::debug!(
28 "{} precision clamped from {} to {} for input: {}",
29 context,
30 len,
31 u8::MAX,
32 input
33 );
34 }
35 len.min(u8::MAX as usize) as u8
36}
37
38fn precision_from_scientific(s: &str, trim_trailing_zeros: bool, strict: bool) -> Option<u8> {
51 let e_pos = s.find('e')?;
52 let mantissa = &s[..e_pos];
53 let exponent_str = &s[e_pos + 1..];
54
55 let frac_digits = mantissa.split_once('.').map_or(0, |(_, frac)| {
56 if trim_trailing_zeros {
57 frac.trim_end_matches('0').len()
58 } else {
59 frac.len()
60 }
61 });
62
63 let exponent: i64 = if let Ok(v) = exponent_str.parse::<i64>() {
64 v
65 } else {
66 let (digits, is_negative) = exponent_str
67 .strip_prefix('-')
68 .map(|rest| (rest, true))
69 .or_else(|| exponent_str.strip_prefix('+').map(|rest| (rest, false)))
70 .unwrap_or((exponent_str, false));
71
72 if digits.is_empty() {
73 assert!(
74 !strict,
75 "Invalid scientific notation format: missing exponent value"
76 );
77 return None;
78 }
79
80 if digits.chars().all(|c| c.is_ascii_digit()) {
81 return Some(if is_negative { u8::MAX } else { 0 });
82 }
83
84 assert!(
85 !strict,
86 "Invalid scientific notation exponent '{exponent_str}': must be a valid number"
87 );
88 return None;
89 };
90
91 let precision = i64::try_from(frac_digits)
92 .unwrap_or(i64::MAX)
93 .saturating_sub(exponent)
94 .clamp(0, i64::from(u8::MAX));
95
96 #[expect(
97 clippy::cast_possible_truncation,
98 clippy::cast_sign_loss,
99 reason = "clamped to 0..=u8::MAX above"
100 )]
101 let precision = precision as u8;
102
103 Some(precision)
104}
105
106#[must_use]
117pub fn precision_from_str(s: &str) -> u8 {
118 let s = s.trim().to_ascii_lowercase();
119
120 if s.contains('e') {
121 return precision_from_scientific(&s, false, true)
122 .expect("precision_from_scientific should return Some in strict mode");
123 }
124
125 if let Some((_, decimal_part)) = s.split_once('.') {
126 clamp_precision_with_log(decimal_part.len(), "Decimal", &s)
127 } else {
128 0
129 }
130}
131
132#[must_use]
139pub fn min_increment_precision_from_str(s: &str) -> u8 {
140 let s = s.trim().to_ascii_lowercase();
141
142 if s.contains('e') {
143 return precision_from_scientific(&s, true, false).unwrap_or(0);
144 }
145
146 if let Some(dot_pos) = s.find('.') {
147 let decimal_part = &s[dot_pos + 1..];
148 if decimal_part.chars().any(|c| c != '0') {
149 let trimmed_len = decimal_part.trim_end_matches('0').len();
150 return clamp_precision_with_log(trimmed_len, "Minimum increment", &s);
151 }
152 clamp_precision_with_log(decimal_part.len(), "Decimal", &s)
153 } else {
154 0
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use rstest::rstest;
161
162 use super::*;
163
164 #[rstest]
165 #[case("", 0)]
166 #[case("0", 0)]
167 #[case("1.0", 1)]
168 #[case("1.00", 2)]
169 #[case("1.23456789", 8)]
170 #[case("123456.789101112", 9)]
171 #[case("0.000000001", 9)]
172 #[case("1e-1", 1)]
173 #[case("1e-2", 2)]
174 #[case("1e-3", 3)]
175 #[case("1e8", 0)]
176 #[case("-1.23", 2)]
177 #[case("-1e-2", 2)]
178 #[case("1E-2", 2)]
179 #[case("1.5e-2", 3)]
180 #[case("1.23e-2", 4)]
181 #[case("1.5e2", 0)]
182 #[case("1.5E2", 0)]
183 #[case("1.5e+2", 0)]
184 #[case("1.5e0", 1)]
185 #[case("1.5e-1", 2)]
186 #[case(" 1.23", 2)]
187 #[case("1.23 ", 2)]
188 fn test_precision_from_str(#[case] s: &str, #[case] expected: u8) {
189 let result = precision_from_str(s);
190 assert_eq!(result, expected);
191 }
192
193 #[rstest]
194 #[case("", 0)]
195 #[case("0", 0)]
196 #[case("1.0", 1)]
197 #[case("1.00", 2)]
198 #[case("1.23456789", 8)]
199 #[case("123456.789101112", 9)]
200 #[case("0.000000001", 9)]
201 #[case("1e-1", 1)]
202 #[case("1e-2", 2)]
203 #[case("1e-3", 3)]
204 #[case("1e8", 0)]
205 #[case("-1.23", 2)]
206 #[case("-1e-2", 2)]
207 #[case("1E-2", 2)]
208 #[case("1.5e-2", 3)]
209 #[case("1.23e-2", 4)]
210 #[case("1.5e2", 0)]
211 #[case("1.50e-2", 3)]
212 #[case("1.0e-2", 2)]
213 #[case(" 1.23", 2)]
214 #[case("1.23 ", 2)]
215 #[case("1.010", 2)]
216 #[case("1.00100", 3)]
217 #[case("0.0001000", 4)]
218 #[case("1.000000000", 9)]
219 fn test_min_increment_precision_from_str(#[case] s: &str, #[case] expected: u8) {
220 let result = min_increment_precision_from_str(s);
221 assert_eq!(result, expected);
222 }
223
224 #[rstest]
225 fn test_precision_from_str_large_exponent_clamped() {
226 let result = precision_from_str("1e-999");
228 assert_eq!(result, 255);
229 }
230
231 #[rstest]
232 fn test_precision_from_str_very_large_exponent_clamped() {
233 let result = precision_from_str("1e-300");
235 assert_eq!(result, 255);
236
237 let result = precision_from_str("1e-1000000");
238 assert_eq!(result, 255);
239 }
240
241 #[rstest]
242 #[should_panic(expected = "Invalid scientific notation exponent")]
243 fn test_precision_from_str_invalid_exponent_not_numeric() {
244 let _ = precision_from_str("1e-abc");
245 }
246
247 #[rstest]
248 #[should_panic(expected = "missing exponent value")]
249 fn test_precision_from_str_malformed_scientific_notation() {
250 let _ = precision_from_str("1e-");
252 }
253
254 #[rstest]
255 fn test_precision_from_str_edge_case_max_u8() {
256 let result = precision_from_str("1e-255");
258 assert_eq!(result, 255);
259 }
260
261 #[rstest]
262 fn test_precision_from_str_just_above_max_u8() {
263 let result = precision_from_str("1e-256");
265 assert_eq!(result, 255);
266 }
267
268 #[rstest]
269 fn test_precision_from_str_u32_overflow() {
270 let result = precision_from_str("1e-4294967296");
272 assert_eq!(result, 255);
273 }
274
275 #[rstest]
276 fn test_precision_from_str_u64_overflow() {
277 let result = precision_from_str("1e-99999999999999999999");
279 assert_eq!(result, 255);
280 }
281
282 #[rstest]
283 fn test_min_increment_precision_from_str_large_exponent() {
284 let result = min_increment_precision_from_str("1e-300");
286 assert_eq!(result, 255);
287 }
288
289 #[rstest]
290 fn test_min_increment_precision_from_str_very_large_exponent() {
291 let result = min_increment_precision_from_str("1e-99999999999999999999");
293 assert_eq!(result, 255);
294 }
295
296 #[rstest]
297 fn test_min_increment_precision_from_str_consistency() {
298 let input = "1e-1000";
300 let precision = precision_from_str(input);
301 let min_precision = min_increment_precision_from_str(input);
302 assert_eq!(precision, min_precision);
303 assert_eq!(precision, 255);
304 }
305
306 #[rstest]
307 fn test_precision_from_str_i64_min_exponent_clamped() {
308 let result = precision_from_str("1e-9223372036854775808");
310 assert_eq!(result, 255);
311 }
312
313 #[rstest]
314 fn test_min_increment_precision_from_str_empty_exponent() {
315 let result = min_increment_precision_from_str("1e-");
317 assert_eq!(result, 0);
318 }
319}