use std::fmt;
use std::sync::Arc;
use crate::calc::Value;
pub const MOST_CHARS: usize = 255;
pub const MOST_DECIMALS: usize = 30;
const BUILTINS: [(&str, u32); 8] = [
("0", 1),
("0.00", 2),
("#,##0", 3),
("#,##0.00", 4),
("0%", 9),
("0.00%", 10),
("#,##0 ;(#,##0)", 37),
("#,##0.00;(#,##0.00)", 39),
];
#[derive(Clone)]
pub struct Code(Arc<Parsed>);
struct Parsed {
text: String,
sections: Vec<Section>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Unsupported {
pub what: String,
pub fix: String,
}
impl Unsupported {
fn new(what: impl Into<String>, fix: impl Into<String>) -> Unsupported {
Unsupported {
what: what.into(),
fix: fix.into(),
}
}
}
#[derive(Clone, Debug, PartialEq)]
enum Part {
Digit(bool),
Point,
Percent,
Literal(String),
At,
}
#[derive(Clone, Debug, Default)]
struct Section {
parts: Vec<Part>,
placeholders: bool,
integer: usize,
min_integer: usize,
decimals: Vec<bool>,
grouping: bool,
shift: i32,
}
#[derive(Clone, Debug, PartialEq)]
enum Token {
Digit(bool),
Point,
Comma,
Percent,
Literal(String),
At,
}
impl Code {
pub fn parse(text: &str) -> Result<Code, Unsupported> {
if text.is_empty() {
return Err(Unsupported::new(
"no code at all",
"write general for Excel's General format",
));
}
if text.eq_ignore_ascii_case("general") {
return Err(Unsupported::new(
"Excel's General format as a code",
"write it general, a symbol, unquoted",
));
}
if text == "@" {
return Err(Unsupported::new(
"Excel's Text format as a code",
"write it text, a symbol, unquoted",
));
}
if text.chars().count() > MOST_CHARS {
return Err(Unsupported::new(
format!("more than {MOST_CHARS} characters"),
format!("shorten it to Excel's {MOST_CHARS}"),
));
}
let raw = tokens(text)?;
if raw.len() > 4 {
return Err(Unsupported::new(
"a fifth section",
"keep four, a positive number's, a negative's, zero's and a text's",
));
}
let mut sections = Vec::with_capacity(raw.len());
for (i, toks) in raw.into_iter().enumerate() {
sections.push(if i == 3 {
text_section(toks)?
} else {
number_section(toks)?
});
}
Ok(Code(Arc::new(Parsed {
text: text.to_string(),
sections,
})))
}
pub fn as_str(&self) -> &str {
&self.0.text
}
pub fn builtin(&self) -> Option<u32> {
BUILTINS
.iter()
.find(|(code, _)| *code == self.0.text)
.map(|&(_, id)| id)
}
pub fn text(&self, value: &Value) -> Option<String> {
match value {
Value::Number(n) => self.number(*n),
Value::Text(t) => self.0.sections.get(3).map(|s| s.text_of(t)),
_ => None,
}
}
fn number(&self, x: f64) -> Option<String> {
if !x.is_finite() {
return None;
}
let sections = &self.0.sections;
let numeric = sections.len().min(3);
let (section, minus) = if x == 0.0 {
(§ions[if numeric == 3 { 2 } else { 0 }], false)
} else if x < 0.0 {
if numeric >= 2 {
(§ions[1], false)
} else {
(§ions[0], true)
}
} else {
(§ions[0], false)
};
let mut out = String::new();
let zero = section.draw(x.abs(), &mut out);
if minus && !zero {
out.insert(0, '-');
}
Some(out)
}
}
impl PartialEq for Code {
fn eq(&self, other: &Code) -> bool {
self.0.text == other.0.text
}
}
impl Eq for Code {}
impl fmt::Debug for Code {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("Code").field(&self.0.text).finish()
}
}
fn tokens(text: &str) -> Result<Vec<Vec<Token>>, Unsupported> {
let chars: Vec<char> = text.chars().collect();
let mut sections: Vec<Vec<Token>> = vec![Vec::new()];
let mut i = 0;
while i < chars.len() {
let c = chars[i];
let toks = sections.last_mut().expect("one section at least");
match c {
';' => sections.push(Vec::new()),
'"' => {
let start = i + 1;
let Some(len) = chars[start..].iter().position(|&x| x == '"') else {
return Err(Unsupported::new("a quote left open", "close it"));
};
let quoted: String = chars[start..start + len].iter().collect();
if quoted.chars().any(is_control) {
return Err(control());
}
toks.push(Token::Literal(quoted));
i = start + len;
}
'\\' => {
let Some(&next) = chars.get(i + 1) else {
return Err(Unsupported::new(
"a backslash with nothing after it",
"drop it, or write after it the character it draws",
));
};
if is_control(next) {
return Err(control());
}
toks.push(Token::Literal(next.to_string()));
i += 1;
}
'0' => toks.push(Token::Digit(true)),
'#' => toks.push(Token::Digit(false)),
'.' => toks.push(Token::Point),
',' => toks.push(Token::Comma),
'%' => toks.push(Token::Percent),
'@' => toks.push(Token::At),
'$' | '-' | '+' | '(' | ')' | ':' | '!' | '^' | '&' | '\'' | '~' | '{' | '}' | '<'
| '>' | '=' | ' ' => toks.push(Token::Literal(c.to_string())),
'[' => {
let bracket: String = match chars[i..].iter().position(|&x| x == ']') {
Some(len) => chars[i..=i + len].iter().collect(),
None => chars[i..].iter().collect(),
};
return Err(Unsupported::new(
format!("{bracket}, a color, a condition, a locale or an elapsed time"),
"drop it",
));
}
'?' => {
return Err(Unsupported::new(
"?, a digit's width",
"write 0 or # in its place",
))
}
'/' => {
return Err(Unsupported::new(
"/, a fraction's bar",
"quote it, \"/\", to draw it as written",
))
}
'_' => {
return Err(Unsupported::new(
"_, a character's width",
"drop it, or quote a space in its place",
))
}
'*' => return Err(Unsupported::new("*, a fill", "drop it")),
c if c.is_ascii_alphabetic() => return Err(letter(&chars, i)),
c if c.is_ascii_digit() => {
return Err(Unsupported::new(
format!("{c}, a digit outside quotes"),
format!("quote it, \"{c}\", to draw it as written"),
))
}
c if is_control(c) => return Err(control()),
c if c.is_ascii() => {
return Err(Unsupported::new(
format!("{c}, a character Excel draws only quoted"),
format!("quote it, \"{c}\", to draw it as written"),
))
}
c => toks.push(Token::Literal(c.to_string())),
}
i += 1;
}
Ok(sections)
}
fn is_control(c: char) -> bool {
(c as u32) < 0x20 || c == '\u{7F}'
}
fn control() -> Unsupported {
Unsupported::new("a control character", "drop it")
}
fn letter(chars: &[char], i: usize) -> Unsupported {
let word: String = chars[i..].iter().take(7).collect();
if word.eq_ignore_ascii_case("general") {
return Unsupported::new(
"General inside a code",
"write the digits with 0 and # in its place",
);
}
let c = chars[i];
let sign = chars.get(i + 1).copied();
if (c == 'E' || c == 'e') && (sign == Some('+') || sign == Some('-')) {
let sign = sign.unwrap_or('+');
return Unsupported::new(
format!("{c}{sign}, a scientific exponent"),
"write the digits with 0 and # alone",
);
}
match c.to_ascii_lowercase() {
'y' | 'm' | 'd' | 'h' | 's' | 'a' | 'p' | 'b' | 'e' | 'g' => Unsupported::new(
format!("{c}, a date's or a time's letter"),
format!("quote it, \"{c}\", to draw it as written; this version draws no date or time"),
),
_ => Unsupported::new(
format!("{c}, a letter outside quotes"),
format!("quote it, \"{c}\", to draw it as written"),
),
}
}
fn number_section(toks: Vec<Token>) -> Result<Section, Unsupported> {
if toks.contains(&Token::At) {
return Err(Unsupported::new(
"@ before the fourth section",
"put a text's section fourth, after zero's",
));
}
if toks.iter().filter(|t| **t == Token::Point).count() > 1 {
return Err(Unsupported::new(
"a second point",
"keep one, or quote the other",
));
}
let point = toks.iter().position(|t| *t == Token::Point);
let is_digit = |t: &Token| matches!(t, Token::Digit(_));
let before_point = |k: usize| point.map_or(true, |p| k < p);
let mut s = Section {
placeholders: toks.iter().any(is_digit),
..Section::default()
};
let mut zeros: Vec<bool> = Vec::new();
for (k, t) in toks.iter().enumerate() {
match t {
Token::Digit(zero) => {
if before_point(k) {
s.integer += 1;
zeros.push(*zero);
} else {
s.decimals.push(*zero);
}
s.parts.push(Part::Digit(*zero));
}
Token::Point | Token::Percent if !s.placeholders => {
let what = if *t == Token::Point { "." } else { "%" };
return Err(Unsupported::new(
format!("{what} beside no digit placeholder"),
format!("write a digit with 0 or #, or quote it, \"{what}\""),
));
}
Token::Point => s.parts.push(Part::Point),
Token::Percent => {
s.shift += 2;
s.parts.push(Part::Percent);
}
Token::Literal(text) => match s.parts.last_mut() {
Some(Part::Literal(last)) => last.push_str(text),
_ => s.parts.push(Part::Literal(text.clone())),
},
Token::Comma => {
if !toks[..k].iter().any(is_digit) {
return Err(Unsupported::new(
"a comma before any digit placeholder",
"drop it, or quote it, \",\"",
));
}
match toks[k + 1..].iter().position(is_digit).map(|j| k + 1 + j) {
None => s.shift -= 3,
Some(j) if before_point(j) => s.grouping = true,
Some(_) if !before_point(k) => {
return Err(Unsupported::new(
"a comma among the decimals",
"drop it, or quote it, \",\"",
))
}
Some(_) => {
return Err(Unsupported::new(
"a comma just before the point",
"move it after the last digit placeholder for thousandths, or drop it",
))
}
}
}
Token::At => unreachable!("refused above"),
}
}
if s.decimals.len() > MOST_DECIMALS {
return Err(Unsupported::new(
format!("more than {MOST_DECIMALS} decimals"),
format!("keep {MOST_DECIMALS}, Excel's most"),
));
}
s.min_integer = zeros
.iter()
.position(|zero| *zero)
.map_or(0, |first| zeros.len() - first);
Ok(s)
}
fn text_section(toks: Vec<Token>) -> Result<Section, Unsupported> {
let mut s = Section::default();
for t in toks {
let what = match t {
Token::Literal(text) => {
match s.parts.last_mut() {
Some(Part::Literal(last)) => last.push_str(&text),
_ => s.parts.push(Part::Literal(text)),
}
continue;
}
Token::At => {
s.parts.push(Part::At);
continue;
}
Token::Digit(true) => "0",
Token::Digit(false) => "#",
Token::Point => ".",
Token::Comma => ",",
Token::Percent => "%",
};
return Err(Unsupported::new(
format!("{what} in a text's section"),
format!("quote it, \"{what}\", to draw it as written"),
));
}
Ok(s)
}
impl Section {
fn text_of(&self, text: &str) -> String {
let mut out = String::new();
for p in &self.parts {
match p {
Part::Literal(l) => out.push_str(l),
Part::At => out.push_str(text),
_ => {}
}
}
out
}
fn draw(&self, magnitude: f64, out: &mut String) -> bool {
if !self.placeholders {
for p in &self.parts {
if let Part::Literal(l) = p {
out.push_str(l);
}
}
return false;
}
let (digits, point) = decimal(magnitude);
let (int, frac) = rounded(&digits, point + self.shift, self.decimals.len());
let zero = int.is_empty() && frac.iter().all(|&d| d == 0);
let mut kept = frac.len();
while kept > 0 && !self.decimals[kept - 1] && frac[kept - 1] == 0 {
kept -= 1;
}
let mut int = int;
if int.len() < self.min_integer {
let mut padded = vec![0; self.min_integer - int.len()];
padded.extend_from_slice(&int);
int = padded;
}
let len = int.len();
let push_digit = |j: usize, out: &mut String| {
out.push(char::from(b'0' + int[j]));
let from_right = len - 1 - j;
if self.grouping && from_right > 0 && from_right % 3 == 0 {
out.push(',');
}
};
let mut after_point = false;
let (mut k, mut d) = (0, 0);
for p in &self.parts {
match p {
Part::Digit(_) if !after_point => {
let r = self.integer - 1 - k;
if len > r {
let first = if k == 0 { 0 } else { len - 1 - r };
for j in first..=len - 1 - r {
push_digit(j, out);
}
}
k += 1;
}
Part::Digit(_) => {
if d < kept {
out.push(char::from(b'0' + frac[d]));
}
d += 1;
}
Part::Point => {
if self.integer == 0 {
for j in 0..len {
push_digit(j, out);
}
}
out.push('.');
after_point = true;
}
Part::Percent => out.push('%'),
Part::Literal(l) => out.push_str(l),
Part::At => {}
}
}
zero
}
}
fn decimal(magnitude: f64) -> (Vec<u8>, i32) {
if magnitude == 0.0 {
return (Vec::new(), 0);
}
let spelled = |text: String| -> (Vec<u8>, i32) {
let (mantissa, exponent) = text.split_once('e').expect("an exponent");
let digits: Vec<u8> = mantissa
.bytes()
.filter(u8::is_ascii_digit)
.map(|b| b - b'0')
.collect();
(
digits,
exponent.parse::<i32>().expect("a whole exponent") + 1,
)
};
let (mut digits, mut point) = spelled(format!("{magnitude:e}"));
if digits.len() == 16 && digits[15] == 5 && exactly(magnitude, &digits, point) {
digits.pop();
} else if digits.len() > 15 {
(digits, point) = spelled(format!("{magnitude:.14e}"));
}
while digits.last() == Some(&0) {
digits.pop();
}
(digits, point)
}
fn exactly(magnitude: f64, digits: &[u8], point: i32) -> bool {
let d = digits
.iter()
.fold(0u128, |acc, &x| acc * 10 + u128::from(x));
let p = point - digits.len() as i32;
if p >= 0 {
if p > 20 || magnitude.fract() != 0.0 || magnitude >= 1e38 {
return false;
}
magnitude as u128 == d * 10u128.pow(p as u32)
} else {
let q = (-p) as u32;
if q > 40 {
return false;
}
let five = 5u128.pow(q);
if d % five != 0 {
return false;
}
let scaled = magnitude * 2f64.powi(q as i32);
scaled.fract() == 0.0 && scaled < 1e38 && scaled as u128 == d / five
}
}
fn rounded(digits: &[u8], point: i32, decimals: usize) -> (Vec<u8>, Vec<u8>) {
let at = |j: i32| -> u8 {
if j < 0 {
0
} else {
digits.get(j as usize).copied().unwrap_or(0)
}
};
let keep = point + decimals as i32;
let mut kept: Vec<u8> = (0..keep.max(0)).map(at).collect();
if keep >= 0 && at(keep) >= 5 {
let mut i = kept.len();
loop {
if i == 0 {
kept.insert(0, 1);
break;
}
i -= 1;
if kept[i] == 9 {
kept[i] = 0;
} else {
kept[i] += 1;
break;
}
}
}
if kept.len() < decimals {
let mut padded = vec![0; decimals - kept.len()];
padded.extend_from_slice(&kept);
kept = padded;
}
let split = kept.len() - decimals;
let frac = kept[split..].to_vec();
let int = match kept[..split].iter().position(|&x| x != 0) {
Some(lead) => kept[lead..split].to_vec(),
None => Vec::new(),
};
(int, frac)
}
#[cfg(test)]
mod tests {
use super::*;
fn code(text: &str) -> Code {
Code::parse(text).unwrap_or_else(|u| panic!("{text}: {u:?}"))
}
fn drawn(text: &str, x: f64) -> String {
code(text).text(&Value::Number(x)).expect("a number draws")
}
fn refused(text: &str) -> Unsupported {
match Code::parse(text) {
Ok(c) => panic!("{text} was taken: {c:?}"),
Err(u) => u,
}
}
#[test]
fn a_number_draws_with_its_decimals() {
assert_eq!(drawn("0.00", 1.0), "1.00");
assert_eq!(drawn("0.00", 12.3), "12.30");
assert_eq!(drawn("0.00", 0.0), "0.00");
assert_eq!(drawn("0.00", -0.0), "0.00");
assert_eq!(drawn("0", 0.4), "0");
assert_eq!(drawn("#,##0.00", 1234.5), "1,234.50");
assert_eq!(drawn("#,##0", 1234567.0), "1,234,567");
assert_eq!(drawn("#,##0", 999.0), "999");
assert_eq!(drawn("0.00", 1e16), "10000000000000000.00");
}
#[test]
fn rounding_is_excel_s_half_away_from_zero_over_fifteen_digits() {
assert_eq!(drawn("0.00", 2.675), "2.68");
assert_eq!(drawn("0.00", 1.005), "1.01");
assert_eq!(drawn("0.00", 0.125), "0.13");
assert_eq!(drawn("0", 2.5), "3");
assert_eq!(drawn("0", 0.5), "1");
assert_eq!(drawn("0", -2.5), "-3");
assert_eq!(drawn("0.00", 9.995), "10.00");
assert_eq!(drawn("0.00", 0.005), "0.01");
assert_eq!(drawn("0.00", 0.0049), "0.00");
assert_eq!(drawn("0.00", 0.1 + 0.2), "0.30");
assert_eq!(drawn("0", 123456789012345678.0), "123456789012346000");
assert_eq!(drawn("0", 1234567890123455.0), "1234567890123450");
assert_eq!(drawn("0", 1234567890123465.0), "1234567890123460");
assert_eq!(drawn("0", 617283945061727.5), "617283945061727");
assert_eq!(drawn("0", 1234567890123455.5), "1234567890123460");
assert_eq!(
drawn("0.000000000000000", 0.2234567890123455),
"0.223456789012346"
);
assert_eq!(drawn("0", 1e20), "100000000000000000000");
assert_eq!(drawn("0.00", 5e-324), "0.00");
}
#[test]
fn sections_choose_by_sign_before_rounding() {
assert_eq!(drawn("0.00", -1.5), "-1.50");
assert_eq!(drawn("0.00", -0.001), "0.00");
assert_eq!(drawn("0", -0.4), "0");
assert_eq!(drawn("#,##0.00", -0.001), "0.00");
assert_eq!(drawn("0.00;(0.00)", -1.5), "(1.50)");
assert_eq!(drawn("0.00;(0.00)", -0.001), "(0.00)");
assert_eq!(drawn("0.00;(0.00)", 0.0), "0.00");
assert_eq!(drawn("0.00;(0.00);\"zero\"", 0.0), "zero");
assert_eq!(drawn("0.00;(0.00);\"zero\"", -0.0), "zero");
assert_eq!(drawn("0.00;(0.00);\"zero\"", 0.001), "0.00");
assert_eq!(drawn("▲0.00;▼0.00;=0.00", 1.5), "▲1.50");
assert_eq!(drawn("▲0.00;▼0.00;=0.00", -1.5), "▼1.50");
assert_eq!(drawn("▲0.00;▼0.00;=0.00", 0.0), "=0.00");
assert_eq!(drawn("0;;0", -3.0), "");
assert_eq!(drawn("0.00;-0.00", -2.0), "-2.00");
assert_eq!(drawn("\"x\"", -5.0), "-x");
}
#[test]
fn placeholders_draw_digits_zeros_and_nothing() {
assert_eq!(drawn("#", 0.0), "");
assert_eq!(drawn("#,###", 0.0), "");
assert_eq!(drawn("#.##", 0.5), ".5");
assert_eq!(drawn("0.#", 1.0), "1.");
assert_eq!(drawn("0.0#", 1.5), "1.5");
assert_eq!(drawn("0.0#", 1.25), "1.25");
assert_eq!(drawn("0.#0", 1.5), "1.50");
assert_eq!(drawn("000", 5.0), "005");
assert_eq!(drawn("#0#", 5.0), "05");
assert_eq!(drawn("000-00-0000", 123456789.0), "123-45-6789");
assert_eq!(drawn("000-00-0000", 5.0), "000-00-0005");
assert_eq!(drawn("0-0", 123.0), "12-3");
assert_eq!(drawn(".00", 12.5), "12.50");
assert_eq!(drawn(".00", 0.5), ".50");
}
#[test]
fn percents_and_thousandths_move_the_point_in_decimal() {
assert_eq!(drawn("0%", 0.5), "50%");
assert_eq!(drawn("0.00%", 0.12345), "12.35%");
assert_eq!(drawn("0.0%", 0.145), "14.5%");
assert_eq!(drawn("#,##0,", 1234567.891), "1,235");
assert_eq!(drawn("0.0,,", 12200000.0), "12.2");
assert_eq!(drawn("#,##0,\"K\"", 2500.0), "3K");
assert_eq!(drawn("0%%", 0.5), "5000%%");
}
#[test]
fn literals_draw_as_written() {
assert_eq!(drawn("\\$#,##0.00", 1234.5), "$1,234.50");
assert_eq!(drawn("$#,##0.00", -5.0), "-$5.00");
assert_eq!(drawn("0.00\" kg\"", 2.0), "2.00 kg");
assert_eq!(drawn("(000) 000-0000", 5551234567.0), "(555) 123-4567");
assert_eq!(drawn("#,##0 ;(#,##0)", 1234.0), "1,234 ");
assert_eq!(drawn("#,##0 ;(#,##0)", -1234.0), "(1,234)");
assert_eq!(drawn("\\€#,##0.00", 3.0), "€3.00");
}
#[test]
fn a_text_draws_by_the_fourth_section_alone() {
let c = code("0;-0;0;\"<\"@\">\"");
assert_eq!(c.text(&Value::Text("abc".into())), Some("<abc>".into()));
assert_eq!(code("0.00").text(&Value::Text("abc".into())), None);
assert_eq!(
code("0;-0;0;").text(&Value::Text("abc".into())),
Some(String::new())
);
assert_eq!(code("0.00").text(&Value::Bool(true)), None);
assert_eq!(code("0.00").text(&Value::Empty), None);
assert_eq!(code("0.00").text(&Value::Number(f64::INFINITY)), None);
}
#[test]
fn excel_s_built_ins_are_known_by_their_codes() {
assert_eq!(code("0.00").builtin(), Some(2));
assert_eq!(code("#,##0.00").builtin(), Some(4));
assert_eq!(code("#,##0 ;(#,##0)").builtin(), Some(37));
assert_eq!(code("0.000").builtin(), None);
assert_eq!(code("▲0.00;▼0.00;=0.00").builtin(), None);
assert_eq!(code("0.00"), code("0.00"));
assert_ne!(code("0.00"), code("0.00;-0.00"));
}
#[test]
fn what_this_version_does_not_draw_is_named() {
let named = |text: &str, word: &str| {
let u = refused(text);
assert!(u.what.contains(word), "{text}: {u:?}");
assert!(!u.fix.is_empty(), "{text}: {u:?}");
};
named("", "no code");
named("General", "General format");
named("general", "General format");
named("@", "Text format");
named("[Red]0.00", "[Red]");
named("0.00;[Red]-0.00", "color");
named("yyyy-mm-dd", "date");
named("0.00E+00", "E+");
named("# ?/?", "?");
named("0/0", "/");
named("_(0_)", "_");
named("* 0", "*");
named("General;-General", "General inside");
named("0 kg", "k");
named("0 1", "a digit");
named("0|", "|");
named("0.00.0", "second point");
named("0;0;0;@;0", "fifth section");
named("@;0", "@ before");
named("0;-0;0;0", "in a text's section");
named(",0", "before any digit");
named("0.0,0", "among the decimals");
named("0,.00", "just before the point");
named("\"x\".", "beside no digit");
named("\"open", "quote left open");
named("0\\", "backslash");
named(&format!("0.{}", "0".repeat(31)), "decimals");
named(&"0".repeat(256), "characters");
named("0\t", "control");
assert!(Code::parse(&format!("0.{}", "0".repeat(30))).is_ok());
}
}