Skip to main content

zerodds_float128/
decimal.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Decimal in and decimal out, both exact.
4//!
5//! A corpus value file is JSON and a JSON number is a `double`, so a
6//! `long double` has to travel as text — the same rule this project
7//! already applies to `long long`, for the same reason. Text is only a
8//! carrier if it round-trips, so both directions here are correctly
9//! rounded against the exact value and neither goes through an `f64`.
10//!
11//! - [`Float128::to_exact_decimal`] writes every digit the value has.
12//!   Finite binary values always have finitely many, up to about 11 500
13//!   for the smallest subnormal.
14//! - [`Float128::to_shortest_decimal`] writes the fewest digits that
15//!   read back as the same bit pattern, found by asking the parser
16//!   rather than by trusting a table.
17//! - `FromStr` reads either back, plus `inf`, `-inf` and `nan`.
18
19use alloc::string::{String, ToString};
20use alloc::vec::Vec;
21use core::fmt;
22use core::str::FromStr;
23
24use crate::bigint::BigUint;
25use crate::float::Float128;
26
27/// Why a decimal string is not a binary128 value.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum ParseFloat128Error {
30    /// The string held no digits at all.
31    Empty,
32    /// A character that cannot appear where it did.
33    Unexpected {
34        /// The offending character.
35        found: char,
36        /// Its byte offset in the input.
37        at: usize,
38    },
39    /// An exponent that is not an integer, or does not fit one.
40    BadExponent,
41}
42
43impl fmt::Display for ParseFloat128Error {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        match self {
46            Self::Empty => write!(f, "a binary128 value needs at least one digit"),
47            Self::Unexpected { found, at } => {
48                write!(
49                    f,
50                    "`{found}` cannot appear at byte {at} of a binary128 value"
51                )
52            }
53            Self::BadExponent => write!(f, "the exponent is not an integer this crate can use"),
54        }
55    }
56}
57
58impl core::error::Error for ParseFloat128Error {}
59
60/// Past this the value is certainly out of range in one direction or the
61/// other, and the exact path would build a megabyte of limbs to find
62/// that out. binary128 spans about `1e-4966` to `1e4932`.
63const DECIMAL_LIMIT: i64 = 6000;
64
65impl Float128 {
66    /// Every digit the value has, exactly.
67    ///
68    /// `1.5` gives `"1.5"`; the smallest subnormal gives its full 11 500
69    /// digits. Infinities and NaNs give `"inf"`, `"-inf"` and `"nan"`,
70    /// which is what `FromStr` reads back.
71    #[must_use]
72    pub fn to_exact_decimal(&self) -> String {
73        match self.exact_digits() {
74            None => non_finite(*self).to_string(),
75            Some((negative, digits, point)) => render(negative, &digits, point),
76        }
77    }
78
79    /// The fewest digits that read back as this exact bit pattern.
80    ///
81    /// Found by rounding the exact expansion to one digit, then two, and
82    /// asking `FromStr` whether it comes back — so the answer is true of
83    /// *this* parser rather than true of a rule about digit counts.
84    /// Thirty-six always succeeds; [`Float128::DIGITS_ROUND_TRIP`] is
85    /// that bound.
86    #[must_use]
87    pub fn to_shortest_decimal(&self) -> String {
88        let Some((negative, digits, point)) = self.exact_digits() else {
89            return non_finite(*self).to_string();
90        };
91        if digits.is_empty() {
92            return render(negative, &digits, point);
93        }
94        for length in 1..=Self::DIGITS_ROUND_TRIP {
95            if length >= digits.len() {
96                break;
97            }
98            let (rounded, rounded_point) = round_significant(&digits, point, length);
99            let text = render(negative, &rounded, rounded_point);
100            if Self::from_str(&text).is_ok_and(|back| back.to_bits() == self.to_bits()) {
101                return text;
102            }
103        }
104        render(negative, &digits, point)
105    }
106
107    /// The exact expansion as `(negative, digits, point)`, where the
108    /// value is `0.<digits> * 10^point` and `digits` has no leading
109    /// zero. `None` for an infinity or a NaN; an empty digit list for a
110    /// zero, whose sign is still in the first field.
111    fn exact_digits(&self) -> Option<(bool, Vec<u8>, i32)> {
112        let (negative, significand, exponent) = self.decompose()?;
113        if significand == 0 {
114            return Some((negative, Vec::new(), 0));
115        }
116        let mut value = BigUint::from_u128(significand);
117        let point;
118        if exponent >= 0 {
119            value.shl(exponent as usize);
120            let mut digits = value.to_decimal_digits();
121            point = decimal_point(digits.len(), 0);
122            trim_trailing_zeros(&mut digits);
123            return Some((negative, digits, point));
124        }
125        // m * 2^-k is exactly m * 5^k * 10^-k, which is how a binary
126        // fraction becomes a terminating decimal one.
127        let scale = (-exponent) as usize;
128        value.mul_pow5(scale);
129        let mut digits = value.to_decimal_digits();
130        point = decimal_point(digits.len(), scale);
131        trim_trailing_zeros(&mut digits);
132        Some((negative, digits, point))
133    }
134}
135
136/// The decimal point position for `digits` digits scaled by `10^-scale`.
137///
138/// A separate function because it is the one arithmetic in this file that
139/// could overflow an `i32`, and it cannot: the exact expansion of a
140/// binary128 is at most 11 563 digits and the scale at most 16 494, both
141/// four orders of magnitude below `i32::MAX`. Saturating rather than
142/// wrapping so that a future value which somehow exceeded the bound would
143/// render as an absurd exponent rather than as a plausible wrong one.
144fn decimal_point(digits: usize, scale: usize) -> i32 {
145    let digits = i32::try_from(digits).unwrap_or(i32::MAX);
146    let scale = i32::try_from(scale).unwrap_or(i32::MAX);
147    digits.saturating_sub(scale)
148}
149
150/// Drop trailing zeros, which the point already accounts for.
151///
152/// `0.5` comes out of the exact expansion as a five and 112 zeros —
153/// every binary fraction ends in a long tail of them, because `2^-k`
154/// scaled to a decimal is `5^k` and that always ends in a five. Keeping
155/// them would make the exact form of a half 113 characters long and
156/// `to_shortest_decimal` search for a length it already has.
157fn trim_trailing_zeros(digits: &mut Vec<u8>) {
158    while digits.last() == Some(&0) {
159        digits.pop();
160    }
161}
162
163/// The word for a value with no digits.
164fn non_finite(value: Float128) -> &'static str {
165    if value.is_nan() {
166        "nan"
167    } else if value.is_sign_negative() {
168        "-inf"
169    } else {
170        "inf"
171    }
172}
173
174/// Round `digits` to `length` significant digits, half to even.
175///
176/// Returns the shortened digits and the adjusted point, which moves when
177/// rounding carries `999` up to `1000`.
178fn round_significant(digits: &[u8], point: i32, length: usize) -> (Vec<u8>, i32) {
179    let mut kept: Vec<u8> = digits[..length].to_vec();
180    let next = digits[length];
181    let rest_nonzero = digits[length + 1..].iter().any(|&digit| digit != 0);
182    let round_up = match next.cmp(&5) {
183        core::cmp::Ordering::Greater => true,
184        core::cmp::Ordering::Less => false,
185        core::cmp::Ordering::Equal => rest_nonzero || kept[length - 1] % 2 == 1,
186    };
187    if !round_up {
188        while kept.last() == Some(&0) {
189            kept.pop();
190        }
191        return (kept, point);
192    }
193    let mut index = length;
194    loop {
195        if index == 0 {
196            // Every digit carried: 999 -> 1000, one place wider.
197            kept.insert(0, 1);
198            kept.truncate(length);
199            while kept.last() == Some(&0) {
200                kept.pop();
201            }
202            return (kept, point + 1);
203        }
204        index -= 1;
205        if kept[index] == 9 {
206            kept[index] = 0;
207        } else {
208            kept[index] += 1;
209            break;
210        }
211    }
212    while kept.last() == Some(&0) {
213        kept.pop();
214    }
215    (kept, point)
216}
217
218/// Write `0.<digits> * 10^point` in the most readable form that reads
219/// back the same.
220///
221/// Plain decimal while that stays short, scientific otherwise. The
222/// threshold is about readability only: both forms parse, and
223/// `to_shortest_decimal` checks the one that comes out.
224fn render(negative: bool, digits: &[u8], point: i32) -> String {
225    let mut out = String::new();
226    if negative {
227        out.push('-');
228    }
229    if digits.is_empty() {
230        out.push_str("0.0");
231        return out;
232    }
233    let length = decimal_point(digits.len(), 0);
234    if point > 0 && point <= 34 {
235        // ddd.ddd
236        for (index, &digit) in digits.iter().enumerate() {
237            if decimal_point(index, 0) == point {
238                out.push('.');
239            }
240            out.push(char::from(b'0' + digit));
241        }
242        if length <= point {
243            for _ in length..point {
244                out.push('0');
245            }
246            out.push_str(".0");
247        }
248        return out;
249    }
250    if point <= 0 && point > -6 {
251        // 0.000ddd
252        out.push_str("0.");
253        for _ in 0..-point {
254            out.push('0');
255        }
256        for &digit in digits {
257            out.push(char::from(b'0' + digit));
258        }
259        return out;
260    }
261    // d.ddde±xx
262    out.push(char::from(b'0' + digits[0]));
263    out.push('.');
264    if digits.len() == 1 {
265        out.push('0');
266    } else {
267        for &digit in &digits[1..] {
268            out.push(char::from(b'0' + digit));
269        }
270    }
271    out.push('e');
272    let exponent = point - 1;
273    if exponent >= 0 {
274        out.push('+');
275    }
276    out.push_str(&exponent.to_string());
277    out
278}
279
280impl fmt::Display for Float128 {
281    /// The shortest round-tripping decimal.
282    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
283        f.pad(&self.to_shortest_decimal())
284    }
285}
286
287impl FromStr for Float128 {
288    type Err = ParseFloat128Error;
289
290    /// Correctly rounded, round to nearest with ties to even.
291    ///
292    /// Never goes through an `f64`: a two-step parse rounds twice, and
293    /// double rounding gets the last bit wrong for the values that
294    /// matter here.
295    fn from_str(text: &str) -> Result<Self, Self::Err> {
296        parse(text)
297    }
298}
299
300/// The parser behind `FromStr`.
301fn parse(text: &str) -> Result<Float128, ParseFloat128Error> {
302    let trimmed = text.trim();
303    let (negative, body) = match trimmed.strip_prefix('-') {
304        Some(rest) => (true, rest),
305        None => (false, trimmed.strip_prefix('+').unwrap_or(trimmed)),
306    };
307    if body.is_empty() {
308        return Err(ParseFloat128Error::Empty);
309    }
310    if body.eq_ignore_ascii_case("inf") || body.eq_ignore_ascii_case("infinity") {
311        return Ok(if negative {
312            Float128::NEG_INFINITY
313        } else {
314            Float128::INFINITY
315        });
316    }
317    if body.eq_ignore_ascii_case("nan") {
318        let nan = Float128::NAN;
319        return Ok(if negative { nan.neg() } else { nan });
320    }
321
322    // Split off the exponent first so the digit walk has one job.
323    let (mantissa, exponent_text) = match body.find(['e', 'E']) {
324        Some(index) => (&body[..index], Some(&body[index + 1..])),
325        None => (body, None),
326    };
327    let mut exponent: i64 = match exponent_text {
328        None => 0,
329        Some(digits) => digits
330            .parse()
331            .map_err(|_| ParseFloat128Error::BadExponent)?,
332    };
333
334    let mut significand = BigUint::zero();
335    let mut seen_digit = false;
336    let mut seen_point = false;
337    let mut fraction_digits: i64 = 0;
338    for (offset, character) in mantissa.char_indices() {
339        match character {
340            '0'..='9' => {
341                seen_digit = true;
342                significand.mul_add_small(10, u32::from(character as u8 - b'0'));
343                if seen_point {
344                    fraction_digits += 1;
345                }
346            }
347            '.' if !seen_point => seen_point = true,
348            // An underscore is a Rust literal separator, not a decimal
349            // one, and accepting it here would make `1_5` parse as
350            // fifteen in a value file where it was meant as a typo.
351            other => {
352                return Err(ParseFloat128Error::Unexpected {
353                    found: other,
354                    at: offset,
355                });
356            }
357        }
358    }
359    if !seen_digit {
360        return Err(ParseFloat128Error::Empty);
361    }
362    exponent -= fraction_digits;
363
364    if significand.is_zero() {
365        return Ok(if negative {
366            Float128::NEG_ZERO
367        } else {
368            Float128::ZERO
369        });
370    }
371
372    // Reject the magnitudes whose exact expansion would be megabytes of
373    // limbs, but only from the safe side of the estimate. A digit count
374    // derived from the bit length is off by a digit or two either way,
375    // so the overflow test uses the low estimate and the underflow test
376    // the high one: a bound that guessed in the convenient direction
377    // would answer infinity for a value that has one.
378    // A bit length that did not fit an `i64` would need an exabyte of
379    // limbs; the cast is total on every target this builds for.
380    let bits = significand.bit_length() as i64;
381    let digits_low = bits * 30103 / 100_000;
382    let digits_high = digits_low + 2;
383    if exponent + digits_low > DECIMAL_LIMIT {
384        return Ok(if negative {
385            Float128::NEG_INFINITY
386        } else {
387            Float128::INFINITY
388        });
389    }
390    if exponent + digits_high < -DECIMAL_LIMIT {
391        return Ok(if negative {
392            Float128::NEG_ZERO
393        } else {
394            Float128::ZERO
395        });
396    }
397
398    if exponent >= 0 {
399        // Non-negative by the branch, and bounded by `DECIMAL_LIMIT`
400        // above, so the width is not in question and there is no fallback
401        // for a caller to be wrong about.
402        let mut scaled = significand;
403        scaled.mul_pow10(exponent.unsigned_abs() as usize);
404        let (value, shift) = significand_with_sticky(scaled);
405        return Ok(Float128::from_parts_rounded(negative, value, shift).0);
406    }
407
408    // value = significand / 10^scale. Shift the numerator up far enough
409    // that the quotient carries more bits than binary128 needs, divide,
410    // and fold every discarded remainder into one sticky bit — which is
411    // what makes the single rounding at the end the correct one.
412    // Positive by the branch, and bounded by `DECIMAL_LIMIT`. A fallback
413    // of zero here would divide by 10^0 and answer with a value a
414    // thousand orders of magnitude out.
415    let scale = exponent.unsigned_abs() as usize;
416    let wanted = 130 + scale * 10 / 3;
417    let have = significand.bit_length();
418    let shift = wanted.saturating_sub(have);
419    let mut numerator = significand;
420    numerator.shl(shift);
421    let sticky = divide_by_pow10(&mut numerator, scale);
422    let (mut value, extra) = significand_with_sticky(numerator);
423    if sticky {
424        value |= 1;
425    }
426    let exponent = extra - decimal_point(shift, 0);
427    Ok(Float128::from_parts_rounded(negative, value, exponent).0)
428}
429
430/// Divide by `10^power` in nine-digit steps, returning whether anything
431/// was discarded.
432///
433/// `a / 10^18 == (a / 10^9) / 10^9`, and the two-step remainder is zero
434/// exactly when both single-step remainders are, which is why one
435/// boolean carries the whole sticky state.
436fn divide_by_pow10(value: &mut BigUint, power: usize) -> bool {
437    const CHUNK: u32 = 1_000_000_000;
438    const CHUNK_POWER: usize = 9;
439    let mut sticky = false;
440    let mut left = power;
441    while left >= CHUNK_POWER {
442        sticky |= value.div_rem_small(CHUNK) != 0;
443        left -= CHUNK_POWER;
444    }
445    if left != 0 {
446        let mut divisor = 1u32;
447        for _ in 0..left {
448            divisor *= 10;
449        }
450        sticky |= value.div_rem_small(divisor) != 0;
451    }
452    sticky
453}
454
455/// The top bits of a big integer as `(value, shift)` with
456/// `original == value * 2^shift`, keeping enough bits that one
457/// round-to-nearest at the end is the correct one, and folding what was
458/// dropped into the low bit.
459fn significand_with_sticky(value: BigUint) -> (u128, i32) {
460    let bits = value.bit_length();
461    if bits <= 127 {
462        return (value.to_u128().0, 0);
463    }
464    // 118 kept bits leave five past binary128's 113, so the guard,
465    // round and sticky positions are all inside the `u128`.
466    let drop = bits - 118;
467    let mut kept = value;
468    let lost = kept.shr(drop);
469    let (mut low, _) = kept.to_u128();
470    if lost {
471        low |= 1;
472    }
473    (low, decimal_point(drop, 0))
474}