Link to home
Start Free TrialLog in
Avatar of borghard
borghard

asked on

Need to round decimal stored as a string

I have a std:string with a decimal value.  I want to round this to a certain number of decimal places.

For example, I have "38.987654321" and I want to round it to 3 places, I want to get "38.988".

I can convert this to a float, use the pow function to shift it (38987.654321), add .5, truncate, shift it back, and set it back to a string.

Is there an easier way?
I would like to use a double rather than a float.  How would I do that?


Thanks!


Avatar of millsoft
millsoft

pow will work ok with a double.  However, in my experience, the technique you described produces terrible roundoff error.  THerefore, I created a rounding function using actual strings because I found ecvt to be a cleaner (less round-off error prone) function.

DOUBLE_TYPE Round(DOUBLE_TYPE d, short nPlaces)
{
    DOUBLE_TYPE dTrunc = 0;

    // OK, here's the plan stan:
    // first, save the sign of the input number so we don't have to deal with it.

    BOOL bIsNeg = FALSE;
    if ( d < 0 )
    {
        bIsNeg = TRUE;
        d = -d;
    }
   
    // Second, convert the input value to a string.
    // note that numbers whose absolute value < 1 will get a leading 0.
    // if we are rounding to the tens or higher (negative places), then we
    // create the string with an appropriate number of leading zeros by
    // shifting the location of the sprintf call.
    char szBuf[500];
    memset(szBuf,'0',sizeof(szBuf)); // clear buffer so we can run past the generated string!
    if ( nPlaces < 0 )
        sprintf(szBuf - nPlaces, "%f", d);
    else
        sprintf(szBuf, "%f", d);
    szBuf[sizeof(szBuf)-1]=0;

#ifdef _DEBUG
    CString sBefore1stShift = szBuf;
#endif
    // set carry flag.  
    // we are looking for the situation where a round-up will result in the first non-zero
    // digit rolling back to zero with a carry forward of a 1 in the next most significant
    // position.  This happens when there is a string of nines or zeros from the most
    // significant digit to the "decision digit".
    // Specifically, this string of digits will always be a solid string of zero or more
    // nines immediately left of the decision digit preceeded by a solid string of 0 or more
    // zeros.  Any other configuration will "absorb" the round-up operation in the same
    // number of digits.
    // e.g.  0.998 rounds up to 1.000 when rounded at 0,1 or 2 places.
    // e.g.  0.908 rounds up to 0.910 at 2 places since there is a break between
    //             the 9s and the decision digit (8)
    // e.g.  0.008 rounds up to 0.01 at 2 places, the 9 string is zero bytes long
    // note the most significant digit has moves one place to the left.  Therefore, we shift
    // the decimal place one position to the right in the output (seen below as adding 1 to
    // the "zero" variable containing the decimal position.
    //
    // Implementation of the carry flag is as follows:
    // 1. default to true
    // 2. set to false when any of the following occur:
    //        a. any digit 1-8 is discovered OR
    //        b. if any non-9 digit is encountered AFTER the first 9 digit OR
    //        c. if we don't round-up.
    // note that the "seen first" flag is reversed when shifting the decimal place to the left
    // in the case of tens place rounding (90->100)
    //
    bool bCarryFwd = true;
    bool bSeenFirst9 = false;
    // Third, find the decimal point..
    int zero = -1;
    for(int i = 0; i < lstrlen(szBuf); i++ )
    {
        bool bIsDecisionDigit = false;
        // if rounding to right of the decimal, scan for possible resets to the carry flag.
        // e.g. 199.99, 1 should not have the carry flag set because of the 1,
        // but that is not discovered in the shift loop below after the period is located

        if ( szBuf[i] == '.' )
        {
            zero = i;  // zero contains the zero-based location of the decimal place...
           
            // fourth, calculate number of places to shift the decimal point so that the
            // digit immediately before the decimal point is the decision digit in the
            // rounding operation.  This digit is the one being rounded off, so we add
            // +1 to get decision digit left of the decimal place.
            // e.g. nPlaces = 0, then we are rounding based on the digit immediately
            //      after the decimal point so we shift 1
            // e.g. nPlaces = -1, rounding tens place based on the ones place, which
            //      is already in the correct position.
            // e.g. nPlaces = -2, rounding hundreds, so shift tens to ones
            // examples:
            // value    string        zero (decimal point)    nPlaces        places    dir   string after
            // .9        0.90000              1                       0           1     1    09.00000

            int places = nPlaces + 1;
            if ( places < 0 )
            {
                TRACE("Rounding to Negative Places!\n");
                //ASSERT(0); // stop!
            }
           
            // compute direction of shift
            int dir = places < 0 ? -1 : 1;
           
            // compute string location of shift (0 based..)
            int nStart = zero;
            int nEnd = nStart + places;

            ASSERT( nEnd >= 0 && nEnd <= sizeof(szBuf)-1 );
            if ( !( nEnd >= 0 && nEnd <= sizeof(szBuf)-1 ) )
                return 0;

            // loop through string for two purposes:
            // first, move digits for decimal point, we only do this until we
            // reach nEnd.
            // second, check digits to set the carry flag.  For this purpose, the
            // loop runs until we hit the decimal point...
           
            for( int j = nStart; (dir == 1) ? (j < nEnd) : ( j > nEnd ); j += dir )
            {
                ASSERT( j >= 0 );
                if ( j < 0 )
                {
                    return 0;  // avoid writing in front of string...
                }
                // if j hasn't reached nEnd yet, copy the byte, else don't move, just let the carry flag checks continue
                char c = szBuf[j];
                ASSERT( c == '.' );
                szBuf[j] = szBuf[j+dir];
                szBuf[j+dir] =c;
            }
            szBuf[nEnd] = '.';

            for( j = 0; szBuf[j] != 0 && szBuf[j] != '.'; j++ )
            {
                if ( szBuf[j+1] == '.' )
                {
                    // don't perform this check on the decision digit...
                }
                else
                {
                    char cNext = szBuf[j];
                    if ( cNext >= '1' && cNext <= '8' )
                        bCarryFwd = false;

                    if ( cNext == '9' && dir == 1 )    // carry rule b
                        bSeenFirst9 = true;
                    else if ( bSeenFirst9 )
                        bCarryFwd = false;
                }
            }
#ifdef _DEBUG
            CString sAfter1stShift= szBuf;
#endif

            // count leading zeros because we are about to loose them...
            char* p = szBuf;
            int nLeadingZeros=0;

            for( ; *p == '0'; p++ )
                nLeadingZeros++;

            __int64 l = _atoi64(szBuf);
           
#ifdef _DEBUG
            __int64 lBefore = l;
#endif
            if ( l % 10 >= 5 )
            {
                l += 10;
            }
            else
                bCarryFwd = false; // carry rule c
#ifdef _DEBUG
            __int64 lAfter = l;
#endif
            l /= 10;

#ifdef _DEBUG
            __int64 lRound = l;
            int zeroBefore = zero;
#endif
            if ( bCarryFwd )
                zero += 1;    // shift decimal 1 point right.

#ifdef _DEBUG
            int zeroAfterCarry = zero;
#endif

            sprintf(szBuf, "%I64d00000000000000000", l);

#ifdef _DEBUG
            CString sBefore2ndShift = szBuf;
            int origLeadingZeros = nLeadingZeros;
#endif
            if ( nLeadingZeros )
            {
                memmove(szBuf+nLeadingZeros , szBuf, sizeof(szBuf)-nLeadingZeros );
                while (nLeadingZeros > 0 )
                    szBuf[--nLeadingZeros] = '0';
            }
#ifdef _DEBUG
            CString sAfter2ndShift = szBuf;
#endif

            // shift digits for decimal point
            if ( places+zero+dir < 0 ) // then rounding off out of range (for example round 6 to 100s place)
            {
                memset(szBuf,'0',sizeof(szBuf)); // clear buffer so we can run past the generated string!
                szBuf[sizeof(szBuf)-1]=0;
            }
            else
            {
                for ( j = places; j != 0; j -= dir )
                {
                     szBuf[zero+j+dir] = szBuf[zero+j];
                }
                szBuf[zero+dir] = szBuf[zero];
                szBuf[zero]='.';
            }
#ifdef _DEBUG
            CString sAfter3rdShift = szBuf;
#endif

            if ( bIsNeg )
            {
                memmove(szBuf+1, szBuf, sizeof(szBuf)-1);
                szBuf[0] = '-';
                szBuf[sizeof(szBuf)-1]=0;
            }
            dTrunc = atof(szBuf);

           
#ifdef _DEBUG
    //        TRACE("value\tstring\tzero\tnPlaces\tplaces\tdir\t1stShift\tleading zeros\tl\tlAfter\tlRound\tbCarry\tzac\tsb2ndShift\tsa2ndShift\tsa3rdShift\tResult\n");
    //        TRACE("'%f\t'%s\t%d\t%d\t%d\t%d\t'%s\t%d\t%I64d\t%I64d\t%I64d\t%d\t%d\t'%s\t'%s\t'%s\t'%f\n",
    //                d, sBefore1stShift, i,nPlaces, places,dir,sAfter1stShift,origLeadingZeros ,lBefore,lAfter,lRound,bCarryFwd,zeroAfterCarry,sBefore2ndShift,sAfter2ndShift,sAfter3rdShift,dTrunc);
#endif
            break;
        }
    }
   
    return dTrunc;
}
ASKER CERTIFIED SOLUTION
Avatar of millsoft
millsoft

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
If you have a string, and you want to round it, how about something like:

p = strchr( '.', Num );
if( p >= 0 ) p += 3;  // point to last digit we want;

q = p + 1;
if( Num[ q ] <= '4' )    { }; //keep current last digit
else
 { //Increment digit and carry
                                          r = p;
                                         while( Num[r] == '9' ) Num[r++] = '0';
                                         Num[ r ]++;
                                    }
 }


                     

Avatar of borghard

ASKER

doing it the grg99 seems like it would be fast but I would march the other way, correct (r--)?
This does not care for rounding 99.99999 2 decimal places to 100.00
I think it would give me 99.00
OOPS, right, you have to go down to propagate the carries.  
Also you have to skip the '.'.  and if you fall off the left end, you have to insert a "1".

(And of course plunk down a '\0' past the last digit you want.)