Merge branch 'next'

* next:
  version bump 0.8.0
  minor fixes and enhancements to the makefile
  given a complete semver parser ec_string no longer makes sense to retain
  suport proper semver parsing and comparison in the semver module
  move ec_file away from exceptions to return values
  minor whitespace cleanup
  make sure ec_dictionary gets built first
  fixes to dialyzer
  add travis support to the system
  fix edoc errors in various modules
  add a full makefile that drives rebar
  add rebar files to gitignore
This commit is contained in:
Eric Merritt 2012-09-08 10:00:03 -05:00
commit 25ef2f3496
19 changed files with 823 additions and 466 deletions

10
.gitignore vendored
View file

@ -1,5 +1,13 @@
*.beam
.erlware_commons_plt
.eunit/*
deps/*
doc/*.html
doc/*.css
doc/edoc-info
doc/erlang.png
ebin/*
.*
_build
erl_crash.dump
*.pyc
src/ec_semver_parser.erl

12
.travis.yml Normal file
View file

@ -0,0 +1,12 @@
language: erlang
otp_release:
- R15B02
- R15B01
- R15B
before_script: "make get-deps"
script: "make"
branches:
only:
- master
- next

64
Makefile Normal file
View file

@ -0,0 +1,64 @@
# Copyright 2012 Erlware, LLC. All Rights Reserved.
#
# BSD License see COPYING
ERL = $(shell which erl)
ERLFLAGS= -pa $(CURDIR)/.eunit -pa $(CURDIR)/ebin -pa $(CURDIR)/*/ebin
REBAR=$(shell which rebar)
ifeq ($(REBAR),)
$(error "Rebar not available on this system")
endif
ERLWARE_COMMONS_PLT=$(CURDIR)/.erlware_commons_plt
.PHONY: all compile doc clean test dialyzer typer shell distclean pdf get-deps escript
all: compile test dialyzer
get-deps:
$(REBAR) get-deps
$(REBAR) compile
compile:
$(REBAR) skip_deps=true compile
doc: compile
$(REBAR) skip_deps=true doc
test: compile
$(REBAR) skip_deps=true eunit
$(ERLWARE_COMMONS_PLT):
@echo Building local plt at $(ERLWARE_COMMONS_PLT)
@echo
- dialyzer --output_plt $(ERLWARE_COMMONS_PLT) --build_plt \
--apps erts kernel stdlib eunit -r deps
dialyzer: $(ERLWARE_COMMONS_PLT)
dialyzer --plt $(ERLWARE_COMMONS_PLT) -Wrace_conditions --src src
typer:
typer --plt $(ERLWARE_COMMONS_PLT) -r ./src
shell: compile
# You often want *rebuilt* rebar tests to be available to the
# shell you have to call eunit (to get the tests
# rebuilt). However, eunit runs the tests, which probably
# fails (thats probably why You want them in the shell). This
# runs eunit but tells make to ignore the result.
- @$(REBAR) skip_deps=true eunit
@$(ERL) $(ERLFLAGS)
clean:
$(REBAR) skip_deps=true clean
- rm $(CURDIR)/doc/*.html
- rm $(CURDIR)/doc/*.css
- rm $(CURDIR)/doc/*.png
- rm $(CURDIR)/doc/edoc-info
distclean: clean
rm -rf $(ERLWARE_COMMONS_PLT)
rm -rvf $(CURDIR)/deps/*

View file

@ -1,6 +1,10 @@
Erlware Commons
===============
Current Status
--------------
[![Build Status](https://secure.travis-ci.org/erlware/erlware_commons.png)](http://travis-ci.org/erlware/erlware_commons)
Introduction
------------

View file

@ -1,3 +1,12 @@
%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*-
%% These are all only compile time dependencies
{deps, [{neotoma, "",
{git, "https://github.com/seancribbs/neotoma.git", {tag, "1.5"}}},
{proper, "", {git, "https://github.com/manopapad/proper.git", {branch, master}}}]}.
{erl_first_files, ["ec_dictionary"]}.
{erl_opts,
[debug_info,
warnings_as_errors]}.

View file

@ -4,8 +4,8 @@
%%% @doc
%%% provides an implementation of ec_dictionary using an association
%%% list as a basy
%%% see ec_dictionary
%%% @end
%%% @see ec_dictionary
%%%-------------------------------------------------------------------
-module(ec_assoc_list).
@ -29,7 +29,9 @@
%%%===================================================================
%%% Types
%%%===================================================================
-opaque dictionary(K, V) :: {ec_assoc_list,
%% 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) :: {ec_assoc_list,
[{ec_dictionary:key(K), ec_dictionary:value(V)}]}.
%%%===================================================================
@ -82,7 +84,7 @@ remove(Key, {ec_assoc_list, Data}) ->
has_value(Value, {ec_assoc_list, Data}) ->
lists:keymember(Value, 2, Data).
-spec size(Object::dictionary(_K, _V)) -> integer().
-spec size(Object::dictionary(_K, _V)) -> non_neg_integer().
size({ec_assoc_list, Data}) ->
length(Data).

View file

@ -5,9 +5,9 @@
%%% This provides an implementation of the ec_dictionary type using
%%% erlang dicts as a base. The function documentation for
%%% ec_dictionary applies here as well.
%%% see ec_dictionary
%%% see dict
%%% @end
%%% @see ec_dictionary
%%% @see dict
%%%-------------------------------------------------------------------
-module(ec_dict).
@ -31,7 +31,9 @@
%%%===================================================================
%%% Types
%%%===================================================================
-opaque dictionary(_K, _V) :: dict().
%% 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) :: dict().
%%%===================================================================
%%% API
@ -88,7 +90,7 @@ has_value(Value, Data) ->
false,
Data).
-spec size(Object::dictionary(_K, _V)) -> integer().
-spec size(Object::dictionary(_K, _V)) -> non_neg_integer().
size(Data) ->
dict:size(Data).

View file

@ -10,9 +10,6 @@
%%%-------------------------------------------------------------------
-module(ec_dictionary).
%%% Behaviour Callbacks
-export([behaviour_info/1]).
%% API
-export([new/1,
has_key/2,
@ -38,30 +35,27 @@
{callback,
data}).
-opaque dictionary(_K, _V) :: #dict_t{}.
%% 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) :: #dict_t{}.
-type key(T) :: T.
-type value(T) :: T.
-callback new() -> any().
-callback has_key(key(any()), any()) -> boolean().
-callback get(key(any()), any()) -> any().
-callback add(key(any()), value(any()), T) -> T.
-callback remove(key(any()), T) -> T.
-callback has_value(value(any()), any()) -> boolean().
-callback size(any()) -> non_neg_integer().
-callback to_list(any()) -> [{key(any()), value(any())}].
-callback from_list([{key(any()), value(any())}]) -> any().
-callback keys(any()) -> [key(any())].
%%%===================================================================
%%% API
%%%===================================================================
%% @doc export the behaviour callbacks for this type
%% @private
behaviour_info(callbacks) ->
[{new, 0},
{has_key, 2},
{get, 2},
{add, 3},
{remove, 2},
{has_value, 2},
{size, 1},
{to_list, 1},
{from_list, 1},
{keys, 1}];
behaviour_info(_) ->
undefined.
%% @doc create a new dictionary object from the specified module. The
%% module should implement the dictionary behaviour.
%%
@ -83,7 +77,7 @@ has_key(Key, #dict_t{callback = Mod, data = Data}) ->
%%
%% @param Dict The dictionary object to return the value from
%% @param Key The key requested
%% @throws not_found when the key does not exist
%% when the key does not exist @throws not_found
-spec get(key(K), dictionary(K, V)) -> value(V).
get(Key, #dict_t{callback = Mod, data = Data}) ->
Mod:get(Key, Data).

View file

@ -23,34 +23,24 @@
]).
-export_type([
path/0,
option/0
]).
-include_lib("kernel/include/file.hrl").
%% User friendly exception message (remove line and module info once we
%% get them in stack traces)
-define(UEX(Exception, UMSG, UVARS),
{uex, {?MODULE,
?LINE,
Exception,
lists:flatten(io_lib:fwrite(UMSG, UVARS))}}).
-define(CHECK_PERMS_MSG,
"Try checking that you have the correct permissions and try again~n").
%%============================================================================
%% Types
%%============================================================================
-type path() :: string().
-type option() :: [atom()].
-type option() :: recursive.
-type void() :: ok.
%%%===================================================================
%%% API
%%%===================================================================
%% @doc copy an entire directory to another location.
-spec copy(path(), path(), Options::[option()]) -> ok.
-spec copy(file:name(), file:name(), Options::[option()]) -> void().
copy(From, To, []) ->
copy(From, To);
copy(From, To, [recursive] = Options) ->
@ -63,12 +53,23 @@ copy(From, To, [recursive] = Options) ->
end.
%% @doc copy a file including timestamps,ownership and mode etc.
-spec copy(From::string(), To::string()) -> ok.
-spec copy(From::file:filename(), To::file:filename()) -> ok | {error, Reason::term()}.
copy(From, To) ->
try
ec_file_copy(From, To)
catch
_C:E -> throw(?UEX({copy_failed, E}, ?CHECK_PERMS_MSG, []))
case file:copy(From, To) of
{ok, _} ->
case file:read_file_info(From) of
{ok, FileInfo} ->
case file:write_file_info(To, FileInfo) of
ok ->
ok;
{error, WFError} ->
{error, {write_file_info_failed, WFError}}
end;
{error, RFError} ->
{error, {read_file_info_failed, RFError}}
end;
{error, Error} ->
{error, {copy_failed, Error}}
end.
%% @doc return an md5 checksum string or a binary. Same as unix utility of
@ -81,21 +82,21 @@ md5sum(Value) ->
%% <pre>
%% Example: remove("./tmp_dir", [recursive]).
%% </pre>
-spec remove(path(), Options::[option()]) -> ok | {error, Reason::term()}.
-spec remove(file:name(), Options::[option()]) -> ok | {error, Reason::term()}.
remove(Path, Options) ->
try
ok = ec_file_remove(Path, Options)
catch
_C:E -> throw(?UEX({remove_failed, E}, ?CHECK_PERMS_MSG, []))
case lists:member(recursive, Options) of
false -> file:delete(Path);
true -> remove_recursive(Path, Options)
end.
%% @doc delete a file.
-spec remove(path()) -> ok | {error, Reason::term()}.
-spec remove(file:name()) -> ok | {error, Reason::term()}.
remove(Path) ->
remove(Path, []).
%% @doc indicates witha boolean if the path supplied refers to symlink.
-spec is_symlink(path()) -> boolean().
-spec is_symlink(file:name()) -> boolean().
is_symlink(Path) ->
case file:read_link_info(Path) of
{ok, #file_info{type = symlink}} ->
@ -107,93 +108,70 @@ is_symlink(Path) ->
%% @doc make a unique temorory directory. Similar function to BSD stdlib
%% function of the same name.
-spec insecure_mkdtemp() -> TmpDirPath::path().
-spec insecure_mkdtemp() -> TmpDirPath::file:name().
insecure_mkdtemp() ->
random:seed(now()),
UniqueNumber = erlang:integer_to_list(erlang:trunc(random:uniform() * 1000000000000)),
TmpDirPath =
filename:join([tmp(), lists:flatten([".tmp_dir", UniqueNumber])]),
case filelib:is_dir(TmpDirPath) of
true ->
throw(?UEX({mkdtemp_failed, file_exists}, "tmp directory exists", []));
false ->
try
ok = mkdir_path(TmpDirPath),
TmpDirPath
catch
_C:E -> throw(?UEX({mkdtemp_failed, E}, ?CHECK_PERMS_MSG, []))
end
case mkdir_path(TmpDirPath) of
ok -> TmpDirPath;
Error -> Error
end.
%% @doc Makes a directory including parent dirs if they are missing.
-spec mkdir_path(path()) -> ok.
mkdir_path(Path) ->
-spec mkdir_p(file:name()) -> ok | {error, Reason::term()}.
mkdir_p(Path) ->
%% We are exploiting a feature of ensuredir that that creates all
%% directories up to the last element in the filename, then ignores
%% that last element. This way we ensure that the dir is created
%% and not have any worries about path names
DirName = filename:join([filename:absname(Path), "tmp"]),
try
ok = filelib:ensure_dir(DirName)
catch
_C:E -> throw(?UEX({mkdir_path_failed, E}, ?CHECK_PERMS_MSG, []))
end.
filelib:ensure_dir(DirName).
%% @doc Makes a directory including parent dirs if they are missing.
-spec mkdir_path(file:name()) -> ok | {error, Reason::term()}.
mkdir_path(Path) ->
mkdir_p(Path).
%% @doc consult an erlang term file from the file system.
%% Provide user readible exeption on failure.
-spec consult(FilePath::path()) -> term().
-spec consult(FilePath::file:name()) -> term().
consult(FilePath) ->
case file:consult(FilePath) of
{ok, [Term]} ->
Term;
{error, Error} ->
Msg = "The file at ~p~n" ++
"is either not a valid Erlang term, does not to exist~n" ++
"or you lack the permissions to read it. Please check~n" ++
"to see if the file exists and that it has the correct~n" ++
"permissions~n",
throw(?UEX({failed_to_consult_file, {FilePath, Error}},
Msg, [FilePath]))
Error ->
Error
end.
%% @doc read a file from the file system. Provide UEX exeption on failure.
-spec read(FilePath::string()) -> binary().
-spec read(FilePath::file:filename()) -> binary() | {error, Reason::term()}.
read(FilePath) ->
try
{ok, FileBin} = file:read_file(FilePath),
FileBin
catch
_C:E -> throw(?UEX({read_failed, {FilePath, E}},
"Read failed for the file ~p with ~p~n" ++
?CHECK_PERMS_MSG,
[FilePath, E]))
end.
%% Now that we are moving away from exceptions again this becomes
%% a bit redundant but we want to be backwards compatible as much
%% as possible in the api.
file:read_file(FilePath).
%% @doc write a file to the file system. Provide UEX exeption on failure.
-spec write(FileName::string(), Contents::string()) -> ok.
-spec write(FileName::file:filename(), Contents::string()) -> ok | {error, Reason::term()}.
write(FileName, Contents) ->
case file:write_file(FileName, Contents) of
ok ->
ok;
{error, Reason} ->
Msg = "Writing the file ~s to disk failed with reason ~p.~n" ++
?CHECK_PERMS_MSG,
throw(?UEX({write_file_failure, {FileName, Reason}},
Msg,
[FileName, Reason]))
end.
%% Now that we are moving away from exceptions again this becomes
%% a bit redundant but we want to be backwards compatible as much
%% as possible in the api.
file:write_file(FileName, Contents).
%% @doc write a term out to a file so that it can be consulted later.
-spec write_term(string(), term()) -> ok.
-spec write_term(file:filename(), term()) -> ok | {error, Reason::term()}.
write_term(FileName, Term) ->
write(FileName, lists:flatten(io_lib:fwrite("~p. ", [Term]))).
%% @doc Finds files and directories that match the regexp supplied in
%% the TargetPattern regexp.
-spec find(FromDir::path(), TargetPattern::string()) -> [path()].
-spec find(FromDir::file:name(), TargetPattern::string()) -> [file:name()].
find([], _) ->
[];
find(FromDir, TargetPattern) ->
@ -214,7 +192,7 @@ find(FromDir, TargetPattern) ->
%%%===================================================================
%%% Internal Functions
%%%===================================================================
-spec find_in_subdirs(path(), string()) -> [path()].
-spec find_in_subdirs(file:name(), string()) -> [file:name()].
find_in_subdirs(FromDir, TargetPattern) ->
lists:foldl(fun(CheckFromDir, Acc)
when CheckFromDir == FromDir ->
@ -228,14 +206,8 @@ find_in_subdirs(FromDir, TargetPattern) ->
[],
filelib:wildcard(filename:join(FromDir, "*"))).
-spec ec_file_remove(path(), [{atom(), any()}]) -> ok.
ec_file_remove(Path, Options) ->
case lists:member(recursive, Options) of
false -> file:delete(Path);
true -> remove_recursive(Path, Options)
end.
-spec remove_recursive(path(), Options::list()) -> ok.
-spec remove_recursive(file:name(), Options::list()) -> ok | {error, Reason::term()}.
remove_recursive(Path, Options) ->
case filelib:is_dir(Path) of
false ->
@ -244,10 +216,10 @@ remove_recursive(Path, Options) ->
lists:foreach(fun(ChildPath) ->
remove_recursive(ChildPath, Options)
end, filelib:wildcard(filename:join(Path, "*"))),
ok = file:del_dir(Path)
file:del_dir(Path)
end.
-spec tmp() -> path().
-spec tmp() -> file:name().
tmp() ->
case erlang:system_info(system_architecture) of
"win32" ->
@ -257,7 +229,7 @@ tmp() ->
end.
%% Copy the subfiles of the From directory to the to directory.
-spec copy_subfiles(path(), path(), [option()]) -> ok.
-spec copy_subfiles(file:name(), file:name(), [option()]) -> void().
copy_subfiles(From, To, Options) ->
Fun =
fun(ChildFrom) ->
@ -266,17 +238,11 @@ copy_subfiles(From, To, Options) ->
end,
lists:foreach(Fun, filelib:wildcard(filename:join(From, "*"))).
-spec ec_file_copy(path(), path()) -> ok.
ec_file_copy(From, To) ->
{ok, _} = file:copy(From, To),
{ok, FileInfo} = file:read_file_info(From),
ok = file:write_file_info(To, FileInfo).
-spec make_dir_if_dir(path()) -> ok.
-spec make_dir_if_dir(file:name()) -> ok | {error, Reason::term()}.
make_dir_if_dir(File) ->
case filelib:is_dir(File) of
true -> ok;
false -> ok = mkdir_path(File)
false -> mkdir_path(File)
end.
%% @doc convert a list of integers into hex.
@ -320,7 +286,7 @@ file_test() ->
filelib:ensure_dir(TermFileCopy),
write_term(TermFile, "term"),
?assertMatch("term", consult(TermFile)),
?assertMatch(<<"\"term\". ">>, read(TermFile)),
?assertMatch({ok, <<"\"term\". ">>}, read(TermFile)),
copy(filename:dirname(TermFile),
filename:dirname(TermFileCopy),
[recursive]),
@ -356,5 +322,4 @@ find_test() ->
find(BaseDir, "file[a-z]+\$")),
remove(BaseDir, [recursive]).
-endif.

View file

@ -4,9 +4,9 @@
%%% @doc
%%% This provides an implementation of the type ec_dictionary using
%%% gb_trees as a backin
%%% see ec_dictionary
%%% see gb_trees
%%% @end
%%% @see ec_dictionary
%%% @see gb_trees
%%%-------------------------------------------------------------------
-module(ec_gb_trees).
@ -30,7 +30,9 @@
%%%===================================================================
%%% Types
%%%===================================================================
-opaque dictionary(K, V) :: {non_neg_integer(), ec_gb_tree_node(K, V)}.
%% 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) :: {non_neg_integer(), ec_gb_tree_node(K, V)}.
-type ec_gb_tree_node(K, V) :: 'nil' | {K, V,
ec_gb_tree_node(K, V),
@ -68,7 +70,7 @@ has_key(Key, Data) ->
%%
%% @param Object The dictionary object to return the value from
%% @param Key The key requested
%% @throws not_found when the key does not exist
%% when the key does not exist @throws not_found
-spec get(ec_dictionary:key(K), Object::dictionary(K, V)) ->
ec_dictionary:value(V).
get(Key, Data) ->
@ -124,7 +126,7 @@ has_value(Value, Data) ->
%% @doc return the current number of key value pairs in the dictionary
%%
%% @param Object the object return the size for.
-spec size(Object::dictionary(_K, _V)) -> integer().
-spec size(Object::dictionary(_K, _V)) -> non_neg_integer().
size(Data) ->
gb_trees:size(Data).

View file

@ -23,7 +23,7 @@
%% the third value is the element passed to the function. The purpose
%% of this is to allow a list to be searched where some internal state
%% is important while the input element is not.
-spec search(fun(), list()) -> {ok, Result::term(), Element::term()}.
-spec search(fun(), list()) -> {ok, Result::term(), Element::term()} | not_found.
search(Fun, [H|T]) ->
case Fun(H) of
{ok, Value} ->

View file

@ -5,9 +5,9 @@
%%% 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
%%% @see ec_dictionary
%%% @see orddict
%%%-------------------------------------------------------------------
-module(ec_orddict).
@ -31,7 +31,9 @@
%%%===================================================================
%%% Types
%%%===================================================================
-opaque dictionary(K, V) :: [{K, V}].
%% 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
@ -88,7 +90,7 @@ has_value(Value, Data) ->
false,
Data).
-spec size(Object::dictionary(_K, _V)) -> integer().
-spec size(Object::dictionary(_K, _V)) -> non_neg_integer().
size(Data) ->
orddict:size(Data).

View file

@ -13,6 +13,13 @@
filter/2,
filter/3]).
-export_type([thunk/0]).
%%=============================================================================
%% Types
%%=============================================================================
-type thunk() :: fun((any()) -> any()).
%%=============================================================================
%% Public API
%%=============================================================================
@ -25,7 +32,7 @@
map(Fun, List) ->
map(Fun, List, infinity).
-spec map(fun(), [any()], non_neg_integer()) -> [any()].
-spec map(thunk(), [any()], timeout() | infinity) -> [any()].
map(Fun, List, Timeout) ->
run_list_fun_in_parallel(map, Fun, List, Timeout).
@ -43,29 +50,29 @@ map(Fun, List, Timeout) ->
%% 2> ftmap(fun(N) -> factorial(N) end, [1, 2, 1000000, "not num"], 100)
%% [{value, 1}, {value, 2}, timeout, {badmatch, ...}]
%% </pre>
-spec ftmap(fun(), [any()]) -> [{value, any()} | any()].
-spec ftmap(thunk(), [any()]) -> [{value, any()} | any()].
ftmap(Fun, List) ->
ftmap(Fun, List, infinity).
-spec ftmap(fun(), [any()], non_neg_integer()) -> [{value, any()} | any()].
-spec ftmap(thunk(), [any()], timeout() | infinity) -> [{value, any()} | any()].
ftmap(Fun, List, Timeout) ->
run_list_fun_in_parallel(ftmap, Fun, List, Timeout).
%% @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()].
-spec filter(thunk(), [any()]) -> [any()].
filter(Fun, List) ->
filter(Fun, List, infinity).
-spec filter(fun(), [any()], integer()) -> [any()].
-spec filter(thunk(), [any()], timeout() | infinity) -> [any()].
filter(Fun, List, Timeout) ->
run_list_fun_in_parallel(filter, Fun, List, Timeout).
%%=============================================================================
%% Internal API
%%=============================================================================
-spec run_list_fun_in_parallel(atom(), fun(), [any()], integer()) -> [any()].
-spec run_list_fun_in_parallel(atom(), thunk(), [any()], timeout() | infinity) -> [any()].
run_list_fun_in_parallel(ListFun, Fun, List, Timeout) ->
LocalPid = self(),
Pids =
@ -79,7 +86,7 @@ run_list_fun_in_parallel(ListFun, Fun, List, Timeout) ->
end, List),
gather(ListFun, Pids).
-spec wait(pid(), fun(), any(), integer()) -> any().
-spec wait(pid(), thunk(), any(), timeout() | infinity) -> any().
wait(Parent, Fun, E, Timeout) ->
WaitPid = self(),
Child = spawn(fun() ->
@ -88,7 +95,7 @@ wait(Parent, Fun, E, Timeout) ->
wait(Parent, Child, Timeout).
-spec wait(pid(), pid(), integer()) -> any().
-spec wait(pid(), pid(), timeout() | infinity) -> any().
wait(Parent, Child, Timeout) ->
receive
{Child, Ret} ->
@ -146,7 +153,7 @@ filter_gather([{Pid, E} | Rest]) ->
filter_gather([]) ->
[].
-spec do_f(pid(), fun(), any()) -> no_return().
-spec do_f(pid(), thunk(), any()) -> no_return().
do_f(Parent, F, E) ->
try
Result = F(E),

View file

@ -51,8 +51,8 @@
%%% l/rbalance, the colour, in store etc. is actually slower than not
%%% doing it. Measured.
%%%
%%% see ec_dictionary
%%% @end
%%% @see ec_dictionary
%%%-------------------------------------------------------------------
-module(ec_rbdict).
@ -68,8 +68,9 @@
%%%===================================================================
%%% Types
%%%===================================================================
-opaque dictionary(K, V) :: empty | {color(),
%% 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),
@ -116,7 +117,7 @@ get(K, Default, {_, _, K1, _, Right}) when K > K1 ->
get(_, _, {_, _, _, Val, _}) ->
Val.
-spec add(ec_dicitonary:key(K), ec_dictionary:value(V),
-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),
@ -133,7 +134,7 @@ has_value(Value, Dict) ->
end,
false, Dict).
-spec size(dictionary(_K, _V)) -> integer().
-spec size(dictionary(_K, _V)) -> non_neg_integer().
size(T) ->
size1(T).
@ -227,7 +228,7 @@ erase_aux(K, {r, A, Xk, Xv, B}) ->
end.
-spec erase_min(dictionary(K, V)) ->
{dictionary(K, V), {ec_dictionary:key(K), ec_dictionary:value(V)}, boolean}.
{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}}) ->
@ -274,7 +275,8 @@ unbalright(b, A, Xk, Xv,
D},
false}.
-spec fold(fun(), dictionary(K, V), dictionary(K, V)) -> dictionary(K, V).
-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).
@ -295,7 +297,7 @@ to_list({_, A, Xk, Xv, B}, List) ->
%% Balance a tree afer (possibly) adding a node to the left/right.
-spec lbalance(color(), dictionary(K, V),
ec_dictinary:key(K), ec_dictionary:value(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,
@ -307,7 +309,7 @@ lbalance(b, {r, A, Xk, Xv, {r, B, Yk, Yv, C}}, Zk, Zv,
lbalance(C, A, Xk, Xv, B) -> {C, A, Xk, Xv, B}.
-spec rbalance(color(), dictionary(K, V),
ec_dictinary:key(K), ec_dictionary:value(V),
ec_dictionary:key(K), ec_dictionary:value(V),
dictionary(K, V)) ->
dictionary(K, V).
rbalance(b, A, Xk, Xv,

View file

@ -7,93 +7,213 @@
%%%-------------------------------------------------------------------
-module(ec_semver).
-exports([
compare/2
]).
-export([parse/1,
eql/2,
gt/2,
gte/2,
lt/2,
lte/2,
pes/2,
between/3]).
-export_type([
semvar/0
]).
%% For internal use by the ec_semver_parser peg
-export([internal_parse_version/1]).
-export_type([semver/0,
version_string/0,
any_version/0]).
%%%===================================================================
%%% Public Types
%%%===================================================================
-type semvar() :: string().
-type parsed_semvar() :: {MajorVsn::string(),
MinorVsn::string(),
PatchVsn::string(),
PathString::string()}.
-type major_minor_patch() ::
non_neg_integer()
| {non_neg_integer(), non_neg_integer()}
| {non_neg_integer(), non_neg_integer(), non_neg_integer()}.
-type alpha_part() :: integer() | binary().
-type semver() :: {major_minor_patch(), {PreReleaseVersion::[alpha_part()],
BuildVersion::[alpha_part()]}}.
-type version_string() :: string() | binary().
-type any_version() :: version_string() | semver().
%%%===================================================================
%%% API
%%%===================================================================
%% @doc Is semver version string A bigger than version string B?
%% <pre>
%% Example: compare("3.2.5alpha", "3.10.6") returns: false
%% </pre>
-spec compare(VsnA::string(), VsnB::string()) -> boolean().
compare(VsnA, VsnB) ->
compare_toks(tokens(VsnA),tokens(VsnB)).
%% @doc parse a string or binary into a valid semver representation
-spec parse(any_version()) -> semver().
parse(Version) when erlang:is_list(Version) ->
ec_semver_parser:parse(Version);
parse(Version) when erlang:is_binary(Version) ->
ec_semver_parser:parse(Version);
parse(Version) ->
Version.
%% @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 grammer to parse the iolist into a semver
-spec internal_parse_version(iolist()) -> semver().
internal_parse_version([MMP, AlphaPart, BuildPart, _]) ->
{parse_major_minor_patch(MMP), {parse_alpha_part(AlphaPart),
parse_alpha_part(BuildPart)}}.
%% @doc helper function for the peg grammer to parse the iolist into a major_minor_patch
-spec parse_major_minor_patch(iolist()) -> major_minor_patch().
parse_major_minor_patch([MajVsn, [], []]) ->
MajVsn;
parse_major_minor_patch([MajVsn, [<<".">>, MinVsn], []]) ->
{MajVsn, MinVsn};
parse_major_minor_patch([MajVsn, [<<".">>, MinVsn], [<<".">>, PatchVsn]]) ->
{MajVsn, MinVsn, PatchVsn}.
%% @doc helper function for the peg grammer to parse the iolist into an alpha part
-spec parse_alpha_part(iolist()) -> [alpha_part()].
parse_alpha_part([]) ->
[];
parse_alpha_part([_, AV1, Rest]) ->
[erlang:iolist_to_binary(AV1) |
[format_alpha_part(Part) || Part <- Rest]].
%% @doc according to semver alpha parts that can be treated like
%% numbers must be. We implement that here by taking the alpha part
%% and trying to convert it to a number, if it succeeds we use
%% it. Otherwise we do not.
-spec format_alpha_part(iolist()) -> integer() | binary().
format_alpha_part([<<".">>, AlphaPart]) ->
Bin = erlang:iolist_to_binary(AlphaPart),
try
erlang:list_to_integer(erlang:binary_to_list(Bin))
catch
error:badarg ->
Bin
end.
%%%===================================================================
%%% Internal Functions
%%%===================================================================
%% @doc normalize the semver so they can be compared
-spec normalize(semver()) -> semver().
normalize({Vsn, Rest})
when erlang:is_integer(Vsn) ->
{{Vsn, 0, 0}, Rest};
normalize({{Maj, Min}, Rest}) ->
{{Maj, Min, 0}, Rest};
normalize(Other) ->
Other.
-spec tokens(semvar()) -> parsed_semvar().
tokens(Vsn) ->
[MajorVsn, MinorVsn, RawPatch] = string:tokens(Vsn, "."),
{PatchVsn, PatchString} = split_patch(RawPatch),
{MajorVsn, MinorVsn, PatchVsn, PatchString}.
%% @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.
internal_pes(VsnA, {{LM, LMI}, _}) ->
gte(VsnA, {{LM, LMI, 0}, {[], []}}) andalso
lt(VsnA, {{LM + 1, 0, 0}, {[], []}});
internal_pes(VsnA, {{LM, LMI, LP}, _}) ->
gte(VsnA, {{LM, LMI, LP}, {[], []}})
andalso
lt(VsnA, {{LM, LMI + 1, 0}, {[], []}});
internal_pes(Vsn, LVsn) ->
gte(Vsn, LVsn).
-spec split_patch(string()) ->
{PatchVsn::string(), PatchStr::string()}.
split_patch(RawPatch) ->
{PatchVsn, PatchStr} = split_patch(RawPatch, {"", ""}),
{lists:reverse(PatchVsn), PatchStr}.
-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}.
-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
@ -102,18 +222,305 @@ to_int(String) ->
-ifndef(NOTEST).
-include_lib("eunit/include/eunit.hrl").
split_patch_test() ->
?assertMatch({"123", "alpha1"}, split_patch("123alpha1")).
eql_test() ->
?assertMatch(true, eql("1.0.0-alpha",
"1.0.0-alpha")),
?assertMatch(true, eql("1",
"1.0.0")),
?assertMatch(true, eql("1.0",
"1.0.0")),
?assertMatch(true, eql("1.0.0",
"1")),
?assertMatch(true, eql("1.0+alpha.1",
"1.0.0+alpha.1")),
?assertMatch(true, eql("1.0-alpha.1+build.1",
"1.0.0-alpha.1+build.1")),
?assertMatch(true, not eql("1.0.0",
"1.0.1")),
?assertMatch(true, not eql("1.0.0-alpha",
"1.0.1+alpha")),
?assertMatch(true, not eql("1.0.0+build.1",
"1.0.1+build.2")).
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")).
gt_test() ->
?assertMatch(true, gt("1.0.0-alpha.1",
"1.0.0-alpha")),
?assertMatch(true, gt("1.0.0-beta.2",
"1.0.0-alpha.1")),
?assertMatch(true, gt("1.0.0-beta.11",
"1.0.0-beta.2")),
?assertMatch(true, gt("1.0.0-rc.1", "1.0.0-beta.11")),
?assertMatch(true, gt("1.0.0-rc.1+build.1", "1.0.0-rc.1")),
?assertMatch(true, gt("1.0.0", "1.0.0-rc.1+build.1")),
?assertMatch(true, gt("1.0.0+0.3.7", "1.0.0")),
?assertMatch(true, gt("1.3.7+build", "1.0.0+0.3.7")),
?assertMatch(true, gt("1.3.7+build.2.b8f12d7",
"1.3.7+build")),
?assertMatch(true, gt("1.3.7+build.11.e0f985a",
"1.3.7+build.2.b8f12d7")),
?assertMatch(true, not gt("1.0.0-alpha",
"1.0.0-alpha.1")),
?assertMatch(true, not gt("1.0.0-alpha.1",
"1.0.0-beta.2")),
?assertMatch(true, not gt("1.0.0-beta.2",
"1.0.0-beta.11")),
?assertMatch(true, not gt("1.0.0-beta.11",
"1.0.0-rc.1")),
?assertMatch(true, not gt("1.0.0-rc.1",
"1.0.0-rc.1+build.1")),
?assertMatch(true, not gt("1.0.0-rc.1+build.1",
"1.0.0")),
?assertMatch(true, not gt("1.0.0",
"1.0.0+0.3.7")),
?assertMatch(true, not gt("1.0.0+0.3.7",
"1.3.7+build")),
?assertMatch(true, not gt("1.3.7+build",
"1.3.7+build.2.b8f12d7")),
?assertMatch(true, not gt("1.3.7+build.2.b8f12d7",
"1.3.7+build.11.e0f985a")),
?assertMatch(true, not gt("1.0.0-alpha",
"1.0.0-alpha")),
?assertMatch(true, not gt("1",
"1.0.0")),
?assertMatch(true, not gt("1.0",
"1.0.0")),
?assertMatch(true, not gt("1.0.0",
"1")),
?assertMatch(true, not gt("1.0+alpha.1",
"1.0.0+alpha.1")),
?assertMatch(true, not gt("1.0-alpha.1+build.1",
"1.0.0-alpha.1+build.1")).
lt_test() ->
?assertMatch(true, lt("1.0.0-alpha",
"1.0.0-alpha.1")),
?assertMatch(true, lt("1.0.0-alpha.1",
"1.0.0-beta.2")),
?assertMatch(true, lt("1.0.0-beta.2",
"1.0.0-beta.11")),
?assertMatch(true, lt("1.0.0-beta.11",
"1.0.0-rc.1")),
?assertMatch(true, lt("1.0.0-rc.1",
"1.0.0-rc.1+build.1")),
?assertMatch(true, lt("1.0.0-rc.1+build.1",
"1.0.0")),
?assertMatch(true, lt("1.0.0",
"1.0.0+0.3.7")),
?assertMatch(true, lt("1.0.0+0.3.7",
"1.3.7+build")),
?assertMatch(true, lt("1.3.7+build",
"1.3.7+build.2.b8f12d7")),
?assertMatch(true, lt("1.3.7+build.2.b8f12d7",
"1.3.7+build.11.e0f985a")),
?assertMatch(true, not lt("1.0.0-alpha",
"1.0.0-alpha")),
?assertMatch(true, not lt("1",
"1.0.0")),
?assertMatch(true, not lt("1.0",
"1.0.0")),
?assertMatch(true, not lt("1.0.0",
"1")),
?assertMatch(true, not lt("1.0+alpha.1",
"1.0.0+alpha.1")),
?assertMatch(true, not lt("1.0-alpha.1+build.1",
"1.0.0-alpha.1+build.1")),
?assertMatch(true, not lt("1.0.0-alpha.1",
"1.0.0-alpha")),
?assertMatch(true, not lt("1.0.0-beta.2",
"1.0.0-alpha.1")),
?assertMatch(true, not lt("1.0.0-beta.11",
"1.0.0-beta.2")),
?assertMatch(true, not lt("1.0.0-rc.1", "1.0.0-beta.11")),
?assertMatch(true, not lt("1.0.0-rc.1+build.1", "1.0.0-rc.1")),
?assertMatch(true, not lt("1.0.0", "1.0.0-rc.1+build.1")),
?assertMatch(true, not lt("1.0.0+0.3.7", "1.0.0")),
?assertMatch(true, not lt("1.3.7+build", "1.0.0+0.3.7")),
?assertMatch(true, not lt("1.3.7+build.2.b8f12d7",
"1.3.7+build")),
?assertMatch(true, not lt("1.3.7+build.11.e0f985a",
"1.3.7+build.2.b8f12d7")).
gte_test() ->
?assertMatch(true, gte("1.0.0-alpha",
"1.0.0-alpha")),
?assertMatch(true, gte("1",
"1.0.0")),
?assertMatch(true, gte("1.0",
"1.0.0")),
?assertMatch(true, gte("1.0.0",
"1")),
?assertMatch(true, gte("1.0+alpha.1",
"1.0.0+alpha.1")),
?assertMatch(true, gte("1.0-alpha.1+build.1",
"1.0.0-alpha.1+build.1")),
?assertMatch(true, gte("1.0.0-alpha.1",
"1.0.0-alpha")),
?assertMatch(true, gte("1.0.0-beta.2",
"1.0.0-alpha.1")),
?assertMatch(true, gte("1.0.0-beta.11",
"1.0.0-beta.2")),
?assertMatch(true, gte("1.0.0-rc.1", "1.0.0-beta.11")),
?assertMatch(true, gte("1.0.0-rc.1+build.1", "1.0.0-rc.1")),
?assertMatch(true, gte("1.0.0", "1.0.0-rc.1+build.1")),
?assertMatch(true, gte("1.0.0+0.3.7", "1.0.0")),
?assertMatch(true, gte("1.3.7+build", "1.0.0+0.3.7")),
?assertMatch(true, gte("1.3.7+build.2.b8f12d7",
"1.3.7+build")),
?assertMatch(true, gte("1.3.7+build.11.e0f985a",
"1.3.7+build.2.b8f12d7")),
?assertMatch(true, not gte("1.0.0-alpha",
"1.0.0-alpha.1")),
?assertMatch(true, not gte("1.0.0-alpha.1",
"1.0.0-beta.2")),
?assertMatch(true, not gte("1.0.0-beta.2",
"1.0.0-beta.11")),
?assertMatch(true, not gte("1.0.0-beta.11",
"1.0.0-rc.1")),
?assertMatch(true, not gte("1.0.0-rc.1",
"1.0.0-rc.1+build.1")),
?assertMatch(true, not gte("1.0.0-rc.1+build.1",
"1.0.0")),
?assertMatch(true, not gte("1.0.0",
"1.0.0+0.3.7")),
?assertMatch(true, not gte("1.0.0+0.3.7",
"1.3.7+build")),
?assertMatch(true, not gte("1.0.0",
"1.0.0+build.1")),
?assertMatch(true, not gte("1.3.7+build",
"1.3.7+build.2.b8f12d7")),
?assertMatch(true, not gte("1.3.7+build.2.b8f12d7",
"1.3.7+build.11.e0f985a")).
lte_test() ->
?assertMatch(true, lte("1.0.0-alpha",
"1.0.0-alpha.1")),
?assertMatch(true, lte("1.0.0-alpha.1",
"1.0.0-beta.2")),
?assertMatch(true, lte("1.0.0-beta.2",
"1.0.0-beta.11")),
?assertMatch(true, lte("1.0.0-beta.11",
"1.0.0-rc.1")),
?assertMatch(true, lte("1.0.0-rc.1",
"1.0.0-rc.1+build.1")),
?assertMatch(true, lte("1.0.0-rc.1+build.1",
"1.0.0")),
?assertMatch(true, lte("1.0.0",
"1.0.0+0.3.7")),
?assertMatch(true, lte("1.0.0+0.3.7",
"1.3.7+build")),
?assertMatch(true, lte("1.3.7+build",
"1.3.7+build.2.b8f12d7")),
?assertMatch(true, lte("1.3.7+build.2.b8f12d7",
"1.3.7+build.11.e0f985a")),
?assertMatch(true, lte("1.0.0-alpha",
"1.0.0-alpha")),
?assertMatch(true, lte("1",
"1.0.0")),
?assertMatch(true, lte("1.0",
"1.0.0")),
?assertMatch(true, lte("1.0.0",
"1")),
?assertMatch(true, lte("1.0+alpha.1",
"1.0.0+alpha.1")),
?assertMatch(true, lte("1.0-alpha.1+build.1",
"1.0.0-alpha.1+build.1")),
?assertMatch(true, not lt("1.0.0-alpha.1",
"1.0.0-alpha")),
?assertMatch(true, not lt("1.0.0-beta.2",
"1.0.0-alpha.1")),
?assertMatch(true, not lt("1.0.0-beta.11",
"1.0.0-beta.2")),
?assertMatch(true, not lt("1.0.0-rc.1", "1.0.0-beta.11")),
?assertMatch(true, not lt("1.0.0-rc.1+build.1", "1.0.0-rc.1")),
?assertMatch(true, not lt("1.0.0", "1.0.0-rc.1+build.1")),
?assertMatch(true, not lt("1.0.0+0.3.7", "1.0.0")),
?assertMatch(true, not lt("1.3.7+build", "1.0.0+0.3.7")),
?assertMatch(true, not lt("1.3.7+build.2.b8f12d7",
"1.3.7+build")),
?assertMatch(true, not lt("1.3.7+build.11.e0f985a",
"1.3.7+build.2.b8f12d7")).
between_test() ->
?assertMatch(true, between("1.0.0-alpha",
"1.0.0-alpha.3",
"1.0.0-alpha.2")),
?assertMatch(true, between("1.0.0-alpha.1",
"1.0.0-beta.2",
"1.0.0-alpha.25")),
?assertMatch(true, between("1.0.0-beta.2",
"1.0.0-beta.11",
"1.0.0-beta.7")),
?assertMatch(true, between("1.0.0-beta.11",
"1.0.0-rc.3",
"1.0.0-rc.1")),
?assertMatch(true, between("1.0.0-rc.1",
"1.0.0-rc.1+build.3",
"1.0.0-rc.1+build.1")),
?assertMatch(true, between("1.0.0-rc.1+build.1",
"1.0.0",
"1.0.0-rc.33")),
?assertMatch(true, between("1.0.0",
"1.0.0+0.3.7",
"1.0.0+0.2")),
?assertMatch(true, between("1.0.0+0.3.7",
"1.3.7+build",
"1.2")),
?assertMatch(true, between("1.3.7+build",
"1.3.7+build.2.b8f12d7",
"1.3.7+build.1")),
?assertMatch(true, between("1.3.7+build.2.b8f12d7",
"1.3.7+build.11.e0f985a",
"1.3.7+build.10.a36faa")),
?assertMatch(true, between("1.0.0-alpha",
"1.0.0-alpha",
"1.0.0-alpha")),
?assertMatch(true, between("1",
"1.0.0",
"1.0.0")),
?assertMatch(true, between("1.0",
"1.0.0",
"1.0.0")),
?assertMatch(true, between("1.0.0",
"1",
"1")),
?assertMatch(true, between("1.0+alpha.1",
"1.0.0+alpha.1",
"1.0.0+alpha.1")),
?assertMatch(true, between("1.0-alpha.1+build.1",
"1.0.0-alpha.1+build.1",
"1.0.0-alpha.1+build.1")),
?assertMatch(true, not between("1.0.0-alpha.1",
"1.0.0-alpha.22",
"1.0.0")),
?assertMatch(true, not between("1.0.0",
"1.0.0-alpha.1",
"2.0")),
?assertMatch(true, not between("1.0.0-beta.1",
"1.0.0-beta.11",
"1.0.0-alpha")),
?assertMatch(true, not between("1.0.0-beta.11", "1.0.0-rc.1", "1.0.0-rc.22")).
pes_test() ->
?assertMatch(true, pes("2.6.0", "2.6")),
?assertMatch(true, pes("2.7", "2.6")),
?assertMatch(true, pes("2.8", "2.6")),
?assertMatch(true, pes("2.9", "2.6")),
?assertMatch(true, not pes("3.0.0", "2.6")),
?assertMatch(true, not pes("2.5", "2.6")),
?assertMatch(true, pes("2.6.5", "2.6.5")),
?assertMatch(true, pes("2.6.6", "2.6.5")),
?assertMatch(true, pes("2.6.7", "2.6.5")),
?assertMatch(true, pes("2.6.8", "2.6.5")),
?assertMatch(true, pes("2.6.9", "2.6.5")),
?assertMatch(true, not pes("2.7", "2.6.5")),
?assertMatch(true, not pes("2.5", "2.6.5")).
-endif.

13
src/ec_semver_parser.peg Normal file
View file

@ -0,0 +1,13 @@
semver <- major_minor_patch ("-" alpha_part ("." alpha_part)*)? ("+" alpha_part ("." alpha_part)*)? !.
` ec_semver:internal_parse_version(Node) ` ;
major_minor_patch <- version_part ("." version_part)? ("." version_part)? ;
version_part <- [0-9]+ `erlang:list_to_integer(erlang:binary_to_list(erlang:iolist_to_binary(Node)))` ;
alpha_part <- [A-Za-z0-9-]+ ;
%% This only exists to get around a bug in erlang where if
%% warnings_as_errors is specified `nowarn` directives are ignored
`-compile(export_all).`

View file

@ -1,128 +0,0 @@
%%%-------------------------------------------------------------------
%%% @copyright (C) 2011, Erlware LLC
%%% @doc
%%% Helper functions for working with strings.
%%% @end
%%%-------------------------------------------------------------------
-module(ec_string).
-export([
compare_versions/2
]).
%%%===================================================================
%%% API
%%%===================================================================
%% @doc Is arbitrary version string A bigger than version string B?
%% Valid version string elements are either separated by . or - or both.
%% Final version string elements may have a numeric followed directly by an
%% alpha numeric and will be compared separately as in 12alpha.
%%
%% <pre>
%% 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
%% </pre>
-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.

View file

@ -49,7 +49,7 @@
%%============================================================================
-type prompt() :: string().
-type type() :: boolean | number | string.
-type supported() :: string() | boolean() | number().
-type supported() :: boolean() | number() | string().
%%============================================================================
%% API
@ -100,8 +100,11 @@ ask_default(Prompt, string, Default) ->
%% between min and max.
-spec ask(prompt(), number(), number()) -> number().
ask(Prompt, Min, Max)
when is_list(Prompt), is_number(Min), is_number(Max) ->
Res = ask(Prompt, fun get_integer/1, none),
when erlang:is_list(Prompt),
erlang:is_number(Min),
erlang:is_number(Max),
Min =< Max ->
Res = ask_convert(Prompt, fun get_integer/1, number, none),
case (Res >= Min andalso Res =< Max) of
true ->
Res;
@ -115,14 +118,16 @@ ask(Prompt, Min, Max)
%% ============================================================================
%% @doc Actually does the work of asking, checking result and
%% translating result into the requested format.
-spec ask_convert(prompt(), fun(), type(), supported()) -> supported().
-spec ask_convert(prompt(), fun((any()) -> any()), type(), supported() | none) -> supported().
ask_convert(Prompt, TransFun, Type, Default) ->
NewPrompt = Prompt ++ case Default of
NewPrompt =
erlang:binary_to_list(erlang:iolist_to_binary([Prompt,
case Default of
none ->
[];
Default ->
" (" ++ sin_utils:term_to_list(Default) ++ ")"
end ++ "> ",
[" (", io_lib:format("~p", [Default]) , ")"]
end, "> "])),
Data = string:strip(string:strip(io:get_line(NewPrompt)), both, $\n),
Ret = TransFun(Data),
case Ret of

View file

@ -1,20 +1,7 @@
%% -*- mode: Erlang; fill-column: 75; comment-column: 50; -*-
{application, erlware_commons,
[{description, "Additional standard library for Erlang"},
{vsn, "0.7.0"},
{modules, [
ec_talk,
ec_lists,
ec_plists,
ec_file,
ec_string,
ec_semver,
ec_date,
ec_dictionary,
ec_assoc_list,
ec_dict,
ec_gb_trees,
ec_rbdict,
ec_orddict]},
{vsn, "0.8.0"},
{modules, []},
{registered, []},
{applications, [kernel, stdlib]}]}.