Complex Numbers in JavaScript

David Banks
6 min readApr 16, 2023
An imaginary monster thinking of a real number. (Image: DALL-E)

Introduction

I’ve been playing with the Mandelbrot Set lately, which has meant that I’ve had to dust off my memory of how to do arithmetic with complex numbers. I created a nice JavaScript class help grease using them. Here’s now it works (it’s a quite a dry explanation, but I hope it’s useful).

A Quick Refresher

When we multiply two numbers together the result will be odd or even depending on whether the numbers we're multiplying are:

-1 *  1 = -1
1 * -1 = -1
1 * 1 = 1
-1 * -1 = 1

Notice that squaring creates a positive number! There is no number that one can square that produces a negative number, which is where the imaginary number comes into play. The letter i is used to represent the imaginary number which, when squared, equals -1.

  i  ≔ √(-1)
∴ i² = -1

It’s literally another number line, just like we have -2, -1, 0, 1, 2… we have -2i, -1i, 0i, 1i, 2i, 3i….

A complex number is a number that have a real part and an imaginary part. They’re written in the form a + bi.

As 0i = 0 the number lines meet at 0, so we can plot these numbers on a graph, typically the x axis is the real number line, and the y axis is the imaginary one.

Some Examples

3 + 2i plotted on the complex plane
3–8i plotted on the complex plane
-15 + 9i plotted on the complex plane

Arithmetic

You may have noticed that complex numbers look a lot like vectors, and with the caveat that i * i = -1 they are. All the operations are just regular vector operations that are adjusted based on that caveat.

Code

The Complex class constructor has two parameters.

  • real
  • imaginary
export class Complex {
constructor(real, imaginary) {
this.real = real;
this.imaginary = imaginary;
}
}

Zero

Zero is 0 + 0i.

/***
* Generate a new <code>Complex(0,0)</code>
* @returns {Complex}
*/
static zero = () => new Complex(0, 0);

toString()

/***
* Returns a string in the form <code>a ± bi</code>.
* @returns {string}
*/
toString() {
const operator = this.imaginary < 0 ? '-' : '+';
return `${this.real} ${operator} ${Math.abs(this.imaginary)}i`;
}

Equals

/***
* Both <i>real</i> and <i>imaginary</i> parts are equal.
* @param other
* @returns {boolean}
*/
equals(other) {
return this.real === other.real && this.imaginary === other.imaginary;
}

Add

add(other) {
return new Complex(
this.real + other.real,
this.imaginary + other.imaginary
);
}

Subtract


subtract(other) {
return new Complex(
this.real — other.real,
this.imaginary — other.imaginary
);
}

Multiply

Derivation of the multiplication formula
/***
* Multiple this Complex with another.<br/>
* <code>(a + bi)(c + di) = (ac - bd) + (ad + bc)i</code>
* @param other
* @returns {Complex}
*/
multiply(other) {
return new Complex(
this.real * other.real - this.imaginary * other.imaginary,
this.real * other.imaginary + this.imaginary * other.real
);
}

Division

/***
* <code>(a + bi) / (c + di) = [(ac + bd) / (c^2 + d^2)] + [(bc - ad) / (c^2 + d^2)]i<code>
* @param other
*/
divide(other) {
const otherMagnitudeSquared =
other.real * other.real + other.imaginary * other.imaginary;
const r =
(this.real * other.real + this.imaginary * other.imaginary) /
otherMagnitudeSquared;
const i =
(this.imaginary * other.real - this.real * other.imaginary) /
otherMagnitudeSquared;

return new Complex(r, i);
}

Complete Code

Application Class

   export class Complex {
constructor(real, imaginary) {
this.real = real;
this.imaginary = imaginary;
}

/***
* Generate a new <code>Complex(0,0)</code>
* @returns {Complex}
*/
static zero = () => new Complex(0, 0);

add(other) {
return new Complex(
this.real + other.real,
this.imaginary + other.imaginary
);
}

subtract(other) {
return new Complex(
this.real - other.real,
this.imaginary - other.imaginary
);
}

/***
* Multiple this Complex with another.<br/>
* <code>(a + bi)(c + di) = (ac - bd) + (ad + bc)i</code>
* @param other
* @returns {Complex}
*/
multiply(other) {
return new Complex(
this.real * other.real - this.imaginary * other.imaginary,
this.real * other.imaginary + this.imaginary * other.real
);
}

/***
* <code>(a + bi) / (c + di) = [(ac + bd) / (c^2 + d^2)] + [(bc - ad) / (c^2 + d^2)]i<code>
* @param other
*/
divide(other) {
const otherMagnitudeSquared =
other.real * other.real + other.imaginary * other.imaginary;
const r =
(this.real * other.real + this.imaginary * other.imaginary) /
otherMagnitudeSquared;
const i =
(this.imaginary * other.real - this.real * other.imaginary) /
otherMagnitudeSquared;

return new Complex(r, i);
}

magnitude() {
return Math.sqrt(
this.real * this.real + this.imaginary * this.imaginary
);
}

/***
* Returns a string in the form <code>a ± bi</code>.
* @returns {string}
*/
toString() {
const operator = this.imaginary < 0 ? '-' : '+';
return `${this.real} ${operator} ${Math.abs(this.imaginary)}i`;
}

/***
* Both <i>real</i> and <i>imaginary</i> parts are equal.
* @param other
* @returns {boolean}
*/
equals(other) {
return this.real === other.real && this.imaginary === other.imaginary;
}
}

