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(
51 s: &str,
52 e_pos: usize,
53 trim_trailing_zeros: bool,
54 strict: bool,
55) -> Option<u8> {
56 let mantissa = &s[..e_pos];
57 let exponent_str = &s[e_pos + 1..];
58
59 let frac_digits = mantissa.split_once('.').map_or(0, |(_, frac)| {
60 if trim_trailing_zeros {
61 frac.trim_end_matches('0').len()
62 } else {
63 frac.len()
64 }
65 });
66
67 let exponent: i64 = if let Ok(v) = exponent_str.parse::<i64>() {
68 v
69 } else {
70 let (digits, is_negative) = exponent_str
71 .strip_prefix('-')
72 .map(|rest| (rest, true))
73 .or_else(|| exponent_str.strip_prefix('+').map(|rest| (rest, false)))
74 .unwrap_or((exponent_str, false));
75
76 if digits.is_empty() {
77 assert!(
78 !strict,
79 "Invalid scientific notation format: missing exponent value"
80 );
81 return None;
82 }
83
84 if digits.chars().all(|c| c.is_ascii_digit()) {
85 return Some(if is_negative { u8::MAX } else { 0 });
86 }
87
88 assert!(
89 !strict,
90 "Invalid scientific notation exponent '{exponent_str}': must be a valid number"
91 );
92 return None;
93 };
94
95 let precision = i64::try_from(frac_digits)
96 .unwrap_or(i64::MAX)
97 .saturating_sub(exponent)
98 .clamp(0, i64::from(u8::MAX));
99
100 #[expect(
101 clippy::cast_possible_truncation,
102 clippy::cast_sign_loss,
103 reason = "clamped to 0..=u8::MAX above"
104 )]
105 let precision = precision as u8;
106
107 Some(precision)
108}
109
110#[must_use]
121pub fn precision_from_str(s: &str) -> u8 {
122 let s = s.trim();
123
124 if let Some(e_pos) = s.find(['e', 'E']) {
125 return precision_from_scientific(s, e_pos, false, true)
126 .expect("precision_from_scientific should return Some in strict mode");
127 }
128
129 if let Some((_, decimal_part)) = s.split_once('.') {
130 clamp_precision_with_log(decimal_part.len(), "Decimal", s)
131 } else {
132 0
133 }
134}
135
136#[must_use]
143pub fn min_increment_precision_from_str(s: &str) -> u8 {
144 let s = s.trim();
145
146 if let Some(e_pos) = s.find(['e', 'E']) {
147 return precision_from_scientific(s, e_pos, true, false).unwrap_or(0);
148 }
149
150 if let Some(dot_pos) = s.find('.') {
151 let decimal_part = &s[dot_pos + 1..];
152 match decimal_part.as_bytes().iter().rposition(|&b| b != b'0') {
153 Some(index) => clamp_precision_with_log(index + 1, "Minimum increment", s),
154 None => clamp_precision_with_log(decimal_part.len(), "Decimal", s),
155 }
156 } else {
157 0
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use rstest::rstest;
164
165 use super::*;
166
167 #[rstest]
168 #[case("", 0)]
169 #[case("0", 0)]
170 #[case("1.0", 1)]
171 #[case("1.00", 2)]
172 #[case("1.23456789", 8)]
173 #[case("123456.789101112", 9)]
174 #[case("0.000000001", 9)]
175 #[case("1e-1", 1)]
176 #[case("1e-2", 2)]
177 #[case("1e-3", 3)]
178 #[case("1e8", 0)]
179 #[case("-1.23", 2)]
180 #[case("-1e-2", 2)]
181 #[case("1E-2", 2)]
182 #[case("1.5e-2", 3)]
183 #[case("1.23e-2", 4)]
184 #[case("1.5e2", 0)]
185 #[case("1.5E2", 0)]
186 #[case("1.5e+2", 0)]
187 #[case("1.5e0", 1)]
188 #[case("1.5e-1", 2)]
189 #[case(" 1.23", 2)]
190 #[case("1.23 ", 2)]
191 fn test_precision_from_str(#[case] s: &str, #[case] expected: u8) {
192 let result = precision_from_str(s);
193 assert_eq!(result, expected);
194 }
195
196 #[rstest]
197 #[case("", 0)]
198 #[case("0", 0)]
199 #[case("1.0", 1)]
200 #[case("1.00", 2)]
201 #[case("1.23456789", 8)]
202 #[case("123456.789101112", 9)]
203 #[case("0.000000001", 9)]
204 #[case("1e-1", 1)]
205 #[case("1e-2", 2)]
206 #[case("1e-3", 3)]
207 #[case("1e8", 0)]
208 #[case("-1.23", 2)]
209 #[case("-1e-2", 2)]
210 #[case("1E-2", 2)]
211 #[case("1.5e-2", 3)]
212 #[case("1.23e-2", 4)]
213 #[case("1.5e2", 0)]
214 #[case("1.50e-2", 3)]
215 #[case("1.0e-2", 2)]
216 #[case(" 1.23", 2)]
217 #[case("1.23 ", 2)]
218 #[case("1.010", 2)]
219 #[case("1.00100", 3)]
220 #[case("0.0001000", 4)]
221 #[case("1.000000000", 9)]
222 fn test_min_increment_precision_from_str(#[case] s: &str, #[case] expected: u8) {
223 let result = min_increment_precision_from_str(s);
224 assert_eq!(result, expected);
225 }
226
227 #[rstest]
228 fn test_precision_from_str_large_exponent_clamped() {
229 let result = precision_from_str("1e-999");
231 assert_eq!(result, 255);
232 }
233
234 #[rstest]
235 fn test_precision_from_str_very_large_exponent_clamped() {
236 let result = precision_from_str("1e-300");
238 assert_eq!(result, 255);
239
240 let result = precision_from_str("1e-1000000");
241 assert_eq!(result, 255);
242 }
243
244 #[rstest]
245 #[should_panic(expected = "Invalid scientific notation exponent")]
246 fn test_precision_from_str_invalid_exponent_not_numeric() {
247 let _ = precision_from_str("1e-abc");
248 }
249
250 #[rstest]
251 #[should_panic(expected = "missing exponent value")]
252 fn test_precision_from_str_malformed_scientific_notation() {
253 let _ = precision_from_str("1e-");
255 }
256
257 #[rstest]
258 fn test_precision_from_str_edge_case_max_u8() {
259 let result = precision_from_str("1e-255");
261 assert_eq!(result, 255);
262 }
263
264 #[rstest]
265 fn test_precision_from_str_just_above_max_u8() {
266 let result = precision_from_str("1e-256");
268 assert_eq!(result, 255);
269 }
270
271 #[rstest]
272 fn test_precision_from_str_u32_overflow() {
273 let result = precision_from_str("1e-4294967296");
275 assert_eq!(result, 255);
276 }
277
278 #[rstest]
279 fn test_precision_from_str_u64_overflow() {
280 let result = precision_from_str("1e-99999999999999999999");
282 assert_eq!(result, 255);
283 }
284
285 #[rstest]
286 fn test_min_increment_precision_from_str_large_exponent() {
287 let result = min_increment_precision_from_str("1e-300");
289 assert_eq!(result, 255);
290 }
291
292 #[rstest]
293 fn test_min_increment_precision_from_str_very_large_exponent() {
294 let result = min_increment_precision_from_str("1e-99999999999999999999");
296 assert_eq!(result, 255);
297 }
298
299 #[rstest]
300 fn test_min_increment_precision_from_str_consistency() {
301 let input = "1e-1000";
303 let precision = precision_from_str(input);
304 let min_precision = min_increment_precision_from_str(input);
305 assert_eq!(precision, min_precision);
306 assert_eq!(precision, 255);
307 }
308
309 #[rstest]
310 fn test_precision_from_str_i64_min_exponent_clamped() {
311 let result = precision_from_str("1e-9223372036854775808");
313 assert_eq!(result, 255);
314 }
315
316 #[rstest]
317 fn test_min_increment_precision_from_str_empty_exponent() {
318 let result = min_increment_precision_from_str("1e-");
320 assert_eq!(result, 0);
321 }
322
323 #[rstest]
324 #[case(
325 "1E-ABC",
326 "Invalid scientific notation exponent '-ABC': must be a valid number"
327 )]
328 #[case(
329 "1eE-2",
330 "Invalid scientific notation exponent 'E-2': must be a valid number"
331 )]
332 #[case("1E-", "Invalid scientific notation format: missing exponent value")]
333 fn test_scientific_precision_reports_original_exponent(
334 #[case] input: &str,
335 #[case] expected: &str,
336 ) {
337 assert_eq!(min_increment_precision_from_str(input), 0);
338 let panic = std::panic::catch_unwind(|| precision_from_str(input)).unwrap_err();
339 let message = panic
340 .downcast_ref::<String>()
341 .map(String::as_str)
342 .or_else(|| panic.downcast_ref::<&str>().copied())
343 .unwrap();
344 assert_eq!(message, expected);
345 }
346
347 #[rstest]
348 #[case("1.\u{e9}00", 2)]
349 #[case("1.\u{660}00", 2)]
350 #[case("1.00\u{660}", 4)]
351 #[case("1.", 0)]
352 fn test_minimum_precision_preserves_utf8_byte_length(
353 #[case] input: &str,
354 #[case] expected: u8,
355 ) {
356 assert_eq!(min_increment_precision_from_str(input), expected);
357 }
358
359 #[rstest]
360 fn test_minimum_precision_clamps_all_zero_fraction() {
361 let input = format!("1.{}", "0".repeat(256));
362 assert_eq!(min_increment_precision_from_str(&input), u8::MAX);
363 }
364}