Skip to main content

malachite_base/num/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::num::arithmetic::traits::{CheckedLogBase2, NegAssign, Pow, UnsignedAbs};
10use crate::num::basic::integers::PrimitiveInt;
11use crate::num::basic::signeds::PrimitiveSigned;
12use crate::num::basic::unsigneds::PrimitiveUnsigned;
13use crate::num::conversion::string::options::{SciSizeOptions, ToSciOptions};
14use crate::num::conversion::string::to_string::{
15    BaseFmtWrapper, digit_to_display_byte_lower, digit_to_display_byte_upper,
16};
17use crate::num::conversion::traits::{ExactFrom, ToSci};
18use crate::rounding_modes::RoundingMode::*;
19use crate::slices::slice_trailing_zeros;
20use alloc::string::String;
21use core::fmt::{Display, Formatter, Write};
22
23/// A `struct` that can be used to format a number in scientific notation.
24pub struct SciWrapper<'a, T: ToSci> {
25    pub(crate) x: &'a T,
26    pub(crate) options: ToSciOptions,
27}
28
29impl<T: ToSci> Display for SciWrapper<'_, T> {
30    #[inline]
31    fn fmt(&self, f: &mut Formatter) -> core::fmt::Result {
32        self.x.fmt_sci(f, self.options)
33    }
34}
35
36#[doc(hidden)]
37pub fn write_exponent<T: PrimitiveInt>(
38    f: &mut Formatter,
39    options: ToSciOptions,
40    exp: T,
41) -> core::fmt::Result {
42    f.write_char(if options.get_e_lowercase() { 'e' } else { 'E' })?;
43    if exp > T::ZERO && (options.get_force_exponent_plus_sign() || options.get_base() >= 15) {
44        f.write_char('+')?;
45    }
46    write!(f, "{exp}")
47}
48
49fn write_helper<T>(x: T, f: &mut Formatter, options: ToSciOptions) -> core::fmt::Result
50where
51    BaseFmtWrapper<T>: Display,
52{
53    let w = BaseFmtWrapper {
54        x,
55        base: options.base,
56    };
57    if options.lowercase {
58        Display::fmt(&w, f)
59    } else {
60        write!(f, "{w:#}")
61    }
62}
63
64fn fmt_sci_valid_unsigned<T: PrimitiveUnsigned>(x: T, options: ToSciOptions) -> bool {
65    if x == T::ZERO || options.rounding_mode != Exact {
66        return true;
67    }
68    match options.size_options {
69        SciSizeOptions::Complete | SciSizeOptions::Scale(_) => true,
70        SciSizeOptions::Precision(precision) => {
71            let t_base = T::from(options.base);
72            let log = x.floor_log_base(t_base);
73            if log < precision {
74                return true;
75            }
76            let neg_scale = log - precision + 1;
77            if let Some(base_log) = options.base.checked_log_base_2() {
78                x.divisible_by_power_of_2(base_log * neg_scale)
79            } else {
80                x.divisible_by(Pow::pow(t_base, neg_scale))
81            }
82        }
83    }
84}
85
86fn fmt_sci_unsigned<T: PrimitiveUnsigned>(
87    mut x: T,
88    f: &mut Formatter,
89    options: ToSciOptions,
90) -> core::fmt::Result
91where
92    BaseFmtWrapper<T>: Display,
93{
94    match options.size_options {
95        SciSizeOptions::Complete | SciSizeOptions::Scale(0) => write_helper(x, f, options),
96        SciSizeOptions::Scale(scale) => {
97            write_helper(x, f, options)?;
98            if options.include_trailing_zeros {
99                f.write_char('.')?;
100                for _ in 0..scale {
101                    f.write_char('0')?;
102                }
103            }
104            Ok(())
105        }
106        SciSizeOptions::Precision(precision) => {
107            let t_base = T::from(options.base);
108            let log = if x == T::ZERO {
109                0
110            } else {
111                x.floor_log_base(t_base)
112            };
113            if log < precision {
114                // no exponent
115                write_helper(x, f, options)?;
116                if options.include_trailing_zeros {
117                    let extra_zeros = precision - log - 1;
118                    if extra_zeros != 0 {
119                        f.write_char('.')?;
120                        for _ in 0..extra_zeros {
121                            f.write_char('0')?;
122                        }
123                    }
124                }
125                Ok(())
126            } else {
127                // exponent
128                let mut e = log;
129                let neg_scale = log - precision + 1;
130                if let Some(base_log) = options.base.checked_log_base_2() {
131                    x.shr_round_assign(base_log * neg_scale, options.rounding_mode);
132                } else {
133                    x.div_round_assign(Pow::pow(t_base, neg_scale), options.rounding_mode);
134                }
135                let mut chars = x.to_digits_desc(&options.base);
136                let mut len = chars.len();
137                let p = usize::exact_from(precision);
138                if len > p {
139                    // rounded up to a power of the base, need to reduce precision
140                    chars.pop();
141                    len -= 1;
142                    e += 1;
143                }
144                assert_eq!(len, p);
145                if !options.include_trailing_zeros {
146                    chars.truncate(len - slice_trailing_zeros(&chars));
147                }
148                if options.lowercase {
149                    for digit in &mut chars {
150                        *digit = digit_to_display_byte_lower(*digit).unwrap();
151                    }
152                } else {
153                    for digit in &mut chars {
154                        *digit = digit_to_display_byte_upper(*digit).unwrap();
155                    }
156                }
157                len = chars.len();
158                if len != 1 {
159                    chars.push(b'0');
160                    chars.copy_within(1..len, 2);
161                    chars[1] = b'.';
162                }
163                f.write_str(&String::from_utf8(chars).unwrap())?;
164                write_exponent(f, options, e)
165            }
166        }
167    }
168}
169
170#[inline]
171fn fmt_sci_valid_signed<T: PrimitiveSigned>(x: T, options: ToSciOptions) -> bool
172where
173    <T as UnsignedAbs>::Output: PrimitiveUnsigned,
174{
175    fmt_sci_valid_unsigned(x.unsigned_abs(), options)
176}
177
178fn fmt_sci_signed<T: PrimitiveSigned>(
179    x: T,
180    f: &mut Formatter,
181    mut options: ToSciOptions,
182) -> core::fmt::Result
183where
184    <T as UnsignedAbs>::Output: PrimitiveUnsigned,
185{
186    let abs = x.unsigned_abs();
187    if x >= T::ZERO {
188        abs.fmt_sci(f, options)
189    } else {
190        options.rounding_mode.neg_assign();
191        f.write_char('-')?;
192        abs.fmt_sci(f, options)
193    }
194}
195
196macro_rules! impl_to_sci_unsigned {
197    ($t:ident) => {
198        impl ToSci for $t {
199            /// Determines whether an unsigned number can be converted to a string using
200            /// [`to_sci_with_options`](super::super::traits::ToSci::to_sci_with_options) and a
201            /// particular set of options.
202            ///
203            /// # Worst-case complexity
204            /// Constant time and additional memory.
205            ///
206            /// # Examples
207            /// See [here](super::to_sci#fmt_sci_valid).
208            #[inline]
209            fn fmt_sci_valid(&self, options: ToSciOptions) -> bool {
210                fmt_sci_valid_unsigned(*self, options)
211            }
212
213            /// Converts an unsigned number to a string using a specified base, possibly formatting
214            /// the number using scientific notation.
215            ///
216            /// See [`ToSciOptions`] for details on the available options. Note that setting
217            /// `neg_exp_threshold` has no effect, since there is never a need to use negative
218            /// exponents when representing an integer.
219            ///
220            /// # Worst-case complexity
221            /// $T(n) = O(n)$
222            ///
223            /// $M(n) = O(n)$
224            ///
225            /// where $T$ is time, $M$ is additional memory, and $n$ is `self.significant_bits()`.
226            ///
227            /// # Panics
228            /// Panics if `options.rounding_mode` is `Exact`, but the size options are such that the
229            /// input must be rounded.
230            ///
231            /// # Examples
232            /// See [here](super::to_sci).
233            #[inline]
234            fn fmt_sci(&self, f: &mut Formatter, options: ToSciOptions) -> core::fmt::Result {
235                fmt_sci_unsigned(*self, f, options)
236            }
237        }
238    };
239}
240apply_to_unsigneds!(impl_to_sci_unsigned);
241
242macro_rules! impl_to_sci_signed {
243    ($t:ident) => {
244        impl ToSci for $t {
245            /// Determines whether a signed number can be converted to a string using
246            /// [`to_sci_with_options`](super::super::traits::ToSci::to_sci_with_options) and a
247            /// particular set of options.
248            ///
249            /// # Worst-case complexity
250            /// Constant time and additional memory.
251            ///
252            /// # Examples
253            /// See [here](super::to_sci#fmt_sci_valid).
254            #[inline]
255            fn fmt_sci_valid(&self, options: ToSciOptions) -> bool {
256                fmt_sci_valid_signed(*self, options)
257            }
258
259            /// Converts a signed number to a string using a specified base, possibly formatting the
260            /// number using scientific notation.
261            ///
262            /// See [`ToSciOptions`] for details on the available options. Note that setting
263            /// `neg_exp_threshold` has no effect, since there is never a need to use negative
264            /// exponents when representing an integer.
265            ///
266            /// # Worst-case complexity
267            /// $T(n) = O(n)$
268            ///
269            /// $M(n) = O(n)$
270            ///
271            /// where $T$ is time, $M$ is additional memory, and $n$ is `self.significant_bits()`.
272            ///
273            /// # Panics
274            /// Panics if `options.rounding_mode` is `Exact`, but the size options are such that the
275            /// input must be rounded.
276            ///
277            /// # Examples
278            /// See [here](super::to_sci).
279            #[inline]
280            fn fmt_sci(&self, f: &mut Formatter, options: ToSciOptions) -> core::fmt::Result {
281                fmt_sci_signed(*self, f, options)
282            }
283        }
284    };
285}
286apply_to_signeds!(impl_to_sci_signed);