+%%% @copyright 2011 Erlware, LLC.
+%%% @doc
+%%% This provides an implementation of the ec_dictionary type using
+%%% erlang orddicts as a base. The function documentation for
+%%% ec_dictionary applies here as well.
+%%% see ec_dictionary
+%%% see orddict
+%%% @end
+%%%-------------------------------------------------------------------
+-module(ec_orddict).
+
+-behaviour(ec_dictionary).
+
+%% API
+-export([new/0,
+ has_key/2,
+ get/2,
+ get/3,
+ add/3,
+ remove/2,
+ has_value/2,
+ size/1,
+ to_list/1,
+ from_list/1,
+ keys/1]).
+
+-export_type([dictionary/2]).
+
+%%%===================================================================
+%%% Types
+%%%===================================================================
+%% This should be opaque, but that kills dialyzer so for now we export it
+%% however you should not rely on the internal representation here
+-type dictionary(K, V) :: [{K, V}].
+
+%%%===================================================================
+%%% API
+%%%===================================================================
+
+-spec new() -> dictionary(_K, _V).
+new() ->
+ orddict:new().
+
+-spec has_key(ec_dictionary:key(K), Object::dictionary(K, _V)) -> boolean().
+has_key(Key, Data) ->
+ orddict:is_key(Key, Data).
+
+-spec get(ec_dictionary:key(K), Object::dictionary(K, V)) ->
+ ec_dictionary:value(V).
+get(Key, Data) ->
+ case orddict:find(Key, Data) of
+ {ok, Value} ->
+ Value;
+ error ->
+ throw(not_found)
+ end.
+
+-spec get(ec_dictionary:key(K),
+ Default::ec_dictionary:value(V),
+ Object::dictionary(K, V)) ->
+ ec_dictionary:value(V).
+get(Key, Default, Data) ->
+ case orddict:find(Key, Data) of
+ {ok, Value} ->
+ Value;
+ error ->
+ Default
+ end.
+
+-spec add(ec_dictionary:key(K), ec_dictionary:value(V),
+ Object::dictionary(K, V)) ->
+ dictionary(K, V).
+add(Key, Value, Data) ->
+ orddict:store(Key, Value, Data).
+
+-spec remove(ec_dictionary:key(K), Object::dictionary(K, V)) ->
+ dictionary(K, V).
+remove(Key, Data) ->
+ orddict:erase(Key, Data).
+
+-spec has_value(ec_dictionary:value(V), Object::dictionary(_K, V)) -> boolean().
+has_value(Value, Data) ->
+ orddict:fold(fun(_, NValue, _) when NValue == Value ->
+ true;
+ (_, _, Acc) ->
+ Acc
+ end,
+ false,
+ Data).
+
+-spec size(Object::dictionary(_K, _V)) -> non_neg_integer().
+size(Data) ->
+ orddict:size(Data).
+
+-spec to_list(dictionary(K, V)) ->
+ [{ec_dictionary:key(K), ec_dictionary:value(V)}].
+to_list(Data) ->
+ orddict:to_list(Data).
+
+-spec from_list([{ec_dictionary:key(K), ec_dictionary:value(V)}]) ->
+ dictionary(K, V).
+from_list(List) when is_list(List) ->
+ orddict:from_list(List).
+
+-spec keys(dictionary(K, _V)) -> [ec_dictionary:key(K)].
+keys(Dict) ->
+ orddict:fetch_keys(Dict).
diff --git a/src/ec_plists.erl b/src/ec_plists.erl
index b7b9260..221075b 100644
--- a/src/ec_plists.erl
+++ b/src/ec_plists.erl
@@ -1,257 +1,945 @@
-%%%-------------------------------------------------------------------
+%%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*-
+%%% vi:ts=4 sw=4 et
+%%% The MIT License
+%%%
+%%% Copyright (c) 2007 Stephen Marsh
+%%%
+%%% Permission is hereby granted, free of charge, to any person obtaining a copy
+%%% of this software and associated documentation files (the "Software"), to deal
+%%% in the Software without restriction, including without limitation the rights
+%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+%%% copies of the Software, and to permit persons to whom the Software is
+%%% furnished to do so, subject to the following conditions:
+%%%
+%%% The above copyright notice and this permission notice shall be included in
+%%% all copies or substantial portions of the Software.
+%%%
+%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+%%% THE SOFTWARE.
+%%%---------------------------------------------------------------------------
+%%% @author Stephen Marsh
+%%% @copyright 2007 Stephen Marsh freeyourmind ++ [$@|gmail.com]
%%% @doc
-%%% simple parrallel map. Originally provided by Joe Armstrong
-%%% on the erlang questions mailing list.
-%%% @end
-%%%-------------------------------------------------------------------
+%%% plists is a drop-in replacement for module lists, making
+%%% most list operations parallel. It can operate on each element in
+%%% parallel, for IO-bound operations, on sublists in parallel, for
+%%% taking advantage of multi-core machines with CPU-bound operations,
+%%% and across erlang nodes, for parallelizing inside a cluster. It
+%%% handles errors and node failures. It can be configured, tuned, and
+%%% tweaked to get optimal performance while minimizing overhead.
+%%%
+%%% Almost all the functions are identical to equivalent functions in
+%%% lists, returning exactly the same result, and having both a form
+%%% with an identical syntax that operates on each element in parallel
+%%% and a form which takes an optional "malt", a specification for how
+%%% to parallelize the operation.
+%%%
+%%% fold is the one exception, parallel fold is different from linear
+%%% fold. This module also include a simple mapreduce implementation,
+%%% and the function runmany. All the other functions are implemented
+%%% with runmany, which is as a generalization of parallel list
+%%% operations.
+%%%
+%%% Malts
+%%% =====
+%%%
+%%% A malt specifies how to break a list into sublists, and can optionally
+%%% specify a timeout, which nodes to run on, and how many processes to start
+%%% per node.
+%%%
+%%% Malt = MaltComponent | [MaltComponent]
+%%% MaltComponent = SubListSize::integer() | {processes, integer()} |
+%%% {processes, schedulers} |
+%%% {timeout, Milliseconds::integer()} | {nodes, [NodeSpec]}
+%%%
+%%% NodeSpec = Node::atom() | {Node::atom(), NumProcesses::integer()} |
+%%% {Node::atom(), schedulers}
+%%%
+%%% An integer can be given to specify the exact size for sublists. 1
+%%% is a good choice for IO-bound operations and when the operation on
+%%% each list element is expensive. Larger numbers minimize overhead
+%%% and are faster for cheap operations.
+%%%
+%%% If the integer is omitted, and you have specified a `{processes,
+%%% X}`, the list is split into X sublists. This is only useful when
+%%% the time to process each element is close to identical and you
+%%% know exactly how many lines of execution are available to you.
+%%%
+%%% If neither of the above applies, the sublist size defaults to 1.
+%%%
+%%% You can use `{processes, X}` to have the list processed by `X`
+%%% processes on the local machine. A good choice for `X` is the
+%%% number of lines of execution (cores) the machine provides. This
+%%% can be done automatically with {processes, schedulers}, which sets
+%%% the number of processes to the number of schedulers in the erlang
+%%% virtual machine (probably equal to the number of cores).
+%%%
+%%% `{timeout, Milliseconds}` specifies a timeout. This is a timeout
+%%% for the entire operation, both operating on the sublists and
+%%% combining the results. exit(timeout) is evaluated if the timeout
+%%% is exceeded.
+%%%
+%%% `{nodes, NodeList}` specifies that the operation should be done
+%%% across nodes. Every element of NodeList is of the form
+%%% `{NodeName, NumProcesses}` or NodeName, which means the same as
+%%% `{NodeName, 1}`. plists runs NumProcesses processes on NodeName
+%%% concurrently. A good choice for NumProcesses is the number of
+%%% lines of execution (cores) a node provides plus one. This ensures
+%%% the node is completely busy even when fetching a new sublist. This
+%%% can be done automatically with `{NodeName, schedulers}`, in which
+%%% case plists uses a cached value if it has one, and otherwise finds
+%%% the number of schedulers in the remote node and adds one. This
+%%% will ensure at least one busy process per core (assuming the node
+%%% has a scheduler for each core).
+%%%
+%%% plists is able to recover if a node goes down. If all nodes go
+%%% down, exit(allnodescrashed) is evaluated.
+%%%
+%%% Any of the above may be used as a malt, or may be combined into a
+%%% list. `{nodes, NodeList}` and {processes, X} may not be combined.
+%%%
+%%% Examples
+%%% ========
+%%%
+%%% %%start a process for each element (1-element sublists)<
+%%% 1
+%%%
+%%% %% start a process for each ten elements (10-element sublists)
+%%% 10
+%%%
+%%% %% split the list into two sublists and process in two processes
+%%% {processes, 2}
+%%%
+%%% %% split the list into X sublists and process in X processes,
+%%% %% where X is the number of cores in the machine
+%%% {processes, schedulers}
+%%%
+%%% %% split the list into 10-element sublists and process in two processes
+%%% [10, {processes, 2}]
+%%%
+%%% %% timeout after one second. Assumes that a process should be started
+%%% %% for each element.
+%%% {timeout, 1000}
+%%%
+%%% %% Runs 3 processes at a time on apple@desktop, and 2 on orange@laptop
+%%% %% This is the best way to utilize all the CPU-power of a dual-core
+%%% %% desktop and a single-core laptop. Assumes that the list should be
+%%% %% split into 1-element sublists.
+%%% {nodes, [{apple@desktop, 3}, {orange@laptop, 2}]}
+%%%
+%%% %% Like above, but makes plists figure out how many processes to use.
+%%% {nodes, [{apple@desktop, schedulers}, {orange@laptop, schedulers}]}
+%%%
+%%% %% Gives apple and orange three seconds to process the list as
+%%% %% 100-element sublists.
+%%% [100, {timeout, 3000}, {nodes, [{apple@desktop, 3}, {orange@laptop, 2}]}]
+%%%
+%%% Aside: Why Malt?
+%%% ================
+%%%
+%%% I needed a word for this concept, so maybe my subconsciousness
+%%% gave me one by making me misspell multiply. Maybe it is an acronym
+%%% for Malt is A List Tearing Specification. Maybe it is a beer
+%%% metaphor, suggesting that code only runs in parallel if bribed
+%%% with spirits. It's jargon, learn it or you can't be part of the
+%%% in-group.
+%%%
+%%% Messages and Errors
+%%% ===================
+%%%
+%%% plists assures that no extraneous messages are left in or will
+%%% later enter the message queue. This is guaranteed even in the
+%%% event of an error.
+%%%
+%%% Errors in spawned processes are caught and propagated to the
+%%% calling process. If you invoke
+%%%
+%%% plists:map(fun (X) -> 1/X end, [1, 2, 3, 0]).
+%%%
+%%% you get a badarith error, exactly like when you use lists:map.
+%%%
+%%% plists uses monitors to watch the processes it spawns. It is not a
+%%% good idea to invoke plists when you are already monitoring
+%%% processes. If one of them does a non-normal exit, plists receives
+%%% the 'DOWN' message believing it to be from one of its own
+%%% processes. The error propagation system goes into effect, which
+%%% results in the error occurring in the calling process.
+%%%
-module(ec_plists).
--export([map/2,
- map/3,
- ftmap/2,
- ftmap/3,
- filter/2,
- filter/3]).
+-export([all/2, all/3,
+ any/2, any/3,
+ filter/2, filter/3,
+ fold/3, fold/4, fold/5,
+ foreach/2, foreach/3,
+ map/2, map/3,
+ ftmap/2, ftmap/3,
+ partition/2, partition/3,
+ sort/1, sort/2, sort/3,
+ usort/1, usort/2, usort/3,
+ mapreduce/2, mapreduce/3, mapreduce/5,
+ runmany/3, runmany/4]).
-%%=============================================================================
-%% Public API
-%%=============================================================================
+-export_type([malt/0, malt_component/0, node_spec/0, fuse/0, fuse_fun/0]).
-%% @doc Takes a function and produces a list of the result of the function
-%% applied to each element of the argument list. A timeout is optional.
-%% In the event of a timeout or an exception the entire map will fail
-%% with an excption with class throw.
--spec map(fun(), [any()]) -> [any()].
-map(Fun, List) ->
- map(Fun, List, infinity).
+%%============================================================================
+%% types
+%%============================================================================
--spec map(fun(), [any()], non_neg_integer()) -> [any()].
-map(Fun, List, Timeout) ->
- run_list_fun_in_parallel(map, Fun, List, Timeout).
+-type malt() :: malt_component() | [malt_component()].
-%% @doc Takes a function and produces a list of the result of the function
-%% applied to each element of the argument list. A timeout is optional.
-%% This function differes from regular map in that it is fault tolerant.
-%% If a timeout or an exception occurs while processing an element in
-%% the input list the ftmap operation will continue to function. Timeouts
-%% and exceptions will be reflected in the output of this function.
-%% All application level results are wrapped in a tuple with the tag
-%% 'value'. Exceptions will come through as they are and timeouts will
-%% return as the atom timeout.
-%% This is useful when the ftmap is being used for side effects.
-%%
-%% 2> ftmap(fun(N) -> factorial(N) end, [1, 2, 1000000, "not num"], 100)
-%% [{value, 1}, {value, 2}, timeout, {badmatch, ...}]
-%%
--spec ftmap(fun(), [any()]) -> [{value, any()} | any()].
-ftmap(Fun, List) ->
- ftmap(Fun, List, infinity).
+-type malt_component() :: SubListSize::integer()
+ | {processes, integer()}
+ | {processes, schedulers}
+ | {timeout, Milliseconds::integer()}
+ | {nodes, [node_spec()]}.
--spec ftmap(fun(), [any()], non_neg_integer()) -> [{value, any()} | any()].
-ftmap(Fun, List, Timeout) ->
- run_list_fun_in_parallel(ftmap, Fun, List, Timeout).
+-type node_spec() :: Node::atom()
+ | {Node::atom(), NumProcesses::integer()}
+ | {Node::atom(), schedulers}.
-%% @doc Returns a list of the elements in the supplied list which
-%% the function Fun returns true. A timeout is optional. In the
-%% event of a timeout the filter operation fails.
--spec filter(fun(), [any()]) -> [any()].
-filter(Fun, List) ->
- filter(Fun, List, infinity).
+-type fuse_fun() :: fun((term(), term()) -> term()).
+-type fuse() :: fuse_fun() | {recursive, fuse_fun()} | {reverse, fuse_fun()}.
+-type el_fun() :: fun((term()) -> term()).
--spec filter(fun(), [any()], integer()) -> [any()].
-filter(Fun, List, Timeout) ->
- run_list_fun_in_parallel(filter, Fun, List, Timeout).
+%%============================================================================
+%% API
+%%============================================================================
-%%=============================================================================
-%% Internal API
-%%=============================================================================
--spec run_list_fun_in_parallel(atom(), fun(), [any()], integer()) -> [any()].
-run_list_fun_in_parallel(ListFun, Fun, List, Timeout) ->
- LocalPid = self(),
- Pids =
- lists:map(fun(E) ->
- Pid =
- proc_lib:spawn(fun() ->
- wait(LocalPid, Fun,
- E, Timeout)
- end),
- {Pid, E}
- end, List),
- gather(ListFun, Pids).
+%% Everything here is defined in terms of runmany.
+%% The following methods are convient interfaces to runmany.
--spec wait(pid(), fun(), any(), integer()) -> any().
-wait(Parent, Fun, E, Timeout) ->
- WaitPid = self(),
- Child = spawn(fun() ->
- do_f(WaitPid, Fun, E)
- end),
+%% @doc Same semantics as in module
+%% lists.
+-spec all(el_fun(), list()) -> boolean().
+all(Fun, List) ->
+ all(Fun, List, 1).
- wait(Parent, Child, Timeout).
-
--spec wait(pid(), pid(), integer()) -> any().
-wait(Parent, Child, Timeout) ->
- receive
- {Child, Ret} ->
- Parent ! {self(), Ret}
- after Timeout ->
- exit(Child, timeout),
- Parent ! {self(), timeout}
- end.
-
--spec gather(atom(), [any()]) -> [any()].
-gather(map, PidElementList) ->
- map_gather(PidElementList);
-gather(ftmap, PidElementList) ->
- ftmap_gather(PidElementList);
-gather(filter, PidElementList) ->
- filter_gather(PidElementList).
-
--spec map_gather([pid()]) -> [any()].
-map_gather([{Pid, _E} | Rest]) ->
- receive
- {Pid, {value, Ret}} ->
- [Ret|map_gather(Rest)];
- % timeouts fall here too. Should timeouts be a return value
- % or an exception? I lean toward return value, but the code
- % is easier with the exception. Thoughts?
- {Pid, Exception} ->
- killall(Rest),
- throw(Exception)
- end;
-map_gather([]) ->
- [].
-
--spec ftmap_gather([pid()]) -> [any()].
-ftmap_gather([{Pid, _E} | Rest]) ->
- receive
- {Pid, Value} -> [Value|ftmap_gather(Rest)]
- end;
-ftmap_gather([]) ->
- [].
-
--spec filter_gather([pid()]) -> [any()].
-filter_gather([{Pid, E} | Rest]) ->
- receive
- {Pid, {value, false}} ->
- filter_gather(Rest);
- {Pid, {value, true}} ->
- [E|filter_gather(Rest)];
- {Pid, {value, NotBool}} ->
- killall(Rest),
- throw({bad_return_value, NotBool});
- {Pid, Exception} ->
- killall(Rest),
- throw(Exception)
- end;
-filter_gather([]) ->
- [].
-
--spec do_f(pid(), fun(), any()) -> no_return().
-do_f(Parent, F, E) ->
+%% @doc Same semantics as in module
+%% lists.
+-spec all(el_fun(), list(), malt()) -> boolean().
+all(Fun, List, Malt) ->
try
- Result = F(E),
- Parent ! {self(), {value, Result}}
+ runmany(fun (L) ->
+ B = lists:all(Fun, L),
+ if
+ B ->
+ nil;
+ true ->
+ erlang:throw(notall)
+ end
+ end,
+ fun (_A1, _A2) ->
+ nil
+ end,
+ List, Malt),
+ true
catch
- _Class:Exception ->
- % Losing class info here, but since throw does not accept
- % that arg anyhow and forces a class of throw it does not
- % matter.
- Parent ! {self(), Exception}
+ throw:notall ->
+ false
end.
--spec killall([pid()]) -> ok.
-killall([{Pid, _E}|T]) ->
- exit(Pid, kill),
- killall(T);
-killall([]) ->
- ok.
+%% @doc Same semantics as in module
+%% lists.
+-spec any(fun(), list()) -> boolean().
+any(Fun, List) ->
+ any(Fun, List, 1).
-%%=============================================================================
-%% Tests
-%%=============================================================================
-
--ifndef(NOTEST).
--include_lib("eunit/include/eunit.hrl").
-
-map_good_test() ->
- Results = map(fun(_) ->
- ok
- end,
- lists:seq(1, 5), infinity),
- ?assertMatch([ok, ok, ok, ok, ok],
- Results).
-
-ftmap_good_test() ->
- Results = ftmap(fun(_) ->
- ok
- end,
- lists:seq(1, 3), infinity),
- ?assertMatch([{value, ok}, {value, ok}, {value, ok}],
- Results).
-
-filter_good_test() ->
- Results = filter(fun(X) ->
- X == show
- end,
- [show, show, remove], infinity),
- ?assertMatch([show, show],
- Results).
-
-map_timeout_test() ->
- Results =
- try
- map(fun(T) ->
- timer:sleep(T),
- T
+%% @doc Same semantics as in module
+%% lists.
+-spec any(fun(), list(), malt()) -> boolean().
+any(Fun, List, Malt) ->
+ try
+ runmany(fun (L) ->
+ B = lists:any(Fun, L),
+ if B ->
+ erlang:throw(any);
+ true ->
+ nil
+ end
end,
- [1, 100], 10)
- catch
- C:E -> {C, E}
- end,
- ?assertMatch({throw, timeout}, Results).
+ fun (_A1, _A2) ->
+ nil
+ end,
+ List, Malt) of
+ _ ->
+ false
+ catch throw:any ->
+ true
+ end.
-ftmap_timeout_test() ->
- Results = ftmap(fun(X) ->
- timer:sleep(X),
- true
- end,
- [100, 1], 10),
- ?assertMatch([timeout, {value, true}], Results).
+%% @doc Same semantics as in module
+%% lists.
+-spec filter(fun(), list()) -> list().
+filter(Fun, List) ->
+ filter(Fun, List, 1).
-filter_timeout_test() ->
- Results =
- try
- filter(fun(T) ->
- timer:sleep(T),
- T == 1
+%% @doc Same semantics as in module
+%% lists.
+-spec filter(fun(), list(), malt()) -> list().
+filter(Fun, List, Malt) ->
+ runmany(fun (L) ->
+ lists:filter(Fun, L)
+ end,
+ {reverse, fun (A1, A2) ->
+ A1 ++ A2
+ end},
+ List, Malt).
+
+%% Note that with parallel fold there is not foldl and foldr,
+%% instead just one fold that can fuse Accumlators.
+
+%% @doc Like below, but assumes 1 as the Malt. This function is almost useless,
+%% and is intended only to aid converting code from using lists to plists.
+-spec fold(fun(), InitAcc::term(), list()) -> term().
+fold(Fun, InitAcc, List) ->
+ fold(Fun, Fun, InitAcc, List, 1).
+
+%% @doc Like below, but uses the Fun as the Fuse by default.
+-spec fold(fun(), InitAcc::term(), list(), malt()) -> term().
+fold(Fun, InitAcc, List, Malt) ->
+ fold(Fun, Fun, InitAcc, List, Malt).
+
+%% @doc fold is more complex when made parallel. There is no foldl and
+%% foldr, accumulators aren't passed in any defined order. The list
+%% is split into sublists which are folded together. Fun is identical
+%% to the function passed to lists:fold[lr], it takes (an element, and
+%% the accumulator) and returns -> a new accumulator. It is used for
+%% the initial stage of folding sublists. Fuse fuses together the
+%% results, it takes (Results1, Result2) and returns -> a new result.
+%% By default sublists are fused left to right, each result of a fuse
+%% being fed into the first element of the next fuse. The result of
+%% the last fuse is the result.
+%%
+%% Fusing may also run in parallel using a recursive algorithm,
+%% by specifying the fuse as {recursive, Fuse}. See
+%% the discussion in {@link runmany/4}.
+%%
+%% Malt is the malt for the initial folding of sublists, and for the
+%% possible recursive fuse.
+-spec fold(fun(), fuse(), InitAcc::term(), list(), malt()) -> term().
+fold(Fun, Fuse, InitAcc, List, Malt) ->
+ Fun2 = fun (L) ->
+ lists:foldl(Fun, InitAcc, L)
+ end,
+ runmany(Fun2, Fuse, List, Malt).
+
+%% @doc Similar to foreach in module
+%% lists
+%% except it makes no guarantee about the order it processes list elements.
+-spec foreach(fun(), list()) -> ok.
+foreach(Fun, List) ->
+ foreach(Fun, List, 1).
+
+%% @doc Similar to foreach in module
+%% lists
+%% except it makes no guarantee about the order it processes list elements.
+-spec foreach(fun(), list(), malt()) -> ok.
+foreach(Fun, List, Malt) ->
+ runmany(fun (L) ->
+ lists:foreach(Fun, L)
+ end,
+ fun (_A1, _A2) ->
+ ok
+ end,
+ List, Malt).
+
+%% @doc Same semantics as in module
+%% lists.
+-spec map(fun(), list()) -> list().
+map(Fun, List) ->
+ map(Fun, List, 1).
+
+%% @doc Same semantics as in module
+%% lists.
+-spec map(fun(), list(), malt()) -> list().
+map(Fun, List, Malt) ->
+ runmany(fun (L) ->
+ lists:map(Fun, L)
+ end,
+ {reverse, fun (A1, A2) ->
+ A1 ++ A2
+ end},
+ List, Malt).
+
+%% @doc values are returned as {value, term()}.
+-spec ftmap(fun(), list()) -> list().
+ftmap(Fun, List) ->
+ map(fun(L) ->
+ try
+ {value, Fun(L)}
+ catch
+ Class:Type ->
+ {error, {Class, Type}}
+ end
+ end, List).
+
+%% @doc values are returned as {value, term()}.
+-spec ftmap(fun(), list(), malt()) -> list().
+ftmap(Fun, List, Malt) ->
+ map(fun(L) ->
+ try
+ {value, Fun(L)}
+ catch
+ Class:Type ->
+ {error, {Class, Type}}
+ end
+ end, List, Malt).
+
+%% @doc Same semantics as in module
+%% lists.
+-spec partition(fun(), list()) -> {list(), list()}.
+partition(Fun, List) ->
+ partition(Fun, List, 1).
+
+%% @doc Same semantics as in module
+%% lists.
+-spec partition(fun(), list(), malt()) -> {list(), list()}.
+partition(Fun, List, Malt) ->
+ runmany(fun (L) ->
+ lists:partition(Fun, L)
+ end,
+ {reverse, fun ({True1, False1}, {True2, False2}) ->
+ {True1 ++ True2, False1 ++ False2}
+ end},
+ List, Malt).
+
+%% SORTMALT needs to be tuned
+-define(SORTMALT, 100).
+
+%% @doc Same semantics as in module
+%% lists.
+-spec sort(list()) -> list().
+sort(List) ->
+ sort(fun (A, B) ->
+ A =< B
+ end,
+ List).
+
+%% @doc Same semantics as in module
+%% lists.
+-spec sort(fun(), list()) -> list().
+sort(Fun, List) ->
+ sort(Fun, List, ?SORTMALT).
+
+%% @doc This version lets you specify your own malt for sort.
+%%
+%% sort splits the list into sublists and sorts them, and it merges the
+%% sorted lists together. These are done in parallel. Each sublist is
+%% sorted in a separate process, and each merging of results is done in a
+%% separate process. Malt defaults to 100, causing the list to be split into
+%% 100-element sublists.
+-spec sort(fun(), list(), malt()) -> list().
+sort(Fun, List, Malt) ->
+ Fun2 = fun (L) ->
+ lists:sort(Fun, L)
+ end,
+ Fuse = fun (A1, A2) ->
+ lists:merge(Fun, A1, A2)
+ end,
+ runmany(Fun2, {recursive, Fuse}, List, Malt).
+
+%% @doc Same semantics as in module
+%% lists.
+-spec usort(list()) -> list().
+usort(List) ->
+ usort(fun (A, B) ->
+ A =< B
+ end,
+ List).
+
+%% @doc Same semantics as in module
+%% lists.
+-spec usort(fun(), list()) -> list().
+usort(Fun, List) ->
+ usort(Fun, List, ?SORTMALT).
+
+%% @doc This version lets you specify your own malt for usort.
+%%
+%% usort splits the list into sublists and sorts them, and it merges the
+%% sorted lists together. These are done in parallel. Each sublist is
+%% sorted in a separate process, and each merging of results is done in a
+%% separate process. Malt defaults to 100, causing the list to be split into
+%% 100-element sublists.
+%%
+%% usort removes duplicate elements while it sorts.
+-spec usort(fun(), list(), malt()) -> list().
+usort(Fun, List, Malt) ->
+ Fun2 = fun (L) ->
+ lists:usort(Fun, L)
+ end,
+ Fuse = fun (A1, A2) ->
+ lists:umerge(Fun, A1, A2)
+ end,
+ runmany(Fun2, {recursive, Fuse}, List, Malt).
+
+%% @doc Like below, assumes default MapMalt of 1.
+-spec mapreduce(MapFunc, list()) -> dict:dict() when
+ MapFunc :: fun((term()) -> DeepListOfKeyValuePairs),
+ DeepListOfKeyValuePairs :: [DeepListOfKeyValuePairs] | {Key::term(), Value::term()}.
+
+mapreduce(MapFunc, List) ->
+ mapreduce(MapFunc, List, 1).
+
+%% Like below, but uses a default reducer that collects all
+%% {Key, Value} pairs into a
+%% dict,
+%% with values {Key, [Value1, Value2...]}.
+%% This dict is returned as the result.
+mapreduce(MapFunc, List, MapMalt) ->
+ mapreduce(MapFunc, List, dict:new(), fun add_key/3, MapMalt).
+
+%% @doc This is a very basic mapreduce. You won't write a
+%% Google-rivaling search engine with it. It has no equivalent in
+%% lists. Each element in the list is run through the MapFunc, which
+%% produces either a {Key, Value} pair, or a lists of key value pairs,
+%% or a list of lists of key value pairs...etc. A reducer process runs
+%% in parallel with the mapping processes, collecting the key value
+%% pairs. It starts with a state given by InitState, and for each
+%% {Key, Value} pair that it receives it invokes ReduceFunc(OldState,
+%% Key, Value) to compute its new state. mapreduce returns the
+%% reducer's final state.
+%%
+%% MapMalt is the malt for the mapping operation, with a default value of 1,
+%% meaning each element of the list is mapped by a separate process.
+%%
+%% mapreduce requires OTP R11B, or it may leave monitoring messages in the
+%% message queue.
+-spec mapreduce(MapFunc, list(), InitState::term(), ReduceFunc, malt()) -> dict:dict() when
+ MapFunc :: fun((term()) -> DeepListOfKeyValuePairs),
+ DeepListOfKeyValuePairs :: [DeepListOfKeyValuePairs] | {Key::term(), Value::term()},
+ ReduceFunc :: fun((OldState::term(), Key::term(), Value::term()) -> NewState::term()).
+mapreduce(MapFunc, List, InitState, ReduceFunc, MapMalt) ->
+ Parent = self(),
+ {Reducer, ReducerRef} =
+ erlang:spawn_monitor(fun () ->
+ reducer(Parent, 0, InitState, ReduceFunc)
+ end),
+ MapFunc2 = fun (L) ->
+ Reducer ! lists:map(MapFunc, L),
+ 1
+ end,
+ SentMessages = try
+ runmany(MapFunc2, fun (A, B) -> A+B end, List, MapMalt)
+ catch
+ exit:Reason ->
+ erlang:demonitor(ReducerRef, [flush]),
+ Reducer ! die,
+ exit(Reason)
end,
- [1, 100], 10)
- catch
- C:E -> {C, E}
- end,
- ?assertMatch({throw, timeout}, Results).
-
-map_bad_test() ->
- Results =
- try
- map(fun(_) ->
- throw(test_exception)
- end,
- lists:seq(1, 5), infinity)
- catch
- C:E -> {C, E}
- end,
- ?assertMatch({throw, test_exception}, Results).
-
-ftmap_bad_test() ->
- Results =
- ftmap(fun(2) ->
- throw(test_exception);
- (N) ->
- N
+ Reducer ! {mappers, done, SentMessages},
+ Results = receive
+ {Reducer, Results2} ->
+ Results2;
+ {'DOWN', _, _, Reducer, Reason2} ->
+ exit(Reason2)
end,
- lists:seq(1, 5), infinity),
- ?assertMatch([{value, 1}, test_exception, {value, 3},
- {value, 4}, {value, 5}] , Results).
+ receive
+ {'DOWN', _, _, Reducer, normal} ->
+ nil
+ end,
+ Results.
--endif.
+reducer(Parent, NumReceived, State, Func) ->
+ receive
+ die ->
+ nil;
+ {mappers, done, NumReceived} ->
+ Parent ! {self (), State};
+ Keys ->
+ reducer(Parent, NumReceived + 1, each_key(State, Func, Keys), Func)
+ end.
+
+each_key(State, Func, {Key, Value}) ->
+ Func(State, Key, Value);
+each_key(State, Func, [List|Keys]) ->
+ each_key(each_key(State, Func, List), Func, Keys);
+each_key(State, _, []) ->
+ State.
+
+add_key(Dict, Key, Value) ->
+ case dict:is_key(Key, Dict) of
+ true ->
+ dict:append(Key, Value, Dict);
+ false ->
+ dict:store(Key, [Value], Dict)
+ end.
+
+%% @doc Like below, but assumes a Malt of 1,
+%% meaning each element of the list is processed by a separate process.
+-spec runmany(fun(), fuse(), list()) -> term().
+runmany(Fun, Fuse, List) ->
+ runmany(Fun, Fuse, List, 1).
+
+%% Begin internal stuff (though runmany/4 is exported).
+
+%% @doc All of the other functions are implemented with runmany. runmany
+%% takes a List, splits it into sublists, and starts processes to operate on
+%% each sublist, all done according to Malt. Each process passes its sublist
+%% into Fun and sends the result back.
+%%
+%% The results are then fused together to get the final result. There are two
+%% ways this can operate, lineraly and recursively. If Fuse is a function,
+%% a fuse is done linearly left-to-right on the sublists, the results
+%% of processing the first and second sublists being passed to Fuse, then
+%% the result of the first fuse and processing the third sublits, and so on. If
+%% Fuse is {reverse, FuseFunc}, then a fuse is done right-to-left, the results
+%% of processing the second-to-last and last sublists being passed to FuseFunc,
+%% then the results of processing the third-to-last sublist and
+%% the results of the first fuse, and and so forth.
+%% Both methods preserve the original order of the lists elements.
+%%
+%% To do a recursive fuse, pass Fuse as {recursive, FuseFunc}.
+%% The recursive fuse makes no guarantee about the order the results of
+%% sublists, or the results of fuses are passed to FuseFunc. It
+%% continues fusing pairs of results until it is down to one.
+%%
+%% Recursive fuse is down in parallel with processing the sublists, and a
+%% process is spawned to fuse each pair of results. It is a parallelized
+%% algorithm. Linear fuse is done after all results of processing sublists
+%% have been collected, and can only run in a single process.
+%%
+%% Even if you pass {recursive, FuseFunc}, a recursive fuse is only done if
+%% the malt contains {nodes, NodeList} or {processes, X}. If this is not the
+%% case, a linear fuse is done.
+-spec runmany(fun(([term()]) -> term()), fuse(), list(), malt()) -> term().
+runmany(Fun, Fuse, List, Malt)
+ when erlang:is_list(Malt) ->
+ runmany(Fun, Fuse, List, local, no_split, Malt);
+runmany(Fun, Fuse, List, Malt) ->
+ runmany(Fun, Fuse, List, [Malt]).
+
+runmany(Fun, Fuse, List, Nodes, no_split, [MaltTerm|Malt])
+ when erlang:is_integer(MaltTerm) ->
+ runmany(Fun, Fuse, List, Nodes, MaltTerm, Malt);
+runmany(Fun, Fuse, List, local, Split, [{processes, schedulers}|Malt]) ->
+ %% run a process for each scheduler
+ S = erlang:system_info(schedulers),
+ runmany(Fun, Fuse, List, local, Split, [{processes, S}|Malt]);
+runmany(Fun, Fuse, List, local, no_split, [{processes, X}|_]=Malt) ->
+ %% Split the list into X sublists, where X is the number of processes
+ L = erlang:length(List),
+ case (L rem X) of
+ 0 ->
+ runmany(Fun, Fuse, List, local, (L / X), Malt);
+ _ ->
+ runmany(Fun, Fuse, List, local, (L / X) + 1, Malt)
+ end;
+runmany(Fun, Fuse, List, local, Split, [{processes, X}|Malt]) ->
+ %% run X process on local machine
+ Nodes = lists:duplicate(X, node()),
+ runmany(Fun, Fuse, List, Nodes, Split, Malt);
+runmany(Fun, Fuse, List, Nodes, Split, [{timeout, X}|Malt]) ->
+ Parent = erlang:self(),
+ Timer = proc_lib:spawn(fun () ->
+ receive
+ stoptimer ->
+ Parent ! {timerstopped, erlang:self()}
+ after X ->
+ Parent ! {timerrang, erlang:self()},
+ receive
+ stoptimer ->
+ Parent ! {timerstopped, erlang:self()}
+ end
+ end
+ end),
+ Ans = try
+ runmany(Fun, Fuse, List, Nodes, Split, Malt)
+ catch
+ %% we really just want the after block, the syntax
+ %% makes this catch necessary.
+ willneverhappen ->
+ nil
+ after
+ Timer ! stoptimer,
+ cleanup_timer(Timer)
+ end,
+ Ans;
+runmany(Fun, Fuse, List, local, Split, [{nodes, NodeList}|Malt]) ->
+ Nodes = lists:foldl(fun ({Node, schedulers}, A) ->
+ X = schedulers_on_node(Node) + 1,
+ lists:reverse(lists:duplicate(X, Node), A);
+ ({Node, X}, A) ->
+ lists:reverse(lists:duplicate(X, Node), A);
+ (Node, A) ->
+ [Node|A]
+ end,
+ [], NodeList),
+ runmany(Fun, Fuse, List, Nodes, Split, Malt);
+runmany(Fun, {recursive, Fuse}, List, local, Split, []) ->
+ %% local recursive fuse, for when we weren't invoked with {processes, X}
+ %% or {nodes, NodeList}. Degenerates recursive fuse into linear fuse.
+ runmany(Fun, Fuse, List, local, Split, []);
+runmany(Fun, Fuse, List, Nodes, no_split, []) ->
+ %% by default, operate on each element separately
+ runmany(Fun, Fuse, List, Nodes, 1, []);
+runmany(Fun, Fuse, List, local, Split, []) ->
+ List2 = splitmany(List, Split),
+ local_runmany(Fun, Fuse, List2);
+runmany(Fun, Fuse, List, Nodes, Split, []) ->
+ List2 = splitmany(List, Split),
+ cluster_runmany(Fun, Fuse, List2, Nodes).
+
+cleanup_timer(Timer) ->
+ receive
+ {timerrang, Timer} ->
+ cleanup_timer(Timer);
+ {timerstopped, Timer} ->
+ nil
+ end.
+
+schedulers_on_node(Node) ->
+ case erlang:get(ec_plists_schedulers_on_nodes) of
+ undefined ->
+ X = determine_schedulers(Node),
+ erlang:put(ec_plists_schedulers_on_nodes,
+ dict:store(Node, X, dict:new())),
+ X;
+ Dict ->
+ case dict:is_key(Node, Dict) of
+ true ->
+ dict:fetch(Node, Dict);
+ false ->
+ X = determine_schedulers(Node),
+ erlang:put(ec_plists_schedulers_on_nodes,
+ dict:store(Node, X, Dict)),
+ X
+ end
+ end.
+
+determine_schedulers(Node) ->
+ Parent = erlang:self(),
+ Child = proc_lib:spawn(Node, fun () ->
+ Parent ! {self(), erlang:system_info(schedulers)}
+ end),
+ erlang:monitor(process, Child),
+ receive
+ {Child, X} ->
+ receive
+ {'DOWN', _, _, Child, _Reason} ->
+ nil
+ end,
+ X;
+ {'DOWN', _, _, Child, Reason} when Reason =/= normal ->
+ 0
+ end.
+
+%% @doc local runmany, for when we weren't invoked with {processes, X}
+%% or {nodes, NodeList}. Every sublist is processed in parallel.
+local_runmany(Fun, Fuse, List) ->
+ Parent = self (),
+ Pids = lists:map(fun (L) ->
+ F = fun () ->
+ Parent ! {self (), Fun(L)}
+ end,
+ {Pid, _} = erlang:spawn_monitor(F),
+ Pid
+ end,
+ List),
+ Answers = try
+ lists:map(fun receivefrom/1, Pids)
+ catch
+ throw:Message ->
+ {BadPid, Reason} = Message,
+ handle_error(BadPid, Reason, Pids)
+ end,
+ lists:foreach(fun (Pid) ->
+ normal_cleanup(Pid)
+ end, Pids),
+ fuse(Fuse, Answers).
+
+receivefrom(Pid) ->
+ receive
+ {Pid, R} ->
+ R;
+ {'DOWN', _, _, Pid, Reason} when Reason =/= normal ->
+ erlang:throw({Pid, Reason});
+ {timerrang, _} ->
+ erlang:throw({nil, timeout})
+ end.
+
+%% Convert List into [{Number, Sublist}]
+cluster_runmany(Fun, Fuse, List, Nodes) ->
+ {List2, _} = lists:foldl(fun (X, {L, Count}) ->
+ {[{Count, X}|L], Count+1}
+ end,
+ {[], 0}, List),
+ cluster_runmany(Fun, Fuse, List2, Nodes, [], []).
+
+%% @doc Add a pair of results into the TaskList as a fusing task
+cluster_runmany(Fun, {recursive, Fuse}, [], Nodes, Running,
+ [{_, R1}, {_, R2}|Results]) ->
+ cluster_runmany(Fun, {recursive, Fuse}, [{fuse, R1, R2}], Nodes,
+ Running, Results);
+cluster_runmany(_, {recursive, _Fuse}, [], _Nodes, [], [{_, Result}]) ->
+ %% recursive fuse done, return result
+ Result;
+cluster_runmany(_, {recursive, _Fuse}, [], _Nodes, [], []) ->
+ %% edge case where we are asked to do nothing
+ [];
+cluster_runmany(_, Fuse, [], _Nodes, [], Results) ->
+ %% We're done, now we just have to [linear] fuse the results
+ fuse(Fuse, lists:map(fun ({_, R}) ->
+ R
+ end,
+ lists:sort(fun ({A, _}, {B, _}) ->
+ A =< B
+ end,
+ lists:reverse(Results))));
+cluster_runmany(Fun, Fuse, [Task|TaskList], [N|Nodes], Running, Results) ->
+%% We have a ready node and a sublist or fuse to be processed, so we start
+%% a new process
+
+ Parent = erlang:self(),
+ case Task of
+ {Num, L2} ->
+ Fun2 = fun () ->
+ Parent ! {erlang:self(), Num, Fun(L2)}
+ end;
+ {fuse, R1, R2} ->
+ {recursive, FuseFunc} = Fuse,
+ Fun2 = fun () ->
+ Parent ! {erlang:self(), fuse, FuseFunc(R1, R2)}
+ end
+ end,
+ Fun3 = fun() -> runmany_wrap(Fun2, Parent) end,
+ Pid = proc_lib:spawn(N, Fun3),
+ erlang:monitor(process, Pid),
+ cluster_runmany(Fun, Fuse, TaskList, Nodes, [{Pid, N, Task}|Running], Results);
+cluster_runmany(Fun, Fuse, TaskList, Nodes, Running, Results) when length(Running) > 0 ->
+ %% We can't start a new process, but can watch over already running ones
+ receive
+ {_Pid, error, Reason} ->
+ RunningPids = lists:map(fun ({Pid, _, _}) ->
+ Pid
+ end,
+ Running),
+ handle_error(junkvalue, Reason, RunningPids);
+ {Pid, Num, Result} ->
+ %% throw out the exit message, Reason should be
+ %% normal, noproc, or noconnection
+ receive
+ {'DOWN', _, _, Pid, _Reason} ->
+ nil
+ end,
+ {Running2, FinishedNode, _} = delete_running(Pid, Running, []),
+ cluster_runmany(Fun, Fuse, TaskList,
+ [FinishedNode|Nodes], Running2, [{Num, Result}|Results]);
+ {timerrang, _} ->
+ RunningPids = lists:map(fun ({Pid, _, _}) ->
+ Pid
+ end,
+ Running),
+ handle_error(nil, timeout, RunningPids);
+ %% node failure
+ {'DOWN', _, _, Pid, noconnection} ->
+ {Running2, _DeadNode, Task} = delete_running(Pid, Running, []),
+ cluster_runmany(Fun, Fuse, [Task|TaskList], Nodes,
+ Running2, Results);
+ %% could a noproc exit message come before the message from
+ %% the process? we are assuming it can't.
+ %% this clause is unlikely to get invoked due to cluster_runmany's
+ %% spawned processes. It will still catch errors in mapreduce's
+ %% reduce process, however.
+ {'DOWN', _, _, BadPid, Reason} when Reason =/= normal ->
+ RunningPids = lists:map(fun ({Pid, _, _}) ->
+ Pid
+ end,
+ Running),
+ handle_error(BadPid, Reason, RunningPids)
+ end;
+cluster_runmany(_, _, [_Non|_Empty], []=_Nodes, []=_Running, _) ->
+%% We have data, but no nodes either available or occupied
+ erlang:exit(allnodescrashed).
+
+runmany_wrap(Fun, Parent) ->
+ try
+ Fun()
+ catch
+ exit:siblingdied ->
+ ok;
+ exit:Reason ->
+ Parent ! {erlang:self(), error, Reason};
+ error:R:Stacktrace ->
+ Parent ! {erlang:self(), error, {R, Stacktrace}};
+ throw:R:Stacktrace ->
+ Parent ! {erlang:self(), error, {{nocatch, R}, Stacktrace}}
+ end.
+
+delete_running(Pid, [{Pid, Node, List}|Running], Acc) ->
+ {Running ++ Acc, Node, List};
+delete_running(Pid, [R|Running], Acc) ->
+ delete_running(Pid, Running, [R|Acc]).
+
+handle_error(BadPid, Reason, Pids) ->
+ lists:foreach(fun (Pid) ->
+ erlang:exit(Pid, siblingdied)
+ end, Pids),
+ lists:foreach(fun (Pid) ->
+ error_cleanup(Pid, BadPid)
+ end, Pids),
+ erlang:exit(Reason).
+
+error_cleanup(BadPid, BadPid) ->
+ ok;
+error_cleanup(Pid, BadPid) ->
+ receive
+ {Pid, _} ->
+ error_cleanup(Pid, BadPid);
+ {Pid, _, _} ->
+ error_cleanup(Pid, BadPid);
+ {'DOWN', _, _, Pid, _Reason} ->
+ ok
+ end.
+
+normal_cleanup(Pid) ->
+ receive
+ {'DOWN', _, _, Pid, _Reason} ->
+ ok
+ end.
+
+%% edge case
+fuse(_, []) ->
+ [];
+fuse({reverse, _}=Fuse, Results) ->
+ [RL|ResultsR] = lists:reverse(Results),
+ fuse(Fuse, ResultsR, RL);
+fuse(Fuse, [R1|Results]) ->
+ fuse(Fuse, Results, R1).
+
+fuse({reverse, FuseFunc}=Fuse, [R2|Results], R1) ->
+ fuse(Fuse, Results, FuseFunc(R2, R1));
+fuse(Fuse, [R2|Results], R1) ->
+ fuse(Fuse, Results, Fuse(R1, R2));
+fuse(_, [], R) ->
+ R.
+
+%% @doc Splits a list into a list of sublists, each of size Size,
+%% except for the last element which is less if the original list
+%% could not be evenly divided into Size-sized lists.
+splitmany(List, Size) ->
+ splitmany(List, [], Size).
+
+splitmany([], Acc, _) ->
+ lists:reverse(Acc);
+splitmany(List, Acc, Size) ->
+ {Top, NList} = split(Size, List),
+ splitmany(NList, [Top|Acc], Size).
+
+%% @doc Like lists:split, except it splits a list smaller than its first
+%% parameter
+split(Size, List) ->
+ split(Size, List, []).
+
+split(0, List, Acc) ->
+ {lists:reverse(Acc), List};
+split(Size, [H|List], Acc) ->
+ split(Size - 1, List, [H|Acc]);
+split(_, [], Acc) ->
+ {lists:reverse(Acc), []}.
diff --git a/src/ec_rbdict.erl b/src/ec_rbdict.erl
new file mode 100644
index 0000000..9f3b617
--- /dev/null
+++ b/src/ec_rbdict.erl
@@ -0,0 +1,322 @@
+%%% vi:ts=4 sw=4 et
+%%% Copyright (c) 2008 Robert Virding. All rights reserved.
+%%%
+%%% Redistribution and use in source and binary forms, with or without
+%%% modification, are permitted provided that the following conditions
+%%% are met:
+%%%
+%%% 1. Redistributions of source code must retain the above copyright
+%%% notice, this list of conditions and the following disclaimer.
+%%% 2. Redistributions in binary form must reproduce the above copyright
+%%% notice, this list of conditions and the following disclaimer in the
+%%% documentation and/or other materials provided with the distribution.
+%%%
+%%% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+%%% "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+%%% LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+%%% FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+%%% COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+%%% INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+%%% BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+%%% LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+%%% CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+%%% LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+%%% ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+%%% POSSIBILITY OF SUCH DAMAGE.
+%%%-------------------------------------------------------------------
+%%% @copyright 2008 Robert Verding
+%%%
+%%% @doc
+%%%
+%%% Rbdict implements a Key - Value dictionary. An rbdict is a
+%%% representation of a dictionary, where a red-black tree is used to
+%%% store the keys and values.
+%%%
+%%% This module implements exactly the same interface as the module
+%%% ec_dictionary but with a defined representation. One difference is
+%%% that while dict considers two keys as different if they do not
+%%% match (=:=), this module considers two keys as different if and
+%%% only if they do not compare equal (==).
+%%%
+%%% The algorithms here are taken directly from Okasaki and Rbset
+%%% in ML/Scheme. The interface is compatible with the standard dict
+%%% interface.
+%%%
+%%% The following structures are used to build the the RB-dict:
+%%%
+%%% {r,Left,Key,Val,Right}
+%%% {b,Left,Key,Val,Right}
+%%% empty
+%%%
+%%% It is interesting to note that expanding out the first argument of
+%%% l/rbalance, the colour, in store etc. is actually slower than not
+%%% doing it. Measured.
+%%%
+%%% see ec_dictionary
+%%% @end
+%%%-------------------------------------------------------------------
+-module(ec_rbdict).
+
+-behaviour(ec_dictionary).
+
+%% Standard interface.
+-export([add/3, from_list/1, get/2, get/3, has_key/2,
+ has_value/2, new/0, remove/2, size/1, to_list/1,
+ keys/1]).
+
+-export_type([dictionary/2]).
+
+%%%===================================================================
+%%% Types
+%%%===================================================================
+%% This should be opaque, but that kills dialyzer so for now we export it
+%% however you should not rely on the internal representation here
+-type dictionary(K, V) :: empty | {color(),
+ dictionary(K, V),
+ ec_dictionary:key(K),
+ ec_dictionary:value(V),
+ dictionary(K, V)}.
+
+-type color() :: r | b.
+
+%%%===================================================================
+%%% API
+%%%===================================================================
+
+-spec new() -> dictionary(_K, _V).
+new() -> empty.
+
+-spec has_key(ec_dictionary:key(K), dictionary(K, _V)) -> boolean().
+has_key(_, empty) ->
+ false;
+has_key(K, {_, Left, K1, _, _}) when K < K1 ->
+ has_key(K, Left);
+has_key(K, {_, _, K1, _, Right}) when K > K1 ->
+ has_key(K, Right);
+has_key(_, {_, _, _, _, _}) ->
+ true.
+
+-spec get(ec_dictionary:key(K), dictionary(K, V)) -> ec_dictionary:value(V).
+get(_, empty) ->
+ throw(not_found);
+get(K, {_, Left, K1, _, _}) when K < K1 ->
+ get(K, Left);
+get(K, {_, _, K1, _, Right}) when K > K1 ->
+ get(K, Right);
+get(_, {_, _, _, Val, _}) ->
+ Val.
+
+-spec get(ec_dictionary:key(K),
+ ec_dictionary:value(V),
+ dictionary(K, V)) -> ec_dictionary:value(V).
+get(_, Default, empty) ->
+ Default;
+get(K, Default, {_, Left, K1, _, _}) when K < K1 ->
+ get(K, Default, Left);
+get(K, Default, {_, _, K1, _, Right}) when K > K1 ->
+ get(K, Default, Right);
+get(_, _, {_, _, _, Val, _}) ->
+ Val.
+
+-spec add(ec_dictionary:key(K), ec_dictionary:value(V),
+ dictionary(K, V)) -> dictionary(K, V).
+add(Key, Value, Dict) ->
+ {_, L, K1, V1, R} = add1(Key, Value, Dict),
+ {b, L, K1, V1, R}.
+
+-spec remove(ec_dictionary:key(K), dictionary(K, V)) -> dictionary(K, V).
+remove(Key, Dictionary) ->
+ {Dict1, _} = erase_aux(Key, Dictionary), Dict1.
+
+-spec has_value(ec_dictionary:value(V), dictionary(_K, V)) -> boolean().
+has_value(Value, Dict) ->
+ fold(fun (_, NValue, _) when NValue == Value -> true;
+ (_, _, Acc) -> Acc
+ end,
+ false, Dict).
+
+-spec size(dictionary(_K, _V)) -> non_neg_integer().
+size(T) ->
+ size1(T).
+
+-spec to_list(dictionary(K, V)) ->
+ [{ec_dictionary:key(K), ec_dictionary:value(V)}].
+to_list(T) ->
+ to_list(T, []).
+
+-spec from_list([{ec_dictionary:key(K), ec_dictionary:value(V)}]) ->
+ dictionary(K, V).
+from_list(L) ->
+ lists:foldl(fun ({K, V}, D) ->
+ add(K, V, D)
+ end, new(),
+ L).
+
+-spec keys(dictionary(K, _V)) -> [ec_dictionary:key(K)].
+keys(Dict) ->
+ keys(Dict, []).
+
+%%%===================================================================
+%%% Enternal functions
+%%%===================================================================
+-spec keys(dictionary(K, _V), [ec_dictionary:key(K)]) ->
+ [ec_dictionary:key(K)].
+keys(empty, Tail) ->
+ Tail;
+keys({_, L, K, _, R}, Tail) ->
+ keys(L, [K | keys(R, Tail)]).
+
+
+-spec erase_aux(ec_dictionary:key(K), dictionary(K, V)) ->
+ {dictionary(K, V), boolean()}.
+erase_aux(_, empty) ->
+ {empty, false};
+erase_aux(K, {b, A, Xk, Xv, B}) ->
+ if K < Xk ->
+ {A1, Dec} = erase_aux(K, A),
+ if Dec ->
+ unbalright(b, A1, Xk, Xv, B);
+ true ->
+ {{b, A1, Xk, Xv, B}, false}
+ end;
+ K > Xk ->
+ {B1, Dec} = erase_aux(K, B),
+ if Dec ->
+ unballeft(b, A, Xk, Xv, B1);
+ true ->
+ {{b, A, Xk, Xv, B1}, false}
+ end;
+ true ->
+ case B of
+ empty ->
+ blackify(A);
+ _ ->
+ {B1, {Mk, Mv}, Dec} = erase_min(B),
+ if Dec ->
+ unballeft(b, A, Mk, Mv, B1);
+ true ->
+ {{b, A, Mk, Mv, B1}, false}
+ end
+ end
+ end;
+erase_aux(K, {r, A, Xk, Xv, B}) ->
+ if K < Xk ->
+ {A1, Dec} = erase_aux(K, A),
+ if Dec ->
+ unbalright(r, A1, Xk, Xv, B);
+ true ->
+ {{r, A1, Xk, Xv, B}, false}
+ end;
+ K > Xk ->
+ {B1, Dec} = erase_aux(K, B),
+ if Dec ->
+ unballeft(r, A, Xk, Xv, B1);
+ true ->
+ {{r, A, Xk, Xv, B1}, false}
+ end;
+ true ->
+ case B of
+ empty ->
+ {A, false};
+ _ ->
+ {B1, {Mk, Mv}, Dec} = erase_min(B),
+ if Dec ->
+ unballeft(r, A, Mk, Mv, B1);
+ true ->
+ {{r, A, Mk, Mv, B1}, false}
+ end
+ end
+ end.
+
+-spec erase_min(dictionary(K, V)) ->
+ {dictionary(K, V), {ec_dictionary:key(K), ec_dictionary:value(V)}, boolean()}.
+erase_min({b, empty, Xk, Xv, empty}) ->
+ {empty, {Xk, Xv}, true};
+erase_min({b, empty, Xk, Xv, {r, A, Yk, Yv, B}}) ->
+ {{b, A, Yk, Yv, B}, {Xk, Xv}, false};
+erase_min({b, empty, _, _, {b, _, _, _, _}}) ->
+ exit(boom);
+erase_min({r, empty, Xk, Xv, A}) ->
+ {A, {Xk, Xv}, false};
+erase_min({b, A, Xk, Xv, B}) ->
+ {A1, Min, Dec} = erase_min(A),
+ if Dec ->
+ {T, Dec1} = unbalright(b, A1, Xk, Xv, B),
+ {T, Min, Dec1};
+ true -> {{b, A1, Xk, Xv, B}, Min, false}
+ end;
+erase_min({r, A, Xk, Xv, B}) ->
+ {A1, Min, Dec} = erase_min(A),
+ if Dec ->
+ {T, Dec1} = unbalright(r, A1, Xk, Xv, B),
+ {T, Min, Dec1};
+ true -> {{r, A1, Xk, Xv, B}, Min, false}
+ end.
+
+blackify({r, A, K, V, B}) -> {{b, A, K, V, B}, false};
+blackify(Node) -> {Node, true}.
+
+unballeft(r, {b, A, Xk, Xv, B}, Yk, Yv, C) ->
+ {lbalance(b, {r, A, Xk, Xv, B}, Yk, Yv, C), false};
+unballeft(b, {b, A, Xk, Xv, B}, Yk, Yv, C) ->
+ {lbalance(b, {r, A, Xk, Xv, B}, Yk, Yv, C), true};
+unballeft(b, {r, A, Xk, Xv, {b, B, Yk, Yv, C}}, Zk, Zv,
+ D) ->
+ {{b, A, Xk, Xv,
+ lbalance(b, {r, B, Yk, Yv, C}, Zk, Zv, D)},
+ false}.
+
+unbalright(r, A, Xk, Xv, {b, B, Yk, Yv, C}) ->
+ {rbalance(b, A, Xk, Xv, {r, B, Yk, Yv, C}), false};
+unbalright(b, A, Xk, Xv, {b, B, Yk, Yv, C}) ->
+ {rbalance(b, A, Xk, Xv, {r, B, Yk, Yv, C}), true};
+unbalright(b, A, Xk, Xv,
+ {r, {b, B, Yk, Yv, C}, Zk, Zv, D}) ->
+ {{b, rbalance(b, A, Xk, Xv, {r, B, Yk, Yv, C}), Zk, Zv,
+ D},
+ false}.
+
+-spec fold(fun((ec_dictionary:key(K), ec_dictionary:value(V), any()) -> any()),
+ any(), dictionary(K, V)) -> any().
+fold(_, Acc, empty) -> Acc;
+fold(F, Acc, {_, A, Xk, Xv, B}) ->
+ fold(F, F(Xk, Xv, fold(F, Acc, B)), A).
+
+add1(K, V, empty) -> {r, empty, K, V, empty};
+add1(K, V, {C, Left, K1, V1, Right}) when K < K1 ->
+ lbalance(C, add1(K, V, Left), K1, V1, Right);
+add1(K, V, {C, Left, K1, V1, Right}) when K > K1 ->
+ rbalance(C, Left, K1, V1, add1(K, V, Right));
+add1(K, V, {C, L, _, _, R}) -> {C, L, K, V, R}.
+
+size1(empty) -> 0;
+size1({_, L, _, _, R}) -> size1(L) + size1(R) + 1.
+
+to_list(empty, List) -> List;
+to_list({_, A, Xk, Xv, B}, List) ->
+ to_list(A, [{Xk, Xv} | to_list(B, List)]).
+
+%% Balance a tree after (possibly) adding a node to the left/right.
+-spec lbalance(color(), dictionary(K, V),
+ ec_dictionary:key(K), ec_dictionary:value(V),
+ dictionary(K, V)) ->
+ dictionary(K, V).
+lbalance(b, {r, {r, A, Xk, Xv, B}, Yk, Yv, C}, Zk, Zv,
+ D) ->
+ {r, {b, A, Xk, Xv, B}, Yk, Yv, {b, C, Zk, Zv, D}};
+lbalance(b, {r, A, Xk, Xv, {r, B, Yk, Yv, C}}, Zk, Zv,
+ D) ->
+ {r, {b, A, Xk, Xv, B}, Yk, Yv, {b, C, Zk, Zv, D}};
+lbalance(C, A, Xk, Xv, B) -> {C, A, Xk, Xv, B}.
+
+-spec rbalance(color(), dictionary(K, V),
+ ec_dictionary:key(K), ec_dictionary:value(V),
+ dictionary(K, V)) ->
+ dictionary(K, V).
+rbalance(b, A, Xk, Xv,
+ {r, {r, B, Yk, Yv, C}, Zk, Zv, D}) ->
+ {r, {b, A, Xk, Xv, B}, Yk, Yv, {b, C, Zk, Zv, D}};
+rbalance(b, A, Xk, Xv,
+ {r, B, Yk, Yv, {r, C, Zk, Zv, D}}) ->
+ {r, {b, A, Xk, Xv, B}, Yk, Yv, {b, C, Zk, Zv, D}};
+rbalance(C, A, Xk, Xv, B) -> {C, A, Xk, Xv, B}.
diff --git a/src/ec_semver.erl b/src/ec_semver.erl
index 77b43cd..3ffd591 100644
--- a/src/ec_semver.erl
+++ b/src/ec_semver.erl
@@ -1,3 +1,4 @@
+%%% vi:ts=4 sw=4 et
%%%-------------------------------------------------------------------
%%% @copyright (C) 2011, Erlware LLC
%%% @doc
@@ -7,113 +8,304 @@
%%%-------------------------------------------------------------------
-module(ec_semver).
--exports([
- compare/2
- ]).
+-export([parse/1,
+ format/1,
+ eql/2,
+ gt/2,
+ gte/2,
+ lt/2,
+ lte/2,
+ pes/2,
+ between/3]).
--export_type([
- semvar/0
- ]).
+%% For internal use by the ec_semver_parser peg
+-export([internal_parse_version/1]).
+
+-export_type([semver/0,
+ version_string/0,
+ any_version/0]).
%%%===================================================================
%%% Public Types
%%%===================================================================
--type semvar() :: string().
--type parsed_semvar() :: {MajorVsn::string(),
- MinorVsn::string(),
- PatchVsn::string(),
- PathString::string()}.
+-type version_element() :: non_neg_integer() | binary().
+
+-type major_minor_patch_minpatch() ::
+ version_element()
+ | {version_element(), version_element()}
+ | {version_element(), version_element(), version_element()}
+ | {version_element(), version_element(),
+ version_element(), version_element()}.
+
+-type alpha_part() :: integer() | binary() | string().
+-type alpha_info() :: {PreRelease::[alpha_part()],
+ BuildVersion::[alpha_part()]}.
+
+-type semver() :: {major_minor_patch_minpatch(), alpha_info()}.
+
+-type version_string() :: string() | binary().
+
+-type any_version() :: version_string() | semver().
%%%===================================================================
%%% API
%%%===================================================================
-%% @doc Is semver version string A bigger than version string B?
-%%
-%% Example: compare("3.2.5alpha", "3.10.6") returns: false
-%%
--spec compare(VsnA::string(), VsnB::string()) -> boolean().
-compare(VsnA, VsnB) ->
- compare_toks(tokens(VsnA),tokens(VsnB)).
+%% @doc parse a string or binary into a valid semver representation
+-spec parse(any_version()) -> semver().
+parse(Version) when erlang:is_list(Version) ->
+ case ec_semver_parser:parse(Version) of
+ {fail, _} ->
+ {erlang:iolist_to_binary(Version), {[],[]}};
+ Good ->
+ Good
+ end;
+parse(Version) when erlang:is_binary(Version) ->
+ case ec_semver_parser:parse(Version) of
+ {fail, _} ->
+ {Version, {[],[]}};
+ Good ->
+ Good
+ end;
+parse(Version) ->
+ Version.
+
+-spec format(semver()) -> iolist().
+format({Maj, {AlphaPart, BuildPart}})
+ when erlang:is_integer(Maj);
+ erlang:is_binary(Maj) ->
+ [format_version_part(Maj),
+ format_vsn_rest(<<"-">>, AlphaPart),
+ format_vsn_rest(<<"+">>, BuildPart)];
+format({{Maj, Min}, {AlphaPart, BuildPart}}) ->
+ [format_version_part(Maj), ".",
+ format_version_part(Min),
+ format_vsn_rest(<<"-">>, AlphaPart),
+ format_vsn_rest(<<"+">>, BuildPart)];
+format({{Maj, Min, Patch}, {AlphaPart, BuildPart}}) ->
+ [format_version_part(Maj), ".",
+ format_version_part(Min), ".",
+ format_version_part(Patch),
+ format_vsn_rest(<<"-">>, AlphaPart),
+ format_vsn_rest(<<"+">>, BuildPart)];
+format({{Maj, Min, Patch, MinPatch}, {AlphaPart, BuildPart}}) ->
+ [format_version_part(Maj), ".",
+ format_version_part(Min), ".",
+ format_version_part(Patch), ".",
+ format_version_part(MinPatch),
+ format_vsn_rest(<<"-">>, AlphaPart),
+ format_vsn_rest(<<"+">>, BuildPart)].
+
+-spec format_version_part(integer() | binary()) -> iolist().
+format_version_part(Vsn)
+ when erlang:is_integer(Vsn) ->
+ erlang:integer_to_list(Vsn);
+format_version_part(Vsn)
+ when erlang:is_binary(Vsn) ->
+ Vsn.
+
+
+
+%% @doc test for quality between semver versions
+-spec eql(any_version(), any_version()) -> boolean().
+eql(VsnA, VsnB) ->
+ NVsnA = normalize(parse(VsnA)),
+ NVsnB = normalize(parse(VsnB)),
+ NVsnA =:= NVsnB.
+
+%% @doc Test that VsnA is greater than VsnB
+-spec gt(any_version(), any_version()) -> boolean().
+gt(VsnA, VsnB) ->
+ {MMPA, {AlphaA, PatchA}} = normalize(parse(VsnA)),
+ {MMPB, {AlphaB, PatchB}} = normalize(parse(VsnB)),
+ ((MMPA > MMPB)
+ orelse
+ ((MMPA =:= MMPB)
+ andalso
+ ((AlphaA =:= [] andalso AlphaB =/= [])
+ orelse
+ ((not (AlphaB =:= [] andalso AlphaA =/= []))
+ andalso
+ (AlphaA > AlphaB))))
+ orelse
+ ((MMPA =:= MMPB)
+ andalso
+ (AlphaA =:= AlphaB)
+ andalso
+ ((PatchB =:= [] andalso PatchA =/= [])
+ orelse
+ PatchA > PatchB))).
+
+%% @doc Test that VsnA is greater than or equal to VsnB
+-spec gte(any_version(), any_version()) -> boolean().
+gte(VsnA, VsnB) ->
+ NVsnA = normalize(parse(VsnA)),
+ NVsnB = normalize(parse(VsnB)),
+ gt(NVsnA, NVsnB) orelse eql(NVsnA, NVsnB).
+
+%% @doc Test that VsnA is less than VsnB
+-spec lt(any_version(), any_version()) -> boolean().
+lt(VsnA, VsnB) ->
+ {MMPA, {AlphaA, PatchA}} = normalize(parse(VsnA)),
+ {MMPB, {AlphaB, PatchB}} = normalize(parse(VsnB)),
+ ((MMPA < MMPB)
+ orelse
+ ((MMPA =:= MMPB)
+ andalso
+ ((AlphaB =:= [] andalso AlphaA =/= [])
+ orelse
+ ((not (AlphaA =:= [] andalso AlphaB =/= []))
+ andalso
+ (AlphaA < AlphaB))))
+ orelse
+ ((MMPA =:= MMPB)
+ andalso
+ (AlphaA =:= AlphaB)
+ andalso
+ ((PatchA =:= [] andalso PatchB =/= [])
+ orelse
+ PatchA < PatchB))).
+
+%% @doc Test that VsnA is less than or equal to VsnB
+-spec lte(any_version(), any_version()) -> boolean().
+lte(VsnA, VsnB) ->
+ NVsnA = normalize(parse(VsnA)),
+ NVsnB = normalize(parse(VsnB)),
+ lt(NVsnA, NVsnB) orelse eql(NVsnA, NVsnB).
+
+%% @doc Test that VsnMatch is greater than or equal to Vsn1 and
+%% less than or equal to Vsn2
+-spec between(any_version(), any_version(), any_version()) -> boolean().
+between(Vsn1, Vsn2, VsnMatch) ->
+ NVsnA = normalize(parse(Vsn1)),
+ NVsnB = normalize(parse(Vsn2)),
+ NVsnMatch = normalize(parse(VsnMatch)),
+ gte(NVsnMatch, NVsnA) andalso
+ lte(NVsnMatch, NVsnB).
+
+%% @doc check that VsnA is Approximately greater than VsnB
+%%
+%% Specifying ">= 2.6.5" is an optimistic version constraint. All
+%% versions greater than the one specified, including major releases
+%% (e.g. 3.0.0) are allowed.
+%%
+%% Conversely, specifying "~> 2.6" is pessimistic about future major
+%% revisions and "~> 2.6.5" is pessimistic about future minor
+%% revisions.
+%%
+%% "~> 2.6" matches cookbooks >= 2.6.0 AND < 3.0.0
+%% "~> 2.6.5" matches cookbooks >= 2.6.5 AND < 2.7.0
+pes(VsnA, VsnB) ->
+ internal_pes(parse(VsnA), parse(VsnB)).
+
+%%%===================================================================
+%%% Friend Functions
+%%%===================================================================
+%% @doc helper function for the peg grammar to parse the iolist into a semver
+-spec internal_parse_version(iolist()) -> semver().
+internal_parse_version([MMP, AlphaPart, BuildPart, _]) ->
+ {parse_major_minor_patch_minpatch(MMP), {parse_alpha_part(AlphaPart),
+ parse_alpha_part(BuildPart)}}.
+
+%% @doc helper function for the peg grammar to parse the iolist into a major_minor_patch
+-spec parse_major_minor_patch_minpatch(iolist()) -> major_minor_patch_minpatch().
+parse_major_minor_patch_minpatch([MajVsn, [], [], []]) ->
+ strip_maj_version(MajVsn);
+parse_major_minor_patch_minpatch([MajVsn, [<<".">>, MinVsn], [], []]) ->
+ {strip_maj_version(MajVsn), MinVsn};
+parse_major_minor_patch_minpatch([MajVsn,
+ [<<".">>, MinVsn],
+ [<<".">>, PatchVsn], []]) ->
+ {strip_maj_version(MajVsn), MinVsn, PatchVsn};
+parse_major_minor_patch_minpatch([MajVsn,
+ [<<".">>, MinVsn],
+ [<<".">>, PatchVsn],
+ [<<".">>, MinPatch]]) ->
+ {strip_maj_version(MajVsn), MinVsn, PatchVsn, MinPatch}.
+
+%% @doc helper function for the peg grammar to parse the iolist into an alpha part
+-spec parse_alpha_part(iolist()) -> [alpha_part()].
+parse_alpha_part([]) ->
+ [];
+parse_alpha_part([_, AV1, Rest]) ->
+ [erlang:iolist_to_binary(AV1) |
+ [format_alpha_part(Part) || Part <- Rest]].
+
+%% @doc according to semver alpha parts that can be treated like
+%% numbers must be. We implement that here by taking the alpha part
+%% and trying to convert it to a number, if it succeeds we use
+%% it. Otherwise we do not.
+-spec format_alpha_part(iolist()) -> integer() | binary().
+format_alpha_part([<<".">>, AlphaPart]) ->
+ Bin = erlang:iolist_to_binary(AlphaPart),
+ try
+ erlang:list_to_integer(erlang:binary_to_list(Bin))
+ catch
+ error:badarg ->
+ Bin
+ end.
%%%===================================================================
%%% Internal Functions
%%%===================================================================
+-spec strip_maj_version(iolist()) -> version_element().
+strip_maj_version([<<"v">>, MajVsn]) ->
+ MajVsn;
+strip_maj_version([[], MajVsn]) ->
+ MajVsn;
+strip_maj_version(MajVsn) ->
+ MajVsn.
--spec tokens(semvar()) -> parsed_semvar().
-tokens(Vsn) ->
- [MajorVsn, MinorVsn, RawPatch] = string:tokens(Vsn, "."),
- {PatchVsn, PatchString} = split_patch(RawPatch),
- {MajorVsn, MinorVsn, PatchVsn, PatchString}.
+-spec to_list(integer() | binary() | string()) -> string() | binary().
+to_list(Detail) when erlang:is_integer(Detail) ->
+ erlang:integer_to_list(Detail);
+to_list(Detail) when erlang:is_list(Detail); erlang:is_binary(Detail) ->
+ Detail.
--spec split_patch(string()) ->
- {PatchVsn::string(), PatchStr::string()}.
-split_patch(RawPatch) ->
- {PatchVsn, PatchStr} = split_patch(RawPatch, {"", ""}),
- {lists:reverse(PatchVsn), PatchStr}.
+-spec format_vsn_rest(binary() | string(), [integer() | binary()]) -> iolist().
+format_vsn_rest(_TypeMark, []) ->
+ [];
+format_vsn_rest(TypeMark, [Head | Rest]) ->
+ [TypeMark, Head |
+ [[".", to_list(Detail)] || Detail <- Rest]].
--spec split_patch(string(), {AccPatchVsn::string(), AccPatchStr::string()}) ->
- {PatchVsn::string(), PatchStr::string()}.
-split_patch([], Acc) ->
- Acc;
-split_patch([Dig|T], {PatchVsn, PatchStr}) when Dig >= $0 andalso Dig =< $9 ->
- split_patch(T, {[Dig|PatchVsn], PatchStr});
-split_patch(PatchStr, {PatchVsn, ""}) ->
- {PatchVsn, PatchStr}.
+%% @doc normalize the semver so they can be compared
+-spec normalize(semver()) -> semver().
+normalize({Vsn, Rest})
+ when erlang:is_binary(Vsn);
+ erlang:is_integer(Vsn) ->
+ {{Vsn, 0, 0, 0}, Rest};
+normalize({{Maj, Min}, Rest}) ->
+ {{Maj, Min, 0, 0}, Rest};
+normalize({{Maj, Min, Patch}, Rest}) ->
+ {{Maj, Min, Patch, 0}, Rest};
+normalize(Other = {{_, _, _, _}, {_,_}}) ->
+ Other.
--spec compare_toks(parsed_semvar(), parsed_semvar()) -> boolean().
-compare_toks({MajA, MinA, PVA, PSA}, {MajB, MinB, PVB, PSB}) ->
- compare_toks2({to_int(MajA), to_int(MinA), to_int(PVA), PSA},
- {to_int(MajB), to_int(MinB), to_int(PVB), PSB}).
-
--spec compare_toks2(parsed_semvar(), parsed_semvar()) -> boolean().
-compare_toks2({MajA, _MinA, _PVA, _PSA}, {MajB, _MinB, _PVB, _PSB})
- when MajA > MajB ->
- true;
-compare_toks2({_Maj, MinA, _PVA, _PSA}, {_Maj, MinB, _PVB, _PSB})
- when MinA > MinB ->
- true;
-compare_toks2({_Maj, _Min, PVA, _PSA}, {_Maj, _Min, PVB, _PSB})
- when PVA > PVB ->
- true;
-compare_toks2({_Maj, _Min, _PV, ""}, {_Maj, _Min, _PV, PSB}) when PSB /= ""->
- true;
-compare_toks2({_Maj, _Min, _PV, PSA}, {_Maj, _Min, _PV, ""}) when PSA /= ""->
- false;
-compare_toks2({_Maj, _Min, _PV, PSA}, {_Maj, _Min, _PV, PSB}) when PSA > PSB ->
- true;
-compare_toks2(_ToksA, _ToksB) ->
- false.
-
--spec to_int(string()) -> integer().
-to_int(String) ->
- try
- list_to_integer(String)
- catch
- error:badarg ->
- throw(invalid_semver_string)
- end.
-
-%%%===================================================================
-%%% Test Functions
-%%%===================================================================
-
--ifndef(NOTEST).
--include_lib("eunit/include/eunit.hrl").
-
-split_patch_test() ->
- ?assertMatch({"123", "alpha1"}, split_patch("123alpha1")).
-
-compare_test() ->
- ?assertMatch(true, compare("1.2.3", "1.2.3alpha")),
- ?assertMatch(true, compare("1.2.3beta", "1.2.3alpha")),
- ?assertMatch(true, compare("1.2.4", "1.2.3")),
- ?assertMatch(true, compare("1.3.3", "1.2.3")),
- ?assertMatch(true, compare("2.2.3", "1.2.3")),
- ?assertMatch(true, compare("4.2.3", "3.10.3")),
- ?assertMatch(false, compare("1.2.3", "2.2.3")),
- ?assertThrow(invalid_semver_string, compare("1.b.2", "1.3.4")),
- ?assertThrow(invalid_semver_string, compare("1.2.2", "1.3.t")).
-
--endif.
+%% @doc to do the pessimistic compare we need a parsed semver. This is
+%% the internal implementation of the of the pessimistic run. The
+%% external just ensures that versions are parsed.
+-spec internal_pes(semver(), semver()) -> boolean().
+internal_pes(VsnA, {{LM, LMI}, Alpha})
+ when erlang:is_integer(LM),
+ erlang:is_integer(LMI) ->
+ gte(VsnA, {{LM, LMI, 0}, Alpha}) andalso
+ lt(VsnA, {{LM + 1, 0, 0, 0}, {[], []}});
+internal_pes(VsnA, {{LM, LMI, LP}, Alpha})
+ when erlang:is_integer(LM),
+ erlang:is_integer(LMI),
+ erlang:is_integer(LP) ->
+ gte(VsnA, {{LM, LMI, LP}, Alpha})
+ andalso
+ lt(VsnA, {{LM, LMI + 1, 0, 0}, {[], []}});
+internal_pes(VsnA, {{LM, LMI, LP, LMP}, Alpha})
+ when erlang:is_integer(LM),
+ erlang:is_integer(LMI),
+ erlang:is_integer(LP),
+ erlang:is_integer(LMP) ->
+ gte(VsnA, {{LM, LMI, LP, LMP}, Alpha})
+ andalso
+ lt(VsnA, {{LM, LMI, LP + 1, 0}, {[], []}});
+internal_pes(Vsn, LVsn) ->
+ gte(Vsn, LVsn).
diff --git a/src/ec_semver_parser.erl b/src/ec_semver_parser.erl
new file mode 100644
index 0000000..c2fe186
--- /dev/null
+++ b/src/ec_semver_parser.erl
@@ -0,0 +1,302 @@
+-module(ec_semver_parser).
+-export([parse/1,file/1]).
+-define(p_anything,true).
+-define(p_charclass,true).
+-define(p_choose,true).
+-define(p_not,true).
+-define(p_one_or_more,true).
+-define(p_optional,true).
+-define(p_scan,true).
+-define(p_seq,true).
+-define(p_string,true).
+-define(p_zero_or_more,true).
+
+
+
+-spec file(file:name()) -> any().
+file(Filename) -> case file:read_file(Filename) of {ok,Bin} -> parse(Bin); Err -> Err end.
+
+-spec parse(binary() | list()) -> any().
+parse(List) when is_list(List) -> parse(unicode:characters_to_binary(List));
+parse(Input) when is_binary(Input) ->
+ _ = setup_memo(),
+ Result = case 'semver'(Input,{{line,1},{column,1}}) of
+ {AST, <<>>, _Index} -> AST;
+ Any -> Any
+ end,
+ release_memo(), Result.
+
+-spec 'semver'(input(), index()) -> parse_result().
+'semver'(Input, Index) ->
+ p(Input, Index, 'semver', fun(I,D) -> (p_seq([fun 'major_minor_patch_min_patch'/2, p_optional(p_seq([p_string(<<"-">>), fun 'alpha_part'/2, p_zero_or_more(p_seq([p_string(<<".">>), fun 'alpha_part'/2]))])), p_optional(p_seq([p_string(<<"+">>), fun 'alpha_part'/2, p_zero_or_more(p_seq([p_string(<<".">>), fun 'alpha_part'/2]))])), p_not(p_anything())]))(I,D) end, fun(Node, _Idx) -> ec_semver:internal_parse_version(Node) end).
+
+-spec 'major_minor_patch_min_patch'(input(), index()) -> parse_result().
+'major_minor_patch_min_patch'(Input, Index) ->
+ p(Input, Index, 'major_minor_patch_min_patch', fun(I,D) -> (p_seq([p_choose([p_seq([p_optional(p_string(<<"v">>)), fun 'numeric_part'/2]), fun 'alpha_part'/2]), p_optional(p_seq([p_string(<<".">>), fun 'version_part'/2])), p_optional(p_seq([p_string(<<".">>), fun 'version_part'/2])), p_optional(p_seq([p_string(<<".">>), fun 'version_part'/2]))]))(I,D) end, fun(Node, Idx) ->transform('major_minor_patch_min_patch', Node, Idx) end).
+
+-spec 'version_part'(input(), index()) -> parse_result().
+'version_part'(Input, Index) ->
+ p(Input, Index, 'version_part', fun(I,D) -> (p_choose([fun 'numeric_part'/2, fun 'alpha_part'/2]))(I,D) end, fun(Node, Idx) ->transform('version_part', Node, Idx) end).
+
+-spec 'numeric_part'(input(), index()) -> parse_result().
+'numeric_part'(Input, Index) ->
+ p(Input, Index, 'numeric_part', fun(I,D) -> (p_one_or_more(p_charclass(<<"[0-9]">>)))(I,D) end, fun(Node, _Idx) ->erlang:list_to_integer(erlang:binary_to_list(erlang:iolist_to_binary(Node))) end).
+
+-spec 'alpha_part'(input(), index()) -> parse_result().
+'alpha_part'(Input, Index) ->
+ p(Input, Index, 'alpha_part', fun(I,D) -> (p_one_or_more(p_charclass(<<"[A-Za-z0-9-]">>)))(I,D) end, fun(Node, _Idx) ->erlang:iolist_to_binary(Node) end).
+
+
+transform(_,Node,_Index) -> Node.
+-type index() :: {{line, pos_integer()}, {column, pos_integer()}}.
+-type input() :: binary().
+-type parse_failure() :: {fail, term()}.
+-type parse_success() :: {term(), input(), index()}.
+-type parse_result() :: parse_failure() | parse_success().
+-type parse_fun() :: fun((input(), index()) -> parse_result()).
+-type xform_fun() :: fun((input(), index()) -> term()).
+
+-spec p(input(), index(), atom(), parse_fun(), xform_fun()) -> parse_result().
+p(Inp, StartIndex, Name, ParseFun, TransformFun) ->
+ case get_memo(StartIndex, Name) of % See if the current reduction is memoized
+ {ok, Memo} -> %Memo; % If it is, return the stored result
+ Memo;
+ _ -> % If not, attempt to parse
+ Result = case ParseFun(Inp, StartIndex) of
+ {fail,_} = Failure -> % If it fails, memoize the failure
+ Failure;
+ {Match, InpRem, NewIndex} -> % If it passes, transform and memoize the result.
+ Transformed = TransformFun(Match, StartIndex),
+ {Transformed, InpRem, NewIndex}
+ end,
+ memoize(StartIndex, Name, Result),
+ Result
+ end.
+
+-spec setup_memo() -> ets:tid().
+setup_memo() ->
+ put({parse_memo_table, ?MODULE}, ets:new(?MODULE, [set])).
+
+-spec release_memo() -> true.
+release_memo() ->
+ ets:delete(memo_table_name()).
+
+-spec memoize(index(), atom(), parse_result()) -> true.
+memoize(Index, Name, Result) ->
+ Memo = case ets:lookup(memo_table_name(), Index) of
+ [] -> [];
+ [{Index, Plist}] -> Plist
+ end,
+ ets:insert(memo_table_name(), {Index, [{Name, Result}|Memo]}).
+
+-spec get_memo(index(), atom()) -> {ok, term()} | {error, not_found}.
+get_memo(Index, Name) ->
+ case ets:lookup(memo_table_name(), Index) of
+ [] -> {error, not_found};
+ [{Index, Plist}] ->
+ case proplists:lookup(Name, Plist) of
+ {Name, Result} -> {ok, Result};
+ _ -> {error, not_found}
+ end
+ end.
+
+-spec memo_table_name() -> ets:tid().
+memo_table_name() ->
+ get({parse_memo_table, ?MODULE}).
+
+-ifdef(p_eof).
+-spec p_eof() -> parse_fun().
+p_eof() ->
+ fun(<<>>, Index) -> {eof, [], Index};
+ (_, Index) -> {fail, {expected, eof, Index}} end.
+-endif.
+
+-ifdef(p_optional).
+-spec p_optional(parse_fun()) -> parse_fun().
+p_optional(P) ->
+ fun(Input, Index) ->
+ case P(Input, Index) of
+ {fail,_} -> {[], Input, Index};
+ {_, _, _} = Success -> Success
+ end
+ end.
+-endif.
+
+-ifdef(p_not).
+-spec p_not(parse_fun()) -> parse_fun().
+p_not(P) ->
+ fun(Input, Index)->
+ case P(Input,Index) of
+ {fail,_} ->
+ {[], Input, Index};
+ {Result, _, _} -> {fail, {expected, {no_match, Result},Index}}
+ end
+ end.
+-endif.
+
+-ifdef(p_assert).
+-spec p_assert(parse_fun()) -> parse_fun().
+p_assert(P) ->
+ fun(Input,Index) ->
+ case P(Input,Index) of
+ {fail,_} = Failure-> Failure;
+ _ -> {[], Input, Index}
+ end
+ end.
+-endif.
+
+-ifdef(p_seq).
+-spec p_seq([parse_fun()]) -> parse_fun().
+p_seq(P) ->
+ fun(Input, Index) ->
+ p_all(P, Input, Index, [])
+ end.
+
+-spec p_all([parse_fun()], input(), index(), [term()]) -> parse_result().
+p_all([], Inp, Index, Accum ) -> {lists:reverse( Accum ), Inp, Index};
+p_all([P|Parsers], Inp, Index, Accum) ->
+ case P(Inp, Index) of
+ {fail, _} = Failure -> Failure;
+ {Result, InpRem, NewIndex} -> p_all(Parsers, InpRem, NewIndex, [Result|Accum])
+ end.
+-endif.
+
+-ifdef(p_choose).
+-spec p_choose([parse_fun()]) -> parse_fun().
+p_choose(Parsers) ->
+ fun(Input, Index) ->
+ p_attempt(Parsers, Input, Index, none)
+ end.
+
+-spec p_attempt([parse_fun()], input(), index(), none | parse_failure()) -> parse_result().
+p_attempt([], _Input, _Index, Failure) -> Failure;
+p_attempt([P|Parsers], Input, Index, FirstFailure)->
+ case P(Input, Index) of
+ {fail, _} = Failure ->
+ case FirstFailure of
+ none -> p_attempt(Parsers, Input, Index, Failure);
+ _ -> p_attempt(Parsers, Input, Index, FirstFailure)
+ end;
+ Result -> Result
+ end.
+-endif.
+
+-ifdef(p_zero_or_more).
+-spec p_zero_or_more(parse_fun()) -> parse_fun().
+p_zero_or_more(P) ->
+ fun(Input, Index) ->
+ p_scan(P, Input, Index, [])
+ end.
+-endif.
+
+-ifdef(p_one_or_more).
+-spec p_one_or_more(parse_fun()) -> parse_fun().
+p_one_or_more(P) ->
+ fun(Input, Index)->
+ Result = p_scan(P, Input, Index, []),
+ case Result of
+ {[_|_], _, _} ->
+ Result;
+ _ ->
+ {fail, {expected, Failure, _}} = P(Input,Index),
+ {fail, {expected, {at_least_one, Failure}, Index}}
+ end
+ end.
+-endif.
+
+-ifdef(p_label).
+-spec p_label(atom(), parse_fun()) -> parse_fun().
+p_label(Tag, P) ->
+ fun(Input, Index) ->
+ case P(Input, Index) of
+ {fail,_} = Failure ->
+ Failure;
+ {Result, InpRem, NewIndex} ->
+ {{Tag, Result}, InpRem, NewIndex}
+ end
+ end.
+-endif.
+
+-ifdef(p_scan).
+-spec p_scan(parse_fun(), input(), index(), [term()]) -> {[term()], input(), index()}.
+p_scan(_, <<>>, Index, Accum) -> {lists:reverse(Accum), <<>>, Index};
+p_scan(P, Inp, Index, Accum) ->
+ case P(Inp, Index) of
+ {fail,_} -> {lists:reverse(Accum), Inp, Index};
+ {Result, InpRem, NewIndex} -> p_scan(P, InpRem, NewIndex, [Result | Accum])
+ end.
+-endif.
+
+-ifdef(p_string).
+-spec p_string(binary()) -> parse_fun().
+p_string(S) ->
+ Length = erlang:byte_size(S),
+ fun(Input, Index) ->
+ try
+ <> = Input,
+ {S, Rest, p_advance_index(S, Index)}
+ catch
+ error:{badmatch,_} -> {fail, {expected, {string, S}, Index}}
+ end
+ end.
+-endif.
+
+-ifdef(p_anything).
+-spec p_anything() -> parse_fun().
+p_anything() ->
+ fun(<<>>, Index) -> {fail, {expected, any_character, Index}};
+ (Input, Index) when is_binary(Input) ->
+ <> = Input,
+ {<>, Rest, p_advance_index(<>, Index)}
+ end.
+-endif.
+
+-ifdef(p_charclass).
+-spec p_charclass(string() | binary()) -> parse_fun().
+p_charclass(Class) ->
+ {ok, RE} = re:compile(Class, [unicode, dotall]),
+ fun(Inp, Index) ->
+ case re:run(Inp, RE, [anchored]) of
+ {match, [{0, Length}|_]} ->
+ {Head, Tail} = erlang:split_binary(Inp, Length),
+ {Head, Tail, p_advance_index(Head, Index)};
+ _ -> {fail, {expected, {character_class, binary_to_list(Class)}, Index}}
+ end
+ end.
+-endif.
+
+-ifdef(p_regexp).
+-spec p_regexp(binary()) -> parse_fun().
+p_regexp(Regexp) ->
+ {ok, RE} = re:compile(Regexp, [unicode, dotall, anchored]),
+ fun(Inp, Index) ->
+ case re:run(Inp, RE) of
+ {match, [{0, Length}|_]} ->
+ {Head, Tail} = erlang:split_binary(Inp, Length),
+ {Head, Tail, p_advance_index(Head, Index)};
+ _ -> {fail, {expected, {regexp, binary_to_list(Regexp)}, Index}}
+ end
+ end.
+-endif.
+
+-ifdef(line).
+-spec line(index() | term()) -> pos_integer() | undefined.
+line({{line,L},_}) -> L;
+line(_) -> undefined.
+-endif.
+
+-ifdef(column).
+-spec column(index() | term()) -> pos_integer() | undefined.
+column({_,{column,C}}) -> C;
+column(_) -> undefined.
+-endif.
+
+-spec p_advance_index(input() | unicode:charlist() | pos_integer(), index()) -> index().
+p_advance_index(MatchedInput, Index) when is_list(MatchedInput) orelse is_binary(MatchedInput)-> % strings
+ lists:foldl(fun p_advance_index/2, Index, unicode:characters_to_list(MatchedInput));
+p_advance_index(MatchedInput, Index) when is_integer(MatchedInput) -> % single characters
+ {{line, Line}, {column, Col}} = Index,
+ case MatchedInput of
+ $\n -> {{line, Line+1}, {column, 1}};
+ _ -> {{line, Line}, {column, Col+1}}
+ end.
diff --git a/src/ec_string.erl b/src/ec_string.erl
deleted file mode 100644
index 2a06257..0000000
--- a/src/ec_string.erl
+++ /dev/null
@@ -1,128 +0,0 @@
-%%%-------------------------------------------------------------------
-%%% @copyright (C) 2011, Erlware LLC
-%%% @doc
-%%% Helper functions for working with strings.
-%%% @end
-%%%-------------------------------------------------------------------
--module(ec_string).
-
--export([
- compare_versions/2
- ]).
-%%%===================================================================
-%%% API
-%%%===================================================================
-
-%% @doc Is arbitrary version string A bigger than version string B?
-%% Valid version string elements are either separated by . or - or both.
-%% Final version string elements may have a numeric followed directly by an
-%% alpha numeric and will be compared separately as in 12alpha.
-%%
-%%
-%% Example: compare_versions("3-2-5-alpha", "3.10.6") will return false
-%% compare_versions("3-2-alpha", "3.2.1-alpha") will return false
-%% compare_versions("3-2alpha", "3.2.1-alpha") will return false
-%% compare_versions("3.2.2", "3.2.2") will return false
-%% compare_versions("3.2.1", "3.2.1-rc2") will return true
-%% compare_versions("3.2.2", "3.2.1") will return true
-%%
--spec compare_versions(VsnA::string(), VsnB::string()) -> boolean().
-compare_versions(VsnA, VsnB) ->
- compare(string:tokens(VsnA, ".-"),string:tokens(VsnB, ".-")).
-
-%%%===================================================================
-%%% Internal Functions
-%%%===================================================================
-
--spec compare(string(), string()) -> boolean().
-compare([Str|TA], [Str|TB]) ->
- compare(TA, TB);
-compare([StrA|TA], [StrB|TB]) ->
- fine_compare(split_numeric_alpha(StrA), TA,
- split_numeric_alpha(StrB), TB);
-compare([], [Str]) ->
- not compare_against_nothing(Str);
-compare([Str], []) ->
- compare_against_nothing(Str);
-compare([], [_,_|_]) ->
- false;
-compare([_,_|_], []) ->
- true;
-compare([], []) ->
- false.
-
--spec compare_against_nothing(string()) -> boolean().
-compare_against_nothing(Str) ->
- case split_numeric_alpha(Str) of
- {_StrDig, ""} -> true;
- {"", _StrAlpha} -> false;
- {_StrDig, _StrAlpha} -> true
- end.
-
--spec fine_compare({string(), string()}, string(),
- {string(), string()}, string()) ->
- boolean().
-fine_compare({_StrDigA, StrA}, TA, {_StrDigB, _StrB}, _TB)
- when StrA /= "", TA /= [] ->
- throw(invalid_version_string);
-fine_compare({_StrDigA, _StrA}, _TA, {_StrDigB, StrB}, TB)
- when StrB /= "", TB /= [] ->
- throw(invalid_version_string);
-fine_compare({"", _StrA}, _TA, {StrDigB, _StrB}, _TB) when StrDigB /= "" ->
- false;
-fine_compare({StrDigA, _StrA}, _TA, {"", _StrB}, _TB) when StrDigA /= "" ->
- true;
-fine_compare({StrDig, ""}, _TA, {StrDig, StrB}, _TB) when StrB /= "" ->
- true;
-fine_compare({StrDig, StrA}, _TA, {StrDig, ""}, _TB) when StrA /= "" ->
- false;
-fine_compare({StrDig, StrA}, _TA, {StrDig, StrB}, _TB) ->
- StrA > StrB;
-fine_compare({StrDigA, _StrA}, _TA, {StrDigB, _StrB}, _TB) ->
- list_to_integer(StrDigA) > list_to_integer(StrDigB).
-
-%% In the case of a version sub part with a numeric then an alpha,
-%% split out the numeric and alpha "24alpha" becomes {"24", "alpha"}
--spec split_numeric_alpha(string()) ->
- {PatchVsn::string(), PatchStr::string()}.
-split_numeric_alpha(RawVsn) ->
- {Num, Str} = split_numeric_alpha(RawVsn, {"", ""}),
- {lists:reverse(Num), Str}.
-
--spec split_numeric_alpha(string(), {PatchVsnAcc::string(),
- PatchStrAcc::string()}) ->
- {PatchVsn::string(), PatchStr::string()}.
-split_numeric_alpha([], Acc) ->
- Acc;
-split_numeric_alpha([Dig|T], {PatchVsn, PatchStr})
- when Dig >= $0 andalso Dig =< $9 ->
- split_numeric_alpha(T, {[Dig|PatchVsn], PatchStr});
-split_numeric_alpha(PatchStr, {PatchVsn, ""}) ->
- {PatchVsn, PatchStr}.
-
-%%%===================================================================
-%%% Test Functions
-%%%===================================================================
-
--ifndef(NOTEST).
--include_lib("eunit/include/eunit.hrl").
-
-split_numeric_alpha_test() ->
- ?assertMatch({"123", "alpha1"}, split_numeric_alpha("123alpha1")).
-
-compare_versions_test() ->
- ?assertMatch(true, compare_versions("1.2.3", "1.2.3alpha")),
- ?assertMatch(true, compare_versions("1.2.3-beta", "1.2.3-alpha")),
- ?assertMatch(true, compare_versions("1-2-3", "1-2-3alpha")),
- ?assertMatch(true, compare_versions("1-2-3", "1-2-3-rc3")),
- ?assertMatch(true, compare_versions("1.2.3beta", "1.2.3alpha")),
- ?assertMatch(true, compare_versions("1.2.4", "1.2.3")),
- ?assertMatch(true, compare_versions("1.3.3", "1.2.3")),
- ?assertMatch(true, compare_versions("2.2.3", "1.2.3")),
- ?assertMatch(true, compare_versions("4.2.3", "3.10.3")),
- ?assertMatch(false, compare_versions("1.2.3", "2.2.3")),
- ?assertMatch(false, compare_versions("1.2.2", "1.3.t")),
- ?assertMatch(false, compare_versions("1.2t", "1.3.t")),
- ?assertThrow(invalid_version_string, compare_versions("1.b.2", "1.3.4")).
-
--endif.
diff --git a/src/ec_talk.erl b/src/ec_talk.erl
index 0823169..8c3a105 100644
--- a/src/ec_talk.erl
+++ b/src/ec_talk.erl
@@ -1,4 +1,5 @@
%% -*- mode: Erlang; fill-column: 79; comment-column: 70; -*-
+%% vi:ts=4 sw=4 et
%%%---------------------------------------------------------------------------
%%% Permission is hereby granted, free of charge, to any person
%%% obtaining a copy of this software and associated documentation
@@ -38,18 +39,21 @@
say/1,
say/2]).
+-ifdef(TEST).
+-export([get_boolean/1,
+ get_integer/1]).
+-endif.
+
-export_type([prompt/0,
type/0,
supported/0]).
--include_lib("eunit/include/eunit.hrl").
-
%%============================================================================
%% Types
%%============================================================================
-type prompt() :: string().
-type type() :: boolean | number | string.
--type supported() :: string() | boolean() | number().
+-type supported() :: boolean() | number() | string().
%%============================================================================
%% API
@@ -76,7 +80,7 @@ ask(Prompt) ->
ask_default(Prompt, Default) ->
ask_convert(Prompt, fun get_string/1, string, Default).
-%% @doc Asks the user to respond to the prompt. Trys to return the
+%% @doc Asks the user to respond to the prompt. Tries to return the
%% value in the format specified by 'Type'.
-spec ask(prompt(), type()) -> supported().
ask(Prompt, boolean) ->
@@ -84,9 +88,9 @@ ask(Prompt, boolean) ->
ask(Prompt, number) ->
ask_convert(Prompt, fun get_integer/1, number, none);
ask(Prompt, string) ->
- ask_convert(Prompt, fun get_integer/1, string, none).
+ ask_convert(Prompt, fun get_string/1, string, none).
-%% @doc Asks the user to respond to the prompt. Trys to return the
+%% @doc Asks the user to respond to the prompt. Tries to return the
%% value in the format specified by 'Type'.
-spec ask_default(prompt(), type(), supported()) -> supported().
ask_default(Prompt, boolean, Default) ->
@@ -100,8 +104,11 @@ ask_default(Prompt, string, Default) ->
%% between min and max.
-spec ask(prompt(), number(), number()) -> number().
ask(Prompt, Min, Max)
- when is_list(Prompt), is_number(Min), is_number(Max) ->
- Res = ask(Prompt, fun get_integer/1, none),
+ when erlang:is_list(Prompt),
+ erlang:is_number(Min),
+ erlang:is_number(Max),
+ Min =< Max ->
+ Res = ask_convert(Prompt, fun get_integer/1, number, none),
case (Res >= Min andalso Res =< Max) of
true ->
Res;
@@ -115,15 +122,17 @@ ask(Prompt, Min, Max)
%% ============================================================================
%% @doc Actually does the work of asking, checking result and
%% translating result into the requested format.
--spec ask_convert(prompt(), fun(), type(), supported()) -> supported().
+-spec ask_convert(prompt(), fun((any()) -> any()), type(), supported() | none) -> supported().
ask_convert(Prompt, TransFun, Type, Default) ->
- NewPrompt = Prompt ++ case Default of
- none ->
- [];
- Default ->
- " (" ++ sin_utils:term_to_list(Default) ++ ")"
- end ++ "> ",
- Data = string:strip(string:strip(io:get_line(NewPrompt)), both, $\n),
+ NewPrompt =
+ erlang:binary_to_list(erlang:iolist_to_binary([Prompt,
+ case Default of
+ none ->
+ [];
+ Default ->
+ [" (", io_lib:format("~p", [Default]) , ")"]
+ end, "> "])),
+ Data = string:trim(string:trim(io:get_line(NewPrompt)), both, [$\n]),
Ret = TransFun(Data),
case Ret of
no_data ->
@@ -141,7 +150,7 @@ ask_convert(Prompt, TransFun, Type, Default) ->
Ret
end.
-%% @doc Trys to translate the result into a boolean
+%% @doc Tries to translate the result into a boolean
-spec get_boolean(string()) -> boolean().
get_boolean([]) ->
no_data;
@@ -168,7 +177,7 @@ get_boolean([$N | _]) ->
get_boolean(_) ->
no_clue.
-%% @doc Trys to translate the result into an integer
+%% @doc Tries to translate the result into an integer
-spec get_integer(string()) -> integer().
get_integer([]) ->
no_data;
@@ -192,21 +201,3 @@ get_string(String) ->
false ->
no_clue
end.
-
-%%%====================================================================
-%%% tests
-%%%====================================================================
-general_test_() ->
- [?_test(42 == get_integer("42")),
- ?_test(500211 == get_integer("500211")),
- ?_test(1234567890 == get_integer("1234567890")),
- ?_test(12345678901234567890 == get_integer("12345678901234567890")),
- ?_test(true == get_boolean("true")),
- ?_test(false == get_boolean("false")),
- ?_test(true == get_boolean("Ok")),
- ?_test(true == get_boolean("ok")),
- ?_test(true == get_boolean("Y")),
- ?_test(true == get_boolean("y")),
- ?_test(false == get_boolean("False")),
- ?_test(false == get_boolean("No")),
- ?_test(false == get_boolean("no"))].
diff --git a/src/ec_vsn.erl b/src/ec_vsn.erl
new file mode 100644
index 0000000..e407b9f
--- /dev/null
+++ b/src/ec_vsn.erl
@@ -0,0 +1,51 @@
+%%% vi:ts=4 sw=4 et
+%%%-------------------------------------------------------------------
+%%% @author Eric Merritt
+%%% @copyright 2014 Erlware, LLC.
+%%% @doc
+%%% Provides a signature to manage returning semver formatted versions
+%%% from various version control repositories.
+%%%
+%%% This interface is a member of the Erlware Commons Library.
+%%% @end
+%%%-------------------------------------------------------------------
+-module(ec_vsn).
+
+%% API
+-export([new/1,
+ vsn/1]).
+
+-export_type([t/0]).
+
+%%%===================================================================
+%%% Types
+%%%===================================================================
+
+-record(t, {callback, data}).
+
+%% This should be opaque, but that kills dialyzer so for now we export it
+%% however you should not rely on the internal representation here
+-type t() :: #t{}.
+
+-callback new() -> any().
+-callback vsn(any()) -> {ok, string()} | {error, Reason::any()}.
+
+%%%===================================================================
+%%% API
+%%%===================================================================
+
+%% @doc create a new dictionary object from the specified module. The
+%% module should implement the dictionary behaviour.
+%%
+%% @param ModuleName The module name.
+-spec new(module()) -> t().
+new(ModuleName) when erlang:is_atom(ModuleName) ->
+ #t{callback = ModuleName, data = ModuleName:new()}.
+
+%% @doc Return the semver or an error depending on what is possible
+%% with this implementation in this directory.
+%%
+%% @param The dictionary object
+-spec vsn(t()) -> {ok, string()} | {error, Reason::any()}.
+vsn(#t{callback = Mod, data = Data}) ->
+ Mod:vsn(Data).
diff --git a/src/erlware_commons.app.src b/src/erlware_commons.app.src
new file mode 100644
index 0000000..7709d81
--- /dev/null
+++ b/src/erlware_commons.app.src
@@ -0,0 +1,11 @@
+{application,erlware_commons,
+ [{description,"Additional standard library for Erlang"},
+ {vsn,"git"},
+ {modules,[]},
+ {registered,[]},
+ {applications,[kernel,stdlib,cf]},
+ {maintainers,["Eric Merritt","Tristan Sloughter",
+ "Jordan Wilberding","Martin Logan"]},
+ {licenses,["Apache", "MIT"]},
+ {links,[{"Github",
+ "https://github.com/erlware/erlware_commons"}]}]}.
diff --git a/test/ec_cmd_log_tests.erl b/test/ec_cmd_log_tests.erl
new file mode 100644
index 0000000..f1d1181
--- /dev/null
+++ b/test/ec_cmd_log_tests.erl
@@ -0,0 +1,39 @@
+%%% @copyright 2024 Erlware, LLC.
+-module(ec_cmd_log_tests).
+
+-include("include/ec_cmd_log.hrl").
+-include("src/ec_cmd_log.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+should_test() ->
+ ErrorLogState = ec_cmd_log:new(error),
+ ?assertMatch(true, ec_cmd_log:should(ErrorLogState, ?EC_ERROR)),
+ ?assertMatch(true, not ec_cmd_log:should(ErrorLogState, ?EC_INFO)),
+ ?assertMatch(true, not ec_cmd_log:should(ErrorLogState, ?EC_DEBUG)),
+ ?assertEqual(?EC_ERROR, ec_cmd_log:log_level(ErrorLogState)),
+ ?assertEqual(error, ec_cmd_log:atom_log_level(ErrorLogState)),
+
+ InfoLogState = ec_cmd_log:new(info),
+ ?assertMatch(true, ec_cmd_log:should(InfoLogState, ?EC_ERROR)),
+ ?assertMatch(true, ec_cmd_log:should(InfoLogState, ?EC_INFO)),
+ ?assertMatch(true, not ec_cmd_log:should(InfoLogState, ?EC_DEBUG)),
+ ?assertEqual(?EC_INFO, ec_cmd_log:log_level(InfoLogState)),
+ ?assertEqual(info, ec_cmd_log:atom_log_level(InfoLogState)),
+
+ DebugLogState = ec_cmd_log:new(debug),
+ ?assertMatch(true, ec_cmd_log:should(DebugLogState, ?EC_ERROR)),
+ ?assertMatch(true, ec_cmd_log:should(DebugLogState, ?EC_INFO)),
+ ?assertMatch(true, ec_cmd_log:should(DebugLogState, ?EC_DEBUG)),
+ ?assertEqual(?EC_DEBUG, ec_cmd_log:log_level(DebugLogState)),
+ ?assertEqual(debug, ec_cmd_log:atom_log_level(DebugLogState)).
+
+
+no_color_test() ->
+ LogState = ec_cmd_log:new(debug, command_line, none),
+ ?assertEqual("test",
+ ec_cmd_log:colorize(LogState, ?RED, true, "test")).
+
+color_test() ->
+ LogState = ec_cmd_log:new(debug, command_line, high),
+ ?assertEqual("\e[1;31m===> test\e[0m",
+ ec_cmd_log:colorize(LogState, ?RED, true, "test")).
diff --git a/test/ec_cnv_tests.erl b/test/ec_cnv_tests.erl
new file mode 100644
index 0000000..6bbad6e
--- /dev/null
+++ b/test/ec_cnv_tests.erl
@@ -0,0 +1,28 @@
+%%% @copyright 2024 Erlware, LLC.
+-module(ec_cnv_tests).
+
+-include_lib("eunit/include/eunit.hrl").
+
+to_integer_test() ->
+ ?assertError(badarg, ec_cnv:to_integer(1.5, strict)).
+
+to_float_test() ->
+ ?assertError(badarg, ec_cnv:to_float(10, strict)).
+
+to_atom_test() ->
+ ?assertMatch(true, ec_cnv:to_atom("true")),
+ ?assertMatch(true, ec_cnv:to_atom(<<"true">>)),
+ ?assertMatch(false, ec_cnv:to_atom(<<"false">>)),
+ ?assertMatch(false, ec_cnv:to_atom(false)),
+ ?assertError(badarg, ec_cnv:to_atom("hello_foo_bar_baz")),
+
+ S = erlang:list_to_atom("1"),
+ ?assertMatch(S, ec_cnv:to_atom(1)).
+
+to_boolean_test()->
+ ?assertMatch(true, ec_cnv:to_boolean(<<"true">>)),
+ ?assertMatch(true, ec_cnv:to_boolean("true")),
+ ?assertMatch(true, ec_cnv:to_boolean(true)),
+ ?assertMatch(false, ec_cnv:to_boolean(<<"false">>)),
+ ?assertMatch(false, ec_cnv:to_boolean("false")),
+ ?assertMatch(false, ec_cnv:to_boolean(false)).
diff --git a/test/ec_file_tests.erl b/test/ec_file_tests.erl
new file mode 100644
index 0000000..885f3dc
--- /dev/null
+++ b/test/ec_file_tests.erl
@@ -0,0 +1,84 @@
+%%% @copyright 2024 Erlware, LLC.
+-module(ec_file_tests).
+
+-include_lib("eunit/include/eunit.hrl").
+
+setup_test() ->
+ Dir = ec_file:insecure_mkdtemp(),
+ ec_file:mkdir_path(Dir),
+ ?assertMatch(false, ec_file:is_symlink(Dir)),
+ ?assertMatch(true, filelib:is_dir(Dir)).
+
+md5sum_test() ->
+ ?assertMatch("cfcd208495d565ef66e7dff9f98764da", ec_file:md5sum("0")).
+
+sha1sum_test() ->
+ ?assertMatch("b6589fc6ab0dc82cf12099d1c2d40ab994e8410c", ec_file:sha1sum("0")).
+
+file_test() ->
+ Dir = ec_file:insecure_mkdtemp(),
+ TermFile = filename:join(Dir, "ec_file/dir/file.term"),
+ TermFileCopy = filename:join(Dir, "ec_file/dircopy/file.term"),
+ filelib:ensure_dir(TermFile),
+ filelib:ensure_dir(TermFileCopy),
+ ec_file:write_term(TermFile, "term"),
+ ?assertMatch({ok, <<"\"term\". ">>}, ec_file:read(TermFile)),
+ ec_file:copy(filename:dirname(TermFile),
+ filename:dirname(TermFileCopy),
+ [recursive]).
+
+teardown_test() ->
+ Dir = ec_file:insecure_mkdtemp(),
+ ec_file:remove(Dir, [recursive]),
+ ?assertMatch(false, filelib:is_dir(Dir)).
+
+setup_base_and_target() ->
+ BaseDir = ec_file:insecure_mkdtemp(),
+ DummyContents = <<"This should be deleted">>,
+ SourceDir = filename:join([BaseDir, "source"]),
+ ok = file:make_dir(SourceDir),
+ Name1 = filename:join([SourceDir, "fileone"]),
+ Name2 = filename:join([SourceDir, "filetwo"]),
+ Name3 = filename:join([SourceDir, "filethree"]),
+ NoName = filename:join([SourceDir, "noname"]),
+
+ ok = file:write_file(Name1, DummyContents),
+ ok = file:write_file(Name2, DummyContents),
+ ok = file:write_file(Name3, DummyContents),
+ ok = file:write_file(NoName, DummyContents),
+ {BaseDir, SourceDir, {Name1, Name2, Name3, NoName}}.
+
+exists_test() ->
+ BaseDir = ec_file:insecure_mkdtemp(),
+ SourceDir = filename:join([BaseDir, "source1"]),
+ NoName = filename:join([SourceDir, "noname"]),
+ ok = file:make_dir(SourceDir),
+ Name1 = filename:join([SourceDir, "fileone"]),
+ ok = file:write_file(Name1, <<"Testn">>),
+ ?assertMatch(true, ec_file:exists(Name1)),
+ ?assertMatch(false, ec_file:exists(NoName)).
+
+real_path_test() ->
+ BaseDir = "foo",
+ Dir = filename:absname(filename:join(BaseDir, "source1")),
+ LinkDir = filename:join([BaseDir, "link"]),
+ ok = ec_file:mkdir_p(Dir),
+ file:make_symlink(Dir, LinkDir),
+ ?assertEqual(Dir, ec_file:real_dir_path(LinkDir)),
+ ?assertEqual(directory, ec_file:type(Dir)),
+ ?assertEqual(symlink, ec_file:type(LinkDir)),
+ TermFile = filename:join(BaseDir, "test_file"),
+ ok = ec_file:write_term(TermFile, foo),
+ ?assertEqual(file, ec_file:type(TermFile)),
+ ?assertEqual(true, ec_file:is_symlink(LinkDir)),
+ ?assertEqual(false, ec_file:is_symlink(Dir)).
+
+find_test() ->
+ %% Create a directory in /tmp for the test. Clean everything afterwards
+ {BaseDir, _SourceDir, {Name1, Name2, Name3, _NoName}} = setup_base_and_target(),
+ Result = ec_file:find(BaseDir, "file[a-z]+\$"),
+ ?assertMatch(3, erlang:length(Result)),
+ ?assertEqual(true, lists:member(Name1, Result)),
+ ?assertEqual(true, lists:member(Name2, Result)),
+ ?assertEqual(true, lists:member(Name3, Result)),
+ ec_file:remove(BaseDir, [recursive]).
diff --git a/test/ec_gb_trees_tests.erl b/test/ec_gb_trees_tests.erl
new file mode 100644
index 0000000..2c0ee12
--- /dev/null
+++ b/test/ec_gb_trees_tests.erl
@@ -0,0 +1,67 @@
+%%% @copyright 2024 Erlware, LLC.
+-module(ec_gb_trees_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+%% For me unit testing initially is about covering the obvious case. A
+%% check to make sure that what you expect the tested functionality to
+%% do, it actually does. As time goes on and people detect bugs you
+%% add tests for those specific problems to the unit test suit.
+%%
+%% However, when getting started you can only test your basic
+%% expectations. So here are the expectations I have for the add
+%% functionality.
+%%
+%% 1) I can put arbitrary terms into the dictionary as keys
+%% 2) I can put arbitrary terms into the dictionary as values
+%% 3) When I put a value in the dictionary by a key, I can retrieve
+%% that same value
+%% 4) When I put a different value in the dictionary by key it does
+%% not change other key value pairs.
+%% 5) When I update a value the new value in available by the new key
+%% 6) When a value does not exist a not found exception is created
+
+add_test() ->
+ Dict0 = ec_dictionary:new(ec_gb_trees),
+
+ Key1 = foo,
+ Key2 = [1, 3],
+ Key3 = {"super"},
+ Key4 = <<"fabulous">>,
+ Key5 = {"Sona", 2, <<"Zuper">>},
+
+ Value1 = Key5,
+ Value2 = Key4,
+ Value3 = Key2,
+ Value4 = Key3,
+ Value5 = Key1,
+
+ Dict01 = ec_dictionary:add(Key1, Value1, Dict0),
+ Dict02 = ec_dictionary:add(Key3, Value3,
+ ec_dictionary:add(Key2, Value2,
+ Dict01)),
+ Dict1 =
+ ec_dictionary:add(Key5, Value5,
+ ec_dictionary:add(Key4, Value4,
+ Dict02)),
+
+ ?assertMatch(Value1, ec_dictionary:get(Key1, Dict1)),
+ ?assertMatch(Value2, ec_dictionary:get(Key2, Dict1)),
+ ?assertMatch(Value3, ec_dictionary:get(Key3, Dict1)),
+ ?assertMatch(Value4, ec_dictionary:get(Key4, Dict1)),
+ ?assertMatch(Value5, ec_dictionary:get(Key5, Dict1)),
+
+
+ Dict2 = ec_dictionary:add(Key3, Value5,
+ ec_dictionary:add(Key2, Value4, Dict1)),
+
+
+ ?assertMatch(Value1, ec_dictionary:get(Key1, Dict2)),
+ ?assertMatch(Value4, ec_dictionary:get(Key2, Dict2)),
+ ?assertMatch(Value5, ec_dictionary:get(Key3, Dict2)),
+ ?assertMatch(Value4, ec_dictionary:get(Key4, Dict2)),
+ ?assertMatch(Value5, ec_dictionary:get(Key5, Dict2)),
+
+
+ ?assertThrow(not_found, ec_dictionary:get(should_blow_up, Dict2)),
+ ?assertThrow(not_found, ec_dictionary:get("This should blow up too",
+ Dict2)).
diff --git a/test/ec_git_vsn_tests.erl b/test/ec_git_vsn_tests.erl
new file mode 100644
index 0000000..0d2efe1
--- /dev/null
+++ b/test/ec_git_vsn_tests.erl
@@ -0,0 +1,13 @@
+%%% @copyright 2024 Erlware, LLC.
+-module(ec_git_vsn_tests).
+
+-include_lib("eunit/include/eunit.hrl").
+
+parse_tags_test() ->
+ ?assertEqual({undefined, ""}, ec_git_vsn:parse_tags("a.b.c")).
+
+get_patch_count_test() ->
+ ?assertEqual(0, ec_git_vsn:get_patch_count("a.b.c")).
+
+collect_default_refcount_test() ->
+ ?assertMatch({"", _, _}, ec_git_vsn:collect_default_refcount("a.b.c")).
diff --git a/test/ec_lists_tests.erl b/test/ec_lists_tests.erl
new file mode 100644
index 0000000..f6f4025
--- /dev/null
+++ b/test/ec_lists_tests.erl
@@ -0,0 +1,172 @@
+%%% @copyright 2024 Erlware, LLC.
+-module(ec_lists_tests).
+
+-include_lib("eunit/include/eunit.hrl").
+
+find1_test() ->
+ TestData = [1, 2, 3, 4, 5, 6],
+ Result = ec_lists:find(fun(5) ->
+ true;
+ (_) ->
+ false
+ end,
+ TestData),
+ ?assertMatch({ok, 5}, Result),
+
+ Result2 = ec_lists:find(fun(37) ->
+ true;
+ (_) ->
+ false
+ end,
+ TestData),
+ ?assertMatch(error, Result2).
+
+find2_test() ->
+ TestData = ["one", "two", "three", "four", "five", "six"],
+ Result = ec_lists:find(fun("five") ->
+ true;
+ (_) ->
+ false
+ end,
+ TestData),
+ ?assertMatch({ok, "five"}, Result),
+
+ Result2 = ec_lists:find(fun(super_duper) ->
+ true;
+ (_) ->
+ false
+ end,
+ TestData),
+ ?assertMatch(error, Result2).
+
+find3_test() ->
+ TestData = [{"one", 1}, {"two", 2}, {"three", 3}, {"four", 5}, {"five", 5},
+ {"six", 6}],
+ Result = ec_lists:find(fun({"one", 1}) ->
+ true;
+ (_) ->
+ false
+ end,
+ TestData),
+ ?assertMatch({ok, {"one", 1}}, Result),
+
+ Result2 = ec_lists:find(fun([fo, bar, baz]) ->
+ true;
+ ({"onehundred", 100}) ->
+ true;
+ (_) ->
+ false
+ end,
+ TestData),
+ ?assertMatch(error, Result2).
+
+fetch1_test() ->
+ TestData = [1, 2, 3, 4, 5, 6],
+ Result = ec_lists:fetch(fun(5) ->
+ true;
+ (_) ->
+ false
+ end,
+ TestData),
+ ?assertMatch(5, Result),
+
+ ?assertThrow(not_found,
+ ec_lists:fetch(fun(37) ->
+ true;
+ (_) ->
+ false
+ end,
+ TestData)).
+
+fetch2_test() ->
+ TestData = ["one", "two", "three", "four", "five", "six"],
+ Result = ec_lists:fetch(fun("five") ->
+ true;
+ (_) ->
+ false
+ end,
+ TestData),
+ ?assertMatch("five", Result),
+
+ ?assertThrow(not_found,
+ ec_lists:fetch(fun(super_duper) ->
+ true;
+ (_) ->
+ false
+ end,
+ TestData)).
+
+fetch3_test() ->
+ TestData = [{"one", 1}, {"two", 2}, {"three", 3}, {"four", 5}, {"five", 5},
+ {"six", 6}],
+ Result = ec_lists:fetch(fun({"one", 1}) ->
+ true;
+ (_) ->
+ false
+ end,
+ TestData),
+ ?assertMatch({"one", 1}, Result),
+
+ ?assertThrow(not_found,
+ ec_lists:fetch(fun([fo, bar, baz]) ->
+ true;
+ ({"onehundred", 100}) ->
+ true;
+ (_) ->
+ false
+ end,
+ TestData)).
+
+search1_test() ->
+ TestData = [1, 2, 3, 4, 5, 6],
+ Result = ec_lists:search(fun(5) ->
+ {ok, 5};
+ (_) ->
+ not_found
+ end,
+ TestData),
+ ?assertMatch({ok, 5, 5}, Result),
+
+ Result2 = ec_lists:search(fun(37) ->
+ {ok, 37};
+ (_) ->
+ not_found
+ end,
+ TestData),
+ ?assertMatch(not_found, Result2).
+
+search2_test() ->
+ TestData = [1, 2, 3, 4, 5, 6],
+ Result = ec_lists:search(fun(1) ->
+ {ok, 10};
+ (_) ->
+ not_found
+ end,
+ TestData),
+ ?assertMatch({ok, 10, 1}, Result),
+
+ Result2 = ec_lists:search(fun(6) ->
+ {ok, 37};
+ (_) ->
+ not_found
+ end,
+ TestData),
+ ?assertMatch({ok, 37, 6}, Result2).
+
+search3_test() ->
+ TestData = [1, 2, 3, 4, 5, 6],
+ Result = ec_lists:search(fun(10) ->
+ {ok, 10};
+ (_) ->
+ not_found
+ end,
+ TestData),
+ ?assertMatch(not_found, Result),
+
+ Result2 = ec_lists:search(fun(-1) ->
+ {ok, 37};
+ (_) ->
+ not_found
+ end,
+ TestData),
+ ?assertMatch(not_found, Result2).
diff --git a/test/ec_plists_tests.erl b/test/ec_plists_tests.erl
new file mode 100644
index 0000000..3f945e9
--- /dev/null
+++ b/test/ec_plists_tests.erl
@@ -0,0 +1,84 @@
+%%% @copyright Erlware, LLC.
+-module(ec_plists_tests).
+
+-include_lib("eunit/include/eunit.hrl").
+
+%%%===================================================================
+%%% Tests
+%%%===================================================================
+
+map_good_test() ->
+ Results = ec_plists:map(fun(_) ->
+ ok
+ end,
+ lists:seq(1, 5)),
+ ?assertMatch([ok, ok, ok, ok, ok],
+ Results).
+
+ftmap_good_test() ->
+ Results = ec_plists:ftmap(fun(_) ->
+ ok
+ end,
+ lists:seq(1, 3)),
+ ?assertMatch([{value, ok}, {value, ok}, {value, ok}],
+ Results).
+
+filter_good_test() ->
+ Results = ec_plists:filter(fun(X) ->
+ X == show
+ end,
+ [show, show, remove]),
+ ?assertMatch([show, show],
+ Results).
+
+map_timeout_test() ->
+ ?assertExit(timeout,
+ ec_plists:map(fun(T) ->
+ timer:sleep(T),
+ T
+ end,
+ [1, 100], {timeout, 10})).
+
+ftmap_timeout_test() ->
+ ?assertExit(timeout,
+ ec_plists:ftmap(fun(X) ->
+ timer:sleep(X),
+ true
+ end,
+ [100, 1], {timeout, 10})).
+
+filter_timeout_test() ->
+ ?assertExit(timeout,
+ ec_plists:filter(fun(T) ->
+ timer:sleep(T),
+ T == 1
+ end,
+ [1, 100], {timeout, 10})).
+
+map_bad_test() ->
+ ?assertExit({{nocatch,test_exception}, _},
+ ec_plists:map(fun(_) ->
+ erlang:throw(test_exception)
+ end,
+ lists:seq(1, 5))).
+
+
+ftmap_bad_test() ->
+ Results =
+ ec_plists:ftmap(fun(2) ->
+ erlang:throw(test_exception);
+ (N) ->
+ N
+ end,
+ lists:seq(1, 5)),
+ ?assertMatch([{value, 1}, {error,{throw,test_exception}}, {value, 3},
+ {value, 4}, {value, 5}] , Results).
+
+external_down_message_test() ->
+ erlang:spawn_monitor(fun() -> erlang:throw(fail) end),
+ Results = ec_plists:map(fun(_) ->
+ ok
+ end,
+ lists:seq(1, 5)),
+ ?assertMatch([ok, ok, ok, ok, ok],
+ Results).
diff --git a/test/ec_semver_tests.erl b/test/ec_semver_tests.erl
new file mode 100644
index 0000000..0d3a18a
--- /dev/null
+++ b/test/ec_semver_tests.erl
@@ -0,0 +1,447 @@
+%%% @copyright 2024 Erlware, LLC.
+-module(ec_semver_tests).
+
+-include_lib("eunit/include/eunit.hrl").
+
+eql_test() ->
+ ?assertMatch(true, ec_semver:eql("1.0.0-alpha",
+ "1.0.0-alpha")),
+ ?assertMatch(true, ec_semver:eql("v1.0.0-alpha",
+ "1.0.0-alpha")),
+ ?assertMatch(true, ec_semver:eql("1",
+ "1.0.0")),
+ ?assertMatch(true, ec_semver:eql("v1",
+ "v1.0.0")),
+ ?assertMatch(true, ec_semver:eql("1.0",
+ "1.0.0")),
+ ?assertMatch(true, ec_semver:eql("1.0.0",
+ "1")),
+ ?assertMatch(true, ec_semver:eql("1.0.0.0",
+ "1")),
+ ?assertMatch(true, ec_semver:eql("1.0+alpha.1",
+ "1.0.0+alpha.1")),
+ ?assertMatch(true, ec_semver:eql("1.0-alpha.1+build.1",
+ "1.0.0-alpha.1+build.1")),
+ ?assertMatch(true, ec_semver:eql("1.0-alpha.1+build.1",
+ "1.0.0.0-alpha.1+build.1")),
+ ?assertMatch(true, ec_semver:eql("1.0-alpha.1+build.1",
+ "v1.0.0.0-alpha.1+build.1")),
+ ?assertMatch(true, ec_semver:eql("1.0-pre-alpha.1",
+ "1.0.0-pre-alpha.1")),
+ ?assertMatch(true, ec_semver:eql("aa", "aa")),
+ ?assertMatch(true, ec_semver:eql("AA.BB", "AA.BB")),
+ ?assertMatch(true, ec_semver:eql("BBB-super", "BBB-super")),
+ ?assertMatch(true, not ec_semver:eql("1.0.0",
+ "1.0.1")),
+ ?assertMatch(true, not ec_semver:eql("1.0.0-alpha",
+ "1.0.1+alpha")),
+ ?assertMatch(true, not ec_semver:eql("1.0.0+build.1",
+ "1.0.1+build.2")),
+ ?assertMatch(true, not ec_semver:eql("1.0.0.0+build.1",
+ "1.0.0.1+build.2")),
+ ?assertMatch(true, not ec_semver:eql("FFF", "BBB")),
+ ?assertMatch(true, not ec_semver:eql("1", "1BBBB")).
+
+gt_test() ->
+ ?assertMatch(true, ec_semver:gt("1.0.0-alpha.1",
+ "1.0.0-alpha")),
+ ?assertMatch(true, ec_semver:gt("1.0.0.1-alpha.1",
+ "1.0.0.1-alpha")),
+ ?assertMatch(true, ec_semver:gt("1.0.0.4-alpha.1",
+ "1.0.0.2-alpha")),
+ ?assertMatch(true, ec_semver:gt("1.0.0.0-alpha.1",
+ "1.0.0-alpha")),
+ ?assertMatch(true, ec_semver:gt("1.0.0-beta.2",
+ "1.0.0-alpha.1")),
+ ?assertMatch(true, ec_semver:gt("1.0.0-beta.11",
+ "1.0.0-beta.2")),
+ ?assertMatch(true, ec_semver:gt("1.0.0-pre-alpha.14",
+ "1.0.0-pre-alpha.3")),
+ ?assertMatch(true, ec_semver:gt("1.0.0-beta.11",
+ "1.0.0.0-beta.2")),
+ ?assertMatch(true, ec_semver:gt("1.0.0-rc.1", "1.0.0-beta.11")),
+ ?assertMatch(true, ec_semver:gt("1.0.0-rc.1+build.1", "1.0.0-rc.1")),
+ ?assertMatch(true, ec_semver:gt("1.0.0", "1.0.0-rc.1+build.1")),
+ ?assertMatch(true, ec_semver:gt("1.0.0+0.3.7", "1.0.0")),
+ ?assertMatch(true, ec_semver:gt("1.3.7+build", "1.0.0+0.3.7")),
+ ?assertMatch(true, ec_semver:gt("1.3.7+build.2.b8f12d7",
+ "1.3.7+build")),
+ ?assertMatch(true, ec_semver:gt("1.3.7+build.2.b8f12d7",
+ "1.3.7.0+build")),
+ ?assertMatch(true, ec_semver:gt("1.3.7+build.11.e0f985a",
+ "1.3.7+build.2.b8f12d7")),
+ ?assertMatch(true, ec_semver:gt("aa.cc",
+ "aa.bb")),
+ ?assertMatch(true, not ec_semver:gt("1.0.0-alpha",
+ "1.0.0-alpha.1")),
+ ?assertMatch(true, not ec_semver:gt("1.0.0-alpha",
+ "1.0.0.0-alpha.1")),
+ ?assertMatch(true, not ec_semver:gt("1.0.0-alpha.1",
+ "1.0.0-beta.2")),
+ ?assertMatch(true, not ec_semver:gt("1.0.0-beta.2",
+ "1.0.0-beta.11")),
+ ?assertMatch(true, not ec_semver:gt("1.0.0-beta.11",
+ "1.0.0-rc.1")),
+ ?assertMatch(true, not ec_semver:gt("1.0.0-pre-alpha.3",
+ "1.0.0-pre-alpha.14")),
+ ?assertMatch(true, not ec_semver:gt("1.0.0-rc.1",
+ "1.0.0-rc.1+build.1")),
+ ?assertMatch(true, not ec_semver:gt("1.0.0-rc.1+build.1",
+ "1.0.0")),
+ ?assertMatch(true, not ec_semver:gt("1.0.0",
+ "1.0.0+0.3.7")),
+ ?assertMatch(true, not ec_semver:gt("1.0.0+0.3.7",
+ "1.3.7+build")),
+ ?assertMatch(true, not ec_semver:gt("1.3.7+build",
+ "1.3.7+build.2.b8f12d7")),
+ ?assertMatch(true, not ec_semver:gt("1.3.7+build.2.b8f12d7",
+ "1.3.7+build.11.e0f985a")),
+ ?assertMatch(true, not ec_semver:gt("1.0.0-alpha",
+ "1.0.0-alpha")),
+ ?assertMatch(true, not ec_semver:gt("1",
+ "1.0.0")),
+ ?assertMatch(true, not ec_semver:gt("aa.bb",
+ "aa.bb")),
+ ?assertMatch(true, not ec_semver:gt("aa.cc",
+ "aa.dd")),
+ ?assertMatch(true, not ec_semver:gt("1.0",
+ "1.0.0")),
+ ?assertMatch(true, not ec_semver:gt("1.0.0",
+ "1")),
+ ?assertMatch(true, not ec_semver:gt("1.0+alpha.1",
+ "1.0.0+alpha.1")),
+ ?assertMatch(true, not ec_semver:gt("1.0-alpha.1+build.1",
+ "1.0.0-alpha.1+build.1")).
+
+lt_test() ->
+ ?assertMatch(true, ec_semver:lt("1.0.0-alpha",
+ "1.0.0-alpha.1")),
+ ?assertMatch(true, ec_semver:lt("1.0.0-alpha",
+ "1.0.0.0-alpha.1")),
+ ?assertMatch(true, ec_semver:lt("1.0.0-alpha.1",
+ "1.0.0-beta.2")),
+ ?assertMatch(true, ec_semver:lt("1.0.0-beta.2",
+ "1.0.0-beta.11")),
+ ?assertMatch(true, ec_semver:lt("1.0.0-pre-alpha.3",
+ "1.0.0-pre-alpha.14")),
+ ?assertMatch(true, ec_semver:lt("1.0.0-beta.11",
+ "1.0.0-rc.1")),
+ ?assertMatch(true, ec_semver:lt("1.0.0.1-beta.11",
+ "1.0.0.1-rc.1")),
+ ?assertMatch(true, ec_semver:lt("1.0.0-rc.1",
+ "1.0.0-rc.1+build.1")),
+ ?assertMatch(true, ec_semver:lt("1.0.0-rc.1+build.1",
+ "1.0.0")),
+ ?assertMatch(true, ec_semver:lt("1.0.0",
+ "1.0.0+0.3.7")),
+ ?assertMatch(true, ec_semver:lt("1.0.0+0.3.7",
+ "1.3.7+build")),
+ ?assertMatch(true, ec_semver:lt("1.3.7+build",
+ "1.3.7+build.2.b8f12d7")),
+ ?assertMatch(true, ec_semver:lt("1.3.7+build.2.b8f12d7",
+ "1.3.7+build.11.e0f985a")),
+ ?assertMatch(true, not ec_semver:lt("1.0.0-alpha",
+ "1.0.0-alpha")),
+ ?assertMatch(true, not ec_semver:lt("1",
+ "1.0.0")),
+ ?assertMatch(true, ec_semver:lt("1",
+ "1.0.0.1")),
+ ?assertMatch(true, ec_semver:lt("AA.DD",
+ "AA.EE")),
+ ?assertMatch(true, not ec_semver:lt("1.0",
+ "1.0.0")),
+ ?assertMatch(true, not ec_semver:lt("1.0.0.0",
+ "1")),
+ ?assertMatch(true, not ec_semver:lt("1.0+alpha.1",
+ "1.0.0+alpha.1")),
+ ?assertMatch(true, not ec_semver:lt("AA.DD", "AA.CC")),
+ ?assertMatch(true, not ec_semver:lt("1.0-alpha.1+build.1",
+ "1.0.0-alpha.1+build.1")),
+ ?assertMatch(true, not ec_semver:lt("1.0.0-alpha.1",
+ "1.0.0-alpha")),
+ ?assertMatch(true, not ec_semver:lt("1.0.0-beta.2",
+ "1.0.0-alpha.1")),
+ ?assertMatch(true, not ec_semver:lt("1.0.0-beta.11",
+ "1.0.0-beta.2")),
+ ?assertMatch(true, not ec_semver:lt("1.0.0-pre-alpha.14",
+ "1.0.0-pre-alpha.3")),
+ ?assertMatch(true, not ec_semver:lt("1.0.0-rc.1", "1.0.0-beta.11")),
+ ?assertMatch(true, not ec_semver:lt("1.0.0-rc.1+build.1", "1.0.0-rc.1")),
+ ?assertMatch(true, not ec_semver:lt("1.0.0", "1.0.0-rc.1+build.1")),
+ ?assertMatch(true, not ec_semver:lt("1.0.0+0.3.7", "1.0.0")),
+ ?assertMatch(true, not ec_semver:lt("1.3.7+build", "1.0.0+0.3.7")),
+ ?assertMatch(true, not ec_semver:lt("1.3.7+build.2.b8f12d7",
+ "1.3.7+build")),
+ ?assertMatch(true, not ec_semver:lt("1.3.7+build.11.e0f985a",
+ "1.3.7+build.2.b8f12d7")).
+
+gte_test() ->
+ ?assertMatch(true, ec_semver:gte("1.0.0-alpha",
+ "1.0.0-alpha")),
+
+ ?assertMatch(true, ec_semver:gte("1",
+ "1.0.0")),
+
+ ?assertMatch(true, ec_semver:gte("1.0",
+ "1.0.0")),
+
+ ?assertMatch(true, ec_semver:gte("1.0.0",
+ "1")),
+
+ ?assertMatch(true, ec_semver:gte("1.0.0.0",
+ "1")),
+
+ ?assertMatch(true, ec_semver:gte("1.0+alpha.1",
+ "1.0.0+alpha.1")),
+
+ ?assertMatch(true, ec_semver:gte("1.0-alpha.1+build.1",
+ "1.0.0-alpha.1+build.1")),
+
+ ?assertMatch(true, ec_semver:gte("1.0.0-alpha.1+build.1",
+ "1.0.0.0-alpha.1+build.1")),
+ ?assertMatch(true, ec_semver:gte("1.0.0-alpha.1",
+ "1.0.0-alpha")),
+ ?assertMatch(true, ec_semver:gte("1.0.0-pre-alpha.2",
+ "1.0.0-pre-alpha")),
+ ?assertMatch(true, ec_semver:gte("1.0.0-beta.2",
+ "1.0.0-alpha.1")),
+ ?assertMatch(true, ec_semver:gte("1.0.0-beta.11",
+ "1.0.0-beta.2")),
+ ?assertMatch(true, ec_semver:gte("aa.bb", "aa.bb")),
+ ?assertMatch(true, ec_semver:gte("dd", "aa")),
+ ?assertMatch(true, ec_semver:gte("1.0.0-rc.1", "1.0.0-beta.11")),
+ ?assertMatch(true, ec_semver:gte("1.0.0-rc.1+build.1", "1.0.0-rc.1")),
+ ?assertMatch(true, ec_semver:gte("1.0.0", "1.0.0-rc.1+build.1")),
+ ?assertMatch(true, ec_semver:gte("1.0.0+0.3.7", "1.0.0")),
+ ?assertMatch(true, ec_semver:gte("1.3.7+build", "1.0.0+0.3.7")),
+ ?assertMatch(true, ec_semver:gte("1.3.7+build.2.b8f12d7",
+ "1.3.7+build")),
+ ?assertMatch(true, ec_semver:gte("1.3.7+build.11.e0f985a",
+ "1.3.7+build.2.b8f12d7")),
+ ?assertMatch(true, not ec_semver:gte("1.0.0-alpha",
+ "1.0.0-alpha.1")),
+ ?assertMatch(true, not ec_semver:gte("1.0.0-pre-alpha",
+ "1.0.0-pre-alpha.1")),
+ ?assertMatch(true, not ec_semver:gte("CC", "DD")),
+ ?assertMatch(true, not ec_semver:gte("1.0.0-alpha.1",
+ "1.0.0-beta.2")),
+ ?assertMatch(true, not ec_semver:gte("1.0.0-beta.2",
+ "1.0.0-beta.11")),
+ ?assertMatch(true, not ec_semver:gte("1.0.0-beta.11",
+ "1.0.0-rc.1")),
+ ?assertMatch(true, not ec_semver:gte("1.0.0-rc.1",
+ "1.0.0-rc.1+build.1")),
+ ?assertMatch(true, not ec_semver:gte("1.0.0-rc.1+build.1",
+ "1.0.0")),
+ ?assertMatch(true, not ec_semver:gte("1.0.0",
+ "1.0.0+0.3.7")),
+ ?assertMatch(true, not ec_semver:gte("1.0.0+0.3.7",
+ "1.3.7+build")),
+ ?assertMatch(true, not ec_semver:gte("1.0.0",
+ "1.0.0+build.1")),
+ ?assertMatch(true, not ec_semver:gte("1.3.7+build",
+ "1.3.7+build.2.b8f12d7")),
+ ?assertMatch(true, not ec_semver:gte("1.3.7+build.2.b8f12d7",
+ "1.3.7+build.11.e0f985a")).
+lte_test() ->
+ ?assertMatch(true, ec_semver:lte("1.0.0-alpha",
+ "1.0.0-alpha.1")),
+ ?assertMatch(true, ec_semver:lte("1.0.0-alpha.1",
+ "1.0.0-beta.2")),
+ ?assertMatch(true, ec_semver:lte("1.0.0-beta.2",
+ "1.0.0-beta.11")),
+ ?assertMatch(true, ec_semver:lte("1.0.0-pre-alpha.2",
+ "1.0.0-pre-alpha.11")),
+ ?assertMatch(true, ec_semver:lte("1.0.0-beta.11",
+ "1.0.0-rc.1")),
+ ?assertMatch(true, ec_semver:lte("1.0.0-rc.1",
+ "1.0.0-rc.1+build.1")),
+ ?assertMatch(true, ec_semver:lte("1.0.0-rc.1+build.1",
+ "1.0.0")),
+ ?assertMatch(true, ec_semver:lte("1.0.0",
+ "1.0.0+0.3.7")),
+ ?assertMatch(true, ec_semver:lte("1.0.0+0.3.7",
+ "1.3.7+build")),
+ ?assertMatch(true, ec_semver:lte("1.3.7+build",
+ "1.3.7+build.2.b8f12d7")),
+ ?assertMatch(true, ec_semver:lte("1.3.7+build.2.b8f12d7",
+ "1.3.7+build.11.e0f985a")),
+ ?assertMatch(true, ec_semver:lte("1.0.0-alpha",
+ "1.0.0-alpha")),
+ ?assertMatch(true, ec_semver:lte("1",
+ "1.0.0")),
+ ?assertMatch(true, ec_semver:lte("1.0",
+ "1.0.0")),
+ ?assertMatch(true, ec_semver:lte("1.0.0",
+ "1")),
+ ?assertMatch(true, ec_semver:lte("1.0+alpha.1",
+ "1.0.0+alpha.1")),
+ ?assertMatch(true, ec_semver:lte("1.0.0.0+alpha.1",
+ "1.0.0+alpha.1")),
+ ?assertMatch(true, ec_semver:lte("1.0-alpha.1+build.1",
+ "1.0.0-alpha.1+build.1")),
+ ?assertMatch(true, ec_semver:lte("aa","cc")),
+ ?assertMatch(true, ec_semver:lte("cc","cc")),
+ ?assertMatch(true, not ec_semver:lte("1.0.0-alpha.1",
+ "1.0.0-alpha")),
+ ?assertMatch(true, not ec_semver:lte("1.0.0-pre-alpha.2",
+ "1.0.0-pre-alpha")),
+ ?assertMatch(true, not ec_semver:lte("cc", "aa")),
+ ?assertMatch(true, not ec_semver:lte("1.0.0-beta.2",
+ "1.0.0-alpha.1")),
+ ?assertMatch(true, not ec_semver:lte("1.0.0-beta.11",
+ "1.0.0-beta.2")),
+ ?assertMatch(true, not ec_semver:lte("1.0.0-rc.1", "1.0.0-beta.11")),
+ ?assertMatch(true, not ec_semver:lte("1.0.0-rc.1+build.1", "1.0.0-rc.1")),
+ ?assertMatch(true, not ec_semver:lte("1.0.0", "1.0.0-rc.1+build.1")),
+ ?assertMatch(true, not ec_semver:lte("1.0.0+0.3.7", "1.0.0")),
+ ?assertMatch(true, not ec_semver:lte("1.3.7+build", "1.0.0+0.3.7")),
+ ?assertMatch(true, not ec_semver:lte("1.3.7+build.2.b8f12d7",
+ "1.3.7+build")),
+ ?assertMatch(true, not ec_semver:lte("1.3.7+build.11.e0f985a",
+ "1.3.7+build.2.b8f12d7")).
+
+between_test() ->
+ ?assertMatch(true, ec_semver:between("1.0.0-alpha",
+ "1.0.0-alpha.3",
+ "1.0.0-alpha.2")),
+ ?assertMatch(true, ec_semver:between("1.0.0-alpha.1",
+ "1.0.0-beta.2",
+ "1.0.0-alpha.25")),
+ ?assertMatch(true, ec_semver:between("1.0.0-beta.2",
+ "1.0.0-beta.11",
+ "1.0.0-beta.7")),
+ ?assertMatch(true, ec_semver:between("1.0.0-pre-alpha.2",
+ "1.0.0-pre-alpha.11",
+ "1.0.0-pre-alpha.7")),
+ ?assertMatch(true, ec_semver:between("1.0.0-beta.11",
+ "1.0.0-rc.3",
+ "1.0.0-rc.1")),
+ ?assertMatch(true, ec_semver:between("1.0.0-rc.1",
+ "1.0.0-rc.1+build.3",
+ "1.0.0-rc.1+build.1")),
+
+ ?assertMatch(true, ec_semver:between("1.0.0.0-rc.1",
+ "1.0.0-rc.1+build.3",
+ "1.0.0-rc.1+build.1")),
+ ?assertMatch(true, ec_semver:between("1.0.0-rc.1+build.1",
+ "1.0.0",
+ "1.0.0-rc.33")),
+ ?assertMatch(true, ec_semver:between("1.0.0",
+ "1.0.0+0.3.7",
+ "1.0.0+0.2")),
+ ?assertMatch(true, ec_semver:between("1.0.0+0.3.7",
+ "1.3.7+build",
+ "1.2")),
+ ?assertMatch(true, ec_semver:between("1.3.7+build",
+ "1.3.7+build.2.b8f12d7",
+ "1.3.7+build.1")),
+ ?assertMatch(true, ec_semver:between("1.3.7+build.2.b8f12d7",
+ "1.3.7+build.11.e0f985a",
+ "1.3.7+build.10.a36faa")),
+ ?assertMatch(true, ec_semver:between("1.0.0-alpha",
+ "1.0.0-alpha",
+ "1.0.0-alpha")),
+ ?assertMatch(true, ec_semver:between("1",
+ "1.0.0",
+ "1.0.0")),
+ ?assertMatch(true, ec_semver:between("1.0",
+ "1.0.0",
+ "1.0.0")),
+
+ ?assertMatch(true, ec_semver:between("1.0",
+ "1.0.0.0",
+ "1.0.0.0")),
+ ?assertMatch(true, ec_semver:between("1.0.0",
+ "1",
+ "1")),
+ ?assertMatch(true, ec_semver:between("1.0+alpha.1",
+ "1.0.0+alpha.1",
+ "1.0.0+alpha.1")),
+ ?assertMatch(true, ec_semver:between("1.0-alpha.1+build.1",
+ "1.0.0-alpha.1+build.1",
+ "1.0.0-alpha.1+build.1")),
+ ?assertMatch(true, ec_semver:between("aaa",
+ "ddd",
+ "cc")),
+ ?assertMatch(true, not ec_semver:between("1.0.0-alpha.1",
+ "1.0.0-alpha.22",
+ "1.0.0")),
+ ?assertMatch(true, not ec_semver:between("1.0.0-pre-alpha.1",
+ "1.0.0-pre-alpha.22",
+ "1.0.0")),
+ ?assertMatch(true, not ec_semver:between("1.0.0",
+ "1.0.0-alpha.1",
+ "2.0")),
+ ?assertMatch(true, not ec_semver:between("1.0.0-beta.1",
+ "1.0.0-beta.11",
+ "1.0.0-alpha")),
+ ?assertMatch(true, not ec_semver:between("1.0.0-beta.11", "1.0.0-rc.1",
+ "1.0.0-rc.22")),
+ ?assertMatch(true, not ec_semver:between("aaa", "ddd", "zzz")).
+
+pes_test() ->
+ ?assertMatch(true, ec_semver:pes("1.0.0-rc.0", "1.0.0-rc.0")),
+ ?assertMatch(true, ec_semver:pes("1.0.0-rc.1", "1.0.0-rc.0")),
+ ?assertMatch(true, ec_semver:pes("1.0.0", "1.0.0-rc.0")),
+ ?assertMatch(false, ec_semver:pes("1.0.0-rc.0", "1.0.0-rc.1")),
+ ?assertMatch(true, ec_semver:pes("2.6.0", "2.6")),
+ ?assertMatch(true, ec_semver:pes("2.7", "2.6")),
+ ?assertMatch(true, ec_semver:pes("2.8", "2.6")),
+ ?assertMatch(true, ec_semver:pes("2.9", "2.6")),
+ ?assertMatch(true, ec_semver:pes("A.B", "A.A")),
+ ?assertMatch(true, not ec_semver:pes("3.0.0", "2.6")),
+ ?assertMatch(true, not ec_semver:pes("2.5", "2.6")),
+ ?assertMatch(true, ec_semver:pes("2.6.5", "2.6.5")),
+ ?assertMatch(true, ec_semver:pes("2.6.6", "2.6.5")),
+ ?assertMatch(true, ec_semver:pes("2.6.7", "2.6.5")),
+ ?assertMatch(true, ec_semver:pes("2.6.8", "2.6.5")),
+ ?assertMatch(true, ec_semver:pes("2.6.9", "2.6.5")),
+ ?assertMatch(true, ec_semver:pes("2.6.0.9", "2.6.0.5")),
+ ?assertMatch(true, not ec_semver:pes("2.7", "2.6.5")),
+ ?assertMatch(true, not ec_semver:pes("2.1.7", "2.1.6.5")),
+ ?assertMatch(true, not ec_semver:pes("A.A", "A.B")),
+ ?assertMatch(true, not ec_semver:pes("2.5", "2.6.5")).
+
+parse_test() ->
+ ?assertEqual({1, {[],[]}}, ec_semver:parse(<<"1">>)),
+ ?assertEqual({{1,2,34},{[],[]}}, ec_semver:parse(<<"1.2.34">>)),
+ ?assertEqual({<<"a">>, {[],[]}}, ec_semver:parse(<<"a">>)),
+ ?assertEqual({{<<"a">>,<<"b">>}, {[],[]}}, ec_semver:parse(<<"a.b">>)),
+ ?assertEqual({1, {[],[]}}, ec_semver:parse(<<"1">>)),
+ ?assertEqual({{1,2}, {[],[]}}, ec_semver:parse(<<"1.2">>)),
+ ?assertEqual({{1,2,2}, {[],[]}}, ec_semver:parse(<<"1.2.2">>)),
+ ?assertEqual({{1,99,2}, {[],[]}}, ec_semver:parse(<<"1.99.2">>)),
+ ?assertEqual({{1,99,2}, {[<<"alpha">>],[]}}, ec_semver:parse(<<"1.99.2-alpha">>)),
+ ?assertEqual({{1,99,2}, {[<<"alpha">>,1], []}}, ec_semver:parse(<<"1.99.2-alpha.1">>)),
+ ?assertEqual({{1,99,2}, {[<<"pre-alpha">>,1], []}}, ec_semver:parse(<<"1.99.2-pre-alpha.1">>)),
+ ?assertEqual({{1,99,2}, {[], [<<"build">>, 1, <<"a36">>]}},
+ ec_semver:parse(<<"1.99.2+build.1.a36">>)),
+ ?assertEqual({{1,99,2,44}, {[], [<<"build">>, 1, <<"a36">>]}},
+ ec_semver:parse(<<"1.99.2.44+build.1.a36">>)),
+ ?assertEqual({{1,99,2}, {[<<"alpha">>, 1], [<<"build">>, 1, <<"a36">>]}},
+ ec_semver:parse("1.99.2-alpha.1+build.1.a36")),
+ ?assertEqual({{1,99,2}, {[<<"pre-alpha">>, 1], [<<"build">>, 1, <<"a36">>]}},
+ ec_semver:parse("1.99.2-pre-alpha.1+build.1.a36")).
+
+version_format_test() ->
+ ?assertEqual(["1", [], []], ec_semver:format({1, {[],[]}})),
+ ?assertEqual(["1", ".", "2", ".", "34", [], []], ec_semver:format({{1,2,34},{[],[]}})),
+ ?assertEqual(<<"a">>, erlang:iolist_to_binary(ec_semver:format({<<"a">>, {[],[]}}))),
+ ?assertEqual(<<"a.b">>, erlang:iolist_to_binary(ec_semver:format({{<<"a">>,<<"b">>}, {[],[]}}))),
+ ?assertEqual(<<"1">>, erlang:iolist_to_binary(ec_semver:format({1, {[],[]}}))),
+ ?assertEqual(<<"1.2">>, erlang:iolist_to_binary(ec_semver:format({{1,2}, {[],[]}}))),
+ ?assertEqual(<<"1.2.2">>, erlang:iolist_to_binary(ec_semver:format({{1,2,2}, {[],[]}}))),
+ ?assertEqual(<<"1.99.2">>, erlang:iolist_to_binary(ec_semver:format({{1,99,2}, {[],[]}}))),
+ ?assertEqual(<<"1.99.2-alpha">>, erlang:iolist_to_binary(ec_semver:format({{1,99,2}, {[<<"alpha">>],[]}}))),
+ ?assertEqual(<<"1.99.2-alpha.1">>, erlang:iolist_to_binary(ec_semver:format({{1,99,2}, {[<<"alpha">>,1], []}}))),
+ ?assertEqual(<<"1.99.2-pre-alpha.1">>, erlang:iolist_to_binary(ec_semver:format({{1,99,2}, {[<<"pre-alpha">>,1], []}}))),
+ ?assertEqual(<<"1.99.2+build.1.a36">>,
+ erlang:iolist_to_binary(ec_semver:format({{1,99,2}, {[], [<<"build">>, 1, <<"a36">>]}}))),
+ ?assertEqual(<<"1.99.2.44+build.1.a36">>,
+ erlang:iolist_to_binary(ec_semver:format({{1,99,2,44}, {[], [<<"build">>, 1, <<"a36">>]}}))),
+ ?assertEqual(<<"1.99.2-alpha.1+build.1.a36">>,
+ erlang:iolist_to_binary(ec_semver:format({{1,99,2}, {[<<"alpha">>, 1], [<<"build">>, 1, <<"a36">>]}}))),
+ ?assertEqual(<<"1.99.2-pre-alpha.1+build.1.a36">>,
+ erlang:iolist_to_binary(ec_semver:format({{1,99,2}, {[<<"pre-alpha">>, 1], [<<"build">>, 1, <<"a36">>]}}))),
+ ?assertEqual(<<"1">>, erlang:iolist_to_binary(ec_semver:format({1, {[],[]}}))).
diff --git a/test/ec_talk_tests.erl b/test/ec_talk_tests.erl
new file mode 100644
index 0000000..9b7bd07
--- /dev/null
+++ b/test/ec_talk_tests.erl
@@ -0,0 +1,19 @@
+%%% @copyright 2024 Erlware, LLC.
+-module(ec_talk_tests).
+
+-include_lib("eunit/include/eunit.hrl").
+
+general_test_() ->
+ [?_test(42 == ec_talk:get_integer("42")),
+ ?_test(500_211 == ec_talk:get_integer("500211")),
+ ?_test(1_234_567_890 == ec_talk:get_integer("1234567890")),
+ ?_test(12_345_678_901_234_567_890 == ec_talk:get_integer("12345678901234567890")),
+ ?_test(true == ec_talk:get_boolean("true")),
+ ?_test(false == ec_talk:get_boolean("false")),
+ ?_test(true == ec_talk:get_boolean("Ok")),
+ ?_test(true == ec_talk:get_boolean("ok")),
+ ?_test(true == ec_talk:get_boolean("Y")),
+ ?_test(true == ec_talk:get_boolean("y")),
+ ?_test(false == ec_talk:get_boolean("False")),
+ ?_test(false == ec_talk:get_boolean("No")),
+ ?_test(false == ec_talk:get_boolean("no"))].