use mf2_catalog::Catalog;
use mf2_catalog::currency::{Currencies, Currency};
use mf2_catalog::format::locale_key;
use mf2_catalog::number::{Patterns, Style, Symbols, Template, TemplatePart};
use mf2_catalog::unit::{Unit, Units, Width};
use mf2_runtime::{
Category, Digits, Dir, ErrorSink, FnContext, FormatError, Function, INTL_NUMBERS, Measure,
MeasureUnit, Number, NumberOut, NumberSpec, Options, Sink, SubPartSink, Value, plural_category,
};
use crate::intl;
use crate::localize::{self, Out, Seps, Symbol, ends_with_currency, starts_with_currency};
const DISPLAY_SHIFT: u32 = 0;
const SIGN_SHIFT: u32 = 3;
const DIGITS_SHIFT: u32 = 5;
const WIDTH_SHIFT: u32 = 12;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum Display {
NarrowSymbol = 1,
Symbol = 2,
Name = 3,
Code = 4,
Never = 5,
}
const DISPLAYS: [(&str, u32); 5] = [
("narrowSymbol", Display::NarrowSymbol as u32),
("symbol", Display::Symbol as u32),
("name", Display::Name as u32),
("code", Display::Code as u32),
("never", Display::Never as u32),
];
const SIGNS: [(&str, u32); 2] = [("standard", 1), ("accounting", 2)];
const WIDTHS: [(&str, u32); 3] = [("short", 1), ("narrow", 2), ("long", 3)];
fn field(flags: u32, shift: u32, bits: u32) -> u32 {
(flags >> shift) & ((1 << bits) - 1)
}
fn set(flags: &mut u32, shift: u32, bits: u32, v: u32) {
let mask = ((1 << bits) - 1) << shift;
*flags = (*flags & !mask) | ((v << shift) & mask);
}
pub(crate) fn display_of(flags: u32) -> Display {
match field(flags, DISPLAY_SHIFT, 3) {
1 => Display::NarrowSymbol,
3 => Display::Name,
4 => Display::Code,
5 => Display::Never,
_ => Display::Symbol,
}
}
pub(crate) fn accounting(flags: u32) -> bool {
field(flags, SIGN_SHIFT, 2) == 2
}
pub(crate) fn own_digits(flags: u32) -> bool {
field(flags, DIGITS_SHIFT, 7) < 2
}
pub(crate) fn width_of(flags: u32) -> Width {
match field(flags, WIDTH_SHIFT, 2) {
2 => Width::Narrow,
3 => Width::Long,
_ => Width::Short,
}
}
fn str_of<'a>(v: &Value<'a>) -> Option<&'a str> {
match v {
Value::Str(s) | Value::Decimal(s) => Some(s),
Value::Custom(c) => c.as_str(),
_ => None,
}
}
fn currency_code(s: &str) -> Option<[u8; 3]> {
match *s.as_bytes() {
[a, b, c]
if a.is_ascii_alphabetic() && b.is_ascii_alphabetic() && c.is_ascii_alphabetic() =>
{
Some([
a.to_ascii_uppercase(),
b.to_ascii_uppercase(),
c.to_ascii_uppercase(),
])
}
_ => None,
}
}
fn is_unit_id(s: &str) -> bool {
let b = s.as_bytes();
if b.is_empty() || b.len() > 64 || b.first() == Some(&b'-') || b.last() == Some(&b'-') {
return false;
}
let mut prev_dash = false;
for &c in b {
let dash = c == b'-';
if !(dash || c.is_ascii_lowercase() || c.is_ascii_digit()) || (dash && prev_dash) {
return false;
}
prev_dash = dash;
}
true
}
fn category(catalog: &Catalog, d: &Digits<'_>) -> Category {
let rules = catalog
.locale_entry(locale_key::PLURAL_CARDINAL)
.unwrap_or(&[]);
plural_category(rules, &d.operands())
}
fn operand_measure<'a>(operand: Option<&Value<'a>>) -> Option<(MeasureUnit<'a>, u32)> {
match operand? {
Value::Measure(m) => Some((m.unit, m.flags)),
Value::Custom(c) => c.as_measure().map(|m| (m.unit, m.flags)),
_ => None,
}
}
fn keyword(
text: Option<&str>,
table: &[(&str, u32)],
shift: u32,
bits: u32,
flags: &mut u32,
errs: &mut dyn ErrorSink,
) {
match text.and_then(|t| table.iter().find(|(k, _)| *k == t)) {
Some(&(_, code)) => set(flags, shift, bits, code),
None => errs.error(FormatError::BadOption),
}
}
fn resolve_measure<'a>(
unit: bool,
cx: &FnContext<'_>,
operand: Option<&Value<'a>>,
options: Options<'_, 'a>,
errs: &mut dyn ErrorSink,
) -> Option<Value<'a>> {
let inherited =
operand_measure(operand).filter(|(u, _)| matches!(u, MeasureUnit::Unit(_)) == unit);
let mut what = inherited.map(|(u, _)| u);
let mut flags = inherited.map_or(0, |(_, f)| f);
for (name, v) in options.iter() {
let text = str_of(v.value);
match (unit, name) {
(false, "currency") => match text.and_then(currency_code) {
Some(_) if inherited.is_some() => errs.error(FormatError::BadOption),
Some(c) => what = Some(MeasureUnit::Currency(c)),
None => errs.error(FormatError::BadOption),
},
(true, "unit") => match text.filter(|u| is_unit_id(u)).map(MeasureUnit::Unit) {
Some(u) if inherited.is_some_and(|(i, _)| i != u) => {
errs.error(FormatError::BadOption);
}
Some(u) => what = Some(u),
None => errs.error(FormatError::BadOption),
},
(false, "currencyDisplay") => {
keyword(text, &DISPLAYS, DISPLAY_SHIFT, 3, &mut flags, errs);
}
(false, "currencySign") => keyword(text, &SIGNS, SIGN_SHIFT, 2, &mut flags, errs),
(true, "unitDisplay") => keyword(text, &WIDTHS, WIDTH_SHIFT, 2, &mut flags, errs),
(false, "fractionDigits") => {
if text == Some("auto") {
set(&mut flags, DIGITS_SHIFT, 7, 1);
} else if let Some(n) = v.value.digit_size() {
set(&mut flags, DIGITS_SHIFT, 7, 2 + u32::from(n));
} else {
errs.error(FormatError::BadOption);
}
}
(true, "usage") => errs.error(FormatError::UnsupportedOperation),
_ => {}
}
}
let Some(what) = what else {
errs.error(FormatError::BadOperand);
return None;
};
let spec = match what {
MeasureUnit::Currency(code) => NumberSpec::currency(match field(flags, DIGITS_SHIFT, 7) {
n if n >= 2 => u8::try_from(n - 2).unwrap_or(2),
_ if INTL_NUMBERS => 2,
_ => auto_digits(cx.catalog(), code),
}),
MeasureUnit::Unit(_) => NumberSpec::UNIT,
};
let number = Number::resolve(spec, cx, operand, &options, errs)?;
Some(Value::Measure(Measure::new(number, what, flags)))
}
#[derive(Clone, Copy, Default, Debug)]
pub struct CurrencyFunction;
pub static CURRENCY: CurrencyFunction = CurrencyFunction;
impl Function for CurrencyFunction {
fn resolve<'a>(
&self,
cx: &FnContext<'_>,
operand: Option<&Value<'a>>,
options: &Options<'_, 'a>,
errs: &mut dyn ErrorSink,
) -> Option<Value<'a>> {
resolve_measure(false, cx, operand, *options, errs)
}
fn formattable(&self, _cx: &FnContext<'_>, value: &Value<'_>) -> Result<(), FormatError> {
match value {
Value::Measure(m) if matches!(m.unit, MeasureUnit::Currency(_)) => Ok(()),
_ => Err(FormatError::MessageFunctionError),
}
}
fn format(&self, cx: &FnContext<'_>, value: &Value<'_>, out: &mut dyn Sink) {
if let Value::Measure(m) = value {
if INTL_NUMBERS {
return intl::measure(cx, m, NumberOut::Text(out));
}
write_currency(cx.catalog(), m, &mut Out::Text(out));
}
}
fn format_parts(&self, cx: &FnContext<'_>, value: &Value<'_>, out: &mut dyn SubPartSink) {
if let Value::Measure(m) = value {
if INTL_NUMBERS {
return intl::measure(cx, m, NumberOut::Parts(out));
}
write_currency(cx.catalog(), m, &mut Out::Parts(out));
}
}
fn part_kind(&self) -> &'static str {
"number"
}
fn dir(&self, _cx: &FnContext<'_>, _value: &Value<'_>) -> Dir {
Dir::Ltr
}
}
fn auto_digits(catalog: &Catalog, code: [u8; 3]) -> u8 {
Currencies::of(catalog).map_or(2, |c| {
c.get(code)
.map_or(c.default_digits(), |x| x.fraction_digits())
})
}
fn write_currency(catalog: &Catalog, m: &Measure<'_>, out: &mut Out<'_>) {
let Some(d) = m.number.digits() else {
return;
};
let code = m.unit.as_str();
let Some(sym) = Symbols::of(catalog) else {
localize::neutral(&d, out);
out.put("literal", "\u{a0}");
out.put("currency", code);
return;
};
let bytes = match m.unit {
MeasureUnit::Currency(c) => c,
MeasureUnit::Unit(_) => return,
};
let data = Currencies::of(catalog);
let cur: Option<Currency<'_>> = data.and_then(|c| c.get(bytes));
let display = display_of(m.flags);
let mut seps = Seps::of(&sym);
if let Some(c) = cur {
if let Some(dec) = c.decimal() {
seps.decimal = dec;
}
if let Some(g) = c.group() {
seps.group = g;
}
}
if display == Display::Name {
return write_currency_name(catalog, &sym, seps, cur, code, &d, m, out);
}
let (text, first, last) = match (display, cur) {
(Display::Never, _) => ("", false, false),
(Display::Code, _) | (_, None) => (code, true, true),
(Display::NarrowSymbol, Some(c)) => match c.narrow_symbol() {
Some(n) => (n, c.narrow_edges().first, c.narrow_edges().last),
None => (c.symbol(), c.symbol_edges().first, c.symbol_edges().last),
},
(_, Some(c)) => (c.symbol(), c.symbol_edges().first, c.symbol_edges().last),
};
let accounting = accounting(m.flags);
let (base, alpha) = match (accounting, display == Display::Never) {
(false, false) => (Style::Currency, Some(Style::CurrencyAlpha)),
(true, false) => (Style::Accounting, Some(Style::AccountingAlpha)),
(false, true) => (Style::CurrencyNoSymbol, None),
(true, true) => (Style::AccountingNoSymbol, None),
};
let patterns = Patterns::of(catalog);
let own = cur
.and_then(|c| c.pattern())
.filter(|_| base == Style::Currency);
let mut pattern = own.or_else(|| patterns.and_then(|p| p.resolve(base)));
let (mut before, mut after) = pattern.map_or((last, false), |p| {
let s = p.signed(localize::shown(&d));
(
last && ends_with_currency(s.prefix),
first && starts_with_currency(s.suffix),
)
});
if own.is_none()
&& (before || after)
&& let Some(a) = alpha.and_then(|a| patterns.and_then(|p| p.get(a)))
{
pattern = Some(a);
(before, after) = (false, false);
}
let sizes = pattern.map_or(sym.grouping(), |p| p.grouping());
let symbol = Symbol {
text,
before,
after,
};
if pattern.is_none() {
out.put("currency", text);
if before {
out.put("literal", "\u{a0}");
}
}
localize::write_number(
&sym,
pattern,
Some(symbol),
seps,
sizes,
&d,
m.number.grouping(),
out,
);
}
#[allow(clippy::too_many_arguments)]
fn write_currency_name(
catalog: &Catalog,
sym: &Symbols<'_>,
seps: Seps<'_>,
cur: Option<Currency<'_>>,
code: &str,
d: &Digits<'_>,
m: &Measure<'_>,
out: &mut Out<'_>,
) {
let cat = category(catalog, d) as u8;
let name = cur.and_then(|c| c.name_for(cat)).unwrap_or(code);
let template = Currencies::of(catalog).and_then(|c| c.name_pattern(cat));
let mut number = |out: &mut Out<'_>| {
localize::write_number(
sym,
None,
None,
seps,
sym.grouping(),
d,
m.number.grouping(),
out,
);
};
if let Some(t) = template {
fill(t, out, &mut number, &mut |out| out.put("currency", name));
} else {
number(out);
out.put("literal", " ");
out.put("currency", name);
}
}
fn fill(
t: Template<'_>,
out: &mut Out<'_>,
arg0: &mut dyn FnMut(&mut Out<'_>),
arg1: &mut dyn FnMut(&mut Out<'_>),
) {
for p in t.parts() {
match p {
TemplatePart::Arg0 => arg0(out),
TemplatePart::Arg1 => arg1(out),
TemplatePart::Text(s) => {
let (lead, text, trail) = split_blank(s);
out.put("literal", lead);
out.put("unit", text);
out.put("literal", trail);
}
}
}
}
const BLANKS: [&str; 4] = [" ", "\u{a0}", "\u{2009}", "\u{202f}"];
fn split_blank(text: &str) -> (&str, &str, &str) {
let mut rest = text;
while let Some(r) = BLANKS.iter().find_map(|b| rest.strip_prefix(b)) {
rest = r;
}
let (head, mut mid) = text
.split_at_checked(text.len() - rest.len())
.unwrap_or(("", text));
while let Some(r) = BLANKS.iter().find_map(|b| mid.strip_suffix(b)) {
mid = r;
}
let tail = rest.get(mid.len()..).unwrap_or("");
(head, mid, tail)
}
#[derive(Clone, Copy, Default, Debug)]
pub struct UnitFunction;
pub static UNIT: UnitFunction = UnitFunction;
impl Function for UnitFunction {
fn resolve<'a>(
&self,
cx: &FnContext<'_>,
operand: Option<&Value<'a>>,
options: &Options<'_, 'a>,
errs: &mut dyn ErrorSink,
) -> Option<Value<'a>> {
resolve_measure(true, cx, operand, *options, errs)
}
fn formattable(&self, cx: &FnContext<'_>, value: &Value<'_>) -> Result<(), FormatError> {
match value {
Value::Measure(m) => match m.unit {
MeasureUnit::Unit(_) if INTL_NUMBERS => intl::unit_formattable(cx, m),
MeasureUnit::Unit(id) => {
let units = Units::of(cx.catalog());
if Symbols::of(cx.catalog()).is_none()
|| units.is_some_and(|u| resolve_unit(&u, id).is_some())
{
Ok(())
} else {
Err(FormatError::UnsupportedOperation)
}
}
MeasureUnit::Currency(_) => Err(FormatError::MessageFunctionError),
},
_ => Err(FormatError::MessageFunctionError),
}
}
fn format(&self, cx: &FnContext<'_>, value: &Value<'_>, out: &mut dyn Sink) {
if let Value::Measure(m) = value {
if INTL_NUMBERS {
return intl::measure(cx, m, NumberOut::Text(out));
}
write_unit(cx.catalog(), m, &mut Out::Text(out));
}
}
fn format_parts(&self, cx: &FnContext<'_>, value: &Value<'_>, out: &mut dyn SubPartSink) {
if let Value::Measure(m) = value {
if INTL_NUMBERS {
return intl::measure(cx, m, NumberOut::Parts(out));
}
write_unit(cx.catalog(), m, &mut Out::Parts(out));
}
}
fn part_kind(&self) -> &'static str {
"number"
}
fn dir(&self, _cx: &FnContext<'_>, _value: &Value<'_>) -> Dir {
Dir::Ltr
}
}
enum Resolved<'u> {
One(Unit<'u>),
Per(Unit<'u>, Unit<'u>),
}
fn resolve_unit<'u>(units: &Units<'u>, id: &str) -> Option<Resolved<'u>> {
if let Some(u) = units.get(id) {
return Some(Resolved::One(u));
}
let b = id.as_bytes();
let mut at = 0;
while at + 5 <= b.len() {
if b.get(at..at + 5) == Some(b"-per-") {
let (x, y) = (id.get(..at)?, id.get(at + 5..)?);
if let (Some(x), Some(y)) = (units.get(x), units.get(y)) {
return Some(Resolved::Per(x, y));
}
}
at += 1;
}
None
}
fn width_for(u: &Unit<'_>, want: Width) -> Width {
if u.has_patterns(want) {
return want;
}
[Width::Short, Width::Long, Width::Narrow]
.into_iter()
.find(|w| u.has_patterns(*w))
.unwrap_or(want)
}
fn write_unit(catalog: &Catalog, measure: &Measure<'_>, out: &mut Out<'_>) {
let Some(digits) = measure.number.digits() else {
return;
};
let MeasureUnit::Unit(id) = measure.unit else {
return;
};
let (Some(sym), Some(units)) = (Symbols::of(catalog), Units::of(catalog)) else {
localize::neutral(&digits, out);
out.put("literal", " ");
out.put("unit", id);
return;
};
let (unit, per) = match resolve_unit(&units, id) {
Some(Resolved::One(u)) => (u, None),
Some(Resolved::Per(x, y)) => (x, Some(y)),
None => return,
};
let seps = Seps::of(&sym);
let grouping = measure.number.grouping();
let cat = category(catalog, &digits) as u8;
let mut number = |out: &mut Out<'_>| {
localize::write_number(
&sym,
None,
None,
seps,
sym.grouping(),
&digits,
grouping,
out,
);
};
let w = width_for(&unit, width_of(measure.flags));
let mut numerator = |out: &mut Out<'_>| match unit.pattern(w, cat) {
Some(t) => fill(t, out, &mut number, &mut |_| {}),
None => number(out),
};
let Some(den) = per else {
return numerator(out);
};
if let Some(p) = den.per_unit_pattern(w) {
return fill(p, out, &mut numerator, &mut |_| {});
}
let Some(p) = units.per_pattern(w) else {
return numerator(out);
};
let denominator = den.pattern(w, Category::One as u8);
fill(p, out, &mut numerator, &mut |out| {
for part in denominator.iter().flat_map(Template::parts) {
if let TemplatePart::Text(s) = part {
out.put("unit", split_blank(s).1);
}
}
});
}