Alternative to this switch statement in C

Viewed 248

The code which I posted below is a part of my whole program. The rest of the code is not related to this question. Is there any solution other than using this long switch statement? The main objective is to shorten the code. The if statement is used simply to capitalize lowercase letters.

int POINTS[] = { 1, 3, 3, 2, 1, 4, 2, 4, 1, 8, 5, 1, 3, 1, 1, 3, 10, 1, 1, 1, 1, 4, 4, 8, 4, 10 };

int compute_score(string word)
{
  // TODO: Compute and return a score for string
  int word_length = strlen(word);
  int i, sum = 0;

  for (i = 0; i < word_length; i++)
  {
    //This if statement is used to captalise lower-case letters.
    if (islower(word[i]))
    {
      word[i] = toupper(word[i]);
    }

    //This switch statement is the key in this program. It will check 
    //each letter of word and then modify variable sum according to it.
    switch (word[i])
    {
    case 'A':
      sum = sum + POINTS[0];
      break;
    case 'B':
      sum = sum + POINTS[1];
      break;
    case 'C':
      sum = sum + POINTS[2];
      break;
    case 'D':
      sum = sum + POINTS[3];
      break;
    case 'E':
      sum = sum + POINTS[4];
      break;
    case 'F':
      sum = sum + POINTS[5];
      break;
    case 'G':
      sum = sum + POINTS[6];
      break;
    case 'H':
      sum = sum + POINTS[7];
      break;
    case 'I':
      sum = sum + POINTS[8];
      break;
    case 'J':
      sum = sum + POINTS[9];
      break;
    case 'K':
      sum = sum + POINTS[10];
      break;
    case 'L':
      sum = sum + POINTS[11];
      break;
    case 'M':
      sum = sum + POINTS[12];
      break;
    case 'N':
      sum = sum + POINTS[13];
      break;
    case 'O':
      sum = sum + POINTS[14];
      break;
    case 'P':
      sum = sum + POINTS[15];
      break;
    case 'Q':
      sum = sum + POINTS[16];
      break;
    case 'R':
      sum = sum + POINTS[17];
      break;
    case 'S':
      sum = sum + POINTS[18];
      break;
    case 'T':
      sum = sum + POINTS[19];
      break;
    case 'U':
      sum = sum + POINTS[20];
      break;
    case 'V':
      sum = sum + POINTS[21];
      break;
    case 'W':
      sum = sum + POINTS[22];
      break;
    case 'X':
      sum = sum + POINTS[23];
      break;
    case 'Y':
      sum = sum + POINTS[24];
      break;
    case 'Z':
      sum = sum + POINTS[25];
      break;
    }
  }
  return sum;
}
9 Answers

I'm guessing that you want something like this and simply haven't bothered to set all the indices in your switch statement correctly:

if ('A' <= word[i] && word[i] <= 'Z') {
    sum += POINTS[word[i] - 'A'];
}

The trick is that chars are just numbers as well, so you can compare them to see if the character is an uppercase Latin letter, and subtract 'A' to get the 0-based position in the POINTS array.

However, note that this only works if the character set on your target system contains the letters A through Z contiguously in alphabetical order. By far most systems these days use (a superset of) ASCII, where the above code works fine. But if a more exotic character set such as EBCDIC is used, you will need a different approach, such as a lookup table, a hash table, or... a big switch statement.

A direct and fast alternative is to assign a point value to each char.

This also makes for a clear relationship between the letter and value.

#include <limits.h>

// Array elements not explicitly initialized are 0.
unsigned char POINTS[UCHAR_MAX + 1] = {
    ['A'] = 1, ['a'] = 1, 
    ['B'] = 3, ['b'] = 3, 
    ['C'] = 3, ['c'] = 3,
    /* 23 more pairs */ };  

int compute_score(string word) {
  sum = 0;
  while (*word) {
    sum += POINTS[(unsigned char) *word];
    word++;
  }
  return sum;
}

Or with only using 26 explicit initializers.

