Adding and Subtracting Complex Numbers (for Working Developers)

A few years into writing production code, you start noticing the same math patterns popping up in wildly different places: audio filters, 2D rotations, impedance in circuits, FFT pipelines, control systems, even simple “rotate this point around that point” UI work. Complex numbers are one of those patterns. The nice part is that you do not need advanced math to get value out of them day-to-day: addition and subtraction alone cover a surprising amount of practical work (mixing signals, combining offsets, accumulating error terms, composing phasors before you multiply/rotate, etc.).

When I teach this topic internally, I frame complex addition/subtraction as “vector math with an extra rule for multiplication.” For this post, we stay focused on the easy, high-impact part: adding and subtracting complex numbers reliably, reading and writing them in code, and avoiding the small sign mistakes that waste the most debugging time.

You will see the algebra, the geometry, and a few runnable implementations (Python and JavaScript/TypeScript style). If you already know the formulas, pay special attention to the sections on edge cases, equality, and formatting: that is where real-world bugs hide.

Complex Numbers as Two Numbers Glued Together

A complex number is typically written as:

  • \(z = a + i b\)

where:

  • \(a\) is the real part
  • \(b\) is the imaginary part
  • \(i\) is the imaginary unit, defined by \(i^2 = -1\)

A quick mental model that I find sticks:

  • Treat \(a\) and \(b\) like an ordered pair \((a, b)\).
  • The real axis is the x-axis.
  • The imaginary axis is the y-axis.

So \(3 + 2i\) is the point (or vector) \((3, 2)\) in a 2D plane.

Two details that matter in code:

1) Every real number is already a complex number with imaginary part 0.

  • \(7\) is the same as \(7 + 0i\).

2) The symbol \(i\) is not a variable.

  • It is a special constant with \(i^2 = -1\).
  • In electrical engineering you often see \(j\) instead of \(i\) to avoid confusion with current.

If you keep the “pair of numbers” view in your head, addition and subtraction become almost automatic.

A tiny but useful naming convention

When I’m writing code, I avoid a/b once I leave the whiteboard:

  • I name the components re (real) and im (imag).
  • I avoid naming loop counters i anywhere near this code. I use idx.

It’s not about style points. It’s about preventing the kind of “I stared at it for 20 minutes and it was one letter” bug.

Addition: Add Real-to-Real, Imaginary-to-Imaginary

Let:

  • \(z_1 = a + ib\)
  • \(z_2 = c + id\)

Add them by combining like parts:

\[

z1 + z2 = (a + ib) + (c + id) = (a + c) + i(b + d)

\]

That is the whole rule. No special trick. No cross terms.

Geometric picture (the one I actually use)

If you see complex numbers as vectors, addition is just vector addition:

  • Add x-coordinates: \(a + c\)
  • Add y-coordinates: \(b + d\)

So adding complex numbers is “stacking displacements.” If \(z1\) is “move 3 right and 2 up” and \(z2\) is “move 1 left and 5 up,” then \(z1 + z2\) is the combined move.

Worked example

Compute \((3 + 5i) + (6 – 2i)\):

  • Real parts: \(3 + 6 = 9\)
  • Imaginary parts: \(5 + (-2) = 3\)

Result:

  • \(9 + 3i\)

This is also a nice place to call out a frequent beginner slip: people see “\(-2i\)” and forget it is “\(-2\) times \(i\).” The coefficient is \(-2\), so you add \(5 + (-2)\), not \(5 – 2\) by intuition alone.

Addition as “accumulation” (why it shows up everywhere)

In real code, addition rarely appears as a single z1 + z2. It shows up as:

  • Summing many terms (reductions)
  • Integrating a signal (running total)
  • Adding offsets (position updates)
  • Combining small corrections (feedback control)

Because complex addition is component-wise, you can reason about stability and correctness by reasoning about two independent channels: the real channel and the imaginary channel.

That mental split is especially useful when you’re debugging: if the real part looks right but the imaginary part is off by a sign, you know you’re looking for a sign flip or swapped coordinate somewhere, not a “math is hard” problem.

Subtraction: Same Idea, Just Subtract the Parts

Let:

  • \(z_1 = a + ib\)
  • \(z_2 = c + id\)

Subtract component-wise:

\[

z1 – z2 = (a + ib) – (c + id) = (a – c) + i(b – d)

\]

