Shared Rust kernels
format/cell_info
All modules · quoted at commit 9e1884369a67 of DnaCalc/DnaCalc. Comments are the source's own.
sections · 24 lines · calc-func/src/format/cell_info.rs on GitHub/// The code's sections, split at `;` outside quotes, brackets and escapes.
fn sections(code: &str) -> Vec<&str> {
let mut out = Vec::new();
let (mut start, mut quoted, mut bracket, mut escaped) = (0, false, false, false);
for (i, c) in code.char_indices() {
if escaped {
escaped = false;
continue;
}
match c {
'\\' if !quoted => escaped = true,
'"' if !bracket => quoted = !quoted,
'[' if !quoted => bracket = true,
']' if !quoted => bracket = false,
';' if !quoted && !bracket => {
out.push(&code[start..i]);
start = i + 1;
}
_ => {}
}
}
out.push(&code[start..]);
out
}
Used by 5 functions.
tokens · 139 lines · calc-func/src/format/cell_info.rs on GitHubfn tokens(section: &str) -> Vec<Token> {
let chars: Vec<char> = section.chars().collect();
let mut out = Vec::new();
let mut i = 0;
let starts = |i: usize, word: &str| -> bool {
let w: Vec<char> = word.chars().collect();
i + w.len() <= chars.len()
&& chars[i..i + w.len()]
.iter()
.zip(&w)
.all(|(a, b)| a.eq_ignore_ascii_case(b))
};
while i < chars.len() {
let c = chars[i];
let run = |i: usize, lower: char| {
chars[i..]
.iter()
.take_while(|x| x.to_ascii_lowercase() == lower)
.count()
};
match c {
'"' => {
let end = chars[i + 1..]
.iter()
.position(|&x| x == '"')
.map_or(chars.len(), |p| i + 1 + p);
out.push(Token::Quoted(chars[i + 1..end].iter().collect()));
i = end + 1;
}
'\\' => {
if let Some(&x) = chars.get(i + 1) {
out.push(Token::Literal(x));
}
i += 2;
}
'_' | '*' => {
out.push(Token::Spacing);
i += 2;
}
'[' => {
let end = chars[i + 1..]
.iter()
.position(|&x| x == ']')
.map_or(chars.len(), |p| i + 1 + p);
let inner: String = chars[i + 1..end].iter().collect();
let lower = inner.to_ascii_lowercase();
out.push(if let Some(rest) = inner.strip_prefix('$') {
Token::Locale {
with_id: rest.contains('-'),
}
} else if inner.starts_with(['<', '>', '=']) {
Token::Condition
} else if !lower.is_empty()
&& (lower.chars().all(|x| x == 'h')
|| lower.chars().all(|x| x == 'm')
|| lower.chars().all(|x| x == 's'))
{
Token::Elapsed
} else {
Token::Color
});
i = end + 1;
}
_ if starts(i, "general") => {
out.push(Token::General);
i += 7;
}
_ if starts(i, "am/pm") => {
out.push(Token::AmPm);
i += 5;
}
_ if starts(i, "a/p") => {
out.push(Token::AmPmShort);
i += 3;
}
'0' | '#' | '?' => {
out.push(Token::Placeholder);
i += 1;
}
'.' => {
out.push(Token::Point);
i += 1;
}
',' => {
out.push(Token::Group);
i += 1;
}
'%' => {
out.push(Token::Percent);
i += 1;
}
'/' => {
out.push(Token::Slash);
i += 1;
}
'@' => {
out.push(Token::At);
i += 1;
}
'E' | 'e' if matches!(chars.get(i + 1), Some('+' | '-')) => {
out.push(Token::Exponent);
i += 2;
}
_ => match c.to_ascii_lowercase() {
'd' => {
let n = run(i, 'd');
out.push(Token::Day(n));
i += n;
}
'm' => {
let n = run(i, 'm');
out.push(Token::Month(n));
i += n;
}
'y' => {
i += run(i, 'y');
out.push(Token::Year);
}
'h' => {
i += run(i, 'h');
out.push(Token::Hour);
}
's' => {
i += run(i, 's');
out.push(Token::Second);
}
'e' | 'g' | 'b' => {
i += run(i, c.to_ascii_lowercase());
out.push(Token::Era);
}
_ => {
out.push(Token::Literal(c));
i += 1;
}
},
}
}
out
}
Used by 5 functions.
is_date_time · 14 lines · calc-func/src/format/cell_info.rs on GitHubfn is_date_time(token: &Token) -> bool {
matches!(
token,
Token::Day(_)
| Token::Month(_)
| Token::Year
| Token::Hour
| Token::Second
| Token::AmPm
| Token::AmPmShort
| Token::Era
| Token::Elapsed
)
}
Used by 5 functions.
date_time_class · 69 lines · calc-func/src/format/cell_info.rs on GitHub/// The class of a date or time section: `None` for `G`.
fn date_time_class(tokens: &[Token]) -> Option<u8> {
if tokens.iter().any(|t| {
matches!(t, Token::Era | Token::Elapsed) || matches!(t, Token::Day(n) if *n >= 3)
}) {
return None;
}
let tagged = tokens
.iter()
.any(|t| matches!(t, Token::Locale { with_id: true }));
// The date and time tokens in order, each month token resolved to month (`M`) or minute.
let parts: Vec<&Token> = tokens
.iter()
.filter(|t| is_date_time(t) && !matches!(t, Token::AmPm | Token::AmPmShort))
.collect();
let minute = |at: usize| {
matches!(parts[at], Token::Month(_))
&& ((at > 0 && matches!(parts[at - 1], Token::Hour))
|| matches!(parts.get(at + 1), Some(Token::Second)))
};
match parts.first()? {
Token::Hour => {
if tagged {
return None;
}
let has_minute = (0..parts.len()).any(minute);
if !has_minute {
return None;
}
let seconds = parts.iter().any(|t| matches!(t, Token::Second));
let am_pm = tokens.iter().any(|t| matches!(t, Token::AmPm | Token::AmPmShort));
Some(match (seconds, am_pm) {
(true, true) => 6,
(true, false) => 8,
(false, true) => 7,
(false, false) => 9,
})
}
Token::Second => None,
Token::Month(_) if minute(0) => None,
_ => {
let mut order = Vec::new();
for (at, part) in parts.iter().enumerate() {
let key = match part {
Token::Day(_) => 'd',
Token::Month(_) if !minute(at) => 'm',
Token::Year => 'y',
_ => continue,
};
if !order.contains(&key) {
order.push(key);
}
}
let class: u8 = match order.as_slice() {
['d', 'm', 'y'] => 1,
['d', 'm'] => 2,
['m', 'y'] => 3,
['m', 'd', 'y'] => 4,
['m', 'd'] => 5,
_ => return None,
};
if tagged {
class.checked_sub(2).filter(|c| *c > 0)
} else {
Some(class)
}
}
}
}
Used by 5 functions.
places · 13 lines · calc-func/src/format/cell_info.rs on GitHub/// The decimal places a number section shows: the placeholders after its decimal point, before
/// an exponent, at most 15.
fn places(tokens: &[Token]) -> usize {
let Some(point) = tokens.iter().position(|t| *t == Token::Point) else {
return 0;
};
tokens[point + 1..]
.iter()
.take_while(|t| **t != Token::Exponent)
.filter(|t| **t == Token::Placeholder)
.count()
.min(15)
}
Used by 5 functions.
number_class · 32 lines · calc-func/src/format/cell_info.rs on GitHubfn number_class(tokens: &[Token], profile: &FormatProfile) -> String {
if tokens.iter().any(|t| *t == Token::General) {
return "G".to_owned();
}
let n = places(tokens);
let symbol = profile.currency_symbol;
let currency = !symbol.is_empty()
&& tokens
.iter()
.any(|t| matches!(t, Token::Quoted(q) if q == symbol));
if currency {
return format!("C{n}");
}
if tokens.iter().any(|t| *t == Token::Percent) {
return format!("P{n}");
}
if tokens.iter().any(|t| *t == Token::Exponent) {
return format!("S{n}");
}
let slash = tokens.iter().position(|t| *t == Token::Slash);
if slash.is_some_and(|at| {
tokens[..at].iter().any(|t| *t == Token::Placeholder)
|| tokens[at + 1..].iter().any(|t| *t == Token::Placeholder)
}) {
return "G".to_owned();
}
if tokens.iter().any(|t| *t == Token::Placeholder) {
let grouped = tokens.windows(2).any(|w| w == [Token::Placeholder, Token::Group]);
return if grouped { format!(",{n}") } else { format!("F{n}") };
}
"G".to_owned()
}
Used by 5 functions.
classify · 35 lines · calc-func/src/format/cell_info.rs on GitHub/// What CELL reports of the number format `code` under `profile`; `builtin` is the built-in
/// format ID the cell's format names, when it names one.
pub(crate) fn classify(code: &str, builtin: Option<u32>, profile: &FormatProfile) -> FormatClass {
let short_date;
let code = match builtin {
Some(14) => profile.short_date_pattern,
Some(22) => {
short_date = format!("{} h:mm", profile.short_date_pattern);
short_date.as_str()
}
_ => code,
};
let parts = sections(code);
let first = tokens(parts[0]);
let mut class = if first.iter().any(is_date_time) {
date_time_class(&first).map_or_else(|| "G".to_owned(), |c| format!("D{c}"))
} else {
number_class(&first, profile)
};
let color = parts
.get(1)
.is_some_and(|second| tokens(second).iter().any(|t| *t == Token::Color));
let parentheses = first.iter().any(|t| *t == Token::Literal('('));
if color {
class.push('-');
}
if parentheses {
class.push_str("()");
}
FormatClass {
code: class,
color,
parentheses,
}
}
Used by 5 functions.