Current section
Files
Jump to
Current section
Files
lib/table.ex
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, String.length(content) - string_length(content) + width, pad) end)
|> Enum.join(mid)
"#{first}#{line}#{last}"
end
defp string_length(str) do
Regex.replace(~r/[\x{001b}\x{009b}][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/u, str, "")
|> String.length
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(thing) do
if is_bitstring(thing), do: thing, else: inspect(thing, pretty: true)
end
def handle_multi_line(rows) do
rows |> Enum.flat_map(&handle_multi_line_row/1)
end
defp handle_multi_line_row(row) do
splited = row |> Enum.map(&(String.split(&1, "\n")))
max_lines = splited
|> Enum.map(&Enum.count/1)
|> Enum.max
splited
|> Enum.map(&fill_list(&1, max_lines, ""))
|> List.zip
|> Enum.map(&Tuple.to_list(&1))
end
defp fill_list(l, length, val) do
len = length - Enum.count(l)
cond do
len > 0 -> Enum.concat(l, Enum.map(1..len, fn(_)-> val end))
true -> l
end
end
defp ensure_string(rows) do
Enum.map(rows, fn(row)-> Enum.map(row, &force_string/1) end)
end
defp matrix(body, style) do
body = body |> ensure_string |> handle_multi_line
style = @styles[style]
sizes = compute_size(body)
empty = Enum.zip(Enum.map(sizes, fn(_)-> "" end), sizes)
"""
#{row(empty, style[:top], 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
defp matrix(header, body, style) do
header = Enum.map(header, &force_string/1)
body = body |> ensure_string |> handle_multi_line
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"=> :plain},
%{"style"=> :unicode}], :unicode)
┌──────────┐
│ style │
├──────────┤
│ :plain │
│ :unicode │
└──────────┘
"""
def table(data, style \\ :plain) 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, style)
true -> matrix(Enum.map(Enum.zip(keys, values), &Tuple.to_list/1), style)
end
is_list(data) ->
cond do
Enum.count(data) == 0 -> ""
Enum.all?(data, &is_list/1) -> matrix(data, 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, style)
true -> data |> Enum.map(fn(x)-> [x] end) |> matrix(style)
end
end
end
end