///|
fn convert_align(align : Align) -> @papergrid.HAlign {
match align {
Left => @papergrid.HAlign::left()
Right => @papergrid.HAlign::right()
Center => @papergrid.HAlign::center()
}
}
///|
fn convert_valign(align : VAlign) -> @papergrid.VAlign {
match align {
Top => @papergrid.VAlign::top()
Bottom => @papergrid.VAlign::bottom()
Center => @papergrid.VAlign::center()
}
}
///|
fn convert_width(width : Width) -> @papergrid.WidthMode {
match width {
Wrap(size, placeholder) =>
@papergrid.WidthMode::wrap_with(size, placeholder)
Truncate(size, suffix) => @papergrid.WidthMode::truncate(size, suffix)
}
}
///|
fn convert_height(height : Height) -> @papergrid.HeightMode {
match height {
Increase(size) => @papergrid.HeightMode::increase(size)
Limit(size) => @papergrid.HeightMode::limit(size)
}
}
///|
fn join_lines(lines : Array[String]) -> String {
let sb = StringBuilder::new()
for i, line in lines {
if i > 0 {
sb.write_char('\n')
}
sb.write_string(line)
}
sb.to_string()
}
///|
fn format_content_once(
value : String,
format : Format,
row : Int,
col : Int,
) -> String {
match format {
Surround(prefix, suffix, _) => prefix + value + suffix
Value(text, _) => text
Content(transform, _) => transform(value)
Positioned(transform, _) => transform(value, row, col)
}
}
///|
fn format_content(
value : String,
format : Format,
row : Int,
col : Int,
) -> String {
let multiline = match format {
Surround(_, _, multiline) => multiline
Value(_, multiline) => multiline
Content(_, multiline) => multiline
Positioned(_, multiline) => multiline
}
if !multiline {
return format_content_once(value, format, row, col)
}
let lines = @papergrid.split_lines(value)
let formatted = []
lines.each(line => formatted.push(format_content_once(line, format, row, col)))
join_lines(formatted)
}
///|
fn clamp_start(value : Int) -> Int {
if value < 0 {
0
} else {
value
}
}
///|
fn clamp_end(value : Int, limit : Int) -> Int {
if value < 0 || value > limit {
limit
} else {
value
}
}
///|
fn resolve_indexes(start : Int, end_ : Int, count : Int) -> Array[Int] {
let result = []
if start <= -2 && end_ == 0 {
let offset = -start - 2
let index = count - offset - 1
if index >= 0 && index < count {
result.push(index)
}
return result
}
let start_index = clamp_start(start)
let end_index = clamp_end(end_, count)
for index = start_index; index < end_index; index = index + 1 {
result.push(index)
}
result
}
///|
fn[T] apply_skip_step(items : Array[T], skip_n : Int, step_n : Int) -> Array[T] {
let result = []
let mut seen = 0
items.each(item => {
if seen < skip_n {
seen = seen + 1
} else {
if step_n <= 1 || (seen - skip_n) % step_n == 0 {
result.push(item)
}
seen = seen + 1
}
})
result
}
///|
fn resolve_rows(rows : Rows, count : Int) -> Array[Int] {
let base = resolve_indexes(rows.start, rows.end_, count)
let after_exclude = match rows.exclude {
Some(excluded_rows) => {
let excluded_set = @hashset.from_array(
resolve_rows(excluded_rows, count)[:],
)
let filtered = []
base.each(index => {
if !excluded_set.contains(index) {
filtered.push(index)
}
})
filtered
}
None => base
}
let after_skip_step = apply_skip_step(after_exclude, rows.skip_n, rows.step_n)
match rows.predicate {
Some(predicate) => {
let filtered = []
after_skip_step.each(index => {
if predicate(Row(index)) {
filtered.push(index)
}
})
filtered
}
None => after_skip_step
}
}
///|
fn resolve_cols(cols : Cols, count : Int) -> Array[Int] {
let base = resolve_indexes(cols.start, cols.end_, count)
let after_exclude = match cols.exclude {
Some(excluded_cols) => {
let excluded_set = @hashset.from_array(
resolve_cols(excluded_cols, count)[:],
)
let filtered = []
base.each(index => {
if !excluded_set.contains(index) {
filtered.push(index)
}
})
filtered
}
None => base
}
let after_skip_step = apply_skip_step(after_exclude, cols.skip_n, cols.step_n)
match cols.predicate {
Some(predicate) => {
let filtered = []
after_skip_step.each(index => {
if predicate(Col(index)) {
filtered.push(index)
}
})
filtered
}
None => after_skip_step
}
}
///|
fn resolve_cols_by_name(
rows : Array[Array[String]],
by_name : ByColName,
) -> Array[Int] {
if rows.is_empty() {
return []
}
let result = []
rows[0].eachi((index, value) => {
if value == by_name.name {
result.push(index)
}
})
result
}
///|
fn unique_cells(cells : Array[(Int, Int)]) -> Array[(Int, Int)] {
let seen : @hashset.HashSet[(Int, Int)] = @hashset.new()
let result = []
cells.each(cell => {
if !seen.contains(cell) {
seen.add(cell)
result.push(cell)
}
})
result
}
///|
fn unique_indexes(indexes : Array[Int]) -> Array[Int] {
let seen = @hashset.new()
let result = []
indexes.each(index => {
if !seen.contains(index) {
seen.add(index)
result.push(index)
}
})
result
}
///|
fn sort_indexes_desc(indexes : Array[Int]) -> Array[Int] {
let result = indexes.copy()
result.sort()
result.rev()
}
///|
fn build_identity_map(count : Int) -> Array[Int] {
let result = Array::make(count, -1)
for index in 0.. Array[Int] {
let result = Array::make(count, -1)
let mut next = 0
indexes.each(index => {
if index >= 0 && index < count && result[index] < 0 {
result[index] = next
next = next + 1
}
})
result
}
///|
fn build_remove_map(count : Int, indexes : Array[Int]) -> Array[Int] {
let removed = @hashset.from_array(indexes[:])
let result = Array::make(count, -1)
let mut next = 0
for index in 0.. Unit {
let unique = unique_indexes(indexes)
let sorted = sort_indexes_desc(unique)
sorted.each(index => {
if index >= 0 && index < rows.length() {
rows.remove(index) |> ignore
}
})
}
///|
fn remove_cols_at(rows : Array[Array[String]], indexes : Array[Int]) -> Unit {
let unique = unique_indexes(indexes)
let sorted = sort_indexes_desc(unique)
rows.each(row => {
sorted.each(index => {
if index >= 0 && index < row.length() {
row.remove(index) |> ignore
}
})
})
}
///|
fn extract_rows_at(
rows : Array[Array[String]],
indexes : Array[Int],
) -> Array[Array[String]] {
let result = []
indexes.each(index => {
if index >= 0 && index < rows.length() {
result.push(rows[index].copy())
}
})
result
}
///|
fn extract_cols_at(
rows : Array[Array[String]],
indexes : Array[Int],
) -> Array[Array[String]] {
let result = []
rows.each(row => {
let next = []
indexes.each(index => {
if index >= 0 && index < row.length() {
next.push(row[index])
}
})
result.push(next)
})
result
}
///|
fn resolve_segment_cells(
segment : Segment,
row_count : Int,
col_count : Int,
) -> Array[(Int, Int)] {
let result = []
let skip_n = segment.skip_n
let step_n = segment.step_n
let (row_start, row_end, col_start, col_end) = match segment.kind {
All => (0, row_count, 0, col_count)
Range(row_start, row_end, col_start, col_end) =>
(
clamp_start(row_start),
clamp_end(row_end, row_count),
clamp_start(col_start),
clamp_end(col_end, col_count),
)
RowsOnly(rows) => {
let row_indexes = resolve_rows(rows, row_count)
let cells = []
row_indexes.each(row => {
for col in 0.. {
let col_indexes = resolve_cols(cols, col_count)
let cells = []
for row in 0.. cells.push((row, col)))
}
return apply_skip_step(cells, skip_n, step_n)
}
CellOnly(cell) => {
let cells = if cell.row >= 0 &&
cell.row < row_count &&
cell.col >= 0 &&
cell.col < col_count {
[(cell.row, cell.col)]
} else {
[]
}
return apply_skip_step(cells, skip_n, step_n)
}
Cross(rows, cols) => {
let row_indexes = resolve_rows(rows, row_count)
let col_indexes = resolve_cols(cols, col_count)
let cells = []
row_indexes.each(row => col_indexes.each(col => cells.push((row, col))))
return apply_skip_step(cells, skip_n, step_n)
}
InverseRows(rows) => {
let excluded = @hashset.from_array(resolve_rows(rows, row_count)[:])
let cells = []
for row in 0.. {
let excluded = @hashset.from_array(resolve_cols(cols, col_count)[:])
let cells = []
for row in 0.. {
let cells = []
for row in 0.. {
let lhs_cells = resolve_segment_cells(lhs, row_count, col_count)
let rhs_cells = resolve_segment_cells(rhs, row_count, col_count)
return apply_skip_step(
unique_cells(lhs_cells + rhs_cells),
skip_n,
step_n,
)
}
Diff2(lhs, rhs) => {
let lhs_cells = resolve_segment_cells(lhs, row_count, col_count)
let rhs_set = @hashset.from_array(
resolve_segment_cells(rhs, row_count, col_count)[:],
)
let cells = []
lhs_cells.each(cell => if !rhs_set.contains(cell) { cells.push(cell) })
return apply_skip_step(unique_cells(cells), skip_n, step_n)
}
}
for row = row_start; row < row_end; row = row + 1 {
for col = col_start; col < col_end; col = col + 1 {
result.push((row, col))
}
}
apply_skip_step(result, skip_n, step_n)
}
///|
fn apply_setting_to_row(
config : @papergrid.SpannedConfig,
row : Int,
setting : Setting,
) -> Unit {
match setting {
Align(align) =>
config.set_align_for_rows(row, row + 1, convert_align(align))
VAlign(align) =>
config.set_valign_for_rows(row, row + 1, convert_valign(align))
Pad(padding) =>
config.set_padding_for_rows(
row,
row + 1,
padding.left,
padding.right,
padding.top,
padding.bottom,
)
W(width) =>
config.set_width_mode_for_rows(row, row + 1, convert_width(width))
H(height) =>
config.set_height_mode_for_rows(row, row + 1, convert_height(height))
Fmt(_) => ()
SpanCol(_) => ()
SpanRow(_) => ()
}
}
///|
fn apply_setting_to_col(
config : @papergrid.SpannedConfig,
col : Int,
setting : Setting,
) -> Unit {
match setting {
Align(align) =>
config.set_align_for_cols(col, col + 1, convert_align(align))
VAlign(align) =>
config.set_valign_for_cols(col, col + 1, convert_valign(align))
Pad(padding) =>
config.set_padding_for_cols(
col,
col + 1,
padding.left,
padding.right,
padding.top,
padding.bottom,
)
W(width) =>
config.set_width_mode_for_cols(col, col + 1, convert_width(width))
H(height) =>
config.set_height_mode_for_cols(col, col + 1, convert_height(height))
Fmt(_) => ()
SpanCol(_) => ()
SpanRow(_) => ()
}
}
///|
fn apply_setting_to_cell(
config : @papergrid.SpannedConfig,
row : Int,
col : Int,
setting : Setting,
) -> Unit {
match setting {
Align(align) => config.set_align_for_cell(row, col, convert_align(align))
VAlign(align) => config.set_valign_for_cell(row, col, convert_valign(align))
Pad(padding) =>
config.set_padding_for_cell(
row,
col,
padding.left,
padding.right,
padding.top,
padding.bottom,
)
W(width) => config.set_width_mode_for_cell(row, col, convert_width(width))
H(height) =>
config.set_height_mode_for_cell(row, col, convert_height(height))
Fmt(_) => ()
SpanCol(_) => ()
SpanRow(_) => ()
}
}
///|
fn span_intersects_col(
config : @papergrid.SpannedConfig,
row : Int,
col : Int,
span : Int,
) -> Bool {
let end_ = col + span
for current = col; current < end_; current = current + 1 {
if !config.is_cell_visible(row, current) {
return true
}
}
false
}
///|
fn span_intersects_row(
config : @papergrid.SpannedConfig,
row : Int,
col : Int,
span : Int,
) -> Bool {
let end_ = row + span
for current = row; current < end_; current = current + 1 {
if !config.is_cell_visible(current, col) {
return true
}
}
false
}
///|
fn resolved_span_anchor(
config : @papergrid.SpannedConfig,
row : Int,
col : Int,
) -> (Int, Int) {
match config.span_anchor_for_source(row, col) {
Some(anchor) => anchor
None => (row, col)
}
}
///|
fn apply_col_span(
rows : Array[Array[String]],
config : @papergrid.SpannedConfig,
row : Int,
col : Int,
size : Int,
) -> Unit {
if row < 0 || row >= rows.length() {
return
}
let col_count = @papergrid.IterRecords::new(rows).count_cols()
if col < 0 || col >= col_count {
return
}
let current_anchor = resolved_span_anchor(config, row, col)
if (current_anchor.0 == row && current_anchor.1 == col) &&
!config.is_cell_visible(row, col) {
return
}
if size == 1 {
config.set_col_span(current_anchor.0, current_anchor.1, 1)
config.clear_unused_span_source(current_anchor.0, current_anchor.1)
return
}
config.set_col_span(current_anchor.0, current_anchor.1, 1)
config.clear_unused_span_source(current_anchor.0, current_anchor.1)
let start = if size > 1 {
col
} else if size == 0 {
0
} else {
let normalized = col + size
if normalized < 0 {
0
} else {
normalized
}
}
let span = if size > 1 {
if start + size > col_count {
col_count - start
} else {
size
}
} else if size == 0 {
col_count - start
} else {
col - start + 1
}
if span <= 1 || span_intersects_col(config, row, start, span) {
return
}
config.set_col_span(row, start, span)
config.set_span_source(row, start, row, col)
}
///|
fn apply_row_span(
rows : Array[Array[String]],
config : @papergrid.SpannedConfig,
row : Int,
col : Int,
size : Int,
) -> Unit {
if rows.is_empty() || row < 0 || row >= rows.length() {
return
}
let col_count = @papergrid.IterRecords::new(rows).count_cols()
if col < 0 || col >= col_count {
return
}
let current_anchor = resolved_span_anchor(config, row, col)
if (current_anchor.0 == row && current_anchor.1 == col) &&
!config.is_cell_visible(row, col) {
return
}
if size == 1 {
config.set_row_span(current_anchor.0, current_anchor.1, 1)
config.clear_unused_span_source(current_anchor.0, current_anchor.1)
return
}
config.set_row_span(current_anchor.0, current_anchor.1, 1)
config.clear_unused_span_source(current_anchor.0, current_anchor.1)
let start = if size > 1 {
row
} else if size == 0 {
0
} else {
let normalized = row + size
if normalized < 0 {
0
} else {
normalized
}
}
let span = if size > 1 {
if row + size > rows.length() {
rows.length() - start
} else {
size
}
} else if size == 0 {
rows.length() - start
} else {
row - start + 1
}
if span <= 1 || span_intersects_row(config, start, col, span) {
return
}
config.set_row_span(start, col, span)
config.set_span_source(start, col, row, col)
}
///|
fn apply_format_to_cell(
rows : Array[Array[String]],
row : Int,
col : Int,
format : Format,
) -> Unit {
if row < 0 || row >= rows.length() {
return
}
let row_values = rows[row]
if col < 0 || col >= row_values.length() {
return
}
row_values[col] = format_content(row_values[col], format, row, col)
}
///|
fn apply_setting_to_row_values(
rows : Array[Array[String]],
row : Int,
setting : Setting,
) -> Unit {
match setting {
Fmt(format) =>
if row >= 0 && row < rows.length() {
let row_values = rows[row]
for col in 0.. ()
H(_) => ()
_ => ()
}
}
///|
fn apply_setting_to_col_values(
rows : Array[Array[String]],
col : Int,
setting : Setting,
) -> Unit {
match setting {
Fmt(format) =>
for row in 0.. ()
H(_) => ()
_ => ()
}
}
///|
fn apply_setting_to_cell_value(
rows : Array[Array[String]],
config : @papergrid.SpannedConfig,
row : Int,
col : Int,
setting : Setting,
) -> Unit {
match setting {
Fmt(format) => apply_format_to_cell(rows, row, col, format)
W(_) => ()
H(_) => ()
SpanCol(size) => apply_col_span(rows, config, row, col, size)
SpanRow(size) => apply_row_span(rows, config, row, col, size)
_ => ()
}
}
///|
fn normalize_rows(
rows : Array[Array[String]],
empty_cell : String,
) -> Array[Array[String]] {
let mut max_cols = 0
rows.each(row => if row.length() > max_cols { max_cols = row.length() })
rows.map(row => {
let normalized = []
for col in 0.. IndexBuilder {
{
rows: self.rows,
empty_cell: self.empty_cell,
index_col: None,
index_name: Some(""),
show_index: true,
}
}
///|
/// Use the specified col as the index col (0-based).
pub fn IndexBuilder::col(self : IndexBuilder, col : Int) -> IndexBuilder {
self.index_col = Some(col)
self
}
///|
/// Set the name for the index col. None removes the name row.
pub fn IndexBuilder::name(self : IndexBuilder, name : String?) -> IndexBuilder {
self.index_name = name
self
}
///|
/// Hide the index col.
pub fn IndexBuilder::hide(self : IndexBuilder) -> IndexBuilder {
self.show_index = false
self
}
///|
/// Transpose the table (swap rows and cols).
pub fn IndexBuilder::transpose(self : IndexBuilder) -> IndexBuilder {
let norm = normalize_rows(self.rows, self.empty_cell)
let row_count = norm.length()
let col_count = if row_count > 0 { norm[0].length() } else { 0 }
if row_count == 0 || col_count == 0 {
return self
}
// After transpose, first col = original headers (row 0 values),
// remaining cols = original data rows, new header = row indices
let result : Array[Array[String]] = []
// Header row: empty + original row indices
let header : Array[String] = [""]
for r = 1; r < row_count; r = r + 1 {
header.push((r - 1).to_string())
}
result.push(header)
// Each original col becomes a data row
for c in 0.. self.rows.push(row))
// After transpose, index is already baked in; disable further index processing
self.show_index = false
self
}
///|
/// Build the final Table from the IndexBuilder.
pub fn IndexBuilder::build(self : IndexBuilder) -> Table {
let norm = normalize_rows(self.rows, self.empty_cell)
let row_count = norm.length()
let col_count = if row_count > 0 { norm[0].length() } else { 0 }
if row_count == 0 || col_count == 0 {
return Table::from_rows(norm)
}
if !self.show_index {
return Table::from_rows(norm)
}
match self.index_col {
None => {
// Default: add a numeric index col at position 0
let result : Array[Array[String]] = []
for i in 0.. row.push(cell))
result.push(row)
}
Table::from_rows(result)
}
Some(col_idx) => {
// Use specified col as index, move it to position 0
if col_idx >= col_count {
return Table::from_rows(norm)
}
let result : Array[Array[String]] = []
// First row: header with index name
let header_row : Array[String] = []
header_row.push("")
for c in 0.. 0 { norm[0][c] } else { "" })
}
}
result.push(header_row)
// Index name row (if set)
match self.index_name {
Some(name) => {
let name_row : Array[String] = Array::make(col_count, "")
name_row[0] = if name == "" { norm[0][col_idx] } else { name }
result.push(name_row)
}
None => ()
}
// Data rows: use col value as index
for r = 1; r < row_count; r = r + 1 {
let data_row : Array[String] = []
data_row.push(norm[r][col_idx])
for c in 0.. Table {
let section_length = split.index
let count_rows = self.rows.length()
let count_cols = if count_rows > 0 { self.rows[0].length() } else { 0 }
// Early return for empty table or zero index
if count_cols == 0 || count_rows == 0 || section_length == 0 {
return self
}
let (primary_length, secondary_length) = match split.direction {
SplitCol => (count_cols, count_rows)
SplitRow => (count_rows, count_cols)
}
let sections_per_direction = split_ceil_div(primary_length, section_length)
let (outer_start, outer_end, inner_start, inner_end) = match split.behavior {
SplitConcat => (1, sections_per_direction, 0, secondary_length)
SplitZip => (0, secondary_length, 1, sections_per_direction)
}
let mut filtered_sections = 0
for outer_index = outer_start
outer_index < outer_end
outer_index = outer_index + 1 {
let from_secondary_index = outer_index * sections_per_direction -
filtered_sections
for inner_index = inner_start
inner_index < inner_end
inner_index = inner_index + 1 {
let (section_index, from_sec, to_sec) = match split.behavior {
SplitConcat => {
let si = outer_index
let fs = inner_index
let ts = inner_index +
outer_index * secondary_length -
filtered_sections
(si, fs, ts)
}
SplitZip => {
let si = inner_index
let fs = from_secondary_index
let ts = outer_index * sections_per_direction +
inner_index -
filtered_sections
(si, fs, ts)
}
}
// Insert new row/col
match (split.direction, split.behavior) {
(SplitCol, SplitConcat) => split_push_row(self.rows)
(SplitCol, SplitZip) => split_insert_row(self.rows, to_sec)
(SplitRow, SplitConcat) => split_push_col(self.rows)
(SplitRow, SplitZip) => split_insert_col(self.rows, to_sec)
}
// Copy cells from overflow position to new position
let section_is_empty = split_copy_section(
self.rows,
section_length,
section_index,
primary_length,
from_sec,
to_sec,
split.direction,
)
// If section is empty and display is Clean, delete the inserted row/col
if section_is_empty && split.display == SplitClean {
match split.direction {
SplitCol => split_remove_row(self.rows, to_sec)
SplitRow => split_remove_col(self.rows, to_sec)
}
filtered_sections = filtered_sections + 1
}
}
}
// Cleanup: remove extra cols/rows beyond section_length
for segment = primary_length - 1
segment >= section_length
segment = segment - 1 {
match split.direction {
SplitCol => split_remove_col(self.rows, segment)
SplitRow => split_remove_row(self.rows, segment)
}
}
// Clear spans and border overrides since the table shape changed completely
self.config.clear_spans_and_overrides()
self
}