2

Can someone please provide code (any language will do, but I write the .Net languages and VB6) for an algorithm that rounds numbers ending with 5 towards a given number?

RoundTo(double value, double toWards, int numberOfDigitsBehindComma)

RoundTo(1.25,1,1)=1.2 RoundTo(1.25,2,1)=1.3

RoundTo(1.26,1,1)=1.3 RoundTo(1.24,2,1)=1.2

Include a solution for negative numbers please.

EDIT: There seems to be a lot of confusion about my requirements I will fill in all the Assertions that the resulting code has to meet. My solution does so.

  [TestMethod]
   public void RoundTowards()
   {
      double x=3.44;double y=3.45;double z=4.45;
      double a = 3.51; double b = 4.5001; double c = -1.14; double d = -1.15;
      var mean=4;
      Assert.AreEqual(3.4,x.RoundTowards(mean,1));
      Assert.AreEqual(3.5, y.RoundTowards(mean, 1));
      Assert.AreEqual(4.4, z.RoundTowards(mean, 1));
      Assert.AreEqual(3.5, a.RoundTowards(mean, 1));
      Assert.AreEqual(4.5, b.RoundTowards(mean, 1));
      mean = 5;
      Assert.AreEqual(3.4, x.RoundTowards(mean, 1));
      Assert.AreEqual(3.5, y.RoundTowards(mean, 1));
      Assert.AreEqual(4.5, z.RoundTowards(mean, 1));
      Assert.AreEqual(3.5, a.RoundTowards(mean, 1));
      Assert.AreEqual(4.5, b.RoundTowards(mean, 1));
      mean = 3;
      Assert.AreEqual(3.4, x.RoundTowards(mean, 1));
      Assert.AreEqual(3.4, y.RoundTowards(mean, 1));
      Assert.AreEqual(4.4, z.RoundTowards(mean, 1));
      Assert.AreEqual(3.5, a.RoundTowards(mean, 1));
      Assert.AreEqual(4.5, b.RoundTowards(mean, 1));
      Assert.AreEqual(Math.Round(-1.1,4),Math.Round( c.RoundTowards(mean, 1),4));
      Assert.AreEqual(Math.Round(-1.1,4),Math.Round(d.RoundTowards(mean, 1),4));
      mean = -2;
      Assert.AreEqual(Math.Round(3.4,4),Math.Round( x.RoundTowards(mean, 1),4));
      Assert.AreEqual(Math.Round(3.4,4),Math.Round( y.RoundTowards(mean, 1),4));
      Assert.AreEqual(Math.Round(4.4,4),Math.Round( z.RoundTowards(mean, 1),4));
      Assert.AreEqual(Math.Round(3.5,4),Math.Round( a.RoundTowards(mean, 1),4));
      Assert.AreEqual(Math.Round(4.5,4),Math.Round( b.RoundTowards(mean, 1),4));
      Assert.AreEqual(Math.Round(-1.1, 4), Math.Round(c.RoundTowards(mean, 1), 4));
      Assert.AreEqual(Math.Round(-1.2, 4), Math.Round(d.RoundTowards(mean, 1), 4));

   }

    [TestMethod]
    public void RoundTowardsTowardZero()
    {
        double x = 3.45; double y = -3.45;
        double a = -3.551; double b = 4.551; double c = 4.5500001; double d = 4.5501;
        var mean = 0;
        Assert.AreEqual(3.4, x.RoundTowards(mean, 1));
        Assert.AreEqual(-3.4, y.RoundTowards(mean, 1));
        Assert.AreEqual(-3.6, a.RoundTowards(mean, 1));
        Assert.AreEqual(4.6, b.RoundTowards(mean, 1));
        Assert.AreEqual(4.5, c.RoundTowards(mean, 1));
        Assert.AreEqual(4.6, d.RoundTowards(mean, 1));

    }

  [TestMethod]
  public void Test14_55()
  {
      Assert.AreEqual((14.55).RoundTowards(9, 1) ,14.5);
      Assert.AreEqual((14.55).RoundTowards(15,1), 14.6);
  }

  [TestMethod]
  public void Test14_5499999()
  {
      Assert.AreEqual((14.54999999).RoundTowards(9, 1) ,14.5);
      Assert.AreEqual((14.54999999).RoundTowards(15,1), 14.6);
  }

Thanks!!!

Dabblernl
  • 15,831
  • 18
  • 96
  • 148

6 Answers6

2

