3

I am trying to write a program that counts several elements in a string. The first one of those being letters.

The assignment is part of the CS50 week 2 problem set, hence the libraries included.

Using the while condition I was able to count every single character, but the code stopped working once I´ve added isalnum (which checks if the character is alphanumerical).

What am I doing wrong?

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

int main(void) {
    string text = get_string("Text: ");
    printf("%s, \n", text);

    int letters = 0;
    while (text[letters] != '\0') {
        if (isalnum(text[letters])) {
            letters++;
        }
    }
    printf("%i \n", letters);
}
chqrlie
  • 131,814
  • 10
  • 121
  • 189
LauraT
  • 33
  • 1
  • 4
  • 5
    You are trying to use `letters` both as an index to your array, and to count how many letters you've come across. What happens when you come across a character that isn't a letter? – Christian Gibbons May 07 '20 at 20:02
  • 5
    You'll loop forever as soon as `isalnum(text[letters])` is false. The easiest thing to do is to use two `int`s one a counter for how many alpha characters you have, and the other as the current position in your string. – George May 07 '20 at 20:03
  • Also that you probably should write `return 0` though this is not related to the problem... – user12986714 May 07 '20 at 20:06
  • 3
    @user12986714 no. `main` is the exception. – 0___________ May 07 '20 at 20:28
  • Thank you very much for the answers! It finally clicked for me going through your responses on where the loop was supposed to end vs where it was ending. – LauraT May 07 '20 at 20:44
  • @LauraT: you can accept one of the answers by clicking on the grey checkmark below its score. – chqrlie May 19 '20 at 22:37

5 Answers5

3

Here is shown how a correct loop can be defined

size_t letters = 0;
for ( size_t i = 0; text[i] !='\0'; i++ )
{
    if ( isalnum( ( unsigned char )text[i] ) )
    {
       letters++;
    }
}

printf( "%zu\n", letters );

If you want to use a while loop then it can look for example like

size_t letters = 0;
size_t i = 0;
while ( text[i] !='\0' )
{
    if ( isalnum( ( unsigned char )text[i++] ) )
    {
       letters++;
    }
}

printf( "%zu\n", letters );

Pay attention to that the function isalnum detects letters and digits. If you need to count only letters then use the function isalpha.

Vlad from Moscow
  • 301,070
  • 26
  • 186
  • 335
  • I had completely forgotten about casting, thank you Vlad! – LauraT May 07 '20 at 20:49
  • 1
    @LauraT The casting issue is a good practice, but without it unless you are reading non-ASCII characters, then it won't have any impact on the operation of your code. It is a protection against a stray non-ASCII character in your input. Understand it for what it is. Your code wasn't misbehaving as a result of not casting to `unsigned char`. – David C. Rankin May 07 '20 at 21:06
2

If you want to count only letters:

size_t count_letters(const char *str)
{
    size_t count = 0;

    while(*str)
    {
        count += !!isalpha(*str++);
    }

    return count;
}

or if you prefer for loop

size_t count_letters_for_loop(const char *str)
{
    size_t count = 0;

    for(; *str; str++)
    {
        count += !!isalpha(*str);
    }

    return count;
}

As David suggested:

!! operation logically negates two times the value. As any logical operation gives 0 or 1, this double negation gives 1 for any non zero value or 0 for zero. It is needed as isalpha function Returns non-zero value if c is an alphabet, else it returns 0.

try to move such a logic to the separate functions. It is a very important skill in the C language

0___________
  • 60,014
  • 4
  • 34
  • 74
  • Thank you very much for the tip @P__J__! I intend to create separate functions for each element, but first I wanted to have a working letter counter, before I move it to its own function. – LauraT May 07 '20 at 20:47
  • 1
    Better explain the `!!` - no doubt that would not be intuitive... – David C. Rankin May 07 '20 at 21:00
1

You while loop has a flaw: you use the same variable to count the alphanumeric characters and to index the string. It only works if all characters are alphanumeric, otherwise you get an infinite loop because you stop incrementing letter.

You should use a for loop with an index i.

Also note that isalnum() all all functions from <ctype.h> are undefined for negative values, except EOF. On platforms where char is signed by default, some characters in the string may have negative values, causing undefined behavior if you pass them to isalnum(). You should cast char values as (unsigned char) to avoid this.

