Issue #7: Some Zones no longer observing DST are broen. The matchspec generation function for universal time was wrong, whereas it was fine for wall and local times. Fixing that line, and considering the `current` flatzone time, was enough to get TZs like Tokyo and Jamaica working correctly in tested cases.
335 lines
9.9 KiB
Erlang
335 lines
9.9 KiB
Erlang
-module(ezic_flatten).
|
|
-include("include/ezic.hrl").
|
|
|
|
|
|
-define(FLAT(W,S,U), #flatzone{wall_from=W, std_from=S, utc_from=U}).
|
|
-define(ENDFLAT(F,W,S,U,D), F#flatzone{wall_to=W, std_to=S, utc_to=U, dstoffset=D}).
|
|
-define(MINFLAT, ?FLAT(minimum,minimum,minimum)).
|
|
|
|
|
|
|
|
%% @todo move MAXYEAR to config file
|
|
-define(MAXYEAR, 2500). % last year to process flatzones for.
|
|
|
|
|
|
-export([
|
|
flatten/2
|
|
, contains_date/2
|
|
, ms/2
|
|
]).
|
|
|
|
|
|
flatten(Zones, AllRules) ->
|
|
flatten_all_zones(Zones, AllRules).
|
|
|
|
|
|
contains_date(FlatZone, Date) ->
|
|
NDate= ezic_date:normalize(Date),
|
|
contains_date2(FlatZone, NDate).
|
|
|
|
|
|
%% create matchspec for the given date and name
|
|
%% date is expected to have a tztime with accurate flag
|
|
ms(Date, Name) ->
|
|
|
|
M= #flatzone{tzname=Name
|
|
, wall_from='$1', wall_to='$2'
|
|
, std_from='$3', std_to='$4'
|
|
, utc_from='$5', utc_to='$6'
|
|
, _='_'},
|
|
R= ['$_'],
|
|
|
|
{D, #tztime{time=T, flag=F}}=Date,
|
|
DComp= {{ {D},{T} }},
|
|
G= ms_guards(F, DComp),
|
|
MS= [{M,G,R}],
|
|
|
|
|
|
|
|
%% @todo move to eunit
|
|
%% TestFZ= #flatzone{tzname=Name
|
|
%% , wall_from=SDate, wall_to=SDate
|
|
%% , std_from=SDate, std_to=SDate
|
|
%% , utc_from=SDate, utc_to=SDate
|
|
%% },
|
|
%% {ok, TestResult}= ets:test_ms(TestFZ, MS),
|
|
%% io:format("TestFZ: ~p~n", [TestFZ]),
|
|
%% io:format("MS: ~p~n", [MS]),
|
|
%% io:format("TestResult: ~p~n", [TestResult]),
|
|
|
|
MS.
|
|
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
% PRIVATE
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
|
|
flatten_all_zones(AllZones, AllRules) ->
|
|
flatten_all_zones(AllZones, AllRules, []).
|
|
|
|
% recursively processes sets of similar zones until they're all done,
|
|
% passing zone sets to flatten_zone_set/1
|
|
flatten_all_zones([], _AllRules, FlatZones) ->
|
|
FlatZones;
|
|
flatten_all_zones([Z1|_]= AllZones, AllRules, FlatZones) ->
|
|
io:format("Flattening zones: ~s~n", [Z1#zone.name]),
|
|
{CurrentZones, RestZones}= ezic_zone:split_by_name(Z1, AllZones),
|
|
Flats= flatten_zone_set(CurrentZones, AllRules),
|
|
flatten_all_zones(RestZones, AllRules, Flats ++ FlatZones).
|
|
|
|
|
|
|
|
contains_date2(#flatzone{utc_from=From, utc_to=To}, {Dt,#tztime{time=T, flag=F}})
|
|
when F=:= u; F=:= g; F=:=z ->
|
|
Date={Dt, T},
|
|
ezic_date:compare(From, Date) andalso ezic_date:compare(Date, To);
|
|
|
|
contains_date2(#flatzone{std_from=From, std_to=To}, {Dt,#tztime{time=T, flag=s}}) ->
|
|
Date={Dt, T},
|
|
ezic_date:compare(From, Date) andalso ezic_date:compare(Date, To);
|
|
|
|
contains_date2(#flatzone{wall_from=From, wall_to=To}, {Dt,#tztime{time=T, flag=F}})
|
|
when F=:=w; F=:=undefined ->
|
|
Date={Dt, T},
|
|
ezic_date:compare(From, Date) andalso ezic_date:compare(Date, To).
|
|
|
|
|
|
|
|
|
|
%% takes zones one-by-one from a list of zones of the same name. It
|
|
%% gathers relevant rules and creates flat periods of the same gmt
|
|
%% offset (#flatzone). This is a recursive solution, eliminating Zones
|
|
%% from the list until it's been exhausted
|
|
flatten_zone_set(Zones, AllRules) ->
|
|
flatten_zone_set(?MINFLAT, Zones, AllRules, [], none).
|
|
|
|
|
|
% flatten_zone_set(FromTime, Zones, AllRules, Flats) -> [#flatzone{}]
|
|
% FromTime= #flatzone{*_from =/= undefined}
|
|
% AllRules= [#rule{}]
|
|
% Zones= [#zone{}]
|
|
% Flats= [#flatzone{}]
|
|
%
|
|
% assumes a new zone every time it is called
|
|
flatten_zone_set(FromTimeStub=#flatzone{utc_from=UTCFrom, dstoffset=DSTOffset}
|
|
, Zones %[Z1=#zone{rule=RuleName, until=UntilTime, gmtoff=Offset} | _RestZones],
|
|
, AllRules
|
|
, Flats
|
|
, CurrentRule) ->
|
|
|
|
[Zone | RestZones] = ezic_zone:next(Zones, UTCFrom, DSTOffset),
|
|
#zone{rule=RuleName}= Zone,
|
|
|
|
%% ?debugVal(Zone),
|
|
|
|
%% we have a flatzone with start times;
|
|
%% must populate the base gmt offset and name for the current zone
|
|
|
|
FromTime= populate_flatzone(FromTimeStub, Zone),
|
|
|
|
%% 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. This
|
|
%% assumes they don't.
|
|
|
|
%% gather all rules that _may_ apply
|
|
|
|
Rules= [R || R <- AllRules, R#rule.name =:= RuleName ],
|
|
|
|
%% ?debugVal(FromTime),
|
|
%% ?debugVal(Rules),
|
|
|
|
%% apply all rules in order, creating flatzones, until this zone
|
|
%% ends, then regain control. rules have been exhausted, and zone
|
|
%% is ending. let's finish this.
|
|
|
|
{RuleFlats, NextFlat, EndingRule}= flatten_rule_set(FromTime, Zone, Rules, CurrentRule, []),
|
|
FinalFlats= lists:append([RuleFlats, Flats]),
|
|
|
|
%% ?debugVal(FinalFlats),
|
|
%% ?debugVal(NextFlat),
|
|
|
|
%% return flats if we've exceeded our years, or recurse if we can keep going
|
|
NFUTCFrom = NextFlat#flatzone.utc_from,
|
|
try maxyear_reached(NFUTCFrom) of
|
|
true ->
|
|
%% ?debugMsg("maxyear reached from flatten_zone_set"),
|
|
FinalFlats;
|
|
false ->
|
|
flatten_zone_set(NextFlat, RestZones, AllRules, FinalFlats, EndingRule)
|
|
catch
|
|
exit:Reason ->
|
|
%% ?debugMsg("bad year for nextflat:"),
|
|
%% ?debugVal(NextFlat),
|
|
erlang:error(Reason)
|
|
end.
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
{Year, D} -> [{D,Year, R} | Acc]
|
|
end
|
|
end
|
|
,[], ValidRules),
|
|
|
|
|
|
{UTCEndingRuleDate, EndingRuleYear, EndingRule}=
|
|
case length(RulesWithDates) > 0 of
|
|
false -> {maximum, maximum, none};
|
|
true -> hd(lists:sort(RulesWithDates))
|
|
end,
|
|
|
|
ZoneDate= ezic_zone:project_end_utc(Zone, DSTOffset),
|
|
|
|
|
|
%% ?debugVal(UTCEndingRuleDate),
|
|
%% ?debugVal(EndingRuleYear),
|
|
%% ?debugVal(EndingRule),
|
|
%% ?debugVal(ZoneDate),
|
|
|
|
|
|
try maxyear_reached(UTCEndingRuleDate) andalso maxyear_reached(ZoneDate) of
|
|
true ->
|
|
%% ?debugMsg("maxyear reached from flatten_rule_set"),
|
|
{EndFlat, NextFlat}= finish_and_start_flat(max_year, FlatStart, DSTOffset),
|
|
NewFlats= [EndFlat | Flats],
|
|
{NewFlats, NextFlat, none};
|
|
false ->
|
|
|
|
case ezic_date:compare(ZoneDate, UTCEndingRuleDate) of
|
|
false ->
|
|
%% same zone, new rule
|
|
{EndFlat, NextFlat}= finish_and_start_flat(FlatStart, EndingRule, EndingRuleYear),
|
|
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
|
|
catch
|
|
error:{bad_year,Val} ->
|
|
io:format("bad year for EndingRule: ~p~n", [EndingRule]),
|
|
erlang:error(bad_year, Val)
|
|
end.
|
|
|
|
|
|
|
|
|
|
%% rule is ending, while timezone remains
|
|
finish_and_start_flat(FlatStub=#flatzone{offset=Offset, dstoffset=OldDSTOffset}
|
|
, NewRule=#rule{save=NewDSTSave}
|
|
, 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, EndingRuleYear),
|
|
{WDm, SDm, UDm}= ezic_date:m1s({WD, SD, UD}),
|
|
|
|
EndFlat= ?ENDFLAT(FlatStub, WDm, SDm, UDm, OldDSTOffset),
|
|
NewFlat1= ?FLAT(WDn, SD, UD),
|
|
NewFlat2= NewFlat1#flatzone{offset=Offset, dstoffset=NewDSTSave, tzname=EndFlat#flatzone.tzname},
|
|
|
|
FinalNewFlat= NewFlat2,
|
|
|
|
%% ?debugVal(EndFlat),
|
|
%% ?debugVal(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
|
|
finish_and_start_flat(FlatStub=#flatzone{}, Zone=#zone{}, EndingDST) ->
|
|
EndDatesP1={_,_,UD}= ezic_zone:project_end(Zone, EndingDST),
|
|
{WDm, SDm, UDm}= ezic_date:m1s(EndDatesP1),
|
|
EndFlat= ?ENDFLAT(FlatStub, WDm, SDm, UDm, EndingDST),
|
|
|
|
RetNextFlat= #flatzone{dstoffset=EndingDST, utc_from=UD},
|
|
|
|
%% ?debugVal(EndFlat),
|
|
% ?debugVal(RetNextFlat),
|
|
|
|
{EndFlat, RetNextFlat};
|
|
|
|
finish_and_start_flat(max_year, FlatStub, DSTOffset) ->
|
|
EndFlat= ?ENDFLAT(FlatStub, current, current, current, DSTOffset),
|
|
Stop= {{?MAXYEAR+1,1,1},{0,0,0}},
|
|
NextFlat= #flatzone{utc_from=Stop},
|
|
{EndFlat, NextFlat}.
|
|
|
|
|
|
|
|
%% 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={_,_,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}
|
|
, #zone{name=Name, gmtoff=Offset}) ->
|
|
UTCFromTZ= ezic_date:normalize(UTCFrom, u),
|
|
{WT, ST, _}= 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).
|
|
|
|
|
|
|
|
|
|
|
|
ms_guards(X, D) when X=:=u;X=:=g;X=:=z ->
|
|
ms_guards2(D, '$5', '$6');
|
|
ms_guards(s, D) ->
|
|
ms_guards2(D, '$3', '$4');
|
|
ms_guards(X, D) when X=:=w;X=:=undefined ->
|
|
ms_guards2(D, '$1', '$2').
|
|
|
|
|
|
ms_guards2(D, From, To) ->
|
|
[
|
|
{'=<', From, D}
|
|
, {'or', {'=<', D, To}, {'=:=', current, To}}
|
|
].
|