From a0a5df20b419f2bb8397c0c4887f3793395854f8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 23:46:42 +0000 Subject: [PATCH 1/4] =?UTF-8?q?Iteration=20296:=20+hashPandasObject=20?= =?UTF-8?q?=E2=80=94=20FNV-1a=2064-bit=20hashing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run: https://github.com/githubnext/tsessebe/actions/runs/25139337654 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- playground/hash_pandas_object.html | 98 ++++++++++++ playground/index.html | 5 + src/index.ts | 2 + src/stats/hash_pandas_object.ts | 209 +++++++++++++++++++++++++ src/stats/index.ts | 2 + tests/stats/hash_pandas_object.test.ts | 209 +++++++++++++++++++++++++ 6 files changed, 525 insertions(+) create mode 100644 playground/hash_pandas_object.html create mode 100644 src/stats/hash_pandas_object.ts create mode 100644 tests/stats/hash_pandas_object.test.ts diff --git a/playground/hash_pandas_object.html b/playground/hash_pandas_object.html new file mode 100644 index 00000000..972d0fc1 --- /dev/null +++ b/playground/hash_pandas_object.html @@ -0,0 +1,98 @@ + + + + + + tsb Β· hashPandasObject + + + +

πŸ“¦ hashPandasObject

+

+ Compute FNV-1a 64-bit hash values for each element of a + Series or each row of a DataFrame. + Mirrors pandas.util.hash_pandas_object. +

+ +

Series hashing

+
import { Series, hashPandasObject } from "tsb";
+
+const s = new Series({ data: ["apple", "banana", "apple"], index: [0, 1, 2] });
+const h = hashPandasObject(s, { index: false });
+
+// Same value β†’ same hash
+console.log(h.iat(0) === h.iat(2)); // true  (both "apple")
+console.log(h.iat(0) === h.iat(1)); // false ("apple" β‰  "banana")
+
+ +

DataFrame row hashing

+
import { DataFrame, hashPandasObject } from "tsb";
+
+const df = new DataFrame({
+  id:   [1, 2, 3],
+  name: ["Alice", "Bob", "Alice"],
+  age:  [30, 25, 30],
+});
+
+const rowHashes = hashPandasObject(df, { index: false });
+// Rows 0 and 2 are identical β†’ same hash
+console.log(rowHashes.iat(0) === rowHashes.iat(2)); // true
+console.log(rowHashes.iat(0) === rowHashes.iat(1)); // false
+
+ +

Deduplication with hashes

+
import { DataFrame, hashPandasObject } from "tsb";
+
+const df = new DataFrame({
+  a: [1, 2, 1, 3],
+  b: ["x", "y", "x", "z"],
+});
+
+const hashes = hashPandasObject(df, { index: false });
+const seen = new Set<number>();
+const uniqueRows: number[] = [];
+
+for (let i = 0; i < df.shape[0]; i++) {
+  const h = hashes.iat(i);
+  if (!seen.has(h)) {
+    seen.add(h);
+    uniqueRows.push(i);
+  }
+}
+// uniqueRows = [0, 1, 3]  β€” row 2 is a duplicate of row 0
+console.log(uniqueRows);
+
+ +

Controlling index inclusion

+
import { Series, hashPandasObject } from "tsb";
+
+const s = new Series({ data: [42, 42], index: ["a", "b"] });
+
+// index=true (default): different index β†’ different hash
+const withIdx = hashPandasObject(s, { index: true });
+console.log(withIdx.iat(0) === withIdx.iat(1)); // false
+
+// index=false: only values matter
+const noIdx = hashPandasObject(s, { index: false });
+console.log(noIdx.iat(0) === noIdx.iat(1)); // true
+
+ +
+ Algorithm: FNV-1a 64-bit (Fowler–Noll–Vo), a fast non-cryptographic hash + chosen for its excellent avalanche properties on short inputs. Results are stored as + float64 numbers (the 64-bit bit-pattern cast via Number(BigInt)). +
+ +

← Back to tsb playground

+ + diff --git a/playground/index.html b/playground/index.html index 62e78e60..27ccedd2 100644 --- a/playground/index.html +++ b/playground/index.html @@ -454,6 +454,11 @@

dataFrameStyle(df) Β· highlightMax / highlightMin / highlightNull / highlightBetween Β· backgroundGradient / textGradient Β· barChart Β· format / formatIndex Β· apply / applymap / map Β· setCaption / setTableStyles / hide Β· toHtml / toLatex. Mirrors pandas.DataFrame.style (Styler).

