Skip to content

feat: add filter() macro for array filtering#384

Open
sheshnath08 wants to merge 2 commits intoschibsted:masterfrom
sheshnath08:feature/filter-macro
Open

feat: add filter() macro for array filtering#384
sheshnath08 wants to merge 2 commits intoschibsted:masterfrom
sheshnath08:feature/filter-macro

Conversation

@sheshnath08
Copy link
Copy Markdown

@sheshnath08 sheshnath08 commented Mar 15, 2026

Summary

Adds a built-in filter(array, predicate) macro that returns only the
elements of an array for which the predicate expression is truthy.

Why a macro (not a function)?

The predicate must be re-evaluated with each element as the context
node (.). A regular function receives arguments already evaluated to
a single value, so the predicate would be lost. This is the same reason
for is a language construct and fallback is a macro.

Behaviour

Input Result
filter(null, expr) null
filter([], expr) []
filter(nonArray, expr) JsltException
falsy predicate result element excluded

Examples

filter(.items, .active)
filter([1, 2, 3, 4, 5], . > 3)                  // => [4, 5]
size(filter(.items, .active))
[for (filter(.items, .active)) .id]

let active = filter(.items, .active)
{ "count": size($active), "ids": [for ($active) .id] }

Changes

File Change
core/src/main/java/com/schibsted/spt/data/jslt/impl/BuiltinFunctions.java Filter macro class + registered in macros map
core/src/test/resources/filter-tests.json 6 happy-path test cases
core/src/test/resources/filter-error-tests.json 1 error test case (non-array input)
core/src/test/java/com/schibsted/spt/data/jslt/QueryTest.java Load filter-tests.json
core/src/test/java/com/schibsted/spt/data/jslt/QueryErrorTest.java Load filter-error-tests.json
functions.md Documented filter() alongside fallback

Implementation Details

Filter extends AbstractMacro (same base as fallback) with a fixed
arity of 2. At runtime:

  1. Evaluates parameters[0] once to get the array
  2. Returns null if the array is null; throws JsltException if not an array
  3. For each element, evaluates parameters[1] with that element as the context node
  4. Keeps elements where NodeUtils.isTrue(result) — consistent with how for handles its if clause

No grammar or parser changes were needed — macros are dispatched by name
via ParseContext.getMacro() in ParserImpl.chainable2Expr(), the same
mechanism used by fallback.

Related

Adds a built-in `filter(array, predicate)` macro that returns only the
elements of an array for which the predicate expression is truthy.

Must be a macro (not a function) so the predicate is re-evaluated with
each element as the context node (`.`), matching the same pattern used
by the existing `for` expression and `fallback` macro.

Behaviour follows existing JSLT conventions:
- null input → null (matches `for` expression)
- non-array input → JsltException (matches `for` expression)
- empty array → []
- falsy/null predicate result → element excluded

Includes dedicated test files (filter-tests.json,
filter-error-tests.json) registered in QueryTest and QueryErrorTest.
@larsga
Copy link
Copy Markdown
Collaborator

larsga commented Mar 19, 2026

I guess the question is whether this is necessary, since for expressions already do this.

For example, filter([1, 2, 3, 4, 5], . > 3)

can be written [for ([1, 2, 3, 4, 5]) . if (. > 3)].

The gain in brevity is very small.

@sheshnath08
Copy link
Copy Markdown
Author

Yeah, for+if works — but I think there's still value in having filter() explicitly.

The main thing for me is readability: filter(.items, .active) is immediately obvious, whereas [for (.items) . if (.active)] makes you stop and mentally parse the for to realise it's just filtering. And composability gets awkward too — size(filter(.items, .active)) is cleaner than wrapping the whole for in brackets.

There's also a discoverability angle: people coming from JS/Python will just search for filter and not find it, then probably open a discussion (like #227).

That said, if the preference here is to keep built-ins minimal, I get it — happy to close if that's the direction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants