Replace the old Lisp-like array expression representation (["AND", ["PARAM", ...], ...]) with a class hierarchy rooted in an abstract Expression class (Literal, Parameter, Binary, Unary, FunctionCall, Conditional). This gives compile-time type safety to expression construction, evaluation, and pattern matching. Key changes: - New lib/common/expression.ts defines the class hierarchy with map/evaluate/toString methods - Rewrite parser as hand-rolled recursive descent, removing the parsimmon dependency - Extend Path with a colon separator to unify parameter identity and attributes into a single address space (e.g. "Device.WiFi.SSID.1.Name:writable"). The colon field tracks how many trailing segments are attribute qualifiers. PathSet rewritten with separate param/attr segment indexes - Flatten device attribute representation in the UI to colon-delimited keys mirroring the new path syntax
219 lines
7.7 KiB
TypeScript
219 lines
7.7 KiB
TypeScript
import Expression from "../expression.ts";
|
|
import { likePatternToRegExp } from "./parser.ts";
|
|
|
|
function compare(
|
|
a: boolean | number | string,
|
|
b: boolean | number | string,
|
|
): number {
|
|
if (typeof a === "boolean") a = +a;
|
|
if (typeof b === "boolean") b = +b;
|
|
if (typeof a !== typeof b) return typeof a === "string" ? 1 : -1;
|
|
return a > b ? 1 : a < b ? -1 : 0;
|
|
}
|
|
|
|
function toNumber(a: boolean | number | string): number {
|
|
switch (typeof a) {
|
|
case "number":
|
|
return a;
|
|
case "boolean":
|
|
return +a;
|
|
case "string":
|
|
return parseFloat(a) || 0;
|
|
}
|
|
}
|
|
|
|
function toString(a: boolean | number | string): string {
|
|
switch (typeof a) {
|
|
case "string":
|
|
return a;
|
|
case "number":
|
|
return a.toString();
|
|
case "boolean":
|
|
return (+a).toString();
|
|
}
|
|
}
|
|
|
|
const regExpCache: WeakMap<Expression.Literal, RegExp> = new WeakMap();
|
|
|
|
export function reduce(exp: Expression): Expression {
|
|
if (exp instanceof Expression.Literal) return exp;
|
|
|
|
if (exp instanceof Expression.Unary) {
|
|
if (exp.operator === "NOT") {
|
|
if (exp.operand instanceof Expression.Literal) {
|
|
if (exp.operand.value == null) return exp.operand;
|
|
return new Expression.Literal(!exp.operand.value);
|
|
} else if (exp.operand instanceof Expression.Unary) {
|
|
if (exp.operand.operator === "NOT") return exp.operand.operand;
|
|
}
|
|
} else if (exp.operator === "IS NULL") {
|
|
if (exp.operand instanceof Expression.Literal) {
|
|
return new Expression.Literal(exp.operand.value == null);
|
|
}
|
|
} else if (exp.operator === "IS NOT NULL") {
|
|
if (exp.operand instanceof Expression.Literal) {
|
|
return new Expression.Literal(exp.operand.value != null);
|
|
}
|
|
}
|
|
} else if (exp instanceof Expression.Binary) {
|
|
if (exp.operator === "AND") {
|
|
return Expression.and(exp.left, exp.right);
|
|
} else if (exp.operator === "OR") {
|
|
return Expression.or(exp.left, exp.right);
|
|
} else if (["=", ">", "<", "<>", ">=", "<="].includes(exp.operator)) {
|
|
if (exp.left instanceof Expression.Literal && exp.left.value == null)
|
|
return exp.left;
|
|
if (exp.right instanceof Expression.Literal && exp.right.value == null)
|
|
return exp.right;
|
|
if (
|
|
exp.left instanceof Expression.Literal &&
|
|
exp.right instanceof Expression.Literal
|
|
) {
|
|
const c = compare(exp.left.value, exp.right.value);
|
|
switch (exp.operator) {
|
|
case "=":
|
|
return new Expression.Literal(c === 0);
|
|
case ">":
|
|
return new Expression.Literal(c > 0);
|
|
case "<":
|
|
return new Expression.Literal(c < 0);
|
|
case "<>":
|
|
return new Expression.Literal(c !== 0);
|
|
case ">=":
|
|
return new Expression.Literal(c >= 0);
|
|
case "<=":
|
|
return new Expression.Literal(c <= 0);
|
|
}
|
|
}
|
|
} else if (["+", "-", "*", "/"].includes(exp.operator)) {
|
|
if (exp.left instanceof Expression.Literal && exp.left.value == null)
|
|
return exp.left;
|
|
if (exp.right instanceof Expression.Literal && exp.right.value == null)
|
|
return exp.right;
|
|
if (
|
|
exp.left instanceof Expression.Literal &&
|
|
exp.right instanceof Expression.Literal
|
|
) {
|
|
const a = toNumber(exp.left.value);
|
|
const b = toNumber(exp.right.value);
|
|
switch (exp.operator) {
|
|
case "+":
|
|
return new Expression.Literal(a + b);
|
|
case "-":
|
|
return new Expression.Literal(a - b);
|
|
case "*":
|
|
return new Expression.Literal(a * b);
|
|
case "/":
|
|
return new Expression.Literal(a / b);
|
|
}
|
|
}
|
|
} else if (exp.operator === "%") {
|
|
if (exp.left instanceof Expression.Literal && exp.left.value == null)
|
|
return exp.left;
|
|
if (exp.right instanceof Expression.Literal && exp.right.value == null)
|
|
return exp.right;
|
|
if (
|
|
exp.left instanceof Expression.Literal &&
|
|
exp.right instanceof Expression.Literal
|
|
) {
|
|
const a = toNumber(exp.left.value);
|
|
const b = Math.trunc(toNumber(exp.right.value));
|
|
if (b === 0) return new Expression.Literal(null);
|
|
return new Expression.Literal(a % b);
|
|
}
|
|
} else if (exp.operator === "||") {
|
|
if (exp.left instanceof Expression.Literal && exp.left.value == null)
|
|
return exp.left;
|
|
if (exp.right instanceof Expression.Literal && exp.right.value == null)
|
|
return exp.right;
|
|
if (
|
|
exp.left instanceof Expression.Literal &&
|
|
exp.right instanceof Expression.Literal
|
|
) {
|
|
const a = toString(exp.left.value);
|
|
const b = toString(exp.right.value);
|
|
return new Expression.Literal(a + b);
|
|
}
|
|
} else if (exp.operator === "LIKE") {
|
|
if (exp.left instanceof Expression.Literal && exp.left.value == null)
|
|
return exp.left;
|
|
if (exp.right instanceof Expression.Literal && exp.right.value == null)
|
|
return exp.right;
|
|
if (
|
|
exp.left instanceof Expression.Literal &&
|
|
exp.right instanceof Expression.Literal
|
|
) {
|
|
const s = toString(exp.left.value);
|
|
let r = regExpCache.get(exp.right);
|
|
if (!r) {
|
|
r = likePatternToRegExp(toString(exp.right.value));
|
|
regExpCache.set(exp.right, r);
|
|
}
|
|
return new Expression.Literal(r.test(s));
|
|
}
|
|
} else if (exp.operator === "NOT LIKE") {
|
|
if (exp.left instanceof Expression.Literal && exp.left.value == null)
|
|
return exp.left;
|
|
if (exp.right instanceof Expression.Literal && exp.right.value == null)
|
|
return exp.right;
|
|
if (
|
|
exp.left instanceof Expression.Literal &&
|
|
exp.right instanceof Expression.Literal
|
|
) {
|
|
const s = toString(exp.left.value);
|
|
let r = regExpCache.get(exp.right);
|
|
if (!r) {
|
|
r = likePatternToRegExp(toString(exp.right.value));
|
|
regExpCache.set(exp.right, r);
|
|
}
|
|
return new Expression.Literal(!r.test(s));
|
|
}
|
|
}
|
|
} else if (exp instanceof Expression.FunctionCall) {
|
|
if (exp.name === "COALESCE") {
|
|
const args = exp.args.filter(
|
|
(arg) => !(arg instanceof Expression.Literal && arg.value == null),
|
|
);
|
|
if (!args.length) return new Expression.Literal(null);
|
|
if (args.length === 1) return args[0];
|
|
if (args[0] instanceof Expression.Literal) return args[0];
|
|
if (args.length !== exp.args.length)
|
|
return new Expression.FunctionCall("COALESCE", args);
|
|
} else if (exp.name === "UPPER") {
|
|
if (exp.args[0] instanceof Expression.Literal) {
|
|
if (exp.args[0].value == null) return exp.args[0];
|
|
const a = toString(exp.args[0].value);
|
|
return new Expression.Literal(a.toUpperCase());
|
|
}
|
|
} else if (exp.name === "LOWER") {
|
|
if (exp.args[0] instanceof Expression.Literal) {
|
|
if (exp.args[0].value == null) return exp.args[0];
|
|
const a = toString(exp.args[0].value);
|
|
return new Expression.Literal(a.toLowerCase());
|
|
}
|
|
} else if (exp.name === "ROUND") {
|
|
let p = 0;
|
|
if (exp.args.length > 1) {
|
|
if (exp.args[1] instanceof Expression.Literal) {
|
|
if (exp.args[1].value == null) return exp.args[1];
|
|
p = Math.trunc(toNumber(exp.args[1].value));
|
|
}
|
|
}
|
|
if (exp.args[0] instanceof Expression.Literal) {
|
|
if (exp.args[0].value == null) return exp.args[0];
|
|
const n = toNumber(exp.args[0].value);
|
|
const d = 10 ** p;
|
|
const m = n * d * (1 + Number.EPSILON);
|
|
return new Expression.Literal(Math.round(m) / d);
|
|
}
|
|
}
|
|
} else if (exp instanceof Expression.Conditional) {
|
|
if (exp.condition instanceof Expression.Literal) {
|
|
if (exp.condition.value) return exp.then;
|
|
return exp.otherwise;
|
|
}
|
|
}
|
|
|
|
return exp;
|
|
}
|