malachite_base/num/conversion/string/
to_sci.rs

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