///|
/// Canonical arbitrary-precision decimal used only for ordering. `digits` has
/// no leading zero, `scale` counts fractional digits, and zero is never negative.
pub(all) struct DecimalKeyValue {
negative : Bool
digits : String
scale : Int
} derive(Debug, Eq)
///|
/// Parse a plain decimal without converting through floating point. Exponents,
/// NaN and infinity are deliberately rejected to keep the accepted language
/// reproducible across MoonBit backends.
pub fn parse_decimal_key(text : String) -> DecimalKeyValue raise SortError {
if text == "" {
raise InvalidKey("decimal key must not be empty")
}
let chars = text.to_array()
let mut offset = 0
let mut negative = false
if chars[0] == '-' || chars[0] == '+' {
negative = chars[0] == '-'
offset = 1
}
if offset == chars.length() {
raise InvalidKey("decimal key must contain a digit")
}
let digits : Array[Char] = []
let mut seen_point = false
let mut fractional_digits = 0
let mut digit_count = 0
for index = offset; index < chars.length(); index = index + 1 {
let char = chars[index]
if char >= '0' && char <= '9' {
digits.push(char)
digit_count += 1
if seen_point {
fractional_digits += 1
}
} else if char == '.' && !seen_point {
seen_point = true
} else if char == '.' {
raise InvalidKey(
"decimal key contains more than one decimal point: " + text,
)
} else {
raise InvalidKey(
"invalid decimal character at offset " + index.to_string(),
)
}
}
if digit_count == 0 {
raise InvalidKey("decimal key must contain a digit")
}
let mut first = 0
while first < digits.length() && digits[first] == '0' {
first += 1
}
if first == digits.length() {
return { negative: false, digits: "0", scale: 0, }
}
let mut end = digits.length()
let mut scale = fractional_digits
while scale > 0 && end > first && digits[end - 1] == '0' {
end -= 1
scale -= 1
}
let output = StringBuilder()
for index = first; index < end; index = index + 1 {
output.write_char(digits[index])
}
{ negative, digits: output.to_string(), scale, }
}
///|
/// Compare exact decimal values without allocation proportional to their
/// aligned scale and without integer overflow.
pub fn compare_decimal_keys(
left : DecimalKeyValue,
right : DecimalKeyValue,
) -> Int {
if left.digits == "0" && right.digits == "0" {
return 0
}
if left.negative != right.negative {
return if left.negative { -1 } else { 1 }
}
let magnitude = compare_decimal_magnitude(left, right)
if left.negative {
-magnitude
} else {
magnitude
}
}
///|
fn compare_decimal_magnitude(
left : DecimalKeyValue,
right : DecimalKeyValue,
) -> Int {
let left_exponent = left.digits.length() - left.scale
let right_exponent = right.digits.length() - right.scale
if left_exponent < right_exponent {
return -1
}
if left_exponent > right_exponent {
return 1
}
let left_chars = left.digits.to_array()
let right_chars = right.digits.to_array()
let width = if left_chars.length() > right_chars.length() {
left_chars.length()
} else {
right_chars.length()
}
for index = 0; index < width; index = index + 1 {
let left_digit = if index < left_chars.length() {
left_chars[index]
} else {
'0'
}
let right_digit = if index < right_chars.length() {
right_chars[index]
} else {
'0'
}
if left_digit < right_digit {
return -1
}
if left_digit > right_digit {
return 1
}
}
0
}
///|
/// Render a unique plain-decimal representation suitable for Run files.
pub fn DecimalKeyValue::to_canonical_string(self : DecimalKeyValue) -> String {
if self.digits == "0" {
return "0"
}
let output = StringBuilder()
if self.negative {
output.write_char('-')
}
let chars = self.digits.to_array()
let integer_digits = chars.length() - self.scale
if self.scale == 0 {
output.write_string(self.digits)
} else if integer_digits > 0 {
for index = 0; index < chars.length(); index = index + 1 {
if index == integer_digits {
output.write_char('.')
}
output.write_char(chars[index])
}
} else {
output.write_string("0.")
for index = integer_digits; index < 0; index = index + 1 {
output.write_char('0')
}
output.write_string(self.digits)
}
output.to_string()
}