// Python-compatible repr helpers.
///|
/// Python `repr()` of a string.
pub fn py_repr(s : String) -> String {
@pystr.repr(s)
}
///|
fn py_bool(b : Bool) -> String {
if b {
"True"
} else {
"False"
}
}
///|
/// Python `repr(float)`.
pub fn py_float_repr(d : Double) -> String {
if d.is_nan() {
return "nan"
}
if d.is_pos_inf() {
return "inf"
}
if d.is_neg_inf() {
return "-inf"
}
if d == 0.0 {
return if 1.0 / d < 0.0 { "-0.0" } else { "0.0" }
}
let (negative, digits, decpt) = shortest_digits(d)
let sign = if negative { "-" } else { "" }
let nd = digits.length()
if decpt > 16 || decpt <= -4 {
// scientific: d.ddde[+-]XX
let exp = decpt - 1
let mantissa = if nd == 1 {
digits
} else {
digits.view(end_offset=1).to_owned() +
"." +
digits.view(start_offset=1).to_owned()
}
let exp_sign = if exp < 0 { "-" } else { "+" }
let e = exp.abs()
let exp_text = if e < 10 { "0\{e}" } else { e.to_string() }
sign + mantissa + "e" + exp_sign + exp_text
} else if decpt <= 0 {
sign + "0." + "0".repeat(-decpt) + digits
} else if decpt >= nd {
sign + digits + "0".repeat(decpt - nd) + ".0"
} else {
sign +
digits.view(end_offset=decpt).to_owned() +
"." +
digits.view(start_offset=decpt).to_owned()
}
}
///|
/// Shortest round-trip digits of a finite non-zero double, as
/// `(negative, digits, decpt)` meaning `0.digits × 10^decpt`.
fn shortest_digits(d : Double) -> (Bool, String, Int) {
let s = d.to_string()
let (negative, s) = match s.strip_prefix("-") {
Some(rest) => (true, rest.to_owned())
None => (false, s)
}
let (mantissa, exp) = match s.find("e") {
Some(i) =>
(
s.view(end_offset=i).to_owned(),
@string.parse_int(s.view(start_offset=i + 1).to_owned()) catch {
_ => 0
},
)
None => (s, 0)
}
let (int_part, frac_part) = match mantissa.find(".") {
Some(i) =>
(
mantissa.view(end_offset=i).to_owned(),
mantissa.view(start_offset=i + 1).to_owned(),
)
None => (mantissa, "")
}
let all = int_part + frac_part
// strip leading zeros
let mut lead = 0
while lead < all.length() - 1 && all[lead] == '0' {
lead += 1
}
let mut digits = all.view(start_offset=lead).to_owned()
let decpt = int_part.length() - lead + exp
// strip trailing zeros
let mut end = digits.length()
while end > 1 && digits[end - 1] == '0' {
end -= 1
}
digits = digits.view(end_offset=end).to_owned()
(negative, digits, decpt)
}
///|
/// Decompose a finite double into `(negative, mantissa, exponent)` with
/// `|d| = mantissa * 2^exponent`.
fn decompose(d : Double) -> (Bool, UInt64, Int) {
let bits = d.reinterpret_as_uint64()
let negative = bits >> 63 != 0UL
let exp_bits = ((bits >> 52) & 0x7ffUL).to_int()
let frac = bits & 0xfffffffffffffUL
if exp_bits == 0 {
(negative, frac, -1074)
} else {
(negative, frac | 0x10000000000000UL, exp_bits - 1075)
}
}
///|
fn big(n : Int) -> @bigint.BigInt {
@bigint.BigInt::from_int(n)
}
///|
fn pow_big(base : Int, exp : Int) -> @bigint.BigInt {
big(base).pow(big(exp))
}
///|
/// `round_half_even(mantissa * 2^exponent / 10^k)` computed exactly.
fn exact_round(mantissa : UInt64, exponent : Int, k : Int) -> @bigint.BigInt {
let mut num = @bigint.BigInt::from_uint64(mantissa)
let mut den = big(1)
if exponent >= 0 {
num = num * pow_big(2, exponent)
} else {
den = den * pow_big(2, -exponent)
}
if k >= 0 {
den = den * pow_big(10, k)
} else {
num = num * pow_big(10, -k)
}
let q = num / den
let r = num - q * den
let twice = r * big(2)
if twice > den || (twice == den && q % big(2) != big(0)) {
q + big(1)
} else {
q
}
}
///|
/// Python `format(d, ".{precision}f")`: fixed-point, correctly rounded
/// (round half to even on the exact binary value).
pub fn py_format_fixed(d : Double, precision : Int) -> String {
if d.is_nan() {
return "nan"
}
if d.is_pos_inf() {
return "inf"
}
if d.is_neg_inf() {
return "-inf"
}
let (negative, mantissa, exponent) = decompose(d)
let n = exact_round(mantissa, exponent, -precision)
let digits = n.to_string()
let digits = if digits.length() <= precision {
"0".repeat(precision - digits.length() + 1) + digits
} else {
digits
}
let int_part = digits.view(end_offset=digits.length() - precision).to_owned()
let frac_part = digits
.view(start_offset=digits.length() - precision)
.to_owned()
let sign = if negative { "-" } else { "" }
if precision == 0 {
sign + int_part
} else {
sign + int_part + "." + frac_part
}
}
///|
/// Round to `sig` significant digits: `(negative, digits, x)` meaning
/// `d.ddd × 10^x` with `digits.length() == sig`.
fn round_significant(d : Double, sig : Int) -> (Bool, String, Int) {
let (negative, mantissa, exponent) = decompose(d)
let (_, _, decpt) = shortest_digits(d)
let mut x = decpt - 1
let mut n = exact_round(mantissa, exponent, x - (sig - 1))
let lower = pow_big(10, sig - 1)
let upper = pow_big(10, sig)
if n < lower {
x -= 1
n = exact_round(mantissa, exponent, x - (sig - 1))
}
if n >= upper {
x += 1
n = exact_round(mantissa, exponent, x - (sig - 1))
}
(negative, n.to_string(), x)
}
///|
fn exp_suffix(x : Int, upper : Bool) -> String {
let e = x.abs()
(if upper { "E" } else { "e" }) +
(if x < 0 { "-" } else { "+" }) +
(if e < 10 { "0\{e}" } else { e.to_string() })
}
///|
fn non_finite(d : Double, upper : Bool) -> String? {
let s = if d.is_nan() {
"nan"
} else if d.is_pos_inf() {
"inf"
} else if d.is_neg_inf() {
"-inf"
} else {
return None
}
Some(if upper { s.to_upper() } else { s })
}
///|
/// Python `format(d, ".{precision}e")` (scientific notation, correctly
/// rounded).
pub fn py_format_exp(
d : Double,
precision : Int,
upper? : Bool = false,
) -> String {
if non_finite(d, upper) is Some(s) {
return s
}
if d == 0.0 {
let sign = if 1.0 / d < 0.0 { "-" } else { "" }
let frac = if precision > 0 { "." + "0".repeat(precision) } else { "" }
return sign + "0" + frac + exp_suffix(0, upper)
}
let (negative, digits, x) = round_significant(d, precision + 1)
let sign = if negative { "-" } else { "" }
let mantissa = if precision > 0 {
digits.view(end_offset=1).to_owned() +
"." +
digits.view(start_offset=1).to_owned()
} else {
digits
}
sign + mantissa + exp_suffix(x, upper)
}
///|
/// Python `format(d, ".{precision}g")` (general format; `alternate` keeps
/// trailing zeros like `%#g`).
pub fn py_format_general(
d : Double,
precision? : Int = 6,
upper? : Bool = false,
alternate? : Bool = false,
) -> String {
if non_finite(d, upper) is Some(s) {
return s
}
let precision = if precision == 0 { 1 } else { precision }
fn strip(s : String) -> String {
if alternate || !s.contains(".") {
return s
}
let mut end = s.length()
while end > 0 && s[end - 1] == '0' {
end -= 1
}
if end > 0 && s[end - 1] == '.' {
end -= 1
}
s.view(end_offset=end).to_owned()
}
if d == 0.0 {
let sign = if 1.0 / d < 0.0 { "-" } else { "" }
return sign + strip("0." + "0".repeat(precision - 1))
}
let (negative, digits, x) = round_significant(d, precision)
let sign = if negative { "-" } else { "" }
if x < -4 || x >= precision {
let mantissa = strip(
if digits.length() > 1 || alternate {
digits.view(end_offset=1).to_owned() +
"." +
digits.view(start_offset=1).to_owned()
} else {
digits
},
)
sign + mantissa + exp_suffix(x, upper)
} else if x >= 0 {
let int_part = digits.view(end_offset=x + 1).to_owned()
let frac = digits.view(start_offset=x + 1).to_owned()
sign + strip(int_part + "." + frac)
} else {
sign + strip("0." + "0".repeat(-x - 1) + digits)
}
}
///|
/// Python `format(d, "g")` (6 significant digits, trailing zeros removed).
pub fn py_format_g(d : Double) -> String {
py_format_general(d)
}
///|
/// Insert thousands separators (Python's `,` format option) into the
/// integer part of a formatted number.
pub fn py_group_thousands(s : String) -> String {
let (sign, rest) = match s.strip_prefix("-") {
Some(rest) => ("-", rest.to_owned())
None => ("", s)
}
let (int_part, tail) = match rest.find(".") {
Some(i) =>
(rest.view(end_offset=i).to_owned(), rest.view(start_offset=i).to_owned())
None => (rest, "")
}
let buf = StringBuilder()
let n = int_part.length()
for i in 0.. 0 && (n - i) % 3 == 0 {
buf.write_char(',')
}
buf.write_char(int_part[i].to_int().unsafe_to_char())
}
sign + buf.to_string() + tail
}
///|
/// Python `repr()` of a JSON value interpreted as the Python value it
/// came from (dicts, lists, str, int/float, True/False/None). Integral
/// numbers are shown as ints.
pub fn py_json_repr(value : Json) -> String {
match value {
Null => "None"
True => "True"
False => "False"
Number(n, ..) =>
if n == n.floor() && n.abs() < 1.0e16 {
n.to_int64().to_string()
} else {
py_float_repr(n)
}
String(s) => py_repr(s)
Array(items) => "[" + items.map(py_json_repr).join(", ") + "]"
Object(m) => {
let parts = []
for k, v in m {
parts.push(py_repr(k) + ": " + py_json_repr(v))
}
"{" + parts.join(", ") + "}"
}
}
}