Expressions and Predicates¶
Summary¶
The Expression class lets you build boolean filter conditions for DataFrames using chainable method calls.
Instead of writing manual loops with nested conditionals, you build conditions step-by-step:
// Instead of: "get rows where age > 18 AND department == 'Engineering'"
const filtered = df.filter(
gaslamp.Expression.col('age').gt(18)
.and(gaslamp.Expression.col('department').eq('Engineering'))
.toFunction()
);
Expressions compile to fast, reusable predicate functions.
Philosophy: Make Complex Conditions Safe and Readable¶
In standard Google Apps Script, filtering rows with complex conditions is tedious and error-prone:
// Standard GAS: manual loop with nested conditions
const results = [];
for (const row of data) {
if (row.age > 18 && row.department === 'Engineering' && !row.inactive) {
results.push(row);
}
}
Problems with this approach:
- Repetitive: Every complex filter requires a new loop
- Error-prone: Easy to mix up operators, forget parentheses, or confuse logic
- Hard to reuse: Can't save a condition and apply it elsewhere
- Unclear intent: The condition is buried in imperative code
The Solution: Builder Pattern for Conditions¶
gaslamp's Expression class separates building a condition from evaluating it.
Build step: Create the condition as a reusable object
const adultAndEngineer = gaslamp.Expression.col('age').gt(18)
.and(gaslamp.Expression.col('department').eq('Engineering'));
Evaluate step: Turn it into a function and apply it
const results = df.filter(adultAndEngineer.toFunction());
Reuse: Apply the same condition to other DataFrames
const other = df2.filter(adultAndEngineer.toFunction());
How It Works: Building and Evaluating¶
Building: Chainable Methods¶
Expression provides leaf methods (comparison operations) and logical operators:
Leaf methods — create a condition on a single column:
gaslamp.Expression.col('age').gt(18) // numeric: >, >=, <, <=
gaslamp.Expression.col('name').eq('Alice') // equality: ==, !=
gaslamp.Expression.col('email').contains('@') // string: contains, startsWith, endsWith
gaslamp.Expression.col('status').isNull() // null checks
Logical operators — combine conditions:
const cond1 = gaslamp.Expression.col('age').gt(18);
const cond2 = gaslamp.Expression.col('status').eq('active');
cond1.and(cond2) // both must be true
cond1.or(cond2) // either must be true
cond1.not() // negate the condition
Operators combine left-to-right. For complex precedence, build step-by-step:
// This means: (age > 18 AND status == 'active') OR country == 'USA'
const complex = gaslamp.Expression.col('age').gt(18)
.and(gaslamp.Expression.col('status').eq('active'))
.or(gaslamp.Expression.col('country').eq('USA'));
Evaluating: toFunction()¶
The .toFunction() method compiles an Expression into a fast, evaluable function:
const expr = gaslamp.Expression.col('age').gt(18);
const predicate = expr.toFunction(); // returns a function that checks age > 18
// Use with DataFrame.filter()
const adults = df.filter(predicate);
// Or use directly with a row Map
const row = new Map([['age', 25], ['name', 'Alice']]);
console.log(predicate(row)); // true
The compiled function:
- Takes a row (a
Map<string, Cell>) as input - Returns a boolean
- Is fast (no interpretation overhead)
Design Insight: Why Separate Building from Evaluating?¶
You might ask: "Why not just evaluate immediately?"
Because:
- Reuse: Build once, use many times
- Same condition applied to multiple DataFrames
-
Conditions saved in variables and combined
-
Clarity: Separates concerns
- Building reads like "what am I filtering for?"
-
Evaluation reads like "apply this filter"
-
Optimization: Compiler can optimize predicates
- No interpretation at filter time
-
No repeated parsing of the condition
-
Composability: Conditions are first-class values
- Store them, pass them, log them
- Build libraries of reusable filters
Handling Edge Cases¶
Null Values¶
Expressions handle nulls explicitly:
// Rows where status is null
df.filter(gaslamp.Expression.col('status').isNull().toFunction());
// Rows where status is not null
df.filter(gaslamp.Expression.col('status').isNotNull().toFunction());
// Comparisons with null: gt/lt/eq all return false for null
// This is SQL-like behavior: null > 18 → false, not undefined
Type Safety¶
Expression assumes column values are the right type. If you need type safety:
- Validate first: Use FlameFrame to validate input
- Then build conditions: Expression works on validated data
- This is intentional: Expression focuses on filtering; validation is FlameFrame's job
Extending Expression: Adding New Operations¶
If you need a new operation (for example, between, in, regex), the pattern is:
-
Add the operation to the type:
TypeScripttype ExpressionOperation = "gt" | "ge" | ... | "between"; -
Add the leaf method:
TypeScriptbetween(min: number, max: number): Expression { // Create new Expression instance with this operation } -
Add the evaluator in the evaluation logic:
TypeScriptcase "between": return value >= min && value <= max;
The architecture is open: new operations fit naturally without changing existing code.
Performance Considerations¶
Compiling: .toFunction() is fast (one-time cost per Expression)
Filtering: Compiled predicates are as fast as hand-written conditionals
Memory: Each Expression node is small (6 fields, all primitive or null)
Don't worry about building many intermediate Expressions during construction—the cost is minimal.
Related¶
- BareFrame-First Design — How DataFrame.filter() uses Expressions
- Cell and Primitive Types — What types can appear in Expression comparisons