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}