Functional Weave
Code in Rust

collections.group-by-key@1.0.0

impl/typescript.ts

2,769 bytes · the TypeScript implementation · view raw

/** An open record, the manifest's `record`: a JSON-ish map whose shape is not known ahead of time. */
type GroupableRecord = Readonly<Record<string, unknown>>;

/** Largest integer JavaScript can hold exactly; beyond it the three languages disagree. */
const SAFE_INTEGER = 9007199254740991;

/**
 * The name of the group a value belongs to.
 *
 * Each language has its own default string conversion and they disagree
 * (Python prints True where JavaScript prints true, and 1.0 where JavaScript
 * prints 1), so the rendering is spelled out here instead of inherited.
 */
function groupNameOf(value: unknown, key: string): string {
  // Absent and null are the same thing: a document that omits a field and one
  // that nulls it mean the same to every reader.
  if (value === undefined || value === null) return "";
  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 group by the fractional number ${value} at "${key}"`);
    }
    if (Math.abs(value) > SAFE_INTEGER) {
      throw new RangeError(`cannot group by the out-of-range number ${value} at "${key}"`);
    }
    return String(value);
  }
  throw new TypeError(`cannot group by the list or map at "${key}"`);
}

/**
 * Group `records` by the value at `key`.
 *
 * Records keep their input order inside each group, so grouping a sorted list
 * never unsorts it - the property callers actually depend on.
 */
export function groupByKey(
  records: readonly GroupableRecord[],
  key: string,
): Readonly<Record<string, readonly GroupableRecord[]>> {
  if (!Array.isArray(records)) {
    throw new TypeError("groupByKey needs a list of records");
  }
  if (typeof key !== "string" || key.length === 0) {
    throw new TypeError("groupByKey needs a non-empty key name");
  }

  // A null prototype, so that a record grouping under "__proto__" or
  // "constructor" creates an ordinary group instead of corrupting the result.
  const groups: { [group: string]: GroupableRecord[] } = Object.create(null);
  for (const record of records) {
    const name = groupNameOf(record === null || record === undefined ? undefined : record[key], key);
    if (groups[name] === undefined) groups[name] = [];
    groups[name].push(record);
  }
  return groups;
}

/** How many records fall in each group, without carrying the records themselves. */
export function countByKey(records: readonly GroupableRecord[], key: string): Readonly<Record<string, number>> {
  const counts: { [group: string]: number } = Object.create(null);
  for (const [name, group] of Object.entries(groupByKey(records, key))) {
    counts[name] = group.length;
  }
  return counts;
}