Subversion Repositories SE.SVN

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
12 7u83 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
 
27
-author('bob@mochimedia.com').
28
 
29
-export([convert_field/2, format/2, format_field/2,
30
	 get_field/2, get_value/2]).
31
 
32
-export([format/3, format_field/3, get_field/3,
33
	 tokenize/1]).
34
 
35
-export([bformat/2, bformat/3]).
36
 
37
-export([f/2, f/3]).
38
 
39
-record(conversion,
40
	{length, precision, ctype, align, fill_char, sign}).
41
 
42
%% @spec tokenize(S::string()) -> tokens()
43
%% @doc Tokenize a format string into mochifmt's internal format.
44
tokenize(S) -> {?MODULE, tokenize(S, "", [])}.
45
 
46
%% @spec convert_field(Arg, Conversion::conversion()) -> term()
47
%% @doc Process Arg according to the given explicit conversion specifier.
48
convert_field(Arg, "") -> Arg;
49
convert_field(Arg, "r") -> repr(Arg);
50
convert_field(Arg, "s") -> str(Arg).
51
 
52
%% @spec get_value(Key::string(), Args::args()) -> term()
53
%% @doc Get the Key from Args. If Args is a tuple then convert Key to
54
%%      an integer and get element(1 + Key, Args). If Args is a list and Key
55
%%      can be parsed as an integer then use lists:nth(1 + Key, Args),
56
%%      otherwise try and look for Key in Args as a proplist, converting
57
%%      Key to an atom or binary if necessary.
58
get_value(Key, Args) when is_tuple(Args) ->
59
    element(1 + list_to_integer(Key), Args);
60
get_value(Key, Args) when is_list(Args) ->
61
    try lists:nth(1 + list_to_integer(Key), Args) catch
62
      error:_ -> {_K, V} = proplist_lookup(Key, Args), V
63
    end.
64
 
65
%% @spec get_field(Key::string(), Args) -> term()
66
%% @doc Consecutively call get_value/2 on parts of Key delimited by ".",
67
%%      replacing Args with the result of the previous get_value. This
68
%%      is used to implement formats such as {0.0}.
69
get_field(Key, Args) -> get_field(Key, Args, ?MODULE).
70
 
71
%% @spec get_field(Key::string(), Args, Module) -> term()
72
%% @doc Consecutively call Module:get_value/2 on parts of Key delimited by ".",
73
%%      replacing Args with the result of the previous get_value. This
74
%%      is used to implement formats such as {0.0}.
75
get_field(Key, Args, Module) ->
76
    {Name, Next} = lists:splitwith(fun (C) -> C =/= $. end,
77
				   Key),
78
    Res = mod_get_value(Name, Args, Module),
79
    case Next of
80
      "" -> Res;
81
      "." ++ S1 -> get_field(S1, Res, Module)
82
    end.
83
 
84
mod_get_value(Name, Args, Module) ->
85
    try tuple_apply(Module, get_value, [Name, Args]) catch
86
      error:undef -> get_value(Name, Args)
87
    end.
88
 
89
tuple_apply(Module, F, Args) when is_atom(Module) ->
90
    erlang:apply(Module, F, Args);
91
tuple_apply(Module, F, Args)
92
    when is_tuple(Module), is_atom(element(1, Module)) ->
93
    erlang:apply(element(1, Module), F, Args ++ [Module]).
94
 
95
%% @spec format(Format::string(), Args) -> iolist()
96
%% @doc Format Args with Format.
97
format(Format, Args) -> format(Format, Args, ?MODULE).
98
 
99
%% @spec format(Format::string(), Args, Module) -> iolist()
100
%% @doc Format Args with Format using Module.
101
format({?MODULE, Parts}, Args, Module) ->
102
    format2(Parts, Args, Module, []);
103
format(S, Args, Module) ->
104
    format(tokenize(S), Args, Module).
105
 
106
%% @spec format_field(Arg, Format) -> iolist()
107
%% @doc Format Arg with Format.
108
format_field(Arg, Format) ->
109
    format_field(Arg, Format, ?MODULE).
110
 
111
%% @spec format_field(Arg, Format, _Module) -> iolist()
112
%% @doc Format Arg with Format.
113
format_field(Arg, Format, _Module) ->
114
    F = default_ctype(Arg, parse_std_conversion(Format)),
