// Copyright 2026 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
/// Maximum shift that we can do in one pass without overflow.
/// We have to be able to accommodate 9 << max_shift.
let max_shift = 59
///|
/// Decimal power of ten to binary power of two.
/// The Ith entry (starting at I = 0) is the largest power of 2 less than (10 ** I)
let powtab : ReadOnlyArray[Int] = [
1, 3, 6, 9, 13, 16, 19, 23, 26, 29, 33, 36, 39, 43, 46, 49, 53, 56, 59,
]
///|
fn Decimal::new_priv() -> Decimal {
{
digits: FixedArray::make(800, b'\x00'),
digits_num: 0,
decimal_point: 0,
negative: false,
truncated: false,
overflowed: 0,
}
}
///|
fn Decimal::from_int64_priv(v : Int64) -> Decimal {
let d = Decimal::new_priv()
d.assign(v)
d
}
///|
fn parse_decimal_priv(str : StringView) -> Decimal raise StrConvError {
parse_decimal_from_view(str)
}
///|
fn parse_decimal_from_view(str : StringView) -> Decimal raise StrConvError {
let d = Decimal::new_priv()
let mut has_dp = false
let mut has_digits = false
// read sign
let rest = match str {
['-', .. rest] => {
d.negative = true
rest
}
['+', .. rest] => rest
_ => str
}
// read digits
let rest = for s = rest {
match s {
['_', .. rest] => continue rest
['.', .. rest] => {
guard !has_dp else { syntax_err() }
has_dp = true
d.decimal_point = d.digits_num
continue rest
}
['0'..='9' as digit, .. rest] => {
has_digits = true
if digit == '0' && d.digits_num == 0 {
// ignore leading zeros
d.decimal_point -= 1
continue rest
}
if d.digits_num < d.digits.length() {
d.digits[d.digits_num] = (digit.to_int() - '0').to_byte()
d.digits_num += 1
} else {
if !has_dp {
d.overflowed += 1
}
if digit != '0' {
d.truncated = true
}
}
continue rest
}
rest => break rest
}
}
guard has_digits else { syntax_err() }
if !has_dp {
d.decimal_point = d.digits_num
}
// read exponent part
let rest = match rest {
['e' | 'E', .. rest] => {
let mut exp_sign = 1
let rest = match rest {
['+', .. rest] => rest
['-', .. rest] => {
exp_sign = -1
rest
}
rest => rest
}
guard rest is ['0'..='9', ..] else { syntax_err() }
// Clamp the exponent accumulator so huge exponents cannot overflow `Int`.
// The clamp lands `decimal_point` exactly at the boundary that
// `to_double_priv` already treats as overflow / underflow:
// - `decimal_point > 310` -> range_err (Double max ~1.8e+308, so
// anything past 1e+310 is unreachable). Hence the `+311` ceiling.
// - `decimal_point < -330` -> underflow to 0 (Double min subnormal
// ~5e-324, so anything past 1e-331 underflows). Hence the `-331`
// floor, expressed as `decimal_point + 331` for the negative-exp arm.
// Once `exp` reaches `exp_limit`, additional digits cannot change the
// result, so we stop accumulating before `Int` can wrap.
let effective_dp = d.decimal_point + d.overflowed
let exp_limit = if exp_sign > 0 {
if effective_dp < 311 {
311 - effective_dp
} else {
0
}
} else if effective_dp > -331 {
effective_dp + 331
} else {
0
}
let mut exp = 0
let rest = for s = rest {
match s {
['_', .. rest] => continue rest
['0'..='9' as digit, .. rest] => {
if exp < exp_limit {
exp = exp * 10 + (digit.to_int() - '0')
if exp > exp_limit {
exp = exp_limit
}
}
continue rest
}
rest => break rest
}
}
d.decimal_point += exp_sign * exp
rest
}
rest => rest
}
// finish
guard rest is [] else { syntax_err() }
d.trim()
d
}
///|
fn Decimal::to_double_priv(self : Decimal) -> Double raise StrConvError {
let mut exponent = 0
let mut mantissa = 0L
// check the underflow and overflow
// Double: 1.79769e+308 (10^308) - 2.22507e-308 (10^-308)
let effective_dp = self.decimal_point + self.overflowed
if self.digits_num == 0 || effective_dp < -330 {
// zero
mantissa = 0
exponent = double_info.bias
let bits = assemble_bits(mantissa, exponent, self.negative)
return bits.reinterpret_as_double()
}
if self.decimal_point > 310 {
// overflow
range_err()
}
// Incorporate overflowed digits into decimal_point for correct shift normalization.
// The overflowed digits are part of the coefficient's magnitude even though they
// were discarded from the internal buffer. This must be done before the shift
// loops so that the decimal point position reflects the true value.
self.decimal_point += self.overflowed
// scale by powers of 2 until in range [0.5 .. 1]
// right shift
while self.decimal_point > 0 {
let mut n = 0
if self.decimal_point >= powtab.length() {
n = 60
} else {
n = powtab[self.decimal_point]
}
self.shift_priv(-n)
exponent += n
}
// left shift
while self.decimal_point < 0 ||
(self.decimal_point == 0 && self.digits[0].to_int() < 5) {
let mut n = 0
if -self.decimal_point >= powtab.length() {
n = 60
} else {
n = powtab[-self.decimal_point]
}
self.shift_priv(n)
exponent -= n
}
// normalized floating point range is [1, 2), current [0.5, 1)
// should decrease the exponent by 1
exponent -= 1
// minimum representable exponent is bias + 1
// if the exponent is smaller, move it up and shift decimal accordingly
if exponent < double_info.bias + 1 {
let n = double_info.bias + 1 - exponent
self.shift_priv(-n)
exponent += n
}
if exponent - double_info.bias >= (1 << double_info.exponent_bits) - 1 {
// overflow
range_err()
}
// multiply by (2 ** precision) and round to get mantissa
// extract mantissa_bits + 1 bits
self.shift_priv(double_info.mantissa_bits + 1)
mantissa = self.rounded_integer()
// rounding might have added a bit, shift down.
if mantissa == 2L << double_info.mantissa_bits {
mantissa = mantissa >> 1
exponent += 1
if exponent - double_info.bias >= (1 << double_info.exponent_bits) - 1 {
// overflow
range_err()
}
}
// denormalized
if (mantissa & (1L << double_info.mantissa_bits)) == 0L {
exponent = double_info.bias
}
// combining the 52 mantissa bits with the 11 exponent bits and 1 sign bit
let bits = assemble_bits(mantissa, exponent, self.negative)
bits.reinterpret_as_double()
}
///|
fn Decimal::shift_priv(self : Decimal, s : Int) -> Unit {
if self.digits_num == 0 {
return
}
let mut s = s
if s > 0 {
while s > max_shift {
self.left_shift(max_shift)
s -= max_shift
}
self.left_shift(s)
}
if s < 0 {
while s < -max_shift {
self.right_shift(max_shift)
s += max_shift
}
self.right_shift(-s)
}
}
///|
fn assemble_bits(mantissa : Int64, exponent : Int, negative : Bool) -> Int64 {
let biased_exp = exponent - double_info.bias
// set the mantissa bits
let mut bits = mantissa & ((1L << double_info.mantissa_bits) - 1L)
// set the exponent bits
let exp_bits = (biased_exp & ((1 << double_info.exponent_bits) - 1)).to_int64()
bits = bits | (exp_bits << double_info.mantissa_bits)
// set the sign bit
if negative {
bits = bits | (1L << double_info.mantissa_bits << double_info.exponent_bits)
}
bits
}
///|
/// Extract a rounded 64bit integer
fn Decimal::rounded_integer(self : Decimal) -> Int64 {
if self.decimal_point > 20 {
return 0xFFFFFFFFFFFFFFFFL
}
let (n, i) = for n = 0L, i = 0; i < self.decimal_point && i < self.digits_num; {
continue n * 10L + self.digits[i].to_int64(), i + 1
} nobreak {
(n, i)
}
let n = for n = n, i = i; i < self.decimal_point; {
continue n * 10L, i + 1
} nobreak {
n
}
if self.should_round_up(self.decimal_point) {
n + 1L
} else {
n
}
}
///|
/// Check if truncate at d digits should round up.
/// Typically, when rounding a decimal fraction to an integer, 7.3 rounds down to 7 and 7.6 rounds up to 8.
/// Rounding numbers like 7.5, half-way between two integers, will round to even.
fn Decimal::should_round_up(self : Decimal, d : Int) -> Bool {
if d < 0 || d >= self.digits_num {
return false
}
if self.digits[d].to_int() == 5 && d + 1 == self.digits_num {
// half-way between two integers
// if truncated, the real value is higher than stored value, round up.
if self.truncated {
return true
}
// round to even
return d > 0 && self.digits[d - 1].to_int() % 2 != 0
}
// normal case
self.digits[d].to_int() >= 5
}
///|
/// Assign a Int64 value to decimal.
fn Decimal::assign(self : Decimal, v : Int64) -> Unit {
let buf = FixedArray::make(24, b'\x00')
// write value to buf
let n = for n = 0, v = v; v > 0; {
let v1 = v / 10
buf[n] = (v - v1 * 10).to_byte()
continue n + 1, v1
} nobreak {
n
}
// reverse the buf
self.digits_num = 0
for i in n>..0 {
self.digits[self.digits_num] = buf[i]
self.digits_num += 1
}
self.decimal_point = self.digits_num
self.trim()
}
///|
/// Binary shift right by s bits.
fn Decimal::right_shift(self : Decimal, s : Int) -> Unit {
let mut read_index = 0
let mut write_index = 0
// read enough leading digits to start a shift
let mut acc = 0UL
while acc >> s == 0 {
if read_index >= self.digits_num {
while acc >> s == 0 {
acc *= 10
read_index += 1
}
break
}
let d = self.digits[read_index]
acc = acc * 10 + d.to_int64().reinterpret_as_uint64()
read_index += 1
}
self.decimal_point -= read_index - 1
// read a digit and output a shifted digit
let mask = (1UL << s) - 1
while read_index < self.digits_num {
// output (acc >> s)
let out = acc >> s
self.digits[write_index] = out.to_byte()
write_index += 1
// contract
acc = acc & mask
// expand
let d = self.digits[read_index]
acc = acc * 10 + d.to_int64().reinterpret_as_uint64()
read_index += 1
}
// output extra digits
while acc > 0 {
let out = acc >> s
if write_index < self.digits.length() {
self.digits[write_index] = out.to_byte()
write_index += 1
} else if out > 0 {
self.truncated = true
}
acc = acc & mask
acc *= 10
}
// update and trim
self.digits_num = write_index
self.trim()
}
///|
/// Cheat sheet for left shift: table indexed by shift count giving
/// number of new digits that will be introduced by that shift.
/// left_shift_cheats[s] = (new digits num, (5 ** s))
let left_shift_cheats : ReadOnlyArray[(Int, String)] = [
(0, ""),
(1, "5"), // * 2
(1, "25"), // * 4
(1, "125"), // * 8
(2, "625"), // * 16
(2, "3125"), // * 32
(2, "15625"), // * 64
(3, "78125"), // * 128
(3, "390625"), // * 256
(3, "1953125"), // * 512
(4, "9765625"), // * 1024
(4, "48828125"), // * 2048
(4, "244140625"), // * 4096
(4, "1220703125"), // * 8192
(5, "6103515625"), // * 16384
(5, "30517578125"), // * 32768
(5, "152587890625"), // * 65536
(6, "762939453125"), // * 131072
(6, "3814697265625"), // * 262144
(6, "19073486328125"), // * 524288
(7, "95367431640625"), // * 1048576
(7, "476837158203125"), // * 2097152
(7, "2384185791015625"), // * 4194304
(7, "11920928955078125"), // * 8388608
(8, "59604644775390625"), // * 16777216
(8, "298023223876953125"), // * 33554432
(8, "1490116119384765625"), // * 67108864
(9, "7450580596923828125"), // * 134217728
(9, "37252902984619140625"), // * 268435456
(9, "186264514923095703125"), // * 536870912
(10, "931322574615478515625"), // * 1073741824
(10, "4656612873077392578125"), // * 2147483648
(10, "23283064365386962890625"), // * 4294967296
(10, "116415321826934814453125"), // * 8589934592
(11, "582076609134674072265625"), // * 17179869184
(11, "2910383045673370361328125"), // * 34359738368
(11, "14551915228366851806640625"), // * 68719476736
(12, "72759576141834259033203125"), // * 137438953472
(12, "363797880709171295166015625"), // * 274877906944
(12, "1818989403545856475830078125"), // * 549755813888
(13, "9094947017729282379150390625"), // * 1099511627776
(13, "45474735088646411895751953125"), // * 2199023255552
(13, "227373675443232059478759765625"), // * 4398046511104
(13, "1136868377216160297393798828125"), // * 8796093022208
(14, "5684341886080801486968994140625"), // * 17592186044416
(14, "28421709430404007434844970703125"), // * 35184372088832
(14, "142108547152020037174224853515625"), // * 70368744177664
(15, "710542735760100185871124267578125"), // * 140737488355328
(15, "3552713678800500929355621337890625"), // * 281474976710656
(15, "17763568394002504646778106689453125"), // * 562949953421312
(16, "88817841970012523233890533447265625"), // * 1125899906842624
(16, "444089209850062616169452667236328125"), // * 2251799813685248
(16, "2220446049250313080847263336181640625"), // * 4503599627370496
(16, "11102230246251565404236316680908203125"), // * 9007199254740992
(17, "55511151231257827021181583404541015625"), // * 18014398509481984
(17, "277555756156289135105907917022705078125"), // * 36028797018963968
(17, "1387778780781445675529539585113525390625"), // * 72057594037927936
(18, "6938893903907228377647697925567626953125"), // * 144115188075855872
(18, "34694469519536141888238489627838134765625"), // * 288230376151711744
(18, "173472347597680709441192448139190673828125"), // * 576460752303423488
(19, "867361737988403547205962240695953369140625"), // * 1152921504606846976
]
///|
/// Lookup the cheat sheet to find the new digits num.
fn Decimal::new_digits(self : Decimal, s : Int) -> Int {
let new_digits = left_shift_cheats[s].0
let cheat_num = left_shift_cheats[s].1
// check if the leading digits lexicographically less than cheats num.
let less = for i, code_unit in cheat_num.code_units() {
if i >= self.digits_num {
break true
}
let d = code_unit.to_int() - '0'
if self.digits[i].to_int() != d {
break self.digits[i].to_int() < d
}
} nobreak {
false
}
if less {
new_digits - 1
} else {
new_digits
}
}
///|
/// Binary shift left by s bits.
fn Decimal::left_shift(self : Decimal, s : Int) -> Unit {
let new_digits = self.new_digits(s)
// from right to left
let mut read_index = self.digits_num
let mut write_index = self.digits_num + new_digits
// read a digit and output a shifted digit
let mut acc = 0L
read_index -= 1
while read_index >= 0 {
let d = self.digits[read_index].to_int64()
acc += d << s
let quo = acc / 10L
let rem = (acc - quo * 10L).to_int()
write_index -= 1
if write_index < self.digits.length() {
self.digits[write_index] = rem.to_byte()
} else if rem != 0 {
self.truncated = true
}
acc = quo
read_index -= 1
}
// output extra digits
while acc > 0L {
let quo = acc / 10L
let rem = (acc - 10L * quo).to_int()
write_index -= 1
if write_index < self.digits.length() {
self.digits[write_index] = rem.to_byte()
} else if rem != 0 {
self.truncated = true
}
acc = quo
}
// update and trim
self.digits_num += new_digits
if self.digits_num > self.digits.length() {
self.digits_num = self.digits.length()
}
self.decimal_point += new_digits
self.trim()
}
///|
/// Trim trailing zeros.
fn Decimal::trim(self : Decimal) -> Unit {
while self.digits_num > 0 && self.digits[self.digits_num - 1] == 0 {
self.digits_num -= 1
}
if self.digits_num == 0 {
self.decimal_point = 0
}
}
///|
pub impl Show for Decimal with fn output(self, logger) {
if self.digits_num == 0 {
logger.write_char('0')
return
}
if self.decimal_point <= 0 {
// zeros filling between the decimal point and the digits
logger.write_string("0.")
for _ in 0..<-self.decimal_point {
logger.write_char('0')
}
for i in 0.. 20" {
// This test should trigger the uncovered line 252 in rounded_integer
// We need to create a decimal that will have decimal_point > 20 after shifting
// but not trigger the early overflow check in to_double_priv
// Create a decimal manually to bypass the early overflow checks
let decimal = Decimal::new_priv()
decimal.negative = false
decimal.decimal_point = 25 // This is > 20 but < 310
decimal.digits_num = 1
decimal.digits[0] = (1 : Int).to_byte()
decimal.truncated = false
// Call rounded_integer directly to trigger the uncovered line
let result = decimal.rounded_integer()
inspect(result, content="-1") // Should be Int64::max_value
}
///|
test "corner cases" {
inspect(try? parse_decimal_priv(".123"), content="Ok(0.123)")
inspect(try? parse_decimal_priv("."), content="Err(invalid syntax)")
inspect(try? parse_decimal_priv("-"), content="Err(invalid syntax)")
}
///|
test "parse_double mantissa normalization boundary" {
inspect(parse_double("1.9999999999999999"), content="2")
inspect(parse_double("9007199254740991.5"), content="9007199254740992")
}
///|
test "parse_double large magnitude cancellation" {
let input = "1" + String::make(800, '0') + "e-800"
inspect(parse_double(input), content="1")
let input = "-1" + String::make(800, '0') + "e-800"
inspect(parse_double(input), content="-1")
let input = "1." + String::make(800, '0')
inspect(parse_double(input), content="1")
let input = "1" + String::make(1999, '0') + "e-1999"
inspect(parse_double(input), content="1")
let input = "1" + String::make(399, '0') + ".5"
try parse_double(input) catch {
e => inspect(e, content="value out of range")
} noraise {
_ => fail("expected parse_double to raise")
}
let input = "1" + String::make(800, '0') + ".5"
try parse_double(input) catch {
e => inspect(e, content="value out of range")
} noraise {
_ => fail("expected parse_double to raise")
}
let input = "1" + String::make(1499, '0') + "e-1499"
inspect(parse_double(input), content="1")
let input = "0." + String::make(1999, '0') + "1"
inspect(parse_double(input), content="0")
}