collections.dedupe-by-key@1.0.0
impl/typescript.ts
2,951 bytes · the TypeScript implementation · view raw
import { type DedupeKeep } from "./collections_dedupe_by_key_types.ts";
/** An open record, the manifest's `record`: a JSON-ish map whose shape is not known ahead of time. */
type DedupeRecord = Readonly<Record<string, unknown>>;
/** Largest integer JavaScript can hold exactly; beyond it the three languages disagree. */
const SAFE_INTEGER = 9007199254740991;
/**
* The identity of a record, or null when it has none.
*
* Rendered exactly as collections.group-by-key renders a group name, so the
* two capabilities agree on what "the same key" means. Each language's
* default string conversion differs (Python prints True, JavaScript true), so
* the rendering is spelled out rather than inherited.
*/
function keyOf(value: unknown, key: string): string | null {
// A record with no key is not a duplicate of anything: two records that
// both lack an id are two unknowns, not one thing seen twice.
if (value === undefined || value === null) return null;
if (typeof value === "string") return value;
if (typeof value === "boolean") return value ? "true" : "false";
if (typeof value === "number") {
if (!Number.isInteger(value)) {
throw new TypeError(`cannot dedupe by the fractional number ${value} at "${key}"`);
}
if (Math.abs(value) > SAFE_INTEGER) {
throw new RangeError(`cannot dedupe by the out-of-range number ${value} at "${key}"`);
}
return String(value);
}
throw new TypeError(`cannot dedupe by the list or map at "${key}"`);
}
/**
* Remove records that share the value at `key`, keeping the first or the
* last of each set.
*
* Survivors keep their own positions: with "last", the survivor sits where
* the last occurrence was, which is what "latest wins" means in a change feed.
*/
export function dedupeByKey(
records: readonly DedupeRecord[],
key: string,
keep: DedupeKeep,
): readonly DedupeRecord[] {
if (!Array.isArray(records)) {
throw new TypeError("dedupeByKey needs a list of records");
}
if (typeof key !== "string" || key.length === 0) {
throw new TypeError("dedupeByKey needs a non-empty key name");
}
if (keep !== "first" && keep !== "last") {
throw new RangeError(`keep must be "first" or "last", received "${keep}"`);
}
// Every key is rendered up front, so a bad value raises whichever mode runs.
const keys = records.map((record) =>
keyOf(record === null || record === undefined ? undefined : record[key], key),
);
const seen = new Set<string>();
const survives: boolean[] = new Array(records.length).fill(false);
// Walking backwards for "last" makes the last occurrence the first one seen.
const order = records.map((_, i) => i);
if (keep === "last") order.reverse();
for (const i of order) {
const k = keys[i];
if (k === null) {
survives[i] = true;
} else if (!seen.has(k)) {
seen.add(k);
survives[i] = true;
}
}
return records.filter((_, i) => survives[i]);
}