///|
/// Exact signed decimal with at most 34 significant digits (DMN number type).
/// Division and out-of-range arithmetic are rounded half-to-even, the same
/// rounding mode required by IEEE 754 decimal128.
pub struct Decimal {
coefficient : @bigint.BigInt
scale : Int32
}
///|
pub impl Eq for Decimal with fn equal(self : Decimal, other : Decimal) -> Bool {
self.compare(other) == 0
}
///|
/// Matchable engine failure carrying code, line, column and message.
pub(all) suberror EngineError {
Failure(String, Int32, Int32, String)
} derive(Eq)
///|
/// Construct a semantic failure without a source location.
pub fn fail(code : String, message : String) -> EngineError {
Failure(code, 1, 1, message)
}
///|
/// Construct a failure with a source line and column.
pub fn fail_at(
code : String,
line : Int32,
column : Int32,
message : String,
) -> EngineError {
Failure(code, line, column, message)
}
///|
/// Extract the machine-readable error code.
pub fn error_code(e : EngineError) -> String {
match e {
Failure(code, _, _, _) => code
}
}
///|
/// Extract the diagnostic message.
pub fn error_message(e : EngineError) -> String {
match e {
Failure(_, _, _, message) => message
}
}
///|
pub let max_digits : Int32 = 34
///|
fn pow10(n : Int32) -> @bigint.BigInt {
@bigint.BigInt::from_int(10).pow(@bigint.BigInt::from_int(n))
}
///|
fn digit_count(c : @bigint.BigInt) -> Int32 {
let s = if c < 0N { (-c).to_string() } else { c.to_string() }
s.length()
}
///|
/// Divide and round half-to-even to an integer.
fn div_half_even(n : @bigint.BigInt, d : @bigint.BigInt) -> @bigint.BigInt {
if d.is_zero() {
return 0N
}
let negative = (n < 0N) != (d < 0N)
let a = if n < 0N { -n } else { n }
let b = if d < 0N { -d } else { d }
let q = a / b
let r = a % b
let twice = r * 2N
let rounded = if twice > b {
q + 1N
} else if twice < b {
q
} else if (q % 2N).is_zero() {
q
} else {
q + 1N
}
if negative {
-rounded
} else {
rounded
}
}
///|
/// Remove insignificant trailing zeros, then round to 34 significant digits.
fn normalize(c : @bigint.BigInt, scale : Int32) -> Decimal raise EngineError {
if c.is_zero() {
return zero()
}
let mut c = c
let mut s = scale
let drop = digit_count(c) - max_digits
// Round once at the coarser of the precision and subnormal quantum.
let drop = if s - 6176 > drop { s - 6176 } else { drop }
if drop > 0 {
c = if drop > digit_count(c) { 0N } else { div_half_even(c, pow10(drop)) }
s = s - drop
}
if c.is_zero() {
return zero()
}
if digit_count(c) > max_digits {
c = c / 10N
s = s - 1
}
if s < -6111 {
let pad = -6111 - s
if digit_count(c) + pad > max_digits {
raise fail("E_DECIMAL_RANGE", "decimal128 overflow")
}
c = c * pow10(pad)
s = -6111
}
while s > -6111 && (c % 10N).is_zero() {
c = c / 10N
s = s - 1
}
{ coefficient: c, scale: s, }
}
///|
/// Parse a decimal literal. Accepts an optional sign, a leading dot, and a
/// decimal exponent. Values with more than 34 significant digits are rounded.
pub fn decimal(text : String) -> Decimal raise EngineError {
let chars = text.to_array()
if chars.is_empty() || chars.length() > 10000 {
raise fail("E_DECIMAL_SYNTAX", "invalid decimal: " + text)
}
let digits = StringBuilder()
let mut scale = 0
let mut exp = 0
let mut exp_sign = 1
let mut i = 0
let mut dot = false
let mut exponent = false
let mut exponent_digits = 0
let mut count = 0
if chars[0] == '-' || chars[0] == '+' {
if chars[0] == '-' {
digits.write_char('-')
}
i = 1
}
while i < chars.length() {
let ch = chars[i]
if ch >= '0' && ch <= '9' {
if exponent {
exponent_digits = exponent_digits + 1
if exp > 100000 {
raise fail("E_DECIMAL_RANGE", "decimal exponent too large")
}
exp = exp * 10 + (ch.to_int() - 48)
if exp > 1000000 {
raise fail("E_DECIMAL_RANGE", "decimal exponent too large")
}
} else {
digits.write_char(ch)
count = count + 1
if dot {
scale = scale + 1
}
}
} else if ch == '.' && !dot && !exponent {
dot = true
} else if (ch == 'e' || ch == 'E') && !exponent && count > 0 {
exponent = true
if i + 1 < chars.length() && (chars[i + 1] == '-' || chars[i + 1] == '+') {
if chars[i + 1] == '-' {
exp_sign = -1
}
i = i + 1
}
} else {
raise fail("E_DECIMAL_SYNTAX", "invalid decimal: " + text)
}
i = i + 1
}
if count == 0 || (exponent && exponent_digits == 0) {
raise fail("E_DECIMAL_SYNTAX", "invalid decimal: " + text)
}
normalize(
@bigint.BigInt::from_string(digits.to_string()),
scale - exp_sign * exp,
)
}
///|
/// Construct an exact decimal from a 32-bit integer.
pub fn decimal_int(n : Int32) -> Decimal {
{ coefficient: @bigint.BigInt::from_int(n), scale: 0, }
}
///|
/// Construct an exact decimal from a 64-bit integer.
pub fn decimal_int64(n : Int64) -> Decimal {
{ coefficient: @bigint.BigInt::from_int64(n), scale: 0, }
}
///|
/// Construct exact decimal zero.
pub fn zero() -> Decimal {
{ coefficient: 0N, scale: 0, }
}
///|
/// Test whether the decimal coefficient is zero.
pub fn Decimal::is_zero(self : Decimal) -> Bool {
self.coefficient.is_zero()
}
///|
/// Test the decimal sign.
pub fn Decimal::is_negative(self : Decimal) -> Bool {
self.coefficient < 0N
}
///|
/// Test whether the normalized decimal has no fractional part.
pub fn Decimal::is_integer(self : Decimal) -> Bool {
self.scale <= 0
}
///|
/// Format the normalized decimal without binary floating-point conversion.
pub fn Decimal::text(self : Decimal) -> String {
if self.coefficient.is_zero() {
return "0"
}
let negative = self.coefficient < 0N
let mut raw = (if negative { -self.coefficient } else { self.coefficient }).to_string()
while raw.length() <= self.scale {
raw = "0" + raw
}
let out = StringBuilder()
if negative {
out.write_string("-")
}
for i, ch in raw.to_array() {
if self.scale > 0 && i == raw.length() - self.scale {
out.write_string(".")
}
out.write_char(ch)
}
if self.scale < 0 {
for _ in 0..<-self.scale {
out.write_string("0")
}
}
out.to_string()
}
///|
/// Add decimals with the engine precision limit.
pub fn Decimal::add(a : Decimal, b : Decimal) -> Decimal raise EngineError {
let scale = if a.scale > b.scale { a.scale } else { b.scale }
normalize(
a.coefficient * pow10(scale - a.scale) +
b.coefficient * pow10(scale - b.scale),
scale,
)
}
///|
/// Reverse the decimal sign.
pub fn Decimal::negate(a : Decimal) -> Decimal {
{ coefficient: -a.coefficient, scale: a.scale, }
}
///|
/// Return the non-negative decimal magnitude.
pub fn Decimal::abs(a : Decimal) -> Decimal {
if a.coefficient < 0N {
a.negate()
} else {
a
}
}
///|
/// Multiply decimals with the engine precision limit.
pub fn Decimal::multiply(a : Decimal, b : Decimal) -> Decimal raise EngineError {
normalize(a.coefficient * b.coefficient, a.scale + b.scale)
}
///|
/// Return a negative, zero or positive integer according to numeric order.
pub fn Decimal::compare(a : Decimal, b : Decimal) -> Int32 {
let scale = if a.scale > b.scale { a.scale } else { b.scale }
(a.coefficient * pow10(scale - a.scale)).compare(
b.coefficient * pow10(scale - b.scale),
)
}
///|
/// Divide decimals; zero divisors raise E_DIV_ZERO.
pub fn Decimal::divide(a : Decimal, b : Decimal) -> Decimal raise EngineError {
if b.coefficient.is_zero() {
raise fail("E_DIV_ZERO", "division by zero")
}
if a.is_zero() {
return zero()
}
let x = if a.coefficient < 0N { -a.coefficient } else { a.coefficient }
let y = if b.coefficient < 0N { -b.coefficient } else { b.coefficient }
let estimate = digit_count(x) - digit_count(y)
let below = if estimate >= 0 {
x < y * pow10(estimate)
} else {
x * pow10(-estimate) < y
}
let adjusted = estimate - (if below { 1 } else { 0 }) + b.scale - a.scale
let scale = 33 - adjusted
let scale = if scale > 6176 { 6176 } else { scale }
let shift = scale + b.scale - a.scale
let numerator = if shift >= 0 {
a.coefficient * pow10(shift)
} else {
a.coefficient
}
let denominator = if shift < 0 {
b.coefficient * pow10(-shift)
} else {
b.coefficient
}
normalize(div_half_even(numerator, denominator), scale)
}
///|
/// Floored integer division used by modulo.
fn floor_div(a : @bigint.BigInt, b : @bigint.BigInt) -> @bigint.BigInt {
let q = a / b
let r = a % b
if !r.is_zero() && (r < 0N) != (b < 0N) {
q - 1N
} else {
q
}
}
///|
/// Compute FEEL modulo with the divisor sign convention.
pub fn Decimal::modulo(a : Decimal, b : Decimal) -> Decimal raise EngineError {
if b.coefficient.is_zero() {
raise fail("E_DIV_ZERO", "modulo by zero")
}
let scale = if a.scale > b.scale { a.scale } else { b.scale }
let x = a.coefficient * pow10(scale - a.scale)
let y = b.coefficient * pow10(scale - b.scale)
normalize(x - floor_div(x, y) * y, scale)
}
///|
/// Round toward negative infinity.
pub fn Decimal::floor(self : Decimal) -> Decimal raise EngineError {
if self.scale <= 0 {
return self
}
let q = floor_div(self.coefficient, pow10(self.scale))
normalize(q, 0)
}
///|
/// Round toward positive infinity.
pub fn Decimal::ceiling(self : Decimal) -> Decimal raise EngineError {
if self.scale <= 0 {
return self
}
let q = floor_div(self.coefficient, pow10(self.scale))
let r = self.coefficient - q * pow10(self.scale)
normalize(if r.is_zero() { q } else { q + 1N }, 0)
}
///|
/// Discard fractional digits toward zero.
pub fn Decimal::truncate(self : Decimal) -> Decimal raise EngineError {
if self.scale <= 0 {
return self
}
normalize(self.coefficient / pow10(self.scale), 0)
}
///|
/// Convert an integral decimal to Int32; fractional values raise E_TYPE.
pub fn Decimal::to_int(self : Decimal) -> Int32 raise EngineError {
if !self.is_integer() {
raise fail("E_TYPE", "value is not an integer")
}
if self.compare(decimal_int(-2147483648)) < 0 ||
self.compare(decimal_int(2147483647)) > 0 {
raise fail("E_NUMBER", "integer outside Int32 range")
}
(self.coefficient * pow10(-self.scale)).to_int()
}
///|
/// Round to decimal places using half-even ties; negative places are clamped to zero.
pub fn Decimal::round(
self : Decimal,
places : Int32,
) -> Decimal raise EngineError {
let places = if places < 0 { 0 } else { places }
if self.scale <= places {
return self
}
let drop = self.scale - places
normalize(div_half_even(self.coefficient, pow10(drop)), places)
}
///|
fn Decimal::to_double(self : Decimal) -> Double {
let negative = self.coefficient < 0N
let raw = (if negative { -self.coefficient } else { self.coefficient }).to_string()
let mut value = 0.0
for ch in raw.to_array() {
value = value * 10.0 + (ch.to_int() - 48).to_double()
}
let mut divisor = 1.0
for i = 0; i < self.scale; i = i + 1 {
divisor = divisor * 10.0
}
let value = if self.scale < 0 {
value * @math.pow(10.0, (-self.scale).to_double())
} else {
value / divisor
}
if negative {
-value
} else {
value
}
}
///|
/// Build a decimal from a double rounded to `places` decimal places.
pub fn from_double(value : Double, places : Int32) -> Decimal raise EngineError {
if value.is_nan() || value.is_inf() {
raise fail("E_NUMBER", "non-finite number has no FEEL representation")
}
let mut factor = 1.0
for i = 0; i < places; i = i + 1 {
factor = factor * 10.0
}
let scaled = value * factor
let rounded = if scaled >= 0.0 { scaled + 0.5 } else { scaled - 0.5 }
if rounded > 9.2e18 || rounded < -9.2e18 {
raise fail("E_NUMBER", "number outside supported exact range")
}
let n = rounded.to_int64()
if places == 0 {
return decimal_int64(n)
}
let text = n.to_string()
let negative = text.has_prefix("-")
let body = if negative {
text.strip_prefix("-").unwrap().to_owned()
} else {
text
}
let mut padded = body
while padded.length() <= places {
padded = "0" + padded
}
let cut = padded.length() - places
let head = String::from_array(padded.to_array()[0:cut].to_owned())
let tail = String::from_array(padded.to_array()[cut:].to_owned())
decimal((if negative { "-" } else { "" }) + head + "." + tail)
}
///|
/// Raise to a decimal power, using exact integer powers when supported.
pub fn Decimal::pow(a : Decimal, b : Decimal) -> Decimal raise EngineError {
if b.is_integer() &&
b.compare(decimal_int(-1000)) >= 0 &&
b.compare(decimal_int(1000)) <= 0 &&
(if b.coefficient < 0N { -b.coefficient } else { b.coefficient }).compare(
1000N,
) <=
0 {
let n = b.to_int()
let mut result = decimal_int(1)
let mut factor = a
let mut k = if n < 0 { -n } else { n }
while k > 0 {
if k % 2 == 1 {
result = result.multiply(factor)
}
k = k / 2
if k > 0 {
factor = factor.multiply(factor)
}
}
if n < 0 {
return decimal_int(1).divide(result)
}
return result
}
let value = @math.pow(a.to_double(), b.to_double())
from_double(value, 11)
}
///|
/// Compute a square root through the bounded floating-point bridge.
pub fn Decimal::sqrt(self : Decimal) -> Decimal raise EngineError {
if self.coefficient < 0N {
raise fail("E_NUMBER", "square root of a negative number")
}
from_double(self.to_double().sqrt(), 12)
}
///|
/// Compute the exponential through the floating-point bridge.
pub fn Decimal::exp(self : Decimal) -> Decimal raise EngineError {
from_double(@math.exp(self.to_double()), 8)
}
///|
/// Compute the natural logarithm through the floating-point bridge.
pub fn Decimal::ln(self : Decimal) -> Decimal raise EngineError {
if self.coefficient <= 0N {
raise fail("E_NUMBER", "logarithm of a non-positive number")
}
from_double(@math.ln(self.to_double()), 8)
}
///|
/// Compute the base-ten logarithm through the floating-point bridge.
pub fn Decimal::log10(self : Decimal) -> Decimal raise EngineError {
if self.coefficient <= 0N {
raise fail("E_NUMBER", "logarithm of a non-positive number")
}
from_double(@math.log10(self.to_double()), 8)
}
///|
/// Construct a decimal power of ten.
pub fn Decimal::power_of_ten(n : Int32) -> Decimal raise EngineError {
if n < -6176 || n > 6144 {
raise fail("E_DECIMAL_RANGE", "power outside decimal128 range")
}
normalize(1N, -n)
}
///|
pub extend Decimal with Eq::{equal, not_equal}
///|
pub extend EngineError with Eq::{equal, not_equal}