Skip to content

Commit

Permalink
Implement "in" expression (#8876)
Browse files Browse the repository at this point in the history
Co-authored-by: Ádám Barancsuk <[email protected]>
Co-authored-by: Ryan Hamley <[email protected]>
  • Loading branch information
Ryan Hamley and brncsk authored Oct 31, 2019
1 parent dc178a0 commit 9998ef1
Show file tree
Hide file tree
Showing 10 changed files with 288 additions and 0 deletions.
92 changes: 92 additions & 0 deletions src/style-spec/expression/definitions/in.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// @flow

import {ValueType, BooleanType, toString} from '../types';
import RuntimeError from '../runtime_error';
import {typeOf} from '../values';

import type {Expression} from '../expression';
import type ParsingContext from '../parsing_context';
import type EvaluationContext from '../evaluation_context';
import type {Type} from '../types';
import type {Value} from '../values';

function isComparableType(type: Type) {
return type.kind === 'boolean' ||
type.kind === 'string' ||
type.kind === 'number' ||
type.kind === 'null' ||
type.kind === 'value';
}

function isComparableRuntimeValue(needle: boolean | string | number | null) {
return typeof needle === 'boolean' ||
typeof needle === 'string' ||
typeof needle === 'number';
}

function isSearchableRuntimeValue(haystack: Array<Value> | string) {
return Array.isArray(haystack) ||
typeof haystack === 'string';
}

class In implements Expression {
type: Type;
needle: Expression;
haystack: Expression;

constructor(needle: Expression, haystack: Expression) {
this.type = BooleanType;
this.needle = needle;
this.haystack = haystack;
}

static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext) {
if (args.length !== 3) {
return context.error(`Expected 2 arguments, but found ${args.length - 1} instead.`);
}

const needle = context.parse(args[1], 1, ValueType);

const haystack = context.parse(args[2], 2, ValueType);

if (!needle || !haystack) return null;

if (!isComparableType(needle.type)) {
return context.error(`Expected first argument to be of type boolean, string, number or null, but found ${toString(needle.type)} instead`);
}

return new In(needle, haystack);
}

evaluate(ctx: EvaluationContext) {
const needle = (this.needle.evaluate(ctx): any);
const haystack = (this.haystack.evaluate(ctx): any);

if (!needle || !haystack) return false;

if (!isComparableRuntimeValue(needle)) {
throw new RuntimeError(`Expected first argument to be of type boolean, string or number, but found ${toString(typeOf(needle))} instead.`);
}

if (!isSearchableRuntimeValue(haystack)) {
throw new RuntimeError(`Expected second argument to be of type array or string, but found ${toString(typeOf(haystack))} instead.`);
}

return haystack.indexOf(needle) >= 0;
}

eachChild(fn: (Expression) => void) {
fn(this.needle);
fn(this.haystack);
}

possibleOutputs() {
return [true, false];
}

serialize() {
return ["in", this.needle.serialize(), this.haystack.serialize()];
}
}

