mcap/modules/Basic/float_to_string.jai

729 lines
25 KiB
Plaintext

// This file contains a port of the floating-point printing functionality in stb_sprintf; the original code has been
// significantly refactored to support this single use case.
stb_print_float :: (builder: *String_Builder, format: FormatFloat) -> int {
copy_repeated_character :: (destination: *u8, count: int, $character: u8) {
c :: cast,no_check(u32) character;
characters_wide : u32 : (c << 24) | (c << 16) | (c << 8) | c;
i := count;
d := destination;
while i {
if ((cast,no_check(u64) d) & 3) == 0 break;
d.* = character;
d += 1;
i -= 1;
}
while i >= 4 {
(cast(*u32) d).* = characters_wide;
d += 4;
i -= 4;
}
while i {
d.* = character;
d += 1;
i -= 1;
}
}
copy_integer_part :: (count: int, s: *u8) -> (n: int, s: *u8) #expand {
// This is just a macro so we can use these variables without passing them...
n := 0;
if use_thousand_separators {
while n < count {
cs += 1;
if cs > thousand_group_size {
cs = 0;
s.* = thousand_separator_character;
s += 1;
} else {
s.* = (sn + n).*;
s += 1;
n += 1;
}
}
} else {
while n < count {
s.* = (sn + n).*;
s += 1;
n += 1;
}
}
return n, s;
}
determine_mode_for_shortest :: () -> FormatFloat.Mode #expand {
// clamp the precision and delete extra zeros after clamp
n = pr;
if l > pr l = pr;
while l > 1 && pr && sn[l - 1] == #char "0" {
pr -= 1;
l -= 1;
}
if dp <= -4 || dp > n {
if pr > l pr = l - 1;
else if pr pr -= 1; // when using %e, there is one digit before the decimal
return .SCIENTIFIC;
} else {
// this is the insane action to get the pr to match %g sematics for %f
if dp > 0 pr = ifx dp < l then l - dp else 0;
else pr = -dp + ifx pr > l then l else pr;
return .DECIMAL;
}
}
num: [512] u8; // Big enough for e308 (with thousand separators) or e-307.
exponent: [8] u8; // Stores the exponent string at index 1 and the length of the string at index 0.
fw, pr, tz, dp, l, n, cs: int;
s, sn: *u8;
total_string_length: int;
trailing_zeroes_in_string: int;
decimal_digits_in_string: int;
// @Cleanup: We can just 'using' format here:
decimal_separator_character := format.decimal_separator_character;
thousand_separator_character := format.thousand_separator_character;
exponent_character := format.exponent_character;
use_thousand_separators := format.thousand_separators;
mode_to_use := format.mode;
// @Note: These features aren't available through FormatFloat yet.
use_left_justification :: false;
left_pad_with_zeroes :: false;
fw = format.width;
pr = format.trailing_width;
if pr < 0 pr = default_precision;
any := format.value;
assert(any.type != null);
type := any.type;
while type.type == .VARIANT {
variant := cast(*Type_Info_Variant) type;
type = variant.variant_of;
}
assert(type.type == .FLOAT);
info := cast(*Type_Info_Float) type;
float_value: float64;
if info.runtime_size == 8 {
float_value = (.*)cast(*float64) format.value.value_pointer;
} else {
float_value = (.*)cast(*float32) format.value.value_pointer;
}
pr_flag, pr_bias: u32;
if mode_to_use == {
case .SHORTEST;
if pr == 0 pr = 1;
pr_bias = 1;
pr_flag = 0x80000000;
case .SCIENTIFIC; pr_flag = 0x80000000;
}
is_negative := real_to_str(*sn, *l, num.data, *dp, float_value, (cast,no_check(u32)pr - pr_bias) | pr_flag);
prefix_character: u8;
if is_negative {
prefix_character = #char "-";
} else if format.positive_number_prefix == {
case .SPACE; prefix_character = #char " ";
case .PLUS; prefix_character = #char "+";
}
if dp == INFINITY_OR_NAN {
s = sn; cs = 0; pr = 0;
} else {
if mode_to_use == .SHORTEST mode_to_use = determine_mode_for_shortest();
if mode_to_use == .DECIMAL {
//
// Decimal float.
//
s_start := num.data + 64;
s = s_start;
// handle the three decimal varieties
if dp <= 0 { // handle 0.000*000xxxx
s.* = #char "0"; s += 1;
if pr { s.* = decimal_separator_character; s += 1; }
first_trailing_zero_in_string := s;
n = -dp;
if n > pr n = pr;
copy_repeated_character(s, n, #char "0"); s += n;
if l + n > pr l = pr - n;
decimal_digits_in_string = n + l;
for 1..l {
s.* = sn.*;
if sn.* != #char "0" first_trailing_zero_in_string = s + 1;
s += 1; sn += 1;
}
tz = pr - (n + l);
cs = 1 + (thousand_group_size << 24); // how many tens did we write (for commas below)
trailing_zeroes_in_string = s - first_trailing_zero_in_string;
} else {
cs = ifx use_thousand_separators then (600 - dp) % thousand_group_size else 0;
if dp >= l { // handle xxxx000*000.0
n, s = inline copy_integer_part(l, s);
if n < dp { // Copy the remaining zeroes of the integer part.
n = dp - n;
if !use_thousand_separators {
copy_repeated_character(s, n, #char "0"); s += n;
} else {
while n {
cs += 1;
if cs > thousand_group_size {
cs = 0;
s.* = thousand_separator_character; s += 1;
} else {
s.* = #char "0"; s += 1;
n -= 1;
}
}
}
}
cs = (s - s_start) + (thousand_group_size << 24); // cs is how many tens
if pr {
s.* = decimal_separator_character; s += 1;
tz = pr;
}
} else { // handle xxxxx.xxxx000*000
n, s = inline copy_integer_part(dp, s);
cs = (s - s_start) + (thousand_group_size << 24); // cs is how many tens
if pr { s.* = decimal_separator_character; s += 1; }
if l - dp > pr l = pr + dp;
decimal_digits_in_string = l - n;
first_trailing_zero_in_string := s;
sn = sn + n;
n = l - n;
for 1..n {
s.* = sn.*;
if sn.* != #char "0" first_trailing_zero_in_string = s + 1;
s += 1; sn += 1;
}
tz = pr - (l - dp);
trailing_zeroes_in_string = s - first_trailing_zero_in_string;
}
}
pr = 0;
l = s - s_start; // get the length that we copied
s = s_start;
} else {
//
// Scientific float.
//
s_start := num.data + 64;
s = s_start;
// handle leading chars
s.* = sn.*; s += 1;
if pr { s.* = decimal_separator_character; s += 1; }
// handle after decimal
if (l - 1) > pr l = pr + 1;
decimal_digits_in_string = l - 1;
first_trailing_zero_in_string := s;
n = 1;
while n < l {
s.* = (sn + n).*;
if s.* != #char "0" first_trailing_zero_in_string = s + 1;
s += 1; n += 1;
}
trailing_zeroes_in_string = s - first_trailing_zero_in_string;
// trailing zeros
tz = pr - (l - 1);
pr = 0;
// dump expo
exponent[1] = exponent_character;
dp -= 1;
if dp < 0 {
exponent[2] = #char "-";
dp = -dp;
} else {
exponent[2] = #char "+";
}
n = ifx dp >= 100 then 5 else 4;
exponent[0] = cast,no_check(u8)n;
while true {
exponent[n] = cast,no_check(u8)(#char "0" + (dp % 10));
if n <= 3 break;
n -= 1;
dp /= 10;
}
cs = 1 + (thousand_group_size << 24); // how many tens
l = s - s_start; // get the length that we copied
s = s_start;
}
}
//
// This used to be finalize_string():
//
append_repeated_character :: (builder: *String_Builder, count: int, $character: u8) -> int {
result := count;
while count {
available_range := ensure_contiguous_space_and_return_available_range(builder);
clamped_count := min(count, available_range.count);
count -= clamped_count;
copy_repeated_character(available_range.data, clamped_count, character);
advance_through_ensured_space(builder, clamped_count);
}
return result;
}
characters_removed_by_zero_removal: int;
if format.zero_removal != .NO && mode_to_use != .SHORTEST {
//
// :ZeroRemoval
//
// When we remove zeroes (and possibly the decimal separator itself), special behavior occurs when the width
// and/or trailing width are explicit (i.e., non-negative). When a width is set and a number of characters
// are removed by zero removal, the same number of spaces will be removed from any potential left padding,
// so that the printed number does not shift to the right. When a trailing width is set, any removed
// characters at the end will be replaced by spaces to maintain the width of the string.
//
// Note that the .SHORTEST mode already does its own version of zero removal, so using that in combination
// with a zero removal mode other than .YES might lead to somewhat surprising results.
//
if (decimal_digits_in_string || tz) && dp != INFINITY_OR_NAN {
if (decimal_digits_in_string > 0 && trailing_zeroes_in_string == decimal_digits_in_string) || (decimal_digits_in_string == 0 && tz > 0) {
if format.zero_removal == .YES {
characters_removed_by_zero_removal = trailing_zeroes_in_string + tz + 1;
l -= trailing_zeroes_in_string + 1;
tz = 0;
} else if format.zero_removal == .ONE_ZERO_AFTER_DECIMAL {
if trailing_zeroes_in_string {
characters_removed_by_zero_removal = (trailing_zeroes_in_string - 1) + tz;
l -= (trailing_zeroes_in_string - 1);
tz = 0;
} else {
characters_removed_by_zero_removal = tz - 1;
tz = 1;
}
}
} else if trailing_zeroes_in_string {
characters_removed_by_zero_removal = trailing_zeroes_in_string + tz;
l -= trailing_zeroes_in_string;
tz = 0;
} else if tz {
characters_removed_by_zero_removal = tz;
tz = 0;
}
}
}
// Calculate fw (= leading/trailing space) and pr (= leading zeroes).
pr = max(pr, l);
n = pr + exponent[0] + tz;
if prefix_character n += 1;
fw = max(fw, n);
fw -= n;
pr -= l;
// Maintain number alignment by taking the number of characters removed by zero removal and removing that many spaces from the left padding. :ZeroRemoval
fw -= min(fw, characters_removed_by_zero_removal);
if !use_left_justification {
if left_pad_with_zeroes { // if leading zeros, everything is in pr
pr = ifx fw > pr then fw else pr;
fw = 0;
} else {
use_thousand_separators = false;
}
}
// copy the spaces and/or zeros
if fw + pr {
// copy leading spaces (or when doing %8.4d stuff)
if !use_left_justification total_string_length += append_repeated_character(builder, fw, #char " ");
// copy leader
if prefix_character { append(builder, prefix_character); total_string_length += 1; }
// copy leading zeros
c := cs >> 24;
cs &= 0xffffff;
cs = ifx use_thousand_separators then c - ((pr + cs) % (c + 1)) else 0;
if pr {
if !use_thousand_separators {
total_string_length += append_repeated_character(builder, pr, #char "0");
} else {
for 1..pr {
if cs == c {
cs = 0;
append(builder, thousand_separator_character); total_string_length += 1;
} else {
cs += 1;
append(builder, #char "0"); total_string_length += 1;
}
}
}
}
} else if prefix_character { append(builder, prefix_character); total_string_length += 1; }
append(builder, s, l); total_string_length += l; // Copy number string.
total_string_length += append_repeated_character(builder, tz, #char "0"); // Copy trailing zeros.
append(builder, *exponent[1], exponent[0]); total_string_length += exponent[0]; // Copy exponent (if there is one).
// Add spaces back to compensate for removed characters. :ZeroRemoval
if format.trailing_width > -1 total_string_length += append_repeated_character(builder, characters_removed_by_zero_removal, #char " ");
// In case of left justify, put the spaces at the end.
if use_left_justification && fw > 0 total_string_length += append_repeated_character(builder, fw, #char " ");
return total_string_length;
}
#scope_file
default_precision :: 6;
thousand_group_size :: 3;
nan_label :: "NaN";
inf_label :: "Inf";
INFINITY_OR_NAN :: 0x7000;
// =======================================================================
// low level float utility functions
COPYFP :: inline (dest: *void, src: *void) {
(.*)cast(*u64)dest = (.*)cast(*u64)src;
}
bot :: float64.[0h3ff0000000000000, 0h4024000000000000, 0h4059000000000000, 0h408f400000000000, 0h40c3880000000000, 0h40f86a0000000000, 0h412e848000000000, 0h416312d000000000, 0h4197d78400000000, 0h41cdcd6500000000, 0h4202a05f20000000, 0h42374876e8000000, 0h426d1a94a2000000, 0h42a2309ce5400000, 0h42d6bcc41e900000, 0h430c6bf526340000, 0h4341c37937e08000, 0h4376345785d8a000, 0h43abc16d674ec800, 0h43e158e460913d00, 0h4415af1d78b58c40, 0h444b1ae4d6e2ef50, 0h4480f0cf064dd592];
negbot :: float64.[0h3fb999999999999a, 0h3f847ae147ae147b, 0h3f50624dd2f1a9fc, 0h3f1a36e2eb1c432d, 0h3ee4f8b588e368f1, 0h3eb0c6f7a0b5ed8d, 0h3e7ad7f29abcaf48, 0h3e45798ee2308c3a, 0h3e112e0be826d695, 0h3ddb7cdfd9d7bdbb, 0h3da5fd7fe1796495, 0h3d719799812dea11, 0h3d3c25c268497682, 0h3d06849b86a12b9b, 0h3cd203af9ee75616, 0h3c9cd2b297d889bc, 0h3c670ef54646d497, 0h3c32725dd1d243ac, 0h3bfd83c94fb6d2ac, 0h3bc79ca10c924223, 0h3b92e3b40a0e9b4f, 0h3b5e392010175ee6];
negboterr :: float64.[0hbc5999999999999a, 0hbc0eb851eb851eb8, 0hbbd89374bc6a7efa, 0hbbb6a161e4f765fe, 0hbb8ee78183f91e64, 0h3b4b5a63f9a49c2c, 0h3b15e1e99483b023, 0hbad03023df2d4c94, 0hbab34674bfabb83b, 0hba720a5465df8d2c, 0h3a47f7bc7b4d28aa, 0h39f97f27f0f6e886, 0hb9cecd79a5a0df95, 0h394ea70909833de7, 0hb97937831647f5a0, 0h3925b4c2ebe68799, 0hb90db7b2080a3029, 0hb8d7c628066e8cee, 0h388a52b31e9e3d07, 0h38675447a5d8e536, 0h383f769fb7e0b75e, 0hb7fa7566d9cba769];
top :: float64.[0h44b52d02c7e14af6, 0h497c06a5ec5433c6, 0h4e428bc8abe49f64, 0h53088ba3bf284e24, 0h57d03e29f5c2b18c, 0h5c957f48bb41db7c, 0h615c73892ecbfbf4, 0h6622d3d6f88f0b3d, 0h6ae8eb0138858d0a, 0h6fb07d457124123d, 0h7475d2ce55747a18, 0h793ce2137f743382, 0h7e031cfd3999f7b0];
negtop :: float64.[0h3b282db34012b251, 0h366244ce242c5561, 0h319b9b6364f30304, 0h2cd4dbf7b3f71cb7, 0h280f8587e7083e30, 0h2347d12a4670c123, 0h1e81fee341fc585d, 0h19bb31bb5dc320d2, 0h14f48c22ca71a1bd, 0h102f0ce4839198db, 0h0b677603725064a8, 0h06a1ba03f5b21000, 0h01dac9a7b3b7302f];
toperr :: float64.[0h4160000000000000, 0h45ebb542c80deb40, 0hcae83b80b9aab60a, 0hcfa32e22d17a166c, 0hd4523606902e180e, 0hd9296fb782462e87, 0hddf358952c0bd011, 0he2a78c1376a34b6c, 0he7817569fc243adf, 0hec5d9365a897aaa6, 0h7119050c256123a0, 0hf5db1799d76cc7a6, 0hfaa213fe39571a38];
negtoperr :: float64.[0h37c13badb829e079, 0hb2ee46a98d3d9f64, 0h2e3227c7218a2b65, 0h2951d96999aa01e9, 0ha4acc2229efc3962, 0h9fecd04a2263407a, 0h9b123b80f187a157, 0h965c4e22914ed912, 0h119bc296cdf42f82, 0h8cc9f9e7f4e16fe1, 0h880aeb0a72a8902a, 0h834e228e12c13408, 0h0000000000fa1259];
powten :: u64.[1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000, 10000000000, 100000000000, 1000000000000, 10000000000000, 100000000000000, 1000000000000000, 10000000000000000, 100000000000000000, 1000000000000000000, 10000000000000000000];
tento19th : u64 : 1000000000000000000;
ddmult_high :: (oh: float64, ol: float64, xh: float64, yh: float64) -> (oh: float64, ol: float64) {
ahi: float64 = 0;
bhi: float64 = 0;
bt: s64 = ---;
new_oh := xh*yh;
COPYFP(*bt, *xh);
bt &= cast,no_check(s64)((~cast,no_check(u64)0) << 27);
COPYFP(*ahi, *bt);
alo := xh - ahi;
COPYFP(*bt, *yh);
bt &= cast,no_check(s64)((~cast,no_check(u64)0) << 27);
COPYFP(*bhi, *bt);
blo := yh - bhi;
new_ol := ((ahi * bhi - new_oh) + ahi * blo + alo * bhi) + alo * blo;
return new_oh, new_ol;
}
ddtoS64 :: inline (xh: float64, xl: float64) -> s64 {
bits := cast,no_check(s64)xh;
vh := cast,no_check(float64) bits;
ahi := xh - vh;
t := ahi - xh;
alo := (xh - (ahi - t)) - (vh + t);
bits += cast,no_check(s64)(ahi + alo + xl);
return bits;
}
ddrenorm :: (oh: float64, ol: float64) -> (oh: float64, ol: float64) {
s := cast,no_check(float64)(oh + ol);
return s, ol - (s - oh);
}
ddmult_low :: inline (ol: float64, xh: float64, xl: float64, yh: float64, yl: float64) -> (ol: float64) {
return ol + xh*yl + xl*yh;
}
ddmult_low_small :: inline (ol: float64, xh: float64, yl: float64) -> (ol: float64) {
return ol + xh*yl;
}
raise_to_power10 :: (ohi: *float64, olo: *float64, d: float64, power: s32 /* power can be -323 to +350 */) {
ph, pl: float64;
if (power >= 0) && (power <= 22) {
ph, pl = ddmult_high(ph, pl, d, bot[power]);
} else {
e, et, eb: s32;
p2h, p2l: float64;
e = power;
if power < 0 e = -e;
et = (e * 0x2c9) >> 14;/* %23 */
if et > 13 et = 13;
eb = e - (et * 23);
ph = d; pl = 0;
if power < 0 {
if eb {
eb -= 1;
ph, pl = ddmult_high(ph, pl, d, negbot[eb]);
pl = ddmult_low_small(pl, d, negboterr[eb]);
}
if et {
ph, pl = ddrenorm(ph, pl);
et -= 1;
p2h, p2l = ddmult_high(p2h, p2l, ph, negtop[et]);
p2l = ddmult_low(p2l, ph, pl, negtop[et], negtoperr[et]);
ph = p2h;
pl = p2l;
}
} else {
if eb {
e = eb;
if eb > 22 eb = 22;
e -= eb;
ph, pl = ddmult_high(ph, pl, d, bot[eb]);
if e {
ph, pl = ddrenorm(ph, pl);
p2h, p2l = ddmult_high(p2h, p2l, ph, bot[e]);
p2l = ddmult_low_small(p2l, bot[e], pl);
ph = p2h;
pl = p2l;
}
}
if et {
ph, pl = ddrenorm(ph, pl);
et -= 1;
p2h, p2l = ddmult_high(p2h, p2l, ph, top[et]);
p2l = ddmult_low(p2l, ph, pl, top[et], toperr[et]);
ph = p2h;
pl = p2l;
}
}
}
ph, pl = ddrenorm(ph, pl);
ohi.* = ph;
olo.* = pl;
}
// given a float value, returns the significant bits in bits, and the position of the
// decimal point in decimal_pos. +/-INF and NAN are specified by special values
// returned in the decimal_pos parameter.
// frac_digits is absolute normally, but if you want from first significant digits (got %g and %e), or in 0x80000000
real_to_str :: (start: **u8, len: *int, out: *u8, decimal_pos: *int, value: float64, frac_digits: u32) -> bool {
bits: s64;
e, tens: s32;
d := value;
COPYFP(*bits, *d);
expo := cast,no_check(s32)((bits >> 52) & 2047);
is_negative := cast,no_check(bool)(bits >> 63);
if is_negative d = -d;
if expo == 2047 { // is nan or inf?
if bits & cast,no_check(s64)(((cast,no_check(u64)1) << 52) - 1) {
start.* = nan_label.data;
len.* = nan_label.count;
} else {
start.* = inf_label.data;
len.* = inf_label.count;
}
decimal_pos.* = INFINITY_OR_NAN;
return is_negative;
}
if expo == 0 { // is zero or denormal
if (bits << 1) == 0 { // do zero
decimal_pos.* = 1;
start.*= out;
out.* = #char "0";
len.* = 1;
return is_negative;
}
// find the right expo for denormals
{
one : u64 : 1;
v: s64 = cast,no_check(s64)(one << 51);
while (bits & v) == 0 {
expo -= 1;
v >>= 1;
}
}
}
// find the decimal exponent as well as the decimal bits of the value
{
ph, pl: float64;
// log10 estimate - very specifically tweaked to hit or undershoot by no more than 1 of log10 of all expos 1..2046
tens = expo - 1023;
tens = ifx tens < 0 then (tens * 617) / 2048 else ((tens * 1233) / 4096) + 1;
// move the significant bits into position and stick them into an int
raise_to_power10(*ph, *pl, d, 18 - tens);
// get full as much precision from double-double as possible
bits = ddtoS64(ph, pl);
// check if we undershot
if cast(u64)bits >= tento19th tens += 1;
}
// now do the rounding in integer land
#no_aoc { // In some (legit?) cases the tens + frac_digits can overflow.
frac_digits = ifx frac_digits & 0x80000000 then (frac_digits & 0x7ffffff) + 1 else cast,no_check(u32)tens + frac_digits;
}
if frac_digits < 24 {
dg: u32 = 1;
if cast,no_check(u64)bits >= powten[9] dg = 10;
done_fracing := false;
while cast,no_check(u64)bits >= powten[dg] {
dg += 1;
if dg == 20 {
done_fracing = true;
break;
}
}
if !done_fracing && frac_digits < dg {
// add 0.5 at the right position and round
e = cast,no_check(s32)dg - cast,no_check(s32)frac_digits;
if cast,no_check(u32)e < 24 {
r: u64 = powten[e];
bits = bits + cast,no_check(s64)(r / 2);
if cast,no_check(u64)bits >= powten[dg]
tens += 1;
bits /= cast,no_check(s64)r;
}
}
}
// kill long trailing runs of zeros
if bits {
done_killing := false;
while true {
if bits <= 0xffffffff break;
if bits % 1000 {
done_killing = true;
break;
}
bits /= 1000;
}
if !done_killing {
n := cast,no_check(u32)bits;
while (n % 1000) == 0
n /= 1000;
bits=n;
}
}
// convert to string
out += 64;
e = 0;
digitpair :: "00010203040506070809101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899";
while true {
n: u32 = ---;
o := out - 8;
// do the conversion in chunks of U32s (avoid most 64-bit divides, worth it, constant denomiators be damned)
if bits >= 100000000 {
n = cast,no_check(u32)(bits % 100000000);
bits /= 100000000;
} else {
n = cast,no_check(u32)bits;
bits = 0;
}
while n {
out -= 2;
(.*)cast,no_check(*u16)out = (.*)cast,no_check(*u16)*digitpair[(n % 100) * 2];
n /= 100;
e += 2;
}
if bits == 0 {
if e && out.* == #char "0" {
out += 1;
e -= 1;
}
break;
}
while out != o {
out -= 1;
out.* = #char "0";
e += 1;
}
}
decimal_pos.* = tens;
start.*= out;
len.* = e;
return is_negative;
}