What’s wrong with my class function to diminish the value?

Asked

Viewed 106 times

1

I’m developing a class to better manage my items in a more organized way. When the value of the variable is less than the minimum value was to go to the maximum value, however, in practice this is not happening, it will stop some strange values:

By pressing the button F2 7 times I have it on console of my algorithm:

Item texto: Testando um float
Valor maximo do item: 0.5
Valor atual: 0.4

Item texto: Testando um float
Valor maximo do item: 0.5
Valor atual: 0.3

Item texto: Testando um float
Valor maximo do item: 0.5
Valor atual: 0.2

Item texto: Testando um float
Valor maximo do item: 0.5
Valor atual: 0.1

Item texto: Testando um float
Valor maximo do item: 0.5
Valor atual: 1.49012e-08

Item texto: Testando um float
Valor maximo do item: 0.5
Valor atual: -0.1

Item texto: Testando um float
Valor maximo do item: 0.5
Valor atual: 0.5

From the value 0.1 should have gone for the value 0.5 instead of going to the value 1.49012e-08

My class:

class cVarFloat
{
private:
       float            fValue;
       float            fValueSet;
       float            fMax;
       float            fMin;
       char             ItemText[100];
public:
       float            GetMax()
       {
           return this->fMax;
       }

       float            GetValue()
       {
           return this->fValue;
       }

       char*            GetItemText(void)
       {
           return this->ItemText;
       }

       void         SetValue(float fValue)
       {
           this->fValue = fValue;
       }

       /*
           É nessa função que eu estou tendo o problema
       */
       void         DecValue(void)
       {
           if (this->fValue <= this->fMin)
               this->fValue = this->fMax;
           else
               this->fValue -= this->fValueSet;
       }

       void         IncValue(void)
       {
           if (this->fValue >= this->fMax)
               this->fValue = fMin;
           else
               this->fValue += fValueSet;
       }

       cVarFloat(const char* ItemText, float fMin, float fMax, float fValueSet, float fInitValue)
       {
           this->fMin = fMin;
           this->fMax = fMax;
           this->fValueSet = fValueSet;
           this->fValue = fInitValue;
           strcpy(this->ItemText, ItemText);
       }
   };

In my main:

int main()
{
    // Primeiro argumento = Texto da classe
    // Segundo argumento = Valor mínimo
    // Terceiro argumento = Valor máximo
    // Quarto argumento = Valor na qual vai aumentar/diminuir o valor da variável
    // Quinto argumento = Valor inicial da variável

    cVarFloat Teste("Testando um float", 0.0f, 0.5f, 0.1f, 0.5f);

    while (1)
    {
        if (GetAsyncKeyState(VK_F1) & 1)
        {
            Teste.IncValue();
            cout << "Item texto: " << Teste.GetItemText() << "\nValor maximo do item: " << Teste.GetMax() << "\nValor atual: " << Teste.GetValue() << endl << endl;
        }

        if (GetAsyncKeyState(VK_F2) & 1)
        {
            Teste.DecValue();   // problema nessa função
            cout << "Item texto: " << Teste.GetItemText() << "\nValor maximo do item: " << Teste.GetMax() << "\nValor atual: " << Teste.GetValue() << endl << endl;
        }
    }
    return 0;
}

4 answers

4


Problem

The problem has to do with comparing equality in floating comma values. This becomes a problem because the representation for several of the values is not exact which ends up giving unexpected results!

Consider the following example:

double a = 0.1 + 0.2;
double b = 0.3; 

cout<<(a == b)<<endl; //0 -> falso

That presents 0 on the console, thus indicating that 0.1+0.2 differs from 0.3!

See for yourself on Ideone

In fact 0.1 it is not possible to represent in an exact way in a double, due to the way it is encoded in the binary system.

Making a comparison between systems:

  • 1/10 is not precisely and finitely encoded in binary
  • 10/3 is not precisely and finitely encoded on decimal basis.

This makes it 0.1 binary is then an infinite periodic decima written as:

0.00011001100110011001100110011001100110011....

Usually noted as:

inserir a descrição da imagem aqui

So when we write:

double c = 0.1;

The value that c has really is

0.1000000000000000055511151231257827021181583404541015625

Or even in float:

float d = 0.1f; //0.100000001490116119384765625

Which was precisely the 1.49012e-08 that appeared on the console. These values end up being shown on the console as 0.1 although they are not, and which make exact comparisons impossible.

This is just one of several examples of values that are not accurately representable in binary.

Solution

In its simple case it could use a comparison with margin of error to replace equality, usually called Epsilon:

float epsilon = 0.0001f;

