From 188f31d54373f8ae4477dba58e10ce06895c9b7a Mon Sep 17 00:00:00 2001 From: aj Date: Wed, 3 Nov 2010 15:06:51 -0700 Subject: [PATCH] parsing tzdata files appears to be functionally complete. calling ezic:load("/path/to/tzdata_folder") returns a list of records without erroring-out. The content may not be accurate; @todo create accuracy tests (idea: use system's zic/localtime/tzselect/etc.). --- Makefile | 14 ++- include/ezic.hrl | 17 ++++ src/ezic_loader.erl | 14 ++- src/ezic_record.erl | 170 ++++++++++++++++++++++++++++++++---- tests/ezic_record_tests.erl | 87 ++++++++++++++++++ tests/test_master.erl | 2 +- 6 files changed, 276 insertions(+), 28 deletions(-) create mode 100644 tests/ezic_record_tests.erl diff --git a/Makefile b/Makefile index 7fca5ae..96e630f 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,8 @@ ERLC_WARNINGS = -W1 MNESIA_DIR = db RUN_INIT = -DEBUG = -OPTIONS = +DEBUG = +debug_info -Ddebug +TEST = all : clean compile @@ -10,9 +10,9 @@ nowarn : ERLC_WARNINGS = -W0 nowarn : clean compile test : ERLC_WARNINGS = -W0 -test : OPTIONS := $(OPTIONS) -DTEST +test : TEST := -DTEST test : all - erlc $(ERLC_WARNINGS) $(OPTIONS) -o ./ebin ./tests/*.erl + erlc $(ERLC_WARNINGS) $(TEST) -o ./ebin ./tests/*.erl erl -noshell -pa ebin -s test_master start -s erlang halt compile : @@ -26,7 +26,5 @@ clean : run : erl -pa ebin -mnesia dir $(MNESIA_DIR) $(RUN_INIT) - -debug : DEBUG = +debug_info -debug : RUN_INIT = -s debugger start -debug : all \ No newline at end of file +build : DEBUG = +build : all \ No newline at end of file diff --git a/include/ezic.hrl b/include/ezic.hrl index 4a35ad3..205cee6 100644 --- a/include/ezic.hrl +++ b/include/ezic.hrl @@ -8,5 +8,22 @@ -record(link, {from, to}). +-record(leap, {datetime, corr, rs}). + + +-record(tztime, {time, flag}). +-record(tzon, {day, filter}). + + + % a flattened zone record, giving specific information for a specific time range. -record(flatzone, {tzname, tzrule, from, to, gmtoff}). + + + + +-ifdef(debug). +-define(debug(F,X), io:format("{~p:~p} - " ++ F ++ "~n", [?MODULE, ?LINE] ++ X)). +-else. +-define(debug(F,X), void). +-endif. diff --git a/src/ezic_loader.erl b/src/ezic_loader.erl index ee65c22..01f7339 100644 --- a/src/ezic_loader.erl +++ b/src/ezic_loader.erl @@ -52,6 +52,7 @@ parse_lines({ok, Line}, File, Records) -> parse_lines_2(Line, File, Records) -> + ?debug("Line: ~p", [Line]), [Type | Data] = string:tokens(Line, " \t"), {ok, PrevType, PrevName} = prev_rec_type(Records), {ok, Record} = build_record(Type, Data, {PrevType, PrevName}), @@ -68,10 +69,13 @@ prev_rec_type(List) when is_list(List) -> clean_line(Line) -> Line1= string:strip(Line), Line2= string:strip(Line1, both, $\n), - CPos= string:chr(Line2, $#), + Line3= string:strip(Line2, both, $\t), + + FinalLine=Line3, + CPos= string:chr(FinalLine, $#), case CPos of - 0 -> Line2; - _ -> string:sub_string(Line2, 1, CPos-1) + 0 -> FinalLine; + _ -> string:sub_string(FinalLine, 1, CPos-1) end. @@ -84,6 +88,8 @@ build_record("Link", Data,_) -> ezic_record:link(Data); build_record(GmtOff, Data, {"Zone", PrevName}) -> ezic_record:zone([PrevName,GmtOff|Data]); +build_record("Leap", Data,_) -> + ezic_record:leap(Data); build_record(Type, Data, PT) -> {error, {badLine, Type, Data, PT}}. @@ -92,4 +98,4 @@ build_record(Type, Data, PT) -> % converts tzdata records to flattened records % returns list of #flatzone{} flatten(Records) when is_list(Records) -> - void. + Records. diff --git a/src/ezic_record.erl b/src/ezic_record.erl index e6dca8c..9107caf 100644 --- a/src/ezic_record.erl +++ b/src/ezic_record.erl @@ -1,30 +1,95 @@ -module(ezic_record). -include("include/ezic.hrl"). --export([rule/1,zone/1,link/1]). +-include_lib("eunit/include/eunit.hrl"). -rule([Name,From,To,Type,In,On,At,Save,Letters]) -> +-ifdef(TEST). +-compile([export_all]). +-endif. + + +-export([rule/1,zone/1,link/1,leap/1]). + + + +rule([Name,FromS,ToS,Type,InS,OnS,AtS,SaveS,Letters]) -> + From= parse_from(FromS), + To= parse_to(ToS), + % @todo handle year types + In= month_to_num(InS), + On= parse_on(OnS), + At=parse_at(AtS), + Save=parse_save(SaveS), + {ok, #rule{ name=Name, from=From, to=To, type=Type, in=In, on=On, at=At, save=Save, letters=Letters }}. -zone([Name,GmtOff,Rules,Format | UntilTokens]) -> + +zone([Name,GmtOffS,Rules,FormatS | UntilTokens]) -> + GmtOff=parse_time(GmtOffS), Until = parse_until(UntilTokens), + Format= convert_format(FormatS), + {ok, #zone{ name=Name, gmtoff=GmtOff, rules=Rules, format=Format, until=Until }}. + link([From,To]) -> {ok, #link{from=From, to=To}}. +leap([YearS, MonthS, DayS, TimeS, Corr, RS]) -> + Y=list_to_integer(YearS), + M=month_to_num(MonthS), + D=list_to_integer(DayS), + Time=parse_abs_time(TimeS), + {ok, #leap{datetime={{Y,M,D}, Time}}}. + + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -% INTERNAL +% PRIVATE %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +parse_from(X) when X=:="maximum";X=:="max" -> + maximum; +parse_from(X) when X=:="minimum";X=:="min" -> + minimum; +parse_from(Year) -> + list_to_integer(Year). + + + +parse_to("only") -> + only; +parse_to(Other) -> + parse_from(Other). + + + +parse_on(X=[$l,$a,$s,$t|Day]) -> + case is_day(Day) of + true -> list_to_atom(X); + false -> erlang:error(badday,X) + end; +parse_on(X=[D,A,Y, Sign,$= | IntS]) -> + Int=list_to_integer(IntS), + Day=[D,A,Y], + {ok, FSign} =parse_on_sign(Sign), + + case is_day(Day) of + true -> #tzon{day=list_to_atom(string:to_lower(Day)), filter={FSign, Int}}; + false -> erlang:error(badday, X) + end; +parse_on(X) -> + list_to_integer(X). + + parse_until([]) -> current; parse_until([YS]) -> @@ -37,33 +102,99 @@ parse_until([YS,MS]) -> parse_until([YS,MS,DS]) -> Y= list_to_integer(YS), M=month_to_num(MS), - D= list_to_integer(DS), + D= parse_on(DS), {Y,M,D}; -% @todo broken: time flags must be accounted for parse_until([YS,MS,DS,TS]) -> Y= list_to_integer(YS), M=month_to_num(MS), - D= list_to_integer(DS), - TimeTokens=string:tokens(TS, ":"), - {{Y,M,D},parse_time(TimeTokens)}. + D= parse_on(DS), + {{Y,M,D},parse_time(TS)}. +parse_at(X) -> + parse_time(X). + + + +parse_save("-") -> + parse_time("-"); +parse_save(X) -> + XRev=lists:reverse(X), + parse_save_rev(XRev). + + parse_time("-") -> - {0,0,0}; -parse_time([HS]) -> + #tztime{time={0,0,0}}; +parse_time(TS) -> + parse_time_rev(lists:reverse(TS)). + + +parse_abs_time(T) -> + Tokez= string:tokens(T, ":"), + parse_abs_time_2(Tokez). + + + +convert_format(Format) -> + convert_format(Format, string:str(Format, "%s")). + + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% INTERNAL +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + +parse_on_sign($>) -> {ok, geq}; +parse_on_sign($<) -> {ok, leq}. + + + + +parse_time_rev([F|RTime]) when F =:= $s; F =:= $w; F =:= $u -> + Time=lists:reverse(RTime), + #tztime{time=parse_abs_time(Time), flag=list_to_atom([F])}; +parse_time_rev(X=[F|_]) when F-48 < 0; F-48 > 9 -> + erlang:error(badtime, X); +parse_time_rev(RTime) -> + Time=lists:reverse(RTime), + #tztime{time=parse_abs_time(Time)}. + + + + +parse_abs_time_2([HS]) -> H= list_to_integer(HS), {H,0,0}; -parse_time([HS,MS]) -> +parse_abs_time_2([HS,MS]) -> H= list_to_integer(HS), M= list_to_integer(MS), {H,M,0}; -parse_time([HS,MS,SS]) -> +parse_abs_time_2([HS,MS,SS]) -> H= list_to_integer(HS), M= list_to_integer(MS), S= list_to_integer(SS), - {H,M,S}. + {H,M,S}; +parse_abs_time_2(X) -> + erlang:error(badtime, X). + + +parse_save_rev(X=[F|_]) when F-48 < 0; F-48 > 9 -> + erlang:error(badsave, lists:reverse(X)); +parse_save_rev(X) -> + parse_time(lists:reverse(X)). + + + +convert_format(Format, 0) -> + Format; +convert_format(Format, Pos) -> + First=string:substr(Format, 1, Pos-1), + Last=string:substr(Format, Pos+2), + string:concat(First, [$~, $s|Last]). + @@ -79,4 +210,13 @@ month_to_num("Aug") -> 8; month_to_num("Sep") -> 9; month_to_num("Oct") -> 10; month_to_num("Nov") -> 11; -month_to_num("Dec") -> 12. +month_to_num("Dec") -> 12; +month_to_num(X) -> erlang:error(badMonth, X). + + + +is_day(X) when X=:="Mon";X=:="Tue";X=:="Wed";X=:="Thu";X=:="Fri";X=:="Sat";X=:="Sun" -> + true; +is_day(_) -> + false. + diff --git a/tests/ezic_record_tests.erl b/tests/ezic_record_tests.erl new file mode 100644 index 0000000..98fbe0a --- /dev/null +++ b/tests/ezic_record_tests.erl @@ -0,0 +1,87 @@ +-module(ezic_record_tests). +-include("include/ezic.hrl"). +-include_lib("eunit/include/eunit.hrl"). + + +parse_fromto_test_() -> + [ + ?_assertEqual(minimum, ezic_record:parse_from("minimum")) + , ?_assertEqual(minimum, ezic_record:parse_to("minimum")) + + , ?_assertEqual(maximum, ezic_record:parse_from("maximum")) + , ?_assertEqual(maximum, ezic_record:parse_to("maximum")) + + , ?_assertError(badarg, ezic_record:parse_from("only")) + , ?_assertEqual(only, ezic_record:parse_to("only")) + + , ?_assertEqual(1978, ezic_record:parse_from("1978")) + , ?_assertEqual(1978, ezic_record:parse_to("1978")) + + , ?_assertError(badarg, ezic_record:parse_from("asdf")) + , ?_assertError(badarg, ezic_record:parse_to("asdf")) + ]. + + +parse_time_test_() -> + [ + ?_assertEqual(#tztime{time={0,0,0}}, ezic_record:parse_time("-")) + , ?_assertEqual(#tztime{time={2,0,0}}, ezic_record:parse_time("2:00")) + , ?_assertEqual(#tztime{time={15,0,0}}, ezic_record:parse_time("15:00")) + , ?_assertEqual(#tztime{time={1,28,14}}, ezic_record:parse_time("1:28:14")) + , ?_assertEqual(#tztime{time={1,28,14}, flag=u}, ezic_record:parse_time("1:28:14u")) + , ?_assertEqual(#tztime{time={1,28,14}, flag=w}, ezic_record:parse_time("1:28:14w")) + , ?_assertEqual(#tztime{time={1,28,14}, flag=s}, ezic_record:parse_time("1:28:14s")) + + , ?_assertError(badtime, ezic_record:parse_time("1:28:14wtf?")) + , ?_assertError(badtime, ezic_record:parse_time("1:28:14:00")) + ]. + + +parse_at_test_() -> + [ + ?_assertEqual(#tztime{time={0,0,0}}, ezic_record:parse_at("-")) + , ?_assertEqual(#tztime{time={2,0,0}}, ezic_record:parse_at("2:00")) + , ?_assertEqual(#tztime{time={15,0,0}}, ezic_record:parse_at("15:00")) + , ?_assertEqual(#tztime{time={1,28,14}}, ezic_record:parse_at("1:28:14")) + , ?_assertEqual(#tztime{time={1,28,14}, flag=u}, ezic_record:parse_at("1:28:14u")) + , ?_assertEqual(#tztime{time={1,28,14}, flag=w}, ezic_record:parse_at("1:28:14w")) + , ?_assertEqual(#tztime{time={1,28,14}, flag=s}, ezic_record:parse_at("1:28:14s")) + , ?_assertError(badtime, ezic_record:parse_at("1:28:14wtf?")) + ]. + + +parse_save_test_() -> + [ + ?_assertEqual(#tztime{time={0,0,0}}, ezic_record:parse_save("-")) + , ?_assertEqual(#tztime{time={2,0,0}}, ezic_record:parse_save("2:00")) + , ?_assertEqual(#tztime{time={15,0,0}}, ezic_record:parse_save("15:00")) + , ?_assertEqual(#tztime{time={1,28,14}}, ezic_record:parse_save("1:28:14")) + , ?_assertError(badsave, ezic_record:parse_save("1:28:14u")) + , ?_assertError(badsave, ezic_record:parse_save("1:28:14w")) + , ?_assertError(badsave, ezic_record:parse_save("1:28:14s")) + , ?_assertError(badsave, ezic_record:parse_save("1:28:14wtf?")) + ]. + + + + + +parse_on_test_() -> + [ + ?_assertEqual(5, ezic_record:parse_on("5")) + + , ?_assertEqual(lastSun, ezic_record:parse_on("lastSun")) + , ?_assertEqual(lastMon, ezic_record:parse_on("lastMon")) + , ?_assertError(badday, ezic_record:parse_on("lastFoo")) + + , ?_assertEqual(#tzon{day=sun, filter={geq, 8}}, ezic_record:parse_on("Sun>=8")) + , ?_assertEqual(#tzon{day=sun, filter={leq, 25}}, ezic_record:parse_on("Sun<=25")) + , ?_assertError(badday, ezic_record:parse_on("Foo<=25")) + ]. + + +convert_format_test_() -> + [ + ?_assertEqual("POS", ezic_record:convert_format("POS")) + , ?_assertEqual("P~sS", ezic_record:convert_format("P%sS")) + ]. diff --git a/tests/test_master.erl b/tests/test_master.erl index 1cab61d..868e738 100644 --- a/tests/test_master.erl +++ b/tests/test_master.erl @@ -2,4 +2,4 @@ -export([start/0]). start() -> - void. + ezic_record:test().