Packages

Library to spell a provided term

Current section

Files

Jump to
spellerl src spellerl.erl
Raw

src/spellerl.erl

-module(spellerl).
%% API exports
-export([spell/1]).
%% Public types
-export_type([input_object/0, result/0, ok/0, error/0, error_reasons/0]).
%%====================================================================
%% Types
%%====================================================================
-type input_object() :: any().
-type int() :: integer().
-type result() :: ok() | error().
-type ok() :: {ok, binary()}.
-type error() :: error(error_reasons()).
-type error(Reason) :: {error, Reason}.
-type error_reasons() :: bad_type_error_reason() | int_error_reason().
-type bad_type_error_reason() :: bad_type.
-type int_error() :: error(int_error_reason()).
-type int_error_reason() :: negative_error_reason()
| unknown_scale_error_reason().
-type negative_error_reason() :: negative.
-type unknown_scale_error_reason() :: unknown_scale.
%%====================================================================
%% API functions
%%====================================================================
-spec spell(input_object()) -> result().
%%--------------------------------------------------------------------
%% @doc returns the grammatically correct English phrase for a given a
%% number
%%--------------------------------------------------------------------
spell(Int) when is_integer(Int) ->
spell_int(Int);
spell(_Else) ->
{error, bad_type}.
%%====================================================================
%% Internal functions
%%====================================================================
-spec spell_int(int()) -> ok() | int_error().
spell_int(Int) when Int < 0 ->
{error, negative};
spell_int(Int) ->
case scale_floor(Int) of
ScaleFloor when ScaleFloor > 1000 ->
Scale = trunc(math:log10(ScaleFloor)),
case scale_name(Scale) of
{ok, _Name} ->
{ok, spell_int(Int, ScaleFloor)};
{error, not_found} ->
{error, unknown_scale}
end;
ScaleFloor ->
{ok, spell_int(Int, ScaleFloor)}
end.
scale_floor(Int) when Int < 1000 ->
1;
scale_floor(Int) ->
Floor = floor(math:log10(Int)),
ScaleSize = 3, % trunc(math:log10(1000)),
pow(10, Floor - (Floor rem ScaleSize)).
%% Not using math:pow/2 here as it's backed by c library, which has
%% float limitations
pow(_Base, 0) -> 1;
pow(Base, Exp) -> Base * pow(Base, Exp - 1).
%% Spelling according to https://en.wikipedia.org/w/index.php?oldid=861363276
spell_int(Int, ScaleFloor) ->
spell_int(Int, ScaleFloor, []).
spell_int(0 = Int, _ScaleFloor, [] = _Acc) ->
num_to_cardinal(Int);
spell_int(0 = _Int, _ScaleFloor, Acc) ->
iolist_to_binary(lists:join(<<", ">>, lists:reverse(Acc)));
spell_int(Int, ScaleFloor, Acc) when (Int div ScaleFloor) == 0 ->
spell_int(Int rem ScaleFloor, ScaleFloor div 1000, Acc);
spell_int(Int, 1 = ScaleFloor, [_|_] = Acc) when Int < 100 ->
Words = scale_to_cardinal(Int div ScaleFloor, ScaleFloor),
HeadPhrase = iolist_to_binary([lists:join(<<", ">>, lists:reverse(Acc))]),
iolist_to_binary([HeadPhrase, <<" and ">>, Words]);
spell_int(Int, ScaleFloor, Acc) ->
Words = scale_to_cardinal(Int div ScaleFloor, ScaleFloor),
spell_int(Int rem ScaleFloor, ScaleFloor div 1000, [Words | Acc]).
scale_to_cardinal(Num, 1 = _ScaleFloor) ->
num_to_cardinal(Num);
scale_to_cardinal(Num, ScaleFloor) ->
Scale = trunc(math:log10(ScaleFloor)),
{ok, ScaleName} = scale_name(Scale),
iolist_to_binary([num_to_cardinal(Num), " ", ScaleName]).
num_to_cardinal(Num) ->
Hundreds = Num div 100,
Tens = (Num rem 100) div 10,
Ones = Num rem 10,
num_to_cardinal(Hundreds, Tens, Ones).
num_to_cardinal(0 = _Hundreds, Tens, Ones) ->
num_to_cardinal(Tens, Ones);
num_to_cardinal(Hundreds, 0 = _Tens, 0 = _Ones) ->
hundreds_to_cardinal(Hundreds);
num_to_cardinal(Hundreds, Tens, Ones) ->
HWord = hundreds_to_cardinal(Hundreds),
NumWord = num_to_cardinal(Tens, Ones),
iolist_to_binary([HWord, " and ", NumWord]).
hundreds_to_cardinal(Hundreds) ->
OnesWord = ones_to_cardinal(Hundreds),
iolist_to_binary([OnesWord, " hundred"]).
num_to_cardinal(0 = _Tens, Ones) ->
ones_to_cardinal(Ones);
num_to_cardinal(Tens, Ones) ->
tens_to_cardinal(Tens, Ones).
tens_to_cardinal(1 = _Tens, 0 = _Ones) -> <<"ten">>;
tens_to_cardinal(1 = _Tens, 1 = _Ones) -> <<"eleven">>;
tens_to_cardinal(1 = _Tens, 2 = _Ones) -> <<"twelve">>;
tens_to_cardinal(1 = _Tens, 3 = _Ones) -> <<"thirteen">>;
tens_to_cardinal(1 = _Tens, 4 = _Ones) -> <<"fourteen">>;
tens_to_cardinal(1 = _Tens, 5 = _Ones) -> <<"fifteen">>;
tens_to_cardinal(1 = _Tens, 6 = _Ones) -> <<"sixteen">>;
tens_to_cardinal(1 = _Tens, 7 = _Ones) -> <<"seventeen">>;
tens_to_cardinal(1 = _Tens, 8 = _Ones) -> <<"eighteen">>;
tens_to_cardinal(1 = _Tens, 9 = _Ones) -> <<"nineteen">>;
tens_to_cardinal(Tens, 0 = _Ones) -> tens_to_cardinal(Tens);
tens_to_cardinal(Tens, Ones) ->
Prefix = tens_to_cardinal(Tens),
Suffix = ones_to_cardinal(Ones),
iolist_to_binary([Prefix, "-", Suffix]).
tens_to_cardinal(2) -> <<"twenty">>;
tens_to_cardinal(3) -> <<"thirty">>;
tens_to_cardinal(4) -> <<"forty">>;
tens_to_cardinal(5) -> <<"fifty">>;
tens_to_cardinal(6) -> <<"sixty">>;
tens_to_cardinal(7) -> <<"seventy">>;
tens_to_cardinal(8) -> <<"eighty">>;
tens_to_cardinal(9) -> <<"ninety">>.
ones_to_cardinal(0) -> <<"zero">>;
ones_to_cardinal(1) -> <<"one">>;
ones_to_cardinal(2) -> <<"two">>;
ones_to_cardinal(3) -> <<"three">>;
ones_to_cardinal(4) -> <<"four">>;
ones_to_cardinal(5) -> <<"five">>;
ones_to_cardinal(6) -> <<"six">>;
ones_to_cardinal(7) -> <<"seven">>;
ones_to_cardinal(8) -> <<"eight">>;
ones_to_cardinal(9) -> <<"nine">>.
scale_name(3) -> {ok, <<"thousand">>};
scale_name(6) -> {ok, <<"million">>};
scale_name(9) -> {ok, <<"billion">>};
scale_name(12) -> {ok, <<"trillion">>};
scale_name(15) -> {ok, <<"quadrillion">>};
scale_name(18) -> {ok, <<"quintillion">>};
scale_name(21) -> {ok, <<"sextillion">>};
scale_name(24) -> {ok, <<"septillion">>};
scale_name(27) -> {ok, <<"octillion">>};
scale_name(30) -> {ok, <<"nonillion">>};
scale_name(33) -> {ok, <<"decillion">>};
scale_name(36) -> {ok, <<"undecillion">>};
scale_name(39) -> {ok, <<"duodecillion">>};
scale_name(42) -> {ok, <<"tredecillion">>};
scale_name(45) -> {ok, <<"quattuordecillion">>};
scale_name(48) -> {ok, <<"quindecillion">>};
scale_name(51) -> {ok, <<"sedecillion">>};
scale_name(54) -> {ok, <<"septendecillion">>};
scale_name(57) -> {ok, <<"octodecillion">>};
scale_name(60) -> {ok, <<"novemdecillion">>};
scale_name(63) -> {ok, <<"vigintillion">>};
scale_name(_Scale) -> {error, not_found}.
%%====================================================================
%% Unit tests
%%====================================================================
-ifdef(EUNIT).
-include_lib("eunit/include/eunit.hrl").
pow_test_() ->
[
?_assertEqual(1, pow(1, 0)),
?_assertEqual(2, pow(2, 1)),
?_assertEqual(4, pow(2, 2)),
?_assertEqual(1000, pow(10, 3))
].
num_to_cardinal_test_() ->
[
?_assertEqual(<<"zero">>, num_to_cardinal(0)),
?_assertEqual(<<"one">>, num_to_cardinal(1)),
?_assertEqual(<<"nine">>, num_to_cardinal(9)),
?_assertEqual(<<"ten">>, num_to_cardinal(10)),
?_assertEqual(<<"eleven">>, num_to_cardinal(11)),
?_assertEqual(<<"nineteen">>, num_to_cardinal(19)),
?_assertEqual(<<"twenty">>, num_to_cardinal(20)),
?_assertEqual(<<"twenty-one">>, num_to_cardinal(21)),
?_assertEqual(<<"ninety-nine">>, num_to_cardinal(99)),
?_assertEqual(<<"one hundred">>, num_to_cardinal(100)),
?_assertEqual(<<"nine hundred">>, num_to_cardinal(900)),
?_assertEqual(<<"one hundred and one">>, num_to_cardinal(101)),
?_assertEqual(<<"one hundred and ten">>, num_to_cardinal(110)),
?_assertEqual(<<"one hundred and nineteen">>, num_to_cardinal(119)),
?_assertEqual(<<"one hundred and twenty">>, num_to_cardinal(120)),
?_assertEqual(<<"one hundred and twenty-one">>, num_to_cardinal(121)),
?_assertEqual(<<"nine hundred and ninety-nine">>, num_to_cardinal(999))
].
scale_to_cardinal_test_() ->
[
?_assertEqual(<<"one thousand">>,
scale_to_cardinal(1, pow(10, 3))),
?_assertEqual(<<"one million">>,
scale_to_cardinal(1, pow(10, 6))),
?_assertEqual(<<"nine hundred and ninety-nine thousand">>,
scale_to_cardinal(999, pow(10, 3)))
].
scale_floor_test_() ->
[
?_assertEqual(1, scale_floor(0)),
?_assertEqual(1, scale_floor(1)),
?_assertEqual(1, scale_floor(999)),
?_assertEqual(pow(10, 3), scale_floor(pow(10, 3))),
?_assertEqual(pow(10, 3), scale_floor(pow(10, 4))),
?_assertEqual(pow(10, 3), scale_floor(pow(10, 5))),
?_assertEqual(pow(10, 6), scale_floor(pow(10, 6)))
].
spell_int_test_() ->
[
?_assertEqual({ok, <<"zero">>}, spell_int(0)),
?_assertEqual({ok, <<"one thousand">>}, spell_int(1000)),
?_assertEqual({ok, <<"twenty-eight thousand, one hundred and twenty-three">>},
spell_int(28123)),
?_assertEqual(
{ok, <<"seven million, three hundred and forty thousand, two hundred">>},
spell_int(7340200)),
?_assertEqual({ok, <<"ninety-two million and thirty-one">>},
spell_int(92000031)),
?_assertEqual({ok, <<"one trillion, ten million and twenty-three">>},
spell_int(1000010000023)),
?_assertEqual({ok, <<"ten vigintillion, one hundred novemdecillion and eighty-nine">>},
spell_int(pow(10, 64) + pow(10, 62) + 89))
].
spell_test_() ->
[
?_assertEqual({error, bad_type}, spell(atom)),
?_assertEqual({error, bad_type}, spell("string")),
?_assertEqual({error, bad_type}, spell(<<"binary">>)),
?_assertEqual({error, bad_type}, spell(1.0)),
?_assertEqual({error, negative}, spell(-1)),
?_assertEqual({error, unknown_scale}, spell(pow(10, 68))),
?_assertEqual({ok, <<"ten">>}, spell(10))
].
-endif.