1use mf2_catalog::Catalog;
26use mf2_catalog::currency::{Currencies, Currency};
27use mf2_catalog::format::locale_key;
28use mf2_catalog::number::{Patterns, Style, Symbols, Template, TemplatePart};
29use mf2_catalog::unit::{Unit, Units, Width};
30use mf2_runtime::{
31 Category, Digits, Dir, ErrorSink, FnContext, FormatError, Function, INTL_NUMBERS, Measure,
32 MeasureUnit, Number, NumberOut, NumberSpec, Options, Sink, SubPartSink, Value, plural_category,
33};
34
35use crate::intl;
36use crate::localize::{self, Out, Seps, Symbol, ends_with_currency, starts_with_currency};
37
38const DISPLAY_SHIFT: u32 = 0;
46const SIGN_SHIFT: u32 = 3;
47const DIGITS_SHIFT: u32 = 5;
48const WIDTH_SHIFT: u32 = 12;
49
50#[derive(Clone, Copy, PartialEq, Eq, Debug)]
52pub(crate) enum Display {
53 NarrowSymbol = 1,
54 Symbol = 2,
55 Name = 3,
56 Code = 4,
57 Never = 5,
58}
59
60const DISPLAYS: [(&str, u32); 5] = [
62 ("narrowSymbol", Display::NarrowSymbol as u32),
63 ("symbol", Display::Symbol as u32),
64 ("name", Display::Name as u32),
65 ("code", Display::Code as u32),
66 ("never", Display::Never as u32),
67];
68const SIGNS: [(&str, u32); 2] = [("standard", 1), ("accounting", 2)];
69const WIDTHS: [(&str, u32); 3] = [("short", 1), ("narrow", 2), ("long", 3)];
70
71fn field(flags: u32, shift: u32, bits: u32) -> u32 {
72 (flags >> shift) & ((1 << bits) - 1)
73}
74
75fn set(flags: &mut u32, shift: u32, bits: u32, v: u32) {
76 let mask = ((1 << bits) - 1) << shift;
77 *flags = (*flags & !mask) | ((v << shift) & mask);
78}
79
80pub(crate) fn display_of(flags: u32) -> Display {
81 match field(flags, DISPLAY_SHIFT, 3) {
82 1 => Display::NarrowSymbol,
83 3 => Display::Name,
84 4 => Display::Code,
85 5 => Display::Never,
86 _ => Display::Symbol,
87 }
88}
89
90pub(crate) fn accounting(flags: u32) -> bool {
91 field(flags, SIGN_SHIFT, 2) == 2
92}
93
94pub(crate) fn own_digits(flags: u32) -> bool {
96 field(flags, DIGITS_SHIFT, 7) < 2
97}
98
99pub(crate) fn width_of(flags: u32) -> Width {
100 match field(flags, WIDTH_SHIFT, 2) {
101 2 => Width::Narrow,
102 3 => Width::Long,
103 _ => Width::Short,
104 }
105}
106
107fn str_of<'a>(v: &Value<'a>) -> Option<&'a str> {
112 match v {
113 Value::Str(s) | Value::Decimal(s) => Some(s),
114 Value::Custom(c) => c.as_str(),
115 _ => None,
116 }
117}
118
119fn currency_code(s: &str) -> Option<[u8; 3]> {
122 match *s.as_bytes() {
123 [a, b, c]
124 if a.is_ascii_alphabetic() && b.is_ascii_alphabetic() && c.is_ascii_alphabetic() =>
125 {
126 Some([
127 a.to_ascii_uppercase(),
128 b.to_ascii_uppercase(),
129 c.to_ascii_uppercase(),
130 ])
131 }
132 _ => None,
133 }
134}
135
136fn is_unit_id(s: &str) -> bool {
140 let b = s.as_bytes();
141 if b.is_empty() || b.len() > 64 || b.first() == Some(&b'-') || b.last() == Some(&b'-') {
142 return false;
143 }
144 let mut prev_dash = false;
145 for &c in b {
146 let dash = c == b'-';
147 if !(dash || c.is_ascii_lowercase() || c.is_ascii_digit()) || (dash && prev_dash) {
148 return false;
149 }
150 prev_dash = dash;
151 }
152 true
153}
154
155fn category(catalog: &Catalog, d: &Digits<'_>) -> Category {
157 let rules = catalog
158 .locale_entry(locale_key::PLURAL_CARDINAL)
159 .unwrap_or(&[]);
160 plural_category(rules, &d.operands())
161}
162
163fn operand_measure<'a>(operand: Option<&Value<'a>>) -> Option<(MeasureUnit<'a>, u32)> {
167 match operand? {
168 Value::Measure(m) => Some((m.unit, m.flags)),
169 Value::Custom(c) => c.as_measure().map(|m| (m.unit, m.flags)),
170 _ => None,
171 }
172}
173
174fn keyword(
177 text: Option<&str>,
178 table: &[(&str, u32)],
179 shift: u32,
180 bits: u32,
181 flags: &mut u32,
182 errs: &mut dyn ErrorSink,
183) {
184 match text.and_then(|t| table.iter().find(|(k, _)| *k == t)) {
185 Some(&(_, code)) => set(flags, shift, bits, code),
186 None => errs.error(FormatError::BadOption),
187 }
188}
189
190fn resolve_measure<'a>(
194 unit: bool,
195 cx: &FnContext<'_>,
196 operand: Option<&Value<'a>>,
197 options: Options<'_, 'a>,
198 errs: &mut dyn ErrorSink,
199) -> Option<Value<'a>> {
200 let inherited =
201 operand_measure(operand).filter(|(u, _)| matches!(u, MeasureUnit::Unit(_)) == unit);
202 let mut what = inherited.map(|(u, _)| u);
203 let mut flags = inherited.map_or(0, |(_, f)| f);
204 for (name, v) in options.iter() {
205 let text = str_of(v.value);
206 match (unit, name) {
207 (false, "currency") => match text.and_then(currency_code) {
208 Some(_) if inherited.is_some() => errs.error(FormatError::BadOption),
210 Some(c) => what = Some(MeasureUnit::Currency(c)),
211 None => errs.error(FormatError::BadOption),
212 },
213 (true, "unit") => match text.filter(|u| is_unit_id(u)).map(MeasureUnit::Unit) {
214 Some(u) if inherited.is_some_and(|(i, _)| i != u) => {
216 errs.error(FormatError::BadOption);
217 }
218 Some(u) => what = Some(u),
219 None => errs.error(FormatError::BadOption),
220 },
221 (false, "currencyDisplay") => {
222 keyword(text, &DISPLAYS, DISPLAY_SHIFT, 3, &mut flags, errs);
223 }
224 (false, "currencySign") => keyword(text, &SIGNS, SIGN_SHIFT, 2, &mut flags, errs),
225 (true, "unitDisplay") => keyword(text, &WIDTHS, WIDTH_SHIFT, 2, &mut flags, errs),
226 (false, "fractionDigits") => {
227 if text == Some("auto") {
228 set(&mut flags, DIGITS_SHIFT, 7, 1);
229 } else if let Some(n) = v.value.digit_size() {
230 set(&mut flags, DIGITS_SHIFT, 7, 2 + u32::from(n));
231 } else {
232 errs.error(FormatError::BadOption);
233 }
234 }
235 (true, "usage") => errs.error(FormatError::UnsupportedOperation),
238 _ => {}
239 }
240 }
241 let Some(what) = what else {
242 errs.error(FormatError::BadOperand);
244 return None;
245 };
246 let spec = match what {
247 MeasureUnit::Currency(code) => NumberSpec::currency(match field(flags, DIGITS_SHIFT, 7) {
248 n if n >= 2 => u8::try_from(n - 2).unwrap_or(2),
249 _ if INTL_NUMBERS => 2,
253 _ => auto_digits(cx.catalog(), code),
254 }),
255 MeasureUnit::Unit(_) => NumberSpec::UNIT,
256 };
257 let number = Number::resolve(spec, cx, operand, &options, errs)?;
258 Some(Value::Measure(Measure::new(number, what, flags)))
259}
260
261#[derive(Clone, Copy, Default, Debug)]
265pub struct CurrencyFunction;
266
267pub static CURRENCY: CurrencyFunction = CurrencyFunction;
269
270impl Function for CurrencyFunction {
271 fn resolve<'a>(
272 &self,
273 cx: &FnContext<'_>,
274 operand: Option<&Value<'a>>,
275 options: &Options<'_, 'a>,
276 errs: &mut dyn ErrorSink,
277 ) -> Option<Value<'a>> {
278 resolve_measure(false, cx, operand, *options, errs)
279 }
280
281 fn formattable(&self, _cx: &FnContext<'_>, value: &Value<'_>) -> Result<(), FormatError> {
282 match value {
283 Value::Measure(m) if matches!(m.unit, MeasureUnit::Currency(_)) => Ok(()),
284 _ => Err(FormatError::MessageFunctionError),
285 }
286 }
287
288 fn format(&self, cx: &FnContext<'_>, value: &Value<'_>, out: &mut dyn Sink) {
289 if let Value::Measure(m) = value {
290 if INTL_NUMBERS {
291 return intl::measure(cx, m, NumberOut::Text(out));
292 }
293 write_currency(cx.catalog(), m, &mut Out::Text(out));
294 }
295 }
296
297 fn format_parts(&self, cx: &FnContext<'_>, value: &Value<'_>, out: &mut dyn SubPartSink) {
298 if let Value::Measure(m) = value {
299 if INTL_NUMBERS {
300 return intl::measure(cx, m, NumberOut::Parts(out));
301 }
302 write_currency(cx.catalog(), m, &mut Out::Parts(out));
303 }
304 }
305
306 fn part_kind(&self) -> &'static str {
307 "number"
308 }
309
310 fn dir(&self, _cx: &FnContext<'_>, _value: &Value<'_>) -> Dir {
311 Dir::Ltr
312 }
313}
314
315fn auto_digits(catalog: &Catalog, code: [u8; 3]) -> u8 {
318 Currencies::of(catalog).map_or(2, |c| {
319 c.get(code)
320 .map_or(c.default_digits(), |x| x.fraction_digits())
321 })
322}
323
324fn write_currency(catalog: &Catalog, m: &Measure<'_>, out: &mut Out<'_>) {
325 let Some(d) = m.number.digits() else {
326 return;
327 };
328 let code = m.unit.as_str();
329 let Some(sym) = Symbols::of(catalog) else {
330 localize::neutral(&d, out);
332 out.put("literal", "\u{a0}");
333 out.put("currency", code);
334 return;
335 };
336 let bytes = match m.unit {
337 MeasureUnit::Currency(c) => c,
338 MeasureUnit::Unit(_) => return,
339 };
340 let data = Currencies::of(catalog);
341 let cur: Option<Currency<'_>> = data.and_then(|c| c.get(bytes));
342 let display = display_of(m.flags);
343 let mut seps = Seps::of(&sym);
344 if let Some(c) = cur {
345 if let Some(dec) = c.decimal() {
346 seps.decimal = dec;
347 }
348 if let Some(g) = c.group() {
349 seps.group = g;
350 }
351 }
352 if display == Display::Name {
353 return write_currency_name(catalog, &sym, seps, cur, code, &d, m, out);
354 }
355 let (text, first, last) = match (display, cur) {
357 (Display::Never, _) => ("", false, false),
358 (Display::Code, _) | (_, None) => (code, true, true),
359 (Display::NarrowSymbol, Some(c)) => match c.narrow_symbol() {
360 Some(n) => (n, c.narrow_edges().first, c.narrow_edges().last),
361 None => (c.symbol(), c.symbol_edges().first, c.symbol_edges().last),
362 },
363 (_, Some(c)) => (c.symbol(), c.symbol_edges().first, c.symbol_edges().last),
364 };
365 let accounting = accounting(m.flags);
366 let (base, alpha) = match (accounting, display == Display::Never) {
367 (false, false) => (Style::Currency, Some(Style::CurrencyAlpha)),
368 (true, false) => (Style::Accounting, Some(Style::AccountingAlpha)),
369 (false, true) => (Style::CurrencyNoSymbol, None),
370 (true, true) => (Style::AccountingNoSymbol, None),
371 };
372 let patterns = Patterns::of(catalog);
373 let own = cur
375 .and_then(|c| c.pattern())
376 .filter(|_| base == Style::Currency);
377 let mut pattern = own.or_else(|| patterns.and_then(|p| p.resolve(base)));
378 let (mut before, mut after) = pattern.map_or((last, false), |p| {
383 let s = p.signed(localize::shown(&d));
384 (
385 last && ends_with_currency(s.prefix),
386 first && starts_with_currency(s.suffix),
387 )
388 });
389 if own.is_none()
390 && (before || after)
391 && let Some(a) = alpha.and_then(|a| patterns.and_then(|p| p.get(a)))
392 {
393 pattern = Some(a);
394 (before, after) = (false, false);
395 }
396 let sizes = pattern.map_or(sym.grouping(), |p| p.grouping());
397 let symbol = Symbol {
398 text,
399 before,
400 after,
401 };
402 if pattern.is_none() {
403 out.put("currency", text);
404 if before {
405 out.put("literal", "\u{a0}");
406 }
407 }
408 localize::write_number(
409 &sym,
410 pattern,
411 Some(symbol),
412 seps,
413 sizes,
414 &d,
415 m.number.grouping(),
416 out,
417 );
418}
419
420#[allow(clippy::too_many_arguments)]
424fn write_currency_name(
425 catalog: &Catalog,
426 sym: &Symbols<'_>,
427 seps: Seps<'_>,
428 cur: Option<Currency<'_>>,
429 code: &str,
430 d: &Digits<'_>,
431 m: &Measure<'_>,
432 out: &mut Out<'_>,
433) {
434 let cat = category(catalog, d) as u8;
435 let name = cur.and_then(|c| c.name_for(cat)).unwrap_or(code);
436 let template = Currencies::of(catalog).and_then(|c| c.name_pattern(cat));
437 let mut number = |out: &mut Out<'_>| {
438 localize::write_number(
439 sym,
440 None,
441 None,
442 seps,
443 sym.grouping(),
444 d,
445 m.number.grouping(),
446 out,
447 );
448 };
449 if let Some(t) = template {
450 fill(t, out, &mut number, &mut |out| out.put("currency", name));
451 } else {
452 number(out);
453 out.put("literal", " ");
454 out.put("currency", name);
455 }
456}
457
458fn fill(
462 t: Template<'_>,
463 out: &mut Out<'_>,
464 arg0: &mut dyn FnMut(&mut Out<'_>),
465 arg1: &mut dyn FnMut(&mut Out<'_>),
466) {
467 for p in t.parts() {
468 match p {
469 TemplatePart::Arg0 => arg0(out),
470 TemplatePart::Arg1 => arg1(out),
471 TemplatePart::Text(s) => {
472 let (lead, text, trail) = split_blank(s);
473 out.put("literal", lead);
474 out.put("unit", text);
475 out.put("literal", trail);
476 }
477 }
478 }
479}
480
481const BLANKS: [&str; 4] = [" ", "\u{a0}", "\u{2009}", "\u{202f}"];
484
485fn split_blank(text: &str) -> (&str, &str, &str) {
487 let mut rest = text;
488 while let Some(r) = BLANKS.iter().find_map(|b| rest.strip_prefix(b)) {
489 rest = r;
490 }
491 let (head, mut mid) = text
492 .split_at_checked(text.len() - rest.len())
493 .unwrap_or(("", text));
494 while let Some(r) = BLANKS.iter().find_map(|b| mid.strip_suffix(b)) {
495 mid = r;
496 }
497 let tail = rest.get(mid.len()..).unwrap_or("");
498 (head, mid, tail)
499}
500
501#[derive(Clone, Copy, Default, Debug)]
505pub struct UnitFunction;
506
507pub static UNIT: UnitFunction = UnitFunction;
509
510impl Function for UnitFunction {
511 fn resolve<'a>(
512 &self,
513 cx: &FnContext<'_>,
514 operand: Option<&Value<'a>>,
515 options: &Options<'_, 'a>,
516 errs: &mut dyn ErrorSink,
517 ) -> Option<Value<'a>> {
518 resolve_measure(true, cx, operand, *options, errs)
519 }
520
521 fn formattable(&self, cx: &FnContext<'_>, value: &Value<'_>) -> Result<(), FormatError> {
522 match value {
523 Value::Measure(m) => match m.unit {
524 MeasureUnit::Unit(_) if INTL_NUMBERS => intl::unit_formattable(cx, m),
525 MeasureUnit::Unit(id) => {
526 let units = Units::of(cx.catalog());
527 if Symbols::of(cx.catalog()).is_none()
528 || units.is_some_and(|u| resolve_unit(&u, id).is_some())
529 {
530 Ok(())
531 } else {
532 Err(FormatError::UnsupportedOperation)
533 }
534 }
535 MeasureUnit::Currency(_) => Err(FormatError::MessageFunctionError),
536 },
537 _ => Err(FormatError::MessageFunctionError),
538 }
539 }
540
541 fn format(&self, cx: &FnContext<'_>, value: &Value<'_>, out: &mut dyn Sink) {
542 if let Value::Measure(m) = value {
543 if INTL_NUMBERS {
544 return intl::measure(cx, m, NumberOut::Text(out));
545 }
546 write_unit(cx.catalog(), m, &mut Out::Text(out));
547 }
548 }
549
550 fn format_parts(&self, cx: &FnContext<'_>, value: &Value<'_>, out: &mut dyn SubPartSink) {
551 if let Value::Measure(m) = value {
552 if INTL_NUMBERS {
553 return intl::measure(cx, m, NumberOut::Parts(out));
554 }
555 write_unit(cx.catalog(), m, &mut Out::Parts(out));
556 }
557 }
558
559 fn part_kind(&self) -> &'static str {
560 "number"
561 }
562
563 fn dir(&self, _cx: &FnContext<'_>, _value: &Value<'_>) -> Dir {
564 Dir::Ltr
565 }
566}
567
568enum Resolved<'u> {
570 One(Unit<'u>),
571 Per(Unit<'u>, Unit<'u>),
572}
573
574fn resolve_unit<'u>(units: &Units<'u>, id: &str) -> Option<Resolved<'u>> {
577 if let Some(u) = units.get(id) {
578 return Some(Resolved::One(u));
579 }
580 let b = id.as_bytes();
581 let mut at = 0;
582 while at + 5 <= b.len() {
583 if b.get(at..at + 5) == Some(b"-per-") {
584 let (x, y) = (id.get(..at)?, id.get(at + 5..)?);
585 if let (Some(x), Some(y)) = (units.get(x), units.get(y)) {
586 return Some(Resolved::Per(x, y));
587 }
588 }
589 at += 1;
590 }
591 None
592}
593
594fn width_for(u: &Unit<'_>, want: Width) -> Width {
597 if u.has_patterns(want) {
598 return want;
599 }
600 [Width::Short, Width::Long, Width::Narrow]
601 .into_iter()
602 .find(|w| u.has_patterns(*w))
603 .unwrap_or(want)
604}
605
606fn write_unit(catalog: &Catalog, measure: &Measure<'_>, out: &mut Out<'_>) {
607 let Some(digits) = measure.number.digits() else {
608 return;
609 };
610 let MeasureUnit::Unit(id) = measure.unit else {
611 return;
612 };
613 let (Some(sym), Some(units)) = (Symbols::of(catalog), Units::of(catalog)) else {
614 localize::neutral(&digits, out);
616 out.put("literal", " ");
617 out.put("unit", id);
618 return;
619 };
620 let (unit, per) = match resolve_unit(&units, id) {
622 Some(Resolved::One(u)) => (u, None),
623 Some(Resolved::Per(x, y)) => (x, Some(y)),
624 None => return,
625 };
626 let seps = Seps::of(&sym);
627 let grouping = measure.number.grouping();
628 let cat = category(catalog, &digits) as u8;
629 let mut number = |out: &mut Out<'_>| {
630 localize::write_number(
631 &sym,
632 None,
633 None,
634 seps,
635 sym.grouping(),
636 &digits,
637 grouping,
638 out,
639 );
640 };
641 let w = width_for(&unit, width_of(measure.flags));
642 let mut numerator = |out: &mut Out<'_>| match unit.pattern(w, cat) {
643 Some(t) => fill(t, out, &mut number, &mut |_| {}),
644 None => number(out),
645 };
646 let Some(den) = per else {
647 return numerator(out);
648 };
649 if let Some(p) = den.per_unit_pattern(w) {
650 return fill(p, out, &mut numerator, &mut |_| {});
651 }
652 let Some(p) = units.per_pattern(w) else {
655 return numerator(out);
656 };
657 let denominator = den.pattern(w, Category::One as u8);
658 fill(p, out, &mut numerator, &mut |out| {
659 for part in denominator.iter().flat_map(Template::parts) {
660 if let TemplatePart::Text(s) = part {
661 out.put("unit", split_blank(s).1);
662 }
663 }
664 });
665}