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}