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.
\$\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);
};
\$\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\$
    – soktinpk
    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

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.