///|
pub struct Table {
id : Int
name : String
display_name : String
range : String
range_ref : String
columns : Array[String]
style_name : String
show_first_column : Bool
show_last_column : Bool
show_row_stripes : Bool
show_column_stripes : Bool
show_header_row : Bool
/// Whether the SOURCE table part carried calculatedColumnFormula or
/// totalsRowFormula markup. The model does not retain those constructs
/// and the writer regenerates table XML without them, so a rewrite of a
/// workbook whose table carries them silently drops the formulas —
/// callers that must not lose formula content check this flag. Detection
/// is a raw scan of the part, so a column literally named after one of
/// the elements also raises the flag (conservative, never the reverse).
/// Always false for API-built tables.
carries_source_formulas : Bool
}
///|
struct TableOptions {
range_ref : String
mut name : String
mut style_name : String
mut show_first_column : Bool
mut show_last_column : Bool
mut show_row_stripes : Bool?
mut show_column_stripes : Bool
mut show_header_row : Bool?
} derive(Debug)
///|
pub fn TableOptions::new(range_ref : String) -> TableOptions raise XlsxError {
if range_ref == "" {
raise InvalidTable(msg="table range empty")
}
ignore(parse_range_ref(normalize_range_ref(range_ref)))
{
range_ref: normalize_range_ref(range_ref),
name: "",
style_name: "",
show_first_column: false,
show_last_column: false,
show_row_stripes: None,
show_column_stripes: false,
show_header_row: None,
}
}
///|
pub fn TableOptions::set_name(self : TableOptions, name : String) -> Unit {
self.name = name
}
///|
pub fn TableOptions::set_style_name(
self : TableOptions,
style_name : String,
) -> Unit {
self.style_name = style_name
}
///|
pub fn TableOptions::set_show_first_column(
self : TableOptions,
value : Bool,
) -> Unit {
self.show_first_column = value
}
///|
pub fn TableOptions::set_show_last_column(
self : TableOptions,
value : Bool,
) -> Unit {
self.show_last_column = value
}
///|
pub fn TableOptions::set_show_row_stripes(
self : TableOptions,
value : Bool?,
) -> Unit {
self.show_row_stripes = value
}
///|
pub fn TableOptions::set_show_column_stripes(
self : TableOptions,
value : Bool,
) -> Unit {
self.show_column_stripes = value
}
///|
pub fn TableOptions::set_show_header_row(
self : TableOptions,
value : Bool?,
) -> Unit {
self.show_header_row = value
}
///|
fn Table::new(
name : String,
range_ref : String,
columns : ArrayView[String],
display_name? : String = "",
style_name? : String = "TableStyleMedium9",
show_first_column? : Bool = false,
show_last_column? : Bool = false,
show_row_stripes? : Bool = true,
show_column_stripes? : Bool = false,
show_header_row? : Bool = true,
) -> Table raise XlsxError {
if name == "" {
raise InvalidTable(msg="table name is empty")
}
let display = if display_name == "" { name } else { display_name }
let style = if style_name == "" { "TableStyleMedium9" } else { style_name }
let normalized_input_ref = normalize_range_ref(range_ref)
let (min_row, min_col, max_row, max_col) = parse_range_ref(
normalized_input_ref,
)
let normalized_ref = if show_header_row {
normalized_input_ref
} else {
let adjusted_max_row = if min_row == max_row {
max_row + 1
} else {
max_row
}
normalize_range_ref(
"\{cell_ref_from(min_row + 1, min_col)}:\{cell_ref_from(adjusted_max_row, max_col)}",
)
}
let expected_cols = max_col - min_col + 1
let column_names : Array[String] = []
for column in columns {
column_names.push(column)
}
if column_names.length() == 0 {
raise InvalidTable(msg="table columns empty")
}
if column_names.length() != expected_cols {
raise InvalidTable(msg="table columns do not match range")
}
{
id: 0,
name,
display_name: display,
range: normalized_ref,
range_ref: normalized_ref,
columns: column_names,
style_name: style,
show_first_column,
show_last_column,
show_row_stripes,
show_column_stripes,
show_header_row,
carries_source_formulas: false,
}
}
///|
fn write_table_xml(
table_id : Int,
table : Table,
budget? : WritePartBudget,
) -> String raise XlsxError {
fn bool_attr(value : Bool) -> String {
if value {
"1"
} else {
"0"
}
}
let sb = LimitedXmlBuilder::new(budget)
sb.write_view("\n")
sb.write_view(
"\n")
} else {
sb.write_view("\" totalsRowShown=\"0\">\n")
sb.write_view(" \n")
}
sb.write_view(" \n")
for i, name in table.columns {
let id = i + 1
sb.write_view(" \n")
}
sb.write_view(" \n")
sb.write_view(" \n",
)
sb.write_view("
")
sb.to_string()
}
///|
fn parse_table_columns(xml : StringView) -> Array[String] raise XlsxError {
let columns : Array[String] = []
let body = match extract_tag_body(xml, "tableColumns") {
Some(value) => value
None => raise InvalidTable(msg="table columns missing")
}
let mut first = true
for chunk in body.split("") {
Some(pos) => pos
None =>
match text.find(">") {
Some(pos) => pos
None => raise InvalidXml(msg="tableColumn tag not closed")
}
}
let tag = text[:end]
let name = match attr_value(tag, "name") {
Some(value) => value
None => raise InvalidTable(msg="table column name missing")
}
columns.push(unescape_xml_text(name))
}
columns
}
///|
fn parse_table_style(
xml : StringView,
) -> (String, Bool, Bool, Bool, Bool) raise XlsxError {
fn parse_bool_attr(value : StringView) -> Bool {
match value {
"1" | "true" | "TRUE" => true
_ => false
}
}
let text = xml.to_owned()
let start = match text.find(" pos
None => return ("TableStyleMedium9", false, false, true, false)
}
let rest = text[start:]
let end = match rest.find("/>") {
Some(pos) => pos
None =>
match rest.find(">") {
Some(pos) => pos
None => return ("TableStyleMedium9", false, false, true, false)
}
}
let tag = rest[:end]
let style_name = match attr_value(tag, "name") {
Some(value) => unescape_xml_text(value)
None => "TableStyleMedium9"
}
let show_first_column = match attr_value(tag, "showFirstColumn") {
Some(value) => parse_bool_attr(value)
None => false
}
let show_last_column = match attr_value(tag, "showLastColumn") {
Some(value) => parse_bool_attr(value)
None => false
}
let show_row_stripes = match attr_value(tag, "showRowStripes") {
Some(value) => parse_bool_attr(value)
None => true
}
let show_column_stripes = match attr_value(tag, "showColumnStripes") {
Some(value) => parse_bool_attr(value)
None => false
}
(
style_name, show_first_column, show_last_column, show_row_stripes, show_column_stripes,
)
}
///|
fn parse_table_xml(xml : StringView) -> Table raise XlsxError {
let text = xml.to_owned()
let start = match text.find(" pos
None => raise InvalidXml(msg="table tag missing")
}
let rest = text[start:]
let end = match rest.find(">") {
Some(pos) => pos
None => raise InvalidXml(msg="table tag not closed")
}
let tag = rest[:end]
let id = match attr_value(tag, "id") {
Some(value) =>
@string.parse_int(value, base=10) catch {
_ => raise InvalidXml(msg="table id invalid")
}
None => raise InvalidXml(msg="table id missing")
}
let name = match attr_value(tag, "name") {
Some(value) => unescape_xml_text(value)
None =>
match attr_value(tag, "displayName") {
Some(value) => unescape_xml_text(value)
None => raise InvalidXml(msg="table name missing")
}
}
let display_name = match attr_value(tag, "displayName") {
Some(value) => unescape_xml_text(value)
None => name
}
let ref_value = match attr_value(tag, "ref") {
Some(value) => unescape_xml_text(value)
None => raise InvalidXml(msg="table ref missing")
}
let show_header_row = match attr_value(tag, "headerRowCount") {
Some(value) => value != "0"
None => true
}
let columns = parse_table_columns(xml)
let (_min_row, min_col, _max_row, max_col) = parse_range_ref(ref_value)
let expected_cols = max_col - min_col + 1
if columns.length() != expected_cols {
raise InvalidTable(msg="table columns do not match range")
}
let (
style_name,
show_first_column,
show_last_column,
show_row_stripes,
show_column_stripes,
) = parse_table_style(xml)
let normalized_ref = normalize_range_ref(ref_value)
{
id,
name,
display_name,
range: normalized_ref,
range_ref: normalized_ref,
columns,
style_name,
show_first_column,
show_last_column,
show_row_stripes,
show_column_stripes,
show_header_row,
carries_source_formulas: text.find("calculatedColumnFormula") is Some(_) ||
text.find("totalsRowFormula") is Some(_),
}
}