Here is a modified version.

#include <cs50.h>
#include <stdio.h>
#include <ctype.h>

int main(void) {
    string text = get_string("Text: ");
    printf("%s, \n", text);

    int letters = 0;
    for (int i = 0; text[i] != '\0'; i++) {
        if (isalnum((unsigned char)text[i])) {
            letters++;
        }
    }
    printf("%i\n", letters);
    return 0;
}
chqrlie
  • 131,814
  • 10
  • 121
  • 189
1

As you have found, since not all characters in your string are guaranteed to be alpha-number characters, your use of letters as a counter and an index is flawed. When you encounter a character that is not alpha and not a number, if (isalnum(text[letters])) tests false and letters is never incremented resulting in an infinite loop at that point. (you test the same character again on the next iteration -- with the same result -- and the scenery never changes....)

As all of the other very good and very correct answers suggest, just use a separate loop counter variable (or a pointer) and increment that to iterate over your string.

One other thing you might do to validate your logic (while outputting the isalnum() characters) is simply to do away with reprinting the original string with printf (you have that right in front of you from your entry), and instead output the characters that match your criteria. For example:

    for (int i = 0; text[i]; i++) {
        if (isalnum((unsigned char)text[i])) {
            putchar (text[i]);
            letters++;
        }
    }

That is just a slight variation on your output which serves double-duty providing your output and also providing confirmation of each character that matched the criteria used.

Also note, there is no need to #include <string.h> as there are no functions requiring its inclusion used in your code. With those changes a quick example could be:

#include <cs50.h>
#include <stdio.h>
#include <ctype.h>

int main(void) {

    int letters = 0;
    string text = get_string("Text: ");

    for (int i = 0; text[i]; i++) {
        if (isalnum((unsigned char)text[i])) {
            putchar (text[i]);
            letters++;
        }
    }

    printf (", %d\n", letters);
}

Example Use/Output

$ ./bin/ltrcountcs50-1
Text: 123.abc-456_def_*.*_789
123abc456def789, 15

Moving Count Into A Function

You can move your counting of alpha-num characters into a function easily. One benefit of passing your string to a function is the pointer received by the function is a copy of the pointer from main() (C is pass-by-value). That allows you to simply iterate with the parameter and return the count of alpha-num characters, e.g.

int countalnum (const char *s)
{
    int letters = 0;

    while (*s)
        if (isalnum((unsigned char)*s++))
            letters++;

    return letters;
}

(note: you must pass the parameter as const char * to use a pointer to constant char. You cannot use const string using the cs50 typedef. If you will not be changing the value passed in the function, passing as const allows the compiler to make optimization it would not otherwise be able to make)

With the function above, your code reduces to:

#include <cs50.h>
#include <stdio.h>
#include <ctype.h>

int countalnum (const char *s)
{
    int letters = 0;

    while (*s)
        if (isalnum((unsigned char)*s++))
            letters++;

    return letters;
}

int main(void) {

    string text = get_string("Text: ");

    printf ("%s, %d\n", text, countalnum(text));
}

Example Use/Output

$ ./bin/ltrcountcs50-1
Text: 123.abc-456_def_*.*_789
123.abc-456_def_*.*_789, 15

But here, using the function results in printing the entire original string in main(). You can adjust the output as desired.

David C. Rankin
  • 81,885
  • 6
  • 58
  • 85
0

You said being letters, and then you use isalnum? so numbers will be counted as letters in your case and you increment the value of the letters which the loop counts on to iterate over the string only when it is a letter so that leads to infinite loop when the condition is false here is a solution:

#include <stdio.h>
#include <ctype.h>

int countLetters(char *str) {
  int letters = 0, ind = 0;
  while(str[ind] != '\0') 
    isalpha((unsigned char)str[ind++]) && letters++;
  return letters;
}

int main(void) {
  char text[100];
  fgets(text, sizeof(text), stdin);
  printf("%i", countLetters(text));
  return 0;
}
// input: hello 15-p */x
// output: 7
Saadi Toumi Fouad
  • 2,779
  • 2
  • 5
  • 18