///|
/// The most digits a PartiQL decimal keeps (partiql-tests basic.ion,
/// `decimalRoundUp` and `decimalRoundDown`).
let max_decimal_digits : Int32 = 38
///|
fn pow10(n : Int32) -> @bigint.BigInt {
@bigint.BigInt::from_int(10).pow(@bigint.BigInt::from_int(n))
}
///|
fn is_negative_int(n : @bigint.BigInt) -> Bool {
n.compare_int(0) < 0
}
///|
fn abs_int(n : @bigint.BigInt) -> @bigint.BigInt {
if is_negative_int(n) {
-n
} else {
n
}
}
///|
fn digit_count(n : @bigint.BigInt) -> Int32 {
abs_int(n).to_string().length()
}
///|
/// An exponent computed in Int64 as an Int, or a `DataFailure` when it leaves
/// the Int range.
fn checked_exponent(e : Int64) -> Int32 raise OpFailure {
if e > 2147483647L || e < -2147483648L {
raise DataFailure("decimal exponent out of range")
}
e.to_int()
}
///|
/// Rounds a decimal to 38 significant digits, half to even.
pub fn round_decimal(
d : @ion_core.Decimal,
) -> @ion_core.Decimal raise OpFailure {
let digits = d.coefficient().to_string()
let negative = digits.has_prefix("-")
let magnitude = if negative {
digits.view(start_offset=1).to_owned()
} else {
digits
}
let drop = magnitude.length() - max_decimal_digits
if drop <= 0 {
return d
}
let kept = @bigint.BigInt::from_string(
magnitude.view(end_offset=max_decimal_digits).to_owned(),
)
let rest = magnitude.view(start_offset=max_decimal_digits).to_owned()
let half = "5" + "0".repeat(drop - 1)
let c = rest.lexical_compare(half)
let up = if c < 0 {
false
} else if c > 0 {
true
} else {
kept % @bigint.BigInt::from_int(2) == @bigint.BigInt::from_int(1)
}
let mut rounded = if up { kept + @bigint.BigInt::from_int(1) } else { kept }
let mut exponent = d.exponent().to_int64() + drop.to_int64()
// A carry out of 38 nines gives 10^38 (39 digits): renormalize.
if rounded.to_string().length() > max_decimal_digits {
rounded = rounded / @bigint.BigInt::from_int(10)
exponent += 1L
}
let signed = if negative { -rounded } else { rounded }
@ion_core.Decimal::Decimal(signed, checked_exponent(exponent))
}
///|
/// The widest gap between the exponents of two decimals that `+` and `-`
/// align (a bigger one would need a coefficient of that many digits).
let max_exponent_gap : Int32 = 100000
///|
/// Both coefficients at the smaller exponent, and that exponent.
fn aligned(
a : @ion_core.Decimal,
b : @ion_core.Decimal,
) -> (@bigint.BigInt, @bigint.BigInt, Int32) raise OpFailure {
let gap = a.exponent().to_int64() - b.exponent().to_int64()
if gap.abs() > max_exponent_gap.to_int64() {
raise DataFailure("decimals with exponents too far apart")
}
let e = a.exponent().min(b.exponent())
(
a.coefficient() * pow10(a.exponent() - e),
b.coefficient() * pow10(b.exponent() - e),
e,
)
}
///|
fn decimal_add(
a : @ion_core.Decimal,
b : @ion_core.Decimal,
) -> @ion_core.Decimal raise OpFailure {
let (x, y, e) = aligned(a, b)
round_decimal(@ion_core.Decimal::Decimal(x + y, e))
}
///|
fn decimal_sub(
a : @ion_core.Decimal,
b : @ion_core.Decimal,
) -> @ion_core.Decimal raise OpFailure {
let (x, y, e) = aligned(a, b)
round_decimal(@ion_core.Decimal::Decimal(x - y, e))
}
///|
fn decimal_mul(
a : @ion_core.Decimal,
b : @ion_core.Decimal,
) -> @ion_core.Decimal raise OpFailure {
let e = checked_exponent(a.exponent().to_int64() + b.exponent().to_int64())
round_decimal(
@ion_core.Decimal::Decimal(a.coefficient() * b.coefficient(), e),
)
}
///|
/// a / b to 38 significant digits, half even (partiql-tests nary-operators.ion,
/// `repeatingDecimal`). Trailing zeros are
/// dropped down to the exponent `a.exponent - b.exponent`, so that an exact
/// quotient keeps its natural scale (`6.0 / 3` is `2`, `1.0 / 4` is `0.25`).
fn decimal_div(
a : @ion_core.Decimal,
b : @ion_core.Decimal,
) -> @ion_core.Decimal raise OpFailure {
if b.is_zero() {
raise DataFailure("division by zero")
}
let preferred = a.exponent().to_int64() - b.exponent().to_int64()
let negative = is_negative_int(a.coefficient()) !=
is_negative_int(b.coefficient())
let na = abs_int(a.coefficient())
let nb = abs_int(b.coefficient())
if na.is_zero() {
return @ion_core.Decimal::Decimal(na, checked_exponent(preferred))
}
// Scale the dividend so that the integer quotient has at least 40 digits.
let shift = (max_decimal_digits + 2 + digit_count(nb) - digit_count(na)).max(
0,
)
let scaled = na * pow10(shift)
let q = scaled / nb
let inexact = !(scaled % nb).is_zero()
let mut exponent = preferred - shift.to_int64()
let mut digits = q
let drop = digit_count(q) - max_decimal_digits
if drop > 0 {
let unit = pow10(drop)
let kept = q / unit
let twice = q % unit * @bigint.BigInt::from_int(2)
let c = twice.compare(unit)
let odd = (kept % @bigint.BigInt::from_int(2)).compare_int(0) != 0
let up = c > 0 || (c == 0 && (inexact || odd))
digits = if up { kept + @bigint.BigInt::from_int(1) } else { kept }
exponent += drop.to_int64()
if digit_count(digits) > max_decimal_digits {
digits = digits / @bigint.BigInt::from_int(10)
exponent += 1L
}
}
let ten = @bigint.BigInt::from_int(10)
while exponent < preferred && (digits % ten).is_zero() {
digits = digits / ten
exponent += 1L
}
@ion_core.Decimal::Decimal(
if negative {
-digits
} else {
digits
},
checked_exponent(exponent),
)
}
///|
/// The remainder of a / b, with the sign of the dividend.
fn decimal_rem(
a : @ion_core.Decimal,
b : @ion_core.Decimal,
) -> @ion_core.Decimal raise OpFailure {
if b.is_zero() {
raise DataFailure("division by zero")
}
let (ma, mb, e) = aligned(a, b)
let r = abs_int(ma) % abs_int(mb)
@ion_core.Decimal::Decimal(if is_negative_int(ma) { -r } else { r }, e)
}
///|
/// A decimal from its text, with an optional exponent (`2.25e-1`); in PartiQL
/// a number with an exponent is a decimal (partiql-tests nary-operators.ion,
/// `subtractionOutOfAllowedPrecision`, and extended int.ion).
pub fn parse_decimal(text : String) -> @ion_core.Decimal? {
let parts = text.to_lower().split("e").map(p => p.to_owned()).collect()
if parts.length() == 0 || parts.length() > 2 {
return None
}
let mantissa = try @ion_core.Decimal::parse(parts[0]) catch {
_ => return None
} noraise {
d => d
}
if parts.length() == 1 {
return Some(mantissa)
}
let digits = if parts[1].has_prefix("+") {
parts[1].view(start_offset=1).to_owned()
} else {
parts[1]
}
let exponent = try @string.parse_int(digits) catch {
_ => return None
} noraise {
n => n
}
let total = mantissa.exponent().to_int64() + exponent.to_int64()
if total > 2147483647L || total < -2147483648L {
return None
}
Some(@ion_core.Decimal::Decimal(mantissa.coefficient(), total.to_int()))
}