Compare commits

..

138 commits

Author SHA1 Message Date
Jesse Gumm
108c796687 Remove travis and replace badge with GH actions
Some checks failed
qdate tests and dialyzer / OTP 23.x (push) Has been cancelled
qdate tests and dialyzer / OTP 24.x (push) Has been cancelled
qdate tests and dialyzer / OTP 25.x (push) Has been cancelled
qdate tests and dialyzer / OTP 26.x (push) Has been cancelled
qdate tests and dialyzer / OTP 27.x (push) Has been cancelled
2024-09-05 15:04:50 -05:00
Jesse Gumm
43c1f3412b Update to checkout@v4 2024-09-05 15:01:11 -05:00
Jesse Gumm
dc2184ea06 Remove reference to Postgres/MySQL (GH actions config was largely copied from sql_bridge) 2024-09-05 14:56:59 -05:00
Jesse Gumm
70bab7df95 Updates to fix dialyzer failing on otp 26+ 2024-09-05 14:55:48 -05:00
Jesse Gumm
06347e7f31 Add github actions workflow 2024-09-05 14:35:38 -05:00
Jesse Gumm
9d0fb6d895 Remove ?else macro 2024-09-05 08:36:05 -05:00
Jesse Gumm
2072b49220 Update changelog - remove rebar3 binary 2024-01-09 19:18:29 -06:00
Jesse Gumm
cb873afb22 Update rebar.lock 2024-01-09 19:10:19 -06:00
Jesse Gumm
c109e7969b Rework the filenames because hex doesn't like them 2024-01-09 19:06:22 -06:00
Jesse Gumm
2a20cdea1e Update rebar.config wiht some hex stuff 2024-01-09 19:04:33 -06:00
Jesse Gumm
ea203ec90d Add some tag rules to makefile 2024-01-09 19:01:14 -06:00
Jesse Gumm
5a847014c9 Make qdate's version in app.src track the git tags 2024-01-09 18:59:58 -06:00
Jesse Gumm
794d03a5ee Produce a better error message if qdate hasn't been started 2024-01-09 18:58:02 -06:00
Jesse Gumm
817c14c46e Update copyright date comment in qdate.erl 2023-08-22 09:11:48 -05:00
Jesse Gumm
280f8e72f2 Increment version 2023-08-12 19:46:32 -05:00
Jesse Gumm
5afd1335f1 Add a make dev for easily working with qdate_localtime 2023-08-12 19:44:13 -05:00
Jesse Gumm
2ce94f82ce Fix dialyzer bugs 2023-08-12 19:43:46 -05:00
Jesse Gumm
5a352dc599 Fix the unnused erlnow() type 2023-08-12 10:40:13 -05:00
Jesse Gumm
357b5844eb
Merge pull request #42 from kianmeng/fix-typos
Fix typos
2022-01-28 08:33:04 -06:00
Kian-Meng Ang
d87d7d8b6c Fix typos 2022-01-28 08:17:41 +08:00
Jesse Gumm
92e20c474f
git: -> https: 2022-01-11 11:48:45 -06:00
Jesse Gumm
c5971cdcf4 update rebar3 2021-07-01 09:24:18 -05:00
Jesse Gumm
6d6b6ec9bd Update changelog and makefile 2021-07-01 08:43:18 -05:00
Jesse Gumm
c789fca51f Update to 0.7.0 2021-06-30 21:11:11 -05:00
Jesse Gumm
d6a431362d More dialyzer updates 2021-06-30 18:03:55 -05:00
Jesse Gumm
bbfef69d2e Convert qdate_srv to be an ETS server (dialyzer doesn't like using tuples as keys for application:set_env) 2021-06-30 14:44:45 -05:00
Jesse Gumm
3d72448491 bump to version 0.6.0 2021-06-01 08:35:13 -05:00
Jesse Gumm
596b573775 Erlware Commons => 1.5.0 for rebar2 2021-06-01 08:28:15 -05:00
Jesse Gumm
d0b6fc0011 fix error warning about _SameDay bound twice 2021-06-01 08:25:00 -05:00
Jesse Gumm
d159878f62 update rebar3 version 2021-06-01 08:21:55 -05:00
Jesse Gumm
69d7fdd626
Merge pull request #41 from SiftLogic/fix/preserve-millsec
Fix/preserve millsec
2021-05-03 14:28:56 -05:00
Leonard Boyce
f49368d3fc (feat) Support preserving Millisec in date parsing and formatting 2021-05-03 10:26:56 -04:00
Leonard Boyce
22b48f90de (chore) Ignore editor artifacts 2021-05-03 09:46:08 -04:00
Jesse Gumm
0a7d808290 Add age functions 2020-11-20 14:14:25 -06:00
Jesse Gumm
2f3afd7dbb Travis doesn't support 23.1 yet, so don't test with that 2020-11-15 08:37:52 -06:00
Jesse Gumm
68b462469f travis doesn't support 23.1 yet, so just do 23.0 until then 2020-11-15 01:09:31 -06:00
Jesse Gumm
1c405bbfc3 update travis for 23 2020-11-15 01:09:02 -06:00
Jesse Gumm
accda0db1a Just get rid of the old erlang versions 2019-08-21 13:21:57 -05:00
Jesse Gumm
28847aa896 Use local rebar3 only if there isn't one installed 2019-08-21 11:59:43 -05:00
Jesse Gumm
a0b9ad0e5f contributors=>maintainers for hex 2019-08-21 11:32:35 -05:00
Jesse Gumm
73d9706c7a travis tweaks 2019-08-21 11:30:46 -05:00
Jesse Gumm
b8a5075026 Version => 0.5.0 2019-08-21 11:23:32 -05:00
Jesse Gumm
ef98ff4ea4 Update changelog 2019-08-21 11:21:37 -05:00
Jesse Gumm
08a98032e2 Update changelog. Update rebar version 2019-08-21 11:19:18 -05:00
Jesse Gumm
a443d27c79 Update travis 2019-08-21 11:08:05 -05:00
Jesse Gumm
ed00a606f3 Add more versions for travis testing 2018-12-31 10:58:46 -06:00
Jesse Gumm
3448a6f5e0
Merge pull request #39 from tnt-dev/update-erlware_commons-url
Use actual erlware_commons for rebar 2
2018-12-31 10:35:00 -06:00
Jesse Gumm
b849ebde02
Merge pull request #38 from tnt-dev/fix-stacktrace
Stacktrace workaround for compatibility with OTP 21
2018-12-29 19:53:48 -06:00
Pavel Abalikhin
a51edf2e3e Use actual erlware_commons for rebar 2 2018-12-24 14:16:32 +03:00
Pavel Abalikhin
7314545b34 Stacktrace workaround for compatibility with OTP 21 2018-12-24 13:01:41 +03:00
Jesse Gumm
fa59231e1e
Merge pull request #37 from loudferret/iss36-UTC_problem_with_time_shift
fixed issue https://github.com/choptastic/qdate/issues/36
2018-09-18 13:00:00 -05:00
LoudFerret
67ee655bbb fixed issue https://github.com/choptastic/qdate/issues/36 2018-08-07 20:07:51 +02:00
Jesse Gumm
1cccb8392a Add support for named days of the week in beginning_week 2018-02-04 12:21:47 -06:00
Jesse Gumm
628c87557a Removing io:format left over from debuggin 2018-01-06 12:07:39 -06:00
Jesse Gumm
dd38e5cf98 Unexport fix_year_month. Was for testing only. 2018-01-06 12:05:08 -06:00
Jesse Gumm
99e55979dc Fix add_month(-12, January). Add more tests. Fixes #35 2018-01-06 12:03:26 -06:00
Jesse Gumm
4c91fc4c01
Merge pull request #34 from tnt-dev/fix-floor
Rename floor/1 to flooring/1
2017-11-22 05:35:24 -06:00
Pavel Abalikhin
31e123d9f1 Rename floor/1 to flooring/1
Fix for Erlang/OTP 20
2017-11-22 12:33:15 +03:00
Jesse Gumm
bab222aeb4 Fix adding months to date that ends in dec following year 2017-09-29 15:18:02 -05:00
Jesse Gumm
fc247e296b Update rebar2 to use working erlware_commons 2017-03-18 17:43:30 -05:00
Jesse Gumm
3e2688cbec Update changelog 2017-01-04 19:23:55 -06:00
Jesse Gumm
aa114b1ef8 Add end_week/[0-2] and beginning_week/0 2017-01-04 19:22:21 -06:00
Jesse Gumm
c42272a9eb Update changelog 2017-01-04 15:59:37 -06:00
Jesse Gumm
6fadc548d2 Update readme. Add some sanity guards on beginning_week 2017-01-04 15:58:27 -06:00
Jesse Gumm
d7267a7d67 Add sunday test to beginning_week 2017-01-04 15:53:16 -06:00
Jesse Gumm
ef5c9cfbb6 Add beginning_week/[1-2] 2017-01-04 15:45:33 -06:00
Jesse Gumm
0d5cd470e6 Merge pull request #25 from LIB53/master
Use qdate_localtime in app.src
2016-07-14 15:48:55 -05:00
LIB53
af9cb4dc4b Use qdate_localtime in app.src 2016-07-14 15:41:48 -05:00
Jesse Gumm
78bb08c4f8 Use the qdate_localtime hex package for rebar3 2016-07-07 16:15:39 -05:00
Jesse Gumm
5a3929bcea Update travis 2016-07-06 20:02:42 -05:00
Jesse Gumm
89555d25ef Rebar3 built with R15B03, and also use hex package for erlware_commons 2016-07-06 19:59:18 -05:00
Jesse Gumm
fd493ecf3d Reflect the qdate_localtime in rebar.script 2016-07-06 17:40:16 -05:00
Jesse Gumm
b193876305 Update rebar3, use qdate_localtime 2016-07-06 17:39:00 -05:00
Jesse Gumm
10d56c2e04 Add another note to sort/[1-3] 2016-06-17 17:14:46 -05:00
Jesse Gumm
7230f747e7 Update readme, Ensure sort can crash with option 2016-06-17 17:11:29 -05:00
Jesse Gumm
2a85d5d92f Add sort/[1-3] 2016-06-17 16:49:22 -05:00
Jesse Gumm
2dfcc52dd5 New config opt for default_timezone with fun 2016-05-31 18:46:02 -05:00
Jesse Gumm
366bc5e8d5 Fix beginning_day with different timezone 2016-05-19 03:56:43 +00:00
Jesse Gumm
4d42f5eddf Fix broken beginning_minute/1 2016-04-20 19:36:06 -05:00
Jesse Gumm
e413fcbf56 Add end_X/[0,1] functions. 2016-04-18 20:02:34 -05:00
Jesse Gumm
220883945a Update changelog 2016-04-14 11:24:50 -05:00
Jesse Gumm
66e9dc6ae9 get_timezone should return default tz if not set 2016-04-14 11:20:50 -05:00
Jesse Gumm
7ed65a0af1 Compress phrasing a bit in relative readme 2016-04-13 19:50:26 -05:00
Jesse Gumm
8671750e66 Update readme with relative parser 2016-04-13 19:47:32 -05:00
Jesse Gumm
795b3d8961 Add some between tests with the relative tests 2016-04-13 19:39:30 -05:00
Jesse Gumm
b550391a4e Add tests for relative parser 2016-04-13 19:38:04 -05:00
Jesse Gumm
6d88edea06 get rid of export_all (from testing) 2016-04-13 23:13:49 +00:00
Jesse Gumm
ff6b7139ca Fix relative calculation with improper timezone 2016-04-13 23:12:45 +00:00
Jesse Gumm
bb934b522f Fix relative parsing 2016-03-07 05:48:56 +00:00
Jesse Gumm
3564470578 Merge branch 'relative' of git://github.com/choptastic/qdate 2016-03-07 05:27:23 +00:00
Jesse Gumm
1ea2cadb1a Fix relative parser 2016-03-05 16:26:58 -06:00
Jesse Gumm
41b313d425 Add relative date/time parsing parser. 2016-03-05 15:16:03 -06:00
Jesse Gumm
5d73286e92 Allow a custom parser to return a unix timestamp 2016-03-05 13:52:02 -06:00
Jesse Gumm
5e43673c34 Add before and after as operators 2016-03-05 13:51:45 -06:00
Jesse Gumm
6036208bb5 Add test for #22.
Fixed in erlang_localtime (c0c4e3e)
2016-03-01 00:12:23 -06:00
Jesse Gumm
bf7f84590f Update readme 2016-02-26 13:51:14 -06:00
Jesse Gumm
1f7f5d9f69 Update changelog 2016-02-26 13:50:49 -06:00
Jesse Gumm
f39b4edb92 Add code markers around between 2016-02-24 13:21:00 -06:00
Jesse Gumm
4cd1a41cb4 Add readme for between 2016-02-24 13:17:13 -06:00
Jesse Gumm
569c7db56b Rework between/3, and add between/5 2016-02-24 13:09:33 -06:00
Jesse Gumm
69967d71ff Add initial experiment with between/[2,3] 2016-02-24 11:09:28 -06:00
Jesse Gumm
cc9dee4e08 Add config to readme 2016-02-22 21:14:18 -06:00
Jesse Gumm
d0ee71a0a7 Fix tabs in qdate.config 2016-02-21 16:13:47 -06:00
Jesse Gumm
43f6a78870 Merge branch 'master' of github.com:choptastic/qdate 2016-02-21 16:12:26 -06:00
Jesse Gumm
2eb0f88c61 Add sample qdate.config file 2016-02-21 16:12:05 -06:00
Jesse Gumm
7910de376e Merge branch 'master' of github.com:choptastic/qdate
Conflicts:
	rebar.config
2016-01-31 09:10:19 -06:00
Jesse Gumm
2a053253fb Point to my master branch 2016-01-31 09:08:31 -06:00
Jesse Gumm
2d40869c1a Add some tests for new tz_database changes 2016-01-31 09:07:54 -06:00
Jesse Gumm
0bf12290ec Update travis 2016-01-28 20:18:02 -06:00
Jesse Gumm
63a34f1c68 update readme 2016-01-28 20:12:49 -06:00
Jesse Gumm
37f8bf48e9 Update changelog 2015-11-07 11:42:25 -06:00
Jesse Gumm
d73a31e664 Add beginning_X/0 variations 2015-11-07 11:39:24 -06:00
Jesse Gumm
6ca6037f29 Add beginning_X functions with documentation
* Still need tests
2015-11-07 11:33:16 -06:00
Jesse Gumm
792245bb26 Fix tab issue (again) 2015-11-06 17:29:02 -06:00
Jesse Gumm
faeadb732a Fix tab issue in readme 2015-11-06 17:28:27 -06:00
Jesse Gumm
c685f4dc12 Add range docs to readme 2015-11-06 17:26:26 -06:00
Jesse Gumm
ef2075d475 Update changelog 2015-11-06 17:06:30 -06:00
Jesse Gumm
647e26db15 Update copyright notice on qdate.erl 2015-11-06 17:04:55 -06:00
Jesse Gumm
b1387c08c0 Add range functions
* Documentation needed
2015-11-06 17:03:16 -06:00
Jesse Gumm
c4c20db815 Update changelog 2015-11-05 11:35:16 -06:00
Jesse Gumm
a504a6adfc Update rebar.config back to choptastic/erlang_localtime
*temporary* until a commit is merged
2015-11-05 11:31:38 -06:00
Jesse Gumm
8427b7bdc6 Update dependencies
* Erlware Commons to non-crashing-with-rebar 0.15.0
* Erlang Localtime to base version, since it now has the necessary
  support for qdate functionality.
2015-11-05 09:57:00 -06:00
Jesse Gumm
48e8523e39 Merge pull request #16 from mfelsche/m/ec_date_z
Allow time to be a 4-tuple with subsecond accuracy. Does not yet *truly* support it, but merely accepts it and ignores it.
2015-10-14 14:45:53 -05:00
Matthias Wahl
52770185f9 fix date parse compatibility with ec_date
as it turns out ec_dates datetime() type can also be {{Y,M,D},{H,M,S,Ms}} which was not yet covered
2015-10-14 16:18:16 +02:00
Jesse Gumm
a443d476ac Update makefile and changelog 2015-07-27 14:25:21 -05:00
Jesse Gumm
7ab98975d8 Merge branch 'appsrc-fix' of git://github.com/project-fifo/qdate into project-fifo-appsrc-fix 2015-07-27 14:03:59 -05:00
Heinz N. Gies
15ded5317f added missing applications to app.src 2015-07-27 15:58:38 +02:00
Jesse Gumm
80b3a88530 Add 18 2015-07-26 08:48:58 -05:00
Jesse Gumm
d6ec97246f Merge branch 'master2' into rebar3 2015-07-26 08:35:36 -05:00
Jesse Gumm
b71173dc65 version bump 2015-07-26 08:34:12 -05:00
Jesse Gumm
0e0eb4186f Add some comments to rebar.config 2015-07-26 08:28:49 -05:00
Jesse Gumm
0b963146fe Remove R14 from travis. rebar3 doesn't work on R14 2015-07-26 08:27:46 -05:00
Heinz N. Gies
6dbeefc99b profile trick to make it rebar2 compatible. 2015-07-25 21:36:35 +02:00
Heinz N. Gies
02734c103b Fixed test task in makefile, damn me... 2015-07-25 20:35:09 +02:00
Heinz N. Gies
5f1f4a7785 rebar3ified makefile 2015-07-25 20:33:15 +02:00
Heinz N. Gies
5387ec8f02 rebar3 and hex. 2015-07-25 20:21:07 +02:00
Jesse Gumm
4f19dcf5ec Merge pull request #12 from aramallo/master
Removed io:format/2 call from try_parsers/2.
2015-07-09 11:07:20 -05:00
Alejandro M. Ramallo
c3b490ec52 Removed io:format/2 call from try_parsers/2.
Something that was left during debugging I guess ;-)
2015-07-09 16:54:23 +01:00
17 changed files with 1265 additions and 105 deletions

