use super::funejson::Value; use super::money_amount::{money, money_to_value, Money}; use super::money_currency_digits::currency_digits; use super::money_currency_digits_data::CURRENCY_DIGITS; use super::money_parse_data::CURRENCY_SYMBOLS; const MAX_SAFE: u128 = 9_007_199_254_740_991; // Space, no-break space, narrow no-break space: what real documents use. const SPACES: &[char] = &[' ', '\u{a0}', '\u{202f}']; fn style_of(style: &str) -> (&'static [char], char) { match style { "comma-dot" => (&[','], '.'), "dot-comma" => (&['.'], ','), "space-comma" => (SPACES, ','), "apostrophe-dot" => (&['\'', '\u{2019}'], '.'), other => panic!("unknown number style \"{}\"", other), } } fn is_digits(s: &str) -> bool { !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit()) } /// The longest known symbol or ISO code that `matches`, or None. fn longest_symbol(matches: impl Fn(&str) -> bool) -> Option<&'static str> { let mut best: Option<&'static str> = None; let candidates = CURRENCY_SYMBOLS .iter() .map(|r| r.symbol) .chain(CURRENCY_DIGITS.iter().map(|r| r.code)); for symbol in candidates { // Compare in characters, as TypeScript and Python do. let longer = match best { None => true, Some(b) => symbol.chars().count() > b.chars().count(), }; if matches(symbol) && longer { best = Some(symbol); } } best } /// Whole digits and fraction digits, or None when the text is not a number in this style. fn split_number(s: &str, groups: &[char], decimal: char) -> Option<(String, String)> { let parts: Vec<&str> = s.split(decimal).collect(); if parts.len() > 2 { return None; } let fraction = if parts.len() == 2 { parts[1] } else { "" }; if parts.len() == 2 && !is_digits(fraction) { return None; } let chunks: Vec<&str> = parts[0].split(|c: char| groups.contains(&c)).collect(); if !chunks.iter().all(|c| is_digits(c)) { return None; } if chunks.len() > 1 { // Real thousands grouping only: 1-3 digits without a leading zero, then threes. let first = chunks[0]; if first.len() > 3 || first.starts_with('0') { return None; } if !chunks[1..].iter().all(|c| c.len() == 3) { return None; } } Some((chunks.concat(), fraction.to_string())) } /// Parse an amount written as text into integer minor units. /// /// Strict by design: the caller names the currency and the number style, and /// anything ambiguous (too many decimals, odd grouping, a symbol for another /// currency) is an error rather than a guess. /// /// # Panics /// Panics on an unknown style or currency, a symbol for another currency, text /// that is not an amount in the style, too many decimal places, or an amount /// beyond 2^53 - 1 minor units. pub fn parse_money(text: &str, currency: &str, style: &str) -> Money { let (groups, decimal) = style_of(style); let digits = currency_digits(currency) as usize; let mut s: &str = text.trim_matches(' '); let mut negative = false; if let Some(rest) = s.strip_prefix('-') { negative = true; s = rest; } let mut symbol = longest_symbol(|k| s.starts_with(k)); if let Some(found) = symbol { s = &s[found.len()..]; if let Some(rest) = s.strip_prefix(SPACES) { s = rest; } if !negative { if let Some(rest) = s.strip_prefix('-') { negative = true; s = rest; } } } else { symbol = longest_symbol(|k| s.ends_with(k)); if let Some(found) = symbol { s = &s[..s.len() - found.len()]; if let Some(rest) = s.strip_suffix(SPACES) { s = rest; } } } if let Some(found) = symbol { if found != currency && !CURRENCY_SYMBOLS .iter() .any(|r| r.code == currency && r.symbol == found) { panic!("currency symbol \"{}\" does not match {}", found, currency); } } let (whole, fraction) = match split_number(s, groups, decimal) { Some(parts) => parts, None => panic!("\"{}\" is not a valid amount", text), }; if fraction.len() > digits { panic!("too many decimal places for {} (at most {})", currency, digits); } let all = format!("{}{}{}", whole, fraction, "0".repeat(digits - fraction.len())); let significant = all.trim_start_matches('0'); let minor: u128 = if significant.len() > 16 { MAX_SAFE + 1 } else if significant.is_empty() { 0 } else { significant.parse().unwrap() }; if minor > MAX_SAFE { panic!("amount is too large: \"{}\" exceeds 2^53 - 1 minor units", text); } let value = minor as i64; money(if negative { -value } else { value }, currency) } pub fn fune_vector(args: &[Value]) -> Value { money_to_value(&parse_money( args[0].as_str(), args[1].as_str(), args[2].as_str(), )) }