%%% @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).