Shared Rust kernels
format
All modules · quoted at commit 9e1884369a67 of DnaCalc/DnaCalc. Comments are the source's own.
measured_host · 4 lines · calc-func/src/format/mod.rs on GitHub/// The host profiles measured by the scalar formatting questions.
fn measured_host(profile: &FormatProfile) -> bool {
matches!(profile.id, LocaleId::EnZa | LocaleId::CurrentExcelHost)
}
Used by 18 functions.
currency_literal · 9 lines · calc-func/src/format/mod.rs on GitHub/// A literal atom exactly naming the measured currency ignores ASCII case.
/// Quoted longer words stay literal; unquoted characters arrive one at a time.
fn currency_literal<'a>(profile: &FormatProfile, text: &'a str) -> &'a str {
if measured_host(profile) && profile.currency_symbol == "R" && text.eq_ignore_ascii_case("R") {
profile.currency_symbol
} else {
text
}
}
Used by 18 functions.
digits · 8 lines · calc-func/src/format/mod.rs on GitHub/// The magnitude of `x` rounded to 15 significant digits and then to `places` decimal places
/// (negative places round to tens, hundreds and so on), a half away from zero, as its integer
/// digits (no leading zero; empty for zero) and exactly `max(places, 0)` decimal digits.
pub(crate) fn digits(x: f64, places: i32) -> (String, String) {
let first = fifteen(x.abs());
let rounded = first.round_at(-places, Direction::Nearest).unwrap_or(first);
split(&rounded, places.max(0) as usize)
}
Used by 19 functions.
fifteen · 5 lines · calc-func/src/format/mod.rs on GitHub/// FIXED/TEXT and concatenation share their first15-digit conversion. Reuse
/// its existing near-tie behavior, including the open dc-b1l.25 departures.
fn fifteen(x: f64) -> ExactDecimal {
crate::number_text::decimal(x)
}
Used by 19 functions.
split · 26 lines · calc-func/src/format/mod.rs on GitHub/// A decimal's integer digits (without leading zeros) and `places` decimal digits.
fn split(decimal: &ExactDecimal, places: usize) -> (String, String) {
if decimal.coefficient.is_zero() {
return (String::new(), "0".repeat(places));
}
let digits = decimal.coefficient.digits();
let exponent = decimal.exponent;
let (whole, fraction) = if exponent >= 0 {
(format!("{digits}{}", "0".repeat(exponent as usize)), String::new())
} else {
let point = digits.len() as i64 + exponent as i64;
if point <= 0 {
(String::new(), format!("{}{digits}", "0".repeat((-point) as usize)))
} else {
let point = point as usize;
(digits[..point].to_owned(), digits[point..].to_owned())
}
};
let whole = whole.trim_start_matches('0').to_owned();
let mut fraction = fraction;
fraction.truncate(places);
while fraction.len() < places {
fraction.push('0');
}
(whole, fraction)
}
Used by 19 functions.
grouped · 19 lines · calc-func/src/format/mod.rs on GitHubfn grouped(whole: &str, separator: &str) -> String {
if whole.len() <= 3 {
return whole.to_owned();
}
let mut out = String::new();
let first = whole.len() % 3;
if first > 0 {
out.push_str(&whole[..first]);
}
let mut at = first;
while at < whole.len() {
if !out.is_empty() {
out.push_str(separator);
}
out.push_str(&whole[at..at + 3]);
at += 3;
}
out
}
Used by 18 functions.
fixed · 27 lines · calc-func/src/format/mod.rs on GitHub/// FIXED: the number rounded to `places`, grouped unless `no_commas`.
/// A missing profile refuses only when the output would contain a locale separator.
pub(crate) fn fixed(profile: Option<&FormatProfile>, value: f64, places: i32, no_commas: bool) -> Rendered {
let decimal_separator = if places > 0 {
Some(profile.ok_or(Failure::NotModelled)?.decimal_separator)
} else {
None
};
let (whole, fraction) = digits(value, places);
let negative = value < 0.0 && (whole.chars().chain(fraction.chars()).any(|c| c != '0'));
let whole = if whole.is_empty() { "0".to_owned() } else { whole };
let whole = if no_commas || whole.len() <= 3 {
whole
} else {
grouped(&whole, profile.ok_or(Failure::NotModelled)?.thousands_separator)
};
let mut out = String::new();
if negative {
out.push('-');
}
out.push_str(&whole);
if let Some(separator) = decimal_separator {
out.push_str(separator);
out.push_str(&fraction);
}
Ok(out)
}
Used by 2 functions.
general · 67 lines · calc-func/src/format/mod.rs on GitHub/// The General format's text: at most 11 characters besides the sign, as a cell of Excel's
/// standard column width shows a number. Public as [`crate::general_format`] for an app that
/// shows a value as a cell would.
pub fn general(profile: &FormatProfile, value: f64) -> String {
const WIDTH: i64 = 11;
if !value.is_finite() {
if !value.is_nan() {
return "#NUM!".to_owned();
}
// Excel's retained exponent-field-2047 readings show two significant digits,
// without a minus sign, even when the stored encoding has its sign bit set.
let exact = ExactDecimal::binary(value);
let exponent = exact.magnitude();
let (mantissa, carry) = significant(&exact, 2);
let (head, tail) = mantissa.split_at(1);
let tail = tail.trim_end_matches('0');
let point = if tail.is_empty() { "" } else { profile.decimal_separator };
return format!("{head}{point}{tail}E+{}", i64::from(exponent) + carry);
}
if value == 0.0 || value.is_subnormal() {
return "0".to_owned();
}
let sign = if value < 0.0 { "-" } else { "" };
let magnitude = value.abs();
let first = fifteen(magnitude);
let length = first.coefficient.digits().len() as i64;
// The value is 0.d1d2... * 10^point.
let point = length + first.exponent as i64;
let fixed_fits = point <= WIDTH && point > -9;
if fixed_fits {
let places = if point >= WIDTH {
0
} else {
WIDTH - point.max(1) - 1
};
let (whole, fraction) = digits(magnitude, places as i32);
let fraction = fraction.trim_end_matches('0');
let whole = if whole.is_empty() { "0".to_owned() } else { whole };
if whole.len() as i64 <= WIDTH && (whole != "0" || !fraction.is_empty()) {
let mut out = format!("{sign}{whole}");
if !fraction.is_empty() {
out.push_str(profile.decimal_separator);
out.push_str(fraction);
}
return out;
}
}
// Scientific: d.dddddE+xx, the mantissa's places filling what the exponent leaves.
let mut exponent = point - 1;
let exponent_width = if exponent.abs() >= 100 { 3 } else { 2 };
let places = WIDTH - 2 - 2 - exponent_width;
let (mantissa, carry) = significant(&first, places as usize + 1);
exponent += carry;
let (head, tail) = mantissa.split_at(1);
let tail = tail.trim_end_matches('0');
let mut out = format!("{sign}{head}");
if !tail.is_empty() {
out.push_str(profile.decimal_separator);
out.push_str(tail);
}
out.push_str(&format!(
"E{}{:02}",
if exponent < 0 { '-' } else { '+' },
exponent.abs()
));
out
}
Used by 18 functions.
significant · 18 lines · calc-func/src/format/mod.rs on GitHub/// The first `count` significant digits of a non-zero decimal, rounded a half away from zero, and
/// 1 when the rounding carried into a new leading digit (the digits are then 1 and zeros).
fn significant(decimal: &ExactDecimal, count: usize) -> (String, i64) {
let digits = decimal.coefficient.digits();
if digits.len() <= count {
return (format!("{digits}{}", "0".repeat(count - digits.len())), 0);
}
let dropped = (digits.len() - count) as i32;
let rounded = decimal
.round_at(decimal.exponent + dropped, Direction::Nearest)
.expect("digits below the position");
let out = rounded.coefficient.digits();
if out.len() > count {
(out[..count].to_owned(), 1)
} else {
(out, 0)
}
}
Used by 18 functions.
sections · 27 lines · calc-func/src/format/mod.rs on GitHub/// The sections of a code, split at `;` outside quotes and escapes.
fn sections(code: &str) -> Vec<String> {
let mut out = Vec::new();
let mut current = String::new();
let (mut quoted, mut escaped) = (false, false);
for ch in code.chars() {
if escaped {
current.push(ch);
escaped = false;
continue;
}
match ch {
'\\' => {
current.push(ch);
escaped = true;
}
'"' => {
current.push(ch);
quoted = !quoted;
}
';' if !quoted => out.push(std::mem::take(&mut current)),
_ => current.push(ch),
}
}
out.push(current);
out
}
Used by 18 functions.
bracket · 6 lines · calc-func/src/format/mod.rs on GitHub/// A bracketed token at the start of `text`: its inside and the rest.
fn bracket(text: &str) -> Option<(&str, &str)> {
let rest = text.strip_prefix('[')?;
let close = rest.find(']')?;
Some((&rest[..close], &rest[close + 1..]))
}
Used by 18 functions.
is_condition · 3 lines · calc-func/src/format/mod.rs on GitHubfn is_condition(token: &str) -> bool {
token.starts_with(['<', '>', '='])
}
Used by 18 functions.
is_elapsed · 6 lines · calc-func/src/format/mod.rs on GitHubfn is_elapsed(token: &str) -> bool {
let lower = token.to_ascii_lowercase();
!lower.is_empty()
&& lower.chars().all(|c| matches!(c, 'h' | 'm' | 's'))
&& lower.chars().all(|c| Some(c) == lower.chars().next())
}
Used by 18 functions.
condition_holds · 14 lines · calc-func/src/format/mod.rs on GitHubfn condition_holds(token: &str, value: f64) -> Option<bool> {
let (op, rest) = ["<=", ">=", "<>", "<", ">", "="]
.iter()
.find_map(|op| token.strip_prefix(op).map(|rest| (*op, rest)))?;
let threshold: f64 = rest.trim().parse().ok()?;
Some(match op {
"<" => value < threshold,
"<=" => value <= threshold,
">" => value > threshold,
">=" => value >= threshold,
"=" => value == threshold,
_ => value != threshold,
})
}
Used by 18 functions.
condition_of · 11 lines · calc-func/src/format/mod.rs on GitHub/// The leading condition of a section, if it has one.
fn condition_of(section: &str) -> Option<String> {
let mut rest = section;
while let Some((token, after)) = bracket(rest) {
if is_condition(token) {
return Some(token.to_owned());
}
rest = after;
}
None
}
Used by 18 functions.
strip_tokens · 62 lines · calc-func/src/format/mod.rs on GitHub/// A section with its bracketed tokens resolved: colors dropped, locale recorded for names, a currency tag
/// `[$sym-xxxx]` written as its symbol (quoted), conditions dropped, elapsed-time tokens kept.
fn strip_tokens(section: &str) -> Result<(String, Option<crate::locale::LocaleId>, bool), Failure> {
let mut out = String::new();
let mut name_locale = None;
let mut locale_declared = false;
let mut chars = section.chars().peekable();
let (mut quoted, mut escaped) = (false, false);
while let Some(ch) = chars.next() {
if escaped {
out.push(ch);
escaped = false;
continue;
}
match ch {
'\\' => {
out.push(ch);
escaped = true;
}
'"' => {
out.push(ch);
quoted = !quoted;
}
'[' if !quoted => {
let mut token = String::new();
for next in chars.by_ref() {
if next == ']' {
break;
}
token.push(next);
}
if is_elapsed(&token) {
out.push('[');
out.push_str(&token);
out.push(']');
} else if let Some(body) = token.strip_prefix('$') {
let (symbol, locale) = body.split_once('-').unwrap_or((body, ""));
if !locale.is_empty() {
locale_declared = true;
use crate::locale::LocaleId;
name_locale = u16::from_str_radix(locale, 16).ok()
.and_then(LocaleId::from_excel_lcid)
.or_else(|| LocaleId::from_bcp47_language_tag(locale));
}
if !symbol.is_empty() {
out.push('"');
out.push_str(symbol);
out.push('"');
}
} else if is_condition(&token)
|| token.chars().all(|c| c.is_ascii_alphanumeric())
{
// A color ([Red], [Color10]) or a condition.
} else {
return Err(Failure::NotModelled);
}
}
_ => out.push(ch),
}
}
Ok((out, name_locale, locale_declared))
}
Used by 18 functions.
choose · 42 lines · calc-func/src/format/mod.rs on GitHub/// The section a number is rendered with, and whether that section shows the number's sign
/// itself (so its magnitude is rendered).
fn choose(sections: &[String], value: f64) -> Result<(String, bool), Failure> {
let numeric: Vec<&String> = sections.iter().take(3).collect();
if numeric.iter().any(|s| condition_of(s).is_some()) {
let mut fallback = None;
for section in &numeric {
match condition_of(section) {
Some(token) => {
if condition_holds(&token, value).ok_or(Failure::NotModelled)? {
return Ok(((*section).clone(), false));
}
}
None => {
if fallback.is_none() {
fallback = Some((*section).clone());
}
}
}
}
return fallback.map(|s| (s, false)).ok_or(Failure::NotModelled);
}
Ok(match numeric.len() {
1 => (numeric[0].clone(), false),
2 => {
if value < 0.0 {
(numeric[1].clone(), true)
} else {
(numeric[0].clone(), false)
}
}
_ => {
if value > 0.0 {
(numeric[0].clone(), false)
} else if value < 0.0 {
(numeric[1].clone(), true)
} else {
(numeric[2].clone(), false)
}
}
})
}
Used by 18 functions.
items · 84 lines · calc-func/src/format/mod.rs on GitHub/// A section's items, with quoted text, escapes, `_x` (a space) and `*x` (nothing) resolved.
fn items(section: &str, profile: &FormatProfile) -> Result<Vec<Item>, Failure> {
let decimal = profile.decimal_separator;
let group = profile.thousands_separator;
let chars: Vec<char> = section.chars().collect();
let mut out = Vec::new();
let mut at = 0;
let literal = |out: &mut Vec<Item>, text: &str| {
let text = currency_literal(profile, text);
match out.last_mut() {
Some(Item::Literal(existing)) => existing.push_str(text),
_ => out.push(Item::Literal(text.to_owned())),
}
};
while at < chars.len() {
let ch = chars[at];
let rest: String = chars[at..].iter().collect();
match ch {
'"' => {
let close = chars[at + 1..]
.iter()
.position(|c| *c == '"')
.ok_or(Failure::NotModelled)?;
let text: String = chars[at + 1..at + 1 + close].iter().collect();
literal(&mut out, &text);
at += close + 2;
}
'\\' => {
let next = chars.get(at + 1).ok_or(Failure::NotModelled)?;
let text = next.to_string();
match out.last_mut() {
Some(Item::Literal(existing)) => existing.push_str(&text),
_ => out.push(Item::Literal(text)),
}
at += 2;
}
'_' => {
literal(&mut out, " ");
at += 2;
}
'*' => at += 2,
'0' | '#' | '?' => {
out.push(Item::Digit(ch));
at += 1;
}
'%' => {
out.push(Item::Percent);
at += 1;
}
'@' => {
out.push(Item::TextPlaceholder);
at += 1;
}
'E' | 'e' if matches!(chars.get(at + 1), Some('+' | '-')) => {
out.push(Item::Exponent(chars[at + 1] == '+'));
at += 2;
}
_ if rest.starts_with(decimal) => {
out.push(Item::Point);
at += decimal.chars().count();
}
_ if !group.is_empty() && rest.starts_with(group) => {
out.push(Item::Group);
at += group.chars().count();
}
'/' => return Err(Failure::NotModelled),
'E' if measured_host(profile) => return Err(Failure::Value),
c if c.is_ascii_alphabetic() => {
// A letter that is a token in some other format (dates, eras, A/P) is not modelled.
if "ymdhsbegYMDHSBEG".contains(c)
|| (matches!(c, 'a' | 'A') && !measured_host(profile)) {
return Err(Failure::NotModelled);
}
literal(&mut out, &c.to_string());
at += 1;
}
c => {
literal(&mut out, &c.to_string());
at += 1;
}
}
}
Ok(out)
}
Used by 18 functions.
numeric · 132 lines · calc-func/src/format/mod.rs on GitHub/// Renders a number with a numeric section's items.
fn numeric(
profile: &FormatProfile,
value: f64,
items: &[Item],
show_minus: bool,
) -> Rendered {
let first = items
.iter()
.position(|i| matches!(i, Item::Digit(_)))
.ok_or(Failure::NotModelled)?;
let last = items
.iter()
.rposition(|i| matches!(i, Item::Digit(_)))
.expect("a digit placeholder");
let exponent_at = items.iter().position(|i| matches!(i, Item::Exponent(_)));
let mantissa_end = exponent_at.unwrap_or(last + 1);
let point_at = items[..mantissa_end]
.iter()
.position(|i| *i == Item::Point);
// Group separators right after the last mantissa placeholder divide by 1000 each.
let mut scale_groups = 0;
let mut cursor = items[..mantissa_end]
.iter()
.rposition(|i| matches!(i, Item::Digit(_)))
.map(|p| p + 1)
.unwrap_or(mantissa_end);
while items.get(cursor) == Some(&Item::Group) {
scale_groups += 1;
cursor += 1;
}
let scale_end = cursor;
let percents = items.iter().filter(|i| **i == Item::Percent).count() as i32;
let mantissa_last = items[..mantissa_end]
.iter()
.rposition(|i| matches!(i, Item::Digit(_)))
.unwrap_or(first);
let integer_end = point_at.unwrap_or(mantissa_last + 1);
// A group separator beside a literal in the integer part (dc-b1l, Excel 16.0.20430 on
// 2026-10-25, the catalogue oracle's dcuar_text rows): one that follows a literal and precedes
// a placeholder is written as the separator's text, a literal (TEXT(5415485.8,"##- 0") is
// 541548- 6); one that follows a placeholder and precedes a literal is dropped and groups
// nothing (TEXT(843124.584,"0# ,0") is 84312,5). One between two placeholders groups.
let separator = Item::Literal(profile.thousands_separator.to_owned());
let integer_items: Vec<Item> = items[first..integer_end.max(first)]
.iter()
.enumerate()
.filter_map(|(at, item)| {
let all = &items[first..integer_end.max(first)];
if *item != Item::Group {
return Some(item.clone());
}
let before = at.checked_sub(1).map(|p| &all[p]);
let after = all.get(at + 1);
match (before, after) {
(Some(Item::Literal(_)), Some(Item::Digit(_))) => Some(separator.clone()),
(Some(Item::Digit(_)), Some(Item::Literal(_))) => None,
_ => Some(Item::Group),
}
})
.collect();
let integer: Vec<&Item> = integer_items.iter().collect();
let mut fraction_literals: Vec<(usize, String)> = Vec::new();
let fraction: Vec<char> = match point_at {
Some(p) => {
let mut places = Vec::new();
for item in &items[p + 1..mantissa_end] {
match item {
Item::Digit(c) => places.push(*c),
Item::Group => {}
Item::Literal(text) => fraction_literals.push((places.len(), text.clone())),
_ => return Err(Failure::NotModelled),
}
}
places
}
None => Vec::new(),
};
// A literal among the decimal places stands after as many of them as precede it in the code
// (Excel 16.0.20430, dc-uar: TEXT(1234567.891,"# ##0.0,0") is 1 234 567.8,9); modelled for
// `0` places only.
if !fraction_literals.is_empty() && fraction.iter().any(|c| *c != '0') {
return Err(Failure::NotModelled);
}
let grouping = integer
.iter()
.enumerate()
.any(|(at, i)| **i == Item::Group && integer[at + 1..].iter().any(|j| matches!(j, Item::Digit(_))));
let prefix: String = items[..first]
.iter()
.map(|i| literal_text(i, profile))
.collect::<Result<String, Failure>>()?;
let suffix_from = match exponent_at {
Some(_) => last + 1,
None => scale_end.max(last + 1),
};
let suffix: String = items[suffix_from..]
.iter()
.map(|i| literal_text(i, profile))
.collect::<Result<String, Failure>>()?;
let mut scaled = value.abs() * 100f64.powi(percents);
if scale_groups > 0 {
scaled /= 1000f64.powi(scale_groups);
}
let places = fraction.len() as i32;
let (body, nonzero) = match exponent_at {
Some(e) => scientific(profile, scaled, &integer, &fraction, items, e)?,
None => {
let (whole, frac) = digits(scaled, places);
let nonzero = whole.chars().chain(frac.chars()).any(|c| c != '0');
let whole_text = integer_part(profile, &whole, &integer, grouping)?;
let mut frac_text = fraction_part(&frac, &fraction);
for (after, text) in fraction_literals.iter().rev() {
frac_text.insert_str((*after).min(frac_text.len()), text);
}
let mut body = whole_text;
if point_at.is_some() {
body.push_str(profile.decimal_separator);
body.push_str(&frac_text);
}
(body, nonzero)
}
};
let minus = if show_minus && value < 0.0 && nonzero {
"-"
} else {
""
};
Ok(format!("{minus}{prefix}{body}{suffix}"))
}
Used by 18 functions.
literal_text · 9 lines · calc-func/src/format/mod.rs on GitHubfn literal_text(item: &Item, profile: &FormatProfile) -> Result<String, Failure> {
Ok(match item {
Item::Literal(text) => text.clone(),
Item::Percent => "%".to_owned(),
Item::Group => profile.thousands_separator.to_owned(),
Item::Point => profile.decimal_separator.to_owned(),
Item::Digit(_) | Item::Exponent(_) | Item::TextPlaceholder => return Err(Failure::NotModelled),
})
}
Used by 18 functions.
integer_part · 122 lines · calc-func/src/format/mod.rs on GitHub/// The integer part: digits fill the placeholders from the right, the leftmost placeholder takes
/// any digits left over, `0` writes a zero and `?` a space where no digit is left, literals stay
/// where they stand, and grouping writes the locale's group separator between groups of three.
fn integer_part(
profile: &FormatProfile,
whole: &str,
pattern: &[&Item],
grouping: bool,
) -> Result<String, Failure> {
let placeholders: Vec<char> = pattern
.iter()
.filter_map(|i| match i {
Item::Digit(c) => Some(*c),
_ => None,
})
.collect();
if grouping {
// A literal among grouped placeholders stands where as many digits are to its right as
// there are placeholders to its right in the code, the grouping counted on the digits
// alone (Excel 16.0.20430, dc-uar: TEXT(1234567.891,"# ##0,0") is 1 234 56,8, and
// TEXT(12,"#-# ##0") is -12). Modelled for `0` and `#` placeholders and literals that do
// not fall beside a group separator.
// A group separator beside a literal that the caller did not resolve (one between two
// literals, or at either end) is not modelled.
let beside_literal = pattern.iter().enumerate().any(|(at, item)| {
**item == Item::Group
&& (at == 0
|| !matches!(pattern[at - 1], Item::Digit(_))
|| !matches!(pattern.get(at + 1), Some(Item::Digit(_))))
});
if beside_literal && pattern.iter().any(|i| matches!(i, Item::Literal(_))) {
return Err(Failure::NotModelled);
}
let mut literals: Vec<(usize, String)> = Vec::new();
let mut right = 0;
for item in pattern.iter().rev() {
match item {
Item::Digit('?') => return Err(Failure::NotModelled),
Item::Digit(_) => right += 1,
Item::Group => {}
other => {
// Literals with no placeholder between them stand together, in the code's
// order (dc-b1l: TEXT(503134,"0- 0 #") is 503 1- 34).
let text = literal_text(other, profile)?;
match literals.last_mut() {
Some((count, joined)) if *count == right => joined.insert_str(0, &text),
_ => literals.push((right, text)),
}
}
}
}
let zeros = placeholders
.iter()
.rev()
.take_while(|_| true)
.enumerate()
.filter(|(_, c)| **c == '0')
.map(|(at, _)| at + 1)
.max()
.unwrap_or(0);
let mut digits = whole.to_owned();
while digits.len() < zeros {
digits.insert(0, '0');
}
let mut text = grouped(&digits, profile.thousands_separator);
// Literals nearest the right first; each goes left of its digits.
for (count, literal) in &literals {
let mut seen = 0;
let mut at = text.len();
if *count > 0 {
at = 0;
for (index, c) in text.char_indices().rev() {
if c.is_ascii_digit() {
seen += 1;
if seen == *count {
at = index;
break;
}
}
}
// A literal where a group separator falls stands after it (dc-b1l:
// TEXT(9514218,"0x#0# ###") is 9 x514 218).
if seen < *count {
at = 0;
}
}
text.insert_str(at, literal);
}
return Ok(text);
}
let chars: Vec<char> = whole.chars().collect();
let mut next = chars.len();
let mut out: Vec<String> = Vec::new();
let leftmost = pattern
.iter()
.position(|i| matches!(i, Item::Digit(_)))
.unwrap_or(0);
for (at, item) in pattern.iter().enumerate().rev() {
match item {
Item::Digit(c) => {
let mut piece = String::new();
if next > 0 {
next -= 1;
piece.push(chars[next]);
if at == leftmost && next > 0 {
let rest: String = chars[..next].iter().collect();
piece.insert_str(0, &rest);
next = 0;
}
} else if *c == '0' {
piece.push('0');
} else if *c == '?' {
piece.push(' ');
}
out.push(piece);
}
other => out.push(literal_text(other, profile)?),
}
}
out.reverse();
Ok(out.concat())
}
Used by 18 functions.
fraction_part · 19 lines · calc-func/src/format/mod.rs on GitHub/// The decimal places: `0` writes its digit, `#` drops a trailing zero, `?` writes a trailing
/// zero as a space.
fn fraction_part(digits: &str, pattern: &[char]) -> String {
let chars: Vec<char> = digits.chars().collect();
let mut out: Vec<char> = chars.clone();
for at in (0..chars.len()).rev() {
if chars[at] != '0' {
break;
}
match pattern[at] {
'#' => {
out.pop();
}
'?' => out[at] = ' ',
_ => break,
}
}
out.into_iter().collect()
}
Used by 18 functions.
scientific · 87 lines · calc-func/src/format/mod.rs on GitHub/// A scientific mantissa and exponent.
fn scientific(
profile: &FormatProfile,
value: f64,
integer: &[&Item],
fraction: &[char],
items: &[Item],
exponent_at: usize,
) -> Result<(String, bool), Failure> {
let Item::Exponent(plus) = items[exponent_at] else {
unreachable!()
};
let exponent_digits: Vec<char> = items[exponent_at + 1..]
.iter()
.take_while(|i| matches!(i, Item::Digit(_)))
.map(|i| match i {
Item::Digit(c) => *c,
_ => unreachable!(),
})
.collect();
if exponent_digits.is_empty() {
return Err(Failure::NotModelled);
}
let whole_places: Vec<char> = integer
.iter()
.filter_map(|i| match i {
Item::Digit(c) => Some(*c),
Item::Group => None,
_ => Some('!'),
})
.collect();
if whole_places.contains(&'!') || whole_places.is_empty() {
return Err(Failure::NotModelled);
}
let width = whole_places.len() as i64;
let engineering = width > 1 && whole_places[0] == '#';
if value == 0.0 {
let mantissa = format!(
"0{}{}",
if fraction.is_empty() { "" } else { profile.decimal_separator },
"0".repeat(fraction.len())
);
let exponent = format!("E{}{}", if plus { "+" } else { "" }, "0".repeat(exponent_digits.len()));
return Ok((format!("{mantissa}{exponent}"), false));
}
let first = fifteen(value);
let length = first.coefficient.digits().len() as i64;
let point = length + first.exponent as i64; // value = 0.d * 10^point
let leading = if engineering {
// The exponent is a multiple of the width.
let e = (point - 1).div_euclid(width) * width;
point - e
} else {
width.min(point.max(1)).max(1)
};
let mut exponent = point - leading;
let count = (leading + fraction.len() as i64) as usize;
let (mut mantissa_digits, carry) = significant(&first, count);
if carry == 1 {
exponent += 1;
if engineering && exponent.rem_euclid(width) != 0 {
// A carry past the width moves to the next multiple.
mantissa_digits.push('0');
}
}
let lead = leading as usize;
let (whole, frac) = mantissa_digits.split_at(lead.min(mantissa_digits.len()));
let frac = fraction_part(&frac[..fraction.len().min(frac.len())], fraction);
let mut out = whole.to_owned();
if !fraction.is_empty() {
out.push_str(profile.decimal_separator);
out.push_str(&frac);
}
let sign = if exponent < 0 {
"-"
} else if plus {
"+"
} else {
""
};
out.push_str(&format!(
"E{sign}{:0width$}",
exponent.abs(),
width = exponent_digits.len()
));
Ok((out, true))
}
Used by 18 functions.
fraction_code · 30 lines · calc-func/src/format/mod.rs on GitHubfn fraction_code(section: &str) -> Option<FractionCode> {
let (top, bottom) = section.split_once('/')?;
let placeholders = |s: &str| !s.is_empty() && s.chars().all(|c| matches!(c, '0' | '#' | '?'));
if !placeholders(top) {
return None;
}
let denominator = if placeholders(bottom) {
let chars: Vec<char> = bottom.chars().collect();
if chars.contains(&'0') && chars.iter().any(|c| *c != '0') {
return None;
}
Err(chars)
} else if !bottom.is_empty()
&& bottom.len() <= 9
&& bottom.chars().all(|c| c.is_ascii_digit())
&& !bottom.starts_with('0')
{
Ok(bottom.parse().ok()?)
} else {
return None;
};
let numerator: Vec<char> = top.chars().collect();
if numerator.contains(&'0') && numerator.iter().any(|c| *c != '0') {
return None;
}
Some(FractionCode {
numerator,
denominator,
})
}
Used by 18 functions.
FractionCode::render · 50 lines · calc-func/src/format/mod.rs on GitHubfn render(&self, value: f64, show_minus: bool) -> Rendered {
let x = value.abs();
let (numerator, denominator) = match &self.denominator {
Ok(d) => {
let n = (x * *d as f64 + 0.5).floor();
if !(n <= 32767.0) {
return Err(Failure::Value);
}
(n as u64, *d)
}
Err(places) => {
let largest = 10u64.pow(places.len() as u32) - 1;
convergent(x, largest).ok_or(Failure::NotModelled)?
}
};
let mut out = String::new();
if show_minus && value < 0.0 && numerator != 0 {
out.push('-');
}
let top = numerator.to_string();
let missing = self.numerator.len().saturating_sub(top.len());
for c in &self.numerator[..missing] {
match c {
'0' => out.push('0'),
'?' => out.push(' '),
_ => {}
}
}
out.push_str(&top);
out.push('/');
let bottom = denominator.to_string();
match &self.denominator {
Ok(_) => out.push_str(&bottom),
Err(places) => {
let missing = places.len().saturating_sub(bottom.len());
if places.first() == Some(&'0') {
out.push_str(&"0".repeat(missing));
out.push_str(&bottom);
} else {
out.push_str(&bottom);
for c in &places[places.len() - missing..] {
if *c == '?' {
out.push(' ');
}
}
}
}
}
Ok(out)
}
Used by 18 functions.
mixed_fraction_under_space_grouping · 17 lines · calc-func/src/format/mod.rs on GitHub/// Whether `section` is a fraction with an integer part set off by the group separator, under a
/// locale whose group separator is a space: Excel 16.0.20430 under en-ZA gave #VALUE! for every
/// such code it was given (`# ?/?`, `# ??/??`, `# ???/???`, `# ?/8`, `# ?/100`, `0 ?/?`,
/// `#,##0 ?/?`; dc-uar, the catalogue oracle's sheet dcuar_text).
fn mixed_fraction_under_space_grouping(section: &str, profile: &FormatProfile) -> bool {
if profile.thousands_separator != " " {
return false;
}
let Some((whole, fraction)) = section.rsplit_once(' ') else {
return false;
};
let placeholders = |s: &str| !s.is_empty() && s.chars().all(|c| matches!(c, '0' | '#' | '?'));
whole.chars().any(|c| matches!(c, '0' | '#'))
&& whole.chars().all(|c| matches!(c, '0' | '#' | '?' | ','))
&& fraction_code(fraction).is_some()
&& fraction.split_once('/').is_some_and(|(top, _)| placeholders(top))
}
Used by 18 functions.
convergent · 22 lines · calc-func/src/format/mod.rs on GitHub/// The last convergent of `x`'s continued fraction, computed in binary64, whose denominator is
/// at most `largest`.
fn convergent(x: f64, largest: u64) -> Option<(u64, u64)> {
let (mut h0, mut h1, mut k0, mut k1) = (0f64, 1f64, 1f64, 0f64);
let mut best = None;
let mut y = x;
for _ in 0..64 {
let a = y.floor();
let (h2, k2) = (a * h1 + h0, a * k1 + k0);
if k2 > largest as f64 || !h2.is_finite() || h2 >= 1e15 {
break;
}
(h0, h1, k0, k1) = (h1, h2, k1, k2);
best = Some((h1 as u64, k1 as u64));
let rest = y - a;
if rest == 0.0 {
break;
}
y = 1.0 / rest;
}
best
}
Used by 18 functions.
text_declared_date_locale · 17 lines · calc-func/src/format/mod.rs on GitHub/// A declared date-name locale supplies all profile fields used by these date-only
/// sections. Host separators and designators remain unavailable; admit only fields
/// that never read them, then use the same formatter with the locale the code names.
pub(crate) fn text_declared_date_locale(system: DateSystem, value: f64, code: &str) -> Rendered {
let sections = sections(code);
if sections.iter().any(|section| condition_of(section).is_some()) {
return Err(Failure::NotModelled);
}
let (section, _) = choose(§ions, value).map_err(|_| Failure::NotModelled)?;
let (stripped, name_locale, _) = strip_tokens(§ion).map_err(|_| Failure::NotModelled)?;
let locale = name_locale.ok_or(Failure::NotModelled)?;
if stripped.trim().is_empty() || !stripped.chars().all(|c| matches!(c,
'y' | 'Y' | 'm' | 'M' | 'd' | 'D' | 'g' | 'G' | 'e' | 'E' | ' ')) {
return Err(Failure::NotModelled);
}
text(&crate::locale::format_profile(locale), system, value, code)
}
Used by 18 functions.
text · 76 lines · calc-func/src/format/mod.rs on GitHub/// TEXT of a number under `code`.
pub(crate) fn text(profile: &FormatProfile, system: DateSystem, value: f64, code: &str) -> Rendered {
if !value.is_finite() {
return Err(Failure::Value);
}
let sections = sections(code);
let (section, signed_section) = choose(§ions, value)?;
let (stripped, name_locale, locale_declared) = strip_tokens(§ion)?;
if stripped.trim().eq_ignore_ascii_case("general") {
let shown = general(profile, if signed_section { value.abs() } else { value });
return Ok(shown);
}
let magnitude_only = signed_section;
if !locale_declared && measured_host(profile) && datetime::has_invalid_exponent(&stripped) {
return Err(Failure::Value);
}
if datetime::looks_like_datetime_format(&stripped) {
if value < 0.0 {
return Err(Failure::Value);
}
if value >= 2_958_466.0 {
return Err(Failure::Value);
}
if datetime::without_subseconds(&stripped)
.chars()
.any(|c| matches!(c, 'b' | 'B' | '0') ||
(matches!(c, 'e' | 'g' | 'G') && name_locale != Some(crate::locale::LocaleId::JaJp)
&& !(!locale_declared && measured_host(profile))))
{
return Err(Failure::NotModelled);
}
return datetime::render_with_datetime_tokens(profile, system, value, &stripped, name_locale, !locale_declared)
.ok_or(Failure::NotModelled);
}
if let Some(fraction) = fraction_code(&stripped) {
return fraction.render(value, !magnitude_only);
}
if mixed_fraction_under_space_grouping(&stripped, profile) {
return Err(Failure::Value);
}
let mut items = items(&stripped, profile)?;
if !items.iter().any(|i| matches!(i, Item::Digit(_))) {
// Fn8 literal-only controls: a run of ordinary-space grouping tokens
// writes one space. Numeric grouping and trailing scaling stay below.
if measured_host(profile) && profile.thousands_separator == " " {
items.dedup_by(|a, b| *a == Item::Group && *b == Item::Group);
for item in &mut items {
if *item == Item::Group { *item = Item::Literal(" ".to_owned()); }
}
}
// A section of literals alone writes them (an empty one writes nothing).
if items.iter().any(|i| matches!(i, Item::Point | Item::Group | Item::Exponent(_))) {
return Err(Failure::NotModelled);
}
if items.iter().any(|i| matches!(i, Item::TextPlaceholder)) {
// Fn4 qt001..qt030: active @ and @! select General for numbers,
// including values whose full numeric text has different precision.
return Ok(general(profile, if signed_section { value.abs() } else { value }));
}
let literal = items
.iter()
.map(|i| literal_text(i, profile))
.collect::<Result<String, Failure>>()?;
// Keep the ordinary automatic sign when section selection has not
// suppressed it, even for quoted text (fn4 qt008). Empty stays empty.
return Ok(if value < 0.0 && !signed_section && !literal.is_empty() {
format!("-{literal}")
} else {
literal
});
}
if items.iter().any(|i| matches!(i, Item::TextPlaceholder)) {
return Err(Failure::NotModelled);
}
numeric(profile, value, &items, !magnitude_only)
}
Used by 18 functions.
text_of_text · 22 lines · calc-func/src/format/mod.rs on GitHub/// TEXT of a text value under `code`: the fourth section, or a first section holding `@`, with
/// `@` written as the text; otherwise the text as it is.
pub(crate) fn text_of_text(text: &str, code: &str, profile: &FormatProfile) -> Rendered {
let sections = sections(code);
let section = match sections.get(3) {
Some(section) => section.clone(),
None if sections.len() == 1 && sections[0].contains('@') => sections[0].clone(),
None => return Ok(text.to_owned()),
};
let (stripped, _, _) = strip_tokens(§ion)?;
let mut out = String::new();
for part in stripped.split('@').enumerate() {
if part.0 > 0 {
out.push_str(text);
}
let items = items(part.1, profile)?;
for item in &items {
out.push_str(&literal_text(item, profile)?);
}
}
Ok(out)
}
Used by 18 functions.