Skip to content

Commit 9998ef1

Browse files
Ryan Hamleybrncsk
andauthored
Implement "in" expression (#8876)
Co-authored-by: Ádám Barancsuk <adam.barancsuk@gmail.com> Co-authored-by: Ryan Hamley <ryan.hamley@mapbox.com>
1 parent dc178a0 commit 9998ef1

File tree

10 files changed

+288
-0
lines changed

10 files changed

+288
-0
lines changed
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// @flow
2+
3+
import {ValueType, BooleanType, toString} from '../types';
4+
import RuntimeError from '../runtime_error';
5+
import {typeOf} from '../values';
6+
7+
import type {Expression} from '../expression';
8+
import type ParsingContext from '../parsing_context';
9+
import type EvaluationContext from '../evaluation_context';
10+
import type {Type} from '../types';
11+
import type {Value} from '../values';
12+
13+
function isComparableType(type: Type) {
14+
return type.kind === 'boolean' ||
15+
type.kind === 'string' ||
16+
type.kind === 'number' ||
17+
type.kind === 'null' ||
18+
type.kind === 'value';
19+
}
20+
21+
function isComparableRuntimeValue(needle: boolean | string | number | null) {
22+
return typeof needle === 'boolean' ||
23+
typeof needle === 'string' ||
24+
typeof needle === 'number';
25+
}
26+
27+
function isSearchableRuntimeValue(haystack: Array<Value> | string) {
28+
return Array.isArray(haystack) ||
29+
typeof haystack === 'string';
30+
}
31+
32+
class In implements Expression {
33+
type: Type;
34+
needle: Expression;
35+
haystack: Expression;
36+
37+
constructor(needle: Expression, haystack: Expression) {
38+
this.type = BooleanType;
39+
this.needle = needle;
40+
this.haystack = haystack;
41+
}
42+
43+
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext) {
44+
if (args.length !== 3) {
45+
return context.error(`Expected 2 arguments, but found ${args.length - 1} instead.`);
46+
}
47+
48+
const needle = context.parse(args[1], 1, ValueType);
49+
50+
const haystack = context.parse(args[2], 2, ValueType);
51+
52+
if (!needle || !haystack) return null;
53+
54+
if (!isComparableType(needle.type)) {
55+
return context.error(`Expected first argument to be of type boolean, string, number or null, but found ${toString(needle.type)} instead`);
56+
}
57+
58+
return new In(needle, haystack);
59+
}
60+
61+
evaluate(ctx: EvaluationContext) {
62+
const needle = (this.needle.evaluate(ctx): any);
63+
const haystack = (this.haystack.evaluate(ctx): any);
64+
65+
if (!needle || !haystack) return false;
66+
67+
if (!isComparableRuntimeValue(needle)) {
68+
throw new RuntimeError(`Expected first argument to be of type boolean, string or number, but found ${toString(typeOf(needle))} instead.`);
69+
}
70+
71+
if (!isSearchableRuntimeValue(haystack)) {
72+
throw new RuntimeError(`Expected second argument to be of type array or string, but found ${toString(typeOf(haystack))} instead.`);
73+
}
74+
75+
return haystack.indexOf(needle) >= 0;
76+
}
77+
78+
eachChild(fn: (Expression) => void) {
79+
fn(this.needle);
80+
fn(this.haystack);
81+
}
82+
83+
possibleOutputs() {
84+
return [true, false];
85+
}
86+
87+
serialize() {
88+
return ["in", this.needle.serialize(), this.haystack.serialize()];
89+
}
90+
}
91+
92+
export default In;

