use std::collections::{BTreeMap, HashMap};
use crate::numfmt::Code;
use crate::rows::Visibility;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Rgb(pub u8, pub u8, pub u8);
impl Rgb {
pub fn argb_hex(self) -> String {
format!("FF{:02X}{:02X}{:02X}", self.0, self.1, self.2)
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum NumFmt {
#[default]
General,
Text,
Code(Code),
}
impl NumFmt {
pub fn builtin(&self) -> Option<u32> {
match self {
NumFmt::General => Some(0),
NumFmt::Text => Some(49),
NumFmt::Code(c) => c.builtin(),
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Style {
pub fill: Option<Rgb>,
pub num_fmt: NumFmt,
pub wrap: bool,
}
#[derive(Clone, Debug)]
pub struct Styles {
xfs: Vec<Style>,
}
impl Default for Styles {
fn default() -> Self {
Self::new()
}
}
impl Styles {
pub fn new() -> Styles {
Styles {
xfs: vec![Style::default()],
}
}
pub fn id(&mut self, style: Style) -> u32 {
if let Some(i) = self.xfs.iter().position(|s| *s == style) {
return i as u32;
}
self.xfs.push(style);
(self.xfs.len() - 1) as u32
}
pub fn put(&mut self, index: u32, style: Style) {
let i = index as usize;
if self.xfs.len() <= i {
self.xfs.resize(i + 1, Style::default());
}
self.xfs[i] = style;
}
pub fn get(&self, index: u32) -> Option<&Style> {
self.xfs.get(index as usize)
}
pub fn xfs(&self) -> &[Style] {
&self.xfs
}
pub fn fills(&self) -> Vec<Rgb> {
let mut fills: Vec<Rgb> = Vec::new();
for s in &self.xfs {
if let Some(f) = s.fill {
if !fills.contains(&f) {
fills.push(f);
}
}
}
fills
}
pub fn has_code(&self) -> bool {
self.xfs
.iter()
.any(|s| matches!(s.num_fmt, NumFmt::Code(_)))
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum Content {
Text(String),
Number(f64),
Bool(bool),
Error(String),
Formula(String),
SharedMaster {
text: String,
range: String,
si: u32,
},
SharedChild {
si: u32,
},
DynamicFormula(String),
}
#[derive(Clone, Debug, PartialEq)]
pub struct Cell {
pub content: Content,
pub style: u32,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Column {
pub index: u32,
pub width: Option<f64>,
pub hidden: bool,
pub style: Option<u32>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct A1Range {
pub top: u32,
pub left: u32,
pub bottom: u32,
pub right: u32,
}
impl A1Range {
pub fn cells(&self) -> u64 {
u64::from(self.bottom - self.top + 1) * u64::from(self.right - self.left + 1)
}
pub fn text(&self) -> String {
if self.top == self.bottom && self.left == self.right {
cell_ref(self.top, self.left)
} else {
format!(
"{}:{}",
cell_ref(self.top, self.left),
cell_ref(self.bottom, self.right)
)
}
}
}
pub const MAX_COLUMN: u32 = 16_384;
pub const MAX_ROW: u32 = 1_048_576;
fn parse_a1_cell(s: &str) -> Option<(u32, u32)> {
let s = s.replace('$', "");
let letters: String = s.chars().take_while(|c| c.is_ascii_alphabetic()).collect();
let digits = &s[letters.len()..];
if letters.is_empty() || letters.len() > 3 || digits.is_empty() {
return None;
}
if !digits.chars().all(|c| c.is_ascii_digit()) {
return None;
}
let mut col: u32 = 0;
for c in letters.chars() {
col = col * 26 + (c.to_ascii_uppercase() as u32 - 'A' as u32 + 1);
}
let row: u32 = digits.parse().ok()?;
if col == 0 || col > MAX_COLUMN || row == 0 || row > MAX_ROW {
return None;
}
Some((row, col))
}
pub fn parse_a1_range(s: &str) -> Option<A1Range> {
let s = s.trim();
let (a, b) = match s.split_once(':') {
Some((a, b)) => (a, b),
None => (s, s),
};
let (r1, c1) = parse_a1_cell(a)?;
let (r2, c2) = parse_a1_cell(b)?;
Some(A1Range {
top: r1.min(r2),
left: c1.min(c2),
bottom: r1.max(r2),
right: c1.max(c2),
})
}
#[derive(Clone, Debug, PartialEq)]
pub struct RowSettings {
pub height: Option<f64>,
pub hidden: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Sheet {
pub name: String,
pub cells: BTreeMap<(u32, u32), Cell>,
pub columns: Vec<Column>,
pub rows: BTreeMap<u32, RowSettings>,
pub looks: Vec<(String, u32)>,
pub visibility: Visibility,
pub gridlines: bool,
pub active_cell: (u32, u32),
pub shared_formulas: u32,
pub sentences: BTreeMap<(u32, u32), u32>,
}
impl Sheet {
pub fn new(name: &str) -> Sheet {
Sheet {
name: name.to_string(),
cells: BTreeMap::new(),
columns: Vec::new(),
rows: BTreeMap::new(),
looks: Vec::new(),
visibility: Visibility::Visible,
gridlines: true,
active_cell: (1, 1),
shared_formulas: 0,
sentences: BTreeMap::new(),
}
}
pub fn wrote(&mut self, range: A1Range, row_of_sentence: u32) {
if row_of_sentence == 0 {
return;
}
for row in range.top..=range.bottom {
for col in range.left..=range.right {
self.sentences.insert((row, col), row_of_sentence);
}
}
}
pub fn set(&mut self, row: u32, col: u32, cell: Cell) {
self.cells.insert((row, col), cell);
}
pub fn set_formula_dynamic(&mut self, range: A1Range, text: &str, style: u32) {
for row in range.top..=range.bottom {
for col in range.left..=range.right {
let moved = crate::refers::shift_a1_references(
text,
i64::from(row) - i64::from(range.top),
i64::from(col) - i64::from(range.left),
);
self.set(
row,
col,
Cell {
content: Content::DynamicFormula(moved),
style,
},
);
}
}
}
pub fn set_formula(&mut self, range: A1Range, text: &str, style: u32) {
let si = self.next_shared(range);
for row in range.top..=range.bottom {
for col in range.left..=range.right {
let content = filled(range, text, si, row, col);
self.set(row, col, Cell { content, style });
}
}
}
pub(crate) fn next_shared(&mut self, range: A1Range) -> Option<u32> {
(range.cells() > 1).then(|| {
self.shared_formulas += 1;
self.shared_formulas - 1
})
}
pub fn extent(&self) -> Option<((u32, u32), (u32, u32))> {
#[cfg(test)]
work::extents(self.cells.len());
let mut it = self.cells.keys();
let &(r0, c0) = it.next()?;
let (mut top, mut left, mut bottom, mut right) = (r0, c0, r0, c0);
for &(r, c) in it {
top = top.min(r);
left = left.min(c);
bottom = bottom.max(r);
right = right.max(c);
}
Some(((top, left), (bottom, right)))
}
}
pub fn shared_masters(sheet: &Sheet) -> HashMap<u32, (&str, u32, u32)> {
#[cfg(test)]
work::walked(sheet.cells.len());
let mut masters = HashMap::new();
for ((row, col), cell) in &sheet.cells {
if let Content::SharedMaster { text, si, .. } = &cell.content {
masters.insert(*si, (text.as_str(), *row, *col));
}
}
masters
}
pub fn formula_text(
content: &Content,
row: u32,
col: u32,
masters: &HashMap<u32, (&str, u32, u32)>,
) -> Option<String> {
let raw = match content {
Content::Formula(t) | Content::DynamicFormula(t) => t.clone(),
Content::SharedMaster { text, .. } => text.clone(),
Content::SharedChild { si } => {
let (text, m_row, m_col) = masters.get(si)?;
crate::refers::shift_a1_references(
text,
i64::from(row) - i64::from(*m_row),
i64::from(col) - i64::from(*m_col),
)
}
Content::Text(_) | Content::Number(_) | Content::Bool(_) | Content::Error(_) => {
return None
}
};
Some(strip_future_prefixes(&raw))
}
pub(crate) fn filled(range: A1Range, text: &str, si: Option<u32>, row: u32, col: u32) -> Content {
match si {
None => Content::Formula(text.to_string()),
Some(si) if row == range.top && col == range.left => Content::SharedMaster {
text: text.to_string(),
range: range.text(),
si,
},
Some(si) => Content::SharedChild { si },
}
}
#[cfg(test)]
pub(crate) mod work {
use std::cell::Cell;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct Work {
pub(crate) walked: u64,
pub(crate) moved: u64,
pub(crate) whole: u64,
pub(crate) split: u64,
pub(crate) extents: u64,
pub(crate) copied: u64,
pub(crate) swept: u64,
pub(crate) viewed: u64,
}
thread_local! {
static ON: Cell<bool> = const { Cell::new(false) };
static DONE: Cell<Work> = const {
Cell::new(Work {
walked: 0,
moved: 0,
whole: 0,
split: 0,
extents: 0,
copied: 0,
swept: 0,
viewed: 0,
})
};
}
pub(crate) fn begin() {
ON.with(|on| on.set(true));
DONE.with(|done| done.set(Work::default()));
}
pub(crate) fn end() -> Work {
ON.with(|on| on.set(false));
DONE.with(Cell::get)
}
fn add(f: impl FnOnce(&mut Work)) {
if ON.with(Cell::get) {
DONE.with(|done| {
let mut work = done.get();
f(&mut work);
done.set(work);
});
}
}
pub(crate) fn walked(cells: usize) {
add(|w| w.walked += cells as u64);
}
pub(crate) fn extents(cells: usize) {
add(|w| w.extents += cells as u64);
}
pub(crate) fn moved() {
add(|w| w.moved += 1);
}
pub(crate) fn released(whole: bool) {
add(|w| {
if whole {
w.whole += 1;
} else {
w.split += 1;
}
});
}
pub(crate) fn copied() {
add(|w| w.copied += 1);
}
pub(crate) fn swept(cells: usize) {
add(|w| w.swept += cells as u64);
}
pub(crate) fn viewed(cells: usize) {
add(|w| w.viewed += cells as u64);
}
}
#[derive(Clone, Debug)]
pub struct Workbook {
pub sheets: Vec<Sheet>,
pub styles: Styles,
pub active_sheet: usize,
pub defined_names: Vec<(String, String)>,
}
impl Default for Workbook {
fn default() -> Self {
Self::new()
}
}
impl Workbook {
pub fn new() -> Workbook {
Workbook {
sheets: Vec::new(),
styles: Styles::new(),
active_sheet: 0,
defined_names: Vec::new(),
}
}
pub fn has_dynamic_formulas(&self) -> bool {
self.sheets.iter().any(|s| {
s.cells
.values()
.any(|c| matches!(c.content, Content::DynamicFormula(_)))
})
}
pub fn find_sheet(&self, name: &str) -> Option<usize> {
self.sheets
.iter()
.position(|s| s.name.eq_ignore_ascii_case(name))
}
pub fn ensure_sheet(&mut self, name: &str) -> usize {
if let Some(i) = self.find_sheet(name) {
return i;
}
self.sheets.push(Sheet::new(name));
self.sheets.len() - 1
}
}
pub fn column_letters(col: u32) -> String {
let mut n = col;
let mut letters = Vec::new();
while n > 0 {
let rem = (n - 1) % 26;
letters.push((b'A' + rem as u8) as char);
n = (n - 1) / 26;
}
letters.iter().rev().collect()
}
pub fn cell_ref(row: u32, col: u32) -> String {
format!("{}{}", column_letters(col), row)
}
pub fn column_width_attr(chars: f64) -> f64 {
const MAX_DIGIT_WIDTH: f64 = 7.0;
((chars * MAX_DIGIT_WIDTH + 5.0) / MAX_DIGIT_WIDTH * 256.0).trunc() / 256.0
}
pub fn number_text(v: f64) -> Option<String> {
v.is_finite().then(|| format!("{v}"))
}
pub fn is_name_start(c: char) -> bool {
c.is_ascii_alphabetic() || c == '_'
}
pub fn is_name_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_' || c == '.'
}
pub fn strip_future_prefixes(formula: &str) -> String {
let chars: Vec<char> = formula.chars().collect();
let mut out = String::with_capacity(formula.len());
let mut i = 0;
while i < chars.len() {
let c = chars[i];
if c == '"' {
out.push(c);
i += 1;
while i < chars.len() {
out.push(chars[i]);
if chars[i] == '"' {
if i + 1 < chars.len() && chars[i + 1] == '"' {
out.push('"');
i += 2;
continue;
}
i += 1;
break;
}
i += 1;
}
continue;
}
if is_name_start(c) && (i == 0 || !is_name_char(chars[i - 1])) {
let start = i;
while i < chars.len() && is_name_char(chars[i]) {
i += 1;
}
let name: String = chars[start..i].iter().collect();
let mut bare = name.as_str();
loop {
let before = bare.len();
for prefix in ["_xlfn.", "_xlws.", "_xlpm."] {
if bare.len() > prefix.len()
&& bare[..prefix.len()].eq_ignore_ascii_case(prefix)
{
bare = &bare[prefix.len()..];
}
}
if bare.len() == before {
break;
}
}
out.push_str(bare);
continue;
}
out.push(c);
i += 1;
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn references_and_letters() {
assert_eq!(column_letters(1), "A");
assert_eq!(column_letters(26), "Z");
assert_eq!(column_letters(27), "AA");
assert_eq!(column_letters(702), "ZZ");
assert_eq!(column_letters(703), "AAA");
assert_eq!(cell_ref(1, 2), "B1");
assert_eq!(cell_ref(12, 3), "C12");
}
#[test]
fn a_sheet_is_found_as_the_fold_finds_it() {
use crate::intrinsics::fold;
let mut book = Workbook::new();
for name in ["É", "é", "İSTANBUL", "Straße", "Data", "Screen.last"] {
book.sheets.push(Sheet::new(name));
}
let by_fold = |name: &str| book.sheets.iter().position(|s| fold(&s.name) == fold(name));
let asked = [
("É", Some(0)),
("é", Some(1)),
("İstanbul", Some(2)),
("istanbul", None),
("straße", Some(3)),
("STRASSE", None),
("DATA", Some(4)),
("SCREEN.LAST", Some(5)),
("Screen", None),
("", None),
];
for (name, want) in asked {
assert_eq!(book.find_sheet(name), want, "{name}");
assert_eq!(book.find_sheet(name), by_fold(name), "{name}");
}
}
#[test]
fn the_room_widths_as_excel_writes_them() {
assert_eq!(column_width_attr(72.0), 72.7109375);
assert_eq!(column_width_attr(60.0), 60.7109375);
assert_eq!(column_width_attr(8.43), 9.140625);
}
#[test]
fn styles_dedup_and_fills_keep_first_use_order() {
let mut s = Styles::new();
let a = s.id(Style {
fill: Some(Rgb(1, 2, 3)),
..Style::default()
});
let b = s.id(Style {
fill: Some(Rgb(4, 5, 6)),
wrap: true,
..Style::default()
});
let a2 = s.id(Style {
fill: Some(Rgb(1, 2, 3)),
..Style::default()
});
assert_eq!((a, b, a2), (1, 2, 1));
assert_eq!(s.fills(), vec![Rgb(1, 2, 3), Rgb(4, 5, 6)]);
assert_eq!(Rgb(247, 244, 252).argb_hex(), "FFF7F4FC");
let wrap = Style {
wrap: true,
..Style::default()
};
s.put(5, wrap.clone());
assert_eq!(s.xfs().len(), 6);
assert_eq!(s.get(5), Some(&wrap));
assert_eq!(s.get(4), Some(&Style::default()));
assert_eq!(s.get(6), None);
}
#[test]
fn a1_references_as_a_program_writes_them() {
let r = parse_a1_range("b2").unwrap();
assert_eq!((r.top, r.left, r.bottom, r.right), (2, 2, 2, 2));
assert_eq!(r.text(), "B2");
let r = parse_a1_range("c4:A1").unwrap();
assert_eq!((r.top, r.left, r.bottom, r.right), (1, 1, 4, 3));
assert_eq!(r.text(), "A1:C4");
assert_eq!(r.cells(), 12);
assert_eq!(parse_a1_range("$B$2:$B$4").unwrap().text(), "B2:B4");
assert_eq!(parse_a1_range("XFD1048576").unwrap().text(), "XFD1048576");
for bad in [
"c:c", "1:3", "XFE1", "A0", "A1048577", "AAAA1", "b", "2", "", "b2:",
] {
assert!(parse_a1_range(bad).is_none(), "{bad} should not parse");
}
}
#[test]
fn a_formula_over_a_range_is_shared_from_its_top_left_cell() {
let mut sh = Sheet::new("S");
sh.set_formula(parse_a1_range("c2:c3").unwrap(), "B2+B3", 0);
sh.set_formula(parse_a1_range("d2").unwrap(), "B2*2", 0);
sh.set_formula(parse_a1_range("e2:e3").unwrap(), "1", 0);
assert_eq!(
sh.cells[&(2, 3)].content,
Content::SharedMaster {
text: "B2+B3".to_string(),
range: "C2:C3".to_string(),
si: 0
}
);
assert_eq!(sh.cells[&(3, 3)].content, Content::SharedChild { si: 0 });
assert_eq!(
sh.cells[&(2, 4)].content,
Content::Formula("B2*2".to_string())
);
assert_eq!(
sh.cells[&(2, 5)].content,
Content::SharedMaster {
text: "1".to_string(),
range: "E2:E3".to_string(),
si: 1
}
);
assert_eq!(sh.shared_formulas, 2);
}
#[test]
fn a_dynamic_formula_over_a_range_is_one_per_cell_with_its_references_moved() {
let mut sh = Sheet::new("S");
sh.set_formula_dynamic(
parse_a1_range("c2:d3").unwrap(),
"IFS(B2>3,$A$1,TRUE,B$1)",
0,
);
assert_eq!(
sh.cells[&(2, 3)].content,
Content::DynamicFormula("IFS(B2>3,$A$1,TRUE,B$1)".to_string())
);
assert_eq!(
sh.cells[&(3, 3)].content,
Content::DynamicFormula("IFS(B3>3,$A$1,TRUE,B$1)".to_string())
);
assert_eq!(
sh.cells[&(2, 4)].content,
Content::DynamicFormula("IFS(C2>3,$A$1,TRUE,C$1)".to_string())
);
assert_eq!(
sh.cells[&(3, 4)].content,
Content::DynamicFormula("IFS(C3>3,$A$1,TRUE,C$1)".to_string())
);
let mut wb = Workbook::new();
assert!(!wb.has_dynamic_formulas());
wb.sheets.push(sh);
assert!(wb.has_dynamic_formulas());
}
#[test]
fn sheets_are_found_without_case_and_made_once() {
let mut wb = Workbook::new();
assert_eq!(wb.find_sheet("Output"), None);
let i = wb.ensure_sheet("Output");
assert_eq!(wb.ensure_sheet("output"), i);
assert_eq!(wb.find_sheet("OUTPUT"), Some(i));
assert_eq!(wb.sheets.len(), 1);
assert_eq!(wb.sheets[0].name, "Output");
}
#[test]
fn a_sheet_knows_its_extent() {
let mut sh = Sheet::new("S");
assert_eq!(sh.extent(), None);
sh.set(
3,
2,
Cell {
content: Content::Number(1.0),
style: 0,
},
);
sh.set(
1,
3,
Cell {
content: Content::Bool(true),
style: 0,
},
);
assert_eq!(sh.extent(), Some(((1, 2), (3, 3))));
}
#[test]
fn numbers_as_the_file_holds_them() {
assert_eq!(number_text(5.0).as_deref(), Some("5"));
assert_eq!(number_text(0.1).as_deref(), Some("0.1"));
assert_eq!(number_text(-2.5).as_deref(), Some("-2.5"));
assert_eq!(number_text(f64::NAN), None);
assert_eq!(number_text(f64::INFINITY), None);
}
#[test]
fn the_prefixes_come_off_a_formula_read_back() {
assert_eq!(
strip_future_prefixes("_xlfn.IFS(B2>3,\"big\",TRUE,\"small\")"),
"IFS(B2>3,\"big\",TRUE,\"small\")"
);
assert_eq!(
strip_future_prefixes("_xlfn._xlws.FILTER(A:A,B:B>1)"),
"FILTER(A:A,B:B>1)"
);
assert_eq!(
strip_future_prefixes("_xlfn.LET(_xlpm.x,1,_xlpm.x*2)"),
"LET(x,1,x*2)"
);
assert_eq!(
strip_future_prefixes("SUM(A1:A3)&\"_xlfn.\""),
"SUM(A1:A3)&\"_xlfn.\""
);
assert_eq!(strip_future_prefixes("Rate*B1"), "Rate*B1");
assert_eq!(strip_future_prefixes("_xlfn."), "_xlfn.");
}
}