Skip to main content

malachite_base/num/conversion/string/
from_sci_string.rs

1// Copyright © 2026 Mikhail Hogrefe
2//
3// This file is part of Malachite.
4//
5// Malachite is free software: you can redistribute it and/or modify it under the terms of the GNU
6// Lesser General Public License (LGPL) as published by the Free Software Foundation; either version
7// 3 of the License, or (at your option) any later version. See <https://www.gnu.org/licenses/>.
8
9use crate::num::arithmetic::traits::Parity;
10use crate::num::basic::integers::PrimitiveInt;
11use crate::num::conversion::string::from_string::digit_from_display_byte;
12use crate::num::conversion::string::options::FromSciStringOptions;
13use crate::num::conversion::traits::FromSciString;
14use crate::rounding_modes::RoundingMode::*;
15use alloc::vec::Vec;
16use core::cmp::Ordering::{self, *};
17use core::str::FromStr;
18
19#[doc(hidden)]
20#[inline]
21pub fn parse_exponent(s: &[u8]) -> Option<i64> {
22    i64::from_str(core::str::from_utf8(s).ok()?).ok()
23}
24
25#[doc(hidden)]
26pub fn validate_helper(s: &[u8], base: u8) -> Option<()> {
27    for &c in s {
28        if digit_from_display_byte(c)? >= base {
29            return None;
30        }
31    }
32    Some(())
33}
34
35#[doc(hidden)]
36pub fn is_zero_helper(s: &[u8], base: u8) -> Option<bool> {
37    let mut all_zeros = true;
38    for &c in s {
39        let d = digit_from_display_byte(c)?;
40        if d >= base {
41            return None;
42        }
43        if d != 0 {
44            all_zeros = false;
45        }
46    }
47    Some(all_zeros)
48}
49
50#[doc(hidden)]
51pub fn cmp_half_helper(s: &[u8], base: u8) -> Option<Ordering> {
52    if s.is_empty() {
53        return Some(Less);
54    }
55    let h = base >> 1;
56    let mut done = false;
57    let mut result;
58    if base.even() {
59        // 1/2 is 0.h
60        result = Equal;
61        let mut first = true;
62        for &c in s {
63            let d = digit_from_display_byte(c)?;
64            if d >= base {
65                return None;
66            }
67            if done {
68                continue;
69            }
70            if first {
71                let half_c = d.cmp(&h);
72                if half_c != Equal {
73                    result = half_c;
74                    done = true;
75                }
76                first = false;
77            } else if d != 0 {
78                result = Greater;
79                done = true;
80            }
81        }
82    } else {
83        // 1/2 is 0.hhh...
84        result = Less;
85        for &c in s {
86            let d = digit_from_display_byte(c)?;
87            if done {
88                continue;
89            }
90            let half_c = d.cmp(&h);
91            if half_c != Equal {
92                result = half_c;
93                done = true;
94            }
95        }
96    }
97    Some(result)
98}
99
100fn parse_int<T: PrimitiveInt>(cs: &[u8], base: u8) -> Option<T> {
101    // if T is unsigned, from_string_base won't handle -0
102    let mut test_neg_zero = false;
103    if T::MIN == T::ZERO
104        && let Some(&b'-') = cs.first()
105    {
106        test_neg_zero = true;
107    }
108    if test_neg_zero {
109        if cs.len() == 1 {
110            return None;
111        }
112        for &c in &cs[1..] {
113            if c != b'0' {
114                return None;
115            }
116        }
117        Some(T::ZERO)
118    } else {
119        T::from_string_base(base, core::str::from_utf8(cs).ok()?)
120    }
121}
122
123fn up_1<T: PrimitiveInt>(x: T, neg: bool) -> Option<T> {
124    if neg {
125        x.checked_sub(T::ONE)
126    } else {
127        x.checked_add(T::ONE)
128    }
129}
130
131#[doc(hidden)]
132pub fn preprocess_sci_string(s: &str, options: FromSciStringOptions) -> Option<(Vec<u8>, i64)> {
133    let mut s = s.as_bytes().to_vec();
134    let mut exponent = 0;
135    if options.base < 15 {
136        for (i, &c) in s.iter().enumerate().rev() {
137            if c == b'e' || c == b'E' {
138                if i == 0 || i == s.len() - 1 {
139                    return None;
140                }
141                exponent = parse_exponent(&s[i + 1..])?;
142                s.truncate(i);
143                break;
144            }
145        }
146    } else {
147        for (i, &c) in s.iter().enumerate().rev() {
148            if c == b'+' || c == b'-' {
149                if i == 0 {
150                    break;
151                }
152                if i == 1 || i == s.len() - 1 {
153                    return None;
154                }
155                let exp_indicator = s[i - 1];
156                if exp_indicator != b'e' && exp_indicator != b'E' {
157                    return None;
158                }
159                exponent = parse_exponent(&s[i..])?;
160                s.truncate(i - 1);
161                break;
162            }
163        }
164    }
165    let mut point_index = None;
166    for (i, &c) in s.iter().enumerate() {
167        if c == b'.' {
168            point_index = Some(i);
169            break;
170        }
171    }
172    if let Some(point_index) = point_index {
173        let len = s.len();
174        if point_index != len - 1 {
175            let next_char = s[point_index + 1];
176            if next_char == b'+' || next_char == b'-' {
177                return None;
178            }
179            exponent = exponent.checked_sub(i64::try_from(len - point_index - 1).ok()?)?;
180            s.copy_within(point_index + 1..len, point_index);
181        }
182        s.pop();
183    }
184    Some((s, exponent))
185}
186
187fn from_sci_string_with_options_primitive_int<T: PrimitiveInt>(
188    s: &str,
189    options: FromSciStringOptions,
190) -> Option<T> {
191    let (s, exponent) = preprocess_sci_string(s, options)?;
192    if exponent >= 0 {
193        let x = parse_int::<T>(&s, options.base)?;
194        x.checked_mul(T::wrapping_from(options.base).checked_pow(exponent.unsigned_abs())?)
195    } else {
196        let neg_exponent = usize::try_from(exponent.unsigned_abs()).ok()?;
197        let len = s.len();
198        if len == 0 {
199            return None;
200        }
201        let first = s[0];
202        let neg = first == b'-';
203        let sign = neg || first == b'+';
204        let rm = if neg {
205            -options.rounding_mode
206        } else {
207            options.rounding_mode
208        };
209        let sig_len = if sign { len - 1 } else { len };
210        if sig_len == 0 {
211            return None;
212        }
213        if neg_exponent > sig_len {
214            let s = if sign { &s[1..] } else { &s[..] };
215            return match rm {
216                Down | Floor | Nearest => {
217                    validate_helper(s, options.base)?;
218                    Some(T::ZERO)
219                }
220                Up | Ceiling => {
221                    if is_zero_helper(s, options.base)? {
222                        Some(T::ZERO)
223                    } else {
224                        up_1(T::ZERO, neg)
225                    }
226                }
227                Exact => None,
228            };
229        }
230        let (before_e, after_e) = s.split_at(len - neg_exponent);
231        let x = match before_e {
232            &[] | &[b'-'] | &[b'+'] => T::ZERO,
233            before_e => parse_int(before_e, options.base)?,
234        };
235        if after_e.is_empty() {
236            return Some(x);
237        }
238        match rm {
239            Down | Floor => {
240                validate_helper(after_e, options.base)?;
241                Some(x)
242            }
243            Up | Ceiling => {
244                if is_zero_helper(after_e, options.base)? {
245                    Some(x)
246                } else {
247                    up_1(x, neg)
248                }
249            }
250            Exact => {
251                if is_zero_helper(after_e, options.base)? {
252                    Some(x)
253                } else {
254                    None
255                }
256            }
257            Nearest => match cmp_half_helper(after_e, options.base)? {
258                Less => Some(x),
259                Greater => up_1(x, neg),
260                Equal => {
261                    if x.even() {
262                        Some(x)
263                    } else {
264                        up_1(x, neg)
265                    }
266                }
267            },
268        }
269    }
270}
271
272macro_rules! impl_from_sci_string {
273    ($t:ident) => {
274        impl FromSciString for $t {
275            /// Converts a [`String`], possibly in scientfic notation, to a primitive integer.
276            ///
277            /// Use [`FromSciStringOptions`] to specify the base (from 2 to 36, inclusive) and the
278            /// rounding mode, in case rounding is necessary because the string represents a
279            /// non-integer.
280            ///
281            /// If the base is greater than 10, the higher digits are represented by the letters
282            /// `'a'` through `'z'` or `'A'` through `'Z'`; the case doesn't matter and doesn't need
283            /// to be consistent.
284            ///
285            /// Exponents are allowed, and are indicated using the character `'e'` or `'E'`. If the
286            /// base is 15 or greater, an ambiguity arises where it may not be clear whether `'e'`
287            /// is a digit or an exponent indicator. To resolve this ambiguity, always use a `'+'`
288            /// or `'-'` sign after the exponent indicator when the base is 15 or greater.
289            ///
290            /// The exponent itself is always parsed using base 10.
291            ///
292            /// Decimal (or other-base) points are allowed. These are most useful in conjunction
293            /// with exponents, but they may be used on their own. If the string represents a
294            /// non-integer, the rounding mode specified in `options` is used to round to an
295            /// integer.
296            ///
297            /// If the string is unparseable or parses to an out-of-range integer, `None` is
298            /// returned. `None` is also returned if the rounding mode in options is `Exact`, but
299            /// rounding is necessary.
300            ///
301            /// # Worst-case complexity
302            /// $T(n) = O(n)$
303            ///
304            /// $M(n) = O(1)$
305            ///
306            /// where $T$ is time, $M$ is additional memory, and $n$ is `s.len()`.
307            ///
308            /// # Examples
309            /// See [here](super::from_sci_string).
310            #[inline]
311            fn from_sci_string_with_options(s: &str, options: FromSciStringOptions) -> Option<$t> {
312                from_sci_string_with_options_primitive_int(s, options)
313            }
314        }
315    };
316}
317apply_to_primitive_ints!(impl_from_sci_string);