2

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?

Nisse Engström
  • 4,738
  • 23
  • 27
  • 42
ryfterek
  • 669
  • 6
  • 17
  • 4
    `strtod` and related functions, and `double` itself cannot be "fixed". Mainly because there is nothing wrong with them: [What Every Programmer Should Know About Floating-Point Arithmetic](http://floating-point-gui.de/). Find a solution that does not use a floating point type. – Jongware Jun 02 '15 at 20:06
  • 1
    Welcome to the world of floating-point numbers; because of the way they are represented internally, there are decimal values that cannot be represented exactly. – Scott Hunter Jun 02 '15 at 20:06
  • This is the nature of fixed-precision arithmetic. Just as there is no fixed-length decimal number that yields 1 when multiplied by 3, there is no fixed-length binary number that yields 1 when multiplied by 10. – David Schwartz Jun 02 '15 at 20:14
  • Thank you all for the answers. Well, I did know the arithmetics of f-points is a magic place, but didn't really expect that a trivial matter of just reading a f-point number from the character string is under its influence as well. After all, it's all only about reading one number from a few digits. I guess I'll have to make some bigger changes to the program as a whole then. Once again, thanks for your response! – ryfterek Jun 02 '15 at 20:25
  • @ryfterek Well, think about it - float/double are normally 32 and 64 bits big. So with a fixed amount of bits, there can be only a finite amount of different values. But even just between 0 and 1 there is an infinite amount of decimal numbers and a 32 or 64 bit value can't represent all of them. – nos Jun 02 '15 at 20:39
  • Yes, I can wrap my head around this idea. Yet, sadly, I got some bad news from a colleague of mine. If anyone is still interested in helping me, please, read what I've added as edit. – ryfterek Jun 02 '15 at 20:43
  • If you "HAVE TO" use `double` *and* store cents acurrately, and as `0.01`, then I'm all out of suggestions. You can't have (I mean HAVE) both. – Jongware Jun 02 '15 at 20:50
  • 2
    Your instructor/tutor/whatever is incompetent. Use of floating point for purposes like this is banned in the financial industry for very good reason. Try to dig up a reference for this (maybe someone else here can; I don't have a canonical one right off) and show your instructor. And inform whatever institution they're working for that they have no idea what they're doing. – R.. GitHub STOP HELPING ICE Jun 02 '15 at 21:13
  • He is sure as hell incompetent teacher, but what can I do? My fellow student, being now a part-time web developer, decided to use for the purpose of this program sqlite3 library for C, as he knows SQL really well and would feel better with it than with a binary file we should use. Result? Program rejected, student is supposed to learn the exact solutions from the subject, not come up with his own solutions... Still, that's just a freak we're supposed to somehow work with only till the end of the semester, so I see no point int trying to get him fired/demoted/whatever. – ryfterek Jun 02 '15 at 21:40
  • When using `double` for currency, the floating-point number need not exactly match the text. Generating the closest `double` to the nearest "0.01" is sufficient. Detecting excessive precision in the text can easily be determined by comparing the user input `double` to the `double` that is closest to `0.01`. This is a learning exercise, not a robust financial tool. Such tools either use _decimal_ floating-point numbers like [decinal64](http://en.wikipedia.org/wiki/Decimal64_floating-point_format) or scaled integers. – chux - Reinstate Monica Jun 04 '15 at 20:10

5 Answers5

8

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.

R.. GitHub STOP HELPING ICE
  • 208,859
  • 35
  • 376
  • 711
  • First of all, thanks for the answer. I would really LOVE to, but - again - the tutor who marks our programs can often go crazy if our programs doesn't work as he would imagine them to be. If he wish to enter both dollars and cents at once as in 10.99, we HAVE to make it work. Suppose I'd use strtol, take the non-accepted part of the string, I would then have to introduce additional function which can skip the dot (but only dot), read again... And what is worse, change more or less half of the code so it uses int not double to hold the balance... Well, I guess that's the best I can do now. :/ – ryfterek Jun 02 '15 at 20:19
  • No problem with 10.99. The first `strtol` reads the integer 10. The second one reads the integer 99. Then 99+10*100 gives you 1099 cents. – R.. GitHub STOP HELPING ICE Jun 02 '15 at 21:11
  • The problem is, supposed my freaking tutor faceroll 123.gtrv246vbf1fs, or 0.2.023tdfq or -[iu76tgbr56, the program is supposed to react properly (that is, return an error connected with the wrong input). What is more, as stated in the edit to the original post, he forces us to use double type for account balance, so cents-only approach is out of question. – ryfterek Jun 02 '15 at 21:43
  • Do not agree with "Do not use floating point for currency. Ever". 1) there are _decimal_ floating point types with sw/hw support that well handle the issues of exact conversion of text to/from floating point. 2) Integer types have their issues too as with truncating toward 0 with division rather than rounding to nearest, easy overflow with multiplication, eventual conversion to floating point when calculating interest, rates, etc. IAC certainly this is more of a learning exercise on qualifying user input than tracking the federal budget. – chux - Reinstate Monica Jun 04 '15 at 20:26
2

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.

nategoose
  • 12,054
  • 27
  • 42
2

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);
chux - Reinstate Monica
  • 143,097
  • 13
  • 135
  • 256
0

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.

0

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?