// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ // ┃ Copyright (c) 2017, the Perspective Authors. ┃ // ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ // ┃ This file is part of the Perspective library, distributed under the terms ┃ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ import { test, expect } from "@perspective-dev/test"; import perspective from "../perspective_client"; import * as common from "./common.js"; const NUMERIC_TYPES = [ "i8", "ui8", "i16", "ui16", "i32", "ui32", "i64", "ui64", "f32", "f64", ]; /** * Return all possible permutations of unary operators for all numeric types. */ function generate_unary_operations(operator) { const expressions = []; for (const expr of NUMERIC_TYPES) { expressions.push(`${operator}"${expr}"`); } return expressions; } /** * Return all possible permutations of binary operators for all numeric types. */ function generate_binary_operations(operator) { const expressions = []; for (const expr of NUMERIC_TYPES) { for (let i = 0; i < NUMERIC_TYPES.length; i++) { expressions.push(`"${expr}" ${operator} "${NUMERIC_TYPES[i]}"`); } } return expressions; } function calc_unary(operator, value) { switch (operator) { case "+": return +value; case "-": return -value; default: throw new Error("Unknown operator"); } } function calc_binary(operator, left, right) { switch (operator) { case "+": return left + right; case "-": return left - right; case "*": return left * right; case "/": return left / right; case "%": return left % right; case "^": return Math.pow(left, right); case "==": return left == right ? 1 : 0; case "!=": return left != right ? 1 : 0; case ">": return left > right ? 1 : 0; case "<": return left < right ? 1 : 0; case ">=": return left >= right ? 1 : 0; case "<=": return left <= right ? 1 : 0; default: throw new Error("Unknown operator"); } } /** * Validate the results of operations against all numeric types. * * @param {*} operator */ function validate_unary_operations(output, expressions, operator) { for (const expr of expressions) { const output_col = output[expr]; const input = expr.substr(2, expr.length - 3); expect({ expr, result: output_col, }).toEqual({ expr, result: output[input].map((v) => calc_unary(operator, v)), }); } } /** * Validate the results of operations against all numeric types. * * @param {*} operator */ function validate_binary_operations(output, expressions, operator) { for (const expr of expressions) { const output_col = output[expr]; const inputs = expr.split(` ${operator} `); const left = inputs[0].substr(1, inputs[0].length - 2); const right = inputs[1].substr(1, inputs[1].length - 2); expect({ expr, result: output_col, }).toEqual({ expr, result: output[left].map((v, idx) => calc_binary(operator, v, output[right][idx]), ), }); } } /** * Tests the correctness of expressions allowed by exprtk, using as many * of the expression structures as we can, such as conditionals, loops, etc. */ ((perspective) => { test.describe("Numeric operations", function () { test.describe("All numeric types", function () { test.describe("aggregates", function () { test( "integer", async function () { const table = await perspective.table({ a: "integer", }); const view = await table.view({ expressions: { '"a"': '"a"' }, }); await table.update({ a: [10, 15, 20, 30], }); const result = await view.to_columns(); expect(result['"a"']).toEqual([10, 15, 20, 30]); expect(result["a"]).toEqual([10, 15, 20, 30]); await view.delete(); await table.delete(); }, test("expression integer column sum", async function () { const table = await perspective.table({ a: [10, 15, 20, 30], b: [1, 2, 3, 4], }); const view = await table.view({ group_by: ["b"], aggregates: { a: "sum", b: "sum", }, expressions: { '"a"': '"a"' }, }); await table.update({ a: [40, 50, 60, 70], }); expect(await view.to_columns()).toEqual({ __ROW_PATH__: [[], [null], [1], [2], [3], [4]], a: [295, 220, 10, 15, 20, 30], b: [10, 0, 1, 2, 3, 4], '"a"': [295, 220, 10, 15, 20, 30], }); await view.delete(); await table.delete(); }), ); }); test.describe("unary", function () { test("negative", async function () { const table = await perspective.table( common.all_types_arrow.slice(), ); const expressions = generate_unary_operations("+"); const view = await table.view({ expressions, }); const col_names = await view.column_paths(); for (const expr of expressions) { expect(col_names.includes(expr)).toBe(true); } const result = await view.to_columns(); validate_unary_operations(result, expressions, "+"); await view.delete(); await table.delete(); }); }); test.describe("binary", function () { test("add", async function () { const table = await perspective.table( common.all_types_arrow.slice(), ); const expressions = generate_binary_operations("+"); const view = await table.view({ expressions, }); const col_names = await view.column_paths(); for (const expr of expressions) { expect(col_names.includes(expr)).toBe(true); } const result = await view.to_columns(); validate_binary_operations(result, expressions, "+"); await view.delete(); await table.delete(); }); test("subtract", async function () { const table = await perspective.table( common.all_types_arrow.slice(), ); const expressions = generate_binary_operations("-"); const view = await table.view({ expressions, }); const col_names = await view.column_paths(); for (const expr of expressions) { expect(col_names.includes(expr)).toBe(true); } const result = await view.to_columns(); validate_binary_operations(result, expressions, "-"); await view.delete(); await table.delete(); }); test("multiply", async function () { const table = await perspective.table( common.all_types_arrow.slice(), ); const expressions = generate_binary_operations("*"); const view = await table.view({ expressions, }); const col_names = await view.column_paths(); for (const expr of expressions) { expect(col_names.includes(expr)).toBe(true); } const result = await view.to_columns(); validate_binary_operations(result, expressions, "*"); await view.delete(); await table.delete(); }); test("divide", async function () { const table = await perspective.table( common.all_types_arrow.slice(), ); const expressions = generate_binary_operations("/"); const view = await table.view({ expressions, }); const col_names = await view.column_paths(); for (const expr of expressions) { expect(col_names.includes(expr)).toBe(true); } const result = await view.to_columns(); validate_binary_operations(result, expressions, "/"); await view.delete(); await table.delete(); }); test("modulus", async function () { const table = await perspective.table( common.all_types_arrow.slice(), ); const expressions = generate_binary_operations("%"); const view = await table.view({ expressions, }); const col_names = await view.column_paths(); for (const expr of expressions) { expect(col_names.includes(expr)).toBe(true); } const result = await view.to_columns(); validate_binary_operations(result, expressions, "%"); await view.delete(); await table.delete(); }); test("power", async function () { const table = await perspective.table(common.arrow.slice()); const expressions = generate_binary_operations("^"); const view = await table.view({ expressions, }); const col_names = await view.column_paths(); for (const expr of expressions) { expect(col_names.includes(expr)).toBe(true); } const result = await view.to_columns(); validate_binary_operations(result, expressions, "^"); }); test("==", async function () { const table = await perspective.table( common.all_types_multi_arrow.slice(), ); const expressions = []; // comparisons only work when the two types are the same for (const expr of NUMERIC_TYPES) { expressions.push(`"${expr}" == "${expr} 2"`); } const view = await table.view({ expressions, }); const col_names = await view.column_paths(); for (const expr of expressions) { expect(col_names.includes(expr)).toBe(true); } const result = await view.to_columns(); for (const expr of expressions) { const output_col = result[expr]; const inputs = expr.split(` == `); const left = inputs[0].substr(1, inputs[0].length - 2); const right = inputs[1].substr(1, inputs[1].length - 2); expect(output_col).toEqual( result[left].map( (v, idx) => v == result[right][idx], ), ); } }); test("!=", async function () { const table = await perspective.table( common.all_types_multi_arrow.slice(), ); const expressions = []; // comparisons only work when the two types are the same for (const expr of NUMERIC_TYPES) { expressions.push(`"${expr}" != "${expr} 2"`); } const view = await table.view({ expressions, }); const col_names = await view.column_paths(); for (const expr of expressions) { expect(col_names.includes(expr)).toBe(true); } const result = await view.to_columns(); for (const expr of expressions) { const output_col = result[expr]; const inputs = expr.split(` != `); const left = inputs[0].substr(1, inputs[0].length - 2); const right = inputs[1].substr(1, inputs[1].length - 2); expect(output_col).toEqual( result[left].map( (v, idx) => v != result[right][idx], ), ); } }); test(">", async function () { const table = await perspective.table( common.all_types_multi_arrow.slice(), ); const expressions = []; // comparisons only work when the two types are the same for (const expr of NUMERIC_TYPES) { expressions.push(`"${expr}" > "${expr} 2"`); } const view = await table.view({ expressions, }); const col_names = await view.column_paths(); for (const expr of expressions) { expect(col_names.includes(expr)).toBe(true); } const result = await view.to_columns(); for (const expr of expressions) { const output_col = result[expr]; const inputs = expr.split(` > `); const left = inputs[0].substr(1, inputs[0].length - 2); const right = inputs[1].substr(1, inputs[1].length - 2); expect(output_col).toEqual( result[left].map( (v, idx) => v > result[right][idx], ), ); } }); test("<", async function () { const table = await perspective.table( common.all_types_multi_arrow.slice(), ); const expressions = []; // comparisons only work when the two types are the same for (const expr of NUMERIC_TYPES) { expressions.push(`"${expr}" < "${expr} 2"`); } const view = await table.view({ expressions, }); const col_names = await view.column_paths(); for (const expr of expressions) { expect(col_names.includes(expr)).toBe(true); } const result = await view.to_columns(); for (const expr of expressions) { const output_col = result[expr]; const inputs = expr.split(` < `); const left = inputs[0].substr(1, inputs[0].length - 2); const right = inputs[1].substr(1, inputs[1].length - 2); expect(output_col).toEqual( result[left].map( (v, idx) => v < result[right][idx], ), ); } }); test(">=", async function () { const table = await perspective.table( common.all_types_multi_arrow.slice(), ); const expressions = []; // comparisons only work when the two types are the same for (const expr of NUMERIC_TYPES) { expressions.push(`"${expr}" >= "${expr} 2"`); } const view = await table.view({ expressions, }); const col_names = await view.column_paths(); for (const expr of expressions) { expect(col_names.includes(expr)).toBe(true); } const result = await view.to_columns(); for (const expr of expressions) { const output_col = result[expr]; const inputs = expr.split(` >= `); const left = inputs[0].substr(1, inputs[0].length - 2); const right = inputs[1].substr(1, inputs[1].length - 2); expect(output_col).toEqual( result[left].map( (v, idx) => v >= result[right][idx], ), ); } }); test("<=", async function () { const table = await perspective.table( common.all_types_multi_arrow.slice(), ); const expressions = []; // comparisons only work when the two types are the same for (const expr of NUMERIC_TYPES) { expressions.push(`"${expr}" <= "${expr} 2"`); } const view = await table.view({ expressions, }); const col_names = await view.column_paths(); for (const expr of expressions) { expect(col_names.includes(expr)).toBe(true); } const result = await view.to_columns(); for (const expr of expressions) { const output_col = result[expr]; const inputs = expr.split(` <= `); const left = inputs[0].substr(1, inputs[0].length - 2); const right = inputs[1].substr(1, inputs[1].length - 2); expect(output_col).toEqual( result[left].map( (v, idx) => v <= result[right][idx], ), ); } }); test("Numeric comparisons should be false for different types", async function () { const table = await perspective.table({ a: "integer", b: "float", c: "integer", d: "float", }); const view = await table.view({ expressions: { '"a" == "b"': '"a" == "b"', '"a" != "b"': '"a" != "b"', }, }); await table.update({ a: [1, 2, 3, 4], b: [1.0, 2.0, 3.0, 4.0], c: [1, 0, 1, 0], d: [1.0, 0, 3.0, 0.0], }); const result = await view.to_columns(); expect(result['"a" == "b"']).toEqual([ false, false, false, false, ]); expect(result['"a" != "b"']).toEqual([ true, true, true, true, ]); await view.delete(); await table.delete(); }); test("Numeric comparisons should be true for same types", async function () { const table = await perspective.table({ a: "integer", b: "float", c: "integer", d: "float", }); const view = await table.view({ expressions: [ '"a" == "c"', '"a" != "c"', '"b" == "d"', '"b" != "d"', ].reduce((x, y) => Object.assign(x, { [y]: y }), {}), }); await table.update({ a: [1, 2, 3, 4], b: [1.0, 2.0, 3.0, 4.0], c: [1, 0, 1, 0], d: [1.0, 0, 3.0, 0.0], }); const result = await view.to_columns(); expect(result['"a" == "c"']).toEqual([ true, false, false, false, ]); expect(result['"a" != "c"']).toEqual([ false, true, true, true, ]); expect(result['"b" == "d"']).toEqual([ true, false, true, false, ]); expect(result['"b" != "d"']).toEqual([ false, true, false, true, ]); await view.delete(); await table.delete(); }); test("Should not divide by 0", async function () { const table = await perspective.table({ a: [1, 2, 3, 4], b: [0, 0, 0, 0], c: [1.5, 2.123, 3.125, 4.123809], }); const view = await table.view({ expressions: { '"a" / "b"': '"a" / "b"', '"c" / "b"': '"c" / "b"', }, }); const result = await view.to_columns(); expect(result['"a" / "b"']).toEqual([ null, null, null, null, ]); expect(result['"c" / "b"']).toEqual([ null, null, null, null, ]); await view.delete(); await table.delete(); }); test("Should not modulo by 0", async function () { const table = await perspective.table({ a: [1, 2, 3, 4], b: [0, 0, 0, 0], c: [1.5, 2.123, 3.125, 4.123809], }); const view = await table.view({ expressions: { '"a" % "b"': '"a" % "b"', '"c" % "b"': '"c" % "b"', }, }); const result = await view.to_columns(); expect(result['"a" % "b"']).toEqual([ null, null, null, null, ]); expect(result['"c" % "b"']).toEqual([ null, null, null, null, ]); await view.delete(); await table.delete(); }); }); }); test.describe("Integers", function () { test("Integer outputs with negative values dont overflow", async function () { var data = [ { x: -1, y: "a", z: true }, { x: -2, y: "b", z: false }, { x: -3, y: "c", z: true }, { x: -4, y: "d", z: false }, ]; var table = await perspective.table({ x: "integer", y: "string", z: "boolean", }); await table.update(data); var view = await table.view({ group_by: ["z"], columns: ["x", "w"], expressions: { w: 'integer("x")', }, }); var answer = [ { __ROW_PATH__: [], x: -10, w: -10 }, { __ROW_PATH__: [false], x: -6, w: -6 }, { __ROW_PATH__: [true], x: -4, w: -4 }, ]; let result = await view.to_json(); expect(result).toEqual(answer); view.delete(); table.delete(); }); }); test.describe("Functions", function () { test("min", async function () { const table = await perspective.table({ a: "integer", b: "float", }); const expressions = [ "min(1)", 'min("a")', 'min("a", -10, -10.001)', 'min("b")', 'min("b", 0.00000000001, -10, -100, -100.1)', 'min("a", "b")', ]; const view = await table.view({ expressions, }); await table.update({ a: [1, 2, 3, 4], b: [1.5, 2.5, 3.5, 4.5], }); const schema = await view.expression_schema(); for (const expr of expressions) { expect(schema[expr]).toEqual("float"); } const result = await view.to_columns(); expect(result["min(1)"]).toEqual([1, 1, 1, 1]); expect(result['min("a")']).toEqual([1, 2, 3, 4]); expect(result['min("a", -10, -10.001)']).toEqual([ -10.001, -10.001, -10.001, -10.001, ]); expect(result['min("b")']).toEqual([1.5, 2.5, 3.5, 4.5]); expect( result['min("b", 0.00000000001, -10, -100, -100.1)'], ).toEqual([-100.1, -100.1, -100.1, -100.1]); expect(result['min("a", "b")']).toEqual([1, 2, 3, 4]); await view.delete(); await table.delete(); }); test("max", async function () { const table = await perspective.table({ a: "integer", b: "float", }); const expressions = [ "max(1)", "max(2000000000000.11, 2000000000000.1)", 'max("a")', 'max("a", -1.00001)', 'max("a", "b")', 'max("b")', 'max("a", 10, 20, 0.1, 0.00000001)', 'max("b", -1, -100, 100)', ]; const view = await table.view({ expressions, }); await table.update({ a: [1, 2, 3, 4], b: [1.5, 2.5, 3.5, 4.5], }); const schema = await view.expression_schema(); for (const expr of expressions) { expect(schema[expr]).toEqual("float"); } const result = await view.to_columns(); expect(result["max(1)"]).toEqual([1, 1, 1, 1]); expect( result["max(2000000000000.11, 2000000000000.1)"], ).toEqual([ 2000000000000.11, 2000000000000.11, 2000000000000.11, 2000000000000.11, ]); expect(result['max("a")']).toEqual([1, 2, 3, 4]); expect(result['max("a", "b")']).toEqual([1.5, 2.5, 3.5, 4.5]); expect(result['max("b")']).toEqual([1.5, 2.5, 3.5, 4.5]); expect(result['max("a", 10, 20, 0.1, 0.00000001)']).toEqual([ 20, 20, 20, 20, ]); expect(result['max("b", -1, -100, 100)']).toEqual([ 100, 100, 100, 100, ]); await view.delete(); await table.delete(); }); test("inrange", async function () { const table = await perspective.table({ a: "integer", b: "float", }); // works for floats only const view = await table.view({ expressions: { 'inrange(9, "b", 20)': 'inrange(9, "b", 20)', }, }); await table.update({ a: [10, 15, 20, 30], b: [10.5, 15.5, 20.5, 30.5], }); const result = await view.to_columns(); expect(result['inrange(9, "b", 20)']).toEqual([ true, true, false, false, ]); await view.delete(); await table.delete(); }); test("iclamp", async function () { const table = await perspective.table({ a: "integer", b: "float", }); const view = await table.view({ expressions: { 'iclamp(10, "a", 20)': 'iclamp(10, "a", 20)', 'iclamp(10, "b", 20)': 'iclamp(10, "b", 20)', }, }); await table.update({ a: [10, 15, 20, 30], b: [10.5, 15.5, 20.5, 30.5], }); const result = await view.to_columns(); expect(result['iclamp(10, "a", 20)']).toEqual([10, 15, 20, 30]); expect(result['iclamp(10, "b", 20)']).toEqual([ 10, 20, 20.5, 30.5, ]); await view.delete(); await table.delete(); }); test("pow", async function () { const table = await perspective.table({ a: "integer", b: "float", }); const view = await table.view({ expressions: { 'pow("a", 1)': 'pow("a", 1)', 'pow("b", 3)': 'pow("b", 3)', }, }); await table.update({ a: [1, 2, 3, 4], b: [1.5, 2.5, 3.5, 4.5], }); const result = await view.to_columns(); expect(result['pow("a", 1)']).toEqual([1, 2, 3, 4]); expect(result['pow("b", 3)']).toEqual( [1.5, 2.5, 3.5, 4.5].map((x) => Math.pow(x, 3)), ); await view.delete(); await table.delete(); }); test.skip("logn", async function () { const table = await perspective.table({ a: "integer", b: "float", }); const view = await table.view({ expressions: { 'logn("a", 5)': 'logn("a", 5)', 'logn("b", 3)': 'logn("b", 3)', }, }); await table.update({ a: [100, 200, 300, 400], b: [100.5, 200.5, 300.5, 400.5], }); const result = await view.to_columns(); expect(result['logn("a", 5)']).toEqual( [100, 200, 300, 400].map((x) => Math.log(x) / Math.log(5)), ); expect(result['logn("b", 3)']).toEqual( [100.5, 200.5, 300.5, 400.5].map( (x) => Math.log(x) / Math.log(3), ), ); await view.delete(); await table.delete(); }); test("root", async function () { const table = await perspective.table({ a: "integer", b: "float", }); const view = await table.view({ expressions: { 'root("a", 5)': 'root("a", 5)', 'root("b", 3)': 'root("b", 3)', }, }); await table.update({ a: [100, 200, 300, 400], b: [100.5, 200.5, 300.5, 400.5], }); const result = await view.to_columns(); expect( result['root("a", 5)'].map((x) => Number(x.toFixed(5))), ).toEqual( [100, 200, 300, 400].map((x) => Number(Math.pow(x, 1 / 5).toFixed(5)), ), ); expect( result['root("b", 3)'].map((x) => Number(x.toFixed(5))), ).toEqual( [100.5, 200.5, 300.5, 400.5].map((x) => Number(Math.pow(x, 1 / 3).toFixed(5)), ), ); await view.delete(); await table.delete(); }); test("avg", async function () { const table = await perspective.table({ a: "integer", b: "float", }); const view = await table.view({ expressions: [ 'avg("a", 10, 20, 30, 40, "a")', 'avg("b", 3, 4, 5, "b")', ].reduce((x, y) => Object.assign(x, { [y]: y }), {}), }); await table.update({ a: [1, 2, 3, 4], b: [1.5, 2.5, 3.5, 4.5], }); const result = await view.to_columns(); expect(result['avg("a", 10, 20, 30, 40, "a")']).toEqual( [1, 2, 3, 4].map((x) => (x + x + 10 + 20 + 30 + 40) / 6), ); expect(result['avg("b", 3, 4, 5, "b")']).toEqual( [1.5, 2.5, 3.5, 4.5].map((x) => (x + x + 3 + 4 + 5) / 5), ); await view.delete(); await table.delete(); }); test("sum", async function () { const table = await perspective.table({ a: "integer", b: "float", }); const view = await table.view({ expressions: [ 'sum("a", 10, 20, 30, 40, "a")', 'sum("b", 3, 4, 5, "b")', ].reduce((x, y) => Object.assign(x, { [y]: y }), {}), }); await table.update({ a: [1, 2, 3, 4], b: [1.5, 2.5, 3.5, 4.5], }); const result = await view.to_columns(); expect(result['sum("a", 10, 20, 30, 40, "a")']).toEqual( [1, 2, 3, 4].map((x) => x + x + 10 + 20 + 30 + 40), ); expect(result['sum("b", 3, 4, 5, "b")']).toEqual( [1.5, 2.5, 3.5, 4.5].map((x) => x + x + 3 + 4 + 5), ); await view.delete(); await table.delete(); }); test("trunc", async function () { const table = await perspective.table({ a: "integer", b: "float", }); const view = await table.view({ expressions: { 'trunc("a")': 'trunc("a")', 'trunc("b")': 'trunc("b")', }, }); await table.update({ a: [1, 2, 3, 4], b: [1.5, 2.5, 3.5, 4.5], }); const result = await view.to_columns(); expect(result['trunc("a")']).toEqual([1, 2, 3, 4]); expect(result['trunc("b")']).toEqual([1, 2, 3, 4]); await view.delete(); await table.delete(); }); test("d2r", async function () { const table = await perspective.table({ a: "integer", b: "float", }); const view = await table.view({ expressions: { 'deg2rad("a")': 'deg2rad("a")', 'deg2rad("b")': 'deg2rad("b")', }, }); await table.update({ a: [30, 60, 90, 120], b: [25.5, 45.5, 88.721282, 91.12983], }); const result = await view.to_columns(); expect(result['deg2rad("a")']).toEqual( [30, 60, 90, 120].map((x) => x * (Math.PI / 180)), ); expect(result['deg2rad("b")']).toEqual( [25.5, 45.5, 88.721282, 91.12983].map( (x) => x * (Math.PI / 180), ), ); await view.delete(); await table.delete(); }); test.skip("OG - r2d", async function () { const table = await perspective.table({ a: "integer", b: "float", }); const view = await table.view({ expressions: { 'rad2deg("a")': 'rad2deg("a")', 'rad2deg("b")': 'rad2deg("b")', }, }); await table.update({ a: [1, 2, 3, 4], b: [25.5, 45.5, 88.721282, 91.12983].map( (x) => x * (Math.PI / 180), ), }); const result = await view.to_columns(); expect(result['rad2deg("a")']).toEqual( [1, 2, 3, 4].map((x) => x * (180 / Math.PI)), ); expect(result['rad2deg("b")']).toEqual([ 25.5, 45.5, 88.721282, 91.12983, ]); await view.delete(); await table.delete(); }); test("r2d", async function () { const table = await perspective.table({ a: "integer", b: "float", }); const view = await table.view({ expressions: { 'rad2deg("a")': 'rad2deg("a")', 'rad2deg("b")': 'rad2deg("b")', }, }); await table.update({ a: [1, 2, 3, 4], b: [25.5, 45.5, 88.721282, 91.12983].map( (x) => x * (Math.PI / 180), ), }); const result = await view.to_columns(); expect(result['rad2deg("a")']).toEqual( [1, 2, 3, 4].map((x) => x * (180 / Math.PI)), ); const expected = [25.5, 45.5, 88.721282, 91.12983]; expect(result['rad2deg("b")'].length).toEqual(expected.length); result['rad2deg("b")'].forEach((x, idx) => expect(x).toBeCloseTo(expected[idx], 5), ); await view.delete(); await table.delete(); }); test("is_null", async function () { const table = await perspective.table({ a: "integer", b: "float", }); const view = await table.view({ expressions: [ 'is_null("a")', 'is_null("b")', 'if(is_null("a")) 100; else 0;', 'if(is_null("b")) 100; else 0;', ].reduce((x, y) => Object.assign(x, { [y]: y }), {}), }); await table.update({ a: [1, null, null, 4], b: [null, 2.5, null, 4.5], }); const result = await view.to_columns(); expect(result['is_null("a")']).toEqual([ false, true, true, false, ]); expect(result['is_null("b")']).toEqual([ true, false, true, false, ]); expect(result['if(is_null("a")) 100; else 0;']).toEqual([ 0, 100, 100, 0, ]); expect(result['if(is_null("b")) 100; else 0;']).toEqual([ 100, 0, 100, 0, ]); await view.delete(); await table.delete(); }); test("is_not_null", async function () { const table = await perspective.table({ a: "integer", b: "float", }); const view = await table.view({ expressions: [ 'is_not_null("a")', 'is_not_null("b")', 'if(is_not_null("a")) 100; else 0;', 'if(is_not_null("b")) 100; else 0;', ].reduce((x, y) => Object.assign(x, { [y]: y }), {}), }); await table.update({ a: [1, null, null, 4], b: [null, 2.5, null, 4.5], }); const result = await view.to_columns(); expect(result['is_not_null("a")']).toEqual([ true, false, false, true, ]); expect(result['is_not_null("b")']).toEqual([ false, true, false, true, ]); expect(result['if(is_not_null("a")) 100; else 0;']).toEqual([ 100, 0, 0, 100, ]); expect(result['if(is_not_null("b")) 100; else 0;']).toEqual([ 0, 100, 0, 100, ]); await view.delete(); await table.delete(); }); test("coalesce returns first non-null arg", async function () { const table = await perspective.table({ a: "integer", b: "integer", }); const view = await table.view({ expressions: { coalesce_ab: 'coalesce("a", "b")', coalesce_ab_default: 'coalesce("a", "b", 99)', }, }); await table.update({ a: [1, null, null, 4], b: [10, 20, null, 40], }); const result = await view.to_columns(); expect(result["coalesce_ab"]).toEqual([1, 20, null, 4]); expect(result["coalesce_ab_default"]).toEqual([1, 20, 99, 4]); await view.delete(); await table.delete(); }); test("coalesce promotes mixed numeric inputs to float", async function () { const table = await perspective.table({ i: "integer", f: "float", }); const view = await table.view({ expressions: { coalesce_if: 'coalesce("i", "f")', coalesce_if_default: 'coalesce("i", "f", 0.5)', }, }); await table.update({ i: [1, null, null, 4], f: [null, 2.5, null, 4.5], }); const result = await view.to_columns(); const schema = await view.expression_schema(); expect(schema["coalesce_if"]).toEqual("float"); expect(schema["coalesce_if_default"]).toEqual("float"); expect(result["coalesce_if"]).toEqual([1, 2.5, null, 4]); expect(result["coalesce_if_default"]).toEqual([1, 2.5, 0.5, 4]); await view.delete(); await table.delete(); }); test("coalesce with all-null inputs returns null", async function () { const table = await perspective.table({ a: "integer", b: "integer", }); const view = await table.view({ expressions: { coalesce_nulls: 'coalesce("a", "b")' }, }); await table.update({ a: [null, null, null], b: [null, null, null], }); const result = await view.to_columns(); expect(result["coalesce_nulls"]).toEqual([null, null, null]); await view.delete(); await table.delete(); }); test("coalesce fails validation for incompatible types", async function () { const table = await perspective.table({ a: "integer", b: "string", }); const validated = await table.validate_expressions([ 'coalesce("a", "b")', "coalesce(\"a\", 'fallback')", ]); expect(validated.expression_schema).toEqual({}); expect(validated.errors['coalesce("a", "b")']).toEqual({ column: 0, line: 0, error_message: "Type Error - inputs do not resolve to a valid expression.", }); expect(validated.errors["coalesce(\"a\", 'fallback')"]).toEqual( { column: 0, line: 0, error_message: "Type Error - inputs do not resolve to a valid expression.", }, ); await table.delete(); }); test("null", async function () { const table = await perspective.table({ a: "integer", b: "float", }); const view = await table.view({ expressions: [ "float(null)", "string(null)", "integer(null)", ].reduce((x, y) => Object.assign(x, { [y]: y }), {}), }); await table.update({ a: [1, null, null, 4], b: [null, 2.5, null, 4.5], }); const result = await view.to_columns(); expect(result["float(null)"]).toEqual([null, null, null, null]); expect(result["string(null)"]).toEqual([ null, null, null, null, ]); expect(result["integer(null)"]).toEqual([ null, null, null, null, ]); await view.delete(); await table.delete(); }); test("percent_of", async function () { const table = await perspective.table({ a: "integer", b: "float", }); const view = await table.view({ expressions: [ 'percent_of("a", 500)', 'percent_of("a", "b")', "percent_of(1, 3)", ].reduce((x, y) => Object.assign(x, { [y]: y }), {}), }); await table.update({ a: [100, 200, 300, 400], b: [100.5, 200.5, 300.5, 400.5], }); const result = await view.to_columns(); expect(result['percent_of("a", 500)']).toEqual( [100, 200, 300, 400].map((x) => (x / 500) * 100), ); expect(result['percent_of("a", "b")']).toEqual( [100, 200, 300, 400].map( (x, idx) => (x / result["b"][idx]) * 100, ), ); expect(result["percent_of(1, 3)"]).toEqual([ 33.33333333333333, 33.33333333333333, 33.33333333333333, 33.33333333333333, ]); await view.delete(); await table.delete(); }); test("bucket", async function () { const table = await perspective.table({ a: "integer", b: "float", }); const view = await table.view({ expressions: [ 'bucket("a", 5)', 'bucket("b", 2.5)', `bucket("b", 10)`, ].reduce((x, y) => Object.assign(x, { [y]: y }), {}), }); await table.update({ a: [15, 15, 35, 40, 1250, 1255], b: [2.25, 2, 3.5, 16.5, 28, 8], }); const result = await view.to_columns(); expect(result['bucket("a", 5)']).toEqual([ 15, 15, 35, 40, 1250, 1255, ]); expect(result['bucket("b", 2.5)']).toEqual([ 0, 0, 2.5, 15, 27.5, 7.5, ]); expect(result['bucket("b", 10)']).toEqual([0, 0, 0, 10, 20, 0]); await view.delete(); await table.delete(); }); test("random", async function () { const table = await perspective.table({ x: "float", }); const data = []; for (let i = 0; i < 1000; i++) { data.push(i); } await table.update({ x: data }); const view = await table.view({ expressions: { "random()": "random()" }, }); const schema = await view.expression_schema(); expect(schema).toEqual({ "random()": "float" }); const result = await view.to_columns(); for (let i = 0; i < 1000; i++) { const res = result["random()"][i]; expect(res >= 0 && res <= 1).toBe(true); } await view.delete(); await table.delete(); }); }); test.describe("Booleans", function () { test("AND", async function () { const table = await perspective.table(common.comparison_data); const view = await table.view({ expressions: [ '"u" and "u"', '"u" and "z"', '"z" and "z"', "0 and 0", "1 and 1", "1 and 100", "true and false", ].reduce((x, y) => Object.assign(x, { [y]: y }), {}), }); const result = await view.to_columns(); expect(result['"u" and "u"']).toEqual([ false, true, false, true, ]); expect(result['"u" and "z"']).toEqual([ false, false, false, false, ]); expect(result['"u" and "z"']).toEqual([ false, false, false, false, ]); expect(result["0 and 0"]).toEqual([false, false, false, false]); expect(result["1 and 1"]).toEqual([true, true, true, true]); expect(result["1 and 100"]).toEqual([true, true, true, true]); expect(result["true and false"]).toEqual([ false, false, false, false, ]); await view.delete(); await table.delete(); }); test("mand", async function () { const table = await perspective.table(common.comparison_data); const view = await table.view({ expressions: [ 'mand("u" and "u", "u" and "z", "z" and "z")', "mand(true, true, true, true)", "mand(is_null(null), is_not_null(null))", ].reduce((x, y) => Object.assign(x, { [y]: y }), {}), }); const result = await view.to_columns(); expect( result['mand("u" and "u", "u" and "z", "z" and "z")'], ).toEqual(Array(4).fill(false)); expect( result["mand(is_null(null), is_not_null(null))"], ).toEqual(Array(4).fill(false)); expect(result["mand(true, true, true, true)"]).toEqual( Array(4).fill(true), ); await view.delete(); await table.delete(); }); test("mand should fail validation for non boolean inputs", async function () { const table = await perspective.table(common.comparison_data); const validated = await table.validate_expressions([ 'mand("w", "x", "y")', "mand(100, 200, 300)", "mand('a', 'b', 'cdef')", ]); expect(validated.expression_schema).toEqual({}); expect(validated.errors['mand("w", "x", "y")']).toEqual({ column: 0, line: 0, error_message: "Type Error - inputs do not resolve to a valid expression.", }); expect(validated.errors["mand(100, 200, 300)"]).toEqual({ column: 0, line: 0, error_message: "Type Error - inputs do not resolve to a valid expression.", }); expect(validated.errors["mand('a', 'b', 'cdef')"]).toEqual({ column: 0, line: 0, error_message: "Type Error - inputs do not resolve to a valid expression.", }); await table.delete(); }); test("OR", async function () { const table = await perspective.table({ a: [0, 1, 0, 1], b: [0, 1, 0, 1], c: [0, 0, 0, 0], d: [1, 1, 1, 1], e: [false, false, false, false], f: [true, true, true, true], }); const view = await table.view({ expressions: { '"a" or "b"': '"a" or "b"', '"c" or "d"': '"c" or "d"', '"e" or "f"': '"e" or "f"', "0 or 1": "0 or 1", "true or false": "true or false", "false or false": "false or false", filtered: '"a" > 0.5 or "d" < 0.5', }, }); const result = await view.to_columns(); expect(result['"a" or "b"']).toEqual([ false, true, false, true, ]); expect(result['"c" or "d"']).toEqual([true, true, true, true]); expect(result['"e" or "f"']).toEqual([true, true, true, true]); expect(result["0 or 1"]).toEqual([true, true, true, true]); expect(result["true or false"]).toEqual([ true, true, true, true, ]); expect(result["false or false"]).toEqual([ false, false, false, false, ]); expect(result["filtered"]).toEqual([true, true, true, true]); await view.delete(); await table.delete(); }); test("mor", async function () { const table = await perspective.table(common.comparison_data); const view = await table.view({ expressions: [ 'mor("u" and "u", "u" and "z", "z" and "z")', "mor(false, false, false)", "mor(false, true, false)", ].reduce((x, y) => Object.assign(x, { [y]: y }), {}), }); const result = await view.to_columns(); expect( result['mor("u" and "u", "u" and "z", "z" and "z")'], ).toEqual([true, true, true, true]); // The boolean false is a false expect(result["mor(false, false, false)"]).toEqual([ false, false, false, false, ]); expect(result["mor(false, true, false)"]).toEqual([ true, true, true, true, ]); await view.delete(); await table.delete(); }); test("mor should fail validation for non boolean inputs", async function () { const table = await perspective.table(common.comparison_data); const validated = await table.validate_expressions([ 'mor("w", "x", "y")', "mor(100, 200, 300)", "mor('a', 'b', 'cdef')", ]); expect(validated.expression_schema).toEqual({}); expect(validated.errors['mor("w", "x", "y")']).toEqual({ column: 0, line: 0, error_message: "Type Error - inputs do not resolve to a valid expression.", }); expect(validated.errors["mor(100, 200, 300)"]).toEqual({ column: 0, line: 0, error_message: "Type Error - inputs do not resolve to a valid expression.", }); expect(validated.errors["mor('a', 'b', 'cdef')"]).toEqual({ column: 0, line: 0, error_message: "Type Error - inputs do not resolve to a valid expression.", }); await table.delete(); }); test("NAND", async function () { const table = await perspective.table(common.comparison_data); const view = await table.view({ expressions: [ '"u" nand "u"', '"u" nand "z"', '"z" nand "z"', "0 nand 0", "1 nand 1", "1 nand 100", "true nand true", ].reduce((x, y) => Object.assign(x, { [y]: y }), {}), }); const result = await view.to_columns(); expect(result['"u" nand "u"']).toEqual([ true, false, true, false, ]); expect(result['"u" nand "z"']).toEqual([ true, true, true, true, ]); expect(result['"u" nand "z"']).toEqual([ true, true, true, true, ]); expect(result["0 nand 0"]).toEqual([true, true, true, true]); expect(result["1 nand 1"]).toEqual([ false, false, false, false, ]); expect(result["1 nand 100"]).toEqual([ false, false, false, false, ]); expect(result["true nand true"]).toEqual([ false, false, false, false, ]); await view.delete(); await table.delete(); }); test("NOR", async function () { const table = await perspective.table({ a: [0, 1, 0, 1], b: [0, 1, 0, 1], c: [0, 0, 0, 0], d: [1, 1, 1, 1], e: [false, false, false, false], f: [true, true, true, true], }); const view = await table.view({ expressions: [ '"a" nor "b"', '"c" nor "d"', '"e" nor "f"', "0 nor 1", "false nor false", ].reduce((x, y) => Object.assign(x, { [y]: y }), {}), }); const result = await view.to_columns(); expect(result['"a" nor "b"']).toEqual([ true, false, true, false, ]); expect(result['"c" nor "d"']).toEqual([ false, false, false, false, ]); expect(result['"e" nor "f"']).toEqual([ false, false, false, false, ]); expect(result["0 nor 1"]).toEqual([false, false, false, false]); expect(result["false nor false"]).toEqual([ true, true, true, true, ]); await view.delete(); await table.delete(); }); test("XOR", async function () { const table = await perspective.table({ a: [0, 1, 0, 1], b: [0, 1, 0, 1], c: [0, 0, 0, 0], d: [1, 1, 1, 1], e: [false, false, false, false], f: [true, true, true, true], }); const view = await table.view({ expressions: [ '"a" xor "b"', '"c" xor "d"', '"e" xor "f"', "0 xor 1", "false xor false", ].reduce((x, y) => Object.assign(x, { [y]: y }), {}), }); const result = await view.to_columns(); expect(result['"a" xor "b"']).toEqual([ false, false, false, false, ]); expect(result['"c" xor "d"']).toEqual([true, true, true, true]); expect(result['"e" xor "f"']).toEqual([true, true, true, true]); expect(result["0 xor 1"]).toEqual([true, true, true, true]); expect(result["false xor false"]).toEqual([ false, false, false, false, ]); await view.delete(); await table.delete(); }); }); }); })(perspective);