Again: no cross terms.

Geometric picture

If addition is “stacking displacements,” subtraction is “what displacement takes me from \(z2\) to \(z1\)?”

  • \(z1 – z2\) points from \(z2\) to \(z1\).

This is why subtraction appears constantly in code that computes:

  • error vectors (actual – expected)
  • deltas between states
  • distance computations (magnitude of a difference)

Worked example

Compute \((2 – 3i) – (-4 + 2i)\):

  • Real: \(2 – (-4) = 6\)
  • Imag: \((-3) – 2 = -5\)

Result:

  • \(6 – 5i\)

If you want a quick consistency check, rewrite the subtraction as adding the negation:

  • \(z1 – z2 = z1 + (-z2)\)

Negating \((-4 + 2i)\) gives \((4 – 2i)\). Then:

  • \((2 – 3i) + (4 – 2i) = 6 – 5i\)

Same answer, and it often prevents sign mistakes when you are tired.

Subtraction as “change” (what I look for in domain code)

When I see complex subtraction in a codebase, it’s usually one of these:

  • “What changed?” \(\Delta z = z{now} – z{before}\)
  • “How wrong are we?” \(e = z{target} – z{actual}\) (or the reverse)
  • “How far apart?” \( z1 – z2

    \) (magnitude)

The algebra is the same in each case, but the sign convention is not. Teams will disagree on whether “error” is actual - target or target - actual. Decide once, write it down, and test it.

Properties You Can Rely On (and How They Show Up in Code)

These properties are not just textbook facts; they shape how you write clean, testable implementations.

Closure

Add or subtract two complex numbers and you still have a complex number.

  • If your type is Complex, a + b should return Complex, not a tuple or ad-hoc object.

Associativity (addition only)

\[

(z1 + z2) + z3 = z1 + (z2 + z3)

\]

This is why you can sum a list of complex numbers in any grouping without changing the result (ignoring floating-point rounding details).

Commutativity (addition only)

\[

z1 + z2 = z2 + z1

\]

This matters when you refactor code and reorder terms, or when you parallelize reductions.

Additive identity

\(0\) is the identity:

  • \(z + 0 = z\)
  • In complex form, \(0 = 0 + 0i\)

In code, this is your “zero value.” I often define it as a constant:

  • Complex.ZERO = new Complex(0, 0)

Additive inverse

Every \(z\) has an inverse \(-z\):

  • \(z + (-z) = 0\)

In components:

  • If \(z = a + ib\), then \(-z = -a – ib\)

That becomes a method like neg() or neg.

A developer’s note on floating-point reality

All of the properties above are exact in mathematics. In floating-point code:

  • Commutativity and associativity can appear to “fail” by tiny amounts because \((a + b) + c\) and \(a + (b + c)\) round differently.

That does not mean your complex arithmetic is wrong. It means you need:

  • tolerance-based comparisons
  • tests that account for rounding
  • stable accumulation strategies when it matters

I cover practical patterns for that later.

Adding/Subtracting in Polar Form: My Rule of Thumb

You will sometimes see complex numbers written in polar form:

  • \(z = r(\cos \theta + i\sin \theta)\)
  • Often abbreviated as \(z = r\angle\theta\)

Polar form is great for multiplication and division (magnitudes multiply, angles add). But for addition/subtraction, Cartesian is the better work surface.

My rule of thumb:

  • If your task is add/subtract, convert to Cartesian first.
  • If your task is multiply/divide/raise to powers, polar often reads cleaner.

Why addition in polar is awkward (a quick intuition)

In polar form, two numbers might have:

  • similar magnitudes but opposite directions
  • very different magnitudes but similar directions

Adding them depends on both the magnitudes and the angle difference in a non-trivial way. In Cartesian, it’s just two additions.

Quick conversion reminders

From polar to Cartesian:

  • \(a = r\cos\theta\)
  • \(b = r\sin\theta\)
  • so \(z = a + ib\)

From Cartesian to polar:

  • \(r = \sqrt{a^2 + b^2}\)
  • \(\theta = \text{atan2}(b, a)\)

In code, atan2(imag, real) is the one you want, because it keeps the quadrant correct.

A small production tip: store what you use

If you mostly add and subtract, store complex numbers as (re, im).

