Current section
Files
Jump to
Current section
Files
lib/data_morph.ex
defmodule DataMorph do
@moduledoc ~S"""
Create Elixir structs, maps with atom keys, and keyword lists from CSV/TSV data.
## Examples
Define a struct and return stream of structs created from a `tsv` string, a
`namespace` atom and `name` string.
iex> "name\tiso\n" <>
...> "New Zealand\tnz\n" <>
...> "United Kingdom\tgb" \
...> |> DataMorph.structs_from_tsv(OpenRegister, "country") \
...> |> Enum.to_list
[
%OpenRegister.Country{iso: "nz", name: "New Zealand"},
%OpenRegister.Country{iso: "gb", name: "United Kingdom"}
]
Return stream of maps with atom keys created from a `tsv` stream.
iex> "name\tiso-code\n" <>
...> "New Zealand\tnz\n" <>
...> "United Kingdom\tgb" \
...> |> String.split("\n") \
...> |> Stream.map(& &1) \
...> |> DataMorph.maps_from_tsv() \
...> |> Enum.to_list
[
%{iso_code: "nz", name: "New Zealand"},
%{iso_code: "gb", name: "United Kingdom"}
]
Return stream of keyword lists created from a `tsv` string.
iex> "name\tiso-code\n" <>
...> "New Zealand\tnz\n" <>
...> "United Kingdom\tgb" \
...> |> DataMorph.keyword_lists_from_tsv() \
...> |> Enum.to_list
[
[name: "New Zealand", "iso-code": "nz"],
[name: "United Kingdom", "iso-code": "gb"]
]
"""
require DataMorph.Struct
@doc ~S"""
Defines a struct and returns stream of structs created from `tsv` string or
stream, and a `namespace` and `name`.
Redefines struct when called again with same `namespace` and `name` but
different fields. It sets struct fields to be the union of the old and new
fields.
## Example
Define a struct and return stream of structs created from a `tsv` stream, and
a `namespace` string and `name` atom.
iex> "name\tiso\n" <>
...> "New Zealand\tnz\n" <>
...> "United Kingdom\tgb" \
...> |> String.split("\n") \
...> |> Stream.map(& &1) \
...> |> DataMorph.structs_from_tsv("open-register", :iso_country) \
...> |> Enum.to_list
[
%OpenRegister.IsoCountry{iso: "nz", name: "New Zealand"},
%OpenRegister.IsoCountry{iso: "gb", name: "United Kingdom"}
]
## Example
Add additional new fields to struct when called again with different `tsv`.
iex> "name\tiso\n" <>
...> "New Zealand\tnz\n" <>
...> "United Kingdom\tgb" \
...> |> DataMorph.structs_from_tsv(OpenRegister, "country") \
...> |> Enum.to_list
...>
...> "name\tacronym\n" <>
...> "New Zealand\tNZ\n" <>
...> "United Kingdom\tUK" \
...> |> DataMorph.structs_from_tsv(OpenRegister, "country") \
...> |> Enum.to_list
[
%OpenRegister.Country{acronym: "NZ", iso: nil, name: "New Zealand"},
%OpenRegister.Country{acronym: "UK", iso: nil, name: "United Kingdom"}
]
## Parmeters
- `tsv`: TSV stream or string
- `namespace`: string or atom to form first part of struct alias
- `name`: string or atom to form last part of struct alias
"""
def structs_from_tsv tsv, namespace, name do
tsv |> structs_from_csv(namespace, name, separator: ?\t)
end
@doc ~S"""
Defines a struct and returns stream of structs created from `csv` string or
stream, and a `namespace` and `name`.
See `structs_from_tsv/3` for examples.
## Parmeters
- `csv`: CSV stream or string
- `namespace`: string or atom to form first part of struct alias
- `name`: string or atom to form last part of struct alias
- `options`: optionally pass in separator, e.g. separator: ";"
"""
def structs_from_csv(csv, namespace, name, options \\ [separator: ","]) do
{headers, rows} = csv
|> DataMorph.Csv.to_headers_and_rows_stream(options)
rows
|> DataMorph.Struct.from_rows(namespace, name, headers)
end
@doc ~S"""
Returns stream of maps with atom keys created from `tsv` string or stream.
## Example
Return stream of maps with atom keys created from a `tsv` stream.
iex> "name\tiso-code\n" <>
...> "New Zealand\tnz\n" <>
...> "United Kingdom\tgb" \
...> |> String.split("\n") \
...> |> Stream.map(& &1) \
...> |> DataMorph.maps_from_tsv() \
...> |> Enum.to_list
[
%{iso_code: "nz", name: "New Zealand"},
%{iso_code: "gb", name: "United Kingdom"}
]
## Parmeters
- `tsv`: TSV stream or string
"""
def maps_from_tsv tsv do
tsv |> maps_from_csv(separator: ?\t)
end
@doc ~S"""
Returns stream of maps with atom keys created from `csv` string or stream.
## Parmeters
- `csv`: CSV stream or string
- `options`: optionally pass in separator, e.g. separator: ";"
"""
def maps_from_csv(csv, options \\ [separator: ","]) do
{headers, rows} = csv
|> DataMorph.Csv.to_headers_and_rows_stream(options)
fields = headers |> Enum.map(&DataMorph.Struct.normalize/1)
rows
|> Stream.map(& fields |> Enum.zip(&1) |> Map.new)
end
@doc ~S"""
Returns stream of keyword_lists created from `tsv` string or stream.
Useful when you want to retain the field order of the original stream.
## Example
Return stream of keyword lists created from a `tsv` string.
iex> "name\tiso-code\n" <>
...> "New Zealand\tnz\n" <>
...> "United Kingdom\tgb" \
...> |> DataMorph.keyword_lists_from_tsv() \
...> |> Enum.to_list
[
[name: "New Zealand", "iso-code": "nz"],
[name: "United Kingdom", "iso-code": "gb"]
]
"""
def keyword_lists_from_tsv tsv do
tsv |> keyword_lists_from_csv(separator: ?\t)
end
@doc ~S"""
Returns stream of keyword_lists created from `csv` string or stream.
Useful when you want to retain the field order of the original stream.
## Parmeters
- `csv`: CSV stream or string
- `options`: optionally pass in separator, e.g. separator: ";"
"""
def keyword_lists_from_csv(csv, options \\ [separator: ","]) do
{headers, rows} = csv
|> DataMorph.Csv.to_headers_and_rows_stream(options)
keywords = headers |> Enum.map(& String.to_atom/1)
rows
|> Enum.map(& keywords |> Enum.zip(&1) )
end
@doc ~S"""
Takes stream and applies filter `regexp` when not nil, and takes `count` when
not nil.
## Parmeters
- `stream`: stream of string lines
- `regex`: nil or regexp to match lines via Stream.filter/2 and String.match?/2
- `count`: optional take count to apply via Stream.take/2
"""
def filter_and_take(stream, regex, count \\ nil) do
DataMorph.Stream.filter_and_take stream, regex, count
end
@doc ~S"""
Encode stream of to TSV and write to standard out.
## Example
Write to standard out stream of string lists as TSV lines.
iex> "name\tiso\n" <>
...> "New Zealand\tnz\n" <>
...> "United Kingdom\tgb" \
...> |> String.split("\n") \
...> |> DataMorph.structs_from_tsv("open-register", :iso_country) \
...> |> Stream.map(& [&1.iso, &1.name]) \
...> DataMorph.puts_tsv
nz\tNew Zealand
gb\tUnited Kingdom
"""
def puts_tsv(stream) do
stream
|> CSV.encode(separator: ?\t, delimiter: "\n")
|> Enum.each(& IO.write/1)
end
@doc ~S"""
Concat headers to stream, encode to TSV and write to standard out.
## Example
Write to standard out stream of string lists as TSV lines with headers.
iex> "name\tiso\n" <>
...> "New Zealand\tnz\n" <>
...> "United Kingdom\tgb" \
...> |> String.split("\n") \
...> |> DataMorph.structs_from_tsv("open-register", :iso_country) \
...> |> Stream.map(& [&1.iso, &1.name]) \
...> DataMorph.puts_tsv("iso-code","name")
iso-code\tname
nz\tNew Zealand
gb\tUnited Kingdom
"""
def puts_tsv(stream, headers) do
Stream.concat([headers], stream)
|> puts_tsv
end
end