use super::ZEROS;
pub(crate) const DIGIT_SCRATCH: usize = 48;
pub(crate) struct Digits {
pub buf: [u8; DIGIT_SCRATCH],
pub start: usize,
pub len: usize,
pub e10: i32,
}
const NUM_POW10S: usize = 618;
const DEC_EXP_MIN: i32 = -293;
const POW10_MINOR: [u64; 28] = [
0x8000000000000000,
0xa000000000000000,
0xc800000000000000,
0xfa00000000000000,
0x9c40000000000000,
0xc350000000000000,
0xf424000000000000,
0x9896800000000000,
0xbebc200000000000,
0xee6b280000000000,
0x9502f90000000000,
0xba43b74000000000,
0xe8d4a51000000000,
0x9184e72a00000000,
0xb5e620f480000000,
0xe35fa931a0000000,
0x8e1bc9bf04000000,
0xb1a2bc2ec5000000,
0xde0b6b3a76400000,
0x8ac7230489e80000,
0xad78ebc5ac620000,
0xd8d726b7177a8000,
0x878678326eac9000,
0xa968163f0a57b400,
0xd3c21bcecceda100,
0x84595161401484a0,
0xa56fa5b99019a5c8,
0xcecb8f27f4200f3a,
];
const POW10_MAJOR: [[u64; 2]; 23] = [
[0xaf8e5410288e1b6f, 0x07ecf0ae5ee44dda],
[0xb1442798f49ffb4a, 0x99cd11cfdf41779d],
[0xb2fe3f0b8599ef07, 0x861fa7e6dcb4aa15],
[0xb4bca50b065abe63, 0x0fed077a756b53aa],
[0xb67f6455292cbf08, 0x1a3bc84c17b1d543],
[0xb84687c269ef3bfb, 0x3d5d514f40eea742],
[0xba121a4650e4ddeb, 0x92f34d62616ce413],
[0xbbe226efb628afea, 0x890489f70a55368c],
[0xbdb6b8e905cb600f, 0x5400e987bbc1c921],
[0xbf8fdb78849a5f96, 0xde98520472bdd034],
[0xc16d9a0095928a27, 0x75b7053c0f178294],
[0xc350000000000000, 0x0000000000000000],
[0xc5371912364ce305, 0x6c28000000000000],
[0xc722f0ef9d80aad6, 0x424d3ad2b7b97ef6],
[0xc913936dd571c84c, 0x03bc3a19cd1e38ea],
[0xcb090c8001ab551c, 0x5cadf5bfd3072cc6],
[0xcd036837130890a1, 0x36dba887c37a8c10],
[0xcf02b2c21207ef2e, 0x94f967e45e03f4bc],
[0xd106f86e69d785c7, 0xe13336d701beba52],
[0xd31045a8341ca07c, 0x1ede48111209a051],
[0xd51ea6fa85785631, 0x552a74227f3ea566],
[0xd732290fbacaf133, 0xa97c177947ad4096],
[0xd94ad8b1c7380874, 0x18375281ae7822bc],
];
const POW10_FIXUPS: [u32; 20] = [
0x0a4e363f, 0x00001840, 0x00006400, 0x24200040, 0x00000000, 0x0c000000, 0x82c81380, 0x5e4ce01f,
0xd730f60f, 0x0000001b, 0x00000000, 0xcdf7fffc, 0x6e8201d8, 0x40cd3fd1, 0xdb642501, 0x00000d0d,
0x14042400, 0x53713840, 0x11781db4, 0x00000000,
];
#[inline(always)]
const fn umul_hi(x: u64, y: u64) -> u64 {
((x as u128 * y as u128) >> 64) as u64
}
const fn compute_pow10(i: usize) -> [u64; 2] {
let m = POW10_MINOR[(i + 10) % POW10_MINOR.len()];
let h = POW10_MAJOR[(i + 10) / POW10_MINOR.len()];
let (h_hi, h_lo) = (h[0], h[1]);
let c1_carry = umul_hi(h_lo, m);
let c0 = h_lo.wrapping_mul(m);
let c1 = c1_carry.wrapping_add(h_hi.wrapping_mul(m));
let c2 = ((c1 < c1_carry) as u64).wrapping_add(umul_hi(h_hi, m));
let (hi, lo) = if (c2 >> 63) != 0 {
(c2, c1)
} else {
(c2 << 1 | c1 >> 63, c1 << 1 | c0 >> 63)
};
[
hi,
lo.wrapping_sub(((POW10_FIXUPS[i >> 5] >> (i & 31)) & 1) as u64),
]
}
static POW10: [[u64; 2]; NUM_POW10S] = {
let mut t = [[0u64; 2]; NUM_POW10S];
let mut i = 0;
while i < NUM_POW10S {
t[i] = compute_pow10(i);
i += 1;
}
t
};
#[inline(always)]
fn pow10(dec_exp: i32) -> (u64, u64) {
let e = POW10[(dec_exp - DEC_EXP_MIN) as usize];
(e[0], e[1])
}
#[inline(always)]
const fn compute_dec_exp(bin_exp: i32, regular: bool) -> i32 {
const LOG10_2_SIG: i64 = 315_653;
const LOG10_3_OVER_4_SIG: i64 = 131_072;
let n = bin_exp as i64 * LOG10_2_SIG - (!regular as i64) * LOG10_3_OVER_4_SIG;
(n >> 20) as i32
}
#[inline(always)]
const fn compute_exp_shift(bin_exp: i32, dec_exp: i32) -> i32 {
const LOG2_POW10_SIG: i32 = 217_707;
let pow10_bin_exp = (-dec_exp * LOG2_POW10_SIG) >> 16;
bin_exp + pow10_bin_exp + 1
}
const EXTRA_SHIFT: i32 = 6;
static EXP_SHIFTS: [u8; 2048] = {
let mut t = [0u8; 2048];
let mut raw_exp = 0i32;
while raw_exp < 2048 {
let bin_exp = raw_exp - EXP_OFFSET_F64 + (raw_exp == 0) as i32;
let dec_exp = compute_dec_exp(bin_exp, true);
t[raw_exp as usize] = (compute_exp_shift(bin_exp, dec_exp + 1) + EXTRA_SHIFT) as u8;
raw_exp += 1;
}
t
};
#[inline(always)]
fn umul192_hi128(x_hi: u64, x_lo: u64, y: u64) -> (u64, u64) {
let p = x_hi as u128 * y as u128;
let p_lo = p as u64;
let lo = p_lo.wrapping_add(umul_hi(x_lo, y));
let hi = ((p >> 64) as u64).wrapping_add((lo < p_lo) as u64);
(hi, lo)
}
#[inline(always)]
fn umul_add_hi(x: u64, y: u64, c: u64) -> u64 {
(((x as u128 * y as u128) + c as u128) >> 64) as u64
}
struct Decimal {
sig: u64,
exp: i32,
last_digit: u32,
has_last_digit: bool,
}
impl Decimal {
#[inline(always)]
fn held_digit(&self) -> u64 {
if self.has_last_digit {
self.last_digit as u64
} else {
0
}
}
}
#[cold]
#[inline(never)]
fn rescale_subnormal(d: &Decimal, threshold: u64) -> Decimal {
let mut sig = d.sig * 10 + d.held_digit();
let mut exp = d.exp;
while sig < threshold {
sig *= 10;
exp -= 1;
}
let last_digit = (sig % 10) as u32;
Decimal {
sig: sig / 10,
exp,
last_digit,
has_last_digit: last_digit != 0,
}
}
const EXP_OFFSET_F64: i32 = 1023 + 52;
const EXP_OFFSET_F32: i32 = 127 + 23;
const BIASED_HALF: u64 = (1u64 << 63) + 6;
#[cold]
#[inline(never)]
fn to_decimal_irregular(bin_sig: u64, bin_exp: i32) -> Decimal {
let dec_exp = compute_dec_exp(bin_exp, false);
let shift = compute_exp_shift(bin_exp, dec_exp + 1) + EXTRA_SHIFT;
debug_assert!((4..=EXTRA_SHIFT + 1).contains(&shift));
let (p10_hi, p10_lo) = pow10(-dec_exp - 1);
let (p_hi, p_lo) = umul192_hi128(p10_hi, p10_lo, bin_sig << shift);
let integral = p_hi >> EXTRA_SHIFT;
let fractional = (p_hi << (64 - EXTRA_SHIFT)) | (p_lo >> EXTRA_SHIFT);
let half_ulp = p10_hi >> (EXTRA_SHIFT + 1 - shift);
let round_up = half_ulp > u64::MAX - fractional;
let round_down = (half_ulp >> 1) > fractional;
let nearest = umul_add_hi(fractional, 10, (1u64 << 63) - 1);
let lowest = umul_add_hi(fractional.wrapping_sub(half_ulp >> 1), 10, u64::MAX);
Decimal {
sig: integral + round_up as u64,
exp: dec_exp,
last_digit: if nearest < lowest { lowest } else { nearest } as u32,
has_last_digit: !(round_up | round_down),
}
}
#[inline(always)]
fn to_decimal_f64(bin_sig: u64, raw_exp: i32, regular: bool) -> Decimal {
let bin_exp = raw_exp - EXP_OFFSET_F64;
if !regular {
return to_decimal_irregular(bin_sig, bin_exp);
}
let dec_exp = compute_dec_exp(bin_exp, true);
let shift = EXP_SHIFTS[raw_exp as usize] as i32;
debug_assert_eq!(shift, compute_exp_shift(bin_exp, dec_exp + 1) + EXTRA_SHIFT);
debug_assert!((3..=EXTRA_SHIFT).contains(&shift));
let (p10_hi, p10_lo) = pow10(-dec_exp - 1);
let (p_hi, p_lo) = umul192_hi128(p10_hi, p10_lo, bin_sig << shift);
let integral = p_hi >> EXTRA_SHIFT;
let fractional = (p_hi << (64 - EXTRA_SHIFT)) | (p_lo >> EXTRA_SHIFT);
let even = 1 - (bin_sig & 1);
let half_ulp = (p10_hi >> (EXTRA_SHIFT + 1 - shift)) + even;
let round_up = fractional.wrapping_add(half_ulp) < fractional;
let round_down = half_ulp > fractional;
let mut last_digit = umul_add_hi(fractional, 10, BIASED_HALF);
if fractional == 1u64 << 62 {
last_digit = 2;
}
Decimal {
sig: integral + round_up as u64,
exp: dec_exp,
last_digit: last_digit as u32,
has_last_digit: !(round_up | round_down),
}
}
const F32_EXTRA_SHIFT: i32 = 34;
const F32_FRAC_MASK: u64 = (1u64 << F32_EXTRA_SHIFT) - 1;
#[inline(always)]
fn to_decimal_f32(bin_sig: u32, raw_exp: i32, regular: bool) -> Decimal {
let bin_exp = raw_exp - EXP_OFFSET_F32;
if !regular {
return to_decimal_irregular(bin_sig as u64, bin_exp);
}
let dec_exp = compute_dec_exp(bin_exp, true);
let shift = compute_exp_shift(bin_exp, dec_exp + 1) + F32_EXTRA_SHIFT;
debug_assert!((F32_EXTRA_SHIFT - 3..=F32_EXTRA_SHIFT).contains(&shift));
let p10_hi = pow10(-dec_exp - 1).0;
let p = umul_hi(p10_hi + 1, (bin_sig as u64) << shift);
let integral = p >> F32_EXTRA_SHIFT;
let fractional = p & F32_FRAC_MASK;
let even = 1 - (bin_sig as u64 & 1);
let half_ulp = (p10_hi >> (65 - shift)) + even;
let round_up = (fractional + half_ulp) >> F32_EXTRA_SHIFT != 0;
let round_down = half_ulp > fractional;
let prod = fractional * 10;
let mut last_digit = prod >> F32_EXTRA_SHIFT;
let rem = prod & F32_FRAC_MASK;
let half = 1u64 << (F32_EXTRA_SHIFT - 1);
last_digit += (rem > half || (rem == half && last_digit & 1 != 0)) as u64;
Decimal {
sig: integral + round_up as u64,
exp: dec_exp,
last_digit: last_digit as u32,
has_last_digit: !(round_up | round_down),
}
}
#[inline]
fn to_bcd8(v: u32) -> (u64, usize) {
const DIV10K_SIG: u64 = (1u64 << 40) / 10_000 + 1;
const NEG10K: u64 = (1u64 << 32) - 10_000;
const DIV100_SIG: u64 = (1u64 << 19) / 100 + 1;
const NEG100: u64 = (1u64 << 16) - 100;
const DIV10_SIG: u64 = (1u64 << 10) / 10 + 1;
const NEG10: u64 = (1u64 << 8) - 10;
let v = v as u64;
debug_assert!(v < 100_000_000);
let d4 = v + NEG10K * ((v * DIV10K_SIG) >> 40);
let d2 = d4 + NEG100 * (((d4 * DIV100_SIG) >> 19) & 0x7f_0000_007f);
let d1 = d2 + NEG10 * (((d2 * DIV10_SIG) >> 10) & 0xf_000f_000f_000f);
let bcd = d1.swap_bytes();
(bcd, nonzero_end(bcd))
}
#[inline(always)]
fn nonzero_end(bcd: u64) -> usize {
(70 - (bcd << 1).leading_zeros() as usize) / 8
}
#[inline]
fn lay_out<const WIDTH: usize>(d: &Decimal, padded: bool, e10: i32) -> Digits {
let mut buf = [0u8; DIGIT_SCRATCH];
let end = if WIDTH == 16 {
let hi = (d.sig / 100_000_000) as u32;
let lo = (d.sig % 100_000_000) as u32;
let (bcd_hi, end_hi) = to_bcd8(hi);
let (bcd_lo, end_lo) = to_bcd8(lo);
buf[..8].copy_from_slice(&(bcd_hi + ZEROS).to_le_bytes());
buf[8..16].copy_from_slice(&(bcd_lo + ZEROS).to_le_bytes());
if lo == 0 { end_hi } else { 8 + end_lo }
} else {
let (bcd, end) = to_bcd8(d.sig as u32);
buf[..8].copy_from_slice(&(bcd + ZEROS).to_le_bytes());
end
};
let start = padded as usize;
let len = if d.has_last_digit {
debug_assert!(d.last_digit < 10);
buf[WIDTH] = b'0' + d.last_digit as u8;
WIDTH + 1 - start
} else {
end - start
};
debug_assert!(len >= 1 && buf[start] != b'0');
Digits {
buf,
start,
len,
e10,
}
}
const F64_THRESHOLD: u64 = 1_000_000_000_000_000;
const F32_THRESHOLD: u64 = 10_000_000;
pub(crate) fn digits_f64(mag: f64) -> Digits {
let bits = mag.to_bits();
let raw_exp = ((bits >> 52) & 0x7ff) as i32;
let bin_sig = bits & ((1u64 << 52) - 1);
debug_assert!(raw_exp != 0x7ff && mag > 0.0);
let d = if raw_exp == 0 {
subnormal_f64(bin_sig)
} else {
to_decimal_f64(bin_sig | (1u64 << 52), raw_exp, bin_sig != 0)
};
let full_width = d.sig >= F64_THRESHOLD;
let e10 = d.exp + 15 + full_width as i32;
lay_out::<16>(&d, !full_width, e10)
}
#[cold]
#[inline(never)]
fn subnormal_f64(bin_sig: u64) -> Decimal {
rescale_subnormal(&to_decimal_f64(bin_sig, 1, true), F64_THRESHOLD)
}
#[cold]
#[inline(never)]
fn subnormal_f32(bin_sig: u32) -> Decimal {
rescale_subnormal(&to_decimal_f32(bin_sig, 1, true), F32_THRESHOLD)
}
pub(crate) fn digits_f32(mag: f32) -> Digits {
let bits = mag.to_bits();
let raw_exp = ((bits >> 23) & 0xff) as i32;
let bin_sig = bits & ((1u32 << 23) - 1);
debug_assert!(raw_exp != 0xff && mag > 0.0);
let mut d = if raw_exp == 0 {
subnormal_f32(bin_sig)
} else {
to_decimal_f32(bin_sig | (1u32 << 23), raw_exp, bin_sig != 0)
};
let full_width = d.sig >= F32_THRESHOLD;
let mut e10 = d.exp + 7 + full_width as i32;
if d.sig < 1_000_000 {
d.sig = d.sig * 10 + d.held_digit();
d.has_last_digit = false;
e10 -= 1;
}
lay_out::<8>(&d, !full_width, e10)
}
#[cfg(test)]
mod tests {
use super::to_bcd8;
#[test]
#[ignore = "several seconds: checks all 10^8 inputs"]
fn to_bcd8_exhaustive() {
for v in 0u32..100_000_000 {
let (bcd, end) = to_bcd8(v);
let want = format!("{v:08}");
for (i, (got, wanted)) in bcd.to_le_bytes().iter().zip(want.bytes()).enumerate() {
assert_eq!(*got, wanted - b'0', "v={v} byte {i}");
}
assert_eq!(end, want.trim_end_matches('0').len(), "v={v} end");
}
}
#[test]
fn to_bcd8_boundaries() {
for (v, digits, end) in [
(0u32, "00000000", 0),
(1, "00000001", 8),
(10, "00000010", 7),
(99_999_999, "99999999", 8),
(10_000_000, "10000000", 1),
(12_345_678, "12345678", 8),
(90_000_000, "90000000", 1),
(1_020_000, "01020000", 4),
] {
let (bcd, got_end) = to_bcd8(v);
let got: Vec<u8> = bcd.to_le_bytes().iter().map(|b| b + b'0').collect();
assert_eq!(core::str::from_utf8(&got).unwrap(), digits, "v={v}");
assert_eq!(got_end, end, "v={v}");
}
}
}