@rslike/cmp
Version:
JavaScript Comparison package without undefined behavior!
269 lines (189 loc) • 7.2 kB
Markdown
Rust-inspired comparison traits for JavaScript and TypeScript. Adds `Symbol.compare`, `Symbol.equals`, and `Symbol.partialEquals` to built-in primitives and gives you utility functions to work with any comparable value.
```bash
npm i @rslike/cmp
yarn add @rslike/cmp
pnpm add @rslike/cmp
```
```ts
import "@rslike/cmp/globals"; // patch Symbol once in your entry file
import { compare, equals } from "@rslike/cmp";
compare(2, 5); // -1
compare(5, 2); // 1
compare(3, 3); // 0
equals(42, 42); // true
equals(42, "42"); // false
```
The package augments the global `Symbol` object with three well-known symbols. Import the globals entry **once** in your application entry point:
```ts
// entry.ts
import "@rslike/cmp/globals";
```
After that, `Symbol.compare`, `Symbol.equals`, and `Symbol.partialEquals` are available everywhere without additional imports.
Returns a number:
- `>= 1` — `this` is greater than `another`
- `0` — equal
- `<= -1` — `this` is less than `another`
- `NaN` — values are uncomparable (e.g. `NaN` vs `NaN`)
### `Symbol.equals` — strict equality (`Eq`)
Mirrors `===`. Returns `true` only when types and values both match.
Mirrors `==`. Allows cross-type comparisons (`0 == false`, `"5" == 5`).
## Built-in primitive support
After importing `@rslike/cmp/globals` (or `@rslike/cmp/primitives` directly), all four primitive types get trait implementations automatically.
### Number
```ts
import "@rslike/cmp/globals";
(5)[Symbol.compare](2); // 1
(5)[Symbol.compare](5); // 0
(2)[Symbol.compare](5); // -1
(4)[Symbol.compare](NaN); // NaN (non-finite → NaN)
(5)[Symbol.equals](5); // true
(5)[Symbol.equals](6); // false
(5)[Symbol.partialEquals]("5"); // true (loose ==)
(5)[Symbol.partialEquals](5); // true
```
```ts
"abc"[Symbol.compare]("abd"); // -1 (localeCompare)
"abc"[Symbol.compare]("abc"); // 0
"abd"[Symbol.compare]("abc"); // 1
"1"[Symbol.compare](1); // 0 (number coerced to string)
"true"[Symbol.compare](true); // 0 (boolean coerced to string)
"hello"[Symbol.equals]("hello"); // true
"hello"[Symbol.equals]("world"); // false
"5"[Symbol.partialEquals](5); // true (loose ==)
```
```ts
true[Symbol.compare](false); // 1
false[Symbol.compare](true); // -1
true[Symbol.compare](true); // 0
true[Symbol.compare](1); // inverted via Number[Symbol.compare]
true[Symbol.compare]("yes"); // compares this with Boolean("yes")
true[Symbol.equals](true); // true
true[Symbol.equals](false); // false
false[Symbol.partialEquals](0); // true (loose ==)
false[Symbol.partialEquals](""); // true
```
```ts
const d = new Date();
const earlier = new Date(d.valueOf() - 1000);
d[Symbol.compare](earlier); // 1 (later > earlier)
d[Symbol.compare](d.valueOf()); // 0 (same timestamp)
d[Symbol.compare](d.toISOString()); // 0 (string parsed to Date)
d[Symbol.equals](d.valueOf()); // true
d[Symbol.equals](d.toISOString()); // true
d[Symbol.equals](earlier); // false
d[Symbol.partialEquals](d); // true
d[Symbol.partialEquals](d.valueOf()); // true
```
> Passing an invalid date string (e.g. `"not-a-date"`) to `Date[Symbol.compare]` throws `UndefinedBehaviorError`.
## Utility functions
Import from the main entry point:
```ts
import { compare, equals, partialEquals } from "@rslike/cmp";
```
Calls `Symbol.compare` on the first argument that implements it. Falls back to `compareFn` if provided and neither argument implements the trait.
```ts
compare(5, 2); // 1
compare(2, 5); // -1
compare(1, 1); // 0
// custom objects
class Temperature {
constructor(public celsius: number) {}
[](other: Temperature) {
return this.celsius - other.celsius > 0
? 1
: this.celsius - other.celsius < 0
? -1
: 0;
}
}
compare(new Temperature(100), new Temperature(20)); // 1
// fallback compareFn for plain objects
compare({ n: 1 }, { n: 2 }, (a: any, b: any) => a.n - b.n);
```
**Throws `UndefinedBehaviorError`** when:
- Neither argument implements `Symbol.compare` and no `compareFn` is provided
- `compareFn` is not a function
- `compareFn` returns a non-number
Calls `Symbol.equals` on the first argument (object only) that implements it. Falls back to `equalityFn` or `a === b`.
```ts
equals(42, 42); // true
equals(42, "42"); // false
equals({}, {}); // false (different references, no trait)
// with custom trait
class Id {
constructor(public value: number) {}
[](other: Id) {
return this.value === other.value;
}
}
equals(new Id(1), new Id(1)); // true
```
**Throws `UndefinedBehaviorError`** when:
- `Symbol.equals` trait returns a non-boolean
- `equalityFn` is provided but is not a function
- `equalityFn` returns a non-boolean
Calls `Symbol.partialEquals` on the first argument that implements it. Falls back to `equalityFn` or `a == b`.
```ts
partialEquals(5, "5"); // true (5 == "5")
partialEquals(5, "6"); // false
partialEquals({}, {}); // false (== on two distinct objects)
// custom equalityFn
partialEquals({ v: 1 }, { v: "1" }, (a: any, b: any) => a.v == b.v); // true
```
**Throws `UndefinedBehaviorError`** when:
- `Symbol.partialEquals` trait returns a non-boolean
- `equalityFn` is provided but is not a function
- `equalityFn` returns a non-boolean
```ts
import "@rslike/cmp/globals";
import type { Ord, Eq, PartialEq } from "@rslike/cmp";
import { compare, equals } from "@rslike/cmp";
class Vector2 implements Ord, Eq, PartialEq {
constructor(public x: number, public y: number) {}
get magnitude() {
return Math.sqrt(this.x ** 2 + this.y ** 2);
}
[](other: Vector2): number {
const diff = this.magnitude - other.magnitude;
return diff > 0 ? 1 : diff < 0 ? -1 : 0;
}
[](other: Vector2): boolean {
return this.x === other.x && this.y === other.y;
}
[](other: unknown): boolean {
if (other instanceof Vector2) return this[Symbol.equals](other);
return false;
}
}
const v1 = new Vector2(3, 4); // magnitude 5
const v2 = new Vector2(1, 0); // magnitude 1
compare(v1, v2); // 1
equals(v1, v1); // true
equals(v1, v2); // false
```
| Type | Description |
| ----------- | ---------------------------------------------------- |
| `Ord` | Implement `[Symbol.compare](another): number` |
| `Eq` | Implement `[Symbol.equals](another): boolean` |
| `PartialEq` | Implement `[Symbol.partialEquals](another): boolean` |
- [@rslike/std](https://www.npmjs.com/package/@rslike/std) — `Option`, `Result`, `UndefinedBehaviorError`
- [@rslike/dbg](https://www.npmjs.com/package/@rslike/dbg) — debug printing for variables
- [@rslike/iter](https://www.npmjs.com/package/@rslike/iter) — lazy iterators
https://github.com/vitalics/rslike/wiki