Skip to main content

malachite_nz/natural/conversion/string/
to_sci.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::natural::Natural;
10use crate::natural::arithmetic::log_base::log_base_helper_with_pow;
11use crate::natural::conversion::string::to_string::BaseFmtWrapper;
12use crate::natural::slice_trailing_zeros;
13use alloc::string::String;
14use core::fmt::{Display, Formatter, Write};
15use malachite_base::num::arithmetic::traits::{
16    CheckedLogBase2, DivExact, DivRound, DivisibleBy, DivisibleByPowerOf2, FloorLogBase,
17    FloorLogBasePowerOf2, Pow, ShrRound,
18};
19use malachite_base::num::basic::traits::Zero;
20use malachite_base::num::conversion::string::options::{SciSizeOptions, ToSciOptions};
21use malachite_base::num::conversion::string::to_sci::write_exponent;
22use malachite_base::num::conversion::string::to_string::{
23    digit_to_display_byte_lower, digit_to_display_byte_upper,
24};
25use malachite_base::num::conversion::traits::{Digits, ExactFrom, ToSci};
26use malachite_base::rounding_modes::RoundingMode::*;
27
28fn write_helper<T>(x: &T, f: &mut Formatter, options: ToSciOptions) -> core::fmt::Result
29where
30    for<'a> BaseFmtWrapper<&'a T>: Display,
31{
32    let w = BaseFmtWrapper {
33        x,
34        base: options.get_base(),
35    };
36    if options.get_lowercase() {
37        Display::fmt(&w, f)
38    } else {
39        write!(f, "{w:#}")
40    }
41}
42
43impl ToSci for Natural {
44    /// Determines whether a [`Natural`] can be converted to a string using
45    /// [`to_sci`](`Self::to_sci`) and a particular set of options.
46    ///
47    /// # Worst-case complexity
48    /// $T(n) = O(n \log n \log\log n)$
49    ///
50    /// $M(n) = O(n \log n)$
51    ///
52    /// where $T$ is time, $M$ is additional memory, and $n$ is `self.significant_bits()`.
53    ///
54    /// # Examples
55    /// ```
56    /// use malachite_base::num::conversion::string::options::ToSciOptions;
57    /// use malachite_base::num::conversion::traits::ToSci;
58    /// use malachite_base::rounding_modes::RoundingMode::*;
59    /// use malachite_nz::natural::Natural;
60    ///
61    /// let mut options = ToSciOptions::default();
62    /// assert!(Natural::from(123u8).fmt_sci_valid(options));
63    /// assert!(Natural::from(u128::MAX).fmt_sci_valid(options));
64    /// // u128::MAX has more than 16 significant digits
65    /// options.set_rounding_mode(Exact);
66    /// assert!(!Natural::from(u128::MAX).fmt_sci_valid(options));
67    /// options.set_precision(50);
68    /// assert!(Natural::from(u128::MAX).fmt_sci_valid(options));
69    /// ```
70    fn fmt_sci_valid(&self, options: ToSciOptions) -> bool {
71        if *self == 0u32 || options.get_rounding_mode() != Exact {
72            return true;
73        }
74        match options.get_size_options() {
75            SciSizeOptions::Complete | SciSizeOptions::Scale(_) => true,
76            SciSizeOptions::Precision(precision) => {
77                let n_base = Self::from(options.get_base());
78                let log = self.floor_log_base(&n_base);
79                if log < precision {
80                    return true;
81                }
82                let scale = log - precision + 1;
83                if let Some(base_log) = options.get_base().checked_log_base_2() {
84                    self.divisible_by_power_of_2(base_log * scale)
85                } else {
86                    self.divisible_by(n_base.pow(scale))
87                }
88            }
89        }
90    }
91
92    /// Converts a [`Natural`] to a string using a specified base, possibly formatting the number
93    /// using scientific notation.
94    ///
95    /// See [`ToSciOptions`] for details on the available options. Note that setting
96    /// `neg_exp_threshold` has no effect, since there is never a need to use negative exponents
97    /// when representing a [`Natural`].
98    ///
99    /// # Worst-case complexity
100    /// $T(n) = O(n (\log n)^2 \log\log n)$
101    ///
102    /// $M(n) = O(n \log n)$
103    ///
104    /// where $T$ is time, $M$ is additional memory, and $n$ is `self.significant_bits()`.
105    ///
106    /// # Panics
107    /// Panics if `options.rounding_mode` is `Exact`, but the size options are such that the input
108    /// must be rounded.
109    ///
110    /// # Examples
111    /// ```
112    /// use malachite_base::num::conversion::string::options::ToSciOptions;
113    /// use malachite_base::num::conversion::traits::ToSci;
114    /// use malachite_base::rounding_modes::RoundingMode::*;
115    /// use malachite_nz::natural::Natural;
116    ///
117    /// assert_eq!(
118    ///     format!("{}", Natural::from(u128::MAX).to_sci()),
119    ///     "3.402823669209385e38"
120    /// );
121    /// assert_eq!(
122    ///     Natural::from(u128::MAX).to_sci().to_string(),
123    ///     "3.402823669209385e38"
124    /// );
125    ///
126    /// let n = Natural::from(123456u32);
127    /// let mut options = ToSciOptions::default();
128    /// assert_eq!(format!("{}", n.to_sci_with_options(options)), "123456");
129    /// assert_eq!(n.to_sci_with_options(options).to_string(), "123456");
130    ///
131    /// options.set_precision(3);
132    /// assert_eq!(n.to_sci_with_options(options).to_string(), "1.23e5");
133    ///
134    /// options.set_rounding_mode(Ceiling);
135    /// assert_eq!(n.to_sci_with_options(options).to_string(), "1.24e5");
136    ///
137    /// options.set_e_uppercase();
138    /// assert_eq!(n.to_sci_with_options(options).to_string(), "1.24E5");
139    ///
140    /// options.set_force_exponent_plus_sign(true);
141    /// assert_eq!(n.to_sci_with_options(options).to_string(), "1.24E+5");
142    ///
143    /// options = ToSciOptions::default();
144    /// options.set_base(36);
145    /// assert_eq!(n.to_sci_with_options(options).to_string(), "2n9c");
146    ///
147    /// options.set_uppercase();
148    /// assert_eq!(n.to_sci_with_options(options).to_string(), "2N9C");
149    ///
150    /// options.set_base(2);
151    /// options.set_precision(10);
152    /// assert_eq!(n.to_sci_with_options(options).to_string(), "1.1110001e16");
153    ///
154    /// options.set_include_trailing_zeros(true);
155    /// assert_eq!(n.to_sci_with_options(options).to_string(), "1.111000100e16");
156    /// ```
157    fn fmt_sci(&self, f: &mut Formatter, options: ToSciOptions) -> core::fmt::Result {
158        match options.get_size_options() {
159            SciSizeOptions::Complete | SciSizeOptions::Scale(0) => write_helper(self, f, options),
160            SciSizeOptions::Scale(scale) => {
161                write_helper(self, f, options)?;
162                if options.get_include_trailing_zeros() {
163                    f.write_char('.')?;
164                    for _ in 0..scale {
165                        f.write_char('0')?;
166                    }
167                }
168                Ok(())
169            }
170            SciSizeOptions::Precision(precision) => {
171                let n_base = Self::from(options.get_base());
172                let (base_log, log, power, p) = if *self == 0u32 {
173                    // power and p unused
174                    (None, 0, Self::ZERO, 0)
175                } else if let Some(base_log) = options.get_base().checked_log_base_2() {
176                    // power and p unused
177                    (
178                        Some(base_log),
179                        self.floor_log_base_power_of_2(base_log),
180                        Self::ZERO,
181                        0,
182                    )
183                } else {
184                    // We save base^p so that we can save some computation later
185                    let (log, _, power, p) = log_base_helper_with_pow(self, &n_base);
186                    (None, log, power, p)
187                };
188                if log < precision {
189                    // no exponent
190                    write_helper(self, f, options)?;
191                    if options.get_include_trailing_zeros() {
192                        let extra_zeros = precision - log - 1;
193                        if extra_zeros != 0 {
194                            f.write_char('.')?;
195                            for _ in 0..extra_zeros {
196                                f.write_char('0')?;
197                            }
198                        }
199                    }
200                    Ok(())
201                } else {
202                    // exponent
203                    let mut e = log;
204                    let scale = log - precision + 1;
205                    let shifted = if let Some(base_log) = base_log {
206                        self.shr_round(base_log * scale, options.get_rounding_mode())
207                            .0
208                    } else {
209                        let n = if precision > log >> 1 {
210                            n_base.pow(scale)
211                        } else if p >= scale {
212                            power.div_exact(n_base.pow(p - scale))
213                        } else {
214                            // Not sure if this ever happens
215                            assert!(p == scale + 1);
216                            power * n_base
217                        };
218                        self.div_round(n, options.get_rounding_mode()).0
219                    };
220                    let mut chars = shifted.to_digits_desc(&options.get_base());
221                    let mut len = chars.len();
222                    let p = usize::exact_from(precision);
223                    if len > p {
224                        // rounded up to a power of the base, need to reduce precision
225                        assert_eq!(chars.pop().unwrap(), 0);
226                        len -= 1;
227                        e += 1;
228                    }
229                    assert_eq!(len, p);
230                    if !options.get_include_trailing_zeros() {
231                        chars.truncate(len - slice_trailing_zeros(&chars));
232                    }
233                    if options.get_lowercase() {
234                        for digit in &mut chars {
235                            *digit = digit_to_display_byte_lower(*digit).unwrap();
236                        }
237                    } else {
238                        for digit in &mut chars {
239                            *digit = digit_to_display_byte_upper(*digit).unwrap();
240                        }
241                    }
242                    len = chars.len();
243                    if len != 1 {
244                        chars.push(b'0');
245                        chars.copy_within(1..len, 2);
246                        chars[1] = b'.';
247                    }
248                    f.write_str(&String::from_utf8(chars).unwrap())?;
249                    write_exponent(f, options, e)
250                }
251            }
252        }
253    }
254}