export default In;
2 changes: 2 additions & 0 deletions src/style-spec/expression/definitions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import Literal from './literal';
import Assertion from './assertion';
import Coercion from './coercion';
import At from './at';
import In from './in';
import Match from './match';
import Case from './case';
import Step from './step';
Expand Down Expand Up @@ -61,6 +62,7 @@ const expressions: ExpressionRegistry = {
'collator': CollatorExpression,
'format': FormatExpression,
'image': ImageExpression,
'in': In,
'interpolate': Interpolate,
'interpolate-hcl': Interpolate,
'interpolate-lab': Interpolate,
Expand Down
1 change: 1 addition & 0 deletions src/style-spec/feature_filter/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ function isExpressionFilter(filter: any) {
return filter.length >= 2 && filter[1] !== '$id' && filter[1] !== '$type';

case 'in':
return filter.length >= 3 && Array.isArray(filter[2]);
case '!in':
case '!has':
case 'none':
Expand Down
9 changes: 9 additions & 0 deletions src/style-spec/reference/v8.json
Original file line number Diff line number Diff line change
Expand Up @@ -2606,6 +2606,15 @@
}
}
},
"in": {
"doc": "Determines whether an item exists in an array or a substring exists in a string.",
"group": "Lookup",
"sdk-support": {
"basic functionality": {
"js": "1.6.0"
}
}
},
"case": {
"doc": "Selects the first output whose corresponding test condition evaluates to true, or the fallback value otherwise.",
"group": "Decision",
Expand Down
30 changes: 30 additions & 0 deletions test/integration/expression-tests/in/assert-array/test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"expression": [
"boolean",
["in", ["get", "i"], ["array", ["get", "arr"]]]
],
"inputs": [
[{}, {"properties": {"i": null, "arr": [9, 8, 7]}}],
[{}, {"properties": {"i": 1, "arr": [9, 8, 7]}}],
[{}, {"properties": {"i": 9, "arr": [9, 8, 7]}}],
[{}, {"properties": {"i": 1, "arr": null}}]
],
"expected": {
"compiled": {
"result": "success",
"isFeatureConstant": false,
"isZoomConstant": true,
"type": "boolean"
},
"outputs": [
false,
false,
true,
{"error":"Expected value to be of type array, but found null instead."}
],
"serialized": [
"boolean",
["in", ["get", "i"], ["array", ["get", "arr"]]]
]
}
}
30 changes: 30 additions & 0 deletions test/integration/expression-tests/in/assert-string/test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"expression": [
"boolean",
["in", ["get", "substr"], ["string", ["get", "str"]]]
],
"inputs": [
[{}, {"properties": {"substr": null, "str": "helloworld"}}],
[{}, {"properties": {"substr": "foo", "str": "helloworld"}}],
[{}, {"properties": {"substr": "low", "str": "helloworld"}}],
[{}, {"properties": {"substr": "low", "str": null}}]
],
"expected": {
"compiled": {
"result": "success",
"isFeatureConstant": false,
"isZoomConstant": true,
"type": "boolean"
},
"outputs": [
false,
false,
true,
{"error":"Expected value to be of type string, but found null instead."}
],
"serialized": [
"boolean",
["in", ["get", "substr"], ["string", ["get", "str"]]]
]
}
}
34 changes: 34 additions & 0 deletions test/integration/expression-tests/in/basic-array/test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"expression": [
"boolean",
["in", ["get", "i"], ["get", "arr"]]
],
"inputs": [
[{}, {"properties": {"i": null, "arr": [9, 8, 7]}}],
[{}, {"properties": {"i": 1, "arr": [9, 8, 7]}}],
[{}, {"properties": {"i": 9, "arr": [9, 8, 7]}}],
[{}, {"properties": {"i": "foo", "arr": ["baz", "bar", "hello", "foo", "world"]}}],
[{}, {"properties": {"i": true, "arr": ["foo", 123, null, 456, false, {}, true]}}],
[{}, {"properties": {"i": 1, "arr": null}}]
],
"expected": {
"compiled": {
"result": "success",
"isFeatureConstant": false,
"isZoomConstant": true,
"type": "boolean"
},
"outputs": [
false,
false,
true,
true,
true,
false
],
"serialized": [
"boolean",
["in", ["get", "i"], ["get", "arr"]]
]
}
}
36 changes: 36 additions & 0 deletions test/integration/expression-tests/in/basic-string/test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"expression": [
"boolean",
["in", ["get", "substr"], ["get", "str"]]
],
"inputs": [
[{}, {"properties": {"substr": null, "str": "helloworld"}}],
[{}, {"properties": {"substr": "foo", "str": "helloworld"}}],
[{}, {"properties": {"substr": "low", "str": "helloworld"}}],
[{}, {"properties": {"substr": true, "str": "falsetrue"}}],
[{}, {"properties": {"substr": false, "str": "falsetrue"}}],
[{}, {"properties": {"substr": 123, "str": "hello123world"}}],
[{}, {"properties": {"substr": "low", "str": null}}]
],
"expected": {
"compiled": {
"result": "success",
"isFeatureConstant": false,
"isZoomConstant": true,
"type": "boolean"
},
"outputs": [
false,
false,
true,
true,
false,
true,
false
],
"serialized": [
"boolean",
["in", ["get", "substr"], ["get", "str"]]
]
}
}
28 changes: 28 additions & 0 deletions test/integration/expression-tests/in/invalid-haystack/test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"expression": [
"boolean",
["in", ["get", "needle"], ["get", "haystack"]]
],
"inputs": [
[{}, {"properties": {"needle": 1, "haystack": 123}}],
[{}, {"properties": {"needle": "foo", "haystack": {}}}],
[{}, {"properties": {"needle": "foo", "haystack": null}}]
],
"expected": {
"compiled": {
"result": "success",
"isFeatureConstant": false,
"isZoomConstant": true,
"type": "boolean"
},
"outputs": [
{"error":"Expected second argument to be of type array or string, but found number instead."},
{"error":"Expected second argument to be of type array or string, but found object instead."},
false
],
"serialized": [
"boolean",
["in", ["get", "needle"], ["get", "haystack"]]
]
}
}
26 changes: 26 additions & 0 deletions test/integration/expression-tests/in/invalid-needle/test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"expression": [
"boolean",
["in", ["get", "needle"], ["get", "haystack"]]
],
"inputs": [
[{}, {"properties": {"needle": {}, "haystack": [9, 8, 7]}}],
[{}, {"properties": {"needle": {}, "haystack": "helloworld"}}]
],
"expected": {
"compiled": {
"result": "success",
"isFeatureConstant": false,
"isZoomConstant": true,
"type": "boolean"
},
"outputs": [
{"error":"Expected first argument to be of type boolean, string or number, but found object instead."},
{"error":"Expected first argument to be of type boolean, string or number, but found object instead."}
],
"serialized": [
"boolean",
["in", ["get", "needle"], ["get", "haystack"]]
]
}
}

0 comments on commit 9998ef1

Please sign in to comment.