use crate::form::Form;
use crate::printer::write_datum;
use crate::refers::{self, FormulaRef, Kind};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Value {
Number(String),
Text(String),
Bool(bool),
Error(String),
Date(String),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Visibility {
Visible,
Hidden,
VeryHidden,
}
impl Visibility {
pub fn word(self) -> &'static str {
match self {
Visibility::Visible => "visible",
Visibility::Hidden => "hidden",
Visibility::VeryHidden => "very-hidden",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RefersTo {
Reference(String),
Unreadable(&'static str),
}
impl RefersTo {
pub fn of(r: &FormulaRef, home_sheet: &str) -> RefersTo {
match r.kind {
Kind::Unreadable => RefersTo::Unreadable(r.unreadable_name()),
_ => RefersTo::Reference(refers::spell(r, home_sheet)),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Row<'r> {
Sheet {
name: &'r str,
visibility: Visibility,
},
Name {
name: &'r str,
refers_to: &'r str,
},
Table {
name: &'r str,
sheet: &'r str,
range: &'r str,
},
Cell {
sheet: &'r str,
addr: &'r str,
value: &'r Value,
},
Formula {
sheet: &'r str,
addr: &'r str,
text: &'r str,
},
Refers {
sheet: &'r str,
addr: &'r str,
to: &'r RefersTo,
},
}
pub trait Sink {
fn row(&mut self, row: &Row<'_>);
}
pub fn sheet_prefix(name: &str) -> String {
refers::quote_sheet(name)
}
pub fn quoted(s: &str) -> String {
write_datum(&Form::string(s))
}
pub fn target(to: &RefersTo) -> String {
match to {
RefersTo::Reference(text) => quoted(text),
RefersTo::Unreadable(name) => format!("(unreadable {})", quoted(name)),
}
}
pub fn datum(v: &Value) -> String {
match v {
Value::Number(text) => text.clone(),
Value::Text(text) => quoted(text),
Value::Bool(true) => "true".to_string(),
Value::Bool(false) => "false".to_string(),
Value::Error(text) => format!("(error {})", quoted(text)),
Value::Date(text) => format!("(date {})", quoted(text)),
}
}
pub fn line(row: &Row<'_>) -> String {
match row {
Row::Sheet { name, visibility } => {
format!("(sheet {} {})", quoted(name), visibility.word())
}
Row::Name { name, refers_to } => {
format!("(name {} {})", quoted(name), quoted(refers_to))
}
Row::Table { name, sheet, range } => {
format!(
"(table {} {} {})",
quoted(name),
quoted(sheet),
quoted(range)
)
}
Row::Cell { sheet, addr, value } => {
format!("(cell {} {} {})", quoted(sheet), quoted(addr), datum(value))
}
Row::Formula { sheet, addr, text } => {
format!(
"(formula {} {} {})",
quoted(sheet),
quoted(addr),
quoted(text)
)
}
Row::Refers { sheet, addr, to } => {
format!(
"(refers {} {})",
quoted(&format!("{}!{}", sheet_prefix(sheet), addr)),
target(to)
)
}
}
}
#[derive(Default)]
pub struct Printer {
pub out: String,
}
impl Sink for Printer {
fn row(&mut self, row: &Row<'_>) {
self.out.push_str(&line(row));
self.out.push('\n');
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn each_row_prints_in_the_corpus_notation() {
assert_eq!(
line(&Row::Sheet {
name: "Q1 Data",
visibility: Visibility::VeryHidden
}),
"(sheet \"Q1 Data\" very-hidden)"
);
assert_eq!(
line(&Row::Name {
name: "Rate",
refers_to: "Model!$B$1"
}),
"(name \"Rate\" \"Model!$B$1\")"
);
assert_eq!(
line(&Row::Table {
name: "Sales",
sheet: "Data",
range: "A1:B4"
}),
"(table \"Sales\" \"Data\" \"A1:B4\")"
);
assert_eq!(
line(&Row::Cell {
sheet: "Model",
addr: "B1",
value: &Value::Number("1200".to_string())
}),
"(cell \"Model\" \"B1\" 1200)"
);
assert_eq!(
line(&Row::Cell {
sheet: "Model",
addr: "A1",
value: &Value::Text("say \"hi\"".to_string())
}),
"(cell \"Model\" \"A1\" \"say \\\"hi\\\"\")"
);
assert_eq!(datum(&Value::Bool(true)), "true");
assert_eq!(datum(&Value::Bool(false)), "false");
assert_eq!(
datum(&Value::Error("#DIV/0!".to_string())),
"(error \"#DIV/0!\")"
);
assert_eq!(
datum(&Value::Date("2026-10-03".to_string())),
"(date \"2026-10-03\")"
);
assert_eq!(
line(&Row::Formula {
sheet: "Model",
addr: "B3",
text: "=B1-B2"
}),
"(formula \"Model\" \"B3\" \"=B1-B2\")"
);
assert_eq!(
line(&Row::Refers {
sheet: "Q1 Data",
addr: "B3",
to: &RefersTo::Reference("Model!B1".to_string())
}),
"(refers \"'Q1 Data'!B3\" \"Model!B1\")"
);
assert_eq!(
line(&Row::Refers {
sheet: "Model",
addr: "D2",
to: &RefersTo::Unreadable("INDIRECT")
}),
"(refers \"Model!D2\" (unreadable \"INDIRECT\"))"
);
assert!(
target(&RefersTo::Reference("Model!B1".to_string()))
< target(&RefersTo::Unreadable("OFFSET"))
);
let mut p = Printer::default();
p.row(&Row::Sheet {
name: "A",
visibility: Visibility::Visible,
});
p.row(&Row::Sheet {
name: "B",
visibility: Visibility::Hidden,
});
assert_eq!(p.out, "(sheet \"A\" visible)\n(sheet \"B\" hidden)\n");
}
#[test]
fn a_sheet_name_is_quoted_as_a_reference_quotes_it() {
assert_eq!(sheet_prefix("Model"), "Model");
assert_eq!(sheet_prefix("Sheet.1"), "Sheet.1");
assert_eq!(sheet_prefix("Q1 Data"), "'Q1 Data'");
assert_eq!(sheet_prefix("It's"), "'It''s'");
assert_eq!(sheet_prefix("2026"), "'2026'");
assert_eq!(sheet_prefix("Donn\u{e9}es"), "Donn\u{e9}es");
assert_eq!(sheet_prefix("a-b"), "'a-b'");
assert_eq!(sheet_prefix("A1"), "'A1'");
assert_eq!(sheet_prefix("R1C1"), "'R1C1'");
}
#[test]
fn a_target_is_a_reference_or_an_unreadable_call() {
let refs = refers::scan("=OFFSET(B1,1,0)+'Q1 Data'!A1");
assert_eq!(
RefersTo::of(&refs[0], "Model"),
RefersTo::Unreadable("OFFSET")
);
assert_eq!(
RefersTo::of(&refs[1], "Model"),
RefersTo::Reference("Model!B1".to_string())
);
assert_eq!(
RefersTo::of(&refs[2], "Model"),
RefersTo::Reference("'Q1 Data'!A1".to_string())
);
}
}