Compare commits

..

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

17 changed files with 297 additions and 1761 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
*.sw?
*.iml
deps/
ebin/
.eunit/
.idea/
_build
doc/

10
.travis.yml Normal file
View file

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

View file

@ -1,87 +0,0 @@
## 0.7.3
* Remove the `?else` macro.
## 0.7.2
* Update the error message when qdate is not started with some better instructions.
* Some minor updates for hex.pm
* Removed the `erlnow()` warning
* Fix some typos in the documentation
* Update the makefile to use [rebar3.mk](https://rebar3.mk)
* Skipped 0.7.1 only because there was a partially tagged 0.7.1 for a while,
but was never published to hex. Just to ensure upgrades are easier, I just skipped
a proper 0.7.1 release
## 0.7.0
* Re-introduce the qdate server for storing qdate timezones, formats, and parsers,
rather than overloading the `application` env vars (since the `application`
module only wants keys to be atoms).
* Convert to using `qdate_localtime` 1.2.0 (which passes dialyzer checks)
* `qdate` is passing dialyzer again
## 0.6.0
* Add `age` and `age_days` functions
* Add option to preserve millisecond accuracy in date parsing and formatting (@Leonardb)
## 0.5.0
* Add `range_X` functions for getting a list of dates/times within a range
(such as `range_day/3` to get a range of days between a start and end date.
* Add `beginning_X` functions to return the beginning of the provided precision
(minute, hour, day, week, month, or year)
+ Add `end_X` functions to return the last second of each time period (this is
just the opposite of `beginning_X`)
* Add `between/[2,3,5]` functions for computing whether a date/time is between
two others.
* Update to rebar3 and add hex compatibility. (@Licenser)
* Properly add dependent apps to .app.src (@Licenser)
* Add an optional "relative date/time parser".
* Fix: Ensure `get_timezone()` returns the default timezone (from config) if it
hasn't been set by `get_timezone()`
* Fix UTC/GMT bug (@loudferret)
* Fix Erlang 21 Stacktrace changes (@tnt-dev)
* Set a better rebar2 version of erlware commons (@tnt-dev)
## 0.4.2
* Add partial support for `ec_date`'s 4-tuple subsecond accuracy time format.
* Fix `erlware_commons` dependency to a rebar2-compatible version.
## 0.4.1
* Remove unnecessary `io:format` call.
## 0.4.0
* Remove dependency on a running server for tracking application state.
Instead, parsers and formats are registered to the application environment
vars (e.g. `application:get_env`), and timezones are registered to the
application environment or the process dictionary. A side-effect of this
change is that you can no longer query another process's timezone.
* Add basic date arithmetic (e.g. `qdate:add_hours/[1-2]`, etc).
* Add `get_formats()` and `get_parsers()` to see list of registered formats and
parsers.
* Fix bug related to relying on the application environment variable
`default_timezone`
## 0.3.0
* Add Timezone/Daylight Saving Disambiguation
* Add the `auto` timezone shortcut
* Fix rebar.config to allow for compilation on Erlang 17
## 0.2.1
* Fix allowing timezone names to be binary
## 0.2.0
* Adding `qdate:compare/2,3` for easily comparing dates
## 0.1.0
* Initial Release

View file

View file

@ -1,41 +1,13 @@
all: compile
all: get-deps 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
get-deps:
./rebar get-deps
# rebar3.mk adds a new rebar3 rule to your Makefile
# (see https://github.com/choptastic/rebar3.mk) for full info
include rebar3.mk
compile:
./rebar compile
compile: rebar3
$(REBAR) compile
update: rebar3
$(REBAR) update
test:
EUNIT=1 $(REBAR) compile
EUNIT=1 $(REBAR) eunit
dialyzer: compile
DIALYZER=1 $(REBAR) dialyzer
dev:
mkdir -p _checkouts
cd _checkouts; git clone https://github.com/choptastic/qdate_localtime
run: rebar3
$(REBAR) shell
push_tags:
git push --tag
pull_tags:
git pull --tag
publish: rebar3 pull_tags
$(REBAR) hex publish
test: get-deps compile
./rebar skip_deps=true eunit
run:
erl -pa ebin/ deps/*/ebin/ -eval "application:start(qdate)"

View file

@ -1,6 +1,6 @@
# qdate - Erlang Date and Timezone Library
# qdate - A Wrapper for Erlang Date and Timezone Management
[![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
@ -21,15 +21,12 @@ benefits of `ec_date` and `erlang_localtime`, as well as extending the
capabilities of both to provide for other needed tools found in a single
module.
`qdate` provides date and time formatting and parsing from and into:
`qdate` will provide, under the roof of a single module date and time formatting
and parsing from and into:
+ Formatting Strings
+ Erlang Date Format
+ Erlang Now Format
+ Unixtime integers
+ Timezones
And all this while dealing with timezone parsing, formatting, conversion
and overall management.
#### Acceptable Date Formats
@ -68,15 +65,10 @@ T, Z, r, and c), `qdate` will handle them for us.
+ `to_now(Date)` - converts any date/time format to Erlang now format.
+ `to_unixtime(Date)` - converts any date/time format to a unixtime integer
A **ToTimezone** value of the atom `auto` will automatically determine the
timezone. For example, `to_date(Date, auto)` is exactly the same as
`to_date(Date)`
**A Note About Argument Order**: In all cases, `ToTimezone` is optional and if
omitted, will be determined as described below in "Understanding Timezone
Determining and Conversion". If `ToTimezone` is specified, it will always be
immediately left of the `Disambiguate` argument (if it's specified), which is
always immediately left of `Date` argument. `Date` will always be the last
immediately left of the `Date` argument. `Date` will always be the last
argument to any of the conversion and formatting functions.
#### Understanding Timezone Determining and Conversions
@ -98,88 +90,9 @@ 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
are potentially two different results if the conversion happens to land on in a
timezone that's in the middle of a Daylight Saving conversion. For example,
converting "11-Nov-2013 1:00:am" in "America/New York" to "GMT" could be both
"5am" and "6am" in GMT, since "1am EST". This is a side effect of the
"intelligence" of `qdate` - `qdate` would notice that 1am in New York is EST,
and should be converted to "1am EST", and then do the conversion from "1am EST"
to "GMT". This can lead to confusion.
Further, since `qdate` attempts to be "smart" about mistakenly entered
timezones (ie, if you entered "2013-01-01 EDT", `qdate` knows that "EDT"
(Eastern Daylight Time) doesn't apply to January first, so it *assumes* you
meant "EST".
**THE SOLUTION** to this tangled mess that we call Daylight Saving Time is to
provide an option to disambiguate if you so desire. By default disambiguation
is disabled, and `qdate` will just guess as to it's best choice. But if you so
desire, you can make sure qdate does *both* conversions, and returns both.
You can do this by passing a `Disambiguation` argument to `to_string`,
`to_date`, `to_unixtime`, and `to_now`. `Disambiguation` can be an atom of the
values:
+ `prefer_standard` *(Default Behavior)*: If an ambiguous result occurs,
qdate will return the date in standard time rather than daylight time.
+ `prefer_daylight`: If an ambiguous result occurs, qdate will return the
preferred daylight time.
+ `both`: If an ambiguous result occurs, `qdate` will return the tuple:
`{ambiguous, DateStandard, DateDaylight}`, where `DateStandard` is the date
in Standard Time, and `DateDaylight` is the date in Daylight Saving Time.
So the expanded conversions functions are:
+ `to_date(ToTimezone, Disambiguate, Date)`
+ `to_string(FormatString, ToTimezone, Disambiguate, Date)`
+ `to_unixtime(Disambiguate, Date)`
+ `to_now(Disambiguate, Date)`
Examples:
```erlang
1> qdate:set_timezone("GMT").
ok
%% Here, converting GMT 2013-11-03 6AM to America/New York yields an ambiguous
%% result
2> qdate:to_date("America/New York", both, {{2013,11,3},{6,0,0}}).
{ambiguous,{{2013,11,3},{1,0,0}},{{2013,11,3},{2,0,0}}}
%% Let's just use daylight time
3> qdate:to_date("America/New York", prefer_daylight, {{2013,11,3},{6,0,0}}).
{{2013,11,3},{2,0,0}}
%% Let's just use standard time (the default behavior)
4> qdate:to_date("America/New York", prefer_standard, {{2013,11,3},{6,0,0}}).
{{2013,11,3},{1,0,0}}
5> qdate:set_timezone("America/New York").
ok
%% Switching from 1AM Eastern Time to GMT yields a potentially ambiguous result
6> qdate:to_date("GMT", both, {{2013,11,3},{1,0,0}}).
{ambiguous,{{2013,11,3},{6,0,0}},{{2013,11,3},{5,0,0}}}
%% Use daylight time for conversion
7> qdate:to_date("GMT", prefer_daylight, {{2013,11,3},{1,0,0}}).
{{2013,11,3},{5,0,0}}
%% Here we demonstrated that even if we ask for "both", if there is no
%% ambiguity, the plain date is returned
8> qdate:to_date("GMT", both, {{2013,11,3},{5,0,0}}).
{{2013,11,3},{10,0,0}}
```
#### Conversion Functions provided for API compatibility with `ec_date`
@ -192,72 +105,22 @@ 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
**Note:** 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`
@ -293,7 +156,6 @@ be attempted before engaging the `ec_date` parser.
able to parse the string, then it should return `undefined`.
+ `deregister_parser(Key)` - If you previously registered a parser with the
`qdate` server, you can deregister it by its `Key`.
+ `get_parsers()` - Get the list of all registered parsers and their keys.
### Registering and Deregistering Formatters
+ `register_format(Key, FormatString)` - Register a formatting string with
@ -301,7 +163,6 @@ be attempted before engaging the `ec_date` parser.
formatting string.
+ `deregister_format(Key)` - Deregister the formatting string from the
`qdate` server.
+ `get_formats()` - Get the list of all registered formats and their keys.
### About backwards compatibility with `ec_date` and deterministic parsing
@ -498,31 +359,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 +374,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,175 +530,6 @@ 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
The current implementation of qdate's date arithmetic returns Unixtimes.
There are 8 main functions for date arithmetic:
+ `add_seconds(Seconds, Date)`
+ `add_minutes(Minutes, Date)`
+ `add_hours(Hours, Date)`
+ `add_days(Days, Date)`
+ `add_weeks(Weeks, Date)`
+ `add_months(Months, Date)`
+ `add_years(Years, Date)`
+ `add_date(DateToAdd, Date)` - `DateToAdd` is a shortcut way of adding
numerous options. For example. `qdate:add_date({{1, 2, -3}, {-500, 20, 0}})`
will add 1 year, add 2 months, subtract 3 days, subtract 500 hours, add 20
minutes, and not make any changes to seconds.
For the date arithmetic functions, `Date`, like all `qdate` functions, can be any
format.
### Date Arithmetic from "now"
There are 7 other arithmetic functions that take a single argument, and these do arithmetic from "now." For example, `add_years(4)` is a shortcut for `add_years(4, os:timestamp())`.
+ `add_seconds(Seconds)`
+ `add_minutes(Minutes)`
+ `add_hours(Hours)`
+ `add_days(Days)`
+ `add_weeks(Weeks)`
+ `add_months(Months)`
+ `add_years(Years)`
## Date and Time Ranges
qdate provides a number of `range` functions that give applicable dates/times
within a start and end time. For example, "All days from 2015-01-01 to today",
"every 3rd month from 2000-01-01 to 2009-12-31", or "every 15 minutes from
midnight to 11:59pm on 2015-04-15".
The functions are as follows:
+ `range_seconds(Interval, Start, End)`
+ `range_minutes(Interval, Start, End)`
+ `range_hours(Interval, Start, End)`
+ `range_days(Interval, Start, End)`
+ `range_weeks(Interval, Start, End)`
+ `range_months(Interval, Start, End)`
+ `range_years(Interval, Start, End)`
Where `Interval` is the number of seconds/days/years/etc.
So for example:
```erlang
%% Get every 15th minute from "2015-04-15 12:00am to 2015-04-15 11:59am"
> qdate:range_minutes(15, "2015-04-15 12:00am", "2015-04-15 11:59am").
[1429056000,1429056900,1429057800,1429058700,1429059600,
1429060500,1429061400,1429062300,1429063200,1429064100,
1429065000,1429065900,1429066800,1429067700,1429068600,
1429069500,1429070400,1429071300,1429072200,1429073100,
1429074000,1429074900,1429075800,1429076700,1429077600,
1429078500,1429079400,1429080300,1429081200|...]
%% Get every day of April, 2014
> qdate:range_days(1, "2014-04-01", "2014-04-30").
[1396310400,1396396800,1396483200,1396569600,1396656000,
1396742400,1396828800,1396915200,1397001600,1397088000,
1397174400,1397260800,1397347200,1397433600,1397520000,
1397606400,1397692800,1397779200,1397865600,1397952000,
1398038400,1398124800,1398211200,1398297600,1398384000,
1398470400,1398556800,1398643200,1398729600|...]
```
Note, that the return value (just like qdate's arithmetic functions) is a list
of integers. These integers are unix timestamps and can be easily formatted
with qdate:
```erlang
> Mins = qdate:range_minutes(15, "2015-04-15 12:00am", "2015-04-15 11:59am"),
> [qdate:to_string("Y-m-d h:ia", M) || M <- Mins].
["2015-04-15 00:00am","2015-04-15 00:15am",
"2015-04-15 00:30am","2015-04-15 00:45am",
"2015-04-15 01:00am","2015-04-15 01:15am",
"2015-04-15 01:30am","2015-04-15 01:45am",
"2015-04-15 02:00am","2015-04-15 02:15am",
"2015-04-15 02:30am","2015-04-15 02:45am",
"2015-04-15 03:00am","2015-04-15 03:15am",
"2015-04-15 03:30am","2015-04-15 03:45am",
"2015-04-15 04:00am","2015-04-15 04:15am",
"2015-04-15 04:30am","2015-04-15 04:45am",
"2015-04-15 05:00am","2015-04-15 05:15am",
"2015-04-15 05:30am","2015-04-15 05:45am",
"2015-04-15 06:00am","2015-04-15 06:15am",
"2015-04-15 06:30am","2015-04-15 06:45am",
[...]|...]
```
Also note that the range functions are *inclusive*.
## Age Comparison
There are two main comparisons right now, age in years, and age in days.
+ `age(Date)` - Number of years since `Date`
+ `age(FromDate, ToDate)` - Number of years between `FromDate` to `ToDate`
+ `age_days(Date)` - Number of full days since `Date` (for example from `2pm` yesterday to `1:59pm` today is still 0.
+ `age_days(FromDate, ToDate)` - Number of full days between `FromDate` and `ToDate`.
## Configuration Sample
There is a sample configuration file can be found in the root of the qdate
directory. Or you can just [look at it
here](https://github.com/choptastic/qdate/blob/master/qdate.config).
## Thanks
A few shoutouts to [Dale Harvey](http://github.com/daleharvey) and the
@ -869,24 +538,12 @@ A few shoutouts to [Dale Harvey](http://github.com/daleharvey) and the
package. Without the hard work of all involved in those projects, `qdate` would
not exist.
### Thanks to Additional Contributors
+ [Mark Allen](https://github.com/mrallen1)
+ [Christopher Phillips](https://github.com/lostcolony)
+ [Nicholas Lundgaard](https://github.com/nlundgaard-al)
+ [Alejandro Ramallo](https://github.com/aramallo)
+ [Heinz Gies](https://github.com/Licenser)
## Changelog
See [CHANGELOG.markdown](https://github.com/choptastic/qdate/blob/master/CHANGELOG.markdown)
## TODO
+ Make `qdate` backend-agnostic (allow specifying either ec_date or dh_date as
the backend)
+ Add `-spec` and `-type` info for dialyzer
+ Add date and time arithmetic.
+ 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}
]}].

BIN
rebar vendored Executable file

Binary file not shown.

View file

@ -1,24 +1,13 @@
%% -*- erlang -*-
%% vim:ts=4 sw=4 et ft=erlang
% vim:ts=4 sw=4 et ft=erlang
{require_otp_vsn, "R13B04|R14|R15|R16"}.
{cover_enabled, true}.
{dialyzer, [
{exclude_apps, []},
{warnings, []}
%{erl_opts, [debug_info,{i,"site/include"}]}.
{deps_dir, ["deps"]}.
{deps, [
{erlware_commons, ".*", {git, "git://github.com/erlware/erlware_commons.git", "HEAD"}},
{erlang_localtime, ".*", {git, "git://github.com/choptastic/erlang_localtime.git", "HEAD"}}
]}.
{deps,
[
erlware_commons,
{qdate_localtime, "~> 1.2.0"}
]}.
{project_plugins, [rebar3_ex_doc]}.
{hex, [{doc, ex_doc}]}.
{ex_doc, [
{source_url, <<"https://github.com/choptastic/qdate">>},
{extras, [<<"README.md">>, <<"LICENSE.md">>]},
{main, <<"readme">>}]}.

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,
[
{description, "Simple Date and Timezone handling for Erlang"},
{vsn, git},
{vsn, "0.2.0"},
{registered, []},
{applications, [
kernel,
stdlib,
qdate_localtime,
erlware_commons
stdlib
]},
{modules, [qdate, qdate_srv, qdate_sup, qdate_app]},
{env, []},
{licenses, ["MIT"]},
{mod, {qdate_app, []}},
{links, [{"Github", "https://github.com/choptastic/qdate"}]}
{mod, { qdate_app, []}},
{env, []}
]}.

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,7 @@
% vim: ts=4 sw=4 et
% Copyright (c) 2013 Jesse Gumm
% See LICENSE for licensing information.
-module(qdate_app).
-behaviour(application).
@ -5,11 +9,12 @@
%% Application callbacks
-export([start/2, stop/1]).
%% ===================================================================
%% Application callbacks
%% ===================================================================
start(_StartType, _StartArgs) ->
qdate_sup:start_link().
stop(_State) ->
ok.

View file

@ -1,10 +1,22 @@
% vim: ts=4 sw=4 et
% Copyright (c) 2013-2021 Jesse Gumm
% Copyright (c) 2013 Jesse Gumm
% See LICENSE for licensing information.
-module(qdate_srv).
-behaviour(gen_server).
-define(SRV, ?MODULE).
-export([
start_link/0,
init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
code_change/3,
terminate/2
]).
-export([
set_timezone/1,
set_timezone/2,
@ -21,172 +33,133 @@
register_format/2,
get_format/1,
deregister_format/1,
get_formats/0
deregister_format/1
]).
%% API
-export([start_link/0]).
%% gen_server callbacks
-export([init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3]).
%% Simple wrappers for unique keys
-define(BASETAG, qdate_var).
-define(KEY(Name), {?BASETAG, Name}).
-define(TZTAG, qdate_tz).
-define(TZKEY(Name), {?TZTAG, Name}).
-define(PARSERTAG, qdate_parser).
-define(PARSERKEY(Name), {?PARSERTAG, Name}).
-define(FORMATTAG, qdate_format).
-define(FORMATKEY(Name), {?FORMATTAG, Name}).
-define(SERVER, ?MODULE).
-define(TABLE, ?MODULE).
-record(state, {}).
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
init([]) ->
error_logger:info_msg("Creating qdate ETS Table: ~p",[?TABLE]),
?TABLE = ets:new(?TABLE, [public, {read_concurrency, true}, named_table]),
{ok, #state{}}.
handle_call({set, Key, Val}, _From, State) ->
ets:insert(?TABLE, {Key, Val}),
{reply, ok, State};
handle_call({unset, Key}, _From, State) ->
ets:delete(?TABLE, Key),
{reply, ok, State};
handle_call(_, _From, State) ->
{reply, invalid_request, State}.
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%% PUBLIC API FUNCTIONS
set_timezone(TZ) ->
put_pd(?TZTAG, TZ).
start_link() ->
gen_server:start_link({local, ?SRV}, ?MODULE, [], []).
set_timezone(Key, TZ) ->
set_env(?TZKEY(Key), TZ).
set_timezone(TZ) ->
set_timezone(self(),TZ).
set_timezone(Key,TZ) ->
ok = gen_server:call(?SRV,{set_timezone,Key,TZ}).
get_timezone() ->
get_pd(?TZTAG).
get_timezone(self()).
get_timezone(Key) ->
get_env(?TZKEY(Key)).
gen_server:call(?SRV,{get_timezone,Key}).
clear_timezone() ->
unset_pd(?TZTAG).
clear_timezone(self()).
clear_timezone(Key) ->
unset_env(?TZKEY(Key)).
ok = gen_server:call(?SRV, {clear_timezone, Key}).
register_parser(Parser) when is_function(Parser,1) ->
register_parser(erlang:make_ref(),Parser).
register_parser(Key,Parser) when is_function(Parser,1) ->
set_env(?PARSERKEY(Key), Parser).
Key = gen_server:call(?SRV,{register_parser,Key,Parser}).
deregister_parser(Key) ->
unset_env(?PARSERKEY(Key)).
ok = gen_server:call(?SRV,{deregister_parser,Key}).
deregister_parsers() ->
[deregister_parser(Key) || {Key, _} <- get_parsers()].
ok = gen_server:call(?SRV,{deregister_parsers}).
get_parsers() ->
get_all_env(?PARSERTAG).
gen_server:call(?SRV,{get_parsers}).
register_format(Key, Format) ->
set_env(?FORMATKEY(Key), Format).
register_format(Key,Format) ->
ok = gen_server:call(?SRV,{register_format,Key,Format}).
get_format(Key) ->
get_env(?FORMATKEY(Key)).
gen_server:call(?SRV,{get_format,Key}).
deregister_format(Key) ->
unset_env(?FORMATKEY(Key)).
get_formats() ->
get_all_env(?FORMATTAG).
ok = gen_server:call(?SRV,{deregister_format,Key}).
%% SERVER FUNCTIONS
-record(state, {tz, parsers, formats}).
init(_) ->
State = #state{tz=dict:new(),parsers=dict:new(),formats=dict:new()},
{ok, State}.
handle_cast(_,State) ->
{noreply, State}.
handle_info({'DOWN', MonitorRef, process, Pid, _Reason}, State) ->
erlang:demonitor(MonitorRef),
NewTZ = dict:erase(Pid, State#state.tz),
NewParsers = dict:erase(Pid, State#state.parsers),
NewFormats = dict:erase(Pid, State#state.formats),
NewState = State#state{tz=NewTZ, parsers=NewParsers, formats=NewFormats},
{noreply, NewState };
handle_info(_, State) ->
{noreply, State}.
handle_call({set_timezone,Key,TZ}, _From, State) ->
monitor_if_pid(Key),
NewTZ = dict:store(Key, TZ, State#state.tz),
NewState = State#state{tz=NewTZ},
{reply, ok, NewState};
handle_call({clear_timezone,Key},_From, State) ->
NewTZ = dict:erase(Key, State#state.tz),
NewState = State#state{tz=NewTZ},
{reply, ok, NewState};
handle_call({get_timezone,Key},_From, State) ->
Reply = case dict:find(Key, State#state.tz) of
error -> undefined;
{ok,TZ} -> TZ
end,
{reply, Reply, State};
handle_call({register_parser,Key,Parser},_From,State) ->
NewParsers = dict:store(Key, Parser, State#state.parsers),
NewState = State#state{parsers=NewParsers},
{reply, Key, NewState};
handle_call({get_parsers},_From,State) ->
Reply = dict:to_list(State#state.parsers),
{reply, Reply, State};
handle_call({deregister_parser,Key},_From,State) ->
NewParsers = dict:erase(Key, State#state.parsers),
NewState = State#state{parsers=NewParsers},
{reply, ok, NewState};
handle_call({deregister_parsers},_From,State) ->
NewState = State#state{parsers=dict:new()},
{reply, ok, NewState};
handle_call({register_format,Key,Format},_From,State) ->
NewFormats = dict:store(Key, Format, State#state.formats),
NewState = State#state{formats=NewFormats},
{reply, ok, NewState};
handle_call({get_format,Key},_From,State) ->
Reply = case dict:find(Key, State#state.formats) of
error -> undefined;
{ok, Format} -> Format
end,
{reply, Reply,State};
handle_call({deregister_format,Key},_From,State) ->
NewFormats = dict:erase(Key, State#state.formats),
NewState = State#state{formats=NewFormats},
{reply, ok, NewState}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVersion, State, _Extra) ->
{ok, State}.
%% PRIVATE TOOLS
%% App Vars
set_env(Key, Val) ->
gen_server:call(?SERVER, {set, ?KEY(Key), Val}).
get_env(Key) ->
get_env(Key, undefined).
get_env(Key, Default) ->
case ets:lookup(?TABLE, ?KEY(Key)) of
[{__Key, Val}] -> Val;
[] -> Default
end.
unset_env(Key) ->
gen_server:call(?SERVER, {unset, ?KEY(Key)}).
get_all_env(FilterTag) ->
try ets:tab2list(?TABLE) of
All ->
[{Key, V} || {{?BASETAG, {Tag, Key}}, V} <- All, Tag==FilterTag]
catch
error:badarg:S ->
Msg = maybe_start_msg(),
logger:error(Msg),
error({qdate_get_all_env_failed, #{
original_error => {error, badarg, S}
}})
end.
maybe_start_msg() ->
"Attempting to read qdate environment failed (qdate_srv:get_all_env/1).\n"
"qdate may not have been started properly.\n"
"Please ensure qdate is started properly by either:\n"
"* Putting qdate in the 'applications' key in your .app.src or .app file, or\n"
"* Starting it manually with application:ensure_all_started(qdate) or qdate:start().".
%% ProcDic Vars
get_pd(Key) ->
erlang:get(?KEY(Key)).
put_pd(Key, Val) ->
erlang:put(?KEY(Key), Val),
ok.
unset_pd(Key) ->
put_pd(Key, undefined).
monitor_if_pid(Key) when is_pid(Key) ->
erlang:monitor(process,Key);
monitor_if_pid(_) ->
do_nothing.

View file

@ -1,3 +1,7 @@
% vim: ts=4 sw=4 et
% Copyright (c) 2013 Jesse Gumm
% See LICENSE for licensing information.
-module(qdate_sup).
-behaviour(supervisor).
@ -8,25 +12,21 @@
%% Supervisor callbacks
-export([init/1]).
-define(SERVER, ?MODULE).
%% Helper macro for declaring children of supervisor
-define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}).
%% ===================================================================
%% API functions
%% ===================================================================
start_link() ->
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
%% ===================================================================
%% Supervisor callbacks
%% ===================================================================
init([]) ->
SupFlags = #{},
ChildSpec = #{
id=>qdate_srv,
start=>{qdate_srv, start_link, []}
},
{ok, {SupFlags, [ChildSpec]}}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
Server = ?CHILD(qdate_srv, worker),
{ok, { {one_for_one, 5, 10}, [Server]} }.