4

So I need to read an integer from the stdin where the user may input 1.0, however since this is a double I wouldn't want to accept it. However when I try the method below the 1.0 is converted to 1 and is accepted. I would also like to accept 0001 as a possible integer input as 1.

    first_sentence_to_switch = 0;
    char buf[15]; // large enough
    int number;
    wrong_input = 0;

    scanf("%14s", buf); // read everything we have in stdin
    // printf("buffer: %s", buf);
    if (sscanf(buf, "%d", &number) == 1)
    {
      first_sentence_to_switch = number;
    }
    else
    {
      wrong_input = 1;
    }

3 Answers3

3

You can use the %n format option to tell how much was matched by an sscanf call to make sure there is no extra cruft on the line:

if (sscanf(buf, "%d %n", &number, &end) == 1 && buf[end] == 0) {
    .. ok
} else {
    .. not an integer or something else in the input (besides whitespace) after the integer

Note the space between the %d and %n to skip any whitespace that might exist at the end of the buffer (such as a newline if the input was read by fgets or getline)

Chris Dodd
  • 119,907
  • 13
  • 134
  • 226
  • 1
    Quick and to the point. May be worth a comment that the space between `"%d %n"` is there to gracefully handle whitespace you explain under the `else`. (it may be a bit subtle for a new C programmer to appreciate the connection) – David C. Rankin Dec 12 '21 at 01:42
  • Using `sscanf` to solve this problem has the disadvantage that the function has undefined behavior if the user enters a number that is so high that it is not representable as an `int` ([see §7.21.6.2 ¶10 of the ISO C11 standard](http://port70.net/~nsz/c/c11/n1570.html#7.21.6.2p10)). Therefore, in my opinion, the function `sscanf` should generally not be used for input validation. The function `strtol`, on the other hand, can handle and report this error condition reliably. I am still upvoting this answer though, because it is the simplest solution to OP's immediate problem. – Andreas Wenzel Dec 12 '21 at 03:49
1

How to read a whole line of input

The line

scanf("%14s", buf);

will never read a whole line of input. It will only read a single word of input (which can also consist of digits). For example, if the user enters invalid input such as

"39 jdsuoew"

on a single line, then it will only read the word "39" as input, leaving the rest of the line on the input stream. This means that your program will accept the input as valid, although it should probably be rejected in this case.

Even if the user only entered "39", then it will only read this number, but will leave the newline character on the input stream, which can cause trouble.

If you want to ensure that it reads the entire line, I recommend that you use the function fgets instead, as that function will always read a whole line of input (including the newline character), assuming that the size of the provided memory buffer is large enough to store the entire line.

char line[100];

//attempt to read one line of input
if ( fgets( line, sizeof line, stdin ) == NULL )
{
    fprintf( stderr, "Input error!\n" );
    exit( EXIT_FAILURE );
}

//search for newline character, to verify that entire line was read in
if ( strchr( line, '\n' ) == NULL )
{
    fprintf( stderr, "Line was too long for input buffer!\n" );
    exit( EXIT_FAILURE );
}

Note that the function strchr requires that you #include <string.h>. If, as you state in the comments section, you are not allowed to use that header file, then you will probably have to assume that the memory buffer was large enough for the entire line, without verifying it (which you are also doing in your code). Although it is possible to verify this without using the function strchr, I don't recommend doing this. If the buffer is made large enough, then it is unlikely (but still possible) for the line to not fit into the buffer.

Convert string to integer using strtol

After reading the input line into a memory buffer, you can either use the function sscanf or strtol to attempt to convert the integer to a number. I recommend that you use the function strtol, because the function sscanf has undefined behavior if the user enters a number that is too large to be represented as a long int, whereas the function strtol is able to report such an error condition reliably.

In order to convert the line that you read to an integer, you could simply call strtol like this:

long l;

l = strtol( line, NULL, 10 );

However, calling the function with the second argument set to NULL has the same problem as calling the function atoi: You have no way of knowing whether the input was successfully converted, or if a conversion error occured. And you also have no way of knowing how much of the input was successfully converted, and whether the conversion failed prematurely, for example due to the user entering the decimal point of a floating-point number.

Therefore, it is better to call the function like this:

long l;
char *p;

l = strtol( line, &p, 10 );

Now, the pointer p will point to the first character that was not successfully converted to a number. In the ideal case, it will be pointing to the newline character at the end of the line (or maybe the terminating null character if you are not using fgets). So you could verify that the whole line was converted, and that at least one character was converted, like this:

if ( p == line || *p != '\n' )
{
    printf( "Error converting number!\n" );
    exit( EXIT_FAILURE );
}

However, this is maybe a bit too strict. For example, if the user enters "39 " (with a space after the number), the input will be rejected. You probably would want to accept the input in this case. Therefore, instead of requiring that p is pointing to the newline character and thereby not accepting any other remaining characters on the line, you may want permit whitespace characters to remain in the line, like this:

if ( p == line )
{
    printf( "Error converting number!\n" );
    exit( EXIT_FAILURE );
}

while ( *p != '\n' )
{
    //verify that remaining character is whitespace character
    if ( !isspace( (unsigned char)*p ) )
    {
        printf( "Error converting number!\n" );
        exit( EXIT_FAILURE );
    }

    p++;
}

Note that you must #include <ctype.h> in order to use the function isspace.

Also, as previously stated, the advantage of using the function strtol over sscanf is that it can reliably report whether the number is too large or too small to be representable as a long int. If such an error condition occurs, it will set errno to ERANGE. Note that you must #include <errno.h> in order to use errno.

long l;
char *p;

errno = 0; //make sure that errno is not already set to ERANGE
l = strtol( line, &p, 10 );
if ( errno == ERANGE )
{
    printf( "Number out of range!\n" );
    exit( EXIT_FAILURE );
}

Code example of fgets and strtol

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>

int main( void )
{
    char line[100], *p;
    long l;

    //prompt user for input
    printf( "Please enter an integer: " );

    //attempt to read one line of input
    if ( fgets( line, sizeof line, stdin ) == NULL )
    {
        fprintf( stderr, "Input error!\n" );
        exit( EXIT_FAILURE );
    }

    //search for newline character, to verify that entire line was read in
    if ( strchr( line, '\n' ) == NULL )
    {
        fprintf( stderr, "Line was too long for input buffer!\n" );
        exit( EXIT_FAILURE );
    }

    //make sure that errno is not already set to ERANGE
    errno = 0;

    //attempt to convert input to integer
    l = strtol( line, &p, 10 );

    //verify that conversion was successful
    if ( p == line )
    {
        printf( "Error converting number!\n" );
        exit( EXIT_FAILURE );
    }

    //check for range error
    if ( errno == ERANGE )
    {
        printf( "Number out of range!\n" );
        exit( EXIT_FAILURE );
    }

    //verify that there are either no remaining characters, or that
    //all remaining characters are whitespace characters
    while ( *p != '\n' )
    {
        //verify that remaining character is whitespace character
        if ( !isspace( (unsigned char)*p ) )
        {
            printf( "Error converting number!\n" );
            exit( EXIT_FAILURE );
        }

        p++;
    }

    //print valid input
    printf( "Input is valid.\nYou entered: %ld\n", l );
}

This program has the following output:

Valid input:

Please enter an integer: 39
Input is valid.
You entered: 39

Junk after valid input on same line:

Please enter an integer: 39 jdsuoew
Error converting number!

Attempt to enter floating-point number instead of integer:

Please enter an integer: 1.0
Error converting number!

Attempt to enter number that is so large that it is not representable as a long int:

Please enter an integer: 10000000000000000000000000
Number out of range!
Andreas Wenzel
  • 22,760
  • 4
  • 24
  • 39
1

Since there could be a bunch of possible wrong inputs, you should probably look only for right ones: '1' and '0'.

'I would also like to accept 0001 ...'

I only assume from your explanation that you wouldn't want to accept something like: 0011

I would look from the end of buffer towards beginning. In another words: I'd look only for single '1' at the end of buffer and then only for '0' (zeros) until you reach the beginning of buf.

Everything else is a wrong input.

Since you arbitrarely choose buffer size, you could write something like:

#define BUFF_SZ 15
   ...
char buf[BUFF_SZ];
   ...
while (buf[++i]); // <-- to avoid measuring buffer size at runtime.

This is an example of code with a function that returns correct result:

#include <stdio.h>

int check_input (char *buf);    

int main()
{
   char buf[15]; // large enough

    scanf("%14s", buf);
    if (check_input(buf) == 0) {  printf("Wrong input!"); return(1); };

       ... input OK ...

    return (0);
}

// function returns:    1: on success,  0: on wrong input
int check_input (char *buf)
{
  int i=0;

    while (buf[++i]);   // it will stop counting when NULL char is found ..
                        // so it's like 'strlen(buff)' 
                        // without unnecessary including <string.h>
 
    // buffer is set at end so check if it ends with '1' ..                  
    if (buf[--i] != '1')     return (0);
    
    // now, check all buffer backwards to make sure that ALL of those are '0's..
    while ((--i) > 0) 
        if (buf[i] != '0')    return (0);

    return (1);
}

I've written most important part as a funtion so it would be more readable.

I hope that could help.