diff --git a/src/ezic.erl b/src/ezic.erl index 52f5dcc..a47e901 100644 --- a/src/ezic.erl +++ b/src/ezic.erl @@ -49,7 +49,8 @@ load(Folder) -> dev_start() -> - ezic:load(filename:join("priv","tzdata")). + ezic:load(filename:join("priv","tzdata")), + ezic_flatten:flatten(). test() -> diff --git a/src/ezic_date.erl b/src/ezic_date.erl index ed16ea2..a66fb6d 100644 --- a/src/ezic_date.erl +++ b/src/ezic_date.erl @@ -5,8 +5,10 @@ -export([ + normalize/1 + % rule-specific - for_rule/2 + , for_rule/2 , for_rule_utc/4 @@ -19,6 +21,7 @@ , add_seconds/2 , add_offset/2 , all_times/3 + , m1s/1 % comparisons , compare/2 @@ -32,6 +35,20 @@ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% converts any sane date part into a complete datetime +% USED EXCLUSIVELY FOR COMPARISONS. The resulting "datetime" values may not +% represent true dates or times. +normalize(A) when is_atom(A) -> + A; +normalize(Y) when is_integer(Y) -> + {{Y,1,1}, #tztime{}}; +normalize(Date={Y,M,D}) + when is_integer(Y), is_integer(M), is_integer(D) -> + {Date, #tztime{}}; +normalize(R={{Y,M,D}, #tztime{}}) -> + R. + + % 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}; @@ -97,6 +114,11 @@ add_offset(Datetime, Offset) -> % returns {WallTime, StdTime, UtcTime} % where each is a datetime tuple: {{Y,M,D}{HH,MM,SS}} + +% @todo ensure X is in list: max, maximum, min, minimum, current, ...? +all_times(X,_,_) when is_atom(X) -> + {X,X,X}; + % universal time given all_times({Date, #tztime{time=UTCTime, flag=Flag}}, Offset, DSTOffset) when Flag=:=u; Flag=:=g; Flag=:=z -> @@ -153,6 +175,8 @@ when is_integer(Y1), is_integer(Y2) 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}}) @@ -238,6 +262,19 @@ add_days_in_month(Days, Date={Y,M,D}) -> end end. + + + +m1s(D= {{Y,M,D},{HH,MM,SS}}) + when is_integer(Y), is_integer(M), is_integer(D) + , is_integer(HH), is_integer(MM), is_integer(SS) -> + + calendar:gregorian_seconds_to_datetime(calendar:datetime_to_gregorian_seconds(D) - 1). + + +% @todo type checking +m1s(W,S,U) when is_tuple(W), is_tuple(S), is_tuple(U) -> + {m1s(W), m1s(S), m1s(U)}. diff --git a/src/ezic_flatten.erl b/src/ezic_flatten.erl index 45ec9a2..e8fe94e 100644 --- a/src/ezic_flatten.erl +++ b/src/ezic_flatten.erl @@ -1,5 +1,7 @@ -module(ezic_flatten). -include("include/ezic.hrl"). +-include_lib("eunit/include/eunit.hrl"). + -define(FLAT(W,S,U), #flatzone{wall_from=W, std_from=S, utc_from=U}). -define(ENDFLAT(F,W,S,U), F#flatzone{wall_to=W, std_to=S, utc_to=U}). @@ -19,7 +21,6 @@ flatten() -> not_done. - %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % PRIVATE %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -29,43 +30,11 @@ flatten() -> flatten_zones(Zones) -> flatten_zones(Zones, []). - - -make_next_flat(#flatzone{wall_to=W, std_to=S, utc_to=U, offset=O}) -> - make_next_flat({W,S,U}, O); -make_next_flat(EndTimes) -> - make_next_flat(EndTimes, undefined). - - -make_next_flat({W,S,U}, O) -> - FW= ezic_date:add_seconds(W,1), - FS= ezic_date:add_seconds(S,1), - FU= ezic_date:add_seconds(U,1), - Flat= ?FLAT(FW, FS, FU), - Flat#flatzone{offset=O}. - - - -end_flat(Flat=#flatzone{}, {W,S,U}) -> - ?ENDFLAT(Flat,W,S,U). - - - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -% INTERNAL -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - - - - flatten_zones([], Flats) -> Flats; flatten_zones([Z1|_]= AllZones, Flats) -> {CurrentZones, RestZones}= ezic_zone:split_by_name(Z1, AllZones), - - SortedZones= lists:sort(fun ezic_zone:sort_ascending/2, CurrentZones), - NewFlats= lists:merge(Flats, flatten_zone_set(SortedZones)), - + NewFlats= lists:merge(Flats, flatten_zone_set(CurrentZones)), flatten_zones(RestZones, NewFlats). @@ -88,7 +57,12 @@ flatten_zone_set(Zones) -> % assumes a new zone every time it is called flatten_zone_set(_, [], Flats) -> Flats; -flatten_zone_set(FromTimeStub=#flatzone{utc_from=UTCFrom, dstoffset=DSTOffset}, [Z1=#zone{rule=RuleName, until=UntilTime, gmtoff=Offset} | _RestZones], _Flats) -> +flatten_zone_set(FromTimeStub=#flatzone{utc_from=UTCFrom, dstoffset=DSTOffset} + , Zones %[Z1=#zone{rule=RuleName, until=UntilTime, gmtoff=Offset} | _RestZones], + , Flats) -> + + {Zone, RestZones}= ezic_zone:next(Zones, UTCFrom, DSTOffset), + #zone{rule=RuleName, until=UntilTime, gmtoff=Offset}=Zone, %% we have a flatzone with start times, we populate the base offset FromTime= FromTimeStub#flatzone{offset=Offset}, @@ -97,7 +71,10 @@ flatten_zone_set(FromTimeStub=#flatzone{utc_from=UTCFrom, dstoffset=DSTOffset}, %% we gather all rules that _may_ apply (same year) Rules= ezic_db:rules(RuleName), - RuleDates= lists:foldl( + + %% and add normalized (inaccurate) dates for sorting purposes + %% note this may be empty + RulesWithDates= lists:foldl( fun(R, Acc)-> case ezic_rule:project_next(R, Offset, DSTOffset, UTCFrom) of none -> Acc; @@ -105,11 +82,33 @@ flatten_zone_set(FromTimeStub=#flatzone{utc_from=UTCFrom, dstoffset=DSTOffset}, end end ,[], Rules), - AllDates= [{ezic_zone:project_end(Z1, DSTOffset), Z1} | RuleDates], - - + %% tack on the zone w/ date, in case it ends first + ZoneWithDate= {ezic_zone:project_end_utc(Zone, DSTOffset), Zone}, + [NextEventWithDate | RestWithDates]= lists:sort(RulesAndZoneWithDates), + ?debugVal(NextEventWithDate), + ?debugVal(RestWithDates), + + {_,NextEvent}= NextEventWithDate, + {EndFlat, NextFlat}= finish_and_start_flat(FromTime, NextEvent, Offset, DSTOffset), + NewFlats= [EndFlat | Flats], + + RestZones= [Z || {_,Z} <- RestWithDates], + + flatten_zone_set(NextFlat, RestZones, NewFlats). + + + + + + +finish_and_start_flat(Flat=#flatzone{}, Zone=#zone{}, _Offset, DSTOffset) -> + NextStart= {WT, ST, UT}= ezic_zone:project_end(Zone, DSTOffset), + {MWT, MST, MUT}= ezic_date:m1s(NextStart), + EndFlat= ?ENDFLAT(Flat, MWT, MST, MUT), + NextFlat= ?FLAT(WT, ST, UT), + {EndFlat, NextFlat}; + +finish_and_start_flat(Flat=#flatzone{utc_from=UTCFrom}, Rule=#rule{}, Offset, DSTOffset) -> not_done. - - diff --git a/src/ezic_zone.erl b/src/ezic_zone.erl index 8d5ac83..d7b9247 100644 --- a/src/ezic_zone.erl +++ b/src/ezic_zone.erl @@ -7,11 +7,11 @@ , current/1 , current_as_of_utc/2 , split_by_name/2 - , sort_ascending/2 - , sort_descending/2 , offset_sec/1 , project_end/2 + , project_end_utc/2 + , next/3 ]). @@ -50,16 +50,6 @@ current_as_of_utc(UTCDatetime, TzName) -> get_zone_utc(_, []) -> erlang:error(no_current); get_zone_utc(_UTCDatetime, Zones) -> - _SortedZones= lists:sort(fun sort_descending/2, Zones), -%% [CZone|_]= lists:dropwhile(fun(SZ)-> older_zone_utc(SZ, UTCDatetime) end, SortedZones), -%% CZone. - - - %% begin with GMT offset. - %% foreach rule (oldest first) - %% see whether until=(standard|wall|utc) time - %% - not_done. @@ -76,14 +66,6 @@ split_by_name(#zone{name=N}, Zones) -> , Zones). -sort_descending(#zone{until=U1}, #zone{until=U2}) -> - ezic_date:compare(U2, U1). - -sort_ascending(Z1=#zone{}, Z2=#zone{}) -> - not sort_descending(Z1, Z2). - - - % @bug doesn't normalize times. Times are self-relative (to standard time), and Now may come from any timezone unless we're careful about that. %% older_zone(#zone{until=Until}, UTCDatetime) -> %% ezic_date:compare(Until, UTCDatetime). @@ -98,9 +80,28 @@ 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), + ezic_date:all_times(Until, Offset, DSTOffset). + +% returns just the UTC datetime projected for zone end +project_end_utc(Zone=#zone{}, DSTOffset) -> + {_,_,UTCDatetime}= project_end(Zone, DSTOffset), UTCDatetime. + - +% 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. +next(ZoneList, UTCFrom, DSTOff) -> + DatedList= lists:map( + fun(Z=#zone{until=Until, gmtoff=Offset})-> + NU= ezic_date:normalize(Until), + {_,_,UTCDt}= ezic_date:all_times(NU, Offset, DSTOff), + {UTCDt, Z} + end + , ZoneList), + [{_,Zone} | DRest]= lists:sort(DatedList), + {Zone, [R || {_,R}<- DRest]}. diff --git a/test/ezic_zone_tests.erl b/test/ezic_zone_tests.erl index 25273e3..2b5bd39 100644 --- a/test/ezic_zone_tests.erl +++ b/test/ezic_zone_tests.erl @@ -15,20 +15,20 @@ split_by_name_test_() -> -project_end_test_() -> +project_end_utc_test_() -> [ ?_assertEqual({{2010,11,10},{16,20,0}}, - ezic_zone:project_end( + ezic_zone:project_end_utc( #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( + ezic_zone:project_end_utc( #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( + ezic_zone:project_end_utc( #zone{until={{2010,11,10},#tztime{time={16,20,0}, flag=u}}, gmtoff={-7,0,0}} , {1,0,0})) ].