///|
/// Whether a string is a decimal amount this importer accepts.
///
/// The accepted shape is an optional `+` or `-`, one or more integer digits, an
/// optional `.` followed by one or more digits. Nothing else: no thousands
/// separators, no trailing point, no exponent, no surrounding whitespace. The
/// check is strict on purpose, because a bank that writes `1.234,56` means
/// something the importer must not guess at.
pub fn is_valid_amount(amount : String) -> Bool {
let mut digits_before = 0
let mut digits_after = 0
let mut has_point = false
let mut index = 0
for c in amount.iter() {
if (c == '-' || c == '+') && index == 0 {
()
} else if c == '.' && !has_point {
has_point = true
} else if c >= '0' && c <= '9' {
if has_point {
digits_after += 1
} else {
digits_before += 1
}
} else {
return false
}
index += 1
}
digits_before > 0 && (!has_point || digits_after > 0)
}
///|
/// The scale check shared by the decimal and the integer direction, so both
/// refuse the same scales.
fn valid_scale(scale : Int) -> Bool {
scale >= 0 && scale <= 6
}
///|
/// Convert a decimal amount string to an integer count of minor units.
///
/// `scale` is the number of fractional digits the target ledger stores (`2` for
/// cents); scales from `0` to `6` are accepted. The conversion never uses
/// floating point: it shifts digits and checks `Int64` overflow at every step,
/// so `"9223372036854775808"` at scale `0` returns `None` instead of a wrapped
/// value. Input with more fractional digits than `scale` is rejected rather than
/// rounded, because silently losing a digit is exactly the kind of error an
/// importer must not hide.
pub fn amount_to_minor_units(amount : String, scale : Int) -> Int64? {
if !is_valid_amount(amount) || !valid_scale(scale) {
return None
}
let mut negative = false
let mut seen_point = false
let mut fractional = 0
let mut value = 0L
for c in amount.iter() {
if c == '-' {
negative = true
} else if c == '+' {
()
} else if c == '.' {
seen_point = true
} else {
let digit = (c.to_int() - 48).to_int64()
if value > (9223372036854775807L - digit) / 10L {
return None
}
value = value * 10L + digit
if seen_point {
fractional += 1
}
}
}
if fractional > scale {
return None
}
for _ in fractional.. 922337203685477580L {
return None
}
value = value * 10L
}
Some(if negative { -value } else { value })
}
///|
/// Convert an integer count of minor units back into a decimal amount string.
///
/// This is the inverse of [`amount_to_minor_units`] at the same scale:
/// `-4250` at scale `2` becomes `"-42.50"` and `5` becomes `"0.05"`, so a ledger
/// can be rendered back into the decimal form a bank used. `None` is returned
/// only for a scale outside `0` to `6`; every `Int64` is representable,
/// including the smallest one.
pub fn minor_units_to_amount(minor : Int64, scale : Int) -> String? {
if !valid_scale(scale) {
return None
}
let negative = minor < 0L
let digits : Array[Int] = []
let mut value = minor
// Digits are taken in the negative domain so that the smallest Int64 works:
// dividing it by -10 yields a positive quotient, which is flipped back.
while value != 0L {
let quotient = value / -10L
let remainder = value % -10L
digits.push((if remainder < 0L { -remainder } else { remainder }).to_int())
value = if quotient > 0L { -quotient } else { quotient }
}
if digits.length() == 0 {
digits.push(0)
}
while digits.length() < scale + 1 {
digits.push(0)
}
let out = StringBuilder()
if negative {
out.write_char('-')
}
let count = digits.length()
for position in 0.. String? {
if !is_valid_amount(amount) {
return None
}
let chars = amount.iter().to_array()
let negative = chars[0] == '-'
let start = if chars[0] == '-' || chars[0] == '+' { 1 } else { 0 }
let mut point = chars.length()
let mut any_nonzero = false
for position in start..= '1' && chars[position] <= '9' {
any_nonzero = true
}
}
let out = StringBuilder()
if negative && any_nonzero {
out.write_char('-')
}
let mut first_significant = -1
for position in start.. Int64? {
let sum = left + right
if right > 0L && sum < left {
None
} else if right < 0L && sum > left {
None
} else {
Some(sum)
}
}
///|
/// Subtract two counts of minor units, or return `None` when the difference
/// would overflow.
pub fn checked_sub(left : Int64, right : Int64) -> Int64? {
let difference = left - right
if right < 0L && difference < left {
None
} else if right > 0L && difference > left {
None
} else {
Some(difference)
}
}
///|
/// Sum a list of amounts at one scale, or return `None` when an amount cannot be
/// converted or the total overflows.
///
/// `opening` is added first, so a caller can fold a running balance over a batch
/// of statements without reimplementing the overflow checks.
pub fn sum_amounts(
amounts : Array[String],
scale : Int,
opening : Int64,
) -> Int64? {
let mut total = opening
for amount in amounts {
let minor = match amount_to_minor_units(amount, scale) {
Some(value) => value
None => return None
}
total = match checked_add(total, minor) {
Some(sum) => sum
None => return None
}
}
Some(total)
}