PageRenderTime 74ms CodeModel.GetById 14ms app.highlight 55ms RepoModel.GetById 1ms app.codeStats 0ms

/src/mochifmt.erl

http://github.com/basho/mochiweb
Erlang | 443 lines | 339 code | 42 blank | 62 comment | 0 complexity | fd1d91c2a253014abcebbee59c8bd54c MD5 | raw file
  1%% @author Bob Ippolito <bob@mochimedia.com>
  2%% @copyright 2008 Mochi Media, Inc.
  3%%
  4%% Permission is hereby granted, free of charge, to any person obtaining a
  5%% copy of this software and associated documentation files (the "Software"),
  6%% to deal in the Software without restriction, including without limitation
  7%% the rights to use, copy, modify, merge, publish, distribute, sublicense,
  8%% and/or sell copies of the Software, and to permit persons to whom the
  9%% Software is furnished to do so, subject to the following conditions:
 10%%
 11%% The above copyright notice and this permission notice shall be included in
 12%% all copies or substantial portions of the Software.
 13%%
 14%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 15%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 16%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 17%% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 18%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 19%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 20%% DEALINGS IN THE SOFTWARE.
 21
 22%% @doc String Formatting for Erlang, inspired by Python 2.6
 23%%      (<a href="http://www.python.org/dev/peps/pep-3101/">PEP 3101</a>).
 24%%
 25-module(mochifmt).
 26-author('bob@mochimedia.com').
 27-export([format/2, format_field/2, convert_field/2, get_value/2, get_field/2]).
 28-export([tokenize/1, format/3, get_field/3, format_field/3]).
 29-export([bformat/2, bformat/3]).
 30-export([f/2, f/3]).
 31
 32-record(conversion, {length, precision, ctype, align, fill_char, sign}).
 33
 34%% @spec tokenize(S::string()) -> tokens()
 35%% @doc Tokenize a format string into mochifmt's internal format.
 36tokenize(S) ->
 37    {?MODULE, tokenize(S, "", [])}.
 38
 39%% @spec convert_field(Arg, Conversion::conversion()) -> term()
 40%% @doc Process Arg according to the given explicit conversion specifier.
 41convert_field(Arg, "") ->
 42    Arg;
 43convert_field(Arg, "r") ->
 44    repr(Arg);
 45convert_field(Arg, "s") ->
 46    str(Arg).
 47
 48%% @spec get_value(Key::string(), Args::args()) -> term()
 49%% @doc Get the Key from Args. If Args is a tuple then convert Key to
 50%%      an integer and get element(1 + Key, Args). If Args is a list and Key
 51%%      can be parsed as an integer then use lists:nth(1 + Key, Args),
 52%%      otherwise try and look for Key in Args as a proplist, converting
 53%%      Key to an atom or binary if necessary.
 54get_value(Key, Args) when is_tuple(Args) ->
 55    element(1 + list_to_integer(Key), Args);
 56get_value(Key, Args) when is_list(Args) ->
 57    try lists:nth(1 + list_to_integer(Key), Args)
 58    catch error:_ ->
 59            {_K, V} = proplist_lookup(Key, Args),
 60            V
 61    end.
 62
 63%% @spec get_field(Key::string(), Args) -> term()
 64%% @doc Consecutively call get_value/2 on parts of Key delimited by ".",
 65%%      replacing Args with the result of the previous get_value. This
 66%%      is used to implement formats such as {0.0}.
 67get_field(Key, Args) ->
 68    get_field(Key, Args, ?MODULE).
 69
 70%% @spec get_field(Key::string(), Args, Module) -> term()
 71%% @doc Consecutively call Module:get_value/2 on parts of Key delimited by ".",
 72%%      replacing Args with the result of the previous get_value. This
 73%%      is used to implement formats such as {0.0}.
 74get_field(Key, Args, Module) ->
 75    {Name, Next} = lists:splitwith(fun (C) -> C =/= $. end, Key),
 76    Res = try Module:get_value(Name, Args)
 77          catch error:undef -> get_value(Name, Args) end,
 78    case Next of
 79        "" ->
 80            Res;
 81        "." ++ S1 ->
 82            get_field(S1, Res, Module)
 83    end.
 84
 85%% @spec format(Format::string(), Args) -> iolist()
 86%% @doc Format Args with Format.
 87format(Format, Args) ->
 88    format(Format, Args, ?MODULE).
 89
 90%% @spec format(Format::string(), Args, Module) -> iolist()
 91%% @doc Format Args with Format using Module.
 92format({?MODULE, Parts}, Args, Module) ->
 93    format2(Parts, Args, Module, []);
 94format(S, Args, Module) ->
 95    format(tokenize(S), Args, Module).
 96
 97%% @spec format_field(Arg, Format) -> iolist()
 98%% @doc Format Arg with Format.
 99format_field(Arg, Format) ->
