Packages

Elixir-inspired standard library modules for Erlang

Current section

Files

Jump to
ex_stdlib src keyword.erl
Raw

src/keyword.erl

%%% @doc
%%% A keyword list is a list of two-element tuples where the first element
%%% is an atom (the key) and the second element is any term (the value).
%%%
%%% Keyword lists provide a convenient way to associate keys with values
%%% and are commonly used for options and configuration parameters.
%%%
%%% This module provides functions for working with keyword lists,
%%% inspired by Elixir's Keyword module but adapted for Erlang.
%%%
%%% Examples:
%%% ```
%%% Options = [{port, 8080}, {host, "localhost"}, {ssl, true}],
%%% Port = keyword:get(Options, port, 3000), % Returns 8080
%%% NewOptions = keyword:put(Options, timeout, 5000).
%%% '''
%%% @end
-module(keyword).
%% API
-export([get/2, get/3, get_values/2]).
-export([put/3, put_new/3, replace/3]).
-export([delete/2, delete_first/2]).
-export([has_key/2, keys/1, values/1]).
-export([merge/2, take/2, drop/2]).
-export([split/2, pop/2, pop/3, pop_first/2, pop_first/3]).
-export([equal/2, keyword/1, new/0, new/1]).
-export([fetch/2, fetch/3]).
%% Types
-type keyword() :: [{atom(), term()}].
-type key() :: atom().
-type value() :: term().
-export_type([keyword/0, key/0, value/0]).
%%% @doc
%%% Gets the first value associated with the given key.
%%%
%%% Returns undefined if the key is not found.
%%% @end
-spec get(keyword(), key()) -> value() | undefined.
get(Keywords, Key) ->
get(Keywords, Key, undefined).
%%% @doc
%%% Gets the first value associated with the given key.
%%%
%%% Returns the default value if the key is not found.
%%% @end
-spec get(keyword(), key(), value()) -> value().
get(Keywords, Key, Default) ->
case lists:keyfind(Key, 1, Keywords) of
{Key, Value} -> Value;
false -> Default
end.
%%% @doc
%%% Gets all values associated with the given key.
%%%
%%% Returns a list of values in the order they appear.
%%% @end
-spec get_values(keyword(), key()) -> [value()].
get_values(Keywords, Key) ->
[Value || {K, Value} <- Keywords, K =:= Key].
%%% @doc
%%% Puts a new key-value pair in the keyword list.
%%%
%%% If the key already exists, adds a new entry (does not replace).
%%% @end
-spec put(keyword(), key(), value()) -> keyword().
put(Keywords, Key, Value) ->
[{Key, Value} | Keywords].
%%% @doc
%%% Puts a new key-value pair in the keyword list only if the key doesn't exist.
%%%
%%% If the key already exists, returns the original keyword list unchanged.
%%% @end
-spec put_new(keyword(), key(), value()) -> keyword().
put_new(Keywords, Key, Value) ->
case has_key(Keywords, Key) of
true -> Keywords;
false -> put(Keywords, Key, Value)
end.
%%% @doc
%%% Replaces the first occurrence of the given key.
%%%
%%% If the key doesn't exist, adds it to the front of the list.
%%% @end
-spec replace(keyword(), key(), value()) -> keyword().
replace(Keywords, Key, Value) ->
case lists:keytake(Key, 1, Keywords) of
{value, {Key, _OldValue}, Rest} ->
[{Key, Value} | Rest];
false ->
[{Key, Value} | Keywords]
end.
%%% @doc
%%% Deletes all entries with the given key.
%%%
%%% Returns a new keyword list with all occurrences of the key removed.
%%% @end
-spec delete(keyword(), key()) -> keyword().
delete(Keywords, Key) ->
[KV || {K, _V} = KV <- Keywords, K =/= Key].
%%% @doc
%%% Deletes the first entry with the given key.
%%%
%%% Returns a new keyword list with the first occurrence removed.
%%% @end
-spec delete_first(keyword(), key()) -> keyword().
delete_first(Keywords, Key) ->
case lists:keytake(Key, 1, Keywords) of
{value, _Tuple, Rest} -> Rest;
false -> Keywords
end.
%%% @doc
%%% Checks if the given key exists in the keyword list.
%%% @end
-spec has_key(keyword(), key()) -> boolean().
has_key(Keywords, Key) ->
lists:keymember(Key, 1, Keywords).
%%% @doc
%%% Returns a list of all keys in the keyword list.
%%%
%%% The keys are returned in the same order as they appear, including duplicates.
%%% @end
-spec keys(keyword()) -> [key()].
keys(Keywords) ->
[Key || {Key, _Value} <- Keywords].
%%% @doc
%%% Returns a list of all values in the keyword list.
%%%
%%% The values are returned in the same order as they appear.
%%% @end
-spec values(keyword()) -> [value()].
values(Keywords) ->
[Value || {_Key, Value} <- Keywords].
%%% @doc
%%% Merges two keyword lists.
%%%
%%% Keys in the second list take precedence over keys in the first list.
%%% If a key exists in both lists, all values from the second list will appear first.
%%% @end
-spec merge(keyword(), keyword()) -> keyword().
merge(Keywords1, Keywords2) ->
Keywords2 ++ Keywords1.
%%% @doc
%%% Takes only the entries with the given keys.
%%%
%%% Returns a new keyword list containing only the specified keys.
%%% The order of keys in the result follows the order in the original list.
%%% @end
-spec take(keyword(), [key()]) -> keyword().
take(Keywords, Keys) ->
KeySet = sets:from_list(Keys),
[KV || {Key, _Value} = KV <- Keywords, sets:is_element(Key, KeySet)].
%%% @doc
%%% Drops the entries with the given keys.
%%%
%%% Returns a new keyword list with the specified keys removed.
%%% @end
-spec drop(keyword(), [key()]) -> keyword().
drop(Keywords, Keys) ->
KeySet = sets:from_list(Keys),
[KV || {Key, _Value} = KV <- Keywords, not sets:is_element(Key, KeySet)].
%%% @doc
%%% Splits the keyword list into two lists based on the given keys.
%%%
%%% Returns {Taken, Dropped} where Taken contains entries with the specified keys
%%% and Dropped contains the remaining entries.
%%% @end
-spec split(keyword(), [key()]) -> {keyword(), keyword()}.
split(Keywords, Keys) ->
KeySet = sets:from_list(Keys),
split_acc(Keywords, KeySet, [], []).
%%% @doc
%%% Pops the first value for the given key.
%%%
%%% Returns {Value, NewKeywords} if the key exists, or {undefined, Keywords} if not.
%%% @end
-spec pop(keyword(), key()) -> {value() | undefined, keyword()}.
pop(Keywords, Key) ->
pop(Keywords, Key, undefined).
%%% @doc
%%% Pops the first value for the given key with a default.
%%%
%%% Returns {Value, NewKeywords} if the key exists, or {Default, Keywords} if not.
%%% @end
-spec pop(keyword(), key(), value()) -> {value(), keyword()}.
pop(Keywords, Key, Default) ->
case lists:keytake(Key, 1, Keywords) of
{value, {Key, Value}, Rest} -> {Value, Rest};
false -> {Default, Keywords}
end.
%%% @doc
%%% Pops the first occurrence of the given key.
%%%
%%% Returns {Value, NewKeywords} if found, or error if not found.
%%% @end
-spec pop_first(keyword(), key()) -> {value(), keyword()} | error.
pop_first(Keywords, Key) ->
case lists:keytake(Key, 1, Keywords) of
{value, {Key, Value}, Rest} -> {Value, Rest};
false -> error
end.
%%% @doc
%%% Pops the first occurrence of the given key with a default.
%%%
%%% Returns {Value, NewKeywords} if found, or {Default, Keywords} if not found.
%%% @end
-spec pop_first(keyword(), key(), value()) -> {value(), keyword()}.
pop_first(Keywords, Key, Default) ->
case pop_first(Keywords, Key) of
{Value, Rest} -> {Value, Rest};
error -> {Default, Keywords}
end.
%%% @doc
%%% Checks if two keyword lists are equal.
%%%
%%% Two keyword lists are considered equal if they contain the same key-value pairs
%%% in the same order.
%%% @end
-spec equal(keyword(), keyword()) -> boolean().
equal(Keywords1, Keywords2) ->
Keywords1 =:= Keywords2.
%%% @doc
%%% Checks if the given term is a valid keyword list.
%%%
%%% A keyword list is a list of two-element tuples where the first element is an atom.
%%% @end
-spec keyword(term()) -> boolean().
keyword(Term) when is_list(Term) ->
lists:all(fun
({Key, _Value}) when is_atom(Key) -> true;
(_) -> false
end, Term);
keyword(_) ->
false.
%%% @doc
%%% Creates a new empty keyword list.
%%% @end
-spec new() -> keyword().
new() ->
[].
%%% @doc
%%% Creates a new keyword list from the given list of key-value pairs.
%%%
%%% If the input is already a valid keyword list, returns it unchanged.
%%% If the input is a list of atoms, creates a keyword list with nil values.
%%% @end
-spec new([{key(), value()}] | [key()]) -> keyword().
new(List) when is_list(List) ->
case keyword(List) of
true -> List;
false -> create_keyword_list(List)
end.
%%% @doc
%%% Fetches the first value for the given key.
%%%
%%% Returns {ok, Value} if found, or error if not found.
%%% @end
-spec fetch(keyword(), key()) -> {ok, value()} | error.
fetch(Keywords, Key) ->
case lists:keyfind(Key, 1, Keywords) of
{Key, Value} -> {ok, Value};
false -> error
end.
%%% @doc
%%% Fetches the first value for the given key, raising an error if not found.
%%%
%%% Returns the value if found, or calls error/1 with the given reason if not found.
%%% @end
-spec fetch(keyword(), key(), term()) -> value().
fetch(Keywords, Key, ErrorReason) ->
case fetch(Keywords, Key) of
{ok, Value} -> Value;
error -> error(ErrorReason)
end.
%%%=============================================================================
%%% Internal functions
%%%=============================================================================
%% @private
split_acc([], _KeySet, Taken, Dropped) ->
{lists:reverse(Taken), lists:reverse(Dropped)};
split_acc([{Key, _Value} = KV | Rest], KeySet, Taken, Dropped) ->
case sets:is_element(Key, KeySet) of
true ->
split_acc(Rest, KeySet, [KV | Taken], Dropped);
false ->
split_acc(Rest, KeySet, Taken, [KV | Dropped])
end.
%% @private
create_keyword_list([]) ->
[];
create_keyword_list([Key | Rest]) when is_atom(Key) ->
[{Key, nil} | create_keyword_list(Rest)];
create_keyword_list([{Key, Value} | Rest]) when is_atom(Key) ->
[{Key, Value} | create_keyword_list(Rest)];
create_keyword_list(_) ->
error(badarg).