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::arithmetic::log_base::log_base_helper;
10use crate::Rational;
11use alloc::string::String;
12use core::cmp::{max, Ordering::*};
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 {
148                Some(length)
149            } else {
150                None
151            }
152        } else {
153            None
154        }
155    }
156}
157
158pub_test! {floor_log_base_of_abs(x: &Rational, base: &Rational) -> i64 {
159    if let Some(log_base) = base.checked_log_base_2() {
160        match log_base.sign() {
161            Equal => panic!("Cannot take base-1 logarithm"),
162            Greater => x
163                .floor_log_base_2_abs()
164                .div_round(log_base, Floor).0,
165            Less => {
166                -(x.ceiling_log_base_2_abs()
167                    .div_round(-log_base, Ceiling).0)
168            }
169        }
170    } else {
171        log_base_helper(x, base).0
172    }
173}}
174
175fn fmt_zero(f: &mut Formatter, options: ToSciOptions) -> core::fmt::Result {
176    f.write_char('0')?;
177    let scale = if options.get_include_trailing_zeros() {
178        match options.get_size_options() {
179            SciSizeOptions::Complete => None,
180            SciSizeOptions::Scale(scale) => {
181                if scale == 0 {
182                    None
183                } else {
184                    Some(scale)
185                }
186            }
187            SciSizeOptions::Precision(precision) => {
188                if precision == 1 {
189                    None
190                } else {
191                    Some(precision - 1)
192                }
193            }
194        }
195    } else {
196        None
197    };
198    if let Some(scale) = scale {
199        f.write_char('.')?;
200        for _ in 0..scale {
201            f.write_char('0')?;
202        }
203    }
204    Ok(())
205}
206
207impl ToSci for Rational {
208    /// Determines whether a [`Rational`] can be converted to a string using
209    /// [`to_sci`](malachite_base::num::conversion::traits::ToSci::to_sci) and a particular set of
210    /// options.
211    ///
212    /// # Worst-case complexity
213    /// $T(n) = O(n \log n \log\log n)$
214    ///
215    /// $M(n) = O(n \log n)$
216    ///
217    /// where $T$ is time, $M$ is additional memory, and $n$ is `max(self.significant_bits(), s)`,
218    /// where `s` depends on the size type specified in `options`.
219    /// - If `options` has `scale` specified, then `s` is `options.scale`.
220    /// - If `options` has `precision` specified, then `s` is `options.precision`.
221    /// - If `options` has `size_complete` specified, then `s` is `self.denominator` (not the log of
222    ///   the denominator!). This reflects the fact that setting `size_complete` might result in a
223    ///   very long string.
224    ///
225    /// # Examples
226    /// ```
227    /// use malachite_base::num::conversion::string::options::ToSciOptions;
228    /// use malachite_base::num::conversion::traits::ToSci;
229    /// use malachite_base::rounding_modes::RoundingMode::*;
230    /// use malachite_q::Rational;
231    ///
232    /// let mut options = ToSciOptions::default();
233    /// assert!(Rational::from(123u8).fmt_sci_valid(options));
234    /// assert!(Rational::from(u128::MAX).fmt_sci_valid(options));
235    /// // u128::MAX has more than 16 significant digits
236    /// options.set_rounding_mode(Exact);
237    /// assert!(!Rational::from(u128::MAX).fmt_sci_valid(options));
238    /// options.set_precision(50);
239    /// assert!(Rational::from(u128::MAX).fmt_sci_valid(options));
240    ///
241    /// let mut options = ToSciOptions::default();
242    /// options.set_size_complete();
243    /// // 1/3 is non-terminating in base 10...
244    /// assert!(!Rational::from_signeds(1, 3).fmt_sci_valid(options));
245    /// options.set_size_complete();
246    ///
247    /// // ...but is terminating in base 36
248    /// options.set_base(36);
249    /// assert!(Rational::from_signeds(1, 3).fmt_sci_valid(options));
250    /// ```
251    fn fmt_sci_valid(&self, options: ToSciOptions) -> bool {
252        if *self == 0 {
253            return true;
254        }
255        if let SciSizeOptions::Complete = options.get_size_options() {
256            return self
257                .length_after_point_in_small_base(options.get_base())
258                .is_some();
259        }
260        if options.get_rounding_mode() != Exact {
261            return true;
262        }
263        let q_base = Rational::from(options.get_base());
264        let scale = match options.get_size_options() {
265            SciSizeOptions::Precision(precision) => {
266                let log = floor_log_base_of_abs(self, &q_base);
267                i64::exact_from(precision - 1) - log
268            }
269            SciSizeOptions::Scale(scale) => i64::exact_from(scale),
270            _ => unreachable!(),
271        };
272        (self * q_base.pow(scale)).is_integer()
273    }
274
275    /// Converts a [`Rational` ]to a string using a specified base, possibly formatting the number
276    /// using scientific notation.
277    ///
278    /// See [`ToSciOptions`] for details on the available options.
279    ///
280    /// # Worst-case complexity
281    /// $T(n) = O(n \log n \log\log n)$
282    ///
283    /// $M(n) = O(n \log n)$
284    ///
285    /// where $T$ is time, $M$ is additional memory, and $n$ is `max(self.significant_bits(), s)`,
286    /// where `s` depends on the size type specified in `options`.
287    /// - If `options` has `scale` specified, then `s` is `options.scale`.
288    /// - If `options` has `precision` specified, then `s` is `options.precision`.
289    /// - If `options` has `size_complete` specified, then `s` is `self.denominator` (not the log of
290    ///   the denominator!). This reflects the fact that setting `size_complete` might result in a
291    ///   very long string.
292    ///
293    /// # Panics
294    /// Panics if `options.rounding_mode` is `Exact`, but the size options are such that the input
295    /// must be rounded.
296    ///
297    /// # Examples
298    /// ```
299    /// use malachite_base::num::arithmetic::traits::PowerOf2;
300    /// use malachite_base::num::conversion::string::options::ToSciOptions;
301    /// use malachite_base::num::conversion::traits::ToSci;
302    /// use malachite_base::rounding_modes::RoundingMode::*;
303    /// use malachite_q::Rational;
304    ///
305    /// let q = Rational::from_signeds(22, 7);
306    /// let mut options = ToSciOptions::default();
307    /// assert_eq!(
308    ///     q.to_sci_with_options(options).to_string(),
309    ///     "3.142857142857143"
310    /// );
311    ///
312    /// options.set_precision(3);
313    /// assert_eq!(q.to_sci_with_options(options).to_string(), "3.14");
314    ///
315    /// options.set_rounding_mode(Ceiling);
316    /// assert_eq!(q.to_sci_with_options(options).to_string(), "3.15");
317    ///
318    /// options = ToSciOptions::default();
319    /// options.set_base(20);
320    /// assert_eq!(
321    ///     q.to_sci_with_options(options).to_string(),
322    ///     "3.2h2h2h2h2h2h2h3"
323    /// );
324    ///
325    /// options.set_uppercase();
326    /// assert_eq!(
327    ///     q.to_sci_with_options(options).to_string(),
328    ///     "3.2H2H2H2H2H2H2H3"
329    /// );
330    ///
331    /// options.set_base(2);
332    /// options.set_rounding_mode(Floor);
333    /// options.set_precision(19);
334    /// assert_eq!(
335    ///     q.to_sci_with_options(options).to_string(),
336    ///     "11.001001001001001"
337    /// );
338    ///
339    /// options.set_include_trailing_zeros(true);
340    /// assert_eq!(
341    ///     q.to_sci_with_options(options).to_string(),
342    ///     "11.00100100100100100"
343    /// );
344    ///
345    /// let q = Rational::from_unsigneds(936851431250u64, 1397u64);
346    /// let mut options = ToSciOptions::default();
347    /// options.set_precision(6);
348    /// assert_eq!(q.to_sci_with_options(options).to_string(), "6.70617e8");
349    ///
350    /// options.set_e_uppercase();
351    /// assert_eq!(q.to_sci_with_options(options).to_string(), "6.70617E8");
352    ///
353    /// options.set_force_exponent_plus_sign(true);
354    /// assert_eq!(q.to_sci_with_options(options).to_string(), "6.70617E+8");
355    ///
356    /// let q = Rational::from_signeds(123i64, 45678909876i64);
357    /// let mut options = ToSciOptions::default();
358    /// assert_eq!(
359    ///     q.to_sci_with_options(options).to_string(),
360    ///     "2.692708743135418e-9"
361    /// );
362    ///
363    /// options.set_neg_exp_threshold(-10);
364    /// assert_eq!(
365    ///     q.to_sci_with_options(options).to_string(),
366    ///     "0.000000002692708743135418"
367    /// );
368    ///
369    /// let q = Rational::power_of_2(-30i64);
370    /// let mut options = ToSciOptions::default();
371    /// assert_eq!(
372    ///     q.to_sci_with_options(options).to_string(),
373    ///     "9.313225746154785e-10"
374    /// );
375    ///
376    /// options.set_size_complete();
377    /// assert_eq!(
378    ///     q.to_sci_with_options(options).to_string(),
379    ///     "9.31322574615478515625e-10"
380    /// );
381    /// ```
382    fn fmt_sci(&self, f: &mut Formatter, options: ToSciOptions) -> core::fmt::Result {
383        if *self == 0u32 {
384            return fmt_zero(f, options);
385        }
386        if *self < 0u32 {
387            f.write_char('-')?;
388        }
389        let base = options.get_base();
390        let q_base = Rational::from(base);
391        let mut trim_zeros = !options.get_include_trailing_zeros();
392        let mut log = floor_log_base_of_abs(self, &q_base);
393        // Here, precision 0 means that we're rounding down to zero
394        let (mut scale, mut precision) = match options.get_size_options() {
395            SciSizeOptions::Complete => {
396                trim_zeros = false;
397                let scale = self
398                    .length_after_point_in_small_base(base)
399                    .unwrap_or_else(|| {
400                        panic!("{self} has a non-terminating expansion in base {base}")
401                    });
402                let precision = i64::exact_from(scale) + log + 1;
403                assert!(precision > 0);
404                (i64::exact_from(scale), precision)
405            }
406            SciSizeOptions::Scale(scale) => {
407                (i64::exact_from(scale), i64::exact_from(scale) + log + 1)
408            }
409            SciSizeOptions::Precision(precision) => (
410                i64::exact_from(precision - 1) - log,
411                i64::exact_from(precision),
412            ),
413        };
414        let n = Integer::rounding_from(self * q_base.pow(scale), options.get_rounding_mode())
415            .0
416            .abs();
417        if precision <= 0 {
418            // e.g. we're in base 10, self is 0.01 or 0.000001, but scale is 1
419            if n == 0u32 {
420                return fmt_zero(f, options);
421            } else if n == 1u32 {
422                precision = 1;
423                log = -scale;
424            } else {
425                panic!("Bug: precision <= 0 must mean self.abs() rounds to 0 or 1");
426            };
427        }
428        let mut cs = if options.get_lowercase() {
429            n.to_string_base(base)
430        } else {
431            n.to_string_base_upper(base)
432        }
433        .into_bytes();
434        let mut precision = usize::exact_from(precision);
435        if cs.len() == precision + 1 {
436            // We rounded up to a power of the base, so precision is greater than we expected. If
437            // the options specify the precision, we need to adjust.
438            log += 1;
439            match options.get_size_options() {
440                SciSizeOptions::Complete => panic!(),
441                SciSizeOptions::Precision(_) => {
442                    scale -= 1;
443                    assert_eq!(cs.pop().unwrap(), b'0');
444                }
445                SciSizeOptions::Scale(_) => {
446                    precision += 1;
447                }
448            }
449        }
450        assert_eq!(cs.len(), precision);
451        if log <= options.get_neg_exp_threshold() || scale < 0 {
452            assert_ne!(log, 0);
453            // exponent
454            if trim_zeros {
455                let trailing_zeros = cs.iter().rev().take_while(|&&c| c == b'0').count();
456                precision -= trailing_zeros;
457                cs.truncate(precision);
458            }
459            if precision > 1 {
460                cs.push(0);
461                cs.copy_within(1..precision, 2);
462                cs[1] = b'.';
463            }
464            f.write_str(&String::from_utf8(cs).unwrap())?;
465            write_exponent(f, options, log)
466        } else if scale == 0 {
467            // no exponent or point
468            f.write_str(&String::from_utf8(cs).unwrap())
469        } else {
470            // no exponent
471            if trim_zeros {
472                let trailing_zeros = cs
473                    .iter()
474                    .rev()
475                    .take(usize::exact_from(scale))
476                    .take_while(|&&c| c == b'0')
477                    .count();
478                precision -= trailing_zeros;
479                cs.truncate(precision);
480            }
481            if log < 0 {
482                f.write_char('0')?;
483                f.write_char('.')?;
484                for _ in 0..-log - 1 {
485                    f.write_char('0')?;
486                }
487            } else {
488                let digits_before = usize::exact_from(log) + 1;
489                if precision > digits_before {
490                    cs.push(0);
491                    cs.copy_within(digits_before..precision, digits_before + 1);
492                    cs[digits_before] = b'.';
493                }
494            }
495            f.write_str(&String::from_utf8(cs).unwrap())
496        }
497    }
498}