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));
1 Answer 1
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
).
-
\$\begingroup\$ Thanks for the advice!
gcd
was originally intended to be used only inadd
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 seengcd
for Euclid's Algorithm, but we still called it Greatest Common Factor in school. \$\endgroup\$Joshua Dawson– Joshua Dawson2016年06月23日 22:34:57 +00:00Commented Jun 23, 2016 at 22:34
Explore related questions
See similar questions with these tags.