If you mostly multiply/rotate, you might store (r, theta) or store precomputed cos(theta)/sin(theta) alongside r.

But don’t mix storage formats casually: most bugs I’ve debugged in this area were actually format-mismatch bugs, not arithmetic bugs.

Implementation Patterns (Python, JavaScript/TypeScript)

You can implement complex arithmetic in two common ways:

  • Use the language runtime’s built-in complex type (when available).
  • Create a small Complex type and keep it boring and predictable.

Here is how I think about “traditional vs modern” practice in 2026:

Task

Traditional approach

Modern approach I recommend —

— Simple numeric work in Python

Hand-roll a class early

Use built-in complex, add small helpers for formatting/testing Frontend or Node.js math

Ad-hoc {re, im} objects

A tiny Complex class with immutable operations and tests Safety in large codebases

Trust code review alone

Add property-based tests + type checking + formatting rules Equality checks

== everywhere

almostEqual with tolerance for floats

Python: built-in complex (fastest path)

Python already supports complex numbers using j in literals (because i could be a variable name):

# complexaddsub.py

def demo():

z1 = 3 + 5j

z2 = 6 – 2j

s = z1 + z2

d = z1 – z2

print("sum:", s) # (9+3j)

print("diff:", d) # (-3+7j)

# Access parts

print("real:", s.real)

print("imag:", s.imag)

if name == "main":

demo()

A few practical notes:

  • Python prints j, not i.
  • The printed form includes parentheses in many contexts.
  • .real and .imag are floats.

Python: a small wrapper when you want explicit control

If you need consistent formatting (a + bi), custom parsing, or strict typing, a lightweight wrapper helps.

# complex_number.py

from future import annotations

from dataclasses import dataclass

@dataclass(frozen=True)

class ComplexNumber:

re: float

im: float

def add(self, other: "ComplexNumber") -> "ComplexNumber":

return ComplexNumber(self.re + other.re, self.im + other.im)

def sub(self, other: "ComplexNumber") -> "ComplexNumber":

return ComplexNumber(self.re – other.re, self.im – other.im)

def neg(self) -> "ComplexNumber":

return ComplexNumber(-self.re, -self.im)

def str(self) -> str:

# Format as a + bi with a clean sign.

sign = "+" if self.im >= 0 else "-"

return f"{self.re} {sign} {abs(self.im)}i"

def demo():

z1 = ComplexNumber(3, 5)

z2 = ComplexNumber(6, -2)

print("z1:", z1)

print("z2:", z2)

print("z1 + z2:", z1 + z2)

print("z1 – z2:", z1 – z2)

if name == "main":

demo()

I used frozen=True so your instances are immutable. That makes reasoning about math code much easier.

JavaScript: implement a minimal, reliable Complex class

JavaScript does not have a built-in complex numeric type, so you either pull a library or define a small one. For many apps (signal processing demos, geometry, UI transforms, educational tools) a tiny class is enough.

// complex.js

export class Complex {

constructor(re, im) {

this.re = re;

this.im = im;

Object.freeze(this); // Keep instances immutable

}

add(other) {

return new Complex(this.re + other.re, this.im + other.im);

}

sub(other) {

return new Complex(this.re – other.re, this.im – other.im);

}

neg() {

return new Complex(-this.re, -this.im);

}

toString() {

const sign = this.im >= 0 ? "+" : "-";

return ${this.re} ${sign} ${Math.abs(this.im)}i;

}

}

// Runnable demo

const z1 = new Complex(3, 5);

const z2 = new Complex(6, -2);

console.log("z1:", z1.toString());

console.log("z2:", z2.toString());

console.log("z1 + z2:", z1.add(z2).toString());

console.log("z1 – z2:", z1.sub(z2).toString());

If you are in TypeScript, add types and (optionally) a readonly design:

// complex.ts

export class Complex {

public readonly re: number;

public readonly im: number;

constructor(re: number, im: number) {

this.re = re;

this.im = im;

Object.freeze(this);

}

add(other: Complex): Complex {

return new Complex(this.re + other.re, this.im + other.im);

}

sub(other: Complex): Complex {

return new Complex(this.re – other.re, this.im – other.im);

}

toString(): string {

const sign = this.im >= 0 ? "+" : "-";

return ${this.re} ${sign} ${Math.abs(this.im)}i;

}

}

A more “production-ish” JS/TS shape (helpers you end up writing anyway)

