Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow setting the Currency on #numbers.formatCurrency() #604

Open
jonhkr opened this issue Apr 24, 2017 · 18 comments
Open

Allow setting the Currency on #numbers.formatCurrency() #604

jonhkr opened this issue Apr 24, 2017 · 18 comments

Comments

@jonhkr
Copy link

jonhkr commented Apr 24, 2017

Hello,

It would be good to have a way to set the currency when using #numbers.formatCurrency()

So we can format the numbers correctly in a multi currency application.

@danielfernandez
Copy link
Member

Interestingly, NumberUtils (the class containing the logic executed by #numbers) does already have a formatCurrency(Number, Locale) method, but the Numbers class that models #numbers itself seems to always call this with this.locale.

So a formatCurrency(number, locale) method seems an easy addition to #numbers. Question: would using the locale as an argument solve your need, or do you think this would fall short for some reason? At first thought I think it would be enough and it would offer a quite consistent interface with other methods, but just in case I'm missing something…

@jonhkr
Copy link
Author

jonhkr commented Apr 24, 2017

We need the currency of the amount to properly handle currency symbols.
So we need to set the currency in the NumberFormat using the setCurrency method.

If the locale is en_US, values should be rendered like:
10 Euros -> EUR10.00
10 US Dollars -> $10.00
10 BRL -> R$10.00

@danielfernandez
Copy link
Member

Yes, but I was asking for the signature of the method. Would a method formatCurrency(number, locale) be insufficient for you for some reason?

That locale would then be used at the called NumberUtils like this:

public static String formatCurrency(final Number target, final Locale locale) {
Validate.notNull(locale, "Locale cannot be null");
if (target == null) {
return null;
}
NumberFormat format = NumberFormat.getCurrencyInstance(locale);
return format.format(target);
}
, which is the already existing code (only it is always called from the Numbers class with this.locale, i.e. not letting the user specify a Locale.

@jonhkr
Copy link
Author

jonhkr commented Apr 24, 2017

Yes, it is insufficient because neither the number nor the locale carry the currency information of the passed number.
A monetary amount is composed by a Currency and a Number.

@jonhkr
Copy link
Author

jonhkr commented Apr 24, 2017

I've created a dialect internally in my application to work around this, basically what I do is this:

public String formatCurrency(final Number target, final Currency currency) {
    if (target == null) {
        return null;
    }

    try {
        Validate.notNull(locale, "Locale cannot be null");

        NumberFormat format = NumberFormat.getCurrencyInstance(locale);
        format.setCurrency(currency);

        return format.format(target);
    } catch (final Exception e) {
        throw new TemplateProcessingException("Error formatting currency", e);
    }
}

the important part is this format.setCurrency(currency); that tells the number formatter in which currency the passed number is.

@danielfernandez
Copy link
Member

As long as it specifies a country, a java.util.Locale object does contain three pieces of information that are needed for correctly representing an amount of currency:

  1. The symbol of the currency and its correct positioning (before or after the amount).
  2. The thousands separator symbol (e.g. , for USD, . for EUR).
  3. The decimal separator symbol (e.g. . for USD, , for EUR).

So by specifying a locale, you obtain a completely-correctly formatted currency amount. Take this code:

final BigDecimal amount = new BigDecimal("2412.41");

// United States
final Locale usLocale = Locale.US;
final NumberFormat usFormat = NumberFormat.getCurrencyInstance(usLocale);
System.out.println(usFormat.format(amount));

// Spain
final Locale esLocale = new Locale("es","ES");
final NumberFormat esFormat = NumberFormat.getCurrencyInstance(esLocale);
System.out.println(esFormat.format(amount));

// Brazil
final Locale brLocale = new Locale("pt", "BR");
final NumberFormat brFormat = NumberFormat.getCurrencyInstance(brLocale);
System.out.println(brFormat.format(amount));

The output of this is:

$2,412.41
2.412,41 €
R$ 2.412,41

Which is proper formatting for all three currencies: dollars use , for separating thousands, the symbol goes after the amount, etc.

However, from your code and what you say, it looks to me as if what you want to do is to format the amount in your local locale (e.g. Brazilian, as if all amounts were in reais), but then apply to them an arbitrary currency symbol. Using your code you'd get this instead of the above:

US$ 2.412,41
EUR 2.412,41
R$ 2.412,41

Note here dollars use . for thousands, and EUR is placed before the amount. Also, a whitespace is rendered between the currency symbol and the amount, which in dollars is not usual. All of this happens because that's how reais are formatted, and here we are only changing the symbol, not the format.

Am I right therefore in thinking that you actually need this kind of mix-locale output from the second output example, and that's why you would need to specify the currency independently from the locale?

@jonhkr
Copy link
Author

jonhkr commented Apr 24, 2017

Exactly that, I have a multi-currency application which needs to present values in many currencies (e.g. USD, EUR, BRL).

The current built-in currency formatting support that thymeleaf have works ok for country specific applications where the amounts are represented in only one currency (e.g. BRL), but when there are many currencies involved, it does not work.

I think that it would be ok to add a new method in the #numbers to support that. The signature could be like this:

public String formatCurrency(final Number target, final Currency currency)

@danielfernandez
Copy link
Member

I understand. I still see locale-specific currency representation more generally correct whatever your local language or formats, but I understand your need lies elsewhere.

Note that what you need can be currently done with this — which is admittedly a somewhat more cumbersome expression:

<p th:text="|${currency} ${#numbers.formatDecimal(amount, 1, 'POINT', 2, 'COMMA')}|">

The code above is simply forgetting about the fact that the amount is a currency. In a way, that is what the method you need does: force a foreign currency symbol into the local decimal format.

I'll have to think about the way this could be organised, and what value each of these possibilities add. Thanks for your comments.

@jonhkr
Copy link
Author

jonhkr commented Apr 24, 2017

Ok, If you need any help just ask :)

@danielfernandez danielfernandez removed their assignment May 4, 2017
@astiob
Copy link

astiob commented Aug 16, 2017

I’d like to explain why this is useful (although I personally don’t need this feature right now).

The way a number is presented depends on the language. For example, English separates thousands with commas and puts a full stop between the integral and the fractional parts of a number. Regardless of what currency an amount might be in, as long as the amount is presented in English, this fact does not change. On the other hand, many other languages put a comma between the integral and the fractional parts of a number and use dots, spaces or apostrophes to separate thousands, and some languages use middle dots or other combinations of symbols. If I understand correctly, sometimes the convention differs between regional dialects of the same language, so “language” here means the full language tag including dialect information.

The number of significant digits depends solely on the currency. For example, US dollars and euros are divided into cents and are written with two digits after the decimal point, while Japanese yen are so small that they are not subdivided further, and so they are written without any fractional digits at all. This information is available in Java as Currency.getDefaultFractionDigits().

How a currency symbol is presented next to the number again depends on the language. For example, English puts the currency symbol before the number: $2,412.41, €2,412.41, R$2,412.41. On the other hand, many other languages (I can personally vouch for Russian, but I think most EU languages do so too) put the currency symbol after the number: e. g. in Russian, 2412,41 $; 2412,41 €; 2412,41 R$ (or with more figures: 12 412,41 €).

The currency symbol itself, of course, depends on the currency, but also on the language of presentation. Most (?) currencies have a specific symbol that is used in all languages, e. g. $, £ or €, but some languages may want to make the symbol more specific, like “US$”. Some currencies may not have an actual symbol at all, in which case an abbreviation of the currency name may be used instead, which should be translated to the language of presentation. (For example, the Russian ruble used to be like this, using “р.” or “руб.”, short for “рубль” “ruble”. But then folks created and popularized a proper symbol, so currently it has one.) This is probably hard. But in any case, Java provides localized currency symbols as Currency.getSymbol(Locale).

All in all, if one has a page written in English, one wants to use English number and currency presentation rules with English-localized currency symbols and an appropriate amount of significant digits for each currency. The page may mention $12,412.41, ¥12,412 and XDR 5 (or possibly SDR 5) all at the same time, and all should be formatted appropriately. If the user switches the page language to Russian, they should see 12 412,41 $, 12 412 ¥ and… uh… I don’t know, but probably either 5 СДР or 5 XDR.

The proper formatting of a currency amount is a property not of the currency alone, nor of the presentation language alone, but rather of the combination of both.

@fibsifan
Copy link

I think a very common constellation is thymeleaf being used in a spring boot application. Here the locale is determined by default by the Accept-Language header of the user agent if I'm not mistaken. Given the current implementation of currency formatting this enables the user to change the worth of the value, since different currencies usually have different values. I'm certain that this is rarely intended...

+1 for the overloaded Method suggestion of @jonhkr

fibsifan added a commit to fibsifan/thymeleaf that referenced this issue Oct 1, 2018
@Athas1980
Copy link

I've got an even more complicated requirement.
I have an application that has exactly the same problems but the amounts involved in some cases are a smaller than the default number of decimals. I.e. I've got some values that are equivalent to $0.025 (these are used for aggregation elsewhere). Now this is a really rare case.

Currently I'm handling by passing a NumberFormat to the view.

<td th:text="${unitFormat.invoke(billable.unitPrice)}" class="numeric">US$0.025</td>

The definition of this sorry its not in Java:

    private fun unitFormat(currency: String?): (BigDecimal?) -> String = {
        when (it) {
            null -> ""
            else -> {
                val jdkCurrency = try {
                    Currency.getInstance(currency)
                } catch (e: Exception) {
                    log.warn("Currency with code {} not found", currency)
                    Currency.getInstance("XXX")
                }
                val df = DecimalFormat.getCurrencyInstance(Locale.UK)
                df.currency = jdkCurrency
                df.minimumFractionDigits = 3
                df.maximumFractionDigits = 4
                df.format(it)
            }
        }
    }```

Where is the best place to put in a local extension point?

@wimdeblauwe
Copy link

I personally think about the same lines as @jonhkr . The locale represents the language that you want to show an amount in. If you have an amount in euro with an English locale, you should show € 1,000.00. If you shown an amount in euro with a Dutch locale, you should show € 1.000,00. I already find it weird that the number is just assumed to be in the correct currency of the current locale. I am now just using formatDecimal for now (All my prices in my app are in euro, regardless of the locale of the current usr):

th:text="${'€ ' + #numbers.formatDecimal(finishedRide.ridePrice, 1, 'DEFAULT', 2, 'DEFAULT')}

@goodale
Copy link

goodale commented Apr 3, 2021

I also have a multi-tenant Saas application where each client has its own currency but all clients are on the same database. (So the currency is saved in the database on a per-client basis such as 'EUR', 'USD' , 'CAD', etc)

The view layer needs to present financial data in the currency defined in the database but formatted based on the locale of the browser (eg the number and date formats as defined by the request's locale)

IMHO the th tag should take care of rendering the currency symbol and it wouldn't be necessary to pass this to the format function. Rather, the three-character ISO currency code should be used, along with the locale as a second parameter. In most cases the locale would come from the request but might be overriden if the user chooses to set the locale as an account preference. But the currency would always come from the database/server layer

@totof3110
Copy link

+1 here. Would love to see this resolved especially with the PR already there: #715.

@fibsifan
Copy link

fibsifan commented Oct 23, 2022

Hi @danielfernandez

Maybe you didn't see it, but I created a PR some time ago: #715. I just updated it to be merged into 3.1. I hope it helps (this issue has the label "help wanted")

@gemma1987
Copy link

I personally think about the same lines as @jonhkr . The locale represents the language that you want to show an amount in. If you have an amount in euro with an English locale, you should show € 1,000.00. If you shown an amount in euro with a Dutch locale, you should show € 1.000,00. I already find it weird that the number is just assumed to be in the correct currency of the current locale. I am now just using formatDecimal for now (All my prices in my app are in euro, regardless of the locale of the current usr):

th:text="${'€ ' + #numbers.formatDecimal(finishedRide.ridePrice, 1, 'DEFAULT', 2, 'DEFAULT')}

I tried to solve this for hours and went with your solution too, the numbers.formatCurrency is still not able to update the culture region in 3.0.1.

@goodale
Copy link

goodale commented Mar 28, 2023 via email

fibsifan added a commit to fibsifan/thymeleaf that referenced this issue Aug 25, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants