malachite_q/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::Rational;
10use crate::arithmetic::log_base::log_base_helper;
11use alloc::string::String;
12use core::cmp::{Ordering::*, max};
13use core::fmt::{Formatter, Write};
14use malachite_base::num::arithmetic::traits::{
15    Abs, CheckedLogBase2, DivExact, DivExactAssign, DivRound, DivisibleBy, Pow, Sign,
16};
17use malachite_base::num::conversion::string::options::{SciSizeOptions, ToSciOptions};
18use malachite_base::num::conversion::string::to_sci::write_exponent;
19use malachite_base::num::conversion::traits::{
20    ExactFrom, IsInteger, RoundingFrom, ToSci, ToStringBase, WrappingFrom,
21};
22use malachite_base::rounding_modes::RoundingMode::*;
23use malachite_nz::integer::Integer;
24use malachite_nz::natural::Natural;
25
26const BASE_PRIME_FACTORS: [[(u8, u8); 3]; 37] = [
27    [(0, 0), (0, 0), (0, 0)],
28    [(0, 0), (0, 0), (0, 0)],
29    [(2, 1), (0, 0), (0, 0)],
30    [(3, 1), (0, 0), (0, 0)],
31    [(2, 2), (0, 0), (0, 0)],
32    [(5, 1), (0, 0), (0, 0)],
33    [(2, 1), (3, 1), (0, 0)],
34    [(7, 1), (0, 0), (0, 0)],
35    [(2, 3), (0, 0), (0, 0)],
36    [(3, 2), (0, 0), (0, 0)],
37    [(2, 1), (5, 1), (0, 0)],
38    [(11, 1), (0, 0), (0, 0)],
39    [(2, 2), (3, 1), (0, 0)],
40    [(13, 1), (0, 0), (0, 0)],
41    [(2, 1), (7, 1), (0, 0)],
42    [(3, 1), (5, 1), (0, 0)],
43    [(2, 4), (0, 0), (0, 0)],
44    [(17, 1), (0, 0), (0, 0)],
45    [(2, 1), (3, 2), (0, 0)],
46    [(19, 1), (0, 0), (0, 0)],
47    [(2, 2), (5, 1), (0, 0)],
48    [(3, 1), (7, 1), (0, 0)],
49    [(2, 1), (11, 1), (0, 0)],
50    [(23, 1), (0, 0), (0, 0)],
51    [(2, 3), (3, 1), (0, 0)],
52    [(5, 2), (0, 0), (0, 0)],
53    [(2, 1), (13, 1), (0, 0)],
54    [(3, 3), (0, 0), (0, 0)],
55    [(2, 2), (7, 1), (0, 0)],
56    [(29, 1), (0, 0), (0, 0)],
57    [(2, 1), (3, 1), (5, 1)],
58    [(31, 1), (0, 0), (0, 0)],
59    [(2, 5), (0, 0), (0, 0)],
60    [(3, 1), (11, 1), (0, 0)],
61    [(2, 1), (17, 1), (0, 0)],
62    [(5, 1), (7, 1), (0, 0)],
63    [(2, 2), (3, 2), (0, 0)],
64];
65
66impl Rational {
67    /// When expanding a [`Rational`] in a small base $b$, determines how many digits after the
68    /// decimal (or other-base) point are in the base-$b$ expansion.
69    ///
70    /// If the expansion is non-terminating, this method returns `None`. This happens iff the
71    /// [`Rational`]'s denominator has prime factors not present in $b$.
72    ///
73    /// # Worst-case complexity
74    /// $T(n) = O(n)$
75    ///
76    /// $M(n) = O(n)$
77    ///
78    /// where $T$ is time, $M$ is additional memory, and $n$ is `self.significant_bits()`.
79    ///
80    /// # Panics
81    /// Panics if `base` is less than 2 or greater than 36.
82    ///
83    /// # Examples
84    /// ```
85    /// use malachite_q::Rational;
86    ///
87    /// // 3/8 is 0.375 in base 10.
88    /// assert_eq!(
89    ///     Rational::from_signeds(3, 8).length_after_point_in_small_base(10),
90    ///     Some(3)
91    /// );
92    /// // 1/20 is 0.05 in base 10.
93    /// assert_eq!(
94    ///     Rational::from_signeds(1, 20).length_after_point_in_small_base(10),
95    ///     Some(2)
96    /// );
97    /// // 1/7 is non-terminating in base 10.
98    /// assert_eq!(
99    ///     Rational::from_signeds(1, 7).length_after_point_in_small_base(10),
100    ///     None
101    /// );
102    /// // 1/7 is 0.3 in base 21.
103    /// assert_eq!(
104    ///     Rational::from_signeds(1, 7).length_after_point_in_small_base(21),
105    ///     Some(1)
106    /// );
107    /// ```
108    pub fn length_after_point_in_small_base(&self, base: u8) -> Option<u64> {
109        let d = self.denominator_ref();
110        assert!((2..=36).contains(&base));
111        if *d == 1u32 {
112            return Some(0);
113        }
114        let mut temp = None;
115        let mut length = 0;
116        for (f, m) in BASE_PRIME_FACTORS[usize::wrapping_from(base)] {
117            let count = if f == 0 {
118                break;
119            } else if f == 2 {
120                let twos = d.trailing_zeros().unwrap();
121                if twos != 0 {
122                    temp = Some(d >> twos);
123                }
124                twos
125            } else {
126                let f = Natural::from(f);
127                let mut count = 0;
128                if let Some(temp) = temp.as_mut() {
129                    while (&*temp).divisible_by(&f) {
130                        temp.div_exact_assign(&f);
131                        count += 1;
132                    }
133                } else if d.divisible_by(&f) {
134                    count = 1;
135                    let mut t = d.div_exact(&f);
136                    while (&t).divisible_by(&f) {
137                        t.div_exact_assign(&f);
138                        count += 1;
139                    }
140                    temp = Some(t);
141                }
142                count
143            };
144            length = max(length, count.div_round(u64::from(m), Ceiling).0);
145        }
146        if let Some(temp) = temp {
147            if temp == 1 { Some(length) } else { None }
148        } else {
149            None
150        }
151    }
152}
153
154pub_test! {floor_log_base_of_abs(x: &Rational, base: &Rational) -> i64 {
155    if let Some(log_base) = base.checked_log_base_2() {
156        match log_base.sign() {
157            Equal => panic!("Cannot take base-1 logarithm"),
158            Greater => x
159                .floor_log_base_2_abs()
160                .div_round(log_base, Floor).0,
161            Less => {
162                -(x.ceiling_log_base_2_abs()
163                    .div_round(-log_base, Ceiling).0)
164            }
165        }
166    } else {
167        log_base_helper(x, base).0
168    }
169}}
170
171fn fmt_zero(f: &mut Formatter, options: ToSciOptions) -> core::fmt::Result {
172    f.write_char('0')?;
173    let scale = if options.get_include_trailing_zeros() {
174        match options.get_size_options() {
175            SciSizeOptions::Complete => None,
176            SciSizeOptions::Scale(scale) => {
177                if scale == 0 {
178                    None
179                } else {
180                    Some(scale)
181                }
182            }
183            SciSizeOptions::Precision(precision) => {
184                if precision == 1 {
185                    None
186                } else {
187                    Some(precision - 1)
188                }
189            }
190        }
191    } else {
192        None
193    };
194    if let Some(scale) = scale {
195        f.write_char('.')?;
196        for _ in 0..scale {
197            f.write_char('0')?;
198        }
199    }
200    Ok(())
201}
202
203impl ToSci for Rational {
204    /// Determines whether a [`Rational`] can be converted to a string using
205    /// [`to_sci`](malachite_base::num::conversion::traits::ToSci::to_sci) and a particular set of
206    /// options.
207    ///
208    /// # Worst-case complexity
209    /// $T(n) = O(n \log n \log\log n)$
210    ///
211    /// $M(n) = O(n \log n)$
212    ///
213    /// where $T$ is time, $M$ is additional memory, and $n$ is `max(self.significant_bits(), s)`,
214    /// where `s` depends on the size type specified in `options`.
215    /// - If `options` has `scale` specified, then `s` is `options.scale`.
216    /// - If `options` has `precision` specified, then `s` is `options.precision`.
217    /// - If `options` has `size_complete` specified, then `s` is `self.denominator` (not the log of
218    ///   the denominator!). This reflects the fact that setting `size_complete` might result in a
219    ///   very long string.
220    ///
221    /// # Examples
222    /// ```
223    /// use malachite_base::num::conversion::string::options::ToSciOptions;
224    /// use malachite_base::num::conversion::traits::ToSci;
225    /// use malachite_base::rounding_modes::RoundingMode::*;
226    /// use malachite_q::Rational;
227    ///
228    /// let mut options = ToSciOptions::default();
229    /// assert!(Rational::from(123u8).fmt_sci_valid(options));
230    /// assert!(Rational::from(u128::MAX).fmt_sci_valid(options));
231    /// // u128::MAX has more than 16 significant digits
232    /// options.set_rounding_mode(Exact);
233    /// assert!(!Rational::from(u128::MAX).fmt_sci_valid(options));
234    /// options.set_precision(50);
235    /// assert!(Rational::from(u128::MAX).fmt_sci_valid(options));
236    ///
237    /// let mut options = ToSciOptions::default();
238    /// options.set_size_complete();
239    /// // 1/3 is non-terminating in base 10...
240    /// assert!(!Rational::from_signeds(1, 3).fmt_sci_valid(options));
241    /// options.set_size_complete();
242    ///
243    /// // ...but is terminating in base 36
244    /// options.set_base(36);
245    /// assert!(Rational::from_signeds(1, 3).fmt_sci_valid(options));
246    /// ```
247    fn fmt_sci_valid(&self, options: ToSciOptions) -> bool {
248        if *self == 0 {
249            return true;
250        }
251        if let SciSizeOptions::Complete = options.get_size_options() {
252            return self
253                .length_after_point_in_small_base(options.get_base())
254                .is_some();
255        }
256        if options.get_rounding_mode() != Exact {
257            return true;
258        }
259        let q_base = Self::from(options.get_base());
260        let scale = match options.get_size_options() {
261            SciSizeOptions::Precision(precision) => {
262                let log = floor_log_base_of_abs(self, &q_base);
263                i64::exact_from(precision - 1) - log
264            }
265            SciSizeOptions::Scale(scale) => i64::exact_from(scale),
266            _ => unreachable!(),
267        };
268        (self * q_base.pow(scale)).is_integer()
269    }
270
271    /// Converts a [`Rational` ]to a string using a specified base, possibly formatting the number
272    /// using scientific notation.
273    ///
274    /// See [`ToSciOptions`] for details on the available options.
275    ///
276    /// # Worst-case complexity
277    /// $T(n) = O(n \log n \log\log n)$
278    ///
279    /// $M(n) = O(n \log n)$
280    ///
281    /// where $T$ is time, $M$ is additional memory, and $n$ is `max(self.significant_bits(), s)`,
282    /// where `s` depends on the size type specified in `options`.
283    /// - If `options` has `scale` specified, then `s` is `options.scale`.
284    /// - If `options` has `precision` specified, then `s` is `options.precision`.
285    /// - If `options` has `size_complete` specified, then `s` is `self.denominator` (not the log of
286    ///   the denominator!). This reflects the fact that setting `size_complete` might result in a
287    ///   very long string.
288    ///
289    /// # Panics
290    /// Panics if `options.rounding_mode` is `Exact`, but the size options are such that the input
291    /// must be rounded.
292    ///
293    /// # Examples
294    /// ```
295    /// use malachite_base::num::arithmetic::traits::PowerOf2;
296    /// use malachite_base::num::conversion::string::options::ToSciOptions;
297    /// use malachite_base::num::conversion::traits::ToSci;
298    /// use malachite_base::rounding_modes::RoundingMode::*;
299    /// use malachite_q::Rational;
300    ///
301    /// let q = Rational::from_signeds(22, 7);
302    /// let mut options = ToSciOptions::default();
303    /// assert_eq!(
304    ///     q.to_sci_with_options(options).to_string(),
305    ///     "3.142857142857143"
306    /// );
307    ///
308    /// options.set_precision(3);
309    /// assert_eq!(q.to_sci_with_options(options).to_string(), "3.14");
310    ///
311    /// options.set_rounding_mode(Ceiling);
312    /// assert_eq!(q.to_sci_with_options(options).to_string(), "3.15");
313    ///
314    /// options = ToSciOptions::default();
315    /// options.set_base(20);
316    /// assert_eq!(
317    ///     q.to_sci_with_options(options).to_string(),
318    ///     "3.2h2h2h2h2h2h2h3"
319    /// );
320    ///
321    /// options.set_uppercase();
322    /// assert_eq!(
323    ///     q.to_sci_with_options(options).to_string(),
324    ///     "3.2H2H2H2H2H2H2H3"
325    /// );
326    ///
327    /// options.set_base(2);
328    /// options.set_rounding_mode(Floor);
329    /// options.set_precision(19);
330    /// assert_eq!(
331    ///     q.to_sci_with_options(options).to_string(),
332    ///     "11.001001001001001"
333    /// );
334    ///
335    /// options.set_include_trailing_zeros(true);
336    /// assert_eq!(
337    ///     q.to_sci_with_options(options).to_string(),
338    ///     "11.00100100100100100"
339    /// );
340    ///
341    /// let q = Rational::from_unsigneds(936851431250u64, 1397u64);
342    /// let mut options = ToSciOptions::default();
343    /// options.set_precision(6);
344    /// assert_eq!(q.to_sci_with_options(options).to_string(), "6.70617e8");
345    ///
346    /// options.set_e_uppercase();
347    /// assert_eq!(q.to_sci_with_options(options).to_string(), "6.70617E8");
348    ///
349    /// options.set_force_exponent_plus_sign(true);
350    /// assert_eq!(q.to_sci_with_options(options).to_string(), "6.70617E+8");
351    ///
352    /// let q = Rational::from_signeds(123i64, 45678909876i64);
353    /// let mut options = ToSciOptions::default();
354    /// assert_eq!(
355    ///     q.to_sci_with_options(options).to_string(),
356    ///     "2.692708743135418e-9"
357    /// );
358    ///
359    /// options.set_neg_exp_threshold(-10);
360    /// assert_eq!(
361    ///     q.to_sci_with_options(options).to_string(),
362    ///     "0.000000002692708743135418"
363    /// );
364    ///
365    /// let q = Rational::power_of_2(-30i64);
366    /// let mut options = ToSciOptions::default();
367    /// assert_eq!(
368    ///     q.to_sci_with_options(options).to_string(),
369    ///     "9.313225746154785e-10"
370    /// );
371    ///
372    /// options.set_size_complete();
373    /// assert_eq!(
374    ///     q.to_sci_with_options(options).to_string(),
375    ///     "9.31322574615478515625e-10"
376    /// );
377    /// ```
378    fn fmt_sci(&self, f: &mut Formatter, options: ToSciOptions) -> core::fmt::Result {
379        if *self == 0u32 {
380            return fmt_zero(f, options);
381        }
382        if *self < 0u32 {
383            f.write_char('-')?;
384        }
385        let base = options.get_base();
386        let q_base = Self::from(base);
387        let mut trim_zeros = !options.get_include_trailing_zeros();
388        let mut log = floor_log_base_of_abs(self, &q_base);
389        // Here, precision 0 means that we're rounding down to zero
390        let (mut scale, mut precision) = match options.get_size_options() {
391            SciSizeOptions::Complete => {
392                trim_zeros = false;
393                let scale = self
394                    .length_after_point_in_small_base(base)
395                    .unwrap_or_else(|| {
396                        panic!("{self} has a non-terminating expansion in base {base}")
397                    });
398                let precision = i64::exact_from(scale) + log + 1;
399                assert!(precision > 0);
400                (i64::exact_from(scale), precision)
401            }
402            SciSizeOptions::Scale(scale) => {
403                (i64::exact_from(scale), i64::exact_from(scale) + log + 1)
404            }
405            SciSizeOptions::Precision(precision) => (
406                i64::exact_from(precision - 1) - log,
407                i64::exact_from(precision),
408            ),
409        };
410        let n = Integer::rounding_from(self * q_base.pow(scale), options.get_rounding_mode())
411            .0
412            .abs();
413        if precision <= 0 {
414            // e.g. we're in base 10, self is 0.01 or 0.000001, but scale is 1
415            if n == 0u32 {
416                return fmt_zero(f, options);
417            } else if n == 1u32 {
418                precision = 1;
419                log = -scale;
420            } else {
421                panic!("Bug: precision <= 0 must mean self.abs() rounds to 0 or 1");
422            };
423        }
424        let mut cs = if options.get_lowercase() {
425            n.to_string_base(base)
426        } else {
427            n.to_string_base_upper(base)
428        }
429        .into_bytes();
430        let mut precision = usize::exact_from(precision);
431        if cs.len() == precision + 1 {
432            // We rounded up to a power of the base, so precision is greater than we expected. If
433            // the options specify the precision, we need to adjust.
434            log += 1;
435            match options.get_size_options() {
436                SciSizeOptions::Complete => panic!(),
437                SciSizeOptions::Precision(_) => {
438                    scale -= 1;
439                    assert_eq!(cs.pop().unwrap(), b'0');
440                }
441                SciSizeOptions::Scale(_) => {
442                    precision += 1;
443                }
444            }
445        }
446        assert_eq!(cs.len(), precision);
447        if log <= options.get_neg_exp_threshold() || scale < 0 {
448            assert_ne!(log, 0);
449            // exponent
450            if trim_zeros {
451                let trailing_zeros = cs.iter().rev().take_while(|&&c| c == b'0').count();
452                precision -= trailing_zeros;
453                cs.truncate(precision);
454            }
455            if precision > 1 {
456                cs.push(0);
457                cs.copy_within(1..precision, 2);
458                cs[1] = b'.';
459            }
460            f.write_str(&String::from_utf8(cs).unwrap())?;
461            write_exponent(f, options, log)
462        } else if scale == 0 {
463            // no exponent or point
464            f.write_str(&String::from_utf8(cs).unwrap())
465        } else {
466            // no exponent
467            if trim_zeros {
468                let trailing_zeros = cs
469                    .iter()
470                    .rev()
471                    .take(usize::exact_from(scale))
472                    .take_while(|&&c| c == b'0')
473                    .count();
474                precision -= trailing_zeros;
475                cs.truncate(precision);
476            }
477            if log < 0 {
478                f.write_char('0')?;
479                f.write_char('.')?;
480                for _ in 0..-log - 1 {
481                    f.write_char('0')?;
482                }
483            } else {
484                let digits_before = usize::exact_from(log) + 1;
485                if precision > digits_before {
486                    cs.push(0);
487                    cs.copy_within(digits_before..precision, digits_before + 1);
488                    cs[digits_before] = b'.';
489                }
490            }
491            f.write_str(&String::from_utf8(cs).unwrap())
492        }
493    }
494}