Well, this seems to do the job, at least my tests are 'green', that is. I do not particularly like the necessity to round once more in the end as, the re-addition of the value to round can again give birth to typical doubles like .99999999999999999999, in stead of .1

   <Extension()>
    Public Function RoundTo(ByVal value As Double, mean As Double, digitsBehindComma As Integer) As Double
        Dim correctedValue = value - mean
        Dim increasedValue = correctedValue * Math.Pow(10, digitsBehindComma)
        Dim trailingDigitCorrection = Math.Sign(correctedValue) * Math.Pow(10, -4) 'Safeguard against a trailing bit (i.e. 1.50000000001)
        Dim halfAddition = Math.Sign(correctedValue) * 0.5
        Dim division = 10 ^ digitsBehindComma
        Dim sum = increasedValue - trailingDigitCorrection + halfAddition

        Dim result = Fix(increasedValue - addition + halfAddition) / division
        Return Math.Round(result + mean, digitsBehindComma)

    End Function
Dabblernl
  • 15,831
  • 18
  • 96
  • 148
  • Will it work correctly for both 4.55 round towards 1 and 4.55 round towards 10 ? (not sure as I am not a VB programmer) – CygnusX1 Oct 26 '11 at 19:00
  • Yes sir ;-) the trailingDigitCorrection takes care of that. Do I get the much deserved second upvote on this answer now? ;-) – Dabblernl Oct 26 '11 at 22:28
2

Coded in straight C

I'm not sure I completely understood the question, so I made some assumptions Say you are rounding 1.235, I assumed you only wanted the 5 to matter if you where rounding to the decimal place before the 5. So

So RoundTo(1.235,2,1) = 1.2 but RoundTo(1.235,2,2)= 1.24 and RoundTo(1.235,1,2) = 1.23

Works for negative numbers, It is not the least computationally intensive solution, but should be easy to understand and modify.

#include <cstdlib>
#include <iostream>
#include <math.h>

double round(double value, double toWards, int numberOfDigitsBehindComma)
{
     double value1 = floor(value * pow(10,numberOfDigitsBehindComma));
     double value2 = floor(value * pow(10,numberOfDigitsBehindComma + 1)) - value1 * 10;
     if (fabs(value2) > 5 || (fabs(value2) == 5 && toWards > value))
     {
        value1++;
     }
     double value3 = value1 / pow(10,numberOfDigitsBehindComma);
     return value3;      
}
8bitwide
  • 2,071
  • 1
  • 17
  • 24
  • I'm confused about the usage then, I thought the toWards parameter meant to round towards that number in the event of a trailing 5. So 14.55 Rounds Towards 9(Down). – 8bitwide Oct 24 '11 at 03:41
  • Using your algortithm my tests fail on the following assertions:Round(4.551,0,1)==4.6; Round(14.55,9,1)==14.6; Round(-2.45,-2,1)==-2.4; – Dabblernl Oct 24 '11 at 21:01
  • @Dabblernl Why would 4.551 round to 4.6 when you're rounding towards 0? – Gareth Oct 24 '11 at 22:59
  • @Gareth: you round to one digit behind the comma. Therefor you consider the remaining digits starting with second digit after the comma. Do they start with 0-4 you round down, with 6-9 you round up. Is it *exactly* 5 then you consider the toWards parameter. Are they 5 with trailing digits other than zero then you round up. – Dabblernl Oct 25 '11 at 05:32
  • @Dabblernl Round(4.551,0,1) is going to round toward 0(Down) to 4.5 in my solution because there is a 5 in the hundreds place. Round(14.55,9,1) is going to round toward 9(Down) to 14.5 in my solution because there is a 5 in the hundreds place. Round(-2.45,-2,1) should round to 2.4 as you say, its not due to a floating point round off error. floor(value * pow(10,numberOfDigitsBehindComma + 1)); is evaluating to 246 instead of 245. – 8bitwide Oct 25 '11 at 13:50
  • I regret that I have to vote this one down. It does not meet the requirements. – Dabblernl Oct 25 '11 at 23:03
2

Updating @8bitwide's solution below.

