1
votes

I'm working on an application in Erlang that has to deal with number formatting for several different currencies. There are many complexities to take in consideration, like currency symbols, where it goes (right or left), etc. But one aspect of this kind of formatting use to be straight forward in other programming languages but it's being hard in Erlang, which is the separators to use for thousands and decimal places.

For most of the English speaking countries (independent of the currency) the expected number format is:

9,999,999.99

For several other countries, like Germany and Brazil, the number format is different:

9.999.999,99

There are other variations, like the French variation, with spaces:

9 999 999,99

The problem is that I can't find a nice way to achieve these formats, starting from the floating point number. Sure the io_lib:format/2 can convert it to a string, but it seems to not offer any control over the symbols to use as decimal separator and doesn't output any separator for the thousands (which makes a workaround to "search-and-replace" impossible).

This is an example of what we have so far:

%% currency_l10n.erl

-module(currency_l10n).
-export([format/2]).

number_format(us, Value) ->
    io_lib:format("~.2f", [Value]);

number_format(de, Value) ->
    io_lib:format("~B,~2..0B", [trunc(Value), trunc(Value*100) rem 100]).


%% Examples on the Erlang REPL:

1> currency_l10n:number_format(us, 9999.99).
"9999.99"
2> currency_l10n:number_format(de, 9999.99).
"9999,99"

As you can see, already the workaround for the decimal separator is not exactly pretty and dealing with the delimiters for thousands won't be nicer. Is there anything we are missing here?

2

2 Answers

1
votes

The problem you have is not solve (AFAIK) by any standard library in erlang. It needs several actions to produce the expected result: convert the float to string, split the string in packets, insert 2 kinds of separator and insert the currency sign at the beginning or the end. You need different functions for these tasks. the following code is an example of what you could do:

-module (pcur).

-export ([printCurrency/2]).

% X = value to print, must be a float
% Country = an atom representing the country
printCurrency(X,Country) ->
    % convert to string, split and get the different packets
    [Dec|Num] = splitInReversePackets(toReverseString(X)),               
    % get convention for the country 
    {Before,Dot,Sep,After} = currencyConvention(Country),
    % build the result - Beware of unicode!
    Before ++ printValue(Num,Sep,Dot ++ Dec) ++ After.


toReverseString(X) ->  lists:reverse(io_lib:format("~.2f",[X])).  

splitInThousands([A,B,C|R],Acc) -> splitInThousands(R,[[C,B,A]|Acc]);
splitInThousands([A,B|R],Acc)   -> splitInThousands(R,[[B,A]|Acc]);
splitInThousands([A|R],Acc)     -> splitInThousands(R,[[A]|Acc]);
splitInThousands([],Acc)        -> Acc.

splitInReversePackets([A,B,$.|R]) -> lists:reverse(splitInThousands(R,[[B,A]])).

% return a tuple made of {string to print at the beginning,
%                         string to use to separate the integer part and the decimal,
%                         string to use for thousand separator,
%                         string to print at the end}
currencyConvention(us) -> {"",".",",","$"};
currencyConvention(de) -> {"",",","."," Euro"}; % not found how to print the symbol €
currencyConvention(fr) -> {"Euro ",","," ",""};
currencyConvention(_) -> {"",".",",",""}. % default for unknown country symbol

printValue([A|R=[_|_]],Sep,Acc) -> printValue(R,Sep, Sep ++ A ++ Acc); 
printValue([A],_,Acc) -> A ++ Acc.                    

test in the shell:

1> c(pcur).
{ok,pcur}
2> pcur:printCurrency(123456.256,fr).
"Euro 123 456,26"
3> pcur:printCurrency(123456.256,de).
"123.456,26 Euro"
4> pcur:printCurrency(123456.256,us).
"123,456.26$"
5>

Edit Reading the other proposal and your comments, this solution is clearly not the direction you are going to. Nevertheless, it is in my opinion a valuable way to solve your problem. It should be fast and more important for me, straight and easy to read and update (add currency, split in different size, ??)

0
votes

I suppose you can use re:replace/4 http://erlang.org/doc/man/re.html#replace-4 Eg for DE:

1> re:replace("9999.99", "\\.", ",",[global, {return,list}]).
"9999,99"

But, for adding comma to last two chars as in French, I suppose you can try do something like

1> N = re:replace("9.999.999.99", "\\.", " ",[global, {return,list}]).
"9 999 999 99"
2> Nr = lists:reverse(N).
"99 999 999 9"
3> Nrr = re:replace("99 999 999 9", "\\ ", ",",[{return,list}]).
"99,999 999 9"
4> lists:reverse(Nrr).
"9 999 999,99"

Or you can try create more better regex.

P.S. For convert integer/float to list, you can try use io_lib:format/2, eg

1> [Num] = io_lib:format("~p", [9999.99]).
["9999.99"]
2> Num.
"9999.99"