Scala 3: How to format numbers and currency

This is an excerpt from the Scala Cookbook, 2nd Edition. This is Recipe 3.8, Formatting Numbers and Currency.

Scala 3 Problem

You want to format numbers or currency to control decimal places and separators (commas and decimals), typically for printed output.

Solution

For basic number formatting, use the f string interpolator. For other needs, such as adding commas and working with locales and currency, use instances of the java.text.NumberFormat class:

NumberFormat.getInstance           // general-purpose numbers (floating-point)
NumberFormat.getIntegerInstance    // integers
NumberFormat.getCurrencyInstance   // currency
NumberFormat.getPercentInstance    // percentages

The NumberFormat instances can also be customized for locales.

The f string interpolator

The f string interpolator, which is discussed in detail in Substituting Variables Into Strings, provides simple number formatting capabilities:

val pi = scala.math.Pi   // Double = 3.141592653589793
println(f"${pi}%1.5f")   // 3.14159

A few more examples demonstrate the technique:

// floating-point
f"${pi}%1.2f"    // String = 3.14
f"${pi}%1.3f"    // String = 3.142
f"${pi}%1.5f"    // String = 3.14159
f"${pi}%6.2f"    // String = "  3.14"
f"${pi}%06.2f"   // String = 003.14

// whole numbers
val x = 10_000
f"${x}%d"        // 10000
f"${x}%2d"       // 10000
f"${x}%8d"       // "   10000"
f"${x}%-8d"      // "10000   "

If you prefer the explicit use of the format method that’s available to strings, write the code like this instead:

"%06.2f".format(pi)   // String = 003.14

Commas, locales, and integers

When you want to format integer values, such as by adding commas in a locale like the United States, use NumberFormat’s getIntegerInstance method:

import java.text.NumberFormat
val formatter = NumberFormat.getIntegerInstance

formatter.format(10_000)      // String = 10,000
formatter.format(1_000_000)   // String = 1,000,000

That result shows commas because of my locale (near Denver, Colorado), but you can set a locale with getIntegerInstance and the Locale class:

import java.text.NumberFormat
import java.util.Locale

val formatter = NumberFormat.getIntegerInstance(Locale.GERMANY)
formatter.format(1_000)       // 1.000
formatter.format(10_000)      // 10.000
formatter.format(1_000_000)   // 1.000.000

Commas, locales, and floating-point values

You can handle floating-point values with a formatter returned by getInstance:

val formatter = NumberFormat.getInstance

formatter.format(12.34)          // 12.34
formatter.format(1_234.56)       // 1,234.56
formatter.format(1_234_567.89)   // 1,234,567.89

You can also set a locale with getInstance:

val formatter = NumberFormat.getInstance(Locale.GERMANY)

formatter.format(12.34)          // 12,34
formatter.format(1_234.56)       // 1.234,56
formatter.format(1_234_567.89)   // 1.234.567,89

Currency

For currency output, use the getCurrencyInstance formatter. This is the default output in the United States:

val formatter = NumberFormat.getCurrencyInstance

formatter.format(123.456789)     // $123.46
formatter.format(12_345.6789)    // $12,345.68
formatter.format(1_234_567.89)   // $1,234,567.89

Use a Locale to format international currency:

import java.util.{Currency, Locale}

val deCurrency = Currency.getInstance(Locale.GERMANY)
val deFormatter = java.text.NumberFormat.getCurrencyInstance
deFormatter.setCurrency(deCurrency)

deFormatter.format(123.456789)     // €123.46
deFormatter.format(12_345.6789)    // €12,345.68
deFormatter.format(1_234_567.89)   // €1,234,567.89

If you don’t use a currency library you’ll probably want to use BigDecimal, which also works with getCurrencyInstance. Here’s the default output in the United States:

import java.text.NumberFormat
import scala.math.BigDecimal.RoundingMode

val a = BigDecimal("10000.995")            // BigDecimal = 10000.995
val b = a.setScale(2, RoundingMode.DOWN)   // BigDecimal = 10000.99

val formatter = NumberFormat.getCurrencyInstance
formatter.format(b)                        // String = $10,000.99

Here are two examples of BigDecimal values that uses a locale:

import java.text.NumberFormat
import java.util.Locale
import scala.math.BigDecimal.RoundingMode

val b = BigDecimal("1234567.891").setScale(2, RoundingMode.DOWN)
  // result: BigDecimal = 1234567.89

val deFormatter = NumberFormat.getCurrencyInstance(Locale.GERMANY)
deFormatter.format(b)     // String = 1.234.567,89 €

val ukFormatter = NumberFormat.getCurrencyInstance(Locale.UK)
ukFormatter.format(b)     // String = £1,234,567.89

Custom formatting patterns

You can also create your own formatting patterns with the DecimalFormat class. Just create the pattern you want, then apply the pattern to a number using the format method, as shown in these examples:

import java.text.DecimalFormat

val df = DecimalFormat("0.##")
df.format(123.45)           // 123.45 (type = String)
df.format(123.4567890)      // 123.46
df.format(.1234567890)      // 0.12
df.format(1_234_567_890)    // 1234567890

val df = DecimalFormat("0.####")
df.format(.1234567890)      // 0.1235
df.format(1_234.567890)     // 1234.5679
df.format(1_234_567_890)    // 1234567890

val df = DecimalFormat("#,###,##0.00")
df.format(123)              // 123.00
df.format(123.4567890)      // 123.46
df.format(1_234.567890)     // 1,234.57
df.format(1_234_567_890)    // 1,234,567,890.00

See the Java DecimalFormat class for more formatting pattern characters (and a warning that, in general, you shouldn’t create a direct instance of DecimalFormat).

Locales

The java.util.Locale class has three constructors:

Locale(String language)
Locale(String language, String country)
Locale(String language, String country, String data)

It also includes more than a dozen static instances for locales like CANADA, CHINA, FRANCE, GERMAN, JAPAN, UK, US, and more. For countries and languages that don’t have Locale constants, you can still specify them using a language or a pair of language/country strings. For example, per Oracle’s JDK 10 and JRE 10 Supported Locales page, locales in India can be specified like this:

Locale("hi-IN", "IN")
Locale("en-IN", "IN")

Here are a few other examples:

Locale("en-AU", "AU")   // Australia
Locale("pt-BR", "BR")   // Brazil
Locale("es-ES", "ES")   // Spain

These examples demonstrate how the first India locale is used:

// India
import java.util.{Currency, Locale}

val indiaLocale = Currency.getInstance(Locale("hi-IN", "IN"))
val formatter = java.text.NumberFormat.getCurrencyInstance
formatter.setCurrency(indiaLocale)

formatter.format(123.456789)    // ₹123.46
formatter.format(1_234.56789)   // ₹1,234.57

With all of the get*Instance methods of NumberFormat you can also set a default locale:

import java.text.NumberFormat
import java.util.Locale

val default = Locale.getDefault
val formatter = NumberFormat.getInstance(default)

formatter.format(12.34)          // 12.34
formatter.format(1_234.56)       // 1,234.56
formatter.format(1_234_567.89)   // 1,234,567.89

Discussion

This recipe falls back to the Java approach for printing currency and other formatted numeric fields, though of course the currency solution depends on how you handle currency in your applications. In my work as a consultant, I’ve seen most companies handle currency using the Java BigDecimal class, and others create their own custom currency classes, which are typically wrappers around BigDecimal.

JSR-354, Money and Currency API defines a proposed API that hasn’t made it into the Java SDK at the time of this writing. You can also use libraries like Joda Money.