Functional Weave
Code in Rust

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]);
}