In real apps I nearly always add:

  • fromPolar(r, theta) and toPolar() (even if only occasionally used)
  • scale(k) (multiply by a real scalar)
  • abs() or magnitude() (for error metrics)
  • equalsApprox(other, tol)
  • normalizeZero(eps) (to kill -0 and tiny noise)

Even though this post is about add/sub, these helpers reduce the number of places where developers do ad-hoc math and introduce inconsistency.

Testing: don’t skip it because the math is “simple”

Most bugs here are sign mistakes and formatting mistakes. A handful of unit tests pays off fast.

Python example with pytest:

# testcomplexnumber.py

from complex_number import ComplexNumber

def test_addition():

assert ComplexNumber(3, 5) + ComplexNumber(6, -2) == ComplexNumber(9, 3)

def test_subtraction():

assert ComplexNumber(2, -3) – ComplexNumber(-4, 2) == ComplexNumber(6, -5)

def testadditiveinverse():

z = ComplexNumber(1.5, -2.0)

assert z + (-z) == ComplexNumber(0.0, 0.0)

In JavaScript/TypeScript, I usually add a small helper for approximate equality (more on that next), and then test add/sub plus identity/inverse properties.

Common Mistakes I See (and How You Avoid Them)

1) Dropping parentheses during subtraction

This is the classic:

  • \((a + ib) – (c + id)\)

If you expand without parentheses discipline, you will flip a sign incorrectly. I recommend doing it in two steps:

1) Rewrite subtraction as adding the negative:

  • \(z1 – z2 = z1 + (-z2)\)

2) Negate \(z_2\) cleanly:

  • If \(z2 = c + id\), then \(-z2 = -c – id\)

Now it becomes addition, which is hard to mess up.

2) Mixing the imaginary unit with the coefficient

If you have \(-4 + 2i\), the imaginary coefficient is \(+2\). If you have \(6 – 2i\), the imaginary coefficient is \(-2\).

When I debug, I explicitly write the coefficient as a plain number:

  • \(6 – 2i\) means \(b = -2\)

3) Treating \(i\) like a variable name in code

In Python, i can be a loop variable. That is why the complex literal uses j. In JS, i is just an identifier; there is no imaginary unit built in.

My advice:

  • In code, name your imaginary coefficient im.
  • Avoid naming random loop counters i in code that also deals with complex numbers. Use idx.

4) Comparing complex values with == after floating-point work

If your real/imag parts come from trig (sin, cos), FFT output, sensor data, or repeated additions, you will see rounding noise.

Use a tolerance check:

# almost_equal.py

def complexalmostequal(z1: complex, z2: complex, tol: float = 1e-9) -> bool:

return abs(z1.real – z2.real) <= tol and abs(z1.imag – z2.imag) <= tol

And a JS version:

export function almostEqual(a, b, tol = 1e-9) {

return Math.abs(a – b) <= tol;

}

export function complexAlmostEqual(z1, z2, tol = 1e-9) {

return almostEqual(z1.re, z2.re, tol) && almostEqual(z1.im, z2.im, tol);

}

If you need a distance-based tolerance (often nicer):

  • Check \(\sqrt{(\Delta a)^2 + (\Delta b)^2} \le \text{tol}\)

I tend to prefer distance-based checks when values can vary widely in scale, because it measures “how far apart are these points” in a single number.

5) Formatting: double signs and negative zero

You will eventually print something like:

  • 3 + -2i

That reads poorly. Use sign-aware formatting like the toString() methods earlier.

Also watch out for -0 in JavaScript:

  • Object.is(-0, 0) is false

If you see outputs like 2 - 0i, you can normalize small values:

  • if Math.abs(im) < 1e-15, treat it as 0.

6) Swapping real and imaginary parts (especially with coordinate systems)

This one bites UI and graphics code. Someone stores points as {x, y} and later treats them as {re, im} but accidentally maps:

  • re <- y and im <- x

My defense is boring but effective:

  • Name fields re and im (not x and y) in complex-specific code.
  • Provide conversion helpers fromPoint({x, y}) and toPoint() so the mapping is explicit.

7) Forgetting units and conventions (angles, degrees vs radians)

Even though we’re focusing on add/sub, teams often convert from polar or compute components via trig. If one place uses degrees and another uses radians:

  • your components are “valid numbers” but wrong
  • addition/subtraction happily propagates the mistake

