Current section
Files
Jump to
Current section
Files
lib/term_table.ex
defmodule TermTable do
def render(rows, options \\ :empty) do
IO.puts build(rows, options)
end
def build(data, _options \\ :empty) do
max_lengths_by_index = max_lengths_by_index(data)
{ tbody, last_row_lengths, _ } = do_build(data, max_lengths_by_index)
tbottom = do_separator(last_row_lengths)
tbody <> tbottom <> "\n"
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, _last_index } =
cols
|> Enum.reduce({ "|", [], -1}, fn(i, acc) ->
{ output, cell_lengths, last_index } = acc
{ i, opts } = if is_tuple(i), do: i, else: { i, [] }
colspan = Keyword.get(opts, :colspan, 1)
align = Keyword.get(opts, :align, :left)
index = last_index + 1
cell = adjust(max_lengths_by_index, i, index, align, colspan)
cell_length = String.length(cell)
new_cell_lengths = cell_lengths ++ [ cell_length ]
output = output <> cell <> "|"
last_index = index + colspan - 1
{ output, new_cell_lengths, last_index }
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 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 for spaces between cols
required_length = required_length + (colspan - 1) * 3
content = to_string(content)
aligned =
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
" " <> aligned <> " "
end
defp max_lengths_by_index(data) do
fun = fn(el, acc) ->
{ lengths_by_index, _last_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
data |> Enum.reduce(%{}, fun)
end
defp lengths_by_index(cols) do
cols
|> Enum.reduce({%{}, -1}, &length_for_element/2)
end
defp length_for_element({i, opts}, acc) do
{ acc, last_index } = acc
width = Keyword.get(opts, :width)
colspan = Keyword.get(opts, :colspan)
unless width do
width = String.length(to_string(i))
end
index = last_index + 1
last_index = if colspan, do: index + colspan - 1, else: index
acc = Map.put acc, index, width
{ acc, last_index }
end
defp length_for_element(i, acc) do
{ acc, prev_index } = acc
index = prev_index + 1
acc = Map.put acc, index, String.length(to_string(i))
{ acc, index }
end
end