///|
/// Extract the exact non-negative rational value of a finite binary64.
/// Decimal rounding uses integers so it is identical on Wasm, JS, and native,
/// including halfway cases, subnormal values, and numbers above Int64's range.
fn decimal_ratio(value : Double) -> (@bigint.BigInt, @bigint.BigInt) {
// The ratio is only used with an absolute value. Clear the sign bit here
// as well as at the call sites so negative values follow the same exact
// rounding path as positive values.
let bits = value.reinterpret_as_uint64() & 0x7fffffffffffffffUL
let biased = ((bits >> 52) & 0x7ffUL).to_int()
let fraction = bits & 0xfffffffffffffUL
let significand = if biased == 0 {
fraction
} else {
fraction | 0x10000000000000UL
}
let exponent = if biased == 0 { -1074 } else { biased - 1075 }
let numerator = @bigint.BigInt::from_uint64(significand)
let one = @bigint.BigInt::from_int(1)
if exponent >= 0 {
(numerator << exponent, one)
} else {
(numerator, one << -exponent)
}
}
///|
fn decimal_power(exponent : Int32) -> @bigint.BigInt {
@bigint.BigInt::from_int(10).pow(@bigint.BigInt::from_int(exponent))
}
///|
/// Round abs(value) * 10^places to nearest, with halfway cases going to even.
fn decimal_round(value : Double, places : Int32) -> @bigint.BigInt {
let (n, d) = decimal_ratio(value)
let numerator = if places >= 0 { n * decimal_power(places) } else { n }
let denominator = if places < 0 { d * decimal_power(-places) } else { d }
let quotient = numerator / denominator
let two = @bigint.BigInt::from_int(2)
let remainder = numerator % denominator * two
if remainder > denominator ||
(remainder == denominator && !(quotient % two).is_zero()) {
quotient + @bigint.BigInt::from_int(1)
} else {
quotient
}
}
///|
fn numeric_sign(value : Double) -> String {
if value.reinterpret_as_uint64() >> 63 != 0UL {
"-"
} else {
""
}
}
///|
fn numeric_special(value : Double) -> String? {
if value.is_nan() {
Some("nan")
} else if value.is_inf() {
Some(numeric_sign(value) + "inf")
} else {
None
}
}
///|
/// Locate the exact decimal order; log10 is only an initial approximation.
fn decimal_exponent(value : Double) -> Int32 {
if value == 0.0 {
return 0
}
let (n, d) = decimal_ratio(value)
let mut exponent = @math.log10(Double::abs(value)).floor().to_int()
while decimal_compare_power(n, d, exponent) < 0 {
exponent -= 1
}
while decimal_compare_power(n, d, exponent + 1) >= 0 {
exponent += 1
}
exponent
}
///|
fn decimal_compare_power(
numerator : @bigint.BigInt,
denominator : @bigint.BigInt,
exponent : Int32,
) -> Int32 {
if exponent >= 0 {
numerator.compare(denominator * decimal_power(exponent))
} else {
(numerator * decimal_power(-exponent)).compare(denominator)
}
}
///|
/// Rounded significant digits and the resulting decimal exponent.
fn decimal_significant(value : Double, digits : Int32) -> (String, Int32) {
if value == 0.0 {
return (String::make(digits, '0'), 0)
}
let mut exponent = decimal_exponent(value)
let rounded = decimal_round(value, digits - 1 - exponent).to_string()
if rounded.length() > digits {
exponent += 1
(rounded[:digits].to_owned(), exponent)
} else {
(rounded.pad_start(digits, '0'), exponent)
}
}
///|
fn decimal_point(digits : String, position : Int32) -> String {
if position <= 0 {
"0." + String::make(-position, '0') + digits
} else if position >= digits.length() {
digits + String::make(position - digits.length(), '0')
} else {
digits[:position].to_owned() + "." + digits[position:].to_owned()
}
}
///|
fn decimal_exponent_suffix(exponent : Int32) -> String {
"e" +
(if exponent >= 0 { "+" } else { "-" }) +
Int32::abs(exponent).to_string().pad_start(2, '0')
}
///|
fn format_fixed(value : Double, precision : Int32) -> String {
if numeric_special(value) is Some(text) {
return text
}
let places = Int32::max(0, Int32::min(100, precision))
let digits = decimal_round(value, places)
.to_string()
.pad_start(places + 1, '0')
numeric_sign(value) + decimal_point(digits, digits.length() - places)
}
///|
fn format_integer(value : Double) -> String {
if numeric_special(value) is Some(text) {
return text
}
let (numerator, denominator) = decimal_ratio(value)
let truncated = numerator / denominator
(if truncated.is_zero() { "" } else { numeric_sign(value) }) +
truncated.to_string()
}
///|
fn format_scientific(value : Double, precision : Int32) -> String {
if numeric_special(value) is Some(text) {
return text
}
let places = Int32::max(0, Int32::min(100, precision))
let (digits, exponent) = decimal_significant(value, places + 1)
numeric_sign(value) +
decimal_point(digits, 1) +
decimal_exponent_suffix(exponent)
}
///|
fn format_general(
value : Double,
precision : Int32?,
alternate : Bool,
) -> String {
if numeric_special(value) is Some(text) {
return text
}
let count = Int32::max(1, Int32::min(100, precision.unwrap_or(6)))
let (digits, exponent) = decimal_significant(value, count)
let scientific = exponent < -4 || exponent >= count
let mantissa = decimal_point(
digits,
if scientific {
1
} else {
exponent + 1
},
)
let rendered = if alternate {
if mantissa.contains(".") {
mantissa
} else {
mantissa + "."
}
} else {
trim_decimal(mantissa)
}
numeric_sign(value) +
rendered +
(if scientific { decimal_exponent_suffix(exponent) } else { "" })
}
///|
/// Remove fractional trailing zeros, preserving the integer part and exponent.
fn trim_decimal(value : String) -> String {
let exponent_start = value.find("e").unwrap_or(value.length())
match value.find(".") {
Some(dot) if dot < exponent_start => {
let mut end = exponent_start
while end > dot + 1 && value[end - 1] == 48 {
end -= 1
}
if end == dot + 1 {
end = dot
}
value[:end].to_owned() + value[exponent_start:].to_owned()
}
_ => value
}
}