Table of Contents

Fractions in JavaScript

Does it take more time to teach fractions to JavaScript than to a 12-year-old?

26 Dec 2020. 1513 words.


Motivation

In the world of programming, hardly anyone cares about the differences between $\frac{1}{3}$ and 1.0/3.0 = 0.333333. However, in Maths class, you need to learn how to deal with fractions as-is; for example, what is $\frac{2}{3} x - \frac{1}{2}$ equal to when $x = \frac{1}{6}$? The answer is \begin{align*} \dfrac{2}{3} x - \dfrac{1}{2} &= \dfrac{2}{3} \cdot \dfrac{1}{6} - \dfrac{1}{2} \\ &= \dfrac{2}{18} - \dfrac{1}{2} \\ &= \dfrac{2}{18} - \dfrac{9}{18} \\ &= -\dfrac{7}{18}. \end{align*} While we can argue about whether this skill is necessary for our future generation or not, it is certainly useful for me to have a set of code that performs arithmetic with fractions, instead of something like

console.log(1/3 * 1/6 - 1/2); // -0.3888888888888889

and then trying to build a code that converts -0.3888888888888889 into $-\frac{7}{18}$. Of course, there already exists a library that implements fractions beautifully in JavaScript - Fraction.js, which would be very useful for general purposes. However, most of the features Fraction.js provide are unnecessary for this site, and I specifically wanted the ability to print a fraction instance as a LaTeX expression, such as

new Frac(3, 7).tex(); // \frac{3}{7}

Therefore, I decided to build my own fraction package from scratch, and document the journey to my first (useful) JavaScript class! While this post (and the code) is mostly for myself, I hope the readers to learn something from here - whether it is about fractions, about JavaScript, or both!

update Shortly after I wrote this post, I found Nerdamer, which encompasses most of the features I wish to accomplish. Rather than spending months to reinvent the wheel, I decided to use Nerdamer in conjunction with my classes. Check more information on setting up Nerdamer in Hugo.

The Frac(tion) class

A fraction has a numerator and a denominator, and its value is really just its numerator divided by its denominator; for example, $$ \frac{2}{5} = 2\div 5 = 0.4. $$ Hence we can start with our (very simple) constructor and valueOf() function:

class Frac {
  constructor(n, d = 1) {
    if (d == 0) {
      throw new Error("denominator cannot be zero!");
    }
    this.n = n;
    this.d = d;
  }
  valueOf() {
    return this.n / this.d;
  }
}

To create Frac instances, we feed the values of the numerator and the denominator, or just a single number, in which case it will be interpreted as a whole number.

new Frac(1, 2); // 1/2
new Frac(3);    // 3/1 = 3
new Frac(1, 0); // Error: denominator cannot be zero!

The valueOf() function is executed when you compare its value with another object. This makes comparison between a fraction with another or a float very easy.

new Frac(2, 5).valueOf()        // 0.4
new Frac(1, 4) > new Frac(2, 7) // false
new Frac(0, 3) == 0             // true
new Frac(-1, 2) < 0             // true

Simplification

To simplify a fraction, we need to divide the numerator and the denominator by their greatest common divisor (GCD): $$ \frac{12}{18} = \frac{12\hl{\div 6}}{18\hl{\div 6}} = \frac{2}{3}. $$ So how do we calculate the GCD of two numbers? We can use the Euclidean algorithm: the GCD of two numbers is the same as the GCD of one number, and the remainder when you divide the other number with that number. So we can write a recursive function that does the job:

function findGcd(a, b) {
  if (b != 0) {
    return findGcd(b, a % b);
  } else {
    return a;
  }
}

or, using the ternary operator ?:

function findGcd(a, b) {
  return b ? findGcd(b, a % b) : a;
}

With this, we can write our function that simplifies the fraction:

class Frac {
  ...
  reduce() {
    const gcd = findGcd(this.n, this.d);
    return new Frac(this.n / gcd, this.d / gcd);
  }
}

This is an example:

const exampleFrac = new Frac(3, 6);   // 3/6
const newFrac = exampleFrac.reduce(); // 1/2

Note Returning a new instance of Frac, instead of doing something like,

class Frac {
  reduce() {
    const gcd = findGcd(this.n, this.d);
    this.n /= gcd;
    this.d /= gcd;
  }
}

allows us to chain operations:

const myFrac = new Frac(3, 6).add(new Frac(1, 3)); // 5/6

Decimal entries

What do you think these will return?

new Frac(0.5, 2).reduce();
new Frac(6, 0.7).reduce();

While the first one equals 1/4, which is expected, the second one equals 27021597764222976/3152519739159347 – clearly an overflow happened! So, to fix this issue, I updated reduce() so that it will try to multiply both the numerator and the denominator by 10 until both of them become integers.

class Frac {
  ...
  reduce() {
    if (Number.isInteger(this.n) && Number.isInteger(this.d)) {
      const gcd = findGcd(this.n, this.d);
      return new Frac(this.n / gcd, this.d / gcd);
    } else {
      // try reducing 10n / 10d
      return new Frac(this.n * 10, this.d * 10).reduce();
    }
  }
}

This code simplifies fractions with decimal entries correctly:

new Frac(0.5, 2).reduce(); // 1/4
new Frac(6, 0.7).reduce(); // 60/7

Addition

Our aim is to make a function that adds a number, or another Func instance to itself:

