Skip to content

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:

JavaScript
// 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:

JavaScript
// 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

JavaScript
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

JavaScript
const results = df.filter(adultAndEngineer.toFunction());

Reuse: Apply the same condition to other DataFrames

JavaScript
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:

JavaScript
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:

JavaScript
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:

JavaScript
// 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:

JavaScript
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:

  1. Reuse: Build once, use many times
  2. Same condition applied to multiple DataFrames
  3. Conditions saved in variables and combined

  4. Clarity: Separates concerns

  5. Building reads like "what am I filtering for?"
  6. Evaluation reads like "apply this filter"

  7. Optimization: Compiler can optimize predicates

  8. No interpretation at filter time
  9. No repeated parsing of the condition

  10. Composability: Conditions are first-class values

  11. Store them, pass them, log them
  12. Build libraries of reusable filters

Handling Edge Cases

Null Values

Expressions handle nulls explicitly:

JavaScript
// 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:

  1. Validate first: Use FlameFrame to validate input
  2. Then build conditions: Expression works on validated data
  3. 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:

  1. Add the operation to the type:

    TypeScript
    type ExpressionOperation = "gt" | "ge" | ... | "between";
    

  2. Add the leaf method:

    TypeScript
    between(min: number, max: number): Expression {
      // Create new Expression instance with this operation
    }
    

  3. Add the evaluator in the evaluation logic:

    TypeScript
    case "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.