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

# 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
• 3
• 2
1 Solution

Commented:
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

Commented:
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

char* p = szBuf;

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

__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;
#endif
{
}
#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("'%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

Commented:
BTW, if you're not using MFC you will want to remove my debugging code.

0

Commented:
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

Author 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

Commented:
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

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