Current section
Files
Jump to
Current section
Files
lib/term_table.ex
defmodule TermTable do
def build(data, _options \\ :empty) do
max_lengths_by_index = max_lengths_by_index(data)
{ table_base, last_row_cell_lengths, _ } = do_build(data, max_lengths_by_index)
table_bottom = do_separator(last_row_cell_lengths)
result = table_base <> table_bottom <> "\n"
result
end
defp do_build(rows, max_lengths_by_index) do
rows
|> Enum.with_index
|> Enum.reduce({"", [], false}, fn({cols, row_idx}, acc) ->
{ previous_row_output, previous_row_cell_lengths, sep_preceding } = acc
if cols == [:separator] do
{ previous_row_output, previous_row_cell_lengths, true }
else
{ row_output, row_cell_lengths } =
cols
|> Enum.with_index
|> Enum.reduce({ "|", []}, fn({i, col_idx}, acc) ->
{ output, cell_lengths } = acc
{ i, opts } = if is_tuple(i), do: i, else: { i, [] }
colspan = Keyword.get(opts, :colspan, 1)
align = Keyword.get(opts, :align, :left)
cell = adjust(max_lengths_by_index, i, col_idx, align, colspan)
cell_length = String.length(cell)
new_cell_lengths = cell_lengths ++ [ cell_length ]
output = output <> cell <> "|"
{ output, new_cell_lengths }
end)
if sep_preceding || row_idx == 0 do
separator = separator(previous_row_cell_lengths, row_cell_lengths)
{ previous_row_output <> separator <> "\n" <> row_output <> "\n", row_cell_lengths, false }
else
{ previous_row_output <> row_output <> "\n", row_cell_lengths, false }
end
end
end)
end
defp lengths_by_index(cols) do
cols
|> Enum.with_index
|> Enum.reduce(%{}, &length_for_element/2)
end
defp length_for_element({{i, opts}, col_idx}, acc) do
width = Keyword.get(opts, :width)
unless width do
width = String.length(to_string(i))
end
Map.put acc, col_idx, width
end
defp length_for_element({i, col_idx}, acc) do
Map.put acc, col_idx, String.length(to_string(i))
end
defp separator(prev_lengths, curr_lengths) do
{ a, b } = { length(prev_lengths), length(curr_lengths) }
lengths = if a > b, do: prev_lengths, else: curr_lengths
do_separator(lengths)
end
defp do_separator(lengths) do
fun = fn(x, acc) ->
acc <> String.ljust("", x, ?-) <> "+"
end
lengths |> Enum.reduce("+", fun)
end
defp adjust(max_lengths_by_index, content, col_idx, align, colspan) do
range = col_idx..(col_idx + colspan - 1)
required_length = Enum.reduce range, 0, fn(idx, acc) ->
acc + max_lengths_by_index[idx]
end
# correction by length of spaces between cols
required_length = required_length + (colspan - 1) * 3
content = to_string(content)
case align do
:left ->
" " <> String.ljust(content, required_length) <> " "
:right ->
" " <> String.rjust(content, required_length) <> " "
:center ->
right_adjust = div(required_length + String.length(content), 2)
right_adjusted = String.rjust(content, right_adjust)
" " <> String.ljust(right_adjusted, required_length) <> " "
end
end
defp max_lengths_by_index(cols) do
fun = fn(el, acc) ->
lengths_by_index = lengths_by_index(el)
Enum.reduce lengths_by_index, acc, fn({k, v}, acc) ->
if acc[k] == nil || lengths_by_index[k] > acc[k] do
Map.put(acc, k, v)
else
acc
end
end
end
cols |> Enum.reduce(%{}, fun)
end
def render(rows, options \\ :empty) do
build(rows, options)
end
end