use mf2_catalog::Catalog;
use mf2_catalog::number::{
Affix, AffixPart, Grouping as Sizes, Pattern, Patterns, SignShown, Style, Symbols,
};
use mf2_runtime::{Digits, Grouping, Sign, Sink, SubPartSink};
pub(crate) enum Out<'o> {
Text(&'o mut dyn Sink),
Parts(&'o mut dyn SubPartSink),
}
impl Out<'_> {
pub(crate) fn put(&mut self, kind: &str, text: &str) {
if text.is_empty() {
return;
}
match self {
Out::Text(s) => s.push_str(text),
Out::Parts(p) => p.sub_part(kind, text),
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum Layout {
Decimal,
Percent,
}
fn groups(n: u16, sizes: Sizes, grouping: Grouping, locale_min: u8) -> bool {
let min = match grouping {
Grouping::Never => return false,
Grouping::Always => 1,
Grouping::Min2 => 2,
_ => locale_min.max(1),
};
sizes.primary != 0 && u32::from(n) >= u32::from(sizes.primary) + u32::from(min)
}
struct Run {
buf: [u8; 64],
len: usize,
}
impl Run {
const fn new() -> Run {
Run {
buf: [0; 64],
len: 0,
}
}
fn push(&mut self, s: &str, kind: &str, out: &mut Out<'_>) {
if self.len + s.len() > self.buf.len() {
self.flush(kind, out);
}
if s.len() > self.buf.len() {
out.put(kind, s);
return;
}
let at = self.len;
for (b, &c) in self.buf.iter_mut().skip(at).zip(s.as_bytes()) {
*b = c;
}
self.len += s.len();
}
fn flush(&mut self, kind: &str, out: &mut Out<'_>) {
let text = self
.buf
.get(..self.len)
.and_then(|b| core::str::from_utf8(b).ok())
.unwrap_or("");
out.put(kind, text);
self.len = 0;
}
}
pub(crate) fn write(
catalog: &Catalog,
d: &Digits<'_>,
grouping: Option<Grouping>,
layout: Layout,
out: &mut Out<'_>,
) {
let Some(sym) = Symbols::of(catalog) else {
neutral(d, out);
if layout == Layout::Percent {
out.put("percentSign", "%");
}
return;
};
let pattern = match layout {
Layout::Decimal => None,
Layout::Percent => Patterns::of(catalog).and_then(|p| p.resolve(Style::Percent)),
};
let seps = Seps::of(&sym);
let sizes = pattern.map_or(sym.grouping(), |p| p.grouping());
write_number(&sym, pattern, None, seps, sizes, d, grouping, out);
if pattern.is_none() && layout == Layout::Percent {
out.put("percentSign", sym.percent());
}
}
pub(crate) fn neutral(d: &Digits<'_>, out: &mut Out<'_>) {
match out {
Out::Text(s) => d.write_neutral(*s),
Out::Parts(p) => d.neutral_parts(*p),
}
}
#[derive(Clone, Copy)]
pub(crate) struct Seps<'c> {
pub(crate) decimal: &'c str,
pub(crate) group: &'c str,
}
impl<'c> Seps<'c> {
pub(crate) fn of(sym: &Symbols<'c>) -> Seps<'c> {
Seps {
decimal: sym.decimal(),
group: sym.group(),
}
}
}
#[derive(Clone, Copy)]
pub(crate) struct Symbol<'c> {
pub(crate) text: &'c str,
pub(crate) before: bool,
pub(crate) after: bool,
}
pub(crate) fn shown(d: &Digits<'_>) -> SignShown {
match d.sign() {
Sign::None => SignShown::None,
Sign::Minus => SignShown::Minus,
Sign::Plus => SignShown::Plus,
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn write_number(
sym: &Symbols<'_>,
pattern: Option<Pattern<'_>>,
currency: Option<Symbol<'_>>,
seps: Seps<'_>,
sizes: Sizes,
d: &Digits<'_>,
grouping: Option<Grouping>,
out: &mut Out<'_>,
) {
let shown = shown(d);
let sign = |out: &mut Out<'_>| match shown {
SignShown::None => {}
SignShown::Minus => out.put("minusSign", sym.minus()),
SignShown::Plus => out.put("plusSign", sym.plus()),
};
let affix = |a: Affix<'_>, out: &mut Out<'_>| {
for part in a.parts() {
match part {
AffixPart::Text(t) => out.put("literal", t),
AffixPart::Sign => sign(out),
AffixPart::Percent => out.put("percentSign", sym.percent()),
AffixPart::Currency => {
if let Some(c) = currency {
out.put("currency", c.text);
}
}
}
}
};
let Some(p) = pattern else {
sign(out);
digits(sym, seps, sizes, d, grouping, out);
return;
};
let s = p.signed(shown);
if s.sign_first {
sign(out);
}
affix(s.prefix, out);
if currency.is_some_and(|c| c.before) {
out.put("literal", "\u{a0}");
}
digits(sym, seps, sizes, d, grouping, out);
if currency.is_some_and(|c| c.after) {
out.put("literal", "\u{a0}");
}
affix(s.suffix, out);
}
pub(crate) fn ends_with_currency(a: Affix<'_>) -> bool {
a.parts().last() == Some(AffixPart::Currency)
}
pub(crate) fn starts_with_currency(a: Affix<'_>) -> bool {
a.parts().next() == Some(AffixPart::Currency)
}
fn digits(
sym: &Symbols<'_>,
seps: Seps<'_>,
sizes: Sizes,
d: &Digits<'_>,
grouping: Option<Grouping>,
out: &mut Out<'_>,
) {
let native = sym.digits();
let n = d.integer_count();
let grouped = groups(
n,
sizes,
grouping.unwrap_or(Grouping::Auto),
sym.minimum_grouping_digits(),
);
let mut run = Run::new();
let mut m = n;
while m > 0 {
m -= 1;
let mag = i16::try_from(m).unwrap_or(i16::MAX);
run.push(native.digit(d.digit(mag)), "integer", out);
if grouped && m > 0 && sizes.separator_after(u32::from(m)) {
run.flush("integer", out);
out.put("group", seps.group);
}
}
run.flush("integer", out);
let f = d.fraction_count();
if f > 0 {
out.put("decimal", seps.decimal);
for k in 1..=f {
let mag = i16::try_from(k).map_or(i16::MIN, |k| -k);
run.push(native.digit(d.digit(mag)), "fraction", out);
}
run.flush("fraction", out);
}
}
#[cfg(test)]
mod tests {
use super::{Grouping, Sizes, groups};
#[test]
fn grouping_thresholds() {
let three = Sizes {
primary: 3,
secondary: 3,
};
assert!(!groups(3, three, Grouping::Auto, 1));
assert!(groups(4, three, Grouping::Auto, 1));
assert!(!groups(4, three, Grouping::Auto, 2));
assert!(groups(5, three, Grouping::Auto, 2));
assert!(groups(4, three, Grouping::Always, 2));
assert!(!groups(4, three, Grouping::Min2, 1));
assert!(groups(5, three, Grouping::Min2, 1));
assert!(!groups(9, three, Grouping::Never, 1));
assert!(!groups(9, Sizes::NONE, Grouping::Always, 1));
}
}