diff --git a/.github/workflows/tests-workflow.yml b/.github/workflows/tests-workflow.yml new file mode 100644 index 0000000..70a9145 --- /dev/null +++ b/.github/workflows/tests-workflow.yml @@ -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 diff --git a/.gitignore b/.gitignore index 47010b0..6ff84d8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,10 @@ *~ *.beam *.sw? +*.iml deps/ ebin/ .eunit/ +.idea/ _build -rebar.lock +doc/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9b94420..0000000 --- a/.travis.yml +++ /dev/null @@ -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" diff --git a/CHANGELOG.markdown b/CHANGELOG.markdown index 37f504a..f44c11c 100644 --- a/CHANGELOG.markdown +++ b/CHANGELOG.markdown @@ -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 diff --git a/LICENSE b/LICENSE.md similarity index 100% rename from LICENSE rename to LICENSE.md diff --git a/Makefile b/Makefile index e94d5d0..de4d762 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/README.markdown b/README.md similarity index 97% rename from README.markdown rename to README.md index 25b9e56..a462a9b 100644 --- a/README.markdown +++ b/README.md @@ -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 diff --git a/qdate.config b/qdate.config index 877e139..a2dd0e2 100644 --- a/qdate.config +++ b/qdate.config @@ -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} ]}]. diff --git a/rebar.config b/rebar.config index a7c1af0..9dc3672 100644 --- a/rebar.config +++ b/rebar.config @@ -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">>}]}. + diff --git a/rebar.config.script b/rebar.config.script index 4b5c9b0..61542d6 100644 --- a/rebar.config.script +++ b/rebar.config.script @@ -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. diff --git a/rebar.lock b/rebar.lock new file mode 100644 index 0000000..30b2fae --- /dev/null +++ b/rebar.lock @@ -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">>}]} +]. diff --git a/rebar3 b/rebar3 deleted file mode 100755 index 266c015..0000000 Binary files a/rebar3 and /dev/null differ diff --git a/src/qdate.app.src b/src/qdate.app.src index e5f0288..2479739 100644 --- a/src/qdate.app.src +++ b/src/qdate.app.src @@ -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"}]} ]}. diff --git a/src/qdate.erl b/src/qdate.erl index e81061e..7027b27 100644 --- a/src/qdate.erl +++ b/src/qdate.erl @@ -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. diff --git a/src/qdate_app.erl b/src/qdate_app.erl new file mode 100644 index 0000000..36a8289 --- /dev/null +++ b/src/qdate_app.erl @@ -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. + + diff --git a/src/qdate_srv.erl b/src/qdate_srv.erl index 6aeecc0..731b8b1 100644 --- a/src/qdate_srv.erl +++ b/src/qdate_srv.erl @@ -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). + + + + + + diff --git a/src/qdate_sup.erl b/src/qdate_sup.erl new file mode 100644 index 0000000..492dd8e --- /dev/null +++ b/src/qdate_sup.erl @@ -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 +%%%=================================================================== + + +