defmodule Table do @styles %{plain: %{top: ~w[+- -+- -+], middle: ~w[+- -+- -+], bottom: ~w[+- -+- -+], dash: ?-, header_walls: ["| ", " | ", " |"], body_walls: ["| ", " | ", " |"]}, unicode: %{top: ~w[┌─ ─┬─ ─┐], middle: ~w[├─ ─┼─ ─┤], bottom: ~w[└─ ─┴─ ─┘], dash: ?─, header_walls: ["│ ", " │ ", " │"], body_walls: ["│ ", " ╎ ", " │"]}} defp row(cells, walls, pad \\ ?\s) do [first, mid, last] = walls line = cells |> Enum.map(fn({content, width})-> String.ljust(content, width, pad) end) |> Enum.join(mid) "#{first}#{line}#{last}" end defp compute_size(rows) do rows |> Enum.map(fn(row)-> Enum.map(row, &String.length/1) end) |> Enum.reduce(fn(row, acc)-> Enum.map(Enum.zip(row, acc), fn({a, b})-> max(a, b) end) end) end defp force_string(style, thing) do case thing do x when is_bitstring(x) -> x x when is_list(x) -> table x, style, false x when is_map(x) -> table x, style, false x -> inspect(x) end end defp ensure_string(style, rows) do Enum.map(rows, fn(row)-> Enum.map(row, &(force_string style, &1)) end) end defp matrix(body, border?, style) do body = ensure_string(style, body) style = @styles[style] sizes = compute_size(body) empty = Enum.zip(Enum.map(sizes, fn(_)-> "" end), sizes) body = Enum.map_join(body, "\n", fn(x)-> row(Enum.zip(x, sizes), style[:body_walls]) end) cond do border? -> top = row(empty, style[:top], style[:dash]) bottom = row(empty, style[:bottom], style[:dash]) "#{top}\n#{body}\n#{bottom}" true -> body end end defp matrix(header, body, border?, style) do header = Enum.map(header, &(force_string style, &1)) body = ensure_string(style, body) style = @styles[style] sizes = compute_size([header] ++ body) empty = Enum.zip(Enum.map(sizes, fn(_)-> "" end), sizes) """ #{row(empty, style[:top], style[:dash])} #{row(Enum.zip(header, sizes), style[:header_walls])} #{row(empty, style[:middle], style[:dash])} #{Enum.map_join(body, "\n", fn(x)-> row(Enum.zip(x, sizes), style[:body_walls]) end)} #{row(empty, style[:bottom], style[:dash])} """ end @doc """ Supported types * map * list * list of map * list of list Examples iex> IO.write Table.table(%{"key"=> "value", "more"=> "more val"}) +------+----------+ | key | value | | more | more val | +------+----------+ iex> IO.write Table.table(["list", "is", "vertical"]) +----------+ | list | | is | | vertical | +----------+ iex> IO.write Table.table([%{"style"=> :ascii}, %{"style"=> :unicode}], :unicode) ┌──────────┐ │ style │ ├──────────┤ │ :ascii │ │ :unicode │ └──────────┘ IO.write Table.table(%{"style"=> %{"level" => "aha"}, "styl"=> :unicode}) """ def table(data, style \\ :plain, border? \\ true) do cond do is_map(data) -> keys = Dict.keys(data) values = Enum.map keys, fn(x)-> Dict.get(data, x) end cond do Dict.size(data) == 0 -> "" Enum.all?(values, &is_list/1) -> matrix(keys, values, border?, style) true -> Enum.zip(keys, values) |> Enum.map(&Tuple.to_list/1) |> matrix(border?, style) end is_list(data) -> cond do Enum.count(data) == 0 -> "" Enum.all?(data, &is_list/1) -> matrix(data, border?, style) Enum.all?(data, &is_map/1) -> header = Dict.keys List.first data data = Enum.map(data, fn(row)-> Enum.map(header, &(Dict.get(row, &1))) end) matrix(header, data, border?, style) true -> data |> Enum.map(fn(x)-> [x] end) |> matrix(border?, style) end end end end