47
.github/workflows/tests-workflow.yml vendored Normal file
View 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
View file

@ -1,6 +1,10 @@
*~
*.beam
*.sw?
*.iml
deps/
ebin/
.eunit/
.idea/
_build
doc/

View file

@ -1,13 +0,0 @@
language: erlang
script: "make test"
otp_release:
- 17.4
- 17.1
- 17.0
- R16B
- R15B02
- R15B01
- R15B
- R14B03
- R14B02
before_script: "sudo apt-get --yes --force-yes install libpam0g-dev"

View file

@ -1,3 +1,60 @@
## 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.

View file

View file

@ -1,13 +1,41 @@
all: get-deps compile
all: compile
get-deps:
./rebar get-deps
# Check if rebar3.mk exists, and if not, download it
ifeq ("$(wildcard rebar3.mk)","")
$(shell curl -O https://raw.githubusercontent.com/choptastic/rebar3.mk/master/rebar3.mk)
endif
compile:
./rebar compile
# rebar3.mk adds a new rebar3 rule to your Makefile
# (see https://github.com/choptastic/rebar3.mk) for full info
include rebar3.mk
test: get-deps compile
./rebar skip_deps=true eunit
compile: rebar3
$(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)"

View file

@ -1,6 +1,6 @@
# qdate - Erlang Date and Timezone Library
[![Build Status](https://travis-ci.org/choptastic/qdate.png?branch=master)](https://travis-ci.org/choptastic/qdate)
[![qdate tests and dialyzer](https://github.com/choptastic/qdate/actions/workflows/tests-workflow.yml/badge.svg)](https://github.com/choptastic/qdate/actions/workflows/tests-workflow.yml)
## Purpose
@ -98,14 +98,16 @@ will infer the timezone in the following order.
`set_timezone/1` only applies to that *specific* process. If none is
specified.
+ 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"
for all dates.
+ A timezone value of `auto` will act as if no timezone is specified.
#### Disambiguating Ambiguous Timezone Conversions
Sometimes, when youre converting a datetime from one timezone to another, there
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
@ -190,24 +192,72 @@ ok
`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.
+ `-1`: `A` is less than (before) `B`.
+ `1`: `A` is greater than (after) `B`.
+ `compare(A, Operator, B)` - Operator is an infix comparison operator, and
the function will return true if:
+ `compare(A, Operator, B) -> true|false` - Operator is an infix comparison operator, and
the function will return a boolean. Will return `true` if:
+ `'='`, or `'=='` - `A` is the same time as `B`
+ `'/='`, or `'=/='` or `'!='` - `A` is not the same time as `B`
+ `'<'` - `A` is before `B`
+ `'>'` - `A` is after `B`
+ `'=<'` or `'<='` - `A` is before 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 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)
### 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
+ `set_timezone(Key, TZ)` - Set the timezone to TZ for the key `Key`
@ -448,8 +498,31 @@ the crash.
**Another Note:** Custom parsers are expected to return either:
+ 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
#### 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
@ -463,7 +536,7 @@ the crash.
%% 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
%% 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
16> qdate:register_format(longdate, "l, F jS, Y g:i A T").
@ -619,25 +692,76 @@ ok
%% that timezone to our intended timezone.
```
## Date Arithmetic
## Beginning or Ending of time periods (hours, days, years, weeks, etc)
(not fully tested yet, but will have full tests for 0.4.0)
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.
+ `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.
@ -654,6 +778,89 @@ There are 7 other arithmetic functions that take a single argument, and these do
+ `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
A few shoutouts to [Dale Harvey](http://github.com/daleharvey) and the
@ -667,6 +874,8 @@ not exist.
+ [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
@ -678,7 +887,6 @@ See [CHANGELOG.markdown](https://github.com/choptastic/qdate/blob/master/CHANGEL
+ Make `qdate` backend-agnostic (allow specifying either ec_date or dh_date as
the backend)
+ Add `-spec` and `-type` info for dialyzer
+ Provide a sample qdate.config for users to see
+ Research the viability of [ezic](https://github.com/drfloob/ezic) for a
timezone backend replacement for `erlang_localtime`.

20
qdate.config Normal file
View 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

Binary file not shown.

View file

@ -1,9 +1,24 @@
% vim:ts=4 sw=4 et ft=erlang
{require_otp_vsn, "R13B04|R14|R15|R16|17"}.
%% -*- erlang -*-
%% vim:ts=4 sw=4 et ft=erlang
{cover_enabled, true}.
{deps, [
{erlware_commons, ".*", {git, "git://github.com/erlware/erlware_commons.git", {branch, master}}},
{erlang_localtime, ".*", {git, "git://github.com/choptastic/erlang_localtime.git", {branch, master}}}
{dialyzer, [
{exclude_apps, []},
{warnings, []}
]}.
{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
View 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
View 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">>}]}
].

View file

@ -1,12 +1,17 @@
{application, qdate,
[
{description, "Simple Date and Timezone handling for Erlang"},
{vsn, "0.4.0"},
{vsn, git},
{registered, []},
{applications, [
kernel,
stdlib
stdlib,
qdate_localtime,
erlware_commons
]},
{modules, [qdate, qdate_srv]},
{env, []}
{modules, [qdate, qdate_srv, qdate_sup, qdate_app]},
{env, []},
{licenses, ["MIT"]},
{mod, {qdate_app, []}},
{links, [{"Github", "https://github.com/choptastic/qdate"}]}
]}.

View file

@ -1,9 +1,11 @@
% vim: ts=4 sw=4 et
% Copyright (c) 2013 Jesse Gumm
% Copyright (c) 2013-2023 Jesse Gumm
% See LICENSE for licensing information.
%
-module(qdate).
%-compile(export_all).
-export([
start/0,
stop/0
@ -24,9 +26,50 @@
unixtime/0
]).
-export([
beginning_minute/1,
beginning_minute/0,
beginning_hour/1,
beginning_hour/0,
beginning_day/1,
beginning_day/0,
beginning_week/0,
beginning_week/1,
beginning_week/2,
beginning_month/1,
beginning_month/0,
beginning_year/1,
beginning_year/0
]).
-export([
end_minute/1,
end_minute/0,
end_hour/1,
end_hour/0,
end_day/1,
end_day/0,
end_week/0,
end_week/1,
end_week/2,
end_month/1,
end_month/0,
end_year/1,
end_year/0
]).
-export([
compare/2,
compare/3
compare/3,
between/2,
between/3,
between/5
]).
-export([
sort/1,
sort/2,
sort/3
]).
-export([
@ -44,9 +87,29 @@
add_months/1,
add_years/2,
add_years/1,
add_unit/2,
add_unit/3,
add_date/2
]).
-export([
range/4,
range_seconds/3,
range_minutes/3,
range_hours/3,
range_days/3,
range_weeks/3,
range_months/3,
range_years/3
]).
-export([
age/1,
age/2,
age_days/1,
age_days/2
]).
-export([
register_parser/2,
register_parser/1,
@ -66,6 +129,9 @@
clear_timezone/1
]).
-export([
parse_relative/1
]).
%% Exported for API compatibility with ec_date
-export([
@ -74,28 +140,32 @@
parse/1
]).
-type qdate() :: any().
-type datetime() :: {{integer(), integer(), integer()}, {integer(), integer(), integer()}} |
{{integer(), integer(), integer()}, {integer(), integer(), integer(), integer()}}.
-type erlnow() :: {integer(), integer(), integer()}.
-type binary_or_string() :: binary() | string().
-type disambiguate() :: prefer_standard | prefer_daylight | both.
%% erlang:get_stacktrace/0 is deprecated in OTP 21
-ifndef(OTP_RELEASE).
-define(WITH_STACKTRACE(T, R, S), T:R -> S = erlang:get_stacktrace(), ).
-else.
-define(WITH_STACKTRACE(T, R, S), T:R:S ->).
-endif.
%% This the value in gregorian seconds for jan 1st 1970, 12am
%% It's used to convert to and from unixtime, since unixtime starts
%% 1970-01-01 12:00am
-define(UNIXTIME_BASE,62167219200).
%% This is the timezone only if the qdate application variable
%% "default_timezone" isn't set or is set to undefined.
%% It's recommended that your app sets the var in a config, or at least using
%%
%% application:set_env(qdate, default_timezone, "GMT").
%%
-define(DEFAULT_TZ, case application:get_env(qdate, default_timezone) of
undefined -> "GMT";
{ok, TZ} -> TZ
end).
-define(DETERMINE_TZ, determine_timezone()).
-define(DEFAULT_DISAMBIG, prefer_standard).
-define(else, true).
start() ->
application:load(qdate).
application:ensure_all_started(qdate).
stop() ->
ok.
@ -109,6 +179,7 @@ to_string(Format, Date) ->
to_string(Format, ToTZ, Date) ->
to_string(Format, ToTZ, ?DEFAULT_DISAMBIG, Date).
-spec to_string(Format :: any(), ToTZ :: any(), Disambiguate :: disambiguate(), Date :: qdate()) -> binary_or_string() | {ambiguous, binary_or_string() , binary_or_string()}.
to_string(FormatKey, ToTZ, Disambiguate, Date) when is_atom(FormatKey) orelse is_tuple(FormatKey) ->
Format = case qdate_srv:get_format(FormatKey) of
undefined -> throw({undefined_format_key,FormatKey});
@ -175,7 +246,13 @@ to_string_worker([$r | RestFormat], ToTZ, Disamb, Date) ->
to_string_worker(NewFormat, ToTZ, Disamb, Date) ++ to_string_worker(RestFormat, ToTZ, Disamb, Date);
to_string_worker([$c | RestFormat], ToTZ, Disamb, Date) ->
Format1 = "Y-m-d",
Format2 = "H:i:sP",
Format2 = case Date of
{_, {_,_,_,_}} ->
%% Have milliseconds
"H:i:s.fP";
_ ->
"H:i:sP"
end,
to_string_worker(Format1, ToTZ, Disamb, Date)
++ "T"
++ to_string_worker(Format2, ToTZ, Disamb, Date)
@ -226,6 +303,7 @@ to_date(RawDate) ->
to_date(ToTZ, RawDate) ->
to_date(ToTZ, ?DEFAULT_DISAMBIG, RawDate).
-spec to_date(ToTZ :: any(), Disambiguate :: disambiguate(), RawDate :: any()) -> {ambiguous, datetime(), datetime()} | datetime().
to_date(ToTZ, Disambiguate, RawDate) when is_binary(RawDate) ->
to_date(ToTZ, Disambiguate, binary_to_list(RawDate));
to_date(ToTZ, Disambiguate, RawDate) when is_binary(ToTZ) ->
@ -240,9 +318,15 @@ to_date(ToTZ, Disambiguate, RawDate) ->
{ParsedDate,ParsedTZ} ->
{ParsedDate,ParsedTZ}
end,
PreserveMs = preserve_ms(),
try raw_to_date(RawDate3) of
D={{_,_,_},{_,_,_}} ->
date_tz_to_tz(D, Disambiguate, FromTZ, ToTZ)
date_tz_to_tz(D, Disambiguate, FromTZ, ToTZ);
{{Year, Month, Date},{Hour,Minute,Second,Millis}} when PreserveMs ->
{ODate, {OHour,OMinute,OSecond}} = date_tz_to_tz({{Year, Month, Date},{Hour,Minute,Second}}, Disambiguate, FromTZ, ToTZ),
{ODate, {OHour,OMinute,OSecond, Millis}};
{{Year, Month, Date},{Hour,Minute,Second,_Millis}} ->
date_tz_to_tz({{Year, Month, Date},{Hour,Minute,Second}}, Disambiguate, FromTZ, ToTZ)
catch
_:_ ->
case raw_to_date(RawDate) of
@ -277,9 +361,10 @@ get_deterministic_datetime() ->
to_unixtime(Date) ->
to_unixtime(?DEFAULT_DISAMBIG, Date).
-spec to_unixtime(Disamb :: disambiguate(), qdate()) -> {ambiguous, integer(), integer()} | integer().
to_unixtime(_, Unixtime) when is_integer(Unixtime) ->
Unixtime;
to_unixtime(_, {MegaSecs,Secs,_}) ->
to_unixtime(_, {MegaSecs,Secs,_}) when is_integer(MegaSecs), is_integer(Secs) ->
MegaSecs*1000000 + Secs;
to_unixtime(Disamb, ToParse) ->
%% We want to treat all unixtimes as GMT
@ -298,11 +383,12 @@ unixtime() ->
to_now(Date) ->
to_now(?DEFAULT_DISAMBIG, Date).
-spec to_now(Disamb :: disambiguate(), qdate()) -> erlnow() | {ambiguous, erlnow(), erlnow()}.
to_now(_, Now = {_,_,_}) ->
Now;
to_now(Disamb, ToParse) ->
case to_unixtime(Disamb, ToParse) of
{ambiguous, Standard, Daylight} ->
{ambiguous, Standard, Daylight} when is_integer(Standard), is_integer(Daylight) ->
{ambiguous,
unixtime_to_now(Standard),
unixtime_to_now(Daylight)};
@ -311,10 +397,144 @@ to_now(Disamb, ToParse) ->
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Beginning/Truncation %%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
beginning_minute() ->
beginning_minute({date(),time()}).
beginning_minute(Date) ->
{{Y,M,D},{H,I,_}} = to_date(Date),
{{Y,M,D},{H,I,0}}.
beginning_hour() ->
beginning_hour({date(),time()}).
beginning_hour(Date) ->
{{Y,M,D},{H,_,_}} = to_date(Date),
{{Y,M,D},{H,0,0}}.
beginning_day() ->
beginning_day(unixtime()).
beginning_day(Date) ->
{{Y,M,D},{_,_,_}} = to_date(Date),
{{Y,M,D},{0,0,0}}.
beginning_month() ->
beginning_month({date(),time()}).
beginning_month(Date) ->
{{Y,M,_},{_,_,_}} = to_date(Date),
{{Y,M,1},{0,0,0}}.
beginning_year() ->
beginning_year({date(),time()}).
beginning_year(Date) ->
{{Y,_,_},{_,_,_}} = to_date(Date),
{{Y,1,1},{0,0,0}}.
beginning_week() ->
beginning_week({date(), time()}).
%% 1 = Monday, 7 = Sunday
beginning_week(Date) ->
beginning_week(1, Date).
beginning_week(BeginningDayOfWeek, Date) when is_atom(BeginningDayOfWeek) ->
DOW = weekday_map(BeginningDayOfWeek),
beginning_week(DOW, Date);
beginning_week(BeginningDayOfWeek, Date0) when
BeginningDayOfWeek >= 1,
BeginningDayOfWeek =< 7,
is_integer(BeginningDayOfWeek) ->
{DateOnly, _} = Date = to_date(Date0),
CurDOW = calendar:day_of_the_week(DateOnly),
if
CurDOW==BeginningDayOfWeek ->
{DateOnly, {0,0,0}};
CurDOW > BeginningDayOfWeek->
Diff = CurDOW - BeginningDayOfWeek,
beginning_day(add_days(-Diff, Date));
CurDOW < BeginningDayOfWeek ->
Diff = 7 - (BeginningDayOfWeek - CurDOW),
beginning_day(add_days(-Diff, Date))
end.
weekday_map(mon) -> 1;
weekday_map(tue) -> 2;
weekday_map(wed) -> 3;
weekday_map(thu) -> 4;
weekday_map(fri) -> 5;
weekday_map(sat) -> 6;
weekday_map(sun) -> 7;
weekday_map(monday) -> 1;
weekday_map(tuesday) -> 2;
weekday_map(wednesday) -> 3;
weekday_map(thursday) -> 4;
weekday_map(friday) -> 5;
weekday_map(saturday) -> 6;
weekday_map(sunday) -> 7.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%% End of Period (day/hour, etc) %%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
end_minute() ->
end_minute({date(),time()}).
end_minute(Date) ->
{{Y,M,D},{H,I,_}} = to_date(Date),
{{Y,M,D},{H,I,59}}.
end_hour() ->
end_hour({date(),time()}).
end_hour(Date) ->
{{Y,M,D},{H,_,_}} = to_date(Date),
{{Y,M,D},{H,59,59}}.
end_day() ->
end_day({date(),time()}).
end_day(Date) ->
{{Y,M,D},_} = to_date(Date),
{{Y,M,D},{23,59,59}}.
end_month() ->
end_month({date(), time()}).
end_month(Date) ->
Beginning = beginning_month(Date),
add_seconds(-1, add_months(1, Beginning)).
end_year() ->
end_year({date(),time()}).
end_year(Date) ->
{{Y,_,_},_} = to_date(Date),
{{Y,12,31},{23,59,59}}.
end_week() ->
end_week({date(), time()}).
end_week(Date) ->
end_week(1, Date).
end_week(BeginningDayOfWeek, Date) ->
Beginning = beginning_week(BeginningDayOfWeek, Date),
PlusWeek = add_weeks(1, Beginning),
add_seconds(-1, PlusWeek).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Comparisons %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec compare(A :: qdate(), B :: qdate()) -> integer().
compare(A, B) ->
NowA = to_now(A),
NowB = to_now(B),
@ -324,6 +544,7 @@ compare(A, B) ->
NowA > NowB -> 1
end.
-spec compare(A :: qdate(), Op :: atom(), B :: qdate()) -> boolean().
compare(A, Op, B) ->
Comp = compare(A, B),
case Op of
@ -334,15 +555,93 @@ compare(A, Op, B) ->
'=/=' -> Comp =/= 0;
'/=' -> Comp =/= 0;
'before'-> Comp =:= -1;
'<' -> Comp =:= -1;
'<=' -> Comp =:= -1 orelse Comp =:= 0;
'=<' -> Comp =:= -1 orelse Comp =:= 0;
'after' -> Comp =:= 1;
'>' -> Comp =:= 1;
'>=' -> Comp =:= 1 orelse Comp =:= 0;
'=>' -> Comp =:= 1 orelse Comp =:= 0
end.
between(A, B) ->
between(A, unixtime(), B).
between(A, Date, B) ->
between(A, '=<', Date, '=<', B).
between(A, Op1, Date, Op2, B) ->
compare(A, Op1, Date) andalso compare(Date, Op2, B).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Sorting %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
sort(List) ->
sort('=<', List).
sort(Op, List) ->
sort(Op, List, [{non_dates, back}]).
sort(Op, List, Opts) ->
NonDateOpt = proplists:get_value(non_dates, Opts, back),
WithNorm = add_sort_normalization(List, NonDateOpt),
SortFun = make_sort_fun(Op, NonDateOpt),
Sorted = lists:sort(SortFun, WithNorm),
strip_sort_normalization(Sorted).
%% Normalization pre-processes the dates (converting them to unixtimes for easy
%% comparison, and also tags non-dates (dates that crashed during parsing) as such
add_sort_normalization(List, NonDateOpt) ->
lists:map(fun(Date) ->
Sortable = try to_unixtime(Date)
catch _:_ when NonDateOpt=/=crash ->
{non_date, Date}
end,
{Sortable, Date}
end, List).
%% Remove the normalization tag to return the original term
strip_sort_normalization(List) ->
[Date || {_, Date} <- List].
-spec make_sort_fun(Op :: atom(), NonDateOpt :: front | back) -> fun().
make_sort_fun(Op, NonDateOpt) ->
DateComp = sort_op_comp_fun(Op),
fun({{non_date, A}, _}, {{non_date, B},_}) ->
DateComp(A,B);
({{non_date, _}, _}, _) when NonDateOpt == front ->
true;
({{non_date, _}, _}, _) when NonDateOpt == back ->
false;
(_, {{non_date, _}, _}) when NonDateOpt == front ->
false;
(_, {{non_date, _}, _}) when NonDateOpt == back ->
true;
(A, B) ->
DateComp(A, B)
end.
sort_op_comp_fun(Op) ->
fun(A, B) ->
case Op of
'before'-> A < B;
'<' -> A < B;
'<=' -> A =< B;
'=<' -> A =< B;
'after' -> A > B;
'>' -> A > B;
'>=' -> A >= B;
'=>' -> A >= B
end
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Date Math %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
@ -403,6 +702,48 @@ add_years(Years, Date) ->
add_years(Years) ->
add_years(Years, os:timestamp()).
-type unit() :: second | seconds |
minute | minutes |
hour | hours |
day | days |
week | weeks |
month | months |
year | years.
-spec add_unit(Unit :: unit(), Value :: integer(), Date :: qdate()) -> qdate().
add_unit(second, Value, Date) ->
add_unit(seconds, Value, Date);
add_unit(seconds, Value, Date) ->
add_seconds(Value, Date);
add_unit(minute, Value, Date) ->
add_unit(minutes, Value, Date);
add_unit(minutes, Value, Date) ->
add_minutes(Value, Date);
add_unit(hour, Value, Date) ->
add_unit(hours, Value, Date);
add_unit(hours, Value, Date) ->
add_hours(Value, Date);
add_unit(day, Value, Date) ->
add_unit(days, Value, Date);
add_unit(days, Value, Date) ->
add_days(Value, Date);
add_unit(week, Value, Date) ->
add_unit(weeks, Value, Date);
add_unit(weeks, Value, Date) ->
add_weeks(Value, Date);
add_unit(month, Value, Date) ->
add_unit(months, Value, Date);
add_unit(months, Value, Date) ->
add_months(Value, Date);
add_unit(year, Value, Date) ->
add_unit(years, Value, Date);
add_unit(years, Value, Date) ->
add_years(Value, Date).
add_unit(Unit, Value) ->
add_unit(Unit, Value, os:timestamp()).
add_date({{AddY, AddM, AddD}, {AddH, AddI, AddS}}, Date) ->
{{Y, M, D}, {H, I, S}} = to_date(Date),
Date1 = fix_maybe_improper_date({{Y+AddY, M+AddM, D+AddD}, {H, I, S}}),
@ -418,12 +759,22 @@ fix_maybe_improper_date({Date0, Time}) ->
Date = fmid(Date0),
{Date, Time}.
%% Originally, this function didn't recurse. Here's the story. Some numbers,
%% like M = 12 (December) or M = -11 (January) would trigger an overflow or
%% underflow, resulting in fix_year_month returning something nonsensical like
%% {2018, 13}. I added some extra clauses to special treat those "overflow but
%% shouldn't" situations, but realized it was just cleaner to recurse, calling
%% fix_year_month on the calculated result, knowing that the numbers will
%% normalize on their own. So for all the clauses of fix_year_month, we recurse
%% as a sanity check, eventually only returning the result of the "Everything
%% Looks good" clause at the bottom.
fix_year_month({Y, M}) when M > 12 ->
YearsOver = M div 12,
{Y + YearsOver, M-(YearsOver*12)};
fix_year_month({Y + YearsOver, M-(YearsOver*12)});
fix_year_month({Y, M}) when M < 1 ->
YearsUnder = (abs(M-1) div 12) + 1,
{Y - YearsUnder, M+(YearsUnder*12)};
fix_year_month({Y - YearsUnder, M+(YearsUnder*12)});
fix_year_month({Y, M}) ->
{Y, M}.
@ -458,6 +809,170 @@ fmid({Y, M, D}) when D < 1 ->
fmid(Date) ->
Date.
age(Birth) ->
age(Birth, os:timestamp()).
age(Birth, Now) ->
%% B=Birth
{{BY, BM, BD}, _} = to_date(Birth),
%% C=Current
{{CY, CM, CD}, _} = to_date(Now),
if
(CM > BM);
(CM == BM andalso CD >= BD)
-> CY - BY;
true ->
(CY - BY) - 1
end.
age_days(Birth) ->
age_days(Birth, os:timestamp()).
age_days(Birth, Now) ->
case {to_date(Birth), to_date(Now)} of
{{SameDay, _}, {SameDay, _}} ->
0;
{{BirthDate, BirthTime}, {NowDate, NowTime}} ->
BirthDays = calendar:date_to_gregorian_days(BirthDate),
NowDays = calendar:date_to_gregorian_days(NowDate),
DiffDays = NowDays - BirthDays,
if
NowTime >= BirthTime ->
DiffDays;
true ->
DiffDays-1
end
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Ranges %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
range(seconds, Interval, Start, Finish) ->
range_inner(fun add_seconds/2, Interval, Start, Finish);
range(minutes, Interval, Start, Finish) ->
range_inner(fun add_minutes/2, Interval, Start, Finish);
range(hours, Interval, Start, Finish) ->
range_inner(fun add_hours/2, Interval, Start, Finish);
range(days, Interval, Start, Finish) ->
range_inner(fun add_days/2, Interval, Start, Finish);
range(weeks, Interval, Start, Finish) ->
range_inner(fun add_weeks/2, Interval, Start, Finish);
range(months, Interval, Start, Finish) ->
range_inner(fun add_months/2, Interval, Start, Finish);
range(years, Interval, Start, Finish) ->
range_inner(fun add_years/2, Interval, Start, Finish).
range_inner(IntervalFun, Interval, Start, Finish) when Interval > 0 ->
%% If Interval>0, then we're ascending, and we want to compare start/end
%% dates normally
CompareFun = fun(S, F) -> compare(S, F) end,
range_normalizer(IntervalFun, Interval, CompareFun, Start, Finish);
range_inner(IntervalFun, Interval, Start, Finish) when Interval < 0 ->
%% If Interval<0, then we're descending, and we want to compare start/end
%% dates backwards (we want to end when the Start Date is Lower than
%% Finish)
CompareFun = fun(S, F) -> compare(F, S) end,
range_normalizer(IntervalFun, Interval, CompareFun, Start, Finish);
range_inner(_, Interval, _, _) when Interval==0 ->
throw(interval_cannot_be_zero).
range_normalizer(IntervalFun, Interval, CompareFun, Start0, Finish0) ->
%% Convert dates to unixtime for speed's sake
Start = to_unixtime(Start0),
Finish = to_unixtime(Finish0),
%% Prepare the incrementer, so we just need to pass the date to the incrementer.
Incrementer = fun(D) -> IntervalFun(Interval, D) end,
range_worker(Incrementer, CompareFun, Start, Finish).
range_worker(Incrementer, CompareFun, Start, Finish) ->
case CompareFun(Start, Finish) of
0 -> [Finish]; %% Equal, so we add our Finish value
1 -> []; %% Start is after Finish, so we add nothing
-1 -> %% Start is before Finish, so we include it, and recurse
NextDay = Incrementer(Start),
[Start | range_worker(Incrementer, CompareFun, NextDay, Finish)]
end.
range_seconds(Interval, Start, Finish) ->
range(seconds, Interval, Start, Finish).
range_minutes(Interval, Start, Finish) ->
range(minutes, Interval, Start, Finish).
range_hours(Interval, Start, Finish) ->
range(hours, Interval, Start, Finish).
range_days(Interval, Start, Finish) ->
range(days, Interval, Start, Finish).
range_weeks(Interval, Start, Finish) ->
range(weeks, Interval, Start, Finish).
range_months(Interval, Start, Finish) ->
range(months, Interval, Start, Finish).
range_years(Interval, Start, Finish) ->
range(years, Interval, Start, Finish).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%% Relative Date Parsing %%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
parse_relative({relative, Date, Relation}) when is_atom(Relation) ->
parse_relative({relative, Date, atom_to_list(Relation)});
parse_relative({relative, Date, Relation}) when is_list(Relation); is_binary(Relation) ->
case parse_actual_relation(Relation) of
undefined -> undefined;
{OpStr, NumStr, UnitStr} ->
{Num, Unit} = normalize_relative_matches(OpStr, NumStr, UnitStr),
add_unit(Unit, Num, Date)
end;
parse_relative(now) ->
unixtime();
parse_relative("now") ->
unixtime();
parse_relative(<<"now">>) ->
unixtime();
parse_relative(Relation) when is_list(Relation); is_binary(Relation) ->
parse_relative({relative, unixtime(), Relation});
parse_relative(_) ->
undefined.
%% I would do this function recursively, but the return order of arguments
%% inconsistent, so I just leave it like this. It's a little nasty to have the
%% nested case expressions, but I can deal with it.
parse_actual_relation(Relation) ->
PrefixRE = "^(\\-|\\+|in)\\s?(\\d+) (second|minute|hour|day|week|month|year)s?$",
SuffixRE = "^(\\d+) (second|minute|hour|day|week|month|year)s?\\s?(ago|from now)?$",
case re:run(Relation, PrefixRE, [{capture, all_but_first, list}]) of
nomatch ->
case re:run(Relation, SuffixRE, [{capture, all_but_first, list}]) of
nomatch -> undefined;
{match, [NumStr, UnitStr, OpStr]} ->
{OpStr, NumStr, UnitStr}
end;
{match, [OpStr, NumStr, UnitStr]} ->
{OpStr, NumStr, UnitStr}
end.
normalize_relative_matches(OpStr, NumStr, UnitStr) ->
Op = normalize_relative_op(OpStr),
Num = list_to_integer(Op ++ NumStr),
Unit = list_to_existing_atom(UnitStr),
{Num, Unit}.
normalize_relative_op(Op) ->
case Op of
"+" -> "+";
"-" -> "-";
"ago" -> "-";
"from now" -> "+";
"in" -> "+"
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Timezone Stuff %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
@ -469,6 +984,7 @@ get_timezone_shift(TZ, Disambiguate, Date) ->
{error,T} -> {error,T};
{Sh, _} when Disambiguate==prefer_standard -> Sh;
{_, Sh} when Disambiguate==prefer_daylight -> Sh;
0 -> {'+', 0, 0};
Sh -> Sh
end.
@ -486,6 +1002,8 @@ extract_timezone(DateString) when is_list(DateString) ->
end;
extract_timezone(Date={{_,_,_},{_,_,_}}) ->
{Date, ?DETERMINE_TZ};
extract_timezone(Rel={relative, _, _}) ->
{Rel, "GMT"};
extract_timezone(Now={_,_,_}) ->
{Now, "GMT"};
extract_timezone({MiscDate,TZ}) ->
@ -521,15 +1039,33 @@ extract_timezone_helper(RevDate, [TZ | TZs]) when length(RevDate) >= length(TZ)
extract_timezone_helper(RevDate, [_TZ | TZs]) ->
extract_timezone_helper(RevDate, TZs).
preserve_ms() ->
application:get_env(qdate, preserve_ms, false).
%% This is the timezone only if the qdate application variable
%% "default_timezone" isn't set or is set to undefined.
%% It's recommended that your app sets the var in a config, or at least using
%%
%% application:set_env(qdate, default_timezone, "GMT").
%%
default_timezone() ->
case application:get_env(qdate, default_timezone) of
undefined -> "GMT";
{ok, {Mod, Fun}} -> Mod:Fun();
{ok, TZ} -> TZ
end.
determine_timezone() ->
case qdate_srv:get_timezone() of
undefined -> ?DEFAULT_TZ;
undefined -> default_timezone();
TZ -> TZ
end.
%% If FromTZ is an integer, then it's an integer that represents the number of minutes
%% relative to GMT. So we convert the date to GMT based on that number, then we can
%% do the other timezone conversion.
-spec date_tz_to_tz(Date :: datetime(), Disambiguate :: disambiguate(), FromTZ :: any(), ToTZ :: any()) ->
datetime() | {ambiguous, datetime(), datetime()}.
date_tz_to_tz(Date, Disambiguate, FromTZ, ToTZ) when is_integer(FromTZ) ->
NewDate = localtime:adjust_datetime(Date, FromTZ),
date_tz_to_tz(NewDate, Disambiguate, "GMT", ToTZ);
@ -544,13 +1080,14 @@ date_tz_to_tz(Date, Disambiguate, FromTZ, ToTZ) ->
date_tz_to_tz_both(Date, FromTZ, ToTZ)
end.
-spec date_tz_to_tz_both(Date :: datetime(), FromTZ :: string(), ToTZ :: string()) -> datetime() | {ambiguous, datetime(), datetime()}.
date_tz_to_tz_both(Date, FromTZ, ToTZ) ->
Standard = localtime:local_to_local(Date, FromTZ, ToTZ),
Daylight = localtime:local_to_local_dst(Date, FromTZ, ToTZ),
if
Standard=:=Daylight ->
Standard;
?else ->
true ->
{ambiguous, Standard, Daylight}
end.
@ -566,7 +1103,7 @@ set_timezone(Key,TZ) ->
qdate_srv:set_timezone(Key, TZ).
get_timezone() ->
qdate_srv:get_timezone().
?DETERMINE_TZ.
get_timezone(Key) ->
qdate_srv:get_timezone(Key).
@ -616,8 +1153,9 @@ try_registered_parsers(RawDate) ->
try_parsers(_RawDate,[]) ->
undefined;
try_parsers(RawDate,[{ParserKey,Parser}|Parsers]) ->
io:format("Trying Parser: ~p~n", [ParserKey]),
try Parser(RawDate) of
Timestamp when is_integer(Timestamp) ->
{Timestamp, "GMT"};
{{_,_,_},{_,_,_}} = DateTime ->
{DateTime,undefined};
{DateTime={{_,_,_},{_,_,_}},Timezone} ->
@ -627,8 +1165,8 @@ try_parsers(RawDate,[{ParserKey,Parser}|Parsers]) ->
Other ->
throw({invalid_parser_return_value,[{parser_key,ParserKey},{return,Other}]})
catch
Error:Reason ->
throw({error_in_parser,[{error,{Error,Reason}},{parser_key,ParserKey}]})
?WITH_STACKTRACE(Error, Reason, Stacktrace)
throw({error_in_parser,[{error,{Error,Reason}},{parser_key,ParserKey}, {stacktrace, Stacktrace}]})
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
@ -647,7 +1185,7 @@ deregister_format(Key) ->
unixtime_to_now(T) when is_integer(T) ->
MegaSec = floor(T/1000000),
MegaSec = flooring(T/1000000),
Secs = T - MegaSec*1000000,
{MegaSec,Secs,0}.
@ -655,10 +1193,10 @@ unixtime_to_date(T) ->
Now = unixtime_to_now(T),
calendar:now_to_datetime(Now).
floor(N) when N >= 0 ->
trunc(N);
floor(N) when N < 0 ->
Int = trunc(N),
flooring(N) when N >= 0 ->
erlang:trunc(N);
flooring(N) when N < 0 ->
Int = erlang:trunc(N),
if
Int==N -> Int;
true -> Int-1
@ -668,6 +1206,8 @@ floor(N) when N < 0 ->
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% TESTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-ifdef(EUNIT).
-include_lib("eunit/include/eunit.hrl").
%% emulates as if a forum-type website has a Site tz, and a user-specified tz
@ -695,6 +1235,30 @@ tz_test_() ->
end
}.
tz_preserve_ms_true_test_() ->
{
setup,
fun start_test/0,
fun stop_test/1,
fun(SetupData) ->
{inorder,[
preserve_ms_true_tests(SetupData)
]}
end
}.
tz_preserve_ms_false_test_() ->
{
setup,
fun start_test/0,
fun stop_test/1,
fun(SetupData) ->
{inorder,[
preserve_ms_false_tests(SetupData)
]}
end
}.
test_deterministic_parser(_) ->
{inorder, [
?_assertEqual(ok, application:set_env(qdate, deterministic_parsing, {now, now})),
@ -753,6 +1317,20 @@ tz_tests(_) ->
?_assertEqual({{2013,3,6},{18,0,0}}, to_date("GMT","3/7/2013 12:00am +0600")),
?_assertEqual({{2013,3,6},{12,0,0}}, to_date("CST","3/7/2013 12:00am +0600")),
%% These next two test check to make sure that the tz database properly
%% interprets GMT+/-X timezones (an earlier issue with
%% erlang_localtime's tz database had it incrementing/decrementing the
%% minute field rather than hours.
%%
%% It also ensures that GMT+/-X handling is interpreted the way you'd
%% intuitively expect, rather than the POSIX way, which is, quite
%% frankly, broken.
?_assertEqual({{2013,3,7},{10,0,0}}, to_date("GMT-0","3/7/2013 10:00am GMT")),
?_assertEqual({{2013,3,7},{10,0,0}}, to_date("GMT+0","3/7/2013 10:00am GMT")),
?_assertEqual({{2013,3,7},{9,0,0}}, to_date("GMT-1","3/7/2013 10:00am GMT")),
?_assertEqual({{2013,3,7},{11,0,0}}, to_date("GMT+1","3/7/2013 10:00am GMT")),
%% parsing, then reformatting the same time with a different timezone using the php "r" (rfc2822)
?_assertEqual("Thu, 07 Mar 2013 12:15:00 -0600",
to_string("r","CST",to_string("r","EST",{{2013,3,7},{13,15,0}}))),
@ -767,7 +1345,14 @@ tz_tests(_) ->
?_assertEqual(ok, set_timezone("EST")),
?_assertEqual(555555555,to_unixtime("1987-08-10 00:59:15 GMT")),
?_assertEqual({555,555555,0},to_now("1987-08-10 00:59:15 GMT")),
?_assertEqual(ok, set_timezone("GMT"))
?_assertEqual(ok, set_timezone("GMT")),
?_assertEqual({{1970, 1, 1}, {1, 0, 0}}, to_date("CET", "1970-01-01T00:00:00Z")),
?_assertEqual(ok, set_timezone("UTC")),
?_assertEqual(1521945120, to_unixtime("2018-3-25T2:32:00")),
?_assertEqual(true, between("-1 seconds", os:timestamp(), "+1 seconds")),
?_assertEqual(true, between("60 hours ago", unixtime(), "in 15 days")),
?_assertEqual(false, between("+1 seconds", qdate:to_string("n/j/Y g:ia"), "+2 seconds")),
?_assertEqual(false, between("5 seconds ago","1 second ago"))
]}.
@ -833,22 +1418,81 @@ arith_tests(_) ->
?_assertEqual({{2015,1,31},{23,59,59}}, to_date(add_months(1, {{2014,12,31},{23,59,59}}))),
?_assertEqual({{2015,2,28},{0,0,0}}, to_date(add_months(2, {{2014,12,31},{0,0,0}}))),
?_assertEqual({{2016,2,28},{0,0,0}}, to_date(add_years(1, {{2015,2,28},{0,0,0}}))),
?_assertEqual({{2017,2,1},{0,0,0}}, to_date(add_months(-11, {{2018,1,1},{0,0,0}}))),
?_assertEqual({{2017,1,1},{0,0,0}}, to_date(add_months(-12, {{2018,1,1},{0,0,0}}))),
?_assertEqual({{2016,12,1},{0,0,0}}, to_date(add_months(-13, {{2018,1,1},{0,0,0}}))),
?_assertEqual({{2018,12,1},{0,0,0}}, to_date(add_months(11, {{2018,1,1},{0,0,0}}))),
?_assertEqual({{2019,1,1},{0,0,0}}, to_date(add_months(12, {{2018,1,1},{0,0,0}}))),
?_assertEqual({{2019,2,1},{0,0,0}}, to_date(add_months(13, {{2018,1,1},{0,0,0}}))),
?_assertEqual({{2018,1,1},{0,0,0}}, to_date(add_months(-11, {{2018,12,1},{0,0,0}}))),
?_assertEqual({{2017,12,1},{0,0,0}}, to_date(add_months(-12, {{2018,12,1},{0,0,0}}))),
?_assertEqual({{2017,11,1},{0,0,0}}, to_date(add_months(-13, {{2018,12,1},{0,0,0}}))),
?_assertEqual({{2019,11,1},{0,0,0}}, to_date(add_months(11, {{2018,12,1},{0,0,0}}))),
?_assertEqual({{2019,12,1},{0,0,0}}, to_date(add_months(12, {{2018,12,1},{0,0,0}}))),
?_assertEqual({{2020,1,1},{0,0,0}}, to_date(add_months(13, {{2018,12,1},{0,0,0}}))),
?_assertEqual({{2014,2,28},{0,0,0}}, to_date(add_months(-24, {{2016,2,29},{0,0,0}}))),
?_assertEqual({{2018,12,15},{0,0,0}}, to_date(add_months(24, {{2016,12,15},{0,0,0}}))),
?_assertEqual({{2012,2,29},{0,0,0}}, to_date(add_months(-48, {{2016,2,29},{0,0,0}}))),
?_assertEqual({{2016,2,29},{0,0,0}}, to_date(add_months(-1, {{2016,3,31},{0,0,0}}))),
?_assertEqual({{2017,2,28},{0,0,0}}, to_date(add_years(1, {{2016,2,29},{0,0,0}}))),
?_assertEqual({{2015,3,1},{0,0,0}}, to_date(add_days(1, {{2015,2,28},{0,0,0}}))),
?_assertEqual({{2015,3,3},{0,0,0}}, to_date(add_days(3, {{2015,2,28},{0,0,0}})))
?_assertEqual({{2015,3,3},{0,0,0}}, to_date(add_days(3, {{2015,2,28},{0,0,0}}))),
?_assertEqual({{2017,1,2},{0,0,0}}, beginning_week({{2017,1,2},{0,0,0}})),
?_assertEqual({{2017,1,2},{0,0,0}}, beginning_week({{2017,1,3},{0,0,0}})),
?_assertEqual({{2017,1,3},{0,0,0}}, beginning_week(2, {{2017,1,4},{0,0,0}})),
?_assertEqual({{2016,12,29},{0,0,0}}, beginning_week(4, {{2017,1,4},{0,0,0}})),
?_assertEqual({{2016,12,31},{0,0,0}}, beginning_week(6, {{2017,1,6},{0,0,0}})),
?_assertEqual({{2017,1,1},{0,0,0}}, beginning_week(7, {{2017,1,6},{0,0,0}})),
?_assertEqual(0, age("1981-01-15", "1981-12-31")),
?_assertEqual(39, age("1981-01-15", "2020-01-15")),
?_assertEqual(39, age("1981-01-15", "2020-01-15 12am")),
?_assertEqual(38, age("1981-01-15", "2020-01-14")),
?_assertEqual(38, age("1981-01-15", "2020-01-14 11:59pm")),
%% checking pre-unix-epoch
?_assertEqual(100, age("1901-01-01","2001-01-01")),
?_assertEqual(20, age("1900-01-01", "1920-01-01")),
?_assertEqual(0, age_days("2020-11-20 12am","2020-11-20 11:59:59pm")),
?_assertEqual(1, age_days("2020-11-19 11:59:59pm","2020-11-20 11:59:59pm")),
?_assertEqual(7, age_days("2020-11-20","2020-11-27")),
?_assertEqual(7, age_days("2020-11-27","2020-12-04"))
]}.
preserve_ms_true_tests(_) ->
application:set_env(qdate, preserve_ms, true),
{inorder, [
?_assertEqual({{2021,5,8},{23,0,16,472000}}, qdate:to_date(<<"UTC">>,<<"2021-5-09 0:0:16.472+01:00">>)),
?_assertEqual(<<"2021-05-08T23:00:16.472000+00:00">>, qdate:to_string(<<"Y-m-d\\TH:i:s.fP">>, <<"UTC">>,<<"2021-5-09 0:0:16.472+01:00">>)),
?_assertEqual(<<"2021-05-08T23:00:16+00:00">>, qdate:to_string(<<"Y-m-d\\TH:i:sP">>, <<"UTC">>,<<"2021-5-09 0:0:16.472+01:00">>)),
?_assertEqual(<<"2021-05-08T23:00:16.472000+00:00">>, qdate:to_string(<<"c">>, <<"UTC">>,<<"2021-5-09 0:0:16.472+01:00">>))
]}.
preserve_ms_false_tests(_) ->
application:set_env(qdate, preserve_ms, false),
{inorder, [
?_assertEqual({{2021,5,8},{23,0,16}}, qdate:to_date(<<"UTC">>,<<"2021-5-09 0:0:16.472+01:00">>)),
?_assertEqual(<<"2021-05-08T23:00:16.0+00:00">>, qdate:to_string(<<"Y-m-d\\TH:i:s.fP">>, <<"UTC">>,<<"2021-5-09 0:0:16.472+01:00">>)),
?_assertEqual(<<"2021-05-08T23:00:16+00:00">>, qdate:to_string(<<"Y-m-d\\TH:i:sP">>, <<"UTC">>,<<"2021-5-09 0:0:16.472+01:00">>)),
?_assertEqual(<<"2021-05-08T23:00:16+00:00">>, qdate:to_string(<<"c">>, <<"UTC">>,<<"2021-5-09 0:0:16.472+01:00">>))
]}.
start_test() ->
application:start(qdate),
qdate:start(),
set_timezone(?SELF_TZ),
set_timezone(?SITE_KEY,?SITE_TZ),
set_timezone(?USER_KEY,?USER_TZ),
register_parser(compressed,fun compressed_parser/1),
register_parser(microsoft_date,fun microsoft_parser/1),
register_parser(parse_relative, fun parse_relative/1),
register_format(shortdate,"n/j/Y"),
register_format(longdate,"n/j/Y g:ia").
@ -870,7 +1514,7 @@ compressed_parser(_) ->
microsoft_parser(FloatDate) when is_float(FloatDate) ->
try
DaysSince1900 = floor(FloatDate),
DaysSince1900 = flooring(FloatDate),
Days0to1900 = calendar:date_to_gregorian_days(1900,1,1),
GregorianDays = Days0to1900 + DaysSince1900,
Date = calendar:gregorian_days_to_date(GregorianDays),
@ -887,3 +1531,5 @@ microsoft_parser(_) ->
stop_test(_) ->
ok.
-endif.

15
src/qdate_app.erl Normal file
View file

@ -0,0 +1,15 @@
-module(qdate_app).
-behaviour(application).
%% Application callbacks
-export([start/2, stop/1]).
start(_StartType, _StartArgs) ->
qdate_sup:start_link().
stop(_State) ->
ok.

View file

@ -1,12 +1,9 @@
% vim: ts=4 sw=4 et
% Copyright (c) 2013-2015 Jesse Gumm
% Copyright (c) 2013-2021 Jesse Gumm
% See LICENSE for licensing information.
%
% NOTE: You'll probably notice that this isn't *actually* a server. It *used*
% to be a server, but is now instead just where we interact with the qdate
% application environment. Anyway, sorry for the confusion.
-module(qdate_srv).
-behaviour(gen_server).
-export([
set_timezone/1,
@ -28,6 +25,19 @@
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}).
@ -39,6 +49,42 @@
-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
set_timezone(TZ) ->
@ -91,26 +137,41 @@ get_formats() ->
%% App Vars
set_env(Key, Val) ->
application:set_env(qdate, ?KEY(Key), Val).
gen_server:call(?SERVER, {set, ?KEY(Key), Val}).
get_env(Key) ->
get_env(Key, undefined).
get_env(Key, Default) ->
%% Soon, this can just be replaced with application:get_env/3
%% which was introduced in R16B.
case application:get_env(qdate, ?KEY(Key)) of
undefined -> Default;
{ok, Val} -> Val
case ets:lookup(?TABLE, ?KEY(Key)) of
[{__Key, Val}] -> Val;
[] -> Default
end.
unset_env(Key) ->
application:unset_env(qdate, ?KEY(Key)).
gen_server:call(?SERVER, {unset, ?KEY(Key)}).
get_all_env(FilterTag) ->
All = application:get_all_env(qdate),
%% Maybe this is a little nasty.
[{Key, V} || {{?BASETAG, {Tag, Key}}, V} <- All, Tag==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
@ -123,3 +184,9 @@ put_pd(Key, Val) ->
unset_pd(Key) ->
put_pd(Key, undefined).

32
src/qdate_sup.erl Normal file
View file

@ -0,0 +1,32 @@
-module(qdate_sup).
-behaviour(supervisor).
%% API
-export([start_link/0]).
%% Supervisor callbacks
-export([init/1]).
-define(SERVER, ?MODULE).
start_link() ->
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
init([]) ->
SupFlags = #{},
ChildSpec = #{
id=>qdate_srv,
start=>{qdate_srv, start_link, []}
},
{ok, {SupFlags, [ChildSpec]}}.
%%%===================================================================
%%% Internal functions
%%%===================================================================