Compare commits

..

41 commits

Author SHA1 Message Date
Jesse Gumm
108c796687 Remove travis and replace badge with GH actions
Some checks failed
qdate tests and dialyzer / OTP 23.x (push) Has been cancelled
qdate tests and dialyzer / OTP 24.x (push) Has been cancelled
qdate tests and dialyzer / OTP 25.x (push) Has been cancelled
qdate tests and dialyzer / OTP 26.x (push) Has been cancelled
qdate tests and dialyzer / OTP 27.x (push) Has been cancelled
2024-09-05 15:04:50 -05:00
Jesse Gumm
43c1f3412b Update to checkout@v4 2024-09-05 15:01:11 -05:00
Jesse Gumm
dc2184ea06 Remove reference to Postgres/MySQL (GH actions config was largely copied from sql_bridge) 2024-09-05 14:56:59 -05:00
Jesse Gumm
70bab7df95 Updates to fix dialyzer failing on otp 26+ 2024-09-05 14:55:48 -05:00
Jesse Gumm
06347e7f31 Add github actions workflow 2024-09-05 14:35:38 -05:00
Jesse Gumm
9d0fb6d895 Remove ?else macro 2024-09-05 08:36:05 -05:00
Jesse Gumm
2072b49220 Update changelog - remove rebar3 binary 2024-01-09 19:18:29 -06:00
Jesse Gumm
cb873afb22 Update rebar.lock 2024-01-09 19:10:19 -06:00
Jesse Gumm
c109e7969b Rework the filenames because hex doesn't like them 2024-01-09 19:06:22 -06:00
Jesse Gumm
2a20cdea1e Update rebar.config wiht some hex stuff 2024-01-09 19:04:33 -06:00
Jesse Gumm
ea203ec90d Add some tag rules to makefile 2024-01-09 19:01:14 -06:00
Jesse Gumm
5a847014c9 Make qdate's version in app.src track the git tags 2024-01-09 18:59:58 -06:00
Jesse Gumm
794d03a5ee Produce a better error message if qdate hasn't been started 2024-01-09 18:58:02 -06:00
Jesse Gumm
817c14c46e Update copyright date comment in qdate.erl 2023-08-22 09:11:48 -05:00
Jesse Gumm
280f8e72f2 Increment version 2023-08-12 19:46:32 -05:00
Jesse Gumm
5afd1335f1 Add a make dev for easily working with qdate_localtime 2023-08-12 19:44:13 -05:00
Jesse Gumm
2ce94f82ce Fix dialyzer bugs 2023-08-12 19:43:46 -05:00
Jesse Gumm
5a352dc599 Fix the unnused erlnow() type 2023-08-12 10:40:13 -05:00
Jesse Gumm
357b5844eb
Merge pull request #42 from kianmeng/fix-typos
Fix typos
2022-01-28 08:33:04 -06:00
Kian-Meng Ang
d87d7d8b6c Fix typos 2022-01-28 08:17:41 +08:00
Jesse Gumm
92e20c474f
git: -> https: 2022-01-11 11:48:45 -06:00
Jesse Gumm
c5971cdcf4 update rebar3 2021-07-01 09:24:18 -05:00
Jesse Gumm
6d6b6ec9bd Update changelog and makefile 2021-07-01 08:43:18 -05:00
Jesse Gumm
c789fca51f Update to 0.7.0 2021-06-30 21:11:11 -05:00
Jesse Gumm
d6a431362d More dialyzer updates 2021-06-30 18:03:55 -05:00
Jesse Gumm
bbfef69d2e Convert qdate_srv to be an ETS server (dialyzer doesn't like using tuples as keys for application:set_env) 2021-06-30 14:44:45 -05:00
Jesse Gumm
3d72448491 bump to version 0.6.0 2021-06-01 08:35:13 -05:00
Jesse Gumm
596b573775 Erlware Commons => 1.5.0 for rebar2 2021-06-01 08:28:15 -05:00
Jesse Gumm
d0b6fc0011 fix error warning about _SameDay bound twice 2021-06-01 08:25:00 -05:00
Jesse Gumm
d159878f62 update rebar3 version 2021-06-01 08:21:55 -05:00
Jesse Gumm
69d7fdd626
Merge pull request #41 from SiftLogic/fix/preserve-millsec
Fix/preserve millsec
2021-05-03 14:28:56 -05:00
Leonard Boyce
f49368d3fc (feat) Support preserving Millisec in date parsing and formatting 2021-05-03 10:26:56 -04:00
Leonard Boyce
22b48f90de (chore) Ignore editor artifacts 2021-05-03 09:46:08 -04:00
Jesse Gumm
0a7d808290 Add age functions 2020-11-20 14:14:25 -06:00
Jesse Gumm
2f3afd7dbb Travis doesn't support 23.1 yet, so don't test with that 2020-11-15 08:37:52 -06:00
Jesse Gumm
68b462469f travis doesn't support 23.1 yet, so just do 23.0 until then 2020-11-15 01:09:31 -06:00
Jesse Gumm
1c405bbfc3 update travis for 23 2020-11-15 01:09:02 -06:00
Jesse Gumm
accda0db1a Just get rid of the old erlang versions 2019-08-21 13:21:57 -05:00
Jesse Gumm
28847aa896 Use local rebar3 only if there isn't one installed 2019-08-21 11:59:43 -05:00
Jesse Gumm
a0b9ad0e5f contributors=>maintainers for hex 2019-08-21 11:32:35 -05:00
Jesse Gumm
73d9706c7a travis tweaks 2019-08-21 11:30:46 -05:00
17 changed files with 450 additions and 67 deletions

47
.github/workflows/tests-workflow.yml vendored Normal file
View file

@ -0,0 +1,47 @@
name: qdate tests and dialyzer
on: push
jobs:
linux:
name: OTP ${{ matrix.otp_version }}
runs-on: ${{ matrix.os }}
continue-on-error: true
strategy:
matrix:
include:
- os: ubuntu-22.04
otp_version: '27.x'
rebar3_version: "3.24.0"
- os: ubuntu-22.04
otp_version: '27.x'
rebar3_version: "3.23.0"
- os: ubuntu-22.04
otp_version: '26.x'
rebar3_version: "3.22.1"
- os: ubuntu-22.04
otp_version: '25.x'
rebar3_version: "3.22.1"
- os: ubuntu-22.04
otp_version: '24.x'
rebar3_version: "3.22.1"
- os: ubuntu-20.04
otp_version: '23.x'
rebar3_version: "3.19.0"
steps:
- name: Install OTP ${{matrix.otp_version}}
uses: erlef/setup-beam@v1
with:
version-type: loose
otp-version: ${{ matrix.otp_version}}
rebar3-version: ${{ matrix.rebar3_version}}
- name: Checkout qdate
uses: actions/checkout@v4
- name: Run Tests
run: make test
- name: Run Dialyzer
run: make dialyzer

4
.gitignore vendored
View file

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

View file

@ -1,14 +0,0 @@
language: erlang
script: "make test"
otp_release:
- 22.0
- 21.2
- 21.0
- 20.0
- 19.0
- 18.0
- 17.4
- 17.0
- R16B
- R15B03
before_script: "sudo apt-get --yes --force-yes install libpam-runtime python-software-properties"

View file

@ -1,3 +1,32 @@
## 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
@ -8,7 +37,7 @@
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 compatability. (@Licenser)
* 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

View file

View file

@ -1,21 +1,41 @@
REBAR = $(shell pwd)/rebar3
all: compile
compile:
# Check if rebar3.mk exists, and if not, download it
ifeq ("$(wildcard rebar3.mk)","")
$(shell curl -O https://raw.githubusercontent.com/choptastic/rebar3.mk/master/rebar3.mk)
endif
# rebar3.mk adds a new rebar3 rule to your Makefile
# (see https://github.com/choptastic/rebar3.mk) for full info
include rebar3.mk
compile: rebar3
$(REBAR) compile
update:
update: rebar3
$(REBAR) update
test: compile
$(REBAR) eunit
test:
EUNIT=1 $(REBAR) compile
EUNIT=1 $(REBAR) eunit
run:
dialyzer: compile
DIALYZER=1 $(REBAR) dialyzer
dev:
mkdir -p _checkouts
cd _checkouts; git clone https://github.com/choptastic/qdate_localtime
run: rebar3
$(REBAR) shell
publish:
$(REBAR) as pkg upgrade
$(REBAR) as pkg hex publish
$(REBAR) upgrade
push_tags:
git push --tag
pull_tags:
git pull --tag
publish: rebar3 pull_tags
$(REBAR) hex publish

View file

@ -1,6 +1,6 @@
# qdate - Erlang Date and Timezone Library
[![Build Status](https://travis-ci.org/choptastic/qdate.png?branch=master)](https://travis-ci.org/choptastic/qdate)
[![qdate tests and dialyzer](https://github.com/choptastic/qdate/actions/workflows/tests-workflow.yml/badge.svg)](https://github.com/choptastic/qdate/actions/workflows/tests-workflow.yml)
## Purpose
@ -107,7 +107,7 @@ will infer the timezone in the following order.
#### Disambiguating Ambiguous Timezone Conversions
Sometimes, when youre converting a datetime from one timezone to another, there
Sometimes, when you're converting a datetime from one timezone to another, there
are potentially two different results if the conversion happens to land on in a
timezone that's in the middle of a Daylight Saving conversion. For example,
converting "11-Nov-2013 1:00:am" in "America/New York" to "GMT" could be both
@ -536,7 +536,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
%% easiy refer to that format by its key.
%% easily refer to that format by its key.
%% So let's take that format and register it
16> qdate:register_format(longdate, "l, F jS, Y g:i A T").
@ -720,7 +720,7 @@ the week" calculation. This has three forms, specifically:
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 verions of the days of the week. Specifically:
integers 1-7 or the atom versions of the days of the week. Specifically:
* Monday: `1 | monday | mon`
* Tuesday: `2 | tuesday | tue`
@ -845,6 +845,16 @@ with qdate:
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
@ -879,7 +889,6 @@ See [CHANGELOG.markdown](https://github.com/choptastic/qdate/blob/master/CHANGEL
+ Add `-spec` and `-type` info for dialyzer
+ Research the viability of [ezic](https://github.com/drfloob/ezic) for a
timezone backend replacement for `erlang_localtime`.
+ Add age calculation stuff: `age_years(Date)`, `age_minutes(Date)`, etc.
## Conclusion

View file

@ -11,5 +11,10 @@
%% See readme section here:
%% https://github.com/choptastic/qdate#about-backwards-compatibility-with-ec_date-and-deterministic-parsing
{deterministic_parsing, {zero, zero}}
{deterministic_parsing, {zero, zero}},
%% Preserve Milliseconds in parsing
%% Changes the return for qdate:to_date to include any Ms value
%% and also changes the return of to_string to include the Ms value
{preserve_ms, false}
]}].

View file

@ -2,8 +2,23 @@
%% vim:ts=4 sw=4 et ft=erlang
{cover_enabled, true}.
{dialyzer, [
{exclude_apps, []},
{warnings, []}
]}.
{deps,
[
erlware_commons,
qdate_localtime
{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

@ -9,7 +9,7 @@ case erlang:function_exported(rebar3, main, 1) of
%% Rebuild deps, possibly including those that have been moved to
%% profiles
[{deps, [
{erlware_commons, "", {git, "git://github.com/erlware/erlware_commons", {tag, "v1.3.1"}}}, %% this is the version of erlware_commons that works until erlware tags a new version
{qdate_localtime, "", {git, "git://github.com/choptastic/qdate_localtime", {tag, "1.1.0"}}}
{erlware_commons, "", {git, "https://github.com/erlware/erlware_commons", {tag, "v1.5.0"}}}, %% this is the version of erlware_commons that works until erlware tags a new version
{qdate_localtime, "", {git, "https://github.com/choptastic/qdate_localtime", {tag, "1.1.0"}}}
]} | lists:keydelete(deps, 1, CONFIG)]
end.

14
rebar.lock Normal file
View file

@ -0,0 +1,14 @@
{"1.2.0",
[{<<"cf">>,{pkg,<<"cf">>,<<"0.3.1">>},1},
{<<"erlware_commons">>,{pkg,<<"erlware_commons">>,<<"1.6.0">>},0},
{<<"qdate_localtime">>,{pkg,<<"qdate_localtime">>,<<"1.2.1">>},0}]}.
[
{pkg_hash,[
{<<"cf">>, <<"5CB902239476E141EA70A740340233782D363A31EEA8AD37049561542E6CD641">>},
{<<"erlware_commons">>, <<"E0DA62F91E65DEFAE28BB450DDC3C24E85ECB99B926F857CDC6ED8E7DD7076B4">>},
{<<"qdate_localtime">>, <<"72E1034DC6B7FEE8F588281EDDD0BD0DC5260005D758052F50634D265D382C18">>}]},
{pkg_hash_ext,[
{<<"cf">>, <<"315E8D447D3A4B02BCDBFA397AD03BBB988A6E0AA6F44D3ADD0F4E3C3BF97672">>},
{<<"erlware_commons">>, <<"B57C299C39A2992A8C413F9D2C1B8A20061310FB4432B0EEE5E4C4DF7AAA0AA3">>},
{<<"qdate_localtime">>, <<"1109958D205C65C595C8C5694CB83EBAF2DBE770CF902E4DCE8AFB2C4123764D">>}]}
].

BIN
rebar3

Binary file not shown.

View file

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

View file

@ -1,9 +1,11 @@
% vim: ts=4 sw=4 et
% Copyright (c) 2013-2019 Jesse Gumm
% Copyright (c) 2013-2023 Jesse Gumm
% See LICENSE for licensing information.
%
-module(qdate).
%-compile(export_all).
-export([
start/0,
stop/0
@ -101,6 +103,13 @@
range_years/3
]).
-export([
age/1,
age/2,
age_days/1,
age_days/2
]).
-export([
register_parser/2,
register_parser/1,
@ -131,6 +140,13 @@
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(), ).
@ -146,10 +162,10 @@
-define(DETERMINE_TZ, determine_timezone()).
-define(DEFAULT_DISAMBIG, prefer_standard).
-define(else, true).
start() ->
application:load(qdate).
application:ensure_all_started(qdate).
stop() ->
ok.
@ -163,6 +179,7 @@ to_string(Format, Date) ->
to_string(Format, ToTZ, Date) ->
to_string(Format, ToTZ, ?DEFAULT_DISAMBIG, Date).
-spec to_string(Format :: any(), ToTZ :: any(), Disambiguate :: disambiguate(), Date :: qdate()) -> binary_or_string() | {ambiguous, binary_or_string() , binary_or_string()}.
to_string(FormatKey, ToTZ, Disambiguate, Date) when is_atom(FormatKey) orelse is_tuple(FormatKey) ->
Format = case qdate_srv:get_format(FormatKey) of
undefined -> throw({undefined_format_key,FormatKey});
@ -229,7 +246,13 @@ to_string_worker([$r | RestFormat], ToTZ, Disamb, Date) ->
to_string_worker(NewFormat, ToTZ, Disamb, Date) ++ to_string_worker(RestFormat, ToTZ, Disamb, Date);
to_string_worker([$c | RestFormat], ToTZ, Disamb, Date) ->
Format1 = "Y-m-d",
Format2 = "H:i:sP",
Format2 = case Date of
{_, {_,_,_,_}} ->
%% Have milliseconds
"H:i:s.fP";
_ ->
"H:i:sP"
end,
to_string_worker(Format1, ToTZ, Disamb, Date)
++ "T"
++ to_string_worker(Format2, ToTZ, Disamb, Date)
@ -280,6 +303,7 @@ to_date(RawDate) ->
to_date(ToTZ, RawDate) ->
to_date(ToTZ, ?DEFAULT_DISAMBIG, RawDate).
-spec to_date(ToTZ :: any(), Disambiguate :: disambiguate(), RawDate :: any()) -> {ambiguous, datetime(), datetime()} | datetime().
to_date(ToTZ, Disambiguate, RawDate) when is_binary(RawDate) ->
to_date(ToTZ, Disambiguate, binary_to_list(RawDate));
to_date(ToTZ, Disambiguate, RawDate) when is_binary(ToTZ) ->
@ -294,9 +318,13 @@ to_date(ToTZ, Disambiguate, RawDate) ->
{ParsedDate,ParsedTZ} ->
{ParsedDate,ParsedTZ}
end,
PreserveMs = preserve_ms(),
try raw_to_date(RawDate3) of
D={{_,_,_},{_,_,_}} ->
date_tz_to_tz(D, Disambiguate, FromTZ, ToTZ);
{{Year, Month, Date},{Hour,Minute,Second,Millis}} when PreserveMs ->
{ODate, {OHour,OMinute,OSecond}} = date_tz_to_tz({{Year, Month, Date},{Hour,Minute,Second}}, Disambiguate, FromTZ, ToTZ),
{ODate, {OHour,OMinute,OSecond, Millis}};
{{Year, Month, Date},{Hour,Minute,Second,_Millis}} ->
date_tz_to_tz({{Year, Month, Date},{Hour,Minute,Second}}, Disambiguate, FromTZ, ToTZ)
catch
@ -333,9 +361,9 @@ get_deterministic_datetime() ->
to_unixtime(Date) ->
to_unixtime(?DEFAULT_DISAMBIG, Date).
-spec to_unixtime(Disamb :: disambiguate(), qdate()) -> {ambiguous, integer(), integer()} | integer().
to_unixtime(_, Unixtime) when is_integer(Unixtime) ->
Unixtime;
to_unixtime(_, {MegaSecs,Secs,_}) when is_integer(MegaSecs), is_integer(Secs) ->
MegaSecs*1000000 + Secs;
to_unixtime(Disamb, ToParse) ->
@ -355,11 +383,12 @@ unixtime() ->
to_now(Date) ->
to_now(?DEFAULT_DISAMBIG, Date).
-spec to_now(Disamb :: disambiguate(), qdate()) -> erlnow() | {ambiguous, erlnow(), erlnow()}.
to_now(_, Now = {_,_,_}) ->
Now;
to_now(Disamb, ToParse) ->
case to_unixtime(Disamb, ToParse) of
{ambiguous, Standard, Daylight} ->
{ambiguous, Standard, Daylight} when is_integer(Standard), is_integer(Daylight) ->
{ambiguous,
unixtime_to_now(Standard),
unixtime_to_now(Daylight)};
@ -505,6 +534,7 @@ end_week(BeginningDayOfWeek, Date) ->
%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Comparisons %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec compare(A :: qdate(), B :: qdate()) -> integer().
compare(A, B) ->
NowA = to_now(A),
NowB = to_now(B),
@ -514,6 +544,7 @@ compare(A, B) ->
NowA > NowB -> 1
end.
-spec compare(A :: qdate(), Op :: atom(), B :: qdate()) -> boolean().
compare(A, Op, B) ->
Comp = compare(A, B),
case Op of
@ -558,7 +589,7 @@ sort(Op, List) ->
sort(Op, List, Opts) ->
NonDateOpt = proplists:get_value(non_dates, Opts, back),
WithNorm = add_sort_normalization(List, NonDateOpt),
SortFun = make_sort_fun(Op, Opts),
SortFun = make_sort_fun(Op, NonDateOpt),
Sorted = lists:sort(SortFun, WithNorm),
strip_sort_normalization(Sorted).
@ -577,6 +608,7 @@ add_sort_normalization(List, NonDateOpt) ->
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),
@ -670,6 +702,15 @@ add_years(Years, Date) ->
add_years(Years) ->
add_years(Years, os:timestamp()).
-type unit() :: second | seconds |
minute | minutes |
hour | hours |
day | days |
week | weeks |
month | months |
year | years.
-spec add_unit(Unit :: unit(), Value :: integer(), Date :: qdate()) -> qdate().
add_unit(second, Value, Date) ->
add_unit(seconds, Value, Date);
add_unit(seconds, Value, Date) ->
@ -768,6 +809,42 @@ fmid({Y, M, D}) when D < 1 ->
fmid(Date) ->
Date.
age(Birth) ->
age(Birth, os:timestamp()).
age(Birth, Now) ->
%% B=Birth
{{BY, BM, BD}, _} = to_date(Birth),
%% C=Current
{{CY, CM, CD}, _} = to_date(Now),
if
(CM > BM);
(CM == BM andalso CD >= BD)
-> CY - BY;
true ->
(CY - BY) - 1
end.
age_days(Birth) ->
age_days(Birth, os:timestamp()).
age_days(Birth, Now) ->
case {to_date(Birth), to_date(Now)} of
{{SameDay, _}, {SameDay, _}} ->
0;
{{BirthDate, BirthTime}, {NowDate, NowTime}} ->
BirthDays = calendar:date_to_gregorian_days(BirthDate),
NowDays = calendar:date_to_gregorian_days(NowDate),
DiffDays = NowDays - BirthDays,
if
NowTime >= BirthTime ->
DiffDays;
true ->
DiffDays-1
end
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Ranges %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
@ -962,6 +1039,8 @@ extract_timezone_helper(RevDate, [TZ | TZs]) when length(RevDate) >= length(TZ)
extract_timezone_helper(RevDate, [_TZ | TZs]) ->
extract_timezone_helper(RevDate, TZs).
preserve_ms() ->
application:get_env(qdate, preserve_ms, false).
%% This is the timezone only if the qdate application variable
%% "default_timezone" isn't set or is set to undefined.
@ -985,6 +1064,8 @@ determine_timezone() ->
%% If FromTZ is an integer, then it's an integer that represents the number of minutes
%% relative to GMT. So we convert the date to GMT based on that number, then we can
%% do the other timezone conversion.
-spec date_tz_to_tz(Date :: datetime(), Disambiguate :: disambiguate(), FromTZ :: any(), ToTZ :: any()) ->
datetime() | {ambiguous, datetime(), datetime()}.
date_tz_to_tz(Date, Disambiguate, FromTZ, ToTZ) when is_integer(FromTZ) ->
NewDate = localtime:adjust_datetime(Date, FromTZ),
date_tz_to_tz(NewDate, Disambiguate, "GMT", ToTZ);
@ -999,13 +1080,14 @@ date_tz_to_tz(Date, Disambiguate, FromTZ, ToTZ) ->
date_tz_to_tz_both(Date, FromTZ, ToTZ)
end.
-spec date_tz_to_tz_both(Date :: datetime(), FromTZ :: string(), ToTZ :: string()) -> datetime() | {ambiguous, datetime(), datetime()}.
date_tz_to_tz_both(Date, FromTZ, ToTZ) ->
Standard = localtime:local_to_local(Date, FromTZ, ToTZ),
Daylight = localtime:local_to_local_dst(Date, FromTZ, ToTZ),
if
Standard=:=Daylight ->
Standard;
?else ->
true ->
{ambiguous, Standard, Daylight}
end.
@ -1124,6 +1206,8 @@ flooring(N) when N < 0 ->
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% TESTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-ifdef(EUNIT).
-include_lib("eunit/include/eunit.hrl").
%% emulates as if a forum-type website has a Site tz, and a user-specified tz
@ -1151,6 +1235,30 @@ tz_test_() ->
end
}.
tz_preserve_ms_true_test_() ->
{
setup,
fun start_test/0,
fun stop_test/1,
fun(SetupData) ->
{inorder,[
preserve_ms_true_tests(SetupData)
]}
end
}.
tz_preserve_ms_false_test_() ->
{
setup,
fun start_test/0,
fun stop_test/1,
fun(SetupData) ->
{inorder,[
preserve_ms_false_tests(SetupData)
]}
end
}.
test_deterministic_parser(_) ->
{inorder, [
?_assertEqual(ok, application:set_env(qdate, deterministic_parsing, {now, now})),
@ -1340,13 +1448,45 @@ arith_tests(_) ->
?_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({{2017,1,1},{0,0,0}}, beginning_week(7, {{2017,1,6},{0,0,0}})),
?_assertEqual(0, age("1981-01-15", "1981-12-31")),
?_assertEqual(39, age("1981-01-15", "2020-01-15")),
?_assertEqual(39, age("1981-01-15", "2020-01-15 12am")),
?_assertEqual(38, age("1981-01-15", "2020-01-14")),
?_assertEqual(38, age("1981-01-15", "2020-01-14 11:59pm")),
%% checking pre-unix-epoch
?_assertEqual(100, age("1901-01-01","2001-01-01")),
?_assertEqual(20, age("1900-01-01", "1920-01-01")),
?_assertEqual(0, age_days("2020-11-20 12am","2020-11-20 11:59:59pm")),
?_assertEqual(1, age_days("2020-11-19 11:59:59pm","2020-11-20 11:59:59pm")),
?_assertEqual(7, age_days("2020-11-20","2020-11-27")),
?_assertEqual(7, age_days("2020-11-27","2020-12-04"))
]}.
preserve_ms_true_tests(_) ->
application:set_env(qdate, preserve_ms, true),
{inorder, [
?_assertEqual({{2021,5,8},{23,0,16,472000}}, qdate:to_date(<<"UTC">>,<<"2021-5-09 0:0:16.472+01:00">>)),
?_assertEqual(<<"2021-05-08T23:00:16.472000+00:00">>, qdate:to_string(<<"Y-m-d\\TH:i:s.fP">>, <<"UTC">>,<<"2021-5-09 0:0:16.472+01:00">>)),
?_assertEqual(<<"2021-05-08T23:00:16+00:00">>, qdate:to_string(<<"Y-m-d\\TH:i:sP">>, <<"UTC">>,<<"2021-5-09 0:0:16.472+01:00">>)),
?_assertEqual(<<"2021-05-08T23:00:16.472000+00:00">>, qdate:to_string(<<"c">>, <<"UTC">>,<<"2021-5-09 0:0:16.472+01:00">>))
]}.
preserve_ms_false_tests(_) ->
application:set_env(qdate, preserve_ms, false),
{inorder, [
?_assertEqual({{2021,5,8},{23,0,16}}, qdate:to_date(<<"UTC">>,<<"2021-5-09 0:0:16.472+01:00">>)),
?_assertEqual(<<"2021-05-08T23:00:16.0+00:00">>, qdate:to_string(<<"Y-m-d\\TH:i:s.fP">>, <<"UTC">>,<<"2021-5-09 0:0:16.472+01:00">>)),
?_assertEqual(<<"2021-05-08T23:00:16+00:00">>, qdate:to_string(<<"Y-m-d\\TH:i:sP">>, <<"UTC">>,<<"2021-5-09 0:0:16.472+01:00">>)),
?_assertEqual(<<"2021-05-08T23:00:16+00:00">>, qdate:to_string(<<"c">>, <<"UTC">>,<<"2021-5-09 0:0:16.472+01:00">>))
]}.
start_test() ->
application:start(qdate),
qdate:start(),
set_timezone(?SELF_TZ),
set_timezone(?SITE_KEY,?SITE_TZ),
set_timezone(?USER_KEY,?USER_TZ),
@ -1391,3 +1531,5 @@ microsoft_parser(_) ->
stop_test(_) ->
ok.
-endif.

15
src/qdate_app.erl Normal file
View file

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

View file

@ -1,12 +1,9 @@
% vim: ts=4 sw=4 et
% Copyright (c) 2013-2015 Jesse Gumm
% Copyright (c) 2013-2021 Jesse Gumm
% See LICENSE for licensing information.
%
% NOTE: You'll probably notice that this isn't *actually* a server. It *used*
% to be a server, but is now instead just where we interact with the qdate
% application environment. Anyway, sorry for the confusion.
-module(qdate_srv).
-behaviour(gen_server).
-export([
set_timezone/1,
@ -28,6 +25,19 @@
get_formats/0
]).
%% API
-export([start_link/0]).
%% gen_server callbacks
-export([init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3]).
%% Simple wrappers for unique keys
-define(BASETAG, qdate_var).
-define(KEY(Name), {?BASETAG, Name}).
@ -39,6 +49,42 @@
-define(FORMATTAG, qdate_format).
-define(FORMATKEY(Name), {?FORMATTAG, Name}).
-define(SERVER, ?MODULE).
-define(TABLE, ?MODULE).
-record(state, {}).
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
init([]) ->
error_logger:info_msg("Creating qdate ETS Table: ~p",[?TABLE]),
?TABLE = ets:new(?TABLE, [public, {read_concurrency, true}, named_table]),
{ok, #state{}}.
handle_call({set, Key, Val}, _From, State) ->
ets:insert(?TABLE, {Key, Val}),
{reply, ok, State};
handle_call({unset, Key}, _From, State) ->
ets:delete(?TABLE, Key),
{reply, ok, State};
handle_call(_, _From, State) ->
{reply, invalid_request, State}.
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%% PUBLIC API FUNCTIONS
set_timezone(TZ) ->
@ -91,26 +137,41 @@ get_formats() ->
%% App Vars
set_env(Key, Val) ->
application:set_env(qdate, ?KEY(Key), Val).
gen_server:call(?SERVER, {set, ?KEY(Key), Val}).
get_env(Key) ->
get_env(Key, undefined).
get_env(Key, Default) ->
%% Soon, this can just be replaced with application:get_env/3
%% which was introduced in R16B.
case application:get_env(qdate, ?KEY(Key)) of
undefined -> Default;
{ok, Val} -> Val
case ets:lookup(?TABLE, ?KEY(Key)) of
[{__Key, Val}] -> Val;
[] -> Default
end.
unset_env(Key) ->
application:unset_env(qdate, ?KEY(Key)).
gen_server:call(?SERVER, {unset, ?KEY(Key)}).
get_all_env(FilterTag) ->
All = application:get_all_env(qdate),
%% Maybe this is a little nasty.
[{Key, V} || {{?BASETAG, {Tag, Key}}, V} <- All, Tag==FilterTag].
try ets:tab2list(?TABLE) of
All ->
[{Key, V} || {{?BASETAG, {Tag, Key}}, V} <- All, Tag==FilterTag]
catch
error:badarg:S ->
Msg = maybe_start_msg(),
logger:error(Msg),
error({qdate_get_all_env_failed, #{
original_error => {error, badarg, S}
}})
end.
maybe_start_msg() ->
"Attempting to read qdate environment failed (qdate_srv:get_all_env/1).\n"
"qdate may not have been started properly.\n"
"Please ensure qdate is started properly by either:\n"
"* Putting qdate in the 'applications' key in your .app.src or .app file, or\n"
"* Starting it manually with application:ensure_all_started(qdate) or qdate:start().".
%% ProcDic Vars
@ -123,3 +184,9 @@ put_pd(Key, Val) ->
unset_pd(Key) ->
put_pd(Key, undefined).

32
src/qdate_sup.erl Normal file
View file

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