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