1
\$\begingroup\$

This is my JavaScript Rational class, similar to other languages' BigFraction class, but with one major difference:

This class doesn't support arbitrarily large rational numbers:

i.e. new Rational(Number.MAX_VALUE + 1) may fail.
However, it supports arbitrarily precise numbers
i.e. new Rational(1, 10).add(new Rational(2, 10)) will be exactly new Rational(3, 10) instead of .3000000000004.

window.Rational = (function() {
 var GCF, LCM;
 // Internal GCF and LCM
 GCF = function(a, b) {
 var temp;
 if (a < 0) a *= -1;
 if (b < 0) b *= -1;
 if (a < b) {
 temp = a;
 a = b;
 b = temp;
 }
 return a % b === 0 ? b : GCF(a % b, b);
 };
 LCM = function(a, b) {
 return Math.abs(a * b) / GCF(a, b);
 };
 function Rational(top, bottom) {
 this.top = top;
 // bottom defaults to 1
 this.bottom = bottom != null ? bottom : 1;
 this.simplify();
 }
 Rational.prototype.add = function(other) {
 var lcm = LCM(this.bottom, other.bottom);
 // If we multiply by lcm
 // it becomes an integer
 // but we round to prevent floating point innacuracies
 var top1 = Math.round(this * lcm);
 var top2 = Math.round(other * lcm);
 return new Rational(top1 + top2, lcm);
 };
 Rational.prototype.subtract = function(other) {
 return this.add(other.multiply(Rational.constants.NEGATIVE_ONE));
 };
 Rational.prototype.multiply = function(other) {
 return new Rational(this.top * other.top, this.bottom * other.bottom);
 };
 Rational.prototype.divide = function(other) {
 return this.multiply(other.reciprocal());
 };
 Rational.prototype.reciprocal = function() {
 return new Rational(this.bottom, this.top);
 };
 Rational.prototype.isLessThan = function(other) {
 return this < other;
 };
 Rational.prototype.isGreaterThan = function(other) {
 return this > other;
 };
 Rational.prototype.isEqualTo = function(other) {
 return this.top === other.top && this.bottom === other.bottom;
 };
 Rational.prototype.valueOf = function() {
 return this.top / this.bottom;
 };
 Rational.prototype.simplify = function() {
 var gcf;
 if (this.top === 0) {
 // No need to calculate GCF because GCF = 0
 this.bottom = 1;
 return;
 }
 if (this.bottom === 0) {
 throw new Error("Cannot have denominator 0");
 }
 gcf = GCF(this.top, this.bottom);
 if (this.top < 0 && this.bottom < 0) {
 // Since GCF doesn't handle negatives
 // if both numerator and denominators are negative
 // we can cancel the negative
 gcf *= -1;
 }
 // If the bottom is less than zero, then move the negative sign to the top
 if (this.bottom < 0 && this.top >= 0) {
 this.top *= -1;
 this.bottom *= -1;
 }
 this.top /= gcf;
 this.bottom /= gcf;
 return this;
 };
 Rational.constants = {
 ZERO: new Rational(0),
 ONE: new Rational(1),
 NEGATIVE_ONE: new Rational(-1)
 };
 return Rational;
})();

Questions:

  • I think that by doing this * 5 or other * 6, I implicitly call valueOf(). Is that okay? Should I make it explicit (to me, saying this * 5 is clear: multiply this by 5), but I'm not sure in general.
  • Same as the question above for this < other and this > other.
  • Can I refactor my GCF and LCM algorithms to take into account negative numbers so that I don't have to handle them as special cases in the simplify function? Right now, they simply make everything positive and return a positive number.
Quill
12k5 gold badges41 silver badges93 bronze badges
asked Oct 14, 2014 at 21:25
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

Not a complete answer, but I noticed some bugs:

LCM = function(a, b) {
 return Math.abs(a * b) / GCF(a, b);
};

The computation of a * b will easily overflow (thus losing precision) for large numbers a, b. You can improve matters by rewriting it as

function LCM(a, b) {
 var g = a / GCF(a,b);
 return Math.abs(g * b);
}

I also used function LCM as opposed to LCM = function just as a stylistic preference. See here.

Similarly:

Rational.prototype.add = function(other) {
 var lcm = LCM(this.bottom, other.bottom);
 // If we multiply by lcm
 // it becomes an integer
 // but we round to prevent floating point innacuracies
 var top1 = Math.round(this * lcm);
 var top2 = Math.round(other * lcm);
 return new Rational(top1 + top2, lcm);
};

If I understand correctly, in the expression this * lcm, lcm is a Number, so you're basically doing this.valueOf() * lcm, which again wipes out all your pretended precision. You should avoid big numbers (which become floating-point), and you should definitely avoid unnecessary floating-point divisions.

Rational.prototype.add = function(other) {
 var lcm = LCM(this.bottom, other.bottom);
 var top1 = this.top * (lcm / this.bottom);
 var top2 = other.top * (lcm / other.bottom);
 return new Rational(top1 + top2, lcm);
};
answered Oct 15, 2014 at 7:25
\$\endgroup\$
2
  • \$\begingroup\$ Your add function doesn't work. It should be lcm / this.bottom not this.bottom / lcm. But isn't that the same thing: this.top * (lcm / this.bottom) and (this.top / this.bottom) * lcm? \$\endgroup\$ Commented Oct 15, 2014 at 20:35
  • \$\begingroup\$ Good catch; I'll fix it inline. But to your question: No! A concrete example is top=3, bottom=47, lcm=188. (3 * (188/47)) is correctly 12, but (3/47) * 188 is incorrectly 11.999999999999998. Try it for yourself! :) Remember, the whole point of your project is to use exact rational representations, so any time you have to drop into floating-point should be a huge red flag to you. \$\endgroup\$ Commented Oct 15, 2014 at 23:45

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.