#include <ctype.h>
#include <limits.h>

unsigned char POINTS[UCHAR_MAX + 1] = {['A'] = 1, ['B'] = 3, ['C'] = 3 /* 23 more */ };

int compute_score(string word) {
  sum = 0;
  while (*word) {
    sum += POINTS[toupper((unsigned char) *word)];
    word++;
  }
  return sum;
}

As 1, 3, 3, ... 8, 4, 10 are apparently Scrabble tile values, consider that code may want to handle other versions of the game that include letters outside A-Z. With a complete table look-up, only a table data update is needed and not a functional code one.

Works with ASCII, EBDCIC, ...

Note: On rare machines, the range of unsigned char is large (e.g. "byte" size could be 64-bit) rendering this approach impractical. So maybe add the following for detection.

_Static_assert(UCHAR_MAX < 1024, "char size too large");

Just guessing that you really want to add e.g. POINTS[15] for O, rather than POINTS[1] for every letter past E, since your POINTS array is 26 elements long. If that's the case, then you can simply do:

if (word[i] >= 'A' && word[i] <= 'Z')
    sum += POINTS[word[i] - 'A'];

This will work if your encoding is ASCII (very likely). If it's something else like EBCDIC where the letters aren't consecutive you'll need to use a lookup table.

Beyond the lengthy switch statement, there are some issues in your code:

  • the switch cases for 'F' and greater seem to use an incorrect offset into the POINTS array.
  • the function compute_score() uppercases its argument string as a side effect. This is not very good practice. Such a side effect would have undefined behavior on a string literal for example.
  • If you intend for the string to be uppercased, you should use a function dedicated to that and pass the uppercased string to a simplified version of compute_score(). Alternately, you can easily modify compute_score() so it does not have a side effect on the argument string.
  • You call strlen() on the string and run a second loop iterating on all characters in the string. You could simply iterate once, testing if word[i] == '\0' to detect the end of string.
  • islower() and toupper() like all functions from <ctype.h> are only defined for argument values of the type unsigned char and the special negative value EOF. They have undefined behavior for negative char values which may occur in the string on platforms where char is a signed type. To avoid this undefined behavior, you should cast char values as (unsigned char).

Here is a simplified yet portable version:

#include <ctype.h>
#include <string.h>

int POINTS[] = { 1, 3, 3, 2, 1, 4, 2, 4, 1, 8, 5, 1, 3, 1, 1, 3, 10, 1, 1, 1, 1, 4, 4, 8, 4, 10 };

