1mod decimal;
16#[cfg(not(all(feature = "intl", target_arch = "wasm32", target_os = "unknown")))]
17mod display;
18#[cfg(all(feature = "intl", target_arch = "wasm32", target_os = "unknown"))]
19mod intl;
20mod measure;
21mod options;
22mod request;
23
24#[cfg(not(all(feature = "intl", target_arch = "wasm32", target_os = "unknown")))]
25use display as backend;
26#[cfg(all(feature = "intl", target_arch = "wasm32", target_os = "unknown"))]
27use intl as backend;
28
29use crate::error::FormatError;
30use crate::function::{FnContext, Options};
31use crate::host::Host;
32use crate::plural::{self, Category, OperandsBuilder};
33use crate::sink::{ErrorSink, Sink, SubPartSink};
34use crate::value::Value;
35
36use decimal::{Decimal, ParseError, split_literal};
37use options::{
38 CUR, FracDefaults, INT, Kind, NUM, NumOpts, OPTIONS, PCT, Select, UNIT, apply, digit_plan,
39 select_named,
40};
41
42pub(crate) use options::digit_size;
43
44pub use decimal::RoundingMode;
45pub use measure::{Measure, MeasureUnit};
46pub use options::{Grouping, RoundingPriority, SignDisplay};
47pub use request::{
48 CurrencyDisplay, DigitOptions, NumberOut, NumberRequest, NumberStyle, UnitDisplay,
49};
50
51#[derive(Clone)]
55pub struct Number {
56 pub(crate) value: Decimal,
59 resolved: Option<Resolved>,
61}
62
63#[derive(Clone)]
65struct Resolved {
66 opts: NumOpts,
67 frac: FracDefaults,
68 scale: i16,
70 selectable: bool,
72 shown: backend::Shown,
75}
76
77impl core::fmt::Debug for Number {
81 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
82 f.debug_struct("Number")
83 .field("negative", &self.value.negative())
84 .field("resolved", &self.resolved.is_some())
85 .finish_non_exhaustive()
86 }
87}
88
89#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
91pub enum Sign {
92 None,
94 Minus,
96 Plus,
98}
99
100#[derive(Clone, Copy)]
104pub struct Digits<'n> {
105 dec: &'n Decimal,
106 lo: i16,
107 hi: i16,
108 sign: Sign,
109}
110
111impl Digits<'_> {
112 pub fn sign(&self) -> Sign {
114 self.sign
115 }
116
117 pub fn integer_count(&self) -> u16 {
120 u16::try_from(i32::from(self.hi) + 1).unwrap_or(1)
121 }
122
123 pub fn fraction_count(&self) -> u16 {
125 u16::try_from(-i32::from(self.lo)).unwrap_or(0)
126 }
127
128 pub fn digit(&self, magnitude: i16) -> u8 {
132 if magnitude < self.lo || magnitude > self.hi {
133 return 0;
134 }
135 self.dec.digit_at(magnitude)
136 }
137
138 pub fn is_zero(&self) -> bool {
140 self.dec.is_zero()
141 }
142
143 pub fn write_neutral(&self, out: &mut dyn Sink) {
146 match self.sign {
147 Sign::Minus => out.push_str("-"),
148 Sign::Plus => out.push_str("+"),
149 Sign::None => {}
150 }
151 self.dec.write_digits(self.hi, self.lo, out);
152 }
153
154 pub fn neutral_parts(&self, out: &mut dyn SubPartSink) {
157 write_parts(self.dec, self.sign, self.hi, self.lo, out);
158 }
159
160 pub fn operands(&self) -> plural::Operands {
164 let mut b = OperandsBuilder::default();
165 let mut m = self.hi;
166 while m >= self.lo {
167 b.digit(self.dec.digit_at(m), m >= 0);
168 m -= 1;
169 }
170 b.finish()
171 }
172}
173
174impl Number {
175 pub fn parse(number_literal: &str) -> Option<Number> {
178 Decimal::parse(number_literal.as_bytes())
179 .ok()
180 .map(Number::bare_of)
181 }
182
183 pub fn from_i64(n: i64) -> Number {
185 let mut d = Decimal::from_u64(n.unsigned_abs());
186 d.set_negative(n < 0);
187 Number::bare_of(d)
188 }
189
190 pub fn from_f64(x: f64, host: &dyn Host) -> Option<Number> {
193 if !x.is_finite() {
194 return None;
195 }
196 if x == 0.0 {
197 let mut d = Decimal::from_u64(0);
198 d.set_negative(x.is_sign_negative());
199 return Some(Number::bare_of(d));
200 }
201 let mut buf = [0u8; 32];
202 let text = host.f64_to_text(x, &mut buf)?;
203 Number::parse(text)
204 }
205
206 pub fn is_negative(&self) -> bool {
208 self.value.negative()
209 }
210
211 pub fn is_integer(&self) -> bool {
213 self.value.is_integer()
214 }
215
216 pub fn to_i64(&self) -> Option<i64> {
218 self.value.to_i64()
219 }
220
221 pub fn write_plain(&self, out: &mut dyn Sink) {
224 self.value.write_plain(out);
225 }
226
227 pub fn resolve(
233 spec: NumberSpec,
234 cx: &FnContext<'_>,
235 operand: Option<&Value<'_>>,
236 options: &Options<'_, '_>,
237 errs: &mut dyn ErrorSink,
238 ) -> Option<Number> {
239 resolve(spec, cx, operand, *options, errs)
240 }
241
242 pub fn digits(&self) -> Option<Digits<'_>> {
247 self.resolved.as_ref().and_then(backend::digits)
248 }
249
250 pub fn exact_digits(&self) -> Digits<'_> {
253 let d = &self.value;
254 Digits {
255 dec: d,
256 lo: d.low().min(0),
257 hi: d.high().max(0),
258 sign: if d.negative() {
259 Sign::Minus
260 } else {
261 Sign::None
262 },
263 }
264 }
265
266 pub fn grouping(&self) -> Option<Grouping> {
269 self.resolved.as_ref().and_then(|r| r.opts.use_grouping)
270 }
271
272 pub fn is_selectable(&self) -> bool {
275 self.selectable()
276 }
277
278 pub fn matches(&self, cx: &FnContext<'_>, key: &str, errs: &mut dyn ErrorSink) -> bool {
282 matches(self, cx, key, errs)
283 }
284
285 pub fn better_than(key1: &str, key2: &str) -> bool {
288 better_than(key1, key2)
289 }
290
291 fn bare_of(value: Decimal) -> Number {
292 Number {
293 value,
294 resolved: None,
295 }
296 }
297
298 pub(crate) fn bare(&self) -> Number {
300 Number::bare_of(self.value.clone())
301 }
302
303 pub(crate) fn plain_parts(&self, out: &mut dyn SubPartSink) {
306 self.exact_digits().neutral_parts(out);
307 }
308
309 pub(crate) fn selectable(&self) -> bool {
311 self.resolved.as_ref().is_some_and(|r| r.selectable)
312 }
313
314 pub(crate) fn write_display(&self, cx: &FnContext<'_>, out: &mut dyn Sink) {
317 backend::write_neutral(self, cx, out);
318 }
319
320 pub(crate) fn display_parts(&self, cx: &FnContext<'_>, out: &mut dyn SubPartSink) {
322 backend::neutral_parts(self, cx, out);
323 }
324}
325
326fn write_parts(d: &Decimal, sign: Sign, hi: i16, lo: i16, out: &mut dyn SubPartSink) {
329 match sign {
330 Sign::Minus => out.sub_part("minusSign", "-"),
331 Sign::Plus => out.sub_part("plusSign", "+"),
332 Sign::None => {}
333 }
334 digits_part(d, hi, 0, "integer", out);
335 if lo < 0 {
336 out.sub_part("decimal", ".");
337 digits_part(d, -1, lo, "fraction", out);
338 }
339}
340
341fn digits_part(d: &Decimal, hi: i16, lo: i16, kind: &str, out: &mut dyn SubPartSink) {
342 let mut buf = [0u8; 64];
343 let mut n = 0;
344 let mut m = hi;
345 while m >= lo {
346 if let Some(b) = buf.get_mut(n) {
347 *b = b'0' + d.digit_at(m);
348 n += 1;
349 }
350 if n == buf.len() {
351 out.sub_part(kind, core::str::from_utf8(&buf).unwrap_or(""));
352 n = 0;
353 }
354 m -= 1;
355 }
356 if n > 0 {
357 out.sub_part(
358 kind,
359 core::str::from_utf8(buf.get(..n).unwrap_or(&[])).unwrap_or(""),
360 );
361 }
362}
363
364pub(crate) fn literal_error(s: &str) -> Option<FormatError> {
368 match Decimal::parse(s.as_bytes()) {
369 Ok(_) => None,
370 Err(ParseError::Syntax) => Some(FormatError::BadOperand),
371 Err(ParseError::Limit) => Some(FormatError::UnsupportedOperation),
372 }
373}
374
375fn is_number_literal(key: &str) -> bool {
378 !matches!(split_literal(key.as_bytes()), Err(ParseError::Syntax))
379}
380
381#[derive(Clone, Copy, PartialEq, Eq, Debug)]
388pub struct NumberSpec {
389 bit: u8,
391 frac: Option<FracDefaults>,
393 integer: bool,
395 selectable: bool,
397 scale: i16,
399}
400
401impl NumberSpec {
402 pub const NUMBER: NumberSpec = NumberSpec {
404 bit: NUM,
405 frac: Some(FracDefaults { min: 0, max: 3 }),
406 integer: false,
407 selectable: true,
408 scale: 0,
409 };
410
411 pub const INTEGER: NumberSpec = NumberSpec {
413 bit: INT,
414 frac: Some(FracDefaults { min: 0, max: 0 }),
415 integer: true,
416 selectable: true,
417 scale: 0,
418 };
419
420 pub const OFFSET: NumberSpec = NumberSpec {
422 bit: 0,
423 frac: None,
424 integer: false,
425 selectable: true,
426 scale: 0,
427 };
428
429 pub const PERCENT: NumberSpec = NumberSpec {
432 bit: PCT,
433 frac: Some(FracDefaults { min: 0, max: 0 }),
434 integer: false,
435 selectable: true,
436 scale: 2,
437 };
438
439 pub const UNIT: NumberSpec = NumberSpec {
441 bit: UNIT,
442 frac: Some(FracDefaults { min: 0, max: 3 }),
443 integer: false,
444 selectable: false,
445 scale: 0,
446 };
447
448 pub const fn currency(fraction_digits: u8) -> NumberSpec {
452 NumberSpec {
453 bit: CUR,
454 frac: Some(FracDefaults {
455 min: fraction_digits,
456 max: fraction_digits,
457 }),
458 integer: false,
459 selectable: false,
460 scale: 0,
461 }
462 }
463}
464
465pub(crate) fn resolve(
467 spec: NumberSpec,
468 cx: &FnContext<'_>,
469 operand: Option<&Value<'_>>,
470 options: Options<'_, '_>,
471 errs: &mut dyn ErrorSink,
472) -> Option<Number> {
473 let is_offset = spec.bit == 0;
474 let Some(operand) = operand else {
475 errs.error(FormatError::BadOperand);
476 return None;
477 };
478 let resolved_operand = match operand {
479 Value::Number(n) | Value::Measure(Measure { number: n, .. }) => {
480 n.resolved.as_ref().map(|r| (&n.value, r))
481 }
482 _ => None,
483 };
484 let (mut value, mut o, inherited, mut selectable) = match resolved_operand {
485 Some((value, r)) => {
486 let mut o = r.opts;
487 let mut selectable = r.selectable;
488 if o.select.is_some() {
489 if spec.bit & (NUM | INT) != 0 {
493 errs.error(FormatError::BadOption);
494 selectable = false;
495 }
496 if !is_offset {
497 o.select = None;
498 }
499 }
500 if spec.bit == INT {
501 o.min_frac = None;
502 o.max_frac = None;
503 o.min_sig = None;
504 }
505 if spec.bit == PCT {
506 o.min_int = None;
507 o.rounding_increment = None;
508 }
509 if spec.bit == CUR {
510 o.min_frac = None;
512 o.max_frac = None;
513 }
514 (value.clone(), o, Some((r.frac, r.scale)), selectable)
515 }
516 None => match numeric_operand(operand, cx.host()) {
517 Ok(d) => (d, NumOpts::default(), None, true),
518 Err(e) => {
519 errs.error(e);
520 return None;
521 }
522 },
523 };
524 let mut offset: Option<(bool, u8)> = None;
525 let mut offset_bad = false;
526 for (name, v) in options.iter() {
527 if is_offset {
528 let sub = match name {
529 "add" => false,
530 "subtract" => true,
531 _ => continue, };
533 match (digit_size(v.value), offset) {
534 (Some(d), None) => offset = Some((sub, d)),
535 _ => offset_bad = true,
536 }
537 continue;
538 }
539 let Some(kind) = OPTIONS
540 .iter()
541 .find(|(n, mask, _)| *n == name && mask & spec.bit != 0)
542 .map(|&(_, _, k)| k)
543 else {
544 continue; };
546 if kind == Kind::Select {
547 if v.literal {
549 match v.value.as_str().and_then(select_named) {
550 Some(x) => o.select = Some(x),
551 None => errs.error(FormatError::BadOption),
552 }
553 } else {
554 errs.error(FormatError::BadOption);
555 selectable = false;
556 }
557 continue;
558 }
559 if !apply(&mut o, kind, v.value) {
560 errs.error(FormatError::BadOption);
561 }
562 }
563 if spec.integer {
564 let mode = o.rounding_mode.unwrap_or(RoundingMode::HalfExpand);
566 backend::round_to_integer(&mut value, mode, cx);
567 }
568 let (frac, scale) = match (spec.frac, inherited) {
569 (Some(f), _) => (f, spec.scale),
570 (None, Some(inherited)) => inherited,
571 (None, None) => (FracDefaults { min: 0, max: 3 }, 0),
572 };
573 if is_offset {
574 let Some((sub, d)) = offset.filter(|_| !offset_bad) else {
575 errs.error(FormatError::BadOption);
576 return None;
577 };
578 let mut delta = Decimal::from_u64(u64::from(d));
579 delta.set_negative(sub);
580 let Some(sum) = value.add(&delta) else {
581 errs.error(FormatError::UnsupportedOperation);
583 return None;
584 };
585 value = sum;
586 }
587 backend::limit_digit_sizes(&mut o, errs);
588 let plan = digit_plan(&o, frac, errs);
589 let shown = backend::shown(
590 &value,
591 scale,
592 &plan,
593 o.sign_display.unwrap_or(SignDisplay::Auto),
594 );
595 backend::check_host(cx, errs);
596 Some(Number {
597 value,
598 resolved: Some(Resolved {
599 opts: o,
600 frac,
601 scale,
602 selectable: selectable && spec.selectable,
603 shown,
604 }),
605 })
606}
607
608fn numeric_operand(v: &Value<'_>, host: &dyn Host) -> Result<Decimal, FormatError> {
610 match v {
611 Value::Str(s) | Value::Decimal(s) => Decimal::parse(s.as_bytes()).map_err(|e| match e {
612 ParseError::Syntax => FormatError::BadOperand,
613 ParseError::Limit => FormatError::UnsupportedOperation,
614 }),
615 Value::Int(n) => {
616 let mut d = Decimal::from_u64(n.unsigned_abs());
617 d.set_negative(*n < 0);
618 Ok(d)
619 }
620 _ => v
621 .to_number(host)
622 .map(|n| n.value)
623 .ok_or(FormatError::BadOperand),
624 }
625}
626
627struct Cmp<'k> {
631 key: &'k [u8],
632 pos: usize,
633 ok: bool,
634}
635
636impl Sink for Cmp<'_> {
637 fn push_str(&mut self, s: &str) {
638 let end = self.pos + s.len();
639 self.ok &= self.key.get(self.pos..end) == Some(s.as_bytes());
640 self.pos = end;
641 }
642}
643
644impl Cmp<'_> {
645 fn done(&self) -> bool {
646 self.ok && self.pos == self.key.len()
647 }
648}
649
650pub(crate) fn equals(key: &str, write: impl FnOnce(&mut dyn Sink)) -> bool {
652 let mut c = Cmp {
653 key: key.as_bytes(),
654 pos: 0,
655 ok: true,
656 };
657 write(&mut c);
658 c.done()
659}
660
661fn exact_matches(n: &Number, r: &Resolved, cx: &FnContext<'_>, key: &str) -> bool {
666 let o = &r.opts;
667 let plain =
668 o.min_frac.is_none() && o.min_int.is_none() && o.min_sig.is_none() && o.max_sig.is_none();
669 let mut value = n.value.clone();
670 if r.scale != 0 {
671 value.shift(r.scale);
672 }
673 if plain && value.is_integer() {
674 if value.is_zero() {
675 value.set_negative(false);
676 }
677 equals(key, |s| value.write_plain(s))
678 } else {
679 equals(key, |s| backend::write_selected(n, r, cx, s))
680 }
681}
682
683pub(crate) fn matches(n: &Number, cx: &FnContext<'_>, key: &str, errs: &mut dyn ErrorSink) -> bool {
685 let Some(r) = &n.resolved else {
686 return false;
687 };
688 if is_number_literal(key) {
689 return exact_matches(n, r, cx, key);
690 }
691 let Some(keyword) = Category::from_keyword(key) else {
692 errs.error(FormatError::BadVariantKey);
693 return false;
694 };
695 if r.opts.select == Some(Select::Exact) {
696 return false;
697 }
698 backend::category(n, r, cx) == keyword
699}
700
701pub(crate) fn better_than(key1: &str, key2: &str) -> bool {
704 is_number_literal(key1) && !is_number_literal(key2)
705}
706
707impl core::fmt::Debug for Digits<'_> {
710 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
711 f.debug_struct("Digits")
712 .field("sign", &self.sign)
713 .field("magnitudes", &(self.lo..=self.hi))
714 .finish_non_exhaustive()
715 }
716}
717
718#[cfg(test)]
719mod tests;