1use alloc::string::String;
23use core::sync::atomic::{AtomicU8, Ordering};
24
25use mf2_runtime::{
26 Category, CurrencyDisplay, DateTimeRequest, DigitOptions, Grouping, Host, NumberFormatter,
27 NumberOut, NumberRequest, NumberStyle, RoundingMode, RoundingPriority, SignDisplay, Sink,
28 UnitDisplay,
29};
30use wasm_bindgen::prelude::wasm_bindgen;
31
32use crate::HOST;
33
34#[derive(Clone, Copy)]
42pub struct IntlNumbers(pub &'static dyn Host);
43
44pub static NUMBERS_HOST: IntlNumbers = IntlNumbers(&HOST);
47
48impl Host for IntlNumbers {
49 fn nfc<'a>(&self, s: &'a str, buf: &'a mut String) -> &'a str {
50 self.0.nfc(s, buf)
51 }
52
53 fn f64_to_text<'b>(&self, x: f64, buf: &'b mut [u8; 32]) -> Option<&'b str> {
54 self.0.f64_to_text(x, buf)
55 }
56
57 fn zone_offset(&self, zone: &str, epoch_ms: i64) -> Option<i32> {
58 self.0.zone_offset(zone, epoch_ms)
59 }
60
61 fn format_date_time(
62 &self,
63 locale: &str,
64 request: &DateTimeRequest<'_>,
65 out: &mut dyn Sink,
66 ) -> bool {
67 self.0.format_date_time(locale, request, out)
68 }
69
70 fn numbers(&self) -> Option<&dyn NumberFormatter> {
71 available().then_some(&INTL)
72 }
73}
74
75struct Intl;
78
79static INTL: Intl = Intl;
80
81impl NumberFormatter for Intl {
82 fn format(&self, locale: &str, request: &NumberRequest<'_>, out: NumberOut<'_>) -> bool {
83 format(locale, request, out)
84 }
85
86 fn plural(&self, locale: &str, request: &NumberRequest<'_>) -> Option<Category> {
87 plural(locale, request)
88 }
89}
90
91#[wasm_bindgen(inline_js = r#"
104const N=new Map,P=new Map,K={zero:0,one:1,two:2,few:3,many:4,other:5};
105function g(c,y,k){let f=c.get(y);if(f===void 0){const i=y.indexOf('\x01'),l=y.slice(0,i),o=JSON.parse(y.slice(i+1)),h=o.currencyDisplay=='never';h&&(o.currencyDisplay='code');try{f={i:k?new Intl.PluralRules(l,o):new Intl.NumberFormat(l,o),h}}catch{f=null}c.size>255&&c.delete(c.keys().next().value);c.set(y,f)}return f}
106export function mf2_nf(y,v,p){const f=g(N,y,0);if(f===null)return;if(!p&&!f.h)return f.i.format(v);const q=f.i.formatToParts(v);let r='';for(let i=0;i<q.length;i++){const t=q[i];if(f.h&&(t.type=='currency'||t.type=='literal'&&!t.value.trim()&&(q[i-1]?.type=='currency'||q[i+1]?.type=='currency')))continue;r+=p?(r?'\x1e':'')+t.type+'\x1f'+t.value:t.value}return r}
107export function mf2_pr(y,v){const f=g(P,y,1);return f===null?6:K[f.i.select(+v)]}
108export function mf2_nf_v3(){try{const f=(o,v)=>new Intl.NumberFormat('en',o).format(v);return f({maximumFractionDigits:1,maximumSignificantDigits:1,roundingPriority:'morePrecision'},1.25)=='1.3'&&f({maximumFractionDigits:0,roundingMode:'halfEven'},2.5)=='2'&&f({minimumFractionDigits:2,maximumFractionDigits:2,roundingIncrement:5},'1.23')=='1.25'&&f({minimumFractionDigits:2,trailingZeroDisplay:'stripIfInteger'},1)=='1'&&f({useGrouping:'min2'},1e3)=='1000'&&f({signDisplay:'negative'},-0)=='0'&&f({useGrouping:!1,maximumFractionDigits:20},'9007199254740993.5')=='9007199254740993.5'&&new Intl.PluralRules('en',{maximumFractionDigits:0,roundingMode:'floor'}).select(1.9)=='one'}catch{return!1}}
109"#)]
110extern "C" {
111 fn mf2_nf(key: &str, value: &str, parts: bool) -> Option<alloc::string::String>;
112 fn mf2_pr(key: &str, value: &str) -> u32;
113 fn mf2_nf_v3() -> bool;
114}
115
116static V3: AtomicU8 = AtomicU8::new(0);
118
119fn available() -> bool {
121 match V3.load(Ordering::Relaxed) {
122 1 => true,
123 2 => false,
124 _ => {
125 let yes = mf2_nf_v3();
126 V3.store(if yes { 1 } else { 2 }, Ordering::Relaxed);
127 yes
128 }
129 }
130}
131
132struct Json {
137 buf: [u8; 640],
138 len: usize,
139 ok: bool,
140 fields: u8,
141}
142
143impl Json {
144 fn new(locale: &str) -> Json {
145 let mut j = Json {
146 buf: [0; 640],
147 len: 0,
148 ok: true,
149 fields: 0,
150 };
151 j.raw(locale);
152 j.raw("\u{1}");
153 j
154 }
155
156 fn raw(&mut self, s: &str) {
157 for &b in s.as_bytes() {
158 let Some(slot) = self.buf.get_mut(self.len) else {
159 self.ok = false;
160 return;
161 };
162 *slot = b;
163 self.len += 1;
164 }
165 }
166
167 fn key(&mut self, k: &str) {
168 self.raw(if self.fields == 0 { "{\"" } else { ",\"" });
169 self.fields = self.fields.saturating_add(1);
170 self.raw(k);
171 self.raw("\":");
172 }
173
174 fn text(&mut self, k: &str, v: &str) {
177 self.key(k);
178 self.raw("\"");
179 self.raw(v);
180 self.raw("\"");
181 }
182
183 fn number(&mut self, k: &str, v: u16) {
184 self.key(k);
185 let mut d = [0u8; 5];
186 let mut at = d.len();
187 let mut v = v;
188 loop {
189 at -= 1;
190 if let Some(slot) = d.get_mut(at) {
191 #[allow(clippy::cast_possible_truncation)]
193 {
194 *slot = b'0' + (v % 10) as u8;
195 }
196 }
197 v /= 10;
198 if v == 0 || at == 0 {
199 break;
200 }
201 }
202 self.raw(core::str::from_utf8(d.get(at..).unwrap_or(&[])).unwrap_or("0"));
203 }
204
205 fn finish(&mut self) -> Option<&str> {
206 self.raw(if self.fields == 0 { "{}" } else { "}" });
207 if !self.ok {
208 return None;
209 }
210 core::str::from_utf8(self.buf.get(..self.len)?).ok()
211 }
212}
213
214fn mode_name(m: RoundingMode) -> &'static str {
215 match m {
216 RoundingMode::Ceil => "ceil",
217 RoundingMode::Floor => "floor",
218 RoundingMode::Expand => "expand",
219 RoundingMode::Trunc => "trunc",
220 RoundingMode::HalfCeil => "halfCeil",
221 RoundingMode::HalfFloor => "halfFloor",
222 RoundingMode::HalfTrunc => "halfTrunc",
223 RoundingMode::HalfEven => "halfEven",
224 _ => "halfExpand",
227 }
228}
229
230fn digits(d: &DigitOptions, defaults: Option<(u8, u8)>, j: &mut Json) {
234 if d.minimum_integer != 1 {
235 j.number("minimumIntegerDigits", d.minimum_integer.into());
236 }
237 if let Some((mn, mx)) = d.fraction
238 && defaults != Some((mn, mx))
239 {
240 j.number("minimumFractionDigits", mn.into());
241 j.number("maximumFractionDigits", mx.into());
242 }
243 if let Some((mn, mx)) = d.significant {
244 j.number("minimumSignificantDigits", mn.into());
245 j.number("maximumSignificantDigits", mx.into());
246 }
247 match d.priority {
248 RoundingPriority::MorePrecision => j.text("roundingPriority", "morePrecision"),
249 RoundingPriority::LessPrecision => j.text("roundingPriority", "lessPrecision"),
250 _ => {}
251 }
252 if d.increment != 1 {
253 j.number("roundingIncrement", d.increment);
254 }
255 if d.mode != RoundingMode::HalfExpand {
256 j.text("roundingMode", mode_name(d.mode));
257 }
258 if d.strip_if_integer {
259 j.text("trailingZeroDisplay", "stripIfInteger");
260 }
261}
262
263fn format_options(r: &NumberRequest<'_>, j: &mut Json) {
265 let defaults = match r.style {
266 NumberStyle::Percent => {
267 j.text("style", "percent");
268 Some((0, 0))
269 }
270 NumberStyle::Currency {
271 code,
272 display,
273 accounting,
274 ..
275 } => {
276 j.text("style", "currency");
277 j.text("currency", code);
278 let d = match display {
279 CurrencyDisplay::NarrowSymbol => Some("narrowSymbol"),
280 CurrencyDisplay::Name => Some("name"),
281 CurrencyDisplay::Code => Some("code"),
282 CurrencyDisplay::Never => Some("never"),
283 _ => None,
285 };
286 if let Some(d) = d {
287 j.text("currencyDisplay", d);
288 }
289 if accounting {
290 j.text("currencySign", "accounting");
291 }
292 None
293 }
294 NumberStyle::Unit { unit, display } => {
295 j.text("style", "unit");
296 j.text("unit", unit);
297 match display {
298 UnitDisplay::Narrow => j.text("unitDisplay", "narrow"),
299 UnitDisplay::Long => j.text("unitDisplay", "long"),
300 _ => {}
301 }
302 Some((0, 3))
303 }
304 _ => Some((0, 3)),
305 };
306 if r.neutral {
307 j.text("numberingSystem", "latn");
308 j.key("useGrouping");
309 j.raw("false");
310 } else {
311 match r.grouping {
312 Grouping::Always => j.text("useGrouping", "always"),
313 Grouping::Min2 => j.text("useGrouping", "min2"),
314 Grouping::Never => {
315 j.key("useGrouping");
316 j.raw("false");
317 }
318 _ => {}
319 }
320 }
321 let sign = match r.sign {
322 SignDisplay::Always => Some("always"),
323 SignDisplay::ExceptZero => Some("exceptZero"),
324 SignDisplay::Negative => Some("negative"),
325 SignDisplay::Never => Some("never"),
326 _ => None,
328 };
329 if let Some(s) = sign {
330 j.text("signDisplay", s);
331 }
332 digits(&r.digits, defaults, j);
333}
334
335fn format(locale: &str, r: &NumberRequest<'_>, out: NumberOut<'_>) -> bool {
338 let mut j = Json::new(if r.neutral { "en" } else { locale });
339 format_options(r, &mut j);
340 let Some(key) = j.finish() else {
341 return false;
342 };
343 let parts = matches!(out, NumberOut::Parts(_));
344 let Some(text) = mf2_nf(key, r.value, parts) else {
345 return false;
346 };
347 match out {
348 NumberOut::Text(out) => out.push_str(&text),
349 NumberOut::Parts(out) => {
350 let mut rest = text.as_str();
353 while !rest.is_empty() {
354 let end = rest.bytes().position(|b| b == 0x1e).unwrap_or(rest.len());
355 let record = rest.get(..end).unwrap_or("");
356 if let Some(i) = record.bytes().position(|b| b == 0x1f) {
357 out.sub_part(
358 record.get(..i).unwrap_or(""),
359 record.get(i + 1..).unwrap_or(""),
360 );
361 }
362 rest = rest.get(end + 1..).unwrap_or("");
363 }
364 }
365 }
366 true
367}
368
369fn plural(locale: &str, r: &NumberRequest<'_>) -> Option<Category> {
373 let mut j = Json::new(locale);
374 if r.ordinal {
375 j.text("type", "ordinal");
376 }
377 digits(&r.digits, Some((0, 3)), &mut j);
378 let key = j.finish()?;
379 let c = mf2_pr(key, r.value);
380 #[allow(clippy::cast_possible_truncation)]
382 (c < 6).then(|| Category::from_code(c as u8))
383}