Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

handled case where zone and rule both end. dealing with Tripoli recur…

…sion bug (ambiguous year in ezic_date:for_rule)
  • Loading branch information...
commit 3f438e66e0168c6b3f17fe82b379c7626054fccf 1 parent a7b0adc
@drfloob authored
View
3  .gitignore
@@ -5,4 +5,5 @@ tmp/
commit_msg
tzdata/
\#*
-db/
+db/
+out
View
15 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
+ emacs /tmp/ezic.tmp.diff
+
+
+debug : DEBUG := +debug_info -dNODEBUG
View
4 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() ->
View
36 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;
+
+compare(X,Y) ->
+ XN= normalize(X),
+ YN= normalize(Y),
+ compare(XN, YN).
+
-% 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)
- ->
- DT1 =:= DT2.
+equal(X,Y) when X=:=Y ->
+ true;
+equal(_,_) ->
+ false.
View
152 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}
- end.
+ 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
72 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.
View
10 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))
+
].
View
1  test/test.erl
@@ -8,5 +8,4 @@ all() ->
, eunit:test(ezic_zone)
, eunit:test(ezic_parse)
, eunit:test(ezic_rule)
-
,ok.
Please sign in to comment.
Something went wrong with that request. Please try again.