use crate::core::built_ins_for_strings_encoding::{java_string_enc, js_string_enc};
use crate::core::Environment;
use crate::error::{Result, TemplateError};
use crate::value::TNumber;
use bigdecimal::{BigDecimal, RoundingMode};
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CFormatKind {
#[default]
JavaScriptOrJson,
JavaScript,
Json,
Java,
Legacy,
Xs,
}
impl CFormatKind {
pub fn parse(name: &str) -> Option<CFormatKind> {
Some(match name {
"JavaScript or JSON" => CFormatKind::JavaScriptOrJson,
"JavaScript" => CFormatKind::JavaScript,
"JSON" => CFormatKind::Json,
"Java" => CFormatKind::Java,
"legacy" => CFormatKind::Legacy,
"XS" => CFormatKind::Xs,
_ => return None,
})
}
pub fn name(&self) -> &'static str {
match self {
CFormatKind::JavaScriptOrJson => "JavaScript or JSON",
CFormatKind::JavaScript => "JavaScript",
CFormatKind::Json => "JSON",
CFormatKind::Java => "Java",
CFormatKind::Legacy => "legacy",
CFormatKind::Xs => "XS",
}
}
}
pub fn format_c_string(s: &str, kind: CFormatKind) -> String {
match kind {
CFormatKind::JavaScriptOrJson | CFormatKind::Json | CFormatKind::Legacy => {
format!("\"{}\"", js_string_enc(s, true))
}
CFormatKind::JavaScript => {
format!("\"{}\"", js_string_enc(s, false).replace("\\'", "'"))
}
CFormatKind::Java => format!("\"{}\"", java_string_enc(s)),
CFormatKind::Xs => s.to_string(),
}
}
fn inf_nan_symbols(
kind: CFormatKind,
is_float: bool,
) -> (&'static str, &'static str, &'static str) {
match kind {
CFormatKind::Java => (
if is_float {
"Float.POSITIVE_INFINITY"
} else {
"Double.POSITIVE_INFINITY"
},
if is_float {
"Float.NEGATIVE_INFINITY"
} else {
"Double.NEGATIVE_INFINITY"
},
if is_float { "Float.NaN" } else { "Double.NaN" },
),
CFormatKind::Xs => ("INF", "-INF", "NaN"),
_ => ("Infinity", "-Infinity", "NaN"),
}
}
pub fn format_c_number(n: &TNumber, kind: CFormatKind) -> String {
match n {
TNumber::Int(v) => v.to_string(),
TNumber::Long(v) => v.to_string(),
TNumber::BigInt(v) => v.to_string(),
TNumber::Double(v) => format_c_double(*v, kind),
TNumber::Float(v) => format_c_float(*v, kind),
TNumber::Decimal(d) => format_c_big_decimal(d),
}
}
fn format_c_big_decimal(d: &BigDecimal) -> String {
let stripped = d.normalized();
if stripped.as_bigint_and_scale().1 <= 0 {
stripped.to_plain_string()
} else {
stripped.to_string()
}
}
fn format_c_double(n: f64, kind: CFormatKind) -> String {
let (pos_inf, neg_inf, nan) = inf_nan_symbols(kind, false);
if n == f64::INFINITY {
return pos_inf.to_string();
}
if n == f64::NEG_INFINITY {
return neg_inf.to_string();
}
if n.is_nan() {
return nan.to_string();
}
if n.floor() == n {
if n.abs() <= 9_007_199_254_740_992.0 {
return (n as i64).to_string();
}
} else {
let abs = n.abs();
if abs < 1e-3 && abs > 1e-7 {
return BigDecimal::from_str(&java_float_string(n))
.map(|b| b.to_string())
.unwrap_or_else(|_| java_float_string(n));
}
if abs >= 1e7 {
return BigDecimal::from_str(&java_float_string(n))
.map(|b| b.to_plain_string())
.unwrap_or_else(|_| java_float_string(n));
}
}
remove_redundant_dot0(&java_float_string(n))
}
fn format_c_float(n: f32, kind: CFormatKind) -> String {
let (pos_inf, neg_inf, nan) = inf_nan_symbols(kind, true);
if n == f32::INFINITY {
return pos_inf.to_string();
}
if n == f32::NEG_INFINITY {
return neg_inf.to_string();
}
if n.is_nan() {
return nan.to_string();
}
if n.floor() == n {
if n.abs() <= 16_777_216.0 {
return (n as i64).to_string();
}
} else {
let abs = n.abs();
if abs < 1e-3 && abs > 1e-7 {
let s = java_float_string_f32(n);
return BigDecimal::from_str(&s).map(|b| b.to_string()).unwrap_or(s);
}
}
remove_redundant_dot0(&java_float_string_f32(n))
}
fn remove_redundant_dot0(s: &str) -> String {
if let Some(e_idx) = s.find('E') {
let mantissa = &s[..e_idx];
if let Some(rest) = mantissa.strip_suffix(".0") {
return format!("{rest}{}", &s[e_idx..]);
}
s.to_string()
} else if let Some(rest) = s.strip_suffix(".0") {
rest.to_string()
} else {
s.to_string()
}
}
fn java_float_string(n: f64) -> String {
java_float_string_impl(&format!("{:e}", n))
}
fn java_float_string_f32(n: f32) -> String {
java_float_string_impl(&format!("{:e}", n))
}
fn java_float_string_impl(s: &str) -> String {
let (mant, exp) = s.split_once('e').expect("Rust {:e} always has exponent");
let exp: i32 = exp.parse().expect("exponent parses");
let (sign, mantissa) = match mant.strip_prefix('-') {
Some(m) => ("-", m),
None => ("", mant),
};
let digits: String = mantissa.chars().filter(|c| *c != '.').collect();
let int_digits = mantissa
.split_once('.')
.map_or(mantissa.len(), |(i, _)| i.len());
if !(-3..7).contains(&exp) {
let m = if int_digits == digits.len() {
format!("{}.0", &digits[..int_digits])
} else {
format!("{}.{}", &digits[..int_digits], &digits[int_digits..])
};
format!("{sign}{m}E{exp}")
} else if exp < 0 {
let zeros = "0".repeat((-(exp + 1)) as usize);
format!("{sign}0.{zeros}{digits}")
} else {
let point = (exp + 1) as usize;
if point >= digits.len() {
format!("{sign}{digits}{}.0", "0".repeat(point - digits.len()))
} else {
format!("{sign}{}.{}", &digits[..point], &digits[point..])
}
}
}
fn group_separator(locale: &str) -> char {
match locale.split('_').next().unwrap_or("en") {
"fr" => '\u{202F}', "de" | "es" | "tr" | "it" | "pt" | "nl" | "sv" | "cs" | "pl" | "hu" | "ro" | "ru"
| "uk" | "bg" | "el" | "fi" | "da" | "no" | "sk" | "sl" | "hr" | "lt" | "lv" | "et"
| "id" | "vi" | "th" => '.',
_ => ',',
}
}
fn decimal_separator(locale: &str) -> char {
match locale.split('_').next().unwrap_or("en") {
"fr" | "de" | "es" | "tr" | "it" | "pt" | "nl" | "sv" | "cs" | "pl" | "hu" | "ro"
| "ru" | "uk" | "bg" | "el" | "fi" | "da" | "no" | "sk" | "sl" | "hr" | "lt" | "lv"
| "et" | "id" | "vi" | "th" => ',',
_ => '.',
}
}
pub struct DecimalFmt {
pub prefix: String,
pub suffix: String,
pub min_int: usize,
pub grouping: bool,
pub min_frac: usize,
pub max_frac: usize,
pub decimal_sep: char,
pub group_sep: char,
}
pub fn parse_decimal_format(pattern: &str, locale: &str) -> Result<DecimalFmt> {
let mut prefix = String::new();
let mut suffix = String::new();
let mut int_part = String::new();
let mut frac_part = String::new();
let mut min_int = 0usize;
let mut max_int = 0usize;
let mut min_frac = 0usize;
let mut max_frac = 0usize;
let mut grouping = false;
let mut seen_decimal = false;
let chars: Vec<char> = pattern.chars().collect();
let mut i = 0usize;
while i < chars.len() {
let c = chars[i];
if c == '\'' {
let mut lit = String::new();
i += 1;
while i < chars.len() {
if chars[i] == '\'' {
if i + 1 < chars.len() && chars[i + 1] == '\'' {
lit.push('\'');
i += 2;
continue;
}
i += 1;
break;
}
lit.push(chars[i]);
i += 1;
}
prefix.push_str(&lit);
continue;
}
if c == '0' || c == '#' || c == '.' || c == ',' {
break;
}
prefix.push(c);
i += 1;
}
while i < chars.len() {
let c = chars[i];
match c {
'0' | '#' => {
if seen_decimal {
frac_part.push(c);
if c == '0' {
min_frac += 1;
}
max_frac += 1;
} else {
int_part.push(c);
if c == '0' {
min_int += 1;
}
max_int += 1;
}
}
'.' => {
if seen_decimal {
return Err(TemplateError::misc(format!(
"Invalid number format pattern: {pattern}"
)));
}
seen_decimal = true;
}
',' => grouping = true,
'\'' => {
let mut lit = String::new();
i += 1;
while i < chars.len() {
if chars[i] == '\'' {
if i + 1 < chars.len() && chars[i + 1] == '\'' {
lit.push('\'');
i += 2;
continue;
}
i += 1;
break;
}
lit.push(chars[i]);
i += 1;
}
suffix.push_str(&lit);
i += 1;
while i < chars.len() {
suffix.push(chars[i]);
i += 1;
}
break;
}
_ => {
suffix.push(c);
}
}
i += 1;
}
if i < chars.len() {
while i < chars.len() {
suffix.push(chars[i]);
i += 1;
}
}
if min_int == 0 && max_int == 0 && min_frac == 0 && max_frac == 0 {
return Ok(DecimalFmt {
prefix,
suffix,
min_int: 0,
grouping: false,
min_frac: 0,
max_frac: 0,
decimal_sep: decimal_separator(locale),
group_sep: group_separator(locale),
});
}
Ok(DecimalFmt {
prefix,
suffix,
min_int,
grouping,
min_frac,
max_frac,
decimal_sep: decimal_separator(locale),
group_sep: group_separator(locale),
})
}
pub fn format_decimal(fmt: &DecimalFmt, n: &TNumber) -> String {
match n {
TNumber::Int(v) => return format_integer_decimal(fmt, *v as i64),
TNumber::Long(v) => return format_integer_decimal(fmt, *v),
_ => {}
}
let mut bd = match n {
TNumber::Float(v) => BigDecimal::from_str(&format!("{}", *v as f64)).unwrap_or_default(),
TNumber::Double(v) => BigDecimal::from_str(&format!("{v}")).unwrap_or_default(),
_ => n.as_big_decimal(),
};
if fmt.max_frac == 0 {
bd = bd.with_scale_round(0, RoundingMode::HalfEven);
} else if bd.as_bigint_and_scale().1 > fmt.max_frac as i64 {
bd = bd.with_scale_round(fmt.max_frac as i64, RoundingMode::HalfEven);
}
let (int_digits, frac_digits) = split_digits(&bd);
let mut int_s = int_digits;
while int_s.len() < fmt.min_int {
int_s.insert(0, '0');
}
if int_s.is_empty() {
int_s.push('0');
}
if fmt.grouping && int_s.len() > 3 {
let (sign, digits) = match int_s.strip_prefix('-') {
Some(d) => ("-", d),
None => ("", int_s.as_str()),
};
if digits.len() > 3 {
let chars: Vec<char> = digits.chars().collect();
let n = chars.len();
let first = n % 3;
let mut out = String::new();
let mut idx = 0;
if first > 0 {
out.extend(&chars[..first]);
idx = first;
}
while idx < n {
if !out.is_empty() {
out.push(fmt.group_sep);
}
out.extend(&chars[idx..idx + 3]);
idx += 3;
}
int_s = format!("{sign}{out}");
}
}
let mut frac_s = frac_digits;
while frac_s.len() > fmt.min_frac && frac_s.ends_with('0') {
frac_s.pop();
}
while frac_s.len() < fmt.min_frac {
frac_s.push('0');
}
if frac_s.len() > fmt.max_frac {
frac_s.truncate(fmt.max_frac);
}
let mut out = String::new();
out.push_str(&fmt.prefix);
out.push_str(&int_s);
if !frac_s.is_empty() {
out.push(fmt.decimal_sep);
out.push_str(&frac_s);
}
out.push_str(&fmt.suffix);
out
}
fn format_integer_decimal(fmt: &DecimalFmt, v: i64) -> String {
let mut int_s = v.to_string();
while int_s.len() < fmt.min_int {
int_s.insert(0, '0');
}
if fmt.grouping && int_s.len() > 3 {
let (sign, digits) = match int_s.strip_prefix('-') {
Some(d) => ("-", d),
None => ("", int_s.as_str()),
};
if digits.len() > 3 {
let chars: Vec<char> = digits.chars().collect();
let n = chars.len();
let first = n % 3;
let mut out = String::new();
let mut idx = 0;
if first > 0 {
out.extend(&chars[..first]);
idx = first;
}
while idx < n {
if !out.is_empty() {
out.push(fmt.group_sep);
}
out.extend(&chars[idx..idx + 3]);
idx += 3;
}
int_s = format!("{sign}{out}");
}
}
if fmt.prefix.is_empty() && fmt.suffix.is_empty() && fmt.min_frac == 0 {
return int_s;
}
let mut out = String::new();
out.push_str(&fmt.prefix);
out.push_str(&int_s);
if fmt.min_frac > 0 {
out.push(fmt.decimal_sep);
for _ in 0..fmt.min_frac {
out.push('0');
}
}
out.push_str(&fmt.suffix);
out
}
fn split_digits(bd: &BigDecimal) -> (String, String) {
let s = bd.to_plain_string();
match s.split_once('.') {
Some((i, f)) => (i.to_string(), f.to_string()),
None => (s, String::new()),
}
}
pub fn format_number(env: &Environment, n: &TNumber) -> Result<String> {
let fmt = env.settings.number_format.as_str();
let locale = env.settings.locale.as_str();
if let Some(name) = custom_format_name(fmt) {
return Err(TemplateError::misc(format!(
"No custom number format was defined with name {}",
j_quote(&name)
)));
}
if fmt == "currency" {
return Ok(format_currency_number(locale, n));
}
if fmt == "percent" {
return Ok(format_percent_number(locale, n));
}
if fmt == "number" || fmt.is_empty() {
let mut cache = env.number_fmt_cache.borrow_mut();
let df = match &*cache {
Some((f, l, df)) if f == fmt && l == locale => df.clone(),
_ => {
let parsed = match parse_decimal_format("#,##0.###", locale) {
Ok(df) => df,
Err(_) => return Ok(n.to_plain_string()),
};
let rc = std::rc::Rc::new(parsed);
*cache = Some((fmt.to_string(), locale.to_string(), rc.clone()));
rc
}
};
Ok(format_decimal(&df, n))
} else if fmt == "c" || fmt == "computer" {
Ok(format_c_number(n, CFormatKind::JavaScriptOrJson))
} else {
match parse_decimal_format(fmt, locale) {
Ok(df) => Ok(format_decimal(&df, n)),
Err(_) => Ok(n.to_plain_string()),
}
}
}
fn currency_spec(locale: &str) -> (&'static str, usize, bool) {
match locale {
"en_US" => ("$", 2, true),
"en_GB" => ("£", 2, true),
"zh_CN" | "zh_TW" => ("¥", 2, true),
"ja_JP" => ("¥", 0, true), "de_DE" | "de_AT" | "de_CH" => (" €", 2, false),
"fr_FR" => (" €", 2, false),
_ => ("¤", 2, true),
}
}
fn percent_suffix(locale: &str) -> &'static str {
match locale.split('_').next().unwrap_or("en") {
"de" | "fr" => " %",
_ => "%",
}
}
fn format_currency_number(locale: &str, n: &TNumber) -> String {
let (sym, frac, prefix) = currency_spec(locale);
let pattern = if frac == 0 { "#,##0" } else { "#,##0.00" };
let num = match parse_decimal_format(pattern, locale) {
Ok(df) => format_decimal(&df, n),
Err(_) => n.to_plain_string(),
};
if prefix {
format!("{sym}{num}")
} else {
format!("{num}{sym}")
}
}
fn format_percent_number(locale: &str, n: &TNumber) -> String {
let scaled = TNumber::Decimal(n.as_big_decimal() * bigdecimal::BigDecimal::from(100));
let num = match parse_decimal_format("#,##0", locale) {
Ok(df) => format_decimal(&df, &scaled),
Err(_) => scaled.to_plain_string(),
};
format!("{num}{}", percent_suffix(locale))
}
pub fn format_number_with(fmt: &str, locale: &str, n: &TNumber) -> Result<String> {
if let Some(name) = custom_format_name(fmt) {
return Err(TemplateError::misc(format!(
"No custom number format was defined with name {}",
j_quote(&name)
)));
}
if fmt == "currency" {
return Ok(format_currency_number(locale, n));
}
if fmt == "percent" {
return Ok(format_percent_number(locale, n));
}
if fmt == "number" || fmt.is_empty() {
match parse_decimal_format("#,##0.###", locale) {
Ok(df) => Ok(format_decimal(&df, n)),
Err(_) => Ok(n.to_plain_string()),
}
} else if fmt == "c" || fmt == "computer" {
Ok(format_c_number(n, CFormatKind::JavaScriptOrJson))
} else {
match parse_decimal_format(fmt, locale) {
Ok(df) => Ok(format_decimal(&df, n)),
Err(_) => Ok(n.to_plain_string()),
}
}
}
pub(crate) fn custom_format_name(format_string: &str) -> Option<String> {
let rest = format_string.strip_prefix('@')?;
if !rest.chars().next().is_some_and(|c| c.is_ascii_alphabetic()) {
return None;
}
let name = rest
.split([' ', '_'])
.next()
.unwrap_or_default()
.to_string();
Some(name)
}
pub(crate) fn j_quote(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
_ => out.push(c),
}
}
out.push('"');
out
}
#[cfg(test)]
#[path = "format_tests.rs"]
mod format_tests;