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 eventually behind a
11// `ToSci` 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::Write;
28use malachite_base::num::arithmetic::traits::{Abs, DivRound, Pow};
29use malachite_base::num::conversion::string::options::{SciSizeOptions, ToSciOptions};
30use malachite_base::num::conversion::traits::{ExactFrom, IntegerMantissaAndExponent};
31use malachite_base::rounding_modes::RoundingMode::*;
32use malachite_q::Rational;
33
34// The number of base-`base` digits after the point in the exact expansion of the finite nonzero
35// `Float` with least binary exponent `k` (that is, whose odd mantissa is scaled by 2^k), or `None`
36// if the expansion is non-terminating. A `Float` is a dyadic rational, so the expansion terminates
37// iff the value is an integer or the base is even; when 2^v is the largest power of 2 dividing the
38// base, clearing 2^-|k| takes ceil(|k| / v) digits. This is the `Float` analogue of
39// `Rational::length_after_point_in_small_base`.
40fn length_after_point(k: i64, base: i64) -> Option<u64> {
41    if k >= 0 {
42        Some(0)
43    } else {
44        match u64::from(base.trailing_zeros()) {
45            0 => None,
46            v => Some(k.unsigned_abs().div_round(v, Ceiling).0),
47        }
48    }
49}
50
51// The exact floor of log_`base` of `|x|`, for finite nonzero `x`. `get_str` returns the exponent
52// `e` such that the rounded value is 0.ddd... * base^e; with one digit and truncating rounding no
53// magnitude round-up can occur, so `e - 1` is exact.
54fn floor_log_base(x: &Float, base: i64) -> i64 {
55    get_str(x, base, 1, Down).unwrap().1 - 1
56}
57
58// Writes the exponent part: the exponent character, the sign (an explicit `+` only when forced or
59// when the base is 15 or greater, to distinguish the exponent character from the digit 'e'), and
60// the exponent. This is `write_exponent` from malachite-base's to_sci.rs, writing to a `String`.
61fn push_exponent(out: &mut String, options: ToSciOptions, exp: i64) {
62    out.push(if options.get_e_lowercase() { 'e' } else { 'E' });
63    if exp > 0 && (options.get_force_exponent_plus_sign() || options.get_base() >= 15) {
64        out.push('+');
65    }
66    write!(out, "{exp}").unwrap();
67}
68
69// The string for a zero `Float` with the given sign. This mirrors `fmt_zero` from malachite-q's
70// to_sci.rs, plus the trailing-`.0` convention.
71fn zero_to_string(neg: bool, options: ToSciOptions) -> String {
72    let mut out = String::new();
73    if neg {
74        out.push('-');
75    }
76    out.push('0');
77    if options.get_include_trailing_zeros() {
78        let zeros = match options.get_size_options() {
79            SciSizeOptions::Complete => 0,
80            SciSizeOptions::Scale(scale) => scale,
81            SciSizeOptions::Precision(precision) => precision - 1,
82        };
83        if zeros != 0 {
84            out.push('.');
85            for _ in 0..zeros {
86                out.push('0');
87            }
88        }
89    }
90    if !out.contains('.') {
91        out.push_str(".0");
92    }
93    out
94}
95
96pub_crate_test! {
97// Determines whether `x` can be converted to a string using `to_sci_string` and a particular set of
98// options; this is the future `ToSci::fmt_sci_valid`. Mirrors `Rational::fmt_sci_valid`: with the
99// `Complete` size option the expansion must terminate, and with the `Exact` rounding mode the value
100// must be representable in the digits the size options allow.
101to_sci_valid(x: &Float, options: ToSciOptions) -> bool {
102    if !matches!(x, Float(Finite { .. })) {
103        // NaN, infinities, and zeros have fixed representations
104        return true;
105    }
106    let base = i64::from(options.get_base());
107    let min_scale = length_after_point(x.integer_exponent(), base);
108    if let SciSizeOptions::Complete = options.get_size_options() {
109        return min_scale.is_some();
110    }
111    if options.get_rounding_mode() != Exact {
112        return true;
113    }
114    let Some(min_scale) = min_scale else {
115        return false;
116    };
117    let min_scale = i64::exact_from(min_scale);
118    match options.get_size_options() {
119        SciSizeOptions::Scale(scale) => min_scale <= i64::exact_from(scale),
120        SciSizeOptions::Precision(precision) => {
121            min_scale <= i64::exact_from(precision - 1) - floor_log_base(x, base)
122        }
123        SciSizeOptions::Complete => unreachable!(),
124    }
125}}
126
127pub_crate_test! {
128// Converts a `Float` to a string using a specified base, possibly using scientific notation; this
129// is the engine behind `Display` and the power-of-2-base formatting traits (and eventually
130// `ToSci`). See `ToSciOptions` for details on the available options. The `Float` `Display`
131// conventions apply on top of them: NaN and the infinities are rendered as `NaN`, `Infinity`, and
132// `-Infinity`, and the output for any finite value (including zeros) always contains a point, `.0`
133// being appended if necessary.
134//
135// The digits are computed by `get_str`, which rounds the value directly, so this function never
136// materializes the `Float` as a `Rational` (except in one corner case: deciding a `Nearest` tie
137// when the value's magnitude lies within one base-power of a `Scale` boundary).
138//
139// Panics if the rounding mode is `Exact` but the size options are such that the input must be
140// rounded, or if the size option is `Complete` and the expansion is non-terminating (an odd base
141// and a fractional value); `to_sci_valid` identifies both cases.
142to_sci_string(x: &Float, options: ToSciOptions) -> String {
143    let (neg, sign) = match x {
144        Float(NaN) => return String::from("NaN"),
145        Float(Infinity { sign: true }) => return String::from("Infinity"),
146        Float(Infinity { sign: false }) => return String::from("-Infinity"),
147        Float(Zero { sign }) => return zero_to_string(!*sign, options),
148        Float(Finite { sign, .. }) => (!*sign, *sign),
149    };
150    let base = i64::from(options.get_base());
151    let rm = options.get_rounding_mode();
152    let trim_zeros = !options.get_include_trailing_zeros()
153        && options.get_size_options() != SciSizeOptions::Complete;
154    let log = floor_log_base(x, base);
155    // `scale` is the number of digits after the point and `precision` the total number of digits,
156    // as in `Rational::fmt_sci`. A nonpositive `precision` means the value rounds to 0 or to 1 unit
157    // at the requested scale.
158    let (scale, precision) = match options.get_size_options() {
159        SciSizeOptions::Complete => {
160            let scale = length_after_point(x.integer_exponent(), base).unwrap_or_else(|| {
161                panic!("{x} has a non-terminating expansion in base {base}")
162            });
163            let precision = i64::exact_from(scale) + log + 1;
164            // the digits of the exact expansion begin at the first significant digit
165            assert!(precision > 0);
166            (i64::exact_from(scale), precision)
167        }
168        SciSizeOptions::Scale(scale) => {
169            (i64::exact_from(scale), i64::exact_from(scale) + log + 1)
170        }
171        SciSizeOptions::Precision(precision) => (
172            i64::exact_from(precision - 1) - log,
173            i64::exact_from(precision),
174        ),
175    };
176    let (digits, log) = if precision <= 0 {
177        // 0 < |x| * base^scale < 1: the value rounds to 0 or to 1 in the last place.
178        let round_up_to_one = match rm {
179            Up => true,
180            Down => false,
181            Floor => neg,
182            Ceiling => !neg,
183            Exact => panic!(
184                "Exact rounding was requested, but {x} is not exactly representable with {scale} \
185                digits after the point",
186            ),
187            // |x| < base^(log + 1) <= base^(-scale); it rounds up iff it exceeds base^-scale / 2,
188            // which requires log + 1 == -scale (one base-power below the boundary and it is already
189            // at most half). A tie rounds to the even option, 0.
190            Nearest => {
191                log + 1 == -scale && {
192                    let two_x = Rational::exact_from(x).abs() << 1u32;
193                    two_x > Rational::from(base).pow(-scale)
194                }
195            }
196        };
197        if round_up_to_one {
198            (vec![b'1'], -scale)
199        } else {
200            return zero_to_string(neg, options);
201        }
202    } else {
203        let m = usize::exact_from(precision);
204        // a negative base makes `get_str` produce uppercase digits
205        let get_str_base = if options.get_lowercase() { base } else { -base };
206        let (s, e, o) = get_str(x, get_str_base, m, rm).unwrap();
207        let mut digits = if neg { s[1..].to_vec() } else { s };
208        debug_assert!(options.get_size_options() != SciSizeOptions::Complete || o == Equal);
209        let new_log = e - 1;
210        // Rounding up to a power of the base adds an integral digit. With a requested scale the
211        // number of digits after the point must not shrink, so widen the digit string; this mirrors
212        // `Rational::fmt_sci`, which widens its precision. (With a requested precision the digit
213        // count is fixed and the scale shrinks instead, which the layout below derives from
214        // `new_log`; and a `Complete` conversion is exact, so no rounding up can occur.)
215        if new_log > log && matches!(options.get_size_options(), SciSizeOptions::Scale(_)) {
216            digits.push(b'0');
217        }
218        (digits, new_log)
219    };
220    // the number of digits after the point, for the padding assertions below
221    let target_scale = match options.get_size_options() {
222        SciSizeOptions::Precision(_) => i64::exact_from(digits.len()) - 1 - log,
223        _ => scale,
224    };
225    let mut mantissa: Vec<u8> = Vec::new();
226    let mut exponent = None;
227    if log <= options.get_neg_exp_threshold() || target_scale < 0 {
228        // scientific notation: one digit, the rest after a point, and an exponent
229        let ds = if trim_zeros {
230            strip_trailing_zeros(&digits)
231        } else {
232            &digits
233        };
234        mantissa.push(ds[0]);
235        if ds.len() > 1 {
236            mantissa.push(b'.');
237            mantissa.extend_from_slice(&ds[1..]);
238        }
239        exponent = Some(log);
240    } else if log < 0 {
241        // no exponent; the value is less than 1, so all digits are fractional
242        let ds = if trim_zeros {
243            strip_trailing_zeros(&digits)
244        } else {
245            &digits
246        };
247        mantissa.extend_from_slice(b"0.");
248        mantissa.resize(2 + usize::exact_from(-log - 1), b'0');
249        mantissa.extend_from_slice(ds);
250        debug_assert!(
251            trim_zeros || -log - 1 + i64::exact_from(ds.len()) == target_scale,
252            "fractional length mismatch"
253        );
254    } else {
255        // no exponent; split the digits at the point
256        let digits_before = usize::exact_from(log + 1);
257        mantissa.extend_from_slice(&digits[..digits_before]);
258        let frac = if trim_zeros {
259            strip_trailing_zeros(&digits[digits_before..])
260        } else {
261            &digits[digits_before..]
262        };
263        if !frac.is_empty() {
264            mantissa.push(b'.');
265            mantissa.extend_from_slice(frac);
266        }
267        debug_assert!(
268            trim_zeros || i64::exact_from(frac.len()) == target_scale,
269            "fractional length mismatch"
270        );
271    }
272    // the `Float` `Display` convention: a finite value always shows a point
273    if !mantissa.contains(&b'.') {
274        mantissa.extend_from_slice(b".0");
275    }
276    let mut out = String::new();
277    if !sign {
278        out.push('-');
279    }
280    out.push_str(core::str::from_utf8(&mantissa).unwrap());
281    if let Some(exp) = exponent {
282        push_exponent(&mut out, options, exp);
283    }
284    out
285}}