115
    fix_padding(fix_sign(convert2(Arg, F), F), F).
116
 
117
%% @spec f(Format::string(), Args) -> string()
118
%% @doc Format Args with Format and return a string().
119
f(Format, Args) -> f(Format, Args, ?MODULE).
120
 
121
%% @spec f(Format::string(), Args, Module) -> string()
122
%% @doc Format Args with Format using Module and return a string().
123
f(Format, Args, Module) ->
124
    case lists:member(${, Format) of
125
      true -> binary_to_list(bformat(Format, Args, Module));
126
      false -> Format
127
    end.
128
 
129
%% @spec bformat(Format::string(), Args) -> binary()
130
%% @doc Format Args with Format and return a binary().
131
bformat(Format, Args) ->
132
    iolist_to_binary(format(Format, Args)).
133
 
134
%% @spec bformat(Format::string(), Args, Module) -> binary()
135
%% @doc Format Args with Format using Module and return a binary().
136
bformat(Format, Args, Module) ->
137
    iolist_to_binary(format(Format, Args, Module)).
138
 
139
%% Internal API
140
 
141
add_raw("", Acc) -> Acc;
142
add_raw(S, Acc) -> [{raw, lists:reverse(S)} | Acc].
143
 
144
tokenize([], S, Acc) -> lists:reverse(add_raw(S, Acc));
145
tokenize("{{" ++ Rest, S, Acc) ->
146
    tokenize(Rest, "{" ++ S, Acc);
147
tokenize("{" ++ Rest, S, Acc) ->
148
    {Format, Rest1} = tokenize_format(Rest),
149
    tokenize(Rest1, "",
150
	     [{format, make_format(Format)} | add_raw(S, Acc)]);
151
tokenize("}}" ++ Rest, S, Acc) ->
152
    tokenize(Rest, "}" ++ S, Acc);
153
tokenize([C | Rest], S, Acc) ->
154
    tokenize(Rest, [C | S], Acc).
155
 
156
tokenize_format(S) -> tokenize_format(S, 1, []).
157
 
158
tokenize_format("}" ++ Rest, 1, Acc) ->
159
    {lists:reverse(Acc), Rest};
160
tokenize_format("}" ++ Rest, N, Acc) ->
161
    tokenize_format(Rest, N - 1, "}" ++ Acc);
162
tokenize_format("{" ++ Rest, N, Acc) ->
163
    tokenize_format(Rest, 1 + N, "{" ++ Acc);
164
tokenize_format([C | Rest], N, Acc) ->
165
    tokenize_format(Rest, N, [C | Acc]).
166
 
167
make_format(S) ->
168
    {Name0, Spec} = case lists:splitwith(fun (C) -> C =/= $:
169
					 end,
170
					 S)
171
			of
172
		      {_, ""} -> {S, ""};
173
		      {SN, ":" ++ SS} -> {SN, SS}
174
		    end,
175
    {Name, Transform} = case lists:splitwith(fun (C) ->
176
						     C =/= $!
177
					     end,
178
					     Name0)
179
			    of
180
			  {_, ""} -> {Name0, ""};
181
			  {TN, "!" ++ TT} -> {TN, TT}
182
			end,
183
    {Name, Transform, Spec}.
184
 
185
proplist_lookup(S, P) ->
186
    A = try list_to_existing_atom(S) catch
187
	  error:_ -> make_ref()
188
	end,
189
    B = try list_to_binary(S) catch
190
	  error:_ -> make_ref()
191
	end,
192
    proplist_lookup2({S, A, B}, P).
193
 
194
proplist_lookup2({KS, KA, KB}, [{K, V} | _])
195
    when KS =:= K orelse KA =:= K orelse KB =:= K ->
196
    {K, V};
197
proplist_lookup2(Keys, [_ | Rest]) ->
198
    proplist_lookup2(Keys, Rest).
199
 
200
format2([], _Args, _Module, Acc) -> lists:reverse(Acc);
201
format2([{raw, S} | Rest], Args, Module, Acc) ->
202
    format2(Rest, Args, Module, [S | Acc]);
203
format2([{format, {Key, Convert, Format0}} | Rest],
204
	Args, Module, Acc) ->
205
    Format = f(Format0, Args, Module),
206
    V = case Module of
207
	  ?MODULE ->
208
	      V0 = get_field(Key, Args),
209
	      V1 = convert_field(V0, Convert),
210
	      format_field(V1, Format);
211
	  _ ->
212
	      V0 = try tuple_apply(Module, get_field, [Key, Args])
213
		   catch
214
		     error:undef -> get_field(Key, Args, Module)
215
		   end,
216
	      V1 = try tuple_apply(Module, convert_field,
217
				   [V0, Convert])
218
		   catch
219
		     error:undef -> convert_field(V0, Convert)
220
		   end,
221
	      try tuple_apply(Module, format_field, [V1, Format])
222
	      catch
223
		error:undef -> format_field(V1, Format, Module)
224
	      end
225
	end,
226
    format2(Rest, Args, Module, [V | Acc]).
227
 
228
default_ctype(_Arg, C = #conversion{ctype = N})
229
    when N =/= undefined ->
230
    C;
231
default_ctype(Arg, C) when is_integer(Arg) ->
232
    C#conversion{ctype = decimal};
233
default_ctype(Arg, C) when is_float(Arg) ->
234
    C#conversion{ctype = general};
235
default_ctype(_Arg, C) -> C#conversion{ctype = string}.
236
 
237
fix_padding(Arg, #conversion{length = undefined}) ->
238
    Arg;
239
fix_padding(Arg,
240
	    F = #conversion{length = Length, fill_char = Fill0,
241
			    align = Align0, ctype = Type}) ->
242
    Padding = Length - iolist_size(Arg),
243
    Fill = case Fill0 of
244
	     undefined -> $\s;
245
	     _ -> Fill0
246
	   end,
247
    Align = case Align0 of
248
	      undefined ->
249
		  case Type of
250
		    string -> left;
251
		    _ -> right
252
		  end;
253
	      _ -> Align0
254
	    end,
255
    case Padding > 0 of
256
      true -> do_padding(Arg, Padding, Fill, Align, F);
257
      false -> Arg
258
    end.
259
 
260
do_padding(Arg, Padding, Fill, right, _F) ->
261
    [lists:duplicate(Padding, Fill), Arg];
262
do_padding(Arg, Padding, Fill, center, _F) ->
263
    LPadding = lists:duplicate(Padding div 2, Fill),
264
    RPadding = case Padding band 1 of
265
		 1 -> [Fill | LPadding];
266
		 _ -> LPadding
267
	       end,
268
    [LPadding, Arg, RPadding];
269
do_padding([$- | Arg], Padding, Fill, sign_right, _F) ->
270
    [[$- | lists:duplicate(Padding, Fill)], Arg];
271
do_padding(Arg, Padding, Fill, sign_right,
272
	   #conversion{sign = $-}) ->
273
    [lists:duplicate(Padding, Fill), Arg];
274
do_padding([S | Arg], Padding, Fill, sign_right,
275
	   #conversion{sign = S}) ->
276
    [[S | lists:duplicate(Padding, Fill)], Arg];
277
do_padding(Arg, Padding, Fill, sign_right,
278
	   #conversion{sign = undefined}) ->
279
    [lists:duplicate(Padding, Fill), Arg];
280
do_padding(Arg, Padding, Fill, left, _F) ->
281
    [Arg | lists:duplicate(Padding, Fill)].
282
 
283
fix_sign(Arg, #conversion{sign = $+}) when Arg >= 0 ->
284
    [$+, Arg];
285
fix_sign(Arg, #conversion{sign = $\s}) when Arg >= 0 ->
286
    [$\s, Arg];
287
fix_sign(Arg, _F) -> Arg.
288
 
289
ctype($%) -> percent;
290
ctype($s) -> string;
291
ctype($b) -> bin;
292
ctype($o) -> oct;
293
ctype($X) -> upper_hex;
294
ctype($x) -> hex;
295
ctype($c) -> char;
296
ctype($d) -> decimal;
297
ctype($g) -> general;
298
ctype($f) -> fixed;
299
ctype($e) -> exp.
300
 
301
align($<) -> left;
302
align($>) -> right;
303
align($^) -> center;
304
align($=) -> sign_right.
305
 
306
convert2(Arg, F = #conversion{ctype = percent}) ->
307
    [convert2(1.0e+2 * Arg, F#conversion{ctype = fixed}),
308
     $%];
309
convert2(Arg, #conversion{ctype = string}) -> str(Arg);
310
convert2(Arg, #conversion{ctype = bin}) ->
311
    erlang:integer_to_list(Arg, 2);
312
convert2(Arg, #conversion{ctype = oct}) ->
313
    erlang:integer_to_list(Arg, 8);
314
convert2(Arg, #conversion{ctype = upper_hex}) ->
315
    erlang:integer_to_list(Arg, 16);
316
convert2(Arg, #conversion{ctype = hex}) ->
317
    string:to_lower(erlang:integer_to_list(Arg, 16));
318
convert2(Arg, #conversion{ctype = char})
319
    when Arg < 128 ->
320
    [Arg];
321
convert2(Arg, #conversion{ctype = char}) ->
322
    xmerl_ucs:to_utf8(Arg);
323
convert2(Arg, #conversion{ctype = decimal}) ->
324
    integer_to_list(Arg);
325
convert2(Arg,
326
	 #conversion{ctype = general, precision = undefined}) ->
327
    try mochinum:digits(Arg) catch
328
      error:undef -> io_lib:format("~g", [Arg])
329
    end;
330
convert2(Arg,
331
	 #conversion{ctype = fixed, precision = undefined}) ->
332
    io_lib:format("~f", [Arg]);
333
convert2(Arg,
334
	 #conversion{ctype = exp, precision = undefined}) ->
335
    io_lib:format("~e", [Arg]);
336
convert2(Arg,
337
	 #conversion{ctype = general, precision = P}) ->
338
    io_lib:format("~." ++ integer_to_list(P) ++ "g", [Arg]);
339
convert2(Arg,
340
	 #conversion{ctype = fixed, precision = P}) ->
341
    io_lib:format("~." ++ integer_to_list(P) ++ "f", [Arg]);
342
convert2(Arg,
343
	 #conversion{ctype = exp, precision = P}) ->
344
    io_lib:format("~." ++ integer_to_list(P) ++ "e", [Arg]).
345
 
346
str(A) when is_atom(A) -> atom_to_list(A);
347
str(I) when is_integer(I) -> integer_to_list(I);
348
str(F) when is_float(F) ->
349
    try mochinum:digits(F) catch
350
      error:undef -> io_lib:format("~g", [F])
351
    end;
352
str(L) when is_list(L) -> L;
353
str(B) when is_binary(B) -> B;
354
str(P) -> repr(P).
355
 
356
repr(P) when is_float(P) ->
357
    try mochinum:digits(P) catch
358
      error:undef -> float_to_list(P)
359
    end;
360
repr(P) -> io_lib:format("~p", [P]).
361
 
362
parse_std_conversion(S) ->
363
    parse_std_conversion(S, #conversion{}).
364
 
365
parse_std_conversion("", Acc) -> Acc;
366
parse_std_conversion([Fill, Align | Spec], Acc)
367
    when Align =:= $< orelse
368
	   Align =:= $> orelse Align =:= $= orelse Align =:= $^ ->
369
    parse_std_conversion(Spec,
370
			 Acc#conversion{fill_char = Fill,
371
					align = align(Align)});
372
parse_std_conversion([Align | Spec], Acc)
373
    when Align =:= $< orelse
374
	   Align =:= $> orelse Align =:= $= orelse Align =:= $^ ->
375
    parse_std_conversion(Spec,
376
			 Acc#conversion{align = align(Align)});
377
parse_std_conversion([Sign | Spec], Acc)
378
    when Sign =:= $+ orelse
379
	   Sign =:= $- orelse Sign =:= $\s ->
380
    parse_std_conversion(Spec, Acc#conversion{sign = Sign});
381
parse_std_conversion("0" ++ Spec, Acc) ->
382
    Align = case Acc#conversion.align of
383
	      undefined -> sign_right;
384
	      A -> A
385
	    end,
386
    parse_std_conversion(Spec,
387
			 Acc#conversion{fill_char = $0, align = Align});
388
parse_std_conversion(Spec = [D | _], Acc)
389
    when D >= $0 andalso D =< $9 ->
390
    {W, Spec1} = lists:splitwith(fun (C) ->
391
					 C >= $0 andalso C =< $9
392
				 end,
393
				 Spec),
394
    parse_std_conversion(Spec1,
395
			 Acc#conversion{length = list_to_integer(W)});
396
parse_std_conversion([$. | Spec], Acc) ->
397
    case lists:splitwith(fun (C) -> C >= $0 andalso C =< $9
398
			 end,
399
			 Spec)
400
	of
401
      {"", Spec1} -> parse_std_conversion(Spec1, Acc);
402
      {P, Spec1} ->
403
	  parse_std_conversion(Spec1,
404
			       Acc#conversion{precision = list_to_integer(P)})
405
    end;
406
parse_std_conversion([Type], Acc) ->
407
    parse_std_conversion("",
408
			 Acc#conversion{ctype = ctype(Type)}).
409
 
410
%%
411
%% Tests
412
%%
413
-ifdef(TEST).
414
 
415
-include_lib("eunit/include/eunit.hrl").
416
 
417
tokenize_test() ->
418
    {?MODULE, [{raw, "ABC"}]} = tokenize("ABC"),
419
    {?MODULE, [{format, {"0", "", ""}}]} = tokenize("{0}"),
420
    {?MODULE,
421
     [{raw, "ABC"}, {format, {"1", "", ""}}, {raw, "DEF"}]} =
422
	tokenize("ABC{1}DEF"),
423
    ok.
424
 
425
format_test() ->
426
    <<"  -4">> = bformat("{0:4}", [-4]),
427
    <<"   4">> = bformat("{0:4}", [4]),
428
    <<"   4">> = bformat("{0:{0}}", [4]),
429
    <<"4   ">> = bformat("{0:4}", ["4"]),
430
    <<"4   ">> = bformat("{0:{0}}", ["4"]),
431
    <<"1.2yoDEF">> = bformat("{2}{0}{1}{3}",
432
			     {yo, "DE", 1.19999999999999995559, <<"F">>}),
433
    <<"cafebabe">> = bformat("{0:x}", {3405691582}),
434
    <<"CAFEBABE">> = bformat("{0:X}", {3405691582}),
435
    <<"CAFEBABE">> = bformat("{0:X}", {3405691582}),
436
    <<"755">> = bformat("{0:o}", {493}),
437
    <<"a">> = bformat("{0:c}", {97}),
438
    %% Horizontal ellipsis
439
    <<226, 128, 166>> = bformat("{0:c}", {8230}),
440
    <<"11">> = bformat("{0:b}", {3}),
441
    <<"11">> = bformat("{0:b}", [3]),
442
    <<"11">> = bformat("{three:b}", [{three, 3}]),
443
    <<"11">> = bformat("{three:b}", [{"three", 3}]),
444
    <<"11">> = bformat("{three:b}", [{<<"three">>, 3}]),
445
    <<"\"foo\"">> = bformat("{0!r}", {"foo"}),
446
    <<"2008-5-4">> = bformat("{0.0}-{0.1}-{0.2}",
447
			     {{2008, 5, 4}}),
448
    <<"2008-05-04">> = bformat("{0.0:04}-{0.1:02}-{0.2:02}",
449
			       {{2008, 5, 4}}),
450
    <<"foo6bar-6">> = bformat("foo{1}{0}-{1}", {bar, 6}),
451
    <<"-'atom test'-">> = bformat("-{arg!r}-",
452
				  [{arg, 'atom test'}]),
453
    <<"2008-05-04">> =
454
	bformat("{0.0:0{1.0}}-{0.1:0{1.1}}-{0.2:0{1.2}}",
455
		{{2008, 5, 4}, {4, 2, 2}}),
456
    ok.
457
 
458
std_test() ->
459
    M = mochifmt_std:new(),
460
    <<"01">> = bformat("{0}{1}", [0, 1], M),
461
    ok.
462
 
463
records_test() ->
464
    M = mochifmt_records:new([{conversion,
465
			       record_info(fields, conversion)}]),
466
    R = #conversion{length = long, precision = hard,
467
		    sign = peace},
468
    long = mochifmt_records:get_value("length", R, M),
469
    hard = mochifmt_records:get_value("precision", R, M),
470
    peace = mochifmt_records:get_value("sign", R, M),
471
    <<"long hard">> = bformat("{length} {precision}", R, M),
472
    <<"long hard">> = bformat("{0.length} {0.precision}",
473
			      [R], M),
474
    ok.
475
 
476
-endif.