use alloc::string::String;
use alloc::vec::Vec;
pub use crate::cldr::{NumberSpec, Pattern};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NumberPartType {
Integer,
Group,
Decimal,
Fraction,
Literal,
MinusSign,
PlusSign,
PercentSign,
Currency,
Unit,
Compact,
ExponentSeparator,
ExponentMinusSign,
ExponentInteger,
Nan,
Infinity,
}
impl NumberPartType {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
NumberPartType::Integer => "integer",
NumberPartType::Group => "group",
NumberPartType::Decimal => "decimal",
NumberPartType::Fraction => "fraction",
NumberPartType::Literal => "literal",
NumberPartType::MinusSign => "minusSign",
NumberPartType::PlusSign => "plusSign",
NumberPartType::PercentSign => "percentSign",
NumberPartType::Currency => "currency",
NumberPartType::Unit => "unit",
NumberPartType::Compact => "compact",
NumberPartType::ExponentSeparator => "exponentSeparator",
NumberPartType::ExponentMinusSign => "exponentMinusSign",
NumberPartType::ExponentInteger => "exponentInteger",
NumberPartType::Nan => "nan",
NumberPartType::Infinity => "infinity",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NumberPart {
pub kind: NumberPartType,
pub value: String,
}
impl NumberPart {
fn new(kind: NumberPartType, value: impl Into<String>) -> NumberPart {
NumberPart {
kind,
value: value.into(),
}
}
}
fn join_parts(parts: &[NumberPart]) -> String {
let mut out = String::new();
for p in parts {
out.push_str(&p.value);
}
out
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum NumberStyle {
#[default]
Decimal,
Percent,
Currency,
Unit,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Notation {
#[default]
Standard,
Scientific,
Engineering,
Compact,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CompactDisplay {
#[default]
Short,
Long,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CurrencyDisplay {
#[default]
Symbol,
NarrowSymbol,
Code,
Name,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum UnitDisplay {
#[default]
Short,
Narrow,
Long,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum UseGrouping {
#[default]
Auto,
Always,
Min2,
Never,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SignDisplay {
#[default]
Auto,
Always,
ExceptZero,
Negative,
Never,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RoundingMode {
Ceil,
Floor,
Expand,
Trunc,
HalfCeil,
HalfFloor,
HalfExpand,
HalfTrunc,
#[default]
HalfEven,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct NumberFormatOptions {
pub style: NumberStyle,
pub notation: Notation,
pub compact_display: CompactDisplay,
pub use_grouping: UseGrouping,
pub sign_display: SignDisplay,
pub rounding_mode: RoundingMode,
pub minimum_integer_digits: u8,
pub minimum_fraction_digits: Option<u8>,
pub maximum_fraction_digits: Option<u8>,
pub minimum_significant_digits: Option<u8>,
pub maximum_significant_digits: Option<u8>,
pub currency: Option<&'static str>,
pub currency_display: CurrencyDisplay,
pub unit: Option<&'static str>,
pub unit_display: UnitDisplay,
pub numbering_system: Option<&'static str>,
}
fn spec(lang: &str) -> NumberSpec {
use crate::cldr::number_spec;
let norm: String = lang
.chars()
.map(|c| {
if c == '_' {
'-'
} else {
c.to_ascii_lowercase()
}
})
.collect();
let mut end = norm.len();
loop {
if let Some(s) = number_spec(&norm[..end]) {
return s;
}
match norm[..end].rfind('-') {
Some(i) => end = i,
None => return number_spec("en").expect("root spec present"),
}
}
}
#[must_use]
pub fn format_decimal(lang: &str, value: f64) -> String {
let s = spec(lang);
format_with(&s.dec, value, s.decimal, s.group, s.minus)
}
#[must_use]
pub fn format_percent(lang: &str, value: f64) -> String {
let s = spec(lang);
format_with(&s.pct, value * 100.0, s.decimal, s.group, s.minus)
}
#[must_use]
pub fn format_scientific(lang: &str, value: f64, sig_after: usize) -> String {
if value == 0.0 {
return String::from("0");
}
let s = spec(lang);
let neg = value < 0.0;
let mut m = if neg { -value } else { value };
let mut exp = 0i32;
while m >= 10.0 {
m /= 10.0;
exp += 1;
}
while m < 1.0 {
m *= 10.0;
exp -= 1;
}
let mantissa = alloc::format!("{:.*}", sig_after, m);
let (int_part, frac_full) = mantissa.split_once('.').unwrap_or((&mantissa, ""));
let frac = frac_full.trim_end_matches('0');
let mut out = String::new();
if neg {
out.push_str(s.minus);
}
out.push_str(int_part);
if !frac.is_empty() {
out.push_str(s.decimal);
out.push_str(frac);
}
out.push('E');
if exp < 0 {
out.push_str(s.minus);
}
out.push_str(&alloc::format!("{}", exp.unsigned_abs()));
out
}
#[must_use]
pub fn format_ordinal(lang: &str, n: i64) -> String {
use crate::plural::{PluralOperands, ordinal_category};
let cat = ordinal_category(lang, &PluralOperands::from_int(n)) as usize;
let norm: String = lang
.chars()
.map(|c| {
if c == '_' {
'-'
} else {
c.to_ascii_lowercase()
}
})
.collect();
let mut end = norm.len();
let suffix = loop {
if let Some(s) = crate::cldr::ordinal_suffix(&norm[..end], cat) {
break s;
}
match norm[..end].rfind('-') {
Some(i) => end = i,
None => break crate::cldr::ordinal_suffix("en", cat).unwrap_or(""),
}
};
let mut out = format_decimal(lang, n as f64);
out.push_str(suffix);
out
}
#[must_use]
pub fn to_numbering_system(s: &str, system: &str) -> String {
let Some(glyphs) = crate::cldr::numbering_digits(system) else {
return String::from(s);
};
let table: alloc::vec::Vec<char> = glyphs.chars().collect();
if table.len() != 10 {
return String::from(s);
}
s.chars()
.map(|c| {
if c.is_ascii_digit() {
table[(c as u8 - b'0') as usize]
} else {
c
}
})
.collect()
}
#[must_use]
pub fn format_decimal_native(lang: &str, value: f64) -> String {
let formatted = format_decimal(lang, value);
let norm: String = lang
.chars()
.map(|c| {
if c == '_' {
'-'
} else {
c.to_ascii_lowercase()
}
})
.collect();
let mut end = norm.len();
let system = loop {
if let Some(s) = crate::cldr::default_numbering(&norm[..end]) {
break s;
}
match norm[..end].rfind('-') {
Some(i) => end = i,
None => break "latn",
}
};
if system == "latn" {
formatted
} else {
to_numbering_system(&formatted, system)
}
}
#[must_use]
pub fn format_compact(lang: &str, value: f64) -> String {
let abs = if value < 0.0 { -value } else { value };
if !abs.is_finite() || abs < 1000.0 {
return format_decimal(lang, value);
}
let s = spec(lang);
let norm: String = lang
.chars()
.map(|c| {
if c == '_' {
'-'
} else {
c.to_ascii_lowercase()
}
})
.collect();
let mut end = norm.len();
let table = loop {
if let Some(t) = crate::cldr::compact_patterns(&norm[..end]) {
break t;
}
match norm[..end].rfind('-') {
Some(i) => end = i,
None => break crate::cldr::compact_patterns("en").expect("root compact present"),
}
};
let mut exp = 0usize;
let mut t = abs;
while t >= 10.0 && exp < 14 {
t /= 10.0;
exp += 1;
}
let pattern = table[(exp - 3).min(11)];
let zeros = pattern.chars().filter(|&c| c == '0').count();
let has_suffix = pattern
.chars()
.any(|c| c != '0' && c != '\'' && !c.is_whitespace());
if zeros == 0 || !has_suffix {
return format_decimal(lang, value);
}
let mut divisor = 1.0f64; for _ in 0..(exp + 1).saturating_sub(zeros) {
divisor *= 10.0;
}
let mantissa = value / divisor;
let m = alloc::format!("{mantissa:.1}");
let (mi, mf) = m.split_once('.').unwrap_or((&m, ""));
let mf = mf.trim_end_matches('0');
let mut out = String::new();
let mut wrote_num = false;
let mut chars = pattern.chars().peekable();
while let Some(c) = chars.next() {
match c {
'0' => {
while chars.peek() == Some(&'0') {
chars.next();
}
if !wrote_num {
out.push_str(mi);
if !mf.is_empty() {
out.push_str(s.decimal);
out.push_str(mf);
}
wrote_num = true;
}
}
'\'' => {
for q in chars.by_ref() {
if q == '\'' {
break;
}
out.push(q);
}
}
other => out.push(other),
}
}
out
}
#[must_use]
pub fn parse_decimal(lang: &str, input: &str) -> Option<f64> {
parse_decimal_with(&spec(lang), input)
}
fn parse_decimal_with(s: &NumberSpec, input: &str) -> Option<f64> {
let mut out = String::with_capacity(input.len());
let mut rest = input.trim();
if let Some(r) = rest
.strip_prefix(s.minus)
.or_else(|| rest.strip_prefix('-'))
{
out.push('-');
rest = r;
}
let mut seen_point = false;
while !rest.is_empty() {
if let Some(r) = (!s.group.is_empty())
.then(|| rest.strip_prefix(s.group))
.flatten()
{
rest = r;
} else if !seen_point {
if let Some(r) = (!s.decimal.is_empty())
.then(|| rest.strip_prefix(s.decimal))
.flatten()
{
out.push('.');
seen_point = true;
rest = r;
continue;
} else {
let c = rest.chars().next()?;
if !c.is_ascii_digit() {
return None;
}
out.push(c);
rest = &rest[c.len_utf8()..];
}
} else {
let c = rest.chars().next()?;
if !c.is_ascii_digit() {
return None;
}
out.push(c);
rest = &rest[c.len_utf8()..];
}
}
out.parse().ok()
}
#[must_use]
#[cfg(feature = "currency")]
pub fn format_currency(lang: &str, value: f64, code: &str) -> String {
use crate::cldr as cur;
let s = spec(lang);
let norm: String = lang
.chars()
.map(|c| {
if c == '_' {
'-'
} else {
c.to_ascii_lowercase()
}
})
.collect();
let mut pat = cur::currency_pattern("en").expect("root currency pattern");
let mut symbol = code;
let mut end = norm.len();
let (mut got_pat, mut got_sym) = (false, false);
loop {
if !got_pat && let Some(p) = cur::currency_pattern(&norm[..end]) {
pat = p;
got_pat = true;
}
if !got_sym && let Some((sym, _, _)) = cur::currency_forms(&norm[..end], code) {
symbol = sym;
got_sym = true;
}
if got_pat && got_sym {
break;
}
match norm[..end].rfind('-') {
Some(i) => end = i,
None => break,
}
}
if !got_sym && let Some((sym, _, _)) = cur::currency_forms("en", code) {
symbol = sym;
}
let digits = cur::currency_digits(code);
pat.min_frac = digits;
pat.max_frac = digits;
let formatted = format_with(&pat, value, s.decimal, s.group, s.minus);
formatted.replace('\u{a4}', symbol)
}
fn format_with(p: &Pattern, value: f64, decimal: &str, group: &str, minus: &str) -> String {
join_parts(&format_with_parts(p, value, decimal, group, minus))
}
fn format_with_parts(
p: &Pattern,
value: f64,
decimal: &str,
group: &str,
minus: &str,
) -> Vec<NumberPart> {
let neg = value.is_sign_negative() && value != 0.0;
let abs = if value < 0.0 { -value } else { value };
let formatted = alloc::format!("{:.*}", p.max_frac as usize, abs);
let (int_str, frac_full) = match formatted.split_once('.') {
Some((a, b)) => (a, b),
None => (formatted.as_str(), ""),
};
let mut int_owned;
let int_str: &str = if int_str.len() < p.min_int as usize {
int_owned = String::new();
for _ in 0..(p.min_int as usize - int_str.len()) {
int_owned.push('0');
}
int_owned.push_str(int_str);
&int_owned
} else {
int_str
};
let mut frac = frac_full;
while frac.len() > p.min_frac as usize && frac.ends_with('0') {
frac = &frac[..frac.len() - 1];
}
let mut parts = Vec::new();
if neg {
parts.push(NumberPart::new(NumberPartType::MinusSign, minus));
}
if !p.prefix.is_empty() {
parts.push(NumberPart::new(NumberPartType::Literal, p.prefix));
}
parts.extend(group_parts(
int_str,
p.primary_group,
p.secondary_group,
group,
));
if !frac.is_empty() {
parts.push(NumberPart::new(NumberPartType::Decimal, decimal));
parts.push(NumberPart::new(NumberPartType::Fraction, frac));
}
if !p.suffix.is_empty() {
parts.push(NumberPart::new(NumberPartType::Literal, p.suffix));
}
parts
}
fn group_parts(digits: &str, primary: u8, secondary: u8, sep: &str) -> Vec<NumberPart> {
if primary == 0 || digits.len() <= primary as usize {
return alloc::vec![NumberPart::new(NumberPartType::Integer, digits)];
}
let chars: Vec<char> = digits.chars().collect();
let n = chars.len();
let mut cuts: Vec<usize> = Vec::new();
let mut pos = n - primary as usize;
cuts.push(pos);
if secondary > 0 {
while pos > secondary as usize {
pos -= secondary as usize;
cuts.push(pos);
}
}
cuts.sort_unstable();
let mut parts = Vec::new();
let mut prev = 0;
for &cut in &cuts {
parts.push(NumberPart::new(
NumberPartType::Integer,
chars[prev..cut].iter().collect::<String>(),
));
parts.push(NumberPart::new(NumberPartType::Group, sep));
prev = cut;
}
parts.push(NumberPart::new(
NumberPartType::Integer,
chars[prev..n].iter().collect::<String>(),
));
parts
}
fn should_round_up(digits: &[u8], cut: usize, mode: RoundingMode, negative: bool) -> bool {
if cut >= digits.len() {
return false; }
let first = digits[cut];
let rest_nonzero = digits[cut + 1..].iter().any(|&d| d != 0);
let any_discarded = first != 0 || rest_nonzero;
let gt_half = first > 5 || (first == 5 && rest_nonzero);
let eq_half = first == 5 && !rest_nonzero;
let kept_last_odd = cut > 0 && digits[cut - 1] % 2 == 1;
use RoundingMode::*;
match mode {
Trunc => false,
Expand => any_discarded,
Ceil => any_discarded && !negative,
Floor => any_discarded && negative,
HalfExpand => gt_half || eq_half,
HalfTrunc => gt_half,
HalfEven => gt_half || (eq_half && kept_last_odd),
HalfCeil => gt_half || (eq_half && !negative),
HalfFloor => gt_half || (eq_half && negative),
}
}
#[allow(clippy::too_many_arguments)]
fn round_digits(
abs: f64,
min_int: usize,
min_frac: usize,
max_frac: usize,
min_sig: Option<usize>,
max_sig: Option<usize>,
mode: RoundingMode,
negative: bool,
) -> (String, String) {
let work = max_frac.max(min_frac).saturating_add(2).clamp(40, 320);
let s = alloc::format!("{abs:.work$}");
let (ip, fp) = s.split_once('.').unwrap_or((s.as_str(), ""));
let mut digits: Vec<u8> = ip.bytes().chain(fp.bytes()).map(|b| b - b'0').collect();
let mut point = ip.len();
let cut = if let Some(ms) = max_sig {
match digits.iter().position(|&d| d != 0) {
Some(first_nz) => (first_nz + ms).min(digits.len()),
None => point, }
} else {
(point + max_frac).min(digits.len())
};
let up = should_round_up(&digits, cut, mode, negative);
for d in digits.iter_mut().skip(cut).take(point.saturating_sub(cut)) {
*d = 0;
}
let keep = cut.max(point);
digits.truncate(keep);
if up {
let mut i = cut;
loop {
if i == 0 {
digits.insert(0, 1);
point += 1;
break;
}
i -= 1;
if digits[i] == 9 {
digits[i] = 0;
} else {
digits[i] += 1;
break;
}
}
}
let mut int_digits: String = digits[..point]
.iter()
.map(|&d| (b'0' + d) as char)
.collect();
let mut frac_digits: String = digits[point..]
.iter()
.map(|&d| (b'0' + d) as char)
.collect();
if max_sig.is_some() {
let min_s = min_sig.unwrap_or(1);
let combined: Vec<u8> = int_digits
.bytes()
.chain(frac_digits.bytes())
.map(|b| b - b'0')
.collect();
match combined.iter().position(|&d| d != 0) {
None => {
for _ in 0..min_s.saturating_sub(1) {
frac_digits.push('0');
}
}
Some(first_nz) => {
let mut sig = combined.len() - first_nz;
while sig < min_s {
frac_digits.push('0');
sig += 1;
}
while sig > min_s && frac_digits.ends_with('0') {
frac_digits.pop();
sig -= 1;
}
}
}
} else {
while frac_digits.len() > min_frac && frac_digits.ends_with('0') {
frac_digits.pop();
}
}
while int_digits.len() < min_int {
int_digits.insert(0, '0');
}
(int_digits, frac_digits)
}
fn sign_part(
negative: bool,
is_zero: bool,
opts: &NumberFormatOptions,
s: &NumberSpec,
) -> Option<NumberPart> {
let (show, plus) = match opts.sign_display {
SignDisplay::Auto | SignDisplay::Negative => (negative, false),
SignDisplay::Always => (true, !negative),
SignDisplay::ExceptZero => (!is_zero, !negative),
SignDisplay::Never => (false, false),
};
if !show {
return None;
}
if plus {
Some(NumberPart::new(NumberPartType::PlusSign, s.plus))
} else {
Some(NumberPart::new(NumberPartType::MinusSign, s.minus))
}
}
fn effective_grouping(opts: &NumberFormatOptions, pattern: &Pattern, int_len: usize) -> (u8, u8) {
match opts.use_grouping {
UseGrouping::Never => (0, 0),
UseGrouping::Min2 => {
if int_len > pattern.primary_group as usize + 1 {
(pattern.primary_group, pattern.secondary_group)
} else {
(0, 0)
}
}
UseGrouping::Auto | UseGrouping::Always => (pattern.primary_group, pattern.secondary_group),
}
}
fn map_digits(value: &str, opts: &NumberFormatOptions) -> String {
match opts.numbering_system {
Some(sys) if sys != "latn" => to_numbering_system(value, sys),
_ => String::from(value),
}
}
#[allow(clippy::too_many_arguments)]
fn core_parts(
int_digits: &str,
frac_digits: &str,
negative: bool,
is_zero: bool,
primary: u8,
secondary: u8,
opts: &NumberFormatOptions,
s: &NumberSpec,
) -> Vec<NumberPart> {
let mut parts = Vec::new();
if let Some(sign) = sign_part(negative, is_zero, opts, s) {
parts.push(sign);
}
for mut p in group_parts(int_digits, primary, secondary, s.group) {
if p.kind == NumberPartType::Integer {
p.value = map_digits(&p.value, opts);
}
parts.push(p);
}
if !frac_digits.is_empty() {
parts.push(NumberPart::new(NumberPartType::Decimal, s.decimal));
parts.push(NumberPart::new(
NumberPartType::Fraction,
map_digits(frac_digits, opts),
));
}
parts
}
fn affix_parts(text: &str, style: NumberStyle, s: &NumberSpec, currency: &str) -> Vec<NumberPart> {
let mut parts = Vec::new();
if text.is_empty() {
return parts;
}
match style {
NumberStyle::Percent => {
for (i, seg) in text.split(s.percent).enumerate() {
if i > 0 {
parts.push(NumberPart::new(NumberPartType::PercentSign, s.percent));
}
if !seg.is_empty() {
parts.push(NumberPart::new(NumberPartType::Literal, seg));
}
}
}
NumberStyle::Currency => {
for (i, seg) in text.split('\u{a4}').enumerate() {
if i > 0 {
parts.push(NumberPart::new(NumberPartType::Currency, currency));
}
if !seg.is_empty() {
parts.push(NumberPart::new(NumberPartType::Literal, seg));
}
}
}
_ => parts.push(NumberPart::new(NumberPartType::Literal, text)),
}
parts
}
#[cfg(feature = "units")]
fn unit_index(id: &str) -> Option<usize> {
Some(match id {
"second" => 0,
"minute" => 1,
"hour" => 2,
"day" => 3,
"week" => 4,
"month" => 5,
"year" => 6,
"millimeter" => 7,
"centimeter" => 8,
"meter" => 9,
"kilometer" => 10,
"inch" => 11,
"foot" => 12,
"mile" => 13,
"gram" => 14,
"kilogram" => 15,
"ounce" => 16,
"pound" => 17,
"byte" => 18,
"kilobyte" => 19,
"megabyte" => 20,
"gigabyte" => 21,
"celsius" => 22,
"fahrenheit" => 23,
"kilometer-per-hour" => 24,
"mile-per-hour" => 25,
"liter" => 26,
"milliliter" => 27,
_ => return None,
})
}
#[cfg(feature = "units")]
fn unit_affix(text: &str) -> Vec<NumberPart> {
let mut parts = Vec::new();
let mut buf = String::new();
let mut in_ws = false;
for ch in text.chars() {
let ws = ch.is_whitespace();
if buf.is_empty() {
in_ws = ws;
} else if ws != in_ws {
let kind = if in_ws {
NumberPartType::Literal
} else {
NumberPartType::Unit
};
parts.push(NumberPart::new(kind, core::mem::take(&mut buf)));
in_ws = ws;
}
buf.push(ch);
}
if !buf.is_empty() {
let kind = if in_ws {
NumberPartType::Literal
} else {
NumberPartType::Unit
};
parts.push(NumberPart::new(kind, buf));
}
parts
}
#[cfg(feature = "units")]
fn unit_wrap(
lang: &str,
value: f64,
core: Vec<NumberPart>,
opts: &NumberFormatOptions,
) -> Vec<NumberPart> {
let Some(uidx) = opts.unit.and_then(unit_index) else {
return core;
};
let width = usize::from(opts.unit_display != UnitDisplay::Long);
let ops = if value % 1.0 == 0.0 && value > -1e15 && value < 1e15 {
crate::plural::PluralOperands::from_int(value as i64)
} else {
crate::plural::PluralOperands::parse(&alloc::format!("{value}"))
.unwrap_or_else(|| crate::plural::PluralOperands::from_int(value as i64))
};
let cat = crate::plural::plural_category(lang, &ops) as usize;
let norm: String = lang
.chars()
.map(|c| {
if c == '_' {
'-'
} else {
c.to_ascii_lowercase()
}
})
.collect();
let mut pattern = "{0}";
let mut end = norm.len();
loop {
if let Some(p) = crate::cldr::unit_pattern(&norm[..end], width, uidx, cat) {
pattern = p;
break;
}
match norm[..end].rfind('-') {
Some(i) => end = i,
None => {
if let Some(p) = crate::cldr::unit_pattern("en", width, uidx, cat) {
pattern = p;
}
break;
}
}
}
let (pre, post) = pattern.split_once("{0}").unwrap_or(("", pattern));
let mut parts = unit_affix(pre);
parts.extend(core);
parts.extend(unit_affix(post));
parts
}
#[cfg(feature = "currency")]
fn currency_unit_wrap(
lang: &str,
value: f64,
opts: &NumberFormatOptions,
s: &NumberSpec,
) -> Vec<NumberPart> {
let code = opts.currency.unwrap_or("XXX");
let norm: String = lang
.chars()
.map(|c| {
if c == '_' {
'-'
} else {
c.to_ascii_lowercase()
}
})
.collect();
let mut forms: Option<(&str, &str, &str)> = None;
let mut unit = "{0} {1}";
let mut end = norm.len();
let mut got_unit = false;
loop {
if forms.is_none() {
forms = crate::cldr::currency_forms(&norm[..end], code);
}
if !got_unit && let Some(u) = crate::cldr::currency_unit_pattern(&norm[..end]) {
unit = u;
got_unit = true;
}
if forms.is_some() && got_unit {
break;
}
match norm[..end].rfind('-') {
Some(i) => end = i,
None => break,
}
}
forms = forms.or_else(|| crate::cldr::currency_forms("en", code));
let (_, _, name) = forms.unwrap_or((code, code, code));
let text = if opts.currency_display == CurrencyDisplay::Name {
name
} else {
code
};
let digits = crate::cldr::currency_digits(code) as usize;
let min_frac = opts.minimum_fraction_digits.map_or(digits, usize::from);
let max_frac = opts
.maximum_fraction_digits
.map_or(digits, usize::from)
.max(min_frac);
let min_int = (opts.minimum_integer_digits.max(1)) as usize;
let neg = value.is_sign_negative() && value != 0.0;
let abs = if value < 0.0 { -value } else { value };
let (int_d, frac_d) = round_digits(
abs,
min_int,
min_frac,
max_frac,
opts.minimum_significant_digits.map(usize::from),
opts.maximum_significant_digits.map(usize::from),
opts.rounding_mode,
neg,
);
let is_zero = int_d.bytes().all(|b| b == b'0') && frac_d.bytes().all(|b| b == b'0');
let (pri, sec) = effective_grouping(opts, &s.dec, int_d.len());
let core = core_parts(&int_d, &frac_d, neg, is_zero, pri, sec, opts, s);
let mut parts = Vec::new();
let mut rest = unit;
while !rest.is_empty() {
if let Some(i) = rest.find('{') {
if i > 0 {
parts.push(NumberPart::new(NumberPartType::Literal, &rest[..i]));
}
if rest[i..].starts_with("{0}") {
parts.extend(core.iter().cloned());
rest = &rest[i + 3..];
} else if rest[i..].starts_with("{1}") {
parts.push(NumberPart::new(NumberPartType::Currency, text));
rest = &rest[i + 3..];
} else {
parts.push(NumberPart::new(NumberPartType::Literal, "{"));
rest = &rest[i + 1..];
}
} else {
parts.push(NumberPart::new(NumberPartType::Literal, rest));
break;
}
}
parts
}
fn resolve_style(
lang: &str,
value: f64,
s: &NumberSpec,
opts: &NumberFormatOptions,
) -> (Pattern, f64, String) {
match opts.style {
NumberStyle::Decimal | NumberStyle::Unit => (s.dec, value, String::new()),
NumberStyle::Percent => (s.pct, value * 100.0, String::new()),
#[cfg(not(feature = "currency"))]
NumberStyle::Currency => (s.dec, value, String::new()),
#[cfg(feature = "currency")]
NumberStyle::Currency => {
let code = opts.currency.unwrap_or("XXX");
let norm: String = lang
.chars()
.map(|c| {
if c == '_' {
'-'
} else {
c.to_ascii_lowercase()
}
})
.collect();
let mut pat = crate::cldr::currency_pattern("en").expect("root currency pattern");
let mut forms: Option<(&str, &str, &str)> = None;
let mut end = norm.len();
let mut got_pat = false;
loop {
if !got_pat && let Some(p) = crate::cldr::currency_pattern(&norm[..end]) {
pat = p;
got_pat = true;
}
if forms.is_none() {
forms = crate::cldr::currency_forms(&norm[..end], code);
}
if got_pat && forms.is_some() {
break;
}
match norm[..end].rfind('-') {
Some(i) => end = i,
None => break,
}
}
if forms.is_none() {
forms = crate::cldr::currency_forms("en", code);
}
let (sym, narrow, name) = forms.unwrap_or((code, code, code));
let shown = match opts.currency_display {
CurrencyDisplay::Symbol => String::from(sym),
CurrencyDisplay::NarrowSymbol => String::from(narrow),
CurrencyDisplay::Code => String::from(code),
CurrencyDisplay::Name => String::from(name),
};
let digits = crate::cldr::currency_digits(code);
pat.min_frac = digits;
pat.max_frac = digits;
(pat, value, shown)
}
}
}
fn standard_parts(
lang: &str,
value: f64,
s: &NumberSpec,
opts: &NumberFormatOptions,
) -> Vec<NumberPart> {
#[cfg(feature = "currency")]
if opts.style == NumberStyle::Currency
&& matches!(
opts.currency_display,
CurrencyDisplay::Code | CurrencyDisplay::Name
)
{
return currency_unit_wrap(lang, value, opts, s);
}
let (pattern, scaled, currency) = resolve_style(lang, value, s, opts);
let min_frac = opts
.minimum_fraction_digits
.map_or(pattern.min_frac as usize, usize::from);
let max_frac = opts
.maximum_fraction_digits
.map_or(pattern.max_frac as usize, usize::from)
.max(min_frac);
let min_int = (opts.minimum_integer_digits.max(1)) as usize;
let min_sig = opts.minimum_significant_digits.map(usize::from);
let max_sig = opts.maximum_significant_digits.map(usize::from);
let negative = scaled.is_sign_negative() && scaled != 0.0;
let abs = if scaled < 0.0 { -scaled } else { scaled };
let (int_d, frac_d) = round_digits(
abs,
min_int,
min_frac,
max_frac,
min_sig,
max_sig,
opts.rounding_mode,
negative,
);
let is_zero = int_d.bytes().all(|b| b == b'0') && frac_d.bytes().all(|b| b == b'0');
let (pri, sec) = effective_grouping(opts, &pattern, int_d.len());
let mut parts = Vec::new();
let core = core_parts(&int_d, &frac_d, negative, is_zero, pri, sec, opts, s);
#[cfg(feature = "units")]
if opts.style == NumberStyle::Unit {
return unit_wrap(lang, scaled, core, opts);
}
let mut core_iter = core.into_iter().peekable();
if let Some(first) = core_iter.peek()
&& matches!(
first.kind,
NumberPartType::MinusSign | NumberPartType::PlusSign
)
{
parts.push(core_iter.next().unwrap());
}
parts.extend(affix_parts(pattern.prefix, opts.style, s, ¤cy));
parts.extend(core_iter);
parts.extend(affix_parts(pattern.suffix, opts.style, s, ¤cy));
parts
}
fn exponent_parts(
value: f64,
s: &NumberSpec,
opts: &NumberFormatOptions,
base: i32,
) -> Vec<NumberPart> {
let negative = value.is_sign_negative() && value != 0.0;
let abs = if value < 0.0 { -value } else { value };
let mut exp = 0i32;
let mut m = abs;
if abs != 0.0 {
while m >= 10.0 {
m /= 10.0;
exp += 1;
}
while m < 1.0 {
m *= 10.0;
exp -= 1;
}
}
if base > 1 {
let rem = exp.rem_euclid(base);
for _ in 0..rem {
m *= 10.0;
}
exp -= rem;
}
let min_frac = opts.minimum_fraction_digits.map_or(0usize, usize::from);
let max_frac = opts
.maximum_fraction_digits
.map_or(6usize, usize::from)
.max(min_frac);
let min_sig = opts.minimum_significant_digits.map(usize::from);
let max_sig = opts.maximum_significant_digits.map(usize::from);
let (mut int_d, mut frac_d) = round_digits(
m,
1,
min_frac,
max_frac,
min_sig,
max_sig,
opts.rounding_mode,
negative,
);
let want = base.max(1) as usize;
if int_d.len() > want {
let shift = (int_d.len() - want) as i32;
exp += shift;
let combined = alloc::format!("{int_d}{frac_d}");
int_d = String::from(&combined[..want]);
frac_d = String::from(combined[want..].trim_end_matches('0'));
}
let is_zero = abs == 0.0;
let mut parts = Vec::new();
if let Some(sign) = sign_part(negative, is_zero, opts, s) {
parts.push(sign);
}
parts.push(NumberPart::new(
NumberPartType::Integer,
map_digits(&int_d, opts),
));
if !frac_d.is_empty() {
parts.push(NumberPart::new(NumberPartType::Decimal, s.decimal));
parts.push(NumberPart::new(
NumberPartType::Fraction,
map_digits(&frac_d, opts),
));
}
parts.push(NumberPart::new(NumberPartType::ExponentSeparator, "E"));
if exp < 0 {
parts.push(NumberPart::new(NumberPartType::ExponentMinusSign, s.minus));
}
parts.push(NumberPart::new(
NumberPartType::ExponentInteger,
map_digits(&alloc::format!("{}", exp.unsigned_abs()), opts),
));
parts
}
fn compact_parts(
lang: &str,
value: f64,
s: &NumberSpec,
opts: &NumberFormatOptions,
) -> Vec<NumberPart> {
let abs = if value < 0.0 { -value } else { value };
if !abs.is_finite() || abs < 1000.0 {
return standard_parts(lang, value, s, opts);
}
let norm: String = lang
.chars()
.map(|c| {
if c == '_' {
'-'
} else {
c.to_ascii_lowercase()
}
})
.collect();
let mut end = norm.len();
let table = loop {
if let Some(t) = crate::cldr::compact_patterns(&norm[..end]) {
break t;
}
match norm[..end].rfind('-') {
Some(i) => end = i,
None => break crate::cldr::compact_patterns("en").expect("root compact present"),
}
};
let mut exp = 0usize;
let mut t = abs;
while t >= 10.0 && exp < 14 {
t /= 10.0;
exp += 1;
}
let base = if opts.compact_display == CompactDisplay::Long {
12
} else {
0
};
let pattern = table[base + (exp - 3).min(11)];
let zeros = pattern.chars().filter(|&c| c == '0').count();
let has_suffix = pattern
.chars()
.any(|c| c != '0' && c != '\'' && !c.is_whitespace());
if zeros == 0 || !has_suffix {
return standard_parts(lang, value, s, opts);
}
let mut divisor = 1.0f64;
for _ in 0..(exp + 1).saturating_sub(zeros) {
divisor *= 10.0;
}
let mantissa = value / divisor;
let negative = mantissa.is_sign_negative() && mantissa != 0.0;
let mabs = if mantissa < 0.0 { -mantissa } else { mantissa };
let min_frac = opts.minimum_fraction_digits.map_or(0usize, usize::from);
let max_frac = opts
.maximum_fraction_digits
.map_or(1usize, usize::from)
.max(min_frac);
let (int_d, frac_d) = round_digits(
mabs,
1,
min_frac,
max_frac,
None,
None,
opts.rounding_mode,
negative,
);
let is_zero = false;
let mut parts = Vec::new();
if let Some(sign) = sign_part(negative, is_zero, opts, s) {
parts.push(sign);
}
let mut wrote = false;
let mut chars = pattern.chars().peekable();
let mut lit = String::new();
let flush_lit = |lit: &mut String, parts: &mut Vec<NumberPart>| {
if !lit.is_empty() {
parts.push(NumberPart::new(
NumberPartType::Compact,
core::mem::take(lit),
));
}
};
while let Some(c) = chars.next() {
match c {
'0' => {
while chars.peek() == Some(&'0') {
chars.next();
}
if !wrote {
flush_lit(&mut lit, &mut parts);
parts.push(NumberPart::new(
NumberPartType::Integer,
map_digits(&int_d, opts),
));
if !frac_d.is_empty() {
parts.push(NumberPart::new(NumberPartType::Decimal, s.decimal));
parts.push(NumberPart::new(
NumberPartType::Fraction,
map_digits(&frac_d, opts),
));
}
wrote = true;
}
}
'\'' => {
for q in chars.by_ref() {
if q == '\'' {
break;
}
lit.push(q);
}
}
other => lit.push(other),
}
}
flush_lit(&mut lit, &mut parts);
parts
}
#[must_use]
pub fn format_to_parts(lang: &str, value: f64, opts: &NumberFormatOptions) -> Vec<NumberPart> {
let s = spec(lang);
if value.is_nan() {
return alloc::vec![NumberPart::new(NumberPartType::Nan, "NaN")];
}
if value.is_infinite() {
let mut parts = Vec::new();
if let Some(sign) = sign_part(value < 0.0, false, opts, &s) {
parts.push(sign);
}
parts.push(NumberPart::new(NumberPartType::Infinity, "∞"));
return parts;
}
match opts.notation {
Notation::Standard => standard_parts(lang, value, &s, opts),
Notation::Scientific => exponent_parts(value, &s, opts, 1),
Notation::Engineering => exponent_parts(value, &s, opts, 3),
Notation::Compact => compact_parts(lang, value, &s, opts),
}
}
#[must_use]
pub fn format(lang: &str, value: f64, opts: &NumberFormatOptions) -> String {
join_parts(&format_to_parts(lang, value, opts))
}
#[cfg(test)]
mod tests {
use super::*;
fn empty_sep_spec() -> NumberSpec {
let pat = Pattern {
prefix: "",
suffix: "",
min_int: 1,
min_frac: 0,
max_frac: 3,
primary_group: 3,
secondary_group: 3,
};
NumberSpec {
decimal: "",
group: "",
minus: "-",
plus: "+",
percent: "%",
dec: pat,
pct: pat,
}
}
#[test]
fn parse_empty_separators_does_not_hang() {
let s = empty_sep_spec();
assert_eq!(parse_decimal_with(&s, "1234"), Some(1234.0));
assert_eq!(parse_decimal_with(&s, "-42"), Some(-42.0));
assert_eq!(parse_decimal_with(&s, "1.5"), None);
assert_eq!(parse_decimal_with(&s, "abc"), None);
}
#[test]
fn parse_real_locales_unchanged() {
assert_eq!(parse_decimal("en", "1,234.5"), Some(1234.5));
assert_eq!(parse_decimal("de", "1.234,5"), Some(1234.5));
assert_eq!(parse_decimal("en", "-7.0"), Some(-7.0));
assert_eq!(parse_decimal("en", "abc"), None);
}
#[test]
fn compact_width_saturates() {
assert_eq!(format_compact("en", 1500.0), "1.5K");
assert_eq!(format_compact("en", 2_300_000.0), "2.3M");
assert_eq!(format_compact("en", 999.0), "999");
}
fn opt() -> NumberFormatOptions {
NumberFormatOptions::default()
}
#[test]
fn options_default_matches_decimal() {
assert_eq!(format("en", 1234.5, &opt()), "1,234.5");
assert_eq!(format("de", 1234.5, &opt()), "1.234,5");
assert_eq!(format("hi", 1234567.0, &opt()), "12,34,567");
}
#[test]
fn options_grouping_and_min_int() {
let ng = NumberFormatOptions {
use_grouping: UseGrouping::Never,
..opt()
};
assert_eq!(format("en", 1234567.0, &ng), "1234567");
let mi = NumberFormatOptions {
minimum_integer_digits: 3,
..opt()
};
assert_eq!(format("en", 5.0, &mi), "005");
}
#[test]
fn options_sign_display() {
let always = NumberFormatOptions {
sign_display: SignDisplay::Always,
..opt()
};
assert_eq!(format("en", 5.0, &always), "+5");
assert_eq!(format("en", -5.0, &always), "-5");
assert_eq!(format("en", 0.0, &always), "+0");
let ez = NumberFormatOptions {
sign_display: SignDisplay::ExceptZero,
..opt()
};
assert_eq!(format("en", 0.0, &ez), "0");
assert_eq!(format("en", 3.0, &ez), "+3");
let never = NumberFormatOptions {
sign_display: SignDisplay::Never,
..opt()
};
assert_eq!(format("en", -5.0, &never), "5");
}
#[test]
fn options_fraction_digits() {
let f = NumberFormatOptions {
minimum_fraction_digits: Some(2),
maximum_fraction_digits: Some(2),
..opt()
};
assert_eq!(format("en", 1.5, &f), "1.50");
assert_eq!(format("en", 1.005, &f), "1.00"); }
#[test]
fn options_rounding_modes() {
let mk = |mode, mx| NumberFormatOptions {
rounding_mode: mode,
maximum_fraction_digits: Some(mx),
..opt()
};
assert_eq!(format("en", 1.001, &mk(RoundingMode::Ceil, 2)), "1.01");
assert_eq!(format("en", 1.999, &mk(RoundingMode::Trunc, 2)), "1.99");
assert_eq!(format("en", -1.001, &mk(RoundingMode::Floor, 2)), "-1.01");
assert_eq!(format("en", 2.5, &mk(RoundingMode::HalfEven, 0)), "2");
assert_eq!(format("en", 3.5, &mk(RoundingMode::HalfEven, 0)), "4");
assert_eq!(format("en", 2.5, &mk(RoundingMode::HalfExpand, 0)), "3");
}
#[test]
fn options_significant_digits() {
let mx = NumberFormatOptions {
maximum_significant_digits: Some(3),
..opt()
};
assert_eq!(format("en", 1234.0, &mx), "1,230");
let mn = NumberFormatOptions {
minimum_significant_digits: Some(4),
maximum_significant_digits: Some(6),
..opt()
};
assert_eq!(format("en", 1.5, &mn), "1.500");
}
#[test]
fn options_percent_parts() {
let pct = NumberFormatOptions {
style: NumberStyle::Percent,
..opt()
};
let parts = format_to_parts("en", 0.5, &pct);
assert_eq!(parts.last().unwrap().kind, NumberPartType::PercentSign);
assert_eq!(format("en", 0.5, &pct), "50%");
}
#[cfg(feature = "currency")]
#[test]
fn options_currency_parts() {
let cur = NumberFormatOptions {
style: NumberStyle::Currency,
currency: Some("USD"),
..opt()
};
assert_eq!(format("en", 1234.5, &cur), "$1,234.50");
let parts = format_to_parts("en", 1234.5, &cur);
assert!(
parts
.iter()
.any(|p| p.kind == NumberPartType::Currency && p.value == "$")
);
let code = NumberFormatOptions {
currency_display: CurrencyDisplay::Code,
..cur
};
assert_eq!(format("en", 5.0, &code), "5.00 USD");
}
#[test]
fn options_notation() {
let sci = NumberFormatOptions {
notation: Notation::Scientific,
..opt()
};
let parts = format_to_parts("en", 12345.0, &sci);
let kinds: Vec<_> = parts.iter().map(|p| p.kind).collect();
assert_eq!(
kinds,
alloc::vec![
NumberPartType::Integer,
NumberPartType::Decimal,
NumberPartType::Fraction,
NumberPartType::ExponentSeparator,
NumberPartType::ExponentInteger,
]
);
assert_eq!(format("en", 12345.0, &sci), "1.2345E4");
assert!(
format_to_parts("en", 0.00042, &sci)
.iter()
.any(|p| p.kind == NumberPartType::ExponentMinusSign)
);
let eng = NumberFormatOptions {
notation: Notation::Engineering,
..opt()
};
assert_eq!(format("en", 12345.0, &eng), "12.345E3");
let comp = NumberFormatOptions {
notation: Notation::Compact,
..opt()
};
assert_eq!(format("en", 1500.0, &comp), "1.5K");
assert!(
format_to_parts("en", 1500.0, &comp)
.iter()
.any(|p| p.kind == NumberPartType::Compact && p.value == "K")
);
}
#[test]
fn options_numbering_system() {
let ns = NumberFormatOptions {
numbering_system: Some("arab"),
..opt()
};
assert_eq!(format("en", 123.0, &ns), "١٢٣");
}
#[test]
fn non_finite() {
assert_eq!(format("en", f64::NAN, &opt()), "NaN");
assert_eq!(format("en", f64::INFINITY, &opt()), "∞");
assert_eq!(format("en", f64::NEG_INFINITY, &opt()), "-∞");
}
#[test]
fn parts_join_round_trips() {
let cases = [
NumberFormatOptions::default(),
NumberFormatOptions {
style: NumberStyle::Percent,
..opt()
},
NumberFormatOptions {
style: NumberStyle::Currency,
currency: Some("EUR"),
..opt()
},
NumberFormatOptions {
notation: Notation::Scientific,
..opt()
},
NumberFormatOptions {
notation: Notation::Compact,
..opt()
},
];
for o in cases {
for v in [0.0, 1234.5, -9999.99, 0.001] {
let joined = join_parts(&format_to_parts("en", v, &o));
assert_eq!(joined, format("en", v, &o));
}
}
}
}