Packages

Proplist provides the complete Keyword API, but for Proplists.

Current section

Files

Jump to
proplist lib proplist.ex
Raw

lib/proplist.ex

defmodule Proplist do
@moduledoc """
A proplist is a list of tuples where the first element
of the tuple is a binary and the second element can be
any value.
A proplist may have duplicated props so it is not strictly
a dictionary. However most of the functions in this module
behave exactly as a dictionary and mimic the API defined
by the `Dict` behaviour.
For example, `Proplist.get/3` will get the first entry matching
the given prop, regardless if duplicated entries exist.
Similarly, `Proplist.put/3` and `Proplist.delete/3` ensure all
duplicated entries for a given prop are removed when invoked.
A handful of functions exist to handle duplicated props, in
particular, `Enum.into/2` allows creating new proplist without
removing duplicated props, `get_values/2` returns all values for
a given prop and `delete_first/2` deletes just one of the existing
entries.
Since a proplist list is simply a list, all the operations defined
in `Enum` and `List` can be applied.
"""
@behaviour Dict
@type prop :: binary
@type value :: any
@type t :: [{prop, value}]
@type t(value) :: [{prop, value}]
@doc """
Checks if the given argument is a proplist list or not.
"""
@spec proplist?(term) :: boolean
def proplist?([{prop, _value} | rest]) when is_binary(prop) do
proplist?(rest)
end
def proplist?([]), do: true
def proplist?(_), do: false
@doc """
Returns an empty property list, i.e. an empty list.
"""
@spec new :: t
def new do
[]
end
@doc """
Creates a proplist from an enumerable.
Duplicated entries are removed, the latest one prevails.
Unlike `Enum.into(enumerable, [])`,
`Proplist.new(enumerable)` guarantees the props are unique.
## Examples
iex> Proplist.new([{"b", 1}, {"a", 2}])
[{"a", 2}, {"b", 1}]
"""
@spec new(Enum.t) :: t
def new(pairs) do
Enum.reduce pairs, [], fn {p, v}, proplist ->
put(proplist, p, v)
end
end
@doc """
Creates a proplist from an enumerable via the transformation function.
Duplicated entries are removed, the latest one prevails.
Unlike `Enum.into(enumerable, [], fun)`,
`Proplist.new(enumerable, fun)` guarantees the props are unique.
## Examples
iex> Proplist.new(["a", "b"], fn (x) -> {x, x} end) |> Enum.sort
[{"a", "a"}, {"b", "b"}]
"""
def new(pairs, transform) do
Enum.reduce pairs, [], fn i, proplist ->
{p, v} = transform.(i)
put(proplist, p, v)
end
end
@doc """
Gets the value for a specific `prop`.
If `prop` does not exist, return the default value (`nil` if no default value).
If duplicated entries exist, the first one is returned.
Use `get_values/2` to retrieve all entries.
## Examples
iex> Proplist.get([{"a", 1}], "a")
1
iex> Proplist.get([{"a", 1}], "b")
nil
iex> Proplist.get([{"a", 1}], "b", 3)
3
"""
@spec get(t, prop) :: value
@spec get(t, prop, value) :: value
def get(proplist, prop, default \\ nil) when is_list(proplist) and is_binary(prop) do
case :lists.keyfind(prop, 1, proplist) do
{^prop, value} -> value
false -> default
end
end
@doc """
Fetches the value for a specific `prop` and returns it in a tuple.
If the `prop` does not exist, returns `:error`.
## Examples
iex> Proplist.fetch([{"a", 1}], "a")
{:ok, 1}
iex> Proplist.fetch([{"a", 1}], "b")
:error
"""
@spec fetch(t, prop) :: {:ok, value} | :error
def fetch(proplist, prop) when is_list(proplist) and is_binary(prop) do
case :lists.keyfind(prop, 1, proplist) do
{^prop, value} -> {:ok, value}
false -> :error
end
end
@doc """
Fetches the value for specific `prop`.
If `prop` does not exist, a `KeyError` is raised.
## Examples
iex> Proplist.fetch!([{"a", 1}], "a")
1
iex> Proplist.fetch!([{"a", 1}], "b")
** (KeyError) key "b" not found in: [{"a", 1}]
"""
@spec fetch!(t, prop) :: value | no_return
def fetch!(proplist, prop) when is_list(proplist) and is_binary(prop) do
case :lists.keyfind(prop, 1, proplist) do
{^prop, value} -> value
false -> raise KeyError, key: prop, term: proplist
end
end
@doc """
Gets all values for a specific `prop`.
## Examples
iex> Proplist.get_values([{"a", 1}, {"a", 2}], "a")
[1,2]
"""
@spec get_values(t, prop) :: [value]
def get_values(proplist, prop) when is_list(proplist) and is_binary(prop) do
fun = fn
{p, v} when p === prop -> {true, v}
{_, _} -> false
end
:lists.filtermap(fun, proplist)
end
@doc """
Returns all props from the proplist list.
Duplicated props appear duplicated in the final list of props.
## Examples
iex> Proplist.props([{"a", 1}, {"b", 2}])
["a", "b"]
iex> Proplist.props([{"a", 1}, {"b", 2}, {"a", 3}])
["a", "b", "a"]
"""
@spec props(t) :: [prop]
def props(proplist) when is_list(proplist) do
:lists.map(fn {p, _} -> p end, proplist)
end
@doc """
Returns all values from the proplist list.
## Examples
iex> Proplist.values([{"a", 1}, {"b", 2}])
[1,2]
"""
@spec values(t) :: [value]
def values(proplist) when is_list(proplist) do
:lists.map(fn {_, v} -> v end, proplist)
end
@doc """
Deletes the entries in the proplist list for a `prop` with `value`.
If no `prop` with `value` exists, returns the proplist list unchanged.
## Examples
iex> Proplist.delete([{"a", 1}, {"b", 2}], "a", 1)
[{"b", 2}]
iex> Proplist.delete([{"a", 1}, {"b", 2}, {"a", 3}], "a", 3)
[{"a", 1}, {"b", 2}]
iex> Proplist.delete([{"b", 2}], "a", 5)
[{"b", 2}]
"""
@spec delete(t, prop, value) :: t
def delete(proplist, prop, value) when is_list(proplist) and is_binary(prop) do
:lists.filter(fn {k, v} -> k != prop or v != value end, proplist)
end
@doc """
Deletes the entries in the proplist list for a specific `prop`.
If the `prop` does not exist, returns the proplist list unchanged.
Use `delete_first/2` to delete just the first entry in case of
duplicated props.
## Examples
iex> Proplist.delete([{"a", 1}, {"b", 2}], "a")
[{"b", 2}]
iex> Proplist.delete([{"a", 1}, {"b", 2}, {"a", 3}], "a")
[{"b", 2}]
iex> Proplist.delete([{"b", 2}], "a")
[{"b", 2}]
"""
@spec delete(t, prop) :: t
def delete(proplist, prop) when is_list(proplist) and is_binary(prop) do
:lists.filter(fn {k, _} -> k != prop end, proplist)
end
@doc """
Deletes the first entry in the proplist list for a specific `prop`.
If the `prop` does not exist, returns the proplist list unchanged.
## Examples
iex> Proplist.delete_first([{"a", 1}, {"b", 2}, {"a", 3}], "a")
[{"b", 2}, {"a", 3}]
iex> Proplist.delete_first([{"b", 2}], "a")
[{"b", 2}]
"""
@spec delete_first(t, prop) :: t
def delete_first(proplist, prop) when is_list(proplist) and is_binary(prop) do
:lists.keydelete(prop, 1, proplist)
end
@doc """
Puts the given `value` under `prop`.
If a previous value is already stored, all entries are
removed and the value is overridden.
## Examples
iex> Proplist.put([{"a", 1}, {"b", 2}], "a", 3)
[{"a", 3}, {"b", 2}]
iex> Proplist.put([{"a", 1}, {"b", 2}, {"a", 4}], "a", 3)
[{"a", 3}, {"b", 2}]
"""
@spec put(t, prop, value) :: t
def put(proplist, prop, value) when is_list(proplist) and is_binary(prop) do
[{prop, value}|delete(proplist, prop)]
end
@doc """
Puts the given `value` under `prop` unless the entry `prop`
already exists.
## Examples
iex> Proplist.put_new([{"a", 1}], "b", 2)
[{"b", 2}, {"a", 1}]
iex> Proplist.put_new([{"a", 1}, {"b", 2}], "a", 3)
[{"a", 1}, {"b", 2}]
"""
@spec put_new(t, prop, value) :: t
def put_new(proplist, prop, value) when is_list(proplist) and is_binary(prop) do
case :lists.keyfind(prop, 1, proplist) do
{^prop, _} -> proplist
false -> [{prop, value}|proplist]
end
end
@doc """
Checks if two proplists are equal.
Two proplists are considered to be equal if they contain
the same props and those props contain the same values.
## Examples
iex> Proplist.equal?([{"a", 1}, {"b", 2}], [{"b", 2}, {"a", 1}])
true
"""
@spec equal?(t, t) :: boolean
def equal?(left, right) when is_list(left) and is_list(right) do
:lists.sort(left) == :lists.sort(right)
end
@doc """
Merges two proplist lists into one.
If they have duplicated props, the one given in the second argument wins.
## Examples
iex> Proplist.merge([{"a", 1}, {"b", 2}], [{"a", 3}, {"d", 4}]) |> Enum.sort
[{"a", 3}, {"b", 2}, {"d", 4}]
"""
@spec merge(t, t) :: t
def merge(d1, d2) when is_list(d1) and is_list(d2) do
fun = fn {k, _v} -> not has_prop?(d2, k) end
d2 ++ :lists.filter(fun, d1)
end
@doc """
Merges two proplist lists into one.
If they have duplicated props, the given function is invoked to solve conflicts.
## Examples
iex> Proplist.merge([{"a", 1}, {"b", 2}], [{"a", 3}, {"d", 4}], fn (_k, v1, v2) ->
...> v1 + v2
...> end)
[{"a", 4}, {"b", 2}, {"d", 4}]
"""
@spec merge(t, t, (prop, value, value -> value)) :: t
def merge(d1, d2, fun) when is_list(d1) and is_list(d2) do
do_merge(d2, d1, fun)
end
defp do_merge([{k, v2}|t], acc, fun) do
do_merge t, update(acc, k, v2, fn(v1) -> fun.(k, v1, v2) end), fun
end
defp do_merge([], acc, _fun) do
acc
end
@doc """
Returns whether a given `prop` exists in the given `proplist`.
## Examples
iex> Proplist.has_prop?([{"a", 1}], "a")
true
iex> Proplist.has_prop?([{"a", 1}], "b")
false
"""
@spec has_prop?(t, prop) :: boolean
def has_prop?(proplist, prop) when is_list(proplist) and is_binary(prop) do
:lists.keymember(prop, 1, proplist)
end
@doc """
Updates the `prop` with the given function.
If the `prop` does not exist, raises `KeyError`.
If there are duplicated props, they are all removed and only the first one
is updated.
## Examples
iex> Proplist.update!([{"a", 1}], "a", &(&1 * 2))
[{"a", 2}]
iex> Proplist.update!([{"a", 1}], "b", &(&1 * 2))
** (KeyError) key "b" not found in: [{"a", 1}]
"""
@spec update!(t, prop, (value -> value)) :: t | no_return
def update!(proplist, prop, fun) do
update!(proplist, prop, fun, proplist)
end
defp update!([{prop, value}|proplist], prop, fun, _dict) do
[{prop, fun.(value)}|delete(proplist, prop)]
end
defp update!([{_, _} = e|proplist], prop, fun, dict) do
[e|update!(proplist, prop, fun, dict)]
end
defp update!([], prop, _fun, dict) when is_binary(prop) do
raise(KeyError, key: prop, term: dict)
end
@doc """
Updates the `prop` with the given function.
If the `prop` does not exist, inserts the given `initial` value.
If there are duplicated props, they are all removed and only the first one
is updated.
## Examples
iex> Proplist.update([{"a", 1}], "a", 13, &(&1 * 2))
[{"a", 2}]
iex> Proplist.update([{"a", 1}], "b", 11, &(&1 * 2))
[{"a", 1}, {"b", 11}]
"""
@spec update(t, prop, value, (value -> value)) :: t
def update([{prop, value}|proplist], prop, _initial, fun) do
[{prop, fun.(value)}|delete(proplist, prop)]
end
def update([{_, _} = e|proplist], prop, initial, fun) do
[e|update(proplist, prop, initial, fun)]
end
def update([], prop, initial, _fun) when is_binary(prop) do
[{prop, initial}]
end
@doc """
Takes all entries corresponding to the given props and extracts them into a
separate proplist list.
Returns a tuple with the new list and the old list with removed props.
Keys for which there are no entires in the proplist list are ignored.
Entries with duplicated props end up in the same proplist list.
## Examples
iex> d = [{"a", 1}, {"b", 2}, {"c", 3}, {"d", 4}]
iex> Proplist.split(d, ["a", "c", "e"])
{[{"a", 1}, {"c", 3}], [{"b", 2}, {"d", 4}]}
iex> d = [{"a", 1}, {"b", 2}, {"c", 3}, {"d", 4}, {"e", 5}]
iex> Proplist.split(d, ["a", "c", "e"])
{[{"a", 1}, {"c", 3}, {"e", 5}], [{"b", 2}, {"d", 4}]}
"""
def split(proplist, props) when is_list(proplist) do
fun = fn {k, v}, {take, drop} ->
case k in props do
true -> {[{k, v}|take], drop}
false -> {take, [{k, v}|drop]}
end
end
acc = {[], []}
{take, drop} = :lists.foldl(fun, acc, proplist)
{:lists.reverse(take), :lists.reverse(drop)}
end
@doc """
Takes all entries corresponding to the given props and returns them in a new
proplist list.
Duplicated props are preserved in the new proplist list.
## Examples
iex> d = [{"a", 1}, {"b", 2}, {"c", 3}, {"d", 4}]
iex> Proplist.take(d, ["a", "c", "e"])
[{"a", 1}, {"c", 3}]
iex> d = [{"a", 1}, {"b", 2}, {"c", 3}, {"d", 4}, {"e", 5}]
iex> Proplist.take(d, ["a", "c", "e"])
[{"a", 1}, {"c", 3}, {"e", 5}]
"""
def take(proplist, props) when is_list(proplist) do
:lists.filter(fn {k, _} -> k in props end, proplist)
end
@doc """
Drops the given props from the proplist list.
Duplicated props are preserved in the new proplist list.
## Examples
iex> d = [{"a", 1}, {"b", 2}, {"c", 3}, {"d", 4}]
iex> Proplist.drop(d, ["b", "d"])
[{"a", 1}, {"c", 3}]
iex> d = [{"a", 1}, {"b", 2}, {"c", 3}, {"d", 4}, {"e", 5}]
iex> Proplist.drop(d, ["b", "d"])
[{"a", 1}, {"c", 3}, {"e", 5}]
"""
def drop(proplist, props) when is_list(proplist) do
:lists.filter(fn {k, _} -> not k in props end, proplist)
end
@doc """
Returns the first value associated with `prop` in the proplist
list as well as the proplist list without `prop`.
All duplicated props are removed. See `pop_first/3` for
removing only the first entry.
## Examples
iex> Proplist.pop [{"a", 1}], "a"
{1,[]}
iex> Proplist.pop [{"a", 1}], "b"
{nil,[{"a", 1}]}
iex> Proplist.pop [{"a", 1}], "b", 3
{3,[{"a", 1}]}
iex> Proplist.pop [{"a", 1}], "b", 3
{3,[{"a", 1}]}
iex> Proplist.pop [{"a", 1}, {"a", 2}], "a"
{1,[]}
"""
def pop(proplist, prop, default \\ nil) when is_list(proplist) do
{get(proplist, prop, default), delete(proplist, prop)}
end
@doc """
Returns the first value associated with `prop` in the proplist
list as well as the proplist list without that particular occurrence
of `prop`.
Duplicated props are not removed.
## Examples
iex> Proplist.pop_first [{"a", 1}], "a"
{1,[]}
iex> Proplist.pop_first [{"a", 1}], "b"
{nil,[{"a", 1}]}
iex> Proplist.pop_first [{"a", 1}], "b", 3
{3,[{"a", 1}]}
iex> Proplist.pop_first [{"a", 1}], "b", 3
{3,[{"a", 1}]}
iex> Proplist.pop_first [{"a", 1}, {"a", 2}], "a"
{1,[{"a", 2}]}
"""
def pop_first(proplist, prop, default \\ nil) when is_list(proplist) do
{get(proplist, prop, default), delete_first(proplist, prop)}
end
# Dict callbacks
@doc false
def keys(proplist) when is_list(proplist) do
props(proplist)
end
@doc false
def has_key?(proplist, prop) when is_list(proplist) do
has_prop?(proplist, prop)
end
@doc false
def size(proplist) do
length(proplist)
end
@doc false
def to_list(proplist) do
proplist
end
end