βœ… Complete
+
+

πŸ”‘ hashPandasObject β€” FNV-1a Hashing

+

hashPandasObject(s) Β· hashPandasObject(df) Β· index option. Mirrors pandas.util.hash_pandas_object. FNV-1a 64-bit per element or row.

+
βœ… Complete
+
diff --git a/src/index.ts b/src/index.ts index 27a15d16..5770bb34 100644 --- a/src/index.ts +++ b/src/index.ts @@ -683,3 +683,5 @@ export type { GradientOptions, BarOptions, } from "./stats/index.ts"; +export { hashPandasObject } from "./stats/index.ts"; +export type { HashPandasObjectOptions } from "./stats/index.ts"; diff --git a/src/stats/hash_pandas_object.ts b/src/stats/hash_pandas_object.ts new file mode 100644 index 00000000..43561c63 --- /dev/null +++ b/src/stats/hash_pandas_object.ts @@ -0,0 +1,209 @@ +/** + * hash_pandas_object β€” FNV-1a 64-bit hashes for Series and DataFrame. + * + * Mirrors `pandas.util.hash_pandas_object`, which returns a `Series` of + * `uint64` hash values β€” one per element (for a Series input) or one per row + * (for a DataFrame input). + * + * Implementation uses FNV-1a 64-bit (Fowler–Noll–Vo) running on JavaScript + * `BigInt` arithmetic. The result values are stored as `float64` (the only + * numeric type available in the tsb dtype system) by converting the `uint64` + * bit-pattern to `number` via `Number(bigint)`. For hash-equality checks this + * is fine because every `uint64` value that differs will also differ as a + * `float64` in the range 0 – 2**64-1 that we use. + * + * @example + * ```ts + * import { Series, DataFrame, hashPandasObject } from "tsb"; + * + * const s = new Series({ data: [1, 2, 3], index: ["a", "b", "c"] }); + * const h = hashPandasObject(s); + * // h is a Series with hash values; equal inputs β‡’ equal hashes + * + * const df = new DataFrame({ a: [1, 2], b: ["x", "y"] }); + * const hr = hashPandasObject(df); + * // hr has one hash per row + * ``` + * + * @module + */ + +import type { Scalar } from "../types.ts"; +import { DataFrame } from "../core/frame.ts"; +import { Series } from "../core/series.ts"; + +// ─── FNV-1a 64-bit constants ────────────────────────────────────────────────── + +const FNV_PRIME = BigInt("0x00000100000001B3"); +const FNV_OFFSET = BigInt("0xcbf29ce484222325"); +const MASK64 = (BigInt(1) << BigInt(64)) - BigInt(1); + +/** Hash a single byte into the running FNV-1a state. */ +function fnvByte(hash: bigint, byte: number): bigint { + return ((hash ^ BigInt(byte)) * FNV_PRIME) & MASK64; +} + +/** Hash an arbitrary string (UTF-8 bytes) into the FNV state. */ +function fnvString(hash: bigint, s: string): bigint { + for (let i = 0; i < s.length; i++) { + let code = s.charCodeAt(i); + // Encode as UTF-8 bytes + if (code < 0x80) { + hash = fnvByte(hash, code); + } else if (code < 0x800) { + hash = fnvByte(hash, 0xc0 | (code >> 6)); + hash = fnvByte(hash, 0x80 | (code & 0x3f)); + } else { + hash = fnvByte(hash, 0xe0 | (code >> 12)); + hash = fnvByte(hash, 0x80 | ((code >> 6) & 0x3f)); + hash = fnvByte(hash, 0x80 | (code & 0x3f)); + } + } + return hash; +} + +/** Hash a single scalar value into the FNV state. */ +function fnvScalar(hash: bigint, val: Scalar): bigint { + if (val === null || val === undefined) { + // encode as a sentinel byte sequence + return fnvByte(fnvByte(hash, 0xfe), 0xfe); + } + if (typeof val === "boolean") { + return fnvByte(hash, val ? 1 : 0); + } + if (typeof val === "number") { + if (Number.isNaN(val)) { + return fnvByte(fnvByte(hash, 0xfd), 0xfd); + } + // Encode as little-endian 8-byte IEEE 754 + const buf = new ArrayBuffer(8); + new DataView(buf).setFloat64(0, val, true); + const bytes = new Uint8Array(buf); + for (let i = 0; i < 8; i++) { + hash = fnvByte(hash, bytes[i]!); + } + return hash; + } + if (typeof val === "bigint") { + return fnvString(hash, val.toString()); + } + if (val instanceof Date) { + return fnvString(hash, String(val.getTime())); + } + // string or timedelta-like β€” stringify + return fnvString(hash, String(val)); +} + +// ─── Options ────────────────────────────────────────────────────────────────── + +/** Options for {@link hashPandasObject}. */ +export interface HashPandasObjectOptions { + /** + * Whether to include the index in the hash. Default `true`. + * + * When `false`, two Series with different indexes but identical values will + * produce the same hash values. + */ + index?: boolean; +} + +// ─── Series overload ────────────────────────────────────────────────────────── + +/** + * Return a `Series` of FNV-1a 64-bit hash values for each element + * of `s`. The result index matches `s.index`. + * + * Mirrors `pandas.util.hash_pandas_object` for a `Series` input. + * + * @param obj - A `Series` to hash. + * @param options - Optional settings (see {@link HashPandasObjectOptions}). + * @returns A `Series` of hash values. + * + * @example + * ```ts + * const s = new Series({ data: ["a", "b", "a"], index: [0, 1, 2] }); + * const h = hashPandasObject(s); + * h.iat(0) === h.iat(2); // true β€” same value β†’ same hash + * h.iat(0) !== h.iat(1); // true (with overwhelming probability) + * ``` + */ +export function hashPandasObject( + obj: Series, + options?: HashPandasObjectOptions, +): Series; + +/** + * Return a `Series` of FNV-1a 64-bit row-hashes for each row of `df`. + * The result index matches `df.index`. + * + * Mirrors `pandas.util.hash_pandas_object` for a `DataFrame` input. + * + * @param obj - A `DataFrame` to hash. + * @param options - Optional settings (see {@link HashPandasObjectOptions}). + * @returns A `Series` of row hash values. + * + * @example + * ```ts + * const df = new DataFrame({ a: [1, 2], b: ["x", "y"] }); + * const h = hashPandasObject(df); + * // h.iat(0) is the hash of row 0; h.iat(1) is the hash of row 1 + * ``` + */ +export function hashPandasObject( + obj: DataFrame, + options?: HashPandasObjectOptions, +): Series; + +export function hashPandasObject( + obj: Series | DataFrame, + options: HashPandasObjectOptions = {}, +): Series { + const includeIndex = options.index !== false; + + if (obj instanceof Series) { + return _hashSeries(obj, includeIndex); + } + return _hashDataFrame(obj, includeIndex); +} + +// ─── internal helpers ───────────────────────────────────────────────────────── + +function _hashSeries(s: Series, includeIndex: boolean): Series { + const n = s.index.size; + const hashes: number[] = []; + + for (let i = 0; i < n; i++) { + let h = FNV_OFFSET; + if (includeIndex) { + h = fnvScalar(h, s.index.at(i) as Scalar); + // separator byte between index and value + h = fnvByte(h, 0xff); + } + h = fnvScalar(h, s.iat(i)); + hashes.push(Number(h)); + } + + return new Series({ data: hashes, index: s.index, dtype: "float64" }); +} + +function _hashDataFrame(df: DataFrame, includeIndex: boolean): Series { + const [nRows] = df.shape; + const colNames = df.columns.values as readonly string[]; + const hashes: number[] = []; + + for (let i = 0; i < nRows; i++) { + let h = FNV_OFFSET; + if (includeIndex) { + h = fnvScalar(h, df.index.at(i) as Scalar); + h = fnvByte(h, 0xff); + } + for (const name of colNames) { + const s = df.col(name); + h = fnvScalar(h, s.iat(i)); + h = fnvByte(h, 0xfe); // column separator + } + hashes.push(Number(h)); + } + + return new Series({ data: hashes, index: df.index, dtype: "float64" }); +} diff --git a/src/stats/index.ts b/src/stats/index.ts index 454582aa..2dd26e63 100644 --- a/src/stats/index.ts +++ b/src/stats/index.ts @@ -501,3 +501,5 @@ export type { GradientOptions, BarOptions, } from "./style.ts"; +export { hashPandasObject } from "./hash_pandas_object.ts"; +export type { HashPandasObjectOptions } from "./hash_pandas_object.ts"; diff --git a/tests/stats/hash_pandas_object.test.ts b/tests/stats/hash_pandas_object.test.ts new file mode 100644 index 00000000..3e26e4cf --- /dev/null +++ b/tests/stats/hash_pandas_object.test.ts @@ -0,0 +1,209 @@ +/** + * Tests for hashPandasObject β€” FNV-1a 64-bit hashing of Series and DataFrame. + */ + +import { describe, expect, it } from "bun:test"; +import fc from "fast-check"; +import { DataFrame, Series, hashPandasObject } from "../../src/index.ts"; + +// ─── Series hashing ─────────────────────────────────────────────────────────── + +describe("hashPandasObject β€” Series", () => { + it("returns a Series of the same length", () => { + const s = new Series({ data: [1, 2, 3] }); + const h = hashPandasObject(s); + expect(h.index.size).toBe(3); + }); + + it("returns numeric hash values", () => { + const s = new Series({ data: ["a", "b", "c"] }); + const h = hashPandasObject(s); + for (let i = 0; i < 3; i++) { + expect(typeof h.iat(i)).toBe("number"); + } + }); + + it("equal values β†’ equal hashes (index=true)", () => { + const s = new Series({ data: ["x", "y", "x"], index: [0, 1, 2] }); + const h = hashPandasObject(s); + // index differs (0 vs 2), so hashes must differ + expect(h.iat(0)).not.toBe(h.iat(2)); + }); + + it("equal values + equal index β†’ equal hashes", () => { + const s1 = new Series({ data: [42], index: ["k"] }); + const s2 = new Series({ data: [42], index: ["k"] }); + const h1 = hashPandasObject(s1); + const h2 = hashPandasObject(s2); + expect(h1.iat(0)).toBe(h2.iat(0)); + }); + + it("different values β†’ different hashes (with overwhelming probability)", () => { + const s = new Series({ data: [1, 2, 3, 4, 5], index: [0, 1, 2, 3, 4] }); + const h = hashPandasObject(s); + const unique = new Set(); + for (let i = 0; i < 5; i++) { + unique.add(h.iat(i)); + } + expect(unique.size).toBe(5); + }); + + it("index=false: same value + different index β†’ same hash", () => { + const s = new Series({ data: ["hello", "hello"], index: ["a", "b"] }); + const h = hashPandasObject(s, { index: false }); + expect(h.iat(0)).toBe(h.iat(1)); + }); + + it("index=false: different values β†’ different hashes", () => { + const s = new Series({ data: ["hello", "world"] }); + const h = hashPandasObject(s, { index: false }); + expect(h.iat(0)).not.toBe(h.iat(1)); + }); + + it("preserves the original index on the result", () => { + const s = new Series({ data: [10, 20], index: ["x", "y"] }); + const h = hashPandasObject(s); + expect(h.index.at(0)).toBe("x"); + expect(h.index.at(1)).toBe("y"); + }); + + it("handles null values", () => { + const s = new Series({ data: [null, null], index: [0, 1] }); + const h = hashPandasObject(s, { index: false }); + // Same null β†’ same hash + expect(h.iat(0)).toBe(h.iat(1)); + }); + + it("null β‰  zero hash", () => { + const sNull = new Series({ data: [null], index: [0] }); + const sZero = new Series({ data: [0], index: [0] }); + const hNull = hashPandasObject(sNull, { index: false }); + const hZero = hashPandasObject(sZero, { index: false }); + expect(hNull.iat(0)).not.toBe(hZero.iat(0)); + }); + + it("handles boolean values", () => { + const s = new Series({ data: [true, false, true] }); + const h = hashPandasObject(s, { index: false }); + expect(h.iat(0)).toBe(h.iat(2)); // true === true + expect(h.iat(0)).not.toBe(h.iat(1)); // true β‰  false + }); + + it("handles Date values", () => { + const d1 = new Date("2024-01-01"); + const d2 = new Date("2024-01-02"); + const s = new Series({ data: [d1, d1, d2] }); + const h = hashPandasObject(s, { index: false }); + expect(h.iat(0)).toBe(h.iat(1)); + expect(h.iat(0)).not.toBe(h.iat(2)); + }); + + it("handles empty Series", () => { + const s = new Series({ data: [] }); + const h = hashPandasObject(s); + expect(h.index.size).toBe(0); + }); + + it("property: deterministic β€” same input same hash", () => { + fc.assert( + fc.property(fc.array(fc.oneof(fc.integer(), fc.string(), fc.boolean()), { maxLength: 20 }), (arr) => { + const s1 = new Series({ data: arr }); + const s2 = new Series({ data: arr }); + const h1 = hashPandasObject(s1, { index: false }); + const h2 = hashPandasObject(s2, { index: false }); + for (let i = 0; i < arr.length; i++) { + if (h1.iat(i) !== h2.iat(i)) { + return false; + } + } + return true; + }), + ); + }); +}); + +// ─── DataFrame hashing ──────────────────────────────────────────────────────── + +describe("hashPandasObject β€” DataFrame", () => { + it("returns a Series with one hash per row", () => { + const df = DataFrame.fromColumns({ a: [1, 2, 3], b: ["x", "y", "z"] }); + const h = hashPandasObject(df); + expect(h.index.size).toBe(3); + }); + + it("returns numeric hashes", () => { + const df = DataFrame.fromColumns({ a: [1, 2], b: [3, 4] }); + const h = hashPandasObject(df); + expect(typeof h.iat(0)).toBe("number"); + expect(typeof h.iat(1)).toBe("number"); + }); + + it("identical rows β†’ same hash (index=false)", () => { + const df = DataFrame.fromColumns({ a: [1, 1], b: ["x", "x"] }); + const h = hashPandasObject(df, { index: false }); + expect(h.iat(0)).toBe(h.iat(1)); + }); + + it("different rows β†’ different hashes", () => { + const df = DataFrame.fromColumns({ a: [1, 2], b: ["x", "y"] }); + const h = hashPandasObject(df, { index: false }); + expect(h.iat(0)).not.toBe(h.iat(1)); + }); + + it("preserves df.index on result", () => { + const df = DataFrame.fromColumns({ a: [10, 20] }, { index: ["r0", "r1"] }); + const h = hashPandasObject(df); + expect(h.index.at(0)).toBe("r0"); + expect(h.index.at(1)).toBe("r1"); + }); + + it("index=true: same data, different index β†’ different hashes", () => { + const df = DataFrame.fromColumns({ a: [1, 1] }, { index: [0, 1] }); + const h = hashPandasObject(df, { index: true }); + expect(h.iat(0)).not.toBe(h.iat(1)); + }); + + it("handles null values in rows", () => { + const df = DataFrame.fromColumns({ a: [null, null] }); + const h = hashPandasObject(df, { index: false }); + expect(h.iat(0)).toBe(h.iat(1)); + }); + + it("handles empty DataFrame", () => { + const df = DataFrame.fromColumns({}); + const h = hashPandasObject(df); + expect(h.index.size).toBe(0); + }); + + it("column order matters", () => { + // { a:[1], b:[2] } β‰  { b:[2], a:[1] } β€” different column order β†’ different row hashes + const df1 = DataFrame.fromColumns({ a: [1], b: [2] }); + const df2 = DataFrame.fromColumns({ b: [2], a: [1] }); + const h1 = hashPandasObject(df1, { index: false }); + const h2 = hashPandasObject(df2, { index: false }); + // Column order is reflected in the hash + expect(h1.iat(0)).not.toBe(h2.iat(0)); + }); + + it("property: deterministic for DataFrames", () => { + fc.assert( + fc.property( + fc.array(fc.record({ a: fc.integer(), b: fc.string() }), { minLength: 1, maxLength: 10 }), + (rows) => { + const aVals = rows.map((r) => r.a); + const bVals = rows.map((r) => r.b); + const df1 = DataFrame.fromColumns({ a: aVals, b: bVals }); + const df2 = DataFrame.fromColumns({ a: aVals, b: bVals }); + const h1 = hashPandasObject(df1, { index: false }); + const h2 = hashPandasObject(df2, { index: false }); + for (let i = 0; i < rows.length; i++) { + if (h1.iat(i) !== h2.iat(i)) { + return false; + } + } + return true; + }, + ), + ); + }); +}); From c6462dac65612ce6fe0668572abefa13676dd567 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 06:25:41 +0000 Subject: [PATCH 2/4] Fix TS2322: use Dtype.float64 instead of string literal in hash_pandas_object Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/stats/hash_pandas_object.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/stats/hash_pandas_object.ts b/src/stats/hash_pandas_object.ts index 43561c63..632a4f36 100644 --- a/src/stats/hash_pandas_object.ts +++ b/src/stats/hash_pandas_object.ts @@ -31,6 +31,7 @@ import type { Scalar } from "../types.ts"; import { DataFrame } from "../core/frame.ts"; import { Series } from "../core/series.ts"; +import { Dtype } from "../core/dtype.ts"; // ─── FNV-1a 64-bit constants ────────────────────────────────────────────────── @@ -183,7 +184,7 @@ function _hashSeries(s: Series, includeIndex: boolean): Series { hashes.push(Number(h)); } - return new Series({ data: hashes, index: s.index, dtype: "float64" }); + return new Series({ data: hashes, index: s.index, dtype: Dtype.float64 }); } function _hashDataFrame(df: DataFrame, includeIndex: boolean): Series { @@ -205,5 +206,5 @@ function _hashDataFrame(df: DataFrame, includeIndex: boolean): Series { hashes.push(Number(h)); } - return new Series({ data: hashes, index: df.index, dtype: "float64" }); + return new Series({ data: hashes, index: df.index, dtype: Dtype.float64 }); } From 5e55e966da52739ac00d8908fb1898b0bf671eac Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 16:47:20 +0000 Subject: [PATCH 3/4] Fix lint errors in hash_pandas_object: noParameterAssign, useImportType, format - Use local variable 'h' instead of reassigning 'hash' parameter in fnvString and fnvScalar - Replace non-null assertion bytes[i]! with null-coalescing bytes[i] ?? 0 - Auto-fix: sort imports, make DataFrame import type-only, format test file Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/stats/hash_pandas_object.ts | 38 ++++++++++++-------------- tests/stats/hash_pandas_object.test.ts | 25 +++++++++-------- 2 files changed, 31 insertions(+), 32 deletions(-) diff --git a/src/stats/hash_pandas_object.ts b/src/stats/hash_pandas_object.ts index 632a4f36..d69b2c47 100644 --- a/src/stats/hash_pandas_object.ts +++ b/src/stats/hash_pandas_object.ts @@ -28,10 +28,10 @@ * @module */ -import type { Scalar } from "../types.ts"; -import { DataFrame } from "../core/frame.ts"; -import { Series } from "../core/series.ts"; import { Dtype } from "../core/dtype.ts"; +import type { DataFrame } from "../core/frame.ts"; +import { Series } from "../core/series.ts"; +import type { Scalar } from "../types.ts"; // ─── FNV-1a 64-bit constants ────────────────────────────────────────────────── @@ -46,21 +46,22 @@ function fnvByte(hash: bigint, byte: number): bigint { /** Hash an arbitrary string (UTF-8 bytes) into the FNV state. */ function fnvString(hash: bigint, s: string): bigint { + let h = hash; for (let i = 0; i < s.length; i++) { - let code = s.charCodeAt(i); + const code = s.charCodeAt(i); // Encode as UTF-8 bytes if (code < 0x80) { - hash = fnvByte(hash, code); + h = fnvByte(h, code); } else if (code < 0x800) { - hash = fnvByte(hash, 0xc0 | (code >> 6)); - hash = fnvByte(hash, 0x80 | (code & 0x3f)); + h = fnvByte(h, 0xc0 | (code >> 6)); + h = fnvByte(h, 0x80 | (code & 0x3f)); } else { - hash = fnvByte(hash, 0xe0 | (code >> 12)); - hash = fnvByte(hash, 0x80 | ((code >> 6) & 0x3f)); - hash = fnvByte(hash, 0x80 | (code & 0x3f)); + h = fnvByte(h, 0xe0 | (code >> 12)); + h = fnvByte(h, 0x80 | ((code >> 6) & 0x3f)); + h = fnvByte(h, 0x80 | (code & 0x3f)); } } - return hash; + return h; } /** Hash a single scalar value into the FNV state. */ @@ -80,10 +81,11 @@ function fnvScalar(hash: bigint, val: Scalar): bigint { const buf = new ArrayBuffer(8); new DataView(buf).setFloat64(0, val, true); const bytes = new Uint8Array(buf); + let h = hash; for (let i = 0; i < 8; i++) { - hash = fnvByte(hash, bytes[i]!); + h = fnvByte(h, bytes[i] ?? 0); } - return hash; + return h; } if (typeof val === "bigint") { return fnvString(hash, val.toString()); @@ -128,10 +130,7 @@ export interface HashPandasObjectOptions { * h.iat(0) !== h.iat(1); // true (with overwhelming probability) * ``` */ -export function hashPandasObject( - obj: Series, - options?: HashPandasObjectOptions, -): Series; +export function hashPandasObject(obj: Series, options?: HashPandasObjectOptions): Series; /** * Return a `Series` of FNV-1a 64-bit row-hashes for each row of `df`. @@ -150,10 +149,7 @@ export function hashPandasObject( * // h.iat(0) is the hash of row 0; h.iat(1) is the hash of row 1 * ``` */ -export function hashPandasObject( - obj: DataFrame, - options?: HashPandasObjectOptions, -): Series; +export function hashPandasObject(obj: DataFrame, options?: HashPandasObjectOptions): Series; export function hashPandasObject( obj: Series | DataFrame, diff --git a/tests/stats/hash_pandas_object.test.ts b/tests/stats/hash_pandas_object.test.ts index 3e26e4cf..7c0d0f67 100644 --- a/tests/stats/hash_pandas_object.test.ts +++ b/tests/stats/hash_pandas_object.test.ts @@ -106,18 +106,21 @@ describe("hashPandasObject β€” Series", () => { it("property: deterministic β€” same input same hash", () => { fc.assert( - fc.property(fc.array(fc.oneof(fc.integer(), fc.string(), fc.boolean()), { maxLength: 20 }), (arr) => { - const s1 = new Series({ data: arr }); - const s2 = new Series({ data: arr }); - const h1 = hashPandasObject(s1, { index: false }); - const h2 = hashPandasObject(s2, { index: false }); - for (let i = 0; i < arr.length; i++) { - if (h1.iat(i) !== h2.iat(i)) { - return false; + fc.property( + fc.array(fc.oneof(fc.integer(), fc.string(), fc.boolean()), { maxLength: 20 }), + (arr) => { + const s1 = new Series({ data: arr }); + const s2 = new Series({ data: arr }); + const h1 = hashPandasObject(s1, { index: false }); + const h2 = hashPandasObject(s2, { index: false }); + for (let i = 0; i < arr.length; i++) { + if (h1.iat(i) !== h2.iat(i)) { + return false; + } } - } - return true; - }), + return true; + }, + ), ); }); }); From 7193c021ff3d52495989236d25f292bceb90e516 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 23:26:38 +0000 Subject: [PATCH 4/4] Fix playground/hash_pandas_object.html to conform to interactive playground standards Rewrite the static documentation page as a fully interactive playground matching the structure required by the conformance tests in tests/playground.test.ts: - Add #playground-loading overlay - Add dark-theme CSS variables (--bg, --accent, etc.) - Add .playground-block containers with .playground-editor, .playground-run button, and .playground-output elements - Load playground-runtime.js as ES module Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- playground/hash_pandas_object.html | 371 +++++++++++++++++++++++++---- 1 file changed, 321 insertions(+), 50 deletions(-) diff --git a/playground/hash_pandas_object.html b/playground/hash_pandas_object.html index 972d0fc1..134212d7 100644 --- a/playground/hash_pandas_object.html +++ b/playground/hash_pandas_object.html @@ -1,42 +1,248 @@ - + - - - - tsb Β· hashPandasObject - - - -

