Permalink
Browse files

repaired ezic_date:add_offset. able to normalize plain dates. notes a…

…nd 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).
  • Loading branch information...
1 parent 3e77287 commit a7b0adcbceda18fa70d54f248a9404ff2e213313 @drfloob committed Nov 12, 2010
Showing with 155 additions and 72 deletions.
  1. +56 −16 src/ezic_date.erl
  2. +47 −29 src/ezic_flatten.erl
  3. +13 −9 src/ezic_zone.erl
  4. +39 −18 test/ezic_date_tests.erl
View
@@ -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
View
@@ -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}.
View
@@ -94,21 +94,25 @@ 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),
{_,_,UTCDt}= ezic_date:all_times(NUntil, Offset, 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].
View
@@ -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}))
+ ].

0 comments on commit a7b0adc

Please sign in to comment.