defmodule Archeometer.Repo.Result do
@moduledoc """
A query result with a nicer API to manipulate the data.
"""
defstruct [:headers, :rows]
defimpl String.Chars do
@vsep " |"
@hsep "-"
@vprefix " |"
def to_string(%Archeometer.Repo.Result{headers: headers, rows: rows}) do
widths = widths([headers | rows])
hsep = Enum.map(widths, fn w -> String.duplicate(@hsep, w) end)
[headers, hsep | rows]
|> Enum.map_join(&(@vprefix <> format_row(&1, widths) <> "\n"))
end
defp format_row(row, widths) do
Enum.zip(row, widths)
|> Enum.map_join(fn {r, w} ->
r
|> Kernel.to_string()
|> String.pad_trailing(w)
|> Kernel.<>(@vsep)
end)
end
defp widths(rows) do
rows
|> Enum.zip()
|> Enum.map(fn tpl ->
tpl
|> Tuple.to_list()
|> Enum.map(&(Kernel.to_string(&1) |> String.length()))
|> Enum.max()
end)
end
end
@doc """
Merges two query results into one using a common column to pair the results values.
Rows from second result are dumped into first one.
## Examples
iex> result1 =
...> %Archeometer.Repo.Result{
...> headers: [:id, :name], rows: [[1, "archeometer"]]}
...>
iex> result2 =
...> %Archeometer.Repo.Result{
...> headers: [:name, :num_mods], rows: [["archeometer", 120]]}
...>
iex> Archeometer.Repo.Result.merge(result1, result2, :name)
%Archeometer.Repo.Result{
headers: [:id, :name, :num_mods],
rows: [[1, "archeometer", 120]]
}
Trying to merge without a common column
iex> result1 =
...> %Archeometer.Repo.Result{
...> headers: [:id, :is_external], rows: [[1, false]]}
...>
iex> result2 =
...> %Archeometer.Repo.Result{
...> headers: [:name, :num_mods], rows: [["archeometer", 120]]}
...>
iex> Archeometer.Repo.Result.merge(result1, result2, :name)
{:error, "name is not a common column"}
"""
def merge(result1, result2, joiner, default \\ nil) do
joiner1 = Enum.find_index(result1.headers, &(&1 == joiner))
joiner2 = Enum.find_index(result2.headers, &(&1 == joiner))
if is_nil(joiner1) or is_nil(joiner2) do
{:error, "#{joiner} is not a common column"}
else
default = for _ <- 1..length(result2.headers), do: default
Map.update!(result1, :rows, fn rows ->
Enum.map(rows, &update_row(&1, result2.rows, joiner1, joiner2, default))
end)
|> Map.update!(:headers, &(&1 ++ List.delete_at(result2.headers, joiner2)))
end
end
defp update_row(current_row, result_rows2, joiner1, joiner2, default) do
other_values =
Enum.find(result_rows2, default, &(Enum.at(current_row, joiner1) == Enum.at(&1, joiner2)))
|> List.delete_at(joiner2)
current_row ++ other_values
end
end