Current section
Files
Jump to
Current section
Files
lib/tabula.ex
defmodule Tabula do
import Enum, only: [
concat: 2,
intersperse: 2,
join: 1,
map: 2,
max: 1,
with_index: 1,
zip: 2
]
@index "#"
@sheets \
org_mode: [
heading: " | ",
heading_border: "-+-",
row: " | ",
spacer: "-"
],
github_md: [
heading: " | ",
heading_border: " | ",
row: " | ",
spacer: "-"
]
@default_sheet :org_mode
defmacro __using__(opts) do
quote do
def print_table(rows) do
unquote(__MODULE__).print_table(rows, unquote(opts))
end
def print_table(rows, override_opts) do
unquote(__MODULE__).print_table(
rows, Keyword.merge(unquote(opts), override_opts))
end
def render_table(rows) do
unquote(__MODULE__).render_table(rows, unquote(opts))
end
def render_table(rows, override_opts) do
unquote(__MODULE__).render_table(
rows, Keyword.merge(unquote(opts), override_opts))
end
end
end
defprotocol Row do
@fallback_to_any true
def get(row, col, default \\ nil)
def keys(row)
end
defimpl Row, for: Map do
def get(row, col, default \\ nil), do: row |> Map.get(col, default)
def keys(row), do: row |> Map.keys
end
defimpl Row, for: List do
def get(row, col, default \\ nil), do: row |> Keyword.get(col, default)
def keys(row), do: row |> Keyword.keys
end
defimpl Row, for: Any do
def get(%{__struct__: _} = row, col, default \\ nil), do: row |> Map.get(col, default)
def keys(row), do: row |> Map.from_struct |> Map.keys
end
def print_table(rows, opts \\ []) do
render_table(rows, opts)
|> IO.puts
end
def render_table(rows, opts \\ []) do
cols = extract_cols(rows, opts)
_render_table(rows, cols, opts)
|> :erlang.list_to_binary
end
def max_widths(cols, rows) do
max_index = rows |> length |> strlen
cols |> map(fn k ->
max([
strlen(k), max_index | map(rows, &(Row.get(&1, k) |> strlen))
])
end)
end
defp _render_table(rows, [_|_]=cols, opts) do
widths = max_widths(cols, rows)
formatters = widths |> formatters(opts)
spacers = widths |> spacers(opts)
[ cols |> render_row(:heading, formatters, opts),
spacers |> render_row(:heading_border, formatters, opts),
rows
|> with_index
|> map(fn indexed_row ->
values(cols, indexed_row)
|> render_row(:row, formatters, opts)
end) ]
end
defp extract_cols([h|_], opts) do
case opts[:only] do
nil -> h |> Row.keys
cols when is_list(cols) -> cols
end
end
defp render_row(cells, style_element, formatters, opts) do
separator = style(style_element, opts)
cells
|> zip(formatters)
|> map(fn {k, f} -> f.(k) end)
|> intersperse(separator)
|> concat('\n')
end
defp render_cell(v) when is_binary(v),
do: v
defp render_cell(v) when is_number(v),
do: inspect(v)
defp render_cell(%{__struct__: _} = v) do
if String.Chars.impl_for(v) do
to_string(v)
else
inspect(v)
end
end
defp render_cell(v),
do: inspect(v)
defp formatters(widths, _opts) do
map(widths, fn w ->
fn @index=cell ->
# need to rjust '#' orelse github fails to render
String.rjust(cell, w)
cell when is_binary(cell) ->
String.ljust(cell, w)
cell when is_number(cell) ->
cell |> render_cell() |> String.rjust(w)
cell ->
cell |> render_cell() |> String.ljust(w)
end
end)
end
defp spacers(widths, opts) do
map(widths, &(Stream.repeatedly(fn -> style(:spacer, opts) end))
|> Stream.take(&1)
|> join)
end
defp strlen(x), do: render_cell(x) |> String.length()
defp values(cols, {row, index}) do
cols
|> map(fn (@index) -> index+1
(col) -> Row.get(row, col)
end)
end
defp style(style, opts) do
sheet = Keyword.get(opts, :style, @default_sheet)
@sheets[sheet][style]
end
end