If there is any trig anywhere near this pipeline, I standardize on radians and make degree conversion a dedicated function.

Solved Examples (with a Developer’s “sanity checks”)

Example 1: \((3 + 5i) + (6 – 2i)\)

Compute:

  • Real: \(3 + 6 = 9\)
  • Imag: \(5 + (-2) = 3\)

Answer:

  • \(9 + 3i\)

Sanity check:

  • The imaginary part should shrink (5 plus a negative).

Example 2: \((2 – 3i) – (-4 + 2i)\)

Compute:

  • Real: \(2 – (-4) = 6\)
  • Imag: \((-3) – 2 = -5\)

Answer:

  • \(6 – 5i\)

Sanity check:

  • Subtracting \(-4\) should increase the real part.
  • Subtracting \(+2i\) should push the imaginary part downward.

Example 3: \(z1 + z2 – z3\) with \(z1 = 3 + 2i\), \(z2 = 5 – 3i\), \(z3 = 1 + 2i\)

Compute stepwise:

1) \(z1 + z2 = (3 + 5) + i(2 + (-3)) = 8 – i\)

2) \((8 – i) – (1 + 2i) = (8 – 1) + i((-1) – 2) = 7 – 3i\)

Answer:

  • \(7 – 3i\)

Sanity check:

  • Imaginary: \(2 – 3 – 2 = -3\). I like doing this one “in my head” as a quick verification.

Example 4: Solve for \(v\) given \(z = 6 + 9i\) and \(z + v = 2(z – v)\)

This is a common pattern when you have “one combination is twice another combination.”

Start with the equation:

  • \(z + v = 2(z – v)\)

Expand the right side:

  • \(z + v = 2z – 2v\)

Collect \(v\) terms on the left, \(z\) terms on the right:

  • \(v + 2v = 2z – z\)
  • \(3v = z\)

So:

  • \(v = \frac{z}{3} = \frac{6 + 9i}{3} = 2 + 3i\)

Sanity check:

  • If \(v\) is one-third of \(z\), then \(z – v\) is two-thirds of \(z\), and doubling it gives four-thirds of \(z\). Also \(z + v\) is four-thirds of \(z\). Checks out.

Example 5: “Undo” an accumulated offset (a common UI pattern)

Suppose an object’s current offset (from some anchor) is \(z = 12 – 4i\), and you applied a nudge of \(n = -3 + 10i\). If you want to reverse that nudge, you subtract it:

  • new offset \(= z – n\)

Compute:

  • Real: \(12 – (-3) = 15\)
  • Imag: \(-4 – 10 = -14\)

So the new offset is \(15 – 14i\).

Sanity check:

  • subtracting a negative real nudge increases the real part
  • subtracting a positive imaginary nudge decreases the imaginary part

Where Addition/Subtraction Shows Up in Real Systems

Even if you never say “complex numbers” out loud at work, you might still be doing them.

1) 2D geometry as complex arithmetic

Represent a 2D point \((x, y)\) as a complex number:

  • \(p = x + iy\)

Then:

  • Translating a point by a displacement \(d = \Delta x + i\Delta y\) is just \(p + d\).
  • The vector from point \(p1\) to point \(p2\) is \(p2 – p1\).

This is not a gimmick. It simplifies code because you stop passing pairs of numbers around and start passing one value with well-defined operations.

A practical example: compute a polyline’s per-segment displacement vectors.

  • Given points \(p0, p1, p_2, \dots\)
  • Segment vectors are \(vk = p{k+1} – p_k\)

If you later want segment lengths, you take magnitudes \(

v_k

\). The subtraction step is the part that’s easy to get wrong in raw {x, y} code.

2) Signals and “I/Q” data (baseband processing)

In signal processing, you’ll often see complex samples:

  • real part = in-phase (I)
  • imaginary part = quadrature (Q)

Adding signals is literally complex addition:

  • mixing two streams: \(s = s1 + s2\)
  • subtracting a known interference estimate: \(clean = raw – estimate\)

Even if you never touch FFTs, any time you have two correlated channels that represent “one thing,” complex addition/subtraction is the natural data type.

3) AC circuit analysis (phasors)

