Add deterministic_parsing env_var

This commit is contained in:
Jesse Gumm 2013-04-29 22:16:31 -05:00
parent 5a790ab4ee
commit b602984931
2 changed files with 128 additions and 6 deletions

View file

@ -144,6 +144,90 @@ be attempted before engaging the `ec_date` parser.
+ `deregister_format(Key)` - Deregister the formatting string from the
`qdate` server.
## About backwards compatibility with `ec_date` and deterministic parsing
`ec_date` and `dh_date` both have a quirk that bothers me with respect to the
parsing of dates that causes some date parsing to be *non-deterministic*. That
is, if parsing an incomplete date or time (ie, a text string that is missing a
time or a date), `ec_date` will automatically insert the current values of
those.
For example, if the following lines are run a few seconds apart:
```erlang
1> ec_date:parse("2012-02-04").
{{2012,2,4},{0,1,10}}
2> ec_date:parse("2012-02-04").
{{2012,2,4},{0,1,12}}
3> ec_date:parse("2012-02-04").
{{2012,2,4},{0,1,13}}
```
As you can see, even though the inputs are the same each time, the resulting
parsed dates have the current time inferred. The same behavior can be observed
if parsing a time without a date:
```erlang
4> ec_date:parse("7pm").
{{2013,4,30},{19,0,0}}
```
As you can see, even though the time did not specify a date, the resulting
parsed datetime has the date inferred from the current date. Admittedly,
inferring the date bothers me less than inferring the time, but in the name of
consistency, there should be options for enabling or disabling both.
### The Solution For Both
To solve this issue for users that are bothered by this, while preserving
backwards compatibility for folks who prefer this, we're going to introduce a
`qdate` application environment variable called `deterministic_parsing`.
The value of `deterministic_parsing` can be a tuple of the following format:
`{DatePref, TimePref}`
Where `DatePref` and `TimePref` are either of the following atoms:
+ `now` - Automatically fill in the missing date or time components with the
current time (the is the behavior described above)
+ `zero` - Fill in the missing date or time components with zeroed out
values. This means that if a date is missing, it'll be set to the unix
epoch (`{1970,1,1}`) and if a time is missing, it'll be set to midnight:
`{0,0,0}`.
So, the acceptable combinations can be the following:
+ `{zero, zero}` - Any missing components will be replaced with zero-values.
**(This is the qdate default behavior)**
+ `{now, zero}` - If a date is missing, insert the current date, but if a
time is missing, set it to midnight.
+ `{zero, now}` - If a date is missing, set it to the unix epoch, and if a
time is missing, set it to the current time of day.
+ `{now, now}` - If either date or time are missing, set it to the current
date or current time.
**Note:** If this application value is not set, the default behavior for
`qdate` is to avoid non-determinism and use `{zero, zero}`.
To set this value, you can either set the value manually in code with:
```erlang
application:set_env(qdate, deterministic_parsing, {now, zero}).
```
or (and this is the preferred method) use a config file and load it with
`erl -config path/to/file.config`
Sample config file specifying this application variable:
```erlang
[{qdate, [
{deterministic_parsing, {now, zero}}
]}].
```
## Demonstration
### Basic Conversion and Formatting

View file

@ -182,9 +182,17 @@ to_date(ToTZ, RawDate) ->
{ParsedDate,ParsedTZ} ->
{ParsedDate,ParsedTZ}
end,
Date = raw_to_date(RawDate3),
try raw_to_date(RawDate3) of
D={{_,_,_},{_,_,_}} ->
date_tz_to_tz(D, FromTZ, ToTZ)
catch
_:_ ->
case raw_to_date(RawDate) of
D2={{_,_,_},{_,_,_}} ->
date_tz_to_tz(D2, ?DETERMINE_TZ, ToTZ)
end
end.
date_tz_to_tz(Date, FromTZ, ToTZ).
extract_timezone(Unixtime) when is_integer(Unixtime) ->
{Unixtime, "GMT"};
@ -223,14 +231,16 @@ minutes_from_gmt_relative_timezone("-", HourStr, MinStr) ->
extract_timezone_helper(RevDate, []) ->
{lists:reverse(RevDate), ?DETERMINE_TZ};
extract_timezone_helper(RevDate, [TZ | TZs]) ->
extract_timezone_helper(RevDate, [TZ | TZs]) when length(RevDate) >= length(TZ) ->
RevTZ = lists:reverse(TZ),
case lists:split(length(TZ),RevDate) of
{RevTZ," " ++ Remainder} ->
{lists:reverse(Remainder), TZ};
_ ->
extract_timezone_helper(RevDate, TZs)
end.
end;
extract_timezone_helper(RevDate, [_TZ | TZs]) ->
extract_timezone_helper(RevDate, TZs).
determine_timezone() ->
case qdate_srv:get_timezone() of
@ -243,12 +253,24 @@ determine_timezone() ->
raw_to_date(Unixtime) when is_integer(Unixtime) ->
unixtime_to_date(Unixtime);
raw_to_date(DateString) when is_list(DateString) ->
ec_date:parse(DateString);
ec_date:parse(DateString, get_deterministic_datetime());
raw_to_date(Now = {_,_,_}) ->
calendar:now_to_datetime(Now);
raw_to_date(Date = {{_,_,_},{_,_,_}}) ->
Date.
get_deterministic_datetime() ->
DateZero = {1970,1,1},
TimeZero = {0,0,0},
case application:get_env(qdate, deterministic_parsing) of
{ok, {zero, zero}} -> {DateZero, TimeZero};
{ok, {zero, now}} -> {DateZero, time()};
{ok, {now, zero}} -> {date(), TimeZero};
{ok, {now, now}} -> {date(), time()};
undefined -> {DateZero, TimeZero};
{ok, Val} -> throw({invalid_env_var, {qdate, deterministic_parsing, Val}})
end.
%% 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.
@ -389,11 +411,27 @@ tz_test_() ->
simple_test(SetupData),
tz_tests(SetupData),
test_process_die(SetupData),
parser_format_test(SetupData)
parser_format_test(SetupData),
test_deterministic_parser(SetupData)
]}
end
}.
test_deterministic_parser(_) ->
{inorder, [
?_assertEqual(ok, application:set_env(qdate, deterministic_parsing, {now, now})),
?_assertEqual({date(), {7,0,0}}, qdate:to_date("7am")),
?_assertEqual({{2012,5,10}, time()}, qdate:to_date("2012-5-10")),
?_assertEqual(ok, application:set_env(qdate, deterministic_parsing, {zero, zero})),
?_assertEqual({{1970,1,1}, {7,0,0}}, qdate:to_date("7am")),
?_assertEqual({{2012,5,10}, {0,0,0}}, qdate:to_date("2012-5-10")),
?_assertEqual(ok, application:unset_env(qdate, deterministic_parsing)),
?_assertEqual({{1970,1,1}, {7,0,0}}, qdate:to_date("7am")),
?_assertEqual({{2012,5,10}, {0,0,0}}, qdate:to_date("2012-5-10"))
]}.
tz_tests(_) ->
{inorder,[
?_assertEqual(ok,set_timezone(?SELF_TZ)),