if (fabs(a-b) < epsilon){

Here we compare if the difference between the two is low enough that we consider them equal. Note that the function was used fabs library <cmath> to obtain the absolute value of a float.

How you want to compare with less or equal can even create a function that does this automatically:

bool igualMenor(float a, float b, float epsilon){
    if (fabs(a-b) < epsilon) return true;

    return a-b < 0;
}

Now you just have to apply to your ifs:

void DecValue(void)
{
    //chamando a função que compara passando um epsilon razoável para o caso em questão
    if (igualMenor(this->fValue, this->fMin, 0.0001f))
        this->fValue = this->fMax;
    else
        this->fValue -= this->fValueSet;
}

This will not stop the cout to show the value 0.1 in its exact representation, something we can force on using cout<<fixed thus ensuring that the value is what was expected visually.

View your code with this solution applied to Ideone
I slightly changed the input so that the result would be presentable in Ideone

Improvements

Although this solution works it may also bring some problems with respect to Epsilon, since this is not scaled to the a and b. In this case you can choose to divide the subtraction result by one of the values, which reduces the scale of the same against Epsilon:

fabs((a-b)/b) < epsilon

However this will bring problems in divisions with 0. So he would have to broaden the logic to something much more complex like (which would be exaggerated for his example):

bool quaseIgual(float a, float b, float epsilon) {
    float absA = fabs(a);
    float absB = fabs(b);
    float diff = fabs(a - b);

    if (a == b) { // atalho para resolver infinitos
        return true;
    } else if (a == 0 || b == 0 || diff < FLT_MIN) {
        // a ou b são zeros e extremamente perto entre si
        // erro relativo é ignorável neste caso
        return diff < (epsilon * FLT_MIN );
    } else { // usar erro relativo
        return diff / min((absA + absB), FLT_MAX ) < epsilon;
    }
}

Including the <float.h> to access the constants FLT_MIN and FLT_MAX

References:

0

Floating point formats have precision limits and do not guarantee the exact representation of certain numbers. In the case of simple precision format (float) the relative value of this precision limit is around 1e-7 and in double precision format (double) around 1e-15. Your code performed a few algebraic operations on numbers expressed in simple precision format. As the operations accumulated some rounding errors, in the end you got values below the limit of precision. Everything is working as expected.

To deal with this type of situation you need to take two aspects into account: the first is to know the order of magnitude of the expected result. If you are dealing with numbers that are multiples of 0.5 and expect a possible result to be 0.0 then a result of 1e-8 is clearly zero. To solve this problem it is enough to round up the results taking into account the order of magnitude of the expected valroes. The second aspect is the spread of rounding errors. Each algebraic operation on floating point variables has a potential to introduce rounding errors. With each operation you perform, these rounding errors accumulate. There are operations, like division, that have the potential to blow up the value of error. If you really need accuracy in the results then you need to analyze algebraic operations to avoid unnecessary propagation of rounding errors and predict the maximum limit of errors.

0

The problem is that floating point arithmetic (float) has no infinite precision. If you add up floats values and try to compare by equality you will not always get the expected result. 1.49012e-08 is a value very close to 0, but it is still > 0.

The best thing to do in this situation is: avoid using floats for iterations, or use a comparison function that has a certain margin of error. You can do the for using int for example is to calculate the "current" value by dividing by 10.

  • Could you give me a small example of how it is done? For me it was not clear how this function would be done.

0

Comparisons with floating point numbers are hell! Avoid them!

In this case, you can avoid all these comparisons by pre-calculating in a array the values within the desired range.

Increment and decrease operations now work by moving the position of the Dice and reading the values contained in array, using integer arithmetic:

#include <iostream>
#include <string>
#include <cmath>


class cVarFloat
{
    private:

        int         nPos;
        float       *pfArray;
        int         nArrayLen;
        float       fValueSet;
        float       fMax;
        float       fMin;
        float       fInitValue;
        std::string ItemText;

    private:

        void Initialize( void )
        {
            unsigned int i = 0;
            nArrayLen = static_cast<int>( std::ceil( (fMax - fMin) / fValueSet) ) + 1;
            pfArray = new float[ nArrayLen ];
            for( float f = fMin; f <= fMax; f += fValueSet, i++ ){
                pfArray[i] = f;
                if( this->fInitValue == f )
                    this->nPos = i;
            }
        }

    public:

        cVarFloat( std::string ItemText, float fMin, float fMax, float fValueSet, float fInitValue ) :
            nPos(0), fValueSet(fValueSet), fMax(fMax), fMin(fMin), fInitValue(fInitValue), ItemText(ItemText) { this->Initialize(); }

        virtual ~cVarFloat() { delete [] pfArray; }

        float GetMax(void) { return this->fMax; }
        std::string GetItemText(void) { return this->ItemText; }
        float GetValue(void) { return this->pfArray[ this->nPos ]; }

        void DecValue(void) { if( this->nPos == 0 )  this->nPos = this->nArrayLen - 1; else this->nPos--; }
        void IncValue(void) { if( this->nPos == this->nArrayLen - 1 ) this->nPos = 0; else this->nPos++; }
};


int main(void)
{
    cVarFloat Teste( "Testando um float", 0.0f, 0.5f, 0.1f, 0.5f );

    for( int i = 0; i < 15; i++ )
    {
        std::cout << Teste.GetValue() << std::endl;
        Teste.DecValue();
    }

    for( int i = 0; i < 15; i++ )
    {
        std::cout << Teste.GetValue() << std::endl;
        Teste.IncValue();
    }

}

Exit:

0.5
0.4
0.3
0.2
0.1
0
0.5
0.4
0.3
0.2
0.1
0
0.5
0.4
0.3
0.2
0.3
0.4
0.5
0
0.1
0.2
0.3
0.4
0.5
0
0.1
0.2
0.3
0.4

Browser other questions tagged

You are not signed in. Login or sign up in order to post.