From d664384c603cc12868365f970008e73e4ad3261c Mon Sep 17 00:00:00 2001 From: aj Date: Sat, 13 Nov 2010 22:54:44 -0800 Subject: [PATCH] Costa Rica's flatzones aren't processed after 1992. Gotta check her out. --- Makefile | 7 ++++--- src/ezic.erl | 28 ++++++++++++++++--------- src/ezic_date.erl | 32 +++++++++++++++-------------- src/ezic_db.erl | 27 ++++++++++++++++++++++-- src/ezic_flatten.erl | 44 +++++++++++++++++++++++++++------------- src/ezic_rule.erl | 12 +++++++---- src/ezic_zone.erl | 1 + test/ezic_date_tests.erl | 22 ++++++++++++++++++-- test/ezic_rule_tests.erl | 13 +++++++++--- 9 files changed, 133 insertions(+), 53 deletions(-) diff --git a/Makefile b/Makefile index ceed2c8..13592e3 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ ERLC_WARNINGS := -W1 MNESIA_DIR := db RUN_INIT := -run ezic_db init -DEBUG := -DNODEBUG +DEBUG := -DNODEBUG -Ddebug TEST := TZSET := northamerica SHELL := /bin/bash @@ -33,7 +33,7 @@ run : erl -pa ebin -mnesia dir $(MNESIA_DIR) $(RUN_INIT) -devstart : RUN_INIT := $(RUN_INIT) -s ezic dev_start -s erlang halt +devstart : RUN_INIT += -s ezic dev -s erlang halt devstart : all run @@ -46,4 +46,5 @@ diff : emacs /tmp/ezic.tmp.diff -debug : DEBUG := +debug_info -dNODEBUG +debug : DEBUG += +debug_info +debug : all run \ No newline at end of file diff --git a/src/ezic.erl b/src/ezic.erl index e1e6f71..bd848cb 100644 --- a/src/ezic.erl +++ b/src/ezic.erl @@ -8,7 +8,7 @@ , next_timechange_after/2 ]). --export([load/1, dev_start/0, test/0]). +-export([load/1, dev/0, test/0]). @@ -21,13 +21,19 @@ localtime(TzName) -> utc_to_local(erlang:universaltime(), TzName). -utc_to_local(UTCDatetime, TzName) -> - {ok, Zone, Rule}= current_as_of_utc(UTCDatetime, TzName), - SecDiff= time_offset(Zone, Rule), - ezic_date:add_seconds(UTCDatetime, SecDiff). +%% utc_to_local(UTCDatetime, TzName) -> +%% {ok, Zone, Rule}= current_as_of_utc(UTCDatetime, TzName), +%% SecDiff= time_offset(Zone, Rule), +%% ezic_date:add_seconds(UTCDatetime, SecDiff). + +utc_to_local(UTCDatetime, TzName) -> + #flatzone{offset=Offset, dstoffset=DSTOffset}= ezic_db:flatzone(UTCDatetime, TzName), + ezic_date:add_offset(ezic_date:add_offset(UTCDatetime, Offset), DSTOffset). + + next_timechange(TzName) -> next_timechange_after(erlang:universaltime(), TzName). @@ -48,11 +54,13 @@ load(Folder) -> ezic_loader:load(Folder). -dev_start() -> -% ezic:load(filename:join("priv","tzdata")), -% ezic_flatten:flatten(). - TZones= ezic_db:zones("Africa/Tripoli"), - ezic_flatten:flatten_all_zones(TZones). +dev() -> + ezic:load(filename:join("priv","tzdata")), + ezic_flatten:flatten(), +% Zones= ezic_db:zones("WET"), +% ezic_flatten:flatten_all_zones(Zones), + + ok. test() -> diff --git a/src/ezic_date.erl b/src/ezic_date.erl index 061f5d1..ab5302c 100644 --- a/src/ezic_date.erl +++ b/src/ezic_date.erl @@ -47,13 +47,12 @@ normalize(A) when is_atom(A) -> A; normalize(Y) when is_integer(Y) -> {{Y,1,1}, #tztime{}}; -normalize(Date={Y,M,D}) - when is_integer(Y), is_integer(M), is_integer(D) -> - {Date, #tztime{}}; -normalize(R={{_,_,_}, #tztime{}}) -> - R; -normalize({D={_,_,_}, T={_,_,_}}) -> - {D, #tztime{time=T}}. +normalize({Y,M,D}) -> + {relative_date(Y,M,D), #tztime{}}; +normalize({{Y,M,D}, T=#tztime{}}) -> + {relative_date(Y,M,D), T}; +normalize({{Y,M,D}, T={_,_,_}}) -> + {relative_date(Y,M,D), #tztime{time=T}}. %% normalizes a date, and sets the #tztime{flag=Flag} if appropriate @@ -76,23 +75,26 @@ normalize(D, Flag) -> + + %% returns RELATIVE datetime for a rule and Year %% -> {{Y,M,D},#tztime{}} | {{Y,M,D},{HH,MM,SS}} -for_rule_relative(#rule{in=M, on=D, at=At}, Y) when is_integer(D) -> - {{Y,M,D}, At}; -for_rule_relative(#rule{in=M, on={last, D}, at=At}, Y) -> - {last_day_of(D, Y,M), At}; -for_rule_relative(#rule{in=M, on=#tzon{day=Day, filter=Filter}, at=At}, Y) -> - {first_day_limited(Day, Filter, Y,M), At}. - +for_rule_relative(#rule{in=M, on=D, at=At}, Y) -> + {relative_date(Y,M,D), At}. +relative_date(Y,M,D) when is_integer(D) -> + {Y,M,D}; +relative_date(Y,M,{last, D}) -> + last_day_of(D, Y,M); +relative_date(Y,M,#tzon{day=Day, filter=Filter}) -> + first_day_limited(Day, Filter, Y,M). %% 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. +%% @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), diff --git a/src/ezic_db.erl b/src/ezic_db.erl index 5adebca..114316c 100644 --- a/src/ezic_db.erl +++ b/src/ezic_db.erl @@ -4,7 +4,7 @@ -include_lib("eunit/include/eunit.hrl"). --export([zones/1, rules/1]). +-export([zones/1, rules/1, flatzones/1, flatzone/2]). -export([wipe/0, init/0, insert_all/1, get_all/1]). @@ -49,6 +49,29 @@ get_all(Tab) when is_atom(Tab) -> end, {atomic, Ret}= mnesia:transaction(F), Ret. + + +flatzones(TzName) -> + F = fun() -> + Q = qlc:q([Fz || Fz=#flatzone{tzname=N}<- mnesia:table(flatzone), N=:=TzName]), + qlc:e(Q) + end, + {atomic, Ret}= mnesia:transaction(F), + Ret. + + +flatzone(Date, TzName) -> + F = fun()-> + Q = qlc:q( + [Fz || Fz=#flatzone{tzname=N, utc_from=UF, utc_to=UT} <- mnesia:table(flatzone) + , N=:=TzName + , ezic_date:compare(UF, Date) + , ezic_date:compare(Date, UT)]), + qlc:e(Q) + end, + {atomic, [FlatZone]}= mnesia:transaction(F), + FlatZone. + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -76,7 +99,7 @@ insert_all(Records) -> % initialize the db init() -> create_tabs(mnesia:create_schema([node()])), - mnesia:wait_for_tables([rule, zone, link, leap], 3000). + mnesia:wait_for_tables([rule, zone, link, leap, flatzone], 3000). % WARNING: deletes all db files diff --git a/src/ezic_flatten.erl b/src/ezic_flatten.erl index ce57f38..78df7c5 100644 --- a/src/ezic_flatten.erl +++ b/src/ezic_flatten.erl @@ -37,6 +37,7 @@ flatten() -> flatten_all_zones([]) -> done; flatten_all_zones([Z1|_]= AllZones) -> + io:format("Flattening zones: ~s~n", [Z1#zone.name]), {CurrentZones, RestZones}= ezic_zone:split_by_name(Z1, AllZones), Flats= flatten_zone_set(CurrentZones), @@ -71,7 +72,7 @@ flatten_zone_set(FromTimeStub=#flatzone{utc_from=UTCFrom, dstoffset=DSTOffset} , CurrentRule) -> [Zone | RestZones] = ezic_zone:next(Zones, UTCFrom, DSTOffset), - #zone{rule=RuleName, until=_UntilTime, gmtoff=Offset}= Zone, + #zone{rule=RuleName}= Zone, ?debugVal(Zone), @@ -106,12 +107,17 @@ flatten_zone_set(FromTimeStub=#flatzone{utc_from=UTCFrom, dstoffset=DSTOffset} %% 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 + try maxyear_reached(NFUTCFrom) of true -> ?debugMsg("maxyear reached from flatten_zone_set"), FinalFlats; false -> flatten_zone_set(NextFlat, RestZones, FinalFlats, EndingRule) + catch + exit:Reason -> + ?debugMsg("bad year for nextflat:"), + ?debugVal(NextFlat), + erlang:error(Reason) end. @@ -121,21 +127,25 @@ flatten_zone_set(FromTimeStub=#flatzone{utc_from=UTCFrom, dstoffset=DSTOffset} flatten_rule_set(FlatStart=#flatzone{utc_from=UTCFrom, dstoffset=DSTOffset, offset=Offset} , Zone, Rules, CurrentRule, Flats) -> - + + ?debugVal(Offset), + ?debugVal(DSTOffset), + ?debugVal(UTCFrom), + ValidRules= lists:delete(CurrentRule, Rules), RulesWithDates= lists:foldl( fun(R, Acc)-> case ezic_rule:project_next(R, Offset, DSTOffset, UTCFrom) of none -> Acc; - D -> [{D,R} | Acc] + {Year, D} -> [{D,Year, R} | Acc] end end ,[], ValidRules), - {UTCEndingRuleDate, EndingRule}= + {UTCEndingRuleDate, EndingRuleYear, EndingRule}= case length(RulesWithDates) > 0 of - false -> {maximum, none}; + false -> {maximum, maximum, none}; true -> hd(lists:sort(RulesWithDates)) end, @@ -143,10 +153,12 @@ flatten_rule_set(FlatStart=#flatzone{utc_from=UTCFrom, dstoffset=DSTOffset, offs ?debugVal(UTCEndingRuleDate), + ?debugVal(EndingRuleYear), + ?debugVal(EndingRule), ?debugVal(ZoneDate), - case maxyear_reached(UTCEndingRuleDate) andalso maxyear_reached(ZoneDate) of + try maxyear_reached(UTCEndingRuleDate) andalso maxyear_reached(ZoneDate) of true -> ?debugMsg("maxyear reached from flatten_rule_set"), {Flats, #flatzone{utc_from=maximum}, none}; @@ -155,7 +167,7 @@ flatten_rule_set(FlatStart=#flatzone{utc_from=UTCFrom, dstoffset=DSTOffset, offs case ezic_date:compare(ZoneDate, UTCEndingRuleDate) of false -> %% same zone, new rule - {EndFlat, NextFlat}= finish_and_start_flat(FlatStart, EndingRule, UTCEndingRuleDate), + {EndFlat, NextFlat}= finish_and_start_flat(FlatStart, EndingRule, EndingRuleYear), NewFlats= [EndFlat | Flats], flatten_rule_set(NextFlat, Zone, Rules, EndingRule, NewFlats); true -> @@ -171,6 +183,10 @@ flatten_rule_set(FlatStart=#flatzone{utc_from=UTCFrom, dstoffset=DSTOffset, offs {[IEF | Flats], INF, EndingRule} end end + catch + error:{bad_year,Val} -> + io:format("bad year for EndingRule: ~p~n", [EndingRule]), + erlang:error(bad_year, Val) end. @@ -179,10 +195,10 @@ flatten_rule_set(FlatStart=#flatzone{utc_from=UTCFrom, dstoffset=DSTOffset, offs %% rule is ending, while timezone remains finish_and_start_flat(FlatStub=#flatzone{offset=Offset, dstoffset=OldDSTOffset} , NewRule=#rule{save=NewDSTSave} - , _EndingRuleDate={{ERDY,_,_},_}) -> + , EndingRuleYear) -> %% @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), + {{WD, WDn}, SD, UD}= ezic_date:for_rule(NewRule, Offset, OldDSTOffset, NewDSTSave, EndingRuleYear), {WDm, SDm, UDm}= ezic_date:m1s({WD, SD, UD}), EndFlat= ?ENDFLAT(FlatStub, WDm, SDm, UDm, OldDSTOffset), @@ -202,7 +218,7 @@ finish_and_start_flat(FlatStub=#flatzone{offset=Offset, dstoffset=OldDSTOffset} %% the next zone with the current DST offset and the UTC start %% datetime finish_and_start_flat(FlatStub=#flatzone{}, Zone=#zone{}, EndingDST) -> - EndDatesP1={WD,SD,UD}= ezic_zone:project_end(Zone, EndingDST), + EndDatesP1={_,_,UD}= ezic_zone:project_end(Zone, EndingDST), {WDm, SDm, UDm}= ezic_date:m1s(EndDatesP1), EndFlat= ?ENDFLAT(FlatStub, WDm, SDm, UDm, EndingDST), @@ -218,7 +234,7 @@ finish_and_start_flat(FlatStub=#flatzone{}, Zone=#zone{}, EndingDST) -> finish_flatzone_both(FlatStub=#flatzone{}, EndingZone=#zone{}, ChangingRule=#rule{save=NewDST}, EndingDST) -> ?debugVal(FlatStub), - EndDatesP1={WD,SD,UD}= ezic_zone:project_end(EndingZone, EndingDST), + EndDatesP1={_,_,UD}= ezic_zone:project_end(EndingZone, EndingDST), {WDm, SDm, UDm}= ezic_date:m1s(EndDatesP1), EndFlat= ?ENDFLAT(FlatStub, WDm, SDm, UDm, EndingDST), @@ -231,10 +247,10 @@ finish_flatzone_both(FlatStub=#flatzone{}, EndingZone=#zone{}, ChangingRule=#rul populate_flatzone( FZ=#flatzone{utc_from=UTCFrom, dstoffset=DSTOffset} - , Zone=#zone{name=Name, gmtoff=Offset}) -> + , #zone{name=Name, gmtoff=Offset}) -> UTCFromTZ= ezic_date:normalize(UTCFrom, u), - {WT, ST, UTCFRom}= ezic_date:all_times(UTCFromTZ, Offset, DSTOffset), + {WT, ST, _}= ezic_date:all_times(UTCFromTZ, Offset, DSTOffset), FZ#flatzone{offset=Offset, tzname=Name, wall_from=WT, std_from=ST}. diff --git a/src/ezic_rule.erl b/src/ezic_rule.erl index 5284149..1710d95 100644 --- a/src/ezic_rule.erl +++ b/src/ezic_rule.erl @@ -25,16 +25,20 @@ parse([Name,FromS,ToS,_Type,InS,OnS,AtS,SaveS,Letters]) -> -% returns the projected date of the next "rule event" in UTC time, -% after the given UTCDatetime, with the given the zone Offset and current DST offset. +% returns {Year, UTCDate} where UTCDate is the projected date of the +% next "rule event" in UTC time, after the given UTCDatetime, with the +% given the zone Offset and current DST offset, and Year is the rule +% year (which may differ from the UTC year). project_next(Rule=#rule{from=RFrom}, Offset, DSTOffset, minimum) -> - ezic_date:for_rule_utc(Rule, Offset, DSTOffset, RFrom); + RetDate={{RetYear,_,_},_}= ezic_date:for_rule_utc(Rule, Offset, DSTOffset, RFrom), + {RetYear, RetDate}; project_next(Rule=#rule{}, Offset, DSTOff, UTCAfter={{AY,_,_},_}) -> YearRange= years(Rule), GoodYears= years_after(AY, YearRange), project_next2(Rule, Offset, UTCAfter, DSTOff, GoodYears). + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % PRIVATE %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -71,7 +75,7 @@ project_next2(_,_,_,_,none) -> 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; + true -> {FromYear, RuleDate}; false -> RestYears= years_after(FromYear+1, Years), project_next2(Rule, Offset, UTCAfter, DSTOffset, RestYears) diff --git a/src/ezic_zone.erl b/src/ezic_zone.erl index 30e8978..b2a93ce 100644 --- a/src/ezic_zone.erl +++ b/src/ezic_zone.erl @@ -85,6 +85,7 @@ project_end(#zone{until=Until, gmtoff=Offset}, DSTOffset) -> NUntil= ezic_date:normalize(Until), ezic_date:all_times(NUntil, Offset, DSTOffset). + % returns just the UTC datetime projected for zone end project_end_utc(Zone=#zone{}, DSTOffset) -> % ?debugVal(Zone), diff --git a/test/ezic_date_tests.erl b/test/ezic_date_tests.erl index 48c4355..3fe9135 100644 --- a/test/ezic_date_tests.erl +++ b/test/ezic_date_tests.erl @@ -55,10 +55,10 @@ for_rule_test_() -> , ?_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}} }, + {{1951,12,31},{22,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)) + {1,0,0}, {1,0,0}, {0,0,0}, 1952)) ]. @@ -96,3 +96,21 @@ all_times_test_() -> {{Date,{9,2,0}}, {Date,{8,2,0}}, {Date,{16,2,0}}} , ezic_date:all_times({Date,UTC}, {-8,0,0}, {1,0,0})) ]. + + + +normalize_test_() -> + TZT= #tztime{time={1,2,3}, flag=s}, + [ + ?_assertEqual({{2010,1,1}, #tztime{}}, ezic_date:normalize(2010)) + , ?_assertEqual({{2010,2,3}, #tztime{}}, ezic_date:normalize({2010,2,3})) + , ?_assertEqual({{2010,11,28}, #tztime{}}, ezic_date:normalize({2010,11,{last, "Sun"}})) + , ?_assertEqual({{2010,11,16}, #tztime{}}, ezic_date:normalize({2010,11,#tzon{day="Tue", filter={geq, 10}}})) + + , ?_assertError(function_clause, ezic_date:normalize({2010, #tztime{}})) + + , ?_assertEqual({{2010,2,3}, TZT}, ezic_date:normalize({{2010,2,3}, TZT})) + , ?_assertEqual({{2010,11,28}, TZT}, ezic_date:normalize({{2010,11,{last, "Sun"}}, TZT})) + , ?_assertEqual({{2010,11,16}, TZT}, ezic_date:normalize({{2010,11,#tzon{day="Tue", filter={geq, 10}}}, TZT})) + + ]. diff --git a/test/ezic_rule_tests.erl b/test/ezic_rule_tests.erl index ae0c59e..8eb343a 100644 --- a/test/ezic_rule_tests.erl +++ b/test/ezic_rule_tests.erl @@ -7,21 +7,21 @@ project_next_test_() -> [ % simple UTC example - ?_assertEqual({{2010,11,10},{16,20,0}}, + ?_assertEqual({2010, {{2010,11,10},{16,20,0}}}, ezic_rule:project_next( #rule{from=1984, to=2032, in=11, on=10, at=#tztime{time={16,20,0}, flag=u}} , {-5,0,0}, {-1,0,0}, {{2010,11,10},{0,0,0}} )) % skip a year UTC example - , ?_assertEqual({{2011,11,10},{16,20,0}}, + , ?_assertEqual({2011, {{2011,11,10},{16,20,0}}}, ezic_rule:project_next( #rule{from=1984, to=2032, in=11, on=10, at=#tztime{time={16,20,0}, flag=u}} , {-5,0,0}, {-1,0,0}, {{2010,11,10},{16,21,0}} )) % skip 3 years UTC example - , ?_assertEqual({{2013,11,10},{16,20,0}}, + , ?_assertEqual({2013, {{2013,11,10},{16,20,0}}}, ezic_rule:project_next( #rule{from=1984, to=2032, in=11, on=10, at=#tztime{time={16,20,0}, flag=u}} , {-5,0,0}, {-1,0,0}, {{2012,11,11},{16,20,0}} @@ -34,4 +34,11 @@ project_next_test_() -> , {-5,0,0}, {-1,0,0}, {{2012,11,11},{16,20,0}} )) + + % wacky UTC/DST mismatch example for Africa/Tripoli + , ?_assertEqual({1952, {{1951,12,31},{22,0,0}}}, + ezic_rule:project_next( + #rule{from=1952, to=only, in=1, on=1, at=#tztime{}, save={0,0,0}} + , {1,0,0}, {1,0,0}, {{1951,10,14},{3,0,0}} + )) ].