Engineers represent sinusoidal voltages and currents as complex phasors. The big payoff happens with multiplication/division when you apply impedances, but addition/subtraction is still central:

  • Kirchhoff’s laws become sums of complex voltages/currents.
  • You compute residuals (error terms) as differences.

In code, this often looks like:

  • totalCurrent = i1.add(i2).add(i3)
  • error = measured.sub(predicted)

4) Control systems and estimation (errors and innovations)

If a state lives naturally in 2D (position + velocity components, or a complex plane representation of an angle-like quantity), then your control loop is full of:

  • errors: target - estimate
  • corrections: estimate + gain * error

Even when the math is more advanced, the day-to-day bug is usually a sign convention mistake in a subtraction.

5) Frequency-domain pipelines (sums of complex bins)

Frequency-domain code often does “sum across bins” or “difference between snapshots.” If you’ve ever:

  • averaged spectra over time
  • computed a noise floor estimate
  • subtracted a baseline spectrum from a measured spectrum

…you’ve done repeated complex additions/subtractions.

6) Computational geometry and robotics (relative pose in 2D)

In 2D robotics you constantly compute relative vectors:

  • relative = landmark - robotPosition

Representing the 2D position as a complex number makes that subtraction a single operator, not two lines that can drift apart.

Edge Cases That Actually Matter in Code

This is the section I wish more “intro” posts included, because this is where my team’s bugs have lived.

1) Negative zero (JavaScript) and “almost zero” (everyone)

JavaScript can produce -0 from operations like -1 * 0 or from rounding artifacts. That becomes annoying in formatting and in equality checks.

A simple normalization step makes outputs stable:

function normalizeZero(x, eps = 0) {

// eps = 0 means only normalize -0 to 0

if (Object.is(x, -0)) return 0;

if (eps > 0 && Math.abs(x) < eps) return 0;

return x;

}

Then in your Complex class:

normalized(eps = 0) {

return new Complex(normalizeZero(this.re, eps), normalizeZero(this.im, eps));

}

I keep this separate from core arithmetic. Arithmetic should be pure; normalization is presentation/cleanup.

2) NaN and Infinity

If re or im becomes NaN, it tends to spread quickly through additions/subtractions and you end up with “everything is NaN.”

My defensive approach:

  • In performance-critical numeric code, I let it propagate and I debug upstream.
  • In application code (UI, parsing, user input), I validate early and fail loudly.

A pragmatic validation helper (TS):

function assertFiniteComplex(z: Complex): void {

if (!Number.isFinite(z.re) || !Number.isFinite(z.im)) {

throw new Error(Non-finite complex: ${z.re} + ${z.im}i);

}

}

3) Integer vs float assumptions

Addition/subtraction is safe for integers, but real-world inputs are often floats. If you parse user input like "3.1 + 2i" and store as integers accidentally (or serialize/deserialize with rounding), subtraction-based “delta” code can look wrong.

If your app needs exact decimal behavior (finance-like data but in 2D), consider decimal libraries. For scientific/graphics work, floats are fine—just don’t pretend they are exact.

4) Overflow and underflow (rare, but real)

With very large magnitudes, re + other.re can overflow to Infinity. With very small magnitudes, you can underflow toward 0. Most teams never hit this, but if you’re summing millions of values or working with extreme scales, it can happen.

If you suspect it:

  • scale inputs (normalize units)
  • sum in chunks
  • track magnitude statistics while debugging

Equality: What “Same Complex Number” Means in Practice

Mathematically, equality is exact. In floating-point code, I choose from three meanings depending on the job:

1) Exact structural equality: re and im bits match.

  • Useful for integer-like data, snapshots, caching keys (careful), and deterministic tests.

2) Component-wise tolerance:

Δre

<= tol and

Δim

<= tol.

  • Simple and often good enough.

3) Distance tolerance: sqrt(Δre^2 + Δim^2) <= tol.

  • My default for numeric algorithms.

Here’s a small, practical distance-based helper in Python:

import math

def complex_close(z1: complex, z2: complex, tol: float = 1e-9) -> bool:

dx = z1.real – z2.real

dy = z1.imag – z2.imag

return math.hypot(dx, dy) <= tol

And a TS-style helper:

export function complexClose(a: Complex, b: Complex, tol = 1e-9): boolean {

const dx = a.re – b.re;

const dy = a.im – b.im;

return Math.hypot(dx, dy) <= tol;

}