EDIT: to deal with error in floating point representations, I replaced value2 == 0.5 with isHalf(value2) function that can do a fuzzy compare. Sounds like that is OK for your purposes since your numbers come from computations on at most thousands of low-precision values (based on bridge tournaments I've attended).

That is, if the number 4.5500000000001 occurs, it is surely a representation of 4.55 instead of the actual number 4.5500000000001.

The test case includes 4.5500001. double has about 15 digits of accuracy, so if you're getting numbers accurate to only 7 digits, something is very wrong with your calculations.

#include <cstdlib>
#include <iostream>
#include <math.h>

bool isHalf(double x)
{
    return abs(x - 0.5) <= 1e-10;  // or whatever degree of fuzziness suits you
}

double round(double value, double toWards, int numberOfDigitsBehindComma)
{
     double value0 = value * pow(10,numberOfDigitsBehindComma);
     double value1 = floor(value0);
     double value2 = value0 - value1; // 0 <= value2 < 1
     if (value2 > 0.5 || isHalf(value2) && toWards > value))
     {
        value1++;
     }
     double value3 = value1 / pow(10,numberOfDigitsBehindComma);
     return value3;      
}
xan
  • 7,511
  • 2
  • 32
  • 45
  • 1
    Only one Assertion fails here, but it is a hidden requierement that I am myself unsure about how to deal with it: Round(4.5500001,4,1)==4.5 The problem is that when a double really should be 4.55 (und thus Round(4.55,4,1) =4.5) such a number sometimes can be represented as 4.550000001 . I want to get rid of such trailing bits but am unsure about how toe deal with that problem. In my own algorithm I remove the trailing digits 4 places further than the number of digits behind the comma that I need. – Dabblernl Oct 25 '11 at 05:44
  • You simply can never represent 4.55 as a double (or any other floating point number working in binary)! 4.55 in binary system is 100.10001100(1100). Something like 1/3 in decimal is 0.333(3). If you cut it at _any_ point, you either get something slightly below 4.55 or slightly over 4.55 (depending on the rounding strategy). Precise 4.55 simply cannot appear on any computer working in binary system. – CygnusX1 Oct 25 '11 at 19:16
  • @CygnusX1: All the more reason then that a solution should take this into account, by considering 4.54999999999, or 4.55000000001 as 4.55 – Dabblernl Oct 26 '11 at 05:42
  • The computer does the opposite: It considers 4.55 as 4.54999999 or 4.550000001. Even the constant in your assertion is in fact not 4.55. Moreover, it shows that you cannot really "round to 2 digits after dot", because you simply cannot represent those numbers! To me, it seems now, that your "hidden requirement" is not mathematically precise and until you precisely specify what you need, we cannot solve this. – CygnusX1 Oct 26 '11 at 07:51
  • @CygnusX1: Thanks for the clarification. This is an amazingly complex subject. It is very important to get this right though. What is unclear about the requirement that a number that exactly ends in a 5 on the position (digitsBehindComma+1) should be rounded towards a given number and to the closest number at digitBehindComma otherwise? The fact that a floating point variable is unable to represent this 100% correctly just makes the solution more challenging. What puzzles me even more is that it is so hard to find expert documentation about this not so uncommon subject. – Dabblernl Oct 26 '11 at 09:04
  • May I ask where are you using it? Is it some financial stuff or so? How about forcing a 10-base system - not using floats or doubles but a special system in the base of 10? Something like 'decimal' type in C#. Alternatively you may want to consider a fixed-point arithmetic: it basically works with integers, but you conceptually divide everything by 1000 (or alike). – CygnusX1 Oct 26 '11 at 09:19
  • @CygnusX1: It is used to score bridge-tournaments, a rounding error at the second digit behind the comma can mean the difference between a championship and second place. It may be that a perfect solution is not possible, but the choices made should at least be clearly documented and explained. – Dabblernl Oct 26 '11 at 09:40
  • @Dabbleml [This document](http://download.oracle.com/docs/cd/E19957-01/806-3568/ncg_goldberg.html) is where many people learn about how floating point numbers behave (and how they don't). The short answer is that if you really need this kind of thing (i.e: rounding based on exact decimal representations), floats/doubles are not your friend because exact decimal representations are not available in general. You will need a purpose-built, integer-based data type. – mhum Oct 27 '11 at 03:57
2

I think all the solutions here are too complicated. Your problem seems to be that you want to be able to control the direction of rounding when you are exactly at the midpoint. You can reduce the problem of having N digits trailing the decimal point just by multiplying and then dividing by an integer power of 10, so it's enough to fix this for the case where the "5" is right after to the decimal point. If you want to round number x so that e.g. 0.5 is rounded upwards to 1, you just do

result = floor(x + 0.5);

If you want to round x so that 0.5 is rounded downwards to 0, you just do

result = ceil(x - 0.5);

These work because if x = 0.5, floor(x + 0.5) = floor(1) = 1, and ceil(x - 0.5) = ceil(0) = 0. To see that other numbers are rounded always correctly,

x = 0.4: floor(x + 0.5) = floor(0.9) = 0
         ceil(x - 0.5) = ceil(-0.1) = 0
x = 0.6: floor(x + 0.5) = floor(1.1) = 1
         ceil(x - 0.5) = ceil(0.1) = 1

so the whole code becomes:

double RoundTo(double value, double towards, int digits) {
  double mult = pow(10, digits); /* to handle variable number of digits */
  bool downwards = (towards < value);
  value *= mult; /* scale */
  value = (downwards ? ceil(value - 0.5)    /* round midpoint downwards */
                     : floor(value + 0.5)); /* round midpoint upwards   */
  return value / mult; /* scale back */
}

This solution also offloads the whole process to the actual mathematics library and your CPU's ALU, and is therefore very robust. This handles obviously negative numbers without any extra tweaking, and works correctly with infinities etc.

Antti Huima
  • 25,136
  • 3
  • 52
  • 71
  • Thank you, this solution resembles my own most, but is more concise. It only does not cater for the cases where the final five is stored as 499999999999 or 5000000001, which is common when you work with floating point variables. I solved this by providing a small addition/subtraction four positions further behind the desired rounding position. But I am unsuere whether this is the best approach. – Dabblernl Oct 29 '11 at 08:48
  • I accepted your answer even though my concern above needs to be addressed. I hope you will find time to comment on it. I changed your algorithm in that I changed the +/- value of .5 to .50001. Now all tests are green. – Dabblernl Oct 30 '11 at 17:43
  • Hi, yes, floating point numbers are difficult beasts to handle. I wouldn't add any adjustment factors because with + 0.50001 you will get the incorrect rounding for 0.499990000. If you can't tolerate any errors whatsoever, you shouldn't use floating-point numbers (as they are never really exact), but e.g. fixed-point numbers in 10-base, e.g. represent numbers as pairs (a, b) where both are integers and the numeric value represented is (a + b / 1000000), say. – Antti Huima Oct 31 '11 at 00:08
1

In relation to our discussion:

[...] it is a hidden requierement that I am myself unsure about how to deal with it: Round(4.5500001,4,1)==4.5 The problem is that when a double really should be 4.55 (und thus Round(4.55,4,1) =4.5) such a number sometimes can be represented as 4.550000001 . I want to get rid of such trailing bits but am unsure about how toe deal with that problem. In my own algorithm I remove the trailing digits 4 places further than the number of digits behind the comma that I need.

It is used to score bridge-tournaments, a rounding error at the second digit behind the comma can mean the difference between a championship and second place.

I presume your software is not time-critical: that is --- you don't perform the computation million of times per second, and it is not the case that if it goes a bit too slow, the software will be unusuable.

To avoid the rounding problems coming from the binary nature of floats or doubles, I would suggest using decimal system for all your relevant calcuations. Of course, the computation will be few times slower than when using binary system, but it should get your computation exact.

In Visual Basic 6 there is a type called Currency, but it is a fixed-point variable: it holds always 4 digits (in decimal) after the dot. VB.NET introduces the Decimal which is not fixed, but works in decimal system as well.

I don't know exactly which mathematical operations it supports, but I am pretty sure that all basic ones are there. Using more complex ones (logarithms, exponents, trigonometric functions) may require some damaging casts, but I hope you don't need that in bridge :)