100    format_field(Arg, Format, ?MODULE).
101
102%% @spec format_field(Arg, Format, _Module) -> iolist()
103%% @doc Format Arg with Format.
104format_field(Arg, Format, _Module) ->
105    F = default_ctype(Arg, parse_std_conversion(Format)),
106    fix_padding(fix_sign(convert2(Arg, F), F), F).
107
108%% @spec f(Format::string(), Args) -> string()
109%% @doc Format Args with Format and return a string().
110f(Format, Args) ->
111    f(Format, Args, ?MODULE).
112
113%% @spec f(Format::string(), Args, Module) -> string()
114%% @doc Format Args with Format using Module and return a string().
115f(Format, Args, Module) ->
116    case lists:member(${, Format) of
117        true ->
118            binary_to_list(bformat(Format, Args, Module));
119        false ->
120            Format
121    end.
122
123%% @spec bformat(Format::string(), Args) -> binary()
124%% @doc Format Args with Format and return a binary().
125bformat(Format, Args) ->
126    iolist_to_binary(format(Format, Args)).
127
128%% @spec bformat(Format::string(), Args, Module) -> binary()
129%% @doc Format Args with Format using Module and return a binary().
130bformat(Format, Args, Module) ->
131    iolist_to_binary(format(Format, Args, Module)).
132
133%% Internal API
134
135add_raw("", Acc) ->
136    Acc;
137add_raw(S, Acc) ->
138    [{raw, lists:reverse(S)} | Acc].
139
140tokenize([], S, Acc) ->
141    lists:reverse(add_raw(S, Acc));
142tokenize("{{" ++ Rest, S, Acc) ->
143    tokenize(Rest, "{" ++ S, Acc);
144tokenize("{" ++ Rest, S, Acc) ->
145    {Format, Rest1} = tokenize_format(Rest),
146    tokenize(Rest1, "", [{format, make_format(Format)} | add_raw(S, Acc)]);
147tokenize("}}" ++ Rest, S, Acc) ->
148    tokenize(Rest, "}" ++ S, Acc);
149tokenize([C | Rest], S, Acc) ->
150    tokenize(Rest, [C | S], Acc).
151
152tokenize_format(S) ->
153    tokenize_format(S, 1, []).
154
155tokenize_format("}" ++ Rest, 1, Acc) ->
156    {lists:reverse(Acc), Rest};
157tokenize_format("}" ++ Rest, N, Acc) ->
158    tokenize_format(Rest, N - 1, "}" ++ Acc);
159tokenize_format("{" ++ Rest, N, Acc) ->
160    tokenize_format(Rest, 1 + N, "{" ++ Acc);
161tokenize_format([C | Rest], N, Acc) ->
162    tokenize_format(Rest, N, [C | Acc]).
163
164make_format(S) ->
165    {Name0, Spec} = case lists:splitwith(fun (C) -> C =/= $: end, S) of
166                        {_, ""} ->
167                            {S, ""};
168                        {SN, ":" ++ SS} ->
169                            {SN, SS}
170                    end,
171    {Name, Transform} = case lists:splitwith(fun (C) -> C =/= $! end, Name0) of
172                            {_, ""} ->
173                                {Name0, ""};
174                            {TN, "!" ++ TT} ->
175                                {TN, TT}
176                        end,
177    {Name, Transform, Spec}.
178
179proplist_lookup(S, P) ->
180    A = try list_to_existing_atom(S)
181        catch error:_ -> make_ref() end,
182    B = try list_to_binary(S)
183        catch error:_ -> make_ref() end,
184    proplist_lookup2({S, A, B}, P).
185
186proplist_lookup2({KS, KA, KB}, [{K, V} | _])
187  when KS =:= K orelse KA =:= K orelse KB =:= K ->
188    {K, V};
189proplist_lookup2(Keys, [_ | Rest]) ->
190    proplist_lookup2(Keys, Rest).
191
192format2([], _Args, _Module, Acc) ->
193    lists:reverse(Acc);
194format2([{raw, S} | Rest], Args, Module, Acc) ->
195    format2(Rest, Args, Module, [S | Acc]);
196format2([{format, {Key, Convert, Format0}} | Rest], Args, Module, Acc) ->
197    Format = f(Format0, Args, Module),
198    V = case Module of
199            ?MODULE ->
200                V0 = get_field(Key, Args),
201                V1 = convert_field(V0, Convert),
202                format_field(V1, Format);
203            _ ->
204                V0 = try Module:get_field(Key, Args)
205                     catch error:undef -> get_field(Key, Args, Module) end,
206                V1 = try Module:convert_field(V0, Convert)
207                     catch error:undef -> convert_field(V0, Convert) end,
208                try Module:format_field(V1, Format)
209                catch error:undef -> format_field(V1, Format, Module) end
210        end,
211    format2(Rest, Args, Module, [V | Acc]).
212
213default_ctype(_Arg, C=#conversion{ctype=N}) when N =/= undefined ->
214    C;
215default_ctype(Arg, C) when is_integer(Arg) ->
216    C#conversion{ctype=decimal};
217default_ctype(Arg, C) when is_float(Arg) ->
218    C#conversion{ctype=general};
219default_ctype(_Arg, C) ->
220    C#conversion{ctype=string}.
221
222fix_padding(Arg, #conversion{length=undefined}) ->
223    Arg;
224fix_padding(Arg, F=#conversion{length=Length, fill_char=Fill0, align=Align0,
225                               ctype=Type}) ->
226    Padding = Length - iolist_size(Arg),
227    Fill = case Fill0 of
228               undefined ->
229                   $\s;
230               _ ->
231                   Fill0
232           end,
233    Align = case Align0 of
234                undefined ->
235                    case Type of
236                        string ->
237                            left;
238                        _ ->
239                            right
240                    end;
241                _ ->
242                    Align0
243            end,
244    case Padding > 0 of
245        true ->
246            do_padding(Arg, Padding, Fill, Align, F);
247        false ->
248            Arg
249    end.
250
251do_padding(Arg, Padding, Fill, right, _F) ->
252    [lists:duplicate(Padding, Fill), Arg];
253do_padding(Arg, Padding, Fill, center, _F) ->
254    LPadding = lists:duplicate(Padding div 2, Fill),
255    RPadding = case Padding band 1 of
256                   1 ->
257                       [Fill | LPadding];
258                   _ ->
259                       LPadding
260               end,
261    [LPadding, Arg, RPadding];
262do_padding([$- | Arg], Padding, Fill, sign_right, _F) ->
263    [[$- | lists:duplicate(Padding, Fill)], Arg];
264do_padding(Arg, Padding, Fill, sign_right, #conversion{sign=$-}) ->
265    [lists:duplicate(Padding, Fill), Arg];
266do_padding([S | Arg], Padding, Fill, sign_right, #conversion{sign=S}) ->
267    [[S | lists:duplicate(Padding, Fill)], Arg];
268do_padding(Arg, Padding, Fill, sign_right, #conversion{sign=undefined}) ->
269    [lists:duplicate(Padding, Fill), Arg];
270do_padding(Arg, Padding, Fill, left, _F) ->
271    [Arg | lists:duplicate(Padding, Fill)].
272
273fix_sign(Arg, #conversion{sign=$+}) when Arg >= 0 ->
274    [$+, Arg];
275fix_sign(Arg, #conversion{sign=$\s}) when Arg >= 0 ->
276    [$\s, Arg];
277fix_sign(Arg, _F) ->
278    Arg.
279
280ctype($\%) -> percent;
281ctype($s) -> string;
282ctype($b) -> bin;
283ctype($o) -> oct;
284ctype($X) -> upper_hex;
285ctype($x) -> hex;
286ctype($c) -> char;
287ctype($d) -> decimal;
288ctype($g) -> general;
289ctype($f) -> fixed;
290ctype($e) -> exp.
291
292align($<) -> left;
293align($>) -> right;
294align($^) -> center;
295align($=) -> sign_right.
296
297convert2(Arg, F=#conversion{ctype=percent}) ->
298    [convert2(100.0 * Arg, F#conversion{ctype=fixed}), $\%];
299convert2(Arg, #conversion{ctype=string}) ->
300    str(Arg);
301convert2(Arg, #conversion{ctype=bin}) ->
302    erlang:integer_to_list(Arg, 2);
303convert2(Arg, #conversion{ctype=oct}) ->
304    erlang:integer_to_list(Arg, 8);
305convert2(Arg, #conversion{ctype=upper_hex}) ->
306    erlang:integer_to_list(Arg, 16);
307convert2(Arg, #conversion{ctype=hex}) ->
308    string:to_lower(erlang:integer_to_list(Arg, 16));
309convert2(Arg, #conversion{ctype=char}) when Arg < 16#80 ->
310    [Arg];
311convert2(Arg, #conversion{ctype=char}) ->
312    xmerl_ucs:to_utf8(Arg);
313convert2(Arg, #conversion{ctype=decimal}) ->
314    integer_to_list(Arg);
315convert2(Arg, #conversion{ctype=general, precision=undefined}) ->
316    try mochinum:digits(Arg)
317    catch error:undef -> io_lib:format("~g", [Arg]) end;
318convert2(Arg, #conversion{ctype=fixed, precision=undefined}) ->
319    io_lib:format("~f", [Arg]);
320convert2(Arg, #conversion{ctype=exp, precision=undefined}) ->
321    io_lib:format("~e", [Arg]);
322convert2(Arg, #conversion{ctype=general, precision=P}) ->
323    io_lib:format("~." ++ integer_to_list(P) ++ "g", [Arg]);
324convert2(Arg, #conversion{ctype=fixed, precision=P}) ->
325    io_lib:format("~." ++ integer_to_list(P) ++ "f", [Arg]);
326convert2(Arg, #conversion{ctype=exp, precision=P}) ->
327    io_lib:format("~." ++ integer_to_list(P) ++ "e", [Arg]).
328
329str(A) when is_atom(A) ->
330    atom_to_list(A);
331str(I) when is_integer(I) ->
332    integer_to_list(I);
333str(F) when is_float(F) ->
334    try mochinum:digits(F)
335    catch error:undef -> io_lib:format("~g", [F]) end;
336str(L) when is_list(L) ->
337    L;
338str(B) when is_binary(B) ->
339    B;
340str(P) ->
341    repr(P).
342
343repr(P) when is_float(P) ->
344    try mochinum:digits(P)
345    catch error:undef -> float_to_list(P) end;
346repr(P) ->
347    io_lib:format("~p", [P]).
348
349parse_std_conversion(S) ->
350    parse_std_conversion(S, #conversion{}).
351
352parse_std_conversion("", Acc) ->
353    Acc;
354parse_std_conversion([Fill, Align | Spec], Acc)
355  when Align =:= $< orelse Align =:= $> orelse Align =:= $= orelse Align =:= $^ ->
356    parse_std_conversion(Spec, Acc#conversion{fill_char=Fill,
357                                              align=align(Align)});
358parse_std_conversion([Align | Spec], Acc)
359  when Align =:= $< orelse Align =:= $> orelse Align =:= $= orelse Align =:= $^ ->
360    parse_std_conversion(Spec, Acc#conversion{align=align(Align)});
361parse_std_conversion([Sign | Spec], Acc)
362  when Sign =:= $+ orelse Sign =:= $- orelse Sign =:= $\s ->
363    parse_std_conversion(Spec, Acc#conversion{sign=Sign});
364parse_std_conversion("0" ++ Spec, Acc) ->
365    Align = case Acc#conversion.align of
366                undefined ->
367                    sign_right;
368                A ->
369                    A
370            end,
371    parse_std_conversion(Spec, Acc#conversion{fill_char=$0, align=Align});
372parse_std_conversion(Spec=[D|_], Acc) when D >= $0 andalso D =< $9 ->
373    {W, Spec1} = lists:splitwith(fun (C) -> C >= $0 andalso C =< $9 end, Spec),
374    parse_std_conversion(Spec1, Acc#conversion{length=list_to_integer(W)});
375parse_std_conversion([$. | Spec], Acc) ->
376    case lists:splitwith(fun (C) -> C >= $0 andalso C =< $9 end, Spec) of
377        {"", Spec1} ->
378            parse_std_conversion(Spec1, Acc);
379        {P, Spec1} ->
380            parse_std_conversion(Spec1,
381                                 Acc#conversion{precision=list_to_integer(P)})
382    end;
383parse_std_conversion([Type], Acc) ->
384    parse_std_conversion("", Acc#conversion{ctype=ctype(Type)}).
385
386
387%%
388%% Tests
389%%
390-ifdef(TEST).
391-include_lib("eunit/include/eunit.hrl").
392
393tokenize_test() ->
394    {?MODULE, [{raw, "ABC"}]} = tokenize("ABC"),
395    {?MODULE, [{format, {"0", "", ""}}]} = tokenize("{0}"),
396    {?MODULE, [{raw, "ABC"}, {format, {"1", "", ""}}, {raw, "DEF"}]} =
397        tokenize("ABC{1}DEF"),
398    ok.
399
400format_test() ->
401    <<"  -4">> = bformat("{0:4}", [-4]),
402    <<"   4">> = bformat("{0:4}", [4]),
403    <<"   4">> = bformat("{0:{0}}", [4]),
404    <<"4   ">> = bformat("{0:4}", ["4"]),
405    <<"4   ">> = bformat("{0:{0}}", ["4"]),
406    <<"1.2yoDEF">> = bformat("{2}{0}{1}{3}", {yo, "DE", 1.2, <<"F">>}),
407    <<"cafebabe">> = bformat("{0:x}", {16#cafebabe}),
408    <<"CAFEBABE">> = bformat("{0:X}", {16#cafebabe}),
409    <<"CAFEBABE">> = bformat("{0:X}", {16#cafebabe}),
410    <<"755">> = bformat("{0:o}", {8#755}),
411    <<"a">> = bformat("{0:c}", {97}),
412    %% Horizontal ellipsis
413    <<226, 128, 166>> = bformat("{0:c}", {16#2026}),
414    <<"11">> = bformat("{0:b}", {3}),
415    <<"11">> = bformat("{0:b}", [3]),
416    <<"11">> = bformat("{three:b}", [{three, 3}]),
417    <<"11">> = bformat("{three:b}", [{"three", 3}]),
418    <<"11">> = bformat("{three:b}", [{<<"three">>, 3}]),
419    <<"\"foo\"">> = bformat("{0!r}", {"foo"}),
420    <<"2008-5-4">> = bformat("{0.0}-{0.1}-{0.2}", {{2008,5,4}}),
421    <<"2008-05-04">> = bformat("{0.0:04}-{0.1:02}-{0.2:02}", {{2008,5,4}}),
422    <<"foo6bar-6">> = bformat("foo{1}{0}-{1}", {bar, 6}),
423    <<"-'atom test'-">> = bformat("-{arg!r}-", [{arg, 'atom test'}]),
424    <<"2008-05-04">> = bformat("{0.0:0{1.0}}-{0.1:0{1.1}}-{0.2:0{1.2}}",
425                               {{2008,5,4}, {4, 2, 2}}),
426    ok.
427
428std_test() ->
429    M = mochifmt_std:new(),
430    <<"01">> = bformat("{0}{1}", [0, 1], M),
431    ok.
432
433records_test() ->
434    M = mochifmt_records:new([{conversion, record_info(fields, conversion)}]),
435    R = #conversion{length=long, precision=hard, sign=peace},
436    long = M:get_value("length", R),
437    hard = M:get_value("precision", R),
438    peace = M:get_value("sign", R),
439    <<"long hard">> = bformat("{length} {precision}", R, M),
440    <<"long hard">> = bformat("{0.length} {0.precision}", [R], M),
441    ok.
442
443-endif.