Choosing a tolerance (a rule that saves time)

I don’t pick tolerances by vibe. I pick them based on scale:

  • If values are around 1.0, 1e-9 to 1e-12 can be fine.
  • If values are around 1e6, you might need a larger absolute tolerance.

A robust pattern is combining absolute and relative tolerance:

  • close if distance <= absTol + relTol * max( a

    ,

    b

    )

That’s beyond “intro,” but it’s the standard way to avoid false failures when magnitudes vary.

Formatting and Parsing (Because Your Users Will Ask)

If your code ever displays complex numbers, you’ll eventually be asked to:

  • show i instead of j
  • avoid 3 + -2i
  • round nicely
  • parse user input

Formatting rules I use

When I format a + bi, I handle these cases:

  • im = 0 → show just a (optional) or a + 0i (debug style)
  • re = 0 → show just bi (optional)
  • im = 1 → show + i (optional) or + 1i (explicit)
  • im = -1 → show - i

A simple JS formatter (explicit a ± bi always):

export function formatComplex(z, digits = null) {

const fmt = (x) => (digits === null ? String(x) : x.toFixed(digits));

const re = z.re;

const im = z.im;

const sign = im >= 0 ? "+" : "-";

return ${fmt(re)} ${sign} ${fmt(Math.abs(im))}i;

}

I keep it explicit because it’s predictable in logs. For UI display, I might hide + 0i.

Parsing: keep it narrow unless you really need full generality

Users will enter:

  • "3+2i"
  • "3 + 2i"
  • "-4-5i"
  • "2i"
  • "7"

I recommend supporting a small subset first, and rejecting the rest with a clear error message.

Here’s a practical (but intentionally limited) JS parser for a ± bi, a, or bi:

export function parseComplex(s) {

const t = s.trim().replace(/\s+/g, "");

// Handle pure imaginary like "2i" or "-2i" or "i" or "-i"

if (t.endsWith("i")) {

const body = t.slice(0, -1);

if (body === "" || body === "+") return { re: 0, im: 1 };

if (body === "-") return { re: 0, im: -1 };

// Try a±bi by finding a + or – that is not the first char

const splitIdx = Math.max(body.lastIndexOf("+"), body.lastIndexOf("-", 1));

// If splitIdx <= 0, it's pure imaginary like "2"

if (splitIdx <= 0) {

const im = Number(body);

if (!Number.isFinite(im)) throw new Error(Invalid complex: ${s});

return { re: 0, im };

}

const reStr = body.slice(0, splitIdx);

const imStr = body.slice(splitIdx);

const re = Number(reStr);

const im = Number(imStr);

if (!Number.isFinite(re) || !Number.isFinite(im)) throw new Error(Invalid complex: ${s});

return { re, im };

}

// Pure real

const re = Number(t);

if (!Number.isFinite(re)) throw new Error(Invalid complex: ${s});

return { re, im: 0 };

}

Is this the only parser you could write? No. Is it a good starting point for production UI input? Yes, because it’s constrained and testable.

Practical Scenarios: Patterns I Reuse Constantly

This section is intentionally “developer flavored”: fewer formulas, more use-cases.

Summing Many Complex Numbers (and Not Losing Accuracy)

The straightforward way

If you need the sum of a list z[0..n-1]:

  • sum re parts
  • sum im parts

In Python with built-in complex:

total = 0 + 0j

for z in samples:

total += z

In JS with {re, im}:

let re = 0;

let im = 0;

for (const z of samples) {

re += z.re;

im += z.im;

}

const total = new Complex(re, im);

When accuracy matters: compensated summation (idea, not ceremony)

If you sum huge lists of floats, rounding error can accumulate. A simple improvement is to use compensated summation (Kahan-style) separately on re and im.

A minimal JS version for re/im:

function kahanSum(values) {

let sum = 0;

let c = 0;

for (const x of values) {

const y = x – c;

const t = sum + y;

c = (t – sum) – y;

sum = t;

}

return sum;

}

Then:

  • re = kahanSum(zs.map(z => z.re))
  • im = kahanSum(zs.map(z => z.im))

I only reach for this when:

  • n is large
  • cancellations are common (adding positive and negative values)
  • results feed into sensitive downstream logic

Otherwise, simple summation plus tolerance-based comparisons is enough.

