repaired ezic_date:add_offset. able to normalize plain dates. notes and tests

add_offset was calculating DST differences wrong. It assumed to lose an hour, DST would be {-1,0,0}, in fact, DST went from {1,0,0} to {0,0,0}. add_offset now uses their differences (hence requires both as arguments)
normalize/2 was added to be able to set an arbitrary flag on a plain (i.e. native erlang datetime tuple) date. This is complicated by the fact that atoms "minimum", "max", etc. are still valid and need to fall through the normalization unaltered (for now).
This commit is contained in:
aj
2010-11-12 03:07:15 -08:00
parent 3e77287db6
commit a7b0adcbce
4 changed files with 155 additions and 72 deletions

View File

@@ -6,11 +6,12 @@
-export([ -export([
normalize/1 normalize/1
, normalize/2
% rule-specific % rule-specific
, for_rule/2 , for_rule_relative/2
, for_rule/5
, for_rule_utc/4 , for_rule_utc/4
, for_rule_all/4
% converters % converters
@@ -21,6 +22,7 @@
% date math % date math
, add_seconds/2 , add_seconds/2
, add_offset/2 , add_offset/2
, add_offset/3
, all_times/3 , all_times/3
, m1s/1 , m1s/1
, m1s/3 , m1s/3
@@ -52,26 +54,60 @@ normalize(R={{_,_,_}, #tztime{}}) ->
R. R.
%% 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)
when is_atom(Flag), is_integer(HH) ->
DTz= {D, #tztime{time=T, flag=Flag}},
normalize(DTz);
normalize(D, Flag) ->
case normalize(D) of
X when is_atom(X) -> X;
X when is_record(X, tztime) ->
X#tztime{flag=Flag};
_ ->
erlang:error(badDateTime, D)
end.
%% returns RELATIVE datetime for a rule and Year %% returns RELATIVE datetime for a rule and Year
%% -> {{Y,M,D},#tztime{}} | {{Y,M,D},{HH,MM,SS}} %% -> {{Y,M,D},#tztime{}} | {{Y,M,D},{HH,MM,SS}}
for_rule(#rule{in=M, on=D, at=At}, Y) when is_integer(D) -> for_rule_relative(#rule{in=M, on=D, at=At}, Y) when is_integer(D) ->
{{Y,M,D}, At}; {{Y,M,D}, At};
for_rule(#rule{in=M, on={last, D}, at=At}, Y) -> for_rule_relative(#rule{in=M, on={last, D}, at=At}, Y) ->
{last_day_of(D, Y,M), At}; {last_day_of(D, Y,M), At};
for_rule(#rule{in=M, on=#tzon{day=Day, filter=Filter}, at=At}, Y) -> for_rule_relative(#rule{in=M, on=#tzon{day=Day, filter=Filter}, at=At}, Y) ->
{first_day_limited(Day, Filter, Y,M), At}. {first_day_limited(Day, Filter, Y,M), At}.
%% returns set of ALL datetimes for a rule, given the gmt offset and %% returns set of ALL datetimes for a rule, given the gmt offset and
%% current dst offset. %% current dst offset.
for_rule_all(Rule, Offset, DSTOffset, Year) -> for_rule(Rule, Offset, PrevDSTOffset, NextDSTOffset, Year) ->
DT= for_rule(Rule, Year), {WT,ST,UT}= for_rule_old_dst(Rule, Offset, PrevDSTOffset, Year),
all_times(DT, Offset, DSTOffset). WTNew= add_offset(WT, PrevDSTOffset, NextDSTOffset),
{{WT,WTNew}, ST, UT}.
for_rule_old_dst(Rule, Offset, PrevDSTOffset, Year) ->
DT= for_rule_relative(Rule, Year),
{WT,ST,UT}= all_times(DT, Offset, PrevDSTOffset).
% returns UTC datetime for rule, offset, dst offset, and year % returns UTC datetime for rule, offset, dst offset, and year
for_rule_utc(Rule, Offset, DSTOffset, Year) -> for_rule_utc(Rule, Offset, PrevDSTOffset, Year) ->
{_,_,UTCDatetime} = for_rule_all(Rule, Offset, DSTOffset, Year), {_,_,UTCDatetime} = for_rule_old_dst(Rule, Offset, PrevDSTOffset, Year),
UTCDatetime. UTCDatetime.
@@ -116,8 +152,11 @@ add_seconds(Datetime, Seconds) ->
add_offset(Datetime, Offset) -> add_offset(Datetime, Offset) ->
Sec= calendar:time_to_seconds(Offset), add_offset(Datetime, {0,0,0}, Offset).
add_seconds(Datetime, Sec). add_offset(Datetime, FromOffset, ToOffset) ->
FromSec= calendar:time_to_seconds(FromOffset),
ToSec= calendar:time_to_seconds(ToOffset),
add_seconds(Datetime, ToSec -FromSec).
@@ -174,19 +213,20 @@ all_times({Date, #tztime{time=WallTime, flag=Flag}}, Offset, DSTOffset)
compare(_, current) -> compare(_, current) ->
true; true;
compare(current, current) ->
true;
compare(current, _) -> compare(current, _) ->
false; false;
compare(_, X) when X=:=max; X=:=maximum -> compare(_, X) when X=:=max; X=:=maximum ->
true; true;
compare(X, X) when X=:=max; X=:=maximum ->
true;
compare(X, _) when X=:=max; X=:=maximum -> compare(X, _) when X=:=max; X=:=maximum ->
false; false;
compare(X, _) when X=:=min; X=:=minimum ->
true;
compare(_, X) when X=:=min; X=:=minimum ->
false;
% returns true if DT1 =< DT2. False otherwise. can be used with lists:sort/2 % returns true if DT1 =< DT2. False otherwise. can be used with lists:sort/2
% both times are assumed to be in the same zone/DST context % both times are assumed to be in the same zone/DST context

View File

@@ -67,36 +67,40 @@ flatten_zone_set(FromTimeStub=#flatzone{utc_from=UTCFrom, dstoffset=DSTOffset}
, Flats) -> , Flats) ->
[Zone | RestZones] = ezic_zone:next(Zones, UTCFrom, DSTOffset), [Zone | RestZones] = ezic_zone:next(Zones, UTCFrom, DSTOffset),
#zone{rule=RuleName, until=_UntilTime, gmtoff=Offset}=Zone, #zone{rule=RuleName, until=_UntilTime, gmtoff=Offset}= 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),
% FromTime= FromTimeStub#flatzone{offset=Offset, tzname=Zone#zone.name},
%% we have a flatzone with start times; must populate the base gmt offset
FromTime= FromTimeStub#flatzone{offset=Offset, tzname=Zone#zone.name},
%% if this is the first run, DST offset is {0,0,0} %% 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 %% 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
%% we gather all rules that _may_ apply (same year) %% we gather all rules that _may_ apply
Rules= ezic_db:rules(RuleName), Rules= ezic_db:rules(RuleName),
%% tack the date onto the zone, so we can see if the zone ends before a rule does
TempZoneEndUTC= ezic_zone:project_end_utc(Zone, DSTOffset),
ZoneWithDate= {TempZoneEndUTC, Zone},
?debugVal(FromTime), ?debugVal(FromTime),
?debugVal(ZoneWithDate),
?debugVal(Rules), ?debugVal(Rules),
{RuleFlats, LastFlat, EndingDST}= flatten_rule_set(FromTime, ZoneWithDate, Rules, []), %% apply all rules in order, creating flatzones, until this zone
%% ends, then regain control.
{RuleFlats, LastFlat, EndingDST}= flatten_rule_set(FromTime, Zone, Rules, []),
?debugVal(RuleFlats), ?debugVal(RuleFlats),
?debugVal(LastFlat),
?debugVal(EndingDST), ?debugVal(EndingDST),
%% rules have been exhausted, and zone is ending. %% rules have been exhausted, and zone is ending. let's finish this.
{FinalFlat, NextFlat}= finish_and_start_flat(LastFlat, Zone, EndingDST), {FinalFlat, NextFlat}= finish_and_start_flat(LastFlat, Zone, EndingDST),
?debugVal(FinalFlat),
FinalFlats= lists:merge([[FinalFlat], RuleFlats, Flats]), FinalFlats= lists:merge([[FinalFlat], RuleFlats, Flats]),
?debugVal(FinalFlat),
?debugVal(FinalFlats), ?debugVal(FinalFlats),
@@ -107,7 +111,7 @@ flatten_zone_set(FromTimeStub=#flatzone{utc_from=UTCFrom, dstoffset=DSTOffset}
flatten_rule_set(FlatStart=#flatzone{utc_from=UTCFrom, dstoffset=DSTOffset, offset=Offset} flatten_rule_set(FlatStart=#flatzone{utc_from=UTCFrom, dstoffset=DSTOffset, offset=Offset}
, ZoneWithDate, Rules, Flats) -> , Zone, Rules, Flats) ->
%% add normalized (possibly inaccurate) dates for sorting %% add normalized (possibly inaccurate) dates for sorting
%% purposes. note this may be empty. also note this MUST (I think) %% purposes. note this may be empty. also note this MUST (I think)
@@ -125,23 +129,26 @@ flatten_rule_set(FlatStart=#flatzone{utc_from=UTCFrom, dstoffset=DSTOffset, offs
?debugVal(RulesWithDates), ?debugVal(RulesWithDates),
{EndingRuleDate, EndingRule}=
{ActualEndingRuleDate, EndingRule}=
case length(RulesWithDates) > 0 of case length(RulesWithDates) > 0 of
false -> {maximum, none}; false -> {maximum, none};
true -> hd(lists:sort(RulesWithDates)) true -> hd(lists:sort(RulesWithDates))
end, end,
{ZoneDate, _Zone}= ZoneWithDate, ZoneDate= ezic_zone:project_end_utc(Zone, DSTOffset),
?debugVal(EndingRuleDate),
?debugVal(ActualEndingRuleDate),
?debugVal(ZoneDate), ?debugVal(ZoneDate),
case ezic_date:compare(EndingRuleDate, ZoneDate) of
case ezic_date:compare(ActualEndingRuleDate, ZoneDate) of
true -> true ->
%% same zone, new rule %% same zone, new rule
{EndFlat, NextFlat}= finish_and_start_flat(FlatStart, EndingRule, EndingRuleDate, Offset, DSTOffset), {EndFlat, NextFlat}= finish_and_start_flat(FlatStart, EndingRule, ActualEndingRuleDate, Offset, DSTOffset),
NewFlats= [EndFlat | Flats], NewFlats= [EndFlat | Flats],
flatten_rule_set(NextFlat, ZoneWithDate, Rules, NewFlats); flatten_rule_set(NextFlat, Zone, Rules, NewFlats);
false -> false ->
%% new zone is handled in the caller: flatten_zone_set %% new zone is handled in the caller: flatten_zone_set
{Flats, FlatStart, DSTOffset} {Flats, FlatStart, DSTOffset}
@@ -151,13 +158,13 @@ flatten_rule_set(FlatStart=#flatzone{utc_from=UTCFrom, dstoffset=DSTOffset, offs
finish_and_start_flat(FlatStub=#flatzone{utc_from=_UTCFrom}, NewRule=#rule{save=NewDSTSave}, _EndingRuleDate={{ERDY,_,_},_}, Offset, DSTOffset) -> finish_and_start_flat(FlatStub=#flatzone{}, NewRule=#rule{save=NewDSTSave}, _EndingRuleDate={{ERDY,_,_},_}, Offset, OldDSTOffset) ->
%% @todo for_rule_all was already called in a loop earlier. use those values instead. %% @todo for_rule_all was already called in a loop earlier. use those values instead.
NewFlatStartDates={WD, SD, UD}= ezic_date:for_rule_all(NewRule, Offset, DSTOffset, ERDY), {{WD, WDn}, SD, UD}= ezic_date:for_rule(NewRule, Offset, OldDSTOffset, NewDSTSave, ERDY),
{WDm, SDm, UDm}= ezic_date:m1s(NewFlatStartDates), {WDm, SDm, UDm}= ezic_date:m1s({WD, SD, UD}),
EndFlat= ?ENDFLAT(FlatStub, WDm, SDm, UDm, DSTOffset), EndFlat= ?ENDFLAT(FlatStub, WDm, SDm, UDm, OldDSTOffset),
NewFlat1= ?FLAT(WD, SD, UD), NewFlat1= ?FLAT(WDn, SD, UD),
NewFlat2= NewFlat1#flatzone{offset=Offset, dstoffset=NewDSTSave, tzname=EndFlat#flatzone.tzname}, NewFlat2= NewFlat1#flatzone{offset=Offset, dstoffset=NewDSTSave, tzname=EndFlat#flatzone.tzname},
FinalNewFlat= NewFlat2, FinalNewFlat= NewFlat2,
@@ -168,16 +175,27 @@ finish_and_start_flat(FlatStub=#flatzone{utc_from=_UTCFrom}, NewRule=#rule{save=
{EndFlat, FinalNewFlat}. {EndFlat, FinalNewFlat}.
%% 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) -> finish_and_start_flat(FlatStub=#flatzone{}, Zone=#zone{}, EndingDST) ->
EndDatesP1={WD,SD,UD}= ezic_zone:project_end(Zone, EndingDST), EndDatesP1={WD,SD,UD}= ezic_zone:project_end(Zone, EndingDST),
{WDm, SDm, UDm}= ezic_date:m1s(EndDatesP1), {WDm, SDm, UDm}= ezic_date:m1s(EndDatesP1),
EndFlat= ?ENDFLAT(FlatStub, WDm, SDm, UDm, EndingDST), EndFlat= ?ENDFLAT(FlatStub, WDm, SDm, UDm, EndingDST),
NextFlat1= ?FLAT(WD,SD,UD), RetNextFlat= #flatzone{dstoffset=EndingDST, utc_from=UD},
NextFlat2= NextFlat1#flatzone{dstoffset=EndingDST},
RetNextFlat= NextFlat2,
?debugVal(EndFlat), ?debugVal(EndFlat),
?debugVal(RetNextFlat), ?debugVal(RetNextFlat),
{EndFlat, RetNextFlat}. {EndFlat, RetNextFlat}.
populate_flatzone(
FZ=#flatzone{utc_from=UTCFrom, dstoffset=DSTOffset}
, Zone=#zone{name=Name, gmtoff=Offset}) ->
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}.

View File

@@ -94,14 +94,16 @@ project_end_utc(Zone=#zone{}, DSTOffset) ->
% returns {Zone, Rest} where Zone is the next zone after UTCFrom, %% returns [Zone | Rest] where Zone is the next zone after UTCFrom,
% subject to the DST offset. %% subject to the DST offset.
% Note that dst differences *can* change which zone comes next, %% Note that dst differences *can* change which zone comes next,
% though it's very unlikely (and does not exist in the current tz database files). %% though it's very unlikely (and does not exist in the current tz database files).
% this method covers that event, anyhow. %% this method covers that event, anyhow. see unit tests for examples.
%% BAD!!! UCTFrom is not used!
next(ZoneList, UTCFrom, DSTOff) -> next(ZoneList, UTCFrom, DSTOff) ->
?debugMsg("next:"),
?debugVal(ZoneList),
?debugVal(UTCFrom),
DatedList= lists:map( DatedList= lists:map(
fun(Z=#zone{until=Until, gmtoff=Offset})-> fun(Z=#zone{until=Until, gmtoff=Offset})->
NUntil= ezic_date:normalize(Until), NUntil= ezic_date:normalize(Until),
@@ -109,6 +111,8 @@ next(ZoneList, UTCFrom, DSTOff) ->
{UTCDt, Z} {UTCDt, Z}
end end
, ZoneList), , ZoneList),
FilteredList= lists:filter(fun({IDt,_})-> UTCFrom =< IDt end, DatedList), FilteredList= lists:filter(fun({IDt,_})-> ezic_date:compare(UTCFrom, IDt) end, DatedList),
SortedList= lists:sort(FilteredList),
%x ?debugVal(FilteredList),
SortedList= lists:sort(fun({X,_},{Y,_})->ezic_date:compare(X,Y)end, FilteredList),
[Z || {_,Z}<- SortedList]. [Z || {_,Z}<- SortedList].

View File

@@ -3,24 +3,6 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
for_rule_test_() ->
BR= #rule{at=#tztime{}},
[
% last days of months
?_assertEqual({{2010, 03, 28}, #tztime{}}, ezic_date:for_rule(BR#rule{in=3, on={last, "Sun"}}, 2010))
, ?_assertEqual({{2010, 11, 30}, #tztime{}}, ezic_date:for_rule(BR#rule{in=11, on={last, "Tue"}}, 2010))
, ?_assertEqual({{2010, 11, 24}, #tztime{}}, ezic_date:for_rule(BR#rule{in=11, on={last, "Wed"}}, 2010))
% days =< or >= absolute dates in given month
, ?_assertEqual({{2010, 11, 14}, #tztime{}}, ezic_date:for_rule(BR#rule{in=11, on=#tzon{day="Sun", filter={geq, 9}}}, 2010))
, ?_assertEqual({{2010, 11, 7}, #tztime{}}, ezic_date:for_rule(BR#rule{in=11, on=#tzon{day="Sun", filter={leq, 9}}}, 2010))
% these should fail
, ?_assertError(no_previous_day, ezic_date:for_rule(BR#rule{in=11, on=#tzon{day="Sun", filter={leq, 1}}}, 2010))
, ?_assertError(no_next_day, ezic_date:for_rule(BR#rule{in=11, on=#tzon{day="Sun", filter={geq, 29}}}, 2010))
].
for_rule_utc_dst_test_() -> for_rule_utc_dst_test_() ->
WRule= #rule{in=11, on=11, at=#tztime{time={0,0,0}}, save={0,0,0}}, WRule= #rule{in=11, on=11, at=#tztime{time={0,0,0}}, save={0,0,0}},
SRule= #rule{in=11, on=11, at=#tztime{time={0,0,0}, flag=s}, save={0,0,0}}, SRule= #rule{in=11, on=11, at=#tztime{time={0,0,0}, flag=s}, save={0,0,0}},
@@ -47,6 +29,31 @@ for_rule_utc_dst_test_() ->
]. ].
for_rule_test_() ->
IrkutskRule1= #rule{from=1981, to=1984, in=4, on=1, at=#tztime{}, save={1,0,0}},
IrkutskRule2= #rule{from=1981, to=1984, in=10, on=1, at=#tztime{}, save={0,0,0}},
[
%% Asia/Irkutsk
?_assertEqual({
{ {{1981,4,1},{0,0,0}}, {{1981,4,1},{1,0,0}} }, % {oldDst, newDst}
{{1981,4,1},{0,0,0}},
{{1981,3,31},{16,0,0}}
},
ezic_date:for_rule(IrkutskRule1, {8,0,0}, {0,0,0}, {1,0,0}, 1981))
%% Another Asia/Irkutsk. ezic_date:add_offset repaired.
, ?_assertEqual({
{ {{1981,10,1},{0,0,0}}, {{1981,9,30},{23,0,0}} }, % {oldDst, newDst}
{{1981,9,30},{23,0,0}},
{{1981,9,30},{15,0,0}}
},
ezic_date:for_rule(IrkutskRule2, {8,0,0}, {1,0,0}, {0,0,0}, 1981))
].
add_seconds_test_() -> add_seconds_test_() ->
[ [
?_assertEqual({{2010,11,7},{9,38,01}}, ezic_date:add_seconds({{2010,11,7},{9,38,00}}, 1)) ?_assertEqual({{2010,11,7},{9,38,01}}, ezic_date:add_seconds({{2010,11,7},{9,38,00}}, 1))
@@ -65,3 +72,17 @@ compare_test_() ->
?_assert(ezic_date:compare({2011,12,12}, current)) ?_assert(ezic_date:compare({2011,12,12}, current))
, ?_assertNot(ezic_date:compare(current, {2099,12,12})) % is this the right behavior? , ?_assertNot(ezic_date:compare(current, {2099,12,12})) % is this the right behavior?
]. ].
all_times_test_() ->
Date={2010,11,12},
UTC=#tztime{time={16,2,0}, flag=u},
[
?_assertEqual(
{{Date,{9,2,0}}, {Date,{8,2,0}}, {Date,{16,2,0}}}
, ezic_date:all_times({Date,UTC}, {-8,0,0}, {1,0,0}))
].