0
\$\begingroup\$

I'm trying to understand the strtod() function, and how I can handle any user input given to the function. I assume I tested for everything, however I am hoping for a review to make sure and to catch anything I missed. (I know I should separate everything into functions, however, I am just trying to understand the nature of the function, therefore I left everything in main.)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <float.h>
#include <stdbool.h>

/*@param MAXSIZE max number of chars that are converted to a 64bit double*/
#define MAXSIZE 1077 

int main() {

    char str[MAXSIZE];
    printf("Enter a rational number: ");

    for (;;) {
        if (fgets(str, sizeof str, stdin) != NULL) {
            if (strchr(str, '\n')) {
                str[strcspn(str, "\n")] = '\0';
            }
            if (str[0] != '\0') {
                break;
            }
            printf("\nTry again: ");
        }
    }

    char* endptr = NULL;
    errno = 0;
    double number = strtod(str, &endptr);

    //passes over trailing whitespace
    for (; isspace(*endptr); ++endptr);

    if (errno == ERANGE) {
        fprintf(stderr, "Error out of range...\n");
    }
    else if (*endptr != '\0') {
        fprintf(stderr, "error could not convert: %s\n", str);
    }
    else {
        printf("Your string was converted into the rational number: %lf\n", number);
    }
    printf("Your string was: %s\n", str);
    printf("Press any key to continue...\n");
    getch();
}
\$\endgroup\$
  • \$\begingroup\$ BTW, asking the user to enter a "rational" number might lead them to think they can type 1/3 here. "Decimal" may be a better choice. (not an answer, as it's tangential to the requested review) \$\endgroup\$ – Toby Speight Oct 11 '16 at 17:46
  • \$\begingroup\$ @Toby Speight, I agree completely, thank you for pointing that out! \$\endgroup\$ – chris360 Oct 11 '16 at 17:47
1
\$\begingroup\$

str[strcspn(str, "\n")] = '\0'; is an overkill. fgets reads (at most) one string, that is there is at most one '\n' in the buffer, and strchr will return its position. Consider

    char * newline = strchr(str, '\n');
    if (!newline) {
        .... // handle input too long condition
    } else if (newline == str) {
        .... // handle empty input condition
    } 
    *newline = '\0';
    break;

Notice that even newline = '\0' is redundant: '\n' is a whitespace.


Do not invent error messages. This is what strerror and perror are for. Also keep in mind that strtod may set errno to errors other than ERANGE (some implementations use EINVAL as well). Consider

    if (errno != 0) {
        perror("strtod");
    }
\$\endgroup\$
  • \$\begingroup\$ Thank you! So if errno is not zero, it is some error value and perror will return what error occured? \$\endgroup\$ – chris360 Oct 9 '16 at 2:32
  • \$\begingroup\$ I also have another question, perror(errno) does not compile, however perror(NULL) works as expected, was that a typo or am I missing something? Another thing, can I still use my fprintf() statement? It works fine, I know i'm customizing my own error message but is there a reason I shouldn't? If yes how else could I implement it? Thank you! \$\endgroup\$ – chris360 Oct 9 '16 at 3:14
  • \$\begingroup\$ @chris360 perror will print the standard error message. strerror will return an error message, so you may chose to print it in a more fancy manner. Also yes, there was a typo, sorry. \$\endgroup\$ – vnp Oct 9 '16 at 3:14
  • \$\begingroup\$ How is str[strcspn(str, "\n")] = '\0'; overkill? Certainly a popular answer to Removing trailing newline character from fgets() input. Should it be 2-3 times slower than strchr() method, that time savings is certainly swamped by the fgets() itself. Also lack of '\n' in if (!newline) { is caused by 2 other reasons too beside "input too long": prior embedded null character and end-of-file occurring. True about no need to drop the '\n' as for (; isspace(*endptr); ++endptr); will handle that. \$\endgroup\$ – chux Nov 8 '16 at 18:16
1
\$\begingroup\$
  1. With strtod(), if (errno == ERANGE) { is problematic.

When the input string represents a value exceeding +/- DBL_MAX, errno == ERANGE is set - no problem there.

When the input string represents a tiny non-zero value -DBL_MIN < x < DBL_MIN, errno == ERANGE might be set. It is implementation-defined.

If the result underflows ..., the functions return a value whose magnitude is no greater than the smallest normalized positive number in the return type; whether errno acquires the value ERANGE is implementation-defined. C11 §7.22.1.3 10

These is no truly robust solution. The best is to not flag an error when the answer is tiny.

if (errno == ERANGE) {
  if (number >= -DBL_MIN && number <= DBL_MIN) {
    errno = 0;
  }
}
  1. Printing a double using "%f" does not well present large numbers nor tiny ones. Suggest "%e" or "%g"

  2. 1077 is not the max number of char that are converted to a 64bit double. There is no C specified upper bound. Although 1077 is certainly generous, buffer size needed to print -DBL_TRUE_MIN is 1077 + 1 and so would need 1077 + 1 (\n) + 1 (\0) to read with fgets() for IEEE binary64.

IAC, the buffer size needed is not the number of character used to print a double into a string, but the number of characters needed when reading. Depending on coding goals, this could be as small as about 400 (extras characters will not likely make a difference) or pedantically: unlimited.

  1. str[strcspn(str, "\n")] = '\0'; is sufficient.

    // if (strchr(str, '\n')) {
    //   str[strcspn(str, "\n")] = '\0';
    // }
    str[strcspn(str, "\n")] = '\0';
    
  2. OP's code fails to detect input of all spaces because "passes over trailing whitespace" may be passing over leading white-spaces.

    double number = strtod(str, &endptr);
    // add
    if (str == endptr) fail();
    

I'll offer a more robust "parsing a string into a double" code that allows leading/trailing whitespace taking into account the above §7.22.1.3 10 spec.

// return true on success
bool string_double(const char * restrict str, double * restrict dest) {
  if (str == NULL) return false; // Optional NULL check
  errno = 0;
  char *endptr;
  double y = strtod(str, &endptr);
  if (errno == ERANGE) {
    if (fabs(y) > 1.0) {
      // Alternatives for handling overflow exist, 
      // it depends on coding goals.
      // Go for simply for now.
      return false; // overflow
    }
    // Let all underflow pass as code can't portably detect it.
    // Code has no real portable options. C11 §7.22.1.3 10
    // Suggest `y = 0.0` for highest portability
    // Otherwise leave `y` as is for most informative.
    y = 0.0;  // Optional
  } else if (errno) {
    // Should some other errno occur, 
    // (no other errno value are specified in the standard for strod()),
    // consider returning.
    return false;  
  }

  if (str == endptr) return false;  // no conversion, e.g. all spaces

  // consume trailing white-space
  while (isspace((unsigned char) *endptr)) endptr++;
  if (*endptr) return false; // trailing junk

  if (dest) *dest = y;
  return true;
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.