src/style-spec/expression/definitions/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import Literal from './literal';
2323
import Assertion from './assertion';
2424
import Coercion from './coercion';
2525
import At from './at';
26+
import In from './in';
2627
import Match from './match';
2728
import Case from './case';
2829
import Step from './step';
@@ -61,6 +62,7 @@ const expressions: ExpressionRegistry = {
6162
'collator': CollatorExpression,
6263
'format': FormatExpression,
6364
'image': ImageExpression,
65+
'in': In,
6466
'interpolate': Interpolate,
6567
'interpolate-hcl': Interpolate,
6668
'interpolate-lab': Interpolate,

src/style-spec/feature_filter/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ function isExpressionFilter(filter: any) {
2121
return filter.length >= 2 && filter[1] !== '$id' && filter[1] !== '$type';
2222

2323
case 'in':
24+
return filter.length >= 3 && Array.isArray(filter[2]);
2425
case '!in':
2526
case '!has':
2627
case 'none':

src/style-spec/reference/v8.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2606,6 +2606,15 @@
26062606
}
26072607
}
26082608
},
2609+
"in": {
2610+
"doc": "Determines whether an item exists in an array or a substring exists in a string.",
2611+
"group": "Lookup",
2612+
"sdk-support": {
2613+
"basic functionality": {
2614+
"js": "1.6.0"
2615+
}
2616+
}
2617+
},
26092618
"case": {
26102619
"doc": "Selects the first output whose corresponding test condition evaluates to true, or the fallback value otherwise.",
26112620
"group": "Decision",
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"expression": [
3+
"boolean",
4+
["in", ["get", "i"], ["array", ["get", "arr"]]]
5+
],
6+
"inputs": [
7+
[{}, {"properties": {"i": null, "arr": [9, 8, 7]}}],
8+
[{}, {"properties": {"i": 1, "arr": [9, 8, 7]}}],
9+
[{}, {"properties": {"i": 9, "arr": [9, 8, 7]}}],
10+
[{}, {"properties": {"i": 1, "arr": null}}]
11+
],
12+
"expected": {
13+
"compiled": {
14+
"result": "success",
15+
"isFeatureConstant": false,
16+
"isZoomConstant": true,
17+
"type": "boolean"
18+
},
19+
"outputs": [
20+
false,
21+
false,
22+
true,
23+
{"error":"Expected value to be of type array, but found null instead."}
24+
],
25+
"serialized": [
26+
"boolean",
27+
["in", ["get", "i"], ["array", ["get", "arr"]]]
28+
]
29+
}
30+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"expression": [
3+
"boolean",
4+
["in", ["get", "substr"], ["string", ["get", "str"]]]
5+
],
6+
"inputs": [
7+
[{}, {"properties": {"substr": null, "str": "helloworld"}}],
8+
[{}, {"properties": {"substr": "foo", "str": "helloworld"}}],
9+
[{}, {"properties": {"substr": "low", "str": "helloworld"}}],
10+
[{}, {"properties": {"substr": "low", "str": null}}]
11+
],
12+
"expected": {
13+
"compiled": {
14+
"result": "success",
15+
"isFeatureConstant": false,
16+
"isZoomConstant": true,
17+
"type": "boolean"
18+
},
19+
"outputs": [
20+
false,
21+
false,
22+
true,
23+
{"error":"Expected value to be of type string, but found null instead."}
24+
],
25+
"serialized": [
26+
"boolean",
27+
["in", ["get", "substr"], ["string", ["get", "str"]]]
28+
]
29+
}
30+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"expression": [
3+
"boolean",
4+
["in", ["get", "i"], ["get", "arr"]]
5+
],
6+
"inputs": [
7+
[{}, {"properties": {"i": null, "arr": [9, 8, 7]}}],
8+
[{}, {"properties": {"i": 1, "arr": [9, 8, 7]}}],
9+
[{}, {"properties": {"i": 9, "arr": [9, 8, 7]}}],
10+
[{}, {"properties": {"i": "foo", "arr": ["baz", "bar", "hello", "foo", "world"]}}],
11+
[{}, {"properties": {"i": true, "arr": ["foo", 123, null, 456, false, {}, true]}}],
12+
[{}, {"properties": {"i": 1, "arr": null}}]
13+
],
14+
"expected": {
15+
"compiled": {
16+
"result": "success",
17+
"isFeatureConstant": false,
18+
"isZoomConstant": true,
19+
"type": "boolean"
20+
},
21+
"outputs": [
22+
false,
23+
false,
24+
true,
25+
true,
26+
true,
27+
false
28+
],
29+
"serialized": [
30+
"boolean",
31+
["in", ["get", "i"], ["get", "arr"]]
32+
]
33+
}
34+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"expression": [
3+
"boolean",
4+
["in", ["get", "substr"], ["get", "str"]]
5+
],
6+
"inputs": [
7+
[{}, {"properties": {"substr": null, "str": "helloworld"}}],
8+
[{}, {"properties": {"substr": "foo", "str": "helloworld"}}],
9+
[{}, {"properties": {"substr": "low", "str": "helloworld"}}],
10+
[{}, {"properties": {"substr": true, "str": "falsetrue"}}],
11+
[{}, {"properties": {"substr": false, "str": "falsetrue"}}],
12+
[{}, {"properties": {"substr": 123, "str": "hello123world"}}],
13+
[{}, {"properties": {"substr": "low", "str": null}}]
14+
],
15+
"expected": {
16+
"compiled": {
17+
"result": "success",
18+
"isFeatureConstant": false,
19+
"isZoomConstant": true,
20+
"type": "boolean"
21+
},
22+
"outputs": [
23+
false,
24+
false,
25+
true,
26+
true,
27+
false,
28+
true,
29+
false
30+
],
31+
"serialized": [
32+
"boolean",
33+
["in", ["get", "substr"], ["get", "str"]]
34+
]
35+
}
36+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"expression": [
3+
"boolean",
4+
["in", ["get", "needle"], ["get", "haystack"]]
5+
],
6+
"inputs": [
7+
[{}, {"properties": {"needle": 1, "haystack": 123}}],
8+
[{}, {"properties": {"needle": "foo", "haystack": {}}}],
9+
[{}, {"properties": {"needle": "foo", "haystack": null}}]
10+
],
11+
"expected": {
12+
"compiled": {
13+
"result": "success",
14+
"isFeatureConstant": false,
15+
"isZoomConstant": true,
16+
"type": "boolean"
17+
},
18+
"outputs": [
19+
{"error":"Expected second argument to be of type array or string, but found number instead."},
20+
{"error":"Expected second argument to be of type array or string, but found object instead."},
21+
false
22+
],
23+
"serialized": [
24+
"boolean",
25+
["in", ["get", "needle"], ["get", "haystack"]]
26+
]
27+
}
28+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"expression": [
3+
"boolean",
4+
["in", ["get", "needle"], ["get", "haystack"]]
5+
],
6+
"inputs": [
7+
[{}, {"properties": {"needle": {}, "haystack": [9, 8, 7]}}],
8+
[{}, {"properties": {"needle": {}, "haystack": "helloworld"}}]
9+
],
10+
"expected": {
11+
"compiled": {
12+
"result": "success",
13+
"isFeatureConstant": false,
14+
"isZoomConstant": true,
15+
"type": "boolean"
16+
},
17+
"outputs": [
18+
{"error":"Expected first argument to be of type boolean, string or number, but found object instead."},
19+
{"error":"Expected first argument to be of type boolean, string or number, but found object instead."}
20+
],
21+
"serialized": [
22+
"boolean",
23+
["in", ["get", "needle"], ["get", "haystack"]]
24+
]
25+
}
26+
}

0 commit comments

Comments
 (0)