πŸ“¦ hashPandasObject

+ + + + tsb β€” hashPandasObject Playground + + + + + +
+
+
Initializing playground…
+
+ + ← Back to roadmap +

πŸ“¦ hashPandasObject β€” Interactive Playground

+

+ hashPandasObject(obj) computes FNV-1a 64-bit hash values for each element + of a Series or each row of a DataFrame β€” mirroring + pandas.util.hash_pandas_object.
+ Edit any code block below and press β–Ά Run + (or Ctrl+Enter) to execute it live in your browser. +

+ + +
+

1 Β· Series hashing

- Compute FNV-1a 64-bit hash values for each element of a - Series or each row of a DataFrame. - Mirrors pandas.util.hash_pandas_object. + Hash each element of a Series. Identical values produce identical hashes; + pass { index: false } to ignore the index label when computing the hash.

- -

Series hashing

-
import { Series, hashPandasObject } from "tsb";
+    
+
+ TypeScript +
+ + +
+
+
import { Series, hashPandasObject } from "tsb";
 
 const s = new Series({ data: ["apple", "banana", "apple"], index: [0, 1, 2] });
 const h = hashPandasObject(s, { index: false });
 
 // Same value β†’ same hash
-console.log(h.iat(0) === h.iat(2)); // true  (both "apple")
-console.log(h.iat(0) === h.iat(1)); // false ("apple" β‰  "banana")
-
+console.log("apple===apple:", h.iat(0) === h.iat(2)); // true +console.log("apple===banana:", h.iat(0) === h.iat(1)); // false +console.log("hashes:", [...h.values]);
+
Click β–Ά Run to execute
+
Ctrl+Enter to run
+
+
-

