Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/test-coverage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Test Coverage

on:
push:
branches:
- master
workflow_dispatch:

env:
HUSKY: 0

jobs:
coverage:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- uses: pnpm/action-setup@v4

- id: pnpm-store
run: echo value=$(pnpm store path) >> $GITHUB_OUTPUT

- id: pnpm-cache
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-store.outputs.value }}
key: pnpm-store-${{ runner.os }}-${{ hashFiles('./pnpm-lock.yaml') }}
restore-keys: |
pnpm-store-${{ runner.os }}-

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Run tests with coverage
run: pnpm test:coverage

- name: Upload coverage reports
uses: actions/upload-artifact@v4
with:
name: coverage-reports
path: coverage
retention-days: 30
11 changes: 9 additions & 2 deletions .github/workflows/test-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ on:
env:
HUSKY: 0

permissions:
contents: read
pull-requests: write

jobs:
test:
runs-on: ubuntu-latest
Expand All @@ -29,5 +33,8 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Run tests
run: pnpm test
- name: Run tests with coverage
run: pnpm test:coverage

- name: Report Coverage
uses: davelosert/vitest-coverage-report-action@v2
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
[![NPM](https://img.shields.io/npm/v/@iamsquare/complex.js.svg?style=flat-square)](https://www.npmjs.com/package/@iamsquare/complex.js)
[![GitHub issues](https://img.shields.io/github/issues-raw/iamsquare/complex.js.svg?style=flat-square)](https://github.com/iamsquare/complex.js/issues)
[![GitHub License](https://img.shields.io/github/license/mashape/apistatus.svg?style=flat-square)](https://github.com/iamsquare/complex.js/blob/master/LICENSE)
[![Coverage](https://img.shields.io/github/actions/workflow/status/iamsquare/complex.js/test-coverage.yml?label=coverage&style=flat-square)](https://github.com/iamsquare/complex.js/actions/workflows/test-coverage.yml)
[![NPM](https://nodei.co/npm/@iamsquare/complex.js.png?mini=true)](https://nodei.co/npm/@iamsquare/complex.js)

> A powerful, type-safe complex numbers library for JavaScript and TypeScript. Works seamlessly in browsers and Node.js.
Expand All @@ -17,6 +18,7 @@
- **Trigonometric Functions** - sin, cos, tan, sec, csc, cot and their inverses
- **Hyperbolic Functions** - sinh, cosh, tanh, sech, csch, coth and their inverses
- **Mathematical Functions** - Exponentiation, logarithms, powers, square roots
- **Numerically Stable** - Uses robust floating-point comparisons (combining absolute and relative error) and stable algorithms to handle precision errors
- **TypeScript Support** - Full type definitions included, no `@types` package needed
- **Universal** - Works in both browsers and Node.js
- **Zero Dependencies** - Lightweight and fast
Expand Down
4 changes: 3 additions & 1 deletion docusaurus/docs/intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ sidebar_position: 1
- **Trigonometric Functions** - sin, cos, tan, sec, csc, cot and their inverses
- **Hyperbolic Functions** - sinh, cosh, tanh, sech, csch, coth and their inverses
- **Mathematical Functions** - Exponentiation, logarithms, powers, square roots
- **Numerically Stable** - Uses robust floating-point comparisons (combining absolute and relative error) and stable algorithms to handle precision errors
- **TypeScript Support** - Full type definitions included, no `@types` package needed
- **Universal** - Works in both browsers and Node.js
- **Zero Dependencies** - Lightweight and fast
Expand Down Expand Up @@ -154,7 +155,7 @@ const conj = conjugate(z); // 3 - 4i
// Unit vector
const unitVec = unit(z); // 0.6 + 0.8i

// Equality checks
// Equality checks (uses epsilon-based comparison for floating-point precision)
const isEqual = equals(z, new Complex(3, 4)); // true
const isRealNum = isReal(z); // false
const isZeroNum = isZero(z); // false
Expand Down Expand Up @@ -193,6 +194,7 @@ Complex.EPSILON; // Machine epsilon
- [`unit(z)`](/operations/utility#unit) - Unit vector
- [`equals(z, w)`](/operations/utility#equals) - Equality check
- [`notEquals(z, w)`](/operations/utility#notequals) - Inequality check
- [`isApproximatelyEqual(a, b, epsilon?)`](/operations/utility#isapproximatelyequal) - Compare floating-point numbers

### Type Checking

Expand Down
5 changes: 4 additions & 1 deletion docusaurus/docs/operations/arithmetic.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,10 @@ console.log(realProduct.toString()); // => "5 + 10i"
Divides two complex numbers or a complex number by a real number: $\frac{z}{w}$.

Uses a [modified Smith's Method](http://forge.scilab.org/index.php/p/compdiv/source/tree/21/doc/improved_cdiv.pdf)
to avoid numerical overflow and underflow issues in complex division.
to avoid numerical overflow and underflow issues in complex division. The implementation uses
numerically stable addition and subtraction algorithms, along with scaling techniques to handle
extreme values, to further improve precision and robustness.

Also accepts real numbers, which are treated as complex numbers with zero imaginary part.

```typescript
Expand Down
20 changes: 14 additions & 6 deletions docusaurus/docs/operations/type-checking.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@ sidebar_position: 3

Type checking operations help identify special properties of complex numbers.

All comparison operations use epsilon-based floating-point comparison that combines absolute and relative error to handle precision errors robustly.

## isReal

Checks if a complex number is a real number (has zero imaginary part).

A complex number is real if its imaginary part is approximately zero (within epsilon tolerance),
meaning it lies on the real axis.

```typescript
isReal(z: Complex): boolean
```
Expand All @@ -20,7 +25,7 @@ isReal(z: Complex): boolean

### Returns

`true` if the number is real (imaginary part is zero), `false` otherwise.
`true` if the number is real (imaginary part is approximately zero), `false` otherwise.

### Example

Expand All @@ -39,8 +44,8 @@ console.log(isReal(w)); // => false

Checks if a complex number is purely imaginary (has zero real part and non-zero imaginary part).

A complex number is purely imaginary if its real part is exactly zero and its imaginary part is non-zero.
Note that zero (0 + 0i) is not considered purely imaginary.
A complex number is purely imaginary if its real part is approximately zero (within epsilon tolerance)
and its imaginary part is not approximately zero. Note that zero (0 + 0i) is not considered purely imaginary.

```typescript
isPureImaginary(z: Complex): boolean
Expand All @@ -52,7 +57,7 @@ isPureImaginary(z: Complex): boolean

### Returns

`true` if z is purely imaginary (real part is zero and imaginary part is non-zero), `false` otherwise.
`true` if z is purely imaginary (real part is approximately zero and imaginary part is not approximately zero), `false` otherwise.

### Example

Expand All @@ -73,7 +78,10 @@ console.log(isPureImaginary(z3)); // => false (zero is not purely imaginary)

## isZero

Checks if a complex number is zero.
Checks if a complex number is zero: $z = 0$.

A complex number is zero if both its real and imaginary parts are approximately zero
(within epsilon tolerance).

```typescript
isZero(z: Complex): boolean
Expand All @@ -85,7 +93,7 @@ isZero(z: Complex): boolean

### Returns

`true` if the number is zero (both real and imaginary parts are zero), `false` otherwise.
`true` if the number is zero (both real and imaginary parts are approximately zero), `false` otherwise.

### Example

Expand Down
64 changes: 61 additions & 3 deletions docusaurus/docs/operations/utility.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,16 @@ console.log(argument(u)); // => same as argument(z)

Checks if two complex numbers are equal: $z = w$.

Uses floating-point comparison with epsilon tolerance (Complex.EPSILON) to account
for floating-point precision errors. Two numbers are considered equal if both their
real and imaginary parts differ by at most Complex.EPSILON.
Uses a robust floating-point comparison that combines absolute and relative error
to account for floating-point precision errors. For values near zero, uses absolute error:
$|a - b| < \epsilon$. For values away from zero, uses relative error:
$|a - b| < \epsilon \cdot \max(|a|, |b|)$. Two numbers are considered equal if both their
real and imaginary parts are approximately equal using this method.

**Special cases:**

- Two infinite numbers are considered equal.
- NaN is never equal to anything, including itself.

```typescript
equals(z: Complex, w: Complex): boolean
Expand All @@ -165,6 +172,10 @@ console.log(equals(z1, z2)); // => true

const z3 = new Complex(1.0000001, 2);
console.log(equals(z1, z3)); // => true (within epsilon)

const z4 = Complex.INFINITY;
const z5 = Complex.INFINITY;
console.log(equals(z4, z5)); // => true
```

---
Expand Down Expand Up @@ -200,3 +211,50 @@ console.log(notEquals(z1, z2)); // => true
const z3 = new Complex(1, 2);
console.log(notEquals(z1, z3)); // => false
```

---

## isApproximatelyEqual

Checks if two floating-point numbers are approximately equal using a combination
of absolute and relative error. This is more robust than simple epsilon comparison.

This is the underlying function used by [`equals`](#equals) and other comparison operations.
For values near zero, uses absolute error: $|a - b| < \epsilon$.
For values away from zero, uses relative error: $|a - b| < \epsilon \cdot \max(|a|, |b|)$.

```typescript
isApproximatelyEqual(a: number, b: number, epsilon?: number): boolean
```

### Parameters

- `a` - First number
- `b` - Second number
- `epsilon` - Maximum allowed error (defaults to `Complex.EPSILON`)

### Returns

`true` if the numbers are approximately equal (within epsilon tolerance), `false` otherwise.

### Special Cases

- Returns `false` if either number is NaN
- Returns `true` if both numbers are the same infinite value (positive or negative)
- Returns `false` if one is infinite and the other is not

### Example

```typescript
import { isApproximatelyEqual, Complex } from '@iamsquare/complex.js';

// Handle floating-point precision errors
console.log(isApproximatelyEqual(0.1 + 0.2, 0.3)); // => true

// Custom epsilon tolerance
console.log(isApproximatelyEqual(1, 1.0001, 0.001)); // => true
console.log(isApproximatelyEqual(1, 1.0001, 0.00001)); // => false

// Different numbers
console.log(isApproximatelyEqual(1, 2)); // => false
```
5 changes: 1 addition & 4 deletions src/complex.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
// TODO: test and fix eventual precision errors
// TODO: add rounding function

import { type Cartesian, isCartesian, isPolar, type Polar } from '~/helpers';
import { isPureImaginary } from '~/operations';
import { argument } from '~/operations/argument';
Expand Down Expand Up @@ -190,7 +187,7 @@ export class Complex {

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

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

/**
Expand Down
60 changes: 48 additions & 12 deletions src/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Complex } from '~/complex';

const isNumber = (x: unknown): x is number => typeof x === 'number';
const isNullish = (x: unknown): x is null | undefined => x === undefined || x === null;
const isObject = (x: unknown): x is Record<string, unknown> => typeof x === 'object' && x !== null;
Expand Down Expand Up @@ -82,13 +84,13 @@ export function addStable(x: number, y: number): number {
const xAbs = Math.abs(x);
const yAbs = Math.abs(y);

// If magnitudes are similar, add directly
if (xAbs === 0) return y;
if (yAbs === 0) return x;
const xIsZero = isApproximatelyEqual(xAbs, 0);
const yIsZero = isApproximatelyEqual(yAbs, 0);

if (xIsZero) return yIsZero ? 0 : y;
if (yIsZero) return xIsZero ? 0 : x;

// If magnitudes are very different (ratio < 0.1), add smaller to larger
// Otherwise, add directly (they're similar enough)
return Math.min(xAbs, yAbs) / Math.max(xAbs, yAbs) < 0.1 ? (xAbs < yAbs ? y + x : x + y) : x + y;
return Math.min(xAbs, yAbs) < 0.1 * Math.max(xAbs, yAbs) ? (xAbs < yAbs ? y + x : x + y) : x + y;
}

/**
Expand All @@ -105,20 +107,54 @@ export function subtractStable(x: number, y: number): number {
const xAbs = Math.abs(x);
const yAbs = Math.abs(y);

if (xAbs === 0) return y === 0 ? 0 : -y;
if (yAbs === 0) return x;
const xIsZero = isApproximatelyEqual(xAbs, 0);
const yIsZero = isApproximatelyEqual(yAbs, 0);

if (xIsZero) return yIsZero ? 0 : -y;
if (yIsZero) return xIsZero ? 0 : x;

const minAbs = Math.min(xAbs, yAbs);
const maxAbs = Math.max(xAbs, yAbs);

// If magnitudes are similar, use stable subtraction by subtracting a common value
if (minAbs / maxAbs > 0.5) {
// Choose the value with smaller absolute magnitude as the common value to subtract
// This minimizes the magnitude of the intermediate results
if (minAbs > 0.5 * maxAbs) {
const m = xAbs < yAbs ? x : y;

return x - m - (y - m);
}

return x - y;
}

/**
* Checks if two floating-point numbers are approximately equal using a combination
* of absolute and relative error. This is more robust than simple epsilon comparison.
*
* For values near zero, uses absolute error: |a - b| < ε
* For values away from zero, uses relative error: |a - b| < ε · max(|a|, |b|)
*
* @param a - First number
* @param b - Second number
* @param epsilon - Maximum allowed error (defaults to Complex.EPSILON)
* @returns True if numbers are approximately equal
*
* @example
* ```typescript
* isApproximatelyEqual(0.1 + 0.2, 0.3); // => true
* isApproximatelyEqual(1, 1.0001, 0.001); // => true
* isApproximatelyEqual(1, 2); // => false
* ```
*/
export function isApproximatelyEqual(a: number, b: number, epsilon: number = Complex.EPSILON): boolean {
if (Number.isNaN(a) || Number.isNaN(b)) return false;
if (!Number.isFinite(a) || !Number.isFinite(b)) return a === b;

if (a === b) return true;

const absA = Math.abs(a);
const absB = Math.abs(b);
const maxAbs = Math.max(absA, absB);

if (maxAbs < 1) return Math.abs(a - b) < epsilon;

return Math.abs(a - b) < epsilon * maxAbs;
}
7 changes: 1 addition & 6 deletions src/operations/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,5 @@ export function add(z: Complex, w: Complex) {
}
if (isInfinite(z) || isInfinite(w)) return Complex.INFINITY;

const a = z.getRe();
const b = z.getIm();
const c = w.getRe();
const d = w.getIm();

return new Complex(addStable(a, c), addStable(b, d));
return new Complex(addStable(z.getRe(), w.getRe()), addStable(z.getIm(), w.getIm()));
}
Loading