• Status: Solved
  • Priority: Medium
  • Security: Public
  • Views: 771
  • Last Modified:

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!


0
borghard
Asked:
borghard
  • 3
  • 2
1 Solution
 
millsoftCommented:
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.

0
 
millsoftCommented:
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;
}
0
 
millsoftCommented:
BTW, if you're not using MFC you will want to remove my debugging code.

Brad
0
Never miss a deadline with monday.com

The revolutionary project management tool is here!   Plan visually with a single glance and make sure your projects get done.

 
grg99Commented:
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 ]++;
                                    }
 }


                     

0
 
borghardAuthor Commented:
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
0
 
grg99Commented:
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.)


0

Featured Post

Free Tool: Port Scanner

Check which ports are open to the outside world. Helps make sure that your firewall rules are working as intended.

One of a set of tools we are providing to everyone as a way of saying thank you for being a part of the community.

  • 3
  • 2
Tackle projects and never again get stuck behind a technical roadblock.
Join Now