separated out rule and date logic. WIP: flattening the data, absolute from fuzzy dates, comparing dates

This commit is contained in:
aj
2010-11-03 23:33:06 -07:00
parent 188f31d543
commit e9d247784e
13 changed files with 401 additions and 61 deletions

3
.gitignore vendored
View File

@@ -3,3 +3,6 @@
ebin/
tmp/
commit_msg
tzdata/

View File

@@ -1,22 +1,24 @@
ERLC_WARNINGS = -W1
MNESIA_DIR = db
RUN_INIT =
DEBUG = +debug_info -Ddebug
TEST =
ERLC_WARNINGS := -W1
MNESIA_DIR := db
RUN_INIT :=
DEBUG := +debug_info -Ddebug
TEST :=
TZSET := northamerica
SHELL := /bin/bash
all : clean compile
nowarn : ERLC_WARNINGS = -W0
nowarn : clean compile
test : ERLC_WARNINGS = -W0
test : ERLC_WARNINGS := -W0
test : TEST := -DTEST
test : all
erlc $(ERLC_WARNINGS) $(TEST) -o ./ebin ./tests/*.erl
erl -noshell -pa ebin -s test_master start -s erlang halt
erlc $(ERLC_WARNINGS) $(TEST) -o ./ebin ./test/*.erl
erl -noshell -pa ebin -run test all -s erlang halt
compile :
erlc $(DEBUG) $(ERLC_WARNINGS) $(OPTIONS) -o ./ebin ./src/*.erl
erlc $(DEBUG) $(TEST) $(ERLC_WARNINGS) $(OPTIONS) -o ./ebin ./src/*.erl
-erl -noshell -pa ebin -s erldev make_app . -s erlang halt
clean :
@@ -27,4 +29,17 @@ run :
erl -pa ebin -mnesia dir $(MNESIA_DIR) $(RUN_INIT)
build : DEBUG =
build : all
build : all
devstart : RUN_INIT = -s ezic dev_start
devstart : all run
tzdata :
-cat priv/tzdata/$(TZSET) | sed '/\s*\#/d' | sed '/^\s*$$/d' | less -S
diff :
-git diff > /tmp/ezic.tmp.diff
emacs /tmp/ezic.tmp.diff

View File

@@ -2,7 +2,7 @@
-record(rule, {name, from, to, type, in, on, at, save, letters}).
-record(zone, {name, gmtoff, rules, format, until}).
-record(zone, {name, gmtoff, rule, format, until}).
-record(link, {from, to}).
@@ -11,7 +11,7 @@
-record(leap, {datetime, corr, rs}).
-record(tztime, {time, flag}).
-record(tztime, {time={0,0,0}, flag}).
-record(tzon, {day, filter}).
@@ -27,3 +27,7 @@
-else.
-define(debug(F,X), void).
-endif.

View File

@@ -1,10 +1,19 @@
-module(ezic).
-export([load/1, time2time/3]).
-export([dev_start/0]).
load(Folder) ->
ezic_loader:load(Folder).
time2time(_DateTime, _FromTimeZone, _ToTimeZone) ->
void.
dev_start() ->
ezic:load(filename:join("priv","tzdata")).

131
src/ezic_compile.erl Normal file
View File

@@ -0,0 +1,131 @@
-module(ezic_compile).
-include("include/ezic.hrl").
-export([flatten/2, separate/1, zone_sorter/2]).
% @todo figure out what to do with leap seconds
separate(Records) ->
Zones= [Z || Z=#zone{} <- Records],
Rules= [Z || Z=#rule{} <- Records],
Leaps= [Z || Z=#leap{} <- Records],
Links= [Z || Z=#link{} <- Records],
{ok, Zones, Rules, Leaps, Links}.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% FLATTENING PSEUDOCODE
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% args: Zones, Rules
%
% take the first zone, get all zones with the same name
% sort them by descending date (newest first)
% foreach zone:
% find all rules between start & end(if exists) of zone
% foreach rule:
% create a #flatzone
% flatten the rest of the zones
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% note: recursive
flatten([], Rules) ->
done;
flatten(Zones, Rules) ->
[Zone | _] = Zones,
% ?debug("Zone: ~p", [Zone]),
?debug("Flattening: ~p", [Zone#zone.name]),
{ZoneSet, RestZones} = lists:splitwith(make_zone_filter(Zone), Zones),
% ?debug("ZoneSet: ~p", [ZoneSet]),
NeededRules= lists:takewhile(make_rule_zone_filter(Zone), Rules),
% ?debug("NeededRules: ~p", [NeededRules]),
SortedZoneSet= lists:sort(fun zone_sorter/2, ZoneSet),
flatten_set(SortedZoneSet, NeededRules),
?debug("Sorted Zones: ~p", [SortedZoneSet]),
flatten(RestZones, Rules).
zone_sorter(Z1=#zone{until=U1},#zone{until=U2}) ->
ezic_record:compare_datetimes(U1, U2).
%% case (D1 =< D2) of
%% true ->
%% true;
%% false ->
%% ezic_record:compare_time(T1, T2)
%% end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% INTERNAL
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% filter generators
make_zone_filter(#zone{name=Name}) ->
fun(#zone{name=N}) when Name=:=N-> true; (_)->false end.
make_rule_zone_filter(#zone{rule=Rule}) ->
fun(#rule{name=Rule})-> true; (_)->false end.
make_rule_time_filter(D1, D2) ->
fun(#rule{from=F, to=T}) ->
GoodFrom= ezic_record:compare_datetimes(D1, F) =:= true
andalso ezic_record:compare_datetimes(F, D2) =:= true,
GoodTo= ezic_record:compare_datetimes(D1, T) =:= true
andalso ezic_record:compare_datetimes(T, D2) =:= true,
GoodFrom orelse GoodTo
end.
% note: recursive
flatten_set([], Rules) ->
done;
flatten_set(ZoneSet, Rules) ->
[Zone|Rest] = ZoneSet,
NextZone= next_zone(Rest),
flatten_one(Zone, NextZone, Rules),
flatten_set(Rest, Rules).
flatten_one(Zone=#zone{until=Until}, none, Rules) ->
NeededRules= lists:takewhile(make_rule_time_filter({0,0,0}, Until), Rules),
lists:foreach(fun(R)-> flatten_each(Zone, R) end, Rules);
flatten_one(Zone=#zone{until=UTo}, NextZone=#zone{until=UFrom}, Rules) ->
NeededRules= lists:takewhile(make_rule_time_filter(UFrom, UTo), Rules),
lists:foreach(fun(R)-> flatten_each(Zone, R) end, Rules).
flatten_each(Zone, Rule) ->
not_done.
next_zone([]) ->
none;
next_zone([H|T]) ->
H.

149
src/ezic_date.erl Normal file
View File

@@ -0,0 +1,149 @@
-module(ezic_date).
-include("include/ezic.hrl").
-ifdef(TEST).
-compile([export_all]).
-endif.
-export([for/2]).
-export([month_to_num/1, day_to_num/1]).
-export([compare_times/2, compare_dates/2, compare_datetimes/2]).
% returns the concrete date tuple for a given rule and year
for(#rule{in=M, on={last, D}}, Y) ->
last_day_of(D, Y,M);
for(#rule{in=M, on=#tzon{day=Day, filter=Filter}}, Y) ->
first_day_limited(Day, Filter, Y,M).
last_day_of(Day, Y,M) ->
LastDay= calendar:last_day_of_the_month(Y,M),
previous_day(Day, {Y,M,LastDay}).
first_day_limited(Day, {geq, N}, Y,M) ->
next_day(Day, {Y,M,N});
first_day_limited(Day, {leq, N}, Y,M) ->
previous_day(Day, {Y,M,N}).
previous_day(Day, Date={Y,M,D}) ->
Daynum= day_to_num(Day),
LeqDoW= calendar:day_of_the_week(Date),
case Daynum=:=LeqDoW of
true -> Date;
_ ->
DayDiff= case LeqDoW-Daynum < 0 of
true -> LeqDoW-Daynum+7;
_ -> LeqDoW-Daynum
end,
case D-DayDiff < 0 of
true -> erlang:error(no_previous_day, [Day, Date]);
false -> {Y,M,D-DayDiff}
end
end.
next_day(Day, Date={Y,M,D}) ->
Daynum= day_to_num(Day),
LeqDoW= calendar:day_of_the_week(Date),
case Daynum=:=LeqDoW of
true -> Date;
_ ->
DayDiff= case Daynum-LeqDoW < 0 of
true -> Daynum-LeqDoW+7;
_ -> Daynum-LeqDoW
end,
case D+DayDiff > calendar:last_day_of_the_month(Y,M) of
true -> erlang:error(no_next_day, [Day, Date]);
false -> {Y,M,D+DayDiff}
end
end.
month_to_num("Jan") -> 1;
month_to_num("Feb") -> 2;
month_to_num("Mar") -> 3;
month_to_num("Apr") -> 4;
month_to_num("May") -> 5;
month_to_num("Jun") -> 6;
month_to_num("Jul") -> 7;
month_to_num("Aug") -> 8;
month_to_num("Sep") -> 9;
month_to_num("Oct") -> 10;
month_to_num("Nov") -> 11;
month_to_num("Dec") -> 12;
month_to_num(X) -> erlang:error(badMonth, X).
day_to_num("Mon") -> 1;
day_to_num("Tue") -> 2;
day_to_num("Wed") -> 3;
day_to_num("Thu") -> 4;
day_to_num("Fri") -> 5;
day_to_num("Sat") -> 6;
day_to_num("Sun") -> 7;
day_to_num(_) -> -1.
% returns true if A =< B, false otherwise
compare_datetimes(_, current) ->
true;
compare_datetimes(current,_) ->
false;
compare_datetimes(D1={_,_,_}, D2={_,_,_}) ->
compare_dates(D1, D2);
compare_datetimes(D1={_,_,_}, DT2={_, #tztime{}}) ->
compare_datetimes({D1, #tztime{}}, DT2);
compare_datetimes(DT1={_, #tztime{}}, D2={_,_,_}) ->
compare_datetimes(DT1, {D2, #tztime{}});
compare_datetimes({D1, T1}, {D2, T2}) ->
case compare_dates(D1, D2) of
false -> % D1 > D2
false;
true -> % D1 =< D2
case D1 =:= D2 of
false -> % D1 < D2
true;
true -> % D1 = D2
compare_times(T1, T2)
end
end.
compare_dates(D1={_,_,_}, D2={_,_,_}) ->
D1 =< D2.
compare_times(TZ1=#tztime{}, TZ2=#tztime{}) ->
{NT1, NT2} = time_normalize(TZ1, TZ2),
NT1 =< NT2;
compare_times(T1, T2) ->
T1 =< T2.
time_normalize(#tztime{time=T1, flag=F1}, #tztime{time=T2, flag=F2}) when F1=:=F2 ->
{T1, T2};
time_normalize(T1, T2) ->
%@todo this may be difficult. gotta do it!
erlang:error(wtf_not_done, T1, T2).

View File

@@ -1,5 +1,6 @@
-module(ezic_loader).
-include("include/ezic.hrl").
-export([load/1]).
@@ -12,7 +13,8 @@ load(File) ->
false -> {error, {badFile, File}}
end
end,
flatten(Records).
{ok, Zones, Rules, Leaps, Links} = ezic_compile:separate(Records),
ezic_compile:flatten(Zones, Rules).
% @todo store in mnesia
@@ -52,7 +54,7 @@ parse_lines({ok, Line}, File, Records) ->
parse_lines_2(Line, File, Records) ->
?debug("Line: ~p", [Line]),
%?debug("Line: ~p", [Line]),
[Type | Data] = string:tokens(Line, " \t"),
{ok, PrevType, PrevName} = prev_rec_type(Records),
{ok, Record} = build_record(Type, Data, {PrevType, PrevName}),
@@ -94,8 +96,3 @@ build_record("Leap", Data,_) ->
build_record(Type, Data, PT) ->
{error, {badLine, Type, Data, PT}}.
% converts tzdata records to flattened records
% returns list of #flatzone{}
flatten(Records) when is_list(Records) ->
Records.

View File

@@ -16,7 +16,7 @@ rule([Name,FromS,ToS,Type,InS,OnS,AtS,SaveS,Letters]) ->
From= parse_from(FromS),
To= parse_to(ToS),
% @todo handle year types
In= month_to_num(InS),
In= ezic_date:month_to_num(InS),
On= parse_on(OnS),
At=parse_at(AtS),
Save=parse_save(SaveS),
@@ -27,13 +27,13 @@ rule([Name,FromS,ToS,Type,InS,OnS,AtS,SaveS,Letters]) ->
}}.
zone([Name,GmtOffS,Rules,FormatS | UntilTokens]) ->
zone([Name,GmtOffS,Rule,FormatS | UntilTokens]) ->
GmtOff=parse_time(GmtOffS),
Until = parse_until(UntilTokens),
Format= convert_format(FormatS),
{ok, #zone{
name=Name, gmtoff=GmtOff, rules=Rules,
name=Name, gmtoff=GmtOff, rule=Rule,
format=Format, until=Until
}}.
@@ -44,13 +44,14 @@ link([From,To]) ->
leap([YearS, MonthS, DayS, TimeS, Corr, RS]) ->
Y=list_to_integer(YearS),
M=month_to_num(MonthS),
M=ezic_date:month_to_num(MonthS),
D=list_to_integer(DayS),
Time=parse_abs_time(TimeS),
{ok, #leap{datetime={{Y,M,D}, Time}}}.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% PRIVATE
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
@@ -73,8 +74,8 @@ parse_to(Other) ->
parse_on(X=[$l,$a,$s,$t|Day]) ->
case is_day(Day) of
true -> list_to_atom(X);
case ezic_date:day_to_num(Day) > 0 of
true -> {last, Day};
false -> erlang:error(badday,X)
end;
parse_on(X=[D,A,Y, Sign,$= | IntS]) ->
@@ -82,8 +83,8 @@ parse_on(X=[D,A,Y, Sign,$= | IntS]) ->
Day=[D,A,Y],
{ok, FSign} =parse_on_sign(Sign),
case is_day(Day) of
true -> #tzon{day=list_to_atom(string:to_lower(Day)), filter={FSign, Int}};
case ezic_date:day_to_num(Day) > 0 of
true -> #tzon{day=Day, filter={FSign, Int}};
false -> erlang:error(badday, X)
end;
parse_on(X) ->
@@ -97,16 +98,16 @@ parse_until([YS]) ->
{Y,1,1};
parse_until([YS,MS]) ->
Y= list_to_integer(YS),
M=month_to_num(MS),
M= ezic_date:month_to_num(MS),
{Y,M,1};
parse_until([YS,MS,DS]) ->
Y= list_to_integer(YS),
M=month_to_num(MS),
M= ezic_date:month_to_num(MS),
D= parse_on(DS),
{Y,M,D};
parse_until([YS,MS,DS,TS]) ->
Y= list_to_integer(YS),
M=month_to_num(MS),
M= ezic_date:month_to_num(MS),
D= parse_on(DS),
{{Y,M,D},parse_time(TS)}.
@@ -198,25 +199,3 @@ convert_format(Format, Pos) ->
month_to_num("Jan") -> 1;
month_to_num("Feb") -> 2;
month_to_num("Mar") -> 3;
month_to_num("Apr") -> 4;
month_to_num("May") -> 5;
month_to_num("Jun") -> 6;
month_to_num("Jul") -> 7;
month_to_num("Aug") -> 8;
month_to_num("Sep") -> 9;
month_to_num("Oct") -> 10;
month_to_num("Nov") -> 11;
month_to_num("Dec") -> 12;
month_to_num(X) -> erlang:error(badMonth, X).
is_day(X) when X=:="Mon";X=:="Tue";X=:="Wed";X=:="Thu";X=:="Fri";X=:="Sat";X=:="Sun" ->
true;
is_day(_) ->
false.

13
src/ezic_rule.erl Normal file
View File

@@ -0,0 +1,13 @@
-module(ezic_rule).
-include("include/ezic.hrl").
-export([]).
% returns a list of all {datetime,offset} sets that must be
% projected for this Rule.
project(Rule=#rule{from=F, to=only}) ->
not_done;
project(Rule=#rule{from=F, to=T}) ->
Dates= lists:map(fun(Y)-> ezic_date:for(Rule, Y) end, lists:seq(F, T)).

35
test/ezic_date_tests.erl Normal file
View File

@@ -0,0 +1,35 @@
-module(ezic_date_tests).
-include("include/ezic.hrl").
-include_lib("eunit/include/eunit.hrl").
prevnext_day_test_() ->
[
?_assertEqual({2010,11,3}, ezic_date:previous_day("Wed", {2010, 11, 5}))
, ?_assertEqual({2010,11,10}, ezic_date:next_day("Wed", {2010, 11, 5}))
, ?_assertEqual({2010,11,5}, ezic_date:previous_day("Fri", {2010, 11, 5}))
, ?_assertEqual({2010,11,5}, ezic_date:next_day("Fri", {2010, 11, 5}))
, ?_assertEqual({2010,11,5}, ezic_date:previous_day("Fri", {2010, 11, 10}))
, ?_assertEqual({2010,11,12}, ezic_date:next_day("Fri", {2010, 11, 10}))
, ?_assertError(no_previous_day, ezic_date:previous_day("Wed", {2010, 11, 2}))
, ?_assertError(no_next_day, ezic_date:next_day("Wed", {2010, 11, 25}))
].
firstlast_day_of_test_() ->
[
?_assertEqual({2010,11,24}, ezic_date:last_day_of("Wed", 2010, 11))
, ?_assertEqual({2010,11,30}, ezic_date:last_day_of("Tue", 2010, 11))
, ?_assertEqual({2010,11,14}, ezic_date:first_day_limited("Sun", {geq, 8}, 2010, 11))
, ?_assertEqual({2010,11,21}, ezic_date:first_day_limited("Sun", {leq, 25}, 2010, 11))
].
compare_datetimes_test_() ->
[
?_assertEqual(false, ezic_date:compare_datetimes(
{{1997,3,lastSun},{tztime,{1,0,0},u}}
, {1997,1,1} ))
].

View File

@@ -70,12 +70,12 @@ parse_on_test_() ->
[
?_assertEqual(5, ezic_record:parse_on("5"))
, ?_assertEqual(lastSun, ezic_record:parse_on("lastSun"))
, ?_assertEqual(lastMon, ezic_record:parse_on("lastMon"))
, ?_assertEqual({last, "Sun"}, ezic_record:parse_on("lastSun"))
, ?_assertEqual({last, "Mon"}, ezic_record:parse_on("lastMon"))
, ?_assertError(badday, ezic_record:parse_on("lastFoo"))
, ?_assertEqual(#tzon{day=sun, filter={geq, 8}}, ezic_record:parse_on("Sun>=8"))
, ?_assertEqual(#tzon{day=sun, filter={leq, 25}}, ezic_record:parse_on("Sun<=25"))
, ?_assertEqual(#tzon{day="Sun", filter={geq, 8}}, ezic_record:parse_on("Sun>=8"))
, ?_assertEqual(#tzon{day="Sun", filter={leq, 25}}, ezic_record:parse_on("Sun<=25"))
, ?_assertError(badday, ezic_record:parse_on("Foo<=25"))
].
@@ -85,3 +85,6 @@ convert_format_test_() ->
?_assertEqual("POS", ezic_record:convert_format("POS"))
, ?_assertEqual("P~sS", ezic_record:convert_format("P%sS"))
].

7
test/test.erl Normal file
View File

@@ -0,0 +1,7 @@
-module(test).
-export([all/0]).
-include_lib("eunit/include/eunit.hrl").
all() ->
eunit:test(ezic_record),
eunit:test(ezic_date).

View File

@@ -1,5 +0,0 @@
-module(test_master).
-export([start/0]).
start() ->
ezic_record:test().