Compare commits

...

342 commits

Author SHA1 Message Date
Fred Hebert
4ffd5dd550
Merge pull request #189 from ariel-anieli/ec-gb-trees-tests
Some checks failed
Integration tests / OTP 23 on ubuntu-latest (push) Has been cancelled
Integration tests / OTP 25 on ubuntu-latest (push) Has been cancelled
Integration tests / OTP 27 on ubuntu-latest (push) Has been cancelled
Moved `ec_gb_trees` tests into separate file
2024-11-24 17:44:09 -05:00
Ariel Otilibili
a5712997ef Moved ec_gb_trees tests into separate file
Part of #179.

Signed-off-by: Ariel Otilibili <otilibil@eurecom.fr>
2024-11-23 13:08:05 +01:00
Fred Hebert
20119880fd
Merge pull request #188 from ariel-anieli/ec-list-tests
Moved `ec_lists` tests into separate file, and chores in GitHub actions
2024-11-21 09:27:30 -05:00
Ariel Otilibili
1a08e33b83 Dropped minor versions in GitHub actions
Lastest minor in given version will be taken.

Signed-off-by: Ariel Otilibili <otilibil@eurecom.fr>
2024-11-18 21:25:19 +01:00
Ariel Otilibili
47f7a5540c Moved ec_lists tests into separate file
Part of #179

Signed-off-by: Ariel Otilibili <otilibil@eurecom.fr>
2024-11-18 21:25:06 +01:00
Fred Hebert
2ccc40f89b
Merge pull request #187 from ariel-anieli/ec-file-tests
Moved `ec_file` tests into sepate file; bumped actions/checkout
2024-11-13 09:34:58 -05:00
Ariel Otilibili
fa25b703e5 Bumped actions/checkout to v4
https://github.com/actions/checkout/releases/tag/v4.2.2

Signed-off-by: Ariel Otilibili <otilibil@eurecom.fr>
2024-11-05 22:33:20 +01:00
Ariel Otilibili
8de367f996 Moved ec_file tests into separate file
Signed-off-by: Ariel Otilibili <otilibil@eurecom.fr>
2024-11-05 22:32:48 +01:00
Fred Hebert
279d116dca
Merge pull request #186 from ariel-anieli/ec-semver-tests
Moved `ec_semver` tests in separate file.
2024-10-07 09:05:58 -04:00
Ariel Otilibili
23b00904c8 Moved ec_semver tests in separate file.
Part of #179.

Signed-off-by: Ariel Otilibili <otilibil@eurecom.fr>
2024-10-04 18:16:27 +02:00
Fred Hebert
6cd37a4f9b
Merge pull request #185 from ariel-anieli/ec-talk-test
Moved `ec_talk` tests into separate file
2024-09-24 09:06:37 -04:00
Ariel Otilibili
5305348899 Moved ec_talk tests into separate file
Part of #179.

Signed-off-by: Ariel Otilibili <otilibil@eurecom.fr>
2024-09-23 21:32:58 +02:00
Fred Hebert
6d4e7d14ce
Merge pull request #184 from ariel-anieli/test-cmd-log-cnv
Moved `ec_cmd_log` & `ec_cnv` tests into separate files
2024-08-12 16:39:28 -04:00
Ariel Otilibili
677984e961 Moved ec_env tests into a separate file
Signed-off-by: Ariel Otilibili <otilibil@eurecom.fr>
2024-08-12 20:06:56 +02:00
Ariel Otilibili
ca7581cbb0 Moved ec_cmd_log tests into separate file
* moved colour macros into `src/ec_cmd_log.hrl`
* moved `ec_cmd_log` tests into `test/ec_cmd_log_tests.erl`.

Signed-off-by: Ariel Otilibili <otilibil@eurecom.fr>
2024-08-12 20:05:55 +02:00
Fred Hebert
fc69576978
Merge pull request #183 from ariel-anieli/git-vsn-tests
Moved `ec_git_vsn` tests into separate file
2024-07-31 13:11:35 -04:00
Ariel Otilibili
d24ad72034 Moved ec_git_vsn tests into separate file 2024-07-29 17:24:41 +02:00
Fred Hebert
a54f0623c5
Merge pull request #181 from ariel-anieli/badges-hex-ci
Updated CI/CD and hex badges
2024-07-03 09:32:52 -04:00
Ariel Otilibili
515df6b21e Updated CI/CD and hex badges 2024-07-03 07:31:57 +02:00
Fred Hebert
1fd0a513ff
Merge pull request #178 from ariel-anieli/remove-random-uniform
Replaced random_uniform/1 by rand:uniform/1
2024-07-02 16:01:21 -04:00
Fred Hebert
3d0006fe89
Merge pull request #180 from ariel-anieli/thousand-separators
Use thousand separators for large digits
2024-07-02 15:57:04 -04:00
Ariel Otilibili
15126e0048 Replaced random_uniform/1 by rand:uniform/1
* leftover of 5118421f6f
* code base contains only one occurence of random_uniform/1

```
$ git grep random_uniform
src/ec_file.erl:    UniqueNumber = erlang:integer_to_list(erlang:trunc(random_uniform() * 1000000000000)),
src/ec_file.erl:random_uniform() ->
```
2024-06-20 13:17:41 +02:00
Ariel Otilibili
cdd9240142 Use thousand separators for large digits
```
$ git grep -nP '\d{4,}[^a-zA-Z\"]' | grep \.erl | grep -vi copyright > /dev/null; echo $?
0
```
2024-06-14 21:42:36 +02:00
Fred Hebert
1a42c54981
Merge pull request #177 from ariel-anieli/issue-173-and-type-annotation-change 2024-06-03 19:58:48 -04:00
Ariel Otilibili
ccc1be32be
Changed type annotation
Used 'intensity' in declaration of 'state_t'.
2024-06-03 23:33:10 +02:00
Ariel Otilibili
5f40d8f061
Resolved dialyzer warning on colorize/4
Fixes #173.
2024-06-03 23:32:17 +02:00
Fred Hebert
75c6bae602
Merge pull request #176 from ariel-anieli/github-action-and-otp-bump
GitHub action renaming & OTP bump
2024-06-01 10:54:37 -04:00
Ariel Otilibili
ab7eb3874f OTP bump
```
curl --silent https://packages.debian.org/search?keywords=erlang | \
    sed -ne '/Package erlang</,/<\/ul>/{/<\/a>/p; /br/p}' | \
    sed -e 's/<a.*$//' | \
    perl -0777 -pE 's/<li class="(\w+)">\n/$1/g; s/<br>([^\s]+)/$1/g; s/\+.*(?=\n)//g' | \
    sed -e '1i name otp-version' | \
    column -t

name          otp-version
buster        1:22.2.7
bullseye      1:23.2.6
bookworm      1:25.2.3
trixie        1:25.3.2.12
sid           1:25.3.2.12
experimental  1:27.0
```
2024-05-27 16:26:41 +02:00
Ariel Otilibili
2636b5e21d Renaming in GitHub actions 2024-05-27 16:26:41 +02:00
Fred Hebert
d6315a9541
Merge pull request #175 from ariel-anieli/remove-rebar2-cfg-script
Removed rebar2 case in rebar.config.script
2024-05-16 09:44:14 -04:00
Ariel Otilibili
f9ffd1ce6b Removed rebar2 case in rebar.config.script
* introduced by 505d35996d
* rebar2 is now deprecated [1].

[1] https://github.com/rebar/rebar
2024-05-15 22:20:43 +02:00
Fred Hebert
182c30a950
Merge pull request #172 from ariel-anieli/otp-bump
OTP bump
2024-03-18 13:17:36 -04:00
Ariel Otilibili
63b1798b1e OTP bump
* sequel of 17e6f89078
* added R26 in CI/CD, and cleared out dialyzer warnings
* from R26, by default, `-Wno_unknown` suppresses warnings [1]
* in R25, it was the reverse behavior: `-Wunknown` allows warnings [2].

[1] https://www.erlang.org/doc/man/dialyzer.html#warning_options
[2] https://www.erlang.org/docs/25/man/dialyzer#format_warning-1
2024-03-18 01:01:08 +01:00
Fred Hebert
cb3983741e
Merge pull request #171 from ariel-anieli/pr-stacktrace
Removed unicode_str & fun_stacktrace
2024-03-13 14:21:29 -04:00
Ariel Otilibili
f378d3ec46 Removed unicode_str
* introduced in f8f72b7cc5
* introduced for working around compile warning starting from R20
* CI/CD uses R23 and onwards.
2024-03-07 06:58:59 +01:00
Ariel Otilibili
c0a02892cd Removed fun_stacktrace
* introduced in ad2d57d8b6
* CI/CD uses R23 and onwards
* erlang:get_stacktrace/0 removed in R23.

[1] https://www.erlang.org/doc/general_info/removed#functions-removed-in-otp-23
2024-03-07 06:58:52 +01:00
Fred Hebert
5de3c80cc2
Merge pull request #170 from ariel-anieli/pr-otp-bump
Bumped OTP versions
2024-02-21 16:45:29 -05:00
Ariel Otilibili
17e6f89078 Bumped OTP versions
* aligned OTP versions with Debian
* keeps versions from bullseye (old stable) to sid (unstable).

```
$ curl --silent https://packages.debian.org/search?keywords=erlang | \
  sed -ne '/Package erlang</,/<\/ul>/{/<\/a>/p; /br/p}' | \
  sed -e 's/<a.*$//' | \
  perl -0777 -pE 's/<li class="(\w+)">\n/$1/g; s/<br>([^\s]+)/$1/g; s/\+.*(?=\n)//g' | \
  sed -e '1i name otp-version' | \
  column -t
name          otp-version
buster        1:22.2.7
bullseye      1:23.2.6
bookworm      1:25.2.3
trixie        1:25.3.2.8
sid           1:25.3.2.8
experimental  1:26.2.1

$ curl -I  https://packages.debian.org/search?keywords=erlang
HTTP/2 200
date: Tue, 20 Feb 2024 22:21:53 GMT
server: Apache
last-modified: Tue, 20 Feb 2024 21:50:40 GMT
vary: Accept-Encoding,negotiate,accept-language
x-clacks-overhead: GNU Terry Pratchett
expires: Wed, 21 Feb 2024 09:04:02 +0000
x-content-type-options: nosniff
x-frame-options: sameorigin
referrer-policy: no-referrer
x-xss-protection: 1
permissions-policy: interest-cohort=()
strict-transport-security: max-age=15552000
age: 1873
content-length: 184625
content-type: text/html; charset=UTF-8
```
2024-02-21 22:05:14 +01:00
Fred Hebert
86a6c6ea65
Merge pull request #169 from ariel-anieli/pr-ns-types
Removed namespaced_types
2024-02-20 12:47:25 -05:00
Ariel Otilibili
2286a6ed9b Removed namespaced_types
* introduced for handling deprecated types existing before R17
* introduced in 523a66ad74
* CI/CD handles R19 up to R24
* R19 and onwards have dict:dict/0 [1,2]

