Skip to content
This repository has been archived by the owner on Dec 15, 2023. It is now read-only.

Commit

Permalink
repaired ezic_date:add_offset. able to normalize plain dates. notes a…
Browse files Browse the repository at this point in the history
…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
drfloob committed Nov 12, 2010
1 parent 3e77287 commit a7b0adc
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 72 deletions.
72 changes: 56 additions & 16 deletions src/ezic_date.erl
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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.




Expand Down Expand Up @@ -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).






Expand Down Expand Up @@ -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
Expand Down
76 changes: 47 additions & 29 deletions src/ezic_flatten.erl
Expand Up @@ -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),




Expand All @@ -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)
Expand All @@ -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= ezic_zone:project_end_utc(Zone, DSTOffset),


{ZoneDate, _Zone}= ZoneWithDate,


?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}
Expand All @@ -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,
Expand All @@ -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}.
22 changes: 13 additions & 9 deletions src/ezic_zone.erl
Expand Up @@ -94,21 +94,25 @@ 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),
{_,_,UTCDt}= ezic_date:all_times(NUntil, Offset, DSTOff), {_,_,UTCDt}= ezic_date:all_times(NUntil, Offset, 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].
57 changes: 39 additions & 18 deletions test/ezic_date_tests.erl
Expand Up @@ -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}},
Expand All @@ -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))
Expand All @@ -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}))
].

0 comments on commit a7b0adc

Please sign in to comment.