Once in the world of decimal, you should be able to easily implement the rounding function (e.g. the one provided by xan), without any rounding problems.


An alternative I can suggest -- use integers everywhere. If you always care only about - say - 4 digits after the dot, just multiply all values by 10000 and perform your computation with those "augmented" values. Just pay attention when you perform multiplication.

Community
  • 1
  • 1
CygnusX1
  • 20,968
  • 5
  • 65
  • 109
0

This baby is bit inefficient, maybe it will misbehave on the corner cases, but it appears to do the right thing for the four test points.

#include <stdio.h>
#include <math.h>

double roundto (double val, double towards, unsigned ndigit, double expected)
{
double up, down, mult, res;
int dir;
dir = (val == towards) ? 0 : (val > towards) ? -1  : 1;

mult = pow(10, ndigit);
down = floor (val * mult) / mult;
up = ceil (val * mult) / mult;
if (val-down == up-val) {;}
else dir = (val-down < up-val) ? -1 : 1;
res = dir > 0 ? up : down;

/*
fprintf (stderr, "Val=%f Expected=%f: dir=%d Mult=%f Down=%f Up=%f Res = %f\n"
    , val, expected, dir, mult, down, up, res);
*/

return res;
}

int main(void)
{
double result;

result = roundto(1.25, 1, 1, 1.2);
printf ( "Result = %f\n", result);

result = roundto(1.25, 2, 1, 1.3);
printf ( "Result = %f\n", result);


result = roundto(1.26, 1, 1, 1.3);
printf ( "Result = %f\n", result);

result = roundto(1.24, 2, 1, 1.2);
printf ( "Result = %f\n", result);

return 0;
}
wildplasser
  • 43,142
  • 8
  • 66
  • 109