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!
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!
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(sz Buf)); // 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+nLeadingZero s , szBuf, sizeof(szBuf)-nLeadingZero s );
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(sz Buf)); // 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\td ir\t1stShi ft\tleadin g zeros\tl\tlAfter\tlRound\t bCarry\tza c\tsb2ndSh ift\tsa2nd Shift\tsa3 rdShift\tR esult\n");
// TRACE("'%f\t'%s\t%d\t%d\t% d\t%d\t'%s \t%d\t%I64 d\t%I64d\t %I64d\t%d\ t%d\t'%s\t '%s\t'%s\t '%f\n",
// d, sBefore1stShift, i,nPlaces, places,dir,sAfter1stShift, origLeadin gZeros ,lBefore,lAfter,lRound,bCa rryFwd,zer oAfterCarr y,sBefore2 ndShift,sA fter2ndShi ft,sAfter3 rdShift,dT runc);
#endif
break;
}
}
return dTrunc;
}
{
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(sz
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+nLeadingZero
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(sz
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
// TRACE("'%f\t'%s\t%d\t%d\t%
// d, sBefore1stShift, i,nPlaces, places,dir,sAfter1stShift,
#endif
break;
}
}
return dTrunc;
}
ASKER CERTIFIED SOLUTION
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
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 ]++;
}
}
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 ]++;
}
}
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
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.)
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.)