ryfterek
ryfterek

Reputation: 719

C's strtod() returns wrong values

I'm currently working on a banking terminal program for the laboratory exercises at my university.

What puts me off my stride is a function supposed to take the user's input of amount to be transfered, check if it fits all requirements and if so, returns the value provided to the program.

Our tutor is mad about all means of securing the input, and so I have to call an error on any kind of inconsistency. Value too high? Error. Negative or zero value? Error. A number more precise than 0.01? Error. Non-digit-non-dot characters in the input? Error.

Due to that, my function is definately overcomplicated, yet I'm fine with that. What drives me up the wall is the fact, that both atof() and strtod() functions reads numbers in a somehow wrong way.

long double take_amount()
{
    char input[21];
    bool success, correct;
    unsigned long int control1;
    long double amount, control, control2, control3;

    do
    {
        success = true, correct = true;
        printf("\t\tAmount:\t\t");
        success = scanf("%20[^ \t\n]c", input);
       __fpurge(stdin);

        correct = check(input, 'b');

        amount = strtod(input, NULL);

        printf("\n\tGOT %.20Lf\n", amount); ///

        control = amount * 100;
        control1 = (unsigned long int)floor(control);
        control2 = (long double) control1;
        control3 = control2 - control;    

        printf("\n\tGOT2 %.20Lf\n", control3);  ///

        if (control3 != 0)
        {
            if (control3 >= 1 || control3 <= -1)
            {
                printf("\n\t\tWe are sorry, but for the safety reasons it is impossible to transfer");
                printf("\n\t\tsuch a great amounts while online. If you really wish to finalize");
                printf("\n\t\tthis operation, please, visit the closest division of our bank.");
                printf("\n\t\tWe are sory for the inconvenience and wish you a pleasent day.");
                press_enter();

            }
            correct = false;
            printf("\nDAMN\n");     ///       

        }

        if (amount <= 0)
        {
            correct = false;
            printf("\nGOD DAMN\n");      ///
        }

        if(success == false || correct == false)
        {
            printf("\n\t\tInvalid input, please try again.\n\n");
        }
        else
        {
            printf("\t\t\t\t%.2Lf\n", amount);
            printf("\n\t\tIs it correct input? ((Y)es/(N)o/(E)xit)");
            if (accept())
            {
                break;
            }
            else
            {
                continue;
            }
            break;
        }
    }while (1);
    return amount;

As far as my functions used here goes, check() checks if the string contains only legal characters (digits and dot), press_enter() waits for enter-press to leave to main menu and accept() reads only y/n/e, return true on y, false on n and leaves to menu on e.

The lengthy part with control variables is my solution for checking if the number is not more precise than 0.01. Sadly, it doesn't work due to strtod().

My problem is that strtod() doesn't really work! Even with really medicore numbers, being far from underflow or overflow, the value returned doesn't match the input. Some examples:

    Enter the amount you would like to deposit.
        Amount:     1234.45

    GOT 1234.45000000000004547474

    Enter the amount you would like to deposit.
        Amount:     0.999

    GOT 0.99899999999999999911

It is not unlikely that it's my fault, but after several hours with this code I still couldn't come up with a working solution and that's why I'm asking for you help, vise internauts of Stack Overflow.

Is there any way to fix the reading of strtod()? If not, is there another way to take that input that let's me check all that needs to be checked?

EDIT: ATTENTION, PLEASE!

If I haven't stated already, that my tutor is not the easiest guy to work with, I do so now.
He FORCES us to use DOUBLE format to hold the BALANCE stored on accounts. I know it because one of my collegues got his program REJECTED for using two-int, dollar - cent construction.

I highly appreciate you help here, so can you somehow solve that problem? I HAVE TO use double type to store the money. I also HAVE to check if there were no letters in the input (scanf() set on %Lf will just cut all non-digits from the end), HAVE TO check if the input is not more precise then 0.01, HAVE TO accept xxx.xx structure of the input. Any suggestions?

Upvotes: 2

Views: 3278

Answers (5)

chux
chux

Reputation: 153557

Divide and Conquer

Separate user input from data validation.


User input:

Current code uses input without first validating success

success = scanf("%20[^ \t\n]c", input);
correct = check(input, 'b');
amount = strtod(input, NULL);  // Bad: `input` not known to have been successfully scanned.

Instead:

char input[40]; // no need for tight buffer size
if (fgets(input, sizeof input, stdin) == NULL) Handle_EOF();
// remove potential trailing \n
input[strcspn(input, "\n")] = 0;

Now start assessing if input is a valid with various tests

char *endptr;
errno = 0;
double value = strtod(input, &endptr);
if (input == endptr) Handle_NothingScanned();
if (*endptr != '\0') Handle_GarbageAtEndOfInput();

// Use 1 of the 2: First allows input like 1e-1000 which underflows to 0.0
if (errno == ERANGE && fabs(value) > DBL_TRUE_MIN) Handle_Overflow();
if (errno == ERANGE) Handle_OverUnderflow();

// work with numbers in a reasonable range of 'double'
double max_range = pow(10.0, DBL_DIG - 2);
if (!(value >= -max_range && value <= max_range)) Handle_InfinityNotanumberOverflow();

// Test insures that the number entered is the closest possible `double` to xxxxxx.xx
// Method 1: Math
double rvalue = round(value*100.0)/100.0;
if (rvalue != value) Handle_NumberMorePreciseThan_0_01();
// Method 2: round trip: double --> string --> double
char buffer[DBL_DIG + 10];
sprintf(buffer, "%.2f", value);
if (value != strtod(buffer, 0)) Handle_NumberMorePreciseThan_0_01();

// Insure xxxx.xx format
if ((endptr - input) < 3 || endptr[-3] != '.') Handle_BadFormat();

GoodToUse(value);

Upvotes: 2

Erin Owen
Erin Owen

Reputation: 66

If the requirement to make use of a double number is strict then another property of floating point numbers may be of use: An arbitrary integer value stored in a floating point variable will be more likely to be precise than a number with a fractional component.

This fact leads me to ask: Have you thought of storing your currency values such that $1234.45 is 123445?

Upvotes: 0

Geoffrey Owen
Geoffrey Owen

Reputation: 1

This is a known "issue" with respect to how numbers are represented in floating point. The representation of 1234.45 in floating point IS what is being entered.

This is due to the fact that when translated into the floating point representation the actual precise string of bits is longer than the space used to store it (when dealing with a number that is not a power of two there is always a chance of this occurring.)

Another option that you can do is create a 'Currency' struct that holds two ints (the dollar part and the fractional part) and pass it around. You'll still need to handle the code for splitting the currency string into those parts though.

Upvotes: 0

nategoose
nategoose

Reputation: 12392

IEEE floating point numbers are meant to hold approximate values over a huge range, not precise values. They cannot represent some very simple values precisely, such as 0.1, but they can represent 1.0, 0.5, 0.25, 0.125, ... precisely. They can also represent 2.0, 4.0, 8.0, 16.0, ... precisely. How far these series go is related to how many bits the floating point value is represented by. You should have noticed that all of the examples values that I've given are powers of 2, and from that you should see that floating point values can also be made up of sums of powers of 2, which is mostly correct. Summing the values of 2 works well for representing whole numbers as the entire range from least to greatest can be represented by integer types, but with fractional values things are different. There are many values between the values that can be represented that are not expressible for a given floating point type (32, 64, 80, or 128 bit floating point variables), so when these are read in by C library functions (or by the compiler from your source code) there are rounding rules that come into play. A very similar thing happens when the numbers are written back out by a C program.

When you need to deal with precise values you must be aware of this. For money, which usually needs this type of precision, you will want to convert it to a type that can represent this type of precision. In many languages there are separate types for this, but in C you get to put the values into integer types or structs made up of integers.

Upvotes: 2

R.. GitHub STOP HELPING ICE
R.. GitHub STOP HELPING ICE

Reputation: 215287

Use strtol to read the dollars and again (or just your own trivial function) to read the cents, then combine them as cents += 100*dollars; Do not use floating point for currency. Ever.

Upvotes: 8

Related Questions