ChoiceFormat: Numeric range formatting

Customize the formatting of numeric ranges with ChoiceFormat patterns for arrays, strings, and more

The Javadoc for the ChoiceFormat class states that ChoiceFormat "allows you to attach a format to a range of numbers" and is "generally used in a MessageFormat for handling plurals." This post describesjava.text.ChoiceFormat and provides some examples of applying it in Java code.

One of the most noticeable differences between ChoiceFormat and other "format" classes in the java.text package is that ChoiceFormat does not provide static methods for accessing instances of ChoiceFormat. Instead, ChoiceFormat provides two constructors that are used for instantiating ChoiceFormat objects. The Javadoc for ChoiceFormat highlights and explains this:

ChoiceFormat differs from the other Format classes in that you create a ChoiceFormatobject with a constructor (not with a getInstance style factory method). The factory methods aren't necessary because ChoiceFormat doesn't require any complex setup for a given locale. In fact, ChoiceFormat doesn't implement any locale specific behavior.

Constructing ChoiceFormat with two arrays

The first of two constructors provided by ChoiceFormat accepts two arrays as its arguments. The first array is an array of primitive doubles that represent the smallest value (starting value) of each interval. The second array is an array of Strings that represent the names associated with each interval. The two arrays must have the same number of elements because there is an assumed one-to-one mapping between the numeric (double) intervals and the Strings describing those intervals. If the two arrays do not have the same number of elements, the following exception is encountered.

Exception in thread "main" java.lang.IllegalArgumentException: Array and limit arrays must be of the same length.

The Javadoc for the ChoiceFormat(double[], String[]) constructor states that the first array parameter is named "limits", is of type double[], and is described as "limits in ascending order." The second array parameter is named "formats", is of type String[], and is described as "corresponding format strings." According to the Javadoc, this constructor "constructs with the limits and the corresponding formats."

Use of the ChoiceFormat constructor accepting two array arguments is demonstrated in the next code listing (the writeGradeInformation(ChoiceFormat) method and fredsTestScores variable will be shown later).


/** 
 * Demonstrate ChoiceFormat instantiated with ChoiceFormat 
 * constructor that accepts an array of double and an array 
 * of String. 
 */  
public void demonstrateChoiceFormatWithDoublesAndStringsArrays()  
{  
   final double[] minimumPercentages = {0, 60, 70, 80, 90};  
   final String[] letterGrades = {"F", "D", "C", "B", "A"};  
   final ChoiceFormat gradesFormat = new ChoiceFormat(minimumPercentages, letterGrades);  
   writeGradeInformation(fredsTestScores, gradesFormat);  
}  

The example above satisfies the expectations of the illustrated ChoiceFormat constructor. The two arrays have the same number of elements, the first (double[]) array has its elements in ascending order, and the second (String[]) array has its "formats" in the same order as the corresponding interval-starting limits in the first array.

The writeGradeInformation(ChoiceFormat) method referenced in the code snippet above demonstrates use of a ChoiceFormat instance based on the two arrays to "format" provided numerical values as Strings. The method's implementation is shown next.


/** 
 * Write grade information to standard output 
 * using the provided ChoiceFormat instance. 
 * 
 * @param testScores Test Scores to be displayed with formatting. 
 * @param gradesFormat ChoiceFormat instance to be used to format output. 
 */  
public void writeGradeInformation(  
   final Collection<Double> testScores,  
   final ChoiceFormat gradesFormat)  
{  
   double sum = 0;  
   for (final Double testScore : testScores)  
   {  
      sum += testScore;  
      out.println(testScore + " is a '" + gradesFormat.format(testScore) + "'.");  
   }  
   double average = sum / testScores.size();  
   out.println(  
        "The average score (" + average + ") is a '"  
      + gradesFormat.format(average) + "'.");  
}

The code above uses the ChoiceFormat instance provided to "format" test scores. Instead of printing a numeric value, the "format" prints the String associated with the interval that numeric value falls within. The next code listing shows the definition of fredsTestScores used in these examples.


private static List<Double> fredsTestScores;  
static  
{  
   final ArrayList<Double> scores = new ArrayList<>();  
   scores.add(75.6);  
   scores.add(88.8);  
   scores.add(97.3);  
   scores.add(43.3);  
   fredsTestScores = Collections.unmodifiableList(scores);  
}  

Running these test scores through the ChoiceFormat instance instantiated with two arrays generates the following output:


