Packages

Tabular converts an ascii table string into either a list of lists or a list of maps

Current section

Files

Jump to
tabular lib tabular.ex
Raw

lib/tabular.ex

defmodule Tabular do
@moduledoc """
Tabular converts an ascii table string into either a list of lists or a list of maps.
"""
@row_splitter_re ~r/^\s*[|+]--[-+|]+$/
@doc ~S'''
Converts an ascii table to a list of maps.
## Examples
iex> ascii_table = """
...> |---------------+--------------------|
...> | name | dob |
...> |---------------+--------------------|
...> | Malcolm | September 20, 2468 |
...> | Reynolds | |
...> |---------------+--------------------|
...> | Zoe Washburne | February 15, 2484 |
...> |---------------+--------------------|
...> """
...> Tabular.to_list_of_maps(ascii_table)
[
%{dob: "September 20, 2468", name: "Malcolm Reynolds"},
%{dob: "February 15, 2484", name: "Zoe Washburne"}
]
Without separators:
iex> ascii_table = """
...> |------------------+--------------------|
...> | name | dob |
...> |------------------+--------------------|
...> | Malcolm Reynolds | September 20, 2468 |
...> | Zoe Washburne | February 15, 2484 |
...> |------------------+--------------------|
...> """
...> Tabular.to_list_of_maps(ascii_table)
[
%{dob: "September 20, 2468", name: "Malcolm Reynolds"},
%{dob: "February 15, 2484", name: "Zoe Washburne"}
]
'''
def to_list_of_maps(ascii_table) do
[headers | rows] = to_list_of_lists(ascii_table)
headers_as_atoms = Enum.map(headers, &String.to_atom(&1))
rows
|> Enum.map(fn row ->
Enum.zip(headers_as_atoms, row)
|> Map.new()
end)
end
@doc deprecated: "Use to_list_of_lists(table, header: false) instead"
@doc ~S'''
Converts an ascii table to a list of lists, omitting the header row.
## Examples
iex> ascii_table = """
...> |---------------+--------------------|
...> | name | dob |
...> |---------------+--------------------|
...> | Malcolm | September 20, 2468 |
...> | Reynolds | |
...> |---------------+--------------------|
...> | Zoe Washburne | February 15, 2484 |
...> |---------------+--------------------|
...> """
...> Tabular.to_list_of_lists_no_header(ascii_table)
[
["Malcolm Reynolds", "September 20, 2468"],
["Zoe Washburne", "February 15, 2484"]
]
Without separators:
iex> ascii_table = """
...> |------------------+--------------------|
...> | name | dob |
...> |------------------+--------------------|
...> | Malcolm Reynolds | September 20, 2468 |
...> | Zoe Washburne | February 15, 2484 |
...> |------------------+--------------------|
...> """
...> Tabular.to_list_of_lists_no_header(ascii_table)
[
["Malcolm Reynolds", "September 20, 2468"],
["Zoe Washburne", "February 15, 2484"]
]
'''
def to_list_of_lists_no_header(ascii_table) do
to_list_of_lists(ascii_table, header: false)
end
@doc ~S'''
Converts an ascii table to a list of lists.
## Examples
iex> ascii_table = """
...> |---------------+--------------------|
...> | name | dob |
...> |---------------+--------------------|
...> | Malcolm | September 20, 2468 |
...> | Reynolds | |
...> |---------------+--------------------|
...> | Zoe Washburne | February 15, 2484 |
...> |---------------+--------------------|
...> """
...> Tabular.to_list_of_lists(ascii_table)
[
["name", "dob"],
["Malcolm Reynolds", "September 20, 2468"],
["Zoe Washburne", "February 15, 2484"]
]
...> Tabular.to_list_of_lists(ascii_table, header: false)
[
["Malcolm Reynolds", "September 20, 2468"],
["Zoe Washburne", "February 15, 2484"]
]
Without separators:
iex> ascii_table = """
...> |------------------+--------------------|
...> | name | dob |
...> |------------------+--------------------|
...> | Malcolm Reynolds | September 20, 2468 |
...> | Zoe Washburne | February 15, 2484 |
...> |------------------+--------------------|
...> """
...> Tabular.to_list_of_lists(ascii_table)
[
["name", "dob"],
["Malcolm Reynolds", "September 20, 2468"],
["Zoe Washburne", "February 15, 2484"]
]
...> Tabular.to_list_of_lists(ascii_table, header: false)
[
["Malcolm Reynolds", "September 20, 2468"],
["Zoe Washburne", "February 15, 2484"]
]
'''
def to_list_of_lists(ascii_table, opts \\ [header: true]) do
rows =
ascii_table
|> lines()
|> cell_line_groups(has_separators?: has_separators?(ascii_table))
|> trimmed_and_grouped_cell_contents()
|> folded_cell_contents()
|> specials()
if opts[:header], do: rows, else: Enum.drop(rows, 1)
end
@doc false
def lines(ascii_table) do
ascii_table
|> String.trim()
|> String.split("\n")
end
@doc false
def cell_line_groups(lines, has_separators?: true) do
lines
|> Enum.chunk_by(fn line ->
Regex.match?(@row_splitter_re, line)
end)
|> Enum.filter(fn group ->
!(length(group) == 1 &&
Regex.match?(@row_splitter_re, hd(group)))
end)
end
@doc false
def cell_line_groups(lines, has_separators?: false) do
lines
|> Enum.reject(&(&1 =~ @row_splitter_re))
|> Enum.map(&[&1])
end
@doc false
def trimmed_and_grouped_cell_contents(line_groups) do
line_groups
|> Enum.map(fn line_group ->
line_group
|> Enum.map(fn line ->
[_all, trimmed_line] = Regex.run(~r/^\s*\|\s*(.*)\s*\|\s*$/, line)
trimmed_line
|> String.split("|")
|> Enum.map(fn cell_content ->
String.trim(cell_content)
end)
end)
end)
end
@doc false
def folded_cell_contents(grouped_cell_contents) do
grouped_cell_contents
|> Enum.map(fn row_contents ->
row_contents
|> fold_contents_if_necessary
end)
end
@doc false
def fold_contents_if_necessary(row_contents) when length(row_contents) > 1 do
Enum.zip(row_contents)
|> Enum.map(fn foldables ->
foldables
|> Tuple.to_list()
|> Enum.filter(fn text -> text != "" end)
|> Enum.join(" ")
end)
end
@doc false
def fold_contents_if_necessary(row_contents) do
hd(row_contents)
end
@doc false
def specials(rows_of_columns) do
rows_of_columns
|> Enum.map(fn row ->
row
|> Enum.map(fn text ->
special(text)
end)
end)
end
@doc false
def special("true"), do: true
def special("false"), do: false
def special("nil"), do: nil
def special(""), do: ""
def special(":" <> rest), do: String.to_atom(rest)
def special(not_special), do: not_special
@doc false
def has_separators?(ascii_table) do
[_, _, _ | lines] = lines(ascii_table)
{_, lines} = List.pop_at(lines, -1)
Enum.any?(lines, &(&1 =~ @row_splitter_re))
end
end