///|
/// Preserve the actual scalar produced by the retained list or ndarray.
priv enum NamedFormatValue {
PythonFloat(Double)
PythonInteger(@bigint.BigInt)
NumpyFloat64(Double)
NumpyInt64(Int64)
}
///|
fn named_format_error(
kind : String,
message : String,
) -> String raise @core.DataError {
raise @core.DataError::InvalidTicks(message=kind + ": " + message)
}
///|
fn named_format_value_double(
value : NamedFormatValue,
) -> Double raise @core.DataError {
match value {
PythonFloat(number) | NumpyFloat64(number) => number
PythonInteger(number) => percent_python_integer_double(number)
NumpyInt64(number) => number.to_double()
}
}
///|
fn named_format_value_str(
value : NamedFormatValue,
) -> String raise @core.DataError {
match value {
PythonFloat(number) | NumpyFloat64(number) =>
named_format_float_repr(number)
PythonInteger(number) => scalar_percent_integer_str(number)
NumpyInt64(number) => number.to_string()
}
}
///|
fn named_format_value_repr(
value : NamedFormatValue,
) -> String raise @core.DataError {
match value {
PythonFloat(number) => named_format_float_repr(number)
PythonInteger(number) => scalar_percent_integer_str(number)
NumpyFloat64(number) =>
"np.float64(" + named_format_float_repr(number) + ")"
NumpyInt64(number) => "np.int64(" + number.to_string() + ")"
}
}
///|
/// Core Ryu (and JS Number::toString) supplies shortest round-trip digits.
/// Reposition those digits using Python's -4/16 notation thresholds, retaining
/// signed zero, a fractional digit for fixed integers and two exponent digits.
fn named_format_float_repr(value : Double) -> String {
if numeric_special(value) is Some(text) {
return text
}
if value == 0.0 {
return numeric_sign(value) + "0.0"
}
let shortest = Double::abs(value).to_string()
let exponent_index = shortest.find("e").unwrap_or(shortest.length())
let coefficient = shortest[:exponent_index].to_owned()
let explicit_exponent = if exponent_index < shortest.length() {
try! @string.parse_int(shortest[exponent_index + 1:])
} else {
0
}
let point = coefficient.find(".").unwrap_or(coefficient.length())
let raw_digits = coefficient.replace_all(old=".", new="")
let mut start = 0
while start < raw_digits.length() - 1 && raw_digits[start] == 48 {
start += 1
}
let mut end = raw_digits.length()
while end > start + 1 && raw_digits[end - 1] == 48 {
end -= 1
}
let digits = raw_digits[start:end].to_owned()
let exponent = explicit_exponent + point - start - 1
let scientific = exponent < -4 || exponent >= 16
let mantissa = decimal_point(
digits,
if scientific {
1
} else {
exponent + 1
},
)
let mantissa = if !scientific && !mantissa.contains(".") {
mantissa + ".0"
} else {
mantissa
}
numeric_sign(value) +
mantissa +
(if scientific { decimal_exponent_suffix(exponent) } else { "" })
}
///|
/// Represent a Double with the existing Python shortest-number profile.
pub fn python_float_repr(value : Double) -> String {
named_format_float_repr(value)
}
///|
/// Add the alternate decimal point before a possible scientific exponent.
fn named_format_decimal_point(text : String) -> String {
let index = text.find("e").unwrap_or(text.length())
if text[:index].contains(".") {
text
} else {
text[:index].to_owned() + "." + text[index:].to_owned()
}
}
///|
fn named_format_within_budget(
text : String,
available : Int64,
) -> String raise @core.DataError {
if Int64::from_int(text.length()) > available {
return named_format_error(
"ResourceError", "formatted text exceeds String addressability",
)
}
text
}
///|
/// Exact binary64 rounding, without the legacy convenience precision limit.
/// This layer supplies ASCII digits/sign only; each caller owns its padding,
/// grouping, explicit sign and rounded-negative-zero policies.
fn named_format_float_digits(
input : Double,
code : Char,
precision : Int32?,
alternate : Bool,
available~ : Int64 = 2147483647L,
) -> String raise @core.DataError {
let percent = code == '%'
let value = if percent { input * 100.0 } else { input }
let uppercase = code == 'E' || code == 'F' || code == 'G'
let suffix = if percent { "%" } else { "" }
if numeric_special(value) is Some(text) {
return named_format_within_budget(
(if uppercase { text.to_upper() } else { text }) + suffix,
available,
)
}
let empty_type = code == '\u0000'
if empty_type && precision is None {
let text = named_format_float_repr(value)
return named_format_within_budget(
if alternate {
named_format_decimal_point(text)
} else {
text
},
available,
)
}
let places = precision.unwrap_or(6)
if places < 0 {
return named_format_error("ValueError", "negative format precision")
}
// At these precisions every finite binary64 is already exact, so its decimal
// exponent cannot change by rounding. Preflight before allocating precision
// padding, including any output budget reserved by the enclosing formatter.
// Do not reject a huge non-alternate g:
// it can have a short result after removing the exact decimal's trailing zeros.
if places >= 1075 &&
(
alternate ||
code == 'f' ||
code == 'F' ||
code == 'e' ||
code == 'E' ||
percent
) {
let exponent = decimal_exponent(value)
let sign_length = Int64::from_int(numeric_sign(value).length())
let required = if code == 'f' || code == 'F' || percent {
sign_length +
Int64::from_int(Int32::max(1, exponent + 1)) +
1L +
Int64::from_int(places) +
(if percent { 1L } else { 0L })
} else if code == 'e' || code == 'E' || exponent < -4 {
sign_length +
Int64::from_int(places) +
(if code == 'e' || code == 'E' { 2L } else { 1L }) +
Int64::from_int(decimal_exponent_suffix(exponent).length())
} else {
sign_length +
Int64::from_int(places) +
1L +
Int64::from_int(Int32::max(0, -exponent))
}
if required > available {
return named_format_error(
"ResourceError", "formatted text exceeds String addressability",
)
}
}
let body = if code == 'f' || code == 'F' || percent {
let digits = decimal_round(value, places)
.to_string()
.pad_start(places + 1, '0')
let text = decimal_point(digits, digits.length() - places)
if alternate {
named_format_decimal_point(text)
} else {
text
}
} else if code == 'e' || code == 'E' {
let (digits, exponent) = decimal_significant(value, places + 1)
let text = decimal_point(digits, 1)
(if alternate { named_format_decimal_point(text) } else { text }) +
decimal_exponent_suffix(exponent)
} else if empty_type || code == 'g' || code == 'G' || code == 'n' {
let count = Int32::max(1, places)
// A binary64 is an integer divided by at most 2^1074. Its terminating
// decimal has fewer than 1075 significant digits. Beyond that point the
// extra digits are all zeros, which this non-alternate branch removes.
let work_count = if alternate { count } else { Int32::min(count, 1075) }
let (digits, exponent) = decimal_significant(value, work_count)
// Python's omitted presentation reserves one significant place for '.0'.
let scientific = exponent < -4 ||
exponent >= count - (if empty_type { 1 } else { 0 })
let text = decimal_point(digits, if scientific { 1 } else { exponent + 1 })
let text = if alternate {
named_format_decimal_point(text)
} else {
trim_decimal(text)
}
let text = if empty_type && !scientific && !text.contains(".") {
text + ".0"
} else {
text
}
text + (if scientific { decimal_exponent_suffix(exponent) } else { "" })
} else {
return named_format_error("ValueError", "unsupported floating presentation")
}
let text = numeric_sign(value) + body + suffix
named_format_within_budget(
if uppercase {
text.to_upper()
} else {
text
},
available,
)
}