@rslike/cmp
v3.2.1
Published
JavaScript Comparison package without undefined behavior!
Downloads
18
Maintainers
Readme
@rslike/cmp
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.
Installation
npm i @rslike/cmp
yarn add @rslike/cmp
pnpm add @rslike/cmpQuick start
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"); // falseGlobals
The package augments the global Symbol object with three well-known symbols. Import the globals entry once in your application entry point:
// entry.ts
import "@rslike/cmp/globals";After that, Symbol.compare, Symbol.equals, and Symbol.partialEquals are available everywhere without additional imports.
Traits
Symbol.compare — total ordering (Ord)
Returns a number:
>= 1—thisis greater thananother0— equal<= -1—thisis less thananotherNaN— values are uncomparable (e.g.NaNvsNaN)
Symbol.equals — strict equality (Eq)
Mirrors ===. Returns true only when types and values both match.
Symbol.partialEquals — loose equality (PartialEq)
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
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); // trueString
"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 ==)Boolean
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](""); // trueDate
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()); // truePassing an invalid date string (e.g.
"not-a-date") toDate[Symbol.compare]throwsUndefinedBehaviorError.
Utility functions
Import from the main entry point:
import { compare, equals, partialEquals } from "@rslike/cmp";compare(a, b, [compareFn])
Calls Symbol.compare on the first argument that implements it. Falls back to compareFn if provided and neither argument implements the trait.
compare(5, 2); // 1
compare(2, 5); // -1
compare(1, 1); // 0
// custom objects
class Temperature {
constructor(public celsius: number) {}
[Symbol.compare](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.compareand nocompareFnis provided compareFnis not a functioncompareFnreturns a non-number
equals(a, b, [equalityFn])
Calls Symbol.equals on the first argument (object only) that implements it. Falls back to equalityFn or a === b.
equals(42, 42); // true
equals(42, "42"); // false
equals({}, {}); // false (different references, no trait)
// with custom trait
class Id {
constructor(public value: number) {}
[Symbol.equals](other: Id) {
return this.value === other.value;
}
}
equals(new Id(1), new Id(1)); // trueThrows UndefinedBehaviorError when:
Symbol.equalstrait returns a non-booleanequalityFnis provided but is not a functionequalityFnreturns a non-boolean
partialEquals(a, b, [equalityFn])
Calls Symbol.partialEquals on the first argument that implements it. Falls back to equalityFn or a == b.
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); // trueThrows UndefinedBehaviorError when:
Symbol.partialEqualstrait returns a non-booleanequalityFnis provided but is not a functionequalityFnreturns a non-boolean
Implementing traits on your own classes
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);
}
[Symbol.compare](other: Vector2): number {
const diff = this.magnitude - other.magnitude;
return diff > 0 ? 1 : diff < 0 ? -1 : 0;
}
[Symbol.equals](other: Vector2): boolean {
return this.x === other.x && this.y === other.y;
}
[Symbol.partialEquals](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); // falseTypeScript types
| Type | Description |
| ----------- | ---------------------------------------------------- |
| Ord | Implement [Symbol.compare](another): number |
| Eq | Implement [Symbol.equals](another): boolean |
| PartialEq | Implement [Symbol.partialEquals](another): boolean |
Related packages
- @rslike/std —
Option,Result,UndefinedBehaviorError - @rslike/dbg — debug printing for variables
- @rslike/iter — lazy iterators
WIKI
https://github.com/vitalics/rslike/wiki
