Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::money_amount::{money, money_to_value, Money}; ← from money.amount ^1.0.0 · built alongside by fune
use super::money_currency_digits::currency_digits; ← from money.currency-digits ^1.0.0 · built alongside by fune
use super::money_currency_digits_data::CURRENCY_DIGITS; ← money.currency-digits’s rule data (^1.0.0) · built alongside by fune
use super::money_parse_data::CURRENCY_SYMBOLS; ← this capability’s own data, compiled from data/currency-symbols.json into the same file by fune build
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(),
))
}