Skip to main content

malachite_float/float/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
9// `get_str`-based scientific-string conversion, driven by `ToSciOptions`: the engine behind
10// `Float`'s `Display` and power-of-2-base formatting traits (to_string.rs) and its `ToSci`
11// implementation.
12//
13// The semantics mirror `Rational::fmt_sci` (malachite-q's to_sci.rs) — the same size options,
14// negative-exponent threshold, trailing-zero handling, and digit rounding — with one addition,
15// the `Float` `Display` convention: the output of a finite value always contains a point, so a
16// string that would otherwise lack one gets `.0` appended to its mantissa (`255` becomes `255.0`,
17// `8e-7` becomes `8.0e-7`).
18
19use crate::Float;
20use crate::InnerFloat::{Finite, Infinity, NaN, Zero};
21use crate::float::conversion::string::format_float::strip_trailing_zeros;
22use crate::float::conversion::string::get_str::get_str;
23use alloc::string::String;
24use alloc::vec;
25use alloc::vec::Vec;
26use core::cmp::Ordering::*;
27use core::fmt::{Formatter, Write};
28use malachite_base::num::arithmetic::traits::{Abs, DivRound, DivisibleBy, Pow};
29use malachite_base::num::conversion::string::options::{SciSizeOptions, ToSciOptions};
30use malachite_base::num::conversion::traits::{ExactFrom, IntegerMantissaAndExponent, ToSci};
31use malachite_base::num::logic::traits::SignificantBits;
32use malachite_base::rounding_modes::RoundingMode::*;
33use malachite_nz::natural::Natural;
34use malachite_q::Rational;
35
36// The number of base-`base` digits after the point in the exact expansion of the finite nonzero
37// `Float` with least binary exponent `k` (that is, whose odd mantissa is scaled by 2^k), or `None`
38// if the expansion is non-terminating. A `Float` is a dyadic rational, so the expansion terminates
39// iff the value is an integer or the base is even; when 2^v is the largest power of 2 dividing the
40// base, clearing 2^-|k| takes ceil(|k| / v) digits. This is the `Float` analogue of
41// `Rational::length_after_point_in_small_base`.
42fn length_after_point(k: i64, base: i64) -> Option<u64> {
43    if k >= 0 {
44        Some(0)
45    } else {
46        match u64::from(base.trailing_zeros()) {
47            0 => None,
48            v => Some(k.unsigned_abs().div_round(v, Ceiling).0),
49        }
50    }
51}
52
53// The exact floor of log_`base` of `|x|`, for finite nonzero `x`. `get_str` returns the exponent
54// `e` such that the rounded value is 0.ddd... * base^e; with one digit and truncating rounding no
55// magnitude round-up can occur, so `e - 1` is exact.
56fn floor_log_base(x: &Float, base: i64) -> i64 {
57    get_str(x, base, 1, Down).unwrap().1 - 1
58}
59
60// Writes the exponent part: the exponent character, the sign (an explicit `+` only when forced or
61// when the base is 15 or greater, to distinguish the exponent character from the digit 'e'), and
62// the exponent. This is `write_exponent` from malachite-base's to_sci.rs, writing to a `String`.
63fn push_exponent(out: &mut String, options: ToSciOptions, exp: i64) {
64    out.push(if options.get_e_lowercase() { 'e' } else { 'E' });
65    if exp > 0 && (options.get_force_exponent_plus_sign() || options.get_base() >= 15) {
66        out.push('+');
67    }
68    write!(out, "{exp}").unwrap();
69}
70
71// The string for a zero `Float` with the given sign. This mirrors `fmt_zero` from malachite-q's
72// to_sci.rs, plus the trailing-`.0` convention.
73fn zero_to_string(neg: bool, options: ToSciOptions) -> String {
74    let mut out = String::new();
75    if neg {
76        out.push('-');
77    }
78    out.push('0');
79    if options.get_include_trailing_zeros() {
80        let zeros = match options.get_size_options() {
81            SciSizeOptions::Complete => 0,
82            SciSizeOptions::Scale(scale) => scale,
83            SciSizeOptions::Precision(precision) => precision - 1,
84        };
85        if zeros != 0 {
86            out.push('.');
87            for _ in 0..zeros {
88                out.push('0');
89            }
90        }
91    }
92    if !out.contains('.') {
93        out.push_str(".0");
94    }
95    out
96}
97
98crate_test_fn! {
99// Determines whether `x` can be converted to a string using `to_sci_string` and a particular set of
100// options; this is the engine of `ToSci::fmt_sci_valid`. Mirrors `Rational::fmt_sci_valid`: with
101// the `Complete` size option the expansion must terminate, and with the `Exact` rounding mode the
102// value must be representable in the digits the size options allow.
103to_sci_valid(x: &Float, options: ToSciOptions) -> bool {
104    if !matches!(x, Float(Finite { .. })) {
105        // NaN, infinities, and zeros have fixed representations
106        return true;
107    }
108    let base = i64::from(options.get_base());
109    let min_scale = length_after_point(x.integer_exponent(), base);
110    if let SciSizeOptions::Complete = options.get_size_options() {
111        return min_scale.is_some();
112    }
113    if options.get_rounding_mode() != Exact {
114        return true;
115    }
116    let Some(min_scale) = min_scale else {
117        return false;
118    };
119    let min_scale = i64::exact_from(min_scale);
120    match options.get_size_options() {
121        SciSizeOptions::Scale(scale) => min_scale <= i64::exact_from(scale),
122        SciSizeOptions::Precision(precision) => {
123            let s = i64::exact_from(precision - 1) - floor_log_base(x, base);
124            if s >= 0 {
125                min_scale <= s
126            } else {
127                // The last digit sits at position -s > 0, so the value must be divisible by
128                // base^(-s): 2^(-s * v) must divide via the binary exponent, and the base's odd
129                // part to the -s must divide the odd mantissa. (`min_scale` cannot see this: it
130                // measures digits after the point, and gives no credit for trailing zeros before
131                // it.)
132                let t = s.unsigned_abs();
133                let v = i64::from(base.trailing_zeros());
134                if x.integer_exponent() < i64::exact_from(t) * v {
135                    return false;
136                }
137                let odd_base = base >> v;
138                if odd_base == 1 {
139                    return true;
140                }
141                let mantissa = x.integer_mantissa();
142                // odd_base >= 3, so odd_base^t > 2^t > mantissa: not divisible. This also keeps the
143                // power below from being enormous.
144                if t >= mantissa.significant_bits() {
145                    return false;
146                }
147                mantissa.divisible_by(Natural::from(u64::exact_from(odd_base)).pow(t))
148            }
149        }
150        SciSizeOptions::Complete => unreachable!(),
151    }
152}}
153
154crate_test_fn! {
155// Converts a `Float` to a string using a specified base, possibly using scientific notation; this
156// is the engine behind `Display`, the power-of-2-base formatting traits, and `ToSci`. See
157// `ToSciOptions` for details on the available options. The `Float` `Display` conventions apply on
158// top of them: NaN and the infinities are rendered as `NaN`, `Infinity`, and `-Infinity`, and the
159// output for any finite value (including zeros) always contains a point, `.0` being appended if
160// necessary.
161//
162// The digits are computed by `get_str`, which rounds the value directly, so this function never
163// materializes the `Float` as a `Rational` (except in one corner case: deciding a `Nearest` tie
164// when the value's magnitude lies within one base-power of a `Scale` boundary).
165//
166// Panics if the rounding mode is `Exact` but the size options are such that the input must be
167// rounded, or if the size option is `Complete` and the expansion is non-terminating (an odd base
168// and a fractional value); `to_sci_valid` identifies both cases.
169to_sci_string(x: &Float, options: ToSciOptions) -> String {
170    let (neg, sign) = match x {
171        Float(NaN) => return String::from("NaN"),
172        Float(Infinity { sign: true }) => return String::from("Infinity"),
173        Float(Infinity { sign: false }) => return String::from("-Infinity"),
174        Float(Zero { sign }) => return zero_to_string(!*sign, options),
175        Float(Finite { sign, .. }) => (!*sign, *sign),
176    };
177    let base = i64::from(options.get_base());
178    let rm = options.get_rounding_mode();
179    let trim_zeros = !options.get_include_trailing_zeros()
180        && options.get_size_options() != SciSizeOptions::Complete;
181    let log = floor_log_base(x, base);
182    // `scale` is the number of digits after the point and `precision` the total number of digits,
183    // as in `Rational::fmt_sci`. A nonpositive `precision` means the value rounds to 0 or to 1 unit
184    // at the requested scale.
185    let (scale, precision) = match options.get_size_options() {
186        SciSizeOptions::Complete => {
187            let scale = length_after_point(x.integer_exponent(), base).unwrap_or_else(|| {
188                panic!("{x} has a non-terminating expansion in base {base}")
189            });
190            let precision = i64::exact_from(scale) + log + 1;
191            // the digits of the exact expansion begin at the first significant digit
192            assert!(precision > 0);
193            (i64::exact_from(scale), precision)
194        }
195        SciSizeOptions::Scale(scale) => {
196            (i64::exact_from(scale), i64::exact_from(scale) + log + 1)
197        }
198        SciSizeOptions::Precision(precision) => (
199            i64::exact_from(precision - 1) - log,
200            i64::exact_from(precision),
201        ),
202    };
203    let (digits, log) = if precision <= 0 {
204        // 0 < |x| * base^scale < 1: the value rounds to 0 or to 1 in the last place.
205        let round_up_to_one = match rm {
206            Up => true,
207            Down => false,
208            Floor => neg,
209            Ceiling => !neg,
210            Exact => panic!(
211                "Exact rounding was requested, but {x} is not exactly representable with {scale} \
212                digits after the point",
213            ),
214            // |x| < base^(log + 1) <= base^(-scale); it rounds up iff it exceeds base^-scale / 2,
215            // which requires log + 1 == -scale (one base-power below the boundary and it is already
216            // at most half). A tie rounds to the even option, 0.
217            Nearest => {
218                log + 1 == -scale && {
219                    let two_x = Rational::exact_from(x).abs() << 1u64;
220                    two_x > Rational::from(base).pow(-scale)
221                }
222            }
223        };
224        if round_up_to_one {
225            (vec![b'1'], -scale)
226        } else {
227            return zero_to_string(neg, options);
228        }
229    } else {
230        let m = usize::exact_from(precision);
231        // a negative base makes `get_str` produce uppercase digits
232        let get_str_base = if options.get_lowercase() { base } else { -base };
233        let (s, e, o) = get_str(x, get_str_base, m, rm).unwrap();
234        let mut digits = if neg { s[1..].to_vec() } else { s };
235        debug_assert!(options.get_size_options() != SciSizeOptions::Complete || o == Equal);
236        let new_log = e - 1;
237        // Rounding up to a power of the base adds an integral digit. With a requested scale the
238        // number of digits after the point must not shrink, so widen the digit string; this mirrors
239        // `Rational::fmt_sci`, which widens its precision. (With a requested precision the digit
240        // count is fixed and the scale shrinks instead, which the layout below derives from
241        // `new_log`; and a `Complete` conversion is exact, so no rounding up can occur.)
242        if new_log > log && matches!(options.get_size_options(), SciSizeOptions::Scale(_)) {
243            digits.push(b'0');
244        }
245        (digits, new_log)
246    };
247    // the number of digits after the point, for the padding assertions below
248    let target_scale = match options.get_size_options() {
249        SciSizeOptions::Precision(_) => i64::exact_from(digits.len()) - 1 - log,
250        _ => scale,
251    };
252    let mut mantissa: Vec<u8> = Vec::new();
253    let mut exponent = None;
254    if log <= options.get_neg_exp_threshold() || target_scale < 0 {
255        // scientific notation: one digit, the rest after a point, and an exponent
256        let ds = if trim_zeros {
257            strip_trailing_zeros(&digits)
258        } else {
259            &digits
260        };
261        mantissa.push(ds[0]);
262        if ds.len() > 1 {
263            mantissa.push(b'.');
264            mantissa.extend_from_slice(&ds[1..]);
265        }
266        exponent = Some(log);
267    } else if log < 0 {
268        // no exponent; the value is less than 1, so all digits are fractional
269        let ds = if trim_zeros {
270            strip_trailing_zeros(&digits)
271        } else {
272            &digits
273        };
274        mantissa.extend_from_slice(b"0.");
275        mantissa.resize(2 + usize::exact_from(-log - 1), b'0');
276        mantissa.extend_from_slice(ds);
277        debug_assert!(
278            trim_zeros || -log - 1 + i64::exact_from(ds.len()) == target_scale,
279            "fractional length mismatch"
280        );
281    } else {
282        // no exponent; split the digits at the point
283        let digits_before = usize::exact_from(log + 1);
284        mantissa.extend_from_slice(&digits[..digits_before]);
285        let frac = if trim_zeros {
286            strip_trailing_zeros(&digits[digits_before..])
287        } else {
288            &digits[digits_before..]
289        };
290        if !frac.is_empty() {
291            mantissa.push(b'.');
292            mantissa.extend_from_slice(frac);
293        }
294        debug_assert!(
295            trim_zeros || i64::exact_from(frac.len()) == target_scale,
296            "fractional length mismatch"
297        );
298    }
299    // the `Float` `Display` convention: a finite value always shows a point
300    if !mantissa.contains(&b'.') {
301        mantissa.extend_from_slice(b".0");
302    }
303    let mut out = String::new();
304    if !sign {
305        out.push('-');
306    }
307    out.push_str(core::str::from_utf8(&mantissa).unwrap());
308    if let Some(exp) = exponent {
309        push_exponent(&mut out, options, exp);
310    }
311    out
312}}
313
314impl ToSci for Float {
315    /// Determines whether a [`Float`] can be converted to a string using
316    /// [`to_sci`](malachite_base::num::conversion::traits::ToSci::to_sci) and a particular set of
317    /// options.
318    ///
319    /// NaN, the infinities, and zeros have fixed representations and are always convertible. A
320    /// finite nonzero [`Float`] is convertible unless the options request more digits than the
321    /// value has: if the size option is `Complete`, the value's expansion in the chosen base must
322    /// terminate (any [`Float`] is a dyadic rational, so this holds whenever the value is an
323    /// integer or the base is even), and if the rounding mode is `Exact`, the value must be exactly
324    /// representable in the digits the size options allow.
325    ///
326    /// # Worst-case complexity
327    /// $T(n) = O(n \log n \log\log n)$
328    ///
329    /// $M(n) = O(n \log n)$
330    ///
331    /// where $T$ is time, $M$ is additional memory, and $n$ is `self.significant_bits()`.
332    ///
333    /// # Examples
334    /// ```
335    /// use malachite_base::num::basic::traits::NaN;
336    /// use malachite_base::num::conversion::string::options::ToSciOptions;
337    /// use malachite_base::num::conversion::traits::ToSci;
338    /// use malachite_base::rounding_modes::RoundingMode::*;
339    /// use malachite_float::Float;
340    ///
341    /// let mut options = ToSciOptions::default();
342    /// assert!(Float::NAN.fmt_sci_valid(options));
343    /// // 1.5 has 2 significant bits
344    /// assert!(Float::from(1.5).fmt_sci_valid(options));
345    /// options.set_rounding_mode(Exact);
346    /// options.set_precision(1);
347    /// assert!(!Float::from(1.5).fmt_sci_valid(options));
348    /// options.set_precision(2);
349    /// assert!(Float::from(1.5).fmt_sci_valid(options));
350    ///
351    /// let mut options = ToSciOptions::default();
352    /// options.set_size_complete();
353    /// // 0.5 is non-terminating in base 3...
354    /// options.set_base(3);
355    /// assert!(!Float::from(0.5).fmt_sci_valid(options));
356    /// // ...but is terminating in base 32
357    /// options.set_base(32);
358    /// assert!(Float::from(0.5).fmt_sci_valid(options));
359    /// ```
360    #[inline]
361    fn fmt_sci_valid(&self, options: ToSciOptions) -> bool {
362        to_sci_valid(self, options)
363    }
364
365    /// Converts a [`Float`] to a string using a specified base, possibly formatting the number
366    /// using scientific notation.
367    ///
368    /// See [`ToSciOptions`] for details on the available options. The [`Float`] `Display`
369    /// conventions apply on top of them: NaN and the infinities are rendered as `NaN`, `Infinity`,
370    /// and `-Infinity`, and the output for any finite value (including zeros) always contains a
371    /// point, `.0` being appended if necessary. Note that the digits are those of the value's
372    /// actual expansion, rounded to the requested size; unlike `Display`, which shows the shortest
373    /// string that rounds back to the value, no round-trip shortening occurs.
374    ///
375    /// The digits are computed by rounding the value directly, so the [`Float`] is never
376    /// materialized as a [`Rational`].
377    ///
378    /// # Worst-case complexity
379    /// $T(n) = O(n \log n \log\log n)$
380    ///
381    /// $M(n) = O(n \log n)$
382    ///
383    /// where $T$ is time, $M$ is additional memory, and $n$ is `max(self.significant_bits(), s)`,
384    /// where `s` depends on the size type specified in `options`.
385    /// - If `options` has `scale` specified, then `s` is `options.scale`.
386    /// - If `options` has `precision` specified, then `s` is `options.precision`.
387    /// - If `options` has `size_complete` specified, then `s` is
388    ///   `self.get_exponent().unwrap().unsigned_abs()`. This reflects the fact that setting
389    ///   `size_complete` might result in a very long string when the value's magnitude is very
390    ///   large or very small.
391    ///
392    /// # Panics
393    /// Panics if `options.rounding_mode` is `Exact`, but the size options are such that the input
394    /// must be rounded, or if the size option is `Complete` but `self` has a non-terminating
395    /// expansion in the chosen base (a fractional value in an odd base).
396    ///
397    /// # Examples
398    /// ```
399    /// use malachite_base::num::arithmetic::traits::PowerOf2;
400    /// use malachite_base::num::basic::traits::NaN;
401    /// use malachite_base::num::conversion::string::options::ToSciOptions;
402    /// use malachite_base::num::conversion::traits::ToSci;
403    /// use malachite_base::rounding_modes::RoundingMode::*;
404    /// use malachite_float::Float;
405    ///
406    /// assert_eq!(Float::NAN.to_sci().to_string(), "NaN");
407    /// // a finite value always shows a point
408    /// assert_eq!(Float::from(255.0).to_sci().to_string(), "255.0");
409    ///
410    /// let x = Float::from(1234.5);
411    /// let mut options = ToSciOptions::default();
412    /// assert_eq!(x.to_sci_with_options(options).to_string(), "1234.5");
413    /// options.set_precision(4);
414    /// assert_eq!(x.to_sci_with_options(options).to_string(), "1234.0");
415    /// options.set_precision(2);
416    /// assert_eq!(x.to_sci_with_options(options).to_string(), "1.2e3");
417    ///
418    /// let x = Float::from(1.5);
419    /// let mut options = ToSciOptions::default();
420    /// options.set_scale(0);
421    /// assert_eq!(x.to_sci_with_options(options).to_string(), "2.0");
422    /// options.set_rounding_mode(Down);
423    /// assert_eq!(x.to_sci_with_options(options).to_string(), "1.0");
424    ///
425    /// let mut options = ToSciOptions::default();
426    /// options.set_base(20);
427    /// assert_eq!(x.to_sci_with_options(options).to_string(), "1.a");
428    /// options.set_uppercase();
429    /// assert_eq!(x.to_sci_with_options(options).to_string(), "1.A");
430    ///
431    /// // in bases 15 and up, a positive exponent always gets an explicit sign, to distinguish
432    /// // the exponent indicator from the digit 'e'
433    /// let mut options = ToSciOptions::default();
434    /// options.set_base(16);
435    /// options.set_precision(2);
436    /// assert_eq!(
437    ///     Float::from(1000000.0)
438    ///         .to_sci_with_options(options)
439    ///         .to_string(),
440    ///     "f.4e+4"
441    /// );
442    ///
443    /// // 2^-17, a 1-bit value, printed with its actual digits
444    /// let x = Float::power_of_2(-17i64);
445    /// let mut options = ToSciOptions::default();
446    /// assert_eq!(
447    ///     x.to_sci_with_options(options).to_string(),
448    ///     "7.62939453125e-6"
449    /// );
450    /// options.set_e_uppercase();
451    /// assert_eq!(
452    ///     x.to_sci_with_options(options).to_string(),
453    ///     "7.62939453125E-6"
454    /// );
455    /// options.set_neg_exp_threshold(-10);
456    /// assert_eq!(
457    ///     x.to_sci_with_options(options).to_string(),
458    ///     "0.00000762939453125"
459    /// );
460    /// ```
461    #[inline]
462    fn fmt_sci(&self, f: &mut Formatter, options: ToSciOptions) -> core::fmt::Result {
463        f.write_str(&to_sci_string(self, options))
464    }
465}