int compute_score(const char *word) {
    int sum = 0;

    while (*word != '\0') {
        int c = toupper((unsigned char)*word++);
        if ('Z' - 'A' == 25) {  /* this test is optimized out at compile time */
            /* simple and efficient code if all uppercase letters are contiguous (ASCII) */
            if (c >= 'A' && c <= 'Z') {
                sum += POINTS[c - 'A'];
            }
        } else {
            /* portable code for non-ASCII character sets such as EBCDIC */
            static const char letters[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
            const char *p = strchr(letters, c);
            if (p != NULL) {
                sum += POINTS[p - letters];
            }
        }
    }
    return sum;
}

It seems POINTS[1] is added to sum for all upper-case alphabet case other than B, C, D, and E. Therefore, you can combine them.

            switch (word[i])
            {
                case 'B':
                    sum = sum + POINTS[2];
                    break;
                case 'C':
                    sum = sum + POINTS[3];
                    break;
                case 'D':
                    sum = sum + POINTS[4];
                    break;
                case 'E':
                    sum = sum + POINTS[5];
                    break;
                default:
                    if (isupper(word[i]))
                    {
                        sum = sum + POINTS[1];
                    }
                    break;
            }

If you can assume that the character code for A to Z are continuous (like ASCII and UTF-8, unlike EBCDIC):

if ('A' <= word[i] && word[i] <= 'E') {
    sum = sum + POINTS[(word[i] - 'A') + 1];
} else if ('F' <= word[i] && word[i] <= 'Z') {
    sum = sum + POINTS[1];
}

You can simplify your switch by converting word[i] into an int to take advantage of the ascii table. According to the Ascii table the letters 'A' to 'Z' are represented by the number from 65 to 90. Therefore, because you array of points starts at 0 we need to shift 65 elements to the left from the value that will be return by (int)word[i], namely:

int index = (int)word[i] - 'A';

In your code:

sum = sum + points[index];

So the code would look like the following :

int POINTS[] = {1, 3, 3, 2, 1, 4, 2, 4, 1, 8, 5, 1, 3, 1, 1, 3, 10, 1, 1, 1, 1, 4, 4, 8, 4, 10};
int compute_score(string word){
    int word_length = strlen(word);
    int sum = 0;
    for (int i = 0; i < word_length ; i++){
        if (islower(word[i]))
            word[i] = toupper(word[i]);
        int index = (int)word[i] - 'A';
        if ('A' <= word[i] && word[i] <= 'Z') 
            sum += POINTS[index];
    }
    return sum;
}

Bear in mind, however, and quoting Eric Postpischil:

The character codes used in C implementations do not necessarily have all the letters consecutively. ASCII does and is very common, but not required by the standard.

Hence, this solution will work if your encoding is ASCI.

You can create array of strings and define symbols that you want to use for each case:

  const int NUM_POINTS = 5;
  const char *strings[NUM_POINTS];

  strings[0] = "ABC";
  strings[1] = "XYZ";
  ...

  for (int p = 0; p < NUM_POINTS; ++p)
  {
      if (strchr(strings[p], word[i]) != NULL)
      {
          sum += POINTS[p];
          break;
      }
  }

I assume that your "POINTS" array is meant to be points-per-letter. In other words, the first element is points for an 'A', the second is points for a 'B', etc.

Each character has a numeric ASCII value, and you can use characters in arithmetic. The ASCII values are shown below (borrowed from here).

(This only applies if your encoding is ASCII, which is usually the case except for scenarios like if the data was read from a UTF-16 file or you are compiling for a retro computer that predates ASCII.)

enter image description here

So you need a way to convert the letter 'A' to the number '0' (the first item in the list is index 0), the letter 'B' to the number '1', etc.

You can do that by simply subtracting a set value from the letter. Notice that a capital 'A' has value 65, 'B' is 66, 'C' is 67, etc.

So simply subtract the value of 'A'.

const int POINTS[] = {1, 3, 3, 2, 1, 4, 2, 4, 1, 8, 5, 1, 3, 1, 1, 3, 10, 1, 1, 1, 1, 4, 4, 8, 4, 10};

int compute_score(const char *word)
{
    int score = 0;

    // iterate over each character in the word.
    for (int i = 0; word[i] != NULL; i++)
    {
        // if this is a capital letter
        if (word[i] >= 'A' && word[i] <= 'Z')
        {
            // if word[i] is 'A', index is 0. if 'B', then 1. if 'C', then 2. etc.
            int index = word[i] - 'A';
            score += POINTS[index];
        }
        // if this is a lowercase letter
        else if (word[i] >= 'a' && word[i] <= 'z')
        {
            // if word[i] is 'a', index is 0. if 'b', then 1. if 'c', then 2. etc.
            int index = word[i] - 'a';
            score += POINTS[index];
        }
    }

    return score;
}

You could swap the switch with this for loop + if condition:

char cases[4] = "BCDE";
int notFound = 1;

for(int j = 0; j < 4; j++) {
    
    if( word[i] == cases[j] ) {
    
        sum += POINTS[j+2];
        notFound = 0;
        break;
}

if( notFound ) {
    
    sum += POINTS[1];
}

If you wanted the cases from 'F' to 'Z' to add from POINTS[6] to POINTS[26], the algorithm becomes even easier:

char cases[26] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

for(int j = 0; j < 26; j++) {
    
    if( word[i] == cases[j] ) {
    
        sum += POINTS[j+1];
        break;
}
Related