diff --git a/playground/errors.html b/playground/errors.html
new file mode 100644
index 00000000..503c0515
--- /dev/null
+++ b/playground/errors.html
@@ -0,0 +1,303 @@
+
+
+
+
+
+ tsb — pd.errors
+
+
+
+
+
+
+
Initializing playground…
+
+
+ ← Back to roadmap
+ pd.errors
+ Pandas-compatible error and warning classes — mirrors Python's pd.errors module.
+ All classes extend native Error and integrate with try/catch and instanceof.
+
+
+
1 — Base classes: ValueError, KeyError, IndexError
+
Three base classes mirror Python's built-in exceptions. They extend native JS error types so they
+ work with standard error-handling idioms.
+
+
+
+
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
+
+
+
+
2 — Catching specific errors with instanceof
+
Use instanceof in catch blocks to handle specific error types — just like Python's
+ except SpecificError.
+
+
+
+
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
+
+
+
+
3 — The errors namespace (pd.errors style)
+
All error classes are grouped under the errors namespace, mirroring
+ Python's pd.errors.ParserError etc.
+
+
+
+
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
+
+
+
+
4 — AbstractMethodError for extension classes
+
AbstractMethodError is thrown when a subclass forgets to implement a required method —
+ mirroring Python's raise NotImplementedError.
+
+
+
+
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
+
+
+
+
+
+
+
diff --git a/src/core/align.ts b/src/core/align.ts
index 144f53b5..ebda596e 100644
--- a/src/core/align.ts
+++ b/src/core/align.ts
@@ -92,6 +92,8 @@ function resolveIndex(left: Index, right: Index, join: JoinHow): I
return left;
case "right":
return right;
+ default:
+ return left.union(right);
}
}
@@ -161,8 +163,14 @@ export function alignDataFrame(
const { join = "outer", fillValue = null, axis } = options;
// Normalise axis: null/undefined → align both
- const normalised: 0 | 1 | null =
- axis === null || axis === undefined ? null : axis === 0 || axis === "index" ? 0 : 1;
+ let normalised: 0 | 1 | null;
+ if (axis === null || axis === undefined) {
+ normalised = null;
+ } else if (axis === 0 || axis === "index") {
+ normalised = 0;
+ } else {
+ normalised = 1;
+ }
const alignRows = normalised === null || normalised === 0;
const alignCols = normalised === null || normalised === 1;
diff --git a/src/core/astype.ts b/src/core/astype.ts
index 572352c4..3359eb2a 100644
--- a/src/core/astype.ts
+++ b/src/core/astype.ts
@@ -31,6 +31,63 @@ const INT_RANGES: Readonly= 0) {
dist[i] = i - lastIdx;
val[i] = values[lastIdx];
@@ -183,7 +185,9 @@ function buildRightNearest(
const val: Scalar[] = new Array(n).fill(null);
let nextIdx = -1;
for (let i = n - 1; i >= 0; i--) {
- if (present[i]) nextIdx = i;
+ if (present[i]) {
+ nextIdx = i;
+ }
if (nextIdx >= 0) {
dist[i] = nextIdx - i;
val[i] = values[nextIdx];
@@ -193,9 +197,15 @@ function buildRightNearest(
}
function pickNearest(ld: number, rd: number, leftVal: Scalar, rightVal: Scalar): Scalar {
- if (ld === -1 && rd === -1) return null;
- if (ld === -1) return rightVal;
- if (rd === -1) return leftVal;
+ if (ld === -1 && rd === -1) {
+ return null;
+ }
+ if (ld === -1) {
+ return rightVal;
+ }
+ if (rd === -1) {
+ return leftVal;
+ }
return rd <= ld ? rightVal : leftVal; // prefer right on tie
}
diff --git a/src/core/sample.ts b/src/core/sample.ts
index 76d87ad9..15431117 100644
--- a/src/core/sample.ts
+++ b/src/core/sample.ts
@@ -70,10 +70,10 @@ function lcgNext(seed: number): [number, number] {
/** Build a seeded random float generator that returns [0,1). */
function makeRng(seed: number | undefined): () => number {
if (seed === undefined) {
- return () => Math.random();
+ return (): number => Math.random();
}
let s = seed >>> 0; // ensure 32-bit unsigned
- return () => {
+ return (): number => {
const [ns, r] = lcgNext(s);
s = ns;
return r;
diff --git a/src/core/searchsorted.ts b/src/core/searchsorted.ts
index e00333db..bfc91a0b 100644
--- a/src/core/searchsorted.ts
+++ b/src/core/searchsorted.ts
@@ -55,6 +55,33 @@ function isMissing(v: Scalar): v is null | undefined {
return v === null || v === undefined;
}
+/** Compare two ordered (same-type) values: returns -1, 0, or 1. */
+function compareOrdered(a: T, b: T): number {
+ if (a < b) {
+ return -1;
+ }
+ if (a > b) {
+ return 1;
+ }
+ return 0;
+}
+
+/** Compare two numbers, treating NaN as greater than everything. */
+function compareNumbers(a: number, b: number): number {
+ const aNaN = Number.isNaN(a);
+ const bNaN = Number.isNaN(b);
+ if (aNaN && bNaN) {
+ return 0;
+ }
+ if (aNaN) {
+ return 1;
+ }
+ if (bNaN) {
+ return -1;
+ }
+ return a - b;
+}
+
/**
* Default scalar comparator.
*
@@ -78,23 +105,11 @@ function defaultCompare(a: Scalar, b: Scalar): number {
// Same type fast-paths
if (typeof a === "number" && typeof b === "number") {
- // NaN sorts last (treat NaN as greater than everything)
- const aNaN = Number.isNaN(a);
- const bNaN = Number.isNaN(b);
- if (aNaN && bNaN) {
- return 0;
- }
- if (aNaN) {
- return 1;
- }
- if (bNaN) {
- return -1;
- }
- return a - b;
+ return compareNumbers(a, b);
}
if (typeof a === "bigint" && typeof b === "bigint") {
- return a < b ? -1 : a > b ? 1 : 0;
+ return compareOrdered(a, b);
}
if (a instanceof Date && b instanceof Date) {
@@ -102,7 +117,7 @@ function defaultCompare(a: Scalar, b: Scalar): number {
}
if (typeof a === "string" && typeof b === "string") {
- return a < b ? -1 : a > b ? 1 : 0;
+ return compareOrdered(a, b);
}
if (typeof a === "boolean" && typeof b === "boolean") {
@@ -110,9 +125,7 @@ function defaultCompare(a: Scalar, b: Scalar): number {
}
// Cross-type fallback
- const sa = String(a);
- const sb = String(b);
- return sa < sb ? -1 : sa > sb ? 1 : 0;
+ return compareOrdered(String(a), String(b));
}
/**
@@ -235,7 +248,9 @@ export function searchsortedMany(
const { side = "left", sorter, compareFn = defaultCompare } = options;
const n = a.length;
const get: (i: number) => Scalar =
- sorter !== undefined ? (i) => valueAtSorted(a, sorter, i) : (i) => valueAt(a, i);
+ sorter !== undefined
+ ? (i: number): Scalar => valueAtSorted(a, sorter, i)
+ : (i: number): Scalar => valueAt(a, i);
return vs.map((v) => bisect(n, get, v, side, compareFn));
}
diff --git a/src/core/series.ts b/src/core/series.ts
index ea6e19e5..29063e91 100644
--- a/src/core/series.ts
+++ b/src/core/series.ts
@@ -9,12 +9,12 @@
import { SeriesGroupBy } from "../groupby/index.ts";
import type { Label, Scalar } from "../types.ts";
-import { EWM } from "../window/ewm.ts";
-import type { EwmOptions } from "../window/ewm.ts";
-import { Expanding } from "../window/expanding.ts";
-import type { ExpandingOptions } from "../window/expanding.ts";
-import { Rolling } from "../window/rolling.ts";
-import type { RollingOptions } from "../window/rolling.ts";
+import { EWM } from "../window/index.ts";
+import type { EwmOptions } from "../window/index.ts";
+import { Expanding } from "../window/index.ts";
+import type { ExpandingOptions } from "../window/index.ts";
+import { Rolling } from "../window/index.ts";
+import type { RollingOptions } from "../window/index.ts";
import { Index } from "./base-index.ts";
import { CategoricalAccessor } from "./cat_accessor.ts";
import type { CatSeriesLike } from "./cat_accessor.ts";
diff --git a/src/core/timedelta_range.ts b/src/core/timedelta_range.ts
index c639ea3a..4b4a5733 100644
--- a/src/core/timedelta_range.ts
+++ b/src/core/timedelta_range.ts
@@ -219,53 +219,69 @@ export function timedelta_range(options: TimedeltaRangeOptions): TimedeltaIndex
const hasFreq = options.freq !== undefined;
const hasPeriods = periods !== undefined;
- // Validate: at least two of the four parameters must be provided
const given = [hasStart, hasEnd, hasPeriods, hasFreq].filter(Boolean).length;
if (given < 2) {
throw new Error(
"timedelta_range: must specify at least two of 'start', 'end', 'periods', 'freq'",
);
}
+ if (hasPeriods && periods !== undefined && periods < 0) {
+ throw new RangeError("timedelta_range: periods must be non-negative");
+ }
- let values: number[];
const startMs = hasStart ? toMs(options.start as Timedelta | string | number) : null;
const endMs = hasEnd ? toMs(options.end as Timedelta | string | number) : null;
+ const values = buildValues(
+ options,
+ hasStart,
+ hasEnd,
+ hasFreq,
+ hasPeriods,
+ startMs,
+ endMs,
+ periods,
+ );
- if (hasPeriods && periods !== undefined && periods < 0) {
- throw new RangeError("timedelta_range: periods must be non-negative");
- }
+ const filtered = applyClosedFilter(values, startMs, endMs, closed);
+ const deltas = filtered.map((ms) => Timedelta.fromMilliseconds(ms));
+ return TimedeltaIndex.fromTimedeltas(deltas, { name });
+}
+function buildValues(
+ options: TimedeltaRangeOptions,
+ hasStart: boolean,
+ hasEnd: boolean,
+ hasFreq: boolean,
+ hasPeriods: boolean,
+ startMs: number | null,
+ endMs: number | null,
+ periods: number | undefined,
+): number[] {
if (hasStart && hasEnd && !hasFreq && hasPeriods && periods !== undefined) {
- // Linear spacing between start and end with exactly `periods` points
- values = buildLinear(startMs as number, endMs as number, periods);
- } else if (hasStart && hasEnd && hasFreq) {
- // Build from start to end stepping by freq
+ return buildLinear(startMs as number, endMs as number, periods);
+ }
+ if (hasStart && hasEnd && hasFreq) {
const stepMs = freqToMs(options.freq as TimedeltaFreq | number);
- values = buildStartEnd(startMs as number, endMs as number, stepMs);
- } else if (hasStart && hasFreq && hasPeriods && periods !== undefined) {
- // Build forward from start for `periods` items
+ return buildStartEnd(startMs as number, endMs as number, stepMs);
+ }
+ if (hasStart && hasFreq && hasPeriods && periods !== undefined) {
const stepMs = freqToMs(options.freq as TimedeltaFreq | number);
- values = buildStartPeriods(startMs as number, stepMs, periods);
- } else if (hasEnd && hasFreq && hasPeriods && periods !== undefined) {
- // Build backward from end for `periods` items
+ return buildStartPeriods(startMs as number, stepMs, periods);
+ }
+ if (hasEnd && hasFreq && hasPeriods && periods !== undefined) {
const stepMs = freqToMs(options.freq as TimedeltaFreq | number);
- values = buildEndPeriods(endMs as number, stepMs, periods);
- } else if (hasStart && hasEnd && !hasFreq && !hasPeriods) {
- // Only start and end given — include both endpoints (single step if equal)
- values = startMs === endMs ? [startMs as number] : [startMs as number, endMs as number];
- } else if (hasStart && hasPeriods && !hasFreq && periods !== undefined) {
- // start + periods with no freq: default to 1-day step
- values = buildStartPeriods(startMs as number, 86_400_000, periods);
- } else {
- throw new Error(
- "timedelta_range: unsupported combination of parameters — " +
- "provide start+end+freq, start+periods+freq, end+periods+freq, or start+end+periods",
- );
+ return buildEndPeriods(endMs as number, stepMs, periods);
}
-
- const filtered = applyClosedFilter(values, startMs, endMs, closed);
- const deltas = filtered.map((ms) => Timedelta.fromMilliseconds(ms));
- return TimedeltaIndex.fromTimedeltas(deltas, { name });
+ if (hasStart && hasEnd && !hasFreq && !hasPeriods) {
+ return startMs === endMs ? [startMs as number] : [startMs as number, endMs as number];
+ }
+ if (hasStart && hasPeriods && !hasFreq && periods !== undefined) {
+ return buildStartPeriods(startMs as number, 86_400_000, periods);
+ }
+ throw new Error(
+ "timedelta_range: unsupported combination of parameters — " +
+ "provide start+end+freq, start+periods+freq, end+periods+freq, or start+end+periods",
+ );
}
// ─── internal builders ────────────────────────────────────────────────────────
diff --git a/src/errors.ts b/src/errors.ts
new file mode 100644
index 00000000..4ea24681
--- /dev/null
+++ b/src/errors.ts
@@ -0,0 +1,265 @@
+/**
+ * `pd.errors` — pandas-compatible error and warning classes.
+ *
+ * Provides the full hierarchy of exceptions and warnings that pandas raises,
+ * adapted to TypeScript idioms. All classes extend native Error (or its
+ * subclasses) so they integrate naturally with `try/catch` and `instanceof`.
+ *
+ * @packageDocumentation
+ */
+
+// ---------------------------------------------------------------------------
+// Base error helpers (must be declared first so derived classes can extend them)
+// ---------------------------------------------------------------------------
+
+/** TypeError-compatible base for value-related errors (mirrors Python's ValueError). */
+export class ValueError extends TypeError {
+ override readonly name: string = "ValueError";
+}
+
+/** Error-compatible base for key-related errors (mirrors Python's KeyError). */
+export class KeyError extends Error {
+ override readonly name: string = "KeyError";
+}
+
+/** Error-compatible base for index-related errors (mirrors Python's IndexError). */
+export class IndexError extends RangeError {
+ override readonly name: string = "IndexError";
+}
+
+// ---------------------------------------------------------------------------
+// Error classes
+// ---------------------------------------------------------------------------
+
+/** Raised when an abstract method is called that subclasses must override. */
+export class AbstractMethodError extends Error {
+ override readonly name = "AbstractMethodError";
+ constructor(classOrMethod: string) {
+ super(`This method must be defined in the concrete class: ${classOrMethod}`);
+ }
+}
+
+/** Raised when there is a conflicting attribute during attribute access. */
+export class AttributeConflictWarning extends Error {
+ override readonly name = "AttributeConflictWarning";
+}
+
+/** Raised when CSS stylesheet parsing encounters a problem. */
+export class CSSWarning extends Error {
+ override readonly name = "CSSWarning";
+}
+
+/**
+ * Raised when chained assignment is detected.
+ * Equivalent to pandas `ChainedAssignmentError`.
+ */
+export class ChainedAssignmentError extends Error {
+ override readonly name = "ChainedAssignmentError";
+ constructor(message = "A value is trying to be set on a copy of a slice from a DataFrame") {
+ super(message);
+ }
+}
+
+/** Raised when there is a database-related error. */
+export class DatabaseError extends Error {
+ override readonly name = "DatabaseError";
+}
+
+/** Raised when a groupby aggregate operation encounters an error with the data. */
+export class DataError extends Error {
+ override readonly name = "DataError";
+}
+
+/**
+ * Warning raised when reading a file with mismatched dtypes.
+ * Equivalent to pandas `DtypeWarning`.
+ */
+export class DtypeWarning extends Error {
+ override readonly name = "DtypeWarning";
+}
+
+/** Raised when attempting to read an empty file. */
+export class EmptyDataError extends Error {
+ override readonly name = "EmptyDataError";
+ constructor(message = "No columns to parse from file") {
+ super(message);
+ }
+}
+
+/** Raised when casting to integer would lose data due to NaN values. */
+export class IntCastingNaNError extends Error {
+ override readonly name = "IntCastingNaNError";
+ constructor(message = "Cannot convert non-finite values (NA or inf) to integer") {
+ super(message);
+ }
+}
+
+/** Raised when an invalid column name is used. */
+export class InvalidColumnName extends Error {
+ override readonly name = "InvalidColumnName";
+}
+
+/** Raised when a comparison is attempted with incompatible types. */
+export class InvalidComparison extends TypeError {
+ override readonly name = "InvalidComparison";
+}
+
+/** Raised when an invalid label is used for indexing. */
+export class InvalidIndexError extends Error {
+ override readonly name = "InvalidIndexError";
+ constructor(message = "label not found in index") {
+ super(message);
+ }
+}
+
+/** Raised when a boolean index with an invalid shape is used. */
+export class InvalidUseOfBooleanIndex extends IndexError {
+ override readonly name = "InvalidUseOfBooleanIndex";
+}
+
+/** Raised when a version string cannot be parsed. */
+export class InvalidVersion extends ValueError {
+ override readonly name = "InvalidVersion";
+}
+
+/** Raised when a setitem operation would silently lose information. */
+export class LossySetitemError extends Error {
+ override readonly name = "LossySetitemError";
+}
+
+/** Raised when a merge operation is performed incorrectly. */
+export class MergeError extends ValueError {
+ override readonly name = "MergeError";
+}
+
+/** Raised when an operation requires a non-null frequency but the frequency is null. */
+export class NullFrequencyError extends ValueError {
+ override readonly name = "NullFrequencyError";
+ constructor(message = "Cannot have a null frequency with a non-trivial period index") {
+ super(message);
+ }
+}
+
+/** Raised when a Numba utility encounters an error. */
+export class NumbaUtilError extends Error {
+ override readonly name = "NumbaUtilError";
+}
+
+/** Raised when an invalid option is encountered. */
+export class OptionError extends KeyError {
+ override readonly name = "OptionError";
+}
+
+/** Raised when a datetime value is out of the supported range. */
+export class OutOfBoundsDatetime extends ValueError {
+ override readonly name = "OutOfBoundsDatetime";
+ constructor(message = "Out of bounds nanosecond timestamp") {
+ super(message);
+ }
+}
+
+/** Raised when a timedelta value is out of the supported range. */
+export class OutOfBoundsTimedelta extends ValueError {
+ override readonly name = "OutOfBoundsTimedelta";
+ constructor(message = "Out of bounds timedelta") {
+ super(message);
+ }
+}
+
+/** Raised when a file or string cannot be parsed. */
+export class ParserError extends ValueError {
+ override readonly name = "ParserError";
+}
+
+/** Warning raised when a parser falls back to a less-efficient parser. */
+export class ParserWarning extends Error {
+ override readonly name = "ParserWarning";
+}
+
+/** Warning raised when a performance issue is detected. */
+export class PerformanceWarning extends Error {
+ override readonly name = "PerformanceWarning";
+}
+
+/** Raised when writing to a file may result in data loss. */
+export class PossibleDataLossError extends Error {
+ override readonly name = "PossibleDataLossError";
+}
+
+/** Warning raised when floating-point precision loss may occur. */
+export class PossiblePrecisionLoss extends Error {
+ override readonly name = "PossiblePrecisionLoss";
+}
+
+/** Raised when there is a specification error in a groupby agg call. */
+export class SpecificationError extends ValueError {
+ override readonly name = "SpecificationError";
+}
+
+/** Raised when an unsorted MultiIndex is used in a way that requires sorting. */
+export class UnsortedIndexError extends KeyError {
+ override readonly name = "UnsortedIndexError";
+ constructor(message = "MultiIndex slicing requires the index to be lexsorted") {
+ super(message);
+ }
+}
+
+/** Raised when calling a function on an object that does not support it. */
+export class UnsupportedFunctionCall extends ValueError {
+ override readonly name = "UnsupportedFunctionCall";
+}
+
+/** Warning raised when accessor registration may shadow a built-in attribute. */
+export class AccessorRegistrationWarning extends Error {
+ override readonly name = "AccessorRegistrationWarning";
+}
+
+/** Raised when a value and label have mismatched types in a Categorical. */
+export class ValueLabelTypeMismatch extends Error {
+ override readonly name = "ValueLabelTypeMismatch";
+}
+
+// ---------------------------------------------------------------------------
+// Namespace export (mirrors `pd.errors`)
+// ---------------------------------------------------------------------------
+
+/** All pandas-compatible error and warning classes, grouped as `pd.errors`. */
+export const errors = {
+ AbstractMethodError,
+ AccessorRegistrationWarning,
+ AttributeConflictWarning,
+ CSSWarning,
+ ChainedAssignmentError,
+ DatabaseError,
+ DataError,
+ DtypeWarning,
+ EmptyDataError,
+ IntCastingNaNError,
+ InvalidColumnName,
+ InvalidComparison,
+ InvalidIndexError,
+ InvalidUseOfBooleanIndex,
+ InvalidVersion,
+ LossySetitemError,
+ MergeError,
+ NullFrequencyError,
+ NumbaUtilError,
+ OptionError,
+ OutOfBoundsDatetime,
+ OutOfBoundsTimedelta,
+ ParserError,
+ ParserWarning,
+ PerformanceWarning,
+ PossibleDataLossError,
+ PossiblePrecisionLoss,
+ SpecificationError,
+ UnsortedIndexError,
+ UnsupportedFunctionCall,
+ ValueLabelTypeMismatch,
+ // base classes
+ ValueError,
+ KeyError,
+ IndexError,
+} as const;
+
+export type PandasError = InstanceType<(typeof errors)[keyof typeof errors]>;
diff --git a/src/index.ts b/src/index.ts
index a6180397..f1b07645 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -741,3 +741,43 @@ export {
seriesToLaTeX,
} from "./stats/format_table.ts";
export type { ToMarkdownOptions, ToLaTeXOptions } from "./stats/format_table.ts";
+
+// pd.errors — pandas-compatible error and warning classes
+export {
+ errors,
+ AbstractMethodError,
+ AccessorRegistrationWarning,
+ AttributeConflictWarning,
+ CSSWarning,
+ ChainedAssignmentError,
+ DatabaseError,
+ DataError,
+ DtypeWarning,
+ EmptyDataError,
+ IntCastingNaNError,
+ InvalidColumnName,
+ InvalidComparison,
+ InvalidIndexError,
+ InvalidUseOfBooleanIndex,
+ InvalidVersion,
+ LossySetitemError,
+ MergeError,
+ NullFrequencyError,
+ NumbaUtilError,
+ OptionError,
+ OutOfBoundsDatetime,
+ OutOfBoundsTimedelta,
+ ParserError,
+ ParserWarning,
+ PerformanceWarning,
+ PossibleDataLossError,
+ PossiblePrecisionLoss,
+ SpecificationError,
+ UnsortedIndexError,
+ UnsupportedFunctionCall,
+ ValueLabelTypeMismatch,
+ ValueError,
+ KeyError,
+ IndexError,
+} from "./errors.ts";
+export type { PandasError } from "./errors.ts";
diff --git a/tests/errors.test.ts b/tests/errors.test.ts
new file mode 100644
index 00000000..980d5724
--- /dev/null
+++ b/tests/errors.test.ts
@@ -0,0 +1,331 @@
+/**
+ * Tests for pd.errors — pandas-compatible error and warning classes.
+ */
+import { describe, expect, test } from "bun:test";
+import {
+ AbstractMethodError,
+ AccessorRegistrationWarning,
+ AttributeConflictWarning,
+ CSSWarning,
+ ChainedAssignmentError,
+ DataError,
+ DatabaseError,
+ DtypeWarning,
+ EmptyDataError,
+ IndexError,
+ IntCastingNaNError,
+ InvalidColumnName,
+ InvalidComparison,
+ InvalidIndexError,
+ InvalidUseOfBooleanIndex,
+ InvalidVersion,
+ KeyError,
+ LossySetitemError,
+ MergeError,
+ NullFrequencyError,
+ NumbaUtilError,
+ OptionError,
+ OutOfBoundsDatetime,
+ OutOfBoundsTimedelta,
+ ParserError,
+ ParserWarning,
+ PerformanceWarning,
+ PossibleDataLossError,
+ PossiblePrecisionLoss,
+ SpecificationError,
+ UnsortedIndexError,
+ UnsupportedFunctionCall,
+ ValueError,
+ ValueLabelTypeMismatch,
+ errors,
+} from "../src/index.ts";
+
+// ---------------------------------------------------------------------------
+// Helper: assert that a class is throwable and catchable
+// ---------------------------------------------------------------------------
+function assertThrowable(
+ Cls: new (...args: A) => T,
+ args: A,
+ expectedMessage?: string,
+): void {
+ const instance = new Cls(...args);
+ expect(instance).toBeInstanceOf(Error);
+ expect(instance).toBeInstanceOf(Cls);
+ if (expectedMessage !== undefined) {
+ expect(instance.message).toBe(expectedMessage);
+ }
+ // Verify it can be caught with instanceof
+ let caught: unknown;
+ try {
+ throw instance;
+ } catch (e) {
+ caught = e;
+ }
+ expect(caught).toBeInstanceOf(Cls);
+}
+
+// ---------------------------------------------------------------------------
+// Base classes
+// ---------------------------------------------------------------------------
+
+describe("ValueError", () => {
+ test("is a TypeError", () => {
+ const e = new ValueError("bad value");
+ expect(e).toBeInstanceOf(TypeError);
+ expect(e).toBeInstanceOf(ValueError);
+ expect(e.name).toBe("ValueError");
+ expect(e.message).toBe("bad value");
+ });
+});
+
+describe("KeyError", () => {
+ test("is an Error", () => {
+ const e = new KeyError("missing key");
+ expect(e).toBeInstanceOf(Error);
+ expect(e.name).toBe("KeyError");
+ expect(e.message).toBe("missing key");
+ });
+});
+
+describe("IndexError", () => {
+ test("is a RangeError", () => {
+ const e = new IndexError("out of bounds");
+ expect(e).toBeInstanceOf(RangeError);
+ expect(e.name).toBe("IndexError");
+ });
+});
+
+// ---------------------------------------------------------------------------
+// All error classes
+// ---------------------------------------------------------------------------
+
+describe("AbstractMethodError", () => {
+ test("message includes class name", () => {
+ const e = new AbstractMethodError("MyClass.myMethod");
+ expect(e.message).toContain("MyClass.myMethod");
+ expect(e).toBeInstanceOf(Error);
+ expect(e.name).toBe("AbstractMethodError");
+ });
+ test("instanceof check works", () => assertThrowable(AbstractMethodError, ["Foo"]));
+});
+
+describe("AccessorRegistrationWarning", () => {
+ test("throwable", () => {
+ const e = new AccessorRegistrationWarning("shadowing built-in");
+ expect(e.name).toBe("AccessorRegistrationWarning");
+ expect(e.message).toBe("shadowing built-in");
+ });
+});
+
+describe("AttributeConflictWarning", () => {
+ test("throwable", () => assertThrowable(AttributeConflictWarning, ["conflict"]));
+});
+
+describe("CSSWarning", () => {
+ test("name is CSSWarning", () => {
+ expect(new CSSWarning("bad CSS").name).toBe("CSSWarning");
+ });
+});
+
+describe("ChainedAssignmentError", () => {
+ test("default message", () => {
+ const e = new ChainedAssignmentError();
+ expect(e.message).toContain("copy of a slice");
+ expect(e.name).toBe("ChainedAssignmentError");
+ });
+ test("custom message", () => {
+ const e = new ChainedAssignmentError("custom");
+ expect(e.message).toBe("custom");
+ });
+});
+
+describe("DatabaseError", () => {
+ test("throwable", () => assertThrowable(DatabaseError, ["DB error"]));
+});
+
+describe("DataError", () => {
+ test("throwable", () => assertThrowable(DataError, ["data error"]));
+});
+
+describe("DtypeWarning", () => {
+ test("throwable", () => assertThrowable(DtypeWarning, ["dtype warning"]));
+});
+
+describe("EmptyDataError", () => {
+ test("default message", () => {
+ expect(new EmptyDataError().message).toContain("No columns");
+ });
+ test("custom message", () => {
+ const e = new EmptyDataError("custom");
+ expect(e.message).toBe("custom");
+ });
+ test("instanceof", () => assertThrowable(EmptyDataError, []));
+});
+
+describe("IntCastingNaNError", () => {
+ test("default message", () => {
+ expect(new IntCastingNaNError().message).toContain("non-finite");
+ });
+ test("instanceof", () => assertThrowable(IntCastingNaNError, []));
+});
+
+describe("InvalidColumnName", () => {
+ test("throwable", () => assertThrowable(InvalidColumnName, ["bad col"]));
+});
+
+describe("InvalidComparison", () => {
+ test("is TypeError", () => {
+ const e = new InvalidComparison("bad compare");
+ expect(e).toBeInstanceOf(TypeError);
+ expect(e.name).toBe("InvalidComparison");
+ });
+});
+
+describe("InvalidIndexError", () => {
+ test("default message", () => {
+ expect(new InvalidIndexError().message).toContain("label not found");
+ });
+ test("instanceof", () => assertThrowable(InvalidIndexError, []));
+});
+
+describe("InvalidUseOfBooleanIndex", () => {
+ test("is IndexError", () => {
+ const e = new InvalidUseOfBooleanIndex("bad bool");
+ expect(e).toBeInstanceOf(IndexError);
+ expect(e.name).toBe("InvalidUseOfBooleanIndex");
+ });
+});
+
+describe("InvalidVersion", () => {
+ test("is ValueError", () => {
+ const e = new InvalidVersion("bad ver");
+ expect(e).toBeInstanceOf(ValueError);
+ expect(e.name).toBe("InvalidVersion");
+ });
+});
+
+describe("LossySetitemError", () => {
+ test("throwable", () => assertThrowable(LossySetitemError, ["lossy"]));
+});
+
+describe("MergeError", () => {
+ test("is ValueError", () => {
+ const e = new MergeError("merge fail");
+ expect(e).toBeInstanceOf(ValueError);
+ expect(e.name).toBe("MergeError");
+ });
+});
+
+describe("NullFrequencyError", () => {
+ test("default message", () => {
+ expect(new NullFrequencyError().message).toContain("null frequency");
+ });
+ test("is ValueError", () => {
+ expect(new NullFrequencyError()).toBeInstanceOf(ValueError);
+ });
+});
+
+describe("NumbaUtilError", () => {
+ test("throwable", () => assertThrowable(NumbaUtilError, ["numba"]));
+});
+
+describe("OptionError", () => {
+ test("is KeyError", () => {
+ const e = new OptionError("bad option");
+ expect(e).toBeInstanceOf(KeyError);
+ expect(e.name).toBe("OptionError");
+ });
+});
+
+describe("OutOfBoundsDatetime", () => {
+ test("default message", () => {
+ expect(new OutOfBoundsDatetime().message).toContain("nanosecond");
+ });
+ test("is ValueError", () => {
+ expect(new OutOfBoundsDatetime()).toBeInstanceOf(ValueError);
+ });
+});
+
+describe("OutOfBoundsTimedelta", () => {
+ test("default message", () => {
+ expect(new OutOfBoundsTimedelta().message).toContain("timedelta");
+ });
+ test("is ValueError", () => {
+ expect(new OutOfBoundsTimedelta()).toBeInstanceOf(ValueError);
+ });
+});
+
+describe("ParserError", () => {
+ test("is ValueError", () => {
+ const e = new ParserError("parse fail");
+ expect(e).toBeInstanceOf(ValueError);
+ expect(e.name).toBe("ParserError");
+ });
+});
+
+describe("ParserWarning", () => {
+ test("throwable", () => assertThrowable(ParserWarning, ["fallback parser"]));
+});
+
+describe("PerformanceWarning", () => {
+ test("throwable", () => assertThrowable(PerformanceWarning, ["slow op"]));
+});
+
+describe("PossibleDataLossError", () => {
+ test("throwable", () => assertThrowable(PossibleDataLossError, ["data loss"]));
+});
+
+describe("PossiblePrecisionLoss", () => {
+ test("throwable", () => assertThrowable(PossiblePrecisionLoss, ["precision loss"]));
+});
+
+describe("SpecificationError", () => {
+ test("is ValueError", () => {
+ expect(new SpecificationError("bad spec")).toBeInstanceOf(ValueError);
+ expect(new SpecificationError("bad spec").name).toBe("SpecificationError");
+ });
+});
+
+describe("UnsortedIndexError", () => {
+ test("default message", () => {
+ expect(new UnsortedIndexError().message).toContain("lexsorted");
+ });
+ test("is KeyError", () => {
+ expect(new UnsortedIndexError()).toBeInstanceOf(KeyError);
+ });
+});
+
+describe("UnsupportedFunctionCall", () => {
+ test("is ValueError", () => {
+ expect(new UnsupportedFunctionCall("no fn")).toBeInstanceOf(ValueError);
+ });
+});
+
+describe("ValueLabelTypeMismatch", () => {
+ test("throwable", () => assertThrowable(ValueLabelTypeMismatch, ["mismatch"]));
+});
+
+// ---------------------------------------------------------------------------
+// errors namespace
+// ---------------------------------------------------------------------------
+
+describe("errors namespace", () => {
+ test("contains all classes", () => {
+ expect(errors.AbstractMethodError).toBe(AbstractMethodError);
+ expect(errors.EmptyDataError).toBe(EmptyDataError);
+ expect(errors.MergeError).toBe(MergeError);
+ expect(errors.ParserError).toBe(ParserError);
+ expect(errors.ValueError).toBe(ValueError);
+ expect(errors.KeyError).toBe(KeyError);
+ expect(errors.IndexError).toBe(IndexError);
+ });
+
+ test("all namespace entries are constructors", () => {
+ const errorKeys = Object.keys(errors) as Array;
+ for (const key of errorKeys) {
+ const Cls = errors[key];
+ const instance = new Cls(key as never);
+ expect(instance).toBeInstanceOf(Error);
+ }
+ });
+});
diff --git a/tests/window/indexers.test.ts b/tests/window/indexers.test.ts
index 09bb9c28..a4402c69 100644
--- a/tests/window/indexers.test.ts
+++ b/tests/window/indexers.test.ts
@@ -11,8 +11,8 @@ import {
FixedForwardWindowIndexer,
VariableOffsetWindowIndexer,
applyIndexer,
-} from "../../src/window/index.ts";
-import type { WindowBounds } from "../../src/window/index.ts";
+} from "../../src/index.ts";
+import type { WindowBounds } from "../../src/index.ts";
// ─── FixedForwardWindowIndexer ────────────────────────────────────────────────