├── ebin
└── .gitignore
├── rebar
├── .gitignore
├── src
├── walrus.app.src
├── walrus_lexer.xrl
├── walrus_parser.yrl
├── walrus.erl
└── walrus_mochinum.erl
├── Makefile
├── test
├── render_test.erl
├── compile_test.erl
├── lexer_test.erl
└── parser_test.erl
├── LICENSE
├── UNLICENSE
├── .dialyzer-ignore-warnings
└── README.md
/ebin/.gitignore:
--------------------------------------------------------------------------------
1 | *.app
2 | *.beam
3 |
--------------------------------------------------------------------------------
/rebar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devinus/walrus/HEAD/rebar
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .eunit
2 | .dialyzer_plt
3 | src/walrus_lexer.erl
4 | src/walrus_parser.erl
5 |
--------------------------------------------------------------------------------
/src/walrus.app.src:
--------------------------------------------------------------------------------
1 | {application, walrus, [
2 | {description, "Erlang Mustache compiler"},
3 | {vsn, "0.1.0"},
4 | {applications, [kernel, stdlib]},
5 | {registered, []},
6 | {env, []}
7 | ]}.
8 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | REBAR = ./rebar
2 | DIALYZER = dialyzer
3 |
4 | DIALYZER_WARNINGS = -Wunmatched_returns -Werror_handling \
5 | -Wrace_conditions -Wbehaviours -Wunderspecs
6 |
7 | .PHONY: all compile test clean
8 |
9 | all: compile
10 |
11 | compile:
12 | @$(REBAR) compile
13 |
14 | test: compile
15 | @$(REBAR) eunit
16 |
17 | clean:
18 | @$(REBAR) clean
19 |
20 | build-plt:
21 | @$(DIALYZER) --build_plt --output_plt .dialyzer_plt \
22 | --apps kernel stdlib
23 |
24 | dialyze: compile
25 | @$(DIALYZER) --src src --plt .dialyzer_plt $(DIALYZER_WARNINGS) | \
26 | fgrep -vf .dialyzer-ignore-warnings
27 |
--------------------------------------------------------------------------------
/src/walrus_lexer.xrl:
--------------------------------------------------------------------------------
1 | Definitions.
2 |
3 | Key = [a-zA-Z0-9_]+
4 |
5 | Rules.
6 |
7 | {{!.*}} : skip_token.
8 | ([^{}]|({[^{])|(}[^}]))+ : {token,{text,TokenLine,?ltb(TokenChars)}}.
9 | {{ : {token,{'{{',TokenLine}}.
10 | {{# : {token,{'{{#',TokenLine}}.
11 | {{/ : {token,{'{{/',TokenLine}}.
12 | {{\^ : {token,{'{{^',TokenLine}}.
13 | {{{ : {token,{'{{{',TokenLine}}.
14 | {{& : {token,{'{{&',TokenLine}}.
15 | \s*{Key}\s*}} : {token,{key,TokenLine,?key(TokenChars,TokenLen)},"}}"}.
16 | }} : {token,{'}}',TokenLine}}.
17 | }}} : {token,{'}}}',TokenLine}}.
18 |
19 | Erlang code.
20 |
21 | -define(ltb(List), list_to_binary(List)).
22 |
23 | -define(key(Chars, Len),
24 | list_to_atom(string:strip(string:left(Chars, Len-2)))).
25 |
--------------------------------------------------------------------------------
/test/render_test.erl:
--------------------------------------------------------------------------------
1 | -module(render_test).
2 |
3 | -compile([export_all]).
4 |
5 | basic_test() ->
6 | Template = "Hello {{{name}}}.\n"
7 | "Drinks:\n"
8 | "{{#drinks}}\n"
9 | " - {{name}}, {{tastiness}}\n"
10 | "{{/drinks}}",
11 | Context = [{name, "Devin & Jane"},
12 | {drinks, [[{name, "Beer"},
13 | {tastiness, 5}],
14 | [{name, "Juice"},
15 | {tastiness, 8}]]}],
16 | Expected = <<"Hello Devin & Jane.\n"
17 | "Drinks:\n\n"
18 | " - Beer, 5\n\n"
19 | " - Juice, 8\n">>,
20 | Expected = walrus:render(Template, Context).
21 |
--------------------------------------------------------------------------------
/test/compile_test.erl:
--------------------------------------------------------------------------------
1 | -module(compile_test).
2 |
3 | -compile([export_all]).
4 |
5 | basic_test() ->
6 | Template = "Hello {{{name}}}.\n"
7 | "Drinks:\n"
8 | "{{#drinks}}\n"
9 | " - {{name}}, {{tastiness}}\n"
10 | "{{/drinks}}",
11 |
12 | Renderer = walrus:compile(Template),
13 |
14 | Context = [{name, "Devin & Jane"},
15 | {drinks, [[{name, "Beer"},
16 | {tastiness, 5}],
17 | [{name, "Juice"},
18 | {tastiness, 8}]]}],
19 |
20 | Expected = <<"Hello Devin & Jane.\n"
21 | "Drinks:\n\n"
22 | " - Beer, 5\n\n"
23 | " - Juice, 8\n">>,
24 |
25 | Expected = Renderer(Context).
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | This is the MIT license.
2 |
3 | Copyright (c) 2007 Mochi Media, Inc.
4 |
5 | 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:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | 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.
--------------------------------------------------------------------------------
/test/lexer_test.erl:
--------------------------------------------------------------------------------
1 | -module(lexer_test).
2 |
3 | -compile([export_all]).
4 |
5 | basic_test() ->
6 | Tmpl = "Hello {{name}}. Would you like a\n"
7 | "{{#over18}}beer{{/over18}}\n"
8 | "{{^over18}}juice box{{/over18}}?",
9 | Expected = {ok,[{text,1,<<"Hello ">>},
10 | {'{{',1},
11 | {key,1,name},
12 | {'}}',1},
13 | {text,1,<<". Would you like a\n">>},
14 | {'{{#',2},
15 | {key,2,over18},
16 | {'}}',2},
17 | {text,2,<<"beer">>},
18 | {'{{/',2},
19 | {key,2,over18},
20 | {'}}',2},
21 | {text,2,<<"\n">>},
22 | {'{{^',3},
23 | {key,3,over18},
24 | {'}}',3},
25 | {text,3,<<"juice box">>},
26 | {'{{/',3},
27 | {key,3,over18},
28 | {'}}',3},
29 | {text,3,<<"?">>}],3},
30 | Expected = walrus_lexer:string(Tmpl).
31 |
--------------------------------------------------------------------------------
/test/parser_test.erl:
--------------------------------------------------------------------------------
1 | -module(parser_test).
2 |
3 | -compile([export_all]).
4 |
5 | basic_test() ->
6 | Tokens = [{text,1,"Hello "},
7 | {'{{',1},
8 | {key,1,name},
9 | {'}}',1},
10 | {text,1,". Would you like a\n"},
11 | {'{{#',2},
12 | {key,2,over18},
13 | {'}}',2},
14 | {text,2,"beer"},
15 | {'{{/',2},
16 | {key,2,over18},
17 | {'}}',2},
18 | {text,2,"\n"},
19 | {'{{^',3},
20 | {key,3,over18},
21 | {'}}',3},
22 | {text,3,"juice box?"},
23 | {'{{/',3},
24 | {key,3,over18},
25 | {'}}',3},
26 | {text,3,"?"}],
27 | Expected = {ok,[{text,"Hello "},
28 | {var,name},
29 | {text,". Would you like a\n"},
30 | {block,over18,[{text,"beer"}]},
31 | {text,"\n"},
32 | {inverse,over18,[{text,"juice box?"}]},
33 | {text,"?"}]},
34 | Expected = walrus_parser:parse(Tokens).
35 |
--------------------------------------------------------------------------------
/src/walrus_parser.yrl:
--------------------------------------------------------------------------------
1 | Nonterminals template token var block inverse.
2 |
3 | Terminals text key '{{' '{{{' '{{&' '{{#' '{{/' '{{^' '}}' '}}}'.
4 |
5 | Rootsymbol template.
6 |
7 | template -> token : ['$1'].
8 | template -> token template : ['$1' | '$2'].
9 |
10 | token -> text : {text, ?value('$1')}.
11 | token -> var : '$1'.
12 | token -> block : '$1'.
13 | token -> inverse : '$1'.
14 |
15 | var -> '{{' key '}}' : {var, ?value('$2')}.
16 | var -> '{{{' key '}}}' : {var_unescaped, ?value('$2')}.
17 | var -> '{{&' key '}}' : {var_unescaped, ?value('$2')}.
18 |
19 | block -> '{{#' key '}}' template '{{/' key '}}'
20 | : section(block, '$2', '$6', '$4').
21 |
22 | inverse -> '{{^' key '}}' template '{{/' key '}}'
23 | : section(inverse, '$2', '$6', '$4').
24 |
25 | Erlang code.
26 |
27 | -define(value(Token), element(3, Token)).
28 |
29 | -type token() :: {atom(), integer(), atom() | list()}.
30 |
31 | -spec section(Type :: atom(), token(), token(), list())
32 | -> {Type :: atom(), Key1 :: atom(), Tmpl :: list()}.
33 | section(Type, {_, _, Key1}, {_, _, Key2}, Tmpl) when Key1 =:= Key2 ->
34 | {Type, Key1, Tmpl};
35 | section(_, {_, Line, Key}, _, _) ->
36 | return_error(Line, {"Unmatched section tag", Key}).
37 |
--------------------------------------------------------------------------------
/UNLICENSE:
--------------------------------------------------------------------------------
1 | This is free and unencumbered software released into the public domain.
2 |
3 | Anyone is free to copy, modify, publish, use, compile, sell, or
4 | distribute this software, either in source code form or as a compiled
5 | binary, for any purpose, commercial or non-commercial, and by any
6 | means.
7 |
8 | In jurisdictions that recognize copyright laws, the author or authors
9 | of this software dedicate any and all copyright interest in the
10 | software to the public domain. We make this dedication for the benefit
11 | of the public at large and to the detriment of our heirs and
12 | successors. We intend this dedication to be an overt act of
13 | relinquishment in perpetuity of all present and future rights to this
14 | software under copyright law.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
24 | For more information, please refer to
25 |
--------------------------------------------------------------------------------
/.dialyzer-ignore-warnings:
--------------------------------------------------------------------------------
1 | leexinc.hrl:52: The pattern can never match the type <_,_,'error' | 'skip_token',[{'{{' | '{{#' | '{{/' | '{{^' | '{{{' | '}}' | '}}}',_} | {'key',_,atom()} | {'text',_,binary()}]>
2 | leexinc.hrl:54: The pattern can never match the type <_,_,'error' | 'skip_token',[{'{{' | '{{#' | '{{/' | '{{^' | '{{{' | '}}' | '}}}',_} | {'key',_,atom()} | {'text',_,binary()}]>
3 | leexinc.hrl:59: The pattern can never match the type <_,_,'error',[{'{{' | '{{#' | '{{/' | '{{^' | '{{{' | '}}' | '}}}',_} | {'key',_,atom()} | {'text',_,binary()}]>
4 | leexinc.hrl:62: The pattern <_Rest, Line, {'error', S}, _Ts> can never match the type <_,_,'error',[{'{{' | '{{#' | '{{/' | '{{^' | '{{{' | '}}' | '}}}',_} | {'key',_,atom()} | {'text',_,binary()}]>
5 | leexinc.hrl:121: The pattern can never match the type <_,_,'error' | 'skip_token'>
6 | leexinc.hrl:123: The pattern can never match the type <_,_,'error' | 'skip_token'>
7 | leexinc.hrl:128: The pattern can never match the type <_,_,'error'>
8 | leexinc.hrl:131: The pattern can never match the type <_,_,'error'>
9 | leexinc.hrl:195: The pattern can never match the type <_,_,'error' | 'skip_token',_>
10 | leexinc.hrl:197: The pattern can never match the type <_,_,'error' | 'skip_token',_>
11 | leexinc.hrl:202: The pattern can never match the type <_,_,'error',_>
12 | leexinc.hrl:205: The pattern can never match the type <_,_,'error',_>
13 | leexinc.hrl:246: The pattern can never match the type <_,_,'error' | 'skip_token',_>
14 | leexinc.hrl:248: The pattern can never match the type <_,_,'error' | 'skip_token',_>
15 | leexinc.hrl:253: The pattern can never match the type <_,_,'error',_>
16 | leexinc.hrl:256: The pattern can never match the type <_,_,'error',_>
17 | leexinc.hrl:260: Function yyrev/2 will never be called
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Walrus - Mustache-like Templating
2 | =================================
3 |
4 | Walrus is 50% Mustache and 50% large flippered marine mammal.
5 |
6 | Lexing is done with Leex and parsing is done with Yecc, both
7 | of which are included in newer versions of Erlang.
8 |
9 | Most Mustache constructs work, such as variables, unescaped
10 | variables, blocks, and inverse blocks. However, The Walrus is
11 | an opinionated animal, and partials will never be supported.
12 | Also, functions passed into the context are simply evaluated
13 | and are not passed the raw template.
14 |
15 | You can skip lexing and parsing templates every time you want
16 | to render them by using `compile/1`, which returns a renderer
17 | you can then pass a context that renders the template.
18 |
19 | Examples
20 | --------
21 |
22 | ### Simple Render
23 |
24 | 1> Tmpl = "Hello {{{name}}}.
25 | 1>
26 | 1> Drinks:
27 | 1>
28 | 1> {{#drinks}}
29 | 1> - {{name}}, {{tastiness}}
30 | 1> {{/drinks}}".
31 | "Hello {{{name}}}.\n\nDrinks:\n\n{{#drinks}}\n - {{name}}, {{tastiness}}\n{{/drinks}}"
32 | 2>
33 | 2> Ctx = [{name, "Devin & Jane"},
34 | 2> {drinks, [[{name, "Beer"},
35 | 2> {tastiness, 5}],
36 | 2> [{name, "Juice"},
37 | 2> {tastiness, 8}]]}].
38 | [{name,"Devin & Jane"},
39 | {drinks,[[{name,"Beer"},{tastiness,5}],
40 | [{name,"Juice"},{tastiness,8}]]}]
41 | 3>
42 | 3> walrus:render(Tmpl, Ctx).
43 | <<"Hello Devin & Jane.\n\nDrinks:\n\n\n - Beer, 5\n\n - Juice, 8\n">>
44 |
45 | ### Compiled Renderer
46 |
47 | 1> Renderer = walrus:compile("Hello {{{name}}}.\n\nDrinks:\n\n{{#drinks}}\n - {{name}}, {{tastiness}}\n{{/drinks}}").
48 | #Fun
49 | 2> Ctx = [{name, "Devin & Jane"},
50 | 2> {drinks, [[{name, "Beer"},
51 | 2> {tastiness, 5}],
52 | 2> [{name, "Juice"},
53 | 2> {tastiness, 8}]]}].
54 | [{name,"Devin & Jane"},
55 | {drinks,[[{name,"Beer"},{tastiness,5}],
56 | [{name,"Juice"},{tastiness,8}]]}]
57 | 3>
58 | 3> Renderer(Ctx).
59 | <<"Hello Devin & Jane.\n\nDrinks:\n\n\n - Beer, 5\n\n - Juice, 8\n">>
60 |
61 | Acknowledgments
62 | ---------------
63 | Robert Virding helped me tremendously when I struggled to
64 | understand Leex. He's also responsible for
65 | `([^{}]|({[^{])|(}[^}]))+`.
66 |
67 | License
68 | -------
69 |
70 | All code released into the public domain (see `UNLICENSE`)
71 | except for the file `walrus_mochinum.erl`, which has it's own
72 | license (see `LICENSE`).
73 |
--------------------------------------------------------------------------------
/src/walrus.erl:
--------------------------------------------------------------------------------
1 | -module(walrus).
2 | -author("Devin Torres ").
3 |
4 | -export([compile/1, render/2]).
5 |
6 | -define(is_falsy(V),
7 | (V =:= false orelse V =:= [] orelse V =:= undefined orelse V =:= null)).
8 |
9 | -type value() :: list() | binary() | integer() | float() | atom().
10 | -type context() :: [{Key :: string(), Value :: value()}, ...].
11 | -type stringifiable() :: value() | fun((Context :: context())
12 | -> value()).
13 |
14 | -spec compile(Template :: list() | binary()) -> fun((Context :: context())
15 | -> binary()).
16 | compile(Template) when is_binary(Template) ->
17 | compile(binary_to_list(Template));
18 | compile(Template) when is_list(Template) ->
19 | {ok, Tokens, _} = walrus_lexer:string(Template),
20 | {ok, ParseTree} = walrus_parser:parse(Tokens),
21 | fun (Context) ->
22 | render(ParseTree, Context, [])
23 | end.
24 |
25 | -spec render(Template :: list() | binary(), Context :: context())
26 | -> binary().
27 | render(Template, Context) when is_binary(Template) ->
28 | render(binary_to_list(Template), Context);
29 | render(Template, Context) when is_list(Template) ->
30 | {ok, Tokens, _} = walrus_lexer:string(Template),
31 | {ok, ParseTree} = walrus_parser:parse(Tokens),
32 | render(ParseTree, Context, []).
33 |
34 | -spec render(ParseTree :: list(), Context :: context(), Acc :: list())
35 | -> binary().
36 | render([{text, Text} | ParseTree], Context, Acc) ->
37 | render(ParseTree, Context, [Text | Acc]);
38 | render([{var, Key} | ParseTree], Context, Acc) ->
39 | Value = get(Key, Context),
40 | render(ParseTree, Context, [stringify(Value, Context, true) | Acc]);
41 | render([{var_unescaped, Key} | ParseTree], Context, Acc) ->
42 | Value = get(Key, Context),
43 | render(ParseTree, Context, [stringify(Value, Context, false) | Acc]);
44 | render([{block, Key, SubParseTree} | ParseTree], Context, Acc) ->
45 | Value = get(Key, Context),
46 | case Value of
47 | Val when ?is_falsy(Val) ->
48 | render(ParseTree, Context, Acc);
49 | Val when is_list(Val) ->
50 | Tmpl = [render(SubParseTree, Ctx, []) || Ctx <- Val],
51 | render(ParseTree, Context, [Tmpl | Acc]);
52 | _ ->
53 | Tmpl = render(SubParseTree, Context, []),
54 | render(ParseTree, Context, [Tmpl | Acc])
55 | end;
56 | render([{inverse, Key, SubParseTree} | ParseTree], Context, Acc) ->
57 | Value = get(Key, Context),
58 | case Value of
59 | Val when ?is_falsy(Val) ->
60 | Tmpl = render(SubParseTree, Context, []),
61 | render(ParseTree, Context, [Tmpl | Acc]);
62 | _ ->
63 | render(ParseTree, Context, Acc)
64 | end;
65 | render([], _Context, Acc) ->
66 | iolist_to_binary(lists:reverse(Acc)).
67 |
68 | -spec get(Key :: atom(), Context :: context()) -> stringifiable().
69 | get(Key, Context) ->
70 | Value = proplists:get_value(Key, Context),
71 | if
72 | is_function(Value) -> Value(Context);
73 | true -> Value
74 | end.
75 |
76 | -spec stringify(Value :: stringifiable(), Context :: context(), Escape :: boolean())
77 | -> iolist().
78 | stringify(Value, _Context, false) when is_list(Value) ->
79 | Value;
80 | stringify(Value, _Context, true) when is_list(Value) ->
81 | escape(Value);
82 | stringify(Value, _Context, true) when is_binary(Value) ->
83 | escape(binary_to_list(Value));
84 | stringify(Value, _Context, _Escape) when is_integer(Value) ->
85 | integer_to_list(Value);
86 | stringify(Value, _Context, _Escape) when is_float(Value) ->
87 | walrus_mochinum:digits(Value);
88 | stringify(Value, _Context, false) when is_atom(Value) ->
89 | atom_to_list(Value);
90 | stringify(Value, _Context, true) when is_atom(Value) ->
91 | escape(atom_to_list(Value));
92 | stringify(Value, Context, Escape) when is_function(Value) ->
93 | stringify(Value(Context), Context, Escape).
94 |
95 | escape(Value) ->
96 | escape(Value, []).
97 |
98 | -spec escape(Value :: list(), Acc :: list()) -> iolist().
99 | escape([$< | Tail], Acc) ->
100 | escape(Tail, [<<"<">> | Acc]);
101 | escape([$> | Tail], Acc) ->
102 | escape(Tail, [<<">">> | Acc]);
103 | escape([$& | Tail], Acc) ->
104 | escape(Tail, [<<"&">> | Acc]);
105 | escape([C | Tail], Acc) ->
106 | escape(Tail, [C | Acc]);
107 | escape([], Acc) ->
108 | lists:reverse(Acc).
109 |
--------------------------------------------------------------------------------
/src/walrus_mochinum.erl:
--------------------------------------------------------------------------------
1 | %% @copyright 2007 Mochi Media, Inc.
2 | %% @author Bob Ippolito
3 |
4 | %% @doc Useful numeric algorithms for floats that cover some deficiencies
5 | %% in the math module. More interesting is digits/1, which implements
6 | %% the algorithm from:
7 | %% http://www.cs.indiana.edu/~burger/fp/index.html
8 | %% See also "Printing Floating-Point Numbers Quickly and Accurately"
9 | %% in Proceedings of the SIGPLAN '96 Conference on Programming Language
10 | %% Design and Implementation.
11 |
12 | -module(walrus_mochinum).
13 | -author("Bob Ippolito ").
14 | -export([digits/1, frexp/1, int_pow/2, int_ceil/1]).
15 |
16 | %% IEEE 754 Float exponent bias
17 | -define(FLOAT_BIAS, 1022).
18 | -define(MIN_EXP, -1074).
19 | -define(BIG_POW, 4503599627370496).
20 |
21 | %% External API
22 |
23 | %% @spec digits(number()) -> string()
24 | %% @doc Returns a string that accurately represents the given integer or float
25 | %% using a conservative amount of digits. Great for generating
26 | %% human-readable output, or compact ASCII serializations for floats.
27 | digits(N) when is_integer(N) ->
28 | integer_to_list(N);
29 | digits(0.0) ->
30 | "0.0";
31 | digits(Float) ->
32 | {Frac1, Exp1} = frexp_int(Float),
33 | [Place0 | Digits0] = digits1(Float, Exp1, Frac1),
34 | {Place, Digits} = transform_digits(Place0, Digits0),
35 | R = insert_decimal(Place, Digits),
36 | case Float < 0 of
37 | true ->
38 | [$- | R];
39 | _ ->
40 | R
41 | end.
42 |
43 | %% @spec frexp(F::float()) -> {Frac::float(), Exp::float()}
44 | %% @doc Return the fractional and exponent part of an IEEE 754 double,
45 | %% equivalent to the libc function of the same name.
46 | %% F = Frac * pow(2, Exp).
47 | frexp(F) ->
48 | frexp1(unpack(F)).
49 |
50 | %% @spec int_pow(X::integer(), N::integer()) -> Y::integer()
51 | %% @doc Moderately efficient way to exponentiate integers.
52 | %% int_pow(10, 2) = 100.
53 | int_pow(_X, 0) ->
54 | 1;
55 | int_pow(X, N) when N > 0 ->
56 | int_pow(X, N, 1).
57 |
58 | %% @spec int_ceil(F::float()) -> integer()
59 | %% @doc Return the ceiling of F as an integer. The ceiling is defined as
60 | %% F when F == trunc(F);
61 | %% trunc(F) when F < 0;
62 | %% trunc(F) + 1 when F > 0.
63 | int_ceil(X) ->
64 | T = trunc(X),
65 | case (X - T) of
66 | Pos when Pos > 0 -> T + 1;
67 | _ -> T
68 | end.
69 |
70 |
71 | %% Internal API
72 |
73 | int_pow(X, N, R) when N < 2 ->
74 | R * X;
75 | int_pow(X, N, R) ->
76 | int_pow(X * X, N bsr 1, case N band 1 of 1 -> R * X; 0 -> R end).
77 |
78 | insert_decimal(0, S) ->
79 | "0." ++ S;
80 | insert_decimal(Place, S) when Place > 0 ->
81 | L = length(S),
82 | case Place - L of
83 | 0 ->
84 | S ++ ".0";
85 | N when N < 0 ->
86 | {S0, S1} = lists:split(L + N, S),
87 | S0 ++ "." ++ S1;
88 | N when N < 6 ->
89 | %% More places than digits
90 | S ++ lists:duplicate(N, $0) ++ ".0";
91 | _ ->
92 | insert_decimal_exp(Place, S)
93 | end;
94 | insert_decimal(Place, S) when Place > -6 ->
95 | "0." ++ lists:duplicate(abs(Place), $0) ++ S;
96 | insert_decimal(Place, S) ->
97 | insert_decimal_exp(Place, S).
98 |
99 | insert_decimal_exp(Place, S) ->
100 | [C | S0] = S,
101 | S1 = case S0 of
102 | [] ->
103 | "0";
104 | _ ->
105 | S0
106 | end,
107 | Exp = case Place < 0 of
108 | true ->
109 | "e-";
110 | false ->
111 | "e+"
112 | end,
113 | [C] ++ "." ++ S1 ++ Exp ++ integer_to_list(abs(Place - 1)).
114 |
115 |
116 | digits1(Float, Exp, Frac) ->
117 | Round = ((Frac band 1) =:= 0),
118 | case Exp >= 0 of
119 | true ->
120 | BExp = 1 bsl Exp,
121 | case (Frac =/= ?BIG_POW) of
122 | true ->
123 | scale((Frac * BExp * 2), 2, BExp, BExp,
124 | Round, Round, Float);
125 | false ->
126 | scale((Frac * BExp * 4), 4, (BExp * 2), BExp,
127 | Round, Round, Float)
128 | end;
129 | false ->
130 | case (Exp =:= ?MIN_EXP) orelse (Frac =/= ?BIG_POW) of
131 | true ->
132 | scale((Frac * 2), 1 bsl (1 - Exp), 1, 1,
133 | Round, Round, Float);
134 | false ->
135 | scale((Frac * 4), 1 bsl (2 - Exp), 2, 1,
136 | Round, Round, Float)
137 | end
138 | end.
139 |
140 | scale(R, S, MPlus, MMinus, LowOk, HighOk, Float) ->
141 | Est = int_ceil(math:log10(abs(Float)) - 1.0e-10),
142 | %% Note that the scheme implementation uses a 326 element look-up table
143 | %% for int_pow(10, N) where we do not.
144 | case Est >= 0 of
145 | true ->
146 | fixup(R, S * int_pow(10, Est), MPlus, MMinus, Est,
147 | LowOk, HighOk);
148 | false ->
149 | Scale = int_pow(10, -Est),
150 | fixup(R * Scale, S, MPlus * Scale, MMinus * Scale, Est,
151 | LowOk, HighOk)
152 | end.
153 |
154 | fixup(R, S, MPlus, MMinus, K, LowOk, HighOk) ->
155 | TooLow = case HighOk of
156 | true ->
157 | (R + MPlus) >= S;
158 | false ->
159 | (R + MPlus) > S
160 | end,
161 | case TooLow of
162 | true ->
163 | [(K + 1) | generate(R, S, MPlus, MMinus, LowOk, HighOk)];
164 | false ->
165 | [K | generate(R * 10, S, MPlus * 10, MMinus * 10, LowOk, HighOk)]
166 | end.
167 |
168 | generate(R0, S, MPlus, MMinus, LowOk, HighOk) ->
169 | D = R0 div S,
170 | R = R0 rem S,
171 | TC1 = case LowOk of
172 | true ->
173 | R =< MMinus;
174 | false ->
175 | R < MMinus
176 | end,
177 | TC2 = case HighOk of
178 | true ->
179 | (R + MPlus) >= S;
180 | false ->
181 | (R + MPlus) > S
182 | end,
183 | case TC1 of
184 | false ->
185 | case TC2 of
186 | false ->
187 | [D | generate(R * 10, S, MPlus * 10, MMinus * 10,
188 | LowOk, HighOk)];
189 | true ->
190 | [D + 1]
191 | end;
192 | true ->
193 | case TC2 of
194 | false ->
195 | [D];
196 | true ->
197 | case R * 2 < S of
198 | true ->
199 | [D];
200 | false ->
201 | [D + 1]
202 | end
203 | end
204 | end.
205 |
206 | unpack(Float) ->
207 | <> = <>,
208 | {Sign, Exp, Frac}.
209 |
210 | frexp1({_Sign, 0, 0}) ->
211 | {0.0, 0};
212 | frexp1({Sign, 0, Frac}) ->
213 | Exp = log2floor(Frac),
214 | <> = <>,
215 | {Frac1, -(?FLOAT_BIAS) - 52 + Exp};
216 | frexp1({Sign, Exp, Frac}) ->
217 | <> = <>,
218 | {Frac1, Exp - ?FLOAT_BIAS}.
219 |
220 | log2floor(Int) ->
221 | log2floor(Int, 0).
222 |
223 | log2floor(0, N) ->
224 | N;
225 | log2floor(Int, N) ->
226 | log2floor(Int bsr 1, 1 + N).
227 |
228 |
229 | transform_digits(Place, [0 | Rest]) ->
230 | transform_digits(Place, Rest);
231 | transform_digits(Place, Digits) ->
232 | {Place, [$0 + D || D <- Digits]}.
233 |
234 |
235 | frexp_int(F) ->
236 | case unpack(F) of
237 | {_Sign, 0, Frac} ->
238 | {Frac, ?MIN_EXP};
239 | {_Sign, Exp, Frac} ->
240 | {Frac + (1 bsl 52), Exp - 53 - ?FLOAT_BIAS}
241 | end.
242 |
243 | %%
244 | %% Tests
245 | %%
246 | -ifdef(TEST).
247 | -include_lib("eunit/include/eunit.hrl").
248 |
249 | int_ceil_test() ->
250 | ?assertEqual(1, int_ceil(0.0001)),
251 | ?assertEqual(0, int_ceil(0.0)),
252 | ?assertEqual(1, int_ceil(0.99)),
253 | ?assertEqual(1, int_ceil(1.0)),
254 | ?assertEqual(-1, int_ceil(-1.5)),
255 | ?assertEqual(-2, int_ceil(-2.0)),
256 | ok.
257 |
258 | int_pow_test() ->
259 | ?assertEqual(1, int_pow(1, 1)),
260 | ?assertEqual(1, int_pow(1, 0)),
261 | ?assertEqual(1, int_pow(10, 0)),
262 | ?assertEqual(10, int_pow(10, 1)),
263 | ?assertEqual(100, int_pow(10, 2)),
264 | ?assertEqual(1000, int_pow(10, 3)),
265 | ok.
266 |
267 | digits_test() ->
268 | ?assertEqual("0",
269 | digits(0)),
270 | ?assertEqual("0.0",
271 | digits(0.0)),
272 | ?assertEqual("1.0",
273 | digits(1.0)),
274 | ?assertEqual("-1.0",
275 | digits(-1.0)),
276 | ?assertEqual("0.1",
277 | digits(0.1)),
278 | ?assertEqual("0.01",
279 | digits(0.01)),
280 | ?assertEqual("0.001",
281 | digits(0.001)),
282 | ?assertEqual("1.0e+6",
283 | digits(1000000.0)),
284 | ?assertEqual("0.5",
285 | digits(0.5)),
286 | ?assertEqual("4503599627370496.0",
287 | digits(4503599627370496.0)),
288 | %% small denormalized number
289 | %% 4.94065645841246544177e-324 =:= 5.0e-324
290 | <> = <<0,0,0,0,0,0,0,1>>,
291 | ?assertEqual("5.0e-324",
292 | digits(SmallDenorm)),
293 | ?assertEqual(SmallDenorm,
294 | list_to_float(digits(SmallDenorm))),
295 | %% large denormalized number
296 | %% 2.22507385850720088902e-308
297 | <> = <<0,15,255,255,255,255,255,255>>,
298 | ?assertEqual("2.225073858507201e-308",
299 | digits(BigDenorm)),
300 | ?assertEqual(BigDenorm,
301 | list_to_float(digits(BigDenorm))),
302 | %% small normalized number
303 | %% 2.22507385850720138309e-308
304 | <> = <<0,16,0,0,0,0,0,0>>,
305 | ?assertEqual("2.2250738585072014e-308",
306 | digits(SmallNorm)),
307 | ?assertEqual(SmallNorm,
308 | list_to_float(digits(SmallNorm))),
309 | %% large normalized number
310 | %% 1.79769313486231570815e+308
311 | <> = <<127,239,255,255,255,255,255,255>>,
312 | ?assertEqual("1.7976931348623157e+308",
313 | digits(LargeNorm)),
314 | ?assertEqual(LargeNorm,
315 | list_to_float(digits(LargeNorm))),
316 | %% issue #10 - mochinum:frexp(math:pow(2, -1074)).
317 | ?assertEqual("5.0e-324",
318 | digits(math:pow(2, -1074))),
319 | ok.
320 |
321 | frexp_test() ->
322 | %% zero
323 | ?assertEqual({0.0, 0}, frexp(0.0)),
324 | %% one
325 | ?assertEqual({0.5, 1}, frexp(1.0)),
326 | %% negative one
327 | ?assertEqual({-0.5, 1}, frexp(-1.0)),
328 | %% small denormalized number
329 | %% 4.94065645841246544177e-324
330 | <> = <<0,0,0,0,0,0,0,1>>,
331 | ?assertEqual({0.5, -1073}, frexp(SmallDenorm)),
332 | %% large denormalized number
333 | %% 2.22507385850720088902e-308
334 | <> = <<0,15,255,255,255,255,255,255>>,
335 | ?assertEqual(
336 | {0.99999999999999978, -1022},
337 | frexp(BigDenorm)),
338 | %% small normalized number
339 | %% 2.22507385850720138309e-308
340 | <> = <<0,16,0,0,0,0,0,0>>,
341 | ?assertEqual({0.5, -1021}, frexp(SmallNorm)),
342 | %% large normalized number
343 | %% 1.79769313486231570815e+308
344 | <> = <<127,239,255,255,255,255,255,255>>,
345 | ?assertEqual(
346 | {0.99999999999999989, 1024},
347 | frexp(LargeNorm)),
348 | %% issue #10 - mochinum:frexp(math:pow(2, -1074)).
349 | ?assertEqual(
350 | {0.5, -1073},
351 | frexp(math:pow(2, -1074))),
352 | ok.
353 |
354 | -endif.
--------------------------------------------------------------------------------