Skip to main content

oxinum_float/native/
format_ext.rs

1//! Extended string formatting for the native [`BigFloat`].
2//!
3//! This module adds two families of human- and machine-readable string
4//! renderings on top of the binary-exact [`fmt::Display`](core::fmt::Display)
5//! (`0xb…p…`) provided in `float.rs`:
6//!
7//! 1. **Decimal scientific / engineering notation** — base-10 renderings with
8//!    a caller-chosen number of significant digits.
9//!    - [`BigFloat::to_scientific_string`] — `d.ddd…e±E` (one integer digit).
10//!    - [`BigFloat::to_engineering_string`] — like scientific, but the decimal
11//!      exponent is *always* a multiple of three and the displayed mantissa
12//!      lies in `[1, 1000)`.
13//!
14//! 2. **C99 `%a`-style hexadecimal float** — `±0x1.<hex-frac>p±<binexp>`.
15//!    - [`BigFloat::to_hex_string`] — binary-exact (no rounding).
16//!    - [`BigFloat::from_hex_float`] — the exact inverse parser.
17//!
18//! # Decimal conversion strategy
19//!
20//! A `BigFloat` is `±m·2^e` with `m` a non-negative big integer. To render
21//! `D` significant decimal digits we work entirely in exact big-integer
22//! arithmetic:
23//!
24//! 1. Find `E = floor(log10(|V|))` by seeding from the binary exponent and
25//!    correcting with exact cross-multiplied comparisons against powers of ten.
26//! 2. Form the rational `|V| / 10^(E-D+1)` as a pair of big integers and round
27//!    it to the nearest integer `N` (ties to even). `N` then has exactly `D`
28//!    digits (or `D+1` on a `999…→100…0` carry, which bumps `E`).
29//!
30//! Because the conversion never touches `f64` for the actual digits, it is
31//! correct at arbitrary magnitude and precision.
32//!
33//! # Hex conversion strategy
34//!
35//! Hex maps directly onto the binary representation with **no rounding**: the
36//! leading mantissa bit becomes the `1` before the point, the remaining bits
37//! are grouped into 4-bit nibbles after the point, and the `p` exponent is the
38//! binary exponent of that leading bit. Round-tripping is therefore bit-exact.
39
40use core::cmp::Ordering;
41
42use oxinum_core::{OxiNumError, OxiNumResult, Sign};
43use oxinum_int::native::BigUint;
44
45use super::float::{BigFloat, FloatClass, RoundingMode};
46
47// ===========================================================================
48// Shared big-integer helpers
49// ===========================================================================
50
51/// `10^n` as a [`BigUint`]. `10^0 == 1`.
52fn pow10(n: u64) -> BigUint {
53    // Group by chunks of 10^19 (largest power of ten fitting in u64) to keep
54    // the number of big-integer multiplies small.
55    const CHUNK_EXP: u64 = 19;
56    const CHUNK_VAL: u64 = 10_000_000_000_000_000_000; // 10^19
57    let mut acc = BigUint::one();
58    let full = n / CHUNK_EXP;
59    let rem = n % CHUNK_EXP;
60    let chunk = BigUint::from_u64(CHUNK_VAL);
61    for _ in 0..full {
62        acc = &acc * &chunk;
63    }
64    if rem > 0 {
65        let mut tail: u64 = 1;
66        for _ in 0..rem {
67            tail *= 10;
68        }
69        acc = &acc * &BigUint::from_u64(tail);
70    }
71    acc
72}
73
74/// `2^n` as a [`BigUint`].
75fn pow2(n: u64) -> BigUint {
76    BigUint::one().shl_bits(n)
77}
78
79/// Round the exact rational `num / den` (with `den > 0`) to the nearest
80/// integer, breaking ties to even. Returns the rounded big integer.
81fn round_ratio_half_even(num: &BigUint, den: &BigUint) -> BigUint {
82    let quotient = num / den;
83    let remainder = num % den;
84    if remainder.is_zero() {
85        return quotient;
86    }
87    // Compare 2*remainder against den.
88    let twice_rem = remainder.shl_bits(1);
89    match twice_rem.cmp(den) {
90        Ordering::Less => quotient,
91        Ordering::Greater => &quotient + &BigUint::one(),
92        Ordering::Equal => {
93            // Exact half — round to even.
94            if quotient.test_bit(0) {
95                &quotient + &BigUint::one()
96            } else {
97                quotient
98            }
99        }
100    }
101}
102
103// ===========================================================================
104// Decimal scientific / engineering notation
105// ===========================================================================
106
107/// Decimal rendering of the *magnitude* of a non-zero `BigFloat`.
108///
109/// Returns `(digits, exp10)` where `digits` is a string of exactly
110/// `sig_digits` decimal characters (`'0'..='9'`) and the value's magnitude
111/// equals `digits[0] . digits[1..] × 10^exp10`. In other words `exp10` is the
112/// base-10 exponent of the leading digit.
113fn decimal_magnitude(value: &BigFloat, sig_digits: usize) -> (String, i64) {
114    let d = sig_digits.max(1);
115    let m = value.mantissa();
116    let e = value.exponent();
117
118    // --- Step 1: estimate E = floor(log10(|V|)). ---
119    // top_bit position = e + (bit_length - 1) is the binary exponent of |V|.
120    let top_bit = e.saturating_add(m.bit_length() as i64 - 1);
121    // log10(2) ≈ 0.30102999566398114.
122    let mut big_e = (top_bit as f64 * core::f64::consts::LOG10_2).floor() as i64;
123
124    // --- Step 2: correct E with exact comparisons until 10^E <= |V| < 10^(E+1). ---
125    // Compare |V| = m·2^e against 10^cand via cross-multiplied big integers:
126    //   m·2^e  ⪋ 10^cand
127    //   m·2^max(e,0)·10^max(-cand,0)  ⪋  2^max(-e,0)·10^max(cand,0)
128    let cmp_vs_pow10 = |cand: i64| -> Ordering {
129        let e_pos = e.max(0) as u64;
130        let e_neg = (-e).max(0) as u64;
131        let c_pos = cand.max(0) as u64;
132        let c_neg = (-cand).max(0) as u64;
133        let lhs = {
134            let mut x = m.shl_bits(e_pos);
135            if c_neg > 0 {
136                x = &x * &pow10(c_neg);
137            }
138            x
139        };
140        let rhs = {
141            let mut x = pow2(e_neg);
142            if c_pos > 0 {
143                x = &x * &pow10(c_pos);
144            }
145            x
146        };
147        lhs.cmp(&rhs)
148    };
149    // Nudge up while |V| >= 10^(E+1).
150    while cmp_vs_pow10(big_e + 1) != Ordering::Less {
151        big_e += 1;
152    }
153    // Nudge down while |V| < 10^E.
154    while cmp_vs_pow10(big_e) == Ordering::Less {
155        big_e -= 1;
156    }
157
158    // --- Step 3: N = round(|V| / 10^(E-D+1)), ties to even. ---
159    let k = big_e - (d as i64) + 1; // place value of the least significant digit
160    let e_pos = e.max(0) as u64;
161    let e_neg = (-e).max(0) as u64;
162    let k_pos = k.max(0) as u64;
163    let k_neg = (-k).max(0) as u64;
164    // num = m · 2^max(e,0) · 10^max(-k,0)
165    let num = {
166        let mut x = m.shl_bits(e_pos);
167        if k_neg > 0 {
168            x = &x * &pow10(k_neg);
169        }
170        x
171    };
172    // den = 2^max(-e,0) · 10^max(k,0)
173    let den = {
174        let mut x = pow2(e_neg);
175        if k_pos > 0 {
176            x = &x * &pow10(k_pos);
177        }
178        x
179    };
180    let n = round_ratio_half_even(&num, &den);
181
182    // --- Step 4: render N and fix a possible carry (D+1 digits). ---
183    let mut digits = match n.to_radix(10) {
184        Ok(s) => s,
185        Err(_) => "0".repeat(d),
186    };
187    let mut exp10 = big_e;
188    if digits.len() == d + 1 {
189        // Rounding rolled 9…9 into 10…0; the trailing digit is '0'.
190        digits.pop();
191        exp10 += 1;
192    }
193    // Defensive: pad on the right if the integer rendered shorter than D
194    // (cannot normally happen for a non-zero value, but keeps the slice math
195    // total).
196    while digits.len() < d {
197        digits.push('0');
198    }
199    // Trim to exactly D characters.
200    if digits.len() > d {
201        digits.truncate(d);
202    }
203    (digits, exp10)
204}
205
206impl BigFloat {
207    /// Render this value in decimal **scientific** notation with `sig_digits`
208    /// significant digits: `d.ddd…e±E` (a single digit before the point).
209    ///
210    /// The value is rounded (ties to even) to `sig_digits` significant decimal
211    /// digits. `sig_digits` is clamped to at least 1.
212    ///
213    /// # Examples
214    ///
215    /// ```
216    /// use oxinum_float::native::{BigFloat, RoundingMode};
217    /// let x = BigFloat::from_i64(12345, 64, RoundingMode::HalfEven);
218    /// assert_eq!(x.to_scientific_string(5), "1.2345e4");
219    /// assert_eq!(x.to_scientific_string(1), "1e4");
220    /// ```
221    pub fn to_scientific_string(&self, sig_digits: usize) -> String {
222        // Non-finite values must be handled before the is_zero() check, since
223        // NaN and Inf have mantissa=0 and would otherwise format incorrectly.
224        match self.class {
225            FloatClass::Nan => return "NaN".to_string(),
226            FloatClass::Infinite => {
227                return if self.sign() == Sign::Negative {
228                    "-inf".to_string()
229                } else {
230                    "inf".to_string()
231                };
232            }
233            FloatClass::Finite => {}
234        }
235        let d = sig_digits.max(1);
236        if self.is_zero() {
237            if d == 1 {
238                return "0e0".to_string();
239            }
240            let mut s = String::from("0.");
241            for _ in 1..d {
242                s.push('0');
243            }
244            s.push_str("e0");
245            return s;
246        }
247        let (digits, exp10) = decimal_magnitude(self, d);
248        let mut out = String::new();
249        if self.sign() == Sign::Negative {
250            out.push('-');
251        }
252        // First digit, then the fractional tail.
253        out.push_str(&digits[..1]);
254        if digits.len() > 1 {
255            out.push('.');
256            out.push_str(&digits[1..]);
257        }
258        out.push('e');
259        out.push_str(&exp10.to_string());
260        out
261    }
262
263    /// Render this value in decimal **engineering** notation with `sig_digits`
264    /// significant digits.
265    ///
266    /// Engineering notation is scientific notation constrained so the decimal
267    /// exponent is always a multiple of three and the displayed mantissa lies
268    /// in `[1, 1000)`. The mantissa therefore carries one, two, or three digits
269    /// before the point.
270    ///
271    /// The value is rounded (ties to even) to `sig_digits` significant decimal
272    /// digits. `sig_digits` is clamped to at least 1.
273    ///
274    /// # Examples
275    ///
276    /// ```
277    /// use oxinum_float::native::{BigFloat, RoundingMode};
278    /// let x = BigFloat::from_i64(12345, 64, RoundingMode::HalfEven);
279    /// assert_eq!(x.to_engineering_string(5), "12.345e3");
280    /// ```
281    pub fn to_engineering_string(&self, sig_digits: usize) -> String {
282        // Non-finite values must be handled before the is_zero() check, since
283        // NaN and Inf have mantissa=0 and would otherwise format incorrectly.
284        match self.class {
285            FloatClass::Nan => return "NaN".to_string(),
286            FloatClass::Infinite => {
287                return if self.sign() == Sign::Negative {
288                    "-inf".to_string()
289                } else {
290                    "inf".to_string()
291                };
292            }
293            FloatClass::Finite => {}
294        }
295        let d = sig_digits.max(1);
296        if self.is_zero() {
297            if d == 1 {
298                return "0e0".to_string();
299            }
300            let mut s = String::from("0.");
301            for _ in 1..d {
302                s.push('0');
303            }
304            s.push_str("e0");
305            return s;
306        }
307        let (digits, exp10) = decimal_magnitude(self, d);
308        // Engineering exponent: snap down to the nearest multiple of three.
309        // `int_digits` (= 1, 2, or 3) digits sit before the decimal point.
310        let shift = exp10.rem_euclid(3); // 0, 1, or 2
311        let eng_exp = exp10 - shift;
312        let int_digits = (shift + 1) as usize;
313
314        // Pad the digit string on the right so it has at least `int_digits`
315        // characters (only relevant when sig_digits < int_digits).
316        let mut padded = digits;
317        while padded.len() < int_digits {
318            padded.push('0');
319        }
320
321        let mut out = String::new();
322        if self.sign() == Sign::Negative {
323            out.push('-');
324        }
325        out.push_str(&padded[..int_digits]);
326        if padded.len() > int_digits {
327            out.push('.');
328            out.push_str(&padded[int_digits..]);
329        }
330        out.push('e');
331        out.push_str(&eng_exp.to_string());
332        out
333    }
334}
335
336// ===========================================================================
337// C99 %a-style hexadecimal float
338// ===========================================================================
339
340/// Map a hex digit character to its 0..=15 value, or `None` if not hex.
341fn hex_value(c: u8) -> Option<u8> {
342    match c {
343        b'0'..=b'9' => Some(c - b'0'),
344        b'a'..=b'f' => Some(c - b'a' + 10),
345        b'A'..=b'F' => Some(c - b'A' + 10),
346        _ => None,
347    }
348}
349
350/// Map a 0..=15 value to a lowercase hex digit character.
351fn hex_char(v: u8) -> char {
352    match v {
353        0..=9 => (b'0' + v) as char,
354        10..=15 => (b'a' + (v - 10)) as char,
355        _ => '0',
356    }
357}
358
359impl BigFloat {
360    /// Render this value as a C99 `%a`-style hexadecimal float string:
361    /// `±0x1.<hex-frac>p±<binexp>`.
362    ///
363    /// The rendering is **binary-exact**: the leading mantissa bit becomes the
364    /// `1` before the point, the remaining bits are grouped (MSB-first) into
365    /// 4-bit hex nibbles after the point, and the `p` exponent is the binary
366    /// exponent of the leading bit. The canonical zero renders as `0x0p0`.
367    ///
368    /// [`BigFloat::from_hex_float`] is the exact inverse, so
369    /// `from_hex_float(x.to_hex_string())` reproduces `x` bit-for-bit.
370    ///
371    /// # Examples
372    ///
373    /// ```
374    /// use oxinum_float::native::BigFloat;
375    /// let x = BigFloat::from_f64(12.0, 53).expect("finite");
376    /// // 12 = 1.1000…b × 2^3  →  0x1.8p3
377    /// assert_eq!(x.to_hex_string(), "0x1.8p3");
378    /// ```
379    pub fn to_hex_string(&self) -> String {
380        // Non-finite values must be handled before the is_zero() check, since
381        // NaN and Inf have mantissa=0 and would otherwise format incorrectly.
382        match self.class {
383            FloatClass::Nan => return "NaN".to_string(),
384            FloatClass::Infinite => {
385                return if self.sign() == Sign::Negative {
386                    "-inf".to_string()
387                } else {
388                    "inf".to_string()
389                };
390            }
391            FloatClass::Finite => {}
392        }
393        if self.is_zero() {
394            return "0x0p0".to_string();
395        }
396        let m = self.mantissa();
397        let bits = m.bit_length(); // >= 1 for non-zero, normalized to precision
398                                   // Leading bit is at index bits-1; its binary place value is
399                                   // exponent + (bits - 1).
400        let leading_index = bits - 1;
401        let p_exp = self.exponent().saturating_add(leading_index as i64);
402
403        let mut out = String::new();
404        if self.sign() == Sign::Negative {
405            out.push('-');
406        }
407        out.push_str("0x1");
408
409        // Fractional bits: the `leading_index` bits below the top bit, MSB
410        // first, grouped into nibbles. Pad the final nibble on the right with
411        // zero bits so the bit count is a multiple of four.
412        if leading_index > 0 {
413            let mut frac = String::new();
414            // Walk bit positions from (leading_index - 1) down to 0, four at a
415            // time, assembling each nibble MSB-first.
416            let mut pos = leading_index as i64 - 1;
417            while pos >= 0 {
418                let mut nibble: u8 = 0;
419                for _ in 0..4 {
420                    nibble <<= 1;
421                    if pos >= 0 {
422                        if m.test_bit(pos as u64) {
423                            nibble |= 1;
424                        }
425                        pos -= 1;
426                    }
427                    // When pos < 0 we shift in implicit zero bits (right pad).
428                }
429                frac.push(hex_char(nibble));
430            }
431            // Strip trailing '0' nibbles — they carry no information and keep
432            // the representation canonical/short while remaining exact.
433            while frac.ends_with('0') {
434                frac.pop();
435            }
436            if !frac.is_empty() {
437                out.push('.');
438                out.push_str(&frac);
439            }
440        }
441
442        out.push('p');
443        out.push_str(&p_exp.to_string());
444        out
445    }
446
447    /// Parse a C99 `%a`-style hexadecimal float string into a `BigFloat` at
448    /// `prec` bits of precision.
449    ///
450    /// Accepts `±0x<hexint>[.<hexfrac>]p±<decexp>` (the `0x`/`0X` prefix, at
451    /// least one hex digit overall, and the `p`/`P` binary exponent are all
452    /// mandatory). The parse is binary-exact before the final
453    /// normalization/rounding to `prec` bits.
454    ///
455    /// # Errors
456    ///
457    /// Returns [`OxiNumError::Parse`] on any malformed input: a missing `0x`
458    /// prefix, a missing `p` exponent, non-hex digits in the significand, a
459    /// non-decimal exponent, or stray characters.
460    ///
461    /// # Examples
462    ///
463    /// ```
464    /// use oxinum_float::native::BigFloat;
465    /// // 0x1.8p3 = 1.5 × 2^3 = 12.
466    /// let x = BigFloat::from_hex_float("0x1.8p3", 53).expect("valid hex float");
467    /// assert_eq!(x.to_f64(), 12.0);
468    /// ```
469    pub fn from_hex_float(s: &str, prec: u32) -> OxiNumResult<Self> {
470        let bytes = s.as_bytes();
471        let mut idx = 0usize;
472        let len = bytes.len();
473
474        let parse_err = |msg: &str| OxiNumError::Parse(format!("hex float: {msg}").into());
475
476        // --- Optional sign. ---
477        let sign = match bytes.first() {
478            Some(b'-') => {
479                idx += 1;
480                Sign::Negative
481            }
482            Some(b'+') => {
483                idx += 1;
484                Sign::Positive
485            }
486            _ => Sign::Positive,
487        };
488
489        // --- Mandatory 0x / 0X prefix. ---
490        if idx + 1 >= len || bytes[idx] != b'0' || (bytes[idx + 1] | 0x20) != b'x' {
491            return Err(parse_err("missing '0x' prefix"));
492        }
493        idx += 2;
494
495        // --- Integer hex part (zero or more hex digits). ---
496        let int_start = idx;
497        while idx < len && hex_value(bytes[idx]).is_some() {
498            idx += 1;
499        }
500        let int_part = &bytes[int_start..idx];
501
502        // --- Optional fractional hex part. ---
503        let mut frac_part: &[u8] = &[];
504        if idx < len && bytes[idx] == b'.' {
505            idx += 1;
506            let frac_start = idx;
507            while idx < len && hex_value(bytes[idx]).is_some() {
508                idx += 1;
509            }
510            frac_part = &bytes[frac_start..idx];
511        }
512
513        // At least one significand digit (integer or fractional) is required.
514        if int_part.is_empty() && frac_part.is_empty() {
515            return Err(parse_err("no significand digits"));
516        }
517
518        // --- Mandatory p / P binary exponent. ---
519        if idx >= len || (bytes[idx] | 0x20) != b'p' {
520            return Err(parse_err("missing 'p' exponent marker"));
521        }
522        idx += 1;
523
524        // --- Signed decimal exponent. ---
525        let exp_sign_neg = match bytes.get(idx) {
526            Some(b'-') => {
527                idx += 1;
528                true
529            }
530            Some(b'+') => {
531                idx += 1;
532                false
533            }
534            _ => false,
535        };
536        let exp_start = idx;
537        while idx < len && bytes[idx].is_ascii_digit() {
538            idx += 1;
539        }
540        if exp_start == idx {
541            return Err(parse_err("missing exponent digits"));
542        }
543        // Trailing garbage?
544        if idx != len {
545            return Err(parse_err("trailing characters"));
546        }
547        let exp_str = core::str::from_utf8(&bytes[exp_start..idx])
548            .map_err(|_| parse_err("non-UTF-8 exponent"))?;
549        let exp_mag: i64 = exp_str
550            .parse::<i64>()
551            .map_err(|_| parse_err("exponent out of range"))?;
552        let p_exp = if exp_sign_neg { -exp_mag } else { exp_mag };
553
554        // --- Assemble the mantissa from the concatenated hex digits. ---
555        // value = significand × 2^p_exp, where the significand's last hex digit
556        // sits 4·(frac nibbles) bits below the radix point. Treat all the hex
557        // digits as one integer `digits`, then:
558        //   value = digits × 2^(p_exp - 4·frac_nibbles)
559        let frac_nibbles = frac_part.len() as i64;
560        let mut all_digits: Vec<u8> = Vec::with_capacity(int_part.len() + frac_part.len());
561        all_digits.extend_from_slice(int_part);
562        all_digits.extend_from_slice(frac_part);
563        // Build a big integer from the hex digits (4 bits each), MSB-first.
564        let mut mantissa = BigUint::zero();
565        let sixteen = BigUint::from_u64(16);
566        for &c in &all_digits {
567            let v = hex_value(c).ok_or_else(|| parse_err("invalid hex digit"))?;
568            mantissa = &(&mantissa * &sixteen) + &BigUint::from_u64(v as u64);
569        }
570
571        if mantissa.is_zero() {
572            // e.g. 0x0p0, 0x0.0p5 — all forms of zero.
573            return Ok(Self::zero(prec));
574        }
575
576        let exponent = p_exp.saturating_sub(frac_nibbles.saturating_mul(4));
577        Ok(Self::from_parts(
578            sign,
579            mantissa,
580            exponent,
581            prec,
582            RoundingMode::HalfEven,
583        ))
584    }
585}