diff --git a/include/ezic.hrl b/include/ezic.hrl index 800def2..42aa960 100644 --- a/include/ezic.hrl +++ b/include/ezic.hrl @@ -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}}). diff --git a/src/ezic_date.erl b/src/ezic_date.erl index f1675b6..ed16ea2 100644 --- a/src/ezic_date.erl +++ b/src/ezic_date.erl @@ -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. - diff --git a/src/ezic_flatten.erl b/src/ezic_flatten.erl index b6df047..45ec9a2 100644 --- a/src/ezic_flatten.erl +++ b/src/ezic_flatten.erl @@ -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. - - - - diff --git a/src/ezic_parse.erl b/src/ezic_parse.erl index 74d9e8b..0913288 100644 --- a/src/ezic_parse.erl +++ b/src/ezic_parse.erl @@ -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}. diff --git a/src/ezic_record.erl b/src/ezic_record.erl index a85eb49..aed4559 100644 --- a/src/ezic_record.erl +++ b/src/ezic_record.erl @@ -3,11 +3,6 @@ -include_lib("eunit/include/eunit.hrl"). --ifdef(TEST). --compile([export_all]). --endif. - - -export([ link/1 , leap/1 diff --git a/src/ezic_rule.erl b/src/ezic_rule.erl index b34dc60..7ce8a29 100644 --- a/src/ezic_rule.erl +++ b/src/ezic_rule.erl @@ -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. diff --git a/src/ezic_zone.erl b/src/ezic_zone.erl index 3c43718..8d5ac83 100644 --- a/src/ezic_zone.erl +++ b/src/ezic_zone.erl @@ -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. + diff --git a/test/ezic_date_tests.erl b/test/ezic_date_tests.erl index dddc24f..e0db655 100644 --- a/test/ezic_date_tests.erl +++ b/test/ezic_date_tests.erl @@ -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)) ]. diff --git a/test/ezic_rule_tests.erl b/test/ezic_rule_tests.erl new file mode 100644 index 0000000..ae0c59e --- /dev/null +++ b/test/ezic_rule_tests.erl @@ -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}} + )) + + ]. diff --git a/test/ezic_zone_tests.erl b/test/ezic_zone_tests.erl index 5db7fff..25273e3 100644 --- a/test/ezic_zone_tests.erl +++ b/test/ezic_zone_tests.erl @@ -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})) + ]. diff --git a/test/test.erl b/test/test.erl index 05ebae2..0b5a3ce 100644 --- a/test/test.erl +++ b/test/test.erl @@ -7,5 +7,6 @@ all() -> , eunit:test(ezic_date) , eunit:test(ezic_zone) , eunit:test(ezic_parse) + , eunit:test(ezic_rule) ,ok.