datetime manipulation is now much clearer. tests were added, and expectations stated explicitly in many places. rules got lots of attention.

This commit is contained in:
aj
2010-11-10 01:52:13 -08:00
parent 216a8d9ff8
commit d84a0c1822
11 changed files with 245 additions and 116 deletions

View File

@@ -7,7 +7,7 @@
-export([
% rule-specific
for_rule/2
, for_rule_zone/3
, for_rule_utc/4
% converters
@@ -17,7 +17,12 @@
% date math
, add_seconds/2
, add_offset/2
, all_times/3
% comparisons
, compare/2
, equal/2
]).
@@ -27,26 +32,20 @@
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% returns {Y,M,D} for a rule and Year
for_rule(#rule{in=M, on={last, D}}, Y) ->
last_day_of(D, Y,M);
for_rule(#rule{in=M, on=#tzon{day=Day, filter=Filter}}, Y) ->
first_day_limited(Day, Filter, Y,M).
% returns RELATIVE {Y,M,D} for a rule and Year
for_rule(#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) ->
{last_day_of(D, Y,M), At};
for_rule(#rule{in=M, on=#tzon{day=Day, filter=Filter}, at=At}, Y) ->
{first_day_limited(Day, Filter, Y,M), At}.
% for_rule(#rule{}, Zone, Year) -> {WallTime, STDTime, UTCTime}
% returns the date set for a given rule and year
% same for all timezones.
for_rule_zone(#rule{in=M, on={last, D}, at=_Time, save=_DSTOffset}, _Zone=#zone{gmtoff=_Offset}, Y) ->
_LDO= last_day_of(D, Y,M),
not_done;
for_rule_zone(#rule{in=M, on=#tzon{day=Day, filter=Filter}}, _Zone, Y) ->
first_day_limited(Day, Filter, Y,M),
not_done.
% returns UTC datetime for rule, offset, and year
for_rule_utc(Rule, Offset, DSTOffset, Year) ->
DT= for_rule(Rule, Year),
{_,_,UTCDatetime} = all_times(DT, Offset, DSTOffset),
UTCDatetime.
@@ -89,47 +88,83 @@ add_seconds(Datetime, Seconds) ->
add_offset(Datetime, Offset) ->
Sec= calendar:time_to_seconds(Offset),
add_seconds(Datetime, Sec).
% returns {WallTime, StdTime, UtcTime}
% where each is a datetime tuple: {{Y,M,D}{HH,MM,SS}}
% universal time given
all_times(#tztime{time=UTCTime, flag=Flag}, Offset, DSTOffset)
all_times({Date, #tztime{time=UTCTime, flag=Flag}}, Offset, DSTOffset)
when Flag=:=u; Flag=:=g; Flag=:=z ->
UTCDatetime= {Date, UTCTime},
OSec= calendar:time_to_seconds(Offset),
DSTSec= calendar:time_to_seconds(DSTOffset),
STDTime= add_seconds(UTCTime, OSec),
STDTime= add_seconds(UTCDatetime, OSec),
WallTime= add_seconds(STDTime, DSTSec),
{WallTime, STDTime, UTCTime};
{WallTime, STDTime, UTCDatetime};
% standard time given
all_times(#tztime{time=STDTime, flag=s}, Offset, DSTOffset) ->
all_times({Date, #tztime{time=STDTime, flag=s}}, Offset, DSTOffset) ->
STDDatetime= {Date, STDTime},
OSec= calendar:time_to_seconds(Offset),
DSTSec= calendar:time_to_seconds(DSTOffset),
UTCTime= add_seconds(STDTime, -1*OSec),
WallTime= add_seconds(STDTime, DSTSec),
UTCTime= add_seconds(STDDatetime, -1*OSec),
WallTime= add_seconds(STDDatetime, DSTSec),
{WallTime, STDTime, UTCTime};
{WallTime, STDDatetime, UTCTime};
% wall time given
all_times(#tztime{time=WallTime, flag=Flag}, Offset, DSTOffset)
when Flag=:=w, Flag=:=undefined ->
all_times({Date, #tztime{time=WallTime, flag=Flag}}, Offset, DSTOffset)
when Flag=:=w; Flag=:=undefined ->
WallDatetime= {Date, WallTime},
OSec= calendar:time_to_seconds(Offset),
DSTSec= calendar:time_to_seconds(DSTOffset),
STDTime= add_seconds(WallTime, -1*DSTSec),
STDTime= add_seconds(WallDatetime, -1*DSTSec),
UTCTime= add_seconds(STDTime, -1*OSec),
{WallTime, STDTime, UTCTime}.
{WallDatetime, STDTime, UTCTime}.
% 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
compare(DT1={{Y1,M1,D1},{HH1,MM1,SS1}}, DT2={{Y2,M2,D2},{HH2,MM2,SS2}})
when is_integer(Y1), is_integer(Y2)
, is_integer(M1), is_integer(M2)
, is_integer(D1), is_integer(D2)
, is_integer(HH1), is_integer(HH2)
, is_integer(MM1), is_integer(MM2)
, is_integer(SS1), is_integer(SS2)
->
DT1 =< DT2.
% returns true if DT1 =:= DT2. False otherwise
% both times are assumed to be in the same zone/DST context
equal(DT1={{Y1,M1,D1},{HH1,MM1,SS1}}, DT2={{Y2,M2,D2},{HH2,MM2,SS2}})
when is_integer(Y1), is_integer(Y2)
, is_integer(M1), is_integer(M2)
, is_integer(D1), is_integer(D2)
, is_integer(HH1), is_integer(HH2)
, is_integer(MM1), is_integer(MM2)
, is_integer(SS1), is_integer(SS2)
->
DT1 =:= DT2.
@@ -211,10 +246,3 @@ add_days_in_month(Days, Date={Y,M,D}) ->
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
normalize(_Datetime) ->
not_done.
compare_normal(_DT1, _DT2) ->
not_done.

View File

@@ -88,55 +88,28 @@ flatten_zone_set(Zones) ->
% assumes a new zone every time it is called
flatten_zone_set(_, [], Flats) ->
Flats;
flatten_zone_set(FromTimeStub, [Z1=#zone{rule=RuleName, until=UntilTime, gmtoff=Offset} | _RestZones] = _Zones, _Flats) ->
flatten_zone_set(FromTimeStub=#flatzone{utc_from=UTCFrom, dstoffset=DSTOffset}, [Z1=#zone{rule=RuleName, until=UntilTime, gmtoff=Offset} | _RestZones], _Flats) ->
%% we have a flatzone with start times, we populate the base offset
FromTime= FromTimeStub#flatzone{offset=Offset},
%% note that dst offset default to {0,0,0} in #flatzone{}
%% we gather all rules that _may_ apply (same year)
Rules= ezic_db:rules(RuleName),
RelevantRules= ezic_rule:filter(FromTime, UntilTime, Rules),
SortedRules= lists:sort(fun ezic_rule:sort_ascending/2, RelevantRules),
%% Then we plot points, bouncing from rule to rule, ending when the zone ends
{ok, _NextFromTimeStub, _NewFlats}= flatten_zone(FromTime, Z1, SortedRules),
RuleDates= lists:foldl(
fun(R, Acc)->
case ezic_rule:project_next(R, Offset, DSTOffset, UTCFrom) of
none -> Acc;
D -> [{D,R} | Acc]
end
end
,[], Rules),
AllDates= [{ezic_zone:project_end(Z1, DSTOffset), Z1} | RuleDates],
not_done.
% flattens a single zone into multiple flatzones.
% FromTime has its *_from fields and offset field populated. (it should anyway)
% @todo invalid input checking
% Rules are sorted
flatten_zone(BaseFlat, Zone, Rules) ->
flatten_zone(BaseFlat, Zone, Rules, []).
flatten_zone(BaseFlat, Zone=#zone{until=Until, gmtoff=Offset}, Rules, Flats) ->
case ezic_rule:next_event(BaseFlat, Until, Offset, Rules) of
% is this enough information to compare all times corectly?
{end_until, EndTimes} ->
NewFlats= [end_flat(BaseFlat, EndTimes) | Flats],
NewFrom= make_next_flat(EndTimes),
{ok, NewFrom, NewFlats}; % base case
{end_rule, EndTimes, NextOffset} ->
NewFlats= [end_flat(BaseFlat, EndTimes) | Flats],
NewFrom= make_next_flat(EndTimes, NextOffset),
flatten_zone(NewFrom, Zone, Rules, NewFlats) % tail recursion
end.

View File

@@ -145,7 +145,7 @@ time_flag_rev([F|RTime]) when F =:= $g; F =:= $z ->
time_flag_rev(X=[F|_]) when F-48 < 0; F-48 > 9 ->
erlang:error(badtime, lists:reverse(X));
time_flag_rev(RTime) ->
{undefined, RTime}.
{w, RTime}.

View File

@@ -3,11 +3,6 @@
-include_lib("eunit/include/eunit.hrl").
-ifdef(TEST).
-compile([export_all]).
-endif.
-export([
link/1
, leap/1

View File

@@ -3,8 +3,7 @@
-export([parse/1]).
-export([next_event/4]).
-export([project_next/4]).
parse([Name,FromS,ToS,_Type,InS,OnS,AtS,SaveS,Letters]) ->
@@ -25,37 +24,38 @@ parse([Name,FromS,ToS,_Type,InS,OnS,AtS,SaveS,Letters]) ->
% determines the next zone or dst change event
% returns the latest possible times the previous period is in effect (`Until -1sec` in most cases)
% Rules are sorted
next_event(_BaseFlat, ZoneEnd, Offset, []) ->
{end_until, make_times(ZoneEnd, Offset)};
% BaseFlat has offset defined
% @todo ensure offset defined on BaseFlat
% Rules are sorted in ascending order (oldest first)
next_event(_BaseFlat=#flatzone{utc_from=#tztime{time={{_Year,_,_},_}}},
_ZoneEnd, _ZoneOffset, [_Rule|_RestRules]) ->
not_done.
% returns the projected date of the next "rule event" in UTC time,
% after the given UTCDatetime, with the given the zone Offset and current DST offset.
project_next(Rule=#rule{from=RFrom, to=RTo}, Offset, DSTOff, UTCAfter={{AY,_,_},_}) ->
AllYears= years(Rule),
GoodYears= years_after(AY, AllYears),
project_next2(Rule, Offset, UTCAfter, DSTOff, GoodYears).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% PRIVATE
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% create endtimes for a #flatzone, with no dst offset
make_times(Until, Offset) ->
make_times(Until, Offset, 0).
make_times(Until, Offset, DSTOffset) ->
ezic_date:all_times(Until, Offset, DSTOffset).
% returns the sorted list of years a rule existed for.
years(Rule=#rule{from=From, to=only}) ->
[From];
years(Rule=#rule{from=From, to=To}) ->
try lists:seq(From, To)
catch error:function_clause ->
erlang:error(bad_year_range)
end.
% returns the years from the list that are greater than OR equal to Y
years_after(Y, []) ->
[];
years_after(Y, Years) ->
{GOOD, _}= lists:partition(
fun(X) when Y =< X -> true;
(_) -> false
end, Years),
GOOD.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% INTERNAL
@@ -63,4 +63,45 @@ make_times(Until, Offset, DSTOffset) ->
project_next2(_,_,_,_,[]) ->
none;
project_next2(Rule, Offset, UTCAfter, DSTOffset, [Year|RestYears]) ->
RuleDate= ezic_date:for_rule_utc(Rule, Offset, DSTOffset, Year),
case ezic_date:compare(UTCAfter, RuleDate) andalso (not ezic_date:equal(UTCAfter, RuleDate)) of
true -> RuleDate;
_ -> project_next2(Rule, Offset, UTCAfter, DSTOffset, RestYears)
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Tests of Private Methods
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
years_test_() ->
[
?_assertEqual([1952], years(#rule{from=1952,to=only}))
, ?_assertEqual([1952,1953], years(#rule{from=1952,to=1953}))
, ?_assertEqual([1952,1953,1954,1955], years(#rule{from=1952,to=1955}))
, ?_assertError(bad_year_range, years(#rule{from=1955,to=1952}))
].
% @todo type checking. lack of types worries me sometimes :(
years_after_test_() ->
[
?_assertEqual([2010], years_after(2010, [2010]))
, ?_assertEqual([2010,2011], years_after(2010, [2010,2011]))
, ?_assertEqual([2011], years_after(2011, [2010,2011]))
, ?_assertEqual([], years_after(2012, [2010,2011]))
, ?_assertEqual([], years_after(2012, []))
].
-endif.

View File

@@ -10,6 +10,8 @@
, sort_ascending/2
, sort_descending/2
, offset_sec/1
, project_end/2
]).
@@ -94,6 +96,11 @@ offset_sec(Zone) ->
% returns the UTC datetime projected for zone end (given current DST Offset)
project_end(#zone{until=Until, gmtoff=Offset}, DSTOffset) ->
{_,_,UTCDatetime}= ezic_date:all_times(Until, Offset, DSTOffset),
UTCDatetime.