use crate::error::TypeError;
use crate::number::is_space;
const PLACES: usize = 2;
pub fn cash_in(s: &str) -> Result<i64, TypeError> {
let b = s.as_bytes();
let at = |i: usize| b.get(i).copied().unwrap_or(0);
let range = || TypeError::range(s, "money");
let skip = |mut i: usize| {
while is_space(at(i)) {
i += 1;
}
if at(i) == b'$' {
i += 1;
}
while is_space(at(i)) {
i += 1;
}
i
};
let mut i = skip(0);
let mut negative = false;
match at(i) {
b'-' | b'(' => {
negative = true;
i += 1;
}
b'+' => i += 1,
_ => {}
}
i = skip(i);
let mut value: i64 = 0;
let mut places = 0;
let mut seen_dot = false;
while i < b.len() {
let c = b[i];
if c.is_ascii_digit() && (!seen_dot || places < PLACES) {
value = value
.checked_mul(10)
.and_then(|v| v.checked_sub(i64::from(c - b'0')))
.ok_or_else(range)?;
if seen_dot {
places += 1;
}
} else if c == b'.' && !seen_dot {
seen_dot = true;
} else if c != b',' {
break;
}
i += 1;
}
if at(i) >= b'5' && at(i).is_ascii_digit() {
value = value.checked_sub(1).ok_or_else(range)?;
}
for _ in places..PLACES {
value = value.checked_mul(10).ok_or_else(range)?;
}
while at(i).is_ascii_digit() {
i += 1;
}
while i < b.len() {
match b[i] {
b'-' => negative = true,
c if is_space(c) || matches!(c, b')' | b'+' | b'$') => {}
_ => return Err(TypeError::syntax("money", s)),
}
i += 1;
}
if negative { Ok(value) } else { value.checked_neg().ok_or_else(range) }
}
pub fn cash_out(value: i64, out: &mut Vec<u8>) {
let mut digits = value.unsigned_abs().to_string().into_bytes();
if digits.len() <= PLACES {
let mut padded = vec![b'0'; PLACES + 1 - digits.len()];
padded.extend_from_slice(&digits);
digits = padded;
}
let (whole, fraction) = digits.split_at(digits.len() - PLACES);
if value < 0 {
out.push(b'-');
}
out.push(b'$');
for (n, d) in whole.iter().enumerate() {
if n > 0 && (whole.len() - n) % 3 == 0 {
out.push(b',');
}
out.push(*d);
}
out.push(b'.');
out.extend_from_slice(fraction);
}
fn num_words(value: u64, out: &mut String) {
const SMALL: [&str; 28] = [
"zero",
"one",
"two",
"three",
"four",
"five",
"six",
"seven",
"eight",
"nine",
"ten",
"eleven",
"twelve",
"thirteen",
"fourteen",
"fifteen",
"sixteen",
"seventeen",
"eighteen",
"nineteen",
"twenty",
"thirty",
"forty",
"fifty",
"sixty",
"seventy",
"eighty",
"ninety",
];
let small = |n: u64| SMALL[usize::try_from(n).unwrap_or(0)];
let big = |n: u64| SMALL[usize::try_from(n + 18).unwrap_or(0)];
let tu = value % 100;
if value <= 20 {
out.push_str(small(value));
return;
}
if tu == 0 {
out.push_str(small(value / 100));
out.push_str(" hundred");
return;
}
if value > 99 {
out.push_str(small(value / 100));
out.push_str(" hundred ");
if tu < 20 {
out.push_str("and ");
}
}
if value.is_multiple_of(10) && tu > 10 {
out.push_str(big(tu / 10));
} else if tu < 20 {
out.push_str(small(tu));
} else {
out.push_str(big(tu / 10));
out.push(' ');
out.push_str(small(tu % 10));
}
}
pub fn cash_words(value: i64) -> String {
let mut out = String::new();
if value < 0 {
out.push_str("minus ");
}
let val = value.unsigned_abs();
let dollars = val / 100;
let units = [
(100_000_000_000_000_000, " quadrillion "),
(100_000_000_000_000, " trillion "),
(100_000_000_000, " billion "),
(100_000_000, " million "),
(100_000, " thousand "),
];
for (divisor, word) in units {
let part = (val / divisor) % 1000;
if part != 0 {
num_words(part, &mut out);
out.push_str(word);
}
}
let hundreds = dollars % 1000;
if hundreds != 0 {
num_words(hundreds, &mut out);
}
if dollars == 0 {
out.push_str("zero");
}
out.push_str(if dollars == 1 { " dollar and " } else { " dollars and " });
let cents = val % 100;
num_words(cents, &mut out);
out.push_str(if cents == 1 { " cent" } else { " cents" });
out[..1].make_ascii_uppercase();
out
}
#[cfg(test)]
mod tests {
use super::*;
fn out(value: i64) -> String {
let mut buf = Vec::new();
cash_out(value, &mut buf);
String::from_utf8(buf).unwrap()
}
#[test]
fn text_forms() {
for (input, output) in [
("12.34", "$12.34"),
("-12.34", "-$12.34"),
("0", "$0.00"),
("$1,234,567.89", "$1,234,567.89"),
("1234567.891", "$1,234,567.89"),
("1234567.895", "$1,234,567.90"),
(" $ 12", "$12.00"),
("-$12", "-$12.00"),
("$-12", "-$12.00"),
("(12.5)", "-$12.50"),
("12-", "-$12.00"),
("+12", "$12.00"),
("12 $", "$12.00"),
("1,2,3.4", "$123.40"),
(".5", "$0.50"),
("$", "$0.00"),
("", "$0.00"),
("92233720368547758.07", "$92,233,720,368,547,758.07"),
("-92233720368547758.08", "-$92,233,720,368,547,758.08"),
("1.2345", "$1.23"),
("1.2355", "$1.24"),
("0.005", "$0.01"),
("-0.005", "-$0.01"),
("0.004", "$0.00"),
] {
assert_eq!(out(cash_in(input).unwrap()), output, "{input}");
}
for (input, sqlstate) in [
("92233720368547758.08", "22003"),
("-92233720368547758.09", "22003"),
("92233720368547758.075", "22003"),
("12.3.4", "22P02"),
("abc", "22P02"),
("12 34", "22P02"),
] {
assert_eq!(cash_in(input).unwrap_err().sqlstate.as_str(), sqlstate, "{input}");
}
}
#[test]
fn words() {
assert_eq!(
cash_words(123_456),
"One thousand two hundred thirty four dollars and fifty six cents"
);
assert_eq!(cash_words(-1), "Minus zero dollars and one cent");
assert_eq!(cash_words(0), "Zero dollars and zero cents");
assert_eq!(cash_words(100_000_000), "One million dollars and zero cents");
}
}