pub mod pattern;
pub mod table;
pub mod tag;
use std::collections::{BTreeMap, BTreeSet};
use std::sync::OnceLock;
use mf2_catalog::format::locale_key;
use mf2_catalog::number::Style;
use mf2_catalog::writer::number::{SymbolsSpec, patterns, symbols};
use mf2_model::{Declaration, Expression, FunctionRef, Message, OptionValue, Pattern, PatternPart};
pub use self::table::{Record, Table};
pub use self::tag::Tag;
use crate::error::Error;
const TABLE: &str = include_str!("../data/numbers.txt");
fn table() -> Result<&'static Table<'static>, Error> {
static PARSED: OnceLock<Result<Table<'static>, String>> = OnceLock::new();
match PARSED.get_or_init(|| Table::parse(TABLE).map_err(|e| e.to_string())) {
Ok(t) => Ok(t),
Err(message) => Err(Error::Table {
line: 0,
message: message.clone(),
}),
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Selection {
Listed(BTreeSet<String>),
All,
}
impl Default for Selection {
fn default() -> Selection {
Selection::Listed(BTreeSet::new())
}
}
impl Selection {
pub fn add(&mut self, v: &str) {
if let Selection::Listed(set) = self {
set.insert(v.to_owned());
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct NumberNeeds {
pub symbols: bool,
pub percent: bool,
pub currency: Option<CurrencyNeeds>,
pub unit: Option<UnitNeeds>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
#[allow(clippy::struct_excessive_bools)]
pub struct CurrencyNeeds {
pub accounting: bool,
pub hidden: bool,
pub narrow: bool,
pub names: bool,
pub codes: Selection,
}
impl CurrencyNeeds {
pub const ALL: CurrencyNeeds = CurrencyNeeds {
accounting: true,
hidden: true,
narrow: true,
names: true,
codes: Selection::All,
};
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct UnitNeeds {
pub ids: Selection,
pub widths: [bool; 3],
pub names: bool,
}
impl Default for UnitNeeds {
fn default() -> UnitNeeds {
UnitNeeds {
ids: Selection::default(),
widths: [false, true, false],
names: false,
}
}
}
impl UnitNeeds {
pub const ALL: UnitNeeds = UnitNeeds {
ids: Selection::All,
widths: [true; 3],
names: false,
};
}
enum Opt<'m> {
Absent,
Literal(&'m str),
Variable,
}
fn option<'m>(f: &'m FunctionRef<'_>, name: &str) -> Opt<'m> {
match f.options.get(name) {
None => Opt::Absent,
Some(OptionValue::Literal(l)) => Opt::Literal(l.value.as_ref()),
Some(_) => Opt::Variable,
}
}
impl NumberNeeds {
pub fn from_functions<'a>(functions: impl IntoIterator<Item = &'a str>) -> NumberNeeds {
let mut n = NumberNeeds::default();
for f in functions {
match f {
"number" | "integer" | "offset" => n.symbols = true,
"percent" => n.percent = true,
"currency" => n.currency = Some(CurrencyNeeds::ALL),
"unit" => n.unit = Some(UnitNeeds::ALL),
_ => {}
}
}
n
}
pub fn add_message(&mut self, message: &Message<'_>) {
for d in message.declarations() {
match d {
Declaration::Input(i) => self.add_function(i.value.function.as_ref()),
Declaration::Local(l) => {
if l.value.function().is_none() && matches!(l.value, Expression::Variable(_)) {
self.symbols = true;
}
self.add_function(l.value.function());
}
_ => {}
}
}
let patterns: Vec<&Pattern<'_>> = match message {
Message::Pattern(p) => vec![&p.pattern],
Message::Select(s) => s.variants.iter().map(|v| &v.value).collect(),
_ => Vec::new(),
};
for p in patterns {
for part in p {
if let PatternPart::Expression(e) = part {
if e.function().is_none() && matches!(e, Expression::Variable(_)) {
self.symbols = true;
}
self.add_function(e.function());
}
}
}
}
fn add_function(&mut self, f: Option<&FunctionRef<'_>>) {
let Some(f) = f else {
return;
};
match f.name.as_ref() {
"number" | "integer" | "offset" => self.symbols = true,
"percent" => self.percent = true,
"currency" => {
let c = self.currency.get_or_insert_with(CurrencyNeeds::default);
match option(f, "currency") {
Opt::Literal(code)
if code.len() == 3 && code.bytes().all(|b| b.is_ascii_alphabetic()) =>
{
c.codes.add(&code.to_ascii_uppercase());
}
Opt::Variable => c.codes = Selection::All,
Opt::Literal(_) | Opt::Absent => {}
}
if matches!(
option(f, "currencySign"),
Opt::Literal("accounting") | Opt::Variable
) {
c.accounting = true;
}
match option(f, "currencyDisplay") {
Opt::Literal("never") => c.hidden = true,
Opt::Literal("name") => c.names = true,
Opt::Literal("narrowSymbol") => c.narrow = true,
Opt::Variable => {
c.hidden = true;
c.names = true;
c.narrow = true;
}
Opt::Literal(_) | Opt::Absent => {}
}
}
"unit" => {
let u = self.unit.get_or_insert_with(|| UnitNeeds {
widths: [false; 3],
..UnitNeeds::default()
});
match option(f, "unit") {
Opt::Literal(id) => u.ids.add(id),
Opt::Variable => u.ids = Selection::All,
Opt::Absent => {}
}
match option(f, "unitDisplay") {
Opt::Literal("long") => u.widths[0] = true,
Opt::Literal("narrow") => u.widths[2] = true,
Opt::Variable => u.widths = [true; 3],
Opt::Literal(_) | Opt::Absent => u.widths[1] = true,
}
}
_ => {}
}
}
pub const fn any(&self) -> bool {
self.symbols || self.percent || self.currency.is_some() || self.unit.is_some()
}
pub fn needs_cardinal(&self) -> bool {
self.unit.is_some() || self.currency.as_ref().is_some_and(|c| c.names)
}
pub fn styles(&self) -> Vec<Style> {
let mut out = Vec::new();
if self.percent {
out.push(Style::Percent);
}
if let Some(c) = &self.currency {
out.extend([Style::Currency, Style::CurrencyAlpha]);
if c.hidden {
out.push(Style::CurrencyNoSymbol);
}
if c.accounting {
out.extend([Style::Accounting, Style::AccountingAlpha]);
if c.hidden {
out.push(Style::AccountingNoSymbol);
}
}
}
out
}
}
pub const fn style_field(style: Style) -> &'static str {
match style {
Style::Percent => "percent-pattern",
Style::Currency => "currency",
Style::CurrencyAlpha => "currency-alpha",
Style::CurrencyNoSymbol => "currency-none",
Style::Accounting => "accounting",
Style::AccountingAlpha => "accounting-alpha",
Style::AccountingNoSymbol => "accounting-none",
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct NumberData {
pub locale: &'static str,
pub numbering_system: &'static str,
pub symbols_system: &'static str,
pub digits: Option<&'static str>,
pub minimum_grouping_digits: u8,
pub decimal: &'static str,
pub group: &'static str,
pub minus: &'static str,
pub plus: &'static str,
pub percent: &'static str,
pub decimal_pattern: &'static str,
pub patterns: BTreeMap<Style, &'static str>,
pub currency_name_patterns: Vec<(u8, &'static str)>,
pub chain: Vec<&'static str>,
}
fn resolve_locale(t: &'static Table<'static>, tag: &Tag) -> &'static str {
let mut tag = tag.clone();
if tag.script.is_none()
&& let Some(region) = &tag.region
&& let Some(script) = t.likely.get(format!("{}-{region}", tag.language).as_str())
{
tag.script = Some((*script).to_owned());
}
t.lookup(&tag.base())
}
fn field(
t: &'static Table<'static>,
locale: &'static str,
system: &'static str,
name: &'static str,
) -> Result<&'static str, Error> {
t.system_field(locale, system, name)
.ok_or_else(|| Error::Missing {
locale: locale.to_owned(),
system: system.to_owned(),
field: name,
})
}
pub fn number_data(tag: &str) -> Result<NumberData, Error> {
let t = table()?;
let parsed = Tag::parse(tag);
let locale = resolve_locale(t, &parsed);
let default = t.locale_field(locale, "default").unwrap_or("latn");
let nu: &'static str = match parsed.nu.as_deref() {
Some("native") => t.locale_field(locale, "native").unwrap_or(default),
Some(n) => t
.digits
.get_key_value(n)
.map(|(k, _)| *k)
.or((n == "latn").then_some("latn"))
.unwrap_or(default),
None => default,
};
let system = if t.has_system(locale, nu) { nu } else { "latn" };
let digits = if nu == "latn" {
None
} else {
Some(
t.digits
.get(nu)
.map(AsRef::as_ref)
.ok_or_else(|| Error::Missing {
locale: locale.to_owned(),
system: nu.to_owned(),
field: "digits",
})?,
)
};
let min = t.locale_field(locale, "min-grouping").unwrap_or("1");
let minimum_grouping_digits = min.parse().map_err(|_| Error::Table {
line: 0,
message: format!("{locale}: min-grouping {min:?}"),
})?;
let mut pats = BTreeMap::new();
for style in Style::ALL {
if let Some(p) = t.system_field(locale, system, style_field(style)) {
pats.insert(style, p);
}
}
let names_system = if t
.system_field(locale, system, "currency-name-other")
.is_some()
{
system
} else {
"latn"
};
let currency_name_patterns = crate::unit::CATEGORIES
.iter()
.zip(0u8..)
.filter_map(|(c, k)| {
Some((
k,
t.system_field(locale, names_system, &format!("currency-name-{c}"))?,
))
})
.collect();
Ok(NumberData {
currency_name_patterns,
chain: t.chain(locale),
locale,
numbering_system: nu,
symbols_system: system,
digits,
minimum_grouping_digits,
decimal: field(t, locale, system, "decimal")?,
group: field(t, locale, system, "group")?,
minus: field(t, locale, system, "minus")?,
plus: field(t, locale, system, "plus")?,
percent: field(t, locale, system, "percent")?,
decimal_pattern: field(t, locale, system, "decimal-pattern")?,
patterns: pats,
})
}
impl NumberData {
pub fn symbols_entry(&self) -> Result<Vec<u8>, Error> {
let grouping = pattern::parse(self.decimal_pattern)?.grouping;
Ok(symbols(&SymbolsSpec {
grouping,
minimum_grouping_digits: self.minimum_grouping_digits,
decimal: self.decimal,
group: self.group,
minus: self.minus,
plus: self.plus,
percent: self.percent,
digits: self.digits,
})?)
}
pub fn patterns_entry(&self, styles: &[Style]) -> Result<Vec<u8>, Error> {
let mut parsed = Vec::new();
for style in styles {
if let Some(p) = self.patterns.get(style) {
parsed.push((*style, pattern::parse(p)?));
}
}
let specs: Vec<_> = parsed.iter().map(|(s, p)| (*s, p.spec())).collect();
Ok(patterns(&specs)?)
}
}
pub fn number_locale_entries(tag: &str, needs: &NumberNeeds) -> Result<Vec<(u32, Vec<u8>)>, Error> {
if !needs.any() {
return Ok(Vec::new());
}
let data = number_data(tag)?;
let mut out = vec![(locale_key::NUMBER_SYMBOLS, data.symbols_entry()?)];
let styles = needs.styles();
if !styles.is_empty() {
out.push((locale_key::NUMBER_PATTERNS, data.patterns_entry(&styles)?));
}
if let Some(c) = &needs.currency {
let list = crate::currency::currency_data(&data.chain, &c.codes)?;
out.push((
locale_key::CURRENCY_DATA,
crate::currency::currency_entry(&data, &list, c)?,
));
}
if let Some(u) = &needs.unit {
let (per, list) = crate::unit::unit_data(&data.chain, &u.ids)?;
out.push((
locale_key::UNIT_DATA,
crate::unit::unit_entry(&per, &list, u)?,
));
}
Ok(out)
}
pub fn number_locales() -> Result<Vec<&'static str>, Error> {
Ok(table()?.locales.keys().copied().collect())
}