///|
/// Exact decimal arithmetic for Structured Field Decimals (RFC 9651 §3.3.2).
///
/// A decimal is stored as an exact rational `coefficient × 10^-scale`
/// using an `Int64` coefficient. No floating-point type is used anywhere
/// in the value's representation, so parsing, comparison, and rounding are
/// exact.
///
/// Wire-format restrictions (at most 3 fractional digits, at most 12
/// integer digits) are enforced by the parser and by serialization.
pub(all) struct SfDecimal {
coefficient : Int64
scale : Int
} derive(Debug)
///|
/// Two decimals are equal when they denote the same rational value.
/// `1.2` (12, 1) and `1.20` (120, 2) compare equal even though their
/// stored coefficients differ.
pub impl Eq for SfDecimal with fn equal(self, other) {
self.compare(other) == 0
}
///|
const MAX_INTEGER_PART : Int64 = 999_999_999_999L
///|
const MAX_SCALE : Int = 15
///|
/// Constructs a decimal `coefficient × 10^-scale`, normalizing `-0`.
pub fn SfDecimal::new(coefficient : Int64, scale : Int) -> SfDecimal {
if coefficient == 0L {
return { coefficient: 0L, scale: scale.max(0) }
}
{ coefficient, scale: scale.max(0) }
}
///|
/// Constructs a decimal from raw parts; alias of [`SfDecimal::new`].
pub fn SfDecimal::from_parts(coefficient : Int64, scale : Int) -> SfDecimal {
SfDecimal::new(coefficient, scale)
}
///|
/// The signed coefficient.
pub fn SfDecimal::coefficient(self : SfDecimal) -> Int64 {
self.coefficient
}
///|
/// The exponent (`value = coefficient × 10^-scale`).
pub fn SfDecimal::scale(self : SfDecimal) -> Int {
self.scale
}
///|
/// Strips trailing zeros from the fractional part and normalizes `-0`.
/// Example: `1.200` (1200, 3) becomes `1.2` (12, 1).
pub fn SfDecimal::normalize(self : SfDecimal) -> SfDecimal {
if self.coefficient == 0L {
return { coefficient: 0L, scale: 0 }
}
let mut c = self.coefficient
let mut s = self.scale
while s > 0 && c % 10L == 0L {
c = c / 10L
s = s - 1
}
{ coefficient: c, scale: s }
}
///|
/// Whether the value is negative (nonzero).
pub fn SfDecimal::is_negative(self : SfDecimal) -> Bool {
self.coefficient < 0L
}
///|
/// Exact comparison. Returns a negative value if `self < other`, zero if
/// equal, positive if `self > other`.
pub fn SfDecimal::compare(self : SfDecimal, other : SfDecimal) -> Int {
let a = self.normalize()
let b = other.normalize()
if a.coefficient == 0L && b.coefficient == 0L {
return 0
}
if a.coefficient == 0L {
return if b.coefficient > 0L { -1 } else { 1 }
}
if b.coefficient == 0L {
return if a.coefficient > 0L { 1 } else { -1 }
}
let sga = if a.coefficient < 0L { -1 } else { 1 }
let sgb = if b.coefficient < 0L { -1 } else { 1 }
if sga != sgb {
return if sga < sgb { -1 } else { 1 }
}
let mag = compare_magnitude(a, b)
if sga > 0 {
mag
} else {
-mag
}
}
///|
fn compare_magnitude(a : SfDecimal, b : SfDecimal) -> Int {
// Both normalized and nonzero. Compare |a| against |b| exactly.
let da = decimal_digits(a.coefficient.abs()) + (b.scale - a.scale).max(0)
let db = decimal_digits(b.coefficient.abs()) + (a.scale - b.scale).max(0)
if da != db {
return if da < db { -1 } else { 1 }
}
// Same digit count: scale both up to a common scale; the products are
// then guaranteed to fit because both have the same digit count.
let ca = if b.scale >= a.scale {
match pow10_checked(b.scale - a.scale) {
Some(f) => a.coefficient * f
None => a.coefficient
}
} else {
a.coefficient
}
let cb = if a.scale >= b.scale {
match pow10_checked(a.scale - b.scale) {
Some(f) => b.coefficient * f
None => b.coefficient
}
} else {
b.coefficient
}
// Both values have the same sign here; compare magnitudes.
compare_i64(ca.abs(), cb.abs())
}
///|
/// Serializes the decimal per RFC 9651 §4.1.5: rounds to three decimal
/// places using ties-to-even, rejects more than twelve integer digits, and
/// always keeps at least one fractional digit.
pub fn SfDecimal::to_canonical_string(
self : SfDecimal,
) -> Result[String, SfError] {
if self.scale < 0 {
return Err(SfError::make(DecimalOutOfRange, 0, "negative scale"))
}
if self.scale > MAX_SCALE {
return Err(
SfError::make(DecimalOutOfRange, 0, "scale exceeds supported precision"),
)
}
let rounded = self.round_to_three()
let mag = rounded.coefficient.abs()
let int_part = mag / 1000L
if int_part > MAX_INTEGER_PART {
return Err(
SfError::make(DecimalOutOfRange, 0, "more than 12 integer digits"),
)
}
let frac = (mag % 1000L).to_int()
let mut frac_str = frac.to_string()
while frac_str.length() < 3 {
frac_str = "0" + frac_str
}
// Strip trailing zeros but keep at least one fractional digit.
let mut trimmed = frac_str
while trimmed.length() > 1 && trimmed.has_suffix("0") {
trimmed = trimmed[:trimmed.length() - 1].to_owned()
}
let sign = if rounded.coefficient < 0L && mag > 0L { "-" } else { "" }
Ok(sign + int_part.to_string() + "." + trimmed)
}
///|
fn SfDecimal::round_to_three(self : SfDecimal) -> SfDecimal {
let c = self.coefficient
let s = self.scale
if s <= 3 {
match pow10_checked(3 - s) {
Some(f) => return { coefficient: c * f, scale: 3 }
None => return { coefficient: c, scale: s }
}
}
match pow10_checked(s - 3) {
None => { coefficient: c, scale: s }
Some(div) => {
let sign : Int64 = if c < 0L { -1L } else { 1L }
let mag = c.abs()
let q = mag / div
let r = mag % div
let twice = r * 2L
if twice > div {
return { coefficient: sign * (q + 1L), scale: 3 }
}
if twice == div && q % 2L != 0L {
return { coefficient: sign * (q + 1L), scale: 3 }
}
return { coefficient: sign * q, scale: 3 }
}
}
}
///|
fn pow10_checked(n : Int) -> Int64? {
if n < 0 || n > 18 {
return None
}
let mut v : Int64 = 1L
let mut k = n
while k > 0 {
v = v * 10L
k = k - 1
}
Some(v)
}
///|
fn compare_i64(a : Int64, b : Int64) -> Int {
if a < b {
-1
} else if a > b {
1
} else {
0
}
}
///|
fn decimal_digits(x : Int64) -> Int {
let mut v = x.abs()
if v == 0L {
return 1
}
let mut n = 0
while v > 0L {
v = v / 10L
n = n + 1
}
n
}