| %% @author Bob Ippolito <bob@mochimedia.com> |
| %% @copyright 2008 Mochi Media, Inc. |
| |
| %% @doc String Formatting for Erlang, inspired by Python 2.6 |
| %% (<a href="http://www.python.org/dev/peps/pep-3101/">PEP 3101</a>). |
| %% |
| -module(mochifmt). |
| -author('bob@mochimedia.com'). |
| -export([format/2, format_field/2, convert_field/2, get_value/2, get_field/2]). |
| -export([tokenize/1, format/3, get_field/3, format_field/3]). |
| -export([bformat/2, bformat/3]). |
| -export([f/2, f/3]). |
| |
| -record(conversion, {length, precision, ctype, align, fill_char, sign}). |
| |
| %% @spec tokenize(S::string()) -> tokens() |
| %% @doc Tokenize a format string into mochifmt's internal format. |
| tokenize(S) -> |
| {?MODULE, tokenize(S, "", [])}. |
| |
| %% @spec convert_field(Arg, Conversion::conversion()) -> term() |
| %% @doc Process Arg according to the given explicit conversion specifier. |
| convert_field(Arg, "") -> |
| Arg; |
| convert_field(Arg, "r") -> |
| repr(Arg); |
| convert_field(Arg, "s") -> |
| str(Arg). |
| |
| %% @spec get_value(Key::string(), Args::args()) -> term() |
| %% @doc Get the Key from Args. If Args is a tuple then convert Key to |
| %% an integer and get element(1 + Key, Args). If Args is a list and Key |
| %% can be parsed as an integer then use lists:nth(1 + Key, Args), |
| %% otherwise try and look for Key in Args as a proplist, converting |
| %% Key to an atom or binary if necessary. |
| get_value(Key, Args) when is_tuple(Args) -> |
| element(1 + list_to_integer(Key), Args); |
| get_value(Key, Args) when is_list(Args) -> |
| try lists:nth(1 + list_to_integer(Key), Args) |
| catch error:_ -> |
| {_K, V} = proplist_lookup(Key, Args), |
| V |
| end. |
| |
| %% @spec get_field(Key::string(), Args) -> term() |
| %% @doc Consecutively call get_value/2 on parts of Key delimited by ".", |
| %% replacing Args with the result of the previous get_value. This |
| %% is used to implement formats such as {0.0}. |
| get_field(Key, Args) -> |
| get_field(Key, Args, ?MODULE). |
| |
| %% @spec get_field(Key::string(), Args, Module) -> term() |
| %% @doc Consecutively call Module:get_value/2 on parts of Key delimited by ".", |
| %% replacing Args with the result of the previous get_value. This |
| %% is used to implement formats such as {0.0}. |
| get_field(Key, Args, Module) -> |
| {Name, Next} = lists:splitwith(fun (C) -> C =/= $. end, Key), |
| Res = try Module:get_value(Name, Args) |
| catch error:undef -> get_value(Name, Args) end, |
| case Next of |
| "" -> |
| Res; |
| "." ++ S1 -> |
| get_field(S1, Res, Module) |
| end. |
| |
| %% @spec format(Format::string(), Args) -> iolist() |
| %% @doc Format Args with Format. |
| format(Format, Args) -> |
| format(Format, Args, ?MODULE). |
| |
| %% @spec format(Format::string(), Args, Module) -> iolist() |
| %% @doc Format Args with Format using Module. |
| format({?MODULE, Parts}, Args, Module) -> |
| format2(Parts, Args, Module, []); |
| format(S, Args, Module) -> |
| format(tokenize(S), Args, Module). |
| |
| %% @spec format_field(Arg, Format) -> iolist() |
| %% @doc Format Arg with Format. |
| format_field(Arg, Format) -> |
| format_field(Arg, Format, ?MODULE). |
| |
| %% @spec format_field(Arg, Format, _Module) -> iolist() |
| %% @doc Format Arg with Format. |
| format_field(Arg, Format, _Module) -> |
| F = default_ctype(Arg, parse_std_conversion(Format)), |
| fix_padding(fix_sign(convert2(Arg, F), F), F). |
| |
| %% @spec f(Format::string(), Args) -> string() |
| %% @doc Format Args with Format and return a string(). |
| f(Format, Args) -> |
| f(Format, Args, ?MODULE). |
| |
| %% @spec f(Format::string(), Args, Module) -> string() |
| %% @doc Format Args with Format using Module and return a string(). |
| f(Format, Args, Module) -> |
| case lists:member(${, Format) of |
| true -> |
| binary_to_list(bformat(Format, Args, Module)); |
| false -> |
| Format |
| end. |
| |
| %% @spec bformat(Format::string(), Args) -> binary() |
| %% @doc Format Args with Format and return a binary(). |
| bformat(Format, Args) -> |
| iolist_to_binary(format(Format, Args)). |
| |
| %% @spec bformat(Format::string(), Args, Module) -> binary() |
| %% @doc Format Args with Format using Module and return a binary(). |
| bformat(Format, Args, Module) -> |
| iolist_to_binary(format(Format, Args, Module)). |
| |
| %% Internal API |
| |
| add_raw("", Acc) -> |
| Acc; |
| add_raw(S, Acc) -> |
| [{raw, lists:reverse(S)} | Acc]. |
| |
| tokenize([], S, Acc) -> |
| lists:reverse(add_raw(S, Acc)); |
| tokenize("{{" ++ Rest, S, Acc) -> |
| tokenize(Rest, "{" ++ S, Acc); |
| tokenize("{" ++ Rest, S, Acc) -> |
| {Format, Rest1} = tokenize_format(Rest), |
| tokenize(Rest1, "", [{format, make_format(Format)} | add_raw(S, Acc)]); |
| tokenize("}}" ++ Rest, S, Acc) -> |
| tokenize(Rest, "}" ++ S, Acc); |
| tokenize([C | Rest], S, Acc) -> |
| tokenize(Rest, [C | S], Acc). |
| |
| tokenize_format(S) -> |
| tokenize_format(S, 1, []). |
| |
| tokenize_format("}" ++ Rest, 1, Acc) -> |
| {lists:reverse(Acc), Rest}; |
| tokenize_format("}" ++ Rest, N, Acc) -> |
| tokenize_format(Rest, N - 1, "}" ++ Acc); |
| tokenize_format("{" ++ Rest, N, Acc) -> |
| tokenize_format(Rest, 1 + N, "{" ++ Acc); |
| tokenize_format([C | Rest], N, Acc) -> |
| tokenize_format(Rest, N, [C | Acc]). |
| |
| make_format(S) -> |
| {Name0, Spec} = case lists:splitwith(fun (C) -> C =/= $: end, S) of |
| {_, ""} -> |
| {S, ""}; |
| {SN, ":" ++ SS} -> |
| {SN, SS} |
| end, |
| {Name, Transform} = case lists:splitwith(fun (C) -> C =/= $! end, Name0) of |
| {_, ""} -> |
| {Name0, ""}; |
| {TN, "!" ++ TT} -> |
| {TN, TT} |
| end, |
| {Name, Transform, Spec}. |
| |
| proplist_lookup(S, P) -> |
| A = try list_to_existing_atom(S) |
| catch error:_ -> make_ref() end, |
| B = try list_to_binary(S) |
| catch error:_ -> make_ref() end, |
| proplist_lookup2({S, A, B}, P). |
| |
| proplist_lookup2({KS, KA, KB}, [{K, V} | _]) |
| when KS =:= K orelse KA =:= K orelse KB =:= K -> |
| {K, V}; |
| proplist_lookup2(Keys, [_ | Rest]) -> |
| proplist_lookup2(Keys, Rest). |
| |
| format2([], _Args, _Module, Acc) -> |
| lists:reverse(Acc); |
| format2([{raw, S} | Rest], Args, Module, Acc) -> |
| format2(Rest, Args, Module, [S | Acc]); |
| format2([{format, {Key, Convert, Format0}} | Rest], Args, Module, Acc) -> |
| Format = f(Format0, Args, Module), |
| V = case Module of |
| ?MODULE -> |
| V0 = get_field(Key, Args), |
| V1 = convert_field(V0, Convert), |
| format_field(V1, Format); |
| _ -> |
| V0 = try Module:get_field(Key, Args) |
| catch error:undef -> get_field(Key, Args, Module) end, |
| V1 = try Module:convert_field(V0, Convert) |
| catch error:undef -> convert_field(V0, Convert) end, |
| try Module:format_field(V1, Format) |
| catch error:undef -> format_field(V1, Format, Module) end |
| end, |
| format2(Rest, Args, Module, [V | Acc]). |
| |
| default_ctype(_Arg, C=#conversion{ctype=N}) when N =/= undefined -> |
| C; |
| default_ctype(Arg, C) when is_integer(Arg) -> |
| C#conversion{ctype=decimal}; |
| default_ctype(Arg, C) when is_float(Arg) -> |
| C#conversion{ctype=general}; |
| default_ctype(_Arg, C) -> |
| C#conversion{ctype=string}. |
| |
| fix_padding(Arg, #conversion{length=undefined}) -> |
| Arg; |
| fix_padding(Arg, F=#conversion{length=Length, fill_char=Fill0, align=Align0, |
| ctype=Type}) -> |
| Padding = Length - iolist_size(Arg), |
| Fill = case Fill0 of |
| undefined -> |
| $\s; |
| _ -> |
| Fill0 |
| end, |
| Align = case Align0 of |
| undefined -> |
| case Type of |
| string -> |
| left; |
| _ -> |
| right |
| end; |
| _ -> |
| Align0 |
| end, |
| case Padding > 0 of |
| true -> |
| do_padding(Arg, Padding, Fill, Align, F); |
| false -> |
| Arg |
| end. |
| |
| do_padding(Arg, Padding, Fill, right, _F) -> |
| [lists:duplicate(Padding, Fill), Arg]; |
| do_padding(Arg, Padding, Fill, center, _F) -> |
| LPadding = lists:duplicate(Padding div 2, Fill), |
| RPadding = case Padding band 1 of |
| 1 -> |
| [Fill | LPadding]; |
| _ -> |
| LPadding |
| end, |
| [LPadding, Arg, RPadding]; |
| do_padding([$- | Arg], Padding, Fill, sign_right, _F) -> |
| [[$- | lists:duplicate(Padding, Fill)], Arg]; |
| do_padding(Arg, Padding, Fill, sign_right, #conversion{sign=$-}) -> |
| [lists:duplicate(Padding, Fill), Arg]; |
| do_padding([S | Arg], Padding, Fill, sign_right, #conversion{sign=S}) -> |
| [[S | lists:duplicate(Padding, Fill)], Arg]; |
| do_padding(Arg, Padding, Fill, sign_right, #conversion{sign=undefined}) -> |
| [lists:duplicate(Padding, Fill), Arg]; |
| do_padding(Arg, Padding, Fill, left, _F) -> |
| [Arg | lists:duplicate(Padding, Fill)]. |
| |
| fix_sign(Arg, #conversion{sign=$+}) when Arg >= 0 -> |
| [$+, Arg]; |
| fix_sign(Arg, #conversion{sign=$\s}) when Arg >= 0 -> |
| [$\s, Arg]; |
| fix_sign(Arg, _F) -> |
| Arg. |
| |
| ctype($\%) -> percent; |
| ctype($s) -> string; |
| ctype($b) -> bin; |
| ctype($o) -> oct; |
| ctype($X) -> upper_hex; |
| ctype($x) -> hex; |
| ctype($c) -> char; |
| ctype($d) -> decimal; |
| ctype($g) -> general; |
| ctype($f) -> fixed; |
| ctype($e) -> exp. |
| |
| align($<) -> left; |
| align($>) -> right; |
| align($^) -> center; |
| align($=) -> sign_right. |
| |
| convert2(Arg, F=#conversion{ctype=percent}) -> |
| [convert2(100.0 * Arg, F#conversion{ctype=fixed}), $\%]; |
| convert2(Arg, #conversion{ctype=string}) -> |
| str(Arg); |
| convert2(Arg, #conversion{ctype=bin}) -> |
| erlang:integer_to_list(Arg, 2); |
| convert2(Arg, #conversion{ctype=oct}) -> |
| erlang:integer_to_list(Arg, 8); |
| convert2(Arg, #conversion{ctype=upper_hex}) -> |
| erlang:integer_to_list(Arg, 16); |
| convert2(Arg, #conversion{ctype=hex}) -> |
| string:to_lower(erlang:integer_to_list(Arg, 16)); |
| convert2(Arg, #conversion{ctype=char}) when Arg < 16#80 -> |
| [Arg]; |
| convert2(Arg, #conversion{ctype=char}) -> |
| xmerl_ucs:to_utf8(Arg); |
| convert2(Arg, #conversion{ctype=decimal}) -> |
| integer_to_list(Arg); |
| convert2(Arg, #conversion{ctype=general, precision=undefined}) -> |
| try mochinum:digits(Arg) |
| catch error:undef -> io_lib:format("~g", [Arg]) end; |
| convert2(Arg, #conversion{ctype=fixed, precision=undefined}) -> |
| io_lib:format("~f", [Arg]); |
| convert2(Arg, #conversion{ctype=exp, precision=undefined}) -> |
| io_lib:format("~e", [Arg]); |
| convert2(Arg, #conversion{ctype=general, precision=P}) -> |
| io_lib:format("~." ++ integer_to_list(P) ++ "g", [Arg]); |
| convert2(Arg, #conversion{ctype=fixed, precision=P}) -> |
| io_lib:format("~." ++ integer_to_list(P) ++ "f", [Arg]); |
| convert2(Arg, #conversion{ctype=exp, precision=P}) -> |
| io_lib:format("~." ++ integer_to_list(P) ++ "e", [Arg]). |
| |
| str(A) when is_atom(A) -> |
| atom_to_list(A); |
| str(I) when is_integer(I) -> |
| integer_to_list(I); |
| str(F) when is_float(F) -> |
| try mochinum:digits(F) |
| catch error:undef -> io_lib:format("~g", [F]) end; |
| str(L) when is_list(L) -> |
| L; |
| str(B) when is_binary(B) -> |
| B; |
| str(P) -> |
| repr(P). |
| |
| repr(P) when is_float(P) -> |
| try mochinum:digits(P) |
| catch error:undef -> float_to_list(P) end; |
| repr(P) -> |
| io_lib:format("~p", [P]). |
| |
| parse_std_conversion(S) -> |
| parse_std_conversion(S, #conversion{}). |
| |
| parse_std_conversion("", Acc) -> |
| Acc; |
| parse_std_conversion([Fill, Align | Spec], Acc) |
| when Align =:= $< orelse Align =:= $> orelse Align =:= $= orelse Align =:= $^ -> |
| parse_std_conversion(Spec, Acc#conversion{fill_char=Fill, |
| align=align(Align)}); |
| parse_std_conversion([Align | Spec], Acc) |
| when Align =:= $< orelse Align =:= $> orelse Align =:= $= orelse Align =:= $^ -> |
| parse_std_conversion(Spec, Acc#conversion{align=align(Align)}); |
| parse_std_conversion([Sign | Spec], Acc) |
| when Sign =:= $+ orelse Sign =:= $- orelse Sign =:= $\s -> |
| parse_std_conversion(Spec, Acc#conversion{sign=Sign}); |
| parse_std_conversion("0" ++ Spec, Acc) -> |
| Align = case Acc#conversion.align of |
| undefined -> |
| sign_right; |
| A -> |
| A |
| end, |
| parse_std_conversion(Spec, Acc#conversion{fill_char=$0, align=Align}); |
| parse_std_conversion(Spec=[D|_], Acc) when D >= $0 andalso D =< $9 -> |
| {W, Spec1} = lists:splitwith(fun (C) -> C >= $0 andalso C =< $9 end, Spec), |
| parse_std_conversion(Spec1, Acc#conversion{length=list_to_integer(W)}); |
| parse_std_conversion([$. | Spec], Acc) -> |
| case lists:splitwith(fun (C) -> C >= $0 andalso C =< $9 end, Spec) of |
| {"", Spec1} -> |
| parse_std_conversion(Spec1, Acc); |
| {P, Spec1} -> |
| parse_std_conversion(Spec1, |
| Acc#conversion{precision=list_to_integer(P)}) |
| end; |
| parse_std_conversion([Type], Acc) -> |
| parse_std_conversion("", Acc#conversion{ctype=ctype(Type)}). |
| |
| |
| %% |
| %% Tests |
| %% |
| -ifdef(TEST). |
| -include_lib("eunit/include/eunit.hrl"). |
| |
| tokenize_test() -> |
| {?MODULE, [{raw, "ABC"}]} = tokenize("ABC"), |
| {?MODULE, [{format, {"0", "", ""}}]} = tokenize("{0}"), |
| {?MODULE, [{raw, "ABC"}, {format, {"1", "", ""}}, {raw, "DEF"}]} = |
| tokenize("ABC{1}DEF"), |
| ok. |
| |
| format_test() -> |
| <<" -4">> = bformat("{0:4}", [-4]), |
| <<" 4">> = bformat("{0:4}", [4]), |
| <<" 4">> = bformat("{0:{0}}", [4]), |
| <<"4 ">> = bformat("{0:4}", ["4"]), |
| <<"4 ">> = bformat("{0:{0}}", ["4"]), |
| <<"1.2yoDEF">> = bformat("{2}{0}{1}{3}", {yo, "DE", 1.2, <<"F">>}), |
| <<"cafebabe">> = bformat("{0:x}", {16#cafebabe}), |
| <<"CAFEBABE">> = bformat("{0:X}", {16#cafebabe}), |
| <<"CAFEBABE">> = bformat("{0:X}", {16#cafebabe}), |
| <<"755">> = bformat("{0:o}", {8#755}), |
| <<"a">> = bformat("{0:c}", {97}), |
| %% Horizontal ellipsis |
| <<226, 128, 166>> = bformat("{0:c}", {16#2026}), |
| <<"11">> = bformat("{0:b}", {3}), |
| <<"11">> = bformat("{0:b}", [3]), |
| <<"11">> = bformat("{three:b}", [{three, 3}]), |
| <<"11">> = bformat("{three:b}", [{"three", 3}]), |
| <<"11">> = bformat("{three:b}", [{<<"three">>, 3}]), |
| <<"\"foo\"">> = bformat("{0!r}", {"foo"}), |
| <<"2008-5-4">> = bformat("{0.0}-{0.1}-{0.2}", {{2008,5,4}}), |
| <<"2008-05-04">> = bformat("{0.0:04}-{0.1:02}-{0.2:02}", {{2008,5,4}}), |
| <<"foo6bar-6">> = bformat("foo{1}{0}-{1}", {bar, 6}), |
| <<"-'atom test'-">> = bformat("-{arg!r}-", [{arg, 'atom test'}]), |
| <<"2008-05-04">> = bformat("{0.0:0{1.0}}-{0.1:0{1.1}}-{0.2:0{1.2}}", |
| {{2008,5,4}, {4, 2, 2}}), |
| ok. |
| |
| std_test() -> |
| M = mochifmt_std:new(), |
| <<"01">> = bformat("{0}{1}", [0, 1], M), |
| ok. |
| |
| records_test() -> |
| M = mochifmt_records:new([{conversion, record_info(fields, conversion)}]), |
| R = #conversion{length=long, precision=hard, sign=peace}, |
| long = M:get_value("length", R), |
| hard = M:get_value("precision", R), |
| peace = M:get_value("sign", R), |
| <<"long hard">> = bformat("{length} {precision}", R, M), |
| <<"long hard">> = bformat("{0.length} {0.precision}", [R], M), |
| ok. |
| |
| -endif. |