Compare commits

..

No commits in common. "master" and "1.0.4" have entirely different histories.

35 changed files with 1363 additions and 1383 deletions

View file

@ -1,31 +0,0 @@
name: Integration tests
on:
pull_request:
branches:
- 'master'
push:
branches:
- 'master'
jobs:
build:
name: OTP ${{ matrix.otp_version }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
container:
image: erlang:${{matrix.otp_version}}
strategy:
matrix:
otp_version: ['27', '25', '23']
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v4
- name: Compile
run: rebar3 compile
- name: Dialyzer
run: rebar3 as test dialyzer
- name: EUnit
run: TERM=xterm rebar3 eunit

3
.gitignore vendored
View file

@ -7,12 +7,9 @@ doc/edoc-info
doc/erlang.png doc/erlang.png
ebin/* ebin/*
.* .*
!.github
_build _build
erl_crash.dump erl_crash.dump
*.pyc *.pyc
*~ *~
TEST-*.xml
/foo
src/ec_semver_parser.peg src/ec_semver_parser.peg

22
.travis.yml Normal file
View file

@ -0,0 +1,22 @@
language: erlang
otp_release:
- 18.0
- 17.0
- R16B03-1
- R16B03
- R16B02
- R16B01
- R16B
script: "./rebar3 update && ./rebar3 compile && ./rebar3 eunit"
branches:
only:
- master
notifications:
email:
- core@erlware.org
irc:
channels:
- "irc.freenode.org#erlware"
use_notice: true
skip_join: true
sudo: false

View file

@ -73,7 +73,7 @@ $ git stash pop
``` ```
You SHOULD use these commands both before working on your patch and before You SHOULD use these commands both before working on your patch and before
submitting the pull request. If conflicts arise it is your responsibility submitting the pull request. If conflicts arise it is your responsability
to deal with them. to deal with them.
You MUST create a new branch for your work. First make sure you have You MUST create a new branch for your work. First make sure you have

View file

@ -3,9 +3,7 @@ Erlware Commons
Current Status Current Status
-------------- --------------
[![Build Status](https://secure.travis-ci.org/erlware/erlware_commons.png)](http://travis-ci.org/erlware/erlware_commons)
[![Hex.pm](https://img.shields.io/hexpm/v/erlware_commons)](https://hex.pm/packages/erlware_commons)
[![Tests](https://github.com/erlware/erlware_commons/workflows/EUnit/badge.svg)](https://github.com/erlware/erlware_commons/actions)
Introduction Introduction
------------ ------------
@ -26,18 +24,6 @@ Goals for the project
* Well Documented * Well Documented
* Well Tested * Well Tested
Licenses
--------
This project contains elements licensed with Apache License, Version 2.0,
as well as elements licensed with The MIT License.
You'll find license-related information in the header of specific files,
where warranted.
In cases where no such information is present refer to
[COPYING](COPYING).
Currently Available Modules/Systems Currently Available Modules/Systems
------------------------------------ ------------------------------------
@ -70,7 +56,7 @@ href="http://www.erlang.org/doc/man/lists.html">lists</a>, making most
list operations parallel. It can operate on each element in parallel, list operations parallel. It can operate on each element in parallel,
for IO-bound operations, on sublists in parallel, for taking advantage for IO-bound operations, on sublists in parallel, for taking advantage
of multi-core machines with CPU-bound operations, and across erlang of multi-core machines with CPU-bound operations, and across erlang
nodes, for parallelizing inside a cluster. It handles errors and node nodes, for parallizing inside a cluster. It handles errors and node
failures. It can be configured, tuned, and tweaked to get optimal failures. It can be configured, tuned, and tweaked to get optimal
performance while minimizing overhead. performance while minimizing overhead.
@ -78,7 +64,7 @@ Almost all the functions are identical to equivalent functions in
lists, returning exactly the same result, and having both a form with lists, returning exactly the same result, and having both a form with
an identical syntax that operates on each element in parallel and a an identical syntax that operates on each element in parallel and a
form which takes an optional "malt", a specification for how to form which takes an optional "malt", a specification for how to
parallelize the operation. parallize the operation.
fold is the one exception, parallel fold is different from linear fold is the one exception, parallel fold is different from linear
fold. This module also include a simple mapreduce implementation, and fold. This module also include a simple mapreduce implementation, and
@ -90,7 +76,7 @@ runmany, which is as a generalization of parallel list operations.
A complete parser for the [semver](http://semver.org/) A complete parser for the [semver](http://semver.org/)
standard. Including a complete set of conforming comparison functions. standard. Including a complete set of conforming comparison functions.
### [ec_lists](https://github.com/erlware/erlware_commons/blob/master/src/ec_lists.erl) ### [ec_lists](https://github.com/ericbmerritt/erlware_commons/blob/master/src/ec_lists.erl)
A set of additional list manipulation functions designed to supliment A set of additional list manipulation functions designed to supliment
the `lists` module in stdlib. the `lists` module in stdlib.
@ -107,7 +93,7 @@ Other languages, have built in support for **Interface** or
**signature** functionality. Java has Interfaces, SML has **signature** functionality. Java has Interfaces, SML has
Signatures. Erlang, though, doesn't currently support this model, at Signatures. Erlang, though, doesn't currently support this model, at
least not directly. There are a few ways you can approximate it. We least not directly. There are a few ways you can approximate it. We
have defined a mechanism called *signatures* and several modules that have defined a mechnism called *signatures* and several modules that
to serve as examples and provide a good set of *dictionary* to serve as examples and provide a good set of *dictionary*
signatures. More information about signatures can be found at signatures. More information about signatures can be found at
[signature](https://github.com/erlware/erlware_commons/blob/master/doc/signatures.md). [signature](https://github.com/erlware/erlware_commons/blob/master/doc/signatures.md).
@ -124,19 +110,19 @@ This provides an implementation of the ec_dictionary signature using
erlang's dicts as a base. The function documentation for ec_dictionary erlang's dicts as a base. The function documentation for ec_dictionary
applies here as well. applies here as well.
### [ec_gb_trees](https://github.com/erlware/erlware_commons/blob/master/src/ec_gb_trees.erl) ### [ec_gb_trees](https://github.com/ericbmerritt/erlware_commons/blob/master/src/ec_gb_trees.erl)
This provides an implementation of the ec_dictionary signature using This provides an implementation of the ec_dictionary signature using
erlang's gb_trees as a base. The function documentation for erlang's gb_trees as a base. The function documentation for
ec_dictionary applies here as well. ec_dictionary applies here as well.
### [ec_orddict](https://github.com/erlware/erlware_commons/blob/master/src/ec_orddict.erl) ### [ec_orddict](https://github.com/ericbmerritt/erlware_commons/blob/master/src/ec_orddict.erl)
This provides an implementation of the ec_dictionary signature using This provides an implementation of the ec_dictionary signature using
erlang's orddict as a base. The function documentation for erlang's orddict as a base. The function documentation for
ec_dictionary applies here as well. ec_dictionary applies here as well.
### [ec_rbdict](https://github.com/erlware/erlware_commons/blob/master/src/ec_rbdict.erl) ### [ec_rbdict](https://github.com/ericbmerritt/erlware_commons/blob/master/src/ec_rbdict.erl)
This provides an implementation of the ec_dictionary signature using This provides an implementation of the ec_dictionary signature using
Robert Virding's rbdict module as a base. The function documentation Robert Virding's rbdict module as a base. The function documentation

View file

@ -2,10 +2,10 @@ Signatures
========== ==========
It often occurs in coding that we need a library, a set of It often occurs in coding that we need a library, a set of
functionalities. Often there are several algorithms that could provide functionaly. Often there are several algorithms that could provide
each of these functionalities. However, the code that uses it, either doesn't this functionality. However, the code that uses it, either doesn't
care about the individual algorithm or wishes to delegate choosing care about the individual algorithm or wishes to delegate choosing
that algorithm to some higher level. Let's take the concrete example of that algorithm to some higher level. Lets take the concrete example of
dictionaries. A dictionary provides the ability to access a value via dictionaries. A dictionary provides the ability to access a value via
a key (other things as well but primarily this). There are may ways to a key (other things as well but primarily this). There are may ways to
implement a dictionary. Just a few are: implement a dictionary. Just a few are:
@ -16,17 +16,17 @@ implement a dictionary. Just a few are:
* [Skip Lists](http://en.wikipedia.org/wiki/Skip_list) * [Skip Lists](http://en.wikipedia.org/wiki/Skip_list)
* Many, many more .... * Many, many more ....
Each of these approaches has their own performance characteristics, Each of these approaches has there own performance characteristics,
memory footprints, etc. For example, a table of size $n$ with open memory footprints etc. For example, a table of size n with open
addressing has no collisions and holds up to $n$ elements, with a single addressing has no collisions and holds up to n elements, with a single
comparison for successful lookup, and a table of size $n$ with chaining comparison for successful lookup, and a table of size n with chaining
and $k$ keys has the minimum $\max(0, k-n)$ collisions and $\mathcal{O}(1 + k/n)$ and k keys has the minimum max(0, k-n) collisions and O(1 + k/n)
comparisons for lookup. While for skip lists the performance comparisons for lookup. While for skip lists the performance
characteristics are about as good as that of randomly-built binary characteristics are about as good as that of randomly-built binary
search trees - namely $\mathcal{O}(\log n)$. So the choice of which to select search trees - namely (O log n). So the choice of which to select
depends very much on memory available, insert/read characteristics, depends very much on memory available, insert/read characteristics,
etc. So delegating the choice to a single point in your code is a very etc. So delegating the choice to a single point in your code is a very
good idea. Unfortunately, in Erlang that's so easy to do at the moment. good idea. Unfortunately, in Erlang thats ot so easy to do at the moment.
Other languages, have built in support for this Other languages, have built in support for this
functionality. [Java](http://en.wikipedia.org/wiki/Java_(programming_language)) functionality. [Java](http://en.wikipedia.org/wiki/Java_(programming_language))
@ -39,20 +39,17 @@ directly. There are a few ways you can approximate it. One way is to
pass the Module name to the calling functions along with the data that pass the Module name to the calling functions along with the data that
it is going to be called on. it is going to be called on.
```erlang :::erlang
add(ModuleToUse, Key, Value, DictData) -> add(ModuleToUse, Key, Value, DictData) ->
ModuleToUse:add(Key, Value, DictData). ModuleToUse:add(Key, Value, DictData).
```
This works, and you can vary how you want to pass the data. For This works, and you can vary how you want to pass the data. For
example, you could easily use a tuple to contain the data. That is, example, you could easily use a tuple to contain the data. That is,
you could pass in `{ModuleToUse, DictData}` and that would make it a you could pass in `{ModuleToUse, DictData}` and that would make it a
bit cleaner. bit cleaner.
:::erlang
```erlang add(Key, Value, {ModuleToUse, DictData}) ->
add(Key, Value, {ModuleToUse, DictData}) -> ModuleToUse:add(Key, Value, DictData).
ModuleToUse:add(Key, Value, DictData).
```
Either way, there are a few problems with this approach. One of the Either way, there are a few problems with this approach. One of the
biggest is that you lose code locality, by looking at this bit of code biggest is that you lose code locality, by looking at this bit of code
@ -66,22 +63,21 @@ mistakes that you might have made. Tools like
[Dialyzer](http://www.erlang.org/doc/man/dialyzer.html) have just as [Dialyzer](http://www.erlang.org/doc/man/dialyzer.html) have just as
hard a time figuring out the what `ModuleToUse` is pointing to as you hard a time figuring out the what `ModuleToUse` is pointing to as you
do. So they can't give you warnings about potential problems. In fact do. So they can't give you warnings about potential problems. In fact
someone could inadvertently pass an unexpected function name as someone could inadvertantly pass an unexpected function name as
`ModuleToUse` and you would never get any warnings, just an exception `ModuleToUse` and you would never get any warnings, just an exception
at run time. at run time.
Fortunately, Erlang is a pretty flexible language so we can use a Fortunately, Erlang is a pretty flexable language so we can use a
similar approach with a few adjustments to give us the best of both similar approach with a few adjustments to give us the best of both
worlds. Both the flexibility of ignoring a specific implementation worlds. Both the flexibiltiy of ignoreing a specific implementation
and keeping all the nice locality we get by using an explicit module and keeping all the nice locality we get by using an explicit module
name. name.
So what we actually want to do is something mole like this: So what we actually want to do is something mole like this:
```erlang :::erlang
add(Key, Value, DictData) -> add(Key, Value, DictData) ->
dictionary:add(Key, Value, DictData). dictionary:add(Key, Value, DictData).
```
Doing this we retain the locality. We can easily look up the Doing this we retain the locality. We can easily look up the
`dictionary` Module. We immediately have a good idea what a `dictionary` Module. We immediately have a good idea what a
@ -94,56 +90,54 @@ reasons, this is a much better approach to the problem. This is what
Signatures Signatures
---------- ----------
How do we actually do this in Erlang now that Erlang is missing what Java, SML and friends have built in? How do we actually do this in Erlang now that Erlang is missing what Java, SML and friends has built in?
The first thing we need to do is to define The first thing we need to do is to define
a [Behaviour](http://metajack.im/2008/10/29/custom-behaviors-in-erlang/) a [Behaviour](http://metajack.im/2008/10/29/custom-behaviors-in-erlang/)
for our functionality. To continue our example we will define a for our functionality. To continue our example we will define a
Behaviour for dictionaries. That Behaviour looks like this: Behaviour for dictionaries. That Behaviour looks like this:
```erlang :::erlang
-module(ec_dictionary). -module(ec_dictionary).
-export([behaviour_info/1]). -export([behaviour_info/1]).
behaviour_info(callbacks) -> behaviour_info(callbacks) ->
[{new, 0}, [{new, 0},
{has_key, 2}, {has_key, 2},
{get, 2}, {get, 2},
{add, 3}, {add, 3},
{remove, 2}, {remove, 2},
{has_value, 2}, {has_value, 2},
{size, 1}, {size, 1},
{to_list, 1}, {to_list, 1},
{from_list, 1}, {from_list, 1},
{keys, 1}]; {keys, 1}];
behaviour_info(_) -> behaviour_info(_) ->
undefined. undefined.
```
So we have our Behaviour now. Unfortunately, this doesn't give us much So we have our Behaviour now. Unfortunately, this doesn't give us much
yet. It will make sure that any dictionaries we write will have all yet. It will make sure that any dictionaries we write will have all
the functions they need to have, but it won't help us actually use the the functions they need to have, but it wont help use actually use the
dictionaries in an abstract way in our code. To do that we need to add dictionaries in an abstract way in our code. To do that we need to add
a bit of functionality. We do that by actually implementing our own a bit of functionality. We do that by actually implementing our own
behaviour, starting with `new/1`. behaviour, starting with `new/1`.
```erlang :::erlang
%% @doc create a new dictionary object from the specified module. The %% @doc create a new dictionary object from the specified module. The
%% module should implement the dictionary behaviour. %% module should implement the dictionary behaviour.
%% %%
%% @param ModuleName The module name. %% @param ModuleName The module name.
-spec new(module()) -> dictionary(_K, _V). -spec new(module()) -> dictionary(_K, _V).
new(ModuleName) when is_atom(ModuleName) -> new(ModuleName) when is_atom(ModuleName) ->
#dict_t{callback = ModuleName, data = ModuleName:new()}. #dict_t{callback = ModuleName, data = ModuleName:new()}.
```
This code creates a new dictionary for us. Or to be more specific it This code creates a new dictionary for us. Or to be more specific it
actually creates a new dictionary Signature record, that will be used actually creates a new dictionary Signature record, that will be used
subsequently in other calls. This might look a bit familiar from our subsequently in other calls. This might look a bit familiar from our
previous less optimal approach. We have both the module name and the previous less optimal approach. We have both the module name and the
data in the record. We call the module name named in data. here in the record. We call the module name named in
`ModuleName` to create the initial data. We then construct the record `ModuleName` to create the initial data. We then construct the record
and return that record to the caller and we have a new and return that record to the caller and we have a new
dictionary. What about the other functions, the ones that don't create dictionary. What about the other functions, the ones that don't create
@ -154,17 +148,16 @@ dictionary and another that just retrieves data.
The first we will look at is the one that updates the dictionary by The first we will look at is the one that updates the dictionary by
adding a value. adding a value.
```erlang :::erlang
%% @doc add a new value to the existing dictionary. Return a new %% @doc add a new value to the existing dictionary. Return a new
%% dictionary containing the value. %% dictionary containing the value.
%% %%
%% @param Dict the dictionary object to add too %% @param Dict the dictionary object to add too
%% @param Key the key to add %% @param Key the key to add
%% @param Value the value to add %% @param Value the value to add
-spec add(key(K), value(V), dictionary(K, V)) -> dictionary(K, V). -spec add(key(K), value(V), dictionary(K, V)) -> dictionary(K, V).
add(Key, Value, #dict_t{callback = Mod, data = Data} = Dict) -> add(Key, Value, #dict_t{callback = Mod, data = Data} = Dict) ->
Dict#dict_t{data = Mod:add(Key, Value, Data)}. Dict#dict_t{data = Mod:add(Key, Value, Data)}.
```
There are two key things here. There are two key things here.
@ -180,17 +173,16 @@ implementation to do the work itself.
Now lets do a data retrieval function. In this case, the `get` function Now lets do a data retrieval function. In this case, the `get` function
of the dictionary Signature. of the dictionary Signature.
```erlang :::erlang
%% @doc given a key return that key from the dictionary. If the key is %% @doc given a key return that key from the dictionary. If the key is
%% not found throw a 'not_found' exception. %% not found throw a 'not_found' exception.
%% %%
%% @param Dict The dictionary object to return the value from %% @param Dict The dictionary object to return the value from
%% @param Key The key requested %% @param Key The key requested
%% @throws not_found when the key does not exist %% @throws not_found when the key does not exist
-spec get(key(K), dictionary(K, V)) -> value(V). -spec get(key(K), dictionary(K, V)) -> value(V).
get(Key, #dict_t{callback = Mod, data = Data}) -> get(Key, #dict_t{callback = Mod, data = Data}) ->
Mod:get(Key, Data). Mod:get(Key, Data).
```
In this case, you can see a very similar approach to deconstructing In this case, you can see a very similar approach to deconstructing
the dict record. We still need to pull out the callback module and the the dict record. We still need to pull out the callback module and the
@ -205,7 +197,7 @@ implementation in
Using Signatures Using Signatures
---------------- ----------------
It's a good idea to work through an example so we have a bit better Its a good idea to work through an example so we have a bit better
idea of how to use these Signatures. If you are like me, you probably idea of how to use these Signatures. If you are like me, you probably
have some questions about what kind of performance burden this places have some questions about what kind of performance burden this places
on the code. At the very least we have an additional function call on the code. At the very least we have an additional function call
@ -214,7 +206,7 @@ lets write a little timing test, so we can get a good idea of how much
this is all costing us. this is all costing us.
In general, there are two kinds of concrete implementations for In general, there are two kinds of concrete implementations for
Signatures. The first is a native implementation, the second is a Signatures. The first is a native implementations, the second is a
wrapper. wrapper.
### Native Signature Implementations ### Native Signature Implementations
@ -231,33 +223,32 @@ implements the ec_dictionary module directly.
A Signature Wrapper is a module that wraps another module. Its A Signature Wrapper is a module that wraps another module. Its
purpose is to help a preexisting module implement the Behaviour purpose is to help a preexisting module implement the Behaviour
defined by a Signature. A good example of this in our current example defined by a Signature. A good example if this in our current example
is the is the
[erlware_commons/ec_dict](https://github.com/ericbmerritt/erlware_commons/blob/types/src/ec_dict.erl) [erlware_commons/ec_dict](https://github.com/ericbmerritt/erlware_commons/blob/types/src/ec_dict.erl)
module. It implements the `ec_dictionary` Behaviour, but all the module. It implements the ec_dictionary Behaviour, but all the
functionality is provided by the functionality is provided by the
[stdlib/dict](http://www.erlang.org/doc/man/dict.html) module [stdlib/dict](http://www.erlang.org/doc/man/dict.html) module
itself. Let's take a look at one example to see how this is done. itself. Lets take a look at one example to see how this is done.
We will take a look at one of the functions we have already seen. The We will take a look at one of the functions we have already seen. The
`get` function in `ec_dictionary` doesn't have quite the same `get` function an ec_dictionary `get` doesn't have quite the same
semantics as any of the functions in the `dict` module. So a bit of semantics as any of the functions in the dict module. So a bit of
translation needs to be done. We do that in the `ec_dict:get/2` function. translation needs to be done. We do that in the ec_dict module `get` function.
```erlang :::erlang
-spec get(ec_dictionary:key(K), Object::dictionary(K, V)) -> -spec get(ec_dictionary:key(K), Object::dictionary(K, V)) ->
ec_dictionary:value(V). ec_dictionary:value(V).
get(Key, Data) -> get(Key, Data) ->
case dict:find(Key, Data) of case dict:find(Key, Data) of
{ok, Value} -> {ok, Value} ->
Value; Value;
error -> error ->
throw(not_found) throw(not_found)
end. end.
```
So the `ec_dict` module's purpose for existence is to help the So the ec_dict module's purpose for existence is to help the
preexisting `dict` module implement the Behaviour defined by the preexisting dict module implement the Behaviour defined by the
Signature. Signature.
@ -267,25 +258,24 @@ the mix and that adds a bit of additional overhead.
### Creating the Timing Module ### Creating the Timing Module
We are going to be creating timings for both Native Signature We are going to creating timings for both Native Signature
Implementations and Signature Wrappers. Implementations and Signature Wrappers.
Let's get started by looking at some helper functions. We want Lets get started by looking at some helper functions. We want
dictionaries to have a bit of data in them. So to that end we will dictionaries to have a bit of data in them. So to that end we are will
create a couple of functions that create dictionaries for each type we create a couple of functions that create dictionaries for each type we
want to test. The first we want to time is the Signature Wrapper, so want to test. The first we want to time is the Signature Wrapper, so
`dict` vs `ec_dict` called as a Signature. `dict` vs `ec_dict` called as a Signature.
```erlang :::erlang
create_dict() -> create_dict() ->
lists:foldl(fun(El, Dict) -> lists:foldl(fun(El, Dict) ->
dict:store(El, El, Dict) dict:store(El, El, Dict)
end, dict:new(), end, dict:new(),
lists:seq(1,100)). lists:seq(1,100)).
```
The only thing we do here is create a sequence of numbers 1 to 100, The only thing we do here is create a sequence of numbers 1 to 100,
and then add each of those to the `dict` as an entry. We aren't too and then add each of those to the dict as an entry. We aren't too
worried about replicating real data in the dictionary. We care about worried about replicating real data in the dictionary. We care about
timing the function call overhead of Signatures, not the performance timing the function call overhead of Signatures, not the performance
of the dictionaries themselves. of the dictionaries themselves.
@ -293,61 +283,58 @@ of the dictionaries themselves.
We need to create a similar function for our Signature based We need to create a similar function for our Signature based
dictionary `ec_dict`. dictionary `ec_dict`.
```erlang :::erlang
create_dictionary(Type) -> create_dictionary(Type) ->
lists:foldl(fun(El, Dict) -> lists:foldl(fun(El, Dict) ->
ec_dictionary:add(El, El, Dict) ec_dictionary:add(El, El, Dict)
end, end,
ec_dictionary:new(Type), ec_dictionary:new(Type),
lists:seq(1,100)). lists:seq(1,100)).
```
Here we actually create everything using the Signature. So we don't Here we actually create everything using the Signature. So we don't
need one function for each type. We can have one function that can need one function for each type. We can have one function that can
create anything that implements the Signature. That is the magic of create anything that implements the Signature. That is the magic of
Signatures. Otherwise, this does the exact same thing as the dictionary Signatures. Otherwise, this does the exact same thing as the dict
given by `create_dict/0`. `create_dict/1`.
We are going to use two function calls in our timing. One that updates We are going to use two function calls in our timing. One that updates
data and one that returns data, just to get good coverage. For our data and one that returns data, just to get good coverage. For our
dictionaries we are going to use the `size` function as well as dictionaries that we are going to use the `size` function as well as
the `add` function. the `add` function.
```erlang :::erlang
time_direct_vs_signature_dict() -> time_direct_vs_signature_dict() ->
io:format("Timing dict~n"), io:format("Timing dict~n"),
Dict = create_dict(), Dict = create_dict(),
test_avg(fun() -> test_avg(fun() ->
dict:size(dict:store(some_key, some_value, Dict)) dict:size(dict:store(some_key, some_value, Dict))
end, end,
1000000), 1000000),
io:format("Timing ec_dict implementation of ec_dictionary~n"), io:format("Timing ec_dict implementation of ec_dictionary~n"),
time_dict_type(ec_dict). time_dict_type(ec_dict).
```
The `test_avg` function runs the provided function the number of times The `test_avg` function runs the provided function the number of times
specified in the second argument and collects timing information. We specified in the second argument and collects timing information. We
are going to run these one million times to get a good average (it's are going to run these one million times to get a good average (its
fast so it doesn't take long). You can see in the anonymous fast so it doesn't take long). You can see that in the anonymous
function that we directly call `dict:size/1` and `dict:store/3` to perform function that we directly call `dict:size/1` and `dict:store/3` to perform
the test. However, because we are in the wonderful world of Signatures the test. However, because we are in the wonderful world of Signatures
we don't have to hard code the calls for the Signature we don't have to hard code the calls for the Signature
implementations. Lets take a look at the `time_dict_type` function. implementations. Lets take a look at the `time_dict_type` function.
```erlang :::erlang
time_dict_type(Type) -> time_dict_type(Type) ->
io:format("Testing ~p~n", [Type]), io:format("Testing ~p~n", [Type]),
Dict = create_dictionary(Type), Dict = create_dictionary(Type),
test_avg(fun() -> test_avg(fun() ->
ec_dictionary:size(ec_dictionary:add(some_key, some_value, Dict)) ec_dictionary:size(ec_dictionary:add(some_key, some_value, Dict))
end, end,
1000000). 1000000).
```
As you can see we take the type as an argument (we need it for `dict` As you can see we take the type as an argument (we need it for `dict`
creation) and call our create function. Then we run the same timings creation) and call our create function. Then we run the same timings
that we did for `ec_dict`. In this case though, the type of dictionary that we did for ec dict. In this case though, the type of dictionary
is never specified, we only ever call ec_dictionary, so this test will is never specified, we only ever call ec_dictionary, so this test will
work for anything that implements that Signature. work for anything that implements that Signature.
@ -356,26 +343,25 @@ work for anything that implements that Signature.
So we have our tests, what was the result. Well on my laptop this is So we have our tests, what was the result. Well on my laptop this is
what it looked like. what it looked like.
```sh :::sh
Erlang R14B01 (erts-5.8.2) [source] [64-bit] [smp:4:4] [rq:4] [async-threads:0] [hipe] [kernel-poll:false] Erlang R14B01 (erts-5.8.2) [source] [64-bit] [smp:4:4] [rq:4] [async-threads:0] [hipe] [kernel-poll:false]
Eshell V5.8.2 (abort with ^G) Eshell V5.8.2 (abort with ^G)
1> ec_timing:time_direct_vs_signature_dict(). 1> ec_timing:time_direct_vs_signature_dict().
Timing dict Timing dict
Range: 2 - 5621 mics Range: 2 - 5621 mics
Median: 3 mics Median: 3 mics
Average: 3 mics Average: 3 mics
Timing ec_dict implementation of ec_dictionary Timing ec_dict implementation of ec_dictionary
Testing ec_dict Testing ec_dict
Range: 3 - 6097 mics Range: 3 - 6097 mics
Median: 3 mics Median: 3 mics
Average: 4 mics Average: 4 mics
2> 2>
```
So for the direct `dict` call, we average about 3 mics per call, while So for the direct dict call, we average about 3 mics per call, while
for the Signature Wrapper we average around 4. That's a 25% cost for for the Signature Wrapper we average around 4. Thats a 25% cost for
Signature Wrappers in this example, for a very small number of Signature Wrappers in this example, for a very small number of
calls. Depending on what you are doing that is going to be greater or calls. Depending on what you are doing that is going to be greater or
lesser. In any case, we can see that there is some cost associated lesser. In any case, we can see that there is some cost associated
@ -387,32 +373,30 @@ Signature, but it is not a Signature Wrapper. It is a native
implementation of the Signature. To use `ec_rbdict` directly we have implementation of the Signature. To use `ec_rbdict` directly we have
to create a creation helper just like we did for dict. to create a creation helper just like we did for dict.
```erlang :::erlang
create_rbdict() -> create_rbdict() ->
lists:foldl(fun(El, Dict) -> lists:foldl(fun(El, Dict) ->
ec_rbdict:add(El, El, Dict) ec_rbdict:add(El, El, Dict)
end, ec_rbdict:new(), end, ec_rbdict:new(),
lists:seq(1,100)). lists:seq(1,100)).
```
This is exactly the same as `create_dict` with the exception that dict This is exactly the same as `create_dict` with the exception that dict
is replaced by `ec_rbdict`. is replaced by `ec_rbdict`.
The timing function itself looks very similar as well. Again notice The timing function itself looks very similar as well. Again notice
that we have to hard code the concrete name for the concrete that we have to hard code the concrete name for the concrete
implementation, but we don't for the `ec_dictionary` test. implementation, but we don't for the ec_dictionary test.
```erlang :::erlang
time_direct_vs_signature_rbdict() -> time_direct_vs_signature_rbdict() ->
io:format("Timing rbdict~n"), io:format("Timing rbdict~n"),
Dict = create_rbdict(), Dict = create_rbdict(),
test_avg(fun() -> test_avg(fun() ->
ec_rbdict:size(ec_rbdict:add(some_key, some_value, Dict)) ec_rbdict:size(ec_rbdict:add(some_key, some_value, Dict))
end, end,
1000000), 1000000),
io:format("Timing ec_dict implementation of ec_dictionary~n"), io:format("Timing ec_dict implementation of ec_dictionary~n"),
time_dict_type(ec_rbdict). time_dict_type(ec_rbdict).
```
And there we have our test. What do the results look like? And there we have our test. What do the results look like?
@ -422,35 +406,34 @@ The main thing we are timing here is the additional cost of the
dictionary Signature itself. Keep that in mind as we look at the dictionary Signature itself. Keep that in mind as we look at the
results. results.
```sh :::sh
Erlang R14B01 (erts-5.8.2) [source] [64-bit] [smp:4:4] [rq:4] [async-threads:0] [hipe] [kernel-poll:false] Erlang R14B01 (erts-5.8.2) [source] [64-bit] [smp:4:4] [rq:4] [async-threads:0] [hipe] [kernel-poll:false]
Eshell V5.8.2 (abort with ^G) Eshell V5.8.2 (abort with ^G)
1> ec_timing:time_direct_vs_signature_rbdict(). 1> ec_timing:time_direct_vs_signature_rbdict().
Timing rbdict Timing rbdict
Range: 6 - 15070 mics Range: 6 - 15070 mics
Median: 7 mics Median: 7 mics
Average: 7 mics Average: 7 mics
Timing ec_dict implementation of ec_dictionary Timing ec_dict implementation of ec_dictionary
Testing ec_rbdict Testing ec_rbdict
Range: 6 - 6013 mics Range: 6 - 6013 mics
Median: 7 mics Median: 7 mics
Average: 7 mics Average: 7 mics
2> 2>
```
So no difference it time. Well the reality is that there is a So no difference it time. Well the reality is that there is a
difference in timing, there must be, but we don't have enough difference in timing, there must be, but we don't have enough
resolution in the timing system to be able to figure out what that resolution in the timing system to be able to figure out what that
difference is. Essentially that means it's really, really small - or small difference is. Essentially that means its really, really small - or small
enough not to worry about at the very least. enough not to worry about at the very least.
Conclusion Conclusion
---------- ----------
Signatures are a viable, useful approach to the problem of interfaces Signatures are a viable, useful approach to the problem of interfaces
in Erlang. They have little or no overhead depending on the type of in Erlang. The have little or no over head depending on the type of
implementation, and greatly increase the flexibility of the a library implementation, and greatly increase the flexibility of the a library
while retaining testability and locality. while retaining testability and locality.
@ -473,7 +456,7 @@ Signature Wrapper
### Code Referenced ### Code Referenced
* [ec_dictionary Implementation](https://github.com/ericbmerritt/erlware_commons/blob/types/src/ec_dictionary.erl) * [ec_dictionary Implementation] (https://github.com/ericbmerritt/erlware_commons/blob/types/src/ec_dictionary.erl)
* [ec_dict Signature Wrapper](https://github.com/ericbmerritt/erlware_commons/blob/types/src/ec_dict.erl) * [ec_dict Signature Wrapper] (https://github.com/ericbmerritt/erlware_commons/blob/types/src/ec_dict.erl)
* [ec_rbdict Native Signature Implementation](https://github.com/ericbmerritt/erlware_commons/blob/types/src/ec_rbdict.erl) * [ec_rbdict Native Signature Implementation] (https://github.com/ericbmerritt/erlware_commons/blob/types/src/ec_rbdict.erl)
* [ec_timing Signature Use Example and Timing Collector](https://github.com/ericbmerritt/erlware_commons/blob/types/examples/ec_timing.erl) * [ec_timing Signature Use Example and Timing Collector] (https://github.com/ericbmerritt/erlware_commons/blob/types/examples/ec_timing.erl)

View file

@ -2,13 +2,21 @@
%% Dependencies ================================================================ %% Dependencies ================================================================
{deps, [ {deps, [
{cf, "~>0.3"} {cf, "0.2.2"}
]}. ]}.
{erl_first_files, ["ec_dictionary", "ec_vsn"]}. {erl_first_files, ["ec_dictionary", "ec_vsn"]}.
%% Compiler Options ============================================================ %% Compiler Options ============================================================
{erl_opts, [debug_info, warnings_as_errors]}. {erl_opts,
[{platform_define, "^[0-9]+", namespaced_types},
{platform_define, "^[0-9]+", have_callback_support},
{platform_define, "^R1[4|5]", deprecated_crypto},
{platform_define, "^1[8|9]", rand_module},
{platform_define, "^2", rand_module},
{platform_define, "^2", unicode_str},
debug_info,
warnings_as_errors]}.
%% EUnit ======================================================================= %% EUnit =======================================================================
{eunit_opts, [verbose, {eunit_opts, [verbose,

View file

@ -1,7 +1,17 @@
NoDialWarns = {dialyzer, [{warnings, [no_unknown]}]}, IsRebar3 = case application:get_key(rebar, vsn) of
OTPRelease = erlang:list_to_integer(erlang:system_info(otp_release)), {ok, Vsn} ->
[MajorVersion|_] = string:tokens(Vsn, "."),
(list_to_integer(MajorVersion) >= 3);
undefined ->
false
end,
case OTPRelease<26 of Rebar2Deps = [
true -> CONFIG; {cf, ".*", {git, "https://github.com/project-fifo/cf", {tag, "0.2.2"}}}
false -> lists:keystore(dialyzer, 1, CONFIG, NoDialWarns) ],
case IsRebar3 of
true -> CONFIG;
false ->
lists:keyreplace(deps, 1, CONFIG, {deps, Rebar2Deps})
end. end.

View file

@ -1,8 +1,6 @@
{"1.2.0", {"1.1.0",
[{<<"cf">>,{pkg,<<"cf">>,<<"0.3.1">>},0}]}. [{<<"cf">>,{pkg,<<"cf">>,<<"0.2.2">>},0}]}.
[ [
{pkg_hash,[ {pkg_hash,[
{<<"cf">>, <<"5CB902239476E141EA70A740340233782D363A31EEA8AD37049561542E6CD641">>}]}, {<<"cf">>, <<"7F2913FFF90ABCABD0F489896CFEB0B0674F6C8DF6C10B17A83175448029896C">>}]}
{pkg_hash_ext,[
{<<"cf">>, <<"315E8D447D3A4B02BCDBFA397AD03BBB988A6E0AA6F44D3ADD0F4E3C3BF97672">>}]}
]. ].

View file

@ -19,12 +19,9 @@
%%% @copyright (C) 2012 Erlware, LLC. %%% @copyright (C) 2012 Erlware, LLC.
%%% %%%
%%% @doc This provides simple output functions for command line apps. You should %%% @doc This provides simple output functions for command line apps. You should
%%% use this to talk to the users if you are writing code for the system %%% use this to talk to the users if you are wrting code for the system
-module(ec_cmd_log). -module(ec_cmd_log).
%% Avoid clashing with `error/3` BIF added in Erlang/OTP 24
-compile({no_auto_import,[error/3]}).
-export([new/1, -export([new/1,
new/2, new/2,
new/3, new/3,
@ -40,17 +37,22 @@
warn/3, warn/3,
log_level/1, log_level/1,
atom_log_level/1, atom_log_level/1,
colorize/4,
format/1]). format/1]).
-include("include/ec_cmd_log.hrl"). -include("ec_cmd_log.hrl").
-include("src/ec_cmd_log.hrl").
-define(RED, $r).
-define(GREEN, $g).
-define(YELLOW, $y).
-define(BLUE, $b).
-define(MAGENTA, $m).
-define(CYAN, $c).
-define(PREFIX, "===> "). -define(PREFIX, "===> ").
-record(state_t, {log_level=0 :: int_log_level(), -record(state_t, {log_level=0 :: int_log_level(),
caller=api :: caller(), caller=api :: caller(),
intensity=low :: intensity()}). intensity=low :: none | low | high}).
%%============================================================================ %%============================================================================
%% types %% types
@ -121,10 +123,10 @@ debug(LogState, Fun)
colorize(LogState, ?CYAN, false, Fun()) colorize(LogState, ?CYAN, false, Fun())
end); end);
debug(LogState, String) -> debug(LogState, String) ->
debug(LogState, "~ts~n", [String]). debug(LogState, "~s~n", [String]).
%% @doc log at the debug level given the current log state with a format string %% @doc log at the debug level given the current log state with a format string
%% and arguments @see io:format/2 %% and argements @see io:format/2
-spec debug(t(), string(), [any()]) -> ok. -spec debug(t(), string(), [any()]) -> ok.
debug(LogState, FormatString, Args) -> debug(LogState, FormatString, Args) ->
log(LogState, ?EC_DEBUG, colorize(LogState, ?CYAN, false, FormatString), Args). log(LogState, ?EC_DEBUG, colorize(LogState, ?CYAN, false, FormatString), Args).
@ -138,10 +140,10 @@ info(LogState, Fun)
colorize(LogState, ?GREEN, false, Fun()) colorize(LogState, ?GREEN, false, Fun())
end); end);
info(LogState, String) -> info(LogState, String) ->
info(LogState, "~ts~n", [String]). info(LogState, "~s~n", [String]).
%% @doc log at the info level given the current log state with a format string %% @doc log at the info level given the current log state with a format string
%% and arguments @see io:format/2 %% and argements @see io:format/2
-spec info(t(), string(), [any()]) -> ok. -spec info(t(), string(), [any()]) -> ok.
info(LogState, FormatString, Args) -> info(LogState, FormatString, Args) ->
log(LogState, ?EC_INFO, colorize(LogState, ?GREEN, false, FormatString), Args). log(LogState, ?EC_INFO, colorize(LogState, ?GREEN, false, FormatString), Args).
@ -155,10 +157,10 @@ error(LogState, Fun)
colorize(LogState, ?RED, false, Fun()) colorize(LogState, ?RED, false, Fun())
end); end);
error(LogState, String) -> error(LogState, String) ->
error(LogState, "~ts~n", [String]). error(LogState, "~s~n", [String]).
%% @doc log at the error level given the current log state with a format string %% @doc log at the error level given the current log state with a format string
%% and arguments @see io:format/2 %% and argements @see io:format/2
-spec error(t(), string(), [any()]) -> ok. -spec error(t(), string(), [any()]) -> ok.
error(LogState, FormatString, Args) -> error(LogState, FormatString, Args) ->
log(LogState, ?EC_ERROR, colorize(LogState, ?RED, false, FormatString), Args). log(LogState, ?EC_ERROR, colorize(LogState, ?RED, false, FormatString), Args).
@ -170,10 +172,10 @@ warn(LogState, Fun)
when erlang:is_function(Fun) -> when erlang:is_function(Fun) ->
log(LogState, ?EC_WARN, fun() -> colorize(LogState, ?MAGENTA, false, Fun()) end); log(LogState, ?EC_WARN, fun() -> colorize(LogState, ?MAGENTA, false, Fun()) end);
warn(LogState, String) -> warn(LogState, String) ->
warn(LogState, "~ts~n", [String]). warn(LogState, "~s~n", [String]).
%% @doc log at the warn level given the current log state with a format string %% @doc log at the warn level given the current log state with a format string
%% and arguments @see io:format/2 %% and argements @see io:format/2
-spec warn(t(), string(), [any()]) -> ok. -spec warn(t(), string(), [any()]) -> ok.
warn(LogState, FormatString, Args) -> warn(LogState, FormatString, Args) ->
log(LogState, ?EC_WARN, colorize(LogState, ?MAGENTA, false, FormatString), Args). log(LogState, ?EC_WARN, colorize(LogState, ?MAGENTA, false, FormatString), Args).
@ -182,7 +184,7 @@ warn(LogState, FormatString, Args) ->
-spec log(t(), int_log_level(), log_fun()) -> ok. -spec log(t(), int_log_level(), log_fun()) -> ok.
log(#state_t{log_level=DetailLogLevel}, LogLevel, Fun) log(#state_t{log_level=DetailLogLevel}, LogLevel, Fun)
when DetailLogLevel >= LogLevel -> when DetailLogLevel >= LogLevel ->
io:format("~ts~n", [Fun()]); io:format("~s~n", [Fun()]);
log(_, _, _) -> log(_, _, _) ->
ok. ok.
@ -238,20 +240,61 @@ format(Log) ->
colorize(#state_t{intensity=none}, _, _, Msg) -> colorize(#state_t{intensity=none}, _, _, Msg) ->
Msg; Msg;
%% When it is supposed to be bold and we already have a uppercase %% When it is suposed to be bold and we already have a uppercase
%% (bold color) we don't need to modify the color %% (bold color) we don't need to modify the color
colorize(State, Color, true, Msg) when ?VALID_COLOR(Color), colorize(State, Color, true, Msg) when ?VALID_COLOR(Color),
Color >= $A, Color =< $Z -> Color >= $A, Color =< $Z ->
colorize(State, Color, false, Msg); colorize(State, Color, false, Msg);
%% We're sneaky we can subtract 32 to get the uppercase character if we want %% We're sneaky we can substract 32 to get the uppercase character if we want
%% bold but have a non bold color. %% bold but have a non bold color.
colorize(State, Color, true, Msg) when ?VALID_COLOR(Color) -> colorize(State, Color, true, Msg) when ?VALID_COLOR(Color) ->
colorize(State, Color - 32, false, Msg); colorize(State, Color - 32, false, Msg);
colorize(#state_t{caller=command_line, intensity = high}, colorize(#state_t{caller=command_line, intensity = high},
Color, false, Msg) when ?VALID_COLOR(Color) -> Color, false, Msg) when ?VALID_COLOR(Color) ->
lists:flatten(cf:format("~!" ++ [Color] ++"~ts~ts", [?PREFIX, Msg])); lists:flatten(cf:format("~!" ++ [Color] ++"~s~s", [?PREFIX, Msg]));
colorize(#state_t{caller=command_line, intensity = low}, colorize(#state_t{caller=command_line, intensity = low},
Color, false, Msg) when ?VALID_COLOR(Color) -> Color, false, Msg) when ?VALID_COLOR(Color) ->
lists:flatten(cf:format("~!" ++ [Color] ++"~ts~!!~ts", [?PREFIX, Msg])); lists:flatten(cf:format("~!" ++ [Color] ++"~s~!!~s", [?PREFIX, Msg]));
colorize(_LogState, _Color, _Bold, Msg) -> colorize(_LogState, _Color, _Bold, Msg) ->
Msg. Msg.
%%%===================================================================
%%% Test Functions
%%%===================================================================
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
should_test() ->
ErrorLogState = new(error),
?assertMatch(true, should(ErrorLogState, ?EC_ERROR)),
?assertMatch(true, not should(ErrorLogState, ?EC_INFO)),
?assertMatch(true, not should(ErrorLogState, ?EC_DEBUG)),
?assertEqual(?EC_ERROR, log_level(ErrorLogState)),
?assertEqual(error, atom_log_level(ErrorLogState)),
InfoLogState = new(info),
?assertMatch(true, should(InfoLogState, ?EC_ERROR)),
?assertMatch(true, should(InfoLogState, ?EC_INFO)),
?assertMatch(true, not should(InfoLogState, ?EC_DEBUG)),
?assertEqual(?EC_INFO, log_level(InfoLogState)),
?assertEqual(info, atom_log_level(InfoLogState)),
DebugLogState = new(debug),
?assertMatch(true, should(DebugLogState, ?EC_ERROR)),
?assertMatch(true, should(DebugLogState, ?EC_INFO)),
?assertMatch(true, should(DebugLogState, ?EC_DEBUG)),
?assertEqual(?EC_DEBUG, log_level(DebugLogState)),
?assertEqual(debug, atom_log_level(DebugLogState)).
no_color_test() ->
LogState = new(debug, command_line, none),
?assertEqual("test",
colorize(LogState, ?RED, true, "test")).
color_test() ->
LogState = new(debug, command_line, high),
?assertEqual("\e[1;31m===> test\e[0m",
colorize(LogState, ?RED, true, "test")).
-endif.

View file

@ -1,7 +0,0 @@
%%% @copyright 2024 Erlware, LLC.
-define(RED, $r).
-define(GREEN, $g).
-define(YELLOW, $y).
-define(BLUE, $b).
-define(MAGENTA, $m).
-define(CYAN, $c).

View file

@ -212,3 +212,36 @@ to_atom(X)
erlang:list_to_existing_atom(X); erlang:list_to_existing_atom(X);
to_atom(X) -> to_atom(X) ->
to_atom(to_list(X)). to_atom(to_list(X)).
%%%===================================================================
%%% Tests
%%%===================================================================
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
to_integer_test() ->
?assertError(badarg, to_integer(1.5, strict)).
to_float_test() ->
?assertError(badarg, to_float(10, strict)).
to_atom_test() ->
?assertMatch(true, to_atom("true")),
?assertMatch(true, to_atom(<<"true">>)),
?assertMatch(false, to_atom(<<"false">>)),
?assertMatch(false, to_atom(false)),
?assertError(badarg, to_atom("hello_foo_bar_baz")),
S = erlang:list_to_atom("1"),
?assertMatch(S, to_atom(1)).
to_boolean_test()->
?assertMatch(true, to_boolean(<<"true">>)),
?assertMatch(true, to_boolean("true")),
?assertMatch(true, to_boolean(true)),
?assertMatch(false, to_boolean(<<"false">>)),
?assertMatch(false, to_boolean("false")),
?assertMatch(false, to_boolean(false)).
-endif.

View file

@ -38,7 +38,7 @@ beam_to_erl_source(BeamFName, ErlFName) ->
Src = Src =
erl_prettypr:format(erl_syntax:form_list(tl(Forms))), erl_prettypr:format(erl_syntax:form_list(tl(Forms))),
{ok, Fd} = file:open(ErlFName, [write]), {ok, Fd} = file:open(ErlFName, [write]),
io:fwrite(Fd, "~ts~n", [Src]), io:fwrite(Fd, "~s~n", [Src]),
file:close(Fd); file:close(Fd);
Error -> Error ->
Error Error

View file

@ -44,9 +44,9 @@
-define( is_month(X), ( (is_integer(X) andalso X =< 12) orelse ?is_hinted_month(X) ) ). -define( is_month(X), ( (is_integer(X) andalso X =< 12) orelse ?is_hinted_month(X) ) ).
-define( is_tz_offset(H1,H2,M1,M2), (?is_num(H1) andalso ?is_num(H2) andalso ?is_num(M1) andalso ?is_num(M2)) ). -define( is_tz_offset(H1,H2,M1,M2), (?is_num(H1) andalso ?is_num(H2) andalso ?is_num(M1) andalso ?is_num(M2)) ).
-define(GREGORIAN_SECONDS_1970, 62_167_219_200). -define(GREGORIAN_SECONDS_1970, 62167219200).
-define(ISO_8601_DATETIME_FORMAT, "Y-m-dTH:i:sZ"). -define(ISO_8601_DATETIME_FORMAT, "Y-m-dTG:i:sZ").
-define(ISO_8601_DATETIME_WITH_MS_FORMAT, "Y-m-dTH:i:s.fZ"). -define(ISO_8601_DATETIME_WITH_MS_FORMAT, "Y-m-dTG:i:s.fZ").
-type year() :: non_neg_integer(). -type year() :: non_neg_integer().
-type month() :: 1..12 | {?MONTH_TAG, 1..12}. -type month() :: 1..12 | {?MONTH_TAG, 1..12}.
@ -54,7 +54,7 @@
-type hour() :: 0..23. -type hour() :: 0..23.
-type minute() :: 0..59. -type minute() :: 0..59.
-type second() :: 0..59. -type second() :: 0..59.
-type microsecond() :: 0..999_999. -type microsecond() :: 0..999999.
-type daynum() :: 1..7. -type daynum() :: 1..7.
-type date() :: {year(),month(),day()}. -type date() :: {year(),month(),day()}.
@ -101,7 +101,7 @@ parse(Date, Now) ->
do_parse(Date, Now, []). do_parse(Date, Now, []).
do_parse(Date, Now, Opts) -> do_parse(Date, Now, Opts) ->
case filter_hints(parse(tokenise(string:uppercase(Date), []), Now, Opts)) of case filter_hints(parse(tokenise(uppercase(Date), []), Now, Opts)) of
{error, bad_date} -> {error, bad_date} ->
erlang:throw({?MODULE, {bad_date, Date}}); erlang:throw({?MODULE, {bad_date, Date}});
{D1, T1} = {{Y, M, D}, {H, M1, S}} {D1, T1} = {{Y, M, D}, {H, M1, S}}
@ -138,11 +138,11 @@ nparse(Date) ->
{DateS, {H, M, S, Ms} } -> {DateS, {H, M, S, Ms} } ->
GSeconds = calendar:datetime_to_gregorian_seconds({DateS, {H, M, S} }), GSeconds = calendar:datetime_to_gregorian_seconds({DateS, {H, M, S} }),
ESeconds = GSeconds - ?GREGORIAN_SECONDS_1970, ESeconds = GSeconds - ?GREGORIAN_SECONDS_1970,
{ESeconds div 1_000_000, ESeconds rem 1_000_000, Ms}; {ESeconds div 1000000, ESeconds rem 1000000, Ms};
DateTime -> DateTime ->
GSeconds = calendar:datetime_to_gregorian_seconds(DateTime), GSeconds = calendar:datetime_to_gregorian_seconds(DateTime),
ESeconds = GSeconds - ?GREGORIAN_SECONDS_1970, ESeconds = GSeconds - ?GREGORIAN_SECONDS_1970,
{ESeconds div 1_000_000, ESeconds rem 1_000_000, 0} {ESeconds div 1000000, ESeconds rem 1000000, 0}
end. end.
%% %%
@ -151,7 +151,7 @@ nparse(Date) ->
parse([Year, X, Month, X, Day, Hour, $:, Min, $:, Sec, $., Micros, $Z ], _Now, _Opts) parse([Year, X, Month, X, Day, Hour, $:, Min, $:, Sec, $., Micros, $Z ], _Now, _Opts)
when ?is_world_sep(X) when ?is_world_sep(X)
andalso (Micros >= 0 andalso Micros < 1_000_000) andalso (Micros >= 0 andalso Micros < 1000000)
andalso Year > 31 -> andalso Year > 31 ->
{{Year, Month, Day}, {hour(Hour, []), Min, Sec}, {Micros}}; {{Year, Month, Day}, {hour(Hour, []), Min, Sec}, {Micros}};
@ -162,7 +162,7 @@ parse([Year, X, Month, X, Day, Hour, $:, Min, $:, Sec, $Z ], _Now, _Opts)
parse([Year, X, Month, X, Day, Hour, $:, Min, $:, Sec, $., Micros, $+, Off | _Rest ], _Now, _Opts) parse([Year, X, Month, X, Day, Hour, $:, Min, $:, Sec, $., Micros, $+, Off | _Rest ], _Now, _Opts)
when (?is_us_sep(X) orelse ?is_world_sep(X)) when (?is_us_sep(X) orelse ?is_world_sep(X))
andalso (Micros >= 0 andalso Micros < 1_000_000) andalso (Micros >= 0 andalso Micros < 1000000)
andalso Year > 31 -> andalso Year > 31 ->
{{Year, Month, Day}, {hour(Hour, []) - Off, Min, Sec}, {Micros}}; {{Year, Month, Day}, {hour(Hour, []) - Off, Min, Sec}, {Micros}};
@ -173,7 +173,7 @@ parse([Year, X, Month, X, Day, Hour, $:, Min, $:, Sec, $+, Off | _Rest ], _Now,
parse([Year, X, Month, X, Day, Hour, $:, Min, $:, Sec, $., Micros, $-, Off | _Rest ], _Now, _Opts) parse([Year, X, Month, X, Day, Hour, $:, Min, $:, Sec, $., Micros, $-, Off | _Rest ], _Now, _Opts)
when (?is_us_sep(X) orelse ?is_world_sep(X)) when (?is_us_sep(X) orelse ?is_world_sep(X))
andalso (Micros >= 0 andalso Micros < 1_000_000) andalso (Micros >= 0 andalso Micros < 1000000)
andalso Year > 31 -> andalso Year > 31 ->
{{Year, Month, Day}, {hour(Hour, []) + Off, Min, Sec}, {Micros}}; {{Year, Month, Day}, {hour(Hour, []) + Off, Min, Sec}, {Micros}};
@ -197,6 +197,17 @@ parse([Day,X,Month,X,Year,Hour,$:,Min,$:,Sec,$., Ms | PAM], _Now, _Opts)
andalso ?is_year(Year) -> andalso ?is_year(Year) ->
{{Year, Month, Day}, {hour(Hour, PAM), Min, Sec}, {Ms}}; {{Year, Month, Day}, {hour(Hour, PAM), Min, Sec}, {Ms}};
parse([Year,X,Month,X,Day,Hour,$:,Min,$:,Sec,$., Ms], _Now, _Opts)
when (?is_us_sep(X) orelse ?is_world_sep(X))
andalso ?is_year(Year) ->
{{Year, Month, Day}, {hour(Hour,[]), Min, Sec}, {Ms}};
parse([Month,X,Day,X,Year,Hour,$:,Min,$:,Sec,$., Ms], _Now, _Opts)
when ?is_us_sep(X) andalso ?is_month(Month) ->
{{Year, Month, Day}, {hour(Hour, []), Min, Sec}, {Ms}};
parse([Day,X,Month,X,Year,Hour,$:,Min,$:,Sec,$., Ms ], _Now, _Opts)
when ?is_world_sep(X) andalso ?is_month(Month) ->
{{Year, Month, Day}, {hour(Hour, []), Min, Sec}, {Ms}};
%% Date/Times Dec 1st, 2012 6:25 PM %% Date/Times Dec 1st, 2012 6:25 PM
parse([Month,Day,Year,Hour,$:,Min,$:,Sec | PAM], _Now, _Opts) parse([Month,Day,Year,Hour,$:,Min,$:,Sec | PAM], _Now, _Opts)
when ?is_meridian(PAM) andalso ?is_hinted_month(Month) andalso ?is_day(Day) -> when ?is_meridian(PAM) andalso ?is_hinted_month(Month) andalso ?is_day(Day) ->
@ -208,6 +219,14 @@ parse([Month,Day,Year,Hour | PAM], _Now, _Opts)
when ?is_meridian(PAM) andalso ?is_hinted_month(Month) andalso ?is_day(Day) -> when ?is_meridian(PAM) andalso ?is_hinted_month(Month) andalso ?is_day(Day) ->
{{Year, Month, Day}, {hour(Hour, PAM), 0, 0}}; {{Year, Month, Day}, {hour(Hour, PAM), 0, 0}};
%% Date/Times Dec 1st, 2012 18:25:15 (no AM/PM)
parse([Month,Day,Year,Hour,$:,Min,$:,Sec], _Now, _Opts)
when ?is_hinted_month(Month) andalso ?is_day(Day) ->
{{Year, Month, Day}, {hour(Hour, []), Min, Sec}};
parse([Month,Day,Year,Hour,$:,Min], _Now, _Opts)
when ?is_hinted_month(Month) andalso ?is_day(Day) ->
{{Year, Month, Day}, {hour(Hour, []), Min, 0}};
%% Date/Times Fri Nov 21 14:55:26 +0000 2014 (Twitter format) %% Date/Times Fri Nov 21 14:55:26 +0000 2014 (Twitter format)
parse([Month, Day, Hour,$:,Min,$:,Sec, Year], _Now, _Opts) parse([Month, Day, Hour,$:,Min,$:,Sec, Year], _Now, _Opts)
when ?is_hinted_month(Month), ?is_day(Day), ?is_year(Year) -> when ?is_hinted_month(Month), ?is_day(Day), ?is_year(Year) ->
@ -316,11 +335,11 @@ tokenise([$., N1, N2, N3, N4 | Rest], Acc)
when ?is_num(N1), ?is_num(N2), ?is_num(N3), ?is_num(N4) -> when ?is_num(N1), ?is_num(N2), ?is_num(N3), ?is_num(N4) ->
tokenise(Rest, [ ltoi([N1, N2, N3, N4]) * 100, $. | Acc]); tokenise(Rest, [ ltoi([N1, N2, N3, N4]) * 100, $. | Acc]);
tokenise([$., N1, N2, N3 | Rest], Acc) when ?is_num(N1), ?is_num(N2), ?is_num(N3) -> tokenise([$., N1, N2, N3 | Rest], Acc) when ?is_num(N1), ?is_num(N2), ?is_num(N3) ->
tokenise(Rest, [ ltoi([N1, N2, N3]) * 1_000, $. | Acc]); tokenise(Rest, [ ltoi([N1, N2, N3]) * 1000, $. | Acc]);
tokenise([$., N1, N2 | Rest], Acc) when ?is_num(N1), ?is_num(N2) -> tokenise([$., N1, N2 | Rest], Acc) when ?is_num(N1), ?is_num(N2) ->
tokenise(Rest, [ ltoi([N1, N2]) * 10_000, $. | Acc]); tokenise(Rest, [ ltoi([N1, N2]) * 10000, $. | Acc]);
tokenise([$., N1 | Rest], Acc) when ?is_num(N1) -> tokenise([$., N1 | Rest], Acc) when ?is_num(N1) ->
tokenise(Rest, [ ltoi([N1]) * 100_000, $. | Acc]); tokenise(Rest, [ ltoi([N1]) * 100000, $. | Acc]);
tokenise([N1, N2, N3, N4, N5, N6 | Rest], Acc) tokenise([N1, N2, N3, N4, N5, N6 | Rest], Acc)
when ?is_num(N1), ?is_num(N2), ?is_num(N3), ?is_num(N4), ?is_num(N5), ?is_num(N6) -> when ?is_num(N1), ?is_num(N2), ?is_num(N3), ?is_num(N4), ?is_num(N5), ?is_num(N6) ->
@ -503,7 +522,7 @@ format([$g|T], {_,{H,_,_}}=Dt, Acc) when H > 12 ->
format([$g|T], {_,{H,_,_}}=Dt, Acc) -> format([$g|T], {_,{H,_,_}}=Dt, Acc) ->
format(T, Dt, [itol(H)|Acc]); format(T, Dt, [itol(H)|Acc]);
format([$G|T], {_,{H,_,_}}=Dt, Acc) -> format([$G|T], {_,{H,_,_}}=Dt, Acc) ->
format(T, Dt, [itol(H)|Acc]); format(T, Dt, [pad2(H)|Acc]);
format([$h|T], {_,{H,_,_}}=Dt, Acc) when H > 12 -> format([$h|T], {_,{H,_,_}}=Dt, Acc) when H > 12 ->
format(T, Dt, [pad2(H-12)|Acc]); format(T, Dt, [pad2(H-12)|Acc]);
format([$h|T], {_,{H,_,_}}=Dt, Acc) -> format([$h|T], {_,{H,_,_}}=Dt, Acc) ->
@ -709,6 +728,12 @@ pad6(X) when is_integer(X) ->
ltoi(X) -> ltoi(X) ->
list_to_integer(X). list_to_integer(X).
-ifdef(unicode_str).
uppercase(Str) -> string:uppercase(Str).
-else.
uppercase(Str) -> string:to_upper(Str).
-endif.
%%%=================================================================== %%%===================================================================
%%% Tests %%% Tests
%%%=================================================================== %%%===================================================================
@ -718,7 +743,7 @@ ltoi(X) ->
-define(DATE, {{2001,3,10},{17,16,17}}). -define(DATE, {{2001,3,10},{17,16,17}}).
-define(DATEMS, {{2001,3,10},{17,16,17,123_456}}). -define(DATEMS, {{2001,3,10},{17,16,17,123456}}).
-define(DATE_NOON, {{2001,3,10},{12,0,0}}). -define(DATE_NOON, {{2001,3,10},{12,0,0}}).
-define(DATE_MIDNIGHT, {{2001,3,10},{0,0,0}}). -define(DATE_MIDNIGHT, {{2001,3,10},{0,0,0}}).
-define(ISO, "o \\WW"). -define(ISO, "o \\WW").
@ -737,8 +762,6 @@ basic_format_test_() ->
?_assertEqual(format("H:i:s",?DATE), "17:16:17"), ?_assertEqual(format("H:i:s",?DATE), "17:16:17"),
?_assertEqual(format("z",?DATE), "68"), ?_assertEqual(format("z",?DATE), "68"),
?_assertEqual(format("D M j G:i:s Y",?DATE), "Sat Mar 10 17:16:17 2001"), ?_assertEqual(format("D M j G:i:s Y",?DATE), "Sat Mar 10 17:16:17 2001"),
?_assertEqual(format("D M j G:i:s Y", {{2001,3,10},{5,16,17}}), "Sat Mar 10 5:16:17 2001"),
?_assertEqual(format("D M j H:i:s Y", {{2001,3,10},{5,16,17}}), "Sat Mar 10 05:16:17 2001"),
?_assertEqual(format("ga",?DATE_NOON), "12pm"), ?_assertEqual(format("ga",?DATE_NOON), "12pm"),
?_assertEqual(format("gA",?DATE_NOON), "12PM"), ?_assertEqual(format("gA",?DATE_NOON), "12PM"),
?_assertEqual(format("ga",?DATE_MIDNIGHT), "12am"), ?_assertEqual(format("ga",?DATE_MIDNIGHT), "12am"),
@ -955,7 +978,7 @@ ms_test_() ->
Now=os:timestamp(), Now=os:timestamp(),
[ [
?_assertEqual({{2012,12,12}, {12,12,12,1234}}, parse("2012-12-12T12:12:12.001234")), ?_assertEqual({{2012,12,12}, {12,12,12,1234}}, parse("2012-12-12T12:12:12.001234")),
?_assertEqual({{2012,12,12}, {12,12,12,123_000}}, parse("2012-12-12T12:12:12.123")), ?_assertEqual({{2012,12,12}, {12,12,12,123000}}, parse("2012-12-12T12:12:12.123")),
?_assertEqual(format("H:m:s.f \\m \\i\\s \\m\\o\\n\\t\\h",?DATEMS), ?_assertEqual(format("H:m:s.f \\m \\i\\s \\m\\o\\n\\t\\h",?DATEMS),
"17:03:17.123456 m is month"), "17:03:17.123456 m is month"),
?_assertEqual(format("Y-m-d\\TH:i:s.f",?DATEMS), ?_assertEqual(format("Y-m-d\\TH:i:s.f",?DATEMS),
@ -994,21 +1017,21 @@ format_iso8601_test_() ->
?_assertEqual("2001-03-10T17:16:17.000000Z", ?_assertEqual("2001-03-10T17:16:17.000000Z",
format_iso8601({{2001,3,10},{17,16,17,0}})), format_iso8601({{2001,3,10},{17,16,17,0}})),
?_assertEqual("2001-03-10T17:16:17.100000Z", ?_assertEqual("2001-03-10T17:16:17.100000Z",
format_iso8601({{2001,3,10},{17,16,17,100_000}})), format_iso8601({{2001,3,10},{17,16,17,100000}})),
?_assertEqual("2001-03-10T17:16:17.120000Z", ?_assertEqual("2001-03-10T17:16:17.120000Z",
format_iso8601({{2001,3,10},{17,16,17,120_000}})), format_iso8601({{2001,3,10},{17,16,17,120000}})),
?_assertEqual("2001-03-10T17:16:17.123000Z", ?_assertEqual("2001-03-10T17:16:17.123000Z",
format_iso8601({{2001,3,10},{17,16,17,123_000}})), format_iso8601({{2001,3,10},{17,16,17,123000}})),
?_assertEqual("2001-03-10T17:16:17.123400Z", ?_assertEqual("2001-03-10T17:16:17.123400Z",
format_iso8601({{2001,3,10},{17,16,17,123_400}})), format_iso8601({{2001,3,10},{17,16,17,123400}})),
?_assertEqual("2001-03-10T17:16:17.123450Z", ?_assertEqual("2001-03-10T17:16:17.123450Z",
format_iso8601({{2001,3,10},{17,16,17,123_450}})), format_iso8601({{2001,3,10},{17,16,17,123450}})),
?_assertEqual("2001-03-10T17:16:17.123456Z", ?_assertEqual("2001-03-10T17:16:17.123456Z",
format_iso8601({{2001,3,10},{17,16,17,123_456}})), format_iso8601({{2001,3,10},{17,16,17,123456}})),
?_assertEqual("2001-03-10T17:16:17.023456Z", ?_assertEqual("2001-03-10T17:16:17.023456Z",
format_iso8601({{2001,3,10},{17,16,17,23_456}})), format_iso8601({{2001,3,10},{17,16,17,23456}})),
?_assertEqual("2001-03-10T17:16:17.003456Z", ?_assertEqual("2001-03-10T17:16:17.003456Z",
format_iso8601({{2001,3,10},{17,16,17,3_456}})), format_iso8601({{2001,3,10},{17,16,17,3456}})),
?_assertEqual("2001-03-10T17:16:17.000456Z", ?_assertEqual("2001-03-10T17:16:17.000456Z",
format_iso8601({{2001,3,10},{17,16,17,456}})), format_iso8601({{2001,3,10},{17,16,17,456}})),
?_assertEqual("2001-03-10T17:16:17.000056Z", ?_assertEqual("2001-03-10T17:16:17.000056Z",
@ -1020,21 +1043,21 @@ format_iso8601_test_() ->
?_assertEqual("2001-03-10T07:16:17.000000Z", ?_assertEqual("2001-03-10T07:16:17.000000Z",
format_iso8601({{2001,3,10},{07,16,17,0}})), format_iso8601({{2001,3,10},{07,16,17,0}})),
?_assertEqual("2001-03-10T07:16:17.100000Z", ?_assertEqual("2001-03-10T07:16:17.100000Z",
format_iso8601({{2001,3,10},{07,16,17,100_000}})), format_iso8601({{2001,3,10},{07,16,17,100000}})),
?_assertEqual("2001-03-10T07:16:17.120000Z", ?_assertEqual("2001-03-10T07:16:17.120000Z",
format_iso8601({{2001,3,10},{07,16,17,120_000}})), format_iso8601({{2001,3,10},{07,16,17,120000}})),
?_assertEqual("2001-03-10T07:16:17.123000Z", ?_assertEqual("2001-03-10T07:16:17.123000Z",
format_iso8601({{2001,3,10},{07,16,17,123_000}})), format_iso8601({{2001,3,10},{07,16,17,123000}})),
?_assertEqual("2001-03-10T07:16:17.123400Z", ?_assertEqual("2001-03-10T07:16:17.123400Z",
format_iso8601({{2001,3,10},{07,16,17,123_400}})), format_iso8601({{2001,3,10},{07,16,17,123400}})),
?_assertEqual("2001-03-10T07:16:17.123450Z", ?_assertEqual("2001-03-10T07:16:17.123450Z",
format_iso8601({{2001,3,10},{07,16,17,123_450}})), format_iso8601({{2001,3,10},{07,16,17,123450}})),
?_assertEqual("2001-03-10T07:16:17.123456Z", ?_assertEqual("2001-03-10T07:16:17.123456Z",
format_iso8601({{2001,3,10},{07,16,17,123_456}})), format_iso8601({{2001,3,10},{07,16,17,123456}})),
?_assertEqual("2001-03-10T07:16:17.023456Z", ?_assertEqual("2001-03-10T07:16:17.023456Z",
format_iso8601({{2001,3,10},{07,16,17,23_456}})), format_iso8601({{2001,3,10},{07,16,17,23456}})),
?_assertEqual("2001-03-10T07:16:17.003456Z", ?_assertEqual("2001-03-10T07:16:17.003456Z",
format_iso8601({{2001,3,10},{07,16,17,3_456}})), format_iso8601({{2001,3,10},{07,16,17,3456}})),
?_assertEqual("2001-03-10T07:16:17.000456Z", ?_assertEqual("2001-03-10T07:16:17.000456Z",
format_iso8601({{2001,3,10},{07,16,17,456}})), format_iso8601({{2001,3,10},{07,16,17,456}})),
?_assertEqual("2001-03-10T07:16:17.000056Z", ?_assertEqual("2001-03-10T07:16:17.000056Z",
@ -1051,31 +1074,31 @@ parse_iso8601_test_() ->
parse("2001-03-10T17:16:17.000Z")), parse("2001-03-10T17:16:17.000Z")),
?_assertEqual({{2001,3,10},{17,16,17,0}}, ?_assertEqual({{2001,3,10},{17,16,17,0}},
parse("2001-03-10T17:16:17.000000Z")), parse("2001-03-10T17:16:17.000000Z")),
?_assertEqual({{2001,3,10},{17,16,17,100_000}}, ?_assertEqual({{2001,3,10},{17,16,17,100000}},
parse("2001-03-10T17:16:17.1Z")), parse("2001-03-10T17:16:17.1Z")),
?_assertEqual({{2001,3,10},{17,16,17,120_000}}, ?_assertEqual({{2001,3,10},{17,16,17,120000}},
parse("2001-03-10T17:16:17.12Z")), parse("2001-03-10T17:16:17.12Z")),
?_assertEqual({{2001,3,10},{17,16,17,123_000}}, ?_assertEqual({{2001,3,10},{17,16,17,123000}},
parse("2001-03-10T17:16:17.123Z")), parse("2001-03-10T17:16:17.123Z")),
?_assertEqual({{2001,3,10},{17,16,17,123_400}}, ?_assertEqual({{2001,3,10},{17,16,17,123400}},
parse("2001-03-10T17:16:17.1234Z")), parse("2001-03-10T17:16:17.1234Z")),
?_assertEqual({{2001,3,10},{17,16,17,123_450}}, ?_assertEqual({{2001,3,10},{17,16,17,123450}},
parse("2001-03-10T17:16:17.12345Z")), parse("2001-03-10T17:16:17.12345Z")),
?_assertEqual({{2001,3,10},{17,16,17,123_456}}, ?_assertEqual({{2001,3,10},{17,16,17,123456}},
parse("2001-03-10T17:16:17.123456Z")), parse("2001-03-10T17:16:17.123456Z")),
?_assertEqual({{2001,3,10},{15,16,17,100_000}}, ?_assertEqual({{2001,3,10},{15,16,17,100000}},
parse("2001-03-10T16:16:17.1+01:00")), parse("2001-03-10T16:16:17.1+01:00")),
?_assertEqual({{2001,3,10},{15,16,17,123_456}}, ?_assertEqual({{2001,3,10},{15,16,17,123456}},
parse("2001-03-10T16:16:17.123456+01:00")), parse("2001-03-10T16:16:17.123456+01:00")),
?_assertEqual({{2001,3,10},{17,16,17,100_000}}, ?_assertEqual({{2001,3,10},{17,16,17,100000}},
parse("2001-03-10T16:16:17.1-01:00")), parse("2001-03-10T16:16:17.1-01:00")),
?_assertEqual({{2001,3,10},{17,16,17,123_456}}, ?_assertEqual({{2001,3,10},{17,16,17,123456}},
parse("2001-03-10T16:16:17.123456-01:00")), parse("2001-03-10T16:16:17.123456-01:00")),
?_assertEqual({{2001,3,10},{17,16,17,456}}, ?_assertEqual({{2001,3,10},{17,16,17,456}},
parse("2001-03-10T17:16:17.000456Z")), parse("2001-03-10T17:16:17.000456Z")),
?_assertEqual({{2001,3,10},{17,16,17,123_000}}, ?_assertEqual({{2001,3,10},{17,16,17,123000}},
parse("2001-03-10T17:16:17.123000Z")) parse("2001-03-10T17:16:17.123000Z"))
]. ].

View file

@ -34,7 +34,11 @@
%%%=================================================================== %%%===================================================================
%% This should be opaque, but that kills dialyzer so for now we export it %% This should be opaque, but that kills dialyzer so for now we export it
%% however you should not rely on the internal representation here %% however you should not rely on the internal representation here
-ifdef(namespaced_types).
-type dictionary(_K, _V) :: dict:dict(). -type dictionary(_K, _V) :: dict:dict().
-else.
-type dictionary(_K, _V) :: dict().
-endif.
%%%=================================================================== %%%===================================================================
%%% API %%% API

View file

@ -42,6 +42,8 @@
-type key(T) :: T. -type key(T) :: T.
-type value(T) :: T. -type value(T) :: T.
-ifdef(have_callback_support).
-callback new() -> any(). -callback new() -> any().
-callback has_key(key(any()), any()) -> boolean(). -callback has_key(key(any()), any()) -> boolean().
-callback get(key(any()), any()) -> any(). -callback get(key(any()), any()) -> any().
@ -53,6 +55,27 @@
-callback from_list([{key(any()), value(any())}]) -> any(). -callback from_list([{key(any()), value(any())}]) -> any().
-callback keys(any()) -> [key(any())]. -callback keys(any()) -> [key(any())].
-else.
%% In the case where R14 or lower is being used to compile the system
%% we need to export a behaviour info
-export([behaviour_info/1]).
-spec behaviour_info(atom()) -> [{atom(), arity()}] | undefined.
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(_Other) ->
undefined.
-endif.
%%%=================================================================== %%%===================================================================
%%% API %%% API
%%%=================================================================== %%%===================================================================

View file

@ -11,7 +11,6 @@
exists/1, exists/1,
copy/2, copy/2,
copy/3, copy/3,
copy_file_info/3,
insecure_mkdtemp/0, insecure_mkdtemp/0,
mkdir_path/1, mkdir_path/1,
mkdir_p/1, mkdir_p/1,
@ -41,8 +40,7 @@
%%============================================================================ %%============================================================================
%% Types %% Types
%%============================================================================ %%============================================================================
-type file_info() :: mode | time | owner | group. -type option() :: recursive.
-type option() :: recursive | {file_info, [file_info()]}.
%%%=================================================================== %%%===================================================================
%%% API %%% API
@ -59,100 +57,53 @@ exists(Filename) ->
%% @doc copy an entire directory to another location. %% @doc copy an entire directory to another location.
-spec copy(file:name(), file:name(), Options::[option()]) -> ok | {error, Reason::term()}. -spec copy(file:name(), file:name(), Options::[option()]) -> ok | {error, Reason::term()}.
copy(From, To, []) -> copy(From, To, []) ->
copy_(From, To, []); copy(From, To);
copy(From, To, Options) -> copy(From, To, [recursive] = Options) ->
case proplists:get_value(recursive, Options, false) of case is_dir(From) of
true ->
case is_dir(From) of
false ->
copy_(From, To, Options);
true ->
make_dir_if_dir(To),
copy_subfiles(From, To, Options)
end;
false -> false ->
copy_(From, To, Options) copy(From, To);
true ->
make_dir_if_dir(To),
copy_subfiles(From, To, Options)
end. end.
%% @doc copy a file including timestamps,ownership and mode etc. %% @doc copy a file including timestamps,ownership and mode etc.
-spec copy(From::file:filename(), To::file:filename()) -> ok | {error, Reason::term()}. -spec copy(From::file:filename(), To::file:filename()) -> ok | {error, Reason::term()}.
copy(From, To) -> copy(From, To) ->
copy_(From, To, [{file_info, [mode, time, owner, group]}]). case file:copy(From, To) of
copy_(From, To, Options) ->
Linked
= case file:read_link(From) of
{ok, Linked0} -> Linked0;
{error, _} -> undefined
end,
case Linked =/= undefined orelse file:copy(From, To) of
true ->
file:make_symlink(Linked, To);
{ok, _} -> {ok, _} ->
copy_file_info(To, From, proplists:get_value(file_info, Options, [])); 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, Error} ->
{error, {copy_failed, Error}} {error, {copy_failed, Error}}
end. end.
copy_file_info(To, From, FileInfoToKeep) -> %% @doc return an md5 checksum string or a binary. Same as unix utility of
case file:read_file_info(From) of %% same name.
{ok, FileInfo} ->
case write_file_info(To, FileInfo, FileInfoToKeep) of
[] ->
ok;
Errors ->
{error, {write_file_info_failed_for, Errors}}
end;
{error, RFError} ->
{error, {read_file_info_failed, RFError}}
end.
write_file_info(To, FileInfo, FileInfoToKeep) ->
WriteInfoFuns = [{mode, fun try_write_mode/2},
{time, fun try_write_time/2},
{group, fun try_write_group/2},
{owner, fun try_write_owner/2}],
lists:foldl(fun(Info, Acc) ->
case proplists:get_value(Info, WriteInfoFuns, undefined) of
undefined ->
Acc;
F ->
case F(To, FileInfo) of
ok ->
Acc;
{error, Reason} ->
[{Info, Reason} | Acc]
end
end
end, [], FileInfoToKeep).
try_write_mode(To, #file_info{mode=Mode}) ->
file:write_file_info(To, #file_info{mode=Mode}).
try_write_time(To, #file_info{atime=Atime, mtime=Mtime}) ->
file:write_file_info(To, #file_info{atime=Atime, mtime=Mtime}).
try_write_owner(To, #file_info{uid=OwnerId}) ->
file:write_file_info(To, #file_info{uid=OwnerId}).
try_write_group(To, #file_info{gid=OwnerId}) ->
file:write_file_info(To, #file_info{gid=OwnerId}).
%% @doc return the MD5 digest of a string or a binary,
%% named after the UNIX utility.
-spec md5sum(string() | binary()) -> string(). -spec md5sum(string() | binary()) -> string().
md5sum(Value) -> md5sum(Value) ->
bin_to_hex(crypto:hash(md5, Value)). hex(binary_to_list(erlang:md5(Value))).
%% @doc return the SHA-1 digest of a string or a binary, %% @doc return an sha1sum checksum string or a binary. Same as unix utility of
%% named after the UNIX utility. %% same name.
-ifdef(deprecated_crypto).
-spec sha1sum(string() | binary()) -> string(). -spec sha1sum(string() | binary()) -> string().
sha1sum(Value) -> sha1sum(Value) ->
bin_to_hex(crypto:hash(sha, Value)). hex(binary_to_list(crypto:sha(Value))).
-else.
bin_to_hex(Bin) -> -spec sha1sum(string() | binary()) -> string().
hex(binary_to_list(Bin)). sha1sum(Value) ->
hex(binary_to_list(crypto:hash(sha, Value))).
-endif.
%% @doc delete a file. Use the recursive option for directories. %% @doc delete a file. Use the recursive option for directories.
%% <pre> %% <pre>
@ -171,7 +122,7 @@ remove(Path, Options) ->
remove(Path) -> remove(Path) ->
remove(Path, []). remove(Path, []).
%% @doc indicates with a boolean if the path supplied refers to symlink. %% @doc indicates witha boolean if the path supplied refers to symlink.
-spec is_symlink(file:name()) -> boolean(). -spec is_symlink(file:name()) -> boolean().
is_symlink(Path) -> is_symlink(Path) ->
case file:read_link_info(Path) of case file:read_link_info(Path) of
@ -221,9 +172,9 @@ real_dir_path(Path) ->
%% @doc make a unique temporary directory. Similar function to BSD stdlib %% @doc make a unique temporary directory. Similar function to BSD stdlib
%% function of the same name. %% function of the same name.
-spec insecure_mkdtemp() -> TmpDirPath::file:name() | {error, term()}. -spec insecure_mkdtemp() -> TmpDirPath::file:name().
insecure_mkdtemp() -> insecure_mkdtemp() ->
UniqueNumber = erlang:integer_to_list(erlang:trunc(rand:uniform() * 1_000_000_000_000)), UniqueNumber = erlang:integer_to_list(erlang:trunc(random_uniform() * 1000000000000)),
TmpDirPath = TmpDirPath =
filename:join([tmp(), lists:flatten([".tmp_dir", UniqueNumber])]), filename:join([tmp(), lists:flatten([".tmp_dir", UniqueNumber])]),
@ -249,7 +200,7 @@ mkdir_path(Path) ->
mkdir_p(Path). mkdir_p(Path).
%% @doc read a file from the file system. Provide UEX exception on failure. %% @doc read a file from the file system. Provide UEX exeption on failure.
-spec read(FilePath::file:filename()) -> {ok, binary()} | {error, Reason::term()}. -spec read(FilePath::file:filename()) -> {ok, binary()} | {error, Reason::term()}.
read(FilePath) -> read(FilePath) ->
%% Now that we are moving away from exceptions again this becomes %% Now that we are moving away from exceptions again this becomes
@ -258,7 +209,7 @@ read(FilePath) ->
file:read_file(FilePath). file:read_file(FilePath).
%% @doc write a file to the file system. Provide UEX exception on failure. %% @doc write a file to the file system. Provide UEX exeption on failure.
-spec write(FileName::file:filename(), Contents::string()) -> ok | {error, Reason::term()}. -spec write(FileName::file:filename(), Contents::string()) -> ok | {error, Reason::term()}.
write(FileName, Contents) -> write(FileName, Contents) ->
%% Now that we are moving away from exceptions again this becomes %% Now that we are moving away from exceptions again this becomes
@ -326,15 +277,9 @@ remove_recursive(Path, Options) ->
tmp() -> tmp() ->
case erlang:system_info(system_architecture) of case erlang:system_info(system_architecture) of
"win32" -> "win32" ->
case os:getenv("TEMP") of "./tmp";
false -> "./tmp";
Val -> Val
end;
_SysArch -> _SysArch ->
case os:getenv("TMPDIR") of "/tmp"
false -> "/tmp";
Val -> Val
end
end. end.
%% Copy the subfiles of the From directory to the to directory. %% Copy the subfiles of the From directory to the to directory.
@ -375,3 +320,101 @@ hex0(I) -> $0 + I.
sub_files(From) -> sub_files(From) ->
{ok, SubFiles} = file:list_dir(From), {ok, SubFiles} = file:list_dir(From),
[filename:join(From, SubFile) || SubFile <- SubFiles]. [filename:join(From, SubFile) || SubFile <- SubFiles].
-ifdef(rand_module).
random_uniform() ->
rand:uniform().
-else.
random_uniform() ->
random:seed(os:timestamp()),
random:uniform().
-endif.
%%%===================================================================
%%% Test Functions
%%%===================================================================
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
setup_test() ->
Dir = insecure_mkdtemp(),
mkdir_path(Dir),
?assertMatch(false, is_symlink(Dir)),
?assertMatch(true, filelib:is_dir(Dir)).
md5sum_test() ->
?assertMatch("cfcd208495d565ef66e7dff9f98764da", md5sum("0")).
sha1sum_test() ->
?assertMatch("b6589fc6ab0dc82cf12099d1c2d40ab994e8410c", sha1sum("0")).
file_test() ->
Dir = 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),
write_term(TermFile, "term"),
?assertMatch({ok, <<"\"term\". ">>}, read(TermFile)),
copy(filename:dirname(TermFile),
filename:dirname(TermFileCopy),
[recursive]).
teardown_test() ->
Dir = insecure_mkdtemp(),
remove(Dir, [recursive]),
?assertMatch(false, filelib:is_dir(Dir)).
setup_base_and_target() ->
BaseDir = 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 = 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, exists(Name1)),
?assertMatch(false, exists(NoName)).
real_path_test() ->
BaseDir = "foo",
Dir = filename:absname(filename:join(BaseDir, "source1")),
LinkDir = filename:join([BaseDir, "link"]),
ok = mkdir_p(Dir),
file:make_symlink(Dir, LinkDir),
?assertEqual(Dir, real_dir_path(LinkDir)),
?assertEqual(directory, type(Dir)),
?assertEqual(symlink, type(LinkDir)),
TermFile = filename:join(BaseDir, "test_file"),
ok = write_term(TermFile, foo),
?assertEqual(file, type(TermFile)),
?assertEqual(true, is_symlink(LinkDir)),
?assertEqual(false, 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 = 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)),
remove(BaseDir, [recursive]).
-endif.

View file

@ -135,3 +135,79 @@ from_list(List) when is_list(List) ->
-spec keys(gb_trees:tree(K,_V)) -> [ec_dictionary:key(K)]. -spec keys(gb_trees:tree(K,_V)) -> [ec_dictionary:key(K)].
keys(Data) -> keys(Data) ->
gb_trees:keys(Data). gb_trees:keys(Data).
%%%===================================================================
%%% Tests
%%%===================================================================
-ifdef(TEST).
-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)).
-endif.

View file

@ -17,13 +17,6 @@
-export([new/0, -export([new/0,
vsn/1]). vsn/1]).
-ifdef(TEST).
-export([parse_tags/1,
get_patch_count/1,
collect_default_refcount/1
]).
-endif.
-export_type([t/0]). -export_type([t/0]).
%%%=================================================================== %%%===================================================================
@ -41,7 +34,7 @@
new() -> new() ->
{}. {}.
-spec vsn(t()|string()) -> {ok, string()} | {error, Reason::any()}. -spec vsn(t()) -> {ok, string()} | {error, Reason::any()}.
vsn(Data) -> vsn(Data) ->
{Vsn, RawRef, RawCount} = collect_default_refcount(Data), {Vsn, RawRef, RawCount} = collect_default_refcount(Data),
{ok, build_vsn_string(Vsn, RawRef, RawCount)}. {ok, build_vsn_string(Vsn, RawRef, RawCount)}.
@ -68,7 +61,12 @@ collect_default_refcount(Data) ->
build_vsn_string(Vsn, RawRef, RawCount) -> build_vsn_string(Vsn, RawRef, RawCount) ->
%% Cleanup the tag and the Ref information. Basically leading 'v's and %% Cleanup the tag and the Ref information. Basically leading 'v's and
%% whitespace needs to go away. %% whitespace needs to go away.
RefTag = [".ref", re:replace(RawRef, "\\s", "", [global])], RefTag = case RawRef of
undefined ->
"";
RawRef ->
[".ref", re:replace(RawRef, "\\s", "", [global])]
end,
Count = erlang:iolist_to_binary(re:replace(RawCount, "\\s", "", [global])), Count = erlang:iolist_to_binary(re:replace(RawCount, "\\s", "", [global])),
%% Create the valid [semver](http://semver.org) version from the tag %% Create the valid [semver](http://semver.org) version from the tag
@ -82,26 +80,28 @@ build_vsn_string(Vsn, RawRef, RawCount) ->
get_patch_count(RawRef) -> get_patch_count(RawRef) ->
Ref = re:replace(RawRef, "\\s", "", [global]), Ref = re:replace(RawRef, "\\s", "", [global]),
Cmd = io_lib:format("git rev-list --count ~ts..HEAD", Cmd = io_lib:format("git rev-list --count ~s..HEAD",
[Ref]), [Ref]),
case os:cmd(Cmd) of os:cmd(Cmd).
"fatal: " ++ _ ->
0;
Count ->
Count
end.
-spec parse_tags(t()|string()) -> {string()|undefined, ec_semver:version_string()}. -spec parse_tags(t()) -> {string()|undefined, ec_semver:version_string()}.
parse_tags({}) -> parse_tags({}) ->
parse_tags(""); parse_tags("");
parse_tags(Pattern) -> parse_tags(Pattern) ->
Cmd = io_lib:format("git describe --abbrev=0 --tags --match \"~ts*\"", [Pattern]), Cmd = io_lib:format("git describe --abbrev=0 --tags --match \"~s*\"", [Pattern]),
Tag = os:cmd(Cmd), Tag = os:cmd(Cmd),
case Tag of Vsn = slice(Tag, len(Pattern) + 1),
"fatal: " ++ _ -> Vsn1 = trim(trim(Vsn, left, "v"), right, "\n"),
{undefined, ""}; {Tag, Vsn1}.
_ ->
Vsn = string:slice(Tag, string:length(Pattern)), -ifdef(unicode_str).
Vsn1 = string:trim(string:trim(Vsn, leading, "v"), trailing, "\n"), len(Str) -> string:length(Str).
{Tag, Vsn1} trim(Str, right, Chars) -> string:trim(Str, trailing, Chars);
end. trim(Str, left, Chars) -> string:trim(Str, leading, Chars);
trim(Str, both, Chars) -> string:trim(Str, both, Chars).
slice(Str, Len) -> string:slice(Str, Len).
-else.
len(Str) -> string:len(Str).
trim(Str, Dir, [Chars|_]) -> string:strip(Str, Dir, Chars).
slice(Str, Len) -> string:substr(Str, Len).
-endif.

View file

@ -52,7 +52,7 @@ find(_Fun, []) ->
error. error.
%% @doc Fetch a value from the list. If the function returns true the %% @doc Fetch a value from the list. If the function returns true the
%% value is returned. If processing reaches the end of the list and %% value is returend. If processing reaches the end of the list and
%% the function has never returned true an exception not_found is %% the function has never returned true an exception not_found is
%% thrown. %% thrown.
-spec fetch(fun(), list()) -> term(). -spec fetch(fun(), list()) -> term().
@ -63,3 +63,184 @@ fetch(Fun, List) when is_list(List), is_function(Fun) ->
error -> error ->
throw(not_found) throw(not_found)
end. end.
%%%===================================================================
%%% Test Functions
%%%===================================================================
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
find1_test() ->
TestData = [1, 2, 3, 4, 5, 6],
Result = find(fun(5) ->
true;
(_) ->
false
end,
TestData),
?assertMatch({ok, 5}, Result),
Result2 = find(fun(37) ->
true;
(_) ->
false
end,
TestData),
?assertMatch(error, Result2).
find2_test() ->
TestData = ["one", "two", "three", "four", "five", "six"],
Result = find(fun("five") ->
true;
(_) ->
false
end,
TestData),
?assertMatch({ok, "five"}, Result),
Result2 = 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 = find(fun({"one", 1}) ->
true;
(_) ->
false
end,
TestData),
?assertMatch({ok, {"one", 1}}, Result),
Result2 = 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 = fetch(fun(5) ->
true;
(_) ->
false
end,
TestData),
?assertMatch(5, Result),
?assertThrow(not_found,
fetch(fun(37) ->
true;
(_) ->
false
end,
TestData)).
fetch2_test() ->
TestData = ["one", "two", "three", "four", "five", "six"],
Result = fetch(fun("five") ->
true;
(_) ->
false
end,
TestData),
?assertMatch("five", Result),
?assertThrow(not_found,
fetch(fun(super_duper) ->
true;
(_) ->
false
end,
TestData)).
fetch3_test() ->
TestData = [{"one", 1}, {"two", 2}, {"three", 3}, {"four", 5}, {"five", 5},
{"six", 6}],
Result = fetch(fun({"one", 1}) ->
true;
(_) ->
false
end,
TestData),
?assertMatch({"one", 1}, Result),
?assertThrow(not_found,
fetch(fun([fo, bar, baz]) ->
true;
({"onehundred", 100}) ->
true;
(_) ->
false
end,
TestData)).
search1_test() ->
TestData = [1, 2, 3, 4, 5, 6],
Result = search(fun(5) ->
{ok, 5};
(_) ->
not_found
end,
TestData),
?assertMatch({ok, 5, 5}, Result),
Result2 = search(fun(37) ->
{ok, 37};
(_) ->
not_found
end,
TestData),
?assertMatch(not_found, Result2).
search2_test() ->
TestData = [1, 2, 3, 4, 5, 6],
Result = search(fun(1) ->
{ok, 10};
(_) ->
not_found
end,
TestData),
?assertMatch({ok, 10, 1}, Result),
Result2 = search(fun(6) ->
{ok, 37};
(_) ->
not_found
end,
TestData),
?assertMatch({ok, 37, 6}, Result2).
search3_test() ->
TestData = [1, 2, 3, 4, 5, 6],
Result = search(fun(10) ->
{ok, 10};
(_) ->
not_found
end,
TestData),
?assertMatch(not_found, Result),
Result2 = search(fun(-1) ->
{ok, 37};
(_) ->
not_found
end,
TestData),
?assertMatch(not_found, Result2).
-endif.

View file

@ -30,7 +30,7 @@
%%% most list operations parallel. It can operate on each element in %%% most list operations parallel. It can operate on each element in
%%% parallel, for IO-bound operations, on sublists in parallel, for %%% parallel, for IO-bound operations, on sublists in parallel, for
%%% taking advantage of multi-core machines with CPU-bound operations, %%% taking advantage of multi-core machines with CPU-bound operations,
%%% and across erlang nodes, for parallelizing inside a cluster. It %%% and across erlang nodes, for parallizing inside a cluster. It
%%% handles errors and node failures. It can be configured, tuned, and %%% handles errors and node failures. It can be configured, tuned, and
%%% tweaked to get optimal performance while minimizing overhead. %%% tweaked to get optimal performance while minimizing overhead.
%%% %%%
@ -38,7 +38,7 @@
%%% lists, returning exactly the same result, and having both a form %%% lists, returning exactly the same result, and having both a form
%%% with an identical syntax that operates on each element in parallel %%% with an identical syntax that operates on each element in parallel
%%% and a form which takes an optional "malt", a specification for how %%% and a form which takes an optional "malt", a specification for how
%%% to parallelize the operation. %%% to parallize the operation.
%%% %%%
%%% fold is the one exception, parallel fold is different from linear %%% fold is the one exception, parallel fold is different from linear
%%% fold. This module also include a simple mapreduce implementation, %%% fold. This module also include a simple mapreduce implementation,
@ -169,7 +169,7 @@
%%% processes. If one of them does a non-normal exit, plists receives %%% processes. If one of them does a non-normal exit, plists receives
%%% the 'DOWN' message believing it to be from one of its own %%% the 'DOWN' message believing it to be from one of its own
%%% processes. The error propagation system goes into effect, which %%% processes. The error propagation system goes into effect, which
%%% results in the error occurring in the calling process. %%% results in the error occuring in the calling process.
%%% %%%
-module(ec_plists). -module(ec_plists).
@ -330,14 +330,14 @@ fold(Fun, Fuse, InitAcc, List, Malt) ->
end, end,
runmany(Fun2, Fuse, List, Malt). runmany(Fun2, Fuse, List, Malt).
%% @doc Similar to foreach in module %% @doc Similiar to foreach in module
%% <a href="http://www.erlang.org/doc/man/lists.html">lists</a> %% <a href="http://www.erlang.org/doc/man/lists.html">lists</a>
%% except it makes no guarantee about the order it processes list elements. %% except it makes no guarantee about the order it processes list elements.
-spec foreach(fun(), list()) -> ok. -spec foreach(fun(), list()) -> ok.
foreach(Fun, List) -> foreach(Fun, List) ->
foreach(Fun, List, 1). foreach(Fun, List, 1).
%% @doc Similar to foreach in module %% @doc Similiar to foreach in module
%% <a href="http://www.erlang.org/doc/man/lists.html">lists</a> %% <a href="http://www.erlang.org/doc/man/lists.html">lists</a>
%% except it makes no guarantee about the order it processes list elements. %% except it makes no guarantee about the order it processes list elements.
-spec foreach(fun(), list(), malt()) -> ok. -spec foreach(fun(), list(), malt()) -> ok.
@ -432,8 +432,8 @@ sort(Fun, List) ->
%% %%
%% sort splits the list into sublists and sorts them, and it merges the %% 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 lists together. These are done in parallel. Each sublist is
%% sorted in a separate process, and each merging of results is done in a %% sorted in a seperate process, and each merging of results is done in a
%% separate process. Malt defaults to 100, causing the list to be split into %% seperate process. Malt defaults to 100, causing the list to be split into
%% 100-element sublists. %% 100-element sublists.
-spec sort(fun(), list(), malt()) -> list(). -spec sort(fun(), list(), malt()) -> list().
sort(Fun, List, Malt) -> sort(Fun, List, Malt) ->
@ -464,11 +464,11 @@ usort(Fun, List) ->
%% %%
%% usort splits the list into sublists and sorts them, and it merges the %% 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 lists together. These are done in parallel. Each sublist is
%% sorted in a separate process, and each merging of results is done in a %% sorted in a seperate process, and each merging of results is done in a
%% separate process. Malt defaults to 100, causing the list to be split into %% seperate process. Malt defaults to 100, causing the list to be split into
%% 100-element sublists. %% 100-element sublists.
%% %%
%% usort removes duplicate elements while it sorts. %% usort removes duplicate elments while it sorts.
-spec usort(fun(), list(), malt()) -> list(). -spec usort(fun(), list(), malt()) -> list().
usort(Fun, List, Malt) -> usort(Fun, List, Malt) ->
Fun2 = fun (L) -> Fun2 = fun (L) ->
@ -480,9 +480,16 @@ usort(Fun, List, Malt) ->
runmany(Fun2, {recursive, Fuse}, List, Malt). runmany(Fun2, {recursive, Fuse}, List, Malt).
%% @doc Like below, assumes default MapMalt of 1. %% @doc Like below, assumes default MapMalt of 1.
-ifdef(namespaced_types).
-spec mapreduce(MapFunc, list()) -> dict:dict() when -spec mapreduce(MapFunc, list()) -> dict:dict() when
MapFunc :: fun((term()) -> DeepListOfKeyValuePairs), MapFunc :: fun((term()) -> DeepListOfKeyValuePairs),
DeepListOfKeyValuePairs :: [DeepListOfKeyValuePairs] | {Key::term(), Value::term()}. DeepListOfKeyValuePairs :: [DeepListOfKeyValuePairs] | {Key::term(), Value::term()}.
-else.
-spec mapreduce(MapFunc, list()) -> dict() when
MapFunc :: fun((term()) -> DeepListOfKeyValuePairs),
DeepListOfKeyValuePairs :: [DeepListOfKeyValuePairs] | {Key::term(), Value::term()}.
-endif.
mapreduce(MapFunc, List) -> mapreduce(MapFunc, List) ->
mapreduce(MapFunc, List, 1). mapreduce(MapFunc, List, 1).
@ -507,14 +514,21 @@ mapreduce(MapFunc, List, MapMalt) ->
%% reducer's final state. %% reducer's final state.
%% %%
%% MapMalt is the malt for the mapping operation, with a default value of 1, %% 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. %% meaning each element of the list is mapped by a seperate process.
%% %%
%% mapreduce requires OTP R11B, or it may leave monitoring messages in the %% mapreduce requires OTP R11B, or it may leave monitoring messages in the
%% message queue. %% message queue.
-ifdef(namespaced_types).
-spec mapreduce(MapFunc, list(), InitState::term(), ReduceFunc, malt()) -> dict:dict() when -spec mapreduce(MapFunc, list(), InitState::term(), ReduceFunc, malt()) -> dict:dict() when
MapFunc :: fun((term()) -> DeepListOfKeyValuePairs), MapFunc :: fun((term()) -> DeepListOfKeyValuePairs),
DeepListOfKeyValuePairs :: [DeepListOfKeyValuePairs] | {Key::term(), Value::term()}, DeepListOfKeyValuePairs :: [DeepListOfKeyValuePairs] | {Key::term(), Value::term()},
ReduceFunc :: fun((OldState::term(), Key::term(), Value::term()) -> NewState::term()). ReduceFunc :: fun((OldState::term(), Key::term(), Value::term()) -> NewState::term()).
-else.
-spec mapreduce(MapFunc, list(), InitState::term(), ReduceFunc, malt()) -> dict() when
MapFunc :: fun((term()) -> DeepListOfKeyValuePairs),
DeepListOfKeyValuePairs :: [DeepListOfKeyValuePairs] | {Key::term(), Value::term()},
ReduceFunc :: fun((OldState::term(), Key::term(), Value::term()) -> NewState::term()).
-endif.
mapreduce(MapFunc, List, InitState, ReduceFunc, MapMalt) -> mapreduce(MapFunc, List, InitState, ReduceFunc, MapMalt) ->
Parent = self(), Parent = self(),
{Reducer, ReducerRef} = {Reducer, ReducerRef} =
@ -572,7 +586,7 @@ add_key(Dict, Key, Value) ->
end. end.
%% @doc Like below, but assumes a Malt of 1, %% @doc Like below, but assumes a Malt of 1,
%% meaning each element of the list is processed by a separate process. %% meaning each element of the list is processed by a seperate process.
-spec runmany(fun(), fuse(), list()) -> term(). -spec runmany(fun(), fuse(), list()) -> term().
runmany(Fun, Fuse, List) -> runmany(Fun, Fuse, List) ->
runmany(Fun, Fuse, List, 1). runmany(Fun, Fuse, List, 1).
@ -601,7 +615,7 @@ runmany(Fun, Fuse, List) ->
%% continues fusing pairs of results until it is down to one. %% continues fusing pairs of results until it is down to one.
%% %%
%% Recursive fuse is down in parallel with processing the sublists, and a %% 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 %% process is spawned to fuse each pair of results. It is a parallized
%% algorithm. Linear fuse is done after all results of processing sublists %% algorithm. Linear fuse is done after all results of processing sublists
%% have been collected, and can only run in a single process. %% have been collected, and can only run in a single process.
%% %%
@ -677,7 +691,7 @@ runmany(Fun, {recursive, Fuse}, List, local, Split, []) ->
%% or {nodes, NodeList}. Degenerates recursive fuse into linear fuse. %% or {nodes, NodeList}. Degenerates recursive fuse into linear fuse.
runmany(Fun, Fuse, List, local, Split, []); runmany(Fun, Fuse, List, local, Split, []);
runmany(Fun, Fuse, List, Nodes, no_split, []) -> runmany(Fun, Fuse, List, Nodes, no_split, []) ->
%% by default, operate on each element separately %% by default, operate on each element seperately
runmany(Fun, Fuse, List, Nodes, 1, []); runmany(Fun, Fuse, List, Nodes, 1, []);
runmany(Fun, Fuse, List, local, Split, []) -> runmany(Fun, Fuse, List, local, Split, []) ->
List2 = splitmany(List, Split), List2 = splitmany(List, Split),
@ -808,7 +822,20 @@ cluster_runmany(Fun, Fuse, [Task|TaskList], [N|Nodes], Running, Results) ->
Parent ! {erlang:self(), fuse, FuseFunc(R1, R2)} Parent ! {erlang:self(), fuse, FuseFunc(R1, R2)}
end end
end, end,
Fun3 = fun() -> runmany_wrap(Fun2, Parent) end, Fun3 = fun () ->
try
Fun2()
catch
exit:siblingdied ->
ok;
exit:Reason ->
Parent ! {erlang:self(), error, Reason};
error:R ->
Parent ! {erlang:self(), error, {R, erlang:get_stacktrace()}};
throw:R ->
Parent ! {erlang:self(), error, {{nocatch, R}, erlang:get_stacktrace()}}
end
end,
Pid = proc_lib:spawn(N, Fun3), Pid = proc_lib:spawn(N, Fun3),
erlang:monitor(process, Pid), erlang:monitor(process, Pid),
cluster_runmany(Fun, Fuse, TaskList, Nodes, [{Pid, N, Task}|Running], Results); cluster_runmany(Fun, Fuse, TaskList, Nodes, [{Pid, N, Task}|Running], Results);
@ -858,20 +885,6 @@ cluster_runmany(_, _, [_Non|_Empty], []=_Nodes, []=_Running, _) ->
%% We have data, but no nodes either available or occupied %% We have data, but no nodes either available or occupied
erlang:exit(allnodescrashed). erlang:exit(allnodescrashed).
runmany_wrap(Fun, Parent) ->
try
Fun()
catch
exit:siblingdied ->
ok;
exit:Reason ->
Parent ! {erlang:self(), error, Reason};
error:R:Stacktrace ->
Parent ! {erlang:self(), error, {R, Stacktrace}};
throw:R:Stacktrace ->
Parent ! {erlang:self(), error, {{nocatch, R}, Stacktrace}}
end.
delete_running(Pid, [{Pid, Node, List}|Running], Acc) -> delete_running(Pid, [{Pid, Node, List}|Running], Acc) ->
{Running ++ Acc, Node, List}; {Running ++ Acc, Node, List};
delete_running(Pid, [R|Running], Acc) -> delete_running(Pid, [R|Running], Acc) ->

View file

@ -32,7 +32,7 @@
%%% representation of a dictionary, where a red-black tree is used to %%% representation of a dictionary, where a red-black tree is used to
%%% store the keys and values. %%% store the keys and values.
%%% %%%
%%% This module implements exactly the same interface as the module %%% This module implents exactly the same interface as the module
%%% ec_dictionary but with a defined representation. One difference is %%% ec_dictionary but with a defined representation. One difference is
%%% that while dict considers two keys as different if they do not %%% that while dict considers two keys as different if they do not
%%% match (=:=), this module considers two keys as different if and %%% match (=:=), this module considers two keys as different if and
@ -296,7 +296,7 @@ to_list(empty, List) -> List;
to_list({_, A, Xk, Xv, B}, List) -> to_list({_, A, Xk, Xv, B}, List) ->
to_list(A, [{Xk, Xv} | to_list(B, List)]). to_list(A, [{Xk, Xv} | to_list(B, List)]).
%% Balance a tree after (possibly) adding a node to the left/right. %% Balance a tree afer (possibly) adding a node to the left/right.
-spec lbalance(color(), dictionary(K, V), -spec lbalance(color(), dictionary(K, V),
ec_dictionary:key(K), ec_dictionary:value(V), ec_dictionary:key(K), ec_dictionary:value(V),
dictionary(K, V)) -> dictionary(K, V)) ->

View file

@ -202,13 +202,13 @@ pes(VsnA, VsnB) ->
%%%=================================================================== %%%===================================================================
%%% Friend Functions %%% Friend Functions
%%%=================================================================== %%%===================================================================
%% @doc helper function for the peg grammar to parse the iolist into a semver %% @doc helper function for the peg grammer to parse the iolist into a semver
-spec internal_parse_version(iolist()) -> semver(). -spec internal_parse_version(iolist()) -> semver().
internal_parse_version([MMP, AlphaPart, BuildPart, _]) -> internal_parse_version([MMP, AlphaPart, BuildPart, _]) ->
{parse_major_minor_patch_minpatch(MMP), {parse_alpha_part(AlphaPart), {parse_major_minor_patch_minpatch(MMP), {parse_alpha_part(AlphaPart),
parse_alpha_part(BuildPart)}}. parse_alpha_part(BuildPart)}}.
%% @doc helper function for the peg grammar to parse the iolist into a major_minor_patch %% @doc helper function for the peg grammer to parse the iolist into a major_minor_patch
-spec parse_major_minor_patch_minpatch(iolist()) -> major_minor_patch_minpatch(). -spec parse_major_minor_patch_minpatch(iolist()) -> major_minor_patch_minpatch().
parse_major_minor_patch_minpatch([MajVsn, [], [], []]) -> parse_major_minor_patch_minpatch([MajVsn, [], [], []]) ->
strip_maj_version(MajVsn); strip_maj_version(MajVsn);
@ -224,7 +224,7 @@ parse_major_minor_patch_minpatch([MajVsn,
[<<".">>, MinPatch]]) -> [<<".">>, MinPatch]]) ->
{strip_maj_version(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 %% @doc helper function for the peg grammer to parse the iolist into an alpha part
-spec parse_alpha_part(iolist()) -> [alpha_part()]. -spec parse_alpha_part(iolist()) -> [alpha_part()].
parse_alpha_part([]) -> parse_alpha_part([]) ->
[]; [];
@ -287,25 +287,425 @@ normalize(Other = {{_, _, _, _}, {_,_}}) ->
%% the internal implementation of the of the pessimistic run. The %% the internal implementation of the of the pessimistic run. The
%% external just ensures that versions are parsed. %% external just ensures that versions are parsed.
-spec internal_pes(semver(), semver()) -> boolean(). -spec internal_pes(semver(), semver()) -> boolean().
internal_pes(VsnA, {{LM, LMI}, Alpha}) internal_pes(VsnA, {{LM, LMI}, _})
when erlang:is_integer(LM), when erlang:is_integer(LM),
erlang:is_integer(LMI) -> erlang:is_integer(LMI) ->
gte(VsnA, {{LM, LMI, 0}, Alpha}) andalso gte(VsnA, {{LM, LMI, 0}, {[], []}}) andalso
lt(VsnA, {{LM + 1, 0, 0, 0}, {[], []}}); lt(VsnA, {{LM + 1, 0, 0, 0}, {[], []}});
internal_pes(VsnA, {{LM, LMI, LP}, Alpha}) internal_pes(VsnA, {{LM, LMI, LP}, _})
when erlang:is_integer(LM), when erlang:is_integer(LM),
erlang:is_integer(LMI), erlang:is_integer(LMI),
erlang:is_integer(LP) -> erlang:is_integer(LP) ->
gte(VsnA, {{LM, LMI, LP}, Alpha}) gte(VsnA, {{LM, LMI, LP}, {[], []}})
andalso andalso
lt(VsnA, {{LM, LMI + 1, 0, 0}, {[], []}}); lt(VsnA, {{LM, LMI + 1, 0, 0}, {[], []}});
internal_pes(VsnA, {{LM, LMI, LP, LMP}, Alpha}) internal_pes(VsnA, {{LM, LMI, LP, LMP}, _})
when erlang:is_integer(LM), when erlang:is_integer(LM),
erlang:is_integer(LMI), erlang:is_integer(LMI),
erlang:is_integer(LP), erlang:is_integer(LP),
erlang:is_integer(LMP) -> erlang:is_integer(LMP) ->
gte(VsnA, {{LM, LMI, LP, LMP}, Alpha}) gte(VsnA, {{LM, LMI, LP, LMP}, {[], []}})
andalso andalso
lt(VsnA, {{LM, LMI, LP + 1, 0}, {[], []}}); lt(VsnA, {{LM, LMI, LP + 1, 0}, {[], []}});
internal_pes(Vsn, LVsn) -> internal_pes(Vsn, LVsn) ->
gte(Vsn, LVsn). gte(Vsn, LVsn).
%%%===================================================================
%%% Test Functions
%%%===================================================================
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
eql_test() ->
?assertMatch(true, eql("1.0.0-alpha",
"1.0.0-alpha")),
?assertMatch(true, eql("v1.0.0-alpha",
"1.0.0-alpha")),
?assertMatch(true, eql("1",
"1.0.0")),
?assertMatch(true, eql("v1",
"v1.0.0")),
?assertMatch(true, eql("1.0",
"1.0.0")),
?assertMatch(true, eql("1.0.0",
"1")),
?assertMatch(true, eql("1.0.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, eql("1.0-alpha.1+build.1",
"1.0.0.0-alpha.1+build.1")),
?assertMatch(true, eql("1.0-alpha.1+build.1",
"v1.0.0.0-alpha.1+build.1")),
?assertMatch(true, eql("aa", "aa")),
?assertMatch(true, eql("AA.BB", "AA.BB")),
?assertMatch(true, eql("BBB-super", "BBB-super")),
?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")),
?assertMatch(true, not eql("1.0.0.0+build.1",
"1.0.0.1+build.2")),
?assertMatch(true, not eql("FFF", "BBB")),
?assertMatch(true, not eql("1", "1BBBB")).
gt_test() ->
?assertMatch(true, gt("1.0.0-alpha.1",
"1.0.0-alpha")),
?assertMatch(true, gt("1.0.0.1-alpha.1",
"1.0.0.1-alpha")),
?assertMatch(true, gt("1.0.0.4-alpha.1",
"1.0.0.2-alpha")),
?assertMatch(true, gt("1.0.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-beta.11",
"1.0.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.2.b8f12d7",
"1.3.7.0+build")),
?assertMatch(true, gt("1.3.7+build.11.e0f985a",
"1.3.7+build.2.b8f12d7")),
?assertMatch(true, gt("aa.cc",
"aa.bb")),
?assertMatch(true, not gt("1.0.0-alpha",
"1.0.0-alpha.1")),
?assertMatch(true, not gt("1.0.0-alpha",
"1.0.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("aa.bb",
"aa.bb")),
?assertMatch(true, not gt("aa.cc",
"aa.dd")),
?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.0.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.1-beta.11",
"1.0.0.1-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, lt("1",
"1.0.0.1")),
?assertMatch(true, lt("AA.DD",
"AA.EE")),
?assertMatch(true, not lt("1.0",
"1.0.0")),
?assertMatch(true, not lt("1.0.0.0",
"1")),
?assertMatch(true, not lt("1.0+alpha.1",
"1.0.0+alpha.1")),
?assertMatch(true, not lt("AA.DD", "AA.CC")),
?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.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+build.1",
"1.0.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("aa.bb", "aa.bb")),
?assertMatch(true, gte("dd", "aa")),
?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("CC", "DD")),
?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.0.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, lte("aa","cc")),
?assertMatch(true, lte("cc","cc")),
?assertMatch(true, not lte("1.0.0-alpha.1",
"1.0.0-alpha")),
?assertMatch(true, not lte("cc", "aa")),
?assertMatch(true, not lte("1.0.0-beta.2",
"1.0.0-alpha.1")),
?assertMatch(true, not lte("1.0.0-beta.11",
"1.0.0-beta.2")),
?assertMatch(true, not lte("1.0.0-rc.1", "1.0.0-beta.11")),
?assertMatch(true, not lte("1.0.0-rc.1+build.1", "1.0.0-rc.1")),
?assertMatch(true, not lte("1.0.0", "1.0.0-rc.1+build.1")),
?assertMatch(true, not lte("1.0.0+0.3.7", "1.0.0")),
?assertMatch(true, not lte("1.3.7+build", "1.0.0+0.3.7")),
?assertMatch(true, not lte("1.3.7+build.2.b8f12d7",
"1.3.7+build")),
?assertMatch(true, not lte("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.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",
"1.0.0.0",
"1.0.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, between("aaa",
"ddd",
"cc")),
?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")),
?assertMatch(true, not between("aaa", "ddd", "zzz")).
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, pes("A.B", "A.A")),
?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, pes("2.6.0.9", "2.6.0.5")),
?assertMatch(true, not pes("2.7", "2.6.5")),
?assertMatch(true, not pes("2.1.7", "2.1.6.5")),
?assertMatch(true, not pes("A.A", "A.B")),
?assertMatch(true, not pes("2.5", "2.6.5")).
version_format_test() ->
?assertEqual(["1", [], []], format({1, {[],[]}})),
?assertEqual(["1", ".", "2", ".", "34", [], []], format({{1,2,34},{[],[]}})),
?assertEqual(<<"a">>, erlang:iolist_to_binary(format({<<"a">>, {[],[]}}))),
?assertEqual(<<"a.b">>, erlang:iolist_to_binary(format({{<<"a">>,<<"b">>}, {[],[]}}))),
?assertEqual(<<"1">>, erlang:iolist_to_binary(format({1, {[],[]}}))),
?assertEqual(<<"1.2">>, erlang:iolist_to_binary(format({{1,2}, {[],[]}}))),
?assertEqual(<<"1.2.2">>, erlang:iolist_to_binary(format({{1,2,2}, {[],[]}}))),
?assertEqual(<<"1.99.2">>, erlang:iolist_to_binary(format({{1,99,2}, {[],[]}}))),
?assertEqual(<<"1.99.2-alpha">>, erlang:iolist_to_binary(format({{1,99,2}, {[<<"alpha">>],[]}}))),
?assertEqual(<<"1.99.2-alpha.1">>, erlang:iolist_to_binary(format({{1,99,2}, {[<<"alpha">>,1], []}}))),
?assertEqual(<<"1.99.2+build.1.a36">>,
erlang:iolist_to_binary(format({{1,99,2}, {[], [<<"build">>, 1, <<"a36">>]}}))),
?assertEqual(<<"1.99.2.44+build.1.a36">>,
erlang:iolist_to_binary(format({{1,99,2,44}, {[], [<<"build">>, 1, <<"a36">>]}}))),
?assertEqual(<<"1.99.2-alpha.1+build.1.a36">>,
erlang:iolist_to_binary(format({{1,99,2}, {[<<"alpha">>, 1], [<<"build">>, 1, <<"a36">>]}}))),
?assertEqual(<<"1">>, erlang:iolist_to_binary(format({1, {[],[]}}))).
-endif.

View file

@ -44,10 +44,11 @@ parse(Input) when is_binary(Input) ->
-spec 'alpha_part'(input(), index()) -> parse_result(). -spec 'alpha_part'(input(), index()) -> parse_result().
'alpha_part'(Input, Index) -> '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). 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. transform(_,Node,_Index) -> Node.
-file("peg_includes.hrl", 1).
-type index() :: {{line, pos_integer()}, {column, pos_integer()}}. -type index() :: {{line, pos_integer()}, {column, pos_integer()}}.
-type input() :: binary(). -type input() :: binary().
-type parse_failure() :: {fail, term()}. -type parse_failure() :: {fail, term()}.

View file

@ -39,11 +39,6 @@
say/1, say/1,
say/2]). say/2]).
-ifdef(TEST).
-export([get_boolean/1,
get_integer/1]).
-endif.
-export_type([prompt/0, -export_type([prompt/0,
type/0, type/0,
supported/0]). supported/0]).
@ -80,7 +75,7 @@ ask(Prompt) ->
ask_default(Prompt, Default) -> ask_default(Prompt, Default) ->
ask_convert(Prompt, fun get_string/1, string, 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'. %% value in the format specified by 'Type'.
-spec ask(prompt(), type()) -> supported(). -spec ask(prompt(), type()) -> supported().
ask(Prompt, boolean) -> ask(Prompt, boolean) ->
@ -90,7 +85,7 @@ ask(Prompt, number) ->
ask(Prompt, string) -> ask(Prompt, string) ->
ask_convert(Prompt, fun get_string/1, string, none). ask_convert(Prompt, fun get_string/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'. %% value in the format specified by 'Type'.
-spec ask_default(prompt(), type(), supported()) -> supported(). -spec ask_default(prompt(), type(), supported()) -> supported().
ask_default(Prompt, boolean, Default) -> ask_default(Prompt, boolean, Default) ->
@ -132,7 +127,7 @@ ask_convert(Prompt, TransFun, Type, Default) ->
Default -> Default ->
[" (", io_lib:format("~p", [Default]) , ")"] [" (", io_lib:format("~p", [Default]) , ")"]
end, "> "])), end, "> "])),
Data = string:trim(string:trim(io:get_line(NewPrompt)), both, [$\n]), Data = trim(trim(io:get_line(NewPrompt)), both, [$\n]),
Ret = TransFun(Data), Ret = TransFun(Data),
case Ret of case Ret of
no_data -> no_data ->
@ -150,7 +145,7 @@ ask_convert(Prompt, TransFun, Type, Default) ->
Ret Ret
end. end.
%% @doc Tries to translate the result into a boolean %% @doc Trys to translate the result into a boolean
-spec get_boolean(string()) -> boolean(). -spec get_boolean(string()) -> boolean().
get_boolean([]) -> get_boolean([]) ->
no_data; no_data;
@ -177,7 +172,7 @@ get_boolean([$N | _]) ->
get_boolean(_) -> get_boolean(_) ->
no_clue. 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(). -spec get_integer(string()) -> integer().
get_integer([]) -> get_integer([]) ->
no_data; no_data;
@ -201,3 +196,36 @@ get_string(String) ->
false -> false ->
no_clue no_clue
end. end.
-ifdef(unicode_str).
trim(Str) -> string:trim(Str).
trim(Str, right, Chars) -> string:trim(Str, trailing, Chars);
trim(Str, left, Chars) -> string:trim(Str, leading, Chars);
trim(Str, both, Chars) -> string:trim(Str, both, Chars).
-else.
trim(Str) -> string:strip(Str).
trim(Str, Dir, [Chars|_]) -> string:strip(Str, Dir, Chars).
-endif.
%%%====================================================================
%%% tests
%%%====================================================================
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
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"))].
-endif.

View file

@ -27,9 +27,24 @@
%% however you should not rely on the internal representation here %% however you should not rely on the internal representation here
-type t() :: #t{}. -type t() :: #t{}.
-ifdef(have_callback_support).
-callback new() -> any(). -callback new() -> any().
-callback vsn(any()) -> {ok, string()} | {error, Reason::any()}. -callback vsn(any()) -> {ok, string()} | {error, Reason::any()}.
-else.
%% In the case where R14 or lower is being used to compile the system
%% we need to export a behaviour info
-export([behaviour_info/1]).
-spec behaviour_info(atom()) -> [{atom(), arity()}] | undefined.
behaviour_info(callbacks) ->
[{new, 0},
{vsn, 1}];
behaviour_info(_Other) ->
undefined.
-endif.
%%%=================================================================== %%%===================================================================
%%% API %%% API
%%%=================================================================== %%%===================================================================

View file

@ -1,11 +1,11 @@
{application,erlware_commons, {application,erlware_commons,
[{description,"Additional standard library for Erlang"}, [{description,"Additional standard library for Erlang"},
{vsn,"git"}, {vsn,"1.0.4"},
{modules,[]}, {modules,[]},
{registered,[]}, {registered,[]},
{applications,[kernel,stdlib,cf]}, {applications,[kernel,stdlib,cf]},
{maintainers,["Eric Merritt","Tristan Sloughter", {maintainers,["Eric Merritt","Tristan Sloughter",
"Jordan Wilberding","Martin Logan"]}, "Jordan Wilberding","Martin Logan"]},
{licenses,["Apache", "MIT"]}, {licenses,["Apache"]},
{links,[{"Github", {links,[{"Github",
"https://github.com/erlware/erlware_commons"}]}]}. "https://github.com/erlware/erlware_commons"}]}]}.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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, {[],[]}}))).

View file

@ -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"))].