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})) + ].