Last active
June 7, 2019 23:37
-
-
Save eburlingame/7186971307143116dd7cc631bae24756 to your computer and use it in GitHub Desktop.
Todoist Filter Expressions
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const BINARY_EXPRESSION = "BinaryExpression"; | |
const UNARY_EXPRESSION = "UnaryExpression"; | |
const IDENTIFIER = "Identifier"; | |
const LITERAL = "Literal"; | |
const SYMBOL_DECORATORS = ["@", "#"]; | |
const binOps = { | |
"|": (l, r, env) => (l || r), | |
"&": (l, r, env) => (l && r) | |
}; | |
const unOps = { | |
"@": (v, env) => (env.labels.includes(v)), | |
"#": (v, env) => (v === env.project), | |
"!": (v, env) => (!v) | |
}; | |
function evalSymbol(expr, env) { | |
switch (expr.type) { | |
case IDENTIFIER: { | |
return expr.name; | |
} break; | |
case LITERAL: { | |
return expr.raw; | |
} break; | |
default: { | |
throw `Invalid id: ${JSON.stringify(expr)}`; | |
} | |
} | |
} | |
function evalExpression(expr, env) { | |
switch(expr.type) { | |
case BINARY_EXPRESSION: { | |
const res = binOps[expr.operator]( | |
evalExpression(expr.left, env), | |
evalExpression(expr.right, env), | |
env, | |
); | |
return res; | |
}; | |
case UNARY_EXPRESSION: { | |
let subexpr; | |
// Do not recurse on symbol decorators (@ and #) | |
if (SYMBOL_DECORATORS.includes(expr.operator)) { | |
subexpr = evalSymbol(expr.argument); | |
} else { | |
subexpr = evalExpression(expr.argument, env); | |
} | |
return unOps[expr.operator](subexpr, env); | |
}; | |
default: { | |
throw `Unexpected expression: ${JSON.stringify(expr)}` | |
} break; | |
} | |
} | |
function compileFilterExpression(strExpr) { | |
const jsep = require("jsep"); | |
// Only use a subset of ops | |
jsep.removeAllUnaryOps(); | |
jsep.removeAllBinaryOps(); | |
jsep.removeAllLiterals(); | |
jsep.addUnaryOp('@', 1); | |
jsep.addUnaryOp('#', 2); | |
jsep.addUnaryOp('!', 3); | |
jsep.addBinaryOp('&', 4); | |
jsep.addBinaryOp('|', 5); | |
// Return the compiled expression AST | |
return jsep(strExpr); | |
} | |
function evaluateFilterExpression(compiledExpr, item) { | |
const e = { | |
labels: item.labels, | |
project: item.project | |
} | |
// Evaluate the expression | |
return evalExpression(compiledExpr, e); | |
} | |
module.exports = { | |
evaluateFilterExpression: evaluateFilterExpression, | |
compileFilterExpression: compileFilterExpression | |
}; | |
// console.log( | |
// evalExpression( | |
// compileFilterExpression("@Label8 & @Label6 & #Project"), | |
// { | |
// project: "Project", | |
// labels: ["Label6", "Label8"] | |
// }) | |
// ) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const expr = require('./expr'); | |
// compileFilterExpression tests | |
test("Single unary @ AST", () => | |
expect(expr.compileFilterExpression("@Computer")) | |
.toEqual({ | |
"argument": { | |
"name": "Computer", | |
"type": "Identifier", | |
}, | |
"operator": "@", | |
"prefix": true, | |
"type": "UnaryExpression" | |
}) | |
); | |
test("Single unary ! AST", () => | |
expect(expr.compileFilterExpression("!@Computer")) | |
.toEqual({ | |
"argument": { | |
"argument": { | |
"name": "Computer", | |
"type": "Identifier", | |
}, | |
"operator": "@", | |
"prefix": true, | |
"type": "UnaryExpression", | |
}, | |
"operator": "!", | |
"prefix": true, | |
"type": "UnaryExpression", | |
}) | |
); | |
test("Two binops and labels", () => | |
expect(expr.compileFilterExpression("@Computer | (#Test & #Test)")) | |
.toEqual({ | |
"left": { | |
"argument": { | |
"name": "Computer", | |
"type": "Identifier", | |
}, | |
"operator": "@", | |
"prefix": true, | |
"type": "UnaryExpression", | |
}, | |
"operator": "|", | |
"right": { | |
"left": { | |
"argument": { | |
"name": "Test", | |
"type": "Identifier", | |
}, | |
"operator": "#", | |
"prefix": true, | |
"type": "UnaryExpression", | |
}, | |
"operator": "&", | |
"right": { | |
"argument": { | |
"name": "Test", | |
"type": "Identifier", | |
}, | |
"operator": "#", | |
"prefix": true, | |
"type": "UnaryExpression", | |
}, | |
"type": "BinaryExpression", | |
}, | |
"type": "BinaryExpression", | |
}) | |
); | |
// evaluateFilterExpression tests | |
const evalExpression = (exprStr, item) => ( | |
expr.evaluateFilterExpression( | |
expr.compileFilterExpression(exprStr), | |
item) | |
); | |
test("Single project", () => expect( | |
evalExpression( | |
"#Project", | |
{ | |
project: "Project", | |
labels: [] | |
})) | |
.toBe(true) | |
); | |
test("Not single project", () => expect( | |
evalExpression( | |
"!#Project", | |
{ | |
project: "Project", | |
labels: [] | |
})) | |
.toBe(false) | |
); | |
test("Single label", () => expect( | |
evalExpression( | |
"@Label", | |
{ | |
project: "", | |
labels: ["Label"] | |
})) | |
.toBe(true) | |
); | |
test("Not single label", () => expect( | |
evalExpression( | |
"!@Label", | |
{ | |
project: "", | |
labels: ["Label"] | |
})) | |
.toBe(false) | |
); | |
test("Label or project", () => expect( | |
evalExpression( | |
"!@Label | #Project", | |
{ | |
project: "Project", | |
labels: ["Label"] | |
})) | |
.toBe(true) | |
); | |
test("Label and project", () => expect( | |
evalExpression( | |
"!@Label & #Project", | |
{ | |
project: "Project", | |
labels: ["Label"] | |
})) | |
.toBe(false) | |
); | |
test("And subexpression", () => expect( | |
evalExpression( | |
"#Project & (@Label1 & @Label2)", | |
{ | |
project: "Project", | |
labels: ["Label1", "Label2"] | |
})) | |
.toBe(true) | |
); | |
test("Or subexpression", () => expect( | |
evalExpression( | |
"#Project & (@Label1 | @Label2)", | |
{ | |
project: "Project", | |
labels: ["Label1", "Label5"] | |
})) | |
.toBe(true) | |
); | |
test("Compound ors", () => expect( | |
evalExpression( | |
"@Label6 | @Label7 | #Project", | |
{ | |
project: "Project", | |
labels: ["Label1", "Label5"] | |
})) | |
.toBe(true) | |
); | |
test("Compound ands false", () => expect( | |
evalExpression( | |
"@Label8 & @Label6 & #Project", | |
{ | |
project: "Project", | |
labels: ["Label1", "Label5"] | |
})) | |
.toBe(false) | |
); | |
test("Compound ands true", () => expect( | |
evalExpression( | |
"@Label8 & @Label6 & #Project", | |
{ | |
project: "Project", | |
labels: ["Label6", "Label8"] | |
})) | |
.toBe(true) | |
); | |
test("Compound ands with nested", () => expect( | |
evalExpression( | |
"@Label8 & @Label6 & (#Project2 | (((@Label15))))", | |
{ | |
project: "Project", | |
labels: ["Label6", "Label8", "Label15"] | |
})) | |
.toBe(true) | |
); | |
test("Wrong project", () => expect( | |
evalExpression( | |
"(((((#Project2)))))", | |
{ | |
project: "Project", | |
labels: ["Label1", "Label5"] | |
})) | |
.toBe(false) | |
); | |
test("Numeric project", () => expect( | |
evalExpression( | |
"#3.01", | |
{ | |
project: "3.01", | |
labels: [] | |
})) | |
.toBe(true) | |
); | |
test("Invalid label id", () => expect( | |
() => evalExpression( | |
"@(#Project|@Label)", | |
{ | |
project: "Project", | |
labels: ["Label"] | |
})) | |
.toThrow(/Invalid/) | |
); | |
test("Invalid project id", () => expect( | |
() => evalExpression( | |
"#(#Project|@Label)", | |
{ | |
project: "Project", | |
labels: ["Label"] | |
})) | |
.toThrow(/Invalid/) | |
); | |
test("Random symbols", () => expect( | |
() => evalExpression( | |
"these|arent|valid|symbols", | |
{ | |
project: "Project", | |
labels: ["Label"] | |
})) | |
.toThrow(/Unexpected expression/) | |
); | |
test("Math expression", () => expect( | |
() => evalExpression( | |
"@1+234", | |
{ | |
project: "Project", | |
labels: ["Label"] | |
})) | |
.toThrow(/Unexpected/) | |
); | |
test("Numeric literal", () => expect( | |
() => evalExpression( | |
"@1|234", | |
{ | |
project: "Project", | |
labels: ["Label"] | |
})) | |
.toThrow(/Unexpected/) | |
); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment