Compare commits

..

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

17 changed files with 105 additions and 1265 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,6 @@
*~ *~
*.beam *.beam
*.sw? *.sw?
*.iml
deps/ deps/
ebin/ ebin/
.eunit/ .eunit/
.idea/
_build
doc/

13
.travis.yml Normal file
View file

@ -0,0 +1,13 @@
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,60 +1,3 @@
## 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 ## 0.4.0
* Remove dependency on a running server for tracking application state. * Remove dependency on a running server for tracking application state.

View file

View file

@ -1,41 +1,13 @@
all: compile all: get-deps compile
# Check if rebar3.mk exists, and if not, download it get-deps:
ifeq ("$(wildcard rebar3.mk)","") ./rebar get-deps
$(shell curl -O https://raw.githubusercontent.com/choptastic/rebar3.mk/master/rebar3.mk)
endif
# rebar3.mk adds a new rebar3 rule to your Makefile compile:
# (see https://github.com/choptastic/rebar3.mk) for full info ./rebar compile
include rebar3.mk
compile: rebar3 test: get-deps compile
$(REBAR) compile ./rebar skip_deps=true eunit
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 # 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 ## 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 `set_timezone/1` only applies to that *specific* process. If none is
specified. specified.
+ If no timezone is specified for the process, `qdate` looks at the `qdate` + If no timezone is specified for the process, `qdate` looks at the `qdate`
application variable `default_timezone`. `default_timezone` can be either a application variable `default_timezone`.
hard-specified timezone, or a `{Module, Function}` tuple. The tuple format
should return either a timezone or the atom `undefined`.
+ If no timezone is specified by either of the above, `qdate` assumes "GMT" + If no timezone is specified by either of the above, `qdate` assumes "GMT"
for all dates. for all dates.
+ A timezone value of `auto` will act as if no timezone is specified. + A timezone value of `auto` will act as if no timezone is specified.
#### Disambiguating Ambiguous Timezone Conversions #### 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 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, 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 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. `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. + `0`: `A` and `B` are exactly the same.
+ `-1`: `A` is less than (before) `B`. + `-1`: `A` is less than (before) `B`.
+ `1`: `A` is greater than (after) `B`. + `1`: `A` is greater than (after) `B`.
+ `compare(A, Operator, B) -> true|false` - Operator is an infix comparison operator, and + `compare(A, Operator, B)` - Operator is an infix comparison operator, and
the function will return a boolean. Will return `true` if: the function will return true if:
+ `'='`, or `'=='` - `A` is the same time as `B` + `'='`, or `'=='` - `A` is the same time as `B`
+ `'/='`, or `'=/='` or `'!='` - `A` is not the same time as `B` + `'/='`, or `'=/='` or `'!='` - `A` is not the same time as `B`
+ `'<'` - `A` is before `B` + `'<'` - `A` is before `B`
+ `'>'` - `A` is after `B` + `'>'` - `A` is after `B`
+ `'=<'` or `'<='` - `A` is before or equal to `B` + `'=<'` or `'<='` - `A` is before or equal to `B`
+ `'>='` or `'=>'` - `A` is after or equal to `B` + `'>='` or `'=>'` - `A` is after or equal to `B`
+ `between(A, Date, B) -> true|false` - The provided `Date` is (inclusively)
between `A` and `B`. That is, `A =< Date =< B`.
+ `between(A, B) -> true|false` - shortcut for `between(A, now(), B)`
+ `between(A, Op1, Date, Op2, B) -> true|false` - the fully verbose option of
comparing between. `Op1` and `Op2` are custom operators. For example, if
you wanted to do an exclusive `between`, you can do:
`between(A, '<', Date, '<', B)`
**Note 1:** `Operator` must be an atom. **Note 1:** `Operator` must be an atom.
**Note 2:** These functions will properly compare times with different timezones **Note 2:** These functions will properly compare times with different timezones
(for example: `compare("12am CST",'==',"1am EST")` will properly return true) (for example: `compare("12am CST",'==',"1am EST")` will properly return true)
### Sorting
`qdate` also provides a convenience functions for sorting lists of dates/times:
+ `sort(List)` - Sort the list in ascending order of earliest to latest.
+ `sort(Op, List)` - Sort the list where `Op` is one of the following:
+ `'<'` or `'=<'` or `'<='` - Sort ascending
+ `'>'` or `'>='` or `'=>'` - Sort descending
+ `sort(Op, List, Opts)` - Sort the list according to the `Op`, with options provided in `Opts`. `Opts` is a proplist of the following options:
+ `{non_dates, NonDates}` - Tells it how to handle non-dates. `NonDates` can be any of the following:
+ `back` **(default)** - put any non-dates at the end (the back) of the list
+ `front` - put any non-dates at the beginning of the list
+ `crash` - if there are any non-dates, crash.
Example:
```erlang
1> Dates = ["non date string", <<"garbage">>,
1466200861, "2011-01-01", "7pm",
{{1999,6,21},{5,30,0}}, non_date_atom, {some_tuple,123}].
2> qdate:sort('>=', Dates, [{non_dates, front}]).
[<<"garbage">>,"non date string",
{some_tuple,123},
non_date_atom,1466200861,"2011-01-01",
{{1999,6,21},{5,30,0}},
"7pm"]
```
**Note 1:** This sorting is optimized to be much faster than using a home-grown
sort using the `compare` functions, as this normalizes the items in the list
before comparing (so it's only really comparing integers, which is quite fast).
**Note 2:** This is one of the few qdate functions that don't have the "Date"
as the last argument. This follows the pattern in Erlang/OTP to put options as
the last argument (for example, `re:run/3`)
**Note 3:** You'll notice that qdate's sorting retains the original terms (in
the example above, we compared a datetime tuple, unix timestamp, and two
strings (along with a number of non-dates, which were just prepended to the
front of the list).
### Timezone Functions ### Timezone Functions
+ `set_timezone(Key, TZ)` - Set the timezone to TZ for the key `Key` + `set_timezone(Key, TZ)` - Set the timezone to TZ for the key `Key`
@ -498,31 +448,8 @@ the crash.
**Another Note:** Custom parsers are expected to return either: **Another Note:** Custom parsers are expected to return either:
+ A `datetime()` tuple. (ie {{2012,12,21},{14,45,23}}). + A `datetime()` tuple. (ie {{2012,12,21},{14,45,23}}).
+ An integer, which represents the Unix timestamp.
+ The atom `undefined` if this parser is not a match for the supplied value + The atom `undefined` if this parser is not a match for the supplied value
#### Included Parser: Relative Times
`qdate` ships with an optional relative time parser. To speed up performance
(since this parser uses regular expressions), this parser is disabled by
default. But if you wish to use it, make sure you call
`qdate:register_parser(parse_relative, fun qdate:parse_relative/1)`.
Doing this allows you to parse relative time strings of the following formats:
+ "1 hour ago"
+ "-15 minutes"
+ "in 45 days"
+ "+2 years"
And doing so allows you to construct slightly more readable comparison calls
for sometimes common comparisons. For example, the following two calls are identical:
```erlang
qdate:between("-15 minutes", Date, "+15 minutes").
qdate:between(qdate:add_minutes(-15), Date, qdate:add_minutes(15)).
```
### Registering Custom Formats ### Registering Custom Formats
@ -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 %% But, you don't have to: if that's a common format you use in your
%% application, you can register your format with the `qdate` server, and then %% application, you can register your format with the `qdate` server, and then
%% 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 %% So let's take that format and register it
16> qdate:register_format(longdate, "l, F jS, Y g:i A T"). 16> qdate:register_format(longdate, "l, F jS, Y g:i A T").
@ -692,76 +619,25 @@ ok
%% that timezone to our intended timezone. %% that timezone to our intended timezone.
``` ```
## Beginning or Ending of time periods (hours, days, years, weeks, etc)
qdate can determine beginnings and endings of time periods, like "beginning of the month"
This is abstracted to `beginning_X` functions, which return a date/time format
with the dates and times truncated to the specified level.
+ `beginning_minute(Date)`
+ `beginning_hour(Date)`
+ `beginning_day(Date)`
+ `beginning_month(Date)`
+ `beginning_year(Date)`
There are also 0-arity versions of the above, in which `Date` is assumed to be
"right now". For example, calling `qdate:beginning_month()` would return
midnight on the first day of the current month.
#### Beginning of Week
qdate can also do a special "beginning" case, particularly the "beginning of
the week" calculation. This has three forms, specifically:
+ `beginning_week()` - Returns first day of the current week.
+ `beginning_week(Date)` - Assumes the beginning of the week is Monday
(chosen because Erlang's calendar:day_of_the_week uses 1=Monday and
7=Sunday).
+ `beginning_week(DayOfWeek, Date)` - Calculates the beginning of the week
based on the provided `DayOfWeek`. Valid values for DayOfWeek are the
integers 1-7 or the atom versions of the days of the week. Specifically:
* Monday: `1 | monday | mon`
* Tuesday: `2 | tuesday | tue`
* Wednesday: `3 | wednesday | wed`
* Thursday: `4 | thursday | thu`
* Friday: `5 | friday | fri`
* Saturday: `6 | saturday | sat`
* Sunday: `7 | sunday | sun`
These all return 12am on the day that is the first day of the week of the
provided date.
(My apologies to non-English speakers. I'm a lazy American who only speaks
English, hence the Anglocentric day names).
### End of time period
There are also the related `end_X` functions available, using the same
conventions, except return the last second of that time period.
So `end_month("2016-01-05")` will return the unix timestamp representing
"2016-01-31 11:59:59pm"
## Date Arithmetic ## 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. The current implementation of qdate's date arithmetic returns Unixtimes.
There are 8 main functions for date arithmetic: There are 8 main functions for date arithmetic:
+ `add_seconds(Seconds, Date)` + `add_seconds(Seconds, Date)`
+ `add_minutes(Minutes, Date)` + `add_minutes(Minutes, Date)`
+ `add_hours(Hours, Date)` + `add_hours(Hours, Date)`
+ `add_days(Days, Date)` + `add_days(Days, Date)`
+ `add_weeks(Weeks, Date)` + `add_weeks(Weeks, Date)`
+ `add_months(Months, Date)` + `add_months(Months, Date)`
+ `add_years(Years, Date)` + `add_years(Years, Date)`
+ `add_date(DateToAdd, Date)` - `DateToAdd` is a shortcut way of adding + `add_date(DateToAdd, Date)` - `DateToAdd` is a shortcut way of adding
numerous options. For example. `qdate:add_date({{1, 2, -3}, {-500, 20, 0}})` 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 will add 1 year, add 2 months, subtract 3 days, subtract 500 hours, add 20
minutes, and not make any changes to seconds. minutes, and not make any changes to seconds.
For the date arithmetic functions, `Date`, like all `qdate` functions, can be any For the date arithmetic functions, `Date`, like all `qdate` functions, can be any
format. format.
@ -778,89 +654,6 @@ There are 7 other arithmetic functions that take a single argument, and these do
+ `add_months(Months)` + `add_months(Months)`
+ `add_years(Years)` + `add_years(Years)`
## Date and Time Ranges
qdate provides a number of `range` functions that give applicable dates/times
within a start and end time. For example, "All days from 2015-01-01 to today",
"every 3rd month from 2000-01-01 to 2009-12-31", or "every 15 minutes from
midnight to 11:59pm on 2015-04-15".
The functions are as follows:
+ `range_seconds(Interval, Start, End)`
+ `range_minutes(Interval, Start, End)`
+ `range_hours(Interval, Start, End)`
+ `range_days(Interval, Start, End)`
+ `range_weeks(Interval, Start, End)`
+ `range_months(Interval, Start, End)`
+ `range_years(Interval, Start, End)`
Where `Interval` is the number of seconds/days/years/etc.
So for example:
```erlang
%% Get every 15th minute from "2015-04-15 12:00am to 2015-04-15 11:59am"
> qdate:range_minutes(15, "2015-04-15 12:00am", "2015-04-15 11:59am").
[1429056000,1429056900,1429057800,1429058700,1429059600,
1429060500,1429061400,1429062300,1429063200,1429064100,
1429065000,1429065900,1429066800,1429067700,1429068600,
1429069500,1429070400,1429071300,1429072200,1429073100,
1429074000,1429074900,1429075800,1429076700,1429077600,
1429078500,1429079400,1429080300,1429081200|...]
%% Get every day of April, 2014
> qdate:range_days(1, "2014-04-01", "2014-04-30").
[1396310400,1396396800,1396483200,1396569600,1396656000,
1396742400,1396828800,1396915200,1397001600,1397088000,
1397174400,1397260800,1397347200,1397433600,1397520000,
1397606400,1397692800,1397779200,1397865600,1397952000,
1398038400,1398124800,1398211200,1398297600,1398384000,
1398470400,1398556800,1398643200,1398729600|...]
```
Note, that the return value (just like qdate's arithmetic functions) is a list
of integers. These integers are unix timestamps and can be easily formatted
with qdate:
```erlang
> Mins = qdate:range_minutes(15, "2015-04-15 12:00am", "2015-04-15 11:59am"),
> [qdate:to_string("Y-m-d h:ia", M) || M <- Mins].
["2015-04-15 00:00am","2015-04-15 00:15am",
"2015-04-15 00:30am","2015-04-15 00:45am",
"2015-04-15 01:00am","2015-04-15 01:15am",
"2015-04-15 01:30am","2015-04-15 01:45am",
"2015-04-15 02:00am","2015-04-15 02:15am",
"2015-04-15 02:30am","2015-04-15 02:45am",
"2015-04-15 03:00am","2015-04-15 03:15am",
"2015-04-15 03:30am","2015-04-15 03:45am",
"2015-04-15 04:00am","2015-04-15 04:15am",
"2015-04-15 04:30am","2015-04-15 04:45am",
"2015-04-15 05:00am","2015-04-15 05:15am",
"2015-04-15 05:30am","2015-04-15 05:45am",
"2015-04-15 06:00am","2015-04-15 06:15am",
"2015-04-15 06:30am","2015-04-15 06:45am",
[...]|...]
```
Also note that the range functions are *inclusive*.
## Age Comparison
There are two main comparisons right now, age in years, and age in days.
+ `age(Date)` - Number of years since `Date`
+ `age(FromDate, ToDate)` - Number of years between `FromDate` to `ToDate`
+ `age_days(Date)` - Number of full days since `Date` (for example from `2pm` yesterday to `1:59pm` today is still 0.
+ `age_days(FromDate, ToDate)` - Number of full days between `FromDate` and `ToDate`.
## Configuration Sample
There is a sample configuration file can be found in the root of the qdate
directory. Or you can just [look at it
here](https://github.com/choptastic/qdate/blob/master/qdate.config).
## Thanks ## Thanks
A few shoutouts to [Dale Harvey](http://github.com/daleharvey) and the A few shoutouts to [Dale Harvey](http://github.com/daleharvey) and the
@ -874,8 +667,6 @@ not exist.
+ [Mark Allen](https://github.com/mrallen1) + [Mark Allen](https://github.com/mrallen1)
+ [Christopher Phillips](https://github.com/lostcolony) + [Christopher Phillips](https://github.com/lostcolony)
+ [Nicholas Lundgaard](https://github.com/nlundgaard-al) + [Nicholas Lundgaard](https://github.com/nlundgaard-al)
+ [Alejandro Ramallo](https://github.com/aramallo)
+ [Heinz Gies](https://github.com/Licenser)
## Changelog ## Changelog
@ -887,6 +678,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 + Make `qdate` backend-agnostic (allow specifying either ec_date or dh_date as
the backend) the backend)
+ Add `-spec` and `-type` info for dialyzer + Add `-spec` and `-type` info for dialyzer
+ Provide a sample qdate.config for users to see
+ Research the viability of [ezic](https://github.com/drfloob/ezic) for a + Research the viability of [ezic](https://github.com/drfloob/ezic) for a
timezone backend replacement for `erlang_localtime`. timezone backend replacement for `erlang_localtime`.

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}
]}].

BIN
rebar vendored Executable file

Binary file not shown.

View file

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

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">>}]}
].

View file

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

View file

@ -1,11 +1,9 @@
% vim: ts=4 sw=4 et % vim: ts=4 sw=4 et
% Copyright (c) 2013-2023 Jesse Gumm % Copyright (c) 2013 Jesse Gumm
% See LICENSE for licensing information. % See LICENSE for licensing information.
% %
-module(qdate). -module(qdate).
%-compile(export_all).
-export([ -export([
start/0, start/0,
stop/0 stop/0
@ -26,50 +24,9 @@
unixtime/0 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([ -export([
compare/2, compare/2,
compare/3, compare/3
between/2,
between/3,
between/5
]).
-export([
sort/1,
sort/2,
sort/3
]). ]).
-export([ -export([
@ -87,29 +44,9 @@
add_months/1, add_months/1,
add_years/2, add_years/2,
add_years/1, add_years/1,
add_unit/2,
add_unit/3,
add_date/2 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([ -export([
register_parser/2, register_parser/2,
register_parser/1, register_parser/1,
@ -129,9 +66,6 @@
clear_timezone/1 clear_timezone/1
]). ]).
-export([
parse_relative/1
]).
%% Exported for API compatibility with ec_date %% Exported for API compatibility with ec_date
-export([ -export([
@ -140,32 +74,28 @@
parse/1 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 %% This the value in gregorian seconds for jan 1st 1970, 12am
%% It's used to convert to and from unixtime, since unixtime starts %% It's used to convert to and from unixtime, since unixtime starts
%% 1970-01-01 12:00am %% 1970-01-01 12:00am
-define(UNIXTIME_BASE,62167219200). -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(DETERMINE_TZ, determine_timezone()).
-define(DEFAULT_DISAMBIG, prefer_standard). -define(DEFAULT_DISAMBIG, prefer_standard).
-define(else, true).
start() -> start() ->
application:ensure_all_started(qdate). application:load(qdate).
stop() -> stop() ->
ok. ok.
@ -179,7 +109,6 @@ to_string(Format, Date) ->
to_string(Format, ToTZ, Date) -> to_string(Format, ToTZ, Date) ->
to_string(Format, ToTZ, ?DEFAULT_DISAMBIG, 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) -> to_string(FormatKey, ToTZ, Disambiguate, Date) when is_atom(FormatKey) orelse is_tuple(FormatKey) ->
Format = case qdate_srv:get_format(FormatKey) of Format = case qdate_srv:get_format(FormatKey) of
undefined -> throw({undefined_format_key,FormatKey}); 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(NewFormat, ToTZ, Disamb, Date) ++ to_string_worker(RestFormat, ToTZ, Disamb, Date);
to_string_worker([$c | RestFormat], ToTZ, Disamb, Date) -> to_string_worker([$c | RestFormat], ToTZ, Disamb, Date) ->
Format1 = "Y-m-d", Format1 = "Y-m-d",
Format2 = case Date of Format2 = "H:i:sP",
{_, {_,_,_,_}} ->
%% Have milliseconds
"H:i:s.fP";
_ ->
"H:i:sP"
end,
to_string_worker(Format1, ToTZ, Disamb, Date) to_string_worker(Format1, ToTZ, Disamb, Date)
++ "T" ++ "T"
++ to_string_worker(Format2, ToTZ, Disamb, Date) ++ to_string_worker(Format2, ToTZ, Disamb, Date)
@ -303,7 +226,6 @@ to_date(RawDate) ->
to_date(ToTZ, RawDate) -> to_date(ToTZ, RawDate) ->
to_date(ToTZ, ?DEFAULT_DISAMBIG, 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, RawDate) when is_binary(RawDate) ->
to_date(ToTZ, Disambiguate, binary_to_list(RawDate)); to_date(ToTZ, Disambiguate, binary_to_list(RawDate));
to_date(ToTZ, Disambiguate, RawDate) when is_binary(ToTZ) -> to_date(ToTZ, Disambiguate, RawDate) when is_binary(ToTZ) ->
@ -317,16 +239,10 @@ to_date(ToTZ, Disambiguate, RawDate) ->
{ParsedDate,ExtractedTZ}; {ParsedDate,ExtractedTZ};
{ParsedDate,ParsedTZ} -> {ParsedDate,ParsedTZ} ->
{ParsedDate,ParsedTZ} {ParsedDate,ParsedTZ}
end, end,
PreserveMs = preserve_ms(),
try raw_to_date(RawDate3) of try raw_to_date(RawDate3) of
D={{_,_,_},{_,_,_}} -> 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 catch
_:_ -> _:_ ->
case raw_to_date(RawDate) of case raw_to_date(RawDate) of
@ -361,10 +277,9 @@ get_deterministic_datetime() ->
to_unixtime(Date) -> to_unixtime(Date) ->
to_unixtime(?DEFAULT_DISAMBIG, Date). to_unixtime(?DEFAULT_DISAMBIG, Date).
-spec to_unixtime(Disamb :: disambiguate(), qdate()) -> {ambiguous, integer(), integer()} | integer().
to_unixtime(_, Unixtime) when is_integer(Unixtime) -> to_unixtime(_, Unixtime) when is_integer(Unixtime) ->
Unixtime; Unixtime;
to_unixtime(_, {MegaSecs,Secs,_}) when is_integer(MegaSecs), is_integer(Secs) -> to_unixtime(_, {MegaSecs,Secs,_}) ->
MegaSecs*1000000 + Secs; MegaSecs*1000000 + Secs;
to_unixtime(Disamb, ToParse) -> to_unixtime(Disamb, ToParse) ->
%% We want to treat all unixtimes as GMT %% We want to treat all unixtimes as GMT
@ -383,12 +298,11 @@ unixtime() ->
to_now(Date) -> to_now(Date) ->
to_now(?DEFAULT_DISAMBIG, Date). to_now(?DEFAULT_DISAMBIG, Date).
-spec to_now(Disamb :: disambiguate(), qdate()) -> erlnow() | {ambiguous, erlnow(), erlnow()}.
to_now(_, Now = {_,_,_}) -> to_now(_, Now = {_,_,_}) ->
Now; Now;
to_now(Disamb, ToParse) -> to_now(Disamb, ToParse) ->
case to_unixtime(Disamb, ToParse) of case to_unixtime(Disamb, ToParse) of
{ambiguous, Standard, Daylight} when is_integer(Standard), is_integer(Daylight) -> {ambiguous, Standard, Daylight} ->
{ambiguous, {ambiguous,
unixtime_to_now(Standard), unixtime_to_now(Standard),
unixtime_to_now(Daylight)}; unixtime_to_now(Daylight)};
@ -397,144 +311,10 @@ to_now(Disamb, ToParse) ->
end. 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 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%% Comparisons %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec compare(A :: qdate(), B :: qdate()) -> integer().
compare(A, B) -> compare(A, B) ->
NowA = to_now(A), NowA = to_now(A),
NowB = to_now(B), NowB = to_now(B),
@ -544,7 +324,6 @@ compare(A, B) ->
NowA > NowB -> 1 NowA > NowB -> 1
end. end.
-spec compare(A :: qdate(), Op :: atom(), B :: qdate()) -> boolean().
compare(A, Op, B) -> compare(A, Op, B) ->
Comp = compare(A, B), Comp = compare(A, B),
case Op of case Op of
@ -555,93 +334,15 @@ compare(A, Op, B) ->
'=/=' -> Comp =/= 0; '=/=' -> Comp =/= 0;
'/=' -> Comp =/= 0; '/=' -> Comp =/= 0;
'before'-> Comp =:= -1;
'<' -> Comp =:= -1; '<' -> Comp =:= -1;
'<=' -> Comp =:= -1 orelse Comp =:= 0; '<=' -> Comp =:= -1 orelse Comp =:= 0;
'=<' -> Comp =:= -1 orelse Comp =:= 0; '=<' -> Comp =:= -1 orelse Comp =:= 0;
'after' -> Comp =:= 1;
'>' -> Comp =:= 1; '>' -> Comp =:= 1;
'>=' -> Comp =:= 1 orelse Comp =:= 0; '>=' -> Comp =:= 1 orelse Comp =:= 0;
'=>' -> Comp =:= 1 orelse Comp =:= 0 '=>' -> Comp =:= 1 orelse Comp =:= 0
end. 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 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%% Date Math %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
@ -702,48 +403,6 @@ add_years(Years, Date) ->
add_years(Years) -> add_years(Years) ->
add_years(Years, os:timestamp()). 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) -> add_date({{AddY, AddM, AddD}, {AddH, AddI, AddS}}, Date) ->
{{Y, M, D}, {H, I, S}} = to_date(Date), {{Y, M, D}, {H, I, S}} = to_date(Date),
Date1 = fix_maybe_improper_date({{Y+AddY, M+AddM, D+AddD}, {H, I, S}}), Date1 = fix_maybe_improper_date({{Y+AddY, M+AddM, D+AddD}, {H, I, S}}),
@ -759,22 +418,12 @@ fix_maybe_improper_date({Date0, Time}) ->
Date = fmid(Date0), Date = fmid(Date0),
{Date, Time}. {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 -> fix_year_month({Y, M}) when M > 12 ->
YearsOver = M div 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 -> fix_year_month({Y, M}) when M < 1 ->
YearsUnder = (abs(M-1) div 12) + 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}) -> fix_year_month({Y, M}) ->
{Y, M}. {Y, M}.
@ -809,170 +458,6 @@ fmid({Y, M, D}) when D < 1 ->
fmid(Date) -> fmid(Date) ->
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 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%% Timezone Stuff %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
@ -984,7 +469,6 @@ get_timezone_shift(TZ, Disambiguate, Date) ->
{error,T} -> {error,T}; {error,T} -> {error,T};
{Sh, _} when Disambiguate==prefer_standard -> Sh; {Sh, _} when Disambiguate==prefer_standard -> Sh;
{_, Sh} when Disambiguate==prefer_daylight -> Sh; {_, Sh} when Disambiguate==prefer_daylight -> Sh;
0 -> {'+', 0, 0};
Sh -> Sh Sh -> Sh
end. end.
@ -1002,8 +486,6 @@ extract_timezone(DateString) when is_list(DateString) ->
end; end;
extract_timezone(Date={{_,_,_},{_,_,_}}) -> extract_timezone(Date={{_,_,_},{_,_,_}}) ->
{Date, ?DETERMINE_TZ}; {Date, ?DETERMINE_TZ};
extract_timezone(Rel={relative, _, _}) ->
{Rel, "GMT"};
extract_timezone(Now={_,_,_}) -> extract_timezone(Now={_,_,_}) ->
{Now, "GMT"}; {Now, "GMT"};
extract_timezone({MiscDate,TZ}) -> extract_timezone({MiscDate,TZ}) ->
@ -1039,33 +521,15 @@ extract_timezone_helper(RevDate, [TZ | TZs]) when length(RevDate) >= length(TZ)
extract_timezone_helper(RevDate, [_TZ | TZs]) -> extract_timezone_helper(RevDate, [_TZ | TZs]) ->
extract_timezone_helper(RevDate, 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() -> determine_timezone() ->
case qdate_srv:get_timezone() of case qdate_srv:get_timezone() of
undefined -> default_timezone(); undefined -> ?DEFAULT_TZ;
TZ -> TZ TZ -> TZ
end. end.
%% If FromTZ is an integer, then it's an integer that represents the number of minutes %% 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 %% relative to GMT. So we convert the date to GMT based on that number, then we can
%% do the other timezone conversion. %% 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) -> date_tz_to_tz(Date, Disambiguate, FromTZ, ToTZ) when is_integer(FromTZ) ->
NewDate = localtime:adjust_datetime(Date, FromTZ), NewDate = localtime:adjust_datetime(Date, FromTZ),
date_tz_to_tz(NewDate, Disambiguate, "GMT", ToTZ); date_tz_to_tz(NewDate, Disambiguate, "GMT", ToTZ);
@ -1080,14 +544,13 @@ date_tz_to_tz(Date, Disambiguate, FromTZ, ToTZ) ->
date_tz_to_tz_both(Date, FromTZ, ToTZ) date_tz_to_tz_both(Date, FromTZ, ToTZ)
end. 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) -> date_tz_to_tz_both(Date, FromTZ, ToTZ) ->
Standard = localtime:local_to_local(Date, FromTZ, ToTZ), Standard = localtime:local_to_local(Date, FromTZ, ToTZ),
Daylight = localtime:local_to_local_dst(Date, FromTZ, ToTZ), Daylight = localtime:local_to_local_dst(Date, FromTZ, ToTZ),
if if
Standard=:=Daylight -> Standard=:=Daylight ->
Standard; Standard;
true -> ?else ->
{ambiguous, Standard, Daylight} {ambiguous, Standard, Daylight}
end. end.
@ -1103,7 +566,7 @@ set_timezone(Key,TZ) ->
qdate_srv:set_timezone(Key, TZ). qdate_srv:set_timezone(Key, TZ).
get_timezone() -> get_timezone() ->
?DETERMINE_TZ. qdate_srv:get_timezone().
get_timezone(Key) -> get_timezone(Key) ->
qdate_srv:get_timezone(Key). qdate_srv:get_timezone(Key).
@ -1153,9 +616,8 @@ try_registered_parsers(RawDate) ->
try_parsers(_RawDate,[]) -> try_parsers(_RawDate,[]) ->
undefined; undefined;
try_parsers(RawDate,[{ParserKey,Parser}|Parsers]) -> try_parsers(RawDate,[{ParserKey,Parser}|Parsers]) ->
io:format("Trying Parser: ~p~n", [ParserKey]),
try Parser(RawDate) of try Parser(RawDate) of
Timestamp when is_integer(Timestamp) ->
{Timestamp, "GMT"};
{{_,_,_},{_,_,_}} = DateTime -> {{_,_,_},{_,_,_}} = DateTime ->
{DateTime,undefined}; {DateTime,undefined};
{DateTime={{_,_,_},{_,_,_}},Timezone} -> {DateTime={{_,_,_},{_,_,_}},Timezone} ->
@ -1165,8 +627,8 @@ try_parsers(RawDate,[{ParserKey,Parser}|Parsers]) ->
Other -> Other ->
throw({invalid_parser_return_value,[{parser_key,ParserKey},{return,Other}]}) throw({invalid_parser_return_value,[{parser_key,ParserKey},{return,Other}]})
catch catch
?WITH_STACKTRACE(Error, Reason, Stacktrace) Error:Reason ->
throw({error_in_parser,[{error,{Error,Reason}},{parser_key,ParserKey}, {stacktrace, Stacktrace}]}) throw({error_in_parser,[{error,{Error,Reason}},{parser_key,ParserKey}]})
end. end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
@ -1185,7 +647,7 @@ deregister_format(Key) ->
unixtime_to_now(T) when is_integer(T) -> unixtime_to_now(T) when is_integer(T) ->
MegaSec = flooring(T/1000000), MegaSec = floor(T/1000000),
Secs = T - MegaSec*1000000, Secs = T - MegaSec*1000000,
{MegaSec,Secs,0}. {MegaSec,Secs,0}.
@ -1193,10 +655,10 @@ unixtime_to_date(T) ->
Now = unixtime_to_now(T), Now = unixtime_to_now(T),
calendar:now_to_datetime(Now). calendar:now_to_datetime(Now).
flooring(N) when N >= 0 -> floor(N) when N >= 0 ->
erlang:trunc(N); trunc(N);
flooring(N) when N < 0 -> floor(N) when N < 0 ->
Int = erlang:trunc(N), Int = trunc(N),
if if
Int==N -> Int; Int==N -> Int;
true -> Int-1 true -> Int-1
@ -1206,8 +668,6 @@ flooring(N) when N < 0 ->
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% TESTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% TESTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-ifdef(EUNIT).
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
%% emulates as if a forum-type website has a Site tz, and a user-specified tz %% emulates as if a forum-type website has a Site tz, and a user-specified tz
@ -1235,30 +695,6 @@ tz_test_() ->
end 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(_) -> test_deterministic_parser(_) ->
{inorder, [ {inorder, [
?_assertEqual(ok, application:set_env(qdate, deterministic_parsing, {now, now})), ?_assertEqual(ok, application:set_env(qdate, deterministic_parsing, {now, now})),
@ -1317,20 +753,6 @@ tz_tests(_) ->
?_assertEqual({{2013,3,6},{18,0,0}}, to_date("GMT","3/7/2013 12:00am +0600")), ?_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")), ?_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) %% parsing, then reformatting the same time with a different timezone using the php "r" (rfc2822)
?_assertEqual("Thu, 07 Mar 2013 12:15:00 -0600", ?_assertEqual("Thu, 07 Mar 2013 12:15:00 -0600",
to_string("r","CST",to_string("r","EST",{{2013,3,7},{13,15,0}}))), to_string("r","CST",to_string("r","EST",{{2013,3,7},{13,15,0}}))),
@ -1345,14 +767,7 @@ tz_tests(_) ->
?_assertEqual(ok, set_timezone("EST")), ?_assertEqual(ok, set_timezone("EST")),
?_assertEqual(555555555,to_unixtime("1987-08-10 00:59:15 GMT")), ?_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({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"))
]}. ]}.
@ -1418,81 +833,22 @@ arith_tests(_) ->
?_assertEqual({{2015,1,31},{23,59,59}}, to_date(add_months(1, {{2014,12,31},{23,59,59}}))), ?_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({{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({{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({{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({{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({{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({{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,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() -> start_test() ->
qdate:start(), application:start(qdate),
set_timezone(?SELF_TZ), set_timezone(?SELF_TZ),
set_timezone(?SITE_KEY,?SITE_TZ), set_timezone(?SITE_KEY,?SITE_TZ),
set_timezone(?USER_KEY,?USER_TZ), set_timezone(?USER_KEY,?USER_TZ),
register_parser(compressed,fun compressed_parser/1), register_parser(compressed,fun compressed_parser/1),
register_parser(microsoft_date,fun microsoft_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(shortdate,"n/j/Y"),
register_format(longdate,"n/j/Y g:ia"). register_format(longdate,"n/j/Y g:ia").
@ -1514,7 +870,7 @@ compressed_parser(_) ->
microsoft_parser(FloatDate) when is_float(FloatDate) -> microsoft_parser(FloatDate) when is_float(FloatDate) ->
try try
DaysSince1900 = flooring(FloatDate), DaysSince1900 = floor(FloatDate),
Days0to1900 = calendar:date_to_gregorian_days(1900,1,1), Days0to1900 = calendar:date_to_gregorian_days(1900,1,1),
GregorianDays = Days0to1900 + DaysSince1900, GregorianDays = Days0to1900 + DaysSince1900,
Date = calendar:gregorian_days_to_date(GregorianDays), Date = calendar:gregorian_days_to_date(GregorianDays),
@ -1531,5 +887,3 @@ microsoft_parser(_) ->
stop_test(_) -> stop_test(_) ->
ok. 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 % vim: ts=4 sw=4 et
% Copyright (c) 2013-2021 Jesse Gumm % Copyright (c) 2013-2015 Jesse Gumm
% See LICENSE for licensing information. % 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). -module(qdate_srv).
-behaviour(gen_server).
-export([ -export([
set_timezone/1, set_timezone/1,
@ -25,19 +28,6 @@
get_formats/0 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 %% Simple wrappers for unique keys
-define(BASETAG, qdate_var). -define(BASETAG, qdate_var).
-define(KEY(Name), {?BASETAG, Name}). -define(KEY(Name), {?BASETAG, Name}).
@ -49,42 +39,6 @@
-define(FORMATTAG, qdate_format). -define(FORMATTAG, qdate_format).
-define(FORMATKEY(Name), {?FORMATTAG, Name}). -define(FORMATKEY(Name), {?FORMATTAG, Name}).
-define(SERVER, ?MODULE).
-define(TABLE, ?MODULE).
-record(state, {}).
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
init([]) ->
error_logger:info_msg("Creating qdate ETS Table: ~p",[?TABLE]),
?TABLE = ets:new(?TABLE, [public, {read_concurrency, true}, named_table]),
{ok, #state{}}.
handle_call({set, Key, Val}, _From, State) ->
ets:insert(?TABLE, {Key, Val}),
{reply, ok, State};
handle_call({unset, Key}, _From, State) ->
ets:delete(?TABLE, Key),
{reply, ok, State};
handle_call(_, _From, State) ->
{reply, invalid_request, State}.
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%% PUBLIC API FUNCTIONS %% PUBLIC API FUNCTIONS
set_timezone(TZ) -> set_timezone(TZ) ->
@ -137,41 +91,26 @@ get_formats() ->
%% App Vars %% App Vars
set_env(Key, Val) -> 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) ->
get_env(Key, undefined). get_env(Key, undefined).
get_env(Key, Default) -> get_env(Key, Default) ->
case ets:lookup(?TABLE, ?KEY(Key)) of %% Soon, this can just be replaced with application:get_env/3
[{__Key, Val}] -> Val; %% which was introduced in R16B.
[] -> Default case application:get_env(qdate, ?KEY(Key)) of
undefined -> Default;
{ok, Val} -> Val
end. end.
unset_env(Key) -> unset_env(Key) ->
gen_server:call(?SERVER, {unset, ?KEY(Key)}). application:unset_env(qdate, ?KEY(Key)).
get_all_env(FilterTag) -> get_all_env(FilterTag) ->
try ets:tab2list(?TABLE) of All = application:get_all_env(qdate),
All -> %% Maybe this is a little nasty.
[{Key, V} || {{?BASETAG, {Tag, Key}}, V} <- All, Tag==FilterTag] [{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 %% ProcDic Vars
@ -184,9 +123,3 @@ put_pd(Key, Val) ->
unset_pd(Key) -> unset_pd(Key) ->
put_pd(Key, undefined). 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
%%%===================================================================