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) andim(imag). - I avoid naming loop counters
ianywhere near this code. I useidx.
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 + bshould returnComplex, 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
Complextype and keep it boring and predictable.
Here is how I think about “traditional vs modern” practice in 2026:
Traditional approach
—
Hand-roll a class early
complex, add small helpers for formatting/testing Ad-hoc {re, im} objects
Complex class with immutable operations and tests Trust code review alone
== 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, noti. - The printed form includes parentheses in many contexts.
.realand.imagare 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)andtoPolar()(even if only occasionally used)scale(k)(multiply by a real scalar)abs()ormagnitude()(for error metrics)equalsApprox(other, tol)normalizeZero(eps)(to kill-0and 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
iin code that also deals with complex numbers. Useidx.
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)isfalse
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 <- yandim <- x
My defense is boring but effective:
- Name fields
reandim(notxandy) in complex-specific code. - Provide conversion helpers
fromPoint({x, y})andtoPoint()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 \(
\). 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:
<= tol and
<= 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-9to1e-12can 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
iinstead ofj - avoid
3 + -2i - round nicely
- parse user input
Formatting rules I use
When I format a + bi, I handle these cases:
im = 0→ show justa(optional) ora + 0i(debug style)re = 0→ show justbi(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:
nis 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 - prevandprev - 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
imis wrong, it’s almost always: swapped coords, sign, or parsing.
4) Search for a convention mismatch.
- Is error defined as
target - actualoractual - 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
complexis 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
complexis 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
Complexobject 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[]andim[]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.


