///|
/// Drop rows where every declared column is empty or missing.
pub fn table_drop_empty_rows(table : CsvTable) -> CsvTable {
  let rows : Array[Array[String]] = Array::new()
  for row in table.rows {
    if !cleaning_row_empty(row, table.headers.length()) {
      rows.push(row)
    }
  }
  { headers: table.headers, rows }
}

///|
/// Drop columns where every row is empty or missing.
pub fn table_drop_empty_columns(table : CsvTable) -> CsvTable {
  let keep : Array[String] = Array::new()
  for index in 0.. CsvTable {
  let rows : Array[Array[String]] = Array::new()
  for row in table.rows {
    let next : Array[String] = Array::new()
    for field in row {
      if field == old_value {
        next.push(new_value)
      } else {
        next.push(field)
      }
    }
    rows.push(next)
  }
  { headers: table.headers, rows }
}

///|
/// Replace exact cell values in one column.
pub fn table_replace_in_column(
  table : CsvTable,
  column : String,
  old_value : String,
  new_value : String,
) -> CsvTable {
  match cleaning_column_index(table.headers, column) {
    Some(index) => {
      let rows : Array[Array[String]] = Array::new()
      for row in table.rows {
        let next = row.copy()
        if index < next.length() && next[index] == old_value {
          next[index] = new_value
        }
        rows.push(next)
      }
      { headers: table.headers, rows }
    }
    None => table
  }
}

///|
/// Convert values in one column to lowercase.
pub fn table_lowercase_column(table : CsvTable, column : String) -> CsvTable {
  match cleaning_column_index(table.headers, column) {
    Some(index) =>
      cleaning_map_column(table, index, fn(value) { value.to_lower() })
    None => table
  }
}

///|
/// Convert values in one column to uppercase.
pub fn table_uppercase_column(table : CsvTable, column : String) -> CsvTable {
  match cleaning_column_index(table.headers, column) {
    Some(index) =>
      cleaning_map_column(table, index, fn(value) { value.to_upper() })
    None => table
  }
}

///|
/// Add a target column containing the first non-empty value among source columns.
pub fn table_coalesce_columns(
  table : CsvTable,
  columns : Array[String],
  target : String,
) -> CsvTable {
  let indices : Array[Int] = Array::new()
  for column in columns {
    match cleaning_column_index(table.headers, column) {
      Some(index) => indices.push(index)
      None => ()
    }
  }
  table_add_column(table, target, fn(row) {
    for index in indices {
      let value = cleaning_cell_at(row, index)
      if !value.is_empty() {
        return value
      }
    }
    ""
  })
}

///|
/// Normalize common boolean spellings to `true` or `false` in one column.
pub fn table_normalize_boolean(table : CsvTable, column : String) -> CsvTable {
  match cleaning_column_index(table.headers, column) {
    Some(index) =>
      cleaning_map_column(table, index, fn(value) {
        match value.to_lower() {
          "true" | "1" | "yes" | "y" | "on" => "true"
          "false" | "0" | "no" | "n" | "off" => "false"
          _ => value
        }
      })
    None => table
  }
}

///|
/// Build a per-column missingness summary table.
pub fn table_missing_summary(table : CsvTable) -> CsvTable {
  let rows : Array[Array[String]] = Array::new()
  for index in 0.. CsvTable {
  let headers : Array[String] = Array::new()
  headers.push(column)
  for header in table.headers {
    headers.push(header)
  }
  let rows : Array[Array[String]] = Array::new()
  for i in 0.. String,
) -> CsvTable {
  let rows : Array[Array[String]] = Array::new()
  for row in table.rows {
    let next = row.copy()
    if index < next.length() {
      next[index] = transform(next[index])
    }
    rows.push(next)
  }
  { headers: table.headers, rows }
}

///|
fn cleaning_column_index(headers : Array[String], column : String) -> Int? {
  for i in 0.. String {
  if index >= 0 && index < row.length() {
    row[index]
  } else {
    ""
  }
}

///|
fn cleaning_row_empty(row : Array[String], width : Int) -> Bool {
  for i in 0..