Compare commits

..

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

17 changed files with 122 additions and 1232 deletions

View file

@ -1,47 +0,0 @@
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,10 +1,8 @@
*~
*.beam
*.sw?
*.iml
deps/
ebin/
.eunit/
.idea/
_build
doc/
rebar.lock

11
.travis.yml Normal file
View file

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

View file

@ -1,50 +1,8 @@
## 0.7.3
## 0.5.0 (in development)
* 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)
* Update to rebar3 and add hex compatability. (@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)
* Remove R14 from travis testing.
## 0.4.2

View file

View file

@ -1,41 +1,21 @@
REBAR = $(shell pwd)/rebar3
all: compile
# 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
# rebar3.mk adds a new rebar3 rule to your Makefile
# (see https://github.com/choptastic/rebar3.mk) for full info
include rebar3.mk
compile: rebar3
compile:
$(REBAR) compile
update: rebar3
update:
$(REBAR) update
test:
EUNIT=1 $(REBAR) compile
EUNIT=1 $(REBAR) eunit
test: compile
$(REBAR) eunit
dialyzer: compile
DIALYZER=1 $(REBAR) dialyzer
dev:
mkdir -p _checkouts
cd _checkouts; git clone https://github.com/choptastic/qdate_localtime
run: rebar3
run:
$(REBAR) shell
push_tags:
git push --tag
pull_tags:
git pull --tag
publish: rebar3 pull_tags
$(REBAR) hex publish
publish:
$(REBAR) as pkg upgrade
$(REBAR) as pkg hex publish
$(REBAR) upgrade

View file

@ -1,6 +1,6 @@
# qdate - Erlang Date and Timezone Library
[![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)
[![Build Status](https://travis-ci.org/choptastic/qdate.png?branch=master)](https://travis-ci.org/choptastic/qdate)
## Purpose
@ -98,16 +98,14 @@ 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`. `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`.
application variable `default_timezone`.
+ 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 you're converting a datetime from one timezone to another, there
Sometimes, when youre 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
@ -192,72 +190,24 @@ ok
`qdate` provides a few convenience functions for performing date comparisons.
+ `compare(A, B) -> -1|0|1` - Like C's `strcmp`, returns:
+ `compare(A, B)` - 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) -> true|false` - Operator is an infix comparison operator, and
the function will return a boolean. Will return `true` if:
+ `compare(A, Operator, B)` - Operator is an infix comparison operator, and
the function 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`
@ -498,31 +448,8 @@ 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
@ -536,7 +463,7 @@ qdate:between(qdate:add_minutes(-15), Date, qdate:add_minutes(15)).
%% 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
%% easily refer to that format by its key.
%% easiy 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").
@ -692,61 +619,10 @@ ok
%% 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
(not fully tested yet, but will have full tests for 0.4.0)
The current implementation of qdate's date arithmetic returns Unixtimes.
There are 8 main functions for date arithmetic:
@ -778,89 +654,6 @@ 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
@ -887,6 +680,7 @@ 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`.

View file

@ -1,20 +0,0 @@
%% 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}
]}].

View file

@ -2,23 +2,25 @@
%% vim:ts=4 sw=4 et ft=erlang
{cover_enabled, true}.
{dialyzer, [
{exclude_apps, []},
{warnings, []}
]}.
%% For rebar2 compat
{deps,
[
erlware_commons,
{qdate_localtime, "~> 1.2.0"}
%% This uses an older erlware_commons version so retain compatibility with
%% rebar2. v0.16.1 introduced a 'cf' dependency, which seems to cause
%% breakage.
{erlware_commons, ".*", {git, "git://github.com/erlware/erlware_commons.git", {tag, "v0.15.0"}}},
%% We'll temporarily still use choptastic/erlang_localtime until
%% https://github.com/dmitryme/erlang_localtime/pull/24 gets merged. Then we
%% can switch to the mainline repo
{erlang_localtime, ".*", {git, "git://github.com/choptastic/erlang_localtime.git", {branch, master}}}
]}.
{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">>}]}.
%% for rebar3
{profiles,
[{pkg,
[{deps,
[
erlware_commons,
erlang_localtime
]}]}]}.

View file

@ -1,15 +0,0 @@
%% -*- 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.

View file

@ -1,14 +0,0 @@
{"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">>}]}
].

