From a7b0adcbceda18fa70d54f248a9404ff2e213313 Mon Sep 17 00:00:00 2001 From: aj Date: Fri, 12 Nov 2010 03:07:15 -0800 Subject: [PATCH] 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). --- src/ezic_date.erl | 72 ++++++++++++++++++++++++++++--------- src/ezic_flatten.erl | 76 +++++++++++++++++++++++++--------------- src/ezic_zone.erl | 22 +++++++----- test/ezic_date_tests.erl | 57 ++++++++++++++++++++---------- 4 files changed, 155 insertions(+), 72 deletions(-) diff --git a/src/ezic_date.erl b/src/ezic_date.erl index d269561..c56b8c1 100644 --- a/src/ezic_date.erl +++ b/src/ezic_date.erl @@ -6,11 +6,12 @@ -export([ normalize/1 + , normalize/2 % rule-specific - , for_rule/2 + , for_rule_relative/2 + , for_rule/5 , for_rule_utc/4 - , for_rule_all/4 % converters @@ -21,6 +22,7 @@ % date math , add_seconds/2 , add_offset/2 + , add_offset/3 , all_times/3 , m1s/1 , m1s/3 @@ -52,26 +54,60 @@ normalize(R={{_,_,_}, #tztime{}}) -> 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 %% -> {{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}; -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}; -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}. + + + + %% returns set of ALL datetimes for a rule, given the gmt offset and %% current dst offset. -for_rule_all(Rule, Offset, DSTOffset, Year) -> - DT= for_rule(Rule, Year), - all_times(DT, Offset, DSTOffset). +for_rule(Rule, Offset, PrevDSTOffset, NextDSTOffset, Year) -> + {WT,ST,UT}= for_rule_old_dst(Rule, Offset, PrevDSTOffset, Year), + 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 -for_rule_utc(Rule, Offset, DSTOffset, Year) -> - {_,_,UTCDatetime} = for_rule_all(Rule, Offset, DSTOffset, Year), +for_rule_utc(Rule, Offset, PrevDSTOffset, Year) -> + {_,_,UTCDatetime} = for_rule_old_dst(Rule, Offset, PrevDSTOffset, Year), UTCDatetime. @@ -116,8 +152,11 @@ add_seconds(Datetime, Seconds) -> add_offset(Datetime, Offset) -> - Sec= calendar:time_to_seconds(Offset), - add_seconds(Datetime, Sec). + add_offset(Datetime, {0,0,0}, Offset). +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) -> true; -compare(current, current) -> - true; compare(current, _) -> false; compare(_, X) when X=:=max; X=:=maximum -> true; -compare(X, X) when X=:=max; X=:=maximum -> - true; compare(X, _) when X=:=max; X=:=maximum -> 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 % both times are assumed to be in the same zone/DST context diff --git a/src/ezic_flatten.erl b/src/ezic_flatten.erl index 4e4b9d0..e952123 100644 --- a/src/ezic_flatten.erl +++ b/src/ezic_flatten.erl @@ -67,36 +67,40 @@ flatten_zone_set(FromTimeStub=#flatzone{utc_from=UTCFrom, dstoffset=DSTOffset} , Flats) -> [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 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), - %% 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(ZoneWithDate), ?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(LastFlat), ?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), - ?debugVal(FinalFlat), FinalFlats= lists:merge([[FinalFlat], RuleFlats, Flats]), + + ?debugVal(FinalFlat), ?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} - , ZoneWithDate, Rules, Flats) -> + , Zone, Rules, Flats) -> %% add normalized (possibly inaccurate) dates for sorting %% 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), - {EndingRuleDate, EndingRule}= + + {ActualEndingRuleDate, EndingRule}= case length(RulesWithDates) > 0 of false -> {maximum, none}; true -> hd(lists:sort(RulesWithDates)) end, + + ZoneDate= ezic_zone:project_end_utc(Zone, DSTOffset), - {ZoneDate, _Zone}= ZoneWithDate, - ?debugVal(EndingRuleDate), + ?debugVal(ActualEndingRuleDate), ?debugVal(ZoneDate), - case ezic_date:compare(EndingRuleDate, ZoneDate) of + + case ezic_date:compare(ActualEndingRuleDate, ZoneDate) of true -> %% 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], - flatten_rule_set(NextFlat, ZoneWithDate, Rules, NewFlats); + flatten_rule_set(NextFlat, Zone, Rules, NewFlats); false -> %% new zone is handled in the caller: flatten_zone_set {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. - NewFlatStartDates={WD, SD, UD}= ezic_date:for_rule_all(NewRule, Offset, DSTOffset, ERDY), - {WDm, SDm, UDm}= ezic_date:m1s(NewFlatStartDates), + {{WD, WDn}, SD, UD}= ezic_date:for_rule(NewRule, Offset, OldDSTOffset, NewDSTSave, ERDY), + {WDm, SDm, UDm}= ezic_date:m1s({WD, SD, UD}), - EndFlat= ?ENDFLAT(FlatStub, WDm, SDm, UDm, DSTOffset), - NewFlat1= ?FLAT(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, @@ -168,16 +175,27 @@ finish_and_start_flat(FlatStub=#flatzone{utc_from=_UTCFrom}, NewRule=#rule{save= {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) -> EndDatesP1={WD,SD,UD}= ezic_zone:project_end(Zone, EndingDST), {WDm, SDm, UDm}= ezic_date:m1s(EndDatesP1), EndFlat= ?ENDFLAT(FlatStub, WDm, SDm, UDm, EndingDST), - NextFlat1= ?FLAT(WD,SD,UD), - NextFlat2= NextFlat1#flatzone{dstoffset=EndingDST}, - RetNextFlat= NextFlat2, + RetNextFlat= #flatzone{dstoffset=EndingDST, utc_from=UD}, ?debugVal(EndFlat), ?debugVal(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}. diff --git a/src/ezic_zone.erl b/src/ezic_zone.erl index ef662dd..30e8978 100644 --- a/src/ezic_zone.erl +++ b/src/ezic_zone.erl @@ -94,14 +94,16 @@ project_end_utc(Zone=#zone{}, DSTOffset) -> -% returns {Zone, Rest} where Zone is the next zone after UTCFrom, -% subject to the DST offset. -% 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). -% this method covers that event, anyhow. - -%% BAD!!! UCTFrom is not used! +%% returns [Zone | Rest] where Zone is the next zone after UTCFrom, +%% subject to the DST offset. +%% 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). +%% this method covers that event, anyhow. see unit tests for examples. next(ZoneList, UTCFrom, DSTOff) -> + ?debugMsg("next:"), + ?debugVal(ZoneList), + ?debugVal(UTCFrom), + DatedList= lists:map( fun(Z=#zone{until=Until, gmtoff=Offset})-> NUntil= ezic_date:normalize(Until), @@ -109,6 +111,8 @@ next(ZoneList, UTCFrom, DSTOff) -> {UTCDt, Z} end , ZoneList), - FilteredList= lists:filter(fun({IDt,_})-> UTCFrom =< IDt end, DatedList), - SortedList= lists:sort(FilteredList), + FilteredList= lists:filter(fun({IDt,_})-> ezic_date:compare(UTCFrom, IDt) end, DatedList), + +%x ?debugVal(FilteredList), + SortedList= lists:sort(fun({X,_},{Y,_})->ezic_date:compare(X,Y)end, FilteredList), [Z || {_,Z}<- SortedList]. diff --git a/test/ezic_date_tests.erl b/test/ezic_date_tests.erl index 4972851..2d19a2a 100644 --- a/test/ezic_date_tests.erl +++ b/test/ezic_date_tests.erl @@ -3,24 +3,6 @@ -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_() -> 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}}, @@ -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_() -> [ ?_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)) , ?_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})) + ].