new Frac(1, 4).add(new Frac(1, 6)); // 5/12
new Frac(1, 2).add(-2);             // -3/2

Obviously the second is equivalent to adding new Frac(-2, 1), we only need to deal with the first case. Let’s think about how we add two fractions - we find the least common multiple (LCM) of the denominators, and match the denominators to their LCM in order to add them: \begin{align*} \dfrac{1}{4} + \dfrac{1}{6} &= \dfrac{1\hl{\times 3}}{4\hl{\times 3}} + \dfrac{1\hl{\times 2}}{6\hl{\times 2}} \qquad \text{(LCM is 12)} \\ &= \dfrac{3}{12} + \dfrac{2}{12} = \dfrac{5}{12}. \end{align*}

While this is the simplest method for humans, multiplying the numbers to their LCM is a complicated process. We use the fact $$ \text{LCM}(a, b)\times \text{GCD}(a,b)=a\times b $$ to generalise addition of two fractions: \begin{align*} \dfrac{n_1}{d_1} + \dfrac{n_2}{d_2} &= \dfrac{n_1 \hl{\times d_2/\text{GCD}(d_1,d_2)}}{d_1\hl{\times d_2/\text{GCD}(d_1,d_2)}} + \dfrac{n_2\hl{\times d_1/\text{GCD}(d_1,d_2)}}{d_2\hl{\times d_1/\text{GCD}(d_1,d_2)}} \\ &= \dfrac{n_1 d_2 / \text{GCD}(d_1, d_2)}{\text{LCM}(d_1, d_2)} + \dfrac{n_2 d_1 / \text{GCD}(d_1, d_2)}{\text{LCM}(d_1, d_2)} \\ &= \dfrac{n_1 d_2 / \text{GCD}(d_1, d_2) + n_2 d_1 / \text{GCD}(d_1, d_2)}{\text{LCM}(d_1, d_2)}. \end{align*}

If we write this up, we have the add() function!

class Frac {
  ...
  add(other) {
    if (other instanceof Frac) {
      let gcd = findGcd(this.d, other.d);
      return new Frac(
        this.n * other.d / gcd + other.n * this.d / gcd,
        this.d * other.d / gcd
      ).reduce();
    } else if (!isNaN(other)) {
      // if other is a number
      return this.add(new Frac(other));
    } else {
      throw new Error("can only add numbers!");
    }
  }
}

Multiplication

Likewise, we would like to achieve something like this:

new Frac(2,9).mult(-3,2); // returns -1/3
new Frac(1,4).mult(6);    // returns 3/2

Fortunately, multiplying two fractions is much easier than adding! \begin{align*} \dfrac{n_1}{d_1} \times \dfrac{n_2}{d_2} = \dfrac{n_1\times n_2}{d_1 \times d_2}, \end{align*}

hence

class Frac {
  ...
  mult(other) {
    if (other instanceof Frac) {
      return new Frac(this.n * other.n, this.d * other.d).reduce();
    } else if (!isNaN(other)) {
      // if other is a number
      return this.mult(new Frac(other));
    } else {
      throw new Error("can only multiply numbers!");
    }
  }
}

Subtraction and division

Because subtracting a number is equivalent to adding its negative, and dividing by a number is equivalent to multiplying its reciprocal, we can simply define subtraction and division as follows.

class Frac {
  ...
  // returns the reciprocal of the fraction
  reci() {
    return new Frac(this.d, this.n);
  }

  // subtracts a number or fraction from this fraction
  sub(other) {
    if (other instanceof Frac) {
      return this.add(other.mult(-1));
    } else if (!isNaN(other)) {
      return this.add(-other);
    } else {
      throw new Error("can only subtract numbers!");
    }
  }

  // divide this fraction by a number or fraction
  div(other) {
    if (other instanceof Frac) {
      return this.mult(other.reci());
    } else if (!isNaN(other)) {
      return this.mult(new Frac(1, other));
    } else {
      throw new Error("can only divide numbers!");
    }
  }
}

Power

We know the following holds for any integer $n$ : \begin{align*} & \left( \dfrac{a}{b} \right)^n = \dfrac{a^n}{b^n}, \\ & \left( \dfrac{a}{b} \right)^{-n} = \left( \dfrac{b}{a} \right)^n. \end{align*}

We can write this up as a simple code.

class Frac {
  ...
  // calculate the n-th power of the fraction
  pow(exp) {
    if (exp < 0) {
      return this.reci().pow(-exp);
    } else if (exp == 1) {
      return this;
    } else {
      return new Frac(this.n ** exp, this.d ** exp);
    }
  }
}

Note that this does allow exp to be rational as well, but we cannot deal with irrational numbers now, so we need to be careful.

new Frac(1, 2).pow(0.5).reduce() // 125000000000000/176776695296637

Extensions

  1. Extending the fractions to have surds or complex numbers. Then, calling .reduce() will also rationalise or realise the denominator.

  2. Tracking the operations acted on itself, so it is easier to write questions. For example, we can do

    const answer = oldFunc.mult(3).add(new Func(-1, 2)).div(2, 7);
    
    console.log(answer.trace);
    /* [["init", {n: 1, d: 4}], ["mult", {n: 3, d: 1}],
       ["add", {n: -1, d: 2}], ["div", {n: 2, d: 7}]]  */
    answer.trace.tex()
    /* \left(\frac{1}{4} \times 3 - \frac{1}{2} \right) \div \frac{2}{7} */