75.6 is a 'C'.
88.8 is a 'B'.
97.3 is a 'A'.
43.3 is a 'F'.
The average score (76.25) is a 'C'.

Constructing ChoiceFormat with a pattern String

The ChoiceFormat(String) constructor that accepts a String-based pattern may be more appealing to developers who are comfortable using String-based pattern with similar formatting classes such as DateFormat and DecimalFormat. The next code listing demonstrates use of this constructor. The pattern provided to the constructor leads to an instance of ChoiceFormat that should format the same way as theChoiceFormat instance created in the earlier example with the constructor that takes two arrays.


/** 
 * Demonstrate ChoiceFormat instantiated with ChoiceFormat 
 * constructor that accepts a String pattern. 
 */  
public void demonstrateChoiceFormatWithStringPattern()  
{  
   final String limitFormatPattern = "0#F | 60#D | 70#C | 80#B | 90#A";  
   final ChoiceFormat gradesFormat = new ChoiceFormat(limitFormatPattern);  
   writeGradeInformation(fredsTestScores, gradesFormat);  
}  

The writeGradeInformation method called here is the same as the one called earlier and the output is also the same (not shown here because it is the same).

ChoiceFormat behavior on the extremes and boundaries

The examples so far have worked well with test scores in the expected ranges. Another set of test scores will now be used to demonstrate some other features of ChoiceFormat. This new set of test scores is set up in the next code listing and includes an "impossible" negative score and another "likely impossible" score above 100.


private static List<Double> boundaryTestScores;  
static  
{  
   final ArrayList<Double> boundaryScores = new ArrayList<Double>();  
   boundaryScores.add(-25.0);  
   boundaryScores.add(0.0);  
   boundaryScores.add(20.0);  
   boundaryScores.add(60.0);  
   boundaryScores.add(70.0);  
   boundaryScores.add(80.0);  
   boundaryScores.add(90.0);  
   boundaryScores.add(100.0);  
   boundaryScores.add(115.0);  
   boundaryTestScores = boundaryScores;  
}  

When the set of test scores above is run through either of the ChoiceFormat instances created earlier, the output is as shown next.


-25.0 is a 'F '.
0.0 is a 'F '.
20.0 is a 'F '.
60.0 is a 'D '.
70.0 is a 'C '.
80.0 is a 'B '.
90.0 is a 'A'.
100.0 is a 'A'.
115.0 is a 'A'.
The average score (56.666666666666664) is a 'F '.

The output just shown demonstrates that the "limits" set in the ChoiceFormat constructors are "inclusive," meaning that those limits apply to the specified limit and above (until the next limit). In other words, the range of number is defined as greater than or equal to the specified limit. The Javadoc documentation forChoiceFormat describes this with a mathematical description:

X matches j if and only if limit[j] ≤ X < limit[j+1]

The output from the boundaries test scores example also demonstrates another characteristic ofChoiceFormat described in its Javadoc documentation: "If there is no match, then either the first or last index is used, depending on whether the number (X) is too low or too high." Because there is no match for -25.0 in the provided ChoiceFormat instances, the lowest ('F' for limit of 0) range is applied to that number lower than the lowest range. In these test score examples, there is no higher limit specified than the "90" for an "A", so all scores higher than 90 (including those above 100) are for "A". Let's suppose that we wanted to enforce the ranges of scores to be between 0 and 100 or else have the formatted result indicate "Invalid" for scores less than 0 or greater than 100. This can be done as shown in the next code listing.


/** 
 * Demonstrating enforcing of lower and upper boundaries 
 * with ChoiceFormat instances. 
 */  
public void demonstrateChoiceFormatBoundariesEnforced()  
{  
   // Demonstrating boundary enforcement with ChoiceFormat(double[], String[])  
   final double[] minimumPercentages = {Double.NEGATIVE_INFINITY, 0, 60, 70, 80, 90, 100.000001};  
   final String[] letterGrades = {"Invalid - Too Low", "F", "D", "C", "B", "A", "Invalid - Too High"};  
   final ChoiceFormat gradesFormat = new ChoiceFormat(minimumPercentages, letterGrades);  
   writeGradeInformation(boundaryTestScores, gradesFormat);  
  
   // Demonstrating boundary enforcement with ChoiceFormat(String)  
   final String limitFormatPattern = "-\u221E#Invalid - Too Low | 0#F | 60#D | 70#C | 80#B | 90#A | 100.0<Invalid - Too High";  
   final ChoiceFormat gradesFormat2 = new ChoiceFormat(limitFormatPattern);  
   writeGradeInformation(boundaryTestScores, gradesFormat2);  
}  

