-%%% @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 221075b..b7b9260 100644
--- a/src/ec_plists.erl
+++ b/src/ec_plists.erl
@@ -1,945 +1,257 @@
-%%% -*- 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
-%%% 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.
-%%%
+%%% simple parrallel map. Originally provided by Joe Armstrong
+%%% on the erlang questions mailing list.
+%%% @end
+%%%-------------------------------------------------------------------
-module(ec_plists).
--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]).
+-export([map/2,
+ map/3,
+ ftmap/2,
+ ftmap/3,
+ filter/2,
+ filter/3]).
--export_type([malt/0, malt_component/0, node_spec/0, fuse/0, fuse_fun/0]).
+%%=============================================================================
+%% Public API
+%%=============================================================================
-%%============================================================================
-%% types
-%%============================================================================
-
--type malt() :: malt_component() | [malt_component()].
-
--type malt_component() :: SubListSize::integer()
- | {processes, integer()}
- | {processes, schedulers}
- | {timeout, Milliseconds::integer()}
- | {nodes, [node_spec()]}.
-
--type node_spec() :: Node::atom()
- | {Node::atom(), NumProcesses::integer()}
- | {Node::atom(), schedulers}.
-
--type fuse_fun() :: fun((term(), term()) -> term()).
--type fuse() :: fuse_fun() | {recursive, fuse_fun()} | {reverse, fuse_fun()}.
--type el_fun() :: fun((term()) -> term()).
-
-%%============================================================================
-%% API
-%%============================================================================
-
-%% Everything here is defined in terms of runmany.
-%% The following methods are convient interfaces to runmany.
-
-%% @doc Same semantics as in module
-%% lists.
--spec all(el_fun(), list()) -> boolean().
-all(Fun, List) ->
- all(Fun, List, 1).
-
-%% @doc Same semantics as in module
-%% lists.
--spec all(el_fun(), list(), malt()) -> boolean().
-all(Fun, List, Malt) ->
- try
- 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
- throw:notall ->
- false
- end.
-
-%% @doc Same semantics as in module
-%% lists.
--spec any(fun(), list()) -> boolean().
-any(Fun, List) ->
- any(Fun, List, 1).
-
-%% @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,
- fun (_A1, _A2) ->
- nil
- end,
- List, Malt) of
- _ ->
- false
- catch throw:any ->
- true
- end.
-
-%% @doc Same semantics as in module
-%% lists.
--spec filter(fun(), list()) -> list().
-filter(Fun, List) ->
- filter(Fun, List, 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().
+%% @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, 1).
+ map(Fun, List, infinity).
-%% @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).
+-spec map(fun(), [any()], non_neg_integer()) -> [any()].
+map(Fun, List, Timeout) ->
+ run_list_fun_in_parallel(map, Fun, List, Timeout).
-%% @doc values are returned as {value, term()}.
--spec ftmap(fun(), list()) -> list().
+%% @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) ->
- map(fun(L) ->
- try
- {value, Fun(L)}
- catch
- Class:Type ->
- {error, {Class, Type}}
- end
- end, List).
+ ftmap(Fun, List, infinity).
-%% @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).
+-spec ftmap(fun(), [any()], non_neg_integer()) -> [{value, any()} | any()].
+ftmap(Fun, List, Timeout) ->
+ run_list_fun_in_parallel(ftmap, Fun, List, Timeout).
-%% @doc Same semantics as in module
-%% lists.
--spec partition(fun(), list()) -> {list(), list()}.
-partition(Fun, List) ->
- partition(Fun, List, 1).
+%% @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).
-%% @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).
+-spec filter(fun(), [any()], integer()) -> [any()].
+filter(Fun, List, Timeout) ->
+ run_list_fun_in_parallel(filter, Fun, List, Timeout).
-%% SORTMALT needs to be tuned
--define(SORTMALT, 100).
+%%=============================================================================
+%% 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).
-%% @doc Same semantics as in module
-%% lists.
--spec sort(list()) -> list().
-sort(List) ->
- sort(fun (A, B) ->
- A =< B
- end,
- List).
+-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 sort(fun(), list()) -> list().
-sort(Fun, List) ->
- sort(Fun, List, ?SORTMALT).
+ wait(Parent, Child, Timeout).
-%% @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,
- Reducer ! {mappers, done, SentMessages},
- Results = receive
- {Reducer, Results2} ->
- Results2;
- {'DOWN', _, _, Reducer, Reason2} ->
- exit(Reason2)
- end,
+-spec wait(pid(), pid(), integer()) -> any().
+wait(Parent, Child, Timeout) ->
receive
- {'DOWN', _, _, Reducer, normal} ->
- nil
- end,
- Results.
-
-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)
+ {Child, Ret} ->
+ Parent ! {self(), Ret}
+ after Timeout ->
+ exit(Child, timeout),
+ Parent ! {self(), timeout}
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.
+-spec gather(atom(), [any()]) -> [any()].
+gather(map, PidElementList) ->
+ map_gather(PidElementList);
+gather(ftmap, PidElementList) ->
+ ftmap_gather(PidElementList);
+gather(filter, PidElementList) ->
+ filter_gather(PidElementList).
-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)
+-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;
-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).
+map_gather([]) ->
+ [].
-cleanup_timer(Timer) ->
+-spec ftmap_gather([pid()]) -> [any()].
+ftmap_gather([{Pid, _E} | Rest]) ->
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)
+ {Pid, Value} -> [Value|ftmap_gather(Rest)]
end;
-cluster_runmany(_, _, [_Non|_Empty], []=_Nodes, []=_Running, _) ->
-%% We have data, but no nodes either available or occupied
- erlang:exit(allnodescrashed).
+ftmap_gather([]) ->
+ [].
-runmany_wrap(Fun, Parent) ->
+-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) ->
try
- Fun()
+ Result = F(E),
+ Parent ! {self(), {value, Result}}
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}}
+ _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}
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]).
+-spec killall([pid()]) -> ok.
+killall([{Pid, _E}|T]) ->
+ exit(Pid, kill),
+ killall(T);
+killall([]) ->
+ ok.
-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).
+%%=============================================================================
+%% Tests
+%%=============================================================================
-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.
+-ifndef(NOTEST).
+-include_lib("eunit/include/eunit.hrl").
-normal_cleanup(Pid) ->
- receive
- {'DOWN', _, _, Pid, _Reason} ->
- ok
- end.
+map_good_test() ->
+ Results = map(fun(_) ->
+ ok
+ end,
+ lists:seq(1, 5), infinity),
+ ?assertMatch([ok, ok, ok, ok, ok],
+ Results).
-%% edge case
-fuse(_, []) ->
- [];
-fuse({reverse, _}=Fuse, Results) ->
- [RL|ResultsR] = lists:reverse(Results),
- fuse(Fuse, ResultsR, RL);
-fuse(Fuse, [R1|Results]) ->
- fuse(Fuse, Results, R1).
+ftmap_good_test() ->
+ Results = ftmap(fun(_) ->
+ ok
+ end,
+ lists:seq(1, 3), infinity),
+ ?assertMatch([{value, ok}, {value, ok}, {value, ok}],
+ Results).
-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.
+filter_good_test() ->
+ Results = filter(fun(X) ->
+ X == show
+ end,
+ [show, show, remove], infinity),
+ ?assertMatch([show, show],
+ Results).
-%% @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).
+map_timeout_test() ->
+ Results =
+ try
+ map(fun(T) ->
+ timer:sleep(T),
+ T
+ end,
+ [1, 100], 10)
+ catch
+ C:E -> {C, E}
+ end,
+ ?assertMatch({throw, timeout}, Results).
-splitmany([], Acc, _) ->
- lists:reverse(Acc);
-splitmany(List, Acc, Size) ->
- {Top, NList} = split(Size, List),
- splitmany(NList, [Top|Acc], Size).
+ftmap_timeout_test() ->
+ Results = ftmap(fun(X) ->
+ timer:sleep(X),
+ true
+ end,
+ [100, 1], 10),
+ ?assertMatch([timeout, {value, true}], Results).
-%% @doc Like lists:split, except it splits a list smaller than its first
-%% parameter
-split(Size, List) ->
- split(Size, List, []).
+filter_timeout_test() ->
+ Results =
+ try
+ filter(fun(T) ->
+ timer:sleep(T),
+ T == 1
+ end,
+ [1, 100], 10)
+ catch
+ C:E -> {C, E}
+ end,
+ ?assertMatch({throw, timeout}, Results).
-split(0, List, Acc) ->
- {lists:reverse(Acc), List};
-split(Size, [H|List], Acc) ->
- split(Size - 1, List, [H|Acc]);
-split(_, [], Acc) ->
- {lists:reverse(Acc), []}.
+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
+ end,
+ lists:seq(1, 5), infinity),
+ ?assertMatch([{value, 1}, test_exception, {value, 3},
+ {value, 4}, {value, 5}] , Results).
+
+-endif.
diff --git a/src/ec_rbdict.erl b/src/ec_rbdict.erl
deleted file mode 100644
index 9f3b617..0000000
--- a/src/ec_rbdict.erl
+++ /dev/null
@@ -1,322 +0,0 @@
-%%% 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 3ffd591..77b43cd 100644
--- a/src/ec_semver.erl
+++ b/src/ec_semver.erl
@@ -1,4 +1,3 @@
-%%% vi:ts=4 sw=4 et
%%%-------------------------------------------------------------------
%%% @copyright (C) 2011, Erlware LLC
%%% @doc
@@ -8,304 +7,113 @@
%%%-------------------------------------------------------------------
-module(ec_semver).
--export([parse/1,
- format/1,
- eql/2,
- gt/2,
- gte/2,
- lt/2,
- lte/2,
- pes/2,
- between/3]).
+-exports([
+ compare/2
+ ]).
-%% For internal use by the ec_semver_parser peg
--export([internal_parse_version/1]).
-
--export_type([semver/0,
- version_string/0,
- any_version/0]).
+-export_type([
+ semvar/0
+ ]).
%%%===================================================================
%%% Public Types
%%%===================================================================
--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().
+-type semvar() :: string().
+-type parsed_semvar() :: {MajorVsn::string(),
+ MinorVsn::string(),
+ PatchVsn::string(),
+ PathString::string()}.
%%%===================================================================
%%% API
%%%===================================================================
-%% @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.
+%% @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)).
%%%===================================================================
%%% 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 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 tokens(semvar()) -> parsed_semvar().
+tokens(Vsn) ->
+ [MajorVsn, MinorVsn, RawPatch] = string:tokens(Vsn, "."),
+ {PatchVsn, PatchString} = split_patch(RawPatch),
+ {MajorVsn, MinorVsn, PatchVsn, PatchString}.
--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()) ->
+ {PatchVsn::string(), PatchStr::string()}.
+split_patch(RawPatch) ->
+ {PatchVsn, PatchStr} = split_patch(RawPatch, {"", ""}),
+ {lists:reverse(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 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 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).
+-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.
diff --git a/src/ec_semver_parser.erl b/src/ec_semver_parser.erl
deleted file mode 100644
index c2fe186..0000000
--- a/src/ec_semver_parser.erl
+++ /dev/null
@@ -1,302 +0,0 @@
--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
new file mode 100644
index 0000000..2a06257
--- /dev/null
+++ b/src/ec_string.erl
@@ -0,0 +1,128 @@
+%%%-------------------------------------------------------------------
+%%% @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 8c3a105..0823169 100644
--- a/src/ec_talk.erl
+++ b/src/ec_talk.erl
@@ -1,5 +1,4 @@
%% -*- 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
@@ -39,21 +38,18 @@
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() :: boolean() | number() | string().
+-type supported() :: string() | boolean() | number().
%%============================================================================
%% API
@@ -80,7 +76,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. Tries to return the
+%% @doc Asks the user to respond to the prompt. Trys to return the
%% value in the format specified by 'Type'.
-spec ask(prompt(), type()) -> supported().
ask(Prompt, boolean) ->
@@ -88,9 +84,9 @@ ask(Prompt, boolean) ->
ask(Prompt, number) ->
ask_convert(Prompt, fun get_integer/1, number, none);
ask(Prompt, string) ->
- ask_convert(Prompt, fun get_string/1, string, none).
+ ask_convert(Prompt, fun get_integer/1, string, none).
-%% @doc Asks the user to respond to the prompt. Tries to return the
+%% @doc Asks the user to respond to the prompt. Trys to return the
%% value in the format specified by 'Type'.
-spec ask_default(prompt(), type(), supported()) -> supported().
ask_default(Prompt, boolean, Default) ->
@@ -104,11 +100,8 @@ ask_default(Prompt, string, Default) ->
%% between min and max.
-spec ask(prompt(), number(), number()) -> number().
ask(Prompt, Min, Max)
- 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),
+ when is_list(Prompt), is_number(Min), is_number(Max) ->
+ Res = ask(Prompt, fun get_integer/1, none),
case (Res >= Min andalso Res =< Max) of
true ->
Res;
@@ -122,17 +115,15 @@ 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((any()) -> any()), type(), supported() | none) -> supported().
+-spec ask_convert(prompt(), fun(), type(), supported()) -> supported().
ask_convert(Prompt, TransFun, Type, Default) ->
- 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]),
+ 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),
Ret = TransFun(Data),
case Ret of
no_data ->
@@ -150,7 +141,7 @@ ask_convert(Prompt, TransFun, Type, Default) ->
Ret
end.
-%% @doc Tries to translate the result into a boolean
+%% @doc Trys to translate the result into a boolean
-spec get_boolean(string()) -> boolean().
get_boolean([]) ->
no_data;
@@ -177,7 +168,7 @@ get_boolean([$N | _]) ->
get_boolean(_) ->
no_clue.
-%% @doc Tries to translate the result into an integer
+%% @doc Trys to translate the result into an integer
-spec get_integer(string()) -> integer().
get_integer([]) ->
no_data;
@@ -201,3 +192,21 @@ 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
deleted file mode 100644
index e407b9f..0000000
--- a/src/ec_vsn.erl
+++ /dev/null
@@ -1,51 +0,0 @@
-%%% 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
deleted file mode 100644
index 7709d81..0000000
--- a/src/erlware_commons.app.src
+++ /dev/null
@@ -1,11 +0,0 @@
-{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
deleted file mode 100644
index f1d1181..0000000
--- a/test/ec_cmd_log_tests.erl
+++ /dev/null
@@ -1,39 +0,0 @@
-%%% @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
deleted file mode 100644
index 6bbad6e..0000000
--- a/test/ec_cnv_tests.erl
+++ /dev/null
@@ -1,28 +0,0 @@
-%%% @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
deleted file mode 100644
index 885f3dc..0000000
--- a/test/ec_file_tests.erl
+++ /dev/null
@@ -1,84 +0,0 @@
-%%% @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
deleted file mode 100644
index 2c0ee12..0000000
--- a/test/ec_gb_trees_tests.erl
+++ /dev/null
@@ -1,67 +0,0 @@
-%%% @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
deleted file mode 100644
index 0d2efe1..0000000
--- a/test/ec_git_vsn_tests.erl
+++ /dev/null
@@ -1,13 +0,0 @@
-%%% @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
deleted file mode 100644
index f6f4025..0000000
--- a/test/ec_lists_tests.erl
+++ /dev/null
@@ -1,172 +0,0 @@
-%%% @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
deleted file mode 100644
index 3f945e9..0000000
--- a/test/ec_plists_tests.erl
+++ /dev/null
@@ -1,84 +0,0 @@
-%%% @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
deleted file mode 100644
index 0d3a18a..0000000
--- a/test/ec_semver_tests.erl
+++ /dev/null
@@ -1,447 +0,0 @@
-%%% @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
deleted file mode 100644
index 9b7bd07..0000000
--- a/test/ec_talk_tests.erl
+++ /dev/null
@@ -1,19 +0,0 @@
-%%% @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"))].