Merge branch 'master' of github.com:choptastic/qdate
Conflicts: rebar.config
This commit is contained in:
commit
7910de376e
8 changed files with 259 additions and 18 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -5,3 +5,4 @@ deps/
|
||||||
ebin/
|
ebin/
|
||||||
.eunit/
|
.eunit/
|
||||||
_build
|
_build
|
||||||
|
rebar.lock
|
||||||
|
|
|
@ -8,4 +8,4 @@ otp_release:
|
||||||
- R15B02
|
- R15B02
|
||||||
- R15B01
|
- R15B01
|
||||||
- R15B
|
- R15B
|
||||||
before_script: "sudo apt-get --yes --force-yes install libpam0g-dev"
|
before_script: "sudo apt-get --yes --force-yes install libpam-runtime"
|
||||||
|
|
|
@ -1,9 +1,18 @@
|
||||||
## 0.5.0 (in development)
|
## 0.5.0 (in development)
|
||||||
|
|
||||||
|
* 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, month, or year)
|
||||||
* Update to rebar3 and add hex compatability. (@Licenser)
|
* Update to rebar3 and add hex compatability. (@Licenser)
|
||||||
* Properly add dependent apps to .app.src (@Licenser)
|
* Properly add dependent apps to .app.src (@Licenser)
|
||||||
* Remove R14 from travis testing.
|
* Remove R14 from travis testing.
|
||||||
|
|
||||||
|
## 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
|
## 0.4.1
|
||||||
|
|
||||||
* Remove unnecessary `io:format` call.
|
* Remove unnecessary `io:format` call.
|
||||||
|
|
3
Makefile
3
Makefile
|
@ -5,6 +5,9 @@ all: compile
|
||||||
compile:
|
compile:
|
||||||
$(REBAR) compile
|
$(REBAR) compile
|
||||||
|
|
||||||
|
update:
|
||||||
|
$(REBAR) update
|
||||||
|
|
||||||
test: compile
|
test: compile
|
||||||
$(REBAR) eunit
|
$(REBAR) eunit
|
||||||
|
|
||||||
|
|
|
@ -619,9 +619,25 @@ ok
|
||||||
%% that timezone to our intended timezone.
|
%% that timezone to our intended timezone.
|
||||||
```
|
```
|
||||||
|
|
||||||
## Date Arithmetic
|
## Date Truncation (Beginning of X)
|
||||||
|
|
||||||
(not fully tested yet, but will have full tests for 0.4.0)
|
Sometimes you need to truncate a time (say, the beginning of the current
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Date Arithmetic
|
||||||
|
|
||||||
The current implementation of qdate's date arithmetic returns Unixtimes.
|
The current implementation of qdate's date arithmetic returns Unixtimes.
|
||||||
|
|
||||||
|
@ -654,6 +670,74 @@ 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*.
|
||||||
|
|
||||||
|
|
||||||
## 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
|
||||||
|
@ -683,6 +767,7 @@ See [CHANGELOG.markdown](https://github.com/choptastic/qdate/blob/master/CHANGEL
|
||||||
+ Provide a sample qdate.config for users to see
|
+ 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`.
|
||||||
|
+ Add age calculation stuff: `age_years(Date)`, `age_minutes(Date)`, etc.
|
||||||
|
|
||||||
## Conclusion
|
## Conclusion
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,11 @@
|
||||||
%% For rebar2 compat
|
%% For rebar2 compat
|
||||||
{deps,
|
{deps,
|
||||||
[
|
[
|
||||||
|
%% This uses an older erlware_commons version so retain compatibility with
|
||||||
|
%% rebar2. v0.16.1 introduced a 'cf' dependency, which seems to cause
|
||||||
|
%% breakage.
|
||||||
{erlware_commons, ".*", {git, "git://github.com/erlware/erlware_commons.git", {tag, "v0.15.0"}}},
|
{erlware_commons, ".*", {git, "git://github.com/erlware/erlware_commons.git", {tag, "v0.15.0"}}},
|
||||||
|
|
||||||
{erlang_localtime, ".*", {git, "git://github.com/choptastic/erlang_localtime.git", {branch, master}}}
|
{erlang_localtime, ".*", {git, "git://github.com/choptastic/erlang_localtime.git", {branch, master}}}
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{application, qdate,
|
{application, qdate,
|
||||||
[
|
[
|
||||||
{description, "Simple Date and Timezone handling for Erlang"},
|
{description, "Simple Date and Timezone handling for Erlang"},
|
||||||
{vsn, "0.4.1"},
|
{vsn, "0.4.2"},
|
||||||
{registered, []},
|
{registered, []},
|
||||||
{applications, [
|
{applications, [
|
||||||
kernel,
|
kernel,
|
||||||
|
|
145
src/qdate.erl
145
src/qdate.erl
|
@ -1,5 +1,5 @@
|
||||||
% vim: ts=4 sw=4 et
|
% vim: ts=4 sw=4 et
|
||||||
% Copyright (c) 2013 Jesse Gumm
|
% Copyright (c) 2013-2015 Jesse Gumm
|
||||||
% See LICENSE for licensing information.
|
% See LICENSE for licensing information.
|
||||||
%
|
%
|
||||||
-module(qdate).
|
-module(qdate).
|
||||||
|
@ -24,6 +24,20 @@
|
||||||
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/2, %% needs to be /2 because we also need to define what day is considered "beginning of week", since some calendars do sunday and some do monday. We'll hold off on implementation here
|
||||||
|
beginning_month/1,
|
||||||
|
beginning_month/0,
|
||||||
|
beginning_year/1,
|
||||||
|
beginning_year/0
|
||||||
|
]).
|
||||||
|
|
||||||
-export([
|
-export([
|
||||||
compare/2,
|
compare/2,
|
||||||
compare/3
|
compare/3
|
||||||
|
@ -47,6 +61,17 @@
|
||||||
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([
|
-export([
|
||||||
register_parser/2,
|
register_parser/2,
|
||||||
register_parser/1,
|
register_parser/1,
|
||||||
|
@ -242,7 +267,9 @@ to_date(ToTZ, Disambiguate, RawDate) ->
|
||||||
end,
|
end,
|
||||||
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}} ->
|
||||||
|
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
|
||||||
|
@ -311,6 +338,45 @@ to_now(Disamb, ToParse) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Beginning/Truncation %%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
|
||||||
|
beginning_minute() ->
|
||||||
|
beginning_minute({date(),time()}).
|
||||||
|
|
||||||
|
beginning_minute(Date) ->
|
||||||
|
{{Y,M,D},{H,M,_}} = to_date(Date),
|
||||||
|
{{Y,M,D},{H,M,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({date(),time()}).
|
||||||
|
|
||||||
|
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}}.
|
||||||
|
|
||||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Comparisons %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Comparisons %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
@ -458,6 +524,77 @@ fmid({Y, M, D}) when D < 1 ->
|
||||||
fmid(Date) ->
|
fmid(Date) ->
|
||||||
Date.
|
Date.
|
||||||
|
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 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).
|
||||||
|
|
||||||
|
|
||||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Timezone Stuff %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Timezone Stuff %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
@ -780,7 +917,9 @@ 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"))
|
||||||
|
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue