blob: e7b9b59a376e8d6d10318b82f16c52ff2d6ac7f9 [file] [log] [blame]
%% @author Bob Ippolito <bob@mochimedia.com>
%% @copyright 2008 Mochi Media, Inc.
%%
%% Permission is hereby granted, free of charge, to any person obtaining a
%% copy of this software and associated documentation files (the "Software"),
%% to deal in the Software without restriction, including without limitation
%% the rights to use, copy, modify, merge, publish, distribute, sublicense,
%% and/or sell copies of the Software, and to permit persons to whom the
%% Software is furnished to do so, subject to the following conditions:
%%
%% The above copyright notice and this permission notice shall be included in
%% all copies or substantial portions of the Software.
%%
%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
%% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
%% DEALINGS IN THE SOFTWARE.
%% @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([convert_field/2, format/2, format_field/2,
get_field/2, get_value/2]).
-export([format/3, format_field/3, get_field/3,
tokenize/1]).
-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 = mod_get_value(Name, Args, Module),
case Next of
"" -> Res;
"." ++ S1 -> get_field(S1, Res, Module)
end.
mod_get_value(Name, Args, Module) ->
try tuple_apply(Module, get_value, [Name, Args]) catch
error:undef -> get_value(Name, Args)
end.
tuple_apply(Module, F, Args) when is_atom(Module) ->
erlang:apply(Module, F, Args);
tuple_apply(Module, F, Args)
when is_tuple(Module), is_atom(element(1, Module)) ->
erlang:apply(element(1, Module), F, Args ++ [Module]).
%% @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 tuple_apply(Module, get_field, [Key, Args])
catch
error:undef -> get_field(Key, Args, Module)
end,
V1 = try tuple_apply(Module, convert_field,
[V0, Convert])
catch
error:undef -> convert_field(V0, Convert)
end,
try tuple_apply(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(1.0e+2 * 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 < 128 ->
[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.19999999999999995559, <<"F">>}),
<<"cafebabe">> = bformat("{0:x}", {3405691582}),
<<"CAFEBABE">> = bformat("{0:X}", {3405691582}),
<<"CAFEBABE">> = bformat("{0:X}", {3405691582}),
<<"755">> = bformat("{0:o}", {493}),
<<"a">> = bformat("{0:c}", {97}),
%% Horizontal ellipsis
<<226, 128, 166>> = bformat("{0:c}", {8230}),
<<"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 = mochifmt_records:get_value("length", R, M),
hard = mochifmt_records:get_value("precision", R, M),
peace = mochifmt_records:get_value("sign", R, M),
<<"long hard">> = bformat("{length} {precision}", R, M),
<<"long hard">> = bformat("{0.length} {0.precision}",
[R], M),
ok.
-endif.