BIN
rebar3 Executable file

Binary file not shown.

View file

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

View file

@ -1,11 +1,9 @@
% vim: ts=4 sw=4 et
% Copyright (c) 2013-2023 Jesse Gumm
% Copyright (c) 2013 Jesse Gumm
% See LICENSE for licensing information.
%
-module(qdate).
%-compile(export_all).
-export([
start/0,
stop/0
@ -26,50 +24,9 @@
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,
between/2,
between/3,
between/5
]).
-export([
sort/1,
sort/2,
sort/3
compare/3
]).
-export([
@ -87,29 +44,9 @@
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,
@ -129,9 +66,6 @@
clear_timezone/1
]).
-export([
parse_relative/1
]).
%% Exported for API compatibility with ec_date
-export([
@ -140,32 +74,28 @@
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:ensure_all_started(qdate).
application:load(qdate).
stop() ->
ok.
@ -179,7 +109,6 @@ 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});
@ -246,13 +175,7 @@ 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 = case Date of
{_, {_,_,_,_}} ->
%% Have milliseconds
"H:i:s.fP";
_ ->
"H:i:sP"
end,
Format2 = "H:i:sP",
to_string_worker(Format1, ToTZ, Disamb, Date)
++ "T"
++ to_string_worker(Format2, ToTZ, Disamb, Date)
@ -303,7 +226,6 @@ 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) ->
@ -318,13 +240,9 @@ 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);
{{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
@ -361,10 +279,9 @@ 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,_}) when is_integer(MegaSecs), is_integer(Secs) ->
to_unixtime(_, {MegaSecs,Secs,_}) ->
MegaSecs*1000000 + Secs;
to_unixtime(Disamb, ToParse) ->
%% We want to treat all unixtimes as GMT
@ -383,12 +300,11 @@ 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} when is_integer(Standard), is_integer(Daylight) ->
{ambiguous, Standard, Daylight} ->
{ambiguous,
unixtime_to_now(Standard),
unixtime_to_now(Daylight)};
@ -397,144 +313,10 @@ 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),
@ -544,7 +326,6 @@ 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
@ -555,93 +336,15 @@ 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 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
@ -702,48 +405,6 @@ 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}}),
@ -759,22 +420,12 @@ 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,
fix_year_month({Y + YearsOver, M-(YearsOver*12)});
{Y + YearsOver, M-(YearsOver*12)};
fix_year_month({Y, M}) when M < 1 ->
YearsUnder = (abs(M-1) div 12) + 1,
fix_year_month({Y - YearsUnder, M+(YearsUnder*12)});
{Y - YearsUnder, M+(YearsUnder*12)};
fix_year_month({Y, M}) ->
{Y, M}.
@ -809,170 +460,6 @@ 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 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
@ -984,7 +471,6 @@ 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.
@ -1002,8 +488,6 @@ 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}) ->
@ -1039,33 +523,15 @@ 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_timezone();
undefined -> ?DEFAULT_TZ;
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);
@ -1080,14 +546,13 @@ 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;
true ->
?else ->
{ambiguous, Standard, Daylight}
end.
@ -1103,7 +568,7 @@ set_timezone(Key,TZ) ->
qdate_srv:set_timezone(Key, TZ).
get_timezone() ->
?DETERMINE_TZ.
qdate_srv:get_timezone().
get_timezone(Key) ->
qdate_srv:get_timezone(Key).
@ -1154,8 +619,6 @@ try_parsers(_RawDate,[]) ->
undefined;
try_parsers(RawDate,[{ParserKey,Parser}|Parsers]) ->
try Parser(RawDate) of
Timestamp when is_integer(Timestamp) ->
{Timestamp, "GMT"};
{{_,_,_},{_,_,_}} = DateTime ->
{DateTime,undefined};
{DateTime={{_,_,_},{_,_,_}},Timezone} ->
@ -1165,8 +628,8 @@ try_parsers(RawDate,[{ParserKey,Parser}|Parsers]) ->
Other ->
throw({invalid_parser_return_value,[{parser_key,ParserKey},{return,Other}]})
catch
?WITH_STACKTRACE(Error, Reason, Stacktrace)
throw({error_in_parser,[{error,{Error,Reason}},{parser_key,ParserKey}, {stacktrace, Stacktrace}]})
Error:Reason ->
throw({error_in_parser,[{error,{Error,Reason}},{parser_key,ParserKey}]})
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
@ -1185,7 +648,7 @@ deregister_format(Key) ->
unixtime_to_now(T) when is_integer(T) ->
MegaSec = flooring(T/1000000),
MegaSec = floor(T/1000000),
Secs = T - MegaSec*1000000,
{MegaSec,Secs,0}.
@ -1193,10 +656,10 @@ unixtime_to_date(T) ->
Now = unixtime_to_now(T),
calendar:now_to_datetime(Now).
flooring(N) when N >= 0 ->
erlang:trunc(N);
flooring(N) when N < 0 ->
Int = erlang:trunc(N),
floor(N) when N >= 0 ->
trunc(N);
floor(N) when N < 0 ->
Int = trunc(N),
if
Int==N -> Int;
true -> Int-1
@ -1206,8 +669,6 @@ flooring(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
@ -1235,30 +696,6 @@ 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})),
@ -1317,20 +754,6 @@ 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}}))),
@ -1346,13 +769,8 @@ tz_tests(_) ->
?_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({{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"))
?_assertEqual({{1970, 1, 1}, {1, 0, 0}}, to_date("CET", "1970-01-01T00:00:00Z"))
]}.
@ -1418,81 +836,22 @@ 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({{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"))
?_assertEqual({{2015,3,3},{0,0,0}}, to_date(add_days(3, {{2015,2,28},{0,0,0}})))
]}.
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() ->
qdate:start(),
application:start(qdate),
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").
@ -1514,7 +873,7 @@ compressed_parser(_) ->
microsoft_parser(FloatDate) when is_float(FloatDate) ->
try
DaysSince1900 = flooring(FloatDate),
DaysSince1900 = floor(FloatDate),
Days0to1900 = calendar:date_to_gregorian_days(1900,1,1),
GregorianDays = Days0to1900 + DaysSince1900,
Date = calendar:gregorian_days_to_date(GregorianDays),
@ -1531,5 +890,3 @@ microsoft_parser(_) ->
stop_test(_) ->
ok.
-endif.

View file

@ -1,15 +0,0 @@
-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,9 +1,12 @@
% vim: ts=4 sw=4 et
% Copyright (c) 2013-2021 Jesse Gumm
% Copyright (c) 2013-2015 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,
@ -25,19 +28,6 @@
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}).
@ -49,42 +39,6 @@
-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) ->
@ -137,41 +91,26 @@ get_formats() ->
%% App Vars
set_env(Key, Val) ->
gen_server:call(?SERVER, {set, ?KEY(Key), Val}).
application:set_env(qdate, ?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
%% 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
end.
unset_env(Key) ->
gen_server:call(?SERVER, {unset, ?KEY(Key)}).
application:unset_env(qdate, ?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().".
All = application:get_all_env(qdate),
%% Maybe this is a little nasty.
[{Key, V} || {{?BASETAG, {Tag, Key}}, V} <- All, Tag==FilterTag].
%% ProcDic Vars
@ -184,9 +123,3 @@ put_pd(Key, Val) ->
unset_pd(Key) ->
put_pd(Key, undefined).

View file

@ -1,32 +0,0 @@
-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
%%%===================================================================