From 3f438e66e0168c6b3f17fe82b379c7626054fccf Mon Sep 17 00:00:00 2001 From: aj Date: Sat, 13 Nov 2010 03:25:50 -0800 Subject: [PATCH] handled case where zone and rule both end. dealing with Tripoli recursion bug (ambiguous year in ezic_date:for_rule) --- .gitignore | 3 +- Makefile | 15 ++-- src/ezic.erl | 4 +- src/ezic_date.erl | 36 ++++++---- src/ezic_flatten.erl | 152 +++++++++++++++++++++++++-------------- src/ezic_rule.erl | 72 ++++++++++--------- test/ezic_date_tests.erl | 10 +++ test/test.erl | 1 - 8 files changed, 185 insertions(+), 108 deletions(-) diff --git a/.gitignore b/.gitignore index 8d047d9..80aa7a7 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ tmp/ commit_msg tzdata/ \#* -db/ \ No newline at end of file +db/ +out diff --git a/Makefile b/Makefile index af6b493..ceed2c8 100644 --- a/Makefile +++ b/Makefile @@ -1,16 +1,19 @@ ERLC_WARNINGS := -W1 MNESIA_DIR := db RUN_INIT := -run ezic_db init -DEBUG := +debug_info -Ddebug +DEBUG := -DNODEBUG TEST := TZSET := northamerica SHELL := /bin/bash + all : clean compile + nowarn : ERLC_WARNINGS = -W0 nowarn : clean compile + test : ERLC_WARNINGS := -W0 test : TEST := -DTEST test : all @@ -29,11 +32,8 @@ clean : run : erl -pa ebin -mnesia dir $(MNESIA_DIR) $(RUN_INIT) -build : DEBUG = -build : all - -devstart : RUN_INIT := $(RUN_INIT) -s ezic dev_start +devstart : RUN_INIT := $(RUN_INIT) -s ezic dev_start -s erlang halt devstart : all run @@ -43,4 +43,7 @@ tzdata : diff : -git diff > /tmp/ezic.tmp.diff - emacs /tmp/ezic.tmp.diff \ No newline at end of file + emacs /tmp/ezic.tmp.diff + + +debug : DEBUG := +debug_info -dNODEBUG diff --git a/src/ezic.erl b/src/ezic.erl index a2ac2cb..e1e6f71 100644 --- a/src/ezic.erl +++ b/src/ezic.erl @@ -50,7 +50,9 @@ load(Folder) -> dev_start() -> % ezic:load(filename:join("priv","tzdata")), - ezic_flatten:flatten(). +% ezic_flatten:flatten(). + TZones= ezic_db:zones("Africa/Tripoli"), + ezic_flatten:flatten_all_zones(TZones). test() -> diff --git a/src/ezic_date.erl b/src/ezic_date.erl index c56b8c1..061f5d1 100644 --- a/src/ezic_date.erl +++ b/src/ezic_date.erl @@ -51,13 +51,15 @@ normalize(Date={Y,M,D}) when is_integer(Y), is_integer(M), is_integer(D) -> {Date, #tztime{}}; normalize(R={{_,_,_}, #tztime{}}) -> - R. + R; +normalize({D={_,_,_}, T={_,_,_}}) -> + {D, #tztime{time=T}}. %% normalizes a date, and sets the #tztime{flag=Flag} if appropriate %% @todo ensure flag is valid %% @todo cover all the cases. this is currently just used for standard erlang datetimes -normalize(DT={D={_,_,_},T={HH,_,_}}, Flag) +normalize({D={_,_,_},T={HH,_,_}}, Flag) when is_atom(Flag), is_integer(HH) -> DTz= {D, #tztime{time=T, flag=Flag}}, @@ -90,6 +92,7 @@ for_rule_relative(#rule{in=M, on=#tzon{day=Day, filter=Filter}, at=At}, Y) -> %% returns set of ALL datetimes for a rule, given the gmt offset and %% current dst offset. +%% @bug @todo Year is ambiguous. In the case of Africa/Tripoli, a jan 1st, 1952 rule shows up as 1951 due to offset and dst. for_rule(Rule, Offset, PrevDSTOffset, NextDSTOffset, Year) -> {WT,ST,UT}= for_rule_old_dst(Rule, Offset, PrevDSTOffset, Year), WTNew= add_offset(WT, PrevDSTOffset, NextDSTOffset), @@ -101,7 +104,7 @@ for_rule(Rule, Offset, PrevDSTOffset, NextDSTOffset, Year) -> for_rule_old_dst(Rule, Offset, PrevDSTOffset, Year) -> DT= for_rule_relative(Rule, Year), - {WT,ST,UT}= all_times(DT, Offset, PrevDSTOffset). + all_times(DT, Offset, PrevDSTOffset). @@ -239,22 +242,25 @@ when is_integer(Y1), is_integer(Y2) , is_integer(SS1), is_integer(SS2) -> - DT1 =< DT2. + DT1 =< DT2; +compare({Date1, #tztime{time=Time1, flag=F}} + , {Date2, #tztime{time=Time2, flag=F}}) -> + + Date1 =< Date2 orelse Time1 =< time2; -% returns true if DT1 =:= DT2. False otherwise -% both times are assumed to be in the same zone/DST context -equal(DT1={{Y1,M1,D1},{HH1,MM1,SS1}}, DT2={{Y2,M2,D2},{HH2,MM2,SS2}}) -when is_integer(Y1), is_integer(Y2) - , is_integer(M1), is_integer(M2) - , is_integer(D1), is_integer(D2) - , is_integer(HH1), is_integer(HH2) - , is_integer(MM1), is_integer(MM2) - , is_integer(SS1), is_integer(SS2) - -> +compare(X,Y) -> + XN= normalize(X), + YN= normalize(Y), + compare(XN, YN). + - DT1 =:= DT2. + +equal(X,Y) when X=:=Y -> + true; +equal(_,_) -> + false. diff --git a/src/ezic_flatten.erl b/src/ezic_flatten.erl index e952123..ce57f38 100644 --- a/src/ezic_flatten.erl +++ b/src/ezic_flatten.erl @@ -9,16 +9,22 @@ +%% @todo move MAXYEAR to config file +-define(MAXYEAR, 2500). % last year to process flatzones for. + -export([flatten/0]). +%% debug +-export([flatten_all_zones/1]). + flatten() -> AllZones= ezic_db:get_all(zone), flatten_all_zones(AllZones), - not_done. + done. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -28,15 +34,15 @@ flatten() -> % recursively processes sets of similar zones until they're all done, % passing zone sets to flatten_zone_set/1 -flatten_all_zones(Zones) -> - flatten_all_zones(Zones, []). - -flatten_all_zones([], Flats) -> - Flats; -flatten_all_zones([Z1|_]= AllZones, Flats) -> +flatten_all_zones([]) -> + done; +flatten_all_zones([Z1|_]= AllZones) -> {CurrentZones, RestZones}= ezic_zone:split_by_name(Z1, AllZones), - NewFlats= lists:merge(Flats, flatten_zone_set(CurrentZones)), - flatten_all_zones(RestZones, NewFlats). + + Flats= flatten_zone_set(CurrentZones), + ezic_db:insert_all(Flats), + + flatten_all_zones(RestZones). @@ -50,7 +56,7 @@ flatten_all_zones([Z1|_]= AllZones, Flats) -> %% offset (#flatzone). This is a recursive solution, eliminating Zones %% from the list until it's been exhausted flatten_zone_set(Zones) -> - flatten_zone_set(?MINFLAT, Zones, []). + flatten_zone_set(?MINFLAT, Zones, [], none). % flatten_zone_set(FromTime, Zones, Flats) -> [#flatzone{}] @@ -59,12 +65,10 @@ flatten_zone_set(Zones) -> % Flats= [#flatzone{}] % % assumes a new zone every time it is called -flatten_zone_set(_, [], Flats) -> - erlang:error(debug_quit), - Flats; flatten_zone_set(FromTimeStub=#flatzone{utc_from=UTCFrom, dstoffset=DSTOffset} , Zones %[Z1=#zone{rule=RuleName, until=UntilTime, gmtoff=Offset} | _RestZones], - , Flats) -> + , Flats + , CurrentRule) -> [Zone | RestZones] = ezic_zone:next(Zones, UTCFrom, DSTOffset), #zone{rule=RuleName, until=_UntilTime, gmtoff=Offset}= Zone, @@ -75,11 +79,11 @@ flatten_zone_set(FromTimeStub=#flatzone{utc_from=UTCFrom, dstoffset=DSTOffset} %% must populate the base gmt offset and name for the current zone FromTime= populate_flatzone(FromTimeStub, Zone), -% FromTime= FromTimeStub#flatzone{offset=Offset, tzname=Zone#zone.name}, - %% if this is the first run, DST offset is {0,0,0} - %% if this is a recursion, DST offset is the previous zone's last DST offset - %% @todo see if dst rules carry over zone changes IRL + %% if this is the first run, DST offset is {0,0,0} if this is a + %% recursion, DST offset is the previous zone's last DST offset + %% @todo see if dst rules carry over zone changes IRL. I'll assume + %% they don't. %% we gather all rules that _may_ apply @@ -89,35 +93,36 @@ flatten_zone_set(FromTimeStub=#flatzone{utc_from=UTCFrom, dstoffset=DSTOffset} ?debugVal(FromTime), ?debugVal(Rules), + %% apply all rules in order, creating flatzones, until this zone - %% ends, then regain control. - {RuleFlats, LastFlat, EndingDST}= flatten_rule_set(FromTime, Zone, Rules, []), + %% ends, then regain control. rules have been exhausted, and zone + %% is ending. let's finish this. - ?debugVal(RuleFlats), - ?debugVal(EndingDST), - - %% rules have been exhausted, and zone is ending. let's finish this. - {FinalFlat, NextFlat}= finish_and_start_flat(LastFlat, Zone, EndingDST), - FinalFlats= lists:merge([[FinalFlat], RuleFlats, Flats]), + {RuleFlats, NextFlat, EndingRule}= flatten_rule_set(FromTime, Zone, Rules, CurrentRule, []), + FinalFlats= lists:append([RuleFlats, Flats]), - ?debugVal(FinalFlat), ?debugVal(FinalFlats), - + ?debugVal(NextFlat), - flatten_zone_set(NextFlat, RestZones, FinalFlats). + %% return flats if we've exceeded our years, or recurse if we can keep going + NFUTCFrom = NextFlat#flatzone.utc_from, + case maxyear_reached(NFUTCFrom) of + true -> + ?debugMsg("maxyear reached from flatten_zone_set"), + FinalFlats; + false -> + flatten_zone_set(NextFlat, RestZones, FinalFlats, EndingRule) + end. flatten_rule_set(FlatStart=#flatzone{utc_from=UTCFrom, dstoffset=DSTOffset, offset=Offset} - , Zone, Rules, Flats) -> + , Zone, Rules, CurrentRule, Flats) -> + - %% add normalized (possibly inaccurate) dates for sorting - %% purposes. note this may be empty. also note this MUST (I think) - %% be done in the loop, since UTCFrom and DSTOffset can - %% potentially change which rule comes next (however unlikely that - %% case may be in real life) + ValidRules= lists:delete(CurrentRule, Rules), RulesWithDates= lists:foldl( fun(R, Acc)-> case ezic_rule:project_next(R, Offset, DSTOffset, UTCFrom) of @@ -125,12 +130,10 @@ flatten_rule_set(FlatStart=#flatzone{utc_from=UTCFrom, dstoffset=DSTOffset, offs D -> [{D,R} | Acc] end end - ,[], Rules), + ,[], ValidRules), - ?debugVal(RulesWithDates), - - {ActualEndingRuleDate, EndingRule}= + {UTCEndingRuleDate, EndingRule}= case length(RulesWithDates) > 0 of false -> {maximum, none}; true -> hd(lists:sort(RulesWithDates)) @@ -139,26 +142,45 @@ flatten_rule_set(FlatStart=#flatzone{utc_from=UTCFrom, dstoffset=DSTOffset, offs ZoneDate= ezic_zone:project_end_utc(Zone, DSTOffset), - ?debugVal(ActualEndingRuleDate), + ?debugVal(UTCEndingRuleDate), ?debugVal(ZoneDate), - - case ezic_date:compare(ActualEndingRuleDate, ZoneDate) of + + case maxyear_reached(UTCEndingRuleDate) andalso maxyear_reached(ZoneDate) of true -> - %% same zone, new rule - {EndFlat, NextFlat}= finish_and_start_flat(FlatStart, EndingRule, ActualEndingRuleDate, Offset, DSTOffset), - NewFlats= [EndFlat | Flats], - flatten_rule_set(NextFlat, Zone, Rules, NewFlats); + ?debugMsg("maxyear reached from flatten_rule_set"), + {Flats, #flatzone{utc_from=maximum}, none}; false -> - %% new zone is handled in the caller: flatten_zone_set - {Flats, FlatStart, DSTOffset} + + case ezic_date:compare(ZoneDate, UTCEndingRuleDate) of + false -> + %% same zone, new rule + {EndFlat, NextFlat}= finish_and_start_flat(FlatStart, EndingRule, UTCEndingRuleDate), + NewFlats= [EndFlat | Flats], + flatten_rule_set(NextFlat, Zone, Rules, EndingRule, NewFlats); + true -> + % zone change + case ezic_date:equal(ZoneDate, UTCEndingRuleDate) of + false -> + % zone only + {IEF, INF}= finish_and_start_flat(FlatStart, Zone, DSTOffset), + {[IEF | Flats], INF, CurrentRule}; + true -> + % zone AND rule + {IEF, INF}= finish_flatzone_both(FlatStart, Zone, EndingRule, DSTOffset), + {[IEF | Flats], INF, EndingRule} + end + end end. - -finish_and_start_flat(FlatStub=#flatzone{}, NewRule=#rule{save=NewDSTSave}, _EndingRuleDate={{ERDY,_,_},_}, Offset, OldDSTOffset) -> +%% rule is ending, while timezone remains +finish_and_start_flat(FlatStub=#flatzone{offset=Offset, dstoffset=OldDSTOffset} + , NewRule=#rule{save=NewDSTSave} + , _EndingRuleDate={{ERDY,_,_},_}) -> + %% @todo for_rule_all was already called in a loop earlier. use those values instead. {{WD, WDn}, SD, UD}= ezic_date:for_rule(NewRule, Offset, OldDSTOffset, NewDSTSave, ERDY), {WDm, SDm, UDm}= ezic_date:m1s({WD, SD, UD}), @@ -172,9 +194,10 @@ finish_and_start_flat(FlatStub=#flatzone{}, NewRule=#rule{save=NewDSTSave}, _End ?debugVal(EndFlat), ?debugVal(FinalNewFlat), - {EndFlat, FinalNewFlat}. + {EndFlat, FinalNewFlat}; +%% timezone is ending, while rule remains the same. %% returns the finished flatzone for the current zone, and a stub for %% the next zone with the current DST offset and the UTC start %% datetime @@ -186,11 +209,25 @@ finish_and_start_flat(FlatStub=#flatzone{}, Zone=#zone{}, EndingDST) -> RetNextFlat= #flatzone{dstoffset=EndingDST, utc_from=UD}, ?debugVal(EndFlat), - ?debugVal(RetNextFlat), +% ?debugVal(RetNextFlat), {EndFlat, RetNextFlat}. +%% both timezone and rule are ending at the same time +finish_flatzone_both(FlatStub=#flatzone{}, EndingZone=#zone{}, ChangingRule=#rule{save=NewDST}, EndingDST) -> + ?debugVal(FlatStub), + + EndDatesP1={WD,SD,UD}= ezic_zone:project_end(EndingZone, EndingDST), + {WDm, SDm, UDm}= ezic_date:m1s(EndDatesP1), + EndFlat= ?ENDFLAT(FlatStub, WDm, SDm, UDm, EndingDST), + + ?debugVal(EndFlat), + ?debugVal(ChangingRule), + + RetNextFlat= #flatzone{dstoffset=NewDST, utc_from=UD}, + {EndFlat, RetNextFlat}. + populate_flatzone( FZ=#flatzone{utc_from=UTCFrom, dstoffset=DSTOffset} @@ -199,3 +236,14 @@ populate_flatzone( UTCFromTZ= ezic_date:normalize(UTCFrom, u), {WT, ST, UTCFRom}= ezic_date:all_times(UTCFromTZ, Offset, DSTOffset), FZ#flatzone{offset=Offset, tzname=Name, wall_from=WT, std_from=ST}. + + + +maxyear_reached({{Y,_,_},_}) when Y > ?MAXYEAR -> + true; +maxyear_reached({{Y,_,_},_}) when Y =< ?MAXYEAR -> + false; +maxyear_reached(Atom) when Atom=:=maximum; Atom=:=current -> + true; % for some N, taking n > N => maximum > ?MAXYEAR +maxyear_reached(Val) -> + erlang:error(bad_year, Val). diff --git a/src/ezic_rule.erl b/src/ezic_rule.erl index c5d768a..5284149 100644 --- a/src/ezic_rule.erl +++ b/src/ezic_rule.erl @@ -6,6 +6,7 @@ -export([project_next/4]). + parse([Name,FromS,ToS,_Type,InS,OnS,AtS,SaveS,Letters]) -> From= ezic_parse:year(FromS), To= ezic_parse:year(ToS), @@ -29,8 +30,8 @@ parse([Name,FromS,ToS,_Type,InS,OnS,AtS,SaveS,Letters]) -> project_next(Rule=#rule{from=RFrom}, Offset, DSTOffset, minimum) -> ezic_date:for_rule_utc(Rule, Offset, DSTOffset, RFrom); project_next(Rule=#rule{}, Offset, DSTOff, UTCAfter={{AY,_,_},_}) -> - AllYears= years(Rule), - GoodYears= years_after(AY, AllYears), + YearRange= years(Rule), + GoodYears= years_after(AY, YearRange), project_next2(Rule, Offset, UTCAfter, DSTOff, GoodYears). @@ -39,41 +40,41 @@ project_next(Rule=#rule{}, Offset, DSTOff, UTCAfter={{AY,_,_},_}) -> %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -% returns the sorted list of years a rule existed for. +% returns the range of years a rule existed for, normalized for calculations. years(#rule{from=From, to=only}) -> - [From]; + {From,From}; years(#rule{from=From, to=X}) when X=:=max; X=:=maximum-> - [From,9999]; % let's hope this isn't used past year 9999 + {From,X}; +years(#rule{from=From, to=To}) when From =< To, is_integer(From), is_integer(To) -> + {From, To}; years(#rule{from=From, to=To}) -> - try lists:seq(From, To) - catch error:function_clause -> - erlang:error(bad_year_range, [From, To]) - end. + erlang:error(bad_year_range, [From, To]). -% returns the years from the list that are greater than OR equal to Y -years_after(_, []) -> - []; -years_after(Y, Years) -> - {GOOD, _}= lists:partition( - fun(X) when Y =< X -> true; - (_) -> false - end, Years), - GOOD. - +%% returns the range of years that are greater than OR equal to Y. +%% calculation stops after a fixed year, configurable above. +years_after(Y, {F,M}) when M=:=max; M=:=maximum -> + {erlang:max(Y,F), M}; +years_after(Y, {From, To}) when Y > To -> + none; +years_after(Y, {From, To}) -> + {erlang:max(Y,From), To}. + + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % INTERNAL %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -project_next2(_,_,_,_,[]) -> +project_next2(_,_,_,_,none) -> none; -project_next2(Rule, Offset, UTCAfter, DSTOffset, [Year|RestYears]) -> - RuleDate= ezic_date:for_rule_utc(Rule, Offset, DSTOffset, Year), +project_next2(Rule, Offset, UTCAfter, DSTOffset, Years={FromYear, _}) -> + RuleDate= ezic_date:for_rule_utc(Rule, Offset, DSTOffset, FromYear), case ezic_date:compare(UTCAfter, RuleDate) andalso (not ezic_date:equal(UTCAfter, RuleDate)) of true -> RuleDate; - _ -> project_next2(Rule, Offset, UTCAfter, DSTOffset, RestYears) + false -> + RestYears= years_after(FromYear+1, Years), + project_next2(Rule, Offset, UTCAfter, DSTOffset, RestYears) end. @@ -91,9 +92,13 @@ project_next2(Rule, Offset, UTCAfter, DSTOffset, [Year|RestYears]) -> years_test_() -> [ - ?_assertEqual([1952], years(#rule{from=1952,to=only})) - , ?_assertEqual([1952,1953], years(#rule{from=1952,to=1953})) - , ?_assertEqual([1952,1953,1954,1955], years(#rule{from=1952,to=1955})) + ?_assertEqual({1952,1952}, years(#rule{from=1952,to=only})) + , ?_assertEqual({1952,1953}, years(#rule{from=1952,to=1953})) + , ?_assertEqual({1952,1955}, years(#rule{from=1952,to=1955})) + + , ?_assertEqual({1952,max}, years(#rule{from=1952,to=max})) + , ?_assertEqual({1952,maximum}, years(#rule{from=1952,to=maximum})) + , ?_assertError(bad_year_range, years(#rule{from=1955,to=1952})) ]. @@ -101,11 +106,14 @@ years_test_() -> % @todo type checking. lack of types worries me sometimes :( years_after_test_() -> [ - ?_assertEqual([2010], years_after(2010, [2010])) - , ?_assertEqual([2010,2011], years_after(2010, [2010,2011])) - , ?_assertEqual([2011], years_after(2011, [2010,2011])) - , ?_assertEqual([], years_after(2012, [2010,2011])) - , ?_assertEqual([], years_after(2012, [])) + ?_assertEqual({2010,2010}, years_after(2010, {2010,2010})) + , ?_assertEqual({2010,2011}, years_after(2010, {2010,2011})) + , ?_assertEqual({2011,2011}, years_after(2011, {2010,2011})) + + , ?_assertEqual({2011,max}, years_after(2011, {2010,max})) + , ?_assertEqual({2010,max}, years_after(2009, {2010,max})) + + , ?_assertEqual(none, years_after(2012, {2010,2011})) ]. -endif. diff --git a/test/ezic_date_tests.erl b/test/ezic_date_tests.erl index 2d19a2a..48c4355 100644 --- a/test/ezic_date_tests.erl +++ b/test/ezic_date_tests.erl @@ -50,6 +50,16 @@ for_rule_test_() -> {{1981,9,30},{15,0,0}} }, ezic_date:for_rule(IrkutskRule2, {8,0,0}, {1,0,0}, {0,0,0}, 1981)) + + %% Africa/Tripoli recursion bug + , ?_assertEqual({ + { {{1952,1,1},{0,0,0}}, {{1951,12,31},{23,0,0}} }, % {oldDst, newDst} + {{1951,12,31},{23,0,0}}, + {{1951,12,31},{23,0,0}} }, + ezic_date:for_rule( + #rule{from=1952, to=only, in=1, on=1, at=#tztime{}, save={0,0,0}}, + {1,0,0}, {1,0,0}, {0,0,0}, 1951)) + ]. diff --git a/test/test.erl b/test/test.erl index 0b5a3ce..af669d2 100644 --- a/test/test.erl +++ b/test/test.erl @@ -8,5 +8,4 @@ all() -> , eunit:test(ezic_zone) , eunit:test(ezic_parse) , eunit:test(ezic_rule) - ,ok.