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 => "ient + &BigUint::one(),
92 Ordering::Equal => {
93 // Exact half — round to even.
94 if quotient.test_bit(0) {
95 "ient + &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}