diff --git a/client/__tests__/util/dataframe/dataframe.test.js b/client/__tests__/util/dataframe/dataframe.test.js index d509d88a..bb193a4d 100644 --- a/client/__tests__/util/dataframe/dataframe.test.js +++ b/client/__tests__/util/dataframe/dataframe.test.js @@ -1029,3 +1029,187 @@ describe("label indexing", () => { expect(idx.dropLabel("blue").labels()).toEqual(["red", "green"]); }); }); + +describe("corner cases", () => { + /* error/corner cases */ + + test("identity integer index rejects non-integer labels", () => { + const idx = new Dataframe.IdentityInt32Index(10); + expect(idx.getOffset(0)).toBe(0); + expect(idx.getOffset(9)).toBe(9); + expect(idx.getOffset(10)).toBeUndefined(); + expect(idx.getOffset(-1)).toBeUndefined(); + expect(idx.getOffset("sort")).toBeUndefined(); + expect(idx.getOffset("length")).toBeUndefined(); + expect(idx.getOffset(true)).toBeUndefined(); + expect(idx.getOffset(0.001)).toBeUndefined(); + expect(idx.getOffset({})).toBeUndefined(); + expect(idx.getOffset([])).toBeUndefined(); + expect(idx.getOffset(new Float32Array())).toBeUndefined(); + expect(idx.getOffset("__proto__")).toBeUndefined(); + + expect(idx.getLabel(0)).toBe(0); + expect(idx.getLabel(9)).toBe(9); + expect(idx.getLabel(10)).toBeUndefined(); + expect(idx.getLabel(-1)).toBeUndefined(); + expect(idx.getLabel("sort")).toBeUndefined(); + expect(idx.getLabel("length")).toBeUndefined(); + expect(idx.getLabel(true)).toBeUndefined(); + expect(idx.getLabel(0.001)).toBeUndefined(); + expect(idx.getLabel({})).toBeUndefined(); + expect(idx.getLabel([])).toBeUndefined(); + expect(idx.getLabel(new Float32Array())).toBeUndefined(); + expect(idx.getLabel("__proto__")).toBeUndefined(); + }); + + test("dense integer index rejects non-integer labels", () => { + const idx = new Dataframe.DenseInt32Index([-10, 0, 3, 9, 10]); + expect(idx.getOffset(0)).toBe(1); + expect(idx.getOffset(9)).toBe(3); + expect(idx.getOffset(1)).toBeUndefined(); + expect(idx.getOffset(11)).toBeUndefined(); + expect(idx.getOffset(-1)).toBeUndefined(); + expect(idx.getOffset("sort")).toBeUndefined(); + expect(idx.getOffset("length")).toBeUndefined(); + expect(idx.getOffset(true)).toBeUndefined(); + expect(idx.getOffset(0.001)).toBeUndefined(); + expect(idx.getOffset({})).toBeUndefined(); + expect(idx.getOffset([])).toBeUndefined(); + expect(idx.getOffset(new Float32Array())).toBeUndefined(); + expect(idx.getOffset("__proto__")).toBeUndefined(); + + expect(idx.getLabel(0)).toBe(-10); + expect(idx.getLabel(4)).toBe(10); + expect(idx.getLabel(10)).toBeUndefined(); + expect(idx.getLabel(-1)).toBeUndefined(); + expect(idx.getLabel("sort")).toBeUndefined(); + expect(idx.getLabel("length")).toBeUndefined(); + expect(idx.getLabel(true)).toBeUndefined(); + expect(idx.getLabel(0.001)).toBeUndefined(); + expect(idx.getLabel({})).toBeUndefined(); + expect(idx.getLabel([])).toBeUndefined(); + expect(idx.getLabel(new Float32Array())).toBeUndefined(); + expect(idx.getLabel("__proto__")).toBeUndefined(); + }); + + test("Empty dataframe rejects bogus labels", () => { + const df = Dataframe.Dataframe.empty(); + + expect(df.hasCol("sort")).toBeFalsy(); + expect(df.hasCol(0)).toBeFalsy(); + expect(df.hasCol(true)).toBeFalsy(); + expect(df.hasCol(false)).toBeFalsy(); + expect(df.hasCol([])).toBeFalsy(); + expect(df.hasCol({})).toBeFalsy(); + expect(df.hasCol(null)).toBeFalsy(); + expect(df.hasCol(undefined)).toBeFalsy(); + + expect(df.col("sort")).toBeUndefined(); + expect(df.col(0)).toBeUndefined(); + expect(df.col(true)).toBeUndefined(); + expect(df.col(false)).toBeUndefined(); + expect(df.col([])).toBeUndefined(); + expect(df.col({})).toBeUndefined(); + expect(df.col(null)).toBeUndefined(); + expect(df.col(undefined)).toBeUndefined(); + + expect(df.icol("sort")).toBeUndefined(); + expect(df.icol(0)).toBeUndefined(); + expect(df.icol(true)).toBeUndefined(); + expect(df.icol(false)).toBeUndefined(); + expect(df.icol([])).toBeUndefined(); + expect(df.icol({})).toBeUndefined(); + expect(df.icol(null)).toBeUndefined(); + expect(df.icol(undefined)).toBeUndefined(); + + expect(df.ihas("sort", "length")).toBeFalsy(); + expect(df.ihas("0", "0")).toBeFalsy(); + expect(df.ihas("", "")).toBeFalsy(); + expect(df.ihas(null, null)).toBeFalsy(); + expect(df.ihas(undefined, undefined)).toBeFalsy(); + expect(df.ihas(true, true)).toBeFalsy(); + expect(df.ihas([], [])).toBeFalsy(); + expect(df.ihas({}, {})).toBeFalsy(); + }); + + test("Dataframe rejects bogus labels", () => { + const df = new Dataframe.Dataframe( + [2, 2], + [ + [true, false], + [1, 0], + ], + null, + new Dataframe.KeyIndex(["A", "B"]) + ); + + expect(df.hasCol("sort")).toBeFalsy(); + expect(df.hasCol("__proto__")).toBeFalsy(); + expect(df.hasCol(0)).toBeFalsy(); + expect(df.hasCol(true)).toBeFalsy(); + expect(df.hasCol(false)).toBeFalsy(); + expect(df.hasCol([])).toBeFalsy(); + expect(df.hasCol({})).toBeFalsy(); + expect(df.hasCol(null)).toBeFalsy(); + expect(df.hasCol(undefined)).toBeFalsy(); + + expect(df.col("sort")).toBeUndefined(); + expect(df.col("__proto__")).toBeUndefined(); + expect(df.col(0)).toBeUndefined(); + expect(df.col(true)).toBeUndefined(); + expect(df.col(false)).toBeUndefined(); + expect(df.col([])).toBeUndefined(); + expect(df.col({})).toBeUndefined(); + expect(df.col(null)).toBeUndefined(); + expect(df.col(undefined)).toBeUndefined(); + + expect(df.icol("sort")).toBeUndefined(); + expect(df.icol("__proto__")).toBeUndefined(); + expect(df.icol(-1)).toBeUndefined(); + expect(df.icol(true)).toBeUndefined(); + expect(df.icol(false)).toBeUndefined(); + expect(df.icol([])).toBeUndefined(); + expect(df.icol({})).toBeUndefined(); + expect(df.icol(null)).toBeUndefined(); + expect(df.icol(undefined)).toBeUndefined(); + + expect(df.ihas("sort", "length")).toBeFalsy(); + expect(df.ihas("__proto__", "__proto__")).toBeFalsy(); + + expect(df.ihas(-1, 0)).toBeFalsy(); + expect(df.ihas("0", 0)).toBeFalsy(); + expect(df.ihas("", 0)).toBeFalsy(); + expect(df.ihas(null, 0)).toBeFalsy(); + expect(df.ihas(undefined, 0)).toBeFalsy(); + expect(df.ihas([], 0)).toBeFalsy(); + expect(df.ihas({}, 0)).toBeFalsy(); + + expect(df.ihas(0, -1)).toBeFalsy(); + expect(df.ihas(0, "0")).toBeFalsy(); + expect(df.ihas(0, "")).toBeFalsy(); + expect(df.ihas(0, null)).toBeFalsy(); + expect(df.ihas(0, undefined)).toBeFalsy(); + expect(df.ihas(0, [])).toBeFalsy(); + expect(df.ihas(0, {})).toBeFalsy(); + + expect(df.has("sort", "length")).toBeFalsy(); + expect(df.has("length", "sort")).toBeFalsy(); + expect(df.has("__proto__", "__proto__")).toBeFalsy(); + + expect(df.has(-1, "A")).toBeFalsy(); + expect(df.has("0", "A")).toBeFalsy(); + expect(df.has("", "A")).toBeFalsy(); + expect(df.has(null, "A")).toBeFalsy(); + expect(df.has(undefined, "A")).toBeFalsy(); + expect(df.has([], "A")).toBeFalsy(); + expect(df.has({}, "A")).toBeFalsy(); + + expect(df.has(0, -1)).toBeFalsy(); + expect(df.has(0, "0")).toBeFalsy(); + expect(df.has(0, "")).toBeFalsy(); + expect(df.has(0, null)).toBeFalsy(); + expect(df.has(0, undefined)).toBeFalsy(); + expect(df.has(0, [])).toBeFalsy(); + expect(df.has(0, {})).toBeFalsy(); + }); +}); diff --git a/client/src/util/dataframe/dataframe.js b/client/src/util/dataframe/dataframe.js index 85a01e6d..59196b5a 100644 --- a/client/src/util/dataframe/dataframe.js +++ b/client/src/util/dataframe/dataframe.js @@ -290,10 +290,8 @@ class Dataframe { Use an existing accessor if provided, else compile a new one. */ - const { - getOffset: getRowByOffset, - getLabel: getRowByLabel, - } = this.rowIndex; + const getRowByOffset = this.rowIndex.getOffset.bind(this.rowIndex); + const getRowByLabel = this.rowIndex.getLabel.bind(this.rowIndex); this.__columnsAccessor = this.__columns.map((column, idx) => { if (accessors[idx]) { return accessors[idx]; @@ -704,7 +702,9 @@ class Dataframe { /* Return column accessor by offset. */ - return this.__columnsAccessor[columnOffset]; + return Number.isInteger(columnOffset) + ? this.__columnsAccessor[columnOffset] + : undefined; } at(r, c) { @@ -753,7 +753,14 @@ class Dataframe { dataframe - returns true/false */ const [nRows, nCols] = this.dims; - return c >= 0 && c < nCols && r >= 0 && r < nRows; + return ( + Number.isInteger(r) && + Number.isInteger(c) && + c >= 0 && + c < nCols && + r >= 0 && + r < nRows + ); } hasCol(c) { diff --git a/client/src/util/dataframe/labelIndex.js b/client/src/util/dataframe/labelIndex.js index fa162457..ce7dc9a2 100644 --- a/client/src/util/dataframe/labelIndex.js +++ b/client/src/util/dataframe/labelIndex.js @@ -41,31 +41,29 @@ class IdentityInt32Index { return k; } - /* eslint-disable class-methods-use-this -- having these accessors as class methods allows for polymorphism */ getOffset(i) { // label to offset - return i; + return Number.isInteger(i) && i >= 0 && i < this.maxOffset ? i : undefined; } getOffsets(arr) { // labels to offsets - return arr; + return arr.map((i) => this.getOffset(i)); } getLabel(i) { // offset to label - return i; + return Number.isInteger(i) && i >= 0 && i < this.maxOffset ? i : undefined; } getLabels(arr) { // offsets to labels - return arr; + return arr.map((i) => this.getLabel(i)); } size() { return this.maxOffset; } - /* eslint-enable class-methods-use-this -- enable */ __promote(labelArray) { /* @@ -89,7 +87,7 @@ class IdentityInt32Index { const { maxOffset } = this; for (let i = 0, l = labels.length; i < l; i += 1) { const label = labels[i]; - if (label < 0 || label >= maxOffset) + if (!Number.isInteger(label) || label < 0 || label >= maxOffset) throw new RangeError(`offset or label: ${label}`); } return this.__promote(labels); @@ -153,27 +151,21 @@ class DenseInt32Index { __compile() { const { minLabel, index, rindex } = this; this.getOffset = function getOffset(l) { - return index[l - minLabel]; + if (!Number.isInteger(l)) return undefined; + const offset = index[l - minLabel]; + return offset === -1 ? undefined : offset; }; this.getOffsets = function getOffsets(arr) { - const res = new arr.constructor(arr.length); - for (let i = 0, len = arr.length; i < len; i += 1) { - res[i] = index[arr[i] - minLabel]; - } - return res; + return arr.map((i) => this.getOffset(i)); }; this.getLabel = function getLabel(i) { - return rindex[i]; + return Number.isInteger(i) ? rindex[i] : undefined; }; this.getLabels = function getLabels(arr) { - const res = new arr.constructor(arr.length); - for (let i = 0, len = arr.length; i < len; i += 1) { - res[i] = rindex[arr[i]]; - } - return res; + return arr.map((i) => this.getLabel(i)); }; } @@ -275,23 +267,15 @@ class KeyIndex { }; this.getOffsets = function getOffsets(arr) { - const res = new arr.constructor(arr.length); - for (let i = 0, len = arr.length; i < len; i += 1) { - res[i] = index.get(arr[i]); - } - return res; + return arr.map((l) => this.getOffset(l)); }; this.getLabel = function getLabel(i) { - return rindex[i]; + return Number.isInteger(i) ? rindex[i] : undefined; }; this.getLabels = function getLabels(arr) { - const res = new arr.constructor(arr.length); - for (let i = 0, len = arr.length; i < len; i += 1) { - res[i] = rindex[arr[i]]; - } - return res; + return arr.map((i) => this.getLabel(i)); }; } @@ -308,7 +292,9 @@ class KeyIndex { for (let i = 0, l = labels.length; i < l; i += 1) { const label = labels[i]; const offset = this.getOffset(label); - if (offset === undefined) throw new RangeError(`unknown label: ${label}`); + if (offset === undefined || offset === -1) { + throw new RangeError(`unknown label: ${label}`); + } } return new KeyIndex(labels);