use std::collections::BTreeMap;
use std::sync::{Arc, OnceLock};
use mf2_catalog::number::{OTHER, TemplatePart};
use mf2_catalog::writer::currency::{CurrenciesSpec, CurrencySpec, currencies};
use unicode_properties::{GeneralCategoryGroup, UnicodeGeneralCategory};
use crate::blocks::{Block, Blocks, Cache, get, keys};
use crate::error::Error;
use crate::number::{CurrencyNeeds, NumberData, Selection};
use crate::unit::CATEGORIES;
pub const FIELDS: &[&str] = &[
"symbol",
"narrow",
"pattern",
"decimal",
"group",
"name",
"name-other",
"name-zero",
"name-one",
"name-two",
"name-few",
"name-many",
];
pub fn fallback(field: &str) -> Option<&'static str> {
match field {
"narrow" => Some("symbol"),
"name-other" => Some("name"),
"name-zero" | "name-one" | "name-two" | "name-few" | "name-many" => Some("name-other"),
_ => None,
}
}
const TABLE: &str = include_str!("../data/currencies.txt");
struct Loaded {
blocks: Blocks<'static>,
cache: Cache,
fractions: BTreeMap<&'static str, (u8, u32)>,
}
fn loaded() -> Result<&'static Loaded, Error> {
static LOADED: OnceLock<Result<Loaded, String>> = OnceLock::new();
let r = LOADED.get_or_init(|| {
let blocks = Blocks::index(TABLE, &["currency"]).map_err(|e| e.to_string())?;
let mut fractions = BTreeMap::new();
for line in &blocks.globals {
let mut w = line.split('\t');
if w.next() != Some("fraction") {
continue;
}
let code = w.next().ok_or("fraction without a code")?;
let (mut digits, mut rounding) = (None, 0u32);
for f in w {
match f.split_once('=') {
Some(("digits", v)) => digits = v.parse::<u8>().ok(),
Some(("rounding", v)) => rounding = v.parse().map_err(|_| "bad rounding")?,
_ => return Err(format!("bad fraction field {f:?}")),
}
}
fractions.insert(code, (digits.ok_or("fraction without digits")?, rounding));
}
if !fractions.contains_key("DEFAULT") {
return Err("no DEFAULT fraction".to_owned());
}
Ok(Loaded {
blocks,
cache: Cache::default(),
fractions,
})
});
r.as_ref().map_err(|message| Error::Table {
line: 0,
message: message.clone(),
})
}
fn block(l: &Loaded, locale: &str) -> Result<Arc<Block<'static>>, Error> {
l.cache
.get(&l.blocks, locale, |_| 1, |_, f| FIELDS.contains(&f))
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CurrencyData {
pub code: String,
pub digits: u8,
pub rounding: u32,
pub symbol: String,
pub narrow: String,
pub name: String,
pub names: [String; 6],
pub pattern: Option<String>,
pub decimal: Option<String>,
pub group: Option<String>,
}
fn all_codes(l: &Loaded, chain: &[&Block<'_>]) -> Vec<String> {
let mut out: std::collections::BTreeSet<String> = keys(chain, "currency")
.into_iter()
.filter_map(|k| k.strip_prefix("currency ").map(str::to_owned))
.collect();
out.extend(
l.fractions
.keys()
.filter(|c| **c != "DEFAULT")
.map(|c| (*c).to_owned()),
);
out.into_iter().collect()
}
pub(crate) fn currency_data(
chain_names: &[&str],
codes: &Selection,
) -> Result<Vec<CurrencyData>, Error> {
let l = loaded()?;
let blocks: Vec<Arc<Block<'static>>> = chain_names
.iter()
.map(|n| block(l, n))
.collect::<Result<_, _>>()?;
let chain: Vec<&Block<'_>> = blocks.iter().map(AsRef::as_ref).collect();
let list: Vec<String> = match codes {
Selection::All => all_codes(l, &chain),
Selection::Listed(set) => {
let mut v: Vec<String> = set.iter().map(|c| c.to_ascii_uppercase()).collect();
v.sort();
v.dedup();
v
}
};
let default = l.fractions.get("DEFAULT").copied().unwrap_or((2, 0));
let mut out = Vec::with_capacity(list.len());
for code in list {
if code.len() != 3 || !code.bytes().all(|b| b.is_ascii_uppercase()) {
return Err(Error::Missing {
locale: chain_names.first().copied().unwrap_or("und").to_owned(),
system: code,
field: "a well-formed currency code",
});
}
let key = format!("currency {code}");
let f = |field: &str| get(&chain, &key, field, &fallback).map(str::to_owned);
let symbol = f("symbol").unwrap_or_else(|| code.clone());
let narrow = f("narrow").unwrap_or_else(|| symbol.clone());
let name = f("name").unwrap_or_else(|| code.clone());
let names = CATEGORIES.map(|c| f(&format!("name-{c}")).unwrap_or_else(|| name.clone()));
let (digits, rounding) = l.fractions.get(code.as_str()).copied().unwrap_or(default);
out.push(CurrencyData {
digits,
rounding,
narrow,
name,
names,
pattern: f("pattern"),
decimal: f("decimal"),
group: f("group"),
symbol,
code,
});
}
Ok(out)
}
pub(crate) fn default_digits() -> Result<u8, Error> {
Ok(loaded()?.fractions.get("DEFAULT").map_or(2, |d| d.0))
}
fn letter_like(c: char) -> bool {
!matches!(
c.general_category_group(),
GeneralCategoryGroup::Symbol | GeneralCategoryGroup::Separator
)
}
pub fn edges(s: &str) -> u8 {
let first = s.chars().next().is_some_and(letter_like);
let last = s.chars().next_back().is_some_and(letter_like);
u8::from(first) | u8::from(last) << 1
}
pub(crate) fn currency_entry(
data: &NumberData,
list: &[CurrencyData],
needs: &CurrencyNeeds,
) -> Result<Vec<u8>, Error> {
let name_patterns: Vec<(u8, Vec<TemplatePart<'_>>)> = if needs.names {
data.currency_name_patterns
.iter()
.map(|(c, p)| Ok((*c, crate::template::parse(p, 2)?)))
.collect::<Result<_, Error>>()?
} else {
Vec::new()
};
let parsed: Vec<Option<crate::number::pattern::Parsed>> = list
.iter()
.map(|c| {
c.pattern
.as_deref()
.map(crate::number::pattern::parse)
.transpose()
})
.collect::<Result<_, _>>()?;
let spec = CurrenciesSpec {
default_digits: default_digits()?,
narrow: needs.narrow,
names: needs.names,
name_patterns,
currencies: list
.iter()
.zip(&parsed)
.map(|(c, p)| CurrencySpec {
code: &c.code,
digits: c.digits,
rounding: c.rounding,
symbol: &c.symbol,
narrow: &c.narrow,
name: &c.name,
names: (0u8..=OTHER)
.zip(c.names.iter())
.map(|(k, s)| (k, s.as_str()))
.collect(),
edges: edges(&c.symbol) | edges(&c.narrow) << 2,
pattern: p.as_ref().map(crate::number::pattern::Parsed::spec),
decimal: c.decimal.as_deref(),
group: c.group.as_deref(),
})
.collect(),
};
Ok(currencies(&spec)?)
}