Test Cases

Vitest is used here.

import { describe, it, expect } from 'vitest';
import { Complex } from './Complex.js';

describe('Calling zero()', () => {
const zero = Complex.zero();
it('should return 0 real part', () => {
expect(zero.real).toBe(0);
});

it('should return 0 imaginary part', () => {
expect(zero.imaginary).toBe(0);
});
});

describe('Creating a new number', () => {
const expectedReal = Math.random();
const expectedImaginary = Math.random();

const actual = new Complex(expectedReal, expectedImaginary);

it(`should return the expected real part (${expectedReal})`, () => {
expect(actual.real).toBe(expectedReal);
});

it(`should return the expected imaginary part (${expectedImaginary})`, () => {
expect(actual.imaginary).toBe(expectedImaginary);
});
});

it('Should correctly calculate the magnitude', () => {
const dummyReal = Math.random();
const dummyImaginary = Math.random();
const expected = Math.sqrt(
dummyReal * dummyReal + dummyImaginary * dummyImaginary
);

const actual = new Complex(dummyReal, dummyImaginary).magnitude();

console.dir({ dummyReal, dummyImaginary, actual, expected });

expect(actual).toBe(expected);
});

describe('Equality', () => {
it('should return true when equal', () => {
const complex1 = new Complex(Math.random(), Math.random());
const complex2 = new Complex(complex1.real, complex1.imaginary);

const actual = complex1.equals(complex2);

expect(actual).toBe(true);
});

it('should return false when real part differs', () => {
const complex1 = new Complex(Math.random(), Math.random());
const complex2 = new Complex(complex1.real + 1, complex1.imaginary);

const actual = complex1.equals(complex2);

expect(actual).toBe(false);
});

it('should return true when equal', () => {
const complex1 = new Complex(Math.random(), Math.random());
const complex2 = new Complex(complex1.real, complex1.imaginary + 1);

const actual = complex1.equals(complex2);

expect(actual).toBe(false);
});
});

describe('Arithmetic', () => {
const complex1 = new Complex(6, 3);
const complex2 = new Complex(7, -5);

describe('Add', () => {
const expectedReal = complex1.real + complex2.real;
const expectedImaginary = complex1.imaginary + complex2.imaginary;

const actual = complex1.add(complex2);

it(`Real part should be ${expectedReal}`, () => {
expect(actual.real).toBe(expectedReal);
});

it(`Imaginary part should be ${expectedImaginary}`, () => {
expect(actual.imaginary).toBe(expectedImaginary);
});
});

describe('Subtract', () => {
const expectedReal = complex1.real - complex2.real;
const expectedImaginary = complex1.imaginary - complex2.imaginary;

const actual = complex1.subtract(complex2);

it(`Real part should be ${expectedReal}`, () => {
expect(actual.real).toBe(expectedReal);
});

it(`Imaginary part should be ${expectedImaginary}`, () => {
expect(actual.imaginary).toBe(expectedImaginary);
});
});

describe('Multiply', () => {
const expectedReal = complex1.real + complex2.real;
const expectedImaginary = complex1.imaginary + complex2.imaginary;

const actual = complex1.add(complex2);

it(`Real part should be ${expectedReal}`, () => {
expect(actual.real).toBe(expectedReal);
});

it(`Imaginary part should be ${expectedImaginary}`, () => {
expect(actual.imaginary).toBe(expectedImaginary);
});
});

describe('Divide', () => {
const expectedReal = 27 / 74;
const expectedImaginary = 51 / 74;

const actual = complex1.divide(complex2);
it(`should have correct real`, () => {
expect(actual.real).toBe(expectedReal);
});

it(`should have correct imaginary`, () => {
expect(actual.imaginary).toBe(expectedImaginary);
});
});
});

describe('toString()', () => {
it('for positive imaginary part', () => {
const dummyReal = 1;
const dummyImaginary = 1;

const expected = `${dummyReal} + ${dummyImaginary}i`;

const actual = new Complex(dummyReal, dummyImaginary).toString();

expect(actual).toBe(expected);
});

it('for zero imaginary part', () => {
const dummyReal = 1;
const dummyImaginary = 0;

const expected = `${dummyReal} + ${dummyImaginary}i`;

const actual = new Complex(dummyReal, dummyImaginary).toString();

expect(actual).toBe(expected);
});

it('for negative imaginary part', () => {
const dummyReal = 1;
const dummyImaginary = -1;

const expected = `${dummyReal} - ${Math.abs(dummyImaginary)}i`;

const actual = new Complex(dummyReal, dummyImaginary).toString();

expect(actual).toBe(expected);
});
});

Conclusion

I have to admit, it was quite fun rediscovering those derivations, I haven’t done vector maths for a long time.

This wasn’t exactly an exciting article, it’s very dry but the functions are important if you want to play with Mandelbrot or Julia sets or anything that needs complex numbers.

Thanks for reading.

--

--