Question

Why doesn't scanf("%d %d") complain about decimal input?

Why when I run this code:

#include <stdio.h>

int main () {
    int a, b;
    if (scanf("%d %d", &a, &b) == 2)
        printf("%d %d", a, b);
    else 
        printf("Something went wrong");
    return 0;
}

and input for example:

1 1.5

output is:

1 1

Why scanf reads both numbers before '.' and ignores '.5'? How do you check that the last number is not a float and string ends?

OS: MacOS/linux Compiler: gcc

I just want runs something like above code

input:

1 1.5234

(some float number)

output:

Something went wrong
 3  109  3
1 Jan 1970

Solution

 2

fgets: Reads the entire line of input, ensuring that you capture everything the user types until they press Enter. This avoids scanf's limitation of stopping at the first non-matching character.

sscanf: Parses the input stored in input. It tries to read two integers (%d %d). After that, it tries to read one more character (%c) which should be whitespace (like a space or newline).

Validation:

if (sscanf(input, "%d %d %c", &a, &b, &leftover) == 2)
Checks if exactly two integers were successfully read. isspace(leftover): Checks if the character after the second integer is whitespace (ensuring there are no additional characters like a decimal point).

Output: If the input format matches (scanf successfully reads two integers followed by whitespace), it prints the integers. Otherwise, it prints an error message.

2024-07-15
srinithi mahalakshmi A

Solution

 2

The short answer is that you can't really figure out what's going on with scanf. Its man page unhelpfully states:

It is very difficult to use these functions correctly, and it is preferable to read entire lines with fgets(3) or getline(3) and parse them later with sscanf(3) or more specialized functions such as strtol(3).

It is very easy for scanf (and fscanf) to get out of sync with the input. I use fgets and sscanf. With those I can greatly improve your input validation as follows:

#include <stdio.h>
#include <stdlib.h>

int main () {
    char iline[80];
    int a, b;
    char extra[2];
    int result;

    if (fgets(iline, sizeof(iline), stdin) == NULL) {
        printf("Need an input line.\n"); exit(1);
    }

    result = sscanf(iline, "%d %d%1s", &a, &b, extra);
    if (result == 2) {
        printf("%d %d\n", a, b);  // Success!
    } else  if (result == 3) {
        printf("Extra stuff starting at '%s'\n", extra);
    } else if (result < 2) {
        printf("Could not find two integers\n");
    }
    return 0;
}

Here's some test runs:

$ gcc x.c
$ echo "1" | ./a.out
Could not find two integers
$ echo "1 2" | ./a.out
1 2
$ echo "1 2.5" | ./a.out
Extra stuff starting at '.'
$ echo "1.5 2" | ./a.out
Could not find two integers
$ echo "1 2 5" | ./a.out
Extra stuff starting at '5'

By reading the line separately from scanning it, you can add the "extra" string to test if the scan prematurely ended before the line was consumed. If I use scanf in my code above, it will refuse to return until it actually finds something extra to scan.

2024-07-15
Streve Ford

Solution

 0

