use crate::js::chars::is_line_end;
use crate::js::math::round;
use crate::js::number::{number, to_exponential, to_fixed, to_precision, to_string};
#[derive(Clone, Copy, Debug, Default)]
struct Spec {
plus: bool,
dollar: bool,
comma: bool,
trim: bool,
precision: Option<usize>,
kind: Option<u8>,
}
fn is_align(c: u8) -> bool {
matches!(c, b'<' | b'>' | b'=' | b'^')
}
fn read_spec(s: &str) -> Option<Spec> {
let b = s.as_bytes();
let mut starts = Vec::new();
if let Some(c) = s.chars().next() {
let size = c.len_utf8();
if c.len_utf16() == 1 && !is_line_end(c as i32) && b.get(size).is_some_and(|&a| is_align(a))
{
starts.push(size + 1);
}
}
if b.first().is_some_and(|&a| is_align(a)) {
starts.push(1);
}
starts.push(0);
starts.into_iter().find_map(|i| read_spec_rest(s, i))
}
fn read_spec_rest(s: &str, mut i: usize) -> Option<Spec> {
let b = s.as_bytes();
let mut spec = Spec::default();
let at = |i: usize, c: u8| b.get(i) == Some(&c);
let digit = |k: usize| b.get(k).is_some_and(u8::is_ascii_digit);
if b.get(i).is_some_and(|c| b"-+( ".contains(c)) {
spec.plus = b[i] == b'+';
i += 1;
}
if at(i, b'$') {
spec.dollar = true;
i += 1;
}
while digit(i) {
i += 1;
}
if at(i, b',') {
spec.comma = true;
i += 1;
}
if at(i, b'.') && digit(i + 1) {
i += 1;
let mut p = 0;
while digit(i) {
p = (p * 10 + usize::from(b[i] - b'0')).min(21);
i += 1;
}
spec.precision = Some(p.min(20));
}
if at(i, b'~') {
spec.trim = true;
i += 1;
}
if b.get(i)
.is_some_and(|&c| c.is_ascii_alphabetic() || c == b'%')
{
spec.kind = Some(b[i]);
i += 1;
}
(i == b.len()).then_some(spec)
}
fn group(digits: &str) -> String {
let mut out = String::new();
let mut i = digits.len();
while i > 0 {
let piece = &digits[i.saturating_sub(3)..i];
out = if out.is_empty() {
piece.to_string()
} else {
format!("{piece},{out}")
};
i = i.saturating_sub(3);
}
out
}
pub(crate) fn formatter(spec: &str) -> impl Fn(f64) -> String {
let spec = if spec == "$0,0" { "$," } else { spec };
let spec = read_spec(spec)
.or_else(|| read_spec(","))
.unwrap_or_default();
move |value: f64| {
let abs = value.abs();
let precision = |fallback: usize| spec.precision.unwrap_or(fallback);
let mut suffix = "";
let mut body = match spec.kind {
Some(b'f') => to_fixed(abs, precision(6)),
Some(b'%') => {
suffix = "%";
to_fixed(abs * 100.0, precision(6))
}
Some(b'd') => to_fixed(round(abs), 0),
Some(b'e') => to_exponential(abs, precision(6)),
_ => to_string(number(&to_precision(abs, precision(12).max(1)))),
};
if spec.trim && body.contains('.') && !body.contains('e') {
let kept = body.trim_end_matches('0');
body = kept.strip_suffix('.').unwrap_or(kept).to_string();
}
if spec.comma && !body.contains('e') {
body = match body.find('.') {
Some(dot) => format!("{}{}", group(&body[..dot]), &body[dot..]),
None => group(&body),
};
}
let sign = if value < 0.0 {
"-"
} else if spec.plus {
"+"
} else {
""
};
let dollar = if spec.dollar { "$" } else { "" };
format!("{sign}{dollar}{body}{suffix}")
}
}