Compare commits
342 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
4ffd5dd550 | ||
![]() |
a5712997ef | ||
![]() |
20119880fd | ||
![]() |
1a08e33b83 | ||
![]() |
47f7a5540c | ||
![]() |
2ccc40f89b | ||
![]() |
fa25b703e5 | ||
![]() |
8de367f996 | ||
![]() |
279d116dca | ||
![]() |
23b00904c8 | ||
![]() |
6cd37a4f9b | ||
![]() |
5305348899 | ||
![]() |
6d4e7d14ce | ||
![]() |
677984e961 | ||
![]() |
ca7581cbb0 | ||
![]() |
fc69576978 | ||
![]() |
d24ad72034 | ||
![]() |
a54f0623c5 | ||
![]() |
515df6b21e | ||
![]() |
1fd0a513ff | ||
![]() |
3d0006fe89 | ||
![]() |
15126e0048 | ||
![]() |
cdd9240142 | ||
![]() |
1a42c54981 | ||
![]() |
ccc1be32be | ||
![]() |
5f40d8f061 | ||
![]() |
75c6bae602 | ||
![]() |
ab7eb3874f | ||
![]() |
2636b5e21d | ||
![]() |
d6315a9541 | ||
![]() |
f9ffd1ce6b | ||
![]() |
182c30a950 | ||
![]() |
63b1798b1e | ||
![]() |
cb3983741e | ||
![]() |
f378d3ec46 | ||
![]() |
c0a02892cd | ||
![]() |
5de3c80cc2 | ||
![]() |
17e6f89078 | ||
![]() |
86a6c6ea65 | ||
![]() |
2286a6ed9b | ||
![]() |
7b7d5b559d | ||
![]() |
eca2d2129c | ||
![]() |
20d049ea4f | ||
![]() |
5118421f6f | ||
![]() |
685f08621b | ||
![]() |
952a1d2bc6 | ||
![]() |
fc69b3630c | ||
![]() |
6781f1ba6a | ||
![]() |
bbdbbf313f | ||
![]() |
19c717fb97 | ||
![]() |
6e9c1b0a22 | ||
![]() |
7e69d4949e | ||
![]() |
68e9bbcd0f | ||
![]() |
cd88825861 | ||
![]() |
5c5c264241 | ||
![]() |
6d4c471ff6 | ||
![]() |
62a985b937 | ||
![]() |
7c4911795e | ||
![]() |
ad4b944fc6 | ||
![]() |
4d5811d99b | ||
![]() |
04c0d4fc84 | ||
![]() |
378b88587c | ||
![]() |
d5183f5336 | ||
![]() |
eeb25f4b7f | ||
![]() |
8dd7378a75 | ||
![]() |
4406d56135 | ||
![]() |
e89e95de5f | ||
![]() |
791729c30a | ||
![]() |
d34da1d107 | ||
![]() |
ad559ae1f5 | ||
![]() |
916539338f | ||
![]() |
2a758c9ec7 | ||
![]() |
4c0180f157 | ||
![]() |
6f7a32487a | ||
![]() |
9cdca1c2e1 | ||
![]() |
26bcdf8030 | ||
![]() |
c47c938537 | ||
![]() |
f0347d88d7 | ||
![]() |
0318b467bc | ||
![]() |
9d67e26510 | ||
![]() |
f5e8aa6551 | ||
![]() |
4406953a87 | ||
![]() |
0a4fde35e7 | ||
![]() |
01e08a3605 | ||
![]() |
d9874feccf | ||
![]() |
32e62781bb | ||
![]() |
2256b68e4f | ||
![]() |
45d79af620 | ||
![]() |
c7f166a8a7 | ||
![]() |
2e1b59ece6 | ||
![]() |
8eef97234f | ||
![]() |
c2b7863a53 | ||
![]() |
f41b847b0c | ||
![]() |
0dc260c04c | ||
![]() |
aad7ae4241 | ||
![]() |
8aadd8b278 | ||
![]() |
c3ae625bd1 | ||
![]() |
4e3b177be7 | ||
![]() |
a8b46e0770 | ||
![]() |
d4079cd127 | ||
![]() |
b2d41811c1 | ||
![]() |
f6992d72ca | ||
![]() |
7bf631d326 | ||
![]() |
64d76963c7 | ||
![]() |
1bd107113b | ||
![]() |
8302adf831 | ||
![]() |
e0453faf8f | ||
![]() |
c730da3b9d | ||
![]() |
b23bf733c5 | ||
![]() |
9137b5dc54 | ||
![]() |
885f961c02 | ||
![]() |
2e01d65b99 | ||
![]() |
ad2d57d8b6 | ||
![]() |
9eae901e58 | ||
![]() |
51de0f2c7a | ||
![]() |
d501d710e2 | ||
![]() |
6e9f640c9c | ||
![]() |
012bc2f789 | ||
![]() |
a227a0fce9 | ||
![]() |
a12eed3d1d | ||
![]() |
5956de425f | ||
![]() |
f1ecf12ad3 | ||
![]() |
fb449e9e01 | ||
![]() |
6933a178ec | ||
![]() |
2690bd14a3 | ||
![]() |
df7728d81f | ||
![]() |
d792f8c5ff | ||
![]() |
f09d8f18a1 | ||
![]() |
b8bbc4cfac | ||
![]() |
41ff18e5d1 | ||
![]() |
4f086fc5fa | ||
![]() |
f8f72b7cc5 | ||
![]() |
fa1ec76a9b | ||
![]() |
681973a29c | ||
![]() |
657c767a8c | ||
![]() |
3ad087f8e0 | ||
![]() |
f92f7de6f8 | ||
![]() |
0916834752 | ||
![]() |
0898f1caf0 | ||
![]() |
c9f1c5debe | ||
![]() |
69fbe53eea | ||
![]() |
e76dd80a1e | ||
![]() |
4d2ab621ab | ||
![]() |
ab6617496b | ||
![]() |
4513db483d | ||
![]() |
db88b093d2 | ||
![]() |
f64a4b1661 | ||
![]() |
603441a036 | ||
![]() |
a91c96eb92 | ||
![]() |
5d729253d3 | ||
![]() |
a298a7b045 | ||
![]() |
ed107c94b4 | ||
![]() |
ab321b16e6 | ||
![]() |
8dd9f826db | ||
![]() |
d052e63ba5 | ||
![]() |
fe9120696e | ||
![]() |
8974edb6a6 | ||
![]() |
b666164c0d | ||
![]() |
6d63ffde57 | ||
![]() |
001e7fcf4b | ||
![]() |
57f56c2860 | ||
![]() |
874f2dc821 | ||
![]() |
0c1636e75a | ||
![]() |
8a325c0291 | ||
![]() |
b84eed3634 | ||
![]() |
0a4d2811f1 | ||
![]() |
906a32e6c3 | ||
![]() |
60eb82aed5 | ||
![]() |
05f062d23d | ||
![]() |
5ef37420b6 | ||
![]() |
2240decb95 | ||
![]() |
505d35996d | ||
![]() |
8c1b1133de | ||
![]() |
a4ed7d683e | ||
![]() |
74cc980f94 | ||
![]() |
ad3eed2cd5 | ||
![]() |
d7af2ecdfe | ||
![]() |
056377c2d8 | ||
![]() |
453922a6ed | ||
![]() |
f7650e843d | ||
![]() |
28dd4e812e | ||
![]() |
19df1c0750 | ||
![]() |
0583ffa672 | ||
![]() |
f983b1ac85 | ||
![]() |
95a8e3c32d | ||
![]() |
4aedc36937 | ||
![]() |
87c76aeb2a | ||
![]() |
49bc69e35a | ||
![]() |
3608a576fb | ||
![]() |
7c37ecf949 | ||
![]() |
26c600922e | ||
![]() |
29c711bc39 | ||
![]() |
739a9bcf24 | ||
![]() |
fbf7f7951c | ||
![]() |
885a516f03 | ||
![]() |
83adceaa1a | ||
![]() |
6813d5184c | ||
![]() |
3c69ca001c | ||
![]() |
381189c006 | ||
![]() |
e56c73c940 | ||
![]() |
2d634c5e46 | ||
![]() |
cbe494b1cb | ||
![]() |
d726ba2742 | ||
![]() |
2e23e43079 | ||
![]() |
4c20e1903d | ||
![]() |
ef0d252b11 | ||
![]() |
7015ba2951 | ||
![]() |
e07d08333a | ||
![]() |
05b956da26 | ||
![]() |
aa373dddbe | ||
![]() |
6079300634 | ||
![]() |
02ab88513f | ||
![]() |
441d11820d | ||
![]() |
ae608d26e1 | ||
![]() |
5ef8371020 | ||
![]() |
d4d11ed1ba | ||
![]() |
405c5506a9 | ||
![]() |
0b47f60bfb | ||
![]() |
a299d45899 | ||
![]() |
4fb4199da3 | ||
![]() |
c25dce9689 | ||
![]() |
03f76c6ef2 | ||
![]() |
7a32f52e7d | ||
![]() |
c2d67e6f76 | ||
![]() |
3121c892b4 | ||
![]() |
684afc1230 | ||
![]() |
73f21ee770 | ||
![]() |
d1d19e3248 | ||
![]() |
364726c166 | ||
![]() |
cc7f6dd0df | ||
![]() |
6d742c0cae | ||
![]() |
bd6fd557d5 | ||
![]() |
10ea4e18d6 | ||
![]() |
e2841d6cef | ||
![]() |
523a66ad74 | ||
![]() |
4c97d4a962 | ||
![]() |
449051bcd6 | ||
![]() |
888be01dfe | ||
![]() |
47bcbd49b6 | ||
![]() |
7e64c9ea4f | ||
![]() |
6fb5213f6a | ||
![]() |
d9a83413af | ||
![]() |
b125ce055e | ||
![]() |
741bba1b82 | ||
![]() |
f821e33806 | ||
![]() |
b9a2681a32 | ||
![]() |
96cdc0e623 | ||
![]() |
06390816b9 | ||
![]() |
71e6458251 | ||
![]() |
b8dc0eed6d | ||
![]() |
efdd2a1092 | ||
![]() |
2e61eb481d | ||
![]() |
54f568e826 | ||
![]() |
16b441f0e3 | ||
![]() |
4973a0fb8f | ||
![]() |
6949262dfe | ||
![]() |
031db98d27 | ||
![]() |
82ec98667e | ||
![]() |
558185b9b9 | ||
![]() |
79c5436c85 | ||
![]() |
615d788cab | ||
![]() |
8bc27f62fd | ||
![]() |
0fbd4576ce | ||
![]() |
417a4c3229 | ||
![]() |
7e2c37e2f5 | ||
![]() |
8870c422ca | ||
![]() |
652fcc1e23 | ||
![]() |
f36f333374 | ||
![]() |
a5b713c9bb | ||
![]() |
e984618c3e | ||
![]() |
d9c6ec1d28 | ||
![]() |
38cd7a4d62 | ||
![]() |
38141c5c24 | ||
![]() |
d806fbd119 | ||
![]() |
a1fc04f2b7 | ||
![]() |
1906888a05 | ||
![]() |
d78358af0b | ||
![]() |
58e6b0476d | ||
![]() |
17e08c04a2 | ||
![]() |
fd505767e5 | ||
![]() |
1cd615ef58 | ||
![]() |
e1e30f4a75 | ||
![]() |
4ba12ec4ad | ||
![]() |
a2fac85ff6 | ||
![]() |
97d39ec8db | ||
![]() |
320813e56e | ||
![]() |
4558635813 | ||
![]() |
122af09cb1 | ||
![]() |
5b23329d3a | ||
![]() |
5beeb3ff1b | ||
![]() |
3437fc8c1c | ||
![]() |
5c6af5c7f5 | ||
![]() |
e28130d9f3 | ||
![]() |
4811393957 | ||
![]() |
5429ec2d14 | ||
![]() |
21c5f9fc74 | ||
![]() |
0f409c0bf3 | ||
![]() |
74b0d7318d | ||
![]() |
407ccf886f | ||
![]() |
95f723e1e0 | ||
![]() |
1a39438f3c | ||
![]() |
8d300f5d02 | ||
![]() |
c7717743ed | ||
![]() |
31ebca114a | ||
![]() |
0e10d59b3a | ||
![]() |
dda4c85586 | ||
![]() |
1a1b87bf53 | ||
![]() |
b5371974d1 | ||
![]() |
c4887e2021 | ||
![]() |
1540fb1652 | ||
![]() |
7e4ba401fd | ||
![]() |
0db7042ff9 | ||
![]() |
f77afd43c3 | ||
![]() |
a9f2a771f0 | ||
![]() |
eab58fb660 | ||
![]() |
b4ab414419 | ||
![]() |
5105df48f9 | ||
![]() |
e9161d8688 | ||
![]() |
0c34549901 | ||
![]() |
3a29539285 | ||
![]() |
1b01380613 | ||
![]() |
25ef2f3496 | ||
![]() |
c42581887a | ||
![]() |
67acaaaf3f | ||
![]() |
4db670c812 | ||
![]() |
bf37ad9492 | ||
![]() |
9b9f070a5f | ||
![]() |
a2672cafb1 | ||
![]() |
cf8cad00df | ||
![]() |
e035ae3dbf | ||
![]() |
7e42c243b0 | ||
![]() |
d7b60ccf19 | ||
![]() |
51b5a41c63 | ||
![]() |
9514b16993 | ||
![]() |
8d625ceb46 | ||
![]() |
3e5eeb8cf7 | ||
![]() |
b4eb83cf53 | ||
![]() |
261fb422f9 | ||
![]() |
10557e421e | ||
![]() |
3e6357aea9 | ||
![]() |
c828f7415a | ||
![]() |
26a2e91d4a |
46 changed files with 4653 additions and 1950 deletions
31
.github/workflows/main.yml
vendored
Normal file
31
.github/workflows/main.yml
vendored
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
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
|
15
.gitignore
vendored
15
.gitignore
vendored
|
@ -1,3 +1,18 @@
|
||||||
|
.erlware_commons_plt
|
||||||
|
.eunit/*
|
||||||
|
deps/*
|
||||||
|
doc/*.html
|
||||||
|
doc/*.css
|
||||||
|
doc/edoc-info
|
||||||
|
doc/erlang.png
|
||||||
|
ebin/*
|
||||||
|
.*
|
||||||
|
!.github
|
||||||
_build
|
_build
|
||||||
erl_crash.dump
|
erl_crash.dump
|
||||||
*.pyc
|
*.pyc
|
||||||
|
*~
|
||||||
|
TEST-*.xml
|
||||||
|
/foo
|
||||||
|
|
||||||
|
src/ec_semver_parser.peg
|
||||||
|
|
139
CONTRIBUTING.md
Normal file
139
CONTRIBUTING.md
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
Contributing
|
||||||
|
============
|
||||||
|
|
||||||
|
Introduction
|
||||||
|
------------
|
||||||
|
|
||||||
|
This document describes the usages and rules to follow when contributing
|
||||||
|
to this project.
|
||||||
|
|
||||||
|
It uses the uppercase keywords SHOULD for optional but highly recommended
|
||||||
|
conditions and MUST for required conditions.
|
||||||
|
|
||||||
|
`git` is a distributed source code versioning system. This document refers
|
||||||
|
to three different repositories hosting the source code of the project.
|
||||||
|
`Your local copy` refers to the copy of the repository that you have on
|
||||||
|
your computer. The remote repository `origin` refers to your fork of the
|
||||||
|
project's repository that you can find in your GitHub account. The remote
|
||||||
|
repository `upstream` refers to the official repository for this project.
|
||||||
|
|
||||||
|
Reporting bugs
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Upon identifying a bug you SHOULD submit a ticket, regardless of your
|
||||||
|
plan for fixing it. If you plan to fix the bug, you SHOULD discuss your
|
||||||
|
plans to avoid having your work rejected.
|
||||||
|
|
||||||
|
Before implementing a new feature, you SHOULD submit a ticket for discussion
|
||||||
|
on your plans. The feature might have been rejected already, or the
|
||||||
|
implementation might already be decided.
|
||||||
|
|
||||||
|
Cloning
|
||||||
|
-------
|
||||||
|
|
||||||
|
You MUST fork the project's repository to your GitHub account by clicking
|
||||||
|
on the `Fork` button.
|
||||||
|
|
||||||
|
Then, from your fork's page, copy the `Git Read-Only` URL to your clipboard.
|
||||||
|
You MUST perform the following commands in the folder you choose, replacing
|
||||||
|
`$URL` by the URL you just copied, `$UPSTREAM_URL` by the `Git Read-Only`
|
||||||
|
project of the official repository, and `$PROJECT` by the name of this project.
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
$ git clone "$URL"
|
||||||
|
$ cd $PROJECT
|
||||||
|
$ git remote add upstream $UPSTREAM_URL
|
||||||
|
```
|
||||||
|
|
||||||
|
Branching
|
||||||
|
---------
|
||||||
|
|
||||||
|
Before starting working on the code, you MUST update to `upstream`. The
|
||||||
|
project is always evolving, and as such you SHOULD always strive to keep
|
||||||
|
up to date when submitting patches to make sure they can be merged without
|
||||||
|
conflicts.
|
||||||
|
|
||||||
|
To update the current branch to `upstream`, you can use the following commands.
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
$ git fetch upstream
|
||||||
|
$ git rebase upstream/master
|
||||||
|
```
|
||||||
|
|
||||||
|
It may ask you to stash your changes, in which case you stash with:
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
$ git stash
|
||||||
|
```
|
||||||
|
|
||||||
|
And put your changes back in with:
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
$ git stash pop
|
||||||
|
```
|
||||||
|
|
||||||
|
You SHOULD use these commands both before working on your patch and before
|
||||||
|
submitting the pull request. If conflicts arise it is your responsibility
|
||||||
|
to deal with them.
|
||||||
|
|
||||||
|
You MUST create a new branch for your work. First make sure you have
|
||||||
|
'fetched' `master`
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
$ git checkout -b $BRANCH upstream/master
|
||||||
|
```
|
||||||
|
|
||||||
|
You MUST use a an insightful branch name.
|
||||||
|
|
||||||
|
If you later need to switch back to an existing branch `$BRANCH`, you can use:
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
$ git checkout $BRANCH
|
||||||
|
```
|
||||||
|
|
||||||
|
Source editing
|
||||||
|
--------------
|
||||||
|
|
||||||
|
The following rules MUST be followed:
|
||||||
|
* Indentation uses 4 horizontal spaces
|
||||||
|
* Tabs should not be used
|
||||||
|
* Do NOT align code; only indentation is allowed
|
||||||
|
|
||||||
|
|
||||||
|
The following rules SHOULD be followed:
|
||||||
|
* Write small functions whenever possible
|
||||||
|
* Avoid having too many clauses containing clauses containing clauses
|
||||||
|
* Lines SHOULD NOT span more than 80 columns
|
||||||
|
|
||||||
|
When in doubt indentation as performed in the Erlang Emacs Mode is
|
||||||
|
correct.
|
||||||
|
|
||||||
|
Committing
|
||||||
|
----------
|
||||||
|
|
||||||
|
You MUST ensure that all commits pass all tests and do not have extra
|
||||||
|
Dialyzer warnings.
|
||||||
|
|
||||||
|
You MUST put all the related work in a single commit. Fixing a bug is one
|
||||||
|
commit, adding a feature is one commit, adding two features is two commits.
|
||||||
|
|
||||||
|
You MUST write a proper commit title and message. The commit title MUST be
|
||||||
|
at most 72 characters; it is the first line of the commit text. The second
|
||||||
|
line of the commit text MUST be left blank. The third line and beyond is the
|
||||||
|
commit message. You SHOULD write a commit message. If you do, you MUST make
|
||||||
|
all lines smaller than 80 characters. You SHOULD explain what the commit
|
||||||
|
does, what references you used and any other information that helps
|
||||||
|
understanding your work.
|
||||||
|
|
||||||
|
Submitting the pull request
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
You MUST push your branch `$BRANCH` to GitHub, using the following command:
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
$ git push origin $BRANCH
|
||||||
|
```
|
||||||
|
|
||||||
|
You MUST then submit the pull request by using the GitHub interface to
|
||||||
|
the `master` branch. You SHOULD provide an explanatory message and refer
|
||||||
|
to any previous ticket related to this patch.
|
17
Makefile
17
Makefile
|
@ -1,17 +0,0 @@
|
||||||
ERLC=`which erlc`
|
|
||||||
BEAMDIR=./ebin
|
|
||||||
ERLCFLAGS=+debug_info -pa $(BEAMDIR)
|
|
||||||
SRCDIR=src
|
|
||||||
|
|
||||||
.PHONY=all clean
|
|
||||||
|
|
||||||
all:
|
|
||||||
@echo "Erlware Commons is maintained with Sinan, its much better to use "
|
|
||||||
@echo "sinan to build than this makefile. This is here just to get "
|
|
||||||
@echo "get you started."
|
|
||||||
$(ERLC) $(ERLCFLAGS) -o $(BEAMDIR) $(SRCDIR)/ec_dictionary.erl;
|
|
||||||
$(ERLC) $(ERLCFLAGS) -o $(BEAMDIR) $(SRCDIR)/*.erl ;
|
|
||||||
|
|
||||||
clean:
|
|
||||||
rm $(BEAMDIR)/*.beam
|
|
||||||
rm -rf erl_crush.dump
|
|
122
README.md
122
README.md
|
@ -1,6 +1,12 @@
|
||||||
Erlware Commons
|
Erlware Commons
|
||||||
===============
|
===============
|
||||||
|
|
||||||
|
Current Status
|
||||||
|
--------------
|
||||||
|
|
||||||
|
[](https://hex.pm/packages/erlware_commons)
|
||||||
|
[](https://github.com/erlware/erlware_commons/actions)
|
||||||
|
|
||||||
Introduction
|
Introduction
|
||||||
------------
|
------------
|
||||||
|
|
||||||
|
@ -19,3 +25,119 @@ Goals for the project
|
||||||
* High Quality
|
* High Quality
|
||||||
* 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
|
||||||
|
------------------------------------
|
||||||
|
|
||||||
|
### [ec_date](https://github.com/erlware/erlware_commons/blob/master/src/ec_date.erl)
|
||||||
|
|
||||||
|
This module formats erlang dates in the form {{Year, Month, Day},
|
||||||
|
{Hour, Minute, Second}} to printable strings, using (almost)
|
||||||
|
equivalent formatting rules as http://uk.php.net/date, US vs European
|
||||||
|
dates are disambiguated in the same way as
|
||||||
|
http://uk.php.net/manual/en/function.strtotime.php That is, Dates in
|
||||||
|
the m/d/y or d-m-y formats are disambiguated by looking at the
|
||||||
|
separator between the various components: if the separator is a slash
|
||||||
|
(/), then the American m/d/y is assumed; whereas if the separator is a
|
||||||
|
dash (-) or a dot (.), then the European d-m-y format is assumed. To
|
||||||
|
avoid potential ambiguity, it's best to use ISO 8601 (YYYY-MM-DD)
|
||||||
|
dates.
|
||||||
|
|
||||||
|
erlang has no concept of timezone so the following formats are not
|
||||||
|
implemented: B e I O P T Z formats c and r will also differ slightly
|
||||||
|
|
||||||
|
### [ec_file](https://github.com/erlware/erlware_commons/blob/master/src/ec_file.erl)
|
||||||
|
|
||||||
|
A set of commonly defined helper functions for files that are not
|
||||||
|
included in stdlib.
|
||||||
|
|
||||||
|
### [ec_plists](https://github.com/erlware/erlware_commons/blob/master/src/ec_plists.erl)
|
||||||
|
|
||||||
|
plists is a drop-in replacement for module <a
|
||||||
|
href="http://www.erlang.org/doc/man/lists.html">lists</a>, making most
|
||||||
|
list operations parallel. It can operate on each element in parallel,
|
||||||
|
for IO-bound operations, on sublists in parallel, for taking advantage
|
||||||
|
of multi-core machines with CPU-bound operations, and across erlang
|
||||||
|
nodes, for parallelizing inside a cluster. It handles errors and node
|
||||||
|
failures. It can be configured, tuned, and tweaked to get optimal
|
||||||
|
performance while minimizing overhead.
|
||||||
|
|
||||||
|
Almost all the functions are identical to equivalent functions in
|
||||||
|
lists, returning exactly the same result, and having both a form with
|
||||||
|
an identical syntax that operates on each element in parallel and a
|
||||||
|
form which takes an optional "malt", a specification for how to
|
||||||
|
parallelize the operation.
|
||||||
|
|
||||||
|
fold is the one exception, parallel fold is different from linear
|
||||||
|
fold. This module also include a simple mapreduce implementation, and
|
||||||
|
the function runmany. All the other functions are implemented with
|
||||||
|
runmany, which is as a generalization of parallel list operations.
|
||||||
|
|
||||||
|
### [ec_semver](https://github.com/erlware/erlware_commons/blob/master/src/ec_semver.erl)
|
||||||
|
|
||||||
|
A complete parser for the [semver](http://semver.org/)
|
||||||
|
standard. Including a complete set of conforming comparison functions.
|
||||||
|
|
||||||
|
### [ec_lists](https://github.com/erlware/erlware_commons/blob/master/src/ec_lists.erl)
|
||||||
|
|
||||||
|
A set of additional list manipulation functions designed to supliment
|
||||||
|
the `lists` module in stdlib.
|
||||||
|
|
||||||
|
### [ec_talk](https://github.com/erlware/erlware_commons/blob/master/src/ec_talk.erl)
|
||||||
|
|
||||||
|
A set of simple utility functions to facilitate command line
|
||||||
|
communication with a user.
|
||||||
|
|
||||||
|
Signatures
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Other languages, have built in support for **Interface** or
|
||||||
|
**signature** functionality. Java has Interfaces, SML has
|
||||||
|
Signatures. Erlang, though, doesn't currently support this model, at
|
||||||
|
least not directly. There are a few ways you can approximate it. We
|
||||||
|
have defined a mechanism called *signatures* and several modules that
|
||||||
|
to serve as examples and provide a good set of *dictionary*
|
||||||
|
signatures. More information about signatures can be found at
|
||||||
|
[signature](https://github.com/erlware/erlware_commons/blob/master/doc/signatures.md).
|
||||||
|
|
||||||
|
|
||||||
|
### [ec_dictionary](https://github.com/erlware/erlware_commons/blob/master/src/ec_dictionary.erl)
|
||||||
|
|
||||||
|
A signature that supports association of keys to values. A map cannot
|
||||||
|
contain duplicate keys; each key can map to at most one value.
|
||||||
|
|
||||||
|
### [ec_dict](https://github.com/erlware/erlware_commons/blob/master/src/ec_dict.erl)
|
||||||
|
|
||||||
|
This provides an implementation of the ec_dictionary signature using
|
||||||
|
erlang's dicts as a base. The function documentation for ec_dictionary
|
||||||
|
applies here as well.
|
||||||
|
|
||||||
|
### [ec_gb_trees](https://github.com/erlware/erlware_commons/blob/master/src/ec_gb_trees.erl)
|
||||||
|
|
||||||
|
This provides an implementation of the ec_dictionary signature using
|
||||||
|
erlang's gb_trees as a base. The function documentation for
|
||||||
|
ec_dictionary applies here as well.
|
||||||
|
|
||||||
|
### [ec_orddict](https://github.com/erlware/erlware_commons/blob/master/src/ec_orddict.erl)
|
||||||
|
|
||||||
|
This provides an implementation of the ec_dictionary signature using
|
||||||
|
erlang's orddict as a base. The function documentation for
|
||||||
|
ec_dictionary applies here as well.
|
||||||
|
|
||||||
|
### [ec_rbdict](https://github.com/erlware/erlware_commons/blob/master/src/ec_rbdict.erl)
|
||||||
|
|
||||||
|
This provides an implementation of the ec_dictionary signature using
|
||||||
|
Robert Virding's rbdict module as a base. The function documentation
|
||||||
|
for ec_dictionary applies here as well.
|
||||||
|
|
|
@ -1,363 +0,0 @@
|
||||||
Property based testing for unit testers
|
|
||||||
=======================================
|
|
||||||
|
|
||||||
Main contributors: Torben Hoffmann, Raghav Karol, Eric Merritt
|
|
||||||
|
|
||||||
The purpose of the short document is to help people who are familiar
|
|
||||||
with unit testing understand how property based testing (PBT) differs,
|
|
||||||
but also where the thinking is the same.
|
|
||||||
|
|
||||||
This document focusses on the PBT tool
|
|
||||||
[`PropEr`](https://github.com/manopapad/proper) for Erlang since that is
|
|
||||||
what I am familiar with, but the general principles applies to all PBT
|
|
||||||
tools regardless of which language they are written in.
|
|
||||||
|
|
||||||
The approach taken here is that we hear from people who are used to
|
|
||||||
working with unit testing regarding how they think when designing
|
|
||||||
their tests and how a concrete test might look.
|
|
||||||
|
|
||||||
These descriptions are then "converted" into the way it works with
|
|
||||||
PBT, with a clear focus on what stays the same and what is different.
|
|
||||||
|
|
||||||
## Testing philosophies
|
|
||||||
|
|
||||||
### A quote from Martin Logan:
|
|
||||||
|
|
||||||
> For me unit testing is about contracts. I think about the same things
|
|
||||||
> I think about when I write statements like {ok, Resp} =
|
|
||||||
> Mod:Func(Args). Unit testing and writing specs are very close for me.
|
|
||||||
> Hypothetically speaking lets say a function should return return {ok,
|
|
||||||
> string()} | {error, term()} for all given input parameters then my
|
|
||||||
> unit tests should be able to show that for a representative set of
|
|
||||||
> input parameters that those contracts are honored. The art comes in
|
|
||||||
> thinking about what that set is.
|
|
||||||
|
|
||||||
|
|
||||||
The trap in writing all your own tests can often be that we think
|
|
||||||
about the set in terms of what we coded for and not what may indeed be
|
|
||||||
asked of our function. As the code is tried in further exploratory
|
|
||||||
testing and in production new input parameter sets for which the given
|
|
||||||
function does not meet the stated contract are discovered and added to
|
|
||||||
the test case once a fix has been put into place.
|
|
||||||
|
|
||||||
This is a very good description of what the ground rules for unit
|
|
||||||
testing are:
|
|
||||||
|
|
||||||
* Checking that contracts are obeyed.
|
|
||||||
* Creating a representative set of input parameters.
|
|
||||||
|
|
||||||
The former is very much part of PBT - each property you write will
|
|
||||||
check a contract, so that thinking is the same.
|
|
||||||
|
|
||||||
## xUnit vs PBT
|
|
||||||
|
|
||||||
Unit testing has become popular for software testing with the advent
|
|
||||||
of xUnit tools like jUnit for Java. xUnit like tools typically
|
|
||||||
provide a testing framework with the following functionality
|
|
||||||
|
|
||||||
* test fixture setup
|
|
||||||
* test case execution
|
|
||||||
* test fixture teardown
|
|
||||||
* test suite management
|
|
||||||
* test status reporting and management
|
|
||||||
|
|
||||||
While xUnit tools provide a lot of functionality to execute and manage
|
|
||||||
test cases and suites, reporting results there is no focus on test
|
|
||||||
case execution step, while this is the main focus area of
|
|
||||||
property-based testing (PBT).
|
|
||||||
|
|
||||||
Consider the following function specification
|
|
||||||
|
|
||||||
:::erlang
|
|
||||||
sort(list::integer()) ---> list::integer() | error
|
|
||||||
|
|
||||||
A verbal specification of this function is,
|
|
||||||
|
|
||||||
> For all input lists of integers, the sort function returns a sorted
|
|
||||||
> list of integers.
|
|
||||||
|
|
||||||
For any other kind of argument the function returns the atom error.
|
|
||||||
|
|
||||||
The specification above may be a requirement of how the function
|
|
||||||
should behave or even how the function does behave. This distinction
|
|
||||||
is important; the former is the requirement for the function, the
|
|
||||||
latter is the actual API. Both should be the same and that is what our
|
|
||||||
testing should confirm. Test cases for this function might look like
|
|
||||||
|
|
||||||
:::erlang
|
|
||||||
assertEqual(sort([5,4,3,2,1]), [1,2,3,4,5])
|
|
||||||
assertEqual(sort([1,2,3,4,5]), [1,2,3,4,5])
|
|
||||||
assertEqual(sort([] ), [] )
|
|
||||||
assertEqual(sort([-1,0, 1] ), [-1, 0, 1] )
|
|
||||||
|
|
||||||
How many tests cases should we write to be convinced that the actual
|
|
||||||
behaviour of the function is the same as its specification? Clearly,
|
|
||||||
it is impossible to write tests cases for all possible input values,
|
|
||||||
here all lists of integers, the art of testing is finding individual
|
|
||||||
input values that are representative of a large part of the input
|
|
||||||
space. We hope that the test cases are exhaustive to cover the
|
|
||||||
specification. xUnit tools offer no support for this and this is where
|
|
||||||
PBT and PBT Tools like `PropEr` and `QuickCheck` come in.
|
|
||||||
|
|
||||||
PBT introduces testing with a large set of random input values and
|
|
||||||
verifying that the specification holds for each input value
|
|
||||||
selected. Functions used to generate input values, generators, are
|
|
||||||
specified using rules and can be simply composed together to construct
|
|
||||||
complicated values. So, a property based test for the function above
|
|
||||||
may look like:
|
|
||||||
|
|
||||||
:::erlang
|
|
||||||
FOREACH({I, J, InputList}, {nat(), nat(), integer_list()},
|
|
||||||
SUCHTHAT(I < J andalso J < length(InputList),
|
|
||||||
SortedList = sort(InputList)
|
|
||||||
length(SortedList) == length(InputList)
|
|
||||||
andalso
|
|
||||||
lists:get(SortedList, I) =< lists:get(SortedList, J))
|
|
||||||
|
|
||||||
|
|
||||||
The property above works as follows
|
|
||||||
|
|
||||||
* Generate a random list of integers `InputList` and two natural numbers
|
|
||||||
I, J, such that I < J < size of `InputList`
|
|
||||||
* Check that size of sorted and input lists is the same.
|
|
||||||
* Check that element with smaller index I is less than or equal to
|
|
||||||
element with larger index J in `SortedList`.
|
|
||||||
|
|
||||||
Notice in the property above, we *specify* property. Verification of
|
|
||||||
the property based on random input values will be done by the property
|
|
||||||
based tool, therefore we can generated a large number of tests cases
|
|
||||||
with random input values and have a higher level of confidence that
|
|
||||||
the function when using unit tests alone.
|
|
||||||
|
|
||||||
But it does not stop at generation of input parameters. If you have
|
|
||||||
more complex tests where you have to generate a series of events and
|
|
||||||
keep track of some state then your PBT tool will generate random
|
|
||||||
sequences of events which corresponds to legal sequences of events and
|
|
||||||
test that your system behaves correctly for all sequences.
|
|
||||||
|
|
||||||
So when you have written a property with associated generators you
|
|
||||||
have in fact created something that can create numerous test cases -
|
|
||||||
you just have to tell your PBT tool how many test cases you want to
|
|
||||||
check the property on.
|
|
||||||
|
|
||||||
## Shrinking the bar
|
|
||||||
|
|
||||||
At this point you might still have the feeling that introducing the
|
|
||||||
notion of some sort of generators to your unit testing tool of choice
|
|
||||||
would bring you on par with PBT tools, but wait there is more to
|
|
||||||
come.
|
|
||||||
|
|
||||||
When a PBT tool creates a test case that fails there is real chance
|
|
||||||
that it has created a long test case or some big input parameters -
|
|
||||||
trying to debug that is very much like receiving a humongous log from
|
|
||||||
a system in the field and try to figure out what cause the system to
|
|
||||||
fail.
|
|
||||||
|
|
||||||
Enter shrinking...
|
|
||||||
|
|
||||||
When a test case fails the PBT tool will try to shrink the failing
|
|
||||||
test case down to the essentials by stripping out input elements or
|
|
||||||
events that does not cause the failure. In most cases this results in
|
|
||||||
a very short counterexample that clearly states which events and
|
|
||||||
inputs are required to break a property.
|
|
||||||
|
|
||||||
As we go through some concrete examples later the effects of shrinking
|
|
||||||
will be shown.
|
|
||||||
|
|
||||||
Shrinking makes it a lot easier to debug problems and is as key to the
|
|
||||||
strength of PBT as the generators.
|
|
||||||
|
|
||||||
## Converting a unit test
|
|
||||||
|
|
||||||
We will now take a look at one possible way of translating a unit
|
|
||||||
test into a PBT setting.
|
|
||||||
|
|
||||||
The example comes from Eric Merritt and is about the `add/2` function in
|
|
||||||
the `ec_dictionary` instance `ec_gb_trees`.
|
|
||||||
|
|
||||||
The add function has the following spec:
|
|
||||||
|
|
||||||
:::erlang
|
|
||||||
-spec add(ec_dictionary:key(), ec_dictionary:value(), Object::dictionary()) ->
|
|
||||||
dictionary().
|
|
||||||
|
|
||||||
and it is supposed to do the obvious: add the key and value pair to
|
|
||||||
the dictionary and return a new dictionary.
|
|
||||||
|
|
||||||
Eric states his basic expectations as follows:
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
The first two expectations regarding being able to use arbritrary
|
|
||||||
terms as keys and values is a job for generators.
|
|
||||||
|
|
||||||
The latter four are prime candidates for properties and we will create
|
|
||||||
one for each of them.
|
|
||||||
|
|
||||||
### Generators
|
|
||||||
|
|
||||||
:::erlang
|
|
||||||
key() -> any().
|
|
||||||
|
|
||||||
value() -> any().
|
|
||||||
|
|
||||||
|
|
||||||
For `PropEr` this approach has the drawback that creation and shrinking
|
|
||||||
becomes rather time consuming, so it might be better to narrow to
|
|
||||||
something like this:
|
|
||||||
|
|
||||||
:::erlang
|
|
||||||
key() -> union([integer(),atom()]).
|
|
||||||
|
|
||||||
value() -> union([integer(),atom(),binary(),boolean(),string()]).
|
|
||||||
|
|
||||||
What is best depends on the situation and intended usage.
|
|
||||||
|
|
||||||
Now, being able to generate keys and values is not enough. You also
|
|
||||||
have to tell `PropEr` how to create a dictionary and in this case we
|
|
||||||
will use a symbolic generator (detail to be explained later).
|
|
||||||
|
|
||||||
:::erlang
|
|
||||||
sym_dict() ->
|
|
||||||
?SIZED(N,sym_dict(N)).
|
|
||||||
|
|
||||||
sym_dict(0) ->
|
|
||||||
{'$call',ec_dictionary,new,[ec_gb_trees]};
|
|
||||||
sym_dict(N) ->
|
|
||||||
?LAZY(
|
|
||||||
frequency([
|
|
||||||
{1, {'$call',ec_dictionary,remove,[key(),sym_dict(N-1)]}},
|
|
||||||
{2, {'$call',ec_dictionary,add,[value(),value(),sym_dict(N-1)]}}
|
|
||||||
])).
|
|
||||||
|
|
||||||
|
|
||||||
`sym_dict/0` uses the `?SIZED` macro to control the size of the
|
|
||||||
generated dictionary. `PropEr` will start out with small numbers and
|
|
||||||
gradually raise it.
|
|
||||||
|
|
||||||
`sym_dict/1` is building a dictionary by randomly adding key/value
|
|
||||||
pairs and removing keys. Eventually the base case is reached which
|
|
||||||
will create an empty dictionary.
|
|
||||||
|
|
||||||
The `?LAZY` macro is used to defer the calculation of the
|
|
||||||
`sym_dict(N-1)` until they are needed and `frequency/1` is used
|
|
||||||
to ensure that twice as many adds compared to removes are done. This
|
|
||||||
should give rather more interesting dictionaries in the long run, if
|
|
||||||
not one can alter the frequencies accondingly.
|
|
||||||
|
|
||||||
But does it really work?
|
|
||||||
|
|
||||||
That is a good question and one that should always be asked when
|
|
||||||
looking at genetors. Fortunately there is a way to see what a
|
|
||||||
generator produces provided that the generator functions are exported.
|
|
||||||
|
|
||||||
Hint: in most cases it will not hurt to throw in a
|
|
||||||
`-compile(export_all).` in the module used to specify the
|
|
||||||
properties. And here we actually have a sub-hint: specify the
|
|
||||||
properties in a separate file to avoid peeking inside the
|
|
||||||
implementation! Base the test on the published API as this is what the
|
|
||||||
users of the code will be restricted to.
|
|
||||||
|
|
||||||
When the test module has been loaded you can test the generators by
|
|
||||||
starting up an Erlang shell (this example uses the erlware_commons
|
|
||||||
code so get yourself a clone to play with):
|
|
||||||
|
|
||||||
:::sh
|
|
||||||
$ erl -pz ebin -pz test
|
|
||||||
1> proper_gen:pick(ec_dictionary_proper:key()).
|
|
||||||
{ok,4}
|
|
||||||
2> proper_gen:pick(ec_dictionary_proper:key()).
|
|
||||||
{ok,35}
|
|
||||||
3> proper_gen:pick(ec_dictionary_proper:key()).
|
|
||||||
{ok,-5}
|
|
||||||
4> proper_gen:pick(ec_dictionary_proper:key()).
|
|
||||||
{ok,48}
|
|
||||||
5> proper_gen:pick(ec_dictionary_proper:key()).
|
|
||||||
{ok,'\036\207_là´?\nc'}
|
|
||||||
6> proper_gen:pick(ec_dictionary_proper:value()).
|
|
||||||
{ok,2}
|
|
||||||
7> proper_gen:pick(ec_dictionary_proper:value()).
|
|
||||||
{ok,-14}
|
|
||||||
8> proper_gen:pick(ec_dictionary_proper:value()).
|
|
||||||
{ok,-3}
|
|
||||||
9> proper_gen:pick(ec_dictionary_proper:value()).
|
|
||||||
{ok,27}
|
|
||||||
10> proper_gen:pick(ec_dictionary_proper:value()).
|
|
||||||
{ok,-8}
|
|
||||||
11> proper_gen:pick(ec_dictionary_proper:value()).
|
|
||||||
{ok,[472765,17121]}
|
|
||||||
12> proper_gen:pick(ec_dictionary_proper:value()).
|
|
||||||
{ok,true}
|
|
||||||
13> proper_gen:pick(ec_dictionary_proper:value()).
|
|
||||||
{ok,<<>>}
|
|
||||||
14> proper_gen:pick(ec_dictionary_proper:value()).
|
|
||||||
{ok,<<89,69,18,148,32,42,238,101>>}
|
|
||||||
15> proper_gen:pick(ec_dictionary_proper:sym_dict()).
|
|
||||||
{ok,{'$call',ec_dictionary,add,
|
|
||||||
[[114776,1053475],
|
|
||||||
'fª\020\227\215',
|
|
||||||
{'$call',ec_dictionary,add,
|
|
||||||
['',true,
|
|
||||||
{'$call',ec_dictionary,add,
|
|
||||||
['2^Ø¡',
|
|
||||||
[900408,886056],
|
|
||||||
{'$call',ec_dictionary,add,[[48618|...],<<...>>|...]}]}]}]}}
|
|
||||||
16> proper_gen:pick(ec_dictionary_proper:sym_dict()).
|
|
||||||
{ok,{'$call',ec_dictionary,add,
|
|
||||||
[10,'a¯\214\031fõC',
|
|
||||||
{'$call',ec_dictionary,add,
|
|
||||||
[false,-1,
|
|
||||||
{'$call',ec_dictionary,remove,
|
|
||||||
['d·ÉV÷[',
|
|
||||||
{'$call',ec_dictionary,remove,[12,{'$call',...}]}]}]}]}}
|
|
||||||
|
|
||||||
That does not look too bad, so we will continue with that for now.
|
|
||||||
|
|
||||||
|
|
||||||
### Properties of `add/2`
|
|
||||||
|
|
||||||
The first expectation Eric had about how the dictionary works was that
|
|
||||||
if a key had been stored it could be retrieved.
|
|
||||||
|
|
||||||
One way of expressing this could be with this property:
|
|
||||||
|
|
||||||
:::erlang
|
|
||||||
prop_get_after_add_returns_correct_value() ->
|
|
||||||
?FORALL({Dict,K,V}, {sym_dict(),key(),value()},
|
|
||||||
begin
|
|
||||||
try ec_dictionary:get(K,ec_dictionary:add(K,V,Dict)) of
|
|
||||||
V ->
|
|
||||||
true;
|
|
||||||
_ ->
|
|
||||||
false
|
|
||||||
catch
|
|
||||||
_:_ ->
|
|
||||||
false
|
|
||||||
end
|
|
||||||
end).
|
|
||||||
|
|
||||||
This property reads that for all dictionaries `get/2` using a key
|
|
||||||
from a key/value pair just inserted using the `add/3` function
|
|
||||||
will return that value. If that is not the case the property will
|
|
||||||
evaluate to false.
|
|
||||||
|
|
||||||
Running the property is done using `proper:quickcheck/1`:
|
|
||||||
|
|
||||||
:::sh
|
|
||||||
proper:quickcheck(ec_dictionary_proper:prop_get_after_add_returns_correct_value()).
|
|
||||||
....................................................................................................
|
|
||||||
OK: Passed 100 test(s).
|
|
||||||
true
|
|
||||||
|
|
||||||
|
|
||||||
This was as expected, but at this point we will take a little detour
|
|
||||||
and introduce a mistake in the `ec_gb_trees` implementation and see
|
|
||||||
how that works.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
functionaly. Often there are several algorithms that could provide
|
functionalities. Often there are several algorithms that could provide
|
||||||
this functionality. However, the code that uses it, either doesn't
|
each of these functionalities. 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. Lets take the concrete example of
|
that algorithm to some higher level. Let's 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 there own performance characteristics,
|
Each of these approaches has their 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 O(1 + k/n)
|
and $k$ keys has the minimum $\max(0, k-n)$ collisions and $\mathcal{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 (O log n). So the choice of which to select
|
search trees - namely $\mathcal{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 thats ot so easy to do at the moment.
|
good idea. Unfortunately, in Erlang that's 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,17 +39,20 @@ 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
|
|
||||||
add(Key, Value, {ModuleToUse, DictData}) ->
|
```erlang
|
||||||
ModuleToUse:add(Key, Value, DictData).
|
add(Key, Value, {ModuleToUse, 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
|
||||||
|
@ -63,21 +66,22 @@ 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 inadvertantly pass an unexpected function name as
|
someone could inadvertently 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 flexable language so we can use a
|
Fortunately, Erlang is a pretty flexible 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 flexibiltiy of ignoreing a specific implementation
|
worlds. Both the flexibility of ignoring 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
|
||||||
|
@ -90,54 +94,56 @@ 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 has built in?
|
How do we actually do this in Erlang now that Erlang is missing what Java, SML and friends have 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 wont help use actually use the
|
the functions they need to have, but it won't help us 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. here in the record. We call the module name named in
|
data 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
|
||||||
|
@ -148,16 +154,17 @@ 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.
|
||||||
|
|
||||||
|
@ -173,16 +180,17 @@ 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
|
||||||
|
@ -197,7 +205,7 @@ implementation in
|
||||||
Using Signatures
|
Using Signatures
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
Its a good idea to work through an example so we have a bit better
|
It's 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
|
||||||
|
@ -206,7 +214,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 implementations, the second is a
|
Signatures. The first is a native implementation, the second is a
|
||||||
wrapper.
|
wrapper.
|
||||||
|
|
||||||
### Native Signature Implementations
|
### Native Signature Implementations
|
||||||
|
@ -223,32 +231,33 @@ 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 if this in our current example
|
defined by a Signature. A good example of 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. Lets take a look at one example to see how this is done.
|
itself. Let's 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 an ec_dictionary `get` doesn't have quite the same
|
`get` function in `ec_dictionary` 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 module `get` function.
|
translation needs to be done. We do that in the `ec_dict:get/2` 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.
|
||||||
|
|
||||||
|
|
||||||
|
@ -258,24 +267,25 @@ the mix and that adds a bit of additional overhead.
|
||||||
|
|
||||||
### Creating the Timing Module
|
### Creating the Timing Module
|
||||||
|
|
||||||
We are going to creating timings for both Native Signature
|
We are going to be creating timings for both Native Signature
|
||||||
Implementations and Signature Wrappers.
|
Implementations and Signature Wrappers.
|
||||||
|
|
||||||
Lets get started by looking at some helper functions. We want
|
Let's get started by looking at some helper functions. We want
|
||||||
dictionaries to have a bit of data in them. So to that end we are will
|
dictionaries to have a bit of data in them. So to that end we 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.
|
||||||
|
@ -283,58 +293,61 @@ 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 dict
|
Signatures. Otherwise, this does the exact same thing as the dictionary
|
||||||
`create_dict/1`.
|
given by `create_dict/0`.
|
||||||
|
|
||||||
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 that we are going to use the `size` function as well as
|
dictionaries 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 (its
|
are going to run these one million times to get a good average (it's
|
||||||
fast so it doesn't take long). You can see that in the anonymous
|
fast so it doesn't take long). You can see 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.
|
||||||
|
|
||||||
|
@ -343,25 +356,26 @@ 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. Thats a 25% cost for
|
for the Signature Wrapper we average around 4. That's 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
|
||||||
|
@ -373,30 +387,32 @@ 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?
|
||||||
|
|
||||||
|
@ -406,34 +422,35 @@ 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 its really, really small - or small
|
difference is. Essentially that means it's 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. The have little or no over head depending on the type of
|
in Erlang. They have little or no overhead 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.
|
||||||
|
|
||||||
|
@ -456,7 +473,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)
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
%% -*- mode: Erlang; fill-column: 75; comment-column: 50; -*-
|
|
||||||
{application, erlware_commons,
|
|
||||||
[{description, "Additional standard library for Erlang"},
|
|
||||||
{vsn, "0.6.1"},
|
|
||||||
{modules, [
|
|
||||||
ec_talk,
|
|
||||||
ec_lists,
|
|
||||||
ec_plists,
|
|
||||||
ec_file,
|
|
||||||
ec_string,
|
|
||||||
ec_semver,
|
|
||||||
ec_date,
|
|
||||||
ec_dictionary,
|
|
||||||
ec_assoc_list,
|
|
||||||
ec_dict,
|
|
||||||
ec_gb_trees,
|
|
||||||
ec_rbdict,
|
|
||||||
ec_orddict]},
|
|
||||||
{registered, []},
|
|
||||||
{applications, [kernel, stdlib]}]}.
|
|
24
include/ec_cmd_log.hrl
Normal file
24
include/ec_cmd_log.hrl
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
%% -*- erlang-indent-level: 4; indent-tabs-mode: nil; fill-column: 80 -*-
|
||||||
|
%%% Copyright 2012 Erlware, LLC. All Rights Reserved.
|
||||||
|
%%%
|
||||||
|
%%% This file is provided to you under the Apache License,
|
||||||
|
%%% Version 2.0 (the "License"); you may not use this file
|
||||||
|
%%% except in compliance with the License. You may obtain
|
||||||
|
%%% a copy of the License at
|
||||||
|
%%%
|
||||||
|
%%% http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
%%%
|
||||||
|
%%% Unless required by applicable law or agreed to in writing,
|
||||||
|
%%% software distributed under the License is distributed on an
|
||||||
|
%%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
%%% KIND, either express or implied. See the License for the
|
||||||
|
%%% specific language governing permissions and limitations
|
||||||
|
%%% under the License.
|
||||||
|
%%%---------------------------------------------------------------------------
|
||||||
|
%%% @author Eric Merritt <ericbmerritt@gmail.com>
|
||||||
|
%%% @copyright (C) 2012 Erlware, LLC.
|
||||||
|
|
||||||
|
-define(EC_ERROR, 0).
|
||||||
|
-define(EC_WARN, 1).
|
||||||
|
-define(EC_INFO, 2).
|
||||||
|
-define(EC_DEBUG, 3).
|
9
priv/ec_semver_parser.peg
Normal file
9
priv/ec_semver_parser.peg
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
semver <- major_minor_patch_min_patch ("-" alpha_part ("." alpha_part)*)? ("+" alpha_part ("." alpha_part)*)? !.
|
||||||
|
` ec_semver:internal_parse_version(Node) ` ;
|
||||||
|
|
||||||
|
major_minor_patch_min_patch <- ("v"? numeric_part / alpha_part) ("." version_part)? ("." version_part)? ("." version_part)? ;
|
||||||
|
|
||||||
|
version_part <- numeric_part / alpha_part ;
|
||||||
|
|
||||||
|
numeric_part <- [0-9]+ `erlang:list_to_integer(erlang:binary_to_list(erlang:iolist_to_binary(Node)))` ;
|
||||||
|
alpha_part <- [A-Za-z0-9]+ `erlang:iolist_to_binary(Node)` ;
|
24
rebar.config
Normal file
24
rebar.config
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*-
|
||||||
|
|
||||||
|
%% Dependencies ================================================================
|
||||||
|
{deps, [
|
||||||
|
{cf, "~>0.3"}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{erl_first_files, ["ec_dictionary", "ec_vsn"]}.
|
||||||
|
|
||||||
|
%% Compiler Options ============================================================
|
||||||
|
{erl_opts, [debug_info, warnings_as_errors]}.
|
||||||
|
|
||||||
|
%% EUnit =======================================================================
|
||||||
|
{eunit_opts, [verbose,
|
||||||
|
{report, {eunit_surefire, [{dir, "."}]}}]}.
|
||||||
|
|
||||||
|
{cover_enabled, true}.
|
||||||
|
{cover_print_enabled, true}.
|
||||||
|
|
||||||
|
%% Profiles ====================================================================
|
||||||
|
{profiles, [{dev, [{deps,
|
||||||
|
[{neotoma, "",
|
||||||
|
{git, "https://github.com/seancribbs/neotoma.git", {branch, master}}}]}]}
|
||||||
|
]}.
|
7
rebar.config.script
Normal file
7
rebar.config.script
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
NoDialWarns = {dialyzer, [{warnings, [no_unknown]}]},
|
||||||
|
OTPRelease = erlang:list_to_integer(erlang:system_info(otp_release)),
|
||||||
|
|
||||||
|
case OTPRelease<26 of
|
||||||
|
true -> CONFIG;
|
||||||
|
false -> lists:keystore(dialyzer, 1, CONFIG, NoDialWarns)
|
||||||
|
end.
|
8
rebar.lock
Normal file
8
rebar.lock
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{"1.2.0",
|
||||||
|
[{<<"cf">>,{pkg,<<"cf">>,<<"0.3.1">>},0}]}.
|
||||||
|
[
|
||||||
|
{pkg_hash,[
|
||||||
|
{<<"cf">>, <<"5CB902239476E141EA70A740340233782D363A31EEA8AD37049561542E6CD641">>}]},
|
||||||
|
{pkg_hash_ext,[
|
||||||
|
{<<"cf">>, <<"315E8D447D3A4B02BCDBFA397AD03BBB988A6E0AA6F44D3ADD0F4E3C3BF97672">>}]}
|
||||||
|
].
|
BIN
rebar3
Executable file
BIN
rebar3
Executable file
Binary file not shown.
|
@ -1,11 +1,12 @@
|
||||||
|
%%% vi:ts=4 sw=4 et
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
%%% @author Eric Merritt <ericbmerritt@gmail.com>
|
%%% @author Eric Merritt <ericbmerritt@gmail.com>
|
||||||
%%% @copyright 2011 Erlware, LLC.
|
%%% @copyright 2011 Erlware, LLC.
|
||||||
%%% @doc
|
%%% @doc
|
||||||
%%% provides an implementation of ec_dictionary using an association
|
%%% provides an implementation of ec_dictionary using an association
|
||||||
%%% list as a basy
|
%%% list as a basy
|
||||||
|
%%% see ec_dictionary
|
||||||
%%% @end
|
%%% @end
|
||||||
%%% @see ec_dictionary
|
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
-module(ec_assoc_list).
|
-module(ec_assoc_list).
|
||||||
|
|
||||||
|
@ -29,8 +30,10 @@
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
%%% Types
|
%%% Types
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
-opaque dictionary(K, V) :: {ec_assoc_list,
|
%% This should be opaque, but that kills dialyzer so for now we export it
|
||||||
[{ec_dictionary:key(K), ec_dictionary:value(V)}]}.
|
%% however you should not rely on the internal representation here
|
||||||
|
-type dictionary(K, V) :: {ec_assoc_list,
|
||||||
|
[{ec_dictionary:key(K), ec_dictionary:value(V)}]}.
|
||||||
|
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
%%% API
|
%%% API
|
||||||
|
@ -45,12 +48,12 @@ has_key(Key, {ec_assoc_list, Data}) ->
|
||||||
lists:keymember(Key, 1, Data).
|
lists:keymember(Key, 1, Data).
|
||||||
|
|
||||||
-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, {ec_assoc_list, Data}) ->
|
get(Key, {ec_assoc_list, Data}) ->
|
||||||
case lists:keyfind(Key, 1, Data) of
|
case lists:keyfind(Key, 1, Data) of
|
||||||
{Key, Value} ->
|
{Key, Value} ->
|
||||||
Value;
|
Value;
|
||||||
false ->
|
false ->
|
||||||
throw(not_found)
|
throw(not_found)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
@ -62,19 +65,19 @@ get(Key, Default, {ec_assoc_list, Data}) ->
|
||||||
case lists:keyfind(Key, 1, Data) of
|
case lists:keyfind(Key, 1, Data) of
|
||||||
{Key, Value} ->
|
{Key, Value} ->
|
||||||
Value;
|
Value;
|
||||||
false ->
|
false ->
|
||||||
Default
|
Default
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec add(ec_dictionary:key(K), ec_dictionary:value(V),
|
-spec add(ec_dictionary:key(K), ec_dictionary:value(V),
|
||||||
Object::dictionary(K, V)) ->
|
Object::dictionary(K, V)) ->
|
||||||
dictionary(K, V).
|
dictionary(K, V).
|
||||||
add(Key, Value, {ec_assoc_list, _Data}=Dict) ->
|
add(Key, Value, {ec_assoc_list, _Data}=Dict) ->
|
||||||
{ec_assoc_list, Rest} = remove(Key,Dict),
|
{ec_assoc_list, Rest} = remove(Key,Dict),
|
||||||
{ec_assoc_list, [{Key, Value} | Rest ]}.
|
{ec_assoc_list, [{Key, Value} | Rest ]}.
|
||||||
|
|
||||||
-spec remove(ec_dictionary:key(K), Object::dictionary(K, _V)) ->
|
-spec remove(ec_dictionary:key(K), Object::dictionary(K, _V)) ->
|
||||||
dictionary(K, _V).
|
dictionary(K, _V).
|
||||||
remove(Key, {ec_assoc_list, Data}) ->
|
remove(Key, {ec_assoc_list, Data}) ->
|
||||||
{ec_assoc_list, lists:keydelete(Key, 1, Data)}.
|
{ec_assoc_list, lists:keydelete(Key, 1, Data)}.
|
||||||
|
|
||||||
|
@ -82,22 +85,22 @@ remove(Key, {ec_assoc_list, Data}) ->
|
||||||
has_value(Value, {ec_assoc_list, Data}) ->
|
has_value(Value, {ec_assoc_list, Data}) ->
|
||||||
lists:keymember(Value, 2, Data).
|
lists:keymember(Value, 2, Data).
|
||||||
|
|
||||||
-spec size(Object::dictionary(_K, _V)) -> integer().
|
-spec size(Object::dictionary(_K, _V)) -> non_neg_integer().
|
||||||
size({ec_assoc_list, Data}) ->
|
size({ec_assoc_list, Data}) ->
|
||||||
length(Data).
|
length(Data).
|
||||||
|
|
||||||
-spec to_list(dictionary(K, V)) -> [{ec_dictionary:key(K),
|
-spec to_list(dictionary(K, V)) -> [{ec_dictionary:key(K),
|
||||||
ec_dictionary:value(V)}].
|
ec_dictionary:value(V)}].
|
||||||
to_list({ec_assoc_list, Data}) ->
|
to_list({ec_assoc_list, Data}) ->
|
||||||
Data.
|
Data.
|
||||||
|
|
||||||
-spec from_list([{ec_dictionary:key(K), ec_dictionary:value(V)}]) ->
|
-spec from_list([{ec_dictionary:key(K), ec_dictionary:value(V)}]) ->
|
||||||
dictionary(K, V).
|
dictionary(K, V).
|
||||||
from_list(List) when is_list(List) ->
|
from_list(List) when is_list(List) ->
|
||||||
{ec_assoc_list, List}.
|
{ec_assoc_list, List}.
|
||||||
|
|
||||||
-spec keys(dictionary(K, _V)) -> [ec_dictionary:key(K)].
|
-spec keys(dictionary(K, _V)) -> [ec_dictionary:key(K)].
|
||||||
keys({ec_assoc_list, Data}) ->
|
keys({ec_assoc_list, Data}) ->
|
||||||
lists:map(fun({Key, _Value}) ->
|
lists:map(fun({Key, _Value}) ->
|
||||||
Key
|
Key
|
||||||
end, Data).
|
end, Data).
|
||||||
|
|
257
src/ec_cmd_log.erl
Normal file
257
src/ec_cmd_log.erl
Normal file
|
@ -0,0 +1,257 @@
|
||||||
|
%% -*- erlang-indent-level: 4; indent-tabs-mode: nil; fill-column: 80 -*-
|
||||||
|
%%% Copyright 2012 Erlware, LLC. All Rights Reserved.
|
||||||
|
%%%
|
||||||
|
%%% This file is provided to you under the Apache License,
|
||||||
|
%%% Version 2.0 (the "License"); you may not use this file
|
||||||
|
%%% except in compliance with the License. You may obtain
|
||||||
|
%%% a copy of the License at
|
||||||
|
%%%
|
||||||
|
%%% http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
%%%
|
||||||
|
%%% Unless required by applicable law or agreed to in writing,
|
||||||
|
%%% software distributed under the License is distributed on an
|
||||||
|
%%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
%%% KIND, either express or implied. See the License for the
|
||||||
|
%%% specific language governing permissions and limitations
|
||||||
|
%%% under the License.
|
||||||
|
%%%---------------------------------------------------------------------------
|
||||||
|
%%% @author Eric Merritt <ericbmerritt@gmail.com>
|
||||||
|
%%% @copyright (C) 2012 Erlware, LLC.
|
||||||
|
%%%
|
||||||
|
%%% @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
|
||||||
|
-module(ec_cmd_log).
|
||||||
|
|
||||||
|
%% Avoid clashing with `error/3` BIF added in Erlang/OTP 24
|
||||||
|
-compile({no_auto_import,[error/3]}).
|
||||||
|
|
||||||
|
-export([new/1,
|
||||||
|
new/2,
|
||||||
|
new/3,
|
||||||
|
log/4,
|
||||||
|
should/2,
|
||||||
|
debug/2,
|
||||||
|
debug/3,
|
||||||
|
info/2,
|
||||||
|
info/3,
|
||||||
|
error/2,
|
||||||
|
error/3,
|
||||||
|
warn/2,
|
||||||
|
warn/3,
|
||||||
|
log_level/1,
|
||||||
|
atom_log_level/1,
|
||||||
|
colorize/4,
|
||||||
|
format/1]).
|
||||||
|
|
||||||
|
-include("include/ec_cmd_log.hrl").
|
||||||
|
-include("src/ec_cmd_log.hrl").
|
||||||
|
|
||||||
|
-define(PREFIX, "===> ").
|
||||||
|
|
||||||
|
-record(state_t, {log_level=0 :: int_log_level(),
|
||||||
|
caller=api :: caller(),
|
||||||
|
intensity=low :: intensity()}).
|
||||||
|
|
||||||
|
%%============================================================================
|
||||||
|
%% types
|
||||||
|
%%============================================================================
|
||||||
|
-export_type([t/0,
|
||||||
|
int_log_level/0,
|
||||||
|
atom_log_level/0,
|
||||||
|
log_level/0,
|
||||||
|
caller/0,
|
||||||
|
log_fun/0]).
|
||||||
|
|
||||||
|
-type caller() :: api | command_line.
|
||||||
|
|
||||||
|
-type log_level() :: int_log_level() | atom_log_level().
|
||||||
|
|
||||||
|
-type int_log_level() :: 0..3.
|
||||||
|
|
||||||
|
-type atom_log_level() :: error | warn | info | debug.
|
||||||
|
|
||||||
|
-type intensity() :: none | low | high.
|
||||||
|
|
||||||
|
-type log_fun() :: fun(() -> iolist()).
|
||||||
|
|
||||||
|
-type color() :: char().
|
||||||
|
|
||||||
|
-opaque t() :: #state_t{}.
|
||||||
|
|
||||||
|
%%============================================================================
|
||||||
|
%% API
|
||||||
|
%%============================================================================
|
||||||
|
%% @doc Create a new 'log level' for the system
|
||||||
|
-spec new(log_level()) -> t().
|
||||||
|
new(LogLevel) ->
|
||||||
|
new(LogLevel, api).
|
||||||
|
|
||||||
|
-spec new(log_level(), caller()) -> t().
|
||||||
|
new(LogLevel, Caller) ->
|
||||||
|
new(LogLevel, Caller, high).
|
||||||
|
|
||||||
|
|
||||||
|
-spec new(log_level(), caller(), intensity()) -> t().
|
||||||
|
new(LogLevel, Caller, Intensity) when (Intensity =:= none orelse
|
||||||
|
Intensity =:= low orelse
|
||||||
|
Intensity =:= high),
|
||||||
|
LogLevel >= 0, LogLevel =< 3 ->
|
||||||
|
#state_t{log_level=LogLevel, caller=Caller,
|
||||||
|
intensity=Intensity};
|
||||||
|
new(AtomLogLevel, Caller, Intensity)
|
||||||
|
when AtomLogLevel =:= error;
|
||||||
|
AtomLogLevel =:= warn;
|
||||||
|
AtomLogLevel =:= info;
|
||||||
|
AtomLogLevel =:= debug ->
|
||||||
|
LogLevel = case AtomLogLevel of
|
||||||
|
error -> 0;
|
||||||
|
warn -> 1;
|
||||||
|
info -> 2;
|
||||||
|
debug -> 3
|
||||||
|
end,
|
||||||
|
new(LogLevel, Caller, Intensity).
|
||||||
|
|
||||||
|
|
||||||
|
%% @doc log at the debug level given the current log state with a string or
|
||||||
|
%% function that returns a string
|
||||||
|
-spec debug(t(), string() | log_fun()) -> ok.
|
||||||
|
debug(LogState, Fun)
|
||||||
|
when erlang:is_function(Fun) ->
|
||||||
|
log(LogState, ?EC_DEBUG, fun() ->
|
||||||
|
colorize(LogState, ?CYAN, false, Fun())
|
||||||
|
end);
|
||||||
|
debug(LogState, String) ->
|
||||||
|
debug(LogState, "~ts~n", [String]).
|
||||||
|
|
||||||
|
%% @doc log at the debug level given the current log state with a format string
|
||||||
|
%% and arguments @see io:format/2
|
||||||
|
-spec debug(t(), string(), [any()]) -> ok.
|
||||||
|
debug(LogState, FormatString, Args) ->
|
||||||
|
log(LogState, ?EC_DEBUG, colorize(LogState, ?CYAN, false, FormatString), Args).
|
||||||
|
|
||||||
|
%% @doc log at the info level given the current log state with a string or
|
||||||
|
%% function that returns a string
|
||||||
|
-spec info(t(), string() | log_fun()) -> ok.
|
||||||
|
info(LogState, Fun)
|
||||||
|
when erlang:is_function(Fun) ->
|
||||||
|
log(LogState, ?EC_INFO, fun() ->
|
||||||
|
colorize(LogState, ?GREEN, false, Fun())
|
||||||
|
end);
|
||||||
|
info(LogState, String) ->
|
||||||
|
info(LogState, "~ts~n", [String]).
|
||||||
|
|
||||||
|
%% @doc log at the info level given the current log state with a format string
|
||||||
|
%% and arguments @see io:format/2
|
||||||
|
-spec info(t(), string(), [any()]) -> ok.
|
||||||
|
info(LogState, FormatString, Args) ->
|
||||||
|
log(LogState, ?EC_INFO, colorize(LogState, ?GREEN, false, FormatString), Args).
|
||||||
|
|
||||||
|
%% @doc log at the error level given the current log state with a string or
|
||||||
|
%% format string that returns a function
|
||||||
|
-spec error(t(), string() | log_fun()) -> ok.
|
||||||
|
error(LogState, Fun)
|
||||||
|
when erlang:is_function(Fun) ->
|
||||||
|
log(LogState, ?EC_ERROR, fun() ->
|
||||||
|
colorize(LogState, ?RED, false, Fun())
|
||||||
|
end);
|
||||||
|
error(LogState, String) ->
|
||||||
|
error(LogState, "~ts~n", [String]).
|
||||||
|
|
||||||
|
%% @doc log at the error level given the current log state with a format string
|
||||||
|
%% and arguments @see io:format/2
|
||||||
|
-spec error(t(), string(), [any()]) -> ok.
|
||||||
|
error(LogState, FormatString, Args) ->
|
||||||
|
log(LogState, ?EC_ERROR, colorize(LogState, ?RED, false, FormatString), Args).
|
||||||
|
|
||||||
|
%% @doc log at the warn level given the current log state with a string or
|
||||||
|
%% format string that returns a function
|
||||||
|
-spec warn(t(), string() | log_fun()) -> ok.
|
||||||
|
warn(LogState, Fun)
|
||||||
|
when erlang:is_function(Fun) ->
|
||||||
|
log(LogState, ?EC_WARN, fun() -> colorize(LogState, ?MAGENTA, false, Fun()) end);
|
||||||
|
warn(LogState, String) ->
|
||||||
|
warn(LogState, "~ts~n", [String]).
|
||||||
|
|
||||||
|
%% @doc log at the warn level given the current log state with a format string
|
||||||
|
%% and arguments @see io:format/2
|
||||||
|
-spec warn(t(), string(), [any()]) -> ok.
|
||||||
|
warn(LogState, FormatString, Args) ->
|
||||||
|
log(LogState, ?EC_WARN, colorize(LogState, ?MAGENTA, false, FormatString), Args).
|
||||||
|
|
||||||
|
%% @doc Execute the fun passed in if log level is as expected.
|
||||||
|
-spec log(t(), int_log_level(), log_fun()) -> ok.
|
||||||
|
log(#state_t{log_level=DetailLogLevel}, LogLevel, Fun)
|
||||||
|
when DetailLogLevel >= LogLevel ->
|
||||||
|
io:format("~ts~n", [Fun()]);
|
||||||
|
log(_, _, _) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%% @doc when the module log level is less then or equal to the log level for the
|
||||||
|
%% call then write the log info out. When its not then ignore the call.
|
||||||
|
-spec log(t(), int_log_level(), string(), [any()]) -> ok.
|
||||||
|
log(#state_t{log_level=DetailLogLevel}, LogLevel, FormatString, Args)
|
||||||
|
when DetailLogLevel >= LogLevel,
|
||||||
|
erlang:is_list(Args) ->
|
||||||
|
io:format(FormatString, Args);
|
||||||
|
log(_, _, _, _) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%% @doc return a boolean indicating if the system should log for the specified
|
||||||
|
%% levelg
|
||||||
|
-spec should(t(), int_log_level() | any()) -> boolean().
|
||||||
|
should(#state_t{log_level=DetailLogLevel}, LogLevel)
|
||||||
|
when DetailLogLevel >= LogLevel ->
|
||||||
|
true;
|
||||||
|
should(_, _) ->
|
||||||
|
false.
|
||||||
|
|
||||||
|
%% @doc get the current log level as an integer
|
||||||
|
-spec log_level(t()) -> int_log_level().
|
||||||
|
log_level(#state_t{log_level=DetailLogLevel}) ->
|
||||||
|
DetailLogLevel.
|
||||||
|
|
||||||
|
%% @doc get the current log level as an atom
|
||||||
|
-spec atom_log_level(t()) -> atom_log_level().
|
||||||
|
atom_log_level(#state_t{log_level=?EC_ERROR}) ->
|
||||||
|
error;
|
||||||
|
atom_log_level(#state_t{log_level=?EC_WARN}) ->
|
||||||
|
warn;
|
||||||
|
atom_log_level(#state_t{log_level=?EC_INFO}) ->
|
||||||
|
info;
|
||||||
|
atom_log_level(#state_t{log_level=?EC_DEBUG}) ->
|
||||||
|
debug.
|
||||||
|
|
||||||
|
-spec format(t()) -> iolist().
|
||||||
|
format(Log) ->
|
||||||
|
[<<"(">>,
|
||||||
|
ec_cnv:to_binary(log_level(Log)), <<":">>,
|
||||||
|
ec_cnv:to_binary(atom_log_level(Log)),
|
||||||
|
<<")">>].
|
||||||
|
|
||||||
|
-spec colorize(t(), color(), boolean(), string()) -> string().
|
||||||
|
|
||||||
|
-define(VALID_COLOR(C),
|
||||||
|
C =:= $r orelse C =:= $g orelse C =:= $y orelse
|
||||||
|
C =:= $b orelse C =:= $m orelse C =:= $c orelse
|
||||||
|
C =:= $R orelse C =:= $G orelse C =:= $Y orelse
|
||||||
|
C =:= $B orelse C =:= $M orelse C =:= $C).
|
||||||
|
|
||||||
|
colorize(#state_t{intensity=none}, _, _, Msg) ->
|
||||||
|
Msg;
|
||||||
|
%% When it is supposed to be bold and we already have a uppercase
|
||||||
|
%% (bold color) we don't need to modify the color
|
||||||
|
colorize(State, Color, true, Msg) when ?VALID_COLOR(Color),
|
||||||
|
Color >= $A, Color =< $Z ->
|
||||||
|
colorize(State, Color, false, Msg);
|
||||||
|
%% We're sneaky we can subtract 32 to get the uppercase character if we want
|
||||||
|
%% bold but have a non bold color.
|
||||||
|
colorize(State, Color, true, Msg) when ?VALID_COLOR(Color) ->
|
||||||
|
colorize(State, Color - 32, false, Msg);
|
||||||
|
colorize(#state_t{caller=command_line, intensity = high},
|
||||||
|
Color, false, Msg) when ?VALID_COLOR(Color) ->
|
||||||
|
lists:flatten(cf:format("~!" ++ [Color] ++"~ts~ts", [?PREFIX, Msg]));
|
||||||
|
colorize(#state_t{caller=command_line, intensity = low},
|
||||||
|
Color, false, Msg) when ?VALID_COLOR(Color) ->
|
||||||
|
lists:flatten(cf:format("~!" ++ [Color] ++"~ts~!!~ts", [?PREFIX, Msg]));
|
||||||
|
colorize(_LogState, _Color, _Bold, Msg) ->
|
||||||
|
Msg.
|
7
src/ec_cmd_log.hrl
Normal file
7
src/ec_cmd_log.hrl
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
%%% @copyright 2024 Erlware, LLC.
|
||||||
|
-define(RED, $r).
|
||||||
|
-define(GREEN, $g).
|
||||||
|
-define(YELLOW, $y).
|
||||||
|
-define(BLUE, $b).
|
||||||
|
-define(MAGENTA, $m).
|
||||||
|
-define(CYAN, $c).
|
214
src/ec_cnv.erl
Normal file
214
src/ec_cnv.erl
Normal file
|
@ -0,0 +1,214 @@
|
||||||
|
%% -*- erlang-indent-level: 4; indent-tabs-mode: nil; fill-column: 80 -*-
|
||||||
|
%%% Copyright 2012 Erlware, LLC. All Rights Reserved.
|
||||||
|
%%%
|
||||||
|
%%% This file is provided to you under the Apache License,
|
||||||
|
%%% Version 2.0 (the "License"); you may not use this file
|
||||||
|
%%% except in compliance with the License. You may obtain
|
||||||
|
%%% a copy of the License at
|
||||||
|
%%%
|
||||||
|
%%% http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
%%%
|
||||||
|
%%% Unless required by applicable law or agreed to in writing,
|
||||||
|
%%% software distributed under the License is distributed on an
|
||||||
|
%%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
%%% KIND, either express or implied. See the License for the
|
||||||
|
%%% specific language governing permissions and limitations
|
||||||
|
%%% under the License.
|
||||||
|
%%%---------------------------------------------------------------------------
|
||||||
|
%%% @author Eric Merritt <ericbmerritt@gmail.com>
|
||||||
|
%%% @copyright (C) 2012 Erlware, LLC.
|
||||||
|
%%%
|
||||||
|
-module(ec_cnv).
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([to_integer/1,
|
||||||
|
to_integer/2,
|
||||||
|
to_float/1,
|
||||||
|
to_float/2,
|
||||||
|
to_number/1,
|
||||||
|
to_list/1,
|
||||||
|
to_binary/1,
|
||||||
|
to_atom/1,
|
||||||
|
to_boolean/1,
|
||||||
|
is_true/1,
|
||||||
|
is_false/1]).
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% API
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
%% @doc
|
||||||
|
%% Automatic conversion of a term into integer type. The conversion
|
||||||
|
%% will round a float value if nonstrict is specified otherwise badarg
|
||||||
|
-spec to_integer(string() | binary() | integer() | float()) ->
|
||||||
|
integer().
|
||||||
|
to_integer(X)->
|
||||||
|
to_integer(X, nonstrict).
|
||||||
|
|
||||||
|
-spec to_integer(string() | binary() | integer() | float(),
|
||||||
|
strict | nonstrict) ->
|
||||||
|
integer().
|
||||||
|
to_integer(X, strict)
|
||||||
|
when erlang:is_float(X) ->
|
||||||
|
erlang:error(badarg);
|
||||||
|
to_integer(X, nonstrict)
|
||||||
|
when erlang:is_float(X) ->
|
||||||
|
erlang:round(X);
|
||||||
|
to_integer(X, S)
|
||||||
|
when erlang:is_binary(X) ->
|
||||||
|
to_integer(erlang:binary_to_list(X), S);
|
||||||
|
to_integer(X, S)
|
||||||
|
when erlang:is_list(X) ->
|
||||||
|
try erlang:list_to_integer(X) of
|
||||||
|
Result ->
|
||||||
|
Result
|
||||||
|
catch
|
||||||
|
error:badarg when S =:= nonstrict ->
|
||||||
|
erlang:round(erlang:list_to_float(X))
|
||||||
|
end;
|
||||||
|
to_integer(X, _)
|
||||||
|
when erlang:is_integer(X) ->
|
||||||
|
X.
|
||||||
|
|
||||||
|
%% @doc
|
||||||
|
%% Automatic conversion of a term into float type. badarg if strict
|
||||||
|
%% is defined and an integer value is passed.
|
||||||
|
-spec to_float(string() | binary() | integer() | float()) ->
|
||||||
|
float().
|
||||||
|
to_float(X) ->
|
||||||
|
to_float(X, nonstrict).
|
||||||
|
|
||||||
|
-spec to_float(string() | binary() | integer() | float(),
|
||||||
|
strict | nonstrict) ->
|
||||||
|
float().
|
||||||
|
to_float(X, S) when is_binary(X) ->
|
||||||
|
to_float(erlang:binary_to_list(X), S);
|
||||||
|
to_float(X, S)
|
||||||
|
when erlang:is_list(X) ->
|
||||||
|
try erlang:list_to_float(X) of
|
||||||
|
Result ->
|
||||||
|
Result
|
||||||
|
catch
|
||||||
|
error:badarg when S =:= nonstrict ->
|
||||||
|
erlang:list_to_integer(X) * 1.0
|
||||||
|
end;
|
||||||
|
to_float(X, strict) when
|
||||||
|
erlang:is_integer(X) ->
|
||||||
|
erlang:error(badarg);
|
||||||
|
to_float(X, nonstrict)
|
||||||
|
when erlang:is_integer(X) ->
|
||||||
|
X * 1.0;
|
||||||
|
to_float(X, _) when erlang:is_float(X) ->
|
||||||
|
X.
|
||||||
|
|
||||||
|
%% @doc
|
||||||
|
%% Automatic conversion of a term into number type.
|
||||||
|
-spec to_number(binary() | string() | number()) ->
|
||||||
|
number().
|
||||||
|
to_number(X)
|
||||||
|
when erlang:is_number(X) ->
|
||||||
|
X;
|
||||||
|
to_number(X)
|
||||||
|
when erlang:is_binary(X) ->
|
||||||
|
to_number(to_list(X));
|
||||||
|
to_number(X)
|
||||||
|
when erlang:is_list(X) ->
|
||||||
|
try list_to_integer(X) of
|
||||||
|
Int -> Int
|
||||||
|
catch
|
||||||
|
error:badarg ->
|
||||||
|
list_to_float(X)
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% @doc
|
||||||
|
%% Automatic conversion of a term into Erlang list
|
||||||
|
-spec to_list(atom() | list() | binary() | integer() | float()) ->
|
||||||
|
list().
|
||||||
|
to_list(X)
|
||||||
|
when erlang:is_float(X) ->
|
||||||
|
erlang:float_to_list(X);
|
||||||
|
to_list(X)
|
||||||
|
when erlang:is_integer(X) ->
|
||||||
|
erlang:integer_to_list(X);
|
||||||
|
to_list(X)
|
||||||
|
when erlang:is_binary(X) ->
|
||||||
|
erlang:binary_to_list(X);
|
||||||
|
to_list(X)
|
||||||
|
when erlang:is_atom(X) ->
|
||||||
|
erlang:atom_to_list(X);
|
||||||
|
to_list(X)
|
||||||
|
when erlang:is_list(X) ->
|
||||||
|
X.
|
||||||
|
|
||||||
|
%% @doc
|
||||||
|
%% Known limitations:
|
||||||
|
%% Converting [256 | _], lists with integers > 255
|
||||||
|
-spec to_binary(atom() | string() | binary() | integer() | float()) ->
|
||||||
|
binary().
|
||||||
|
to_binary(X)
|
||||||
|
when erlang:is_float(X) ->
|
||||||
|
to_binary(to_list(X));
|
||||||
|
to_binary(X)
|
||||||
|
when erlang:is_integer(X) ->
|
||||||
|
erlang:iolist_to_binary(integer_to_list(X));
|
||||||
|
to_binary(X)
|
||||||
|
when erlang:is_atom(X) ->
|
||||||
|
erlang:list_to_binary(erlang:atom_to_list(X));
|
||||||
|
to_binary(X)
|
||||||
|
when erlang:is_list(X) ->
|
||||||
|
erlang:iolist_to_binary(X);
|
||||||
|
to_binary(X)
|
||||||
|
when erlang:is_binary(X) ->
|
||||||
|
X.
|
||||||
|
|
||||||
|
-spec to_boolean(binary() | string() | atom()) ->
|
||||||
|
boolean().
|
||||||
|
to_boolean(<<"true">>) ->
|
||||||
|
true;
|
||||||
|
to_boolean("true") ->
|
||||||
|
true;
|
||||||
|
to_boolean(true) ->
|
||||||
|
true;
|
||||||
|
to_boolean(<<"false">>) ->
|
||||||
|
false;
|
||||||
|
to_boolean("false") ->
|
||||||
|
false;
|
||||||
|
to_boolean(false) ->
|
||||||
|
false.
|
||||||
|
|
||||||
|
-spec is_true(binary() | string() | atom()) ->
|
||||||
|
boolean().
|
||||||
|
is_true(<<"true">>) ->
|
||||||
|
true;
|
||||||
|
is_true("true") ->
|
||||||
|
true;
|
||||||
|
is_true(true) ->
|
||||||
|
true;
|
||||||
|
is_true(_) ->
|
||||||
|
false.
|
||||||
|
|
||||||
|
-spec is_false(binary() | string() | atom()) ->
|
||||||
|
boolean().
|
||||||
|
is_false(<<"false">>) ->
|
||||||
|
true;
|
||||||
|
is_false("false") ->
|
||||||
|
true;
|
||||||
|
is_false(false) ->
|
||||||
|
true;
|
||||||
|
is_false(_) ->
|
||||||
|
false.
|
||||||
|
|
||||||
|
%% @doc
|
||||||
|
%% Automation conversion a term to an existing atom. badarg is
|
||||||
|
%% returned if the atom doesn't exist. the safer version, won't let
|
||||||
|
%% you leak atoms
|
||||||
|
-spec to_atom(atom() | list() | binary() | integer() | float()) ->
|
||||||
|
atom().
|
||||||
|
to_atom(X)
|
||||||
|
when erlang:is_atom(X) ->
|
||||||
|
X;
|
||||||
|
to_atom(X)
|
||||||
|
when erlang:is_list(X) ->
|
||||||
|
erlang:list_to_existing_atom(X);
|
||||||
|
to_atom(X) ->
|
||||||
|
to_atom(to_list(X)).
|
131
src/ec_compile.erl
Normal file
131
src/ec_compile.erl
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @author Eric Merritt <>
|
||||||
|
%%% @copyright (C) 2011, Erlware, LLC.
|
||||||
|
%%% @doc
|
||||||
|
%%% These are various utility functions to help with compiling and
|
||||||
|
%%% decompiling erlang source. They are mostly useful to the
|
||||||
|
%%% language/parse transform implementor.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(ec_compile).
|
||||||
|
|
||||||
|
-export([beam_to_erl_source/2,
|
||||||
|
erl_source_to_core_ast/1,
|
||||||
|
erl_source_to_erl_ast/1,
|
||||||
|
erl_source_to_asm/1,
|
||||||
|
erl_source_to_erl_syntax/1,
|
||||||
|
erl_string_to_core_ast/1,
|
||||||
|
erl_string_to_erl_ast/1,
|
||||||
|
erl_string_to_asm/1,
|
||||||
|
erl_string_to_erl_syntax/1]).
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% API
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
%% @doc decompile a beam file that has been compiled with +debug_info
|
||||||
|
%% into a erlang source file
|
||||||
|
%%
|
||||||
|
%% @param BeamFName the name of the beamfile
|
||||||
|
%% @param ErlFName the name of the erlang file where the generated
|
||||||
|
%% source file will be output. This should *not* be the same as the
|
||||||
|
%% source file that created the beamfile unless you want to overwrite
|
||||||
|
%% it.
|
||||||
|
-spec beam_to_erl_source(string(), string()) -> ok | term().
|
||||||
|
beam_to_erl_source(BeamFName, ErlFName) ->
|
||||||
|
case beam_lib:chunks(BeamFName, [abstract_code]) of
|
||||||
|
{ok, {_, [{abstract_code, {raw_abstract_v1,Forms}}]}} ->
|
||||||
|
Src =
|
||||||
|
erl_prettypr:format(erl_syntax:form_list(tl(Forms))),
|
||||||
|
{ok, Fd} = file:open(ErlFName, [write]),
|
||||||
|
io:fwrite(Fd, "~ts~n", [Src]),
|
||||||
|
file:close(Fd);
|
||||||
|
Error ->
|
||||||
|
Error
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% @doc compile an erlang source file into a Core Erlang AST
|
||||||
|
%%
|
||||||
|
%% @param Path - The path to the erlang source file
|
||||||
|
-spec erl_source_to_core_ast(file:filename()) -> CoreAst::term().
|
||||||
|
erl_source_to_core_ast(Path) ->
|
||||||
|
{ok, Contents} = file:read_file(Path),
|
||||||
|
erl_string_to_core_ast(binary_to_list(Contents)).
|
||||||
|
|
||||||
|
%% @doc compile an erlang source file into an Erlang AST
|
||||||
|
%%
|
||||||
|
%% @param Path - The path to the erlang source file
|
||||||
|
-spec erl_source_to_erl_ast(file:filename()) -> ErlangAst::term().
|
||||||
|
erl_source_to_erl_ast(Path) ->
|
||||||
|
{ok, Contents} = file:read_file(Path),
|
||||||
|
erl_string_to_erl_ast(binary_to_list(Contents)).
|
||||||
|
|
||||||
|
%% @doc compile an erlang source file into erlang terms that represent
|
||||||
|
%% the relevant ASM
|
||||||
|
%%
|
||||||
|
%% @param Path - The path to the erlang source file
|
||||||
|
-spec erl_source_to_asm(file:filename()) -> ErlangAsm::term().
|
||||||
|
erl_source_to_asm(Path) ->
|
||||||
|
{ok, Contents} = file:read_file(Path),
|
||||||
|
erl_string_to_asm(binary_to_list(Contents)).
|
||||||
|
|
||||||
|
%% @doc compile an erlang source file to a string that displays the
|
||||||
|
%% 'erl_syntax1 calls needed to reproduce those terms.
|
||||||
|
%%
|
||||||
|
%% @param Path - The path to the erlang source file
|
||||||
|
-spec erl_source_to_erl_syntax(file:filename()) -> string().
|
||||||
|
erl_source_to_erl_syntax(Path) ->
|
||||||
|
{ok, Contents} = file:read_file(Path),
|
||||||
|
erl_string_to_erl_syntax(Contents).
|
||||||
|
|
||||||
|
%% @doc compile a string representing an erlang expression into an
|
||||||
|
%% Erlang AST
|
||||||
|
%%
|
||||||
|
%% @param StringExpr - The path to the erlang source file
|
||||||
|
-spec erl_string_to_erl_ast(string()) -> ErlangAst::term().
|
||||||
|
erl_string_to_erl_ast(StringExpr) ->
|
||||||
|
Forms0 =
|
||||||
|
lists:foldl(fun(<<>>, Acc) ->
|
||||||
|
Acc;
|
||||||
|
(<<"\n\n">>, Acc) ->
|
||||||
|
Acc;
|
||||||
|
(El, Acc) ->
|
||||||
|
{ok, Tokens, _} =
|
||||||
|
erl_scan:string(binary_to_list(El)
|
||||||
|
++ "."),
|
||||||
|
[Tokens | Acc]
|
||||||
|
end, [], re:split(StringExpr, "\\.\n")),
|
||||||
|
%% No need to reverse. This will rereverse for us
|
||||||
|
lists:foldl(fun(Form, Forms) ->
|
||||||
|
{ok, ErlAST} = erl_parse:parse_form(Form),
|
||||||
|
[ErlAST | Forms]
|
||||||
|
end, [], Forms0).
|
||||||
|
|
||||||
|
%% @doc compile a string representing an erlang expression into a
|
||||||
|
%% Core Erlang AST
|
||||||
|
%%
|
||||||
|
%% @param StringExpr - The path to the erlang source file
|
||||||
|
-spec erl_string_to_core_ast(string()) -> CoreAst::term().
|
||||||
|
erl_string_to_core_ast(StringExpr) ->
|
||||||
|
compile:forms(erl_string_to_erl_ast(StringExpr), [to_core]).
|
||||||
|
|
||||||
|
%% @doc compile a string representing an erlang expression into a term
|
||||||
|
%% that represents the ASM
|
||||||
|
%%
|
||||||
|
%% @param StringExpr - The path to the erlang source file
|
||||||
|
-spec erl_string_to_asm(string()) -> ErlangAsm::term().
|
||||||
|
erl_string_to_asm(StringExpr) ->
|
||||||
|
compile:forms(erl_string_to_erl_ast(StringExpr), ['S']).
|
||||||
|
|
||||||
|
%% @doc compile an erlang source file to a string that displays the
|
||||||
|
%% 'erl_syntax1 calls needed to reproduce those terms.
|
||||||
|
%%
|
||||||
|
%% @param StringExpr - The string representing the erlang code.
|
||||||
|
-spec erl_string_to_erl_syntax(string() | binary()) -> string().
|
||||||
|
erl_string_to_erl_syntax(BinaryExpr)
|
||||||
|
when erlang:is_binary(BinaryExpr) ->
|
||||||
|
erlang:binary_to_list(BinaryExpr);
|
||||||
|
erl_string_to_erl_syntax(StringExpr) ->
|
||||||
|
{ok, Tokens, _} = erl_scan:string(StringExpr),
|
||||||
|
{ok, ErlAST} = erl_parse:parse_form(Tokens),
|
||||||
|
io:format(erl_prettypr:format(erl_syntax:meta(ErlAST))).
|
526
src/ec_date.erl
526
src/ec_date.erl
|
@ -1,3 +1,4 @@
|
||||||
|
%% vi:ts=4 sw=4 et
|
||||||
%% @copyright Dale Harvey
|
%% @copyright Dale Harvey
|
||||||
%% @doc Format dates in erlang
|
%% @doc Format dates in erlang
|
||||||
%%
|
%%
|
||||||
|
@ -24,8 +25,10 @@
|
||||||
-author("Dale Harvey <dale@hypernumbers.com>").
|
-author("Dale Harvey <dale@hypernumbers.com>").
|
||||||
|
|
||||||
-export([format/1, format/2]).
|
-export([format/1, format/2]).
|
||||||
|
-export([format_iso8601/1]).
|
||||||
-export([parse/1, parse/2]).
|
-export([parse/1, parse/2]).
|
||||||
-export([nparse/1]).
|
-export([nparse/1]).
|
||||||
|
-export([tokenise/2]).
|
||||||
|
|
||||||
%% These are used exclusively as guards and so the function like
|
%% These are used exclusively as guards and so the function like
|
||||||
%% defines make sense
|
%% defines make sense
|
||||||
|
@ -34,17 +37,28 @@
|
||||||
-define( is_us_sep(X), ( X==$/) ).
|
-define( is_us_sep(X), ( X==$/) ).
|
||||||
-define( is_world_sep(X), ( X==$-) ).
|
-define( is_world_sep(X), ( X==$-) ).
|
||||||
|
|
||||||
-define(GREGORIAN_SECONDS_1970, 62167219200).
|
-define( MONTH_TAG, month ).
|
||||||
|
-define( is_year(X), (is_integer(X) andalso X > 31) ).
|
||||||
|
-define( is_day(X), (is_integer(X) andalso X =< 31) ).
|
||||||
|
-define( is_hinted_month(X), (is_tuple(X) andalso size(X)=:=2 andalso element(1,X)=:=?MONTH_TAG) ).
|
||||||
|
-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(GREGORIAN_SECONDS_1970, 62_167_219_200).
|
||||||
|
-define(ISO_8601_DATETIME_FORMAT, "Y-m-dTH:i:sZ").
|
||||||
|
-define(ISO_8601_DATETIME_WITH_MS_FORMAT, "Y-m-dTH:i:s.fZ").
|
||||||
|
|
||||||
-type year() :: non_neg_integer().
|
-type year() :: non_neg_integer().
|
||||||
-type month() :: 1..12.
|
-type month() :: 1..12 | {?MONTH_TAG, 1..12}.
|
||||||
-type day() :: 1..31.
|
-type day() :: 1..31.
|
||||||
-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 daynum() :: 1..7.
|
-type daynum() :: 1..7.
|
||||||
-type date() :: {year(),month(),day()}.
|
-type date() :: {year(),month(),day()}.
|
||||||
-type time() :: {hour(),minute(),second()}.
|
-type time() :: {hour(),minute(),second()} | {hour(),minute(),second(),microsecond()}.
|
||||||
-type datetime() :: {date(),time()}.
|
-type datetime() :: {date(),time()}.
|
||||||
-type now() :: {integer(),integer(),integer()}.
|
-type now() :: {integer(),integer(),integer()}.
|
||||||
|
|
||||||
|
@ -59,17 +73,27 @@ format(Format) ->
|
||||||
|
|
||||||
-spec format(string(),datetime() | now()) -> string().
|
-spec format(string(),datetime() | now()) -> string().
|
||||||
%% @doc format Date as Format
|
%% @doc format Date as Format
|
||||||
format(Format, {_,_,_}=Now) ->
|
format(Format, {_,_,Ms}=Now) ->
|
||||||
format(Format, calendar:now_to_datetime(Now), []);
|
{Date,{H,M,S}} = calendar:now_to_datetime(Now),
|
||||||
|
format(Format, {Date, {H,M,S,Ms}}, []);
|
||||||
format(Format, Date) ->
|
format(Format, Date) ->
|
||||||
format(Format, Date, []).
|
format(Format, Date, []).
|
||||||
|
|
||||||
|
-spec format_iso8601(datetime()) -> string().
|
||||||
|
%% @doc format date in the ISO8601 format
|
||||||
|
%% This always puts 'Z' as time zone, since we have no notion of timezone
|
||||||
|
format_iso8601({{_, _, _}, {_, _, _}} = Date) ->
|
||||||
|
format(?ISO_8601_DATETIME_FORMAT, Date);
|
||||||
|
format_iso8601({{_, _, _}, {_, _, _, _}} = Date) ->
|
||||||
|
format(?ISO_8601_DATETIME_WITH_MS_FORMAT, Date).
|
||||||
|
|
||||||
-spec parse(string()) -> datetime().
|
-spec parse(string()) -> datetime().
|
||||||
%% @doc parses the datetime from a string
|
%% @doc parses the datetime from a string
|
||||||
parse(Date) ->
|
parse(Date) ->
|
||||||
do_parse(Date, calendar:universal_time(),[]).
|
do_parse(Date, calendar:universal_time(),[]).
|
||||||
|
|
||||||
-spec parse(string(),datetime() | now()) -> datetime().
|
-spec parse(string(),datetime() | now()) -> datetime().
|
||||||
|
|
||||||
%% @doc parses the datetime from a string
|
%% @doc parses the datetime from a string
|
||||||
parse(Date, {_,_,_}=Now) ->
|
parse(Date, {_,_,_}=Now) ->
|
||||||
do_parse(Date, calendar:now_to_datetime(Now), []);
|
do_parse(Date, calendar:now_to_datetime(Now), []);
|
||||||
|
@ -77,47 +101,147 @@ parse(Date, Now) ->
|
||||||
do_parse(Date, Now, []).
|
do_parse(Date, Now, []).
|
||||||
|
|
||||||
do_parse(Date, Now, Opts) ->
|
do_parse(Date, Now, Opts) ->
|
||||||
case parse(tokenise(string:to_upper(Date), []), Now, Opts) of
|
case filter_hints(parse(tokenise(string: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}}
|
||||||
when is_number(Y), is_number(M),
|
when is_number(Y), is_number(M),
|
||||||
is_number(D), is_number(H),
|
is_number(D), is_number(H),
|
||||||
is_number(M1), is_number(S) ->
|
is_number(M1), is_number(S) ->
|
||||||
case calendar:valid_date(D1) of
|
case calendar:valid_date(D1) of
|
||||||
true -> {D1, T1};
|
true -> {D1, T1};
|
||||||
false -> erlang:throw({?MODULE, {bad_date, Date}})
|
false -> erlang:throw({?MODULE, {bad_date, Date}})
|
||||||
end;
|
end;
|
||||||
_ -> erlang:throw({?MODULE, {bad_date, Date}})
|
{D1, _T1, {Ms}} = {{Y, M, D}, {H, M1, S}, {Ms}}
|
||||||
|
when is_number(Y), is_number(M),
|
||||||
|
is_number(D), is_number(H),
|
||||||
|
is_number(M1), is_number(S),
|
||||||
|
is_number(Ms) ->
|
||||||
|
case calendar:valid_date(D1) of
|
||||||
|
true -> {D1, {H,M1,S,Ms}};
|
||||||
|
false -> erlang:throw({?MODULE, {bad_date, Date}})
|
||||||
|
end;
|
||||||
|
Unknown -> erlang:throw({?MODULE, {bad_date, Date, Unknown }})
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
filter_hints({{Y, {?MONTH_TAG, M}, D}, {H, M1, S}}) ->
|
||||||
|
filter_hints({{Y, M, D}, {H, M1, S}});
|
||||||
|
filter_hints({{Y, {?MONTH_TAG, M}, D}, {H, M1, S}, {Ms}}) ->
|
||||||
|
filter_hints({{Y, M, D}, {H, M1, S}, {Ms}});
|
||||||
|
filter_hints(Other) ->
|
||||||
|
Other.
|
||||||
|
|
||||||
-spec nparse(string()) -> now().
|
-spec nparse(string()) -> now().
|
||||||
%% @doc parses the datetime from a string into 'now' format
|
%% @doc parses the datetime from a string into 'now' format
|
||||||
nparse(Date) ->
|
nparse(Date) ->
|
||||||
DateTime = parse(Date),
|
case parse(Date) of
|
||||||
GSeconds = calendar:datetime_to_gregorian_seconds(DateTime),
|
{DateS, {H, M, S, Ms} } ->
|
||||||
ESeconds = GSeconds - ?GREGORIAN_SECONDS_1970,
|
GSeconds = calendar:datetime_to_gregorian_seconds({DateS, {H, M, S} }),
|
||||||
{ESeconds div 1000000, ESeconds rem 1000000, 0}.
|
ESeconds = GSeconds - ?GREGORIAN_SECONDS_1970,
|
||||||
|
{ESeconds div 1_000_000, ESeconds rem 1_000_000, Ms};
|
||||||
|
DateTime ->
|
||||||
|
GSeconds = calendar:datetime_to_gregorian_seconds(DateTime),
|
||||||
|
ESeconds = GSeconds - ?GREGORIAN_SECONDS_1970,
|
||||||
|
{ESeconds div 1_000_000, ESeconds rem 1_000_000, 0}
|
||||||
|
end.
|
||||||
|
|
||||||
%%
|
%%
|
||||||
%% LOCAL FUNCTIONS
|
%% LOCAL FUNCTIONS
|
||||||
%%
|
%%
|
||||||
|
|
||||||
|
parse([Year, X, Month, X, Day, Hour, $:, Min, $:, Sec, $., Micros, $Z ], _Now, _Opts)
|
||||||
|
when ?is_world_sep(X)
|
||||||
|
andalso (Micros >= 0 andalso Micros < 1_000_000)
|
||||||
|
andalso Year > 31 ->
|
||||||
|
{{Year, Month, Day}, {hour(Hour, []), Min, Sec}, {Micros}};
|
||||||
|
|
||||||
|
parse([Year, X, Month, X, Day, Hour, $:, Min, $:, Sec, $Z ], _Now, _Opts)
|
||||||
|
when (?is_us_sep(X) orelse ?is_world_sep(X))
|
||||||
|
andalso Year > 31 ->
|
||||||
|
{{Year, Month, Day}, {hour(Hour, []), Min, Sec}};
|
||||||
|
|
||||||
|
parse([Year, X, Month, X, Day, Hour, $:, Min, $:, Sec, $., Micros, $+, Off | _Rest ], _Now, _Opts)
|
||||||
|
when (?is_us_sep(X) orelse ?is_world_sep(X))
|
||||||
|
andalso (Micros >= 0 andalso Micros < 1_000_000)
|
||||||
|
andalso Year > 31 ->
|
||||||
|
{{Year, Month, Day}, {hour(Hour, []) - Off, Min, Sec}, {Micros}};
|
||||||
|
|
||||||
|
parse([Year, X, Month, X, Day, Hour, $:, Min, $:, Sec, $+, Off | _Rest ], _Now, _Opts)
|
||||||
|
when (?is_us_sep(X) orelse ?is_world_sep(X))
|
||||||
|
andalso Year > 31 ->
|
||||||
|
{{Year, Month, Day}, {hour(Hour, []) - Off, Min, Sec}, {0}};
|
||||||
|
|
||||||
|
parse([Year, X, Month, X, Day, Hour, $:, Min, $:, Sec, $., Micros, $-, Off | _Rest ], _Now, _Opts)
|
||||||
|
when (?is_us_sep(X) orelse ?is_world_sep(X))
|
||||||
|
andalso (Micros >= 0 andalso Micros < 1_000_000)
|
||||||
|
andalso Year > 31 ->
|
||||||
|
{{Year, Month, Day}, {hour(Hour, []) + Off, Min, Sec}, {Micros}};
|
||||||
|
|
||||||
|
parse([Year, X, Month, X, Day, Hour, $:, Min, $:, Sec, $-, Off | _Rest ], _Now, _Opts)
|
||||||
|
when (?is_us_sep(X) orelse ?is_world_sep(X))
|
||||||
|
andalso Year > 31 ->
|
||||||
|
{{Year, Month, Day}, {hour(Hour, []) + Off, Min, Sec}, {0}};
|
||||||
|
|
||||||
|
%% Date/Times 22 Aug 2008 6:35.0001 PM
|
||||||
|
parse([Year,X,Month,X,Day,Hour,$:,Min,$:,Sec,$., Ms | PAM], _Now, _Opts)
|
||||||
|
when ?is_meridian(PAM) andalso
|
||||||
|
(?is_us_sep(X) orelse ?is_world_sep(X))
|
||||||
|
andalso ?is_year(Year) ->
|
||||||
|
{{Year, Month, Day}, {hour(Hour, PAM), Min, Sec}, {Ms}};
|
||||||
|
parse([Month,X,Day,X,Year,Hour,$:,Min,$:,Sec,$., Ms | PAM], _Now, _Opts)
|
||||||
|
when ?is_meridian(PAM) andalso ?is_us_sep(X)
|
||||||
|
andalso ?is_year(Year) ->
|
||||||
|
{{Year, Month, Day}, {hour(Hour, PAM), Min, Sec}, {Ms}};
|
||||||
|
parse([Day,X,Month,X,Year,Hour,$:,Min,$:,Sec,$., Ms | PAM], _Now, _Opts)
|
||||||
|
when ?is_meridian(PAM) andalso ?is_world_sep(X)
|
||||||
|
andalso ?is_year(Year) ->
|
||||||
|
{{Year, Month, Day}, {hour(Hour, PAM), Min, Sec}, {Ms}};
|
||||||
|
|
||||||
|
%% Date/Times Dec 1st, 2012 6:25 PM
|
||||||
|
parse([Month,Day,Year,Hour,$:,Min,$:,Sec | PAM], _Now, _Opts)
|
||||||
|
when ?is_meridian(PAM) andalso ?is_hinted_month(Month) andalso ?is_day(Day) ->
|
||||||
|
{{Year, Month, Day}, {hour(Hour, PAM), Min, Sec}};
|
||||||
|
parse([Month,Day,Year,Hour,$:,Min | PAM], _Now, _Opts)
|
||||||
|
when ?is_meridian(PAM) andalso ?is_hinted_month(Month) andalso ?is_day(Day) ->
|
||||||
|
{{Year, Month, Day}, {hour(Hour, PAM), Min, 0}};
|
||||||
|
parse([Month,Day,Year,Hour | PAM], _Now, _Opts)
|
||||||
|
when ?is_meridian(PAM) andalso ?is_hinted_month(Month) andalso ?is_day(Day) ->
|
||||||
|
{{Year, Month, Day}, {hour(Hour, PAM), 0, 0}};
|
||||||
|
|
||||||
|
%% Date/Times Fri Nov 21 14:55:26 +0000 2014 (Twitter format)
|
||||||
|
parse([Month, Day, Hour,$:,Min,$:,Sec, Year], _Now, _Opts)
|
||||||
|
when ?is_hinted_month(Month), ?is_day(Day), ?is_year(Year) ->
|
||||||
|
{{Year, Month, Day}, {hour(Hour, []), Min, Sec}};
|
||||||
|
|
||||||
%% Times - 21:45, 13:45:54, 13:15PM etc
|
%% Times - 21:45, 13:45:54, 13:15PM etc
|
||||||
parse([Hour,$:,Min,$:,Sec | PAM], {Date, _Time}, _O) when ?is_meridian(PAM) ->
|
parse([Hour,$:,Min,$:,Sec | PAM], {Date, _Time}, _O) when ?is_meridian(PAM) ->
|
||||||
{Date, {hour(Hour, PAM), Min, Sec}};
|
{Date, {hour(Hour, PAM), Min, Sec}};
|
||||||
parse([Hour,$:,Min | PAM], {Date, _Time}, _Opts) when ?is_meridian(PAM) ->
|
parse([Hour,$:,Min | PAM], {Date, _Time}, _Opts) when ?is_meridian(PAM) ->
|
||||||
{Date, {hour(Hour, PAM), Min, 0}};
|
{Date, {hour(Hour, PAM), Min, 0}};
|
||||||
parse([Hour | PAM],{Date,_Time}, _Opts) when ?is_meridian(PAM) ->
|
parse([Hour | PAM],{Date,_Time}, _Opts) when ?is_meridian(PAM) ->
|
||||||
{Date, {hour(Hour,PAM), 0, 0}};
|
{Date, {hour(Hour,PAM), 0, 0}};
|
||||||
|
|
||||||
|
%% Dates (Any combination with word month "aug 8th, 2008", "8 aug 2008", "2008 aug 21" "2008 5 aug" )
|
||||||
|
%% Will work because of the "Hinted month"
|
||||||
|
parse([Day,Month,Year], {_Date, Time}, _Opts)
|
||||||
|
when ?is_day(Day) andalso ?is_hinted_month(Month) andalso ?is_year(Year) ->
|
||||||
|
{{Year, Month, Day}, Time};
|
||||||
|
parse([Month,Day,Year], {_Date, Time}, _Opts)
|
||||||
|
when ?is_day(Day) andalso ?is_hinted_month(Month) andalso ?is_year(Year) ->
|
||||||
|
{{Year, Month, Day}, Time};
|
||||||
|
parse([Year,Day,Month], {_Date, Time}, _Opts)
|
||||||
|
when ?is_day(Day) andalso ?is_hinted_month(Month) andalso ?is_year(Year) ->
|
||||||
|
{{Year, Month, Day}, Time};
|
||||||
|
parse([Year,Month,Day], {_Date, Time}, _Opts)
|
||||||
|
when ?is_day(Day) andalso ?is_hinted_month(Month) andalso ?is_year(Year) ->
|
||||||
|
{{Year, Month, Day}, Time};
|
||||||
|
|
||||||
%% Dates 23/april/1963
|
%% Dates 23/april/1963
|
||||||
parse([Day,Month,Year], {_Date, Time}, _Opts) ->
|
parse([Day,Month,Year], {_Date, Time}, _Opts) ->
|
||||||
{{Year, Month, Day}, Time};
|
{{Year, Month, Day}, Time};
|
||||||
parse([Year,X,Month,X,Day], {_Date, Time}, _Opts)
|
parse([Year,X,Month,X,Day], {_Date, Time}, _Opts)
|
||||||
when (?is_us_sep(X) orelse ?is_world_sep(X))
|
when (?is_us_sep(X) orelse ?is_world_sep(X))
|
||||||
andalso Year > 31 ->
|
andalso ?is_year(Year) ->
|
||||||
{{Year, Month, Day}, Time};
|
{{Year, Month, Day}, Time};
|
||||||
parse([Month,X,Day,X,Year], {_Date, Time}, _Opts) when ?is_us_sep(X) ->
|
parse([Month,X,Day,X,Year], {_Date, Time}, _Opts) when ?is_us_sep(X) ->
|
||||||
{{Year, Month, Day}, Time};
|
{{Year, Month, Day}, Time};
|
||||||
|
@ -129,20 +253,21 @@ parse([Day,X,Month,X,Year], {_Date, Time}, _Opts) when ?is_world_sep(X) ->
|
||||||
parse([Year,X,Month,X,Day,Hour | PAM], _Date, _Opts)
|
parse([Year,X,Month,X,Day,Hour | PAM], _Date, _Opts)
|
||||||
when ?is_meridian(PAM) andalso
|
when ?is_meridian(PAM) andalso
|
||||||
(?is_us_sep(X) orelse ?is_world_sep(X))
|
(?is_us_sep(X) orelse ?is_world_sep(X))
|
||||||
andalso Year > 31 ->
|
andalso ?is_year(Year) ->
|
||||||
{{Year, Month, Day}, {hour(Hour, PAM), 0, 0}};
|
{{Year, Month, Day}, {hour(Hour, PAM), 0, 0}};
|
||||||
parse([Day,X,Month,X,Year,Hour | PAM], _Date, _Opts)
|
parse([Day,X,Month,X,Year,Hour | PAM], _Date, _Opts)
|
||||||
when ?is_meridian(PAM) andalso ?is_world_sep(X) ->
|
when ?is_meridian(PAM) andalso ?is_world_sep(X) ->
|
||||||
{{Year, Month, Day}, {hour(Hour, PAM), 0, 0}};
|
{{Year, Month, Day}, {hour(Hour, PAM), 0, 0}};
|
||||||
parse([Month,X,Day,X,Year,Hour | PAM], _Date, _Opts)
|
parse([Month,X,Day,X,Year,Hour | PAM], _Date, _Opts)
|
||||||
when ?is_meridian(PAM) andalso ?is_us_sep(X) ->
|
when ?is_meridian(PAM) andalso ?is_us_sep(X) ->
|
||||||
{{Year, Month, Day}, {hour(Hour, PAM), 0, 0}};
|
{{Year, Month, Day}, {hour(Hour, PAM), 0, 0}};
|
||||||
|
|
||||||
%% Time is "6:35 PM"
|
|
||||||
|
%% Time is "6:35 PM" ms return
|
||||||
parse([Year,X,Month,X,Day,Hour,$:,Min | PAM], _Date, _Opts)
|
parse([Year,X,Month,X,Day,Hour,$:,Min | PAM], _Date, _Opts)
|
||||||
when ?is_meridian(PAM) andalso
|
when ?is_meridian(PAM) andalso
|
||||||
(?is_us_sep(X) orelse ?is_world_sep(X))
|
(?is_us_sep(X) orelse ?is_world_sep(X))
|
||||||
andalso Year > 31 ->
|
andalso ?is_year(Year) ->
|
||||||
{{Year, Month, Day}, {hour(Hour, PAM), Min, 0}};
|
{{Year, Month, Day}, {hour(Hour, PAM), Min, 0}};
|
||||||
parse([Day,X,Month,X,Year,Hour,$:,Min | PAM], _Date, _Opts)
|
parse([Day,X,Month,X,Year,Hour,$:,Min | PAM], _Date, _Opts)
|
||||||
when ?is_meridian(PAM) andalso ?is_world_sep(X) ->
|
when ?is_meridian(PAM) andalso ?is_world_sep(X) ->
|
||||||
|
@ -155,7 +280,7 @@ parse([Month,X,Day,X,Year,Hour,$:,Min | PAM], _Date, _Opts)
|
||||||
parse([Year,X,Month,X,Day,Hour,$:,Min,$:,Sec | PAM], _Now, _Opts)
|
parse([Year,X,Month,X,Day,Hour,$:,Min,$:,Sec | PAM], _Now, _Opts)
|
||||||
when ?is_meridian(PAM) andalso
|
when ?is_meridian(PAM) andalso
|
||||||
(?is_us_sep(X) orelse ?is_world_sep(X))
|
(?is_us_sep(X) orelse ?is_world_sep(X))
|
||||||
andalso Year > 31 ->
|
andalso ?is_year(Year) ->
|
||||||
{{Year, Month, Day}, {hour(Hour, PAM), Min, Sec}};
|
{{Year, Month, Day}, {hour(Hour, PAM), Min, Sec}};
|
||||||
parse([Month,X,Day,X,Year,Hour,$:,Min,$:,Sec | PAM], _Now, _Opts)
|
parse([Month,X,Day,X,Year,Hour,$:,Min,$:,Sec | PAM], _Now, _Opts)
|
||||||
when ?is_meridian(PAM) andalso ?is_us_sep(X) ->
|
when ?is_meridian(PAM) andalso ?is_us_sep(X) ->
|
||||||
|
@ -164,7 +289,6 @@ parse([Day,X,Month,X,Year,Hour,$:,Min,$:,Sec | PAM], _Now, _Opts)
|
||||||
when ?is_meridian(PAM) andalso ?is_world_sep(X) ->
|
when ?is_meridian(PAM) andalso ?is_world_sep(X) ->
|
||||||
{{Year, Month, Day}, {hour(Hour, PAM), Min, Sec}};
|
{{Year, Month, Day}, {hour(Hour, PAM), Min, Sec}};
|
||||||
|
|
||||||
|
|
||||||
parse([Day,Month,Year,Hour | PAM], _Now, _Opts)
|
parse([Day,Month,Year,Hour | PAM], _Now, _Opts)
|
||||||
when ?is_meridian(PAM) ->
|
when ?is_meridian(PAM) ->
|
||||||
{{Year, Month, Day}, {hour(Hour, PAM), 0, 0}};
|
{{Year, Month, Day}, {hour(Hour, PAM), 0, 0}};
|
||||||
|
@ -181,9 +305,35 @@ parse(_Tokens, _Now, _Opts) ->
|
||||||
tokenise([], Acc) ->
|
tokenise([], Acc) ->
|
||||||
lists:reverse(Acc);
|
lists:reverse(Acc);
|
||||||
|
|
||||||
|
%% ISO 8601 fractions of a second
|
||||||
|
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) ->
|
||||||
|
tokenise(Rest, [ ltoi([N1, N2, N3, N4, N5, N6]), $. | Acc]);
|
||||||
|
tokenise([$., N1, N2, N3, N4, N5 | Rest], Acc)
|
||||||
|
when ?is_num(N1), ?is_num(N2), ?is_num(N3), ?is_num(N4), ?is_num(N5) ->
|
||||||
|
tokenise(Rest, [ ltoi([N1, N2, N3, N4, N5]) * 10, $. | Acc]);
|
||||||
|
tokenise([$., N1, N2, N3, N4 | Rest], Acc)
|
||||||
|
when ?is_num(N1), ?is_num(N2), ?is_num(N3), ?is_num(N4) ->
|
||||||
|
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(Rest, [ ltoi([N1, N2, N3]) * 1_000, $. | Acc]);
|
||||||
|
tokenise([$., N1, N2 | Rest], Acc) when ?is_num(N1), ?is_num(N2) ->
|
||||||
|
tokenise(Rest, [ ltoi([N1, N2]) * 10_000, $. | Acc]);
|
||||||
|
tokenise([$., N1 | Rest], Acc) when ?is_num(N1) ->
|
||||||
|
tokenise(Rest, [ ltoi([N1]) * 100_000, $. | 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) ->
|
||||||
|
tokenise(Rest, [ ltoi([N1, N2, N3, N4, N5, N6]) | Acc]);
|
||||||
|
tokenise([N1, N2, N3, N4, N5 | Rest], Acc)
|
||||||
|
when ?is_num(N1), ?is_num(N2), ?is_num(N3), ?is_num(N4), ?is_num(N5) ->
|
||||||
|
tokenise(Rest, [ ltoi([N1, N2, N3, N4, N5]) | Acc]);
|
||||||
tokenise([N1, N2, N3, N4 | Rest], Acc)
|
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]) | Acc]);
|
tokenise(Rest, [ ltoi([N1, N2, N3, N4]) | Acc]);
|
||||||
|
tokenise([N1, N2, N3 | Rest], Acc)
|
||||||
|
when ?is_num(N1), ?is_num(N2), ?is_num(N3) ->
|
||||||
|
tokenise(Rest, [ ltoi([N1, N2, N3]) | Acc]);
|
||||||
tokenise([N1, N2 | Rest], Acc)
|
tokenise([N1, N2 | Rest], Acc)
|
||||||
when ?is_num(N1), ?is_num(N2) ->
|
when ?is_num(N1), ?is_num(N2) ->
|
||||||
tokenise(Rest, [ ltoi([N1, N2]) | Acc]);
|
tokenise(Rest, [ ltoi([N1, N2]) | Acc]);
|
||||||
|
@ -191,32 +341,37 @@ tokenise([N1 | Rest], Acc)
|
||||||
when ?is_num(N1) ->
|
when ?is_num(N1) ->
|
||||||
tokenise(Rest, [ ltoi([N1]) | Acc]);
|
tokenise(Rest, [ ltoi([N1]) | Acc]);
|
||||||
|
|
||||||
tokenise("JANUARY"++Rest, Acc) -> tokenise(Rest, [1 | Acc]);
|
|
||||||
tokenise("JAN"++Rest, Acc) -> tokenise(Rest, [1 | Acc]);
|
%% Worded Months get tagged with ?MONTH_TAG to let the parser know that these
|
||||||
tokenise("FEBRUARY"++Rest, Acc) -> tokenise(Rest, [2 | Acc]);
|
%% are unambiguously declared to be months. This was there's no confusion
|
||||||
tokenise("FEB"++Rest, Acc) -> tokenise(Rest, [2 | Acc]);
|
%% between, for example: "Aug 12" and "12 Aug"
|
||||||
tokenise("MARCH"++Rest, Acc) -> tokenise(Rest, [3 | Acc]);
|
%% These hint tags are filtered in filter_hints/1 above.
|
||||||
tokenise("MAR"++Rest, Acc) -> tokenise(Rest, [3 | Acc]);
|
tokenise("JANUARY"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,1} | Acc]);
|
||||||
tokenise("APRIL"++Rest, Acc) -> tokenise(Rest, [4 | Acc]);
|
tokenise("JAN"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,1} | Acc]);
|
||||||
tokenise("APR"++Rest, Acc) -> tokenise(Rest, [4 | Acc]);
|
tokenise("FEBRUARY"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,2} | Acc]);
|
||||||
tokenise("MAY"++Rest, Acc) -> tokenise(Rest, [5 | Acc]);
|
tokenise("FEB"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,2} | Acc]);
|
||||||
tokenise("JUNE"++Rest, Acc) -> tokenise(Rest, [6 | Acc]);
|
tokenise("MARCH"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,3} | Acc]);
|
||||||
tokenise("JUN"++Rest, Acc) -> tokenise(Rest, [6 | Acc]);
|
tokenise("MAR"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,3} | Acc]);
|
||||||
tokenise("JULY"++Rest, Acc) -> tokenise(Rest, [7 | Acc]);
|
tokenise("APRIL"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,4} | Acc]);
|
||||||
tokenise("JUL"++Rest, Acc) -> tokenise(Rest, [7 | Acc]);
|
tokenise("APR"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,4} | Acc]);
|
||||||
tokenise("AUGUST"++Rest, Acc) -> tokenise(Rest, [8 | Acc]);
|
tokenise("MAY"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,5} | Acc]);
|
||||||
tokenise("AUG"++Rest, Acc) -> tokenise(Rest, [8 | Acc]);
|
tokenise("JUNE"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,6} | Acc]);
|
||||||
tokenise("SEPTEMBER"++Rest, Acc) -> tokenise(Rest, [9 | Acc]);
|
tokenise("JUN"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,6} | Acc]);
|
||||||
tokenise("SEPT"++Rest, Acc) -> tokenise(Rest, [9 | Acc]);
|
tokenise("JULY"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,7} | Acc]);
|
||||||
tokenise("SEP"++Rest, Acc) -> tokenise(Rest, [9 | Acc]);
|
tokenise("JUL"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,7} | Acc]);
|
||||||
tokenise("OCTOBER"++Rest, Acc) -> tokenise(Rest, [10 | Acc]);
|
tokenise("AUGUST"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,8} | Acc]);
|
||||||
tokenise("OCT"++Rest, Acc) -> tokenise(Rest, [10 | Acc]);
|
tokenise("AUG"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,8} | Acc]);
|
||||||
tokenise("NOVEMBER"++Rest, Acc) -> tokenise(Rest, [11 | Acc]);
|
tokenise("SEPTEMBER"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,9} | Acc]);
|
||||||
tokenise("NOVEM"++Rest, Acc) -> tokenise(Rest, [11 | Acc]);
|
tokenise("SEPT"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,9} | Acc]);
|
||||||
tokenise("NOV"++Rest, Acc) -> tokenise(Rest, [11 | Acc]);
|
tokenise("SEP"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,9} | Acc]);
|
||||||
tokenise("DECEMBER"++Rest, Acc) -> tokenise(Rest, [12 | Acc]);
|
tokenise("OCTOBER"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,10} | Acc]);
|
||||||
tokenise("DECEM"++Rest, Acc) -> tokenise(Rest, [12 | Acc]);
|
tokenise("OCT"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,10} | Acc]);
|
||||||
tokenise("DEC"++Rest, Acc) -> tokenise(Rest, [12 | Acc]);
|
tokenise("NOVEMBER"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,11} | Acc]);
|
||||||
|
tokenise("NOVEM"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,11} | Acc]);
|
||||||
|
tokenise("NOV"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,11} | Acc]);
|
||||||
|
tokenise("DECEMBER"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,12} | Acc]);
|
||||||
|
tokenise("DECEM"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,12} | Acc]);
|
||||||
|
tokenise("DEC"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,12} | Acc]);
|
||||||
|
|
||||||
tokenise([$: | Rest], Acc) -> tokenise(Rest, [ $: | Acc]);
|
tokenise([$: | Rest], Acc) -> tokenise(Rest, [ $: | Acc]);
|
||||||
tokenise([$/ | Rest], Acc) -> tokenise(Rest, [ $/ | Acc]);
|
tokenise([$/ | Rest], Acc) -> tokenise(Rest, [ $/ | Acc]);
|
||||||
|
@ -264,6 +419,10 @@ tokenise("TH"++Rest, Acc) -> tokenise(Rest, Acc);
|
||||||
tokenise("ND"++Rest, Acc) -> tokenise(Rest, Acc);
|
tokenise("ND"++Rest, Acc) -> tokenise(Rest, Acc);
|
||||||
tokenise("ST"++Rest, Acc) -> tokenise(Rest, Acc);
|
tokenise("ST"++Rest, Acc) -> tokenise(Rest, Acc);
|
||||||
tokenise("OF"++Rest, Acc) -> tokenise(Rest, Acc);
|
tokenise("OF"++Rest, Acc) -> tokenise(Rest, Acc);
|
||||||
|
tokenise("T"++Rest, Acc) -> tokenise(Rest, Acc); % 2012-12-12T12:12:12 ISO formatting.
|
||||||
|
tokenise([$Z | Rest], Acc) -> tokenise(Rest, [$Z | Acc]); % 2012-12-12T12:12:12Zulu
|
||||||
|
tokenise([$+, H1,H2,M1,M2| Rest], Acc) when ?is_tz_offset(H1,H2,M1,M2) -> tokenise(Rest, Acc); % Tue Nov 11 15:03:18 +0000 2014 Twitter format
|
||||||
|
tokenise([$+| Rest], Acc) -> tokenise(Rest, [$+ | Acc]); % 2012-12-12T12:12:12.xxxx+ ISO formatting.
|
||||||
|
|
||||||
tokenise([Else | Rest], Acc) ->
|
tokenise([Else | Rest], Acc) ->
|
||||||
tokenise(Rest, [{bad_token, Else} | Acc]).
|
tokenise(Rest, [{bad_token, Else} | Acc]).
|
||||||
|
@ -329,13 +488,13 @@ format([$z|T], {Date,_}=Dt, Acc) ->
|
||||||
format(T, Dt, [itol(days_in_year(Date))|Acc]);
|
format(T, Dt, [itol(days_in_year(Date))|Acc]);
|
||||||
|
|
||||||
%% Time Formats
|
%% Time Formats
|
||||||
format([$a|T], {_,{H,_,_}}=Dt, Acc) when H > 12 ->
|
format([$a|T], Dt={_,{H,_,_}}, Acc) when H >= 12 ->
|
||||||
format(T, Dt, ["pm"|Acc]);
|
format(T, Dt, ["pm"|Acc]);
|
||||||
format([$a|T], Dt, Acc) ->
|
format([$a|T], Dt={_,{_,_,_}}, Acc) ->
|
||||||
format(T, Dt, ["am"|Acc]);
|
format(T, Dt, ["am"|Acc]);
|
||||||
format([$A|T], {_,{H,_,_}}=Dt, Acc) when H > 12 ->
|
format([$A|T], {_,{H,_,_}}=Dt, Acc) when H >= 12 ->
|
||||||
format(T, Dt, ["PM"|Acc]);
|
format(T, Dt, ["PM"|Acc]);
|
||||||
format([$A|T], Dt, Acc) ->
|
format([$A|T], Dt={_,{_,_,_}}, Acc) ->
|
||||||
format(T, Dt, ["AM"|Acc]);
|
format(T, Dt, ["AM"|Acc]);
|
||||||
format([$g|T], {_,{H,_,_}}=Dt, Acc) when H == 12; H == 0 ->
|
format([$g|T], {_,{H,_,_}}=Dt, Acc) when H == 12; H == 0 ->
|
||||||
format(T, Dt, ["12"|Acc]);
|
format(T, Dt, ["12"|Acc]);
|
||||||
|
@ -355,6 +514,38 @@ format([$i|T], {_,{_,M,_}}=Dt, Acc) ->
|
||||||
format(T, Dt, [pad2(M)|Acc]);
|
format(T, Dt, [pad2(M)|Acc]);
|
||||||
format([$s|T], {_,{_,_,S}}=Dt, Acc) ->
|
format([$s|T], {_,{_,_,S}}=Dt, Acc) ->
|
||||||
format(T, Dt, [pad2(S)|Acc]);
|
format(T, Dt, [pad2(S)|Acc]);
|
||||||
|
format([$f|T], {_,{_,_,_}}=Dt, Acc) ->
|
||||||
|
format(T, Dt, [itol(0)|Acc]);
|
||||||
|
|
||||||
|
%% Time Formats ms
|
||||||
|
format([$a|T], Dt={_,{H,_,_,_}}, Acc) when H > 12 ->
|
||||||
|
format(T, Dt, ["pm"|Acc]);
|
||||||
|
format([$a|T], Dt={_,{_,_,_,_}}, Acc) ->
|
||||||
|
format(T, Dt, ["am"|Acc]);
|
||||||
|
format([$A|T], {_,{H,_,_,_}}=Dt, Acc) when H > 12 ->
|
||||||
|
format(T, Dt, ["PM"|Acc]);
|
||||||
|
format([$A|T], Dt={_,{_,_,_,_}}, Acc) ->
|
||||||
|
format(T, Dt, ["AM"|Acc]);
|
||||||
|
format([$g|T], {_,{H,_,_,_}}=Dt, Acc) when H == 12; H == 0 ->
|
||||||
|
format(T, Dt, ["12"|Acc]);
|
||||||
|
format([$g|T], {_,{H,_,_,_}}=Dt, Acc) when H > 12 ->
|
||||||
|
format(T, Dt, [itol(H-12)|Acc]);
|
||||||
|
format([$g|T], {_,{H,_,_,_}}=Dt, Acc) ->
|
||||||
|
format(T, Dt, [itol(H)|Acc]);
|
||||||
|
format([$G|T], {_,{H,_,_,_}}=Dt, Acc) ->
|
||||||
|
format(T, Dt, [pad2(H)|Acc]);
|
||||||
|
format([$h|T], {_,{H,_,_,_}}=Dt, Acc) when H > 12 ->
|
||||||
|
format(T, Dt, [pad2(H-12)|Acc]);
|
||||||
|
format([$h|T], {_,{H,_,_,_}}=Dt, Acc) ->
|
||||||
|
format(T, Dt, [pad2(H)|Acc]);
|
||||||
|
format([$H|T], {_,{H,_,_,_}}=Dt, Acc) ->
|
||||||
|
format(T, Dt, [pad2(H)|Acc]);
|
||||||
|
format([$i|T], {_,{_,M,_,_}}=Dt, Acc) ->
|
||||||
|
format(T, Dt, [pad2(M)|Acc]);
|
||||||
|
format([$s|T], {_,{_,_,S,_}}=Dt, Acc) ->
|
||||||
|
format(T, Dt, [pad2(S)|Acc]);
|
||||||
|
format([$f|T], {_,{_,_,_,Ms}}=Dt, Acc) ->
|
||||||
|
format(T, Dt, [pad6(Ms)|Acc]);
|
||||||
|
|
||||||
%% Whole Dates
|
%% Whole Dates
|
||||||
format([$c|T], {{Y,M,D},{H,Min,S}}=Dt, Acc) ->
|
format([$c|T], {{Y,M,D},{H,Min,S}}=Dt, Acc) ->
|
||||||
|
@ -403,6 +594,10 @@ to_w(X) -> X.
|
||||||
suffix(1) -> "st";
|
suffix(1) -> "st";
|
||||||
suffix(2) -> "nd";
|
suffix(2) -> "nd";
|
||||||
suffix(3) -> "rd";
|
suffix(3) -> "rd";
|
||||||
|
suffix(21) -> "st";
|
||||||
|
suffix(22) -> "nd";
|
||||||
|
suffix(23) -> "rd";
|
||||||
|
suffix(31) -> "st";
|
||||||
suffix(_) -> "th".
|
suffix(_) -> "th".
|
||||||
|
|
||||||
-spec sdayd(date()) -> string().
|
-spec sdayd(date()) -> string().
|
||||||
|
@ -500,36 +695,54 @@ iso_week_one(Y) ->
|
||||||
itol(X) ->
|
itol(X) ->
|
||||||
integer_to_list(X).
|
integer_to_list(X).
|
||||||
|
|
||||||
-spec pad2(integer()) -> list().
|
-spec pad2(integer() | float()) -> list().
|
||||||
%% @doc int padded with 0 to make sure its 2 chars
|
%% @doc int padded with 0 to make sure its 2 chars
|
||||||
pad2(X) when is_integer(X) ->
|
pad2(X) when is_integer(X) ->
|
||||||
io_lib:format("~2.10.0B",[X]);
|
io_lib:format("~2.10.0B",[X]);
|
||||||
pad2(X) when is_float(X) ->
|
pad2(X) when is_float(X) ->
|
||||||
io_lib:format("~2.10.0B",[trunc(X)]).
|
io_lib:format("~2.10.0B",[trunc(X)]).
|
||||||
|
|
||||||
|
-spec pad6(integer()) -> list().
|
||||||
|
pad6(X) when is_integer(X) ->
|
||||||
|
io_lib:format("~6.10.0B",[X]).
|
||||||
|
|
||||||
ltoi(X) ->
|
ltoi(X) ->
|
||||||
list_to_integer(X).
|
list_to_integer(X).
|
||||||
|
|
||||||
%%
|
%%%===================================================================
|
||||||
%% TEST FUNCTIONS
|
%%% Tests
|
||||||
%%
|
%%%===================================================================
|
||||||
%% c(dh_date,[{d,'TEST'}]).
|
|
||||||
%-define(NOTEST, 1).
|
|
||||||
|
|
||||||
|
-ifdef(TEST).
|
||||||
-include_lib("eunit/include/eunit.hrl").
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
|
|
||||||
-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(DATE_NOON, {{2001,3,10},{12,0,0}}).
|
||||||
|
-define(DATE_MIDNIGHT, {{2001,3,10},{0,0,0}}).
|
||||||
-define(ISO, "o \\WW").
|
-define(ISO, "o \\WW").
|
||||||
|
|
||||||
basic_format_test_() ->
|
basic_format_test_() ->
|
||||||
[
|
[
|
||||||
?_assertEqual(format("F j, Y, g:i a",?DATE), "March 10, 2001, 5:16 pm"),
|
?_assertEqual(format("F j, Y, g:i a",?DATE), "March 10, 2001, 5:16 pm"),
|
||||||
|
?_assertEqual(format("F jS, Y, g:i a",?DATE), "March 10th, 2001, 5:16 pm"),
|
||||||
|
?_assertEqual(format("F jS",{{2011,3,21},{0,0,0}}), "March 21st"),
|
||||||
|
?_assertEqual(format("F jS",{{2011,3,22},{0,0,0}}), "March 22nd"),
|
||||||
|
?_assertEqual(format("F jS",{{2011,3,23},{0,0,0}}), "March 23rd"),
|
||||||
|
?_assertEqual(format("F jS",{{2011,3,31},{0,0,0}}), "March 31st"),
|
||||||
?_assertEqual(format("m.d.y",?DATE), "03.10.01"),
|
?_assertEqual(format("m.d.y",?DATE), "03.10.01"),
|
||||||
?_assertEqual(format("j, n, Y",?DATE), "10, 3, 2001"),
|
?_assertEqual(format("j, n, Y",?DATE), "10, 3, 2001"),
|
||||||
?_assertEqual(format("Ymd",?DATE), "20010310"),
|
?_assertEqual(format("Ymd",?DATE), "20010310"),
|
||||||
?_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_MIDNIGHT), "12am"),
|
||||||
|
?_assertEqual(format("gA",?DATE_MIDNIGHT), "12AM"),
|
||||||
|
|
||||||
?_assertEqual(format("h-i-s, j-m-y, it is w Day",?DATE),
|
?_assertEqual(format("h-i-s, j-m-y, it is w Day",?DATE),
|
||||||
"05-16-17, 10-03-01, 1631 1617 6 Satpm01"),
|
"05-16-17, 10-03-01, 1631 1617 6 Satpm01"),
|
||||||
|
@ -569,6 +782,64 @@ basic_parse_test_() ->
|
||||||
parse("22 Aug 2008 6:35 PM", ?DATE)),
|
parse("22 Aug 2008 6:35 PM", ?DATE)),
|
||||||
?_assertEqual({{2008,8,22}, {18,0,0}},
|
?_assertEqual({{2008,8,22}, {18,0,0}},
|
||||||
parse("22 Aug 2008 6 PM", ?DATE)),
|
parse("22 Aug 2008 6 PM", ?DATE)),
|
||||||
|
?_assertEqual({{2008,8,22}, {18,0,0}},
|
||||||
|
parse("Aug 22, 2008 6 PM", ?DATE)),
|
||||||
|
?_assertEqual({{2008,8,22}, {18,0,0}},
|
||||||
|
parse("August 22nd, 2008 6:00 PM", ?DATE)),
|
||||||
|
?_assertEqual({{2008,8,22}, {18,15,15}},
|
||||||
|
parse("August 22nd 2008, 6:15:15pm", ?DATE)),
|
||||||
|
?_assertEqual({{2008,8,22}, {18,15,15}},
|
||||||
|
parse("August 22nd, 2008, 6:15:15pm", ?DATE)),
|
||||||
|
?_assertEqual({{2008,8,22}, {18,15,0}},
|
||||||
|
parse("Aug 22nd 2008, 18:15", ?DATE)),
|
||||||
|
?_assertEqual({{2008,8,2}, {17,16,17}},
|
||||||
|
parse("2nd of August 2008", ?DATE)),
|
||||||
|
?_assertEqual({{2008,8,2}, {17,16,17}},
|
||||||
|
parse("August 2nd, 2008", ?DATE)),
|
||||||
|
?_assertEqual({{2008,8,2}, {17,16,17}},
|
||||||
|
parse("2nd August, 2008", ?DATE)),
|
||||||
|
?_assertEqual({{2008,8,2}, {17,16,17}},
|
||||||
|
parse("2008 August 2nd", ?DATE)),
|
||||||
|
?_assertEqual({{2008,8,2}, {6,0,0}},
|
||||||
|
parse("2-Aug-2008 6 AM", ?DATE)),
|
||||||
|
?_assertEqual({{2008,8,2}, {6,35,0}},
|
||||||
|
parse("2-Aug-2008 6:35 AM", ?DATE)),
|
||||||
|
?_assertEqual({{2008,8,2}, {6,35,12}},
|
||||||
|
parse("2-Aug-2008 6:35:12 AM", ?DATE)),
|
||||||
|
?_assertEqual({{2008,8,2}, {6,0,0}},
|
||||||
|
parse("August/2/2008 6 AM", ?DATE)),
|
||||||
|
?_assertEqual({{2008,8,2}, {6,35,0}},
|
||||||
|
parse("August/2/2008 6:35 AM", ?DATE)),
|
||||||
|
?_assertEqual({{2008,8,2}, {6,35,0}},
|
||||||
|
parse("2 August 2008 6:35 AM", ?DATE)),
|
||||||
|
?_assertEqual({{2008,8,2}, {6,0,0}},
|
||||||
|
parse("2 Aug 2008 6AM", ?DATE)),
|
||||||
|
?_assertEqual({{2008,8,2}, {6,35,0}},
|
||||||
|
parse("2 Aug 2008 6:35AM", ?DATE)),
|
||||||
|
?_assertEqual({{2008,8,2}, {6,35,0}},
|
||||||
|
parse("2 Aug 2008 6:35 AM", ?DATE)),
|
||||||
|
?_assertEqual({{2008,8,2}, {6,0,0}},
|
||||||
|
parse("2 Aug 2008 6", ?DATE)),
|
||||||
|
?_assertEqual({{2008,8,2}, {6,35,0}},
|
||||||
|
parse("2 Aug 2008 6:35", ?DATE)),
|
||||||
|
?_assertEqual({{2008,8,2}, {18,35,0}},
|
||||||
|
parse("2 Aug 2008 6:35 PM", ?DATE)),
|
||||||
|
?_assertEqual({{2008,8,2}, {18,0,0}},
|
||||||
|
parse("2 Aug 2008 6 PM", ?DATE)),
|
||||||
|
?_assertEqual({{2008,8,2}, {18,0,0}},
|
||||||
|
parse("Aug 2, 2008 6 PM", ?DATE)),
|
||||||
|
?_assertEqual({{2008,8,2}, {18,0,0}},
|
||||||
|
parse("August 2nd, 2008 6:00 PM", ?DATE)),
|
||||||
|
?_assertEqual({{2008,8,2}, {18,15,15}},
|
||||||
|
parse("August 2nd 2008, 6:15:15pm", ?DATE)),
|
||||||
|
?_assertEqual({{2008,8,2}, {18,15,15}},
|
||||||
|
parse("August 2nd, 2008, 6:15:15pm", ?DATE)),
|
||||||
|
?_assertEqual({{2008,8,2}, {18,15,0}},
|
||||||
|
parse("Aug 2nd 2008, 18:15", ?DATE)),
|
||||||
|
?_assertEqual({{2012,12,10}, {0,0,0}},
|
||||||
|
parse("Dec 10th, 2012, 12:00 AM", ?DATE)),
|
||||||
|
?_assertEqual({{2012,12,10}, {0,0,0}},
|
||||||
|
parse("10 Dec 2012 12:00 AM", ?DATE)),
|
||||||
?_assertEqual({{2001,3,10}, {11,15,0}},
|
?_assertEqual({{2001,3,10}, {11,15,0}},
|
||||||
parse("11:15", ?DATE)),
|
parse("11:15", ?DATE)),
|
||||||
?_assertEqual({{2001,3,10}, {1,15,0}},
|
?_assertEqual({{2001,3,10}, {1,15,0}},
|
||||||
|
@ -646,7 +917,12 @@ parse_with_days_test_() ->
|
||||||
?_assertEqual({{2008,8,22}, {6,0,0}},
|
?_assertEqual({{2008,8,22}, {6,0,0}},
|
||||||
parse("Monday 22 Aug 2008 6a", ?DATE)),
|
parse("Monday 22 Aug 2008 6a", ?DATE)),
|
||||||
?_assertEqual({{2008,8,22}, {18,35,0}},
|
?_assertEqual({{2008,8,22}, {18,35,0}},
|
||||||
parse("Mon, 22 Aug 2008 6:35 PM", ?DATE))
|
parse("Mon, 22 Aug 2008 6:35 PM", ?DATE)),
|
||||||
|
% Twitter style
|
||||||
|
?_assertEqual({{2008,8,22}, {06,35,04}},
|
||||||
|
parse("Mon Aug 22 06:35:04 +0000 2008", ?DATE)),
|
||||||
|
?_assertEqual({{2008,8,22}, {06,35,04}},
|
||||||
|
parse("Mon Aug 22 06:35:04 +0500 2008", ?DATE))
|
||||||
].
|
].
|
||||||
|
|
||||||
parse_with_TZ_test_() ->
|
parse_with_TZ_test_() ->
|
||||||
|
@ -674,3 +950,133 @@ iso_test_() ->
|
||||||
?_assertEqual("2009 W53",format(?ISO,{{2009,12,31},{1,1,1}})),
|
?_assertEqual("2009 W53",format(?ISO,{{2009,12,31},{1,1,1}})),
|
||||||
?_assertEqual("2009 W53",format(?ISO,{{2010,1,3}, {1,1,1}}))
|
?_assertEqual("2009 W53",format(?ISO,{{2010,1,3}, {1,1,1}}))
|
||||||
].
|
].
|
||||||
|
|
||||||
|
ms_test_() ->
|
||||||
|
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,123_000}}, parse("2012-12-12T12:12:12.123")),
|
||||||
|
?_assertEqual(format("H:m:s.f \\m \\i\\s \\m\\o\\n\\t\\h",?DATEMS),
|
||||||
|
"17:03:17.123456 m is month"),
|
||||||
|
?_assertEqual(format("Y-m-d\\TH:i:s.f",?DATEMS),
|
||||||
|
"2001-03-10T17:16:17.123456"),
|
||||||
|
?_assertEqual(format("Y-m-d\\TH:i:s.f",nparse("2001-03-10T05:16:17.123456")),
|
||||||
|
"2001-03-10T05:16:17.123456"),
|
||||||
|
?_assertEqual(format("Y-m-d\\TH:i:s.f",nparse("2001-03-10T05:16:17.123456")),
|
||||||
|
"2001-03-10T05:16:17.123456"),
|
||||||
|
?_assertEqual(format("Y-m-d\\TH:i:s.f",nparse("2001-03-10T15:16:17.123456")),
|
||||||
|
"2001-03-10T15:16:17.123456"),
|
||||||
|
?_assertEqual(format("Y-m-d\\TH:i:s.f",nparse("2001-03-10T15:16:17.000123")),
|
||||||
|
"2001-03-10T15:16:17.000123"),
|
||||||
|
?_assertEqual(Now, nparse(format("Y-m-d\\TH:i:s.f", Now)))
|
||||||
|
].
|
||||||
|
|
||||||
|
zulu_test_() ->
|
||||||
|
[
|
||||||
|
?_assertEqual(format("Y-m-d\\TH:i:sZ",nparse("2001-03-10T15:16:17.123456")),
|
||||||
|
"2001-03-10T15:16:17Z"),
|
||||||
|
?_assertEqual(format("Y-m-d\\TH:i:s",nparse("2001-03-10T15:16:17Z")),
|
||||||
|
"2001-03-10T15:16:17"),
|
||||||
|
?_assertEqual(format("Y-m-d\\TH:i:s",nparse("2001-03-10T15:16:17+04")),
|
||||||
|
"2001-03-10T11:16:17"),
|
||||||
|
?_assertEqual(format("Y-m-d\\TH:i:s",nparse("2001-03-10T15:16:17+04:00")),
|
||||||
|
"2001-03-10T11:16:17"),
|
||||||
|
?_assertEqual(format("Y-m-d\\TH:i:s",nparse("2001-03-10T15:16:17-04")),
|
||||||
|
"2001-03-10T19:16:17"),
|
||||||
|
?_assertEqual(format("Y-m-d\\TH:i:s",nparse("2001-03-10T15:16:17-04:00")),
|
||||||
|
"2001-03-10T19:16:17")
|
||||||
|
].
|
||||||
|
|
||||||
|
format_iso8601_test_() ->
|
||||||
|
[
|
||||||
|
?_assertEqual("2001-03-10T17:16:17Z",
|
||||||
|
format_iso8601({{2001,3,10},{17,16,17}})),
|
||||||
|
?_assertEqual("2001-03-10T17:16:17.000000Z",
|
||||||
|
format_iso8601({{2001,3,10},{17,16,17,0}})),
|
||||||
|
?_assertEqual("2001-03-10T17:16:17.100000Z",
|
||||||
|
format_iso8601({{2001,3,10},{17,16,17,100_000}})),
|
||||||
|
?_assertEqual("2001-03-10T17:16:17.120000Z",
|
||||||
|
format_iso8601({{2001,3,10},{17,16,17,120_000}})),
|
||||||
|
?_assertEqual("2001-03-10T17:16:17.123000Z",
|
||||||
|
format_iso8601({{2001,3,10},{17,16,17,123_000}})),
|
||||||
|
?_assertEqual("2001-03-10T17:16:17.123400Z",
|
||||||
|
format_iso8601({{2001,3,10},{17,16,17,123_400}})),
|
||||||
|
?_assertEqual("2001-03-10T17:16:17.123450Z",
|
||||||
|
format_iso8601({{2001,3,10},{17,16,17,123_450}})),
|
||||||
|
?_assertEqual("2001-03-10T17:16:17.123456Z",
|
||||||
|
format_iso8601({{2001,3,10},{17,16,17,123_456}})),
|
||||||
|
?_assertEqual("2001-03-10T17:16:17.023456Z",
|
||||||
|
format_iso8601({{2001,3,10},{17,16,17,23_456}})),
|
||||||
|
?_assertEqual("2001-03-10T17:16:17.003456Z",
|
||||||
|
format_iso8601({{2001,3,10},{17,16,17,3_456}})),
|
||||||
|
?_assertEqual("2001-03-10T17:16:17.000456Z",
|
||||||
|
format_iso8601({{2001,3,10},{17,16,17,456}})),
|
||||||
|
?_assertEqual("2001-03-10T17:16:17.000056Z",
|
||||||
|
format_iso8601({{2001,3,10},{17,16,17,56}})),
|
||||||
|
?_assertEqual("2001-03-10T17:16:17.000006Z",
|
||||||
|
format_iso8601({{2001,3,10},{17,16,17,6}})),
|
||||||
|
?_assertEqual("2001-03-10T07:16:17Z",
|
||||||
|
format_iso8601({{2001,3,10},{07,16,17}})),
|
||||||
|
?_assertEqual("2001-03-10T07:16:17.000000Z",
|
||||||
|
format_iso8601({{2001,3,10},{07,16,17,0}})),
|
||||||
|
?_assertEqual("2001-03-10T07:16:17.100000Z",
|
||||||
|
format_iso8601({{2001,3,10},{07,16,17,100_000}})),
|
||||||
|
?_assertEqual("2001-03-10T07:16:17.120000Z",
|
||||||
|
format_iso8601({{2001,3,10},{07,16,17,120_000}})),
|
||||||
|
?_assertEqual("2001-03-10T07:16:17.123000Z",
|
||||||
|
format_iso8601({{2001,3,10},{07,16,17,123_000}})),
|
||||||
|
?_assertEqual("2001-03-10T07:16:17.123400Z",
|
||||||
|
format_iso8601({{2001,3,10},{07,16,17,123_400}})),
|
||||||
|
?_assertEqual("2001-03-10T07:16:17.123450Z",
|
||||||
|
format_iso8601({{2001,3,10},{07,16,17,123_450}})),
|
||||||
|
?_assertEqual("2001-03-10T07:16:17.123456Z",
|
||||||
|
format_iso8601({{2001,3,10},{07,16,17,123_456}})),
|
||||||
|
?_assertEqual("2001-03-10T07:16:17.023456Z",
|
||||||
|
format_iso8601({{2001,3,10},{07,16,17,23_456}})),
|
||||||
|
?_assertEqual("2001-03-10T07:16:17.003456Z",
|
||||||
|
format_iso8601({{2001,3,10},{07,16,17,3_456}})),
|
||||||
|
?_assertEqual("2001-03-10T07:16:17.000456Z",
|
||||||
|
format_iso8601({{2001,3,10},{07,16,17,456}})),
|
||||||
|
?_assertEqual("2001-03-10T07:16:17.000056Z",
|
||||||
|
format_iso8601({{2001,3,10},{07,16,17,56}})),
|
||||||
|
?_assertEqual("2001-03-10T07:16:17.000006Z",
|
||||||
|
format_iso8601({{2001,3,10},{07,16,17,6}}))
|
||||||
|
].
|
||||||
|
|
||||||
|
parse_iso8601_test_() ->
|
||||||
|
[
|
||||||
|
?_assertEqual({{2001,3,10},{17,16,17}},
|
||||||
|
parse("2001-03-10T17:16:17Z")),
|
||||||
|
?_assertEqual({{2001,3,10},{17,16,17,0}},
|
||||||
|
parse("2001-03-10T17:16:17.000Z")),
|
||||||
|
?_assertEqual({{2001,3,10},{17,16,17,0}},
|
||||||
|
parse("2001-03-10T17:16:17.000000Z")),
|
||||||
|
?_assertEqual({{2001,3,10},{17,16,17,100_000}},
|
||||||
|
parse("2001-03-10T17:16:17.1Z")),
|
||||||
|
?_assertEqual({{2001,3,10},{17,16,17,120_000}},
|
||||||
|
parse("2001-03-10T17:16:17.12Z")),
|
||||||
|
?_assertEqual({{2001,3,10},{17,16,17,123_000}},
|
||||||
|
parse("2001-03-10T17:16:17.123Z")),
|
||||||
|
?_assertEqual({{2001,3,10},{17,16,17,123_400}},
|
||||||
|
parse("2001-03-10T17:16:17.1234Z")),
|
||||||
|
?_assertEqual({{2001,3,10},{17,16,17,123_450}},
|
||||||
|
parse("2001-03-10T17:16:17.12345Z")),
|
||||||
|
?_assertEqual({{2001,3,10},{17,16,17,123_456}},
|
||||||
|
parse("2001-03-10T17:16:17.123456Z")),
|
||||||
|
|
||||||
|
?_assertEqual({{2001,3,10},{15,16,17,100_000}},
|
||||||
|
parse("2001-03-10T16:16:17.1+01:00")),
|
||||||
|
?_assertEqual({{2001,3,10},{15,16,17,123_456}},
|
||||||
|
parse("2001-03-10T16:16:17.123456+01:00")),
|
||||||
|
?_assertEqual({{2001,3,10},{17,16,17,100_000}},
|
||||||
|
parse("2001-03-10T16:16:17.1-01:00")),
|
||||||
|
?_assertEqual({{2001,3,10},{17,16,17,123_456}},
|
||||||
|
parse("2001-03-10T16:16:17.123456-01:00")),
|
||||||
|
|
||||||
|
?_assertEqual({{2001,3,10},{17,16,17,456}},
|
||||||
|
parse("2001-03-10T17:16:17.000456Z")),
|
||||||
|
?_assertEqual({{2001,3,10},{17,16,17,123_000}},
|
||||||
|
parse("2001-03-10T17:16:17.123000Z"))
|
||||||
|
].
|
||||||
|
|
||||||
|
-endif.
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
%%% vi:ts=4 sw=4 et
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
%%% @author Eric Merritt <ericbmerritt@gmail.com>
|
%%% @author Eric Merritt <ericbmerritt@gmail.com>
|
||||||
%%% @copyright 2011 Erlware, LLC.
|
%%% @copyright 2011 Erlware, LLC.
|
||||||
|
@ -5,9 +6,9 @@
|
||||||
%%% This provides an implementation of the ec_dictionary type using
|
%%% This provides an implementation of the ec_dictionary type using
|
||||||
%%% erlang dicts as a base. The function documentation for
|
%%% erlang dicts as a base. The function documentation for
|
||||||
%%% ec_dictionary applies here as well.
|
%%% ec_dictionary applies here as well.
|
||||||
|
%%% see ec_dictionary
|
||||||
|
%%% see dict
|
||||||
%%% @end
|
%%% @end
|
||||||
%%% @see ec_dictionary
|
|
||||||
%%% @see dict
|
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
-module(ec_dict).
|
-module(ec_dict).
|
||||||
|
|
||||||
|
@ -31,7 +32,9 @@
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
%%% Types
|
%%% Types
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
-opaque dictionary(_K, _V) :: dict().
|
%% This should be opaque, but that kills dialyzer so for now we export it
|
||||||
|
%% however you should not rely on the internal representation here
|
||||||
|
-type dictionary(_K, _V) :: dict:dict().
|
||||||
|
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
%%% API
|
%%% API
|
||||||
|
@ -88,7 +91,7 @@ has_value(Value, Data) ->
|
||||||
false,
|
false,
|
||||||
Data).
|
Data).
|
||||||
|
|
||||||
-spec size(Object::dictionary(_K, _V)) -> integer().
|
-spec size(Object::dictionary(_K, _V)) -> non_neg_integer().
|
||||||
size(Data) ->
|
size(Data) ->
|
||||||
dict:size(Data).
|
dict:size(Data).
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
%%% vi:ts=4 sw=4 et
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
%%% @author Eric Merritt <ericbmerritt@gmail.com>
|
%%% @author Eric Merritt <ericbmerritt@gmail.com>
|
||||||
%%% @copyright 2011 Erlware, LLC.
|
%%% @copyright 2011 Erlware, LLC.
|
||||||
|
@ -10,9 +11,6 @@
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
-module(ec_dictionary).
|
-module(ec_dictionary).
|
||||||
|
|
||||||
%%% Behaviour Callbacks
|
|
||||||
-export([behaviour_info/1]).
|
|
||||||
|
|
||||||
%% API
|
%% API
|
||||||
-export([new/1,
|
-export([new/1,
|
||||||
has_key/2,
|
has_key/2,
|
||||||
|
@ -38,30 +36,27 @@
|
||||||
{callback,
|
{callback,
|
||||||
data}).
|
data}).
|
||||||
|
|
||||||
-opaque dictionary(_K, _V) :: #dict_t{}.
|
%% This should be opaque, but that kills dialyzer so for now we export it
|
||||||
|
%% however you should not rely on the internal representation here
|
||||||
|
-type dictionary(_K, _V) :: #dict_t{}.
|
||||||
-type key(T) :: T.
|
-type key(T) :: T.
|
||||||
-type value(T) :: T.
|
-type value(T) :: T.
|
||||||
|
|
||||||
|
-callback new() -> any().
|
||||||
|
-callback has_key(key(any()), any()) -> boolean().
|
||||||
|
-callback get(key(any()), any()) -> any().
|
||||||
|
-callback add(key(any()), value(any()), T) -> T.
|
||||||
|
-callback remove(key(any()), T) -> T.
|
||||||
|
-callback has_value(value(any()), any()) -> boolean().
|
||||||
|
-callback size(any()) -> non_neg_integer().
|
||||||
|
-callback to_list(any()) -> [{key(any()), value(any())}].
|
||||||
|
-callback from_list([{key(any()), value(any())}]) -> any().
|
||||||
|
-callback keys(any()) -> [key(any())].
|
||||||
|
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
%%% API
|
%%% API
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
|
|
||||||
%% @doc export the behaviour callbacks for this type
|
|
||||||
%% @private
|
|
||||||
behaviour_info(callbacks) ->
|
|
||||||
[{new, 0},
|
|
||||||
{has_key, 2},
|
|
||||||
{get, 2},
|
|
||||||
{add, 3},
|
|
||||||
{remove, 2},
|
|
||||||
{has_value, 2},
|
|
||||||
{size, 1},
|
|
||||||
{to_list, 1},
|
|
||||||
{from_list, 1},
|
|
||||||
{keys, 1}];
|
|
||||||
behaviour_info(_) ->
|
|
||||||
undefined.
|
|
||||||
|
|
||||||
%% @doc create a new dictionary object from the specified module. The
|
%% @doc create a new dictionary object from the specified module. The
|
||||||
%% module should implement the dictionary behaviour.
|
%% module should implement the dictionary behaviour.
|
||||||
%%
|
%%
|
||||||
|
@ -83,7 +78,7 @@ has_key(Key, #dict_t{callback = Mod, data = Data}) ->
|
||||||
%%
|
%%
|
||||||
%% @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
|
%% when the key does not exist @throws not_found
|
||||||
-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).
|
||||||
|
|
405
src/ec_file.erl
405
src/ec_file.erl
|
@ -1,3 +1,4 @@
|
||||||
|
%%% vi:ts=4 sw=4 et
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
%%% @copyright (C) 2011, Erlware LLC
|
%%% @copyright (C) 2011, Erlware LLC
|
||||||
%%% @doc
|
%%% @doc
|
||||||
|
@ -7,95 +8,171 @@
|
||||||
-module(ec_file).
|
-module(ec_file).
|
||||||
|
|
||||||
-export([
|
-export([
|
||||||
|
exists/1,
|
||||||
copy/2,
|
copy/2,
|
||||||
copy/3,
|
copy/3,
|
||||||
mkdtemp/0,
|
copy_file_info/3,
|
||||||
|
insecure_mkdtemp/0,
|
||||||
mkdir_path/1,
|
mkdir_path/1,
|
||||||
|
mkdir_p/1,
|
||||||
find/2,
|
find/2,
|
||||||
is_symlink/1,
|
is_symlink/1,
|
||||||
|
is_dir/1,
|
||||||
|
type/1,
|
||||||
|
real_dir_path/1,
|
||||||
remove/1,
|
remove/1,
|
||||||
remove/2,
|
remove/2,
|
||||||
md5sum/1,
|
md5sum/1,
|
||||||
|
sha1sum/1,
|
||||||
read/1,
|
read/1,
|
||||||
write/2,
|
write/2,
|
||||||
write_term/2,
|
write_term/2
|
||||||
consult/1
|
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-export_type([
|
-export_type([
|
||||||
path/0,
|
|
||||||
option/0
|
option/0
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-include_lib("kernel/include/file.hrl").
|
-include_lib("kernel/include/file.hrl").
|
||||||
|
|
||||||
%% User friendly exception message (remove line and module info once we
|
|
||||||
%% get them in stack traces)
|
|
||||||
-define(UEX(Exception, UMSG, UVARS),
|
|
||||||
{uex, {?MODULE,
|
|
||||||
?LINE,
|
|
||||||
Exception,
|
|
||||||
lists:flatten(io_lib:fwrite(UMSG, UVARS))}}).
|
|
||||||
|
|
||||||
-define(CHECK_PERMS_MSG,
|
-define(CHECK_PERMS_MSG,
|
||||||
"Try checking that you have the correct permissions and try again~n").
|
"Try checking that you have the correct permissions and try again~n").
|
||||||
|
|
||||||
%%============================================================================
|
%%============================================================================
|
||||||
%% Types
|
%% Types
|
||||||
%%============================================================================
|
%%============================================================================
|
||||||
-type path() :: string().
|
-type file_info() :: mode | time | owner | group.
|
||||||
-type option() :: [atom()].
|
-type option() :: recursive | {file_info, [file_info()]}.
|
||||||
|
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
%%% API
|
%%% API
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
|
-spec exists(file:filename()) -> boolean().
|
||||||
|
exists(Filename) ->
|
||||||
|
case file:read_file_info(Filename) of
|
||||||
|
{ok, _} ->
|
||||||
|
true;
|
||||||
|
{error, _Reason} ->
|
||||||
|
false
|
||||||
|
end.
|
||||||
|
|
||||||
%% @doc copy an entire directory to another location.
|
%% @doc copy an entire directory to another location.
|
||||||
-spec copy(path(), path(), Options::[option()]) -> ok.
|
-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, [recursive] = Options) ->
|
copy(From, To, Options) ->
|
||||||
case filelib:is_dir(From) of
|
case proplists:get_value(recursive, Options, false) of
|
||||||
false ->
|
|
||||||
copy(From, To);
|
|
||||||
true ->
|
true ->
|
||||||
make_dir_if_dir(To),
|
case is_dir(From) of
|
||||||
copy_subfiles(From, To, Options)
|
false ->
|
||||||
|
copy_(From, To, Options);
|
||||||
|
true ->
|
||||||
|
make_dir_if_dir(To),
|
||||||
|
copy_subfiles(From, To, Options)
|
||||||
|
end;
|
||||||
|
false ->
|
||||||
|
copy_(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::string(), To::string()) -> ok.
|
-spec copy(From::file:filename(), To::file:filename()) -> ok | {error, Reason::term()}.
|
||||||
copy(From, To) ->
|
copy(From, To) ->
|
||||||
try
|
copy_(From, To, [{file_info, [mode, time, owner, group]}]).
|
||||||
ec_file_copy(From, To)
|
|
||||||
catch
|
copy_(From, To, Options) ->
|
||||||
_C:E -> throw(?UEX({copy_failed, E}, ?CHECK_PERMS_MSG, []))
|
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, _} ->
|
||||||
|
copy_file_info(To, From, proplists:get_value(file_info, Options, []));
|
||||||
|
{error, Error} ->
|
||||||
|
{error, {copy_failed, Error}}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% @doc return an md5 checksum string or a binary. Same as unix utility of
|
copy_file_info(To, From, FileInfoToKeep) ->
|
||||||
%% same name.
|
case file:read_file_info(From) of
|
||||||
|
{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) ->
|
||||||
hex(binary_to_list(erlang:md5(Value))).
|
bin_to_hex(crypto:hash(md5, Value)).
|
||||||
|
|
||||||
|
%% @doc return the SHA-1 digest of a string or a binary,
|
||||||
|
%% named after the UNIX utility.
|
||||||
|
-spec sha1sum(string() | binary()) -> string().
|
||||||
|
sha1sum(Value) ->
|
||||||
|
bin_to_hex(crypto:hash(sha, Value)).
|
||||||
|
|
||||||
|
bin_to_hex(Bin) ->
|
||||||
|
hex(binary_to_list(Bin)).
|
||||||
|
|
||||||
%% @doc delete a file. Use the recursive option for directories.
|
%% @doc delete a file. Use the recursive option for directories.
|
||||||
%% <pre>
|
%% <pre>
|
||||||
%% Example: remove("./tmp_dir", [recursive]).
|
%% Example: remove("./tmp_dir", [recursive]).
|
||||||
%% </pre>
|
%% </pre>
|
||||||
-spec remove(path(), Options::[option()]) -> ok | {error, Reason::term()}.
|
-spec remove(file:name(), Options::[option()]) -> ok | {error, Reason::term()}.
|
||||||
remove(Path, Options) ->
|
remove(Path, Options) ->
|
||||||
try
|
case lists:member(recursive, Options) of
|
||||||
ok = ec_file_remove(Path, Options)
|
false -> file:delete(Path);
|
||||||
catch
|
true -> remove_recursive(Path, Options)
|
||||||
_C:E -> throw(?UEX({remove_failed, E}, ?CHECK_PERMS_MSG, []))
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
||||||
%% @doc delete a file.
|
%% @doc delete a file.
|
||||||
-spec remove(path()) -> ok | {error, Reason::term()}.
|
-spec remove(file:name()) -> ok | {error, Reason::term()}.
|
||||||
remove(Path) ->
|
remove(Path) ->
|
||||||
remove(Path, []).
|
remove(Path, []).
|
||||||
|
|
||||||
%% @doc indicates witha boolean if the path supplied refers to symlink.
|
%% @doc indicates with a boolean if the path supplied refers to symlink.
|
||||||
-spec is_symlink(path()) -> 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
|
||||||
{ok, #file_info{type = symlink}} ->
|
{ok, #file_info{type = symlink}} ->
|
||||||
|
@ -104,93 +181,103 @@ is_symlink(Path) ->
|
||||||
false
|
false
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
is_dir(Path) ->
|
||||||
%% @doc make a unique temorory directory. Similar function to BSD stdlib
|
case file:read_file_info(Path) of
|
||||||
%% function of the same name.
|
{ok, #file_info{type = directory}} ->
|
||||||
-spec mkdtemp() -> TmpDirPath::path().
|
true;
|
||||||
mkdtemp() ->
|
_ ->
|
||||||
UniqueNumber = integer_to_list(element(3, now())),
|
false
|
||||||
TmpDirPath =
|
|
||||||
filename:join([tmp(), lists:flatten([".tmp_dir", UniqueNumber])]),
|
|
||||||
try
|
|
||||||
ok = mkdir_path(TmpDirPath),
|
|
||||||
TmpDirPath
|
|
||||||
catch
|
|
||||||
_C:E -> throw(?UEX({mkdtemp_failed, E}, ?CHECK_PERMS_MSG, []))
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
%% @doc returns the type of the file.
|
||||||
|
-spec type(file:name()) -> file | symlink | directory | undefined.
|
||||||
|
type(Path) ->
|
||||||
|
case filelib:is_regular(Path) of
|
||||||
|
true ->
|
||||||
|
file;
|
||||||
|
false ->
|
||||||
|
case is_symlink(Path) of
|
||||||
|
true ->
|
||||||
|
symlink;
|
||||||
|
false ->
|
||||||
|
case is_dir(Path) of
|
||||||
|
true -> directory;
|
||||||
|
false -> undefined
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end.
|
||||||
|
%% @doc gets the real path of a directory. This is mostly useful for
|
||||||
|
%% resolving symlinks. Be aware that this temporarily changes the
|
||||||
|
%% current working directory to figure out what the actual path
|
||||||
|
%% is. That means that it can be quite slow.
|
||||||
|
-spec real_dir_path(file:name()) -> file:name().
|
||||||
|
real_dir_path(Path) ->
|
||||||
|
{ok, CurCwd} = file:get_cwd(),
|
||||||
|
ok = file:set_cwd(Path),
|
||||||
|
{ok, RealPath} = file:get_cwd(),
|
||||||
|
ok = file:set_cwd(CurCwd),
|
||||||
|
filename:absname(RealPath).
|
||||||
|
|
||||||
|
%% @doc make a unique temporary directory. Similar function to BSD stdlib
|
||||||
|
%% function of the same name.
|
||||||
|
-spec insecure_mkdtemp() -> TmpDirPath::file:name() | {error, term()}.
|
||||||
|
insecure_mkdtemp() ->
|
||||||
|
UniqueNumber = erlang:integer_to_list(erlang:trunc(rand:uniform() * 1_000_000_000_000)),
|
||||||
|
TmpDirPath =
|
||||||
|
filename:join([tmp(), lists:flatten([".tmp_dir", UniqueNumber])]),
|
||||||
|
|
||||||
|
case mkdir_path(TmpDirPath) of
|
||||||
|
ok -> TmpDirPath;
|
||||||
|
Error -> Error
|
||||||
|
end.
|
||||||
|
|
||||||
%% @doc Makes a directory including parent dirs if they are missing.
|
%% @doc Makes a directory including parent dirs if they are missing.
|
||||||
-spec mkdir_path(path()) -> ok.
|
-spec mkdir_p(file:name()) -> ok | {error, Reason::term()}.
|
||||||
mkdir_path(Path) ->
|
mkdir_p(Path) ->
|
||||||
%% We are exploiting a feature of ensuredir that that creates all
|
%% We are exploiting a feature of ensuredir that that creates all
|
||||||
%% directories up to the last element in the filename, then ignores
|
%% directories up to the last element in the filename, then ignores
|
||||||
%% that last element. This way we ensure that the dir is created
|
%% that last element. This way we ensure that the dir is created
|
||||||
%% and not have any worries about path names
|
%% and not have any worries about path names
|
||||||
DirName = filename:join([filename:absname(Path), "tmp"]),
|
DirName = filename:join([filename:absname(Path), "tmp"]),
|
||||||
try
|
filelib:ensure_dir(DirName).
|
||||||
ok = filelib:ensure_dir(DirName)
|
|
||||||
catch
|
|
||||||
_C:E -> throw(?UEX({mkdir_path_failed, E}, ?CHECK_PERMS_MSG, []))
|
|
||||||
end.
|
|
||||||
|
|
||||||
|
|
||||||
%% @doc consult an erlang term file from the file system.
|
%% @doc Makes a directory including parent dirs if they are missing.
|
||||||
%% Provide user readible exeption on failure.
|
-spec mkdir_path(file:name()) -> ok | {error, Reason::term()}.
|
||||||
-spec consult(FilePath::path()) -> term().
|
mkdir_path(Path) ->
|
||||||
consult(FilePath) ->
|
mkdir_p(Path).
|
||||||
case file:consult(FilePath) of
|
|
||||||
{ok, [Term]} ->
|
|
||||||
Term;
|
|
||||||
{error, Error} ->
|
|
||||||
Msg = "The file at ~p~n" ++
|
|
||||||
"is either not a valid Erlang term, does not to exist~n" ++
|
|
||||||
"or you lack the permissions to read it. Please check~n" ++
|
|
||||||
"to see if the file exists and that it has the correct~n" ++
|
|
||||||
"permissions~n",
|
|
||||||
throw(?UEX({failed_to_consult_file, {FilePath, Error}},
|
|
||||||
Msg, [FilePath]))
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% @doc read a file from the file system. Provide UEX exeption on failure.
|
|
||||||
-spec read(FilePath::string()) -> binary().
|
%% @doc read a file from the file system. Provide UEX exception on failure.
|
||||||
|
-spec read(FilePath::file:filename()) -> {ok, binary()} | {error, Reason::term()}.
|
||||||
read(FilePath) ->
|
read(FilePath) ->
|
||||||
try
|
%% Now that we are moving away from exceptions again this becomes
|
||||||
{ok, FileBin} = file:read_file(FilePath),
|
%% a bit redundant but we want to be backwards compatible as much
|
||||||
FileBin
|
%% as possible in the api.
|
||||||
catch
|
file:read_file(FilePath).
|
||||||
_C:E -> throw(?UEX({read_failed, {FilePath, E}},
|
|
||||||
"Read failed for the file ~p with ~p~n" ++
|
|
||||||
?CHECK_PERMS_MSG,
|
|
||||||
[FilePath, E]))
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% @doc write a file to the file system. Provide UEX exeption on failure.
|
|
||||||
-spec write(FileName::string(), Contents::string()) -> ok.
|
%% @doc write a file to the file system. Provide UEX exception on failure.
|
||||||
|
-spec write(FileName::file:filename(), Contents::string()) -> ok | {error, Reason::term()}.
|
||||||
write(FileName, Contents) ->
|
write(FileName, Contents) ->
|
||||||
case file:write_file(FileName, Contents) of
|
%% Now that we are moving away from exceptions again this becomes
|
||||||
ok ->
|
%% a bit redundant but we want to be backwards compatible as much
|
||||||
ok;
|
%% as possible in the api.
|
||||||
{error, Reason} ->
|
file:write_file(FileName, Contents).
|
||||||
Msg = "Writing the file ~s to disk failed with reason ~p.~n" ++
|
|
||||||
?CHECK_PERMS_MSG,
|
|
||||||
throw(?UEX({write_file_failure, {FileName, Reason}},
|
|
||||||
Msg,
|
|
||||||
[FileName, Reason]))
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% @doc write a term out to a file so that it can be consulted later.
|
%% @doc write a term out to a file so that it can be consulted later.
|
||||||
-spec write_term(string(), term()) -> ok.
|
-spec write_term(file:filename(), term()) -> ok | {error, Reason::term()}.
|
||||||
write_term(FileName, Term) ->
|
write_term(FileName, Term) ->
|
||||||
write(FileName, lists:flatten(io_lib:fwrite("~p. ", [Term]))).
|
write(FileName, lists:flatten(io_lib:fwrite("~p. ", [Term]))).
|
||||||
|
|
||||||
%% @doc Finds files and directories that match the regexp supplied in
|
%% @doc Finds files and directories that match the regexp supplied in
|
||||||
%% the TargetPattern regexp.
|
%% the TargetPattern regexp.
|
||||||
-spec find(FromDir::path(), TargetPattern::string()) -> [path()].
|
-spec find(FromDir::file:name(), TargetPattern::string()) -> [file:name()].
|
||||||
find([], _) ->
|
find([], _) ->
|
||||||
[];
|
[];
|
||||||
find(FromDir, TargetPattern) ->
|
find(FromDir, TargetPattern) ->
|
||||||
case filelib:is_dir(FromDir) of
|
case is_dir(FromDir) of
|
||||||
false ->
|
false ->
|
||||||
case re:run(FromDir, TargetPattern) of
|
case re:run(FromDir, TargetPattern) of
|
||||||
{match, _} -> [FromDir];
|
{match, _} -> [FromDir];
|
||||||
|
@ -207,7 +294,7 @@ find(FromDir, TargetPattern) ->
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
%%% Internal Functions
|
%%% Internal Functions
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
-spec find_in_subdirs(path(), string()) -> [path()].
|
-spec find_in_subdirs(file:name(), string()) -> [file:name()].
|
||||||
find_in_subdirs(FromDir, TargetPattern) ->
|
find_in_subdirs(FromDir, TargetPattern) ->
|
||||||
lists:foldl(fun(CheckFromDir, Acc)
|
lists:foldl(fun(CheckFromDir, Acc)
|
||||||
when CheckFromDir == FromDir ->
|
when CheckFromDir == FromDir ->
|
||||||
|
@ -219,57 +306,52 @@ find_in_subdirs(FromDir, TargetPattern) ->
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
[],
|
[],
|
||||||
filelib:wildcard(filename:join(FromDir, "*"))).
|
sub_files(FromDir)).
|
||||||
|
|
||||||
-spec ec_file_remove(path(), [{atom(), any()}]) -> ok.
|
|
||||||
ec_file_remove(Path, Options) ->
|
|
||||||
case lists:member(recursive, Options) of
|
|
||||||
false -> file:delete(Path);
|
|
||||||
true -> remove_recursive(Path, Options)
|
|
||||||
end.
|
|
||||||
|
|
||||||
-spec remove_recursive(path(), Options::list()) -> ok.
|
|
||||||
|
-spec remove_recursive(file:name(), Options::list()) -> ok | {error, Reason::term()}.
|
||||||
remove_recursive(Path, Options) ->
|
remove_recursive(Path, Options) ->
|
||||||
case filelib:is_dir(Path) of
|
case is_dir(Path) of
|
||||||
false ->
|
false ->
|
||||||
file:delete(Path);
|
file:delete(Path);
|
||||||
true ->
|
true ->
|
||||||
lists:foreach(fun(ChildPath) ->
|
lists:foreach(fun(ChildPath) ->
|
||||||
remove_recursive(ChildPath, Options)
|
remove_recursive(ChildPath, Options)
|
||||||
end, filelib:wildcard(filename:join(Path, "*"))),
|
end, sub_files(Path)),
|
||||||
ok = file:del_dir(Path)
|
file:del_dir(Path)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec tmp() -> path().
|
-spec tmp() -> file:name().
|
||||||
tmp() ->
|
tmp() ->
|
||||||
case erlang:system_info(system_architecture) of
|
case erlang:system_info(system_architecture) of
|
||||||
"win32" ->
|
"win32" ->
|
||||||
"./tmp";
|
case os:getenv("TEMP") of
|
||||||
|
false -> "./tmp";
|
||||||
|
Val -> Val
|
||||||
|
end;
|
||||||
_SysArch ->
|
_SysArch ->
|
||||||
"/tmp"
|
case os:getenv("TMPDIR") of
|
||||||
|
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.
|
||||||
-spec copy_subfiles(path(), path(), [option()]) -> ok.
|
-spec copy_subfiles(file:name(), file:name(), [option()]) -> {error, Reason::term()} | ok.
|
||||||
copy_subfiles(From, To, Options) ->
|
copy_subfiles(From, To, Options) ->
|
||||||
Fun =
|
Fun =
|
||||||
fun(ChildFrom) ->
|
fun(ChildFrom) ->
|
||||||
ChildTo = filename:join([To, filename:basename(ChildFrom)]),
|
ChildTo = filename:join([To, filename:basename(ChildFrom)]),
|
||||||
copy(ChildFrom, ChildTo, Options)
|
copy(ChildFrom, ChildTo, Options)
|
||||||
end,
|
end,
|
||||||
lists:foreach(Fun, filelib:wildcard(filename:join(From, "*"))).
|
lists:foreach(Fun, sub_files(From)).
|
||||||
|
|
||||||
-spec ec_file_copy(path(), path()) -> ok.
|
-spec make_dir_if_dir(file:name()) -> ok | {error, Reason::term()}.
|
||||||
ec_file_copy(From, To) ->
|
|
||||||
{ok, _} = file:copy(From, To),
|
|
||||||
{ok, FileInfo} = file:read_file_info(From),
|
|
||||||
ok = file:write_file_info(To, FileInfo).
|
|
||||||
|
|
||||||
-spec make_dir_if_dir(path()) -> ok.
|
|
||||||
make_dir_if_dir(File) ->
|
make_dir_if_dir(File) ->
|
||||||
case filelib:is_dir(File) of
|
case is_dir(File) of
|
||||||
true -> ok;
|
true -> ok;
|
||||||
false -> ok = mkdir_path(File)
|
false -> mkdir_path(File)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% @doc convert a list of integers into hex.
|
%% @doc convert a list of integers into hex.
|
||||||
|
@ -289,72 +371,7 @@ hex0(14) -> $e;
|
||||||
hex0(15) -> $f;
|
hex0(15) -> $f;
|
||||||
hex0(I) -> $0 + I.
|
hex0(I) -> $0 + I.
|
||||||
|
|
||||||
%%%===================================================================
|
|
||||||
%%% Test Functions
|
|
||||||
%%%===================================================================
|
|
||||||
|
|
||||||
-ifndef(NOTEST).
|
sub_files(From) ->
|
||||||
-include_lib("eunit/include/eunit.hrl").
|
{ok, SubFiles} = file:list_dir(From),
|
||||||
|
[filename:join(From, SubFile) || SubFile <- SubFiles].
|
||||||
setup_test() ->
|
|
||||||
case filelib:is_dir("/tmp/ec_file") of
|
|
||||||
true ->
|
|
||||||
remove("/tmp/ec_file", [recursive]);
|
|
||||||
false ->
|
|
||||||
ok
|
|
||||||
end,
|
|
||||||
mkdir_path("/tmp/ec_file/dir"),
|
|
||||||
?assertMatch(false, is_symlink("/tmp/ec_file/dir")),
|
|
||||||
?assertMatch(true, filelib:is_dir("/tmp/ec_file/dir")).
|
|
||||||
|
|
||||||
|
|
||||||
md5sum_test() ->
|
|
||||||
?assertMatch("cfcd208495d565ef66e7dff9f98764da", md5sum("0")).
|
|
||||||
|
|
||||||
file_test() ->
|
|
||||||
TermFile = "/tmp/ec_file/dir/file.term",
|
|
||||||
TermFileCopy = "/tmp/ec_file/dircopy/file.term",
|
|
||||||
write_term(TermFile, "term"),
|
|
||||||
?assertMatch("term", consult(TermFile)),
|
|
||||||
?assertMatch(<<"\"term\". ">>, read(TermFile)),
|
|
||||||
copy(filename:dirname(TermFile),
|
|
||||||
filename:dirname(TermFileCopy),
|
|
||||||
[recursive]),
|
|
||||||
?assertMatch("term", consult(TermFileCopy)).
|
|
||||||
|
|
||||||
teardown_test() ->
|
|
||||||
remove("/tmp/ec_file", [recursive]),
|
|
||||||
?assertMatch(false, filelib:is_dir("/tmp/ec_file")).
|
|
||||||
|
|
||||||
setup_base_and_target() ->
|
|
||||||
{ok, BaseDir} = ewl_file:create_tmp_dir("/tmp"),
|
|
||||||
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}}.
|
|
||||||
|
|
||||||
find_test() ->
|
|
||||||
%% Create a directory in /tmp for the test. Clean everything afterwards
|
|
||||||
|
|
||||||
{setup,
|
|
||||||
fun setup_base_and_target/0,
|
|
||||||
fun ({BaseDir, _, _}) ->
|
|
||||||
ewl_file:delete_dir(BaseDir)
|
|
||||||
end,
|
|
||||||
fun ({BaseDir, _, {Name1, Name2, Name3, _}}) ->
|
|
||||||
?assertMatch([Name2,
|
|
||||||
Name3,
|
|
||||||
Name1],
|
|
||||||
ewl_file:find(BaseDir, "file[a-z]+\$"))
|
|
||||||
end}.
|
|
||||||
|
|
||||||
-endif.
|
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
|
%%% vi:ts=4 sw=4 et
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
%%% @author Eric Merritt <ericbmerritt@gmail.com>
|
%%% @author Eric Merritt <ericbmerritt@gmail.com>
|
||||||
%%% @copyright 2011 Erlware, LLC.
|
%%% @copyright 2011 Erlware, LLC.
|
||||||
%%% @doc
|
%%% @doc
|
||||||
%%% This provides an implementation of the type ec_dictionary using
|
%%% This provides an implementation of the type ec_dictionary using
|
||||||
%%% gb_trees as a backin
|
%%% gb_trees as a backin
|
||||||
|
%%% see ec_dictionary
|
||||||
|
%%% see gb_trees
|
||||||
%%% @end
|
%%% @end
|
||||||
%%% @see ec_dictionary
|
|
||||||
%%% @see gb_trees
|
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
-module(ec_gb_trees).
|
-module(ec_gb_trees).
|
||||||
|
|
||||||
|
@ -14,27 +15,16 @@
|
||||||
|
|
||||||
%% API
|
%% API
|
||||||
-export([new/0,
|
-export([new/0,
|
||||||
has_key/2,
|
has_key/2,
|
||||||
get/2,
|
get/2,
|
||||||
get/3,
|
get/3,
|
||||||
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]).
|
||||||
|
|
||||||
-export_type([dictionary/2]).
|
|
||||||
|
|
||||||
%%%===================================================================
|
|
||||||
%%% Types
|
|
||||||
%%%===================================================================
|
|
||||||
-opaque dictionary(K, V) :: {non_neg_integer(), ec_gb_tree_node(K, V)}.
|
|
||||||
|
|
||||||
-type ec_gb_tree_node(K, V) :: 'nil' | {K, V,
|
|
||||||
ec_gb_tree_node(K, V),
|
|
||||||
ec_gb_tree_node(K, V)}.
|
|
||||||
|
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
%%% API
|
%%% API
|
||||||
|
@ -46,7 +36,7 @@
|
||||||
%% same implementation is created and returned.
|
%% same implementation is created and returned.
|
||||||
%%
|
%%
|
||||||
%% @param ModuleName|Object The module name or existing dictionary object.
|
%% @param ModuleName|Object The module name or existing dictionary object.
|
||||||
-spec new() -> dictionary(_K, _V).
|
-spec new() -> gb_trees:tree(_K, _V).
|
||||||
new() ->
|
new() ->
|
||||||
gb_trees:empty().
|
gb_trees:empty().
|
||||||
|
|
||||||
|
@ -54,13 +44,13 @@ new() ->
|
||||||
%%
|
%%
|
||||||
%% @param Object The dictory object to check
|
%% @param Object The dictory object to check
|
||||||
%% @param Key The key to check the dictionary for
|
%% @param Key The key to check the dictionary for
|
||||||
-spec has_key(ec_dictionary:key(K), Object::dictionary(K, _V)) -> boolean().
|
-spec has_key(ec_dictionary:key(K), Object::gb_trees:tree(K, _V)) -> boolean().
|
||||||
has_key(Key, Data) ->
|
has_key(Key, Data) ->
|
||||||
case gb_trees:lookup(Key, Data) of
|
case gb_trees:lookup(Key, Data) of
|
||||||
{value, _Val} ->
|
{value, _Val} ->
|
||||||
true;
|
true;
|
||||||
none ->
|
none ->
|
||||||
false
|
false
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% @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
|
||||||
|
@ -68,27 +58,27 @@ has_key(Key, Data) ->
|
||||||
%%
|
%%
|
||||||
%% @param Object The dictionary object to return the value from
|
%% @param Object 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
|
%% when the key does not exist @throws not_found
|
||||||
-spec get(ec_dictionary:key(K), Object::dictionary(K, V)) ->
|
-spec get(ec_dictionary:key(K), Object::gb_trees:tree(K, V)) ->
|
||||||
ec_dictionary:value(V).
|
ec_dictionary:value(V).
|
||||||
get(Key, Data) ->
|
get(Key, Data) ->
|
||||||
case gb_trees:lookup(Key, Data) of
|
case gb_trees:lookup(Key, Data) of
|
||||||
{value, Value} ->
|
{value, Value} ->
|
||||||
Value;
|
Value;
|
||||||
none ->
|
none ->
|
||||||
throw(not_found)
|
throw(not_found)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec get(ec_dictionary:key(K),
|
-spec get(ec_dictionary:key(K),
|
||||||
ec_dictionary:value(V),
|
ec_dictionary:value(V),
|
||||||
Object::dictionary(K, V)) ->
|
Object::gb_trees:tree(K, V)) ->
|
||||||
ec_dictionary:value(V).
|
ec_dictionary:value(V).
|
||||||
get(Key, Default, Data) ->
|
get(Key, Default, Data) ->
|
||||||
case gb_trees:lookup(Key, Data) of
|
case gb_trees:lookup(Key, Data) of
|
||||||
{value, Value} ->
|
{value, Value} ->
|
||||||
Value;
|
Value;
|
||||||
none ->
|
none ->
|
||||||
Default
|
Default
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% @doc add a new value to the existing dictionary. Return a new
|
%% @doc add a new value to the existing dictionary. Return a new
|
||||||
|
@ -98,8 +88,8 @@ get(Key, Default, Data) ->
|
||||||
%% @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(ec_dictionary:key(K), ec_dictionary:value(V),
|
-spec add(ec_dictionary:key(K), ec_dictionary:value(V),
|
||||||
Object::dictionary(K, V)) ->
|
Object::gb_trees:tree(K, V)) ->
|
||||||
dictionary(K, V).
|
gb_trees:tree(K, V).
|
||||||
add(Key, Value, Data) ->
|
add(Key, Value, Data) ->
|
||||||
gb_trees:enter(Key, Value, Data).
|
gb_trees:enter(Key, Value, Data).
|
||||||
|
|
||||||
|
@ -108,8 +98,8 @@ add(Key, Value, Data) ->
|
||||||
%%
|
%%
|
||||||
%% @param Object the dictionary object to remove the value from
|
%% @param Object the dictionary object to remove the value from
|
||||||
%% @param Key the key of the key/value pair to remove
|
%% @param Key the key of the key/value pair to remove
|
||||||
-spec remove(ec_dictionary:key(K), Object::dictionary(K, V)) ->
|
-spec remove(ec_dictionary:key(K), Object::gb_trees:tree(K, V)) ->
|
||||||
dictionary(K, V).
|
gb_trees:tree(K, V).
|
||||||
remove(Key, Data) ->
|
remove(Key, Data) ->
|
||||||
gb_trees:delete_any(Key, Data).
|
gb_trees:delete_any(Key, Data).
|
||||||
|
|
||||||
|
@ -117,107 +107,31 @@ remove(Key, Data) ->
|
||||||
%%
|
%%
|
||||||
%% @param Object the dictionary object to check
|
%% @param Object the dictionary object to check
|
||||||
%% @param Value The value to check if exists
|
%% @param Value The value to check if exists
|
||||||
-spec has_value(ec_dictionary:value(V), Object::dictionary(_K, V)) -> boolean().
|
-spec has_value(ec_dictionary:value(V), Object::gb_trees:tree(_K, V)) -> boolean().
|
||||||
has_value(Value, Data) ->
|
has_value(Value, Data) ->
|
||||||
lists:member(Value, gb_trees:values(Data)).
|
lists:member(Value, gb_trees:values(Data)).
|
||||||
|
|
||||||
%% @doc return the current number of key value pairs in the dictionary
|
%% @doc return the current number of key value pairs in the dictionary
|
||||||
%%
|
%%
|
||||||
%% @param Object the object return the size for.
|
%% @param Object the object return the size for.
|
||||||
-spec size(Object::dictionary(_K, _V)) -> integer().
|
-spec size(Object::gb_trees:tree(_K, _V)) -> non_neg_integer().
|
||||||
size(Data) ->
|
size(Data) ->
|
||||||
gb_trees:size(Data).
|
gb_trees:size(Data).
|
||||||
|
|
||||||
-spec to_list(dictionary(K, V)) -> [{ec_dictionary:key(K),
|
-spec to_list(gb_trees:tree(K, V)) -> [{ec_dictionary:key(K),
|
||||||
ec_dictionary:value(V)}].
|
ec_dictionary:value(V)}].
|
||||||
to_list(Data) ->
|
to_list(Data) ->
|
||||||
gb_trees:to_list(Data).
|
gb_trees:to_list(Data).
|
||||||
|
|
||||||
-spec from_list([{ec_dictionary:key(K), ec_dictionary:value(V)}]) ->
|
-spec from_list([{ec_dictionary:key(K), ec_dictionary:value(V)}]) ->
|
||||||
dictionary(K, V).
|
gb_trees:tree(K, V).
|
||||||
from_list(List) when is_list(List) ->
|
from_list(List) when is_list(List) ->
|
||||||
lists:foldl(fun({Key, Value}, Dict) ->
|
lists:foldl(fun({Key, Value}, Dict) ->
|
||||||
gb_trees:enter(Key, Value, Dict)
|
gb_trees:enter(Key, Value, Dict)
|
||||||
end,
|
end,
|
||||||
gb_trees:empty(),
|
gb_trees:empty(),
|
||||||
List).
|
List).
|
||||||
|
|
||||||
-spec keys(dictionary(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
|
|
||||||
%%%===================================================================
|
|
||||||
|
|
||||||
|
|
||||||
-ifndef(NOTEST).
|
|
||||||
-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.
|
|
||||||
|
|
107
src/ec_git_vsn.erl
Normal file
107
src/ec_git_vsn.erl
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
%%% vi:ts=4 sw=4 et
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @author Eric Merritt <ericbmerritt@gmail.com>
|
||||||
|
%%% @copyright 2011 Erlware, LLC.
|
||||||
|
%%% @doc
|
||||||
|
%%% This provides an implementation of the ec_vsn for git. That is
|
||||||
|
%%% it is capable of returning a semver for a git repository
|
||||||
|
%%% see ec_vsn
|
||||||
|
%%% see ec_semver
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(ec_git_vsn).
|
||||||
|
|
||||||
|
-behaviour(ec_vsn).
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([new/0,
|
||||||
|
vsn/1]).
|
||||||
|
|
||||||
|
-ifdef(TEST).
|
||||||
|
-export([parse_tags/1,
|
||||||
|
get_patch_count/1,
|
||||||
|
collect_default_refcount/1
|
||||||
|
]).
|
||||||
|
-endif.
|
||||||
|
|
||||||
|
-export_type([t/0]).
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% Types
|
||||||
|
%%%===================================================================
|
||||||
|
%% This should be opaque, but that kills dialyzer so for now we export it
|
||||||
|
%% however you should not rely on the internal representation here
|
||||||
|
-type t() :: {}.
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% API
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
-spec new() -> t().
|
||||||
|
new() ->
|
||||||
|
{}.
|
||||||
|
|
||||||
|
-spec vsn(t()|string()) -> {ok, string()} | {error, Reason::any()}.
|
||||||
|
vsn(Data) ->
|
||||||
|
{Vsn, RawRef, RawCount} = collect_default_refcount(Data),
|
||||||
|
{ok, build_vsn_string(Vsn, RawRef, RawCount)}.
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% Internal Functions
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
collect_default_refcount(Data) ->
|
||||||
|
%% Get the tag timestamp and minimal ref from the system. The
|
||||||
|
%% timestamp is really important from an ordering perspective.
|
||||||
|
RawRef = os:cmd("git log -n 1 --pretty=format:'%h\n' "),
|
||||||
|
|
||||||
|
{Tag, TagVsn} = parse_tags(Data),
|
||||||
|
RawCount =
|
||||||
|
case Tag of
|
||||||
|
undefined ->
|
||||||
|
os:cmd("git rev-list --count HEAD");
|
||||||
|
_ ->
|
||||||
|
get_patch_count(Tag)
|
||||||
|
end,
|
||||||
|
{TagVsn, RawRef, RawCount}.
|
||||||
|
|
||||||
|
build_vsn_string(Vsn, RawRef, RawCount) ->
|
||||||
|
%% Cleanup the tag and the Ref information. Basically leading 'v's and
|
||||||
|
%% whitespace needs to go away.
|
||||||
|
RefTag = [".ref", re:replace(RawRef, "\\s", "", [global])],
|
||||||
|
Count = erlang:iolist_to_binary(re:replace(RawCount, "\\s", "", [global])),
|
||||||
|
|
||||||
|
%% Create the valid [semver](http://semver.org) version from the tag
|
||||||
|
case Count of
|
||||||
|
<<"0">> ->
|
||||||
|
erlang:binary_to_list(erlang:iolist_to_binary(Vsn));
|
||||||
|
_ ->
|
||||||
|
erlang:binary_to_list(erlang:iolist_to_binary([Vsn, "+build.",
|
||||||
|
Count, RefTag]))
|
||||||
|
end.
|
||||||
|
|
||||||
|
get_patch_count(RawRef) ->
|
||||||
|
Ref = re:replace(RawRef, "\\s", "", [global]),
|
||||||
|
Cmd = io_lib:format("git rev-list --count ~ts..HEAD",
|
||||||
|
[Ref]),
|
||||||
|
case os:cmd(Cmd) of
|
||||||
|
"fatal: " ++ _ ->
|
||||||
|
0;
|
||||||
|
Count ->
|
||||||
|
Count
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec parse_tags(t()|string()) -> {string()|undefined, ec_semver:version_string()}.
|
||||||
|
parse_tags({}) ->
|
||||||
|
parse_tags("");
|
||||||
|
parse_tags(Pattern) ->
|
||||||
|
Cmd = io_lib:format("git describe --abbrev=0 --tags --match \"~ts*\"", [Pattern]),
|
||||||
|
Tag = os:cmd(Cmd),
|
||||||
|
case Tag of
|
||||||
|
"fatal: " ++ _ ->
|
||||||
|
{undefined, ""};
|
||||||
|
_ ->
|
||||||
|
Vsn = string:slice(Tag, string:length(Pattern)),
|
||||||
|
Vsn1 = string:trim(string:trim(Vsn, leading, "v"), trailing, "\n"),
|
||||||
|
{Tag, Vsn1}
|
||||||
|
end.
|
186
src/ec_lists.erl
186
src/ec_lists.erl
|
@ -1,3 +1,4 @@
|
||||||
|
%%% vi:ts=4 sw=4 et
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
%%% @copyright (C) 2011, Erlware LLC
|
%%% @copyright (C) 2011, Erlware LLC
|
||||||
%%% @doc
|
%%% @doc
|
||||||
|
@ -23,7 +24,7 @@
|
||||||
%% the third value is the element passed to the function. The purpose
|
%% the third value is the element passed to the function. The purpose
|
||||||
%% of this is to allow a list to be searched where some internal state
|
%% of this is to allow a list to be searched where some internal state
|
||||||
%% is important while the input element is not.
|
%% is important while the input element is not.
|
||||||
-spec search(fun(), list()) -> {ok, Result::term(), Element::term()}.
|
-spec search(fun(), list()) -> {ok, Result::term(), Element::term()} | not_found.
|
||||||
search(Fun, [H|T]) ->
|
search(Fun, [H|T]) ->
|
||||||
case Fun(H) of
|
case Fun(H) of
|
||||||
{ok, Value} ->
|
{ok, Value} ->
|
||||||
|
@ -51,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 returend. If processing reaches the end of the list and
|
%% value is returned. 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().
|
||||||
|
@ -62,184 +63,3 @@ fetch(Fun, List) when is_list(List), is_function(Fun) ->
|
||||||
error ->
|
error ->
|
||||||
throw(not_found)
|
throw(not_found)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%%%===================================================================
|
|
||||||
%%% Test Functions
|
|
||||||
%%%===================================================================
|
|
||||||
|
|
||||||
-ifndef(NOTEST).
|
|
||||||
-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.
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
%%% vi:ts=4 sw=4 et
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
%%% @author Eric Merritt <ericbmerritt@gmail.com>
|
%%% @author Eric Merritt <ericbmerritt@gmail.com>
|
||||||
%%% @copyright 2011 Erlware, LLC.
|
%%% @copyright 2011 Erlware, LLC.
|
||||||
|
@ -5,9 +6,9 @@
|
||||||
%%% This provides an implementation of the ec_dictionary type using
|
%%% This provides an implementation of the ec_dictionary type using
|
||||||
%%% erlang orddicts as a base. The function documentation for
|
%%% erlang orddicts as a base. The function documentation for
|
||||||
%%% ec_dictionary applies here as well.
|
%%% ec_dictionary applies here as well.
|
||||||
|
%%% see ec_dictionary
|
||||||
|
%%% see orddict
|
||||||
%%% @end
|
%%% @end
|
||||||
%%% @see ec_dictionary
|
|
||||||
%%% @see orddict
|
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
-module(ec_orddict).
|
-module(ec_orddict).
|
||||||
|
|
||||||
|
@ -31,7 +32,9 @@
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
%%% Types
|
%%% Types
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
-opaque dictionary(K, V) :: [{K, V}].
|
%% This should be opaque, but that kills dialyzer so for now we export it
|
||||||
|
%% however you should not rely on the internal representation here
|
||||||
|
-type dictionary(K, V) :: [{K, V}].
|
||||||
|
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
%%% API
|
%%% API
|
||||||
|
@ -46,59 +49,59 @@ has_key(Key, Data) ->
|
||||||
orddict:is_key(Key, Data).
|
orddict:is_key(Key, Data).
|
||||||
|
|
||||||
-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 orddict:find(Key, Data) of
|
case orddict:find(Key, Data) of
|
||||||
{ok, Value} ->
|
{ok, Value} ->
|
||||||
Value;
|
Value;
|
||||||
error ->
|
error ->
|
||||||
throw(not_found)
|
throw(not_found)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec get(ec_dictionary:key(K),
|
-spec get(ec_dictionary:key(K),
|
||||||
Default::ec_dictionary:value(V),
|
Default::ec_dictionary:value(V),
|
||||||
Object::dictionary(K, V)) ->
|
Object::dictionary(K, V)) ->
|
||||||
ec_dictionary:value(V).
|
ec_dictionary:value(V).
|
||||||
get(Key, Default, Data) ->
|
get(Key, Default, Data) ->
|
||||||
case orddict:find(Key, Data) of
|
case orddict:find(Key, Data) of
|
||||||
{ok, Value} ->
|
{ok, Value} ->
|
||||||
Value;
|
Value;
|
||||||
error ->
|
error ->
|
||||||
Default
|
Default
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec add(ec_dictionary:key(K), ec_dictionary:value(V),
|
-spec add(ec_dictionary:key(K), ec_dictionary:value(V),
|
||||||
Object::dictionary(K, V)) ->
|
Object::dictionary(K, V)) ->
|
||||||
dictionary(K, V).
|
dictionary(K, V).
|
||||||
add(Key, Value, Data) ->
|
add(Key, Value, Data) ->
|
||||||
orddict:store(Key, Value, Data).
|
orddict:store(Key, Value, Data).
|
||||||
|
|
||||||
-spec remove(ec_dictionary:key(K), Object::dictionary(K, V)) ->
|
-spec remove(ec_dictionary:key(K), Object::dictionary(K, V)) ->
|
||||||
dictionary(K, V).
|
dictionary(K, V).
|
||||||
remove(Key, Data) ->
|
remove(Key, Data) ->
|
||||||
orddict:erase(Key, Data).
|
orddict:erase(Key, Data).
|
||||||
|
|
||||||
-spec has_value(ec_dictionary:value(V), Object::dictionary(_K, V)) -> boolean().
|
-spec has_value(ec_dictionary:value(V), Object::dictionary(_K, V)) -> boolean().
|
||||||
has_value(Value, Data) ->
|
has_value(Value, Data) ->
|
||||||
orddict:fold(fun(_, NValue, _) when NValue == Value ->
|
orddict:fold(fun(_, NValue, _) when NValue == Value ->
|
||||||
true;
|
true;
|
||||||
(_, _, Acc) ->
|
(_, _, Acc) ->
|
||||||
Acc
|
Acc
|
||||||
end,
|
end,
|
||||||
false,
|
false,
|
||||||
Data).
|
Data).
|
||||||
|
|
||||||
-spec size(Object::dictionary(_K, _V)) -> integer().
|
-spec size(Object::dictionary(_K, _V)) -> non_neg_integer().
|
||||||
size(Data) ->
|
size(Data) ->
|
||||||
orddict:size(Data).
|
orddict:size(Data).
|
||||||
|
|
||||||
-spec to_list(dictionary(K, V)) ->
|
-spec to_list(dictionary(K, V)) ->
|
||||||
[{ec_dictionary:key(K), ec_dictionary:value(V)}].
|
[{ec_dictionary:key(K), ec_dictionary:value(V)}].
|
||||||
to_list(Data) ->
|
to_list(Data) ->
|
||||||
orddict:to_list(Data).
|
orddict:to_list(Data).
|
||||||
|
|
||||||
-spec from_list([{ec_dictionary:key(K), ec_dictionary:value(V)}]) ->
|
-spec from_list([{ec_dictionary:key(K), ec_dictionary:value(V)}]) ->
|
||||||
dictionary(K, V).
|
dictionary(K, V).
|
||||||
from_list(List) when is_list(List) ->
|
from_list(List) when is_list(List) ->
|
||||||
orddict:from_list(List).
|
orddict:from_list(List).
|
||||||
|
|
||||||
|
|
1154
src/ec_plists.erl
1154
src/ec_plists.erl
File diff suppressed because it is too large
Load diff
|
@ -1,3 +1,4 @@
|
||||||
|
%%% vi:ts=4 sw=4 et
|
||||||
%%% Copyright (c) 2008 Robert Virding. All rights reserved.
|
%%% Copyright (c) 2008 Robert Virding. All rights reserved.
|
||||||
%%%
|
%%%
|
||||||
%%% Redistribution and use in source and binary forms, with or without
|
%%% Redistribution and use in source and binary forms, with or without
|
||||||
|
@ -31,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 implents exactly the same interface as the module
|
%%% This module implements 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
|
||||||
|
@ -51,8 +52,8 @@
|
||||||
%%% l/rbalance, the colour, in store etc. is actually slower than not
|
%%% l/rbalance, the colour, in store etc. is actually slower than not
|
||||||
%%% doing it. Measured.
|
%%% doing it. Measured.
|
||||||
%%%
|
%%%
|
||||||
|
%%% see ec_dictionary
|
||||||
%%% @end
|
%%% @end
|
||||||
%%% @see ec_dictionary
|
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
-module(ec_rbdict).
|
-module(ec_rbdict).
|
||||||
|
|
||||||
|
@ -68,12 +69,13 @@
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
%%% Types
|
%%% Types
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
|
%% This should be opaque, but that kills dialyzer so for now we export it
|
||||||
-opaque dictionary(K, V) :: empty | {color(),
|
%% however you should not rely on the internal representation here
|
||||||
dictionary(K, V),
|
-type dictionary(K, V) :: empty | {color(),
|
||||||
ec_dictionary:key(K),
|
dictionary(K, V),
|
||||||
ec_dictionary:value(V),
|
ec_dictionary:key(K),
|
||||||
dictionary(K, V)}.
|
ec_dictionary:value(V),
|
||||||
|
dictionary(K, V)}.
|
||||||
|
|
||||||
-type color() :: r | b.
|
-type color() :: r | b.
|
||||||
|
|
||||||
|
@ -116,7 +118,7 @@ get(K, Default, {_, _, K1, _, Right}) when K > K1 ->
|
||||||
get(_, _, {_, _, _, Val, _}) ->
|
get(_, _, {_, _, _, Val, _}) ->
|
||||||
Val.
|
Val.
|
||||||
|
|
||||||
-spec add(ec_dicitonary:key(K), ec_dictionary:value(V),
|
-spec add(ec_dictionary:key(K), ec_dictionary:value(V),
|
||||||
dictionary(K, V)) -> dictionary(K, V).
|
dictionary(K, V)) -> dictionary(K, V).
|
||||||
add(Key, Value, Dict) ->
|
add(Key, Value, Dict) ->
|
||||||
{_, L, K1, V1, R} = add1(Key, Value, Dict),
|
{_, L, K1, V1, R} = add1(Key, Value, Dict),
|
||||||
|
@ -133,17 +135,17 @@ has_value(Value, Dict) ->
|
||||||
end,
|
end,
|
||||||
false, Dict).
|
false, Dict).
|
||||||
|
|
||||||
-spec size(dictionary(_K, _V)) -> integer().
|
-spec size(dictionary(_K, _V)) -> non_neg_integer().
|
||||||
size(T) ->
|
size(T) ->
|
||||||
size1(T).
|
size1(T).
|
||||||
|
|
||||||
-spec to_list(dictionary(K, V)) ->
|
-spec to_list(dictionary(K, V)) ->
|
||||||
[{ec_dictionary:key(K), ec_dictionary:value(V)}].
|
[{ec_dictionary:key(K), ec_dictionary:value(V)}].
|
||||||
to_list(T) ->
|
to_list(T) ->
|
||||||
to_list(T, []).
|
to_list(T, []).
|
||||||
|
|
||||||
-spec from_list([{ec_dictionary:key(K), ec_dictionary:value(V)}]) ->
|
-spec from_list([{ec_dictionary:key(K), ec_dictionary:value(V)}]) ->
|
||||||
dictionary(K, V).
|
dictionary(K, V).
|
||||||
from_list(L) ->
|
from_list(L) ->
|
||||||
lists:foldl(fun ({K, V}, D) ->
|
lists:foldl(fun ({K, V}, D) ->
|
||||||
add(K, V, D)
|
add(K, V, D)
|
||||||
|
@ -158,7 +160,7 @@ keys(Dict) ->
|
||||||
%%% Enternal functions
|
%%% Enternal functions
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
-spec keys(dictionary(K, _V), [ec_dictionary:key(K)]) ->
|
-spec keys(dictionary(K, _V), [ec_dictionary:key(K)]) ->
|
||||||
[ec_dictionary:key(K)].
|
[ec_dictionary:key(K)].
|
||||||
keys(empty, Tail) ->
|
keys(empty, Tail) ->
|
||||||
Tail;
|
Tail;
|
||||||
keys({_, L, K, _, R}, Tail) ->
|
keys({_, L, K, _, R}, Tail) ->
|
||||||
|
@ -166,7 +168,7 @@ keys({_, L, K, _, R}, Tail) ->
|
||||||
|
|
||||||
|
|
||||||
-spec erase_aux(ec_dictionary:key(K), dictionary(K, V)) ->
|
-spec erase_aux(ec_dictionary:key(K), dictionary(K, V)) ->
|
||||||
{dictionary(K, V), boolean()}.
|
{dictionary(K, V), boolean()}.
|
||||||
erase_aux(_, empty) ->
|
erase_aux(_, empty) ->
|
||||||
{empty, false};
|
{empty, false};
|
||||||
erase_aux(K, {b, A, Xk, Xv, B}) ->
|
erase_aux(K, {b, A, Xk, Xv, B}) ->
|
||||||
|
@ -227,7 +229,7 @@ erase_aux(K, {r, A, Xk, Xv, B}) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec erase_min(dictionary(K, V)) ->
|
-spec erase_min(dictionary(K, V)) ->
|
||||||
{dictionary(K, V), {ec_dictionary:key(K), ec_dictionary:value(V)}, boolean}.
|
{dictionary(K, V), {ec_dictionary:key(K), ec_dictionary:value(V)}, boolean()}.
|
||||||
erase_min({b, empty, Xk, Xv, empty}) ->
|
erase_min({b, empty, Xk, Xv, empty}) ->
|
||||||
{empty, {Xk, Xv}, true};
|
{empty, {Xk, Xv}, true};
|
||||||
erase_min({b, empty, Xk, Xv, {r, A, Yk, Yv, B}}) ->
|
erase_min({b, empty, Xk, Xv, {r, A, Yk, Yv, B}}) ->
|
||||||
|
@ -239,15 +241,15 @@ erase_min({r, empty, Xk, Xv, A}) ->
|
||||||
erase_min({b, A, Xk, Xv, B}) ->
|
erase_min({b, A, Xk, Xv, B}) ->
|
||||||
{A1, Min, Dec} = erase_min(A),
|
{A1, Min, Dec} = erase_min(A),
|
||||||
if Dec ->
|
if Dec ->
|
||||||
{T, Dec1} = unbalright(b, A1, Xk, Xv, B),
|
{T, Dec1} = unbalright(b, A1, Xk, Xv, B),
|
||||||
{T, Min, Dec1};
|
{T, Min, Dec1};
|
||||||
true -> {{b, A1, Xk, Xv, B}, Min, false}
|
true -> {{b, A1, Xk, Xv, B}, Min, false}
|
||||||
end;
|
end;
|
||||||
erase_min({r, A, Xk, Xv, B}) ->
|
erase_min({r, A, Xk, Xv, B}) ->
|
||||||
{A1, Min, Dec} = erase_min(A),
|
{A1, Min, Dec} = erase_min(A),
|
||||||
if Dec ->
|
if Dec ->
|
||||||
{T, Dec1} = unbalright(r, A1, Xk, Xv, B),
|
{T, Dec1} = unbalright(r, A1, Xk, Xv, B),
|
||||||
{T, Min, Dec1};
|
{T, Min, Dec1};
|
||||||
true -> {{r, A1, Xk, Xv, B}, Min, false}
|
true -> {{r, A1, Xk, Xv, B}, Min, false}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
@ -274,7 +276,8 @@ unbalright(b, A, Xk, Xv,
|
||||||
D},
|
D},
|
||||||
false}.
|
false}.
|
||||||
|
|
||||||
-spec fold(fun(), dictionary(K, V), dictionary(K, V)) -> dictionary(K, V).
|
-spec fold(fun((ec_dictionary:key(K), ec_dictionary:value(V), any()) -> any()),
|
||||||
|
any(), dictionary(K, V)) -> any().
|
||||||
fold(_, Acc, empty) -> Acc;
|
fold(_, Acc, empty) -> Acc;
|
||||||
fold(F, Acc, {_, A, Xk, Xv, B}) ->
|
fold(F, Acc, {_, A, Xk, Xv, B}) ->
|
||||||
fold(F, F(Xk, Xv, fold(F, Acc, B)), A).
|
fold(F, F(Xk, Xv, fold(F, Acc, B)), A).
|
||||||
|
@ -293,11 +296,11 @@ 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 afer (possibly) adding a node to the left/right.
|
%% Balance a tree after (possibly) adding a node to the left/right.
|
||||||
-spec lbalance(color(), dictionary(K, V),
|
-spec lbalance(color(), dictionary(K, V),
|
||||||
ec_dictinary:key(K), ec_dictionary:value(V),
|
ec_dictionary:key(K), ec_dictionary:value(V),
|
||||||
dictionary(K, V)) ->
|
dictionary(K, V)) ->
|
||||||
dictionary(K, V).
|
dictionary(K, V).
|
||||||
lbalance(b, {r, {r, A, Xk, Xv, B}, Yk, Yv, C}, Zk, Zv,
|
lbalance(b, {r, {r, A, Xk, Xv, B}, Yk, Yv, C}, Zk, Zv,
|
||||||
D) ->
|
D) ->
|
||||||
{r, {b, A, Xk, Xv, B}, Yk, Yv, {b, C, Zk, Zv, D}};
|
{r, {b, A, Xk, Xv, B}, Yk, Yv, {b, C, Zk, Zv, D}};
|
||||||
|
@ -307,9 +310,9 @@ lbalance(b, {r, A, Xk, Xv, {r, B, Yk, Yv, C}}, Zk, Zv,
|
||||||
lbalance(C, A, Xk, Xv, B) -> {C, A, Xk, Xv, B}.
|
lbalance(C, A, Xk, Xv, B) -> {C, A, Xk, Xv, B}.
|
||||||
|
|
||||||
-spec rbalance(color(), dictionary(K, V),
|
-spec rbalance(color(), dictionary(K, V),
|
||||||
ec_dictinary:key(K), ec_dictionary:value(V),
|
ec_dictionary:key(K), ec_dictionary:value(V),
|
||||||
dictionary(K, V)) ->
|
dictionary(K, V)) ->
|
||||||
dictionary(K, V).
|
dictionary(K, V).
|
||||||
rbalance(b, A, Xk, Xv,
|
rbalance(b, A, Xk, Xv,
|
||||||
{r, {r, B, Yk, Yv, C}, Zk, Zv, D}) ->
|
{r, {r, B, Yk, Yv, C}, Zk, Zv, D}) ->
|
||||||
{r, {b, A, Xk, Xv, B}, Yk, Yv, {b, C, Zk, Zv, D}};
|
{r, {b, A, Xk, Xv, B}, Yk, Yv, {b, C, Zk, Zv, D}};
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
%%% vi:ts=4 sw=4 et
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
%%% @copyright (C) 2011, Erlware LLC
|
%%% @copyright (C) 2011, Erlware LLC
|
||||||
%%% @doc
|
%%% @doc
|
||||||
|
@ -7,113 +8,304 @@
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
-module(ec_semver).
|
-module(ec_semver).
|
||||||
|
|
||||||
-exports([
|
-export([parse/1,
|
||||||
compare/2
|
format/1,
|
||||||
]).
|
eql/2,
|
||||||
|
gt/2,
|
||||||
|
gte/2,
|
||||||
|
lt/2,
|
||||||
|
lte/2,
|
||||||
|
pes/2,
|
||||||
|
between/3]).
|
||||||
|
|
||||||
-export_type([
|
%% For internal use by the ec_semver_parser peg
|
||||||
semvar/0
|
-export([internal_parse_version/1]).
|
||||||
]).
|
|
||||||
|
-export_type([semver/0,
|
||||||
|
version_string/0,
|
||||||
|
any_version/0]).
|
||||||
|
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
%%% Public Types
|
%%% Public Types
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
|
|
||||||
-type semvar() :: string().
|
-type version_element() :: non_neg_integer() | binary().
|
||||||
-type parsed_semvar() :: {MajorVsn::string(),
|
|
||||||
MinorVsn::string(),
|
-type major_minor_patch_minpatch() ::
|
||||||
PatchVsn::string(),
|
version_element()
|
||||||
PathString::string()}.
|
| {version_element(), version_element()}
|
||||||
|
| {version_element(), version_element(), version_element()}
|
||||||
|
| {version_element(), version_element(),
|
||||||
|
version_element(), version_element()}.
|
||||||
|
|
||||||
|
-type alpha_part() :: integer() | binary() | string().
|
||||||
|
-type alpha_info() :: {PreRelease::[alpha_part()],
|
||||||
|
BuildVersion::[alpha_part()]}.
|
||||||
|
|
||||||
|
-type semver() :: {major_minor_patch_minpatch(), alpha_info()}.
|
||||||
|
|
||||||
|
-type version_string() :: string() | binary().
|
||||||
|
|
||||||
|
-type any_version() :: version_string() | semver().
|
||||||
|
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
%%% API
|
%%% API
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
|
|
||||||
%% @doc Is semver version string A bigger than version string B?
|
%% @doc parse a string or binary into a valid semver representation
|
||||||
%% <pre>
|
-spec parse(any_version()) -> semver().
|
||||||
%% Example: compare("3.2.5alpha", "3.10.6") returns: false
|
parse(Version) when erlang:is_list(Version) ->
|
||||||
%% </pre>
|
case ec_semver_parser:parse(Version) of
|
||||||
-spec compare(VsnA::string(), VsnB::string()) -> boolean().
|
{fail, _} ->
|
||||||
compare(VsnA, VsnB) ->
|
{erlang:iolist_to_binary(Version), {[],[]}};
|
||||||
compare_toks(tokens(VsnA),tokens(VsnB)).
|
Good ->
|
||||||
|
Good
|
||||||
|
end;
|
||||||
|
parse(Version) when erlang:is_binary(Version) ->
|
||||||
|
case ec_semver_parser:parse(Version) of
|
||||||
|
{fail, _} ->
|
||||||
|
{Version, {[],[]}};
|
||||||
|
Good ->
|
||||||
|
Good
|
||||||
|
end;
|
||||||
|
parse(Version) ->
|
||||||
|
Version.
|
||||||
|
|
||||||
|
-spec format(semver()) -> iolist().
|
||||||
|
format({Maj, {AlphaPart, BuildPart}})
|
||||||
|
when erlang:is_integer(Maj);
|
||||||
|
erlang:is_binary(Maj) ->
|
||||||
|
[format_version_part(Maj),
|
||||||
|
format_vsn_rest(<<"-">>, AlphaPart),
|
||||||
|
format_vsn_rest(<<"+">>, BuildPart)];
|
||||||
|
format({{Maj, Min}, {AlphaPart, BuildPart}}) ->
|
||||||
|
[format_version_part(Maj), ".",
|
||||||
|
format_version_part(Min),
|
||||||
|
format_vsn_rest(<<"-">>, AlphaPart),
|
||||||
|
format_vsn_rest(<<"+">>, BuildPart)];
|
||||||
|
format({{Maj, Min, Patch}, {AlphaPart, BuildPart}}) ->
|
||||||
|
[format_version_part(Maj), ".",
|
||||||
|
format_version_part(Min), ".",
|
||||||
|
format_version_part(Patch),
|
||||||
|
format_vsn_rest(<<"-">>, AlphaPart),
|
||||||
|
format_vsn_rest(<<"+">>, BuildPart)];
|
||||||
|
format({{Maj, Min, Patch, MinPatch}, {AlphaPart, BuildPart}}) ->
|
||||||
|
[format_version_part(Maj), ".",
|
||||||
|
format_version_part(Min), ".",
|
||||||
|
format_version_part(Patch), ".",
|
||||||
|
format_version_part(MinPatch),
|
||||||
|
format_vsn_rest(<<"-">>, AlphaPart),
|
||||||
|
format_vsn_rest(<<"+">>, BuildPart)].
|
||||||
|
|
||||||
|
-spec format_version_part(integer() | binary()) -> iolist().
|
||||||
|
format_version_part(Vsn)
|
||||||
|
when erlang:is_integer(Vsn) ->
|
||||||
|
erlang:integer_to_list(Vsn);
|
||||||
|
format_version_part(Vsn)
|
||||||
|
when erlang:is_binary(Vsn) ->
|
||||||
|
Vsn.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
%% @doc test for quality between semver versions
|
||||||
|
-spec eql(any_version(), any_version()) -> boolean().
|
||||||
|
eql(VsnA, VsnB) ->
|
||||||
|
NVsnA = normalize(parse(VsnA)),
|
||||||
|
NVsnB = normalize(parse(VsnB)),
|
||||||
|
NVsnA =:= NVsnB.
|
||||||
|
|
||||||
|
%% @doc Test that VsnA is greater than VsnB
|
||||||
|
-spec gt(any_version(), any_version()) -> boolean().
|
||||||
|
gt(VsnA, VsnB) ->
|
||||||
|
{MMPA, {AlphaA, PatchA}} = normalize(parse(VsnA)),
|
||||||
|
{MMPB, {AlphaB, PatchB}} = normalize(parse(VsnB)),
|
||||||
|
((MMPA > MMPB)
|
||||||
|
orelse
|
||||||
|
((MMPA =:= MMPB)
|
||||||
|
andalso
|
||||||
|
((AlphaA =:= [] andalso AlphaB =/= [])
|
||||||
|
orelse
|
||||||
|
((not (AlphaB =:= [] andalso AlphaA =/= []))
|
||||||
|
andalso
|
||||||
|
(AlphaA > AlphaB))))
|
||||||
|
orelse
|
||||||
|
((MMPA =:= MMPB)
|
||||||
|
andalso
|
||||||
|
(AlphaA =:= AlphaB)
|
||||||
|
andalso
|
||||||
|
((PatchB =:= [] andalso PatchA =/= [])
|
||||||
|
orelse
|
||||||
|
PatchA > PatchB))).
|
||||||
|
|
||||||
|
%% @doc Test that VsnA is greater than or equal to VsnB
|
||||||
|
-spec gte(any_version(), any_version()) -> boolean().
|
||||||
|
gte(VsnA, VsnB) ->
|
||||||
|
NVsnA = normalize(parse(VsnA)),
|
||||||
|
NVsnB = normalize(parse(VsnB)),
|
||||||
|
gt(NVsnA, NVsnB) orelse eql(NVsnA, NVsnB).
|
||||||
|
|
||||||
|
%% @doc Test that VsnA is less than VsnB
|
||||||
|
-spec lt(any_version(), any_version()) -> boolean().
|
||||||
|
lt(VsnA, VsnB) ->
|
||||||
|
{MMPA, {AlphaA, PatchA}} = normalize(parse(VsnA)),
|
||||||
|
{MMPB, {AlphaB, PatchB}} = normalize(parse(VsnB)),
|
||||||
|
((MMPA < MMPB)
|
||||||
|
orelse
|
||||||
|
((MMPA =:= MMPB)
|
||||||
|
andalso
|
||||||
|
((AlphaB =:= [] andalso AlphaA =/= [])
|
||||||
|
orelse
|
||||||
|
((not (AlphaA =:= [] andalso AlphaB =/= []))
|
||||||
|
andalso
|
||||||
|
(AlphaA < AlphaB))))
|
||||||
|
orelse
|
||||||
|
((MMPA =:= MMPB)
|
||||||
|
andalso
|
||||||
|
(AlphaA =:= AlphaB)
|
||||||
|
andalso
|
||||||
|
((PatchA =:= [] andalso PatchB =/= [])
|
||||||
|
orelse
|
||||||
|
PatchA < PatchB))).
|
||||||
|
|
||||||
|
%% @doc Test that VsnA is less than or equal to VsnB
|
||||||
|
-spec lte(any_version(), any_version()) -> boolean().
|
||||||
|
lte(VsnA, VsnB) ->
|
||||||
|
NVsnA = normalize(parse(VsnA)),
|
||||||
|
NVsnB = normalize(parse(VsnB)),
|
||||||
|
lt(NVsnA, NVsnB) orelse eql(NVsnA, NVsnB).
|
||||||
|
|
||||||
|
%% @doc Test that VsnMatch is greater than or equal to Vsn1 and
|
||||||
|
%% less than or equal to Vsn2
|
||||||
|
-spec between(any_version(), any_version(), any_version()) -> boolean().
|
||||||
|
between(Vsn1, Vsn2, VsnMatch) ->
|
||||||
|
NVsnA = normalize(parse(Vsn1)),
|
||||||
|
NVsnB = normalize(parse(Vsn2)),
|
||||||
|
NVsnMatch = normalize(parse(VsnMatch)),
|
||||||
|
gte(NVsnMatch, NVsnA) andalso
|
||||||
|
lte(NVsnMatch, NVsnB).
|
||||||
|
|
||||||
|
%% @doc check that VsnA is Approximately greater than VsnB
|
||||||
|
%%
|
||||||
|
%% Specifying ">= 2.6.5" is an optimistic version constraint. All
|
||||||
|
%% versions greater than the one specified, including major releases
|
||||||
|
%% (e.g. 3.0.0) are allowed.
|
||||||
|
%%
|
||||||
|
%% Conversely, specifying "~> 2.6" is pessimistic about future major
|
||||||
|
%% revisions and "~> 2.6.5" is pessimistic about future minor
|
||||||
|
%% revisions.
|
||||||
|
%%
|
||||||
|
%% "~> 2.6" matches cookbooks >= 2.6.0 AND < 3.0.0
|
||||||
|
%% "~> 2.6.5" matches cookbooks >= 2.6.5 AND < 2.7.0
|
||||||
|
pes(VsnA, VsnB) ->
|
||||||
|
internal_pes(parse(VsnA), parse(VsnB)).
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% Friend Functions
|
||||||
|
%%%===================================================================
|
||||||
|
%% @doc helper function for the peg grammar to parse the iolist into a semver
|
||||||
|
-spec internal_parse_version(iolist()) -> semver().
|
||||||
|
internal_parse_version([MMP, AlphaPart, BuildPart, _]) ->
|
||||||
|
{parse_major_minor_patch_minpatch(MMP), {parse_alpha_part(AlphaPart),
|
||||||
|
parse_alpha_part(BuildPart)}}.
|
||||||
|
|
||||||
|
%% @doc helper function for the peg grammar to parse the iolist into a major_minor_patch
|
||||||
|
-spec parse_major_minor_patch_minpatch(iolist()) -> major_minor_patch_minpatch().
|
||||||
|
parse_major_minor_patch_minpatch([MajVsn, [], [], []]) ->
|
||||||
|
strip_maj_version(MajVsn);
|
||||||
|
parse_major_minor_patch_minpatch([MajVsn, [<<".">>, MinVsn], [], []]) ->
|
||||||
|
{strip_maj_version(MajVsn), MinVsn};
|
||||||
|
parse_major_minor_patch_minpatch([MajVsn,
|
||||||
|
[<<".">>, MinVsn],
|
||||||
|
[<<".">>, PatchVsn], []]) ->
|
||||||
|
{strip_maj_version(MajVsn), MinVsn, PatchVsn};
|
||||||
|
parse_major_minor_patch_minpatch([MajVsn,
|
||||||
|
[<<".">>, MinVsn],
|
||||||
|
[<<".">>, PatchVsn],
|
||||||
|
[<<".">>, MinPatch]]) ->
|
||||||
|
{strip_maj_version(MajVsn), MinVsn, PatchVsn, MinPatch}.
|
||||||
|
|
||||||
|
%% @doc helper function for the peg grammar to parse the iolist into an alpha part
|
||||||
|
-spec parse_alpha_part(iolist()) -> [alpha_part()].
|
||||||
|
parse_alpha_part([]) ->
|
||||||
|
[];
|
||||||
|
parse_alpha_part([_, AV1, Rest]) ->
|
||||||
|
[erlang:iolist_to_binary(AV1) |
|
||||||
|
[format_alpha_part(Part) || Part <- Rest]].
|
||||||
|
|
||||||
|
%% @doc according to semver alpha parts that can be treated like
|
||||||
|
%% numbers must be. We implement that here by taking the alpha part
|
||||||
|
%% and trying to convert it to a number, if it succeeds we use
|
||||||
|
%% it. Otherwise we do not.
|
||||||
|
-spec format_alpha_part(iolist()) -> integer() | binary().
|
||||||
|
format_alpha_part([<<".">>, AlphaPart]) ->
|
||||||
|
Bin = erlang:iolist_to_binary(AlphaPart),
|
||||||
|
try
|
||||||
|
erlang:list_to_integer(erlang:binary_to_list(Bin))
|
||||||
|
catch
|
||||||
|
error:badarg ->
|
||||||
|
Bin
|
||||||
|
end.
|
||||||
|
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
%%% Internal Functions
|
%%% Internal Functions
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
|
-spec strip_maj_version(iolist()) -> version_element().
|
||||||
|
strip_maj_version([<<"v">>, MajVsn]) ->
|
||||||
|
MajVsn;
|
||||||
|
strip_maj_version([[], MajVsn]) ->
|
||||||
|
MajVsn;
|
||||||
|
strip_maj_version(MajVsn) ->
|
||||||
|
MajVsn.
|
||||||
|
|
||||||
-spec tokens(semvar()) -> parsed_semvar().
|
-spec to_list(integer() | binary() | string()) -> string() | binary().
|
||||||
tokens(Vsn) ->
|
to_list(Detail) when erlang:is_integer(Detail) ->
|
||||||
[MajorVsn, MinorVsn, RawPatch] = string:tokens(Vsn, "."),
|
erlang:integer_to_list(Detail);
|
||||||
{PatchVsn, PatchString} = split_patch(RawPatch),
|
to_list(Detail) when erlang:is_list(Detail); erlang:is_binary(Detail) ->
|
||||||
{MajorVsn, MinorVsn, PatchVsn, PatchString}.
|
Detail.
|
||||||
|
|
||||||
-spec split_patch(string()) ->
|
-spec format_vsn_rest(binary() | string(), [integer() | binary()]) -> iolist().
|
||||||
{PatchVsn::string(), PatchStr::string()}.
|
format_vsn_rest(_TypeMark, []) ->
|
||||||
split_patch(RawPatch) ->
|
[];
|
||||||
{PatchVsn, PatchStr} = split_patch(RawPatch, {"", ""}),
|
format_vsn_rest(TypeMark, [Head | Rest]) ->
|
||||||
{lists:reverse(PatchVsn), PatchStr}.
|
[TypeMark, Head |
|
||||||
|
[[".", to_list(Detail)] || Detail <- Rest]].
|
||||||
|
|
||||||
-spec split_patch(string(), {AccPatchVsn::string(), AccPatchStr::string()}) ->
|
%% @doc normalize the semver so they can be compared
|
||||||
{PatchVsn::string(), PatchStr::string()}.
|
-spec normalize(semver()) -> semver().
|
||||||
split_patch([], Acc) ->
|
normalize({Vsn, Rest})
|
||||||
Acc;
|
when erlang:is_binary(Vsn);
|
||||||
split_patch([Dig|T], {PatchVsn, PatchStr}) when Dig >= $0 andalso Dig =< $9 ->
|
erlang:is_integer(Vsn) ->
|
||||||
split_patch(T, {[Dig|PatchVsn], PatchStr});
|
{{Vsn, 0, 0, 0}, Rest};
|
||||||
split_patch(PatchStr, {PatchVsn, ""}) ->
|
normalize({{Maj, Min}, Rest}) ->
|
||||||
{PatchVsn, PatchStr}.
|
{{Maj, Min, 0, 0}, Rest};
|
||||||
|
normalize({{Maj, Min, Patch}, Rest}) ->
|
||||||
|
{{Maj, Min, Patch, 0}, Rest};
|
||||||
|
normalize(Other = {{_, _, _, _}, {_,_}}) ->
|
||||||
|
Other.
|
||||||
|
|
||||||
-spec compare_toks(parsed_semvar(), parsed_semvar()) -> boolean().
|
%% @doc to do the pessimistic compare we need a parsed semver. This is
|
||||||
compare_toks({MajA, MinA, PVA, PSA}, {MajB, MinB, PVB, PSB}) ->
|
%% the internal implementation of the of the pessimistic run. The
|
||||||
compare_toks2({to_int(MajA), to_int(MinA), to_int(PVA), PSA},
|
%% external just ensures that versions are parsed.
|
||||||
{to_int(MajB), to_int(MinB), to_int(PVB), PSB}).
|
-spec internal_pes(semver(), semver()) -> boolean().
|
||||||
|
internal_pes(VsnA, {{LM, LMI}, Alpha})
|
||||||
-spec compare_toks2(parsed_semvar(), parsed_semvar()) -> boolean().
|
when erlang:is_integer(LM),
|
||||||
compare_toks2({MajA, _MinA, _PVA, _PSA}, {MajB, _MinB, _PVB, _PSB})
|
erlang:is_integer(LMI) ->
|
||||||
when MajA > MajB ->
|
gte(VsnA, {{LM, LMI, 0}, Alpha}) andalso
|
||||||
true;
|
lt(VsnA, {{LM + 1, 0, 0, 0}, {[], []}});
|
||||||
compare_toks2({_Maj, MinA, _PVA, _PSA}, {_Maj, MinB, _PVB, _PSB})
|
internal_pes(VsnA, {{LM, LMI, LP}, Alpha})
|
||||||
when MinA > MinB ->
|
when erlang:is_integer(LM),
|
||||||
true;
|
erlang:is_integer(LMI),
|
||||||
compare_toks2({_Maj, _Min, PVA, _PSA}, {_Maj, _Min, PVB, _PSB})
|
erlang:is_integer(LP) ->
|
||||||
when PVA > PVB ->
|
gte(VsnA, {{LM, LMI, LP}, Alpha})
|
||||||
true;
|
andalso
|
||||||
compare_toks2({_Maj, _Min, _PV, ""}, {_Maj, _Min, _PV, PSB}) when PSB /= ""->
|
lt(VsnA, {{LM, LMI + 1, 0, 0}, {[], []}});
|
||||||
true;
|
internal_pes(VsnA, {{LM, LMI, LP, LMP}, Alpha})
|
||||||
compare_toks2({_Maj, _Min, _PV, PSA}, {_Maj, _Min, _PV, ""}) when PSA /= ""->
|
when erlang:is_integer(LM),
|
||||||
false;
|
erlang:is_integer(LMI),
|
||||||
compare_toks2({_Maj, _Min, _PV, PSA}, {_Maj, _Min, _PV, PSB}) when PSA > PSB ->
|
erlang:is_integer(LP),
|
||||||
true;
|
erlang:is_integer(LMP) ->
|
||||||
compare_toks2(_ToksA, _ToksB) ->
|
gte(VsnA, {{LM, LMI, LP, LMP}, Alpha})
|
||||||
false.
|
andalso
|
||||||
|
lt(VsnA, {{LM, LMI, LP + 1, 0}, {[], []}});
|
||||||
-spec to_int(string()) -> integer().
|
internal_pes(Vsn, LVsn) ->
|
||||||
to_int(String) ->
|
gte(Vsn, LVsn).
|
||||||
try
|
|
||||||
list_to_integer(String)
|
|
||||||
catch
|
|
||||||
error:badarg ->
|
|
||||||
throw(invalid_semver_string)
|
|
||||||
end.
|
|
||||||
|
|
||||||
%%%===================================================================
|
|
||||||
%%% Test Functions
|
|
||||||
%%%===================================================================
|
|
||||||
|
|
||||||
-ifndef(NOTEST).
|
|
||||||
-include_lib("eunit/include/eunit.hrl").
|
|
||||||
|
|
||||||
split_patch_test() ->
|
|
||||||
?assertMatch({"123", "alpha1"}, split_patch("123alpha1")).
|
|
||||||
|
|
||||||
compare_test() ->
|
|
||||||
?assertMatch(true, compare("1.2.3", "1.2.3alpha")),
|
|
||||||
?assertMatch(true, compare("1.2.3beta", "1.2.3alpha")),
|
|
||||||
?assertMatch(true, compare("1.2.4", "1.2.3")),
|
|
||||||
?assertMatch(true, compare("1.3.3", "1.2.3")),
|
|
||||||
?assertMatch(true, compare("2.2.3", "1.2.3")),
|
|
||||||
?assertMatch(true, compare("4.2.3", "3.10.3")),
|
|
||||||
?assertMatch(false, compare("1.2.3", "2.2.3")),
|
|
||||||
?assertThrow(invalid_semver_string, compare("1.b.2", "1.3.4")),
|
|
||||||
?assertThrow(invalid_semver_string, compare("1.2.2", "1.3.t")).
|
|
||||||
|
|
||||||
-endif.
|
|
||||||
|
|
302
src/ec_semver_parser.erl
Normal file
302
src/ec_semver_parser.erl
Normal file
|
@ -0,0 +1,302 @@
|
||||||
|
-module(ec_semver_parser).
|
||||||
|
-export([parse/1,file/1]).
|
||||||
|
-define(p_anything,true).
|
||||||
|
-define(p_charclass,true).
|
||||||
|
-define(p_choose,true).
|
||||||
|
-define(p_not,true).
|
||||||
|
-define(p_one_or_more,true).
|
||||||
|
-define(p_optional,true).
|
||||||
|
-define(p_scan,true).
|
||||||
|
-define(p_seq,true).
|
||||||
|
-define(p_string,true).
|
||||||
|
-define(p_zero_or_more,true).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-spec file(file:name()) -> any().
|
||||||
|
file(Filename) -> case file:read_file(Filename) of {ok,Bin} -> parse(Bin); Err -> Err end.
|
||||||
|
|
||||||
|
-spec parse(binary() | list()) -> any().
|
||||||
|
parse(List) when is_list(List) -> parse(unicode:characters_to_binary(List));
|
||||||
|
parse(Input) when is_binary(Input) ->
|
||||||
|
_ = setup_memo(),
|
||||||
|
Result = case 'semver'(Input,{{line,1},{column,1}}) of
|
||||||
|
{AST, <<>>, _Index} -> AST;
|
||||||
|
Any -> Any
|
||||||
|
end,
|
||||||
|
release_memo(), Result.
|
||||||
|
|
||||||
|
-spec 'semver'(input(), index()) -> parse_result().
|
||||||
|
'semver'(Input, Index) ->
|
||||||
|
p(Input, Index, 'semver', fun(I,D) -> (p_seq([fun 'major_minor_patch_min_patch'/2, p_optional(p_seq([p_string(<<"-">>), fun 'alpha_part'/2, p_zero_or_more(p_seq([p_string(<<".">>), fun 'alpha_part'/2]))])), p_optional(p_seq([p_string(<<"+">>), fun 'alpha_part'/2, p_zero_or_more(p_seq([p_string(<<".">>), fun 'alpha_part'/2]))])), p_not(p_anything())]))(I,D) end, fun(Node, _Idx) -> ec_semver:internal_parse_version(Node) end).
|
||||||
|
|
||||||
|
-spec 'major_minor_patch_min_patch'(input(), index()) -> parse_result().
|
||||||
|
'major_minor_patch_min_patch'(Input, Index) ->
|
||||||
|
p(Input, Index, 'major_minor_patch_min_patch', fun(I,D) -> (p_seq([p_choose([p_seq([p_optional(p_string(<<"v">>)), fun 'numeric_part'/2]), fun 'alpha_part'/2]), p_optional(p_seq([p_string(<<".">>), fun 'version_part'/2])), p_optional(p_seq([p_string(<<".">>), fun 'version_part'/2])), p_optional(p_seq([p_string(<<".">>), fun 'version_part'/2]))]))(I,D) end, fun(Node, Idx) ->transform('major_minor_patch_min_patch', Node, Idx) end).
|
||||||
|
|
||||||
|
-spec 'version_part'(input(), index()) -> parse_result().
|
||||||
|
'version_part'(Input, Index) ->
|
||||||
|
p(Input, Index, 'version_part', fun(I,D) -> (p_choose([fun 'numeric_part'/2, fun 'alpha_part'/2]))(I,D) end, fun(Node, Idx) ->transform('version_part', Node, Idx) end).
|
||||||
|
|
||||||
|
-spec 'numeric_part'(input(), index()) -> parse_result().
|
||||||
|
'numeric_part'(Input, Index) ->
|
||||||
|
p(Input, Index, 'numeric_part', fun(I,D) -> (p_one_or_more(p_charclass(<<"[0-9]">>)))(I,D) end, fun(Node, _Idx) ->erlang:list_to_integer(erlang:binary_to_list(erlang:iolist_to_binary(Node))) end).
|
||||||
|
|
||||||
|
-spec 'alpha_part'(input(), index()) -> parse_result().
|
||||||
|
'alpha_part'(Input, Index) ->
|
||||||
|
p(Input, Index, 'alpha_part', fun(I,D) -> (p_one_or_more(p_charclass(<<"[A-Za-z0-9-]">>)))(I,D) end, fun(Node, _Idx) ->erlang:iolist_to_binary(Node) end).
|
||||||
|
|
||||||
|
|
||||||
|
transform(_,Node,_Index) -> Node.
|
||||||
|
-type index() :: {{line, pos_integer()}, {column, pos_integer()}}.
|
||||||
|
-type input() :: binary().
|
||||||
|
-type parse_failure() :: {fail, term()}.
|
||||||
|
-type parse_success() :: {term(), input(), index()}.
|
||||||
|
-type parse_result() :: parse_failure() | parse_success().
|
||||||
|
-type parse_fun() :: fun((input(), index()) -> parse_result()).
|
||||||
|
-type xform_fun() :: fun((input(), index()) -> term()).
|
||||||
|
|
||||||
|
-spec p(input(), index(), atom(), parse_fun(), xform_fun()) -> parse_result().
|
||||||
|
p(Inp, StartIndex, Name, ParseFun, TransformFun) ->
|
||||||
|
case get_memo(StartIndex, Name) of % See if the current reduction is memoized
|
||||||
|
{ok, Memo} -> %Memo; % If it is, return the stored result
|
||||||
|
Memo;
|
||||||
|
_ -> % If not, attempt to parse
|
||||||
|
Result = case ParseFun(Inp, StartIndex) of
|
||||||
|
{fail,_} = Failure -> % If it fails, memoize the failure
|
||||||
|
Failure;
|
||||||
|
{Match, InpRem, NewIndex} -> % If it passes, transform and memoize the result.
|
||||||
|
Transformed = TransformFun(Match, StartIndex),
|
||||||
|
{Transformed, InpRem, NewIndex}
|
||||||
|
end,
|
||||||
|
memoize(StartIndex, Name, Result),
|
||||||
|
Result
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec setup_memo() -> ets:tid().
|
||||||
|
setup_memo() ->
|
||||||
|
put({parse_memo_table, ?MODULE}, ets:new(?MODULE, [set])).
|
||||||
|
|
||||||
|
-spec release_memo() -> true.
|
||||||
|
release_memo() ->
|
||||||
|
ets:delete(memo_table_name()).
|
||||||
|
|
||||||
|
-spec memoize(index(), atom(), parse_result()) -> true.
|
||||||
|
memoize(Index, Name, Result) ->
|
||||||
|
Memo = case ets:lookup(memo_table_name(), Index) of
|
||||||
|
[] -> [];
|
||||||
|
[{Index, Plist}] -> Plist
|
||||||
|
end,
|
||||||
|
ets:insert(memo_table_name(), {Index, [{Name, Result}|Memo]}).
|
||||||
|
|
||||||
|
-spec get_memo(index(), atom()) -> {ok, term()} | {error, not_found}.
|
||||||
|
get_memo(Index, Name) ->
|
||||||
|
case ets:lookup(memo_table_name(), Index) of
|
||||||
|
[] -> {error, not_found};
|
||||||
|
[{Index, Plist}] ->
|
||||||
|
case proplists:lookup(Name, Plist) of
|
||||||
|
{Name, Result} -> {ok, Result};
|
||||||
|
_ -> {error, not_found}
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec memo_table_name() -> ets:tid().
|
||||||
|
memo_table_name() ->
|
||||||
|
get({parse_memo_table, ?MODULE}).
|
||||||
|
|
||||||
|
-ifdef(p_eof).
|
||||||
|
-spec p_eof() -> parse_fun().
|
||||||
|
p_eof() ->
|
||||||
|
fun(<<>>, Index) -> {eof, [], Index};
|
||||||
|
(_, Index) -> {fail, {expected, eof, Index}} end.
|
||||||
|
-endif.
|
||||||
|
|
||||||
|
-ifdef(p_optional).
|
||||||
|
-spec p_optional(parse_fun()) -> parse_fun().
|
||||||
|
p_optional(P) ->
|
||||||
|
fun(Input, Index) ->
|
||||||
|
case P(Input, Index) of
|
||||||
|
{fail,_} -> {[], Input, Index};
|
||||||
|
{_, _, _} = Success -> Success
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
-endif.
|
||||||
|
|
||||||
|
-ifdef(p_not).
|
||||||
|
-spec p_not(parse_fun()) -> parse_fun().
|
||||||
|
p_not(P) ->
|
||||||
|
fun(Input, Index)->
|
||||||
|
case P(Input,Index) of
|
||||||
|
{fail,_} ->
|
||||||
|
{[], Input, Index};
|
||||||
|
{Result, _, _} -> {fail, {expected, {no_match, Result},Index}}
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
-endif.
|
||||||
|
|
||||||
|
-ifdef(p_assert).
|
||||||
|
-spec p_assert(parse_fun()) -> parse_fun().
|
||||||
|
p_assert(P) ->
|
||||||
|
fun(Input,Index) ->
|
||||||
|
case P(Input,Index) of
|
||||||
|
{fail,_} = Failure-> Failure;
|
||||||
|
_ -> {[], Input, Index}
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
-endif.
|
||||||
|
|
||||||
|
-ifdef(p_seq).
|
||||||
|
-spec p_seq([parse_fun()]) -> parse_fun().
|
||||||
|
p_seq(P) ->
|
||||||
|
fun(Input, Index) ->
|
||||||
|
p_all(P, Input, Index, [])
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec p_all([parse_fun()], input(), index(), [term()]) -> parse_result().
|
||||||
|
p_all([], Inp, Index, Accum ) -> {lists:reverse( Accum ), Inp, Index};
|
||||||
|
p_all([P|Parsers], Inp, Index, Accum) ->
|
||||||
|
case P(Inp, Index) of
|
||||||
|
{fail, _} = Failure -> Failure;
|
||||||
|
{Result, InpRem, NewIndex} -> p_all(Parsers, InpRem, NewIndex, [Result|Accum])
|
||||||
|
end.
|
||||||
|
-endif.
|
||||||
|
|
||||||
|
-ifdef(p_choose).
|
||||||
|
-spec p_choose([parse_fun()]) -> parse_fun().
|
||||||
|
p_choose(Parsers) ->
|
||||||
|
fun(Input, Index) ->
|
||||||
|
p_attempt(Parsers, Input, Index, none)
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec p_attempt([parse_fun()], input(), index(), none | parse_failure()) -> parse_result().
|
||||||
|
p_attempt([], _Input, _Index, Failure) -> Failure;
|
||||||
|
p_attempt([P|Parsers], Input, Index, FirstFailure)->
|
||||||
|
case P(Input, Index) of
|
||||||
|
{fail, _} = Failure ->
|
||||||
|
case FirstFailure of
|
||||||
|
none -> p_attempt(Parsers, Input, Index, Failure);
|
||||||
|
_ -> p_attempt(Parsers, Input, Index, FirstFailure)
|
||||||
|
end;
|
||||||
|
Result -> Result
|
||||||
|
end.
|
||||||
|
-endif.
|
||||||
|
|
||||||
|
-ifdef(p_zero_or_more).
|
||||||
|
-spec p_zero_or_more(parse_fun()) -> parse_fun().
|
||||||
|
p_zero_or_more(P) ->
|
||||||
|
fun(Input, Index) ->
|
||||||
|
p_scan(P, Input, Index, [])
|
||||||
|
end.
|
||||||
|
-endif.
|
||||||
|
|
||||||
|
-ifdef(p_one_or_more).
|
||||||
|
-spec p_one_or_more(parse_fun()) -> parse_fun().
|
||||||
|
p_one_or_more(P) ->
|
||||||
|
fun(Input, Index)->
|
||||||
|
Result = p_scan(P, Input, Index, []),
|
||||||
|
case Result of
|
||||||
|
{[_|_], _, _} ->
|
||||||
|
Result;
|
||||||
|
_ ->
|
||||||
|
{fail, {expected, Failure, _}} = P(Input,Index),
|
||||||
|
{fail, {expected, {at_least_one, Failure}, Index}}
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
-endif.
|
||||||
|
|
||||||
|
-ifdef(p_label).
|
||||||
|
-spec p_label(atom(), parse_fun()) -> parse_fun().
|
||||||
|
p_label(Tag, P) ->
|
||||||
|
fun(Input, Index) ->
|
||||||
|
case P(Input, Index) of
|
||||||
|
{fail,_} = Failure ->
|
||||||
|
Failure;
|
||||||
|
{Result, InpRem, NewIndex} ->
|
||||||
|
{{Tag, Result}, InpRem, NewIndex}
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
-endif.
|
||||||
|
|
||||||
|
-ifdef(p_scan).
|
||||||
|
-spec p_scan(parse_fun(), input(), index(), [term()]) -> {[term()], input(), index()}.
|
||||||
|
p_scan(_, <<>>, Index, Accum) -> {lists:reverse(Accum), <<>>, Index};
|
||||||
|
p_scan(P, Inp, Index, Accum) ->
|
||||||
|
case P(Inp, Index) of
|
||||||
|
{fail,_} -> {lists:reverse(Accum), Inp, Index};
|
||||||
|
{Result, InpRem, NewIndex} -> p_scan(P, InpRem, NewIndex, [Result | Accum])
|
||||||
|
end.
|
||||||
|
-endif.
|
||||||
|
|
||||||
|
-ifdef(p_string).
|
||||||
|
-spec p_string(binary()) -> parse_fun().
|
||||||
|
p_string(S) ->
|
||||||
|
Length = erlang:byte_size(S),
|
||||||
|
fun(Input, Index) ->
|
||||||
|
try
|
||||||
|
<<S:Length/binary, Rest/binary>> = Input,
|
||||||
|
{S, Rest, p_advance_index(S, Index)}
|
||||||
|
catch
|
||||||
|
error:{badmatch,_} -> {fail, {expected, {string, S}, Index}}
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
-endif.
|
||||||
|
|
||||||
|
-ifdef(p_anything).
|
||||||
|
-spec p_anything() -> parse_fun().
|
||||||
|
p_anything() ->
|
||||||
|
fun(<<>>, Index) -> {fail, {expected, any_character, Index}};
|
||||||
|
(Input, Index) when is_binary(Input) ->
|
||||||
|
<<C/utf8, Rest/binary>> = Input,
|
||||||
|
{<<C/utf8>>, Rest, p_advance_index(<<C/utf8>>, Index)}
|
||||||
|
end.
|
||||||
|
-endif.
|
||||||
|
|
||||||
|
-ifdef(p_charclass).
|
||||||
|
-spec p_charclass(string() | binary()) -> parse_fun().
|
||||||
|
p_charclass(Class) ->
|
||||||
|
{ok, RE} = re:compile(Class, [unicode, dotall]),
|
||||||
|
fun(Inp, Index) ->
|
||||||
|
case re:run(Inp, RE, [anchored]) of
|
||||||
|
{match, [{0, Length}|_]} ->
|
||||||
|
{Head, Tail} = erlang:split_binary(Inp, Length),
|
||||||
|
{Head, Tail, p_advance_index(Head, Index)};
|
||||||
|
_ -> {fail, {expected, {character_class, binary_to_list(Class)}, Index}}
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
-endif.
|
||||||
|
|
||||||
|
-ifdef(p_regexp).
|
||||||
|
-spec p_regexp(binary()) -> parse_fun().
|
||||||
|
p_regexp(Regexp) ->
|
||||||
|
{ok, RE} = re:compile(Regexp, [unicode, dotall, anchored]),
|
||||||
|
fun(Inp, Index) ->
|
||||||
|
case re:run(Inp, RE) of
|
||||||
|
{match, [{0, Length}|_]} ->
|
||||||
|
{Head, Tail} = erlang:split_binary(Inp, Length),
|
||||||
|
{Head, Tail, p_advance_index(Head, Index)};
|
||||||
|
_ -> {fail, {expected, {regexp, binary_to_list(Regexp)}, Index}}
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
-endif.
|
||||||
|
|
||||||
|
-ifdef(line).
|
||||||
|
-spec line(index() | term()) -> pos_integer() | undefined.
|
||||||
|
line({{line,L},_}) -> L;
|
||||||
|
line(_) -> undefined.
|
||||||
|
-endif.
|
||||||
|
|
||||||
|
-ifdef(column).
|
||||||
|
-spec column(index() | term()) -> pos_integer() | undefined.
|
||||||
|
column({_,{column,C}}) -> C;
|
||||||
|
column(_) -> undefined.
|
||||||
|
-endif.
|
||||||
|
|
||||||
|
-spec p_advance_index(input() | unicode:charlist() | pos_integer(), index()) -> index().
|
||||||
|
p_advance_index(MatchedInput, Index) when is_list(MatchedInput) orelse is_binary(MatchedInput)-> % strings
|
||||||
|
lists:foldl(fun p_advance_index/2, Index, unicode:characters_to_list(MatchedInput));
|
||||||
|
p_advance_index(MatchedInput, Index) when is_integer(MatchedInput) -> % single characters
|
||||||
|
{{line, Line}, {column, Col}} = Index,
|
||||||
|
case MatchedInput of
|
||||||
|
$\n -> {{line, Line+1}, {column, 1}};
|
||||||
|
_ -> {{line, Line}, {column, Col+1}}
|
||||||
|
end.
|
|
@ -1,128 +0,0 @@
|
||||||
%%%-------------------------------------------------------------------
|
|
||||||
%%% @copyright (C) 2011, Erlware LLC
|
|
||||||
%%% @doc
|
|
||||||
%%% Helper functions for working with strings.
|
|
||||||
%%% @end
|
|
||||||
%%%-------------------------------------------------------------------
|
|
||||||
-module(ec_string).
|
|
||||||
|
|
||||||
-export([
|
|
||||||
compare_versions/2
|
|
||||||
]).
|
|
||||||
%%%===================================================================
|
|
||||||
%%% API
|
|
||||||
%%%===================================================================
|
|
||||||
|
|
||||||
%% @doc Is arbitrary version string A bigger than version string B?
|
|
||||||
%% Valid version string elements are either separated by . or - or both.
|
|
||||||
%% Final version string elements may have a numeric followed directly by an
|
|
||||||
%% alpha numeric and will be compared separately as in 12alpha.
|
|
||||||
%%
|
|
||||||
%% <pre>
|
|
||||||
%% Example: compare_versions("3-2-5-alpha", "3.10.6") will return false
|
|
||||||
%% compare_versions("3-2-alpha", "3.2.1-alpha") will return false
|
|
||||||
%% compare_versions("3-2alpha", "3.2.1-alpha") will return false
|
|
||||||
%% compare_versions("3.2.2", "3.2.2") will return false
|
|
||||||
%% compare_versions("3.2.1", "3.2.1-rc2") will return true
|
|
||||||
%% compare_versions("3.2.2", "3.2.1") will return true
|
|
||||||
%% </pre>
|
|
||||||
-spec compare_versions(VsnA::string(), VsnB::string()) -> boolean().
|
|
||||||
compare_versions(VsnA, VsnB) ->
|
|
||||||
compare(string:tokens(VsnA, ".-"),string:tokens(VsnB, ".-")).
|
|
||||||
|
|
||||||
%%%===================================================================
|
|
||||||
%%% Internal Functions
|
|
||||||
%%%===================================================================
|
|
||||||
|
|
||||||
-spec compare(string(), string()) -> boolean().
|
|
||||||
compare([Str|TA], [Str|TB]) ->
|
|
||||||
compare(TA, TB);
|
|
||||||
compare([StrA|TA], [StrB|TB]) ->
|
|
||||||
fine_compare(split_numeric_alpha(StrA), TA,
|
|
||||||
split_numeric_alpha(StrB), TB);
|
|
||||||
compare([], [Str]) ->
|
|
||||||
not compare_against_nothing(Str);
|
|
||||||
compare([Str], []) ->
|
|
||||||
compare_against_nothing(Str);
|
|
||||||
compare([], [_,_|_]) ->
|
|
||||||
false;
|
|
||||||
compare([_,_|_], []) ->
|
|
||||||
true;
|
|
||||||
compare([], []) ->
|
|
||||||
false.
|
|
||||||
|
|
||||||
-spec compare_against_nothing(string()) -> boolean().
|
|
||||||
compare_against_nothing(Str) ->
|
|
||||||
case split_numeric_alpha(Str) of
|
|
||||||
{_StrDig, ""} -> true;
|
|
||||||
{"", _StrAlpha} -> false;
|
|
||||||
{_StrDig, _StrAlpha} -> true
|
|
||||||
end.
|
|
||||||
|
|
||||||
-spec fine_compare({string(), string()}, string(),
|
|
||||||
{string(), string()}, string()) ->
|
|
||||||
boolean().
|
|
||||||
fine_compare({_StrDigA, StrA}, TA, {_StrDigB, _StrB}, _TB)
|
|
||||||
when StrA /= "", TA /= [] ->
|
|
||||||
throw(invalid_version_string);
|
|
||||||
fine_compare({_StrDigA, _StrA}, _TA, {_StrDigB, StrB}, TB)
|
|
||||||
when StrB /= "", TB /= [] ->
|
|
||||||
throw(invalid_version_string);
|
|
||||||
fine_compare({"", _StrA}, _TA, {StrDigB, _StrB}, _TB) when StrDigB /= "" ->
|
|
||||||
false;
|
|
||||||
fine_compare({StrDigA, _StrA}, _TA, {"", _StrB}, _TB) when StrDigA /= "" ->
|
|
||||||
true;
|
|
||||||
fine_compare({StrDig, ""}, _TA, {StrDig, StrB}, _TB) when StrB /= "" ->
|
|
||||||
true;
|
|
||||||
fine_compare({StrDig, StrA}, _TA, {StrDig, ""}, _TB) when StrA /= "" ->
|
|
||||||
false;
|
|
||||||
fine_compare({StrDig, StrA}, _TA, {StrDig, StrB}, _TB) ->
|
|
||||||
StrA > StrB;
|
|
||||||
fine_compare({StrDigA, _StrA}, _TA, {StrDigB, _StrB}, _TB) ->
|
|
||||||
list_to_integer(StrDigA) > list_to_integer(StrDigB).
|
|
||||||
|
|
||||||
%% In the case of a version sub part with a numeric then an alpha,
|
|
||||||
%% split out the numeric and alpha "24alpha" becomes {"24", "alpha"}
|
|
||||||
-spec split_numeric_alpha(string()) ->
|
|
||||||
{PatchVsn::string(), PatchStr::string()}.
|
|
||||||
split_numeric_alpha(RawVsn) ->
|
|
||||||
{Num, Str} = split_numeric_alpha(RawVsn, {"", ""}),
|
|
||||||
{lists:reverse(Num), Str}.
|
|
||||||
|
|
||||||
-spec split_numeric_alpha(string(), {PatchVsnAcc::string(),
|
|
||||||
PatchStrAcc::string()}) ->
|
|
||||||
{PatchVsn::string(), PatchStr::string()}.
|
|
||||||
split_numeric_alpha([], Acc) ->
|
|
||||||
Acc;
|
|
||||||
split_numeric_alpha([Dig|T], {PatchVsn, PatchStr})
|
|
||||||
when Dig >= $0 andalso Dig =< $9 ->
|
|
||||||
split_numeric_alpha(T, {[Dig|PatchVsn], PatchStr});
|
|
||||||
split_numeric_alpha(PatchStr, {PatchVsn, ""}) ->
|
|
||||||
{PatchVsn, PatchStr}.
|
|
||||||
|
|
||||||
%%%===================================================================
|
|
||||||
%%% Test Functions
|
|
||||||
%%%===================================================================
|
|
||||||
|
|
||||||
-ifndef(NOTEST).
|
|
||||||
-include_lib("eunit/include/eunit.hrl").
|
|
||||||
|
|
||||||
split_numeric_alpha_test() ->
|
|
||||||
?assertMatch({"123", "alpha1"}, split_numeric_alpha("123alpha1")).
|
|
||||||
|
|
||||||
compare_versions_test() ->
|
|
||||||
?assertMatch(true, compare_versions("1.2.3", "1.2.3alpha")),
|
|
||||||
?assertMatch(true, compare_versions("1.2.3-beta", "1.2.3-alpha")),
|
|
||||||
?assertMatch(true, compare_versions("1-2-3", "1-2-3alpha")),
|
|
||||||
?assertMatch(true, compare_versions("1-2-3", "1-2-3-rc3")),
|
|
||||||
?assertMatch(true, compare_versions("1.2.3beta", "1.2.3alpha")),
|
|
||||||
?assertMatch(true, compare_versions("1.2.4", "1.2.3")),
|
|
||||||
?assertMatch(true, compare_versions("1.3.3", "1.2.3")),
|
|
||||||
?assertMatch(true, compare_versions("2.2.3", "1.2.3")),
|
|
||||||
?assertMatch(true, compare_versions("4.2.3", "3.10.3")),
|
|
||||||
?assertMatch(false, compare_versions("1.2.3", "2.2.3")),
|
|
||||||
?assertMatch(false, compare_versions("1.2.2", "1.3.t")),
|
|
||||||
?assertMatch(false, compare_versions("1.2t", "1.3.t")),
|
|
||||||
?assertThrow(invalid_version_string, compare_versions("1.b.2", "1.3.4")).
|
|
||||||
|
|
||||||
-endif.
|
|
|
@ -1,4 +1,5 @@
|
||||||
%% -*- mode: Erlang; fill-column: 79; comment-column: 70; -*-
|
%% -*- mode: Erlang; fill-column: 79; comment-column: 70; -*-
|
||||||
|
%% vi:ts=4 sw=4 et
|
||||||
%%%---------------------------------------------------------------------------
|
%%%---------------------------------------------------------------------------
|
||||||
%%% Permission is hereby granted, free of charge, to any person
|
%%% Permission is hereby granted, free of charge, to any person
|
||||||
%%% obtaining a copy of this software and associated documentation
|
%%% obtaining a copy of this software and associated documentation
|
||||||
|
@ -38,18 +39,21 @@
|
||||||
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]).
|
||||||
|
|
||||||
-include_lib("eunit/include/eunit.hrl").
|
|
||||||
|
|
||||||
%%============================================================================
|
%%============================================================================
|
||||||
%% Types
|
%% Types
|
||||||
%%============================================================================
|
%%============================================================================
|
||||||
-type prompt() :: string().
|
-type prompt() :: string().
|
||||||
-type type() :: boolean | number | string.
|
-type type() :: boolean | number | string.
|
||||||
-type supported() :: string() | boolean() | number().
|
-type supported() :: boolean() | number() | string().
|
||||||
|
|
||||||
%%============================================================================
|
%%============================================================================
|
||||||
%% API
|
%% API
|
||||||
|
@ -76,7 +80,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. Trys to return the
|
%% @doc Asks the user to respond to the prompt. Tries 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) ->
|
||||||
|
@ -84,9 +88,9 @@ ask(Prompt, boolean) ->
|
||||||
ask(Prompt, number) ->
|
ask(Prompt, number) ->
|
||||||
ask_convert(Prompt, fun get_integer/1, number, none);
|
ask_convert(Prompt, fun get_integer/1, number, none);
|
||||||
ask(Prompt, string) ->
|
ask(Prompt, string) ->
|
||||||
ask_convert(Prompt, fun get_integer/1, string, none).
|
ask_convert(Prompt, fun get_string/1, string, none).
|
||||||
|
|
||||||
%% @doc Asks the user to respond to the prompt. Trys to return the
|
%% @doc Asks the user to respond to the prompt. Tries to return the
|
||||||
%% value in the format specified by 'Type'.
|
%% 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) ->
|
||||||
|
@ -100,8 +104,11 @@ ask_default(Prompt, string, Default) ->
|
||||||
%% between min and max.
|
%% between min and max.
|
||||||
-spec ask(prompt(), number(), number()) -> number().
|
-spec ask(prompt(), number(), number()) -> number().
|
||||||
ask(Prompt, Min, Max)
|
ask(Prompt, Min, Max)
|
||||||
when is_list(Prompt), is_number(Min), is_number(Max) ->
|
when erlang:is_list(Prompt),
|
||||||
Res = ask(Prompt, fun get_integer/1, none),
|
erlang:is_number(Min),
|
||||||
|
erlang:is_number(Max),
|
||||||
|
Min =< Max ->
|
||||||
|
Res = ask_convert(Prompt, fun get_integer/1, number, none),
|
||||||
case (Res >= Min andalso Res =< Max) of
|
case (Res >= Min andalso Res =< Max) of
|
||||||
true ->
|
true ->
|
||||||
Res;
|
Res;
|
||||||
|
@ -115,15 +122,17 @@ ask(Prompt, Min, Max)
|
||||||
%% ============================================================================
|
%% ============================================================================
|
||||||
%% @doc Actually does the work of asking, checking result and
|
%% @doc Actually does the work of asking, checking result and
|
||||||
%% translating result into the requested format.
|
%% translating result into the requested format.
|
||||||
-spec ask_convert(prompt(), fun(), type(), supported()) -> supported().
|
-spec ask_convert(prompt(), fun((any()) -> any()), type(), supported() | none) -> supported().
|
||||||
ask_convert(Prompt, TransFun, Type, Default) ->
|
ask_convert(Prompt, TransFun, Type, Default) ->
|
||||||
NewPrompt = Prompt ++ case Default of
|
NewPrompt =
|
||||||
none ->
|
erlang:binary_to_list(erlang:iolist_to_binary([Prompt,
|
||||||
[];
|
case Default of
|
||||||
Default ->
|
none ->
|
||||||
" (" ++ sin_utils:term_to_list(Default) ++ ")"
|
[];
|
||||||
end ++ "> ",
|
Default ->
|
||||||
Data = string:strip(string:strip(io:get_line(NewPrompt)), both, $\n),
|
[" (", io_lib:format("~p", [Default]) , ")"]
|
||||||
|
end, "> "])),
|
||||||
|
Data = string:trim(string:trim(io:get_line(NewPrompt)), both, [$\n]),
|
||||||
Ret = TransFun(Data),
|
Ret = TransFun(Data),
|
||||||
case Ret of
|
case Ret of
|
||||||
no_data ->
|
no_data ->
|
||||||
|
@ -141,7 +150,7 @@ ask_convert(Prompt, TransFun, Type, Default) ->
|
||||||
Ret
|
Ret
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% @doc Trys to translate the result into a boolean
|
%% @doc Tries 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;
|
||||||
|
@ -168,7 +177,7 @@ get_boolean([$N | _]) ->
|
||||||
get_boolean(_) ->
|
get_boolean(_) ->
|
||||||
no_clue.
|
no_clue.
|
||||||
|
|
||||||
%% @doc Trys to translate the result into an integer
|
%% @doc Tries to translate the result into an integer
|
||||||
-spec get_integer(string()) -> integer().
|
-spec get_integer(string()) -> integer().
|
||||||
get_integer([]) ->
|
get_integer([]) ->
|
||||||
no_data;
|
no_data;
|
||||||
|
@ -192,21 +201,3 @@ get_string(String) ->
|
||||||
false ->
|
false ->
|
||||||
no_clue
|
no_clue
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%%%====================================================================
|
|
||||||
%%% tests
|
|
||||||
%%%====================================================================
|
|
||||||
general_test_() ->
|
|
||||||
[?_test(42 == get_integer("42")),
|
|
||||||
?_test(500211 == get_integer("500211")),
|
|
||||||
?_test(1234567890 == get_integer("1234567890")),
|
|
||||||
?_test(12345678901234567890 == get_integer("12345678901234567890")),
|
|
||||||
?_test(true == get_boolean("true")),
|
|
||||||
?_test(false == get_boolean("false")),
|
|
||||||
?_test(true == get_boolean("Ok")),
|
|
||||||
?_test(true == get_boolean("ok")),
|
|
||||||
?_test(true == get_boolean("Y")),
|
|
||||||
?_test(true == get_boolean("y")),
|
|
||||||
?_test(false == get_boolean("False")),
|
|
||||||
?_test(false == get_boolean("No")),
|
|
||||||
?_test(false == get_boolean("no"))].
|
|
||||||
|
|
51
src/ec_vsn.erl
Normal file
51
src/ec_vsn.erl
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
%%% vi:ts=4 sw=4 et
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @author Eric Merritt <ericbmerritt@gmail.com>
|
||||||
|
%%% @copyright 2014 Erlware, LLC.
|
||||||
|
%%% @doc
|
||||||
|
%%% Provides a signature to manage returning semver formatted versions
|
||||||
|
%%% from various version control repositories.
|
||||||
|
%%%
|
||||||
|
%%% This interface is a member of the Erlware Commons Library.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(ec_vsn).
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([new/1,
|
||||||
|
vsn/1]).
|
||||||
|
|
||||||
|
-export_type([t/0]).
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% Types
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
-record(t, {callback, data}).
|
||||||
|
|
||||||
|
%% This should be opaque, but that kills dialyzer so for now we export it
|
||||||
|
%% however you should not rely on the internal representation here
|
||||||
|
-type t() :: #t{}.
|
||||||
|
|
||||||
|
-callback new() -> any().
|
||||||
|
-callback vsn(any()) -> {ok, string()} | {error, Reason::any()}.
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% API
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
%% @doc create a new dictionary object from the specified module. The
|
||||||
|
%% module should implement the dictionary behaviour.
|
||||||
|
%%
|
||||||
|
%% @param ModuleName The module name.
|
||||||
|
-spec new(module()) -> t().
|
||||||
|
new(ModuleName) when erlang:is_atom(ModuleName) ->
|
||||||
|
#t{callback = ModuleName, data = ModuleName:new()}.
|
||||||
|
|
||||||
|
%% @doc Return the semver or an error depending on what is possible
|
||||||
|
%% with this implementation in this directory.
|
||||||
|
%%
|
||||||
|
%% @param The dictionary object
|
||||||
|
-spec vsn(t()) -> {ok, string()} | {error, Reason::any()}.
|
||||||
|
vsn(#t{callback = Mod, data = Data}) ->
|
||||||
|
Mod:vsn(Data).
|
11
src/erlware_commons.app.src
Normal file
11
src/erlware_commons.app.src
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{application,erlware_commons,
|
||||||
|
[{description,"Additional standard library for Erlang"},
|
||||||
|
{vsn,"git"},
|
||||||
|
{modules,[]},
|
||||||
|
{registered,[]},
|
||||||
|
{applications,[kernel,stdlib,cf]},
|
||||||
|
{maintainers,["Eric Merritt","Tristan Sloughter",
|
||||||
|
"Jordan Wilberding","Martin Logan"]},
|
||||||
|
{licenses,["Apache", "MIT"]},
|
||||||
|
{links,[{"Github",
|
||||||
|
"https://github.com/erlware/erlware_commons"}]}]}.
|
39
test/ec_cmd_log_tests.erl
Normal file
39
test/ec_cmd_log_tests.erl
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
%%% @copyright 2024 Erlware, LLC.
|
||||||
|
-module(ec_cmd_log_tests).
|
||||||
|
|
||||||
|
-include("include/ec_cmd_log.hrl").
|
||||||
|
-include("src/ec_cmd_log.hrl").
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
|
should_test() ->
|
||||||
|
ErrorLogState = ec_cmd_log:new(error),
|
||||||
|
?assertMatch(true, ec_cmd_log:should(ErrorLogState, ?EC_ERROR)),
|
||||||
|
?assertMatch(true, not ec_cmd_log:should(ErrorLogState, ?EC_INFO)),
|
||||||
|
?assertMatch(true, not ec_cmd_log:should(ErrorLogState, ?EC_DEBUG)),
|
||||||
|
?assertEqual(?EC_ERROR, ec_cmd_log:log_level(ErrorLogState)),
|
||||||
|
?assertEqual(error, ec_cmd_log:atom_log_level(ErrorLogState)),
|
||||||
|
|
||||||
|
InfoLogState = ec_cmd_log:new(info),
|
||||||
|
?assertMatch(true, ec_cmd_log:should(InfoLogState, ?EC_ERROR)),
|
||||||
|
?assertMatch(true, ec_cmd_log:should(InfoLogState, ?EC_INFO)),
|
||||||
|
?assertMatch(true, not ec_cmd_log:should(InfoLogState, ?EC_DEBUG)),
|
||||||
|
?assertEqual(?EC_INFO, ec_cmd_log:log_level(InfoLogState)),
|
||||||
|
?assertEqual(info, ec_cmd_log:atom_log_level(InfoLogState)),
|
||||||
|
|
||||||
|
DebugLogState = ec_cmd_log:new(debug),
|
||||||
|
?assertMatch(true, ec_cmd_log:should(DebugLogState, ?EC_ERROR)),
|
||||||
|
?assertMatch(true, ec_cmd_log:should(DebugLogState, ?EC_INFO)),
|
||||||
|
?assertMatch(true, ec_cmd_log:should(DebugLogState, ?EC_DEBUG)),
|
||||||
|
?assertEqual(?EC_DEBUG, ec_cmd_log:log_level(DebugLogState)),
|
||||||
|
?assertEqual(debug, ec_cmd_log:atom_log_level(DebugLogState)).
|
||||||
|
|
||||||
|
|
||||||
|
no_color_test() ->
|
||||||
|
LogState = ec_cmd_log:new(debug, command_line, none),
|
||||||
|
?assertEqual("test",
|
||||||
|
ec_cmd_log:colorize(LogState, ?RED, true, "test")).
|
||||||
|
|
||||||
|
color_test() ->
|
||||||
|
LogState = ec_cmd_log:new(debug, command_line, high),
|
||||||
|
?assertEqual("\e[1;31m===> test\e[0m",
|
||||||
|
ec_cmd_log:colorize(LogState, ?RED, true, "test")).
|
28
test/ec_cnv_tests.erl
Normal file
28
test/ec_cnv_tests.erl
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
%%% @copyright 2024 Erlware, LLC.
|
||||||
|
-module(ec_cnv_tests).
|
||||||
|
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
|
to_integer_test() ->
|
||||||
|
?assertError(badarg, ec_cnv:to_integer(1.5, strict)).
|
||||||
|
|
||||||
|
to_float_test() ->
|
||||||
|
?assertError(badarg, ec_cnv:to_float(10, strict)).
|
||||||
|
|
||||||
|
to_atom_test() ->
|
||||||
|
?assertMatch(true, ec_cnv:to_atom("true")),
|
||||||
|
?assertMatch(true, ec_cnv:to_atom(<<"true">>)),
|
||||||
|
?assertMatch(false, ec_cnv:to_atom(<<"false">>)),
|
||||||
|
?assertMatch(false, ec_cnv:to_atom(false)),
|
||||||
|
?assertError(badarg, ec_cnv:to_atom("hello_foo_bar_baz")),
|
||||||
|
|
||||||
|
S = erlang:list_to_atom("1"),
|
||||||
|
?assertMatch(S, ec_cnv:to_atom(1)).
|
||||||
|
|
||||||
|
to_boolean_test()->
|
||||||
|
?assertMatch(true, ec_cnv:to_boolean(<<"true">>)),
|
||||||
|
?assertMatch(true, ec_cnv:to_boolean("true")),
|
||||||
|
?assertMatch(true, ec_cnv:to_boolean(true)),
|
||||||
|
?assertMatch(false, ec_cnv:to_boolean(<<"false">>)),
|
||||||
|
?assertMatch(false, ec_cnv:to_boolean("false")),
|
||||||
|
?assertMatch(false, ec_cnv:to_boolean(false)).
|
|
@ -1,223 +0,0 @@
|
||||||
%% compile with
|
|
||||||
%% erl -pz ebin --make
|
|
||||||
%% start test with
|
|
||||||
%% erl -pz ebin -pz test
|
|
||||||
%% proper:module(ec_dictionary_proper).
|
|
||||||
-module(ec_dictionary_proper).
|
|
||||||
|
|
||||||
-export([my_dict/0, dict/1, sym_dict/0, sym_dict/1, gb_tree/0, gb_tree/1, sym_dict2/0]).
|
|
||||||
|
|
||||||
-include_lib("proper/include/proper.hrl").
|
|
||||||
|
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
|
||||||
%% Properties
|
|
||||||
%%------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
prop_size_increases_with_new_key() ->
|
|
||||||
?FORALL({Dict,K}, {sym_dict(),integer()},
|
|
||||||
begin
|
|
||||||
Size = ec_dictionary:size(Dict),
|
|
||||||
case ec_dictionary:has_key(K,Dict) of
|
|
||||||
true ->
|
|
||||||
Size == ec_dictionary:size(ec_dictionary:add(K,0,Dict));
|
|
||||||
false ->
|
|
||||||
(Size + 1) == ec_dictionary:size(ec_dictionary:add(K,0,Dict))
|
|
||||||
end
|
|
||||||
end).
|
|
||||||
|
|
||||||
prop_size_decrease_when_removing() ->
|
|
||||||
?FORALL({Dict,K}, {sym_dict(),integer()},
|
|
||||||
begin
|
|
||||||
Size = ec_dictionary:size(Dict),
|
|
||||||
case ec_dictionary:has_key(K,Dict) of
|
|
||||||
false ->
|
|
||||||
Size == ec_dictionary:size(ec_dictionary:remove(K,Dict));
|
|
||||||
true ->
|
|
||||||
(Size - 1) == ec_dictionary:size(ec_dictionary:remove(K,Dict))
|
|
||||||
end
|
|
||||||
end).
|
|
||||||
|
|
||||||
prop_get_after_add_returns_correct_value() ->
|
|
||||||
?FORALL({Dict,K,V}, {sym_dict(),key(),value()},
|
|
||||||
begin
|
|
||||||
try ec_dictionary:get(K,ec_dictionary:add(K,V,Dict)) of
|
|
||||||
V ->
|
|
||||||
true;
|
|
||||||
_ ->
|
|
||||||
false
|
|
||||||
catch
|
|
||||||
_:_ ->
|
|
||||||
false
|
|
||||||
end
|
|
||||||
end).
|
|
||||||
|
|
||||||
prop_get_default_returns_correct_value() ->
|
|
||||||
?FORALL({Dict,K1,K2,V,Default},
|
|
||||||
{sym_dict(),key(),key(),value(),value()},
|
|
||||||
begin
|
|
||||||
NewDict = ec_dictionary:add(K1,V, Dict),
|
|
||||||
%% In the unlikely event that keys that are the same
|
|
||||||
%% are generated
|
|
||||||
case ec_dictionary:has_key(K2, NewDict) of
|
|
||||||
true ->
|
|
||||||
true;
|
|
||||||
false ->
|
|
||||||
ec_dictionary:get(K2, Default, NewDict) == Default
|
|
||||||
end
|
|
||||||
end).
|
|
||||||
|
|
||||||
|
|
||||||
prop_add_does_not_change_values_for_other_keys() ->
|
|
||||||
?FORALL({Dict,K,V}, {sym_dict(),key(),value()},
|
|
||||||
begin
|
|
||||||
Keys = ec_dictionary:keys(Dict),
|
|
||||||
?IMPLIES(not lists:member(K,Keys),
|
|
||||||
begin
|
|
||||||
Dict2 = ec_dictionary:add(K,V,Dict),
|
|
||||||
try lists:all(fun(B) -> B end,
|
|
||||||
[ ec_dictionary:get(Ka,Dict) ==
|
|
||||||
ec_dictionary:get(Ka,Dict2) ||
|
|
||||||
Ka <- Keys ]) of
|
|
||||||
Bool -> Bool
|
|
||||||
catch
|
|
||||||
throw:not_found -> true
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end).
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
prop_key_is_present_after_add() ->
|
|
||||||
?FORALL({Dict,K,V}, {sym_dict(),integer(),integer()},
|
|
||||||
begin
|
|
||||||
ec_dictionary:has_key(K,ec_dictionary:add(K,V,Dict)) end).
|
|
||||||
|
|
||||||
prop_value_is_present_after_add() ->
|
|
||||||
?FORALL({Dict,K,V}, {sym_dict(),integer(),integer()},
|
|
||||||
begin
|
|
||||||
ec_dictionary:has_value(V,ec_dictionary:add(K,V,Dict))
|
|
||||||
end).
|
|
||||||
|
|
||||||
prop_to_list_matches_get() ->
|
|
||||||
?FORALL(Dict,sym_dict(),
|
|
||||||
begin
|
|
||||||
%% Dict = eval(SymDict),
|
|
||||||
%% io:format("SymDict: ~p~n",[proper_symb:symbolic_seq(SymDict)]),
|
|
||||||
ToList = ec_dictionary:to_list(Dict),
|
|
||||||
%% io:format("ToList:~p~n",[ToList]),
|
|
||||||
GetList =
|
|
||||||
try [ {K,ec_dictionary:get(K,Dict)} || {K,_V} <- ToList ] of
|
|
||||||
List -> List
|
|
||||||
catch
|
|
||||||
throw:not_found -> key_not_found
|
|
||||||
end,
|
|
||||||
%% io:format("~p == ~p~n",[ToList,GetList]),
|
|
||||||
lists:sort(ToList) == lists:sort(GetList)
|
|
||||||
end).
|
|
||||||
|
|
||||||
prop_value_changes_after_update() ->
|
|
||||||
?FORALL({Dict, K1, V1, V2},
|
|
||||||
{sym_dict(),
|
|
||||||
key(), value(), value()},
|
|
||||||
begin
|
|
||||||
Dict1 = ec_dictionary:add(K1, V1, Dict),
|
|
||||||
Dict2 = ec_dictionary:add(K1, V2, Dict1),
|
|
||||||
V1 == ec_dictionary:get(K1, Dict1) andalso
|
|
||||||
V2 == ec_dictionary:get(K1, Dict2)
|
|
||||||
end).
|
|
||||||
|
|
||||||
prop_remove_removes_only_one_key() ->
|
|
||||||
?FORALL({Dict,K},
|
|
||||||
{sym_dict(),key()},
|
|
||||||
begin
|
|
||||||
{KeyGone,Dict2} = case ec_dictionary:has_key(K,Dict) of
|
|
||||||
true ->
|
|
||||||
D2 = ec_dictionary:remove(K,Dict),
|
|
||||||
{ec_dictionary:has_key(K,D2) == false,
|
|
||||||
D2};
|
|
||||||
false ->
|
|
||||||
{true,ec_dictionary:remove(K,Dict)}
|
|
||||||
end,
|
|
||||||
OtherEntries = [ KV || {K1,_} = KV <- ec_dictionary:to_list(Dict),
|
|
||||||
K1 /= K ],
|
|
||||||
KeyGone andalso
|
|
||||||
lists:sort(OtherEntries) == lists:sort(ec_dictionary:to_list(Dict2))
|
|
||||||
end).
|
|
||||||
|
|
||||||
prop_from_list() ->
|
|
||||||
?FORALL({Dict,DictType},
|
|
||||||
{sym_dict(),dictionary()},
|
|
||||||
begin
|
|
||||||
List = ec_dictionary:to_list(Dict),
|
|
||||||
D2 = ec_dictionary:from_list(DictType,List),
|
|
||||||
List2 = ec_dictionary:to_list(D2),
|
|
||||||
lists:sort(List) == lists:sort(List2)
|
|
||||||
end).
|
|
||||||
|
|
||||||
|
|
||||||
%%-----------------------------------------------------------------------------
|
|
||||||
%% Generators
|
|
||||||
%%-----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
key() -> union([integer(),atom()]).
|
|
||||||
|
|
||||||
value() -> union([integer(),atom(),binary(),boolean(),string()]).
|
|
||||||
|
|
||||||
|
|
||||||
my_dict() ->
|
|
||||||
?SIZED(N,dict(N)).
|
|
||||||
|
|
||||||
|
|
||||||
dict(0) ->
|
|
||||||
ec_dictionary:new(ec_gb_trees);
|
|
||||||
dict(N) ->
|
|
||||||
?LET(D,dict(N-1),
|
|
||||||
frequency([
|
|
||||||
{1, dict(0)},
|
|
||||||
{3, ec_dictionary:remove(integer(),D)},
|
|
||||||
{6, ec_dictionary:add(integer(),integer(),D)}
|
|
||||||
])).
|
|
||||||
|
|
||||||
sym_dict() ->
|
|
||||||
?SIZED(N,sym_dict(N)).
|
|
||||||
|
|
||||||
%% This symbolic generator will create a random instance of a ec_dictionary
|
|
||||||
%% that will be used in the properties.
|
|
||||||
sym_dict(0) ->
|
|
||||||
?LET(Dict,dictionary(),
|
|
||||||
{'$call',ec_dictionary,new,[Dict]});
|
|
||||||
sym_dict(N) ->
|
|
||||||
?LAZY(
|
|
||||||
frequency([
|
|
||||||
{1, sym_dict(0)},
|
|
||||||
{3, {'$call',ec_dictionary,remove,[key(),sym_dict(N-1)]}},
|
|
||||||
{6, {'$call',ec_dictionary,add,[value(),value(),sym_dict(N-1)]}}
|
|
||||||
])
|
|
||||||
).
|
|
||||||
|
|
||||||
dictionary() ->
|
|
||||||
union([ec_gb_trees,ec_assoc_list,ec_dict,ec_orddict]).
|
|
||||||
|
|
||||||
sym_dict2() ->
|
|
||||||
?SIZED(N,sym_dict2(N)).
|
|
||||||
|
|
||||||
sym_dict2(0) ->
|
|
||||||
{call,ec_dictionary,new,[ec_gb_trees]};
|
|
||||||
sym_dict2(N) ->
|
|
||||||
D = dict(N-1),
|
|
||||||
frequency([
|
|
||||||
{1, {call,ec_dictionary,remove,[integer(),D]}},
|
|
||||||
{2, {call,ec_dictionary,add,[integer(),integer(),D]}}
|
|
||||||
]).
|
|
||||||
|
|
||||||
|
|
||||||
%% For the tutorial.
|
|
||||||
gb_tree() ->
|
|
||||||
?SIZED(N,gb_tree(N)).
|
|
||||||
|
|
||||||
gb_tree(0) ->
|
|
||||||
gb_trees:empty();
|
|
||||||
gb_tree(N) ->
|
|
||||||
gb_trees:enter(key(),value(),gb_tree(N-1)).
|
|
||||||
|
|
84
test/ec_file_tests.erl
Normal file
84
test/ec_file_tests.erl
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
%%% @copyright 2024 Erlware, LLC.
|
||||||
|
-module(ec_file_tests).
|
||||||
|
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
|
setup_test() ->
|
||||||
|
Dir = ec_file:insecure_mkdtemp(),
|
||||||
|
ec_file:mkdir_path(Dir),
|
||||||
|
?assertMatch(false, ec_file:is_symlink(Dir)),
|
||||||
|
?assertMatch(true, filelib:is_dir(Dir)).
|
||||||
|
|
||||||
|
md5sum_test() ->
|
||||||
|
?assertMatch("cfcd208495d565ef66e7dff9f98764da", ec_file:md5sum("0")).
|
||||||
|
|
||||||
|
sha1sum_test() ->
|
||||||
|
?assertMatch("b6589fc6ab0dc82cf12099d1c2d40ab994e8410c", ec_file:sha1sum("0")).
|
||||||
|
|
||||||
|
file_test() ->
|
||||||
|
Dir = ec_file:insecure_mkdtemp(),
|
||||||
|
TermFile = filename:join(Dir, "ec_file/dir/file.term"),
|
||||||
|
TermFileCopy = filename:join(Dir, "ec_file/dircopy/file.term"),
|
||||||
|
filelib:ensure_dir(TermFile),
|
||||||
|
filelib:ensure_dir(TermFileCopy),
|
||||||
|
ec_file:write_term(TermFile, "term"),
|
||||||
|
?assertMatch({ok, <<"\"term\". ">>}, ec_file:read(TermFile)),
|
||||||
|
ec_file:copy(filename:dirname(TermFile),
|
||||||
|
filename:dirname(TermFileCopy),
|
||||||
|
[recursive]).
|
||||||
|
|
||||||
|
teardown_test() ->
|
||||||
|
Dir = ec_file:insecure_mkdtemp(),
|
||||||
|
ec_file:remove(Dir, [recursive]),
|
||||||
|
?assertMatch(false, filelib:is_dir(Dir)).
|
||||||
|
|
||||||
|
setup_base_and_target() ->
|
||||||
|
BaseDir = ec_file:insecure_mkdtemp(),
|
||||||
|
DummyContents = <<"This should be deleted">>,
|
||||||
|
SourceDir = filename:join([BaseDir, "source"]),
|
||||||
|
ok = file:make_dir(SourceDir),
|
||||||
|
Name1 = filename:join([SourceDir, "fileone"]),
|
||||||
|
Name2 = filename:join([SourceDir, "filetwo"]),
|
||||||
|
Name3 = filename:join([SourceDir, "filethree"]),
|
||||||
|
NoName = filename:join([SourceDir, "noname"]),
|
||||||
|
|
||||||
|
ok = file:write_file(Name1, DummyContents),
|
||||||
|
ok = file:write_file(Name2, DummyContents),
|
||||||
|
ok = file:write_file(Name3, DummyContents),
|
||||||
|
ok = file:write_file(NoName, DummyContents),
|
||||||
|
{BaseDir, SourceDir, {Name1, Name2, Name3, NoName}}.
|
||||||
|
|
||||||
|
exists_test() ->
|
||||||
|
BaseDir = ec_file:insecure_mkdtemp(),
|
||||||
|
SourceDir = filename:join([BaseDir, "source1"]),
|
||||||
|
NoName = filename:join([SourceDir, "noname"]),
|
||||||
|
ok = file:make_dir(SourceDir),
|
||||||
|
Name1 = filename:join([SourceDir, "fileone"]),
|
||||||
|
ok = file:write_file(Name1, <<"Testn">>),
|
||||||
|
?assertMatch(true, ec_file:exists(Name1)),
|
||||||
|
?assertMatch(false, ec_file:exists(NoName)).
|
||||||
|
|
||||||
|
real_path_test() ->
|
||||||
|
BaseDir = "foo",
|
||||||
|
Dir = filename:absname(filename:join(BaseDir, "source1")),
|
||||||
|
LinkDir = filename:join([BaseDir, "link"]),
|
||||||
|
ok = ec_file:mkdir_p(Dir),
|
||||||
|
file:make_symlink(Dir, LinkDir),
|
||||||
|
?assertEqual(Dir, ec_file:real_dir_path(LinkDir)),
|
||||||
|
?assertEqual(directory, ec_file:type(Dir)),
|
||||||
|
?assertEqual(symlink, ec_file:type(LinkDir)),
|
||||||
|
TermFile = filename:join(BaseDir, "test_file"),
|
||||||
|
ok = ec_file:write_term(TermFile, foo),
|
||||||
|
?assertEqual(file, ec_file:type(TermFile)),
|
||||||
|
?assertEqual(true, ec_file:is_symlink(LinkDir)),
|
||||||
|
?assertEqual(false, ec_file:is_symlink(Dir)).
|
||||||
|
|
||||||
|
find_test() ->
|
||||||
|
%% Create a directory in /tmp for the test. Clean everything afterwards
|
||||||
|
{BaseDir, _SourceDir, {Name1, Name2, Name3, _NoName}} = setup_base_and_target(),
|
||||||
|
Result = ec_file:find(BaseDir, "file[a-z]+\$"),
|
||||||
|
?assertMatch(3, erlang:length(Result)),
|
||||||
|
?assertEqual(true, lists:member(Name1, Result)),
|
||||||
|
?assertEqual(true, lists:member(Name2, Result)),
|
||||||
|
?assertEqual(true, lists:member(Name3, Result)),
|
||||||
|
ec_file:remove(BaseDir, [recursive]).
|
67
test/ec_gb_trees_tests.erl
Normal file
67
test/ec_gb_trees_tests.erl
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
%%% @copyright 2024 Erlware, LLC.
|
||||||
|
-module(ec_gb_trees_tests).
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
|
%% For me unit testing initially is about covering the obvious case. A
|
||||||
|
%% check to make sure that what you expect the tested functionality to
|
||||||
|
%% do, it actually does. As time goes on and people detect bugs you
|
||||||
|
%% add tests for those specific problems to the unit test suit.
|
||||||
|
%%
|
||||||
|
%% However, when getting started you can only test your basic
|
||||||
|
%% expectations. So here are the expectations I have for the add
|
||||||
|
%% functionality.
|
||||||
|
%%
|
||||||
|
%% 1) I can put arbitrary terms into the dictionary as keys
|
||||||
|
%% 2) I can put arbitrary terms into the dictionary as values
|
||||||
|
%% 3) When I put a value in the dictionary by a key, I can retrieve
|
||||||
|
%% that same value
|
||||||
|
%% 4) When I put a different value in the dictionary by key it does
|
||||||
|
%% not change other key value pairs.
|
||||||
|
%% 5) When I update a value the new value in available by the new key
|
||||||
|
%% 6) When a value does not exist a not found exception is created
|
||||||
|
|
||||||
|
add_test() ->
|
||||||
|
Dict0 = ec_dictionary:new(ec_gb_trees),
|
||||||
|
|
||||||
|
Key1 = foo,
|
||||||
|
Key2 = [1, 3],
|
||||||
|
Key3 = {"super"},
|
||||||
|
Key4 = <<"fabulous">>,
|
||||||
|
Key5 = {"Sona", 2, <<"Zuper">>},
|
||||||
|
|
||||||
|
Value1 = Key5,
|
||||||
|
Value2 = Key4,
|
||||||
|
Value3 = Key2,
|
||||||
|
Value4 = Key3,
|
||||||
|
Value5 = Key1,
|
||||||
|
|
||||||
|
Dict01 = ec_dictionary:add(Key1, Value1, Dict0),
|
||||||
|
Dict02 = ec_dictionary:add(Key3, Value3,
|
||||||
|
ec_dictionary:add(Key2, Value2,
|
||||||
|
Dict01)),
|
||||||
|
Dict1 =
|
||||||
|
ec_dictionary:add(Key5, Value5,
|
||||||
|
ec_dictionary:add(Key4, Value4,
|
||||||
|
Dict02)),
|
||||||
|
|
||||||
|
?assertMatch(Value1, ec_dictionary:get(Key1, Dict1)),
|
||||||
|
?assertMatch(Value2, ec_dictionary:get(Key2, Dict1)),
|
||||||
|
?assertMatch(Value3, ec_dictionary:get(Key3, Dict1)),
|
||||||
|
?assertMatch(Value4, ec_dictionary:get(Key4, Dict1)),
|
||||||
|
?assertMatch(Value5, ec_dictionary:get(Key5, Dict1)),
|
||||||
|
|
||||||
|
|
||||||
|
Dict2 = ec_dictionary:add(Key3, Value5,
|
||||||
|
ec_dictionary:add(Key2, Value4, Dict1)),
|
||||||
|
|
||||||
|
|
||||||
|
?assertMatch(Value1, ec_dictionary:get(Key1, Dict2)),
|
||||||
|
?assertMatch(Value4, ec_dictionary:get(Key2, Dict2)),
|
||||||
|
?assertMatch(Value5, ec_dictionary:get(Key3, Dict2)),
|
||||||
|
?assertMatch(Value4, ec_dictionary:get(Key4, Dict2)),
|
||||||
|
?assertMatch(Value5, ec_dictionary:get(Key5, Dict2)),
|
||||||
|
|
||||||
|
|
||||||
|
?assertThrow(not_found, ec_dictionary:get(should_blow_up, Dict2)),
|
||||||
|
?assertThrow(not_found, ec_dictionary:get("This should blow up too",
|
||||||
|
Dict2)).
|
13
test/ec_git_vsn_tests.erl
Normal file
13
test/ec_git_vsn_tests.erl
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
%%% @copyright 2024 Erlware, LLC.
|
||||||
|
-module(ec_git_vsn_tests).
|
||||||
|
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
|
parse_tags_test() ->
|
||||||
|
?assertEqual({undefined, ""}, ec_git_vsn:parse_tags("a.b.c")).
|
||||||
|
|
||||||
|
get_patch_count_test() ->
|
||||||
|
?assertEqual(0, ec_git_vsn:get_patch_count("a.b.c")).
|
||||||
|
|
||||||
|
collect_default_refcount_test() ->
|
||||||
|
?assertMatch({"", _, _}, ec_git_vsn:collect_default_refcount("a.b.c")).
|
172
test/ec_lists_tests.erl
Normal file
172
test/ec_lists_tests.erl
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
%%% @copyright 2024 Erlware, LLC.
|
||||||
|
-module(ec_lists_tests).
|
||||||
|
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
|
find1_test() ->
|
||||||
|
TestData = [1, 2, 3, 4, 5, 6],
|
||||||
|
Result = ec_lists:find(fun(5) ->
|
||||||
|
true;
|
||||||
|
(_) ->
|
||||||
|
false
|
||||||
|
end,
|
||||||
|
TestData),
|
||||||
|
?assertMatch({ok, 5}, Result),
|
||||||
|
|
||||||
|
Result2 = ec_lists:find(fun(37) ->
|
||||||
|
true;
|
||||||
|
(_) ->
|
||||||
|
false
|
||||||
|
end,
|
||||||
|
TestData),
|
||||||
|
?assertMatch(error, Result2).
|
||||||
|
|
||||||
|
find2_test() ->
|
||||||
|
TestData = ["one", "two", "three", "four", "five", "six"],
|
||||||
|
Result = ec_lists:find(fun("five") ->
|
||||||
|
true;
|
||||||
|
(_) ->
|
||||||
|
false
|
||||||
|
end,
|
||||||
|
TestData),
|
||||||
|
?assertMatch({ok, "five"}, Result),
|
||||||
|
|
||||||
|
Result2 = ec_lists:find(fun(super_duper) ->
|
||||||
|
true;
|
||||||
|
(_) ->
|
||||||
|
false
|
||||||
|
end,
|
||||||
|
TestData),
|
||||||
|
?assertMatch(error, Result2).
|
||||||
|
|
||||||
|
find3_test() ->
|
||||||
|
TestData = [{"one", 1}, {"two", 2}, {"three", 3}, {"four", 5}, {"five", 5},
|
||||||
|
{"six", 6}],
|
||||||
|
Result = ec_lists:find(fun({"one", 1}) ->
|
||||||
|
true;
|
||||||
|
(_) ->
|
||||||
|
false
|
||||||
|
end,
|
||||||
|
TestData),
|
||||||
|
?assertMatch({ok, {"one", 1}}, Result),
|
||||||
|
|
||||||
|
Result2 = ec_lists:find(fun([fo, bar, baz]) ->
|
||||||
|
true;
|
||||||
|
({"onehundred", 100}) ->
|
||||||
|
true;
|
||||||
|
(_) ->
|
||||||
|
false
|
||||||
|
end,
|
||||||
|
TestData),
|
||||||
|
?assertMatch(error, Result2).
|
||||||
|
|
||||||
|
fetch1_test() ->
|
||||||
|
TestData = [1, 2, 3, 4, 5, 6],
|
||||||
|
Result = ec_lists:fetch(fun(5) ->
|
||||||
|
true;
|
||||||
|
(_) ->
|
||||||
|
false
|
||||||
|
end,
|
||||||
|
TestData),
|
||||||
|
?assertMatch(5, Result),
|
||||||
|
|
||||||
|
?assertThrow(not_found,
|
||||||
|
ec_lists:fetch(fun(37) ->
|
||||||
|
true;
|
||||||
|
(_) ->
|
||||||
|
false
|
||||||
|
end,
|
||||||
|
TestData)).
|
||||||
|
|
||||||
|
fetch2_test() ->
|
||||||
|
TestData = ["one", "two", "three", "four", "five", "six"],
|
||||||
|
Result = ec_lists:fetch(fun("five") ->
|
||||||
|
true;
|
||||||
|
(_) ->
|
||||||
|
false
|
||||||
|
end,
|
||||||
|
TestData),
|
||||||
|
?assertMatch("five", Result),
|
||||||
|
|
||||||
|
?assertThrow(not_found,
|
||||||
|
ec_lists:fetch(fun(super_duper) ->
|
||||||
|
true;
|
||||||
|
(_) ->
|
||||||
|
false
|
||||||
|
end,
|
||||||
|
TestData)).
|
||||||
|
|
||||||
|
fetch3_test() ->
|
||||||
|
TestData = [{"one", 1}, {"two", 2}, {"three", 3}, {"four", 5}, {"five", 5},
|
||||||
|
{"six", 6}],
|
||||||
|
Result = ec_lists:fetch(fun({"one", 1}) ->
|
||||||
|
true;
|
||||||
|
(_) ->
|
||||||
|
false
|
||||||
|
end,
|
||||||
|
TestData),
|
||||||
|
?assertMatch({"one", 1}, Result),
|
||||||
|
|
||||||
|
?assertThrow(not_found,
|
||||||
|
ec_lists:fetch(fun([fo, bar, baz]) ->
|
||||||
|
true;
|
||||||
|
({"onehundred", 100}) ->
|
||||||
|
true;
|
||||||
|
(_) ->
|
||||||
|
false
|
||||||
|
end,
|
||||||
|
TestData)).
|
||||||
|
|
||||||
|
search1_test() ->
|
||||||
|
TestData = [1, 2, 3, 4, 5, 6],
|
||||||
|
Result = ec_lists:search(fun(5) ->
|
||||||
|
{ok, 5};
|
||||||
|
(_) ->
|
||||||
|
not_found
|
||||||
|
end,
|
||||||
|
TestData),
|
||||||
|
?assertMatch({ok, 5, 5}, Result),
|
||||||
|
|
||||||
|
Result2 = ec_lists:search(fun(37) ->
|
||||||
|
{ok, 37};
|
||||||
|
(_) ->
|
||||||
|
not_found
|
||||||
|
end,
|
||||||
|
TestData),
|
||||||
|
?assertMatch(not_found, Result2).
|
||||||
|
|
||||||
|
search2_test() ->
|
||||||
|
TestData = [1, 2, 3, 4, 5, 6],
|
||||||
|
Result = ec_lists:search(fun(1) ->
|
||||||
|
{ok, 10};
|
||||||
|
(_) ->
|
||||||
|
not_found
|
||||||
|
end,
|
||||||
|
TestData),
|
||||||
|
?assertMatch({ok, 10, 1}, Result),
|
||||||
|
|
||||||
|
Result2 = ec_lists:search(fun(6) ->
|
||||||
|
{ok, 37};
|
||||||
|
(_) ->
|
||||||
|
not_found
|
||||||
|
end,
|
||||||
|
TestData),
|
||||||
|
?assertMatch({ok, 37, 6}, Result2).
|
||||||
|
|
||||||
|
search3_test() ->
|
||||||
|
TestData = [1, 2, 3, 4, 5, 6],
|
||||||
|
Result = ec_lists:search(fun(10) ->
|
||||||
|
{ok, 10};
|
||||||
|
(_) ->
|
||||||
|
not_found
|
||||||
|
end,
|
||||||
|
TestData),
|
||||||
|
?assertMatch(not_found, Result),
|
||||||
|
|
||||||
|
Result2 = ec_lists:search(fun(-1) ->
|
||||||
|
{ok, 37};
|
||||||
|
(_) ->
|
||||||
|
not_found
|
||||||
|
end,
|
||||||
|
TestData),
|
||||||
|
?assertMatch(not_found, Result2).
|
84
test/ec_plists_tests.erl
Normal file
84
test/ec_plists_tests.erl
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
%%% @copyright Erlware, LLC.
|
||||||
|
-module(ec_plists_tests).
|
||||||
|
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% Tests
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
map_good_test() ->
|
||||||
|
Results = ec_plists:map(fun(_) ->
|
||||||
|
ok
|
||||||
|
end,
|
||||||
|
lists:seq(1, 5)),
|
||||||
|
?assertMatch([ok, ok, ok, ok, ok],
|
||||||
|
Results).
|
||||||
|
|
||||||
|
ftmap_good_test() ->
|
||||||
|
Results = ec_plists:ftmap(fun(_) ->
|
||||||
|
ok
|
||||||
|
end,
|
||||||
|
lists:seq(1, 3)),
|
||||||
|
?assertMatch([{value, ok}, {value, ok}, {value, ok}],
|
||||||
|
Results).
|
||||||
|
|
||||||
|
filter_good_test() ->
|
||||||
|
Results = ec_plists:filter(fun(X) ->
|
||||||
|
X == show
|
||||||
|
end,
|
||||||
|
[show, show, remove]),
|
||||||
|
?assertMatch([show, show],
|
||||||
|
Results).
|
||||||
|
|
||||||
|
map_timeout_test() ->
|
||||||
|
?assertExit(timeout,
|
||||||
|
ec_plists:map(fun(T) ->
|
||||||
|
timer:sleep(T),
|
||||||
|
T
|
||||||
|
end,
|
||||||
|
[1, 100], {timeout, 10})).
|
||||||
|
|
||||||
|
ftmap_timeout_test() ->
|
||||||
|
?assertExit(timeout,
|
||||||
|
ec_plists:ftmap(fun(X) ->
|
||||||
|
timer:sleep(X),
|
||||||
|
true
|
||||||
|
end,
|
||||||
|
[100, 1], {timeout, 10})).
|
||||||
|
|
||||||
|
filter_timeout_test() ->
|
||||||
|
?assertExit(timeout,
|
||||||
|
ec_plists:filter(fun(T) ->
|
||||||
|
timer:sleep(T),
|
||||||
|
T == 1
|
||||||
|
end,
|
||||||
|
[1, 100], {timeout, 10})).
|
||||||
|
|
||||||
|
map_bad_test() ->
|
||||||
|
?assertExit({{nocatch,test_exception}, _},
|
||||||
|
ec_plists:map(fun(_) ->
|
||||||
|
erlang:throw(test_exception)
|
||||||
|
end,
|
||||||
|
lists:seq(1, 5))).
|
||||||
|
|
||||||
|
|
||||||
|
ftmap_bad_test() ->
|
||||||
|
Results =
|
||||||
|
ec_plists:ftmap(fun(2) ->
|
||||||
|
erlang:throw(test_exception);
|
||||||
|
(N) ->
|
||||||
|
N
|
||||||
|
end,
|
||||||
|
lists:seq(1, 5)),
|
||||||
|
?assertMatch([{value, 1}, {error,{throw,test_exception}}, {value, 3},
|
||||||
|
{value, 4}, {value, 5}] , Results).
|
||||||
|
|
||||||
|
external_down_message_test() ->
|
||||||
|
erlang:spawn_monitor(fun() -> erlang:throw(fail) end),
|
||||||
|
Results = ec_plists:map(fun(_) ->
|
||||||
|
ok
|
||||||
|
end,
|
||||||
|
lists:seq(1, 5)),
|
||||||
|
?assertMatch([ok, ok, ok, ok, ok],
|
||||||
|
Results).
|
447
test/ec_semver_tests.erl
Normal file
447
test/ec_semver_tests.erl
Normal file
|
@ -0,0 +1,447 @@
|
||||||
|
%%% @copyright 2024 Erlware, LLC.
|
||||||
|
-module(ec_semver_tests).
|
||||||
|
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
|
eql_test() ->
|
||||||
|
?assertMatch(true, ec_semver:eql("1.0.0-alpha",
|
||||||
|
"1.0.0-alpha")),
|
||||||
|
?assertMatch(true, ec_semver:eql("v1.0.0-alpha",
|
||||||
|
"1.0.0-alpha")),
|
||||||
|
?assertMatch(true, ec_semver:eql("1",
|
||||||
|
"1.0.0")),
|
||||||
|
?assertMatch(true, ec_semver:eql("v1",
|
||||||
|
"v1.0.0")),
|
||||||
|
?assertMatch(true, ec_semver:eql("1.0",
|
||||||
|
"1.0.0")),
|
||||||
|
?assertMatch(true, ec_semver:eql("1.0.0",
|
||||||
|
"1")),
|
||||||
|
?assertMatch(true, ec_semver:eql("1.0.0.0",
|
||||||
|
"1")),
|
||||||
|
?assertMatch(true, ec_semver:eql("1.0+alpha.1",
|
||||||
|
"1.0.0+alpha.1")),
|
||||||
|
?assertMatch(true, ec_semver:eql("1.0-alpha.1+build.1",
|
||||||
|
"1.0.0-alpha.1+build.1")),
|
||||||
|
?assertMatch(true, ec_semver:eql("1.0-alpha.1+build.1",
|
||||||
|
"1.0.0.0-alpha.1+build.1")),
|
||||||
|
?assertMatch(true, ec_semver:eql("1.0-alpha.1+build.1",
|
||||||
|
"v1.0.0.0-alpha.1+build.1")),
|
||||||
|
?assertMatch(true, ec_semver:eql("1.0-pre-alpha.1",
|
||||||
|
"1.0.0-pre-alpha.1")),
|
||||||
|
?assertMatch(true, ec_semver:eql("aa", "aa")),
|
||||||
|
?assertMatch(true, ec_semver:eql("AA.BB", "AA.BB")),
|
||||||
|
?assertMatch(true, ec_semver:eql("BBB-super", "BBB-super")),
|
||||||
|
?assertMatch(true, not ec_semver:eql("1.0.0",
|
||||||
|
"1.0.1")),
|
||||||
|
?assertMatch(true, not ec_semver:eql("1.0.0-alpha",
|
||||||
|
"1.0.1+alpha")),
|
||||||
|
?assertMatch(true, not ec_semver:eql("1.0.0+build.1",
|
||||||
|
"1.0.1+build.2")),
|
||||||
|
?assertMatch(true, not ec_semver:eql("1.0.0.0+build.1",
|
||||||
|
"1.0.0.1+build.2")),
|
||||||
|
?assertMatch(true, not ec_semver:eql("FFF", "BBB")),
|
||||||
|
?assertMatch(true, not ec_semver:eql("1", "1BBBB")).
|
||||||
|
|
||||||
|
gt_test() ->
|
||||||
|
?assertMatch(true, ec_semver:gt("1.0.0-alpha.1",
|
||||||
|
"1.0.0-alpha")),
|
||||||
|
?assertMatch(true, ec_semver:gt("1.0.0.1-alpha.1",
|
||||||
|
"1.0.0.1-alpha")),
|
||||||
|
?assertMatch(true, ec_semver:gt("1.0.0.4-alpha.1",
|
||||||
|
"1.0.0.2-alpha")),
|
||||||
|
?assertMatch(true, ec_semver:gt("1.0.0.0-alpha.1",
|
||||||
|
"1.0.0-alpha")),
|
||||||
|
?assertMatch(true, ec_semver:gt("1.0.0-beta.2",
|
||||||
|
"1.0.0-alpha.1")),
|
||||||
|
?assertMatch(true, ec_semver:gt("1.0.0-beta.11",
|
||||||
|
"1.0.0-beta.2")),
|
||||||
|
?assertMatch(true, ec_semver:gt("1.0.0-pre-alpha.14",
|
||||||
|
"1.0.0-pre-alpha.3")),
|
||||||
|
?assertMatch(true, ec_semver:gt("1.0.0-beta.11",
|
||||||
|
"1.0.0.0-beta.2")),
|
||||||
|
?assertMatch(true, ec_semver:gt("1.0.0-rc.1", "1.0.0-beta.11")),
|
||||||
|
?assertMatch(true, ec_semver:gt("1.0.0-rc.1+build.1", "1.0.0-rc.1")),
|
||||||
|
?assertMatch(true, ec_semver:gt("1.0.0", "1.0.0-rc.1+build.1")),
|
||||||
|
?assertMatch(true, ec_semver:gt("1.0.0+0.3.7", "1.0.0")),
|
||||||
|
?assertMatch(true, ec_semver:gt("1.3.7+build", "1.0.0+0.3.7")),
|
||||||
|
?assertMatch(true, ec_semver:gt("1.3.7+build.2.b8f12d7",
|
||||||
|
"1.3.7+build")),
|
||||||
|
?assertMatch(true, ec_semver:gt("1.3.7+build.2.b8f12d7",
|
||||||
|
"1.3.7.0+build")),
|
||||||
|
?assertMatch(true, ec_semver:gt("1.3.7+build.11.e0f985a",
|
||||||
|
"1.3.7+build.2.b8f12d7")),
|
||||||
|
?assertMatch(true, ec_semver:gt("aa.cc",
|
||||||
|
"aa.bb")),
|
||||||
|
?assertMatch(true, not ec_semver:gt("1.0.0-alpha",
|
||||||
|
"1.0.0-alpha.1")),
|
||||||
|
?assertMatch(true, not ec_semver:gt("1.0.0-alpha",
|
||||||
|
"1.0.0.0-alpha.1")),
|
||||||
|
?assertMatch(true, not ec_semver:gt("1.0.0-alpha.1",
|
||||||
|
"1.0.0-beta.2")),
|
||||||
|
?assertMatch(true, not ec_semver:gt("1.0.0-beta.2",
|
||||||
|
"1.0.0-beta.11")),
|
||||||
|
?assertMatch(true, not ec_semver:gt("1.0.0-beta.11",
|
||||||
|
"1.0.0-rc.1")),
|
||||||
|
?assertMatch(true, not ec_semver:gt("1.0.0-pre-alpha.3",
|
||||||
|
"1.0.0-pre-alpha.14")),
|
||||||
|
?assertMatch(true, not ec_semver:gt("1.0.0-rc.1",
|
||||||
|
"1.0.0-rc.1+build.1")),
|
||||||
|
?assertMatch(true, not ec_semver:gt("1.0.0-rc.1+build.1",
|
||||||
|
"1.0.0")),
|
||||||
|
?assertMatch(true, not ec_semver:gt("1.0.0",
|
||||||
|
"1.0.0+0.3.7")),
|
||||||
|
?assertMatch(true, not ec_semver:gt("1.0.0+0.3.7",
|
||||||
|
"1.3.7+build")),
|
||||||
|
?assertMatch(true, not ec_semver:gt("1.3.7+build",
|
||||||
|
"1.3.7+build.2.b8f12d7")),
|
||||||
|
?assertMatch(true, not ec_semver:gt("1.3.7+build.2.b8f12d7",
|
||||||
|
"1.3.7+build.11.e0f985a")),
|
||||||
|
?assertMatch(true, not ec_semver:gt("1.0.0-alpha",
|
||||||
|
"1.0.0-alpha")),
|
||||||
|
?assertMatch(true, not ec_semver:gt("1",
|
||||||
|
"1.0.0")),
|
||||||
|
?assertMatch(true, not ec_semver:gt("aa.bb",
|
||||||
|
"aa.bb")),
|
||||||
|
?assertMatch(true, not ec_semver:gt("aa.cc",
|
||||||
|
"aa.dd")),
|
||||||
|
?assertMatch(true, not ec_semver:gt("1.0",
|
||||||
|
"1.0.0")),
|
||||||
|
?assertMatch(true, not ec_semver:gt("1.0.0",
|
||||||
|
"1")),
|
||||||
|
?assertMatch(true, not ec_semver:gt("1.0+alpha.1",
|
||||||
|
"1.0.0+alpha.1")),
|
||||||
|
?assertMatch(true, not ec_semver:gt("1.0-alpha.1+build.1",
|
||||||
|
"1.0.0-alpha.1+build.1")).
|
||||||
|
|
||||||
|
lt_test() ->
|
||||||
|
?assertMatch(true, ec_semver:lt("1.0.0-alpha",
|
||||||
|
"1.0.0-alpha.1")),
|
||||||
|
?assertMatch(true, ec_semver:lt("1.0.0-alpha",
|
||||||
|
"1.0.0.0-alpha.1")),
|
||||||
|
?assertMatch(true, ec_semver:lt("1.0.0-alpha.1",
|
||||||
|
"1.0.0-beta.2")),
|
||||||
|
?assertMatch(true, ec_semver:lt("1.0.0-beta.2",
|
||||||
|
"1.0.0-beta.11")),
|
||||||
|
?assertMatch(true, ec_semver:lt("1.0.0-pre-alpha.3",
|
||||||
|
"1.0.0-pre-alpha.14")),
|
||||||
|
?assertMatch(true, ec_semver:lt("1.0.0-beta.11",
|
||||||
|
"1.0.0-rc.1")),
|
||||||
|
?assertMatch(true, ec_semver:lt("1.0.0.1-beta.11",
|
||||||
|
"1.0.0.1-rc.1")),
|
||||||
|
?assertMatch(true, ec_semver:lt("1.0.0-rc.1",
|
||||||
|
"1.0.0-rc.1+build.1")),
|
||||||
|
?assertMatch(true, ec_semver:lt("1.0.0-rc.1+build.1",
|
||||||
|
"1.0.0")),
|
||||||
|
?assertMatch(true, ec_semver:lt("1.0.0",
|
||||||
|
"1.0.0+0.3.7")),
|
||||||
|
?assertMatch(true, ec_semver:lt("1.0.0+0.3.7",
|
||||||
|
"1.3.7+build")),
|
||||||
|
?assertMatch(true, ec_semver:lt("1.3.7+build",
|
||||||
|
"1.3.7+build.2.b8f12d7")),
|
||||||
|
?assertMatch(true, ec_semver:lt("1.3.7+build.2.b8f12d7",
|
||||||
|
"1.3.7+build.11.e0f985a")),
|
||||||
|
?assertMatch(true, not ec_semver:lt("1.0.0-alpha",
|
||||||
|
"1.0.0-alpha")),
|
||||||
|
?assertMatch(true, not ec_semver:lt("1",
|
||||||
|
"1.0.0")),
|
||||||
|
?assertMatch(true, ec_semver:lt("1",
|
||||||
|
"1.0.0.1")),
|
||||||
|
?assertMatch(true, ec_semver:lt("AA.DD",
|
||||||
|
"AA.EE")),
|
||||||
|
?assertMatch(true, not ec_semver:lt("1.0",
|
||||||
|
"1.0.0")),
|
||||||
|
?assertMatch(true, not ec_semver:lt("1.0.0.0",
|
||||||
|
"1")),
|
||||||
|
?assertMatch(true, not ec_semver:lt("1.0+alpha.1",
|
||||||
|
"1.0.0+alpha.1")),
|
||||||
|
?assertMatch(true, not ec_semver:lt("AA.DD", "AA.CC")),
|
||||||
|
?assertMatch(true, not ec_semver:lt("1.0-alpha.1+build.1",
|
||||||
|
"1.0.0-alpha.1+build.1")),
|
||||||
|
?assertMatch(true, not ec_semver:lt("1.0.0-alpha.1",
|
||||||
|
"1.0.0-alpha")),
|
||||||
|
?assertMatch(true, not ec_semver:lt("1.0.0-beta.2",
|
||||||
|
"1.0.0-alpha.1")),
|
||||||
|
?assertMatch(true, not ec_semver:lt("1.0.0-beta.11",
|
||||||
|
"1.0.0-beta.2")),
|
||||||
|
?assertMatch(true, not ec_semver:lt("1.0.0-pre-alpha.14",
|
||||||
|
"1.0.0-pre-alpha.3")),
|
||||||
|
?assertMatch(true, not ec_semver:lt("1.0.0-rc.1", "1.0.0-beta.11")),
|
||||||
|
?assertMatch(true, not ec_semver:lt("1.0.0-rc.1+build.1", "1.0.0-rc.1")),
|
||||||
|
?assertMatch(true, not ec_semver:lt("1.0.0", "1.0.0-rc.1+build.1")),
|
||||||
|
?assertMatch(true, not ec_semver:lt("1.0.0+0.3.7", "1.0.0")),
|
||||||
|
?assertMatch(true, not ec_semver:lt("1.3.7+build", "1.0.0+0.3.7")),
|
||||||
|
?assertMatch(true, not ec_semver:lt("1.3.7+build.2.b8f12d7",
|
||||||
|
"1.3.7+build")),
|
||||||
|
?assertMatch(true, not ec_semver:lt("1.3.7+build.11.e0f985a",
|
||||||
|
"1.3.7+build.2.b8f12d7")).
|
||||||
|
|
||||||
|
gte_test() ->
|
||||||
|
?assertMatch(true, ec_semver:gte("1.0.0-alpha",
|
||||||
|
"1.0.0-alpha")),
|
||||||
|
|
||||||
|
?assertMatch(true, ec_semver:gte("1",
|
||||||
|
"1.0.0")),
|
||||||
|
|
||||||
|
?assertMatch(true, ec_semver:gte("1.0",
|
||||||
|
"1.0.0")),
|
||||||
|
|
||||||
|
?assertMatch(true, ec_semver:gte("1.0.0",
|
||||||
|
"1")),
|
||||||
|
|
||||||
|
?assertMatch(true, ec_semver:gte("1.0.0.0",
|
||||||
|
"1")),
|
||||||
|
|
||||||
|
?assertMatch(true, ec_semver:gte("1.0+alpha.1",
|
||||||
|
"1.0.0+alpha.1")),
|
||||||
|
|
||||||
|
?assertMatch(true, ec_semver:gte("1.0-alpha.1+build.1",
|
||||||
|
"1.0.0-alpha.1+build.1")),
|
||||||
|
|
||||||
|
?assertMatch(true, ec_semver:gte("1.0.0-alpha.1+build.1",
|
||||||
|
"1.0.0.0-alpha.1+build.1")),
|
||||||
|
?assertMatch(true, ec_semver:gte("1.0.0-alpha.1",
|
||||||
|
"1.0.0-alpha")),
|
||||||
|
?assertMatch(true, ec_semver:gte("1.0.0-pre-alpha.2",
|
||||||
|
"1.0.0-pre-alpha")),
|
||||||
|
?assertMatch(true, ec_semver:gte("1.0.0-beta.2",
|
||||||
|
"1.0.0-alpha.1")),
|
||||||
|
?assertMatch(true, ec_semver:gte("1.0.0-beta.11",
|
||||||
|
"1.0.0-beta.2")),
|
||||||
|
?assertMatch(true, ec_semver:gte("aa.bb", "aa.bb")),
|
||||||
|
?assertMatch(true, ec_semver:gte("dd", "aa")),
|
||||||
|
?assertMatch(true, ec_semver:gte("1.0.0-rc.1", "1.0.0-beta.11")),
|
||||||
|
?assertMatch(true, ec_semver:gte("1.0.0-rc.1+build.1", "1.0.0-rc.1")),
|
||||||
|
?assertMatch(true, ec_semver:gte("1.0.0", "1.0.0-rc.1+build.1")),
|
||||||
|
?assertMatch(true, ec_semver:gte("1.0.0+0.3.7", "1.0.0")),
|
||||||
|
?assertMatch(true, ec_semver:gte("1.3.7+build", "1.0.0+0.3.7")),
|
||||||
|
?assertMatch(true, ec_semver:gte("1.3.7+build.2.b8f12d7",
|
||||||
|
"1.3.7+build")),
|
||||||
|
?assertMatch(true, ec_semver:gte("1.3.7+build.11.e0f985a",
|
||||||
|
"1.3.7+build.2.b8f12d7")),
|
||||||
|
?assertMatch(true, not ec_semver:gte("1.0.0-alpha",
|
||||||
|
"1.0.0-alpha.1")),
|
||||||
|
?assertMatch(true, not ec_semver:gte("1.0.0-pre-alpha",
|
||||||
|
"1.0.0-pre-alpha.1")),
|
||||||
|
?assertMatch(true, not ec_semver:gte("CC", "DD")),
|
||||||
|
?assertMatch(true, not ec_semver:gte("1.0.0-alpha.1",
|
||||||
|
"1.0.0-beta.2")),
|
||||||
|
?assertMatch(true, not ec_semver:gte("1.0.0-beta.2",
|
||||||
|
"1.0.0-beta.11")),
|
||||||
|
?assertMatch(true, not ec_semver:gte("1.0.0-beta.11",
|
||||||
|
"1.0.0-rc.1")),
|
||||||
|
?assertMatch(true, not ec_semver:gte("1.0.0-rc.1",
|
||||||
|
"1.0.0-rc.1+build.1")),
|
||||||
|
?assertMatch(true, not ec_semver:gte("1.0.0-rc.1+build.1",
|
||||||
|
"1.0.0")),
|
||||||
|
?assertMatch(true, not ec_semver:gte("1.0.0",
|
||||||
|
"1.0.0+0.3.7")),
|
||||||
|
?assertMatch(true, not ec_semver:gte("1.0.0+0.3.7",
|
||||||
|
"1.3.7+build")),
|
||||||
|
?assertMatch(true, not ec_semver:gte("1.0.0",
|
||||||
|
"1.0.0+build.1")),
|
||||||
|
?assertMatch(true, not ec_semver:gte("1.3.7+build",
|
||||||
|
"1.3.7+build.2.b8f12d7")),
|
||||||
|
?assertMatch(true, not ec_semver:gte("1.3.7+build.2.b8f12d7",
|
||||||
|
"1.3.7+build.11.e0f985a")).
|
||||||
|
lte_test() ->
|
||||||
|
?assertMatch(true, ec_semver:lte("1.0.0-alpha",
|
||||||
|
"1.0.0-alpha.1")),
|
||||||
|
?assertMatch(true, ec_semver:lte("1.0.0-alpha.1",
|
||||||
|
"1.0.0-beta.2")),
|
||||||
|
?assertMatch(true, ec_semver:lte("1.0.0-beta.2",
|
||||||
|
"1.0.0-beta.11")),
|
||||||
|
?assertMatch(true, ec_semver:lte("1.0.0-pre-alpha.2",
|
||||||
|
"1.0.0-pre-alpha.11")),
|
||||||
|
?assertMatch(true, ec_semver:lte("1.0.0-beta.11",
|
||||||
|
"1.0.0-rc.1")),
|
||||||
|
?assertMatch(true, ec_semver:lte("1.0.0-rc.1",
|
||||||
|
"1.0.0-rc.1+build.1")),
|
||||||
|
?assertMatch(true, ec_semver:lte("1.0.0-rc.1+build.1",
|
||||||
|
"1.0.0")),
|
||||||
|
?assertMatch(true, ec_semver:lte("1.0.0",
|
||||||
|
"1.0.0+0.3.7")),
|
||||||
|
?assertMatch(true, ec_semver:lte("1.0.0+0.3.7",
|
||||||
|
"1.3.7+build")),
|
||||||
|
?assertMatch(true, ec_semver:lte("1.3.7+build",
|
||||||
|
"1.3.7+build.2.b8f12d7")),
|
||||||
|
?assertMatch(true, ec_semver:lte("1.3.7+build.2.b8f12d7",
|
||||||
|
"1.3.7+build.11.e0f985a")),
|
||||||
|
?assertMatch(true, ec_semver:lte("1.0.0-alpha",
|
||||||
|
"1.0.0-alpha")),
|
||||||
|
?assertMatch(true, ec_semver:lte("1",
|
||||||
|
"1.0.0")),
|
||||||
|
?assertMatch(true, ec_semver:lte("1.0",
|
||||||
|
"1.0.0")),
|
||||||
|
?assertMatch(true, ec_semver:lte("1.0.0",
|
||||||
|
"1")),
|
||||||
|
?assertMatch(true, ec_semver:lte("1.0+alpha.1",
|
||||||
|
"1.0.0+alpha.1")),
|
||||||
|
?assertMatch(true, ec_semver:lte("1.0.0.0+alpha.1",
|
||||||
|
"1.0.0+alpha.1")),
|
||||||
|
?assertMatch(true, ec_semver:lte("1.0-alpha.1+build.1",
|
||||||
|
"1.0.0-alpha.1+build.1")),
|
||||||
|
?assertMatch(true, ec_semver:lte("aa","cc")),
|
||||||
|
?assertMatch(true, ec_semver:lte("cc","cc")),
|
||||||
|
?assertMatch(true, not ec_semver:lte("1.0.0-alpha.1",
|
||||||
|
"1.0.0-alpha")),
|
||||||
|
?assertMatch(true, not ec_semver:lte("1.0.0-pre-alpha.2",
|
||||||
|
"1.0.0-pre-alpha")),
|
||||||
|
?assertMatch(true, not ec_semver:lte("cc", "aa")),
|
||||||
|
?assertMatch(true, not ec_semver:lte("1.0.0-beta.2",
|
||||||
|
"1.0.0-alpha.1")),
|
||||||
|
?assertMatch(true, not ec_semver:lte("1.0.0-beta.11",
|
||||||
|
"1.0.0-beta.2")),
|
||||||
|
?assertMatch(true, not ec_semver:lte("1.0.0-rc.1", "1.0.0-beta.11")),
|
||||||
|
?assertMatch(true, not ec_semver:lte("1.0.0-rc.1+build.1", "1.0.0-rc.1")),
|
||||||
|
?assertMatch(true, not ec_semver:lte("1.0.0", "1.0.0-rc.1+build.1")),
|
||||||
|
?assertMatch(true, not ec_semver:lte("1.0.0+0.3.7", "1.0.0")),
|
||||||
|
?assertMatch(true, not ec_semver:lte("1.3.7+build", "1.0.0+0.3.7")),
|
||||||
|
?assertMatch(true, not ec_semver:lte("1.3.7+build.2.b8f12d7",
|
||||||
|
"1.3.7+build")),
|
||||||
|
?assertMatch(true, not ec_semver:lte("1.3.7+build.11.e0f985a",
|
||||||
|
"1.3.7+build.2.b8f12d7")).
|
||||||
|
|
||||||
|
between_test() ->
|
||||||
|
?assertMatch(true, ec_semver:between("1.0.0-alpha",
|
||||||
|
"1.0.0-alpha.3",
|
||||||
|
"1.0.0-alpha.2")),
|
||||||
|
?assertMatch(true, ec_semver:between("1.0.0-alpha.1",
|
||||||
|
"1.0.0-beta.2",
|
||||||
|
"1.0.0-alpha.25")),
|
||||||
|
?assertMatch(true, ec_semver:between("1.0.0-beta.2",
|
||||||
|
"1.0.0-beta.11",
|
||||||
|
"1.0.0-beta.7")),
|
||||||
|
?assertMatch(true, ec_semver:between("1.0.0-pre-alpha.2",
|
||||||
|
"1.0.0-pre-alpha.11",
|
||||||
|
"1.0.0-pre-alpha.7")),
|
||||||
|
?assertMatch(true, ec_semver:between("1.0.0-beta.11",
|
||||||
|
"1.0.0-rc.3",
|
||||||
|
"1.0.0-rc.1")),
|
||||||
|
?assertMatch(true, ec_semver:between("1.0.0-rc.1",
|
||||||
|
"1.0.0-rc.1+build.3",
|
||||||
|
"1.0.0-rc.1+build.1")),
|
||||||
|
|
||||||
|
?assertMatch(true, ec_semver:between("1.0.0.0-rc.1",
|
||||||
|
"1.0.0-rc.1+build.3",
|
||||||
|
"1.0.0-rc.1+build.1")),
|
||||||
|
?assertMatch(true, ec_semver:between("1.0.0-rc.1+build.1",
|
||||||
|
"1.0.0",
|
||||||
|
"1.0.0-rc.33")),
|
||||||
|
?assertMatch(true, ec_semver:between("1.0.0",
|
||||||
|
"1.0.0+0.3.7",
|
||||||
|
"1.0.0+0.2")),
|
||||||
|
?assertMatch(true, ec_semver:between("1.0.0+0.3.7",
|
||||||
|
"1.3.7+build",
|
||||||
|
"1.2")),
|
||||||
|
?assertMatch(true, ec_semver:between("1.3.7+build",
|
||||||
|
"1.3.7+build.2.b8f12d7",
|
||||||
|
"1.3.7+build.1")),
|
||||||
|
?assertMatch(true, ec_semver:between("1.3.7+build.2.b8f12d7",
|
||||||
|
"1.3.7+build.11.e0f985a",
|
||||||
|
"1.3.7+build.10.a36faa")),
|
||||||
|
?assertMatch(true, ec_semver:between("1.0.0-alpha",
|
||||||
|
"1.0.0-alpha",
|
||||||
|
"1.0.0-alpha")),
|
||||||
|
?assertMatch(true, ec_semver:between("1",
|
||||||
|
"1.0.0",
|
||||||
|
"1.0.0")),
|
||||||
|
?assertMatch(true, ec_semver:between("1.0",
|
||||||
|
"1.0.0",
|
||||||
|
"1.0.0")),
|
||||||
|
|
||||||
|
?assertMatch(true, ec_semver:between("1.0",
|
||||||
|
"1.0.0.0",
|
||||||
|
"1.0.0.0")),
|
||||||
|
?assertMatch(true, ec_semver:between("1.0.0",
|
||||||
|
"1",
|
||||||
|
"1")),
|
||||||
|
?assertMatch(true, ec_semver:between("1.0+alpha.1",
|
||||||
|
"1.0.0+alpha.1",
|
||||||
|
"1.0.0+alpha.1")),
|
||||||
|
?assertMatch(true, ec_semver:between("1.0-alpha.1+build.1",
|
||||||
|
"1.0.0-alpha.1+build.1",
|
||||||
|
"1.0.0-alpha.1+build.1")),
|
||||||
|
?assertMatch(true, ec_semver:between("aaa",
|
||||||
|
"ddd",
|
||||||
|
"cc")),
|
||||||
|
?assertMatch(true, not ec_semver:between("1.0.0-alpha.1",
|
||||||
|
"1.0.0-alpha.22",
|
||||||
|
"1.0.0")),
|
||||||
|
?assertMatch(true, not ec_semver:between("1.0.0-pre-alpha.1",
|
||||||
|
"1.0.0-pre-alpha.22",
|
||||||
|
"1.0.0")),
|
||||||
|
?assertMatch(true, not ec_semver:between("1.0.0",
|
||||||
|
"1.0.0-alpha.1",
|
||||||
|
"2.0")),
|
||||||
|
?assertMatch(true, not ec_semver:between("1.0.0-beta.1",
|
||||||
|
"1.0.0-beta.11",
|
||||||
|
"1.0.0-alpha")),
|
||||||
|
?assertMatch(true, not ec_semver:between("1.0.0-beta.11", "1.0.0-rc.1",
|
||||||
|
"1.0.0-rc.22")),
|
||||||
|
?assertMatch(true, not ec_semver:between("aaa", "ddd", "zzz")).
|
||||||
|
|
||||||
|
pes_test() ->
|
||||||
|
?assertMatch(true, ec_semver:pes("1.0.0-rc.0", "1.0.0-rc.0")),
|
||||||
|
?assertMatch(true, ec_semver:pes("1.0.0-rc.1", "1.0.0-rc.0")),
|
||||||
|
?assertMatch(true, ec_semver:pes("1.0.0", "1.0.0-rc.0")),
|
||||||
|
?assertMatch(false, ec_semver:pes("1.0.0-rc.0", "1.0.0-rc.1")),
|
||||||
|
?assertMatch(true, ec_semver:pes("2.6.0", "2.6")),
|
||||||
|
?assertMatch(true, ec_semver:pes("2.7", "2.6")),
|
||||||
|
?assertMatch(true, ec_semver:pes("2.8", "2.6")),
|
||||||
|
?assertMatch(true, ec_semver:pes("2.9", "2.6")),
|
||||||
|
?assertMatch(true, ec_semver:pes("A.B", "A.A")),
|
||||||
|
?assertMatch(true, not ec_semver:pes("3.0.0", "2.6")),
|
||||||
|
?assertMatch(true, not ec_semver:pes("2.5", "2.6")),
|
||||||
|
?assertMatch(true, ec_semver:pes("2.6.5", "2.6.5")),
|
||||||
|
?assertMatch(true, ec_semver:pes("2.6.6", "2.6.5")),
|
||||||
|
?assertMatch(true, ec_semver:pes("2.6.7", "2.6.5")),
|
||||||
|
?assertMatch(true, ec_semver:pes("2.6.8", "2.6.5")),
|
||||||
|
?assertMatch(true, ec_semver:pes("2.6.9", "2.6.5")),
|
||||||
|
?assertMatch(true, ec_semver:pes("2.6.0.9", "2.6.0.5")),
|
||||||
|
?assertMatch(true, not ec_semver:pes("2.7", "2.6.5")),
|
||||||
|
?assertMatch(true, not ec_semver:pes("2.1.7", "2.1.6.5")),
|
||||||
|
?assertMatch(true, not ec_semver:pes("A.A", "A.B")),
|
||||||
|
?assertMatch(true, not ec_semver:pes("2.5", "2.6.5")).
|
||||||
|
|
||||||
|
parse_test() ->
|
||||||
|
?assertEqual({1, {[],[]}}, ec_semver:parse(<<"1">>)),
|
||||||
|
?assertEqual({{1,2,34},{[],[]}}, ec_semver:parse(<<"1.2.34">>)),
|
||||||
|
?assertEqual({<<"a">>, {[],[]}}, ec_semver:parse(<<"a">>)),
|
||||||
|
?assertEqual({{<<"a">>,<<"b">>}, {[],[]}}, ec_semver:parse(<<"a.b">>)),
|
||||||
|
?assertEqual({1, {[],[]}}, ec_semver:parse(<<"1">>)),
|
||||||
|
?assertEqual({{1,2}, {[],[]}}, ec_semver:parse(<<"1.2">>)),
|
||||||
|
?assertEqual({{1,2,2}, {[],[]}}, ec_semver:parse(<<"1.2.2">>)),
|
||||||
|
?assertEqual({{1,99,2}, {[],[]}}, ec_semver:parse(<<"1.99.2">>)),
|
||||||
|
?assertEqual({{1,99,2}, {[<<"alpha">>],[]}}, ec_semver:parse(<<"1.99.2-alpha">>)),
|
||||||
|
?assertEqual({{1,99,2}, {[<<"alpha">>,1], []}}, ec_semver:parse(<<"1.99.2-alpha.1">>)),
|
||||||
|
?assertEqual({{1,99,2}, {[<<"pre-alpha">>,1], []}}, ec_semver:parse(<<"1.99.2-pre-alpha.1">>)),
|
||||||
|
?assertEqual({{1,99,2}, {[], [<<"build">>, 1, <<"a36">>]}},
|
||||||
|
ec_semver:parse(<<"1.99.2+build.1.a36">>)),
|
||||||
|
?assertEqual({{1,99,2,44}, {[], [<<"build">>, 1, <<"a36">>]}},
|
||||||
|
ec_semver:parse(<<"1.99.2.44+build.1.a36">>)),
|
||||||
|
?assertEqual({{1,99,2}, {[<<"alpha">>, 1], [<<"build">>, 1, <<"a36">>]}},
|
||||||
|
ec_semver:parse("1.99.2-alpha.1+build.1.a36")),
|
||||||
|
?assertEqual({{1,99,2}, {[<<"pre-alpha">>, 1], [<<"build">>, 1, <<"a36">>]}},
|
||||||
|
ec_semver:parse("1.99.2-pre-alpha.1+build.1.a36")).
|
||||||
|
|
||||||
|
version_format_test() ->
|
||||||
|
?assertEqual(["1", [], []], ec_semver:format({1, {[],[]}})),
|
||||||
|
?assertEqual(["1", ".", "2", ".", "34", [], []], ec_semver:format({{1,2,34},{[],[]}})),
|
||||||
|
?assertEqual(<<"a">>, erlang:iolist_to_binary(ec_semver:format({<<"a">>, {[],[]}}))),
|
||||||
|
?assertEqual(<<"a.b">>, erlang:iolist_to_binary(ec_semver:format({{<<"a">>,<<"b">>}, {[],[]}}))),
|
||||||
|
?assertEqual(<<"1">>, erlang:iolist_to_binary(ec_semver:format({1, {[],[]}}))),
|
||||||
|
?assertEqual(<<"1.2">>, erlang:iolist_to_binary(ec_semver:format({{1,2}, {[],[]}}))),
|
||||||
|
?assertEqual(<<"1.2.2">>, erlang:iolist_to_binary(ec_semver:format({{1,2,2}, {[],[]}}))),
|
||||||
|
?assertEqual(<<"1.99.2">>, erlang:iolist_to_binary(ec_semver:format({{1,99,2}, {[],[]}}))),
|
||||||
|
?assertEqual(<<"1.99.2-alpha">>, erlang:iolist_to_binary(ec_semver:format({{1,99,2}, {[<<"alpha">>],[]}}))),
|
||||||
|
?assertEqual(<<"1.99.2-alpha.1">>, erlang:iolist_to_binary(ec_semver:format({{1,99,2}, {[<<"alpha">>,1], []}}))),
|
||||||
|
?assertEqual(<<"1.99.2-pre-alpha.1">>, erlang:iolist_to_binary(ec_semver:format({{1,99,2}, {[<<"pre-alpha">>,1], []}}))),
|
||||||
|
?assertEqual(<<"1.99.2+build.1.a36">>,
|
||||||
|
erlang:iolist_to_binary(ec_semver:format({{1,99,2}, {[], [<<"build">>, 1, <<"a36">>]}}))),
|
||||||
|
?assertEqual(<<"1.99.2.44+build.1.a36">>,
|
||||||
|
erlang:iolist_to_binary(ec_semver:format({{1,99,2,44}, {[], [<<"build">>, 1, <<"a36">>]}}))),
|
||||||
|
?assertEqual(<<"1.99.2-alpha.1+build.1.a36">>,
|
||||||
|
erlang:iolist_to_binary(ec_semver:format({{1,99,2}, {[<<"alpha">>, 1], [<<"build">>, 1, <<"a36">>]}}))),
|
||||||
|
?assertEqual(<<"1.99.2-pre-alpha.1+build.1.a36">>,
|
||||||
|
erlang:iolist_to_binary(ec_semver:format({{1,99,2}, {[<<"pre-alpha">>, 1], [<<"build">>, 1, <<"a36">>]}}))),
|
||||||
|
?assertEqual(<<"1">>, erlang:iolist_to_binary(ec_semver:format({1, {[],[]}}))).
|
19
test/ec_talk_tests.erl
Normal file
19
test/ec_talk_tests.erl
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
%%% @copyright 2024 Erlware, LLC.
|
||||||
|
-module(ec_talk_tests).
|
||||||
|
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
|
general_test_() ->
|
||||||
|
[?_test(42 == ec_talk:get_integer("42")),
|
||||||
|
?_test(500_211 == ec_talk:get_integer("500211")),
|
||||||
|
?_test(1_234_567_890 == ec_talk:get_integer("1234567890")),
|
||||||
|
?_test(12_345_678_901_234_567_890 == ec_talk:get_integer("12345678901234567890")),
|
||||||
|
?_test(true == ec_talk:get_boolean("true")),
|
||||||
|
?_test(false == ec_talk:get_boolean("false")),
|
||||||
|
?_test(true == ec_talk:get_boolean("Ok")),
|
||||||
|
?_test(true == ec_talk:get_boolean("ok")),
|
||||||
|
?_test(true == ec_talk:get_boolean("Y")),
|
||||||
|
?_test(true == ec_talk:get_boolean("y")),
|
||||||
|
?_test(false == ec_talk:get_boolean("False")),
|
||||||
|
?_test(false == ec_talk:get_boolean("No")),
|
||||||
|
?_test(false == ec_talk:get_boolean("no"))].
|
|
@ -1,10 +0,0 @@
|
||||||
-module(mock).
|
|
||||||
|
|
||||||
-export([new_dictionary/0]).
|
|
||||||
|
|
||||||
new_dictionary() ->
|
|
||||||
meck:new(ec_dictionary_proper),
|
|
||||||
meck:expect(ec_dictionary_proper, dictionary, fun() ->
|
|
||||||
proper_types:union([ec_dict])
|
|
||||||
end).
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue