2026-05-26 17:35:18 -07:00
|
|
|
/**
|
|
|
|
|
* @module utils/collation
|
|
|
|
|
* @description Deterministic string ordering for persisted indexes and any comparison
|
|
|
|
|
* that must agree with the native (Rust) column store / aggregation engine.
|
|
|
|
|
*
|
|
|
|
|
* Strings are compared by **UTF-8 byte order**, which equals Unicode code-point order
|
|
|
|
|
* and is byte-for-byte identical to Rust's `str::cmp` (`as_bytes().cmp()`). This is:
|
|
|
|
|
* - **deterministic** across environments (unlike `localeCompare`, whose default-locale
|
|
|
|
|
* ordering varies by OS / Node / ICU build — unsafe for a persisted sorted index), and
|
2026-07-02 15:11:41 -07:00
|
|
|
* - **cross-language consistent** with `@soulcraft/cor`'s native column store and
|
2026-05-26 17:35:18 -07:00
|
|
|
* aggregation sort, so query results are identical with or without the native plugin.
|
|
|
|
|
*
|
|
|
|
|
* Use this instead of `String.localeCompare` and the `<`/`>` operators (which compare
|
|
|
|
|
* UTF-16 code units and diverge from code-point order for supplementary-plane characters)
|
2026-07-02 15:11:41 -07:00
|
|
|
* wherever ordering is persisted or must match the native engine. See cor `docs/ADR-001`.
|
2026-05-26 17:35:18 -07:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
const utf8 = new TextEncoder()
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Compare two strings by UTF-8 byte order (== Unicode code-point order == Rust `str::cmp`).
|
|
|
|
|
*
|
|
|
|
|
* @param a - First string.
|
|
|
|
|
* @param b - Second string.
|
|
|
|
|
* @returns Negative if `a < b`, positive if `a > b`, `0` if equal — by UTF-8 byte order.
|
|
|
|
|
* @example
|
|
|
|
|
* ['b', 'A', 'a'].sort(compareCodePoints) // ['A', 'a', 'b'] (byte order: A=65, a=97, b=98)
|
|
|
|
|
*/
|
|
|
|
|
export function compareCodePoints(a: string, b: string): number {
|
|
|
|
|
const ea = utf8.encode(a)
|
|
|
|
|
const eb = utf8.encode(b)
|
|
|
|
|
const len = ea.length < eb.length ? ea.length : eb.length
|
|
|
|
|
for (let i = 0; i < len; i++) {
|
|
|
|
|
if (ea[i] !== eb[i]) return ea[i] - eb[i]
|
|
|
|
|
}
|
|
|
|
|
return ea.length - eb.length
|
|
|
|
|
}
|