Compare commits
178 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
108c796687 | ||
![]() |
43c1f3412b | ||
![]() |
dc2184ea06 | ||
![]() |
70bab7df95 | ||
![]() |
06347e7f31 | ||
![]() |
9d0fb6d895 | ||
![]() |
2072b49220 | ||
![]() |
cb873afb22 | ||
![]() |
c109e7969b | ||
![]() |
2a20cdea1e | ||
![]() |
ea203ec90d | ||
![]() |
5a847014c9 | ||
![]() |
794d03a5ee | ||
![]() |
817c14c46e | ||
![]() |
280f8e72f2 | ||
![]() |
5afd1335f1 | ||
![]() |
2ce94f82ce | ||
![]() |
5a352dc599 | ||
![]() |
357b5844eb | ||
![]() |
d87d7d8b6c | ||
![]() |
92e20c474f | ||
![]() |
c5971cdcf4 | ||
![]() |
6d6b6ec9bd | ||
![]() |
c789fca51f | ||
![]() |
d6a431362d | ||
![]() |
bbfef69d2e | ||
![]() |
3d72448491 | ||
![]() |
596b573775 | ||
![]() |
d0b6fc0011 | ||
![]() |
d159878f62 | ||
![]() |
69d7fdd626 | ||
![]() |
f49368d3fc | ||
![]() |
22b48f90de | ||
![]() |
0a7d808290 | ||
![]() |
2f3afd7dbb | ||
![]() |
68b462469f | ||
![]() |
1c405bbfc3 | ||
![]() |
accda0db1a | ||
![]() |
28847aa896 | ||
![]() |
a0b9ad0e5f | ||
![]() |
73d9706c7a | ||
![]() |
b8a5075026 | ||
![]() |
ef98ff4ea4 | ||
![]() |
08a98032e2 | ||
![]() |
a443d27c79 | ||
![]() |
ed00a606f3 | ||
![]() |
3448a6f5e0 | ||
![]() |
b849ebde02 | ||
![]() |
a51edf2e3e | ||
![]() |
7314545b34 | ||
![]() |
fa59231e1e | ||
![]() |
67ee655bbb | ||
![]() |
1cccb8392a | ||
![]() |
628c87557a | ||
![]() |
dd38e5cf98 | ||
![]() |
99e55979dc | ||
![]() |
4c91fc4c01 | ||
![]() |
31e123d9f1 | ||
![]() |
bab222aeb4 | ||
![]() |
fc247e296b | ||
![]() |
3e2688cbec | ||
![]() |
aa114b1ef8 | ||
![]() |
c42272a9eb | ||
![]() |
6fadc548d2 | ||
![]() |
d7267a7d67 | ||
![]() |
ef5c9cfbb6 | ||
![]() |
0d5cd470e6 | ||
![]() |
af9cb4dc4b | ||
![]() |
78bb08c4f8 | ||
![]() |
5a3929bcea | ||
![]() |
89555d25ef | ||
![]() |
fd493ecf3d | ||
![]() |
b193876305 | ||
![]() |
10d56c2e04 | ||
![]() |
7230f747e7 | ||
![]() |
2a85d5d92f | ||
![]() |
2dfcc52dd5 | ||
![]() |
366bc5e8d5 | ||
![]() |
4d42f5eddf | ||
![]() |
e413fcbf56 | ||
![]() |
220883945a | ||
![]() |
66e9dc6ae9 | ||
![]() |
7ed65a0af1 | ||
![]() |
8671750e66 | ||
![]() |
795b3d8961 | ||
![]() |
b550391a4e | ||
![]() |
6d88edea06 | ||
![]() |
ff6b7139ca | ||
![]() |
bb934b522f | ||
![]() |
3564470578 | ||
![]() |
1ea2cadb1a | ||
![]() |
41b313d425 | ||
![]() |
5d73286e92 | ||
![]() |
5e43673c34 | ||
![]() |
6036208bb5 | ||
![]() |
bf7f84590f | ||
![]() |
1f7f5d9f69 | ||
![]() |
f39b4edb92 | ||
![]() |
4cd1a41cb4 | ||
![]() |
569c7db56b | ||
![]() |
69967d71ff | ||
![]() |
cc9dee4e08 | ||
![]() |
d0ee71a0a7 | ||
![]() |
43f6a78870 | ||
![]() |
2eb0f88c61 | ||
![]() |
7910de376e | ||
![]() |
2a053253fb | ||
![]() |
2d40869c1a | ||
![]() |
0bf12290ec | ||
![]() |
63a34f1c68 | ||
![]() |
37f8bf48e9 | ||
![]() |
d73a31e664 | ||
![]() |
6ca6037f29 | ||
![]() |
792245bb26 | ||
![]() |
faeadb732a | ||
![]() |
c685f4dc12 | ||
![]() |
ef2075d475 | ||
![]() |
647e26db15 | ||
![]() |
b1387c08c0 | ||
![]() |
c4c20db815 | ||
![]() |
a504a6adfc | ||
![]() |
8427b7bdc6 | ||
![]() |
48e8523e39 | ||
![]() |
52770185f9 | ||
![]() |
a443d476ac | ||
![]() |
7ab98975d8 | ||
![]() |
15ded5317f | ||
![]() |
80b3a88530 | ||
![]() |
d6ec97246f | ||
![]() |
b71173dc65 | ||
![]() |
0e0eb4186f | ||
![]() |
0b963146fe | ||
![]() |
6dbeefc99b | ||
![]() |
02734c103b | ||
![]() |
5f1f4a7785 | ||
![]() |
5387ec8f02 | ||
![]() |
4f19dcf5ec | ||
![]() |
c3b490ec52 | ||
![]() |
644ed69cda | ||
![]() |
eeb797e1f8 | ||
![]() |
6d7a031d46 | ||
![]() |
5fbbf93d4d | ||
![]() |
bb33398614 | ||
![]() |
cd1faf8631 | ||
![]() |
b4c3d4a302 | ||
![]() |
c750c339ff | ||
![]() |
91a502550a | ||
![]() |
1e5bc5dfc5 | ||
![]() |
80a856721d | ||
![]() |
ff4a9a6eee | ||
![]() |
e7bf41e3ad | ||
![]() |
aeed7641da | ||
![]() |
72cececf88 | ||
![]() |
928c4e9be4 | ||
![]() |
58f75fc966 | ||
![]() |
8cd386bb40 | ||
![]() |
fb63b7a318 | ||
![]() |
df55df9db6 | ||
![]() |
21a9fc1618 | ||
![]() |
4cc45533ce | ||
![]() |
daa04395f2 | ||
![]() |
5cb5612857 | ||
![]() |
4135dfa8e3 | ||
![]() |
58a92482d1 | ||
![]() |
6f9ca7d13c | ||
![]() |
328b2e0b32 | ||
![]() |
5139108fcc | ||
![]() |
13bab9fc8e | ||
![]() |
4a98e54d9d | ||
![]() |
7384819edb | ||
![]() |
ef659da9a4 | ||
![]() |
a32086d865 | ||
![]() |
6183b60e96 | ||
![]() |
d8b08ae3ba | ||
![]() |
65ff159411 | ||
![]() |
25442e0f74 | ||
![]() |
b8d13ddbb4 | ||
![]() |
3ffaa2a060 |
17 changed files with 1761 additions and 297 deletions
47
.github/workflows/tests-workflow.yml
vendored
Normal file
47
.github/workflows/tests-workflow.yml
vendored
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
name: qdate tests and dialyzer
|
||||||
|
on: push
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
linux:
|
||||||
|
name: OTP ${{ matrix.otp_version }}
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: ubuntu-22.04
|
||||||
|
otp_version: '27.x'
|
||||||
|
rebar3_version: "3.24.0"
|
||||||
|
- os: ubuntu-22.04
|
||||||
|
otp_version: '27.x'
|
||||||
|
rebar3_version: "3.23.0"
|
||||||
|
- os: ubuntu-22.04
|
||||||
|
otp_version: '26.x'
|
||||||
|
rebar3_version: "3.22.1"
|
||||||
|
- os: ubuntu-22.04
|
||||||
|
otp_version: '25.x'
|
||||||
|
rebar3_version: "3.22.1"
|
||||||
|
- os: ubuntu-22.04
|
||||||
|
otp_version: '24.x'
|
||||||
|
rebar3_version: "3.22.1"
|
||||||
|
- os: ubuntu-20.04
|
||||||
|
otp_version: '23.x'
|
||||||
|
rebar3_version: "3.19.0"
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Install OTP ${{matrix.otp_version}}
|
||||||
|
uses: erlef/setup-beam@v1
|
||||||
|
with:
|
||||||
|
version-type: loose
|
||||||
|
otp-version: ${{ matrix.otp_version}}
|
||||||
|
rebar3-version: ${{ matrix.rebar3_version}}
|
||||||
|
|
||||||
|
- name: Checkout qdate
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Run Tests
|
||||||
|
run: make test
|
||||||
|
|
||||||
|
- name: Run Dialyzer
|
||||||
|
run: make dialyzer
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,6 +1,10 @@
|
||||||
*~
|
*~
|
||||||
*.beam
|
*.beam
|
||||||
*.sw?
|
*.sw?
|
||||||
|
*.iml
|
||||||
deps/
|
deps/
|
||||||
ebin/
|
ebin/
|
||||||
.eunit/
|
.eunit/
|
||||||
|
.idea/
|
||||||
|
_build
|
||||||
|
doc/
|
||||||
|
|
10
.travis.yml
10
.travis.yml
|
@ -1,10 +0,0 @@
|
||||||
language: erlang
|
|
||||||
script: "make test"
|
|
||||||
otp_release:
|
|
||||||
- R16B
|
|
||||||
- R15B02
|
|
||||||
- R15B01
|
|
||||||
- R15B
|
|
||||||
- R14B03
|
|
||||||
- R14B02
|
|
||||||
before_script: "sudo apt-get --yes --force-yes install libpam0g-dev"
|
|
87
CHANGELOG.markdown
Normal file
87
CHANGELOG.markdown
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
## 0.7.3
|
||||||
|
|
||||||
|
* Remove the `?else` macro.
|
||||||
|
|
||||||
|
## 0.7.2
|
||||||
|
|
||||||
|
* Update the error message when qdate is not started with some better instructions.
|
||||||
|
* Some minor updates for hex.pm
|
||||||
|
* Removed the `erlnow()` warning
|
||||||
|
* Fix some typos in the documentation
|
||||||
|
* Update the makefile to use [rebar3.mk](https://rebar3.mk)
|
||||||
|
* Skipped 0.7.1 only because there was a partially tagged 0.7.1 for a while,
|
||||||
|
but was never published to hex. Just to ensure upgrades are easier, I just skipped
|
||||||
|
a proper 0.7.1 release
|
||||||
|
|
||||||
|
## 0.7.0
|
||||||
|
|
||||||
|
* Re-introduce the qdate server for storing qdate timezones, formats, and parsers,
|
||||||
|
rather than overloading the `application` env vars (since the `application`
|
||||||
|
module only wants keys to be atoms).
|
||||||
|
* Convert to using `qdate_localtime` 1.2.0 (which passes dialyzer checks)
|
||||||
|
* `qdate` is passing dialyzer again
|
||||||
|
|
||||||
|
## 0.6.0
|
||||||
|
|
||||||
|
* Add `age` and `age_days` functions
|
||||||
|
* Add option to preserve millisecond accuracy in date parsing and formatting (@Leonardb)
|
||||||
|
|
||||||
|
|
||||||
|
## 0.5.0
|
||||||
|
|
||||||
|
* Add `range_X` functions for getting a list of dates/times within a range
|
||||||
|
(such as `range_day/3` to get a range of days between a start and end date.
|
||||||
|
* Add `beginning_X` functions to return the beginning of the provided precision
|
||||||
|
(minute, hour, day, week, month, or year)
|
||||||
|
+ Add `end_X` functions to return the last second of each time period (this is
|
||||||
|
just the opposite of `beginning_X`)
|
||||||
|
* Add `between/[2,3,5]` functions for computing whether a date/time is between
|
||||||
|
two others.
|
||||||
|
* Update to rebar3 and add hex compatibility. (@Licenser)
|
||||||
|
* Properly add dependent apps to .app.src (@Licenser)
|
||||||
|
* Add an optional "relative date/time parser".
|
||||||
|
* Fix: Ensure `get_timezone()` returns the default timezone (from config) if it
|
||||||
|
hasn't been set by `get_timezone()`
|
||||||
|
* Fix UTC/GMT bug (@loudferret)
|
||||||
|
* Fix Erlang 21 Stacktrace changes (@tnt-dev)
|
||||||
|
* Set a better rebar2 version of erlware commons (@tnt-dev)
|
||||||
|
|
||||||
|
## 0.4.2
|
||||||
|
|
||||||
|
* Add partial support for `ec_date`'s 4-tuple subsecond accuracy time format.
|
||||||
|
* Fix `erlware_commons` dependency to a rebar2-compatible version.
|
||||||
|
|
||||||
|
## 0.4.1
|
||||||
|
|
||||||
|
* Remove unnecessary `io:format` call.
|
||||||
|
|
||||||
|
## 0.4.0
|
||||||
|
|
||||||
|
* Remove dependency on a running server for tracking application state.
|
||||||
|
Instead, parsers and formats are registered to the application environment
|
||||||
|
vars (e.g. `application:get_env`), and timezones are registered to the
|
||||||
|
application environment or the process dictionary. A side-effect of this
|
||||||
|
change is that you can no longer query another process's timezone.
|
||||||
|
* Add basic date arithmetic (e.g. `qdate:add_hours/[1-2]`, etc).
|
||||||
|
* Add `get_formats()` and `get_parsers()` to see list of registered formats and
|
||||||
|
parsers.
|
||||||
|
* Fix bug related to relying on the application environment variable
|
||||||
|
`default_timezone`
|
||||||
|
|
||||||
|
## 0.3.0
|
||||||
|
|
||||||
|
* Add Timezone/Daylight Saving Disambiguation
|
||||||
|
* Add the `auto` timezone shortcut
|
||||||
|
* Fix rebar.config to allow for compilation on Erlang 17
|
||||||
|
|
||||||
|
## 0.2.1
|
||||||
|
|
||||||
|
* Fix allowing timezone names to be binary
|
||||||
|
|
||||||
|
## 0.2.0
|
||||||
|
|
||||||
|
* Adding `qdate:compare/2,3` for easily comparing dates
|
||||||
|
|
||||||
|
## 0.1.0
|
||||||
|
|
||||||
|
* Initial Release
|
46
Makefile
46
Makefile
|
@ -1,13 +1,41 @@
|
||||||
all: get-deps compile
|
all: compile
|
||||||
|
|
||||||
get-deps:
|
# Check if rebar3.mk exists, and if not, download it
|
||||||
./rebar get-deps
|
ifeq ("$(wildcard rebar3.mk)","")
|
||||||
|
$(shell curl -O https://raw.githubusercontent.com/choptastic/rebar3.mk/master/rebar3.mk)
|
||||||
|
endif
|
||||||
|
|
||||||
compile:
|
# rebar3.mk adds a new rebar3 rule to your Makefile
|
||||||
./rebar compile
|
# (see https://github.com/choptastic/rebar3.mk) for full info
|
||||||
|
include rebar3.mk
|
||||||
|
|
||||||
test: get-deps compile
|
compile: rebar3
|
||||||
./rebar skip_deps=true eunit
|
$(REBAR) compile
|
||||||
|
|
||||||
|
update: rebar3
|
||||||
|
$(REBAR) update
|
||||||
|
|
||||||
|
test:
|
||||||
|
EUNIT=1 $(REBAR) compile
|
||||||
|
EUNIT=1 $(REBAR) eunit
|
||||||
|
|
||||||
|
dialyzer: compile
|
||||||
|
DIALYZER=1 $(REBAR) dialyzer
|
||||||
|
|
||||||
|
dev:
|
||||||
|
mkdir -p _checkouts
|
||||||
|
cd _checkouts; git clone https://github.com/choptastic/qdate_localtime
|
||||||
|
|
||||||
|
|
||||||
|
run: rebar3
|
||||||
|
$(REBAR) shell
|
||||||
|
|
||||||
|
push_tags:
|
||||||
|
git push --tag
|
||||||
|
|
||||||
|
pull_tags:
|
||||||
|
git pull --tag
|
||||||
|
|
||||||
|
publish: rebar3 pull_tags
|
||||||
|
$(REBAR) hex publish
|
||||||
|
|
||||||
run:
|
|
||||||
erl -pa ebin/ deps/*/ebin/ -eval "application:start(qdate)"
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# qdate - A Wrapper for Erlang Date and Timezone Management
|
# qdate - Erlang Date and Timezone Library
|
||||||
|
|
||||||
[](https://travis-ci.org/choptastic/qdate)
|
[](https://github.com/choptastic/qdate/actions/workflows/tests-workflow.yml)
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
|
@ -21,12 +21,15 @@ benefits of `ec_date` and `erlang_localtime`, as well as extending the
|
||||||
capabilities of both to provide for other needed tools found in a single
|
capabilities of both to provide for other needed tools found in a single
|
||||||
module.
|
module.
|
||||||
|
|
||||||
`qdate` will provide, under the roof of a single module date and time formatting
|
`qdate` provides date and time formatting and parsing from and into:
|
||||||
and parsing from and into:
|
|
||||||
+ Formatting Strings
|
+ Formatting Strings
|
||||||
+ Erlang Date Format
|
+ Erlang Date Format
|
||||||
+ Erlang Now Format
|
+ Erlang Now Format
|
||||||
+ Unixtime integers
|
+ Unixtime integers
|
||||||
|
+ Timezones
|
||||||
|
|
||||||
|
And all this while dealing with timezone parsing, formatting, conversion
|
||||||
|
and overall management.
|
||||||
|
|
||||||
#### Acceptable Date Formats
|
#### Acceptable Date Formats
|
||||||
|
|
||||||
|
@ -65,10 +68,15 @@ T, Z, r, and c), `qdate` will handle them for us.
|
||||||
+ `to_now(Date)` - converts any date/time format to Erlang now format.
|
+ `to_now(Date)` - converts any date/time format to Erlang now format.
|
||||||
+ `to_unixtime(Date)` - converts any date/time format to a unixtime integer
|
+ `to_unixtime(Date)` - converts any date/time format to a unixtime integer
|
||||||
|
|
||||||
|
A **ToTimezone** value of the atom `auto` will automatically determine the
|
||||||
|
timezone. For example, `to_date(Date, auto)` is exactly the same as
|
||||||
|
`to_date(Date)`
|
||||||
|
|
||||||
**A Note About Argument Order**: In all cases, `ToTimezone` is optional and if
|
**A Note About Argument Order**: In all cases, `ToTimezone` is optional and if
|
||||||
omitted, will be determined as described below in "Understanding Timezone
|
omitted, will be determined as described below in "Understanding Timezone
|
||||||
Determining and Conversion". If `ToTimezone` is specified, it will always be
|
Determining and Conversion". If `ToTimezone` is specified, it will always be
|
||||||
immediately left of the `Date` argument. `Date` will always be the last
|
immediately left of the `Disambiguate` argument (if it's specified), which is
|
||||||
|
always immediately left of `Date` argument. `Date` will always be the last
|
||||||
argument to any of the conversion and formatting functions.
|
argument to any of the conversion and formatting functions.
|
||||||
|
|
||||||
#### Understanding Timezone Determining and Conversions
|
#### Understanding Timezone Determining and Conversions
|
||||||
|
@ -90,9 +98,88 @@ will infer the timezone in the following order.
|
||||||
`set_timezone/1` only applies to that *specific* process. If none is
|
`set_timezone/1` only applies to that *specific* process. If none is
|
||||||
specified.
|
specified.
|
||||||
+ If no timezone is specified for the process, `qdate` looks at the `qdate`
|
+ If no timezone is specified for the process, `qdate` looks at the `qdate`
|
||||||
application variable `default_timezone`.
|
application variable `default_timezone`. `default_timezone` can be either a
|
||||||
|
hard-specified timezone, or a `{Module, Function}` tuple. The tuple format
|
||||||
|
should return either a timezone or the atom `undefined`.
|
||||||
+ If no timezone is specified by either of the above, `qdate` assumes "GMT"
|
+ If no timezone is specified by either of the above, `qdate` assumes "GMT"
|
||||||
for all dates.
|
for all dates.
|
||||||
|
+ A timezone value of `auto` will act as if no timezone is specified.
|
||||||
|
|
||||||
|
#### Disambiguating Ambiguous Timezone Conversions
|
||||||
|
|
||||||
|
Sometimes, when you're converting a datetime from one timezone to another, there
|
||||||
|
are potentially two different results if the conversion happens to land on in a
|
||||||
|
timezone that's in the middle of a Daylight Saving conversion. For example,
|
||||||
|
converting "11-Nov-2013 1:00:am" in "America/New York" to "GMT" could be both
|
||||||
|
"5am" and "6am" in GMT, since "1am EST". This is a side effect of the
|
||||||
|
"intelligence" of `qdate` - `qdate` would notice that 1am in New York is EST,
|
||||||
|
and should be converted to "1am EST", and then do the conversion from "1am EST"
|
||||||
|
to "GMT". This can lead to confusion.
|
||||||
|
|
||||||
|
Further, since `qdate` attempts to be "smart" about mistakenly entered
|
||||||
|
timezones (ie, if you entered "2013-01-01 EDT", `qdate` knows that "EDT"
|
||||||
|
(Eastern Daylight Time) doesn't apply to January first, so it *assumes* you
|
||||||
|
meant "EST".
|
||||||
|
|
||||||
|
**THE SOLUTION** to this tangled mess that we call Daylight Saving Time is to
|
||||||
|
provide an option to disambiguate if you so desire. By default disambiguation
|
||||||
|
is disabled, and `qdate` will just guess as to it's best choice. But if you so
|
||||||
|
desire, you can make sure qdate does *both* conversions, and returns both.
|
||||||
|
|
||||||
|
You can do this by passing a `Disambiguation` argument to `to_string`,
|
||||||
|
`to_date`, `to_unixtime`, and `to_now`. `Disambiguation` can be an atom of the
|
||||||
|
values:
|
||||||
|
|
||||||
|
+ `prefer_standard` *(Default Behavior)*: If an ambiguous result occurs,
|
||||||
|
qdate will return the date in standard time rather than daylight time.
|
||||||
|
+ `prefer_daylight`: If an ambiguous result occurs, qdate will return the
|
||||||
|
preferred daylight time.
|
||||||
|
+ `both`: If an ambiguous result occurs, `qdate` will return the tuple:
|
||||||
|
`{ambiguous, DateStandard, DateDaylight}`, where `DateStandard` is the date
|
||||||
|
in Standard Time, and `DateDaylight` is the date in Daylight Saving Time.
|
||||||
|
|
||||||
|
So the expanded conversions functions are:
|
||||||
|
|
||||||
|
+ `to_date(ToTimezone, Disambiguate, Date)`
|
||||||
|
+ `to_string(FormatString, ToTimezone, Disambiguate, Date)`
|
||||||
|
+ `to_unixtime(Disambiguate, Date)`
|
||||||
|
+ `to_now(Disambiguate, Date)`
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```erlang
|
||||||
|
1> qdate:set_timezone("GMT").
|
||||||
|
ok
|
||||||
|
|
||||||
|
%% Here, converting GMT 2013-11-03 6AM to America/New York yields an ambiguous
|
||||||
|
%% result
|
||||||
|
2> qdate:to_date("America/New York", both, {{2013,11,3},{6,0,0}}).
|
||||||
|
{ambiguous,{{2013,11,3},{1,0,0}},{{2013,11,3},{2,0,0}}}
|
||||||
|
|
||||||
|
%% Let's just use daylight time
|
||||||
|
3> qdate:to_date("America/New York", prefer_daylight, {{2013,11,3},{6,0,0}}).
|
||||||
|
{{2013,11,3},{2,0,0}}
|
||||||
|
|
||||||
|
%% Let's just use standard time (the default behavior)
|
||||||
|
4> qdate:to_date("America/New York", prefer_standard, {{2013,11,3},{6,0,0}}).
|
||||||
|
{{2013,11,3},{1,0,0}}
|
||||||
|
|
||||||
|
5> qdate:set_timezone("America/New York").
|
||||||
|
ok
|
||||||
|
|
||||||
|
%% Switching from 1AM Eastern Time to GMT yields a potentially ambiguous result
|
||||||
|
6> qdate:to_date("GMT", both, {{2013,11,3},{1,0,0}}).
|
||||||
|
{ambiguous,{{2013,11,3},{6,0,0}},{{2013,11,3},{5,0,0}}}
|
||||||
|
|
||||||
|
%% Use daylight time for conversion
|
||||||
|
7> qdate:to_date("GMT", prefer_daylight, {{2013,11,3},{1,0,0}}).
|
||||||
|
{{2013,11,3},{5,0,0}}
|
||||||
|
|
||||||
|
%% Here we demonstrated that even if we ask for "both", if there is no
|
||||||
|
%% ambiguity, the plain date is returned
|
||||||
|
8> qdate:to_date("GMT", both, {{2013,11,3},{5,0,0}}).
|
||||||
|
{{2013,11,3},{10,0,0}}
|
||||||
|
```
|
||||||
|
|
||||||
#### Conversion Functions provided for API compatibility with `ec_date`
|
#### Conversion Functions provided for API compatibility with `ec_date`
|
||||||
|
|
||||||
|
@ -105,22 +192,72 @@ will infer the timezone in the following order.
|
||||||
|
|
||||||
`qdate` provides a few convenience functions for performing date comparisons.
|
`qdate` provides a few convenience functions for performing date comparisons.
|
||||||
|
|
||||||
+ `compare(A, B)` - Like C's `strcmp`, returns:
|
+ `compare(A, B) -> -1|0|1` - Like C's `strcmp`, returns:
|
||||||
+ `0`: `A` and `B` are exactly the same.
|
+ `0`: `A` and `B` are exactly the same.
|
||||||
+ `-1`: `A` is less than (before) `B`.
|
+ `-1`: `A` is less than (before) `B`.
|
||||||
+ `1`: `A` is greater than (after) `B`.
|
+ `1`: `A` is greater than (after) `B`.
|
||||||
+ `compare(A, Operator, B)` - Operator is an infix comparison operator, and
|
+ `compare(A, Operator, B) -> true|false` - Operator is an infix comparison operator, and
|
||||||
the function will return true if:
|
the function will return a boolean. Will return `true` if:
|
||||||
+ `'='`, or `'=='` - `A` is the same time as `B`
|
+ `'='`, or `'=='` - `A` is the same time as `B`
|
||||||
+ `'/='`, or `'=/='` or `'!='` - `A` is not the same time as `B`
|
+ `'/='`, or `'=/='` or `'!='` - `A` is not the same time as `B`
|
||||||
+ `'<'` - `A` is before `B`
|
+ `'<'` - `A` is before `B`
|
||||||
+ `'>'` - `A` is after `B`
|
+ `'>'` - `A` is after `B`
|
||||||
+ `'=<'` or `'<='` - `A` is before or equal to `B`
|
+ `'=<'` or `'<='` - `A` is before or equal to `B`
|
||||||
+ `'>='` or `'=>'` - `A` is after or equal to `B`
|
+ `'>='` or `'=>'` - `A` is after or equal to `B`
|
||||||
|
+ `between(A, Date, B) -> true|false` - The provided `Date` is (inclusively)
|
||||||
|
between `A` and `B`. That is, `A =< Date =< B`.
|
||||||
|
+ `between(A, B) -> true|false` - shortcut for `between(A, now(), B)`
|
||||||
|
+ `between(A, Op1, Date, Op2, B) -> true|false` - the fully verbose option of
|
||||||
|
comparing between. `Op1` and `Op2` are custom operators. For example, if
|
||||||
|
you wanted to do an exclusive `between`, you can do:
|
||||||
|
`between(A, '<', Date, '<', B)`
|
||||||
|
|
||||||
**Note:** These functions will properly compare times with different timezones
|
**Note 1:** `Operator` must be an atom.
|
||||||
|
|
||||||
|
**Note 2:** These functions will properly compare times with different timezones
|
||||||
(for example: `compare("12am CST",'==',"1am EST")` will properly return true)
|
(for example: `compare("12am CST",'==',"1am EST")` will properly return true)
|
||||||
|
|
||||||
|
### Sorting
|
||||||
|
|
||||||
|
`qdate` also provides a convenience functions for sorting lists of dates/times:
|
||||||
|
|
||||||
|
+ `sort(List)` - Sort the list in ascending order of earliest to latest.
|
||||||
|
+ `sort(Op, List)` - Sort the list where `Op` is one of the following:
|
||||||
|
+ `'<'` or `'=<'` or `'<='` - Sort ascending
|
||||||
|
+ `'>'` or `'>='` or `'=>'` - Sort descending
|
||||||
|
+ `sort(Op, List, Opts)` - Sort the list according to the `Op`, with options provided in `Opts`. `Opts` is a proplist of the following options:
|
||||||
|
+ `{non_dates, NonDates}` - Tells it how to handle non-dates. `NonDates` can be any of the following:
|
||||||
|
+ `back` **(default)** - put any non-dates at the end (the back) of the list
|
||||||
|
+ `front` - put any non-dates at the beginning of the list
|
||||||
|
+ `crash` - if there are any non-dates, crash.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```erlang
|
||||||
|
1> Dates = ["non date string", <<"garbage">>,
|
||||||
|
1466200861, "2011-01-01", "7pm",
|
||||||
|
{{1999,6,21},{5,30,0}}, non_date_atom, {some_tuple,123}].
|
||||||
|
2> qdate:sort('>=', Dates, [{non_dates, front}]).
|
||||||
|
[<<"garbage">>,"non date string",
|
||||||
|
{some_tuple,123},
|
||||||
|
non_date_atom,1466200861,"2011-01-01",
|
||||||
|
{{1999,6,21},{5,30,0}},
|
||||||
|
"7pm"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note 1:** This sorting is optimized to be much faster than using a home-grown
|
||||||
|
sort using the `compare` functions, as this normalizes the items in the list
|
||||||
|
before comparing (so it's only really comparing integers, which is quite fast).
|
||||||
|
|
||||||
|
**Note 2:** This is one of the few qdate functions that don't have the "Date"
|
||||||
|
as the last argument. This follows the pattern in Erlang/OTP to put options as
|
||||||
|
the last argument (for example, `re:run/3`)
|
||||||
|
|
||||||
|
**Note 3:** You'll notice that qdate's sorting retains the original terms (in
|
||||||
|
the example above, we compared a datetime tuple, unix timestamp, and two
|
||||||
|
strings (along with a number of non-dates, which were just prepended to the
|
||||||
|
front of the list).
|
||||||
|
|
||||||
### Timezone Functions
|
### Timezone Functions
|
||||||
|
|
||||||
+ `set_timezone(Key, TZ)` - Set the timezone to TZ for the key `Key`
|
+ `set_timezone(Key, TZ)` - Set the timezone to TZ for the key `Key`
|
||||||
|
@ -156,6 +293,7 @@ be attempted before engaging the `ec_date` parser.
|
||||||
able to parse the string, then it should return `undefined`.
|
able to parse the string, then it should return `undefined`.
|
||||||
+ `deregister_parser(Key)` - If you previously registered a parser with the
|
+ `deregister_parser(Key)` - If you previously registered a parser with the
|
||||||
`qdate` server, you can deregister it by its `Key`.
|
`qdate` server, you can deregister it by its `Key`.
|
||||||
|
+ `get_parsers()` - Get the list of all registered parsers and their keys.
|
||||||
|
|
||||||
### Registering and Deregistering Formatters
|
### Registering and Deregistering Formatters
|
||||||
+ `register_format(Key, FormatString)` - Register a formatting string with
|
+ `register_format(Key, FormatString)` - Register a formatting string with
|
||||||
|
@ -163,6 +301,7 @@ be attempted before engaging the `ec_date` parser.
|
||||||
formatting string.
|
formatting string.
|
||||||
+ `deregister_format(Key)` - Deregister the formatting string from the
|
+ `deregister_format(Key)` - Deregister the formatting string from the
|
||||||
`qdate` server.
|
`qdate` server.
|
||||||
|
+ `get_formats()` - Get the list of all registered formats and their keys.
|
||||||
|
|
||||||
### About backwards compatibility with `ec_date` and deterministic parsing
|
### About backwards compatibility with `ec_date` and deterministic parsing
|
||||||
|
|
||||||
|
@ -359,8 +498,31 @@ the crash.
|
||||||
|
|
||||||
**Another Note:** Custom parsers are expected to return either:
|
**Another Note:** Custom parsers are expected to return either:
|
||||||
+ A `datetime()` tuple. (ie {{2012,12,21},{14,45,23}}).
|
+ A `datetime()` tuple. (ie {{2012,12,21},{14,45,23}}).
|
||||||
|
+ An integer, which represents the Unix timestamp.
|
||||||
+ The atom `undefined` if this parser is not a match for the supplied value
|
+ The atom `undefined` if this parser is not a match for the supplied value
|
||||||
|
|
||||||
|
#### Included Parser: Relative Times
|
||||||
|
|
||||||
|
`qdate` ships with an optional relative time parser. To speed up performance
|
||||||
|
(since this parser uses regular expressions), this parser is disabled by
|
||||||
|
default. But if you wish to use it, make sure you call
|
||||||
|
`qdate:register_parser(parse_relative, fun qdate:parse_relative/1)`.
|
||||||
|
|
||||||
|
Doing this allows you to parse relative time strings of the following formats:
|
||||||
|
|
||||||
|
+ "1 hour ago"
|
||||||
|
+ "-15 minutes"
|
||||||
|
+ "in 45 days"
|
||||||
|
+ "+2 years"
|
||||||
|
|
||||||
|
And doing so allows you to construct slightly more readable comparison calls
|
||||||
|
for sometimes common comparisons. For example, the following two calls are identical:
|
||||||
|
|
||||||
|
```erlang
|
||||||
|
qdate:between("-15 minutes", Date, "+15 minutes").
|
||||||
|
|
||||||
|
qdate:between(qdate:add_minutes(-15), Date, qdate:add_minutes(15)).
|
||||||
|
```
|
||||||
|
|
||||||
### Registering Custom Formats
|
### Registering Custom Formats
|
||||||
|
|
||||||
|
@ -374,7 +536,7 @@ the crash.
|
||||||
|
|
||||||
%% But, you don't have to: if that's a common format you use in your
|
%% But, you don't have to: if that's a common format you use in your
|
||||||
%% application, you can register your format with the `qdate` server, and then
|
%% application, you can register your format with the `qdate` server, and then
|
||||||
%% easiy refer to that format by its key.
|
%% easily refer to that format by its key.
|
||||||
|
|
||||||
%% So let's take that format and register it
|
%% So let's take that format and register it
|
||||||
16> qdate:register_format(longdate, "l, F jS, Y g:i A T").
|
16> qdate:register_format(longdate, "l, F jS, Y g:i A T").
|
||||||
|
@ -530,6 +692,175 @@ ok
|
||||||
%% that timezone to our intended timezone.
|
%% that timezone to our intended timezone.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Beginning or Ending of time periods (hours, days, years, weeks, etc)
|
||||||
|
|
||||||
|
qdate can determine beginnings and endings of time periods, like "beginning of the month"
|
||||||
|
|
||||||
|
This is abstracted to `beginning_X` functions, which return a date/time format
|
||||||
|
with the dates and times truncated to the specified level.
|
||||||
|
|
||||||
|
+ `beginning_minute(Date)`
|
||||||
|
+ `beginning_hour(Date)`
|
||||||
|
+ `beginning_day(Date)`
|
||||||
|
+ `beginning_month(Date)`
|
||||||
|
+ `beginning_year(Date)`
|
||||||
|
|
||||||
|
There are also 0-arity versions of the above, in which `Date` is assumed to be
|
||||||
|
"right now". For example, calling `qdate:beginning_month()` would return
|
||||||
|
midnight on the first day of the current month.
|
||||||
|
|
||||||
|
#### Beginning of Week
|
||||||
|
|
||||||
|
qdate can also do a special "beginning" case, particularly the "beginning of
|
||||||
|
the week" calculation. This has three forms, specifically:
|
||||||
|
|
||||||
|
+ `beginning_week()` - Returns first day of the current week.
|
||||||
|
+ `beginning_week(Date)` - Assumes the beginning of the week is Monday
|
||||||
|
(chosen because Erlang's calendar:day_of_the_week uses 1=Monday and
|
||||||
|
7=Sunday).
|
||||||
|
+ `beginning_week(DayOfWeek, Date)` - Calculates the beginning of the week
|
||||||
|
based on the provided `DayOfWeek`. Valid values for DayOfWeek are the
|
||||||
|
integers 1-7 or the atom versions of the days of the week. Specifically:
|
||||||
|
|
||||||
|
* Monday: `1 | monday | mon`
|
||||||
|
* Tuesday: `2 | tuesday | tue`
|
||||||
|
* Wednesday: `3 | wednesday | wed`
|
||||||
|
* Thursday: `4 | thursday | thu`
|
||||||
|
* Friday: `5 | friday | fri`
|
||||||
|
* Saturday: `6 | saturday | sat`
|
||||||
|
* Sunday: `7 | sunday | sun`
|
||||||
|
|
||||||
|
These all return 12am on the day that is the first day of the week of the
|
||||||
|
provided date.
|
||||||
|
|
||||||
|
(My apologies to non-English speakers. I'm a lazy American who only speaks
|
||||||
|
English, hence the Anglocentric day names).
|
||||||
|
|
||||||
|
### End of time period
|
||||||
|
|
||||||
|
There are also the related `end_X` functions available, using the same
|
||||||
|
conventions, except return the last second of that time period.
|
||||||
|
|
||||||
|
So `end_month("2016-01-05")` will return the unix timestamp representing
|
||||||
|
"2016-01-31 11:59:59pm"
|
||||||
|
|
||||||
|
|
||||||
|
## Date Arithmetic
|
||||||
|
|
||||||
|
The current implementation of qdate's date arithmetic returns Unixtimes.
|
||||||
|
|
||||||
|
There are 8 main functions for date arithmetic:
|
||||||
|
|
||||||
|
+ `add_seconds(Seconds, Date)`
|
||||||
|
+ `add_minutes(Minutes, Date)`
|
||||||
|
+ `add_hours(Hours, Date)`
|
||||||
|
+ `add_days(Days, Date)`
|
||||||
|
+ `add_weeks(Weeks, Date)`
|
||||||
|
+ `add_months(Months, Date)`
|
||||||
|
+ `add_years(Years, Date)`
|
||||||
|
+ `add_date(DateToAdd, Date)` - `DateToAdd` is a shortcut way of adding
|
||||||
|
numerous options. For example. `qdate:add_date({{1, 2, -3}, {-500, 20, 0}})`
|
||||||
|
will add 1 year, add 2 months, subtract 3 days, subtract 500 hours, add 20
|
||||||
|
minutes, and not make any changes to seconds.
|
||||||
|
|
||||||
|
For the date arithmetic functions, `Date`, like all `qdate` functions, can be any
|
||||||
|
format.
|
||||||
|
|
||||||
|
### Date Arithmetic from "now"
|
||||||
|
|
||||||
|
There are 7 other arithmetic functions that take a single argument, and these do arithmetic from "now." For example, `add_years(4)` is a shortcut for `add_years(4, os:timestamp())`.
|
||||||
|
|
||||||
|
+ `add_seconds(Seconds)`
|
||||||
|
+ `add_minutes(Minutes)`
|
||||||
|
+ `add_hours(Hours)`
|
||||||
|
+ `add_days(Days)`
|
||||||
|
+ `add_weeks(Weeks)`
|
||||||
|
+ `add_months(Months)`
|
||||||
|
+ `add_years(Years)`
|
||||||
|
|
||||||
|
## Date and Time Ranges
|
||||||
|
|
||||||
|
qdate provides a number of `range` functions that give applicable dates/times
|
||||||
|
within a start and end time. For example, "All days from 2015-01-01 to today",
|
||||||
|
"every 3rd month from 2000-01-01 to 2009-12-31", or "every 15 minutes from
|
||||||
|
midnight to 11:59pm on 2015-04-15".
|
||||||
|
|
||||||
|
The functions are as follows:
|
||||||
|
|
||||||
|
+ `range_seconds(Interval, Start, End)`
|
||||||
|
+ `range_minutes(Interval, Start, End)`
|
||||||
|
+ `range_hours(Interval, Start, End)`
|
||||||
|
+ `range_days(Interval, Start, End)`
|
||||||
|
+ `range_weeks(Interval, Start, End)`
|
||||||
|
+ `range_months(Interval, Start, End)`
|
||||||
|
+ `range_years(Interval, Start, End)`
|
||||||
|
|
||||||
|
Where `Interval` is the number of seconds/days/years/etc.
|
||||||
|
|
||||||
|
So for example:
|
||||||
|
|
||||||
|
```erlang
|
||||||
|
%% Get every 15th minute from "2015-04-15 12:00am to 2015-04-15 11:59am"
|
||||||
|
> qdate:range_minutes(15, "2015-04-15 12:00am", "2015-04-15 11:59am").
|
||||||
|
[1429056000,1429056900,1429057800,1429058700,1429059600,
|
||||||
|
1429060500,1429061400,1429062300,1429063200,1429064100,
|
||||||
|
1429065000,1429065900,1429066800,1429067700,1429068600,
|
||||||
|
1429069500,1429070400,1429071300,1429072200,1429073100,
|
||||||
|
1429074000,1429074900,1429075800,1429076700,1429077600,
|
||||||
|
1429078500,1429079400,1429080300,1429081200|...]
|
||||||
|
|
||||||
|
%% Get every day of April, 2014
|
||||||
|
> qdate:range_days(1, "2014-04-01", "2014-04-30").
|
||||||
|
[1396310400,1396396800,1396483200,1396569600,1396656000,
|
||||||
|
1396742400,1396828800,1396915200,1397001600,1397088000,
|
||||||
|
1397174400,1397260800,1397347200,1397433600,1397520000,
|
||||||
|
1397606400,1397692800,1397779200,1397865600,1397952000,
|
||||||
|
1398038400,1398124800,1398211200,1398297600,1398384000,
|
||||||
|
1398470400,1398556800,1398643200,1398729600|...]
|
||||||
|
```
|
||||||
|
|
||||||
|
Note, that the return value (just like qdate's arithmetic functions) is a list
|
||||||
|
of integers. These integers are unix timestamps and can be easily formatted
|
||||||
|
with qdate:
|
||||||
|
|
||||||
|
```erlang
|
||||||
|
> Mins = qdate:range_minutes(15, "2015-04-15 12:00am", "2015-04-15 11:59am"),
|
||||||
|
> [qdate:to_string("Y-m-d h:ia", M) || M <- Mins].
|
||||||
|
["2015-04-15 00:00am","2015-04-15 00:15am",
|
||||||
|
"2015-04-15 00:30am","2015-04-15 00:45am",
|
||||||
|
"2015-04-15 01:00am","2015-04-15 01:15am",
|
||||||
|
"2015-04-15 01:30am","2015-04-15 01:45am",
|
||||||
|
"2015-04-15 02:00am","2015-04-15 02:15am",
|
||||||
|
"2015-04-15 02:30am","2015-04-15 02:45am",
|
||||||
|
"2015-04-15 03:00am","2015-04-15 03:15am",
|
||||||
|
"2015-04-15 03:30am","2015-04-15 03:45am",
|
||||||
|
"2015-04-15 04:00am","2015-04-15 04:15am",
|
||||||
|
"2015-04-15 04:30am","2015-04-15 04:45am",
|
||||||
|
"2015-04-15 05:00am","2015-04-15 05:15am",
|
||||||
|
"2015-04-15 05:30am","2015-04-15 05:45am",
|
||||||
|
"2015-04-15 06:00am","2015-04-15 06:15am",
|
||||||
|
"2015-04-15 06:30am","2015-04-15 06:45am",
|
||||||
|
[...]|...]
|
||||||
|
```
|
||||||
|
|
||||||
|
Also note that the range functions are *inclusive*.
|
||||||
|
|
||||||
|
|
||||||
|
## Age Comparison
|
||||||
|
|
||||||
|
There are two main comparisons right now, age in years, and age in days.
|
||||||
|
|
||||||
|
+ `age(Date)` - Number of years since `Date`
|
||||||
|
+ `age(FromDate, ToDate)` - Number of years between `FromDate` to `ToDate`
|
||||||
|
+ `age_days(Date)` - Number of full days since `Date` (for example from `2pm` yesterday to `1:59pm` today is still 0.
|
||||||
|
+ `age_days(FromDate, ToDate)` - Number of full days between `FromDate` and `ToDate`.
|
||||||
|
|
||||||
|
## Configuration Sample
|
||||||
|
|
||||||
|
There is a sample configuration file can be found in the root of the qdate
|
||||||
|
directory. Or you can just [look at it
|
||||||
|
here](https://github.com/choptastic/qdate/blob/master/qdate.config).
|
||||||
|
|
||||||
## Thanks
|
## Thanks
|
||||||
|
|
||||||
A few shoutouts to [Dale Harvey](http://github.com/daleharvey) and the
|
A few shoutouts to [Dale Harvey](http://github.com/daleharvey) and the
|
||||||
|
@ -538,12 +869,24 @@ A few shoutouts to [Dale Harvey](http://github.com/daleharvey) and the
|
||||||
package. Without the hard work of all involved in those projects, `qdate` would
|
package. Without the hard work of all involved in those projects, `qdate` would
|
||||||
not exist.
|
not exist.
|
||||||
|
|
||||||
|
### Thanks to Additional Contributors
|
||||||
|
|
||||||
|
+ [Mark Allen](https://github.com/mrallen1)
|
||||||
|
+ [Christopher Phillips](https://github.com/lostcolony)
|
||||||
|
+ [Nicholas Lundgaard](https://github.com/nlundgaard-al)
|
||||||
|
+ [Alejandro Ramallo](https://github.com/aramallo)
|
||||||
|
+ [Heinz Gies](https://github.com/Licenser)
|
||||||
|
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
See [CHANGELOG.markdown](https://github.com/choptastic/qdate/blob/master/CHANGELOG.markdown)
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
+ Make `qdate` backend-agnostic (allow specifying either ec_date or dh_date as
|
+ Make `qdate` backend-agnostic (allow specifying either ec_date or dh_date as
|
||||||
the backend)
|
the backend)
|
||||||
+ Add `-spec` and `-type` info for dialyzer
|
+ Add `-spec` and `-type` info for dialyzer
|
||||||
+ Add date and time arithmetic.
|
|
||||||
+ Research the viability of [ezic](https://github.com/drfloob/ezic) for a
|
+ Research the viability of [ezic](https://github.com/drfloob/ezic) for a
|
||||||
timezone backend replacement for `erlang_localtime`.
|
timezone backend replacement for `erlang_localtime`.
|
||||||
|
|
20
qdate.config
Normal file
20
qdate.config
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
%% vim: ts=4 sw=4 et ft=erlang
|
||||||
|
[{qdate, [
|
||||||
|
%% default_timezone can be one of two things:
|
||||||
|
%% 1) An actual timezone. Either short-form like "GMT", "UTC", or a
|
||||||
|
%% longer-form (but more likely to pick the correct daylight saving
|
||||||
|
%% config timezone like "America/Chicago"
|
||||||
|
%% 2) A 2-tuple of {Module, Function}, which will be called as
|
||||||
|
%% Module:Function() to determine the timezone (say you wanted to
|
||||||
|
%% determine timezone based on some kind of environmental conditions)
|
||||||
|
{default_timezone, "GMT"},
|
||||||
|
|
||||||
|
%% See readme section here:
|
||||||
|
%% https://github.com/choptastic/qdate#about-backwards-compatibility-with-ec_date-and-deterministic-parsing
|
||||||
|
{deterministic_parsing, {zero, zero}},
|
||||||
|
|
||||||
|
%% Preserve Milliseconds in parsing
|
||||||
|
%% Changes the return for qdate:to_date to include any Ms value
|
||||||
|
%% and also changes the return of to_string to include the Ms value
|
||||||
|
{preserve_ms, false}
|
||||||
|
]}].
|
BIN
rebar
vendored
BIN
rebar
vendored
Binary file not shown.
31
rebar.config
31
rebar.config
|
@ -1,13 +1,24 @@
|
||||||
% vim:ts=4 sw=4 et ft=erlang
|
%% -*- erlang -*-
|
||||||
{require_otp_vsn, "R13B04|R14|R15|R16"}.
|
%% vim:ts=4 sw=4 et ft=erlang
|
||||||
|
|
||||||
{cover_enabled, true}.
|
{cover_enabled, true}.
|
||||||
|
|
||||||
%{erl_opts, [debug_info,{i,"site/include"}]}.
|
{dialyzer, [
|
||||||
|
{exclude_apps, []},
|
||||||
{deps_dir, ["deps"]}.
|
{warnings, []}
|
||||||
|
|
||||||
{deps, [
|
|
||||||
{erlware_commons, ".*", {git, "git://github.com/erlware/erlware_commons.git", "HEAD"}},
|
|
||||||
{erlang_localtime, ".*", {git, "git://github.com/choptastic/erlang_localtime.git", "HEAD"}}
|
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
|
{deps,
|
||||||
|
[
|
||||||
|
erlware_commons,
|
||||||
|
{qdate_localtime, "~> 1.2.0"}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{project_plugins, [rebar3_ex_doc]}.
|
||||||
|
|
||||||
|
{hex, [{doc, ex_doc}]}.
|
||||||
|
|
||||||
|
{ex_doc, [
|
||||||
|
{source_url, <<"https://github.com/choptastic/qdate">>},
|
||||||
|
{extras, [<<"README.md">>, <<"LICENSE.md">>]},
|
||||||
|
{main, <<"readme">>}]}.
|
||||||
|
|
||||||
|
|
15
rebar.config.script
Normal file
15
rebar.config.script
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
%% -*- mode: erlang -*-
|
||||||
|
%% -*- tab-width: 4;erlang-indent-level: 4;indent-tabs-mode: nil -*-
|
||||||
|
%% ex: ts=4 sw=4 sts ft=erlang et
|
||||||
|
|
||||||
|
case erlang:function_exported(rebar3, main, 1) of
|
||||||
|
true -> % rebar3
|
||||||
|
CONFIG;
|
||||||
|
false -> % rebar 2.x or older
|
||||||
|
%% Rebuild deps, possibly including those that have been moved to
|
||||||
|
%% profiles
|
||||||
|
[{deps, [
|
||||||
|
{erlware_commons, "", {git, "https://github.com/erlware/erlware_commons", {tag, "v1.5.0"}}}, %% this is the version of erlware_commons that works until erlware tags a new version
|
||||||
|
{qdate_localtime, "", {git, "https://github.com/choptastic/qdate_localtime", {tag, "1.1.0"}}}
|
||||||
|
]} | lists:keydelete(deps, 1, CONFIG)]
|
||||||
|
end.
|
14
rebar.lock
Normal file
14
rebar.lock
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{"1.2.0",
|
||||||
|
[{<<"cf">>,{pkg,<<"cf">>,<<"0.3.1">>},1},
|
||||||
|
{<<"erlware_commons">>,{pkg,<<"erlware_commons">>,<<"1.6.0">>},0},
|
||||||
|
{<<"qdate_localtime">>,{pkg,<<"qdate_localtime">>,<<"1.2.1">>},0}]}.
|
||||||
|
[
|
||||||
|
{pkg_hash,[
|
||||||
|
{<<"cf">>, <<"5CB902239476E141EA70A740340233782D363A31EEA8AD37049561542E6CD641">>},
|
||||||
|
{<<"erlware_commons">>, <<"E0DA62F91E65DEFAE28BB450DDC3C24E85ECB99B926F857CDC6ED8E7DD7076B4">>},
|
||||||
|
{<<"qdate_localtime">>, <<"72E1034DC6B7FEE8F588281EDDD0BD0DC5260005D758052F50634D265D382C18">>}]},
|
||||||
|
{pkg_hash_ext,[
|
||||||
|
{<<"cf">>, <<"315E8D447D3A4B02BCDBFA397AD03BBB988A6E0AA6F44D3ADD0F4E3C3BF97672">>},
|
||||||
|
{<<"erlware_commons">>, <<"B57C299C39A2992A8C413F9D2C1B8A20061310FB4432B0EEE5E4C4DF7AAA0AA3">>},
|
||||||
|
{<<"qdate_localtime">>, <<"1109958D205C65C595C8C5694CB83EBAF2DBE770CF902E4DCE8AFB2C4123764D">>}]}
|
||||||
|
].
|
|
@ -1,12 +1,17 @@
|
||||||
{application, qdate,
|
{application, qdate,
|
||||||
[
|
[
|
||||||
{description, "Simple Date and Timezone handling for Erlang"},
|
{description, "Simple Date and Timezone handling for Erlang"},
|
||||||
{vsn, "0.2.0"},
|
{vsn, git},
|
||||||
{registered, []},
|
{registered, []},
|
||||||
{applications, [
|
{applications, [
|
||||||
kernel,
|
kernel,
|
||||||
stdlib
|
stdlib,
|
||||||
|
qdate_localtime,
|
||||||
|
erlware_commons
|
||||||
]},
|
]},
|
||||||
{mod, { qdate_app, []}},
|
{modules, [qdate, qdate_srv, qdate_sup, qdate_app]},
|
||||||
{env, []}
|
{env, []},
|
||||||
|
{licenses, ["MIT"]},
|
||||||
|
{mod, {qdate_app, []}},
|
||||||
|
{links, [{"Github", "https://github.com/choptastic/qdate"}]}
|
||||||
]}.
|
]}.
|
||||||
|
|
1104
src/qdate.erl
1104
src/qdate.erl
File diff suppressed because it is too large
Load diff
|
@ -1,7 +1,3 @@
|
||||||
% vim: ts=4 sw=4 et
|
|
||||||
% Copyright (c) 2013 Jesse Gumm
|
|
||||||
% See LICENSE for licensing information.
|
|
||||||
|
|
||||||
-module(qdate_app).
|
-module(qdate_app).
|
||||||
|
|
||||||
-behaviour(application).
|
-behaviour(application).
|
||||||
|
@ -9,12 +5,11 @@
|
||||||
%% Application callbacks
|
%% Application callbacks
|
||||||
-export([start/2, stop/1]).
|
-export([start/2, stop/1]).
|
||||||
|
|
||||||
%% ===================================================================
|
|
||||||
%% Application callbacks
|
|
||||||
%% ===================================================================
|
|
||||||
|
|
||||||
start(_StartType, _StartArgs) ->
|
start(_StartType, _StartArgs) ->
|
||||||
qdate_sup:start_link().
|
qdate_sup:start_link().
|
||||||
|
|
||||||
|
|
||||||
stop(_State) ->
|
stop(_State) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,10 @@
|
||||||
% vim: ts=4 sw=4 et
|
% vim: ts=4 sw=4 et
|
||||||
% Copyright (c) 2013 Jesse Gumm
|
% Copyright (c) 2013-2021 Jesse Gumm
|
||||||
% See LICENSE for licensing information.
|
% See LICENSE for licensing information.
|
||||||
|
|
||||||
-module(qdate_srv).
|
-module(qdate_srv).
|
||||||
-behaviour(gen_server).
|
-behaviour(gen_server).
|
||||||
|
|
||||||
-define(SRV, ?MODULE).
|
|
||||||
|
|
||||||
-export([
|
|
||||||
start_link/0,
|
|
||||||
init/1,
|
|
||||||
handle_call/3,
|
|
||||||
handle_cast/2,
|
|
||||||
handle_info/2,
|
|
||||||
code_change/3,
|
|
||||||
terminate/2
|
|
||||||
]).
|
|
||||||
|
|
||||||
-export([
|
-export([
|
||||||
set_timezone/1,
|
set_timezone/1,
|
||||||
set_timezone/2,
|
set_timezone/2,
|
||||||
|
@ -33,133 +21,172 @@
|
||||||
|
|
||||||
register_format/2,
|
register_format/2,
|
||||||
get_format/1,
|
get_format/1,
|
||||||
deregister_format/1
|
deregister_format/1,
|
||||||
|
get_formats/0
|
||||||
]).
|
]).
|
||||||
|
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([start_link/0]).
|
||||||
|
|
||||||
|
%% gen_server callbacks
|
||||||
|
-export([init/1,
|
||||||
|
handle_call/3,
|
||||||
|
handle_cast/2,
|
||||||
|
handle_info/2,
|
||||||
|
terminate/2,
|
||||||
|
code_change/3]).
|
||||||
|
|
||||||
|
|
||||||
|
%% Simple wrappers for unique keys
|
||||||
|
-define(BASETAG, qdate_var).
|
||||||
|
-define(KEY(Name), {?BASETAG, Name}).
|
||||||
|
|
||||||
|
-define(TZTAG, qdate_tz).
|
||||||
|
-define(TZKEY(Name), {?TZTAG, Name}).
|
||||||
|
-define(PARSERTAG, qdate_parser).
|
||||||
|
-define(PARSERKEY(Name), {?PARSERTAG, Name}).
|
||||||
|
-define(FORMATTAG, qdate_format).
|
||||||
|
-define(FORMATKEY(Name), {?FORMATTAG, Name}).
|
||||||
|
|
||||||
|
-define(SERVER, ?MODULE).
|
||||||
|
-define(TABLE, ?MODULE).
|
||||||
|
|
||||||
|
-record(state, {}).
|
||||||
|
|
||||||
|
start_link() ->
|
||||||
|
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
|
||||||
|
|
||||||
|
init([]) ->
|
||||||
|
error_logger:info_msg("Creating qdate ETS Table: ~p",[?TABLE]),
|
||||||
|
?TABLE = ets:new(?TABLE, [public, {read_concurrency, true}, named_table]),
|
||||||
|
{ok, #state{}}.
|
||||||
|
|
||||||
|
handle_call({set, Key, Val}, _From, State) ->
|
||||||
|
ets:insert(?TABLE, {Key, Val}),
|
||||||
|
{reply, ok, State};
|
||||||
|
handle_call({unset, Key}, _From, State) ->
|
||||||
|
ets:delete(?TABLE, Key),
|
||||||
|
{reply, ok, State};
|
||||||
|
handle_call(_, _From, State) ->
|
||||||
|
{reply, invalid_request, State}.
|
||||||
|
|
||||||
|
handle_cast(_Msg, State) ->
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
handle_info(_Info, State) ->
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
terminate(_Reason, _State) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
code_change(_OldVsn, State, _Extra) ->
|
||||||
|
{ok, State}.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
%% PUBLIC API FUNCTIONS
|
%% PUBLIC API FUNCTIONS
|
||||||
|
|
||||||
start_link() ->
|
|
||||||
gen_server:start_link({local, ?SRV}, ?MODULE, [], []).
|
|
||||||
|
|
||||||
set_timezone(TZ) ->
|
set_timezone(TZ) ->
|
||||||
set_timezone(self(),TZ).
|
put_pd(?TZTAG, TZ).
|
||||||
|
|
||||||
set_timezone(Key,TZ) ->
|
set_timezone(Key, TZ) ->
|
||||||
ok = gen_server:call(?SRV,{set_timezone,Key,TZ}).
|
set_env(?TZKEY(Key), TZ).
|
||||||
|
|
||||||
get_timezone() ->
|
get_timezone() ->
|
||||||
get_timezone(self()).
|
get_pd(?TZTAG).
|
||||||
|
|
||||||
get_timezone(Key) ->
|
get_timezone(Key) ->
|
||||||
gen_server:call(?SRV,{get_timezone,Key}).
|
get_env(?TZKEY(Key)).
|
||||||
|
|
||||||
clear_timezone() ->
|
clear_timezone() ->
|
||||||
clear_timezone(self()).
|
unset_pd(?TZTAG).
|
||||||
|
|
||||||
clear_timezone(Key) ->
|
clear_timezone(Key) ->
|
||||||
ok = gen_server:call(?SRV, {clear_timezone, Key}).
|
unset_env(?TZKEY(Key)).
|
||||||
|
|
||||||
register_parser(Parser) when is_function(Parser,1) ->
|
register_parser(Parser) when is_function(Parser,1) ->
|
||||||
register_parser(erlang:make_ref(),Parser).
|
register_parser(erlang:make_ref(),Parser).
|
||||||
|
|
||||||
register_parser(Key,Parser) when is_function(Parser,1) ->
|
register_parser(Key,Parser) when is_function(Parser,1) ->
|
||||||
Key = gen_server:call(?SRV,{register_parser,Key,Parser}).
|
set_env(?PARSERKEY(Key), Parser).
|
||||||
|
|
||||||
deregister_parser(Key) ->
|
deregister_parser(Key) ->
|
||||||
ok = gen_server:call(?SRV,{deregister_parser,Key}).
|
unset_env(?PARSERKEY(Key)).
|
||||||
|
|
||||||
deregister_parsers() ->
|
deregister_parsers() ->
|
||||||
ok = gen_server:call(?SRV,{deregister_parsers}).
|
[deregister_parser(Key) || {Key, _} <- get_parsers()].
|
||||||
|
|
||||||
get_parsers() ->
|
get_parsers() ->
|
||||||
gen_server:call(?SRV,{get_parsers}).
|
get_all_env(?PARSERTAG).
|
||||||
|
|
||||||
register_format(Key,Format) ->
|
register_format(Key, Format) ->
|
||||||
ok = gen_server:call(?SRV,{register_format,Key,Format}).
|
set_env(?FORMATKEY(Key), Format).
|
||||||
|
|
||||||
get_format(Key) ->
|
get_format(Key) ->
|
||||||
gen_server:call(?SRV,{get_format,Key}).
|
get_env(?FORMATKEY(Key)).
|
||||||
|
|
||||||
deregister_format(Key) ->
|
deregister_format(Key) ->
|
||||||
ok = gen_server:call(?SRV,{deregister_format,Key}).
|
unset_env(?FORMATKEY(Key)).
|
||||||
|
|
||||||
|
get_formats() ->
|
||||||
%% SERVER FUNCTIONS
|
get_all_env(?FORMATTAG).
|
||||||
|
|
||||||
-record(state, {tz, parsers, formats}).
|
|
||||||
|
|
||||||
init(_) ->
|
|
||||||
State = #state{tz=dict:new(),parsers=dict:new(),formats=dict:new()},
|
|
||||||
{ok, State}.
|
|
||||||
|
|
||||||
handle_cast(_,State) ->
|
|
||||||
{noreply, State}.
|
|
||||||
|
|
||||||
handle_info({'DOWN', MonitorRef, process, Pid, _Reason}, State) ->
|
|
||||||
erlang:demonitor(MonitorRef),
|
|
||||||
NewTZ = dict:erase(Pid, State#state.tz),
|
|
||||||
NewParsers = dict:erase(Pid, State#state.parsers),
|
|
||||||
NewFormats = dict:erase(Pid, State#state.formats),
|
|
||||||
NewState = State#state{tz=NewTZ, parsers=NewParsers, formats=NewFormats},
|
|
||||||
{noreply, NewState };
|
|
||||||
handle_info(_, State) ->
|
|
||||||
{noreply, State}.
|
|
||||||
|
|
||||||
handle_call({set_timezone,Key,TZ}, _From, State) ->
|
|
||||||
monitor_if_pid(Key),
|
|
||||||
NewTZ = dict:store(Key, TZ, State#state.tz),
|
|
||||||
NewState = State#state{tz=NewTZ},
|
|
||||||
{reply, ok, NewState};
|
|
||||||
handle_call({clear_timezone,Key},_From, State) ->
|
|
||||||
NewTZ = dict:erase(Key, State#state.tz),
|
|
||||||
NewState = State#state{tz=NewTZ},
|
|
||||||
{reply, ok, NewState};
|
|
||||||
handle_call({get_timezone,Key},_From, State) ->
|
|
||||||
Reply = case dict:find(Key, State#state.tz) of
|
|
||||||
error -> undefined;
|
|
||||||
{ok,TZ} -> TZ
|
|
||||||
end,
|
|
||||||
{reply, Reply, State};
|
|
||||||
|
|
||||||
handle_call({register_parser,Key,Parser},_From,State) ->
|
|
||||||
NewParsers = dict:store(Key, Parser, State#state.parsers),
|
|
||||||
NewState = State#state{parsers=NewParsers},
|
|
||||||
{reply, Key, NewState};
|
|
||||||
handle_call({get_parsers},_From,State) ->
|
|
||||||
Reply = dict:to_list(State#state.parsers),
|
|
||||||
{reply, Reply, State};
|
|
||||||
handle_call({deregister_parser,Key},_From,State) ->
|
|
||||||
NewParsers = dict:erase(Key, State#state.parsers),
|
|
||||||
NewState = State#state{parsers=NewParsers},
|
|
||||||
{reply, ok, NewState};
|
|
||||||
handle_call({deregister_parsers},_From,State) ->
|
|
||||||
NewState = State#state{parsers=dict:new()},
|
|
||||||
{reply, ok, NewState};
|
|
||||||
|
|
||||||
handle_call({register_format,Key,Format},_From,State) ->
|
|
||||||
NewFormats = dict:store(Key, Format, State#state.formats),
|
|
||||||
NewState = State#state{formats=NewFormats},
|
|
||||||
{reply, ok, NewState};
|
|
||||||
handle_call({get_format,Key},_From,State) ->
|
|
||||||
Reply = case dict:find(Key, State#state.formats) of
|
|
||||||
error -> undefined;
|
|
||||||
{ok, Format} -> Format
|
|
||||||
end,
|
|
||||||
{reply, Reply,State};
|
|
||||||
handle_call({deregister_format,Key},_From,State) ->
|
|
||||||
NewFormats = dict:erase(Key, State#state.formats),
|
|
||||||
NewState = State#state{formats=NewFormats},
|
|
||||||
{reply, ok, NewState}.
|
|
||||||
|
|
||||||
terminate(_Reason, _State) ->
|
|
||||||
ok.
|
|
||||||
|
|
||||||
code_change(_OldVersion, State, _Extra) ->
|
|
||||||
{ok, State}.
|
|
||||||
|
|
||||||
%% PRIVATE TOOLS
|
%% PRIVATE TOOLS
|
||||||
|
|
||||||
monitor_if_pid(Key) when is_pid(Key) ->
|
%% App Vars
|
||||||
erlang:monitor(process,Key);
|
|
||||||
monitor_if_pid(_) ->
|
set_env(Key, Val) ->
|
||||||
do_nothing.
|
gen_server:call(?SERVER, {set, ?KEY(Key), Val}).
|
||||||
|
|
||||||
|
get_env(Key) ->
|
||||||
|
get_env(Key, undefined).
|
||||||
|
|
||||||
|
get_env(Key, Default) ->
|
||||||
|
case ets:lookup(?TABLE, ?KEY(Key)) of
|
||||||
|
[{__Key, Val}] -> Val;
|
||||||
|
[] -> Default
|
||||||
|
end.
|
||||||
|
|
||||||
|
unset_env(Key) ->
|
||||||
|
gen_server:call(?SERVER, {unset, ?KEY(Key)}).
|
||||||
|
|
||||||
|
get_all_env(FilterTag) ->
|
||||||
|
try ets:tab2list(?TABLE) of
|
||||||
|
All ->
|
||||||
|
[{Key, V} || {{?BASETAG, {Tag, Key}}, V} <- All, Tag==FilterTag]
|
||||||
|
catch
|
||||||
|
error:badarg:S ->
|
||||||
|
Msg = maybe_start_msg(),
|
||||||
|
logger:error(Msg),
|
||||||
|
|
||||||
|
error({qdate_get_all_env_failed, #{
|
||||||
|
original_error => {error, badarg, S}
|
||||||
|
}})
|
||||||
|
end.
|
||||||
|
|
||||||
|
maybe_start_msg() ->
|
||||||
|
"Attempting to read qdate environment failed (qdate_srv:get_all_env/1).\n"
|
||||||
|
"qdate may not have been started properly.\n"
|
||||||
|
"Please ensure qdate is started properly by either:\n"
|
||||||
|
"* Putting qdate in the 'applications' key in your .app.src or .app file, or\n"
|
||||||
|
"* Starting it manually with application:ensure_all_started(qdate) or qdate:start().".
|
||||||
|
|
||||||
|
|
||||||
|
%% ProcDic Vars
|
||||||
|
|
||||||
|
get_pd(Key) ->
|
||||||
|
erlang:get(?KEY(Key)).
|
||||||
|
|
||||||
|
put_pd(Key, Val) ->
|
||||||
|
erlang:put(?KEY(Key), Val),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
unset_pd(Key) ->
|
||||||
|
put_pd(Key, undefined).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
% vim: ts=4 sw=4 et
|
|
||||||
% Copyright (c) 2013 Jesse Gumm
|
|
||||||
% See LICENSE for licensing information.
|
|
||||||
|
|
||||||
-module(qdate_sup).
|
-module(qdate_sup).
|
||||||
|
|
||||||
-behaviour(supervisor).
|
-behaviour(supervisor).
|
||||||
|
@ -12,21 +8,25 @@
|
||||||
%% Supervisor callbacks
|
%% Supervisor callbacks
|
||||||
-export([init/1]).
|
-export([init/1]).
|
||||||
|
|
||||||
%% Helper macro for declaring children of supervisor
|
-define(SERVER, ?MODULE).
|
||||||
-define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}).
|
|
||||||
|
|
||||||
%% ===================================================================
|
|
||||||
%% API functions
|
|
||||||
%% ===================================================================
|
|
||||||
|
|
||||||
start_link() ->
|
start_link() ->
|
||||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
|
||||||
|
|
||||||
%% ===================================================================
|
|
||||||
%% Supervisor callbacks
|
|
||||||
%% ===================================================================
|
|
||||||
|
|
||||||
init([]) ->
|
init([]) ->
|
||||||
Server = ?CHILD(qdate_srv, worker),
|
|
||||||
{ok, { {one_for_one, 5, 10}, [Server]} }.
|
SupFlags = #{},
|
||||||
|
|
||||||
|
ChildSpec = #{
|
||||||
|
id=>qdate_srv,
|
||||||
|
start=>{qdate_srv, start_link, []}
|
||||||
|
},
|
||||||
|
|
||||||
|
{ok, {SupFlags, [ChildSpec]}}.
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% Internal functions
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue