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

@@ -11,13 +11,13 @@
-record(leap, {datetime, corr, rs}).
-record(tztime, {time={0,0,0}, flag}).
-record(tztime, {time={0,0,0}, flag=w}).
-record(tzon, {day, filter}).
% a flattened zone record, giving specific information for a specific time range.
-record(flatzone, {tzname, wall_from, wall_to, std_from, std_to, utc_from, utc_to, offset}).
-record(flatzone, {tzname, wall_from, wall_to, std_from, std_to, utc_from, utc_to, offset, dstoffset={0,0,0}}).

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.

View File

@@ -4,19 +4,46 @@
for_rule_test_() ->
BR= #rule{at=#tztime{}},
[
% last days of months
?_assertEqual({2010, 03, 28}, ezic_date:for_rule(#rule{in=3, on={last, "Sun"}}, 2010))
, ?_assertEqual({2010, 11, 30}, ezic_date:for_rule(#rule{in=11, on={last, "Tue"}}, 2010))
, ?_assertEqual({2010, 11, 24}, ezic_date:for_rule(#rule{in=11, on={last, "Wed"}}, 2010))
?_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}, ezic_date:for_rule(#rule{in=11, on=#tzon{day="Sun", filter={geq, 9}}}, 2010))
, ?_assertEqual({2010, 11, 7}, ezic_date:for_rule(#rule{in=11, on=#tzon{day="Sun", filter={leq, 9}}}, 2010))
, ?_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(#rule{in=11, on=#tzon{day="Sun", filter={leq, 1}}}, 2010))
, ?_assertError(no_next_day, ezic_date:for_rule(#rule{in=11, on=#tzon{day="Sun", filter={geq, 29}}}, 2010))
, ?_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}},
URule= #rule{in=11, on=11, at=#tztime{time={0,0,0}, flag=u}, save={0,0,0}},
DST= {1,0,0},
%%% given a DST savings of 1 in a -7 zone, and given rules in each of the relative timezones:
%%% - wall time should respect the given DST and offset
%%% - std time should respect the offset, but not DST
%%% - utc should respect neither
[
%% same "time" with different relative contexts should return different UTC results
?_assertEqual({{2010,11,11}, {6,0,0}}, ezic_date:for_rule_utc(WRule, {-7,0,0}, DST, 2010))
, ?_assertEqual({{2010,11,11}, {7,0,0}}, ezic_date:for_rule_utc(SRule, {-7,0,0}, DST, 2010))
, ?_assertEqual({{2010,11,11}, {0,0,0}}, ezic_date:for_rule_utc(URule, {-7,0,0}, DST, 2010))
%% some absolute time in all contexts should return the same UTC results
, ?_assertEqual({{2010,11,11}, {0,0,0}},
ezic_date:for_rule_utc(WRule#rule{on=10, at=#tztime{flag=w, time={18,0,0}}}, {-7,0,0}, DST, 2010))
, ?_assertEqual({{2010,11,11}, {0,0,0}},
ezic_date:for_rule_utc(SRule#rule{on=10, at=#tztime{flag=s, time={17,0,0}}}, {-7,0,0}, DST, 2010))
, ?_assertEqual({{2010,11,11}, {0,0,0}},
ezic_date:for_rule_utc(URule, {-7,0,0}, DST, 2010))
].

37
test/ezic_rule_tests.erl Normal file
View File

@@ -0,0 +1,37 @@
-module(ezic_rule_tests).
-include("include/ezic.hrl").
-include_lib("eunit/include/eunit.hrl").
project_next_test_() ->
[
% simple UTC example
?_assertEqual({{2010,11,10},{16,20,0}},
ezic_rule:project_next(
#rule{from=1984, to=2032, in=11, on=10, at=#tztime{time={16,20,0}, flag=u}}
, {-5,0,0}, {-1,0,0}, {{2010,11,10},{0,0,0}}
))
% skip a year UTC example
, ?_assertEqual({{2011,11,10},{16,20,0}},
ezic_rule:project_next(
#rule{from=1984, to=2032, in=11, on=10, at=#tztime{time={16,20,0}, flag=u}}
, {-5,0,0}, {-1,0,0}, {{2010,11,10},{16,21,0}}
))
% skip 3 years UTC example
, ?_assertEqual({{2013,11,10},{16,20,0}},
ezic_rule:project_next(
#rule{from=1984, to=2032, in=11, on=10, at=#tztime{time={16,20,0}, flag=u}}
, {-5,0,0}, {-1,0,0}, {{2012,11,11},{16,20,0}}
))
% no solution
, ?_assertEqual(none,
ezic_rule:project_next(
#rule{from=1984, to=2010, in=11, on=10, at=#tztime{time={16,20,0}, flag=u}}
, {-5,0,0}, {-1,0,0}, {{2012,11,11},{16,20,0}}
))
].

View File

@@ -12,3 +12,23 @@ split_by_name_test_() ->
, ?_assertEqual({BZones, AZones}, ezic_zone:split_by_name(B, AllZones))
, ?_assertEqual({[], AllZones}, ezic_zone:split_by_name(#zone{name=c}, AllZones))
].
project_end_test_() ->
[
?_assertEqual({{2010,11,10},{16,20,0}},
ezic_zone:project_end(
#zone{until={{2010,11,10},#tztime{time={10,20,0}, flag=w}}, gmtoff={-7,0,0}}
, {1,0,0}))
, ?_assertEqual({{2010,11,10},{16,20,0}},
ezic_zone:project_end(
#zone{until={{2010,11,10},#tztime{time={9,20,0}, flag=s}}, gmtoff={-7,0,0}}
, {1,0,0}))
, ?_assertEqual({{2010,11,10},{16,20,0}},
ezic_zone:project_end(
#zone{until={{2010,11,10},#tztime{time={16,20,0}, flag=u}}, gmtoff={-7,0,0}}
, {1,0,0}))
].

View File

@@ -7,5 +7,6 @@ all() ->
, eunit:test(ezic_date)
, eunit:test(ezic_zone)
, eunit:test(ezic_parse)
, eunit:test(ezic_rule)
,ok.