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:
@@ -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
|
||||
|
||||
@@ -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}.
|
||||
|
||||
@@ -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].
|
||||
|
||||
@@ -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}))
|
||||
].
|
||||
|
||||
Reference in New Issue
Block a user