[1] https://www.erlang.org/docs/19/man/dict
[2] https://www.erlang.org/docs/24/man/dict#type-dict
2024-02-17 22:04:50 +01:00
Fred Hebert
7b7d5b559d
Merge pull request #168 from ariel-anieli/pr-callback
Removed have_callback_support
2024-02-07 15:51:03 -05:00
Ariel Otilibili
eca2d2129c Removed have_callback_support
* introduced by 95f723e1e0
* mentioned as well in 47bcbd49b6
* introduced for compatibility with OTP R14
* CI/CD now tests from R19 and onwards.
2024-02-04 12:39:42 +01:00
Fred Hebert
20d049ea4f
Merge pull request #167 from ariel-anieli/pr-deprecation-cond
Removed checks on deprecated crypto:sha/1 & random:uniform/0
2024-01-12 15:59:41 -05:00
Ariel Otilibili
5118421f6f Removed checks on deprecated random module 2024-01-12 19:50:54 +01:00
Ariel Otilibili
685f08621b Removed conditions on deprecated cryptos and rand modoles
* CI/CD lowest OTP version is 19.3
* from 19.3, `rand` module exists (https://www.erlang.org/docs/19/man/rand)
* from 19.3, `crypto:hash/1` exists (https://www.erlang.org/docs/19/man/crypto).
2024-01-12 19:46:27 +01:00
Fred Hebert
952a1d2bc6
Merge pull request #166 from ariel-anieli/pr-typo-checksums-ec_file
[src/ec_file.erl] Factorization & typos, in sha1sum/1 & md5sum/1
2024-01-08 11:58:18 -05:00
Ariel Otilibili
fc69b3630c turn_digest_into_hex/1 renamed as bin_to_hex/1 2023-12-21 08:43:36 +01:00
Ariel Otilibili
6781f1ba6a Factorized digest-to-hex transform; used in three functions 2023-12-20 23:58:44 +01:00
Ariel Otilibili
bbdbbf313f Typos, and replaced 'checksum' with 'digest': more consistent with Erlang documentation 2023-12-20 23:56:27 +01:00
Fred Hebert
19c717fb97
Merge pull request #165 from ariel-anieli/pr-typos
[doc/signatures.md] Fixed typos, mis-naming, and syntax highlighting
2023-12-18 16:18:28 -05:00
Ariel
6e9c1b0a22 Another typo in math mode 2023-12-18 21:26:06 +01:00
Ariel
7e69d4949e Replaced tab by four spaces 2023-12-18 21:19:24 +01:00
Ariel
68e9bbcd0f Typo in math mode 2023-12-18 21:00:25 +01:00
Ariel
cd88825861 Syntax highlighted in code snippets 2023-12-18 20:59:57 +01:00
Ariel
5c5c264241 For formulae, used Markdown math mode 2023-12-18 20:33:17 +01:00
Ariel
6d4c471ff6 Fixed typos and misnamings 2023-12-18 20:31:49 +01:00
Ariel
62a985b937 Instead of three colons, syntax is highlighted with three backticks 2023-12-18 20:29:17 +01:00
Fred Hebert
7c4911795e
Merge pull request #164 from ariel-anieli/pr-redundant-parse
Removed other redudant clauses in local parse/3
2023-12-10 11:15:31 -05:00
Ariel
ad4b944fc6 Removed other redudant clauses; same behaviour than https://github.com/erlware/erlware_commons/pull/162 2023-12-10 17:08:41 +01:00
Fred Hebert
4d5811d99b
Merge pull request #162 from ariel-anieli/issue-70-redundant-parse
Removed redudant clauses in local parse/3
2023-12-09 19:48:06 -05:00
Fred Hebert
04c0d4fc84
Merge pull request #163 from ariel-anieli/issue-138-rumany-typos
runmany_wrap/2 didn't call Fun: missing parens
2023-12-09 19:47:05 -05:00
Ariel
378b88587c Fix for https://github.com/erlware/erlware_commons/issues/138 2023-12-10 01:04:46 +01:00
Ariel
d5183f5336 Removed redudant clauses in local parse/3 2023-12-09 14:07:13 +01:00
Fred Hebert
eeb25f4b7f
Merge pull request #161 from kianmeng/fix-typos
Fix typos
2021-12-20 09:14:07 -05:00
Kian-Meng, Ang
8dd7378a75 Fix typos 2021-12-19 17:36:19 +08:00
Fred Hebert
4406d56135
Merge pull request #160 from FlyingLu/patch-1
fixed incorrect 'G' in the format parameter string
2021-11-02 12:52:06 -04:00
FlyingLu
e89e95de5f
Added more test cases.
Added more test cases to show the difference between 'G' and 'H'.
2021-11-03 00:11:05 +08:00
FlyingLu
791729c30a
Fixed the incorrect macro. 2021-11-02 11:27:57 +08:00
FlyingLu
d34da1d107
fixed incorrect 'G' in the format parameter string
In the format parameter string, 'G' should have represented 24-hour format of an hour WITHOUT leading zeros.
But it actually results in 24-hour format of an hour WITH leading zeros.
2021-11-01 16:24:15 +08:00
Tristan Sloughter
ad559ae1f5
Merge pull request #158 from ferd/support-unicode-strings
Support Unicode string formatting
2021-08-09 09:33:43 -06:00
Fred Hebert
916539338f Support Unicode string formatting
Fixes https://github.com/erlware/erlware_commons/issues/157
2021-08-09 15:06:57 +00:00
Fred Hebert
2a758c9ec7
Merge pull request #156 from paulo-ferraz-oliveira/feature/ci-otp-24.0
Tweak CI versions (add 24.0, move 23.2 to 23.3)
2021-05-14 08:27:54 -04:00
Paulo F. Oliveira
4c0180f157 Tweak CI versions (add 24.0, move 23.2 to 23.3) 2021-05-14 10:11:56 +01:00
Fred Hebert
6f7a32487a
Merge pull request #150 from enidgjoleka/remove-unused-file-attribute
Remove -file attribute pointing to a non-existing file on ec_semver_parser
2021-05-12 08:16:38 -04:00
Fred Hebert
9cdca1c2e1
Merge pull request #154 from paulo-ferraz-oliveira/feature/license-info
Improve on license-related information
2021-04-09 21:21:39 -04:00
Fred Hebert
26bcdf8030
Merge pull request #155 from paulo-ferraz-oliveira/feature/symlinks_in_copy
Keep symlinks in copies
2021-04-09 21:17:36 -04:00
Paulo F. Oliveira
c47c938537 Keep symlinks in copies 2021-04-02 22:44:02 +01:00
Paulo F. Oliveira
f0347d88d7 Improve on license-related information 2021-04-02 21:25:49 +01:00
Fred Hebert
0318b467bc
Merge pull request #152 from paulo-ferraz-oliveira/fix/for_dialyzer
Fixes for dialyzer
2021-03-26 13:13:13 -04:00
Paulo F. Oliveira
9d67e26510 Solve dialyzer warnings while approaching expected behaviour
(with minor unit tests)
2021-03-25 22:32:33 +00:00
Paulo F. Oliveira
f5e8aa6551 gitignore test-generated elements 2021-03-25 00:03:37 +00:00
Paulo F. Oliveira
4406953a87 Have CI working with extra checks 2021-03-25 00:03:13 +00:00
Paulo F. Oliveira
0a4fde35e7 Remove dead code 2021-03-25 00:02:53 +00:00
Enid Gjoleka
01e08a3605 Remove unused -file attribute on ec_semver_parser 2021-02-10 14:43:53 +01:00
Tristan Sloughter
d9874feccf
Merge pull request #149 from tsloughter/readme-badge
update readme status badge and CI branch
2021-01-04 12:22:12 -07:00
Tristan Sloughter
32e62781bb
set TERM os var to xterm to get cf to not drop color in logs 2021-01-04 11:52:55 -07:00
Tristan Sloughter
2256b68e4f
update readme status badge and CI branch 2021-01-04 10:18:25 -07:00
Tristan Sloughter
45d79af620
Merge pull request #146 from michaelklishin/patch-1
Avoid a warning on Erlang/OTP 24
2021-01-04 09:53:24 -07:00
Tristan Sloughter
c7f166a8a7
Merge pull request #148 from tsloughter/github-actions
move to github actions for CI
2021-01-04 09:51:40 -07:00
Tristan Sloughter
2e1b59ece6
move to github actions for CI 2021-01-04 09:44:13 -07:00
Michael Klishin
8eef97234f
Better comment wording as suggested by @ferd
Co-authored-by: Fred Hebert <mononcqc@ferd.ca>
2020-12-16 00:10:38 +03:00
Michael Klishin
c2b7863a53
Avoid a warning on Erlang/OTP 24
to make sure Rebar 3 can bootstrap on that version with warnings-as-errors compiler settings.

Closes #145.
2020-12-15 01:45:43 +03:00
Tristan Sloughter
f41b847b0c
Merge pull request #142 from rlipscombe/patch-1
Replace ericbmerritt links in README
2019-08-19 15:55:14 -06:00
Roger Lipscombe
0dc260c04c
Replace ericbmerritt links in README 2019-08-19 22:49:35 +01:00
Tristan Sloughter
aad7ae4241
Merge pull request #141 from martinrehfeld/patch-1
Fix spelling/grammar in signatures.md
2019-02-21 09:12:46 -07:00
Martin Rehfeld
8aadd8b278
Fix spelling/grammar in signatures.md
While reading through the document, I came across some spelling/grammar issues and thought I could just as well help fix them.
2019-02-21 17:11:30 +01:00
Tristan Sloughter
c3ae625bd1
Merge pull request #140 from relayr/extra_hyphens
Allow for extra hyphens in pre-release build version
2018-12-17 07:20:10 -07:00
Kuba Odias
4e3b177be7 Allow for extra hyphens in pre-release build version 2018-12-14 23:09:25 +01:00
Tristan Sloughter
a8b46e0770
Merge pull request #139 from tsloughter/alpha-pes
fix ec_semver:pes test to not ignore alpha versions
2018-10-13 11:50:25 -06:00
Tristan Sloughter
d4079cd127
fix ec_semver:pes test to not ignore alpha versions 2018-10-13 08:39:47 -06:00
Luis Rascão
b2d41811c1
Merge pull request #137 from tsloughter/mkdtemp-spec
fix insecure_mkdtemp type spec
2018-08-28 08:38:55 +00:00
Tristan Sloughter
f6992d72ca fix insecure_mkdtemp type spec 2018-08-27 14:59:32 -06:00
Tristan Sloughter
7bf631d326
Merge pull request #136 from tsloughter/git-tag-vsn
use git tag for version in .app
2018-06-23 16:34:41 -06:00
Tristan Sloughter
64d76963c7
use git tag for version in .app 2018-06-23 15:35:56 -06:00
Tristan Sloughter
1bd107113b
Merge pull request #135 from tsloughter/copy-file-info
support more fine grained file info copy levels
2018-06-23 15:31:35 -06:00
Tristan Sloughter
8302adf831
support more fine grained file info copy levels 2018-06-22 16:28:19 -06:00
Tristan Sloughter
e0453faf8f
Merge pull request #134 from filmor/patch-1
Use environment variables to find the tmp directory
2018-06-14 08:07:22 -06:00
Benedikt Reinartz
c730da3b9d
Use environment variables to find the tmp directory 2018-06-14 08:18:33 +02:00
Luis Rascão
b23bf733c5
Merge pull request #131 from tsloughter/1.1.0-bump
1.1.0 bump
2018-05-03 16:51:10 +01:00
Tristan Sloughter
9137b5dc54
version bump 2018-05-02 20:44:59 -06:00
Tristan Sloughter
885f961c02
upgrade cf dep 2018-05-02 20:44:27 -06:00
Tristan Sloughter
2e01d65b99
Merge pull request #130 from ferd/otp-21-stacktrace-compat
Work around OTP-21 deprecation of get_stacktrace()
2018-05-02 20:36:52 -06:00
Fred Hebert
ad2d57d8b6 Work around OTP-21 deprecation of get_stacktrace() 2018-05-02 21:47:52 -04:00
Luis Rascão
9eae901e58
Merge pull request #129 from tsloughter/master
version bump
2018-02-03 09:21:48 +00:00
Tristan Sloughter
51de0f2c7a
version bump 2018-02-02 20:27:02 -08:00
Tristan Sloughter
d501d710e2
Merge pull request #128 from GalaxyGorilla/fix_git_vsn
Fix git version parsing
2018-02-02 20:09:59 -08:00
Sascha Kattelmann
6e9f640c9c Fix git version parsing 2018-02-01 10:43:38 +01:00
Luis Rascão
012bc2f789
Merge pull request #127 from tsloughter/master
version bump
2017-11-17 21:34:30 +00:00
Tristan Sloughter
a227a0fce9 version bump 2017-11-17 13:30:51 -08:00
Tristan Sloughter
a12eed3d1d
Merge pull request #126 from tsloughter/git-vsn-fix
fix git command to find tags and default empty pattern
2017-11-17 13:27:34 -08:00
Tristan Sloughter
5956de425f fix use of string:trim 2017-11-17 10:18:41 -08:00
Tristan Sloughter
f1ecf12ad3 fix git command to find tags and default empty pattern 2017-11-17 10:15:25 -08:00
Tristan Sloughter
fb449e9e01
Merge pull request #125 from lrascao/1.0.3-bump
Bump to 1.0.3
2017-11-16 08:34:59 -08:00
Luis Rascao
6933a178ec Bump to 1.0.3 2017-11-15 11:35:10 +00:00
Tristan Sloughter
2690bd14a3
Merge pull request #124 from ferd/fix-trim-usage-otp21
Fix bad unicode transition for OTP 20+
2017-11-14 18:58:35 -08:00
Fred Hebert
df7728d81f Fix bad unicode transition for OTP 20+
The trim/3 function accepts a list of graphemes rather than a single
character. This means previous patches were wrong and totally breaking.
2017-11-14 21:20:36 -05:00
Tristan Sloughter
d792f8c5ff
Merge pull request #123 from lrascao/1.0.2-bump
Bump to 1.0.2
2017-11-12 14:55:39 -08:00
Luis Rascao
f09d8f18a1 Bump to 1.0.2 2017-11-03 18:32:52 +00:00
Luis Rascão
b8bbc4cfac
Merge pull request #122 from tsloughter/1.0.1-bump
version bump to 1.0.1
2017-11-03 18:27:40 +00:00
Tristan Sloughter
41ff18e5d1 version bump to 1.0.1 2017-11-03 09:24:55 -07:00
Luis Rascão
4f086fc5fa
Merge pull request #121 from ferd/otp20-unicode-support
Add compile-time switch for OTP-20 string funcs
2017-11-01 18:24:03 +00:00
Fred Hebert
f8f72b7cc5 Add compile-time switch for OTP-20 string funcs
Allows support for Unicode data, and prevents compile warnings that will
start with OTP-20.
2017-11-01 11:26:17 -04:00
Tristan Sloughter
fa1ec76a9b Merge pull request #105 from filmor/semver-prefix
Make ec_git_vsn Windows-compatible, readd prefix functionality.
2017-09-04 19:46:16 -07:00
Tristan Sloughter
681973a29c Merge pull request #118 from shopgun/master
Fixing #117 - microseconds not parsing when offset given
2017-09-04 19:43:46 -07:00
Tristan Sloughter
657c767a8c Merge pull request #119 from wrren/hotfix/iso8601_formatting
Fixing #103 - Updated ISO8601 parsing to add leading zero to hour
2017-09-04 19:41:59 -07:00
Warren Kenny
3ad087f8e0 Updated ISO8601 parsing to add leading zero to hour 2017-02-06 22:28:49 +00:00
Henrik Tudborg
f92f7de6f8 Fixing #117 - microseconds not parsing when offset given 2017-02-03 14:12:50 +01:00
Tristan Sloughter
0916834752 Merge pull request #107 from choptastic/patch-1
Use cf 0.2.2 with rebar2
2017-01-31 09:13:22 -08:00
Eric Merritt
0898f1caf0 Merge pull request #114 from tsloughter/master
1.0: remove unneeded export_all. in OTP20 it is a warning
2017-01-09 07:46:45 -08:00
Tristan Sloughter
c9f1c5debe bump to version 1.0 2017-01-08 10:07:44 -08:00
Tristan Sloughter
69fbe53eea remove unneeded export_all. in OTP20 it is a warning 2017-01-07 18:45:30 -08:00
Luis Rascão
e76dd80a1e Merge pull request #113 from tsloughter/master
version bump
2016-11-27 19:19:42 +00:00
Tristan Sloughter
4d2ab621ab version bump 2016-11-27 11:15:30 -08:00
Tristan Sloughter
ab6617496b Merge pull request #112 from lrascao/feature/bump_cf
Bump cf to 0.2.2
2016-11-27 11:03:26 -08:00
Luis Rascao
4513db483d Bump cf to 0.2.2 2016-11-27 18:57:55 +00:00
Tristan Sloughter
db88b093d2 Merge pull request #111 from joedevivo/master
Add the ability to explicitly turn off ANSI color
2016-10-29 11:44:50 -07:00
Joe DeVivo
f64a4b1661 Add the ability to explicitly turn off ANSI color 2016-10-29 05:35:41 -07:00
Tristan Sloughter
603441a036 Merge pull request #109 from erszcz/pr77-microseconds-fix
Fix parsing of ISO 8601 decimal fractions of a second
2016-09-05 13:12:04 -07:00
Radek Szymczyszyn
a91c96eb92 Support ISO 8601 fractions of a seconds up to 6 places after the comma 2016-08-22 11:10:18 +02:00
Radek Szymczyszyn
5d729253d3 Add one more parsing test (just 3 places after the comma) 2016-08-19 14:09:51 +02:00
Radek Szymczyszyn
a298a7b045 Fix support for ISO 8601 fractions of a second
This is limited to milli- and microseconds interpreted
as 3 or 6 places after decimal comma.
All of the following, while valid according to the standard, won't be accepted:

- 2001-03-10T17:16:17.1Z
- 2001-03-10T17:16:17.12Z
- 2001-03-10T17:16:17.1234Z
- 2001-03-10T17:16:17.12345Z
- 2001-03-10T17:16:17.1234567Z
2016-08-19 12:38:40 +02:00
Radek Szymczyszyn
ed107c94b4 Fix microsecond() range 2016-08-19 11:05:29 +02:00
Kirilll Zaborsky
ab321b16e6 Testcase showing broken microseconds parsing 2016-08-19 10:32:47 +02:00
Kirilll Zaborsky
8dd9f826db pad2 spec fix 2016-08-19 10:32:47 +02:00
Kirilll Zaborsky
d052e63ba5 Proper zero padding for microseconds 2016-08-19 10:32:47 +02:00
Jesse Gumm
fe9120696e Use cf 0.2.2 with rebar2
cf 0.2.0 doesn't compile with rebar2, but 0.2.2 (recently tagged) works and passes tests.
2016-07-07 11:59:42 -05:00
Jordan Wilberding
8974edb6a6 Merge pull request #106 from tsloughter/random
Fixes for OTP-19 support
2016-05-13 06:25:51 -06:00
Tristan Sloughter
b666164c0d version bump 2016-05-12 21:58:56 -05:00
Tristan Sloughter
6d63ffde57 remove typespec syntax that was removed in OTP-19 2016-05-12 21:54:04 -05:00
Tristan Sloughter
001e7fcf4b remove use of deprecated random module 2016-05-12 21:54:04 -05:00
Benedikt Reinartz
57f56c2860 Make ec_git_vsn Windows-compatible, readd prefix functionality. 2016-04-06 16:28:20 +02:00
Tristan Sloughter
874f2dc821 Merge pull request #102 from omarkj/omarkj-compile-on-rebar3
Fix `rebar.config.script` to work with latest `rebar3`.
2016-03-10 13:21:43 -06:00
Ómar Kjartan Yasin
0c1636e75a Fix script so it can compile git-versioned
rebar3 variables as that is the way these versions
can look when updated using
`rebar3 local upgrade`.
2016-03-10 10:46:39 -08:00
Tristan Sloughter
8a325c0291 Merge pull request #99 from ericmj/patch-1
Support non rebar tools evaluating script
2015-12-02 15:47:08 -06:00
Eric Meadows-Jönsson
b84eed3634 Support non rebar tools evaluating script 2015-12-02 22:42:25 +01:00
Jordan Wilberding
0a4d2811f1 Merge pull request #98 from tsloughter/cf_fix
fix colorizing when not bold, type of fase for false
2015-11-21 13:59:50 -08:00
Tristan Sloughter
906a32e6c3 correct hex metadata to maintainers for contributors 2015-11-21 15:25:33 -06:00
Tristan Sloughter
60eb82aed5 fix colorizing when not bold, type of fase for false 2015-11-21 15:23:06 -06:00
Jordan Wilberding
05f062d23d Merge pull request #96 from tsloughter/rebar2_support
add rebar.config.script to fall back to rebar2 style deps if using rebar2
2015-11-03 20:05:53 -08:00
Tristan Sloughter
5ef37420b6 Merge pull request #97 from budzejko/master
fix DOWN messages from external sources issue
2015-11-02 14:32:59 -06:00
Jacek Budzejko
2240decb95 fix DOWN messages from external sources issue 2015-11-02 21:05:58 +01:00
Tristan Sloughter
505d35996d add rebar.config.script to fall back to rebar2 style deps if using rebar2 2015-11-02 09:46:25 -06:00
Tristan Sloughter
8c1b1133de Merge pull request #92 from project-fifo/cf-colouring
Update cmd_log to use cf for color encoding, including term detection
2015-10-31 17:38:36 -05:00
Tristan Sloughter
a4ed7d683e Merge pull request #93 from project-fifo/travis-update
Update to new travis infrastructure
2015-10-31 17:38:29 -05:00
Heinz N. Gies
74cc980f94 Update to new travis infrastructure 2015-10-31 23:31:50 +01:00
Heinz N. Gies
ad3eed2cd5 I'm done with R15 2015-10-31 23:27:25 +01:00
Heinz N. Gies
d7af2ecdfe Fix eunit typo 2015-10-31 23:23:30 +01:00
Heinz N. Gies
056377c2d8 Add R15 back in, run eunit instead of ct and use old rebar3 2015-10-31 23:17:20 +01:00
Heinz N. Gies
453922a6ed Bye bye R15 2015-10-31 23:11:19 +01:00
Heinz N. Gies
f7650e843d Bump travis R15 version to 03 2015-10-31 22:54:59 +01:00
Heinz N. Gies
28dd4e812e Add more debugs 2015-10-31 22:30:48 +01:00
Heinz N. Gies
19df1c0750 Add debug 2015-10-31 21:48:59 +01:00
Heinz N. Gies
0583ffa672 Update rebar3 2015-10-31 21:09:26 +01:00
Heinz N. Gies
f983b1ac85 Update cmd_log to use cf for color encoding, including term detection 2015-10-31 18:03:30 +01:00
Tristan Sloughter
95a8e3c32d Merge pull request #91 from project-fifo/less-colours
Alow less colourful output
2015-10-31 10:18:41 -05:00
Heinz N. Gies
4aedc36937 Add rebar3 update to test run 2015-09-24 19:58:24 +02:00
Heinz N. Gies
87c76aeb2a Alow less colourful output
Basically this adds a new/3 with a third argument of intensity.
The default (and by that the behaviour of new/1 and /2) remain
unchanged so existing code isn't affected.

If intenisty is set to `low` (instead of `high`) then only the
prefix is coloured making the whole output less colourful and
for some cases easyer to read.
2015-09-23 20:41:15 +02:00
Jordan Wilberding
49bc69e35a Merge pull request #89 from tsloughter/git_v_option
Git v option
2015-09-19 14:19:34 -07:00
Tristan Sloughter
3608a576fb version bump to 0.16.0 2015-09-19 13:08:11 -05:00
Tristan Sloughter
7c37ecf949 make v optional as prefix to version in git_vsn 2015-09-19 13:08:11 -05:00
Tristan Sloughter
26c600922e Merge pull request #88 from ericbmerritt/r18-support
R18 support
2015-09-07 12:59:05 -05:00
Eric Merritt
29c711bc39 remove ec_git_vsn_test
This test only had one test that dependended entirely on the
state of *this* git repository. Tests shouldn't depend on external
unmanaged state not changing.
2015-09-07 10:53:36 -07:00
Eric Merritt
739a9bcf24 Swap DEV_ONLY for TEST in the test exclusion macros 2015-09-07 10:45:58 -07:00
Eric Merritt
fbf7f7951c Add R18 (18.0) to the list of travis builds 2015-09-04 10:52:58 -07:00
Eric Merritt
885a516f03 remove proper as a testing tool from the package
PropEr has a problematic license and that makes it unusable.
Historically, there was some ideas that it was going to change but it
never did.
2015-09-04 10:48:21 -07:00
Jordan Wilberding
83adceaa1a Merge pull request #85 from tsloughter/master
version bump
2015-07-12 09:27:22 -07:00
Tristan Sloughter
6813d5184c version bump 2015-07-12 11:10:27 -05:00
Tristan Sloughter
3c69ca001c Merge pull request #84 from joedevivo/master
Made grep version command's grep POSIX
2015-07-12 11:07:35 -05:00
Joe DeVivo
381189c006 Made grep version command's grep POSIX 2015-07-12 07:31:08 -07:00
Tristan Sloughter
e56c73c940 Merge pull request #83 from joedevivo/master
Made version prefix configurable in ec_git_vsn
2015-07-10 12:02:54 -05:00
Joe DeVivo
2d634c5e46 Made version prefix configurable in ec_git_vsn 2015-07-10 09:37:21 -07:00
Jordan Wilberding
cbe494b1cb Merge pull request #82 from tsloughter/rebar3
move to rebar3 and bump version
2015-06-22 15:32:34 -07:00
Tristan Sloughter
d726ba2742 move to rebar3 and bump version 2015-06-22 17:06:50 -05:00
Jordan Wilberding
2e23e43079 Merge pull request #81 from tsloughter/master
fix conversion of ask/2 string
2015-06-01 11:15:41 -07:00
Tristan Sloughter
4c20e1903d fix conversion of ask/2 string 2015-05-31 12:04:54 -05:00
Jordan Wilberding
ef0d252b11 Merge pull request #79 from tsloughter/master
18.0 Support
2015-04-23 16:16:34 -07:00
Tristan Sloughter
7015ba2951 remove use of deprecated function erlang:now/0 2015-04-23 17:56:56 -05:00
Tristan Sloughter
e07d08333a use #atom{} syntax to support 18 2015-04-23 17:49:34 -05:00
Eric Merritt
05b956da26 Merge pull request #78 from jlouis/query-terminal
Introduce simple, preliminary TERM capability query.
2015-02-12 10:09:57 -08:00
Jesper Louis Andersen
aa373dddbe Introduce simple, preliminary TERM capability query.
When erlware_commons logs to the command_line, it assumes the
environment has common modern capabilities and color display. In
general, this is not the case and then color codes are sent verbatim
to the terminal.

This patch introduces a new field in #state_t{}, term_cap, encoding
if the terminal runs with 'full' or 'dumb' capabilities. In the latter case,
color display is suppressed. Initialization of the #state_t{} record queries
the environment once for the TERM variable in order to figure out what
it supports. The default is 'full' capability to be fully backwards compatible.
2015-02-11 17:10:16 +01:00
Jesper Louis Andersen
6079300634 Refactor: fold colorize_/3 into colorize/4.
Fold a local one-line leaf function with exactly one call-site into its caller.
2015-02-11 17:05:33 +01:00
Eric Merritt
02ab88513f Merge pull request #75 from muxspace/master
Support Twitter-style timestamps
2014-12-10 15:31:46 -08:00
Brian Lee Yung Rowe
441d11820d Parse Twitter-style dates 2014-12-10 10:59:24 -05:00
Jordan Wilberding
ae608d26e1 Merge pull request #74 from tsloughter/git_vsn
make git vsn the same as used in rebar_vsn_plugin
2014-11-03 13:57:18 -08:00
Tristan Sloughter
5ef8371020 make git vsn the same as used in rebar_vsn_plugin 2014-11-03 11:55:06 -06:00
Jordan Wilberding
d4d11ed1ba Merge pull request #71 from wk8/jr/iso8601
Adding an `ec_test:format_iso8601/1` fun to format datetimes according to ISO8601 standards
2014-10-01 14:21:41 -07:00
Jordan Wilberding
405c5506a9 Merge pull request #73 from wk8/jr/fix_regex_for_erl_17
v17 does not have the leading 'R'
2014-10-01 14:21:08 -07:00
Jean Rouge
0b47f60bfb Have dialyzer run on Travis for OTP 17 too
v17 does not have the leading 'R'
```
erl -noshell -eval 'io:format("~p", [erlang:system_info(otp_release)]), erlang:halt(0).'
"17"
```
So the dialyzer wouldn't run in the Travis builds.

And a minor spec fix to appease v17's dialyzer.
2014-10-01 12:31:47 -07:00
Jordan Wilberding
a299d45899 Merge pull request #72 from tsloughter/error_color
fix error log message being colored green instead of red
2014-09-27 12:32:40 -07:00
Tristan Sloughter
4fb4199da3 fix error log message being colored green instead of red 2014-09-27 14:06:49 -05:00
Jean Rouge
c25dce9689 Adding an ec_test:format_iso8601/1 fun to format datetimes according to ISO8601 standards
And added a couple unit tests on it.
2014-09-12 12:28:59 -07:00
Tristan Sloughter
03f76c6ef2 Merge pull request #68 from reset/recurse-copy-symlink-dirs
add ec_file:is_dir/1
2014-07-10 17:47:22 -05:00
Jamie Winsor
7a32f52e7d add ec_file:is_dir/1
use ec_file:is_dir/1 to identify symlinks which are directories
Ensure contents of symlinked directories are copied when using ec_file:copy/3
2014-07-09 14:51:50 -07:00
Jordan Wilberding
c2d67e6f76 Merge pull request #66 from tsloughter/master
Adding sha1sum method with r14/r15 support
2014-05-24 17:00:06 +02:00
Tristan Sloughter
3121c892b4 support r14/15 crypto 2014-05-24 09:42:57 -05:00
Tristan Sloughter
684afc1230 Merge pull request #56 from jwilberding/travis_bump
Update to use official 17.0 release for Travis
2014-05-21 15:58:52 -05:00
Low Kian Seong
73f21ee770 Adding sha1sum method 2014-05-16 10:38:13 +08:00
Jordan Wilberding
d1d19e3248 Update to use official 17.0 release for Travis 2014-05-15 09:36:05 +02:00
Jordan Wilberding
364726c166 Merge pull request #63 from lowks/master
changes to add another clause to the directory/file check
2014-05-15 09:34:28 +02:00
Low Kian Seong
cc7f6dd0df changes to add another clause to the directory/file check 2014-05-14 22:42:59 +08:00
Jordan Wilberding
6d742c0cae Merge pull request #60 from lowks/master
Found a typo while reading docs. Correcting
2014-05-08 08:01:55 +02:00
Low Kian Seong
bd6fd557d5 Found a typo while reading docs. Correcting 2014-05-08 11:10:38 +08:00
Jordan Wilberding
10ea4e18d6 Merge pull request #59 from tsloughter/master
handle deprecated pre-defined types for 17.0 and later
2014-04-22 19:51:00 +02:00
Tristan Sloughter
e2841d6cef remove 17.0rc1 from travis, wait for travis 17.0 support 2014-04-22 12:13:58 -05:00
Tristan Sloughter
523a66ad74 handle deprecated pre-defined types for 17.0 and later 2014-04-22 10:32:44 -05:00
Tristan Sloughter
4c97d4a962 Merge pull request #53 from jwilberding/fix_bug
Fix argument for iolist_to_binary call
2014-03-28 09:09:21 -05:00
Jordan Wilberding
449051bcd6 Fix argument for iolist_to_binary call 2014-03-28 08:39:26 +01:00
Jordan Wilberding
888be01dfe Merge pull request #52 from ericbmerritt/git-vsn-additions
Suppport a vsn signature with a git implementation
2014-03-27 11:13:38 +01:00
Eric B Merritt
47bcbd49b6 Suppport a vsn signature with a git implementation 2014-03-26 13:32:24 -07:00
Eric Merritt
7e64c9ea4f Merge pull request #49 from jwilberding/master
Add irc notifications
2014-03-25 18:14:31 -07:00
Jordan Wilberding
6fb5213f6a Merge pull request #51 from rlipscombe/rl-rebar-var
Allow overriding rebar location.
2014-03-19 18:14:25 +01:00
Roger Lipscombe
d9a83413af Allow overriding rebar location. 2014-03-19 12:38:07 +00:00
Jordan Wilberding
b125ce055e Merge pull request #50 from kiela/master
Make erlware_commons be compatible with Erlang 17.0-rc1
2014-02-19 13:42:26 +01:00
Kamil Kieliszczyk
741bba1b82 Make erlware_commons be compatible with Erlang 17.0-rc1 2014-02-19 12:18:04 +01:00
Kamil Kieliszczyk
f821e33806 Add R16Bx and 17.0-rc1 to .travis.yml 2014-02-19 12:07:15 +01:00
Jordan Wilberding
b9a2681a32 Add irc notifications 2013-10-25 12:39:02 -04:00
Eric Merritt
96cdc0e623 Merge pull request #48 from tsloughter/master
fix includings of headers for erldocs
2013-10-24 16:53:42 -07:00
Tristan Sloughter
06390816b9 fix includings of headers 2013-10-24 18:48:34 -05:00
Tristan Sloughter
71e6458251 Merge pull request #47 from ericbmerritt/master
fix various issues in ci and dialyzer
2013-10-14 17:39:57 -07:00
Eric Merritt
b8dc0eed6d make sure that travis runs dialyzer 2013-10-14 17:32:50 -07:00
Eric Merritt
efdd2a1092 fix dialyzer issues 2013-10-14 16:20:33 -07:00
Tristan Sloughter
2e61eb481d Merge pull request #46 from ericbmerritt/master
fix testing for erlware_commons
2013-10-14 14:23:22 -07:00
Eric Merritt
54f568e826 update the ec_semver_parser 2013-10-14 14:10:47 -07:00
Eric Merritt
16b441f0e3 fix up tests to reflect the actual NOTEST variable DEV_ONLY 2013-10-14 14:10:25 -07:00
Eric Merritt
4973a0fb8f fix formatting problems in ec_date 2013-10-14 13:52:45 -07:00
Tristan Sloughter
6949262dfe Merge pull request #45 from jwilberding/fix_travis
Fix travis by not making it run get-deps
2013-10-14 11:03:31 -07:00
Jordan Wilberding
031db98d27 Fix travis by not making it run get-deps 2013-10-14 13:42:26 -04:00
Jordan Wilberding
82ec98667e Merge pull request #44 from ericbmerritt/master
additional features around command line output support and  type conversions
2013-10-14 09:35:42 -07:00
Eric Merritt
558185b9b9 add a command line programming module to the system
This is a general module that helps output from command line
applications written in erlang.
2013-10-14 09:26:54 -07:00
Eric Merritt
79c5436c85 add utility type conversions to erlware commons 2013-10-14 09:26:47 -07:00
Jordan Wilberding
615d788cab Merge pull request #43 from choptastic/vim-patch
Add vi modelines to files. Ignore vim backup files
2013-09-17 05:12:02 -07:00
Jesse Gumm
8bc27f62fd Add vi modelines to files. Ignore vim backup files 2013-08-30 06:56:06 -05:00
Jordan Wilberding
0fbd4576ce Merge pull request #42 from ericbmerritt/next
minor bugfixes and cleanup
2013-04-24 11:12:01 -07:00
Eric B Merritt
417a4c3229 minor enhancements and cleanup to the makefile 2013-04-24 09:55:01 -07:00
Eric B Merritt
7e2c37e2f5 fix bug in rebuild introduced by new makefile changes 2013-04-24 09:54:59 -07:00
Eric B Merritt
8870c422ca minor updates from a new version of neotoma 2013-04-24 09:54:55 -07:00
Eric B Merritt
652fcc1e23 remove ec_file:consult/1 as it provides very little value Fixes #1 2013-04-24 09:54:34 -07:00
Jordan Wilberding
f36f333374 Merge pull request #40 from choptastic/fix-12pm
Fix Formatting AM/PM for 12PM ({Date,{12,0,0}})
2013-04-23 19:02:01 -07:00
Eric Merritt
a5b713c9bb Merge pull request #39 from seth/sf/soft-dep-neotoma
Make proper and neotoma development-time only dependencies
2013-04-23 17:40:05 -07:00
Jesse Gumm
e984618c3e Fix Formatting AM/PM for 12PM ({Date,{12,0,0}}) 2013-04-23 17:07:52 -05:00
Seth Falcon
d9c6ec1d28 Make proper and neotoma dev-only dependencies
This patch makes erlware_commons easier to include as a dependency by
removing depedencies that are not needed at run time.

The top-level Makefile creates a .DEV_MODE marker file which is
detected by rebar.config.script. When the marker file is present, the
development only dependencies proper and neotoma are included and a
macro 'DEV_ONLY' is defined. The macro is used to only enable the
proper tests for development mode.

The ec_semver_parser.peg is now located in priv/ and is moved into
src/ by the Makefile. The generated ec_semver_parser.erl is now under
version control; it need not be rebuilt by all projects wishing to
include erlware_commons. It will be rebuilt, as before this change, on
every make invocation.
2013-04-22 14:48:08 -07:00
Seth Falcon
38cd7a4d62 Use deps directory to trigger get-deps
A `make distclean` now removes the deps directory and all of its
contents. The default target now looks for the presence of the deps
directory to determine if `rebar get-deps compile` should be run. The
main benefit being that one can now do: `make distclean && make`
without a call to `make get-deps`. The get-deps target is left in
place for convenince and back compat.

The get-deps target was updated slightly to run get-deps and compile
as part of a single command. This should be slightly more efficient.
2013-04-22 12:58:47 -07:00
Seth Falcon
38141c5c24 Fix typo in typer target in Makefile
The typer target now works.
2013-04-22 12:44:59 -07:00
Tristan Sloughter
d806fbd119 Merge pull request #38 from ericbmerritt/next
Add contributing document
2013-04-09 15:52:14 -07:00
Eric B Merritt
a1fc04f2b7 Add contributing document 2013-04-09 15:40:31 -07:00
Eric B Merritt
1906888a05 Merge remote-tracking branch 'canonical/next' 2013-04-09 15:32:14 -07:00
Tristan Sloughter
d78358af0b Merge pull request #37 from ericbmerritt/next
make ec_file a bit more friendly to binary file names
2013-04-09 15:00:48 -07:00
Eric B Merritt
58e6b0476d support testing on r16 2013-04-09 10:51:13 -07:00
Eric B Merritt
17e08c04a2 make ec_file a bit more friendly to binary file names 2013-04-09 10:51:11 -07:00
Eric Merritt
fd505767e5 Merge remote-tracking branch 'canonical/next' 2013-03-15 08:43:27 -07:00
Jordan Wilberding
1cd615ef58 Merge pull request #35 from ericbmerritt/next
add file type discovery and resolution to ec_file
2013-03-14 20:46:01 -07:00
Eric Merritt
e1e30f4a75 add file type discovery and resolution to ec_file 2013-03-14 11:50:49 -07:00
Jesse Gumm
4ba12ec4ad Add disambig tests, add disambig parsing date-only
Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2013-02-26 18:03:30 -05:00
Jesse Gumm
a2fac85ff6 Disambiguate parsing "Aug 12" and "12 Aug".
This started with just trying to parse the date format:

December 21st, 2013 7:00pm, which was failing with a bad_date error.

The solution involved setting up "Hinted Months", which was just a term
I used to indicate that a month was specified by name (ie "December"),
rather than by number (ie, "12"). Previously, named months were simply
replaced by their respective numbers in the parser.  This tags those
named months so that the parser will unambiguously parse them correctly.

A tagged "Hinted Month" is simply a tuple with the tag `?MONTH_TAG`. For
example: "December" gets converted to `{?MONTH_TAG, 12}`

For example: "Aug 12" and "12 Aug". It's clear to the *reader* what is
meant, but when converted to simply 8 and 12, the parser has no way of
knowing which is which.

Doing this was aided with the addition of some macros to help it
along, since doing just straight comparisons with the hinted months was
yielding unexpected results. For example: `{mon, 1} > 31`  returns
true, so changing that comparison to an ?is_year/1 macro that does:
`is_integer(Y) andalso Y > 31`.

It might not be a bad idea to help the parser be *very* unambiguous by
putting these macros on all comparisons.

Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2013-02-26 18:03:21 -05:00
Ben Kearns
97d39ec8db Added support for ISO8601 Zulu and TZ time zone support. Remove hard coded version string in rebar.config.script for unit test pass. Remove dializer and edoc from default target to enable tests to run on Travis-ci
Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2013-02-26 17:58:08 -05:00
Ben Kearns
320813e56e Fixed type-o in .travis.yml
Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2013-02-26 17:50:05 -05:00
Ben Kearns
4558635813 Fix broken tests.
Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2013-02-26 17:49:38 -05:00
Ben Kearns
122af09cb1 Added more tests and fixed format string to be 24 hour vs 12 hour.
Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2013-02-26 17:49:27 -05:00
Ben Kearns
5b23329d3a Added tests and validated parse->format->parse and nparse->format->nparse
Conflicts:

	src/ec_date.erl
2013-02-26 17:48:51 -05:00
Ben Kearns
5beeb3ff1b Fix for dializer error.
Conflicts:

	src/ec_date.erl
2013-02-26 17:44:31 -05:00
Ben Kearns
3437fc8c1c Another fix for spec messages.
Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2013-02-26 17:43:03 -05:00
Ben Kearns
5c6af5c7f5 Added dializer fix for new date format.
Conflicts:

	src/ec_date.erl
2013-02-26 17:42:32 -05:00
Ben Kearns
e28130d9f3 Added parsing of ISO formats i.e. "2012-12-19T12:12:12.00001"
Conflicts:

	src/ec_date.erl
2013-02-26 17:40:13 -05:00
Eric Merritt
4811393957 Merge remote-tracking branch 'canonical/next' 2013-01-21 10:55:52 -08:00
Ben Kearns
5429ec2d14 Point rebar to fix of proper library for eunit.
Signed-off-by: Eric Merritt <ericbmerritt@gmail.com>
2013-01-21 10:51:11 -08:00
Ben Kearns
21c5f9fc74 Added parsing of ISO formats i.e. "2012-12-19T12:12:12.00001"
Signed-off-by: Eric Merritt <ericbmerritt@gmail.com>
2013-01-21 10:51:10 -08:00
Jordan Wilberding
0f409c0bf3 Merge pull request #32 from ericbmerritt/next
Next
2013-01-17 14:44:14 -08:00
Eric Merritt
74b0d7318d support parsing a version with a leading 'v'
This *should* make version parsing much, much simpler.
2013-01-17 14:31:45 -08:00
Eric Merritt
407ccf886f support printing erl_syntax meta calls in the same way as erl source
Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2012-12-07 05:53:25 -07:00
Eric Merritt
95f723e1e0 make erlware_commons work on pre-R15 releases
Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2012-12-07 05:53:21 -07:00
Eric Merritt
1a39438f3c Merge remote-tracking branch 'canonical/next'
* canonical/next:
  massively expand the documentation in the README
  bring ec_plists up to erlware standards
  replace ec_plists with Stephan's plists
  add Stephen Marsh's plists to the system
  reorder default tasks so dialyzer is run after compile
  add a clean and rebuild task to makefile
  add fullpath to the makefile
  enable the rebar semver plugin on erlware_commons
  cleanup the rebar config
  support non-numeric versions in major/minor/patch/minor-patch
  support reasonable versioning for erlware_commons
  add exists to ec_file
  fix bug in ec_file:copy/3 spec
  export mkdir_p (this should have been done already)
  support four primary version numbers of in parsing
  minor whitespace cleanup for ec_semver
  provide the ability to format a version into a string as well as parse a version
  make sure the docs get run as part of a bare make
  fixes for edoc compilation
  compilation utilities for the implementors
2012-10-30 13:03:29 -05:00
Eric Merritt
8d300f5d02 massively expand the documentation in the README
Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2012-10-30 11:45:21 -06:00
Eric Merritt
c7717743ed bring ec_plists up to erlware standards
Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2012-10-30 11:45:21 -06:00
Eric Merritt
31ebca114a replace ec_plists with Stephan's plists
Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2012-10-30 11:45:21 -06:00
Eric Merritt
0e10d59b3a add Stephen Marsh's plists to the system
origin: http://code.google.com/p/plists/
detail: http://plists.wordpress.com/2007/09/20/introducing-plists-an-erlang-module-for-doing-list-operations-in-parallel/
Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2012-10-30 11:45:21 -06:00
Eric Merritt
dda4c85586 reorder default tasks so dialyzer is run after compile
Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2012-10-30 11:45:21 -06:00
Eric Merritt
1a1b87bf53 add a clean and rebuild task to makefile
Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2012-10-30 11:45:21 -06:00
Eric Merritt
b5371974d1 add fullpath to the makefile
Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2012-10-30 11:45:21 -06:00
Eric Merritt
c4887e2021 enable the rebar semver plugin on erlware_commons
Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2012-10-30 11:45:21 -06:00
Eric Merritt
1540fb1652 cleanup the rebar config
Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2012-10-30 11:45:21 -06:00
Eric Merritt
7e4ba401fd support non-numeric versions in major/minor/patch/minor-patch
This allows for two things. The first is support for non rigorous
versions. However, it still fully supports semver. So if you have
semver versions they work correctly, if you have alpha versions they
also work correctly but using natural alpha ordering.

Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2012-10-19 11:15:27 -06:00
Eric Merritt
0db7042ff9 support reasonable versioning for erlware_commons
You should get the latest and greatest rebar to build this.

Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2012-10-19 11:15:15 -06:00
Eric Merritt
f77afd43c3 add exists to ec_file
Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2012-10-19 11:15:09 -06:00
Eric Merritt
a9f2a771f0 fix bug in ec_file:copy/3 spec
Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2012-09-18 20:06:47 -06:00
Eric Merritt
eab58fb660 export mkdir_p (this should have been done already)
Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2012-09-18 20:06:47 -06:00
Eric Merritt
b4ab414419 support four primary version numbers of in parsing
The OTP Versions distributed with erlang tend to have four version
numbers not three. This is a fairly minor deviation from semver that
we can support. Basically, the semver parser treats the fourth version
in exactly the same way as the other three.

Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2012-09-18 20:06:47 -06:00
Eric Merritt
5105df48f9 minor whitespace cleanup for ec_semver
Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2012-09-18 20:06:47 -06:00
Eric Merritt
e9161d8688 provide the ability to format a version into a string as well as parse a version
Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2012-09-18 20:06:47 -06:00
Eric Merritt
0c34549901 make sure the docs get run as part of a bare make
Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2012-09-18 20:06:47 -06:00
Eric Merritt
3a29539285 fixes for edoc compilation
Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2012-09-18 20:06:47 -06:00
Eric Merritt
1b01380613 compilation utilities for the implementors
Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2012-09-18 20:06:47 -06:00
Eric Merritt
25ef2f3496 Merge branch 'next'
* next:
  version bump 0.8.0
  minor fixes and enhancements to the makefile
  given a complete semver parser ec_string no longer makes sense to retain
  suport proper semver parsing and comparison in the semver module
  move ec_file away from exceptions to return values
  minor whitespace cleanup
  make sure ec_dictionary gets built first
  fixes to dialyzer
  add travis support to the system
  fix edoc errors in various modules
  add a full makefile that drives rebar
  add rebar files to gitignore
2012-09-08 10:00:03 -05:00
Eric Merritt
c42581887a version bump 0.8.0
Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2012-09-08 09:59:31 -05:00
Eric Merritt
67acaaaf3f minor fixes and enhancements to the makefile
Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2012-09-08 09:59:30 -05:00
Eric Merritt
4db670c812 given a complete semver parser ec_string no longer makes sense to retain
It was really poorly named in any case

Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2012-09-08 09:59:28 -05:00
Eric Merritt
bf37ad9492 suport proper semver parsing and comparison in the semver module
Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2012-09-08 09:59:27 -05:00
Eric Merritt
9b9f070a5f move ec_file away from exceptions to return values
In an attempt to unify on the accepted use of return values ec_file is
changing its API to use return values instead of exceptions.

Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2012-09-08 09:59:26 -05:00
Eric Merritt
a2672cafb1 minor whitespace cleanup
Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2012-09-08 09:59:24 -05:00
Eric Merritt
cf8cad00df make sure ec_dictionary gets built first
Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2012-09-08 09:59:23 -05:00
Eric Merritt
e035ae3dbf fixes to dialyzer
All types should now be correct and dialyzer runs successfully

Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2012-09-08 09:59:21 -05:00
Eric Merritt
7e42c243b0 add travis support to the system
Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2012-09-08 09:59:19 -05:00
Eric Merritt
d7b60ccf19 fix edoc errors in various modules
Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2012-09-08 09:51:32 -05:00
Eric Merritt
51b5a41c63 add a full makefile that drives rebar
Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2012-09-08 09:51:32 -05:00
Eric Merritt
9514b16993 add rebar files to gitignore
Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2012-09-08 09:50:43 -05:00
Jordan Wilberding
8d625ceb46 Merge remote branch 'ericbmerritt/master' into rv
* ericbmerritt/master:
  add beam files to gitignore
  make insecure nature of ec_file:mkdtemp obvious fixes erlware/erlware_commons#16
  fix eunit tests so that they actually work and run
  make mkdtemp a lot more secure (still not fully secure but more).
  add . files to gitignore
  Migrate erlware_commons to rebar support

Signed-off-by: Jordan Wilberding <diginux@gmail.com>
2012-06-05 19:58:17 -04:00
Eric Merritt
3e5eeb8cf7 add beam files to gitignore 2012-06-05 17:59:54 -05:00
Eric Merritt
b4eb83cf53 make insecure nature of ec_file:mkdtemp obvious fixes erlware/erlware_commons#16 2012-06-05 17:59:52 -05:00
Eric Merritt
261fb422f9 fix eunit tests so that they actually work and run 2012-06-05 17:59:51 -05:00
Eric Merritt
10557e421e make mkdtemp a lot more secure (still not fully secure but more). 2012-06-05 17:59:50 -05:00
Eric Merritt
3e6357aea9 add . files to gitignore 2012-06-05 17:59:49 -05:00
Eric Merritt
c828f7415a Migrate erlware_commons to rebar support 2012-06-05 17:59:46 -05:00
Eric Merritt
26a2e91d4a Version bump 0.6.2 2012-03-23 10:01:33 -05:00
46 changed files with 4653 additions and 1950 deletions

31
.github/workflows/main.yml vendored Normal file
View 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
View file

@ -1,3 +1,18 @@
.erlware_commons_plt
.eunit/*
deps/*
doc/*.html
doc/*.css
doc/edoc-info
doc/erlang.png
ebin/*
.*
!.github
_build
erl_crash.dump
*.pyc
*~
TEST-*.xml
/foo
src/ec_semver_parser.peg

139
CONTRIBUTING.md Normal file
View 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.

View file

@ -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
View file

@ -1,6 +1,12 @@
Erlware Commons
===============
Current Status
--------------
[![Hex.pm](https://img.shields.io/hexpm/v/erlware_commons)](https://hex.pm/packages/erlware_commons)
[![Tests](https://github.com/erlware/erlware_commons/workflows/EUnit/badge.svg)](https://github.com/erlware/erlware_commons/actions)
Introduction
------------
@ -19,3 +25,119 @@ Goals for the project
* High Quality
* Well Documented
* 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.

View file

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

View file

@ -2,10 +2,10 @@ Signatures
==========
It often occurs in coding that we need a library, a set of
functionaly. Often there are several algorithms that could provide
this functionality. However, the code that uses it, either doesn't
functionalities. Often there are several algorithms that could provide
each of these functionalities. However, the code that uses it, either doesn't
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
a key (other things as well but primarily this). There are may ways to
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)
* Many, many more ....
Each of these approaches has there own performance characteristics,
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
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)
Each of these approaches has their own performance characteristics,
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
comparison for successful lookup, and a table of size $n$ with chaining
and $k$ keys has the minimum $\max(0, k-n)$ collisions and $\mathcal{O}(1 + k/n)$
comparisons for lookup. While for skip lists the performance
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,
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
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
it is going to be called on.
:::erlang
add(ModuleToUse, Key, Value, DictData) ->
ModuleToUse:add(Key, Value, DictData).
```erlang
add(ModuleToUse, Key, Value, DictData) ->
ModuleToUse:add(Key, Value, DictData).
```
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,
you could pass in `{ModuleToUse, DictData}` and that would make it a
bit cleaner.
:::erlang
add(Key, Value, {ModuleToUse, DictData}) ->
ModuleToUse:add(Key, Value, DictData).
```erlang
add(Key, Value, {ModuleToUse, DictData}) ->
ModuleToUse:add(Key, Value, DictData).
```
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
@ -63,21 +66,22 @@ mistakes that you might have made. Tools like
[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
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
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
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
name.
So what we actually want to do is something mole like this:
:::erlang
add(Key, Value, DictData) ->
dictionary:add(Key, Value, DictData).
```erlang
add(Key, Value, DictData) ->
dictionary:add(Key, Value, DictData).
```
Doing this we retain the locality. We can easily look up the
`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
----------
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
a [Behaviour](http://metajack.im/2008/10/29/custom-behaviors-in-erlang/)
for our functionality. To continue our example we will define a
Behaviour for dictionaries. That Behaviour looks like this:
:::erlang
-module(ec_dictionary).
```erlang
-module(ec_dictionary).
-export([behaviour_info/1]).
-export([behaviour_info/1]).
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.
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.
```
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
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
a bit of functionality. We do that by actually implementing our own
behaviour, starting with `new/1`.
:::erlang
%% @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()) -> dictionary(_K, _V).
new(ModuleName) when is_atom(ModuleName) ->
#dict_t{callback = ModuleName, data = ModuleName:new()}.
```erlang
%% @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()) -> dictionary(_K, _V).
new(ModuleName) when is_atom(ModuleName) ->
#dict_t{callback = ModuleName, data = ModuleName:new()}.
```
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
subsequently in other calls. This might look a bit familiar from our
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
and return that record to the caller and we have a new
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
adding a value.
:::erlang
%% @doc add a new value to the existing dictionary. Return a new
%% dictionary containing the value.
%%
%% @param Dict the dictionary object to add too
%% @param Key the key to add
%% @param Value the value to add
-spec add(key(K), value(V), dictionary(K, V)) -> dictionary(K, V).
add(Key, Value, #dict_t{callback = Mod, data = Data} = Dict) ->
Dict#dict_t{data = Mod:add(Key, Value, Data)}.
```erlang
%% @doc add a new value to the existing dictionary. Return a new
%% dictionary containing the value.
%%
%% @param Dict the dictionary object to add too
%% @param Key the key to add
%% @param Value the value to add
-spec add(key(K), value(V), dictionary(K, V)) -> dictionary(K, V).
add(Key, Value, #dict_t{callback = Mod, data = Data} = Dict) ->
Dict#dict_t{data = Mod:add(Key, Value, Data)}.
```
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
of the dictionary Signature.
:::erlang
%% @doc given a key return that key from the dictionary. If the key is
%% not found throw a 'not_found' exception.
%%
%% @param Dict The dictionary object to return the value from
%% @param Key The key requested
%% @throws not_found when the key does not exist
-spec get(key(K), dictionary(K, V)) -> value(V).
get(Key, #dict_t{callback = Mod, data = Data}) ->
Mod:get(Key, Data).
```erlang
%% @doc given a key return that key from the dictionary. If the key is
%% not found throw a 'not_found' exception.
%%
%% @param Dict The dictionary object to return the value from
%% @param Key The key requested
%% @throws not_found when the key does not exist
-spec get(key(K), dictionary(K, V)) -> value(V).
get(Key, #dict_t{callback = Mod, data = Data}) ->
Mod:get(Key, Data).
```
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
@ -197,7 +205,7 @@ implementation in
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
have some questions about what kind of performance burden this places
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.
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.
### Native Signature Implementations
@ -223,32 +231,33 @@ implements the ec_dictionary module directly.
A Signature Wrapper is a module that wraps another module. Its
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
[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
[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
`get` function an ec_dictionary `get` doesn't have quite the same
semantics as any of the functions in the dict module. So a bit of
translation needs to be done. We do that in the ec_dict module `get` function.
`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
translation needs to be done. We do that in the `ec_dict:get/2` function.
:::erlang
-spec get(ec_dictionary:key(K), Object::dictionary(K, V)) ->
ec_dictionary:value(V).
get(Key, Data) ->
case dict:find(Key, Data) of
{ok, Value} ->
Value;
error ->
throw(not_found)
end.
```erlang
-spec get(ec_dictionary:key(K), Object::dictionary(K, V)) ->
ec_dictionary:value(V).
get(Key, Data) ->
case dict:find(Key, Data) of
{ok, Value} ->
Value;
error ->
throw(not_found)
end.
```
So the ec_dict module's purpose for existence is to help the
preexisting dict module implement the Behaviour defined by the
So the `ec_dict` module's purpose for existence is to help the
preexisting `dict` module implement the Behaviour defined by the
Signature.
@ -258,24 +267,25 @@ the mix and that adds a bit of additional overhead.
### 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.
Lets get started by looking at some helper functions. We want
dictionaries to have a bit of data in them. So to that end we are will
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 will
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
`dict` vs `ec_dict` called as a Signature.
:::erlang
create_dict() ->
```erlang
create_dict() ->
lists:foldl(fun(El, Dict) ->
dict:store(El, El, Dict)
end, dict:new(),
lists:seq(1,100)).
dict:store(El, El, Dict)
end, dict:new(),
lists:seq(1,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
timing the function call overhead of Signatures, not the performance
of the dictionaries themselves.
@ -283,58 +293,61 @@ of the dictionaries themselves.
We need to create a similar function for our Signature based
dictionary `ec_dict`.
:::erlang
create_dictionary(Type) ->
```erlang
create_dictionary(Type) ->
lists:foldl(fun(El, Dict) ->
ec_dictionary:add(El, El, Dict)
end,
ec_dictionary:new(Type),
lists:seq(1,100)).
ec_dictionary:add(El, El, Dict)
end,
ec_dictionary:new(Type),
lists:seq(1,100)).
```
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
create anything that implements the Signature. That is the magic of
Signatures. Otherwise, this does the exact same thing as the dict
`create_dict/1`.
Signatures. Otherwise, this does the exact same thing as the dictionary
given by `create_dict/0`.
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
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.
:::erlang
time_direct_vs_signature_dict() ->
io:format("Timing dict~n"),
Dict = create_dict(),
test_avg(fun() ->
dict:size(dict:store(some_key, some_value, Dict))
end,
1000000),
io:format("Timing ec_dict implementation of ec_dictionary~n"),
time_dict_type(ec_dict).
```erlang
time_direct_vs_signature_dict() ->
io:format("Timing dict~n"),
Dict = create_dict(),
test_avg(fun() ->
dict:size(dict:store(some_key, some_value, Dict))
end,
1000000),
io:format("Timing ec_dict implementation of ec_dictionary~n"),
time_dict_type(ec_dict).
```
The `test_avg` function runs the provided function the number of times
specified in the second argument and collects timing information. We
are going to run these one million times to get a good average (its
fast so it doesn't take long). You can see that in the anonymous
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 in the anonymous
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
we don't have to hard code the calls for the Signature
implementations. Lets take a look at the `time_dict_type` function.
:::erlang
time_dict_type(Type) ->
io:format("Testing ~p~n", [Type]),
Dict = create_dictionary(Type),
test_avg(fun() ->
ec_dictionary:size(ec_dictionary:add(some_key, some_value, Dict))
end,
1000000).
```erlang
time_dict_type(Type) ->
io:format("Testing ~p~n", [Type]),
Dict = create_dictionary(Type),
test_avg(fun() ->
ec_dictionary:size(ec_dictionary:add(some_key, some_value, Dict))
end,
1000000).
```
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
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
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
what it looked like.
:::sh
Erlang R14B01 (erts-5.8.2) [source] [64-bit] [smp:4:4] [rq:4] [async-threads:0] [hipe] [kernel-poll:false]
```sh
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().
Timing dict
Range: 2 - 5621 mics
Median: 3 mics
Average: 3 mics
Timing ec_dict implementation of ec_dictionary
Testing ec_dict
Range: 3 - 6097 mics
Median: 3 mics
Average: 4 mics
2>
1> ec_timing:time_direct_vs_signature_dict().
Timing dict
Range: 2 - 5621 mics
Median: 3 mics
Average: 3 mics
Timing ec_dict implementation of ec_dictionary
Testing ec_dict
Range: 3 - 6097 mics
Median: 3 mics
Average: 4 mics
2>
```
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
So for the direct `dict` call, we average about 3 mics per call, while
for the Signature Wrapper we average around 4. That's a 25% cost for
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
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
to create a creation helper just like we did for dict.
:::erlang
create_rbdict() ->
```erlang
create_rbdict() ->
lists:foldl(fun(El, Dict) ->
ec_rbdict:add(El, El, Dict)
end, ec_rbdict:new(),
lists:seq(1,100)).
ec_rbdict:add(El, El, Dict)
end, ec_rbdict:new(),
lists:seq(1,100)).
```
This is exactly the same as `create_dict` with the exception that dict
is replaced by `ec_rbdict`.
The timing function itself looks very similar as well. Again notice
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
time_direct_vs_signature_rbdict() ->
io:format("Timing rbdict~n"),
Dict = create_rbdict(),
test_avg(fun() ->
ec_rbdict:size(ec_rbdict:add(some_key, some_value, Dict))
end,
1000000),
io:format("Timing ec_dict implementation of ec_dictionary~n"),
time_dict_type(ec_rbdict).
```erlang
time_direct_vs_signature_rbdict() ->
io:format("Timing rbdict~n"),
Dict = create_rbdict(),
test_avg(fun() ->
ec_rbdict:size(ec_rbdict:add(some_key, some_value, Dict))
end,
1000000),
io:format("Timing ec_dict implementation of ec_dictionary~n"),
time_dict_type(ec_rbdict).
```
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
results.
:::sh
Erlang R14B01 (erts-5.8.2) [source] [64-bit] [smp:4:4] [rq:4] [async-threads:0] [hipe] [kernel-poll:false]
```sh
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().
Timing rbdict
Range: 6 - 15070 mics
Median: 7 mics
Average: 7 mics
Timing ec_dict implementation of ec_dictionary
Testing ec_rbdict
Range: 6 - 6013 mics
Median: 7 mics
Average: 7 mics
2>
1> ec_timing:time_direct_vs_signature_rbdict().
Timing rbdict
Range: 6 - 15070 mics
Median: 7 mics
Average: 7 mics
Timing ec_dict implementation of ec_dictionary
Testing ec_rbdict
Range: 6 - 6013 mics
Median: 7 mics
Average: 7 mics
2>
```
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
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.
Conclusion
----------
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
while retaining testability and locality.
@ -456,7 +473,7 @@ Signature Wrapper
### Code Referenced
* [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_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_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_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)

View file

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

View 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
View 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
View 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
View 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

Binary file not shown.

View file

@ -1,11 +1,12 @@
%%% vi:ts=4 sw=4 et
%%%-------------------------------------------------------------------
%%% @author Eric Merritt <ericbmerritt@gmail.com>
%%% @copyright 2011 Erlware, LLC.
%%% @doc
%%% provides an implementation of ec_dictionary using an association
%%% list as a basy
%%% see ec_dictionary
%%% @end
%%% @see ec_dictionary
%%%-------------------------------------------------------------------
-module(ec_assoc_list).
@ -29,8 +30,10 @@
%%%===================================================================
%%% Types
%%%===================================================================
-opaque dictionary(K, V) :: {ec_assoc_list,
[{ec_dictionary:key(K), ec_dictionary:value(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) :: {ec_assoc_list,
[{ec_dictionary:key(K), ec_dictionary:value(V)}]}.
%%%===================================================================
%%% API
@ -45,12 +48,12 @@ has_key(Key, {ec_assoc_list, Data}) ->
lists:keymember(Key, 1, Data).
-spec get(ec_dictionary:key(K), Object::dictionary(K, V)) ->
ec_dictionary:value(V).
ec_dictionary:value(V).
get(Key, {ec_assoc_list, Data}) ->
case lists:keyfind(Key, 1, Data) of
{Key, Value} ->
Value;
false ->
false ->
throw(not_found)
end.
@ -62,19 +65,19 @@ get(Key, Default, {ec_assoc_list, Data}) ->
case lists:keyfind(Key, 1, Data) of
{Key, Value} ->
Value;
false ->
false ->
Default
end.
-spec add(ec_dictionary:key(K), ec_dictionary:value(V),
Object::dictionary(K, V)) ->
dictionary(K, V).
dictionary(K, V).
add(Key, Value, {ec_assoc_list, _Data}=Dict) ->
{ec_assoc_list, Rest} = remove(Key,Dict),
{ec_assoc_list, [{Key, Value} | Rest ]}.
-spec remove(ec_dictionary:key(K), Object::dictionary(K, _V)) ->
dictionary(K, _V).
dictionary(K, _V).
remove(Key, {ec_assoc_list, 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}) ->
lists:keymember(Value, 2, Data).
-spec size(Object::dictionary(_K, _V)) -> integer().
-spec size(Object::dictionary(_K, _V)) -> non_neg_integer().
size({ec_assoc_list, Data}) ->
length(Data).
-spec to_list(dictionary(K, V)) -> [{ec_dictionary:key(K),
ec_dictionary:value(V)}].
to_list({ec_assoc_list, Data}) ->
Data.
Data.
-spec from_list([{ec_dictionary:key(K), ec_dictionary:value(V)}]) ->
dictionary(K, V).
dictionary(K, V).
from_list(List) when is_list(List) ->
{ec_assoc_list, List}.
-spec keys(dictionary(K, _V)) -> [ec_dictionary:key(K)].
keys({ec_assoc_list, Data}) ->
lists:map(fun({Key, _Value}) ->
Key
end, Data).
Key
end, Data).

257
src/ec_cmd_log.erl Normal file
View 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
View 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
View 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
View 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))).

View file

@ -1,3 +1,4 @@
%% vi:ts=4 sw=4 et
%% @copyright Dale Harvey
%% @doc Format dates in erlang
%%
@ -24,8 +25,10 @@
-author("Dale Harvey <dale@hypernumbers.com>").
-export([format/1, format/2]).
-export([format_iso8601/1]).
-export([parse/1, parse/2]).
-export([nparse/1]).
-export([tokenise/2]).
%% These are used exclusively as guards and so the function like
%% defines make sense
@ -34,17 +37,28 @@
-define( is_us_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 month() :: 1..12.
-type month() :: 1..12 | {?MONTH_TAG, 1..12}.
-type day() :: 1..31.
-type hour() :: 0..23.
-type minute() :: 0..59.
-type second() :: 0..59.
-type microsecond() :: 0..999_999.
-type daynum() :: 1..7.
-type date() :: {year(),month(),day()}.
-type time() :: {hour(),minute(),second()}.
-type time() :: {hour(),minute(),second()} | {hour(),minute(),second(),microsecond()}.
-type datetime() :: {date(),time()}.
-type now() :: {integer(),integer(),integer()}.
@ -59,17 +73,27 @@ format(Format) ->
-spec format(string(),datetime() | now()) -> string().
%% @doc format Date as Format
format(Format, {_,_,_}=Now) ->
format(Format, calendar:now_to_datetime(Now), []);
format(Format, {_,_,Ms}=Now) ->
{Date,{H,M,S}} = calendar:now_to_datetime(Now),
format(Format, {Date, {H,M,S,Ms}}, []);
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().
%% @doc parses the datetime from a string
parse(Date) ->
do_parse(Date, calendar:universal_time(),[]).
-spec parse(string(),datetime() | now()) -> datetime().
%% @doc parses the datetime from a string
parse(Date, {_,_,_}=Now) ->
do_parse(Date, calendar:now_to_datetime(Now), []);
@ -77,47 +101,147 @@ parse(Date, Now) ->
do_parse(Date, Now, []).
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} ->
erlang:throw({?MODULE, {bad_date, Date}});
{D1, T1} = {{Y, M, D}, {H, M1, S}}
when is_number(Y), is_number(M),
is_number(D), is_number(H),
is_number(M1), is_number(S) ->
when is_number(Y), is_number(M),
is_number(D), is_number(H),
is_number(M1), is_number(S) ->
case calendar:valid_date(D1) of
true -> {D1, T1};
false -> erlang:throw({?MODULE, {bad_date, Date}})
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.
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().
%% @doc parses the datetime from a string into 'now' format
nparse(Date) ->
DateTime = parse(Date),
GSeconds = calendar:datetime_to_gregorian_seconds(DateTime),
ESeconds = GSeconds - ?GREGORIAN_SECONDS_1970,
{ESeconds div 1000000, ESeconds rem 1000000, 0}.
case parse(Date) of
{DateS, {H, M, S, Ms} } ->
GSeconds = calendar:datetime_to_gregorian_seconds({DateS, {H, M, S} }),
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
%%
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
parse([Hour,$:,Min,$:,Sec | PAM], {Date, _Time}, _O) when ?is_meridian(PAM) ->
{Date, {hour(Hour, PAM), Min, Sec}};
parse([Hour,$:,Min | PAM], {Date, _Time}, _Opts) when ?is_meridian(PAM) ->
{Date, {hour(Hour, PAM), Min, 0}};
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
parse([Day,Month,Year], {_Date, Time}, _Opts) ->
{{Year, Month, Day}, Time};
parse([Year,X,Month,X,Day], {_Date, Time}, _Opts)
when (?is_us_sep(X) orelse ?is_world_sep(X))
andalso Year > 31 ->
andalso ?is_year(Year) ->
{{Year, Month, Day}, Time};
parse([Month,X,Day,X,Year], {_Date, Time}, _Opts) when ?is_us_sep(X) ->
{{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)
when ?is_meridian(PAM) andalso
(?is_us_sep(X) orelse ?is_world_sep(X))
andalso Year > 31 ->
andalso ?is_year(Year) ->
{{Year, Month, Day}, {hour(Hour, PAM), 0, 0}};
parse([Day,X,Month,X,Year,Hour | PAM], _Date, _Opts)
when ?is_meridian(PAM) andalso ?is_world_sep(X) ->
{{Year, Month, Day}, {hour(Hour, PAM), 0, 0}};
parse([Month,X,Day,X,Year,Hour | PAM], _Date, _Opts)
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)
when ?is_meridian(PAM) andalso
(?is_us_sep(X) orelse ?is_world_sep(X))
andalso Year > 31 ->
andalso ?is_year(Year) ->
{{Year, Month, Day}, {hour(Hour, PAM), Min, 0}};
parse([Day,X,Month,X,Year,Hour,$:,Min | PAM], _Date, _Opts)
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)
when ?is_meridian(PAM) andalso
(?is_us_sep(X) orelse ?is_world_sep(X))
andalso Year > 31 ->
andalso ?is_year(Year) ->
{{Year, Month, Day}, {hour(Hour, PAM), Min, Sec}};
parse([Month,X,Day,X,Year,Hour,$:,Min,$:,Sec | PAM], _Now, _Opts)
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) ->
{{Year, Month, Day}, {hour(Hour, PAM), Min, Sec}};
parse([Day,Month,Year,Hour | PAM], _Now, _Opts)
when ?is_meridian(PAM) ->
{{Year, Month, Day}, {hour(Hour, PAM), 0, 0}};
@ -181,9 +305,35 @@ parse(_Tokens, _Now, _Opts) ->
tokenise([], 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)
when ?is_num(N1), ?is_num(N2), ?is_num(N3), ?is_num(N4) ->
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)
when ?is_num(N1), ?is_num(N2) ->
tokenise(Rest, [ ltoi([N1, N2]) | Acc]);
@ -191,32 +341,37 @@ tokenise([N1 | Rest], Acc)
when ?is_num(N1) ->
tokenise(Rest, [ ltoi([N1]) | Acc]);
tokenise("JANUARY"++Rest, Acc) -> tokenise(Rest, [1 | Acc]);
tokenise("JAN"++Rest, Acc) -> tokenise(Rest, [1 | Acc]);
tokenise("FEBRUARY"++Rest, Acc) -> tokenise(Rest, [2 | Acc]);
tokenise("FEB"++Rest, Acc) -> tokenise(Rest, [2 | Acc]);
tokenise("MARCH"++Rest, Acc) -> tokenise(Rest, [3 | Acc]);
tokenise("MAR"++Rest, Acc) -> tokenise(Rest, [3 | Acc]);
tokenise("APRIL"++Rest, Acc) -> tokenise(Rest, [4 | Acc]);
tokenise("APR"++Rest, Acc) -> tokenise(Rest, [4 | Acc]);
tokenise("MAY"++Rest, Acc) -> tokenise(Rest, [5 | Acc]);
tokenise("JUNE"++Rest, Acc) -> tokenise(Rest, [6 | Acc]);
tokenise("JUN"++Rest, Acc) -> tokenise(Rest, [6 | Acc]);
tokenise("JULY"++Rest, Acc) -> tokenise(Rest, [7 | Acc]);
tokenise("JUL"++Rest, Acc) -> tokenise(Rest, [7 | Acc]);
tokenise("AUGUST"++Rest, Acc) -> tokenise(Rest, [8 | Acc]);
tokenise("AUG"++Rest, Acc) -> tokenise(Rest, [8 | Acc]);
tokenise("SEPTEMBER"++Rest, Acc) -> tokenise(Rest, [9 | Acc]);
tokenise("SEPT"++Rest, Acc) -> tokenise(Rest, [9 | Acc]);
tokenise("SEP"++Rest, Acc) -> tokenise(Rest, [9 | Acc]);
tokenise("OCTOBER"++Rest, Acc) -> tokenise(Rest, [10 | Acc]);
tokenise("OCT"++Rest, Acc) -> tokenise(Rest, [10 | Acc]);
tokenise("NOVEMBER"++Rest, Acc) -> tokenise(Rest, [11 | Acc]);
tokenise("NOVEM"++Rest, Acc) -> tokenise(Rest, [11 | Acc]);
tokenise("NOV"++Rest, Acc) -> tokenise(Rest, [11 | Acc]);
tokenise("DECEMBER"++Rest, Acc) -> tokenise(Rest, [12 | Acc]);
tokenise("DECEM"++Rest, Acc) -> tokenise(Rest, [12 | Acc]);
tokenise("DEC"++Rest, Acc) -> tokenise(Rest, [12 | Acc]);
%% Worded Months get tagged with ?MONTH_TAG to let the parser know that these
%% are unambiguously declared to be months. This was there's no confusion
%% between, for example: "Aug 12" and "12 Aug"
%% These hint tags are filtered in filter_hints/1 above.
tokenise("JANUARY"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,1} | Acc]);
tokenise("JAN"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,1} | Acc]);
tokenise("FEBRUARY"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,2} | Acc]);
tokenise("FEB"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,2} | Acc]);
tokenise("MARCH"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,3} | Acc]);
tokenise("MAR"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,3} | Acc]);
tokenise("APRIL"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,4} | Acc]);
tokenise("APR"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,4} | Acc]);
tokenise("MAY"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,5} | Acc]);
tokenise("JUNE"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,6} | Acc]);
tokenise("JUN"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,6} | Acc]);
tokenise("JULY"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,7} | Acc]);
tokenise("JUL"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,7} | Acc]);
tokenise("AUGUST"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,8} | Acc]);
tokenise("AUG"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,8} | Acc]);
tokenise("SEPTEMBER"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,9} | Acc]);
tokenise("SEPT"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,9} | Acc]);
tokenise("SEP"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,9} | Acc]);
tokenise("OCTOBER"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,10} | Acc]);
tokenise("OCT"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,10} | 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]);
@ -264,6 +419,10 @@ tokenise("TH"++Rest, Acc) -> tokenise(Rest, Acc);
tokenise("ND"++Rest, Acc) -> tokenise(Rest, Acc);
tokenise("ST"++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(Rest, [{bad_token, Else} | Acc]).
@ -329,13 +488,13 @@ format([$z|T], {Date,_}=Dt, Acc) ->
format(T, Dt, [itol(days_in_year(Date))|Acc]);
%% 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([$a|T], Dt, Acc) ->
format([$a|T], Dt={_,{_,_,_}}, 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([$A|T], Dt, 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]);
@ -355,6 +514,38 @@ 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], {_,{_,_,_}}=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
format([$c|T], {{Y,M,D},{H,Min,S}}=Dt, Acc) ->
@ -403,6 +594,10 @@ to_w(X) -> X.
suffix(1) -> "st";
suffix(2) -> "nd";
suffix(3) -> "rd";
suffix(21) -> "st";
suffix(22) -> "nd";
suffix(23) -> "rd";
suffix(31) -> "st";
suffix(_) -> "th".
-spec sdayd(date()) -> string().
@ -500,36 +695,54 @@ iso_week_one(Y) ->
itol(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
pad2(X) when is_integer(X) ->
io_lib:format("~2.10.0B",[X]);
pad2(X) when is_float(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) ->
list_to_integer(X).
%%
%% TEST FUNCTIONS
%%
%% c(dh_date,[{d,'TEST'}]).
%-define(NOTEST, 1).
%%%===================================================================
%%% Tests
%%%===================================================================
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-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").
basic_format_test_() ->
[
?_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("j, n, Y",?DATE), "10, 3, 2001"),
?_assertEqual(format("Ymd",?DATE), "20010310"),
?_assertEqual(format("H:i:s",?DATE), "17:16:17"),
?_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", {{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),
"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)),
?_assertEqual({{2008,8,22}, {18,0,0}},
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}},
parse("11:15", ?DATE)),
?_assertEqual({{2001,3,10}, {1,15,0}},
@ -646,7 +917,12 @@ parse_with_days_test_() ->
?_assertEqual({{2008,8,22}, {6,0,0}},
parse("Monday 22 Aug 2008 6a", ?DATE)),
?_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_() ->
@ -674,3 +950,133 @@ iso_test_() ->
?_assertEqual("2009 W53",format(?ISO,{{2009,12,31},{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.

View file

@ -1,3 +1,4 @@
%%% vi:ts=4 sw=4 et
%%%-------------------------------------------------------------------
%%% @author Eric Merritt <ericbmerritt@gmail.com>
%%% @copyright 2011 Erlware, LLC.
@ -5,9 +6,9 @@
%%% This provides an implementation of the ec_dictionary type using
%%% erlang dicts as a base. The function documentation for
%%% ec_dictionary applies here as well.
%%% see ec_dictionary
%%% see dict
%%% @end
%%% @see ec_dictionary
%%% @see dict
%%%-------------------------------------------------------------------
-module(ec_dict).
@ -31,7 +32,9 @@
%%%===================================================================
%%% Types
%%%===================================================================
-opaque dictionary(_K, _V) :: dict().
%% This should be opaque, but that kills dialyzer so for now we export it
%% however you should not rely on the internal representation here
-type dictionary(_K, _V) :: dict:dict().
%%%===================================================================
%%% API
@ -88,7 +91,7 @@ has_value(Value, Data) ->
false,
Data).
-spec size(Object::dictionary(_K, _V)) -> integer().
-spec size(Object::dictionary(_K, _V)) -> non_neg_integer().
size(Data) ->
dict:size(Data).

View file

@ -1,3 +1,4 @@
%%% vi:ts=4 sw=4 et
%%%-------------------------------------------------------------------
%%% @author Eric Merritt <ericbmerritt@gmail.com>
%%% @copyright 2011 Erlware, LLC.
@ -10,9 +11,6 @@
%%%-------------------------------------------------------------------
-module(ec_dictionary).
%%% Behaviour Callbacks
-export([behaviour_info/1]).
%% API
-export([new/1,
has_key/2,
@ -38,30 +36,27 @@
{callback,
data}).
-opaque dictionary(_K, _V) :: #dict_t{}.
%% This should be opaque, but that kills dialyzer so for now we export it
%% however you should not rely on the internal representation here
-type dictionary(_K, _V) :: #dict_t{}.
-type key(T) :: T.
-type value(T) :: T.
-callback new() -> any().
-callback has_key(key(any()), any()) -> boolean().
-callback get(key(any()), any()) -> any().
-callback add(key(any()), value(any()), T) -> T.
-callback remove(key(any()), T) -> T.
-callback has_value(value(any()), any()) -> boolean().
-callback size(any()) -> non_neg_integer().
-callback to_list(any()) -> [{key(any()), value(any())}].
-callback from_list([{key(any()), value(any())}]) -> any().
-callback keys(any()) -> [key(any())].
%%%===================================================================
%%% API
%%%===================================================================
%% @doc export the behaviour callbacks for this type
%% @private
behaviour_info(callbacks) ->
[{new, 0},
{has_key, 2},
{get, 2},
{add, 3},
{remove, 2},
{has_value, 2},
{size, 1},
{to_list, 1},
{from_list, 1},
{keys, 1}];
behaviour_info(_) ->
undefined.
%% @doc create a new dictionary object from the specified module. The
%% module should implement the dictionary behaviour.
%%
@ -83,7 +78,7 @@ has_key(Key, #dict_t{callback = Mod, data = Data}) ->
%%
%% @param Dict The dictionary object to return the value from
%% @param Key The key requested
%% @throws not_found when the key does not exist
%% when the key does not exist @throws not_found
-spec get(key(K), dictionary(K, V)) -> value(V).
get(Key, #dict_t{callback = Mod, data = Data}) ->
Mod:get(Key, Data).

View file

@ -1,3 +1,4 @@
%%% vi:ts=4 sw=4 et
%%%-------------------------------------------------------------------
%%% @copyright (C) 2011, Erlware LLC
%%% @doc
@ -7,95 +8,171 @@
-module(ec_file).
-export([
exists/1,
copy/2,
copy/3,
mkdtemp/0,
copy_file_info/3,
insecure_mkdtemp/0,
mkdir_path/1,
mkdir_p/1,
find/2,
is_symlink/1,
is_dir/1,
type/1,
real_dir_path/1,
remove/1,
remove/2,
md5sum/1,
sha1sum/1,
read/1,
write/2,
write_term/2,
consult/1
write_term/2
]).
-export_type([
path/0,
option/0
]).
-include_lib("kernel/include/file.hrl").
%% User friendly exception message (remove line and module info once we
%% get them in stack traces)
-define(UEX(Exception, UMSG, UVARS),
{uex, {?MODULE,
?LINE,
Exception,
lists:flatten(io_lib:fwrite(UMSG, UVARS))}}).
-define(CHECK_PERMS_MSG,
"Try checking that you have the correct permissions and try again~n").
%%============================================================================
%% Types
%%============================================================================
-type path() :: string().
-type option() :: [atom()].
-type file_info() :: mode | time | owner | group.
-type option() :: recursive | {file_info, [file_info()]}.
%%%===================================================================
%%% 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.
-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, [recursive] = Options) ->
case filelib:is_dir(From) of
false ->
copy(From, To);
copy_(From, To, []);
copy(From, To, Options) ->
case proplists:get_value(recursive, Options, false) of
true ->
make_dir_if_dir(To),
copy_subfiles(From, To, Options)
case is_dir(From) of
false ->
copy_(From, To, Options);
true ->
make_dir_if_dir(To),
copy_subfiles(From, To, Options)
end;
false ->
copy_(From, To, Options)
end.
%% @doc copy a file including timestamps,ownership and mode etc.
-spec copy(From::string(), To::string()) -> ok.
-spec copy(From::file:filename(), To::file:filename()) -> ok | {error, Reason::term()}.
copy(From, To) ->
try
ec_file_copy(From, To)
catch
_C:E -> throw(?UEX({copy_failed, E}, ?CHECK_PERMS_MSG, []))
copy_(From, To, [{file_info, [mode, time, owner, group]}]).
copy_(From, To, Options) ->
Linked
= case file:read_link(From) of
{ok, Linked0} -> Linked0;
{error, _} -> undefined
end,
case Linked =/= undefined orelse file:copy(From, To) of
true ->
file:make_symlink(Linked, To);
{ok, _} ->
copy_file_info(To, From, proplists:get_value(file_info, Options, []));
{error, Error} ->
{error, {copy_failed, Error}}
end.
%% @doc return an md5 checksum string or a binary. Same as unix utility of
%% same name.
copy_file_info(To, From, FileInfoToKeep) ->
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().
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.
%% <pre>
%% Example: remove("./tmp_dir", [recursive]).
%% </pre>
-spec remove(path(), Options::[option()]) -> ok | {error, Reason::term()}.
-spec remove(file:name(), Options::[option()]) -> ok | {error, Reason::term()}.
remove(Path, Options) ->
try
ok = ec_file_remove(Path, Options)
catch
_C:E -> throw(?UEX({remove_failed, E}, ?CHECK_PERMS_MSG, []))
case lists:member(recursive, Options) of
false -> file:delete(Path);
true -> remove_recursive(Path, Options)
end.
%% @doc delete a file.
-spec remove(path()) -> ok | {error, Reason::term()}.
-spec remove(file:name()) -> ok | {error, Reason::term()}.
remove(Path) ->
remove(Path, []).
%% @doc indicates witha boolean if the path supplied refers to symlink.
-spec is_symlink(path()) -> boolean().
%% @doc indicates with a boolean if the path supplied refers to symlink.
-spec is_symlink(file:name()) -> boolean().
is_symlink(Path) ->
case file:read_link_info(Path) of
{ok, #file_info{type = symlink}} ->
@ -104,93 +181,103 @@ is_symlink(Path) ->
false
end.
%% @doc make a unique temorory directory. Similar function to BSD stdlib
%% function of the same name.
-spec mkdtemp() -> TmpDirPath::path().
mkdtemp() ->
UniqueNumber = integer_to_list(element(3, now())),
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, []))
is_dir(Path) ->
case file:read_file_info(Path) of
{ok, #file_info{type = directory}} ->
true;
_ ->
false
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.
-spec mkdir_path(path()) -> ok.
mkdir_path(Path) ->
-spec mkdir_p(file:name()) -> ok | {error, Reason::term()}.
mkdir_p(Path) ->
%% We are exploiting a feature of ensuredir that that creates all
%% directories up to the last element in the filename, then ignores
%% that last element. This way we ensure that the dir is created
%% and not have any worries about path names
DirName = filename:join([filename:absname(Path), "tmp"]),
try
ok = filelib:ensure_dir(DirName)
catch
_C:E -> throw(?UEX({mkdir_path_failed, E}, ?CHECK_PERMS_MSG, []))
end.
filelib:ensure_dir(DirName).
%% @doc consult an erlang term file from the file system.
%% Provide user readible exeption on failure.
-spec consult(FilePath::path()) -> term().
consult(FilePath) ->
case file:consult(FilePath) of
{ok, [Term]} ->
Term;
{error, Error} ->
Msg = "The file at ~p~n" ++
"is either not a valid Erlang term, does not to exist~n" ++
"or you lack the permissions to read it. Please check~n" ++
"to see if the file exists and that it has the correct~n" ++
"permissions~n",
throw(?UEX({failed_to_consult_file, {FilePath, Error}},
Msg, [FilePath]))
end.
%% @doc Makes a directory including parent dirs if they are missing.
-spec mkdir_path(file:name()) -> ok | {error, Reason::term()}.
mkdir_path(Path) ->
mkdir_p(Path).
%% @doc 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) ->
try
{ok, FileBin} = file:read_file(FilePath),
FileBin
catch
_C:E -> throw(?UEX({read_failed, {FilePath, E}},
"Read failed for the file ~p with ~p~n" ++
?CHECK_PERMS_MSG,
[FilePath, E]))
end.
%% Now that we are moving away from exceptions again this becomes
%% a bit redundant but we want to be backwards compatible as much
%% as possible in the api.
file:read_file(FilePath).
%% @doc write a file to the file system. Provide UEX exeption on failure.
-spec write(FileName::string(), Contents::string()) -> ok.
%% @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) ->
case file:write_file(FileName, Contents) of
ok ->
ok;
{error, Reason} ->
Msg = "Writing the file ~s to disk failed with reason ~p.~n" ++
?CHECK_PERMS_MSG,
throw(?UEX({write_file_failure, {FileName, Reason}},
Msg,
[FileName, Reason]))
end.
%% Now that we are moving away from exceptions again this becomes
%% a bit redundant but we want to be backwards compatible as much
%% as possible in the api.
file:write_file(FileName, Contents).
%% @doc write a term out to a file so that it can be consulted later.
-spec write_term(string(), term()) -> ok.
-spec write_term(file:filename(), term()) -> ok | {error, Reason::term()}.
write_term(FileName, Term) ->
write(FileName, lists:flatten(io_lib:fwrite("~p. ", [Term]))).
%% @doc Finds files and directories that match the regexp supplied in
%% the TargetPattern regexp.
-spec find(FromDir::path(), TargetPattern::string()) -> [path()].
-spec find(FromDir::file:name(), TargetPattern::string()) -> [file:name()].
find([], _) ->
[];
find(FromDir, TargetPattern) ->
case filelib:is_dir(FromDir) of
case is_dir(FromDir) of
false ->
case re:run(FromDir, TargetPattern) of
{match, _} -> [FromDir];
@ -207,7 +294,7 @@ find(FromDir, TargetPattern) ->
%%%===================================================================
%%% Internal Functions
%%%===================================================================
-spec find_in_subdirs(path(), string()) -> [path()].
-spec find_in_subdirs(file:name(), string()) -> [file:name()].
find_in_subdirs(FromDir, TargetPattern) ->
lists:foldl(fun(CheckFromDir, Acc)
when CheckFromDir == FromDir ->
@ -219,57 +306,52 @@ find_in_subdirs(FromDir, TargetPattern) ->
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) ->
case filelib:is_dir(Path) of
case is_dir(Path) of
false ->
file:delete(Path);
true ->
lists:foreach(fun(ChildPath) ->
remove_recursive(ChildPath, Options)
end, filelib:wildcard(filename:join(Path, "*"))),
ok = file:del_dir(Path)
end, sub_files(Path)),
file:del_dir(Path)
end.
-spec tmp() -> path().
-spec tmp() -> file:name().
tmp() ->
case erlang:system_info(system_architecture) of
"win32" ->
"./tmp";
case os:getenv("TEMP") of
false -> "./tmp";
Val -> Val
end;
_SysArch ->
"/tmp"
case os:getenv("TMPDIR") of
false -> "/tmp";
Val -> Val
end
end.
%% Copy the subfiles of the From directory to the to directory.
-spec copy_subfiles(path(), path(), [option()]) -> ok.
-spec copy_subfiles(file:name(), file:name(), [option()]) -> {error, Reason::term()} | ok.
copy_subfiles(From, To, Options) ->
Fun =
fun(ChildFrom) ->
ChildTo = filename:join([To, filename:basename(ChildFrom)]),
copy(ChildFrom, ChildTo, Options)
end,
lists:foreach(Fun, filelib:wildcard(filename:join(From, "*"))).
lists:foreach(Fun, sub_files(From)).
-spec ec_file_copy(path(), path()) -> ok.
ec_file_copy(From, To) ->
{ok, _} = file:copy(From, To),
{ok, FileInfo} = file:read_file_info(From),
ok = file:write_file_info(To, FileInfo).
-spec make_dir_if_dir(path()) -> ok.
-spec make_dir_if_dir(file:name()) -> ok | {error, Reason::term()}.
make_dir_if_dir(File) ->
case filelib:is_dir(File) of
case is_dir(File) of
true -> ok;
false -> ok = mkdir_path(File)
false -> mkdir_path(File)
end.
%% @doc convert a list of integers into hex.
@ -289,72 +371,7 @@ hex0(14) -> $e;
hex0(15) -> $f;
hex0(I) -> $0 + I.
%%%===================================================================
%%% Test Functions
%%%===================================================================
-ifndef(NOTEST).
-include_lib("eunit/include/eunit.hrl").
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.
sub_files(From) ->
{ok, SubFiles} = file:list_dir(From),
[filename:join(From, SubFile) || SubFile <- SubFiles].

View file

@ -1,12 +1,13 @@
%%% vi:ts=4 sw=4 et
%%%-------------------------------------------------------------------
%%% @author Eric Merritt <ericbmerritt@gmail.com>
%%% @copyright 2011 Erlware, LLC.
%%% @doc
%%% This provides an implementation of the type ec_dictionary using
%%% gb_trees as a backin
%%% see ec_dictionary
%%% see gb_trees
%%% @end
%%% @see ec_dictionary
%%% @see gb_trees
%%%-------------------------------------------------------------------
-module(ec_gb_trees).
@ -14,27 +15,16 @@
%% API
-export([new/0,
has_key/2,
get/2,
get/3,
add/3,
remove/2,
has_value/2,
size/1,
to_list/1,
from_list/1,
keys/1]).
-export_type([dictionary/2]).
%%%===================================================================
%%% Types
%%%===================================================================
-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)}.
has_key/2,
get/2,
get/3,
add/3,
remove/2,
has_value/2,
size/1,
to_list/1,
from_list/1,
keys/1]).
%%%===================================================================
%%% API
@ -46,7 +36,7 @@
%% same implementation is created and returned.
%%
%% @param ModuleName|Object The module name or existing dictionary object.
-spec new() -> dictionary(_K, _V).
-spec new() -> gb_trees:tree(_K, _V).
new() ->
gb_trees:empty().
@ -54,13 +44,13 @@ new() ->
%%
%% @param Object The dictory object to check
%% @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) ->
case gb_trees:lookup(Key, Data) of
{value, _Val} ->
true;
none ->
false
{value, _Val} ->
true;
none ->
false
end.
%% @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 Key The key requested
%% @throws not_found when the key does not exist
-spec get(ec_dictionary:key(K), Object::dictionary(K, V)) ->
%% when the key does not exist @throws not_found
-spec get(ec_dictionary:key(K), Object::gb_trees:tree(K, V)) ->
ec_dictionary:value(V).
get(Key, Data) ->
case gb_trees:lookup(Key, Data) of
{value, Value} ->
Value;
none ->
throw(not_found)
{value, Value} ->
Value;
none ->
throw(not_found)
end.
-spec get(ec_dictionary:key(K),
ec_dictionary:value(V),
Object::dictionary(K, V)) ->
ec_dictionary:value(V),
Object::gb_trees:tree(K, V)) ->
ec_dictionary:value(V).
get(Key, Default, Data) ->
case gb_trees:lookup(Key, Data) of
{value, Value} ->
Value;
none ->
Default
{value, Value} ->
Value;
none ->
Default
end.
%% @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 Value the value to add
-spec add(ec_dictionary:key(K), ec_dictionary:value(V),
Object::dictionary(K, V)) ->
dictionary(K, V).
Object::gb_trees:tree(K, V)) ->
gb_trees:tree(K, V).
add(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 Key the key of the key/value pair to remove
-spec remove(ec_dictionary:key(K), Object::dictionary(K, V)) ->
dictionary(K, V).
-spec remove(ec_dictionary:key(K), Object::gb_trees:tree(K, V)) ->
gb_trees:tree(K, V).
remove(Key, Data) ->
gb_trees:delete_any(Key, Data).
@ -117,107 +107,31 @@ remove(Key, Data) ->
%%
%% @param Object the dictionary object to check
%% @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) ->
lists:member(Value, gb_trees:values(Data)).
%% @doc return the current number of key value pairs in the dictionary
%%
%% @param Object the object return the size for.
-spec size(Object::dictionary(_K, _V)) -> integer().
-spec size(Object::gb_trees:tree(_K, _V)) -> non_neg_integer().
size(Data) ->
gb_trees:size(Data).
-spec to_list(dictionary(K, V)) -> [{ec_dictionary:key(K),
ec_dictionary:value(V)}].
-spec to_list(gb_trees:tree(K, V)) -> [{ec_dictionary:key(K),
ec_dictionary:value(V)}].
to_list(Data) ->
gb_trees:to_list(Data).
-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) ->
lists:foldl(fun({Key, Value}, Dict) ->
gb_trees:enter(Key, Value, Dict)
end,
gb_trees:empty(),
List).
gb_trees:enter(Key, Value, Dict)
end,
gb_trees:empty(),
List).
-spec keys(dictionary(K,_V)) -> [ec_dictionary:key(K)].
-spec keys(gb_trees:tree(K,_V)) -> [ec_dictionary:key(K)].
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
View 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.

View file

@ -1,3 +1,4 @@
%%% vi:ts=4 sw=4 et
%%%-------------------------------------------------------------------
%%% @copyright (C) 2011, Erlware LLC
%%% @doc
@ -23,7 +24,7 @@
%% the third value is the element passed to the function. The purpose
%% of this is to allow a list to be searched where some internal state
%% is important while the input element is not.
-spec search(fun(), list()) -> {ok, Result::term(), Element::term()}.
-spec search(fun(), list()) -> {ok, Result::term(), Element::term()} | not_found.
search(Fun, [H|T]) ->
case Fun(H) of
{ok, Value} ->
@ -51,7 +52,7 @@ find(_Fun, []) ->
error.
%% @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
%% thrown.
-spec fetch(fun(), list()) -> term().
@ -62,184 +63,3 @@ fetch(Fun, List) when is_list(List), is_function(Fun) ->
error ->
throw(not_found)
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.

View file

@ -1,3 +1,4 @@
%%% vi:ts=4 sw=4 et
%%%-------------------------------------------------------------------
%%% @author Eric Merritt <ericbmerritt@gmail.com>
%%% @copyright 2011 Erlware, LLC.
@ -5,9 +6,9 @@
%%% This provides an implementation of the ec_dictionary type using
%%% erlang orddicts as a base. The function documentation for
%%% ec_dictionary applies here as well.
%%% see ec_dictionary
%%% see orddict
%%% @end
%%% @see ec_dictionary
%%% @see orddict
%%%-------------------------------------------------------------------
-module(ec_orddict).
@ -31,7 +32,9 @@
%%%===================================================================
%%% Types
%%%===================================================================
-opaque dictionary(K, V) :: [{K, V}].
%% This should be opaque, but that kills dialyzer so for now we export it
%% however you should not rely on the internal representation here
-type dictionary(K, V) :: [{K, V}].
%%%===================================================================
%%% API
@ -46,59 +49,59 @@ has_key(Key, Data) ->
orddict:is_key(Key, Data).
-spec get(ec_dictionary:key(K), Object::dictionary(K, V)) ->
ec_dictionary:value(V).
ec_dictionary:value(V).
get(Key, Data) ->
case orddict:find(Key, Data) of
{ok, Value} ->
Value;
error ->
error ->
throw(not_found)
end.
-spec get(ec_dictionary:key(K),
Default::ec_dictionary:value(V),
Object::dictionary(K, V)) ->
ec_dictionary:value(V).
ec_dictionary:value(V).
get(Key, Default, Data) ->
case orddict:find(Key, Data) of
{ok, Value} ->
Value;
error ->
error ->
Default
end.
-spec add(ec_dictionary:key(K), ec_dictionary:value(V),
Object::dictionary(K, V)) ->
dictionary(K, V).
dictionary(K, V).
add(Key, Value, Data) ->
orddict:store(Key, Value, Data).
-spec remove(ec_dictionary:key(K), Object::dictionary(K, V)) ->
dictionary(K, V).
dictionary(K, V).
remove(Key, Data) ->
orddict:erase(Key, Data).
-spec has_value(ec_dictionary:value(V), Object::dictionary(_K, V)) -> boolean().
has_value(Value, Data) ->
orddict:fold(fun(_, NValue, _) when NValue == Value ->
true;
(_, _, Acc) ->
Acc
end,
false,
Data).
true;
(_, _, Acc) ->
Acc
end,
false,
Data).
-spec size(Object::dictionary(_K, _V)) -> integer().
-spec size(Object::dictionary(_K, _V)) -> non_neg_integer().
size(Data) ->
orddict:size(Data).
-spec to_list(dictionary(K, V)) ->
[{ec_dictionary:key(K), ec_dictionary:value(V)}].
[{ec_dictionary:key(K), ec_dictionary:value(V)}].
to_list(Data) ->
orddict:to_list(Data).
-spec from_list([{ec_dictionary:key(K), ec_dictionary:value(V)}]) ->
dictionary(K, V).
dictionary(K, V).
from_list(List) when is_list(List) ->
orddict:from_list(List).

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,4 @@
%%% vi:ts=4 sw=4 et
%%% Copyright (c) 2008 Robert Virding. All rights reserved.
%%%
%%% Redistribution and use in source and binary forms, with or without
@ -31,7 +32,7 @@
%%% representation of a dictionary, where a red-black tree is used to
%%% 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
%%% that while dict considers two keys as different if they do not
%%% 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
%%% doing it. Measured.
%%%
%%% see ec_dictionary
%%% @end
%%% @see ec_dictionary
%%%-------------------------------------------------------------------
-module(ec_rbdict).
@ -68,12 +69,13 @@
%%%===================================================================
%%% Types
%%%===================================================================
-opaque dictionary(K, V) :: empty | {color(),
dictionary(K, V),
ec_dictionary:key(K),
ec_dictionary:value(V),
dictionary(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) :: empty | {color(),
dictionary(K, V),
ec_dictionary:key(K),
ec_dictionary:value(V),
dictionary(K, V)}.
-type color() :: r | b.
@ -116,7 +118,7 @@ get(K, Default, {_, _, K1, _, Right}) when K > K1 ->
get(_, _, {_, _, _, Val, _}) ->
Val.
-spec add(ec_dicitonary:key(K), ec_dictionary:value(V),
-spec add(ec_dictionary:key(K), ec_dictionary:value(V),
dictionary(K, V)) -> dictionary(K, V).
add(Key, Value, Dict) ->
{_, L, K1, V1, R} = add1(Key, Value, Dict),
@ -133,17 +135,17 @@ has_value(Value, Dict) ->
end,
false, Dict).
-spec size(dictionary(_K, _V)) -> integer().
-spec size(dictionary(_K, _V)) -> non_neg_integer().
size(T) ->
size1(T).
-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, []).
-spec from_list([{ec_dictionary:key(K), ec_dictionary:value(V)}]) ->
dictionary(K, V).
dictionary(K, V).
from_list(L) ->
lists:foldl(fun ({K, V}, D) ->
add(K, V, D)
@ -158,7 +160,7 @@ keys(Dict) ->
%%% Enternal functions
%%%===================================================================
-spec keys(dictionary(K, _V), [ec_dictionary:key(K)]) ->
[ec_dictionary:key(K)].
[ec_dictionary:key(K)].
keys(empty, Tail) ->
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)) ->
{dictionary(K, V), boolean()}.
{dictionary(K, V), boolean()}.
erase_aux(_, empty) ->
{empty, false};
erase_aux(K, {b, A, Xk, Xv, B}) ->
@ -227,7 +229,7 @@ erase_aux(K, {r, A, Xk, Xv, B}) ->
end.
-spec erase_min(dictionary(K, V)) ->
{dictionary(K, V), {ec_dictionary:key(K), ec_dictionary:value(V)}, boolean}.
{dictionary(K, V), {ec_dictionary:key(K), ec_dictionary:value(V)}, boolean()}.
erase_min({b, empty, Xk, Xv, empty}) ->
{empty, {Xk, Xv}, true};
erase_min({b, empty, Xk, Xv, {r, A, Yk, Yv, B}}) ->
@ -239,15 +241,15 @@ erase_min({r, empty, Xk, Xv, A}) ->
erase_min({b, A, Xk, Xv, B}) ->
{A1, Min, Dec} = erase_min(A),
if Dec ->
{T, Dec1} = unbalright(b, A1, Xk, Xv, B),
{T, Min, Dec1};
{T, Dec1} = unbalright(b, A1, Xk, Xv, B),
{T, Min, Dec1};
true -> {{b, A1, Xk, Xv, B}, Min, false}
end;
erase_min({r, A, Xk, Xv, B}) ->
{A1, Min, Dec} = erase_min(A),
if Dec ->
{T, Dec1} = unbalright(r, A1, Xk, Xv, B),
{T, Min, Dec1};
{T, Dec1} = unbalright(r, A1, Xk, Xv, B),
{T, Min, Dec1};
true -> {{r, A1, Xk, Xv, B}, Min, false}
end.
@ -274,7 +276,8 @@ unbalright(b, A, Xk, Xv,
D},
false}.
-spec fold(fun(), dictionary(K, V), dictionary(K, V)) -> dictionary(K, V).
-spec fold(fun((ec_dictionary:key(K), ec_dictionary:value(V), any()) -> any()),
any(), dictionary(K, V)) -> any().
fold(_, Acc, empty) -> Acc;
fold(F, Acc, {_, A, Xk, Xv, B}) ->
fold(F, F(Xk, Xv, fold(F, Acc, B)), A).
@ -293,11 +296,11 @@ to_list(empty, List) -> List;
to_list({_, A, Xk, Xv, 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),
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).
lbalance(b, {r, {r, A, Xk, Xv, B}, Yk, Yv, C}, Zk, Zv,
D) ->
{r, {b, A, Xk, Xv, B}, Yk, Yv, {b, C, Zk, Zv, D}};
@ -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}.
-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).
rbalance(b, A, Xk, Xv,
{r, {r, B, Yk, Yv, C}, Zk, Zv, D}) ->
{r, {b, A, Xk, Xv, B}, Yk, Yv, {b, C, Zk, Zv, D}};

View file

@ -1,3 +1,4 @@
%%% vi:ts=4 sw=4 et
%%%-------------------------------------------------------------------
%%% @copyright (C) 2011, Erlware LLC
%%% @doc
@ -7,113 +8,304 @@
%%%-------------------------------------------------------------------
-module(ec_semver).
-exports([
compare/2
]).
-export([parse/1,
format/1,
eql/2,
gt/2,
gte/2,
lt/2,
lte/2,
pes/2,
between/3]).
-export_type([
semvar/0
]).
%% For internal use by the ec_semver_parser peg
-export([internal_parse_version/1]).
-export_type([semver/0,
version_string/0,
any_version/0]).
%%%===================================================================
%%% Public Types
%%%===================================================================
-type semvar() :: string().
-type parsed_semvar() :: {MajorVsn::string(),
MinorVsn::string(),
PatchVsn::string(),
PathString::string()}.
-type version_element() :: non_neg_integer() | binary().
-type major_minor_patch_minpatch() ::
version_element()
| {version_element(), version_element()}
| {version_element(), version_element(), version_element()}
| {version_element(), version_element(),
version_element(), version_element()}.
-type alpha_part() :: integer() | binary() | string().
-type alpha_info() :: {PreRelease::[alpha_part()],
BuildVersion::[alpha_part()]}.
-type semver() :: {major_minor_patch_minpatch(), alpha_info()}.
-type version_string() :: string() | binary().
-type any_version() :: version_string() | semver().
%%%===================================================================
%%% API
%%%===================================================================
%% @doc Is semver version string A bigger than version string B?
%% <pre>
%% Example: compare("3.2.5alpha", "3.10.6") returns: false
%% </pre>
-spec compare(VsnA::string(), VsnB::string()) -> boolean().
compare(VsnA, VsnB) ->
compare_toks(tokens(VsnA),tokens(VsnB)).
%% @doc parse a string or binary into a valid semver representation
-spec parse(any_version()) -> semver().
parse(Version) when erlang:is_list(Version) ->
case ec_semver_parser:parse(Version) of
{fail, _} ->
{erlang:iolist_to_binary(Version), {[],[]}};
Good ->
Good
end;
parse(Version) when erlang:is_binary(Version) ->
case ec_semver_parser:parse(Version) of
{fail, _} ->
{Version, {[],[]}};
Good ->
Good
end;
parse(Version) ->
Version.
-spec format(semver()) -> iolist().
format({Maj, {AlphaPart, BuildPart}})
when erlang:is_integer(Maj);
erlang:is_binary(Maj) ->
[format_version_part(Maj),
format_vsn_rest(<<"-">>, AlphaPart),
format_vsn_rest(<<"+">>, BuildPart)];
format({{Maj, Min}, {AlphaPart, BuildPart}}) ->
[format_version_part(Maj), ".",
format_version_part(Min),
format_vsn_rest(<<"-">>, AlphaPart),
format_vsn_rest(<<"+">>, BuildPart)];
format({{Maj, Min, Patch}, {AlphaPart, BuildPart}}) ->
[format_version_part(Maj), ".",
format_version_part(Min), ".",
format_version_part(Patch),
format_vsn_rest(<<"-">>, AlphaPart),
format_vsn_rest(<<"+">>, BuildPart)];
format({{Maj, Min, Patch, MinPatch}, {AlphaPart, BuildPart}}) ->
[format_version_part(Maj), ".",
format_version_part(Min), ".",
format_version_part(Patch), ".",
format_version_part(MinPatch),
format_vsn_rest(<<"-">>, AlphaPart),
format_vsn_rest(<<"+">>, BuildPart)].
-spec format_version_part(integer() | binary()) -> iolist().
format_version_part(Vsn)
when erlang:is_integer(Vsn) ->
erlang:integer_to_list(Vsn);
format_version_part(Vsn)
when erlang:is_binary(Vsn) ->
Vsn.
%% @doc test for quality between semver versions
-spec eql(any_version(), any_version()) -> boolean().
eql(VsnA, VsnB) ->
NVsnA = normalize(parse(VsnA)),
NVsnB = normalize(parse(VsnB)),
NVsnA =:= NVsnB.
%% @doc Test that VsnA is greater than VsnB
-spec gt(any_version(), any_version()) -> boolean().
gt(VsnA, VsnB) ->
{MMPA, {AlphaA, PatchA}} = normalize(parse(VsnA)),
{MMPB, {AlphaB, PatchB}} = normalize(parse(VsnB)),
((MMPA > MMPB)
orelse
((MMPA =:= MMPB)
andalso
((AlphaA =:= [] andalso AlphaB =/= [])
orelse
((not (AlphaB =:= [] andalso AlphaA =/= []))
andalso
(AlphaA > AlphaB))))
orelse
((MMPA =:= MMPB)
andalso
(AlphaA =:= AlphaB)
andalso
((PatchB =:= [] andalso PatchA =/= [])
orelse
PatchA > PatchB))).
%% @doc Test that VsnA is greater than or equal to VsnB
-spec gte(any_version(), any_version()) -> boolean().
gte(VsnA, VsnB) ->
NVsnA = normalize(parse(VsnA)),
NVsnB = normalize(parse(VsnB)),
gt(NVsnA, NVsnB) orelse eql(NVsnA, NVsnB).
%% @doc Test that VsnA is less than VsnB
-spec lt(any_version(), any_version()) -> boolean().
lt(VsnA, VsnB) ->
{MMPA, {AlphaA, PatchA}} = normalize(parse(VsnA)),
{MMPB, {AlphaB, PatchB}} = normalize(parse(VsnB)),
((MMPA < MMPB)
orelse
((MMPA =:= MMPB)
andalso
((AlphaB =:= [] andalso AlphaA =/= [])
orelse
((not (AlphaA =:= [] andalso AlphaB =/= []))
andalso
(AlphaA < AlphaB))))
orelse
((MMPA =:= MMPB)
andalso
(AlphaA =:= AlphaB)
andalso
((PatchA =:= [] andalso PatchB =/= [])
orelse
PatchA < PatchB))).
%% @doc Test that VsnA is less than or equal to VsnB
-spec lte(any_version(), any_version()) -> boolean().
lte(VsnA, VsnB) ->
NVsnA = normalize(parse(VsnA)),
NVsnB = normalize(parse(VsnB)),
lt(NVsnA, NVsnB) orelse eql(NVsnA, NVsnB).
%% @doc Test that VsnMatch is greater than or equal to Vsn1 and
%% less than or equal to Vsn2
-spec between(any_version(), any_version(), any_version()) -> boolean().
between(Vsn1, Vsn2, VsnMatch) ->
NVsnA = normalize(parse(Vsn1)),
NVsnB = normalize(parse(Vsn2)),
NVsnMatch = normalize(parse(VsnMatch)),
gte(NVsnMatch, NVsnA) andalso
lte(NVsnMatch, NVsnB).
%% @doc check that VsnA is Approximately greater than VsnB
%%
%% Specifying ">= 2.6.5" is an optimistic version constraint. All
%% versions greater than the one specified, including major releases
%% (e.g. 3.0.0) are allowed.
%%
%% Conversely, specifying "~> 2.6" is pessimistic about future major
%% revisions and "~> 2.6.5" is pessimistic about future minor
%% revisions.
%%
%% "~> 2.6" matches cookbooks >= 2.6.0 AND &lt; 3.0.0
%% "~> 2.6.5" matches cookbooks >= 2.6.5 AND &lt; 2.7.0
pes(VsnA, VsnB) ->
internal_pes(parse(VsnA), parse(VsnB)).
%%%===================================================================
%%% Friend Functions
%%%===================================================================
%% @doc helper function for the peg grammar to parse the iolist into a semver
-spec internal_parse_version(iolist()) -> semver().
internal_parse_version([MMP, AlphaPart, BuildPart, _]) ->
{parse_major_minor_patch_minpatch(MMP), {parse_alpha_part(AlphaPart),
parse_alpha_part(BuildPart)}}.
%% @doc helper function for the peg grammar to parse the iolist into a major_minor_patch
-spec parse_major_minor_patch_minpatch(iolist()) -> major_minor_patch_minpatch().
parse_major_minor_patch_minpatch([MajVsn, [], [], []]) ->
strip_maj_version(MajVsn);
parse_major_minor_patch_minpatch([MajVsn, [<<".">>, MinVsn], [], []]) ->
{strip_maj_version(MajVsn), MinVsn};
parse_major_minor_patch_minpatch([MajVsn,
[<<".">>, MinVsn],
[<<".">>, PatchVsn], []]) ->
{strip_maj_version(MajVsn), MinVsn, PatchVsn};
parse_major_minor_patch_minpatch([MajVsn,
[<<".">>, MinVsn],
[<<".">>, PatchVsn],
[<<".">>, MinPatch]]) ->
{strip_maj_version(MajVsn), MinVsn, PatchVsn, MinPatch}.
%% @doc helper function for the peg grammar to parse the iolist into an alpha part
-spec parse_alpha_part(iolist()) -> [alpha_part()].
parse_alpha_part([]) ->
[];
parse_alpha_part([_, AV1, Rest]) ->
[erlang:iolist_to_binary(AV1) |
[format_alpha_part(Part) || Part <- Rest]].
%% @doc according to semver alpha parts that can be treated like
%% numbers must be. We implement that here by taking the alpha part
%% and trying to convert it to a number, if it succeeds we use
%% it. Otherwise we do not.
-spec format_alpha_part(iolist()) -> integer() | binary().
format_alpha_part([<<".">>, AlphaPart]) ->
Bin = erlang:iolist_to_binary(AlphaPart),
try
erlang:list_to_integer(erlang:binary_to_list(Bin))
catch
error:badarg ->
Bin
end.
%%%===================================================================
%%% Internal Functions
%%%===================================================================
-spec strip_maj_version(iolist()) -> version_element().
strip_maj_version([<<"v">>, MajVsn]) ->
MajVsn;
strip_maj_version([[], MajVsn]) ->
MajVsn;
strip_maj_version(MajVsn) ->
MajVsn.
-spec tokens(semvar()) -> parsed_semvar().
tokens(Vsn) ->
[MajorVsn, MinorVsn, RawPatch] = string:tokens(Vsn, "."),
{PatchVsn, PatchString} = split_patch(RawPatch),
{MajorVsn, MinorVsn, PatchVsn, PatchString}.
-spec to_list(integer() | binary() | string()) -> string() | binary().
to_list(Detail) when erlang:is_integer(Detail) ->
erlang:integer_to_list(Detail);
to_list(Detail) when erlang:is_list(Detail); erlang:is_binary(Detail) ->
Detail.
-spec split_patch(string()) ->
{PatchVsn::string(), PatchStr::string()}.
split_patch(RawPatch) ->
{PatchVsn, PatchStr} = split_patch(RawPatch, {"", ""}),
{lists:reverse(PatchVsn), PatchStr}.
-spec format_vsn_rest(binary() | string(), [integer() | binary()]) -> iolist().
format_vsn_rest(_TypeMark, []) ->
[];
format_vsn_rest(TypeMark, [Head | Rest]) ->
[TypeMark, Head |
[[".", to_list(Detail)] || Detail <- Rest]].
-spec split_patch(string(), {AccPatchVsn::string(), AccPatchStr::string()}) ->
{PatchVsn::string(), PatchStr::string()}.
split_patch([], Acc) ->
Acc;
split_patch([Dig|T], {PatchVsn, PatchStr}) when Dig >= $0 andalso Dig =< $9 ->
split_patch(T, {[Dig|PatchVsn], PatchStr});
split_patch(PatchStr, {PatchVsn, ""}) ->
{PatchVsn, PatchStr}.
%% @doc normalize the semver so they can be compared
-spec normalize(semver()) -> semver().
normalize({Vsn, Rest})
when erlang:is_binary(Vsn);
erlang:is_integer(Vsn) ->
{{Vsn, 0, 0, 0}, Rest};
normalize({{Maj, Min}, Rest}) ->
{{Maj, Min, 0, 0}, Rest};
normalize({{Maj, Min, Patch}, Rest}) ->
{{Maj, Min, Patch, 0}, Rest};
normalize(Other = {{_, _, _, _}, {_,_}}) ->
Other.
-spec compare_toks(parsed_semvar(), parsed_semvar()) -> boolean().
compare_toks({MajA, MinA, PVA, PSA}, {MajB, MinB, PVB, PSB}) ->
compare_toks2({to_int(MajA), to_int(MinA), to_int(PVA), PSA},
{to_int(MajB), to_int(MinB), to_int(PVB), PSB}).
-spec compare_toks2(parsed_semvar(), parsed_semvar()) -> boolean().
compare_toks2({MajA, _MinA, _PVA, _PSA}, {MajB, _MinB, _PVB, _PSB})
when MajA > MajB ->
true;
compare_toks2({_Maj, MinA, _PVA, _PSA}, {_Maj, MinB, _PVB, _PSB})
when MinA > MinB ->
true;
compare_toks2({_Maj, _Min, PVA, _PSA}, {_Maj, _Min, PVB, _PSB})
when PVA > PVB ->
true;
compare_toks2({_Maj, _Min, _PV, ""}, {_Maj, _Min, _PV, PSB}) when PSB /= ""->
true;
compare_toks2({_Maj, _Min, _PV, PSA}, {_Maj, _Min, _PV, ""}) when PSA /= ""->
false;
compare_toks2({_Maj, _Min, _PV, PSA}, {_Maj, _Min, _PV, PSB}) when PSA > PSB ->
true;
compare_toks2(_ToksA, _ToksB) ->
false.
-spec to_int(string()) -> integer().
to_int(String) ->
try
list_to_integer(String)
catch
error:badarg ->
throw(invalid_semver_string)
end.
%%%===================================================================
%%% Test Functions
%%%===================================================================
-ifndef(NOTEST).
-include_lib("eunit/include/eunit.hrl").
split_patch_test() ->
?assertMatch({"123", "alpha1"}, split_patch("123alpha1")).
compare_test() ->
?assertMatch(true, compare("1.2.3", "1.2.3alpha")),
?assertMatch(true, compare("1.2.3beta", "1.2.3alpha")),
?assertMatch(true, compare("1.2.4", "1.2.3")),
?assertMatch(true, compare("1.3.3", "1.2.3")),
?assertMatch(true, compare("2.2.3", "1.2.3")),
?assertMatch(true, compare("4.2.3", "3.10.3")),
?assertMatch(false, compare("1.2.3", "2.2.3")),
?assertThrow(invalid_semver_string, compare("1.b.2", "1.3.4")),
?assertThrow(invalid_semver_string, compare("1.2.2", "1.3.t")).
-endif.
%% @doc to do the pessimistic compare we need a parsed semver. This is
%% the internal implementation of the of the pessimistic run. The
%% external just ensures that versions are parsed.
-spec internal_pes(semver(), semver()) -> boolean().
internal_pes(VsnA, {{LM, LMI}, Alpha})
when erlang:is_integer(LM),
erlang:is_integer(LMI) ->
gte(VsnA, {{LM, LMI, 0}, Alpha}) andalso
lt(VsnA, {{LM + 1, 0, 0, 0}, {[], []}});
internal_pes(VsnA, {{LM, LMI, LP}, Alpha})
when erlang:is_integer(LM),
erlang:is_integer(LMI),
erlang:is_integer(LP) ->
gte(VsnA, {{LM, LMI, LP}, Alpha})
andalso
lt(VsnA, {{LM, LMI + 1, 0, 0}, {[], []}});
internal_pes(VsnA, {{LM, LMI, LP, LMP}, Alpha})
when erlang:is_integer(LM),
erlang:is_integer(LMI),
erlang:is_integer(LP),
erlang:is_integer(LMP) ->
gte(VsnA, {{LM, LMI, LP, LMP}, Alpha})
andalso
lt(VsnA, {{LM, LMI, LP + 1, 0}, {[], []}});
internal_pes(Vsn, LVsn) ->
gte(Vsn, LVsn).

302
src/ec_semver_parser.erl Normal file
View 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.

View file

@ -1,128 +0,0 @@
%%%-------------------------------------------------------------------
%%% @copyright (C) 2011, Erlware LLC
%%% @doc
%%% Helper functions for working with strings.
%%% @end
%%%-------------------------------------------------------------------
-module(ec_string).
-export([
compare_versions/2
]).
%%%===================================================================
%%% API
%%%===================================================================
%% @doc Is arbitrary version string A bigger than version string B?
%% Valid version string elements are either separated by . or - or both.
%% Final version string elements may have a numeric followed directly by an
%% alpha numeric and will be compared separately as in 12alpha.
%%
%% <pre>
%% Example: compare_versions("3-2-5-alpha", "3.10.6") will return false
%% compare_versions("3-2-alpha", "3.2.1-alpha") will return false
%% compare_versions("3-2alpha", "3.2.1-alpha") will return false
%% compare_versions("3.2.2", "3.2.2") will return false
%% compare_versions("3.2.1", "3.2.1-rc2") will return true
%% compare_versions("3.2.2", "3.2.1") will return true
%% </pre>
-spec compare_versions(VsnA::string(), VsnB::string()) -> boolean().
compare_versions(VsnA, VsnB) ->
compare(string:tokens(VsnA, ".-"),string:tokens(VsnB, ".-")).
%%%===================================================================
%%% Internal Functions
%%%===================================================================
-spec compare(string(), string()) -> boolean().
compare([Str|TA], [Str|TB]) ->
compare(TA, TB);
compare([StrA|TA], [StrB|TB]) ->
fine_compare(split_numeric_alpha(StrA), TA,
split_numeric_alpha(StrB), TB);
compare([], [Str]) ->
not compare_against_nothing(Str);
compare([Str], []) ->
compare_against_nothing(Str);
compare([], [_,_|_]) ->
false;
compare([_,_|_], []) ->
true;
compare([], []) ->
false.
-spec compare_against_nothing(string()) -> boolean().
compare_against_nothing(Str) ->
case split_numeric_alpha(Str) of
{_StrDig, ""} -> true;
{"", _StrAlpha} -> false;
{_StrDig, _StrAlpha} -> true
end.
-spec fine_compare({string(), string()}, string(),
{string(), string()}, string()) ->
boolean().
fine_compare({_StrDigA, StrA}, TA, {_StrDigB, _StrB}, _TB)
when StrA /= "", TA /= [] ->
throw(invalid_version_string);
fine_compare({_StrDigA, _StrA}, _TA, {_StrDigB, StrB}, TB)
when StrB /= "", TB /= [] ->
throw(invalid_version_string);
fine_compare({"", _StrA}, _TA, {StrDigB, _StrB}, _TB) when StrDigB /= "" ->
false;
fine_compare({StrDigA, _StrA}, _TA, {"", _StrB}, _TB) when StrDigA /= "" ->
true;
fine_compare({StrDig, ""}, _TA, {StrDig, StrB}, _TB) when StrB /= "" ->
true;
fine_compare({StrDig, StrA}, _TA, {StrDig, ""}, _TB) when StrA /= "" ->
false;
fine_compare({StrDig, StrA}, _TA, {StrDig, StrB}, _TB) ->
StrA > StrB;
fine_compare({StrDigA, _StrA}, _TA, {StrDigB, _StrB}, _TB) ->
list_to_integer(StrDigA) > list_to_integer(StrDigB).
%% In the case of a version sub part with a numeric then an alpha,
%% split out the numeric and alpha "24alpha" becomes {"24", "alpha"}
-spec split_numeric_alpha(string()) ->
{PatchVsn::string(), PatchStr::string()}.
split_numeric_alpha(RawVsn) ->
{Num, Str} = split_numeric_alpha(RawVsn, {"", ""}),
{lists:reverse(Num), Str}.
-spec split_numeric_alpha(string(), {PatchVsnAcc::string(),
PatchStrAcc::string()}) ->
{PatchVsn::string(), PatchStr::string()}.
split_numeric_alpha([], Acc) ->
Acc;
split_numeric_alpha([Dig|T], {PatchVsn, PatchStr})
when Dig >= $0 andalso Dig =< $9 ->
split_numeric_alpha(T, {[Dig|PatchVsn], PatchStr});
split_numeric_alpha(PatchStr, {PatchVsn, ""}) ->
{PatchVsn, PatchStr}.
%%%===================================================================
%%% Test Functions
%%%===================================================================
-ifndef(NOTEST).
-include_lib("eunit/include/eunit.hrl").
split_numeric_alpha_test() ->
?assertMatch({"123", "alpha1"}, split_numeric_alpha("123alpha1")).
compare_versions_test() ->
?assertMatch(true, compare_versions("1.2.3", "1.2.3alpha")),
?assertMatch(true, compare_versions("1.2.3-beta", "1.2.3-alpha")),
?assertMatch(true, compare_versions("1-2-3", "1-2-3alpha")),
?assertMatch(true, compare_versions("1-2-3", "1-2-3-rc3")),
?assertMatch(true, compare_versions("1.2.3beta", "1.2.3alpha")),
?assertMatch(true, compare_versions("1.2.4", "1.2.3")),
?assertMatch(true, compare_versions("1.3.3", "1.2.3")),
?assertMatch(true, compare_versions("2.2.3", "1.2.3")),
?assertMatch(true, compare_versions("4.2.3", "3.10.3")),
?assertMatch(false, compare_versions("1.2.3", "2.2.3")),
?assertMatch(false, compare_versions("1.2.2", "1.3.t")),
?assertMatch(false, compare_versions("1.2t", "1.3.t")),
?assertThrow(invalid_version_string, compare_versions("1.b.2", "1.3.4")).
-endif.

View file

@ -1,4 +1,5 @@
%% -*- mode: Erlang; fill-column: 79; comment-column: 70; -*-
%% vi:ts=4 sw=4 et
%%%---------------------------------------------------------------------------
%%% Permission is hereby granted, free of charge, to any person
%%% obtaining a copy of this software and associated documentation
@ -38,18 +39,21 @@
say/1,
say/2]).
-ifdef(TEST).
-export([get_boolean/1,
get_integer/1]).
-endif.
-export_type([prompt/0,
type/0,
supported/0]).
-include_lib("eunit/include/eunit.hrl").
%%============================================================================
%% Types
%%============================================================================
-type prompt() :: string().
-type type() :: boolean | number | string.
-type supported() :: string() | boolean() | number().
-type supported() :: boolean() | number() | string().
%%============================================================================
%% API
@ -76,7 +80,7 @@ ask(Prompt) ->
ask_default(Prompt, Default) ->
ask_convert(Prompt, fun get_string/1, string, Default).
%% @doc Asks the user to respond to the prompt. Trys to return the
%% @doc Asks the user to respond to the prompt. Tries to return the
%% value in the format specified by 'Type'.
-spec ask(prompt(), type()) -> supported().
ask(Prompt, boolean) ->
@ -84,9 +88,9 @@ ask(Prompt, boolean) ->
ask(Prompt, number) ->
ask_convert(Prompt, fun get_integer/1, number, none);
ask(Prompt, string) ->
ask_convert(Prompt, fun get_integer/1, string, none).
ask_convert(Prompt, fun get_string/1, string, none).
%% @doc Asks the user to respond to the prompt. Trys to return the
%% @doc Asks the user to respond to the prompt. Tries to return the
%% value in the format specified by 'Type'.
-spec ask_default(prompt(), type(), supported()) -> supported().
ask_default(Prompt, boolean, Default) ->
@ -100,8 +104,11 @@ ask_default(Prompt, string, Default) ->
%% between min and max.
-spec ask(prompt(), number(), number()) -> number().
ask(Prompt, Min, Max)
when is_list(Prompt), is_number(Min), is_number(Max) ->
Res = ask(Prompt, fun get_integer/1, none),
when erlang:is_list(Prompt),
erlang:is_number(Min),
erlang:is_number(Max),
Min =< Max ->
Res = ask_convert(Prompt, fun get_integer/1, number, none),
case (Res >= Min andalso Res =< Max) of
true ->
Res;
@ -115,15 +122,17 @@ ask(Prompt, Min, Max)
%% ============================================================================
%% @doc Actually does the work of asking, checking result and
%% translating result into the requested format.
-spec ask_convert(prompt(), fun(), type(), supported()) -> supported().
-spec ask_convert(prompt(), fun((any()) -> any()), type(), supported() | none) -> supported().
ask_convert(Prompt, TransFun, Type, Default) ->
NewPrompt = Prompt ++ case Default of
none ->
[];
Default ->
" (" ++ sin_utils:term_to_list(Default) ++ ")"
end ++ "> ",
Data = string:strip(string:strip(io:get_line(NewPrompt)), both, $\n),
NewPrompt =
erlang:binary_to_list(erlang:iolist_to_binary([Prompt,
case Default of
none ->
[];
Default ->
[" (", io_lib:format("~p", [Default]) , ")"]
end, "> "])),
Data = string:trim(string:trim(io:get_line(NewPrompt)), both, [$\n]),
Ret = TransFun(Data),
case Ret of
no_data ->
@ -141,7 +150,7 @@ ask_convert(Prompt, TransFun, Type, Default) ->
Ret
end.
%% @doc Trys to translate the result into a boolean
%% @doc Tries to translate the result into a boolean
-spec get_boolean(string()) -> boolean().
get_boolean([]) ->
no_data;
@ -168,7 +177,7 @@ get_boolean([$N | _]) ->
get_boolean(_) ->
no_clue.
%% @doc Trys to translate the result into an integer
%% @doc Tries to translate the result into an integer
-spec get_integer(string()) -> integer().
get_integer([]) ->
no_data;
@ -192,21 +201,3 @@ get_string(String) ->
false ->
no_clue
end.
%%%====================================================================
%%% tests
%%%====================================================================
general_test_() ->
[?_test(42 == get_integer("42")),
?_test(500211 == get_integer("500211")),
?_test(1234567890 == get_integer("1234567890")),
?_test(12345678901234567890 == get_integer("12345678901234567890")),
?_test(true == get_boolean("true")),
?_test(false == get_boolean("false")),
?_test(true == get_boolean("Ok")),
?_test(true == get_boolean("ok")),
?_test(true == get_boolean("Y")),
?_test(true == get_boolean("y")),
?_test(false == get_boolean("False")),
?_test(false == get_boolean("No")),
?_test(false == get_boolean("no"))].

51
src/ec_vsn.erl Normal file
View 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).

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

View file

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

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

View file

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