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}