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.
|