Moonchild
Moonchild

Reputation: 560

Is there a way to check if a string can be a float in C?

Checking if it can be an int is easy enough -- just check that every digit is between '0' and '9'. But a float is harder. I found this, but none of the answers really work. Consider this code snippet, based on the top (accepted) answer:

float f;
int ret = sscanf("5.23.fkdj", "%f", &f);
printf("%d", ret);

1 will be printed.


Another answer suggested using strpbrk, to check if certain illegal characters are present, but that wouldn't work either because 5fin7 wouldn't be legal, but inf would.


Yet another answer suggested checking the output of strtod. But consider this:

char *number = "5.53 garbanzo beans"
char *foo;

strtod(number, &foo);

printf("%d", isspace(*foo) || *foo == '\0'));

It'll print 1. But I don't want to remove the isspace call entirely, because " 5.53 " should be a valid number.


Is there a good, elegant, idiomatic way to do what I'm trying to do?

Upvotes: 20

Views: 12385

Answers (6)

Sergey Kalinichenko
Sergey Kalinichenko

Reputation: 726709

The first answer should work if you combine it with %n, which is the number of characters read:

int len;
float ignore;
char *str = "5.23.fkdj";
int ret = sscanf(str, "%f %n", &ignore, &len);
printf("%d", ret==1 && !str[len]);

!str[len] expression will be false if the string contains characters not included in the float. Also note space after %f to address trailing spaces.

Demo

Upvotes: 16

Jonathan Leffler
Jonathan Leffler

Reputation: 754190

This code is closely based on the answer by dasblinkenlight. I proffer it as food for thought. Some of the answers it gives may not be what you wanted.

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

static void test_float(const char *str)
{
    int len;
    float dummy = 0.0;
    if (sscanf(str, "%f %n", &dummy, &len) == 1 && len == (int)strlen(str))
        printf("[%s] is valid (%.7g)\n", str, dummy);
    else
        printf("[%s] is not valid (%.7g)\n", str, dummy);
}

int main(void)
{
    test_float("5.23.fkdj");        // Invalid
    test_float("   255.   ");       // Valid
    test_float("255.123456");       // Valid
    test_float("255.12E456");       // Valid
    test_float("   .255   ");       // Valid
    test_float("   Inf    ");       // Valid
    test_float(" Infinity ");       // Valid
    test_float("   Nan    ");       // Valid
    test_float("   255   ");        // Valid
    test_float(" 0x1.23P-24 ");     // Valid
    test_float(" 0x1.23 ");         // Valid
    test_float(" 0x123 ");          // Valid
    test_float("abc");              // Invalid
    test_float("");                 // Invalid
    test_float("   ");              // Invalid
    return 0;
}

Testing on a Mac running macOS Sierra 10.12.6 using GCC 7.1.0 as the compiler, I get the output:

[5.23.fkdj] is not valid (5.23)
[   255.   ] is valid (255)
[255.123456] is valid (255.1235)
[255.12E456] is valid (inf)
[   .255   ] is valid (0.255)
[   Inf    ] is valid (inf)
[ Infinity ] is valid (inf)
[   Nan    ] is valid (nan)
[   255   ] is valid (255)
[ 0x1.23P-24 ] is valid (6.775372e-08)
[ 0x1.23 ] is valid (1.136719)
[ 0x123 ] is valid (291)
[abc] is not valid (0)
[] is not valid (0)
[   ] is not valid (0)

The hexadecimal numbers are likely to be particularly problematic. The various forms of infinity and not-a-number could be troublesome too. And the one example with an exponent (255.12E456) overflows float and generates an infinity — is that really OK?

Most of the problems raised here are definitional — that is, how do you define what you want to be acceptable. But note that strtod() would accept all the valid strings (and a few of the invalid ones, but other testing would reveal those problems).

Clearly, the test code could be revised to use an array of a structure containing a string and the desired result, and this could be used to iterate through the test cases shown and any extras that you add.

The cast on the result of strlen() avoids a compilation warning (error because I compile with -Werror) — comparison between signed and unsigned integer expressions [-Werror=sign-compare]. If your strings are long enough that the result from strlen() overflows a signed int, you've got other problems pretending they're valid values. OTOH, you might want to experiment with 500 digits after a decimal point — that's valid.

This code notes the comments made to dasblinkenlight's answer:

Upvotes: 6

chux
chux

Reputation: 153660

Is there a way to check if a string can be a float?

A problem with the sscanf(...,"%f") approach is on overflow, which is UB. Yet it is commonly handled nicely.

Instead use float strtof(const char * restrict nptr, char ** restrict endptr);

int float_test(const char *s) {
  char *ednptr;
  errno = 0;
  float f = strtof(s, &endptr);
  if (s == endptr)  {
    return No_Conversion;
  }
  while (isspace((unsigned char) *endptr)) {  // look past the number for junk
    endptr++;
  }   
  if (*endptr) {
    return Extra_Junk_At_End; 
  }

  // If desired
  // Special cases with with underflow not considered here.
  if (errno) {
    return errno; // likely under/overflow
  }  

  return Success;
}

Upvotes: 6

chqrlie
chqrlie

Reputation: 144820

This is a variation on the code fragment posted by dasblinkenlight that is slightly simpler and more efficient as strlen(str) could be costly:

const char *str = "5.23.fkdj";
float ignore;
char c;
int ret = sscanf(str, "%f %c", &ignore, &c);
printf("%d", ret == 1);

Explanation: sscanf() returns 1 if and only if a float was converted, followed by optional white space and no other character.

Upvotes: 4

0___________
0___________

Reputation: 67602

Maybe this? Not very good but may do the job. Returns -1 on error 0 on no conversions done and > 0 with converted numbers flags set.

#define INT_CONVERTED       (1 << 0)
#define FLOAT_CONVERTED     (1 << 1)
int ReadNumber(const char *str, double *db, int *in)
{

    int result = (str == NULL || db == NULL || in == NULL) * -1;
    int len = 0;
    char *tmp;

    if (result != -1)
    {
        tmp = (char *)malloc(strlen(str) + 1);
        strcpy(tmp, str);
        for (int i = strlen(str) - 1; i >= 0; i--)
        {
            if (isspace(tmp[i]))
            {
                tmp[i] = 0;
                continue;
            }
            break;
        }
        if (strlen(tmp))
        {
            if (sscanf(tmp, "%lf%n", db, &len) == 1 && strlen(tmp) == len)
            {
                result |= FLOAT_CONVERTED;
            }
            if (sscanf(tmp, "%d%n", in, &len) == 1 && strlen(tmp) == len)
            {
                result |= INT_CONVERTED;
            }
        }
        free(tmp);
    }
    return result;
}

Upvotes: 0

Stephan Lechner
Stephan Lechner

Reputation: 35154

You could check if - after having read a value using strtod - the remainder consists solely of white spaces. Function strspn can help here, and you can even define "your personal set of white spaces" to consider:

int main() {

    char *number = "5.53 garbanzo beans";
    char *foo;

    double d = strtod(number, &foo);
    if (foo == number) {
        printf("invalid number.");

    }
    else if (foo[strspn(foo, " \t\r\n")] != '\0') {
        printf("invalid (non-white-space) trailing characters.");
    }
    else {
        printf("valid number: %lf", d);
    }
}

Upvotes: 10

Related Questions