The %d conversion specifier tells scanf to read and discard any leading whitespace, then read up to the first non-digit character (anything that isn't in the range ['0'-'9'].

Assume your input stream contains the sequence:

{'1', ' ', '1', '.', '5', '\n'}

the first %d reads first '1', stops reading at the space, leaving it in the input stream:

{' ', '1', '.', '5', '\n'}

then converts and assigns 1 to a.

The second %d reads and discards the space, reads the second '1' and stops reading at the ., leaving it in the input stream:

{'.', '5', '\n'}

then converts and assigns 1 to b.

As far as scanf is concerned it successfully read two integer inputs. The %d conversion specifier doesn't know or care that 1.5 is a valid floating-point constant; it only cares that '.' is a non-digit character, so it stops reading at that point.

Same thing if you enter something like 12s4 and read it with %d -- scanf will successfully convert and assign the 12 and leave s4 to foul up the next read.

There are several ways around this, none of them terribly elegant. My preferred method is to read the input as a string with fgets, then tokenize and convert using strtol for integers and strtod for floats:

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

/**
 * This program demonstrates some of the gymnastics you have to go through
 * to validate numeric input.
 *
 * Validation steps:
 *
 *  1.  Make sure the fgets operation did not see an EOF or 
 *      input error;
 *
 *  2.  Make sure there's a newline character in the input
 *      buffer, otherwise the input was too long;
 *
 *  3.  Make sure the strtol operation didn't over/underflow;
 *
 *  4.  Make sure that the first character not converted by
 *      strtol is either whitespace or a terminator;
 *
 *  5.  Make sure the value returned by strtol (which is a long) can be
 *      stored in a 32-bit int. 
 */
int main( void )
{
  /**
   * Input buffer
   *
   * Using fixed-size buffers is always risky, but for the purpose of this
   * demonstration should be good enough.  A 32-bit integer can represent 
   * up to 10 decimal digits, plus sign, so to store two integer inputs
   * we need a buffer that's *at least* 25 elements wide (two integers 
   * plus space plus newline plus terminator); rounding up 
   * to the next power of 2. 
   */
  char buf[32] = {0};

  printf( "Gimme two integer values: " );
  if ( !fgets( buf, sizeof buf, stdin ) )
  {
    if ( feof( stdin ) )
      fprintf( stderr, "EOF signaled on standard input...\n"  );
    else
      fprintf( stderr, "Error on standard input...\n" );
    return EXIT_FAILURE;
  }

  if ( !strchr( buf, '\n' ) )
  {
    fprintf( stderr, "Input too long\n" );
    return EXIT_FAILURE;
  }

  int a = 0, b = 0;
  char *chk = NULL;
  static const char *whitespace = " \n\r\t\f";

  /**
   * Break the input into tokens separated by whitespace, then
   * attempt to convert each token to an integer with strtol.
   * The chk parameter will point to the first character *not*
   * converted; if that character is anything other than whitespace
   * or a terminator, then the input is not a valid integer.
   */
  for ( char *tok = strtok( buf, whitespace ), cnt = 0; tok != NULL && cnt < 2; tok = strtok( NULL, whitespace ), cnt++ )
  {
    errno = 0;
    long tmp = strtol( tok, &chk, 10 );
    if ( !(isspace( *chk ) || *chk != 0) ) 
    {
      fprintf( stderr, "\"%s\" is not a valid integer, exiting...\n", tok );
      return EXIT_FAILURE;
    }
    else if ( (tmp == LONG_MIN || tmp == LONG_MAX) && errno == ERANGE )
    {
      fprintf( stderr, "Overflow detected while converting \"%s\" to long, exiting...\n", tok );
      return EXIT_FAILURE;
    }
    else if ( tmp > INT_MAX || tmp < INT_MIN )
    {
      fprintf( stderr, "\"%s\" cannot be represented in an int...\n", tok );
      return EXIT_FAILURE;
    }
    else if ( cnt == 0 )
    {
      a = tmp;
    }
    else
    {
      b = tmp;
    }
  }

  printf( "a = %d, b = %d\n", a, b );  
      
  return EXIT_SUCCESS;
}  

Yeah. Welcome to programming in C. And this code has some severe weaknesses; strtok is destructive and non-threadsafe, using fixed-size buffers is always risky, I'm relying on long having greater range than int (not necessarily true on some systems), etc.

Some sample runs:

% ./input5
Gimme two integer values: ^D EOF signaled on standard input...

% ./input5
Gimme two integer values: 1234567890123456789012345678901234567890
Input too long

% ./input5
Gimme two integer values: 1234567890123456789012345
Overflow detected while converting "1234567890123456789012345" to long, exiting...

% ./input5
Gimme two integer values: 12345678901234
"12345678901234" cannot be represented in an int...

% ./input5
Gimme two integer values: 123 4.56
"4.56" is not a valid integer, exiting...

% ./input5
Gimme two integer values: 123 456
a = 123, b = 456
2024-07-16
John Bode