Translating numerical scores to letter grades: examples of using Java's if/else construct.

Acknowledgement: This document is an adaptation/expansion of material from Section 4.1 of Building Java Programs, by Reges and Stepp.

Consider the problem of developing a method that, given a numerical score representing a grade in the interval [0, 100], returns the corresponding letter grade. The assumption is that a score in the interval [90, 100] translates to A, a score in the interval [80, 90) translates to B, a score in the interval [70, 80) translates to C, a score in the interval [60, 70) translates to D, and a score in the interval [0, 60) translates to F.

Here is our first attempt:

// This version is WRONG!!
public static char scoreToLetterGrade(int score) {
   
   char result = 'F';
   if (score >= 90) { result = 'A'; }
   if (score >= 80) { result = 'B'; }
   if (score >= 70) { result = 'C'; }
   if (score >= 60) { result = 'D'; }
   return result;
}

Why is this wrong? Because its effect is to return 'D' when the score is 60 or greater and to return 'F' otherwise.

How can we fix it? There are several ways. One is to return the result as soon as we know it:

public static char scoreToLetterGrade(int score) {
   
   if (score >= 90) { return 'A'; }
   if (score >= 80) { return 'B'; }
   if (score >= 70) { return 'C'; }
   if (score >= 60) { return 'D'; }
   return 'F';
}

Many programmers would find this to be a quite acceptable solution. For those purists among us who cringe at the thought of a method having multiple exit points, the following approach, in which the tests are done in the opposite order, is better:

public static char scoreToLetterGrade(int score) {
   
   char result = 'F';
   if (score >= 60) { result = 'D'; }
   if (score >= 70) { result = 'C'; }
   if (score >= 80) { result = 'B'; }
   if (score >= 90) { result = 'A'; }
   return result;
}

If score were, say, 93, five assigments would be made to result, but only the last one "would matter".

A different way to fix the problem is to use else clauses so as to avoid testing any condition that is weaker than one that we already know to be true. (For example, there is no point in testing whether score is at least 80 if we have already ascertained that it is at least 90.)

public static char scoreToLetterGrade(int score) {
   
   char result;
   if (score >= 90)
      { result = 'A'; }
   else {
      if (score >= 80) 
         { result = 'B'; }
      else {
         if (score >= 70) 
            { result = 'C'; }
         else {
            if (score >= 60)
               { result = 'D'; }
            else {
               result = 'F';
            }
         }
      }
   }
   return result;
}

The exact same code, formatted in the following way, is judged by some programmers to be more easily readable:

public static char scoreToLetterGrade(int score) {
   
   char result;
   if (score >= 90)
      { result = 'A'; }
   else if (score >= 80) 
      { result = 'B'; }
   else if (score >= 70) 
      { result = 'C'; }
   else if (score >= 60)
      { result = 'D'; }
   else 
      { result = 'F'; }
   return result;
}

Compressing it even more, we get

public static char scoreToLetterGrade(int score) {
   
   char result;
   if (score >= 90) { result = 'A'; }
   else if (score >= 80) { result = 'B'; }
   else if (score >= 70) { result = 'C'; }
   else if (score >= 60) { result = 'D'; }
   else { result = 'F'; }
   return result;
}

Which of these alternatives is best is a subjective question.

As suggested by the initial discussion, essentially we have five cases:

  1. 90 ≤ score
  2. 80 ≤ score < 90
  3. 70 ≤ score < 80
  4. 60 ≤ score < 70
  5. score < 60

Using the boolean conjunction (i.e.., and) operator &&, we can write the method as follows:

public static char scoreToLetterGrade(int score) {
   
   char result = ' ';
   if (90 <= score) { result = 'A'; }
   if (80 <= score && score < 90) { result = 'B'; }
   if (70 <= score && score < 80) { result = 'C'; }
   if (60 <= score && score < 70) { result = 'D'; }
   if (score < 60) { result = 'F'; }
   return result;
}

Initializing result above is necessary, because otherwise the Java compiler will reject the program on the grounds that it is possible for result not to have been assigned a value before the method tries to return its value. Using the details of the conditions being tested (along with our knowledge of numbers), we know that that cannot happen, but, in general, a sequence of if statements may be such that all the conditions evaluate to false, which, in a segment of code of the same structure as above, would lead to result never having a value assigned to it. So the compiler is reasonable in demanding that the structure of the code (without relying upon the details of the conditions being tested) guarantees that the "return value" exists.

To avoid evaluating (sure-to-be-false) conditions unnecessarily, we could employ else-es in the above, yielding

public static char scoreToLetterGrade(int score) {
   
   char result;
   if (90 <= score) { result = 'A'; }
   else if (80 <= score && score < 90) { result = 'B'; }
   else if (70 <= score && score < 80) { result = 'C'; }
   else if (60 <= score && score < 70) { result = 'D'; }
   else { result = 'F'; }   // score < 60 must hold
   return result;
}