DataFrame row hashing

-
import { DataFrame, hashPandasObject } from "tsb";
+  
+  
+

2 Β· DataFrame row hashing

+

+ Hash each row of a DataFrame. Rows with identical values across all columns + produce the same hash, making this useful for deduplication and change detection. +

+
+
+ TypeScript +
+ + +
+
+
import { DataFrame, hashPandasObject } from "tsb";
 
 const df = new DataFrame({
   id:   [1, 2, 3],
@@ -46,12 +252,29 @@ 

DataFrame row hashing

const rowHashes = hashPandasObject(df, { index: false }); // Rows 0 and 2 are identical β†’ same hash -console.log(rowHashes.iat(0) === rowHashes.iat(2)); // true -console.log(rowHashes.iat(0) === rowHashes.iat(1)); // false -
+console.log("row0===row2:", rowHashes.iat(0) === rowHashes.iat(2)); // true +console.log("row0===row1:", rowHashes.iat(0) === rowHashes.iat(1)); // false
+
Click β–Ά Run to execute
+
Ctrl+Enter to run
+
+ -

Deduplication with hashes

-
import { DataFrame, hashPandasObject } from "tsb";
+  
+  
+

3 Β· Deduplication with hashes

+

+ Use row hashes to find unique rows efficiently β€” a common pattern when + duplicated() is too slow on large DataFrames. +

+
+
+ TypeScript +
+ + +
+
+
import { DataFrame, hashPandasObject } from "tsb";
 
 const df = new DataFrame({
   a: [1, 2, 1, 3],
@@ -59,10 +282,10 @@ 

Deduplication with hashes

}); const hashes = hashPandasObject(df, { index: false }); -const seen = new Set<number>(); +const seen = new Set(); const uniqueRows: number[] = []; -for (let i = 0; i < df.shape[0]; i++) { +for (let i = 0; i < df.shape[0]; i++) { const h = hashes.iat(i); if (!seen.has(h)) { seen.add(h); @@ -70,29 +293,77 @@

Deduplication with hashes

} } // uniqueRows = [0, 1, 3] β€” row 2 is a duplicate of row 0 -console.log(uniqueRows); -
+console.log("unique row indices:", uniqueRows);
+
Click β–Ά Run to execute
+
Ctrl+Enter to run
+ + -

Controlling index inclusion

-
import { Series, hashPandasObject } from "tsb";
+  
+  
+

4 Β· Controlling index inclusion

+

+ By default (index: true), the index label is mixed into the hash. + Set index: false to hash only the values. +

+
+
+ TypeScript +
+ + +
+
+
import { Series, hashPandasObject } from "tsb";
 
 const s = new Series({ data: [42, 42], index: ["a", "b"] });
 
 // index=true (default): different index β†’ different hash
 const withIdx = hashPandasObject(s, { index: true });
-console.log(withIdx.iat(0) === withIdx.iat(1)); // false
+console.log("index=true, iat(0)===iat(1):", withIdx.iat(0) === withIdx.iat(1)); // false
 
 // index=false: only values matter
 const noIdx = hashPandasObject(s, { index: false });
-console.log(noIdx.iat(0) === noIdx.iat(1)); // true
-
+console.log("index=false, iat(0)===iat(1):", noIdx.iat(0) === noIdx.iat(1)); // true
+
Click β–Ά Run to execute
+
Ctrl+Enter to run
+ + + + +
+

πŸ§ͺ Scratch Pad

+

Write your own hashPandasObject code below. All exports from tsb are available.

+
+
+ TypeScript β€” Scratch Pad +
+ + +
+
+
import { Series, DataFrame, hashPandasObject } from "tsb";
+
+// Try it! Hash a Series of numbers.
+const nums = new Series({ data: [10, 20, 10, 30] });
+const hashes = hashPandasObject(nums, { index: false });
 
-    
- Algorithm: FNV-1a 64-bit (Fowler–Noll–Vo), a fast non-cryptographic hash - chosen for its excellent avalanche properties on short inputs. Results are stored as - float64 numbers (the 64-bit bit-pattern cast via Number(BigInt)). +console.log("10===10:", hashes.iat(0) === hashes.iat(2)); +console.log("10===20:", hashes.iat(0) === hashes.iat(1)); +console.log("all hashes:", [...hashes.values]);
+
Click β–Ά Run to execute
+
Ctrl+Enter to run
+
+ + -

← Back to tsb playground

- + + +