diff --git a/.gitignore b/.gitignore index f424ba7..447f283 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ ebin/ tmp/ commit_msg +tzdata/ + + diff --git a/Makefile b/Makefile index 96e630f..6a23090 100644 --- a/Makefile +++ b/Makefile @@ -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 \ No newline at end of file +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 \ No newline at end of file diff --git a/include/ezic.hrl b/include/ezic.hrl index 205cee6..5da1d83 100644 --- a/include/ezic.hrl +++ b/include/ezic.hrl @@ -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. + + + + diff --git a/src/ezic.erl b/src/ezic.erl index c35ac19..4ec4f47 100644 --- a/src/ezic.erl +++ b/src/ezic.erl @@ -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")). diff --git a/src/ezic_compile.erl b/src/ezic_compile.erl new file mode 100644 index 0000000..5775ae3 --- /dev/null +++ b/src/ezic_compile.erl @@ -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. + + diff --git a/src/ezic_date.erl b/src/ezic_date.erl new file mode 100644 index 0000000..51ce110 --- /dev/null +++ b/src/ezic_date.erl @@ -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). + + diff --git a/src/ezic_loader.erl b/src/ezic_loader.erl index 01f7339..de1c2d3 100644 --- a/src/ezic_loader.erl +++ b/src/ezic_loader.erl @@ -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. diff --git a/src/ezic_record.erl b/src/ezic_record.erl index 9107caf..a803799 100644 --- a/src/ezic_record.erl +++ b/src/ezic_record.erl @@ -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. - diff --git a/src/ezic_rule.erl b/src/ezic_rule.erl new file mode 100644 index 0000000..b541508 --- /dev/null +++ b/src/ezic_rule.erl @@ -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)). diff --git a/test/ezic_date_tests.erl b/test/ezic_date_tests.erl new file mode 100644 index 0000000..0e86404 --- /dev/null +++ b/test/ezic_date_tests.erl @@ -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} )) + ]. diff --git a/tests/ezic_record_tests.erl b/test/ezic_record_tests.erl similarity index 90% rename from tests/ezic_record_tests.erl rename to test/ezic_record_tests.erl index 98fbe0a..8c9f503 100644 --- a/tests/ezic_record_tests.erl +++ b/test/ezic_record_tests.erl @@ -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")) ]. + + + diff --git a/test/test.erl b/test/test.erl new file mode 100644 index 0000000..f82ef40 --- /dev/null +++ b/test/test.erl @@ -0,0 +1,7 @@ +-module(test). +-export([all/0]). +-include_lib("eunit/include/eunit.hrl"). + +all() -> + eunit:test(ezic_record), + eunit:test(ezic_date). diff --git a/tests/test_master.erl b/tests/test_master.erl deleted file mode 100644 index 868e738..0000000 --- a/tests/test_master.erl +++ /dev/null @@ -1,5 +0,0 @@ --module(test_master). --export([start/0]). - -start() -> - ezic_record:test().