use std::cmp::Ordering;
const MAX_FORMAT_LENGTH: usize = 255;
const MAX_PLACEHOLDERS: usize = 30;
const MAX_SUB_SECOND_DIGITS: usize = 9;
const VALUE_LIMIT: f64 = 1e9;
const MAX_SECTIONS: usize = 4;
const SERIAL_LIMIT: i64 = 2_958_465;
const SERIAL_LIMIT_1904: i64 = SERIAL_LIMIT - 1_462;
const MAX_SCALED: i128 = 10_i128.pow(30);
const fn builtin(id: u32) -> Option<&'static str> {
Some(match id {
0 => "General",
1 => "0",
2 => "0.00",
3 => "#,##0",
4 => "#,##0.00",
5 => "\"$\"#,##0_);(\"$\"#,##0)",
6 => "\"$\"#,##0_);[Red](\"$\"#,##0)",
7 => "\"$\"#,##0.00_);(\"$\"#,##0.00)",
8 => "\"$\"#,##0.00_);[Red](\"$\"#,##0.00)",
9 => "0%",
10 => "0.00%",
11 => "0.00E+00",
12 => "# ?/?",
13 => "# ??/??",
14 => "mm-dd-yy",
15 => "d-mmm-yy",
16 => "d-mmm",
17 => "mmm-yy",
18 => "h:mm AM/PM",
19 => "h:mm:ss AM/PM",
20 => "h:mm",
21 => "h:mm:ss",
22 => "m/d/yy h:mm",
37 => "#,##0_);(#,##0)",
38 => "#,##0_);[Red](#,##0)",
39 => "#,##0.00_);(#,##0.00)",
40 => "#,##0.00_);[Red](#,##0.00)",
41 => "_(* #,##0_);_(* (#,##0);_(* \"-\"_);_(@_)",
42 => "_(\"$\"* #,##0_);_(\"$\"* (#,##0);_(\"$\"* \"-\"_);_(@_)",
43 => "_(* #,##0.00_);_(* (#,##0.00);_(* \"-\"??_);_(@_)",
44 => "_(\"$\"* #,##0.00_);_(\"$\"* (#,##0.00);_(\"$\"* \"-\"??_);_(@_)",
45 => "mm:ss",
46 => "[h]:mm:ss",
47 => "mm:ss.0",
48 => "##0.0E+0",
49 => "@",
_ => return None,
})
}
#[must_use]
pub(crate) fn is_general(id: u32, code: Option<&str>) -> bool {
match code {
Some("") => false,
Some(code) => is_general_code(code),
None => builtin(id).is_some_and(is_general_code),
}
}
fn is_general_code(code: &str) -> bool {
split_sections(code).map_or_else(
|| general_section(code),
|sections| sections.iter().all(|section| general_section(section)),
)
}
fn general_section(section: &str) -> bool {
split_section(section, 0.0)
.0
.trim()
.eq_ignore_ascii_case("General")
}
#[must_use]
pub(crate) fn display(value: f64, id: u32, code: Option<&str>, date1904: bool) -> Option<String> {
if !value.is_finite() {
return None;
}
let format = match code {
Some("") => return None,
Some(code) => code,
None => builtin(id)?,
};
if format.len() > MAX_FORMAT_LENGTH {
return None;
}
let sections = split_sections(format)?;
let Selection { body, signed } = select_section(§ions, value)?;
if body.trim().is_empty() {
return None;
}
let tokens = tokenize(body);
let rendered = if is_date(&tokens) {
render_date(value, date1904, &tokens)?
} else {
let Rendered { text, shows_number } = render_number(value.abs(), &tokens)?;
if signed && value < 0.0 && !tokens.iter().any(|tok| matches!(tok, Tok::Digit(_))) {
return None;
}
if signed && value < 0.0 && shows_number && !text.starts_with('-') {
format!("-{text}")
} else {
text
}
};
let rendered = rendered.trim();
if rendered.is_empty() {
return None;
}
Some(rendered.to_owned())
}
fn split_sections(format: &str) -> Option<Vec<&str>> {
let bytes = format.as_bytes();
let mut sections = Vec::new();
let mut start = 0usize;
let mut index = 0usize;
while index < bytes.len() {
match bytes[index] {
b'"' => {
index += 1;
while index < bytes.len() && bytes[index] != b'"' {
index += 1;
}
index += 1;
}
b'\\' => index += 2,
b'[' => {
index += 1;
while index < bytes.len() && bytes[index] != b']' {
index += 1;
}
index += 1;
}
b';' => {
sections.push(&format[start..index]);
if sections.len() == MAX_SECTIONS {
return None;
}
index += 1;
start = index;
}
_ => index += 1,
}
}
sections.push(&format[start..]);
Some(sections)
}
struct Selection<'a> {
body: &'a str,
signed: bool,
}
fn select_section<'a>(sections: &[&'a str], value: f64) -> Option<Selection<'a>> {
let mut split = [("", None); MAX_SECTIONS];
for (slot, section) in split.iter_mut().zip(sections) {
*slot = split_section(section, value);
}
let split = &split[..sections.len()];
if split
.iter()
.take(2)
.any(|(_, condition)| condition.is_some())
{
return conditional_section(split, value);
}
let position = sign_position(split.len(), value)?;
Some(Selection {
body: split[position].0,
signed: split.len() == 1,
})
}
fn conditional_section<'a>(
split: &[(&'a str, Option<Condition>)],
value: f64,
) -> Option<Selection<'a>> {
let at = |index: usize| split.get(index).copied().unwrap_or(split[0]);
let (first, second) = (at(0).1, at(1).1);
if first.is_none() && value > 0.0 {
return Some(Selection {
body: at(0).0,
signed: false,
});
}
if let Some(held) = first.filter(|it| it.held) {
return Some(Selection {
body: at(0).0,
signed: !held.names_the_negative(),
});
}
if let Some(held) = second.filter(|it| it.held) {
return Some(Selection {
body: at(1).0,
signed: !held.names_the_negative(),
});
}
let (index, signed) = if second.is_none() && value < 0.0 {
(
1,
!(split.len() >= 3 || first.is_some_and(Condition::covers_the_positive)),
)
} else if split.len() >= 3 {
(2, true)
} else if value < 0.0 || split.len() == 1 {
return None;
} else {
(usize::from(!(first.is_some() && second.is_some())), false)
};
Some(Selection {
body: at(index).0,
signed,
})
}
fn sign_position(count: usize, value: f64) -> Option<usize> {
match count {
1 => Some(0),
2 => Some(usize::from(value < 0.0)),
3 | 4 => Some(if value > 0.0 {
0
} else if value < 0.0 {
1
} else {
2
}),
_ => None,
}
}
fn split_section(section: &str, value: f64) -> (&str, Option<Condition>) {
let mut rest = section;
let mut condition = None;
let mut body = None;
while let Some(inner) = rest.trim_start().strip_prefix('[') {
let Some((bracket, after)) = inner.split_once(']') else {
break;
};
if body.is_none()
&& (elapsed_unit(bracket).is_some() || !currency_symbol(bracket).is_empty())
{
body = Some(rest.trim_start());
}
if condition.is_none()
&& let Some(read) = condition_of(bracket, value)
{
condition = Some(read);
}
rest = after;
}
(body.unwrap_or(rest), condition)
}
const CONDITIONS: [&str; 6] = ["<=", ">=", "<>", "<", ">", "="];
#[derive(Clone, Copy)]
struct Condition {
operator: &'static str,
threshold: f64,
held: bool,
}
impl Condition {
fn names_the_negative(self) -> bool {
match self.operator {
"=" | "<=" => self.threshold < 0.0,
"<" => self.threshold <= 0.0,
_ => false,
}
}
fn covers_the_positive(self) -> bool {
match self.operator {
">" => self.threshold < 0.0,
">=" => self.threshold <= 0.0,
_ => false,
}
}
}
fn condition_of(bracket: &str, value: f64) -> Option<Condition> {
let (operator, operand) = CONDITIONS
.iter()
.find_map(|operator| bracket.strip_prefix(operator).map(|rest| (*operator, rest)))?;
let Some(threshold) = operand.trim().parse::<f64>().ok() else {
return Some(Condition {
operator,
threshold: f64::NAN,
held: false,
});
};
let ordering = value.partial_cmp(&threshold);
let held = match operator {
"<" => ordering == Some(Ordering::Less),
">" => ordering == Some(Ordering::Greater),
"<=" => matches!(ordering, Some(Ordering::Less | Ordering::Equal)),
">=" => matches!(ordering, Some(Ordering::Greater | Ordering::Equal)),
"<>" => ordering != Some(Ordering::Equal),
_ => ordering == Some(Ordering::Equal),
};
Some(Condition {
operator,
threshold,
held,
})
}
#[derive(Clone, Copy)]
enum Unit {
Year,
Month,
Day,
Hour,
Minute,
Second,
AmPm,
}
#[derive(Clone, Copy)]
enum Tok {
Lit(char),
Raw(char),
Digit(char),
Dot,
Comma,
Percent,
Slash,
Text,
Directive(char),
Star,
ExpSign(char),
Date(Unit),
Elapsed(Unit),
}
fn tokenize(section: &str) -> Vec<Tok> {
let chars: Vec<char> = section.chars().collect();
let mut tokens = Vec::new();
let mut index = 0usize;
while index < chars.len() {
let current = chars[index];
match current {
'"' => {
index += 1;
while index < chars.len() && chars[index] != '"' {
tokens.push(Tok::Lit(chars[index]));
index += 1;
}
index += 1;
}
'\\' => {
if let Some(&escaped) = chars.get(index + 1) {
tokens.push(Tok::Lit(escaped));
}
index += 2;
}
'_' => index += 2,
'*' => {
tokens.push(Tok::Star);
index += 2;
}
'[' => {
let close = chars[index + 1..]
.iter()
.position(|&c| c == ']')
.map_or(chars.len(), |offset| index + 1 + offset);
let inner: String = chars[index + 1..close].iter().collect();
if let Some(unit) = elapsed_unit(&inner) {
tokens.push(Tok::Elapsed(unit));
} else {
for symbol in currency_symbol(&inner).chars() {
tokens.push(Tok::Directive(symbol));
}
}
index = close + 1;
}
'0' | '#' | '?' => {
tokens.push(Tok::Digit(current));
index += 1;
}
'.' => {
tokens.push(Tok::Dot);
index += 1;
}
',' => {
tokens.push(Tok::Comma);
index += 1;
}
'%' => {
tokens.push(Tok::Percent);
index += 1;
}
'/' => {
tokens.push(Tok::Slash);
index += 1;
}
'@' => {
tokens.push(Tok::Text);
index += 1;
}
'y' | 'Y' => push_date_run(&chars, &mut index, Unit::Year, &mut tokens),
'd' | 'D' => push_date_run(&chars, &mut index, Unit::Day, &mut tokens),
'h' | 'H' => push_date_run(&chars, &mut index, Unit::Hour, &mut tokens),
's' | 'S' => push_date_run(&chars, &mut index, Unit::Second, &mut tokens),
'm' | 'M' => push_date_run(&chars, &mut index, Unit::Month, &mut tokens),
'a' | 'A' if starts_with_ignore_ascii_case(&chars, index, "am/pm") => {
tokens.push(Tok::Date(Unit::AmPm));
index += 5;
}
'a' | 'A' if starts_with_ignore_ascii_case(&chars, index, "a/p") => {
tokens.push(Tok::Date(Unit::AmPm));
index += 3;
}
'e' | 'E' if matches!(chars.get(index + 1), Some('+' | '-')) => {
tokens.push(Tok::ExpSign(chars[index + 1]));
index += 2;
}
other => {
tokens.push(Tok::Raw(other));
index += 1;
}
}
}
tokens
}
fn push_date_run(chars: &[char], index: &mut usize, unit: Unit, tokens: &mut Vec<Tok>) {
let letter = chars[*index].to_ascii_lowercase();
tokens.push(Tok::Date(unit));
*index += 1;
while *index < chars.len() && chars[*index].to_ascii_lowercase() == letter {
*index += 1;
}
}
fn starts_with_ignore_ascii_case(chars: &[char], at: usize, needle: &str) -> bool {
let needle: Vec<char> = needle.chars().collect();
chars.len() >= at + needle.len()
&& chars[at..at + needle.len()]
.iter()
.zip(&needle)
.all(|(current, expected)| current.to_ascii_lowercase() == *expected)
}
fn elapsed_unit(inner: &str) -> Option<Unit> {
match inner.to_ascii_lowercase().as_str() {
"h" | "hh" => Some(Unit::Hour),
"m" | "mm" => Some(Unit::Minute),
"s" | "ss" => Some(Unit::Second),
_ => None,
}
}
fn currency_symbol(inner: &str) -> &str {
let Some(rest) = inner.strip_prefix('$') else {
return "";
};
match rest.split_once('-') {
Some((symbol, _)) if !symbol.is_empty() => symbol,
Some(_) => "",
None => rest,
}
}
fn is_date(tokens: &[Tok]) -> bool {
tokens
.iter()
.any(|tok| matches!(tok, Tok::Date(_) | Tok::Elapsed(_)))
}
#[derive(Default)]
#[expect(
clippy::struct_excessive_bools,
reason = "each unit is an independent yes/no the renderer reads"
)]
struct DateShape {
has_date: bool,
has_time: bool,
hour: bool,
minute: bool,
second: bool,
elapsed_hour: bool,
elapsed_minute: bool,
elapsed_second: bool,
}
fn date_shape(tokens: &[Tok]) -> DateShape {
let mut shape = DateShape::default();
for (index, tok) in tokens.iter().enumerate() {
match tok {
Tok::Date(Unit::Month) if minute_role(tokens, index) => {
shape.has_time = true;
shape.minute = true;
}
Tok::Date(Unit::Year | Unit::Month | Unit::Day) => shape.has_date = true,
Tok::Date(Unit::Hour) => {
shape.has_time = true;
shape.hour = true;
}
Tok::Date(Unit::Second) => {
shape.has_time = true;
shape.second = true;
}
Tok::Date(Unit::AmPm) => shape.has_time = true,
Tok::Elapsed(Unit::Hour) => {
shape.has_time = true;
shape.hour = true;
shape.elapsed_hour = true;
}
Tok::Elapsed(Unit::Minute) => {
shape.has_time = true;
shape.minute = true;
shape.elapsed_minute = true;
}
Tok::Elapsed(Unit::Second) => {
shape.has_time = true;
shape.second = true;
shape.elapsed_second = true;
}
_ => {}
}
}
shape
}
fn minute_role(tokens: &[Tok], index: usize) -> bool {
let is_date_token = |tok: &&Tok| matches!(tok, Tok::Date(_) | Tok::Elapsed(_));
let preceding = tokens[..index].iter().rev().find(is_date_token);
if matches!(
preceding,
Some(Tok::Date(Unit::Hour) | Tok::Elapsed(Unit::Hour))
) {
return true;
}
let following = tokens[index + 1..].iter().find(is_date_token);
matches!(
following,
Some(Tok::Date(Unit::Second) | Tok::Elapsed(Unit::Second))
)
}
#[expect(
clippy::cast_possible_truncation,
reason = "a value bounded by VALUE_LIMIT has a day count that fits i64"
)]
fn render_date(value: f64, date1904: bool, tokens: &[Tok]) -> Option<String> {
if !(0.0..=VALUE_LIMIT).contains(&value) {
return None;
}
let shape = date_shape(tokens);
let places = fractional_second_places(tokens);
if places > MAX_SUB_SECOND_DIGITS {
return None;
}
let mut days = value.trunc() as i64;
let (mut seconds, ticks) = split_seconds(value.fract() * 86_400.0, places);
if seconds >= 86_400 {
seconds -= 86_400;
days += 1;
}
let fraction = if places == 0 {
String::new()
} else {
format!(".{ticks:0places$}")
};
if shape.elapsed_hour || shape.elapsed_minute || shape.elapsed_second {
let total = days * 86_400 + seconds;
return Some(duration(&shape, total, &fraction));
}
let hour = seconds / 3_600;
let minute = (seconds % 3_600) / 60;
let second = seconds % 60;
let clock = format!("{hour:02}:{minute:02}:{second:02}{fraction}");
if shape.has_date {
let (year, month, day) = civil_date(days, date1904)?;
let mut out = format!("{year:04}-{month:02}-{day:02}");
if shape.has_time || seconds != 0 {
out.push(' ');
out.push_str(&clock);
}
return Some(out);
}
let mut parts = Vec::new();
if shape.hour {
parts.push(format!("{hour:02}"));
}
if shape.minute {
parts.push(format!("{minute:02}"));
}
if shape.second {
parts.push(format!("{second:02}{fraction}"));
}
if parts.is_empty() {
return Some(clock);
}
Some(parts.join(":"))
}
#[expect(
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
reason = "the seconds a day holds bound every count here, so each fits both the integer type it \
is cast to and the 53-bit mantissa it is cast from"
)]
fn split_seconds(raw: f64, places: usize) -> (i64, i64) {
if places == 0 {
return (raw.round() as i64, 0);
}
let scale = 10_i64.pow(u32::try_from(places).unwrap_or(0));
let mut ticks = (raw.fract() * scale as f64).round() as i64;
let mut seconds = raw.trunc() as i64;
if ticks >= scale {
ticks -= scale;
seconds += 1;
}
(seconds, ticks)
}
fn fractional_second_places(tokens: &[Tok]) -> usize {
let Some(dot) = tokens.iter().rposition(|tok| matches!(tok, Tok::Dot)) else {
return 0;
};
let places = tokens[dot + 1..]
.iter()
.filter(|tok| matches!(tok, Tok::Digit(_)))
.count();
if places == 0 {
return 0;
}
match tokens[..dot]
.iter()
.rev()
.find(|tok| matches!(tok, Tok::Date(_) | Tok::Elapsed(_)))
{
Some(Tok::Date(Unit::Second) | Tok::Elapsed(Unit::Second)) => places,
_ => 0,
}
}
fn duration(shape: &DateShape, total_seconds: i64, fraction: &str) -> String {
let mut parts = Vec::new();
if shape.hour {
parts.push(format!("{:02}", total_seconds / 3_600));
}
if shape.minute {
let minutes = if shape.elapsed_minute {
total_seconds / 60
} else {
(total_seconds % 3_600) / 60
};
parts.push(format!("{minutes:02}"));
}
if shape.second {
let seconds = if shape.elapsed_second {
total_seconds
} else {
total_seconds % 60
};
parts.push(format!("{seconds:02}{fraction}"));
}
parts.join(":")
}
struct Rendered {
text: String,
shows_number: bool,
}
fn has_digits(digits: &str) -> bool {
digits
.bytes()
.any(|byte| byte.is_ascii_digit() && byte != b'0')
}
#[derive(Clone, Copy)]
enum Point {
Absent,
At(usize),
}
impl Point {
fn is_integer(self, index: usize) -> bool {
match self {
Self::Absent => true,
Self::At(at) => index < at,
}
}
fn is_fraction(self, index: usize) -> bool {
match self {
Self::Absent => false,
Self::At(at) => index > at,
}
}
}
fn decimal_point(tokens: &[Tok], first: usize, last: usize) -> Option<Point> {
let mut point = Point::Absent;
for (index, tok) in tokens.iter().enumerate() {
if matches!(tok, Tok::Dot) {
if matches!(point, Point::At(_)) {
return None;
}
point = Point::At(index);
}
}
if let Point::At(at) = point
&& (at <= first || at >= last)
{
return None;
}
Some(point)
}
fn render_number(value: f64, tokens: &[Tok]) -> Option<Rendered> {
if tokens
.iter()
.any(|tok| matches!(tok, Tok::Slash | Tok::Text))
{
return None;
}
if tokens.iter().any(|tok| matches!(tok, Tok::ExpSign(_))) {
return render_scientific(value, tokens);
}
let (Some(first), Some(last)) = (
tokens.iter().position(|tok| matches!(tok, Tok::Digit(_))),
tokens.iter().rposition(|tok| matches!(tok, Tok::Digit(_))),
) else {
return tokens
.iter()
.any(|tok| matches!(tok, Tok::Lit(_)))
.then(|| Rendered {
text: literal_text(tokens),
shows_number: false,
});
};
let point = decimal_point(tokens, first, last)?;
if tokens[first..=last].iter().copied().any(is_text) {
return render_layered(value, tokens, first, last, point);
}
render_plain(value, tokens, first, last, point)
}
fn is_text(tok: Tok) -> bool {
matches!(
tok,
Tok::Lit(_) | Tok::Raw(_) | Tok::Directive(_) | Tok::Percent
)
}
fn literal_text(tokens: &[Tok]) -> String {
let mut out = String::new();
for tok in tokens {
push_literal(*tok, &mut out);
}
out
}
fn push_literal(tok: Tok, out: &mut String) {
match tok {
Tok::Lit(c) | Tok::Raw(c) | Tok::Directive(c) => out.push(c),
Tok::Percent => out.push('%'),
Tok::Comma => out.push(','),
_ => {}
}
}
#[derive(Default)]
struct PlainShape {
int_zeros: usize,
frac_zeros: usize,
frac_placeholders: usize,
grouping: bool,
}
fn plain_shape(tokens: &[Tok], first: usize, last: usize, point: Point) -> PlainShape {
let mut shape = PlainShape::default();
for (offset, tok) in tokens[first..=last].iter().enumerate() {
let fractional = point.is_fraction(first + offset);
match *tok {
Tok::Digit('0') if fractional => {
shape.frac_placeholders += 1;
shape.frac_zeros += 1;
}
Tok::Digit('0') => shape.int_zeros += 1,
Tok::Digit(_) if fractional => shape.frac_placeholders += 1,
Tok::Comma => shape.grouping = true,
_ => {}
}
}
shape
}
fn leading_commas(tokens: &[Tok]) -> usize {
tokens
.iter()
.take_while(|tok| matches!(tok, Tok::Comma))
.count()
}
fn scaling_commas(tokens: &[Tok], last: usize) -> usize {
leading_commas(&tokens[last + 1..])
}
fn scaled_plain(value: f64, tokens: &[Tok], last: usize, places: i32) -> Option<i128> {
let mut value = value;
for tok in tokens {
if matches!(tok, Tok::Percent) {
value *= 100.0;
}
}
for _ in 0..scaling_commas(tokens, last) {
value /= 1_000.0;
}
scaled_magnitude(value, places)
}
fn digit_text(scaled: i128, shape: &PlainShape) -> Option<String> {
let rendered = magnitude_text(scaled, u32::try_from(shape.frac_placeholders).ok()?)?;
let (int_raw, mut frac_digits) = rendered.find('.').map_or_else(
|| (rendered.clone(), String::new()),
|at| (rendered[..at].to_owned(), rendered[at + 1..].to_owned()),
);
while frac_digits.len() > shape.frac_zeros && frac_digits.ends_with('0') {
frac_digits.pop();
}
let mut int_digits = if int_raw == "0" && shape.int_zeros == 0 {
String::new()
} else {
int_raw
};
while int_digits.len() < shape.int_zeros {
int_digits.insert(0, '0');
}
if shape.grouping {
int_digits = group_thousands(&int_digits);
}
if shape.frac_placeholders > 0 {
int_digits.push('.');
int_digits.push_str(&frac_digits);
}
Some(int_digits)
}
struct Pieces {
digits: String,
prefix: String,
suffix: String,
}
fn plain_pieces(
value: f64,
tokens: &[Tok],
first: usize,
last: usize,
grouping: bool,
point: Point,
) -> Option<Pieces> {
let shape = plain_shape(tokens, first, last, point);
if shape.int_zeros > MAX_PLACEHOLDERS
|| shape.frac_placeholders > MAX_PLACEHOLDERS
|| (shape.grouping && !grouping)
{
return None;
}
let places = i32::try_from(shape.frac_placeholders).ok()?;
let digits = digit_text(scaled_plain(value, tokens, last, places)?, &shape)?;
Some(Pieces {
digits,
prefix: literal_text(&tokens[..first]),
suffix: literal_text(&tokens[last + 1 + scaling_commas(tokens, last)..]),
})
}
fn render_plain(
value: f64,
tokens: &[Tok],
first: usize,
last: usize,
point: Point,
) -> Option<Rendered> {
let Pieces {
digits,
prefix,
suffix,
} = plain_pieces(value, tokens, first, last, true, point)?;
Some(Rendered {
text: format!("{prefix}{digits}{suffix}"),
shows_number: has_digits(&digits),
})
}
fn render_layered(
value: f64,
tokens: &[Tok],
first: usize,
last: usize,
point: Point,
) -> Option<Rendered> {
let body = &tokens[first..=last];
if matches!(point, Point::At(_)) {
return None;
}
let Pieces {
digits,
prefix,
suffix,
} = plain_pieces(value, tokens, first, last, false, point)?;
let places = body
.iter()
.filter(|tok| matches!(tok, Tok::Digit(_)))
.count();
if digits.len() != places {
return None;
}
let mut chars = digits.chars();
let mut out = prefix;
for tok in body {
match *tok {
Tok::Digit(_) => out.push(chars.next()?),
other => push_literal(other, &mut out),
}
}
out.push_str(&suffix);
Some(Rendered {
text: out,
shows_number: has_digits(&digits),
})
}
fn render_scientific(value: f64, tokens: &[Tok]) -> Option<Rendered> {
let marker = tokens
.iter()
.position(|tok| matches!(tok, Tok::ExpSign(_)))?;
let Tok::ExpSign(sign) = tokens[marker] else {
return None;
};
let mantissa_tokens = &tokens[..marker];
let first = mantissa_tokens
.iter()
.position(|tok| matches!(tok, Tok::Digit(_)))?;
let last = mantissa_tokens
.iter()
.rposition(|tok| matches!(tok, Tok::Digit(_)))?;
let exponent_tokens = &tokens[marker + 1..];
let digits_at = exponent_tokens
.iter()
.position(|tok| matches!(tok, Tok::Digit(_)))?;
let last_digit = exponent_tokens
.iter()
.rposition(|tok| matches!(tok, Tok::Digit(_)))?;
if exponent_tokens.iter().any(|tok| matches!(tok, Tok::Dot))
|| exponent_tokens[digits_at..=last_digit]
.iter()
.any(|tok| !matches!(tok, Tok::Digit(_)))
{
return None;
}
let exponent_width = last_digit - digits_at + 1;
let point = decimal_point(mantissa_tokens, first, last)?;
let int_zeros = plain_shape(mantissa_tokens, first, last, point).int_zeros;
let width = mantissa_tokens
.iter()
.enumerate()
.filter(|(index, tok)| matches!(tok, Tok::Digit(_)) && point.is_integer(*index))
.count()
.max(1);
let places = mantissa_tokens
.iter()
.enumerate()
.filter(|(index, tok)| matches!(tok, Tok::Digit(_)) && point.is_fraction(*index))
.count();
if width > MAX_PLACEHOLDERS || places > MAX_PLACEHOLDERS || exponent_width > MAX_PLACEHOLDERS {
return None;
}
let width = i32::try_from(width).ok()?;
let places = i32::try_from(places).ok()?;
let mut value = value;
for tok in tokens {
if matches!(tok, Tok::Percent) {
value *= 100.0;
}
}
let exponent_end = digits_at + exponent_width;
let scaling = leading_commas(&exponent_tokens[exponent_end..]);
let mantissa_scaling = scaling_commas(mantissa_tokens, last);
for _ in 0..scaling + mantissa_scaling {
value /= 1_000.0;
}
let mut exponent = 0i32;
let mut scaled = 0i128;
if value != 0.0 {
exponent = decade(value.abs())?.div_euclid(width) * width;
scaled = scaled_magnitude(value, places - exponent)?;
let ceiling = pow10_int(usize::try_from(places).ok()?)?
.checked_mul(pow10_int(usize::try_from(width).ok()?)?)?;
if scaled >= ceiling {
exponent += width;
scaled = scaled_magnitude(value, places - exponent)?;
}
}
if scaled == 0 && usize::try_from(width).ok()? > int_zeros {
return None;
}
let mantissa = mantissa_text(mantissa_tokens, first, last, scaled, point)?;
if mantissa.trim().is_empty() {
return None;
}
let mut exponent_digits = exponent.unsigned_abs().to_string();
while exponent_digits.len() < exponent_width {
exponent_digits.insert(0, '0');
}
let mut out = mantissa;
out.push('E');
if exponent < 0 {
out.push('-');
} else if sign == '+' {
out.push('+');
}
out.push_str(&literal_text(&exponent_tokens[..digits_at]));
out.push_str(&exponent_digits);
out.push_str(&literal_text(&exponent_tokens[exponent_end + scaling..]));
Some(Rendered {
text: out,
shows_number: scaled != 0,
})
}
fn mantissa_text(
tokens: &[Tok],
first: usize,
last: usize,
scaled: i128,
point: Point,
) -> Option<String> {
if tokens[first..=last].iter().copied().any(is_text) {
return None;
}
let shape = plain_shape(tokens, first, last, point);
if shape.int_zeros > MAX_PLACEHOLDERS || shape.frac_placeholders > MAX_PLACEHOLDERS {
return None;
}
Some(format!(
"{}{}{}",
literal_text(&tokens[..first]),
digit_text(scaled, &shape)?,
literal_text(&tokens[last + 1 + scaling_commas(tokens, last)..])
))
}
fn magnitude_text(scaled: i128, places: u32) -> Option<String> {
let digits = scaled.to_string();
let places = usize::try_from(places).ok()?;
if places == 0 {
return Some(digits);
}
Some(match digits.len().checked_sub(places) {
Some(point) => format!("{}.{}", &digits[..point], &digits[point..]),
None => format!("0.{}{digits}", "0".repeat(places - digits.len())),
})
}
fn scaled_magnitude(value: f64, shift: i32) -> Option<i128> {
if !value.is_finite() {
return None;
}
let magnitude = value.abs();
let text = format!("{magnitude:.14e}");
let (mantissa, exponent) = text.split_once('e')?;
let exponent: i32 = exponent.parse().ok()?;
let digits: i128 = mantissa.replace('.', "").parse().ok()?;
let shift = exponent.checked_sub(14)?.checked_add(shift)?;
let rounded = if shift >= 0 {
let mut value = digits;
for _ in 0..usize::try_from(shift).ok()? {
value = value.checked_mul(10)?;
}
value
} else {
let Some(divisor) = pow10_int(usize::try_from(shift.unsigned_abs()).ok()?) else {
return Some(0);
};
let quotient = digits / divisor;
let remainder = digits % divisor;
if remainder * 2 >= divisor {
quotient + 1
} else {
quotient
}
};
(rounded <= MAX_SCALED).then_some(rounded)
}
fn decade(magnitude: f64) -> Option<i32> {
let text = format!("{magnitude:.14e}");
text.split_once('e')?.1.parse().ok()
}
fn pow10_int(power: usize) -> Option<i128> {
let mut value: i128 = 1;
for _ in 0..power {
value = value.checked_mul(10)?;
}
Some(value)
}
fn group_thousands(digits: &str) -> String {
let mut out = String::with_capacity(digits.len() + digits.len() / 3);
for (index, digit) in digits.chars().enumerate() {
if index > 0 && (digits.len() - index).is_multiple_of(3) {
out.push(',');
}
out.push(digit);
}
out
}
const EPOCH_1900: i64 = days_from_civil(1899, 12, 31);
const EPOCH_1904: i64 = days_from_civil(1904, 1, 1);
fn civil_date(serial_days: i64, date1904: bool) -> Option<(i64, u32, u32)> {
if date1904 {
return (0..=SERIAL_LIMIT_1904)
.contains(&serial_days)
.then(|| civil_from_days(EPOCH_1904 + serial_days));
}
if !(1..=SERIAL_LIMIT).contains(&serial_days) {
return None;
}
Some(match serial_days.cmp(&60) {
Ordering::Equal => (1900, 2, 29),
Ordering::Greater => civil_from_days(EPOCH_1900 + serial_days - 1),
Ordering::Less => civil_from_days(EPOCH_1900 + serial_days),
})
}
#[expect(
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
reason = "the civil year is bounded by the callers, so every era and day count fits the type it \
is cast to and is non-negative where it is cast to an unsigned one"
)]
const fn days_from_civil(year: i64, month: u32, day: u32) -> i64 {
let year = if month <= 2 { year - 1 } else { year };
let era = if year >= 0 { year } else { year - 399 } / 400;
let year_of_era = (year - era * 400) as u64;
let day_of_year =
(153 * (if month > 2 { month - 3 } else { month + 9 }) as u64 + 2) / 5 + (day as u64 - 1);
let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
era * 146_097 + day_of_era as i64 - 719_468
}
#[expect(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
reason = "the day count is bounded by the callers, so every era, year and day-of-month here \
fits the type it is cast to and is non-negative where it is cast to an unsigned one"
)]
const fn civil_from_days(days: i64) -> (i64, u32, u32) {
let days = days + 719_468;
let era = if days >= 0 { days } else { days - 146_096 } / 146_097;
let day_of_era = (days - era * 146_097) as u64;
let year_of_era =
(day_of_era - day_of_era / 1460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
let year = year_of_era as i64 + era * 400;
let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
let month_phase = (5 * day_of_year + 2) / 153;
let day = (day_of_year - (153 * month_phase + 2) / 5 + 1) as u32;
let month = if month_phase < 10 {
month_phase + 3
} else {
month_phase - 9
} as u32;
let year = if month <= 2 { year + 1 } else { year };
(year, month, day)
}
#[cfg(test)]
mod tests {
use super::*;
fn by_id(value: f64, id: u32) -> Option<String> {
display(value, id, None, false)
}
#[test]
fn general_is_detected_from_the_id_or_the_declared_code() {
assert!(is_general(0, None));
assert!(!is_general(0, Some("")));
assert!(!is_general(14, None));
assert!(is_general(5, Some("General")));
assert!(is_general(5, Some("general")));
assert!(!is_general(0, Some("#,##0")));
assert!(is_general(164, Some("[DBNum1][$-804]General")));
assert!(is_general(164, Some(" General ")));
assert!(is_general(164, Some("General;General")));
assert!(!is_general(164, Some("General;0")));
assert_eq!(by_id(5.0, 0), None);
assert_eq!(display(5.0, 164, Some("General"), false), None);
assert_eq!(display(5.0, 164, Some("[$-409]general"), false), None);
}
#[test]
fn plain_formats_round_pad_and_group() {
assert_eq!(by_id(5.4, 1), Some("5".to_owned()));
assert_eq!(by_id(5.5, 1), Some("6".to_owned()));
assert_eq!(by_id(5.5, 2), Some("5.50".to_owned()));
assert_eq!(by_id(1234.0, 3), Some("1,234".to_owned()));
assert_eq!(by_id(1234.5, 3), Some("1,235".to_owned()));
assert_eq!(by_id(1234.5, 4), Some("1,234.50".to_owned()));
}
#[test]
fn percentages_scale_the_value_by_a_hundred() {
assert_eq!(by_id(0.25, 9), Some("25%".to_owned()));
assert_eq!(by_id(0.25, 10), Some("25.00%".to_owned()));
}
#[test]
fn currency_formats_wrap_negatives_in_parentheses() {
assert_eq!(by_id(1234.0, 5), Some("$1,234".to_owned()));
assert_eq!(by_id(-1234.0, 5), Some("($1,234)".to_owned()));
assert_eq!(by_id(-1234.0, 6), Some("($1,234)".to_owned()));
assert_eq!(by_id(-1234.5, 7), Some("($1,234.50)".to_owned()));
assert_eq!(by_id(1234.5, 8), Some("$1,234.50".to_owned()));
}
#[test]
fn accounting_formats_render_zero_as_the_dash_section() {
assert_eq!(by_id(1234.0, 41), Some("1,234".to_owned()));
assert_eq!(by_id(-1234.0, 41), Some("(1,234)".to_owned()));
assert_eq!(by_id(0.0, 41), Some("-".to_owned()));
assert_eq!(by_id(0.0, 42), Some("$-".to_owned()));
assert_eq!(by_id(0.0, 43), Some("-".to_owned()));
assert_eq!(by_id(0.0, 44), Some("$-".to_owned()));
}
#[test]
fn a_tie_rounds_the_way_excel_displays_it() {
assert_eq!(by_id(1.005, 2), Some("1.01".to_owned()));
assert_eq!(by_id(0.145, 2), Some("0.15".to_owned()));
assert_eq!(by_id(0.285, 2), Some("0.29".to_owned()));
assert_eq!(by_id(8.575, 2), Some("8.58".to_owned()));
assert_eq!(by_id(2.5, 1), Some("3".to_owned()));
assert_eq!(by_id(999.5, 11), Some("1.00E+03".to_owned()));
assert_eq!(by_id(0.9995, 11), Some("1.00E+00".to_owned()));
assert_eq!(by_id(-0.4, 1), Some("0".to_owned()));
assert_eq!(by_id(-0.04, 2), Some("-0.04".to_owned()));
assert_eq!(by_id(-0.5, 1), Some("-1".to_owned()));
}
#[test]
fn scientific_formats_normalise_the_mantissa() {
assert_eq!(by_id(12345.678, 11), Some("1.23E+04".to_owned()));
assert_eq!(by_id(12345.0, 48), Some("12.3E+3".to_owned()));
assert_eq!(by_id(1234.0, 48), Some("1.2E+3".to_owned()));
assert_eq!(by_id(1.0, 48), Some("1.0E+0".to_owned()));
assert_eq!(by_id(1_000_000.0, 48), Some("1.0E+6".to_owned()));
assert_eq!(
display(12_345.0, 164, Some("00.0E+0"), false),
Some("01.2E+4".to_owned())
);
}
#[test]
fn a_value_below_the_shown_place_rounds_to_zero() {
assert_eq!(by_id(1e-300, 2), Some("0.00".to_owned()));
assert_eq!(by_id(5e-324, 2), Some("0.00".to_owned()));
assert_eq!(by_id(1e-30, 2), Some("0.00".to_owned()));
assert_eq!(by_id(-1e-300, 2), Some("0.00".to_owned()));
}
#[test]
fn unsupported_formats_come_back_as_none() {
assert_eq!(by_id(1.5, 12), None);
assert_eq!(by_id(1.5, 999), None);
assert_eq!(
display(1.5, 999, Some("0.0"), false),
Some("1.5".to_owned())
);
}
#[test]
fn a_format_or_value_with_no_display_is_declined() {
let long_code = format!("\"{}\"0", "x".repeat(MAX_FORMAT_LENGTH));
assert_eq!(display(1.5, 164, Some(&long_code), false), None);
let many_places = format!("0.{}", "0".repeat(MAX_PLACEHOLDERS + 1));
assert_eq!(display(1.5, 164, Some(&many_places), false), None);
assert_eq!(
display(1.5, 164, Some(&format!("0{}.0", "0".repeat(100))), false),
None
);
assert_eq!(by_id(1e308, 2), None);
assert_eq!(by_id(1e308, 9), None);
assert_eq!(by_id(1e19, 14), None);
assert_eq!(display(1e19, 164, Some("[h]"), false), None);
assert_eq!(display(f64::MAX, 164, Some("0.000"), false), None);
for code in [".00", "0.5", "0.5%", "0.0.0", "0.", "#,##0."] {
assert_eq!(display(0.5, 164, Some(code), false), None, "{code}");
}
assert_eq!(display(5.0, 164, Some("0.0.0"), false), None);
for code in ["0E+0.0", "0E+0,0", "0E+.0", "0E+0."] {
assert_eq!(display(5.0, 164, Some(code), false), None, "{code}");
}
for code in [".0E+0", "0.E+0", "0.0.0E+0", "#.E+0"] {
assert_eq!(display(1234.0, 164, Some(code), false), None, "{code}");
}
}
#[test]
fn a_conditional_section_serves_the_value_its_condition_names() {
let code = Some("[>1000]0.0;[<=1000]0.00");
assert_eq!(display(500.0, 164, code, false), Some("500.00".to_owned()));
assert_eq!(display(1500.0, 164, code, false), Some("1500.0".to_owned()));
let code = Some("[<0]\"neg\";0");
assert_eq!(display(1234.0, 164, code, false), Some("1234".to_owned()));
assert_eq!(display(-1234.0, 164, code, false), Some("neg".to_owned()));
assert_eq!(
display(1234.0, 164, Some("[Red]0.0"), false),
Some("1234.0".to_owned())
);
let code = Some("[$$-409][>100]0.00;0.0");
assert_eq!(
display(5000.0, 164, code, false),
Some("$5000.00".to_owned())
);
assert_eq!(display(5.0, 164, code, false), Some("5.0".to_owned()));
assert_eq!(display(-5.0, 164, code, false), Some("-5.0".to_owned()));
}
#[test]
fn a_colour_before_a_condition_does_not_hide_it() {
let code = Some("[Red][<100]0.0;0");
assert_eq!(display(50.0, 164, code, false), Some("50.0".to_owned()));
assert_eq!(display(5000.0, 164, code, false), Some("5000".to_owned()));
}
#[test]
fn a_conditional_format_keeps_a_negative_values_own_sign() {
let code = Some("[>=1]0;0");
assert_eq!(display(5.0, 164, code, false), Some("5".to_owned()));
assert_eq!(display(-2.0, 164, code, false), Some("-2".to_owned()));
assert_eq!(
display(-12.3, 164, Some("[>=1000000]#,,\" M\";####.00"), false),
Some("-12.30".to_owned())
);
assert_eq!(
display(-2.0, 164, Some("[>=1]0;0;0.00"), false),
Some("2".to_owned())
);
assert_eq!(
display(-2.0, 164, Some("[>=1]0.0;[>=0]0.0;0.00"), false),
Some("-2.00".to_owned())
);
let code = Some("[>=1000000]0.0,,\"M\";[>=1000]0.0,\"K\";0");
assert_eq!(
display(1_021_021.0, 164, code, false),
Some("1.0M".to_owned())
);
assert_eq!(
display(102_102.0, 164, code, false),
Some("102.1K".to_owned())
);
assert_eq!(display(-500.0, 164, code, false), Some("-500".to_owned()));
assert_eq!(
display(-2.0, 164, Some("[>=-1]0;0"), false),
Some("2".to_owned())
);
assert_eq!(
display(-1.0, 164, Some("[<0]0.0;0.00"), false),
Some("1.0".to_owned())
);
assert_eq!(
display(-2.0, 164, Some("[<=-1]0;0"), false),
Some("2".to_owned())
);
assert_eq!(
display(-3.0, 164, Some("[=-3]0;0"), false),
Some("3".to_owned())
);
assert_eq!(
display(-2.0, 164, Some("[<0]-0.0;0.00"), false),
Some("-2.0".to_owned())
);
assert_eq!(
display(5.0, 164, Some("[>10]0;0.0;\"n/a\""), false),
Some("n/a".to_owned())
);
assert_eq!(
display(-1.0, 164, Some("[>10]0;0.0;\"n/a\""), false),
Some("1.0".to_owned())
);
assert_eq!(
display(200.0, 164, Some("0.00;[>100]#,##0"), false),
Some("200.00".to_owned())
);
assert_eq!(display(-5.0, 164, Some("0.00;[>100]#,##0"), false), None);
assert_eq!(display(-2.0, 164, Some("[>=1]0;[>=0]0"), false), None);
assert_eq!(
display(-1.0, 164, Some("[>0]\"plus\";\"minus\""), false),
None
);
}
#[test]
fn the_reader_writes_the_minus_only_where_no_section_does() {
assert_eq!(
display(-0.4, 164, Some("\"v1 \"0"), false),
Some("v1 0".to_owned())
);
assert_eq!(
display(-1.4, 164, Some("\"v1 \"0"), false),
Some("-v1 1".to_owned())
);
assert_eq!(
display(-2.0, 164, Some("-$0.00"), false),
Some("-$2.00".to_owned())
);
}
#[test]
fn a_section_that_shows_nothing_is_none() {
assert_eq!(display(5.0, 164, Some(";;;"), false), None);
assert_eq!(display(-5.0, 164, Some("0;"), false), None);
assert_eq!(display(0.0, 164, Some("#"), false), None);
assert_eq!(display(5.0, 164, Some("#"), false), Some("5".to_owned()));
}
#[test]
fn a_named_point_stands_and_a_zero_mantissa_follows_its_width() {
assert_eq!(
display(1.0, 164, Some("#.#####"), false),
Some("1.".to_owned())
);
assert_eq!(
display(0.0, 164, Some("#.#####"), false),
Some(".".to_owned())
);
assert_eq!(
display(0.5, 164, Some("#.##"), false),
Some(".5".to_owned())
);
assert_eq!(
display(1000.0, 164, Some("#,##0.##"), false),
Some("1,000.".to_owned())
);
assert_eq!(display(0.0, 48, None, false), None);
assert_eq!(display(0.0, 11, None, false), Some("0.00E+00".to_owned()));
assert_eq!(by_id(1234.0, 48), Some("1.2E+3".to_owned()));
}
#[test]
fn a_declared_code_wins_with_literals_and_a_negative_section() {
let code = Some("#,##0.00\" kr\";[Red](#,##0.00\" kr\")");
assert_eq!(
display(1234.5, 164, code, false),
Some("1,234.50 kr".to_owned())
);
assert_eq!(
display(-1234.5, 164, code, false),
Some("(1,234.50 kr)".to_owned())
);
}
#[test]
fn dates_follow_the_1900_system_by_default() {
assert_eq!(by_id(45000.0, 14), Some("2023-03-15".to_owned()));
assert_eq!(by_id(60.0, 14), Some("1900-02-29".to_owned()));
assert_eq!(by_id(61.0, 14), Some("1900-03-01".to_owned()));
}
#[test]
fn dates_follow_the_1904_system_when_asked() {
assert_eq!(
display(45000.0, 14, None, true),
Some("2027-03-16".to_owned())
);
assert_eq!(display(0.0, 14, None, true), Some("1904-01-01".to_owned()));
}
#[test]
fn date_and_time_sections_append_the_clock() {
assert_eq!(by_id(45000.5, 22), Some("2023-03-15 12:00:00".to_owned()));
}
#[test]
fn time_only_sections_print_the_units_they_name() {
assert_eq!(by_id(0.5, 20), Some("12:00".to_owned()));
assert_eq!(by_id(1805.0 / 86_400.0, 45), Some("30:05".to_owned()));
}
#[test]
fn a_section_naming_fractional_seconds_shows_them() {
assert_eq!(by_id(0.500_046_3, 47), Some("00:04.0".to_owned()));
assert_eq!(by_id(0.500_046_3, 45), Some("00:04".to_owned()));
assert_eq!(
display(4.492 / 86_400.0, 164, Some("hh:mm:ss.000"), false),
Some("00:00:04.492".to_owned())
);
assert_eq!(
display(1.5, 164, Some("[h]:mm:ss.0"), false),
Some("36:00:00.0".to_owned())
);
}
#[test]
fn an_elapsed_unit_counts_the_whole_span() {
assert_eq!(by_id(1.5, 46), Some("36:00:00".to_owned()));
assert_eq!(display(1.5, 164, Some("[h]"), false), Some("36".to_owned()));
}
#[test]
fn a_negative_date_serial_is_declined() {
assert_eq!(by_id(-1.0, 14), None);
assert_eq!(by_id(0.0, 14), None);
}
#[test]
fn a_currency_directive_prints_the_symbol_it_names() {
assert_eq!(
display(1234.5, 164, Some("#,##0.00 [$€-407]"), false),
Some("1,234.50 €".to_owned())
);
assert_eq!(
display(1234.5, 164, Some("[$$-409]#,##0.00"), false),
Some("$1,234.50".to_owned())
);
assert_eq!(
display(1234.5, 164, Some("[$USD-409]#,##0.00"), false),
Some("USD1,234.50".to_owned())
);
assert_eq!(
display(1234.5, 164, Some("#,##0.00[$-409]"), false),
Some("1,234.50".to_owned())
);
assert_eq!(
display(1234.5, 164, Some("[$-en-US]#,##0.00"), false),
Some("1,234.50".to_owned())
);
assert_eq!(
display(1234.5, 164, Some("[$-x-sysdate]0.0"), false),
Some("1234.5".to_owned())
);
}
#[test]
fn a_literal_between_placeholders_lays_out_with_the_digits() {
assert_eq!(
display(
5_551_234.0,
164,
Some("[<=9999999]###-####;(###) ###-####"),
false
),
Some("555-1234".to_owned())
);
assert_eq!(
display(
5_551_234_567.0,
164,
Some("[<=9999999]###-####;(###) ###-####"),
false
),
Some("(555) 123-4567".to_owned())
);
assert_eq!(
display(123_456_789.0, 164, Some("000-00-0000"), false),
Some("123-45-6789".to_owned())
);
assert_eq!(
display(1.0, 164, Some("0%00"), false),
Some("1%00".to_owned())
);
assert_eq!(display(5.0, 164, Some("0%0"), false), None);
assert_eq!(
display(1234.5, 164, Some("\\$#,##0.00"), false),
Some("$1,234.50".to_owned())
);
assert_eq!(
display(-1234.5, 164, Some("#,##0.00\" kr\""), false),
Some("-1,234.50 kr".to_owned())
);
}
#[test]
fn a_layered_body_declines_what_it_cannot_hold() {
assert_eq!(
display(1_234_567_890.0, 164, Some("000-00-0000"), false),
None
);
assert_eq!(display(12_345_678.0, 164, Some("###-####"), false), None);
assert_eq!(
display(55_512_345_678.0, 164, Some("(###) ###-####"), false),
None
);
assert_eq!(display(123.0, 164, Some("###-####"), false), None);
assert_eq!(
display(5_551_234.0, 164, Some("(###) ###-####"), false),
None
);
assert_eq!(display(12_345.0, 164, Some("#,##0-#"), false), None);
assert_eq!(display(1234.5, 164, Some("##-###.0"), false), None);
}
#[test]
fn a_fill_contributes_nothing() {
assert_eq!(by_id(1234.0, 41), Some("1,234".to_owned()));
assert_eq!(
display(5_551_234.0, 164, Some("###-####* "), false),
Some("555-1234".to_owned())
);
}
#[test]
fn a_placeholder_less_section_is_text_only_when_the_format_asks_for_it() {
assert_eq!(by_id(0.0, 41), Some("-".to_owned()));
assert_eq!(
display(-5.0, 164, Some("0;\"neg\""), false),
Some("neg".to_owned())
);
assert_eq!(
display(5.0, 164, Some("\"-\""), false),
Some("-".to_owned())
);
for code in [
"[$-419]ДД.ММ.ГГГГ",
"General",
" General ",
"General;General",
"%",
",",
"[$USD-409]",
] {
assert_eq!(display(5.0, 164, Some(code), false), None, "{code}");
}
}
#[test]
fn a_scientific_section_keeps_its_scaling_and_its_literals() {
assert_eq!(
display(0.25, 164, Some("0.00E+00%"), false),
Some("2.50E+01%".to_owned())
);
assert_eq!(
display(0.25, 164, Some("0.00%E+00"), false),
Some("2.50%E+01".to_owned())
);
assert_eq!(
display(0.25, 164, Some("%0.00E+00"), false),
Some("%2.50E+01".to_owned())
);
assert_eq!(
display(1234.5, 164, Some("0.00E+00\" kg\""), false),
Some("1.23E+03 kg".to_owned())
);
assert_eq!(by_id(-1234.5, 11), Some("-1.23E+03".to_owned()));
assert_eq!(
display(1.5, 164, Some("0.0E+0,,"), false),
Some("1.5E-6".to_owned())
);
}
#[test]
fn the_text_placeholder_is_declined_for_a_number() {
assert_eq!(by_id(1.5, 49), None);
assert_eq!(display(1.5, 164, Some("@"), false), None);
assert_eq!(display(1.5, 164, Some("pre @"), false), None);
}
#[test]
fn a_scientific_exponent_is_as_wide_as_the_section_names() {
assert_eq!(by_id(123_456.0, 11), Some("1.23E+05".to_owned()));
assert_eq!(by_id(123_456.0, 48), Some("123.5E+3".to_owned()));
assert_eq!(by_id(0.000_123_4, 11), Some("1.23E-04".to_owned()));
}
}