2

I'm writing a BigDecimal class in C#. I've successfully implemented +, - and * operators. But I can't figure out a way to calculate division of 2 BigDecimals. What's the quickest way to implement division using those 3 operators? Or is there a better way to do that? (Considering both development time and algorithm speed)

Goal 1: I want the result to be another BigDecimal with a fixed precision (that should be changeable)

Goal 2: As you mentioned, the purpose of BigDecimal is not a fixed precision. So how can I achieve infinite precision?

Another Question: Is it better (regarding speed and flexibility) to use Microsoft BCL's BigRational class for Arbitrary-precision arithmetic and then use Christopher Currens's extension method in this thread: Is there a BigFloat class in C#? to get decimal representation instead of writing a new class?

Community
  • 1
  • 1
SepehrM
  • 1,087
  • 2
  • 20
  • 44
  • Are you trying to implement "long division" explicitly? Did you learn to do division by hand in school (sorry for asking but the math curriculum isn't what it used to be...). http://www.doubledivision.org/ – Floris Aug 29 '13 at 14:09
  • 3
    What would you like the output of 1/3 to be? Infinite number of threes? – Eric Lippert Aug 29 '13 at 14:13
  • @EricLippert I want the result of 1/3 to be 0.33333333333333333333 with my desired precision ... – SepehrM Aug 29 '13 at 14:18
  • So your big decimal is arbitrary precision? – Eric Lippert Aug 29 '13 at 14:20
  • @EricLippert That's right! My bad ... – SepehrM Aug 29 '13 at 14:24
  • @EricLippert How is it possible to have "infinite" precision? – SepehrM Aug 29 '13 at 14:26
  • 1
    @SepehrM You don't actually resolve the result to a single numeric value. The internal state of the `BigDecimal` would simply be a numerator an a denominator (possibly simplified). This would let you represent any rational number without limiting yourself to a fixed precision. It wouldn't let you represent an irrational number though. – Servy Aug 29 '13 at 14:35
  • @Floris I'm a first year student in computer science and I do know long division. However my English is not perfect. So don't mistake my bad English for lack of knowledge. – SepehrM Aug 29 '13 at 19:06
  • @SepehrM - no problem; I was asking what you knew - because it changes what kind of answer would make sense _for you_. Not intended as a "hah you know nothing" comment - I hope you didn't take it that way. – Floris Aug 29 '13 at 20:05
  • 1
    Your "another question" is not answerable. "Better" implies that there is a metric you are using to evaluate the costs of two possibilities against the value they produce in achieving a goal. Without knowing what either your cost metric is or what your goals are, how can we possibly know which is going to be better for you? – Eric Lippert Aug 30 '13 at 15:57
  • @Servy Thanks I see. But then what would be the difference between BigDecimal and BigRational (already implemented in Microsoft's BCL)? – SepehrM Aug 30 '13 at 18:09

3 Answers3

10

First off, I presume that by "big decimal" you mean that you are representing a rational where the denominator is restricted to be any power of ten.

You'll want to think hard about what you want the output of the division of two decimals to be. Decimals are closed over addition, subtraction and multiplication but not closed over division. That is, any two decimals multiplied together produces a third: (7 / 10) * (9 / 100) gives you 63 / 1000, which is another decimal. But divide those two decimals and you get a rational that does not have a power of ten in the denominator.

To answer the question you actually asked: just as multiplication can be built out of addition in a loop, division can be built out of subtraction in a loop. To divide 23 by 7, say:

  • Is 7 < 23? Yes.
  • Subtract 7 from 23 to get 16.
  • Is 7 < 16? Yes.
  • Subtract 7 from 16 to get 9
  • Is 7 < 9? Yes.
  • Subtract 7 from 9 to get 2.
  • Is 7 < 2? No. We have our first digit. We did three subtractions, so the first digit is 3.
  • Multiply 2 by 10 to get 20
  • Is 7 < 20? Yes.
  • Subtract 7 from 20 to get 13.
  • Is 7 < 13? Yes.
  • Subtract 7 from 15 to get 6.
  • Is 7 < 6? No. We did two subtractions and a multiplication by 10, so the next digit is 2.
  • Multiply 6 by 10 to get 60
  • Is 7 < 60? Yes...
  • ...
  • We did eight subtractions, so the next digit is 8...
  • ... and so on

Do you know of a faster algorithm for this purpose?

Sure, there are lots of faster algorithms for division. Here's one: Goldschmidt's Algorithm.

First off, I hope that it is clear that if you are trying to compute X / D then you can first compute 1 / D and then multiply that by X. Moreover, let us assume WOLOG that D is strictly between 0 and 1.

What if it isn't? If D is negative, invert both it and X; if D is zero, give an error; if D is 1 then the answer is X; if D is greater than 1 then divide it and X both by 10, which should be easy for you since your system is decimal. Keep applying those rules until you have a D between zero and one. (As an additional optimization: The algorithm is slowest when D is very small, so if D is less than, say, 0.1, multiply X and D by ten until D is greater than or equal to 0.1.)

OK, so our problem is that we have a number D between zero and one and we wish to compute 1 / D. Probably the easiest thing to do is to work an example. Suppose we are trying to compute 1 / 0.7. The correct answer is 1.42857142857...

Start by subtracting 0.7 from 2 to get 1.3. Now multiply both parts of the fraction by 1.3:

(1 / 0.7) * (1.3 / 1.3) = 1.3 / 0.91

Great. We have now computed 1 / 0.7 to one digit of accuracy.

Now do it again. Subtract 0.91 from 2 to get 1.09. Multiply both parts of the fraction by 1.09:

(1.3 / 0.91) * (1.09 / 1.09) = 1.417 / 0.9919

Great, now we have two correct digits. Now do it again. Subtract 0.9919 from 2 to get 1.0081. Multiply top and bottom by 1.0081:

(1.417 / 0.9919) * (1.0081 / 1.0081) = 1.4284777 / 0.99993439

Hey, now we have four correct digits. See how that goes? Every step of the way the denominator gets much closer to 1, and therefore the numerator gets closer to 1 / 0.7.

This converges much more quickly than the subtraction method.

Do you see why it works?

Since D is between 0 and 1, there is a number E such that D = 1 - E, and E is also between 0 and 1.

When we multiply D by (2 - D), we are multiplying (1 - E) by (1 + E), which gives 1 - E2.

Since 0 < E < 1, clearly E2 is smaller than E and also between 0 and 1, which means that 1 - E2 is closer to 1. In fact it is a lot closer to 1. By repeating this process multiple times we get close to 1 very quickly. Effectively what we're doing here is roughly doubling the number of correct digits on each step. Obviously that is a lot better than the subtraction method, which gives one additional digit on each step.

Keep on doing that until you have the accuracy you desire. Since you're roughly doubling the number of accurate digits on each step, you should be able to get to an acceptable degree of accuracy pretty quickly. Since we already arranged that D is greater than or equal to 0.1 to start with, E is never larger than 0.9; repeatedly squaring 0.9 gets you down to a very small number pretty quickly.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • +1 That looks so easy to implement, but is it wise to do so? Isn't it too slow? – SepehrM Aug 29 '13 at 19:07
  • 1
    @SepehrM: You can answer this question by **setting a performance goal**, implementing the code and then **testing to see if you met your goal**. – Eric Lippert Aug 29 '13 at 19:11
  • I did that and I just tried DreamWalker's code snippet and It was far away from my desired goal. So do you know of a faster algorithm for this purpose? – SepehrM Aug 29 '13 at 19:17
  • @SepehrM: I've posted a much faster algorithm. – Eric Lippert Aug 30 '13 at 15:00
1
  • You can look to Java BigDecimal implementation for some ideas
  • For calculate a/b you can use Binary search algorithm for find such c that b * c = a (you should run algorithm until the desired precision is reached).
  • Also you can look to this BigFloat class, it has interesting implementation with exponential form
  • For infinite precision you can store you BigDecimal as rational fraction of two BigInteger.

Algebra for rational fraction represention:

(x1/x2) + (y1/y2) = (x1*y2+x2*y1)/(x2*y2)    
(x1/x2) - (y1/y2) = (x1*y2-x2*y1)/(x2*y2)    
(x1/x2) * (y1/y2) = (x1*y1)/(x2*y2)
(x1/x2) / (y1/y2) = (x1*y2)/(x2*y1)

Example of implementation for double (fixed precision):

public double Divide(double a, double b, double eps)
{
    double l = 0, r = a;
    while (r - l > eps)
    {
        double m = (l + r) / 2;
        if (m * b < a)
            l = m;
        else
            r = m;
    }
    return (l + r) / 2;
}
AndreyAkinshin
  • 18,603
  • 29
  • 96
  • 155
  • Your second choice only works for integer division (which, if that's what he wants, may be fine). If he needs non-integer division then it won't really help. – Servy Aug 29 '13 at 14:17
  • Fot non-integer division we can use algorithm until the required accuracy is reached – AndreyAkinshin Aug 29 '13 at 14:18
  • But the idea of a BigDecimal is that it doesn't have a fixed precision, it has "infinite" precision. – Servy Aug 29 '13 at 14:19
  • Look to the comment 3 to question by author: it is about the desired accuracy, not about infinite precision – AndreyAkinshin Aug 29 '13 at 14:22
  • He posted that after my comment, and that's not the standard implementation of a BigDecimal, but sure, if that's what he wants, then it's possible to do that. – Servy Aug 29 '13 at 14:22
  • @Servy and DreamWalker: Thanks. I edited my post with my 2nd goal. How is that achievable? – SepehrM Aug 29 '13 at 14:33
  • @DreamWalker I tested your code snippet but It's too slow! Isn't there a faster algorithm? – SepehrM Aug 29 '13 at 19:18
  • You asked about quickest way to implement — it's solution with small amount of code. Solution with good performance takes many lines of code. Try google some articles about long arithmetic — there are a lot of theory about this topic. – AndreyAkinshin Aug 29 '13 at 20:22
0

Eric Lippert's answer looks a lot like "long division in the slowest possible way". It is accurate, budt it's not fast. Assuming that you have a way of approximating your BigDecimal with a double, and since you already have +, - and * implemented in your BD class, you could do (approximately) the following:

BD operator/(BD a, BD b) {
  BD r, result=0, residual;
  double aa, bb, rr;
  residual = a;
  bb = b;
  while (residual > desired) {
    rr = (double)residual / bb;
    r = rr; // assuming you have a conversion from double to bigdecimal with loss of precision
    result += r;
    residual = a - (result * b);
  }
  return result;
}

This will do a successive approximation, but many digits at once (basically you find the error using BD arithmethic, then divide that out using double arithmetic). I think that should be a relatively straightforward and efficient approach.

I have implemented a version of the above using float and double - just to prove to myself that the principle can work. Using a straightforward C framework, it takes just two iterations to get the precision down to the level of the double division. I hope you get the idea.

#include <stdio.h>

double divide(double a, double b);
int main(void) {
  double a, b, result;
  float fa, fb, fr;
  a = 123.5;
  b = 234.6;
  fa = a;
  fb = b;
  fr = fa / fb;
  printf("using float: %f\n", fr);
  result = divide(a, b);
  printf("using double: %lf\n", result);
  printf("difference: %le\n", result - fr);
}

double divide(double a, double b) {
      double r, result=0, residual;
      float aa, bb, rr;
      residual = a;
      bb = b;
      while (residual > 1e-8) {
        rr = (float)residual / bb;
        r = rr; // assuming you have a conversion from double to bigdecimal with loss of precision
        result += r;
        residual = a - (result * b);
        printf("residual is now %le\n", residual);
      }
      return result;
    }

Output:

using float: 0.526428
residual is now 8.881092e-06
residual is now 5.684342e-14
using double: 0.526428
difference: 3.785632e-08
Floris
  • 45,857
  • 6
  • 70
  • 122
  • This answer was "sitting on a page" for several days before I submitted it - I see that since I wrote it Eric produced a much more detailed and faster solution already. I still think my implementation may shed some further light on this, so I'll leave it here. – Floris Sep 05 '13 at 14:46