fastnum/decimal/dec/
format.rs

1use core::{cmp::Ordering, fmt, fmt::Write, ops::Neg};
2
3#[cfg(not(feature = "std"))]
4use alloc::{string::String, vec::Vec};
5
6use crate::decimal::round::round_pair_digits;
7#[cfg(not(feature = "numtraits"))]
8use crate::decimal::utils::cast::ToPrimitive;
9#[cfg(feature = "numtraits")]
10use num_traits::ToPrimitive;
11
12use crate::decimal::{RoundingMode, Sign};
13
14include!(concat!(env!("OUT_DIR"), "/exponential_format_threshold.rs"));
15
16pub(crate) fn write_scientific_notation<W: Write>(
17    digits: String,
18    scale: i16,
19    w: &mut W,
20) -> fmt::Result {
21    let (first_digit, remaining_digits) = digits.as_str().split_at(1);
22    w.write_str(first_digit)?;
23    if !remaining_digits.is_empty() {
24        w.write_str(".")?;
25        w.write_str(remaining_digits)?;
26    }
27    write!(w, "e{}", remaining_digits.len() as i32 - scale as i32)
28}
29
30pub(crate) fn write_engineering_notation<W: Write>(
31    digits: String,
32    scale: i16,
33    out: &mut W,
34) -> fmt::Result {
35    let digit_count = digits.len();
36
37    let top_digit_exponent = digit_count as i32 - scale as i32;
38
39    let shift_amount = match top_digit_exponent.rem_euclid(3) {
40        0 => 3,
41        i => i as usize,
42    };
43
44    let exp = top_digit_exponent - shift_amount as i32;
45
46    // handle adding zero padding
47    if let Some(padding_zero_count) = shift_amount.checked_sub(digits.len()) {
48        let zeros = &"000"[..padding_zero_count];
49        out.write_str(&digits)?;
50        out.write_str(zeros)?;
51        return write!(out, "e{exp}");
52    }
53
54    let (head, rest) = digits.split_at(shift_amount);
55    debug_assert_eq!(exp % 3, 0);
56
57    out.write_str(head)?;
58
59    if !rest.is_empty() {
60        out.write_char('.')?;
61        out.write_str(rest)?;
62    }
63
64    write!(out, "e{exp}")
65}
66
67pub(crate) fn format(
68    digits: String,
69    scale: i16,
70    sign: Sign,
71    f: &mut fmt::Formatter,
72) -> fmt::Result {
73    // number of zeros between the most significant digit and decimal point
74    let leading_zero_count = scale
75        .to_u64()
76        .and_then(|scale| scale.checked_sub(digits.len() as u64))
77        .unwrap_or(0);
78
79    // number of zeros between last significant digit and decimal point
80    let trailing_zero_count = scale.checked_neg().and_then(|d| d.to_u64());
81
82    // this ignores scientific-formatting if precision is requested
83    let trailing_zeros = f
84        .precision()
85        .map(|_| 0)
86        .or(trailing_zero_count)
87        .unwrap_or(0);
88
89    let leading_zero_threshold = EXPONENTIAL_FORMAT_LEADING_ZERO_THRESHOLD as u64;
90    let trailing_zero_threshold = EXPONENTIAL_FORMAT_TRAILING_ZERO_THRESHOLD as u64;
91
92    // use exponential form if decimal point is outside
93    // the upper and lower thresholds of the decimal
94    if leading_zero_threshold < leading_zero_count {
95        format_exponential(digits, scale, sign, f, "E")
96    } else if trailing_zero_threshold < trailing_zeros {
97        // non-scientific notation
98        format_dotless_exponential(digits, scale, sign, f, "e")
99    } else {
100        format_full_scale(digits, scale, sign, f)
101    }
102}
103
104pub(crate) fn format_exponential(
105    digits: String,
106    scale: i16,
107    sign: Sign,
108    f: &mut fmt::Formatter,
109    e_symbol: &str,
110) -> fmt::Result {
111    let exp = (scale as i128).neg();
112    format_exponential_be_ascii_digits(digits, exp, sign, f, e_symbol)
113}
114
115fn format_dotless_exponential(
116    mut digits: String,
117    scale: i16,
118    sign: Sign,
119    f: &mut fmt::Formatter,
120    e_symbol: &str,
121) -> fmt::Result {
122    debug_assert!(scale <= 0);
123    write!(digits, "{}{:+}", e_symbol, -scale)?;
124    let non_negative = matches!(sign, Sign::Plus);
125    f.pad_integral(non_negative, "", &digits)
126}
127
128fn format_full_scale(
129    digits: String,
130    scale: i16,
131    sign: Sign,
132    f: &mut fmt::Formatter,
133) -> fmt::Result {
134    let mut digits = digits.into_bytes();
135    let mut exp = (scale as i128).neg();
136
137    if scale <= 0 {
138        // formatting an integer value (add trailing zeros to the right)
139        zero_right_pad_integer_ascii_digits(&mut digits, &mut exp, f.precision());
140    } else {
141        let scale = scale as u64;
142        // no-precision behaves the same as precision matching scale (i.e. no padding or
143        // rounding)
144        let prec = f
145            .precision()
146            .and_then(|prec| prec.to_u64())
147            .unwrap_or(scale);
148
149        if scale < digits.len() as u64 {
150            // format both integer and fractional digits (always 'trim' to precision)
151            trim_ascii_digits(&mut digits, scale, sign, prec, &mut exp);
152        } else {
153            // format only fractional digits
154            shift_or_trim_fractional_digits(&mut digits, scale, sign, prec, &mut exp);
155        }
156        // never print exp when in this branch
157        exp = 0;
158    }
159
160    // move digits back into String form
161    let mut buf = String::from_utf8(digits).unwrap();
162
163    // add exp part to buffer (if not zero)
164    if exp != 0 {
165        write!(buf, "e{exp:+}")?;
166    }
167
168    // write buffer to formatter
169    let non_negative = matches!(sign, Sign::Plus);
170    f.pad_integral(non_negative, "", &buf)
171}
172
173/// Fill appropriate number of zeros and decimal point into Vec of (ascii/utf-8)
174/// digits
175///
176/// Exponent is set to zero if zeros were added
177fn zero_right_pad_integer_ascii_digits(
178    digits: &mut Vec<u8>,
179    exp: &mut i128,
180    precision: Option<usize>,
181) {
182    debug_assert!(*exp >= 0);
183
184    let trailing_zero_count = match exp.to_usize() {
185        Some(n) => n,
186        None => {
187            return;
188        }
189    };
190    let total_additional_zeros = trailing_zero_count.saturating_add(precision.unwrap_or(0));
191    if total_additional_zeros > FMT_MAX_INTEGER_PADDING {
192        return;
193    }
194
195    // requested 'prec' digits of precision after decimal point
196    match precision {
197        None if trailing_zero_count > 20 => {}
198        None | Some(0) => {
199            digits.resize(digits.len() + trailing_zero_count, b'0');
200            *exp = 0;
201        }
202        Some(prec) => {
203            digits.resize(digits.len() + trailing_zero_count, b'0');
204            digits.push(b'.');
205            digits.resize(digits.len() + prec, b'0');
206            *exp = 0;
207        }
208    }
209}
210
211fn trim_ascii_digits(digits: &mut Vec<u8>, scale: u64, sign: Sign, prec: u64, exp: &mut i128) {
212    debug_assert!(scale < digits.len() as u64);
213    // there are both integer and fractional digits
214    let mut integer_digit_count = (digits.len() as u64 - scale)
215        .to_usize()
216        .expect("Number of digits exceeds maximum usize");
217
218    if prec < scale {
219        let prec = prec.to_usize().expect("Precision exceeds maximum usize");
220        if apply_rounding_to_ascii_digits(digits, exp, sign, integer_digit_count + prec) {
221            digits[0] = b'1';
222            integer_digit_count += 1;
223            digits.push(b'0');
224        }
225    }
226
227    if prec != 0 {
228        digits.insert(integer_digit_count, b'.');
229    }
230
231    if scale < prec {
232        let trailing_zero_count = (prec - scale).to_usize().expect("Too Big");
233
234        // precision required beyond scale
235        digits.resize(digits.len() + trailing_zero_count, b'0');
236    }
237}
238
239fn shift_or_trim_fractional_digits(
240    digits: &mut Vec<u8>,
241    scale: u64,
242    sign: Sign,
243    prec: u64,
244    exp: &mut i128,
245) {
246    debug_assert!(scale >= digits.len() as u64);
247    // there are no integer digits
248    let leading_zeros = scale - digits.len() as u64;
249
250    match prec.checked_sub(leading_zeros) {
251        None => {
252            digits.clear();
253            digits.push(b'0');
254            if prec > 0 {
255                digits.push(b'.');
256                digits.resize(2 + prec as usize, b'0');
257            }
258        }
259        Some(0) => {
260            // precision is at the decimal digit boundary, round one value
261            let insig_digit = digits[0] - b'0';
262            let trailing_zeros = digits[1..].iter().all(|&d| d == b'0');
263
264            let rounded_value = round_pair_digits(
265                (0, insig_digit),
266                sign,
267                RoundingMode::default(),
268                trailing_zeros,
269            );
270
271            digits.clear();
272            if leading_zeros != 0 {
273                digits.push(b'0');
274                digits.push(b'.');
275                digits.resize(1 + leading_zeros as usize, b'0');
276            }
277            digits.push(rounded_value + b'0');
278        }
279        Some(digit_prec) => {
280            let mut carry = false;
281            let digit_prec = digit_prec as usize;
282            let mut leading_zeros = leading_zeros
283                .to_usize()
284                .expect("Number of leading zeros exceeds max usize");
285            let trailing_zeros = digit_prec.saturating_sub(digits.len());
286            if digit_prec < digits.len() {
287                carry = apply_rounding_to_ascii_digits(digits, exp, sign, digit_prec);
288            }
289            if carry {
290                if prec <= digit_prec as u64 {
291                    digits.extend_from_slice(b"1.");
292                } else {
293                    digits.insert(0, b'1');
294                    leading_zeros = leading_zeros.saturating_sub(1);
295                    digits.extend_from_slice(b"0.");
296                }
297            } else {
298                digits.extend_from_slice(b"0.");
299            }
300            digits.resize(digits.len() + leading_zeros, b'0');
301            digits.rotate_right(leading_zeros + 2);
302
303            // add any extra trailing zeros
304            digits.resize(digits.len() + trailing_zeros, b'0');
305        }
306    }
307}
308
309fn format_exponential_be_ascii_digits(
310    digits: String,
311    mut exp: i128,
312    sign: Sign,
313    f: &mut fmt::Formatter,
314    e_symbol: &str,
315) -> fmt::Result {
316    let mut digits = digits.into_bytes();
317
318    // how many zeros to pad at the end of the decimal
319    let mut extra_trailing_zero_count = 0;
320
321    if let Some(prec) = f.precision() {
322        // 'prec' is number of digits after the decimal point
323        let total_prec = prec + 1;
324        let digit_count = digits.len();
325
326        match total_prec.cmp(&digit_count) {
327            Ordering::Equal => {
328                // digit count is one more than precision - do nothing
329            }
330            Ordering::Less => {
331                // round to smaller precision
332                if apply_rounding_to_ascii_digits(&mut digits, &mut exp, sign, total_prec) {
333                    digits[0] = b'1';
334                }
335            }
336            Ordering::Greater => {
337                // increase number of zeros to add to end of digits
338                extra_trailing_zero_count = total_prec - digit_count;
339            }
340        }
341    }
342
343    let needs_decimal_point = digits.len() > 1 || extra_trailing_zero_count > 0;
344
345    let mut abs_int = String::from_utf8(digits).unwrap();
346
347    let exponent = abs_int.len() as i128 + exp - 1;
348
349    if needs_decimal_point {
350        // only add decimal point if there is more than 1 decimal digit
351        abs_int.insert(1, '.');
352    }
353
354    if extra_trailing_zero_count > 0 {
355        abs_int.extend(core::iter::repeat_n('0', extra_trailing_zero_count));
356    }
357
358    // always print exponent in exponential mode
359    write!(abs_int, "{e_symbol}{exponent:+}")?;
360
361    let non_negative = matches!(sign, Sign::Plus);
362    f.pad_integral(non_negative, "", &abs_int)
363}
364
365#[must_use = "must use carry result"]
366fn apply_rounding_to_ascii_digits(
367    ascii_digits: &mut Vec<u8>,
368    exp: &mut i128,
369    sign: Sign,
370    prec: usize,
371) -> bool {
372    if ascii_digits.len() < prec {
373        return false;
374    }
375
376    // shift exp to align with new length of digits
377    *exp += (ascii_digits.len() - prec) as i128;
378
379    // true if all ascii_digits after precision are zeros
380    let trailing_zeros = ascii_digits[prec + 1..].iter().all(|&d| d == b'0');
381
382    let sig_digit = ascii_digits[prec - 1] - b'0';
383    let insig_digit = ascii_digits[prec] - b'0';
384
385    let rounded_digit = round_pair_digits(
386        (sig_digit, insig_digit),
387        sign,
388        RoundingMode::default(),
389        trailing_zeros,
390    );
391
392    // remove insignificant digits
393    ascii_digits.truncate(prec - 1);
394
395    // push rounded value
396    if rounded_digit < 10 {
397        ascii_digits.push(rounded_digit + b'0');
398        return false;
399    }
400
401    debug_assert_eq!(rounded_digit, 10);
402
403    // push zero and carry-the-one
404    ascii_digits.push(b'0');
405
406    // loop through digits in reverse order (skip the 0 we just pushed)
407    let digits = ascii_digits.iter_mut().rev().skip(1);
408    for digit in digits {
409        if *digit < b'9' {
410            // we've carried the one as far as it will go
411            *digit += 1;
412            return false;
413        }
414
415        debug_assert_eq!(*digit, b'9');
416
417        // digit was a 9, set to zero and carry the one
418        // to the next digit
419        *digit = b'0';
420    }
421
422    *exp += 1;
423    true
424}