Differences Over Time (Deltas) and Drift

If position[t] is complex-valued, deltas are:

  • delta = position[t] - position[t-1]

Two common mistakes:

  • off-by-one indexing
  • reversed subtraction (computing prev - now)

My debugging trick is to print a single step with both interpretations:

  • now - prev and prev - now

One will match your intuition about direction.

Centering Data (subtract the mean)

Centering is a classic preprocessing step:

  • compute mean \(\mu\)
  • subtract \(\mu\) from every sample

If samples are complex, this is still just addition/subtraction:

  • \(\mu = \frac{1}{n}\sum z_k\)
  • centered sample: \(z_k – \mu\)

This shows up in clustering, shape alignment, signal normalization, and “remove DC offset” tasks.

Debugging Checklist (My “3-minute” routine)

When I suspect add/sub is wrong, I do these checks:

1) Check one worked example by hand (with small integers).

  • If the code fails on integers, it’s not floating-point error. It’s logic.

2) Rewrite subtraction as addition of a negation.

  • If that fixes it, your bug is parentheses or sign handling.

3) Log re/im separately.

  • If only im is wrong, it’s almost always: swapped coords, sign, or parsing.

4) Search for a convention mismatch.

  • Is error defined as target - actual or actual - target?
  • Are angles degrees or radians upstream?

5) Normalize -0 (JS) and tiny noise.

  • It can disguise sign issues in logs.

This routine is boring, but it’s fast.

Property-Based Tests (The Highest ROI Tests for This Topic)

Unit tests with a few examples are good. Property-based tests are better because they generate lots of random cases and force you to respect the algebra.

Even without a library, you can write “pseudo property” tests:

  • addition is commutative: a + b == b + a
  • subtraction relates to addition: a - b == a + (-b)
  • additive identity: a + 0 == a
  • inverse: a + (-a) == 0

In floating-point code, change == to approximate equality.

A simple Python-style approach (without special libraries) is to run a loop with random floats and assert approximate properties.

The key benefit is psychological: once your arithmetic is locked down by properties, you stop second-guessing it and you debug the real bug faster.

Performance Considerations (Ranges, Not Mythology)

Addition/subtraction of two floats is cheap. The performance question is usually about:

  • how many objects you allocate
  • whether you’re in a tight loop
  • whether you can vectorize

Python

  • Built-in complex is implemented in C and is generally efficient.
  • If you’re doing large numeric arrays, libraries like NumPy represent complex arrays efficiently and vectorize operations.

A high-level rule I use:

  • if it’s scalar math: built-in complex is great
  • if it’s large arrays: use array-based operations (vectorized)

JavaScript/TypeScript

In JS, object allocation can dominate. If you’re adding millions of samples:

  • creating a new Complex object per operation can be slow

Two practical approaches:

1) Keep immutability for correctness in app-level code.

2) Use a “struct-of-arrays” representation for hot loops:

  • store re[] and im[] as separate typed arrays
  • add them in a loop without allocating objects

Example shape:

// re and im are Float64Array

for (let idx = 0; idx < n; idx++) {

re[idx] = re1[idx] + re2[idx];

im[idx] = im1[idx] + im2[idx];

}

That’s still the same math. You’re just choosing a data layout that’s friendly to performance.

When NOT to Use Complex Numbers

This is important: complex numbers are a tool, not a virtue.

I avoid them when:

  • the two components have different units (e.g., meters and seconds)
  • the two components need different transforms (one is linear, one is angular)
  • readability suffers for the team (and there is no payoff)

If your data is just “two unrelated numbers,” keep it as a tuple or object with meaningful names.

Complex numbers shine when:

  • the pair is conceptually one thing (a 2D vector, a phasor, an I/Q sample)
  • you want to use the consistent arithmetic rules

A Quick Recap (What I Want You to Remember)

  • A complex number is just (re, im) for add/sub purposes.
  • Addition: \((a + ib) + (c + id) = (a + c) + i(b + d)\).
  • Subtraction: \((a + ib) – (c + id) = (a – c) + i(b – d)\).
  • Debug subtraction by rewriting it as addition of a negation.
  • In real code, the biggest issues are signs, conventions, formatting, and float comparisons.

If you make those boring details reliable, complex arithmetic becomes one of the cleanest, most pleasant “math tools” you can use in everyday engineering.

Scroll to Top