|
|
|
Contents: |
|
|
|
Related content: |
|
|
|
Subscriptions: |
|
|
| Merlin makes formatting numbers and discovering local
currency codes a snap
John
Zukowski (mailto:jaz@zukowski.net?cc=&subject=Formatting
numbers and currency) President, JZ Ventures, Inc. 13 August
2003
Internationalizing your applications
requires both text messages and displayed numbers to be formatted for
the appropriate language and style of the user's domain. With the Merlin
release of the J2SE platform, formatting integers has gotten simpler,
and you can now find out the ISO 4217 currency codes. In this month's
Magic with Merlin, John Zukowski shows you how to format numbers
and introduces the new support for discovering local currency codes.
Share your thoughts on this article with the author and other readers in
the accompanying discussion
forum. (You can also click Discuss at the top or bottom of
the article to access the forum.)
The java.text package allows you to format text messages,
dates, and numbers in a manner independent of a specific language. Many
people use resource bundles with the MessageFormat class to
localize messages for users. Even more people seem to use the
DateFormat and SimpleDateFormat classes for
working with date strings, both for input and output. The least used of
the bunch seems to be the NumberFormat class and its
associated subclasses DecimalFormat and
ChoiceFormat . This month, we'll take a look at these three
underused classes, plus the Currency class, to see just how
worldly J2SE 1.4 has become.
The root of it all:
NumberFormat If you're from the United States, you place a
comma in large numbers to designate thousands and millions (and so on up
for every three digits). For floating-point numbers, you place a decimal
as the separator between the whole part of the number and the fraction.
With money, the currency symbol is $, placed before the amount. If you
never travel outside the States or never expect your programs to for that
matter, you might not care about formatting currencies in yens (¥) for
Japan, pounds (£) for Great Britain, or euros (€) for most of the rest of
Europe.
For those who do care, there's NumberFormat and its
related classes. Developers use the NumberFormat class to
read numbers entered by users and format output to be displayed for users
to see.
Similar to DateFormat , the NumberFormat class
is abstract. You never create an instance of it -- instead, you always
work with a subclass. While you can create a subclass directly through the
constructor for the subclass, the NumberFormat class offers a
series of getXXXInstance() methods to get
locale-specific versions for different types of number classes. There are
five such methods available:
"You say tomato," . . . Locales
represent a language or country-specific representation of how
things are done, either for text messages or number formats
typically. For instance, in the United States people use the word
"color," while in the United Kingdom, it is "colour." The
java.util.Locale class is used to represent the
different locales.
For an excellent introduction to internationalization using the
Java language, you cannot do better than Joe Sam Shirah's "Java
internationalization basics" tutorial, which covers I18N in
great detail. |
getCurrencyInstance()
getInstance()
getIntegerInstance()
getNumberInstance()
getPercentInstance()
Which one you use depends on what type of number you want to display
(or accept for input). Each method offers two versions -- one that works
for the current locale and one that accepts a Locale as an
argument to possibly specify a different locale.
New to J2SE 1.4 with NumberFormat are the
getIntegerInstance() , getCurrency() , and
setCurrency() methods. Let's see how to use
NumberFormat by examining the new
getIntegerInstance() method. We'll explore the get/set
currency methods later.
The basic process of using NumberFormat is to get an
instance and use it. Picking the right instance does require some thought.
Typically, you don't want to use the general getInstance() or
getNumberInstance() versions because you don't know exactly
what you are going to get. Instead, you'd use something like
getIntegerInstance() because you know you want to display
something as an integer, without any decimal amount. This is demonstrated
in Listing 1, where we display the number 54321 in formats appropriate for
the United States and Germany: Listing 1. Working
with NumberFormat
import java.text.*;
import java.util.*;
public class IntegerSample {
public static void main(String args[]) {
int amount = 54321;
NumberFormat usFormat =
NumberFormat.getIntegerInstance(Locale.US);
System.out.println(usFormat.format(amount));
NumberFormat germanFormat =
NumberFormat.getIntegerInstance(Locale.GERMANY);
System.out.println(germanFormat.format(amount));
}
}
|
Running the code produces the output in Listing 2. Notice the comma for
the first (U.S.) format and the period separator for the second (German)
formatted number. Listing 2. NumberFormat
output
Learning to iterate through the
characters in DecimalFormat While NumberFormat
is abstract and you work with instances of it through various methods like
getIntegerInstance() , the DecimalFormat class
offers a concrete version of that class. You can explicitly specify the
character pattern for how you want positive, negative, fractional, and
exponential numbers to appear. If you don't like the predefined formats
for the different locales, you can create your own. (Under the covers,
DecimalFormat is probably what NumberFormat is
using.) The basic DecimalFormat functionality hasn't changed
with the 1.4. release of the J2SE platform. What has changed is the
addition of the formatToCharacterIterator() ,
getCurrency() , and setCurrency() methods.
We'll take a quick look at the new
formatToCharacterIterator method and its associated
NumberFormat.Field class. J2SE 1.4 introduces the concept of
a CharacterIterator , which allows you to traverse
bi-directionally through text. With
formatToCharacterIterator , you get its subinterface
AttributedCharacterIterator , which allows you to find out
information about that text. In the case of DecimalFormat ,
those attributes are keys from NumberFormat.Field . By using
AttributedCharacterIterator , you literally can build up your
own string output from the generated results. Listing 3 uses a percent
instance to show you a simple demonstration: Listing
3. Working with formatToCharacterIterator()
import java.text.*;
import java.util.*;
public class DecimalSample {
public static void main(String args[]) {
double amount = 50.25;
NumberFormat usFormat = NumberFormat.getPercentInstance(Locale.US);
if (usFormat instanceof DecimalFormat) {
DecimalFormat decFormat = (DecimalFormat)usFormat;
AttributedCharacterIterator iter =
decFormat.formatToCharacterIterator(new Double(amount));
for (char c = iter.first();
c != CharacterIterator.DONE;
c = iter.next()) {
// Get map for current character
Map map = iter.getAttributes();
// Display its attributes
System.out.println("Char: " + c + " / " + map);
}
}
}
}
|
Listing 4 shows the program's output (after a little massaging to make
it more readable). Basically, the formatToCharacterIterator()
method works the same way as calling format() , but in
addition to formatting the output string, it tags each character in the
output with its attributes (for instance, is the character at position
X an integer?). The output of displaying 50.25 as a percentage is
"5,025%" for the U.S. locale. By examining the output, everything but the
"%" is an "integer," including the comma. Besides the numbers, the comma
is also flagged as a grouping separator and the percent sign is flagged as
a percent. The attributes of each digit are a java.util.Map ,
in which each attribute is shown as key=value , where both are
the same. In the case of a comma where multiple attributes are present,
the list is comma separated. Listing 4.
formatToCharacterIterator() output
Char: 5 / {java.text.NumberFormat$Field(integer)=
java.text.NumberFormat$Field(integer)}
Char: , / {java.text.NumberFormat$Field(grouping separator)=
java.text.NumberFormat$Field(grouping separator),
java.text.NumberFormat$Field(integer)=
java.text.NumberFormat$Field(integer)}
Char: 0 / {java.text.NumberFormat$Field(integer)=
java.text.NumberFormat$Field(integer)}
Char: 2 / {java.text.NumberFormat$Field(integer)=
java.text.NumberFormat$Field(integer)}
Char: 5 / {java.text.NumberFormat$Field(integer)=
java.text.NumberFormat$Field(integer)}
Char: % / {java.text.NumberFormat$Field(percent)=
java.text.NumberFormat$Field(percent)}
|
Deciding on messages based on a
value range and ChoiceFormat
ChoiceFormat is
another of those concrete subclasses of NumberFormat . Its
definition and behavior haven't changed with the 1.4 release.
ChoiceFormat doesn't really help you format numbers, but it
does allow you to customize the text associated with a value. In the
simplest case, imagine displaying an error message. If there is a single
reason for failure, you want to use the word "is." If there are two or
more reasons, you want to use the word "are." As Listing 5 illustrates,
ChoiceFormat allows you to map a range of values to different
text strings.
The ChoiceFormat class is typically used with the
MessageFormat class to produce concatenated messages in a
language-neutral format. Not shown here is the use of a
ResourceBundle (which is typically used with
ChoiceFormat ) to get those strings. See Resources
for information on how to work with resource bundles; specifically, the
"Java internationalization basics" tutorial provides an in-depth
discussion. Listing 5. Working with
ChoiceFormat
import java.text.*;
import java.util.*;
public class ChoiceSample {
public static void main(String args[]) {
double limits[] = {0, 1, 2};
String messages[] = {
"is no content",
"is one item",
"are many items"};
ChoiceFormat formats = new ChoiceFormat(limits, messages);
MessageFormat message = new MessageFormat("There {0}.");
message.setFormats(new Format[]{formats});
for (int i=0; i<5; i++) {
Object formatArgs[] = {new Integer(i)};
System.out.println(i + ": " + message.format(formatArgs));
}
}
}
|
Executing the program produces the output shown in Listing 6: Listing 6. ChoiceFormat output
0: There is no content.
1: There is one item.
2: There are many items.
3: There are many items.
4: There are many items.
|
Counting our money with
Currency The previously mentioned getCurrency()
and setCurrency() methods return an instance of the new
java.util.Currency class. This class provides access to the
ISO 4217 currency codes for various countries. While you've been able to
use getCurrencyInstance() with NumberFormat
since it was introduced, you could never get or display the different
currency symbols for a locale apart from their numeric display. With the
new Currency class, you easily can.
As previously mentioned, currency codes come from ISO 4217. By passing
in either the Locale for a country or the actual alphabetic
code for the currency, Currency.getInstance() will give you
back a valid Currency object. The getCurrency()
method of NumberFormat will do the same after creating a
currency instance for a specific locale. Listing 7 shows how to get a
currency instance and how to format a number to be displayed as currency.
Remember that these conversions are for display only. If you need to
convert amounts between currencies, do so before figuring out how to
display the values. Listing 7. Working with
getCurrencyInstance() and Currency
import java.text.*;
import java.util.*;
import java.awt.*;
import javax.swing.*;
public class CurrencySample {
public static void main(String args[]) {
StringBuffer buffer = new StringBuffer(100);
Currency dollars = Currency.getInstance("USD");
Currency pounds = Currency.getInstance(Locale.UK);
buffer.append("Dollars: ");
buffer.append(dollars.getSymbol());
buffer.append("\n");
buffer.append("Pound Sterling: ");
buffer.append(pounds.getSymbol());
buffer.append("\n-----\n");
double amount = 5000.25;
NumberFormat usFormat = NumberFormat.getCurrencyInstance(Locale.US);
buffer.append("Symbol: ");
buffer.append(usFormat.getCurrency().getSymbol());
buffer.append("\n");
buffer.append(usFormat.format(amount));
buffer.append("\n");
NumberFormat germanFormat =
NumberFormat.getCurrencyInstance(Locale.GERMANY);
buffer.append("Symbol: ");
buffer.append(germanFormat.getCurrency().getSymbol());
buffer.append("\n");
buffer.append(germanFormat.format(amount));
JFrame frame = new JFrame("Currency");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JTextArea ta = new JTextArea(buffer.toString());
JScrollPane pane = new JScrollPane(ta);
frame.getContentPane().add(pane, BorderLayout.CENTER);
frame.setSize(200, 200);
frame.show();
}
}
|
Unfortunately, the currency symbols returned for things like euros and
pounds are not the actual symbols, but are instead the three-digit
currency code (from ISO 4217). However, with
getCurrencyInstance() , the symbol is displayed, as shown in
Figure 1.
Figure 1. Seeing the currency symbols
Summary There is more
to globalizing your programs than just customizing the text messages.
While moving text messages into resource bundles is at least half the
work, don't forget about dealing with locale-centric numbers and currency
displays, too. Not everyone uses commas and periods in the same way as in
the United States for numeric display, and everyone has to deal with their
own currency specifics. While we don't have to rely on the old COBOL
picture strings like $$$.99, by using locale-specific
NumberFormat instances, you can make your programs more
internationalized.
Resources
|
|