handled case where zone and rule both end. dealing with Tripoli recursion bug (ambiguous year in ezic_date:for_rule)

This commit is contained in:
aj
2010-11-13 03:25:50 -08:00
parent a7b0adcbce
commit 3f438e66e0
8 changed files with 185 additions and 108 deletions

3
.gitignore vendored
View File

@@ -5,4 +5,5 @@ tmp/
commit_msg
tzdata/
\#*
db/
db/
out

View File

@@ -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
emacs /tmp/ezic.tmp.diff
debug : DEBUG := +debug_info -dNODEBUG

View File

@@ -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() ->

View File

@@ -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.

View File

@@ -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).

View File

@@ -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.

View File

@@ -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))
].

View File

@@ -8,5 +8,4 @@ all() ->
, eunit:test(ezic_zone)
, eunit:test(ezic_parse)
, eunit:test(ezic_rule)
,ok.