diff --git a/.travis.yml b/.travis.yml index efd1082..745f994 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ -=nlanguage: erlang +language: erlang otp_release: - R15B02 - R15B01 diff --git a/Makefile b/Makefile index 538bc3e..51727d2 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,7 @@ # BSD License see COPYING ERL = $(shell which erl) +ERL_VER = $(shell erl -eval 'erlang:display(erlang:system_info(otp_release)), halt().' -noshell) ERLFLAGS= -pa $(CURDIR)/.eunit -pa $(CURDIR)/ebin -pa $(CURDIR)/*/ebin @@ -14,10 +15,9 @@ endif ERLWARE_COMMONS_PLT=$(CURDIR)/.erlware_commons_plt -.PHONY: all compile doc clean test dialyzer typer shell distclean pdf get-deps \ - rebuild +.PHONY: all compile doc clean test shell distclean pdf get-deps rebuild #dialyzer typer #fail on Travis. -all: compile dialyzer doc test +all: compile doc test #dialyzer #fail on travis get-deps: $(REBAR) get-deps @@ -27,22 +27,41 @@ compile: $(REBAR) skip_deps=true compile doc: compile - $(REBAR) skip_deps=true doc + - $(REBAR) skip_deps=true doc test: compile $(REBAR) skip_deps=true eunit -$(ERLWARE_COMMONS_PLT): - @echo Building local plt at $(ERLWARE_COMMONS_PLT) +$(ERLWARE_COMMONS_PLT).$(ERL_VER).erts: + @echo Building local plt at $(ERLWARE_COMMONS_PLT).$(ERL_VER).base @echo - - dialyzer --fullpath --output_plt $(ERLWARE_COMMONS_PLT) --build_plt \ - --apps erts kernel stdlib eunit -r deps -dialyzer: $(ERLWARE_COMMONS_PLT) - dialyzer --fullpath --plt $(ERLWARE_COMMONS_PLT) -Wrace_conditions -r ./ebin + - dialyzer --fullpath --verbose --output_plt $(ERLWARE_COMMONS_PLT).$(ERL_VER).base --build_plt \ + --apps erts -typer: - typer --plt $(ERLWARE_COMMONS_PLT) -r ./src +$(ERLWARE_COMMONS_PLT).$(ERL_VER).kernel:$(ERLWARE_COMMONS_PLT).$(ERL_VER).erts + @echo Building local plt at $(ERLWARE_COMMONS_PLT).$(ERL_VER).base + @echo + - dialyzer --fullpath --verbose --output_plt $(ERLWARE_COMMONS_PLT).$(ERL_VER).base --build_plt \ + --apps kernel + +$(ERLWARE_COMMONS_PLT).$(ERL_VER).base:$(ERLWARE_COMMONS_PLT).$(ERL_VER).kernel + @echo Building local plt at $(ERLWARE_COMMONS_PLT).$(ERL_VER).base + @echo + - dialyzer --fullpath --verbose --output_plt $(ERLWARE_COMMONS_PLT).$(ERL_VER).base --build_plt \ + --apps stdlib + +$(ERLWARE_COMMONS_PLT).$(ERL_VER): $(ERLWARE_COMMONS_PLT).$(ERL_VER).base + @echo Building local plt at $(ERLWARE_COMMONS_PLT).$(ERL_VER) + @echo + - dialyzer --fullpath --verbose --output_plt $(ERLWARE_COMMONS_PLT).$(ERL_VER) --add_to_plt --plt $(ERLWARE_COMMONS_PLT).$(ERL_VER).base \ + --apps eunit -r deps + +dialyzer: $(ERLWARE_COMMONS_PLT).$(ERL_VER) + dialyzer --fullpath --plt $(ERLWARE_COMMONS_PLT).$(ERL_VER) -Wrace_conditions -r ./ebin + +typer: $(ERLWARE_COMMONS_PLT).$(ERL)VER( + typer --plt $(ERLWARE_COMMONS_PLT).$(ERL_VER) -r ./src shell: compile # You often want *rebuilt* rebar tests to be available to the @@ -61,7 +80,7 @@ clean: - rm $(CURDIR)/doc/edoc-info distclean: clean - rm -rf $(ERLWARE_COMMONS_PLT) + rm -rf $(ERLWARE_COMMONS_PLT).$(ERL_VER) rm -rvf $(CURDIR)/deps/* rebuild: distclean get-deps all diff --git a/rebar.config.script b/rebar.config.script index 1d5996c..c19a751 100644 --- a/rebar.config.script +++ b/rebar.config.script @@ -1,4 +1,4 @@ -{match, [ErtsNumber]} = re:run("R15B02", "R(\\d+).+", [{capture, [1], list}]), +{match, [ErtsNumber]} = re:run(erlang:system_info(otp_release), "R(\\d+).+", [{capture, [1], list}]), ErtsVsn = erlang:list_to_integer(ErtsNumber), Opts1 = case lists:keysearch(erl_opts, 1, CONFIG) of {value, {erl_opts, Opts0}} -> @@ -8,8 +8,8 @@ Opts1 = case lists:keysearch(erl_opts, 1, CONFIG) of end, Opts2 = if ErtsVsn >= 15 -> - [{d, have_callback_support} | Opts1]; - true -> + [{d, have_callback_support} | Opts1]; + true -> Opts1 end, lists:keystore(erl_opts, 1, CONFIG, {erl_opts, Opts2}). diff --git a/src/ec_date.erl b/src/ec_date.erl index 16a0af7..66ffaeb 100644 --- a/src/ec_date.erl +++ b/src/ec_date.erl @@ -34,10 +34,16 @@ -define( is_us_sep(X), ( X==$/) ). -define( is_world_sep(X), ( X==$-) ). +-define( MONTH_TAG, month ). +-define( is_year(X), (is_integer(X) andalso X > 31) ). +-define( is_day(X), (is_integer(X) andalso X =< 31) ). +-define( is_hinted_month(X), (is_tuple(X) andalso size(X)=:=2 andalso element(1,X)=:=?MONTH_TAG) ). +-define( is_month(X), ( (is_integer(X) andalso X =< 12) orelse ?is_hinted_month(X) ) ). + -define(GREGORIAN_SECONDS_1970, 62167219200). -type year() :: non_neg_integer(). --type month() :: 1..12. +-type month() :: 1..12 | {?MONTH_TAG, 1..12}. -type day() :: 1..31. -type hour() :: 0..23. -type minute() :: 0..59. @@ -81,7 +87,7 @@ parse(Date, Now) -> do_parse(Date, Now, []). do_parse(Date, Now, Opts) -> - case parse(tokenise(string:to_upper(Date), []), Now, Opts) of + case filter_hints(parse(tokenise(string:to_upper(Date), []), Now, Opts)) of {error, bad_date} -> erlang:throw({?MODULE, {bad_date, Date}}); {D1, T1} = {{Y, M, D}, {H, M1, S}} @@ -96,7 +102,7 @@ do_parse(Date, Now, Opts) -> when is_number(Y), is_number(M), is_number(D), is_number(H), is_number(M1), is_number(S), - is_number(Ms) -> + is_number(Ms) -> case calendar:valid_date(D1) of true -> {D1, {H,M1,S,Ms}}; false -> erlang:throw({?MODULE, {bad_date, Date}}) @@ -104,63 +110,120 @@ do_parse(Date, Now, Opts) -> Unknown -> erlang:throw({?MODULE, {bad_date, Date, Unknown }}) end. +filter_hints({{Y, {?MONTH_TAG, M}, D}, {H, M1, S}}) -> + filter_hints({{Y, M, D}, {H, M1, S}}); +filter_hints({{Y, {?MONTH_TAG, M}, D}, {H, M1, S}, {Ms}}) -> + filter_hints({{Y, M, D}, {H, M1, S}, {Ms}}); +filter_hints(Other) -> + Other. + -spec nparse(string()) -> now(). %% @doc parses the datetime from a string into 'now' format nparse(Date) -> case parse(Date) of - {DateS, {H, M, S, Ms} } -> - GSeconds = calendar:datetime_to_gregorian_seconds({DateS, {H, M, S} }), - ESeconds = GSeconds - ?GREGORIAN_SECONDS_1970, - {ESeconds div 1000000, ESeconds rem 1000000, Ms}; - DateTime -> - GSeconds = calendar:datetime_to_gregorian_seconds(DateTime), - ESeconds = GSeconds - ?GREGORIAN_SECONDS_1970, - {ESeconds div 1000000, ESeconds rem 1000000, 0} + {DateS, {H, M, S, Ms} } -> + GSeconds = calendar:datetime_to_gregorian_seconds({DateS, {H, M, S} }), + ESeconds = GSeconds - ?GREGORIAN_SECONDS_1970, + {ESeconds div 1000000, ESeconds rem 1000000, Ms}; + DateTime -> + GSeconds = calendar:datetime_to_gregorian_seconds(DateTime), + ESeconds = GSeconds - ?GREGORIAN_SECONDS_1970, + {ESeconds div 1000000, ESeconds rem 1000000, 0} end. %% %% LOCAL FUNCTIONS %% +parse([Year, X, Month, X, Day, Hour, $:, Min, $:, Sec, $Z ], _Now, _Opts) + when (?is_us_sep(X) orelse ?is_world_sep(X)) + andalso Year > 31 -> + {{Year, Month, Day}, {hour(Hour, []), Min, Sec}, { 0}}; + +parse([Year, X, Month, X, Day, Hour, $:, Min, $:, Sec, $+, Off | _Rest ], _Now, _Opts) + when (?is_us_sep(X) orelse ?is_world_sep(X)) + andalso Year > 31 -> + {{Year, Month, Day}, {hour(Hour, []) - Off, Min, Sec}, {0}}; + +parse([Year, X, Month, X, Day, Hour, $:, Min, $:, Sec, $-, Off | _Rest ], _Now, _Opts) + when (?is_us_sep(X) orelse ?is_world_sep(X)) + andalso Year > 31 -> + {{Year, Month, Day}, {hour(Hour, []) + Off, Min, Sec}, {0}}; %% Date/Times 22 Aug 2008 6:35.0001 PM parse([Year,X,Month,X,Day,Hour,$:,Min,$:,Sec,$., Ms | PAM], _Now, _Opts) when ?is_meridian(PAM) andalso (?is_us_sep(X) orelse ?is_world_sep(X)) - andalso Year > 31 -> + andalso ?is_year(Year) -> {{Year, Month, Day}, {hour(Hour, PAM), Min, Sec}, {Ms}}; parse([Month,X,Day,X,Year,Hour,$:,Min,$:,Sec,$., Ms | PAM], _Now, _Opts) - when ?is_meridian(PAM) andalso ?is_us_sep(X) -> + when ?is_meridian(PAM) andalso ?is_us_sep(X) + andalso ?is_year(Year) -> {{Year, Month, Day}, {hour(Hour, PAM), Min, Sec}, {Ms}}; parse([Day,X,Month,X,Year,Hour,$:,Min,$:,Sec,$., Ms | PAM], _Now, _Opts) - when ?is_meridian(PAM) andalso ?is_world_sep(X) -> + when ?is_meridian(PAM) andalso ?is_world_sep(X) + andalso ?is_year(Year) -> {{Year, Month, Day}, {hour(Hour, PAM), Min, Sec}, {Ms}}; parse([Year,X,Month,X,Day,Hour,$:,Min,$:,Sec,$., Ms], _Now, _Opts) when (?is_us_sep(X) orelse ?is_world_sep(X)) - andalso Year > 31 -> + andalso ?is_year(Year) -> {{Year, Month, Day}, {hour(Hour,[]), Min, Sec}, {Ms}}; parse([Month,X,Day,X,Year,Hour,$:,Min,$:,Sec,$., Ms], _Now, _Opts) - when ?is_us_sep(X) -> + when ?is_us_sep(X) andalso ?is_month(Month) -> {{Year, Month, Day}, {hour(Hour, []), Min, Sec}, {Ms}}; parse([Day,X,Month,X,Year,Hour,$:,Min,$:,Sec,$., Ms ], _Now, _Opts) - when ?is_world_sep(X) -> + when ?is_world_sep(X) andalso ?is_month(Month) -> {{Year, Month, Day}, {hour(Hour, []), Min, Sec}, {Ms}}; +%% Date/Times Dec 1st, 2012 6:25 PM +parse([Month,Day,Year,Hour,$:,Min,$:,Sec | PAM], _Now, _Opts) + when ?is_meridian(PAM) andalso ?is_hinted_month(Month) andalso ?is_day(Day) -> + {{Year, Month, Day}, {hour(Hour, PAM), Min, Sec}}; +parse([Month,Day,Year,Hour,$:,Min | PAM], _Now, _Opts) + when ?is_meridian(PAM) andalso ?is_hinted_month(Month) andalso ?is_day(Day) -> + {{Year, Month, Day}, {hour(Hour, PAM), Min, 0}}; +parse([Month,Day,Year,Hour | PAM], _Now, _Opts) + when ?is_meridian(PAM) andalso ?is_hinted_month(Month) andalso ?is_day(Day) -> + {{Year, Month, Day}, {hour(Hour, PAM), 0, 0}}; + +%% Date/Times Dec 1st, 2012 18:25:15 (no AM/PM) +parse([Month,Day,Year,Hour,$:,Min,$:,Sec], _Now, _Opts) + when ?is_hinted_month(Month) andalso ?is_day(Day) -> + {{Year, Month, Day}, {hour(Hour, []), Min, Sec}}; +parse([Month,Day,Year,Hour,$:,Min], _Now, _Opts) + when ?is_hinted_month(Month) andalso ?is_day(Day) -> + {{Year, Month, Day}, {hour(Hour, []), Min, 0}}; + %% Times - 21:45, 13:45:54, 13:15PM etc parse([Hour,$:,Min,$:,Sec | PAM], {Date, _Time}, _O) when ?is_meridian(PAM) -> {Date, {hour(Hour, PAM), Min, Sec}}; parse([Hour,$:,Min | PAM], {Date, _Time}, _Opts) when ?is_meridian(PAM) -> {Date, {hour(Hour, PAM), Min, 0}}; parse([Hour | PAM],{Date,_Time}, _Opts) when ?is_meridian(PAM) -> - {Date, {hour(Hour,PAM), 0, 0}}; + {Date, {hour(Hour,PAM), 0, 0}}; + +%% Dates (Any combination with word month "aug 8th, 2008", "8 aug 2008", "2008 aug 21" "2008 5 aug" ) +%% Will work because of the "Hinted month" +parse([Day,Month,Year], {_Date, Time}, _Opts) + when ?is_day(Day) andalso ?is_hinted_month(Month) andalso ?is_year(Year) -> + {{Year, Month, Day}, Time}; +parse([Month,Day,Year], {_Date, Time}, _Opts) + when ?is_day(Day) andalso ?is_hinted_month(Month) andalso ?is_year(Year) -> + {{Year, Month, Day}, Time}; +parse([Year,Day,Month], {_Date, Time}, _Opts) + when ?is_day(Day) andalso ?is_hinted_month(Month) andalso ?is_year(Year) -> + {{Year, Month, Day}, Time}; +parse([Year,Month,Day], {_Date, Time}, _Opts) + when ?is_day(Day) andalso ?is_hinted_month(Month) andalso ?is_year(Year) -> + {{Year, Month, Day}, Time}; %% Dates 23/april/1963 parse([Day,Month,Year], {_Date, Time}, _Opts) -> {{Year, Month, Day}, Time}; parse([Year,X,Month,X,Day], {_Date, Time}, _Opts) when (?is_us_sep(X) orelse ?is_world_sep(X)) - andalso Year > 31 -> + andalso ?is_year(Year) -> {{Year, Month, Day}, Time}; parse([Month,X,Day,X,Year], {_Date, Time}, _Opts) when ?is_us_sep(X) -> {{Year, Month, Day}, Time}; @@ -172,7 +235,7 @@ parse([Day,X,Month,X,Year], {_Date, Time}, _Opts) when ?is_world_sep(X) -> parse([Year,X,Month,X,Day,Hour | PAM], _Date, _Opts) when ?is_meridian(PAM) andalso (?is_us_sep(X) orelse ?is_world_sep(X)) - andalso Year > 31 -> + andalso ?is_year(Year) -> {{Year, Month, Day}, {hour(Hour, PAM), 0, 0}}; parse([Day,X,Month,X,Year,Hour | PAM], _Date, _Opts) when ?is_meridian(PAM) andalso ?is_world_sep(X) -> @@ -186,7 +249,7 @@ parse([Month,X,Day,X,Year,Hour | PAM], _Date, _Opts) parse([Year,X,Month,X,Day,Hour,$:,Min | PAM], _Date, _Opts) when ?is_meridian(PAM) andalso (?is_us_sep(X) orelse ?is_world_sep(X)) - andalso Year > 31 -> + andalso ?is_year(Year) -> {{Year, Month, Day}, {hour(Hour, PAM), Min, 0}}; parse([Day,X,Month,X,Year,Hour,$:,Min | PAM], _Date, _Opts) when ?is_meridian(PAM) andalso ?is_world_sep(X) -> @@ -199,7 +262,7 @@ parse([Month,X,Day,X,Year,Hour,$:,Min | PAM], _Date, _Opts) parse([Year,X,Month,X,Day,Hour,$:,Min,$:,Sec | PAM], _Now, _Opts) when ?is_meridian(PAM) andalso (?is_us_sep(X) orelse ?is_world_sep(X)) - andalso Year > 31 -> + andalso ?is_year(Year) -> {{Year, Month, Day}, {hour(Hour, PAM), Min, Sec}}; parse([Month,X,Day,X,Year,Hour,$:,Min,$:,Sec | PAM], _Now, _Opts) when ?is_meridian(PAM) andalso ?is_us_sep(X) -> @@ -243,32 +306,37 @@ tokenise([N1 | Rest], Acc) when ?is_num(N1) -> tokenise(Rest, [ ltoi([N1]) | Acc]); -tokenise("JANUARY"++Rest, Acc) -> tokenise(Rest, [1 | Acc]); -tokenise("JAN"++Rest, Acc) -> tokenise(Rest, [1 | Acc]); -tokenise("FEBRUARY"++Rest, Acc) -> tokenise(Rest, [2 | Acc]); -tokenise("FEB"++Rest, Acc) -> tokenise(Rest, [2 | Acc]); -tokenise("MARCH"++Rest, Acc) -> tokenise(Rest, [3 | Acc]); -tokenise("MAR"++Rest, Acc) -> tokenise(Rest, [3 | Acc]); -tokenise("APRIL"++Rest, Acc) -> tokenise(Rest, [4 | Acc]); -tokenise("APR"++Rest, Acc) -> tokenise(Rest, [4 | Acc]); -tokenise("MAY"++Rest, Acc) -> tokenise(Rest, [5 | Acc]); -tokenise("JUNE"++Rest, Acc) -> tokenise(Rest, [6 | Acc]); -tokenise("JUN"++Rest, Acc) -> tokenise(Rest, [6 | Acc]); -tokenise("JULY"++Rest, Acc) -> tokenise(Rest, [7 | Acc]); -tokenise("JUL"++Rest, Acc) -> tokenise(Rest, [7 | Acc]); -tokenise("AUGUST"++Rest, Acc) -> tokenise(Rest, [8 | Acc]); -tokenise("AUG"++Rest, Acc) -> tokenise(Rest, [8 | Acc]); -tokenise("SEPTEMBER"++Rest, Acc) -> tokenise(Rest, [9 | Acc]); -tokenise("SEPT"++Rest, Acc) -> tokenise(Rest, [9 | Acc]); -tokenise("SEP"++Rest, Acc) -> tokenise(Rest, [9 | Acc]); -tokenise("OCTOBER"++Rest, Acc) -> tokenise(Rest, [10 | Acc]); -tokenise("OCT"++Rest, Acc) -> tokenise(Rest, [10 | Acc]); -tokenise("NOVEMBER"++Rest, Acc) -> tokenise(Rest, [11 | Acc]); -tokenise("NOVEM"++Rest, Acc) -> tokenise(Rest, [11 | Acc]); -tokenise("NOV"++Rest, Acc) -> tokenise(Rest, [11 | Acc]); -tokenise("DECEMBER"++Rest, Acc) -> tokenise(Rest, [12 | Acc]); -tokenise("DECEM"++Rest, Acc) -> tokenise(Rest, [12 | Acc]); -tokenise("DEC"++Rest, Acc) -> tokenise(Rest, [12 | Acc]); + +%% Worded Months get tagged with ?MONTH_TAG to let the parser know that these +%% are unambiguously declared to be months. This was there's no confusion +%% between, for example: "Aug 12" and "12 Aug" +%% These hint tags are filtered in filter_hints/1 above. +tokenise("JANUARY"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,1} | Acc]); +tokenise("JAN"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,1} | Acc]); +tokenise("FEBRUARY"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,2} | Acc]); +tokenise("FEB"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,2} | Acc]); +tokenise("MARCH"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,3} | Acc]); +tokenise("MAR"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,3} | Acc]); +tokenise("APRIL"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,4} | Acc]); +tokenise("APR"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,4} | Acc]); +tokenise("MAY"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,5} | Acc]); +tokenise("JUNE"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,6} | Acc]); +tokenise("JUN"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,6} | Acc]); +tokenise("JULY"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,7} | Acc]); +tokenise("JUL"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,7} | Acc]); +tokenise("AUGUST"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,8} | Acc]); +tokenise("AUG"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,8} | Acc]); +tokenise("SEPTEMBER"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,9} | Acc]); +tokenise("SEPT"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,9} | Acc]); +tokenise("SEP"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,9} | Acc]); +tokenise("OCTOBER"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,10} | Acc]); +tokenise("OCT"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,10} | Acc]); +tokenise("NOVEMBER"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,11} | Acc]); +tokenise("NOVEM"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,11} | Acc]); +tokenise("NOV"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,11} | Acc]); +tokenise("DECEMBER"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,12} | Acc]); +tokenise("DECEM"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,12} | Acc]); +tokenise("DEC"++Rest, Acc) -> tokenise(Rest, [{?MONTH_TAG,12} | Acc]); tokenise([$: | Rest], Acc) -> tokenise(Rest, [ $: | Acc]); tokenise([$/ | Rest], Acc) -> tokenise(Rest, [ $/ | Acc]); @@ -317,7 +385,9 @@ tokenise("ND"++Rest, Acc) -> tokenise(Rest, Acc); tokenise("ST"++Rest, Acc) -> tokenise(Rest, Acc); tokenise("OF"++Rest, Acc) -> tokenise(Rest, Acc); tokenise("T"++Rest, Acc) -> tokenise(Rest, Acc); % 2012-12-12T12:12:12 ISO formatting. -tokenise([$. | Rest], Acc) -> tokenise(Rest, [$. | Acc]); % 2012-12-12T12:12:12.xxxx ISO formatting. +tokenise([$Z | Rest], Acc) -> tokenise(Rest, [$Z | Acc]); % 2012-12-12T12:12:12Zulu +tokenise([$. | Rest], Acc) -> tokenise(Rest, [$. | Acc]); % 2012-12-12T12:12:12.xxxx ISO formatting. +tokenise([$+| Rest], Acc) -> tokenise(Rest, [$+ | Acc]); % 2012-12-12T12:12:12.xxxx+ ISO formatting. tokenise([Else | Rest], Acc) -> tokenise(Rest, [{bad_token, Else} | Acc]). @@ -489,6 +559,10 @@ to_w(X) -> X. suffix(1) -> "st"; suffix(2) -> "nd"; suffix(3) -> "rd"; +suffix(21) -> "st"; +suffix(22) -> "nd"; +suffix(23) -> "rd"; +suffix(31) -> "st"; suffix(_) -> "th". -spec sdayd(date()) -> string(). @@ -611,6 +685,11 @@ ltoi(X) -> basic_format_test_() -> [ ?_assertEqual(format("F j, Y, g:i a",?DATE), "March 10, 2001, 5:16 pm"), + ?_assertEqual(format("F jS, Y, g:i a",?DATE), "March 10th, 2001, 5:16 pm"), + ?_assertEqual(format("F jS",{{2011,3,21},{0,0,0}}), "March 21st"), + ?_assertEqual(format("F jS",{{2011,3,22},{0,0,0}}), "March 22nd"), + ?_assertEqual(format("F jS",{{2011,3,23},{0,0,0}}), "March 23rd"), + ?_assertEqual(format("F jS",{{2011,3,31},{0,0,0}}), "March 31st"), ?_assertEqual(format("m.d.y",?DATE), "03.10.01"), ?_assertEqual(format("j, n, Y",?DATE), "10, 3, 2001"), ?_assertEqual(format("Ymd",?DATE), "20010310"), @@ -656,6 +735,64 @@ basic_parse_test_() -> parse("22 Aug 2008 6:35 PM", ?DATE)), ?_assertEqual({{2008,8,22}, {18,0,0}}, parse("22 Aug 2008 6 PM", ?DATE)), + ?_assertEqual({{2008,8,22}, {18,0,0}}, + parse("Aug 22, 2008 6 PM", ?DATE)), + ?_assertEqual({{2008,8,22}, {18,0,0}}, + parse("August 22nd, 2008 6:00 PM", ?DATE)), + ?_assertEqual({{2008,8,22}, {18,15,15}}, + parse("August 22nd 2008, 6:15:15pm", ?DATE)), + ?_assertEqual({{2008,8,22}, {18,15,15}}, + parse("August 22nd, 2008, 6:15:15pm", ?DATE)), + ?_assertEqual({{2008,8,22}, {18,15,0}}, + parse("Aug 22nd 2008, 18:15", ?DATE)), + ?_assertEqual({{2008,8,2}, {17,16,17}}, + parse("2nd of August 2008", ?DATE)), + ?_assertEqual({{2008,8,2}, {17,16,17}}, + parse("August 2nd, 2008", ?DATE)), + ?_assertEqual({{2008,8,2}, {17,16,17}}, + parse("2nd August, 2008", ?DATE)), + ?_assertEqual({{2008,8,2}, {17,16,17}}, + parse("2008 August 2nd", ?DATE)), + ?_assertEqual({{2008,8,2}, {6,0,0}}, + parse("2-Aug-2008 6 AM", ?DATE)), + ?_assertEqual({{2008,8,2}, {6,35,0}}, + parse("2-Aug-2008 6:35 AM", ?DATE)), + ?_assertEqual({{2008,8,2}, {6,35,12}}, + parse("2-Aug-2008 6:35:12 AM", ?DATE)), + ?_assertEqual({{2008,8,2}, {6,0,0}}, + parse("August/2/2008 6 AM", ?DATE)), + ?_assertEqual({{2008,8,2}, {6,35,0}}, + parse("August/2/2008 6:35 AM", ?DATE)), + ?_assertEqual({{2008,8,2}, {6,35,0}}, + parse("2 August 2008 6:35 AM", ?DATE)), + ?_assertEqual({{2008,8,2}, {6,0,0}}, + parse("2 Aug 2008 6AM", ?DATE)), + ?_assertEqual({{2008,8,2}, {6,35,0}}, + parse("2 Aug 2008 6:35AM", ?DATE)), + ?_assertEqual({{2008,8,2}, {6,35,0}}, + parse("2 Aug 2008 6:35 AM", ?DATE)), + ?_assertEqual({{2008,8,2}, {6,0,0}}, + parse("2 Aug 2008 6", ?DATE)), + ?_assertEqual({{2008,8,2}, {6,35,0}}, + parse("2 Aug 2008 6:35", ?DATE)), + ?_assertEqual({{2008,8,2}, {18,35,0}}, + parse("2 Aug 2008 6:35 PM", ?DATE)), + ?_assertEqual({{2008,8,2}, {18,0,0}}, + parse("2 Aug 2008 6 PM", ?DATE)), + ?_assertEqual({{2008,8,2}, {18,0,0}}, + parse("Aug 2, 2008 6 PM", ?DATE)), + ?_assertEqual({{2008,8,2}, {18,0,0}}, + parse("August 2nd, 2008 6:00 PM", ?DATE)), + ?_assertEqual({{2008,8,2}, {18,15,15}}, + parse("August 2nd 2008, 6:15:15pm", ?DATE)), + ?_assertEqual({{2008,8,2}, {18,15,15}}, + parse("August 2nd, 2008, 6:15:15pm", ?DATE)), + ?_assertEqual({{2008,8,2}, {18,15,0}}, + parse("Aug 2nd 2008, 18:15", ?DATE)), + ?_assertEqual({{2012,12,10}, {0,0,0}}, + parse("Dec 10th, 2012, 12:00 AM", ?DATE)), + ?_assertEqual({{2012,12,10}, {0,0,0}}, + parse("10 Dec 2012 12:00 AM", ?DATE)), ?_assertEqual({{2001,3,10}, {11,15,0}}, parse("11:15", ?DATE)), ?_assertEqual({{2001,3,10}, {1,15,0}}, @@ -778,3 +915,19 @@ ms_test_() -> "2001-03-10T15:16:17.123456"), ?_assertEqual(Now, nparse(format("Y-m-d\\TH:i:s.f", Now))) ]. + +zulu_test_() -> + [ + ?_assertEqual(format("Y-m-d\\TH:i:sZ",nparse("2001-03-10T15:16:17.123456")), + "2001-03-10T15:16:17Z"), + ?_assertEqual(format("Y-m-d\\TH:i:s",nparse("2001-03-10T15:16:17Z")), + "2001-03-10T15:16:17"), + ?_assertEqual(format("Y-m-d\\TH:i:s",nparse("2001-03-10T15:16:17+04")), + "2001-03-10T11:16:17"), + ?_assertEqual(format("Y-m-d\\TH:i:s",nparse("2001-03-10T15:16:17+04:00")), + "2001-03-10T11:16:17"), + ?_assertEqual(format("Y-m-d\\TH:i:s",nparse("2001-03-10T15:16:17-04")), + "2001-03-10T19:16:17"), + ?_assertEqual(format("Y-m-d\\TH:i:s",nparse("2001-03-10T15:16:17-04:00")), + "2001-03-10T19:16:17") + ]. diff --git a/src/ec_file.erl b/src/ec_file.erl index 45adb2a..5e5d353 100644 --- a/src/ec_file.erl +++ b/src/ec_file.erl @@ -15,6 +15,8 @@ mkdir_p/1, find/2, is_symlink/1, + type/1, + real_dir_path/1, remove/1, remove/2, md5sum/1, @@ -115,7 +117,31 @@ is_symlink(Path) -> _ -> false end. - +%% @doc returns the type of the file. +-spec type(file:name()) -> file | symlink | directory. +type(Path) -> + case filelib:is_regular(Path) of + true -> + file; + false -> + case is_symlink(Path) of + true -> + symlink; + false -> + directory + end + end. +%% @doc gets the real path of a directory. This is mostly useful for +%% resolving symlinks. Be aware that this temporarily changes the +%% current working directory to figure out what the actual path +%% is. That means that it can be quite slow. +-spec real_dir_path(file:name()) -> file:name(). +real_dir_path(Path) -> + {ok, CurCwd} = file:get_cwd(), + ok = file:set_cwd(Path), + {ok, RealPath} = file:get_cwd(), + ok = file:set_cwd(CurCwd), + filename:absname(RealPath). %% @doc make a unique temorory directory. Similar function to BSD stdlib %% function of the same name. @@ -334,6 +360,21 @@ exists_test() -> ?assertMatch(true, exists(Name1)), ?assertMatch(false, exists(NoName)). +real_path_test() -> + BaseDir = "foo", + Dir = filename:absname(filename:join(BaseDir, "source1")), + LinkDir = filename:join([BaseDir, "link"]), + ok = mkdir_p(Dir), + file:make_symlink(Dir, LinkDir), + ?assertEqual(Dir, real_dir_path(LinkDir)), + ?assertEqual(directory, type(Dir)), + ?assertEqual(symlink, type(LinkDir)), + TermFile = filename:join(BaseDir, "test_file"), + ok = write_term(TermFile, foo), + ?assertEqual(file, type(TermFile)), + ?assertEqual(true, is_symlink(LinkDir)), + ?assertEqual(false, is_symlink(Dir)). + find_test() -> %% Create a directory in /tmp for the test. Clean everything afterwards {BaseDir, _SourceDir, {Name1, Name2, Name3, _NoName}} = setup_base_and_target(),