2
\$\begingroup\$

I've been working though Project Euler problems using JavaScript, and along the way I've found it useful to create "classes" for different types of numbers (like an integer that can be arbitrarily large). I recently decided to create a sort of class for Fractions to avoid floating point error.

I wrote the code using a more functional/procedural approach; however, I'm concerned that since JavaScript can't use types to distinguish functions that the function names will overlap with other code I write. I'm also looking for shorter names or better syntax to simplify how messy the math can get as shown in my tests. Here's a JSFiddle if you'd like to see the code in action.

General Global Functions

// Calculates the greatest common factor of two positive integers
function gcf(p, q) {
  if (p > q) return gcf(q, p);
  else if (p === 0) return q;
  else return gcf(q % p, p);
}

// Reduces two numbers by their greatest common factor
function gcd(p, q) {
  var f = gcf(p, q);

  return [
    p / f,
    q / f
  ];
}

Fraction Constructor

function createFraction(n, d) {
  if (d < 0) {
    n *= -1;
    d *= -1;
  }

  var f0 = reduce({
    n: n,
    d: d
  });
  return f0;
}

Fraction Utility Functions

function reduce(f0) {
  var f = gcf(Math.abs(f0.n), f0.d);
  f0.n /= f;
  f0.d /= f;
  return f0;
}

function invert(f0) {
  return createFraction(
    f0.d,
    f0.n
  );
}

function negate(f0) {
  return createFraction(
    f0.n * -1,
    f0.d
  );
}

Fraction Basic Math Functions

function multiply(f1, f2) {
  return createFraction(
    f1.n * f2.n,
    f1.d * f2.d
  );
}

function divide(f1, f2) {
  return multiply(f1, invert(f2));
}

function add(f1, f2) {
  var r = gcd(f1.d, f2.d);

  return createFraction(
    f1.n * r[1] + f2.n * r[0],
    f1.d * r[1]
  );
}

function subtract(f1, f2) {
  return add(f1, negate(f2));
}

Testing Code

function equals(f1, f2) {
  return f1.n === f2.n &&
    f1.d === f2.d;
}

function toString(f0) {
  return f0.n + "/" + f0.d;
}

function testEquals(line, f1, f2) {
  if (!equals(f1, f2)) console.log(line + " failed: " + toString(f1) + " !== " + toString(f2));
  else console.log(line + " good: " + toString(f1));
}

var line = 0;
testEquals(line++, add(createFraction(10, 40), createFraction(3, 30)), createFraction(7, 20));
testEquals(line++, subtract(createFraction(8, 3), createFraction(11, 30)), createFraction(69, 30));

testEquals(line++, multiply(createFraction(8, 3), createFraction(11, 30)), createFraction(88, 90));
testEquals(line++, divide(createFraction(8, 3), createFraction(11, 30)), createFraction(240, 33));

testEquals(line++, add(createFraction(-5, 8), createFraction(-1, -2)), createFraction(-1, 8));
testEquals(line++, subtract(createFraction(10, 40), createFraction(-3, 30)), createFraction(7, 20));

testEquals(line++, multiply(createFraction(-8, 3), createFraction(-11, 30)), createFraction(88, 90));
testEquals(line++, divide(createFraction(8, -3), createFraction(11, -30)), createFraction(240, 33));
\$\endgroup\$
1
\$\begingroup\$

I find a couple of the names misleading. What you call gcf I have only previously heard called HCF (highest common factor) or, more commonly, GCD (greatest common divisor). So I expect that function to be called gcd, and the one which is called gcd to be reduce. Of course, you also have a function called reduce which is basically just gcd with a different calling convention. I would be tempted to move the sign-handling into gcd, eliminate reduce, and then rename.

I'd also prefer to call invert something like recip (for reciprocal). It's less likely to cause a name collision, and also IMO describes better what it does.


function reduce(f0) {
  var f = gcf(Math.abs(f0.n), f0.d);
  f0.n /= f;
  f0.d /= f;
  return f0;
}

I presume that you modify the input and also return it because you want to support a fluent style, but I can't actually see any evidence of that here. I've already suggested refactoring this function away, but if I hadn't then I would suggest making it return a new object and treating fraction objects as immutable. That favours maintainability and probably also helps JIT.


function multiply(f1, f2) {
  return createFraction(
    f1.n * f2.n,
    f1.d * f2.d
  );
}

This function is worth special-casing in a fraction implementation. You already know that each fraction is reduced, so the only common factors can be crosswise. By separately reducing (f1.n, f2.d) and (f2.n, f1.d) you can keep the intermediate values smaller and reduce the risk of overflow. (I see you've made the corresponding optimisation in add).

\$\endgroup\$
1
  • \$\begingroup\$ Thanks for the advice! gcd was originally intended to be used only in add to find the "G̶r̶e̶a̶t̶e̶s̶t̶ Lowest Common Denominator" - just realized how wrong that was. I believe I've also normally seen gcd for Euclid's Algorithm, but we still called it Greatest Common Factor in school. \$\endgroup\$ Jun 23 '16 at 22:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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