Current section

Files

Jump to
tucan lib tucan data.ex
Raw

lib/tucan/data.ex

defmodule Tucan.Data do
@moduledoc false
# Module for inferring types from input data
#
# Copied from https://github.com/livebook-dev/vega_lite/blob/main/lib/vega_lite/data.ex
@doc """
Returns a map with each column and its respective inferred type for the given data.
"""
@spec column_types(Table.Reader.t()) :: map()
def column_types(data) do
with true <- implements?(Table.Reader, data),
data = {_, %{columns: [_ | _] = columns}, _} <- Table.Reader.init(data),
true <- Enum.all?(columns, &implements?(String.Chars, &1)) do
types = infer_types(data)
Enum.zip_with(columns, types, fn column, type -> {to_string(column), type} end) |> Map.new()
else
_ -> %{}
end
end
defp implements?(protocol, value), do: protocol.impl_for(value) != nil
defp infer_types({:columns, %{columns: _columns}, data}) do
Enum.map(data, fn data -> data |> Enum.at(0) |> type_of() end)
end
defp infer_types({:rows, %{columns: _columns}, data}) do
{:ok, row} = Enum.fetch(data, 0)
Enum.map(row, &type_of/1)
end
defp type_of(%mod{}) when mod in [Date, NaiveDateTime, DateTime], do: :temporal
defp type_of(%Time{}), do: :time
defp type_of(data) when is_number(data), do: :quantitative
defp type_of(data) when is_binary(data) do
cond do
date?(data) or date_time?(data) -> :temporal
time?(data) -> :time
true -> :nominal
end
end
defp type_of(data) when is_atom(data), do: :nominal
defp type_of(_), do: nil
defp date?(value), do: match?({:ok, _}, Date.from_iso8601(value))
defp date_time?(value), do: match?({:ok, _, _}, DateTime.from_iso8601(value))
defp time?(value), do: match?({:ok, _}, Time.from_iso8601(value))
end