When the above method is executed, its output shows that both approaches enforce boundary conditions better.


-25.0 is a 'Invalid - Too Low'.
0.0 is a 'F'.
20.0 is a 'F'.
60.0 is a 'D'.
70.0 is a 'C'.
80.0 is a 'B'.
90.0 is a 'A'.
100.0 is a 'A'.
115.0 is a 'Invalid - Too High'.
The average score (56.666666666666664) is a 'F'.
-25.0 is a 'Invalid - Too Low '.
0.0 is a 'F '.
20.0 is a 'F '.
60.0 is a 'D '.
70.0 is a 'C '.
80.0 is a 'B '.
90.0 is a 'A '.
100.0 is a 'A '.
115.0 is a 'Invalid - Too High'.
The average score (56.666666666666664) is a 'F '.

The last code listing demonstrates using Double.NEGATIVE_INFINITY and \u221E (Unicode INFINITY character) to establish a lowest possible limit boundary in each of the examples. For scores above 100.0 to be formatted as invalid, the arrays-based ChoiceFormat uses a number slightly bigger than 100 as the lower limit of that invalid range. The String/pattern-based ChoiceFormat instance provides greater flexibility and exactness in specifying the lower limit of the "Invalid - Too High" range as any number greater than 100.0 using the less-than symbol (<).

Handling none, singular, and plural with ChoiceFormat

I opened this post by quoting the Javadoc stating that ChoiceFormat is "generally used in a MessageFormat for handling plurals," but have not yet demonstrated this common use in this post. I will demonstrate a portion of this (plurals without MessageFormat) very briefly here for completeness, but a much more complete explanation (plurals with MessageFormat) of this common usage of ChoiceFormat is available in the Java TutorialsHandling Plurals lesson (part of the Internationalization trail).

The next code listing demonstrates application of ChoiceFormat to handle singular and plural cases.


/** 
 * Demonstrate ChoiceFormat used for differentiation of 
 * singular from plural and none. 
 */  
public void demonstratePluralAndSingular()  
{  
   final double[] cactiLowerLimits = {0, 1, 2, 3, 4, 10};  
   final String[] cactiRangeDescriptions =  
      {"no cacti", "a cactus", "a couple cacti", "a few cacti", "many cacti", "a plethora of cacti"};  
   final ChoiceFormat cactiFormat = new ChoiceFormat(cactiLowerLimits, cactiRangeDescriptions);  
   for (int cactiCount = 0; cactiCount < 11; cactiCount++)  
   {  
      out.println(cactiCount + ": I own " + cactiFormat.format(cactiCount) + ".");  
   }  
}  

Running the example in the last code listing leads to output that is shown next.


0: I own no cacti.
1: I own a cactus.
2: I own a couple cacti.
3: I own a few cacti.
4: I own many cacti.
5: I own many cacti.
6: I own many cacti.
7: I own many cacti.
8: I own many cacti.
9: I own many cacti.
10: I own a plethora of cacti.

One final symbol supported by ChoiceFormat's pattern

One other symbol that ChoiceFormat pattern parsing recognizes for formatting strings from a generated numeric value is the \u2264 (). This is demonstrated in the next code listing and the output for that code that follows the code listing. Note that in this example the \u2264 works effectively the same as using the simpler # sign shown earlier.


/** 
 * Demonstrate using \u2264 in String pattern for ChoiceFormat 
 * to represent >= sign. Treated differently than less-than 
 * sign but similarly to #. 
 */  
public void demonstrateLessThanOrEquals()  
{  
   final String limitFormatPattern = "0\u2264F | 60\u2264D | 70\u2264C | 80\u2264B | 90\u2264A";  
   final ChoiceFormat gradesFormat = new ChoiceFormat(limitFormatPattern);  
   writeGradeInformation(fredsTestScores, gradesFormat);  
}  

Output:


75.6 is a 'C '.
88.8 is a 'B '.
97.3 is a 'A'.
43.3 is a 'F '.
The average score (76.25) is a 'C '.

Observations in review

In this section, I summarize some of the observations regarding ChoiceFormat made during the course of this post and its examples.

1 2 Page 1
Notice to our Readers
We're now using social media to take your comments and feedback. Learn more about this here.