diff --git a/docs/components/expression-metadata.js b/docs/components/expression-metadata.js index 3b08dddbf9d..7a4110fd2f3 100644 --- a/docs/components/expression-metadata.js +++ b/docs/components/expression-metadata.js @@ -156,6 +156,10 @@ const types = { 'stop_input_n: number, stop_output_n: OutputType, ...' ] }], + length: [{ + type: 'number', + parameters: ['string | array | value'] + }], let: [{ type: 'OutputType', parameters: [{ repeat: ['string (alphanumeric literal)', 'any']}, 'OutputType'] diff --git a/src/style-spec/expression/definitions/index.js b/src/style-spec/expression/definitions/index.js index e547731870d..97aa54c6954 100644 --- a/src/style-spec/expression/definitions/index.js +++ b/src/style-spec/expression/definitions/index.js @@ -28,6 +28,7 @@ const Step = require('./step'); const Interpolate = require('./interpolate'); const Coalesce = require('./coalesce'); const {Equals, NotEquals} = require('./equals'); +const Length = require('./length'); import type { ExpressionRegistry } from '../expression'; @@ -41,6 +42,7 @@ const expressions: ExpressionRegistry = { 'case': Case, 'coalesce': Coalesce, 'interpolate': Interpolate, + 'length': Length, 'let': Let, 'literal': Literal, 'match': Match, @@ -72,10 +74,6 @@ function get(key, obj) { return typeof v === 'undefined' ? null : v; } -function length(ctx, [v]) { - return v.evaluate(ctx).length; -} - function lt(ctx, [a, b]) { return a.evaluate(ctx) < b.evaluate(ctx); } function gt(ctx, [a, b]) { return a.evaluate(ctx) > b.evaluate(ctx); } function lteq(ctx, [a, b]) { return a.evaluate(ctx) <= b.evaluate(ctx); } @@ -144,18 +142,6 @@ CompoundExpression.register(expressions, { [NumberType, NumberType, NumberType, NumberType], rgba ], - 'length': { - type: NumberType, - overloads: [ - [ - [StringType], - length - ], [ - [array(ValueType)], - length - ] - ] - }, 'has': { type: BooleanType, overloads: [ diff --git a/src/style-spec/expression/definitions/length.js b/src/style-spec/expression/definitions/length.js new file mode 100644 index 00000000000..4ff5cd484e7 --- /dev/null +++ b/src/style-spec/expression/definitions/length.js @@ -0,0 +1,58 @@ +// @flow + +const { + NumberType, + toString +} = require('../types'); +const {typeOf} = require('../values'); + +const RuntimeError = require('../runtime_error'); + +import type { Expression } from '../expression'; +import type ParsingContext from '../parsing_context'; +import type EvaluationContext from '../evaluation_context'; +import type { Type } from '../types'; + +class Length implements Expression { + type: Type; + input: Expression; + + constructor(input: Expression) { + this.type = NumberType; + this.input = input; + } + + static parse(args: Array, context: ParsingContext) { + if (args.length !== 2) + return context.error(`Expected 1 argument, but found ${args.length - 1} instead.`); + + const input = context.parse(args[1], 1); + if (!input) return null; + + if (input.type.kind !== 'array' && input.type.kind !== 'string' && input.type.kind !== 'value') + return context.error(`Expected argument of type string or array, but found ${toString(input.type)} instead.`); + + return new Length(input); + } + + evaluate(ctx: EvaluationContext) { + const input = this.input.evaluate(ctx); + if (typeof input === 'string') { + return input.length; + } else if (Array.isArray(input)) { + return input.length; + } else { + throw new RuntimeError(`Expected value to be of type string or array, but found ${toString(typeOf(input))} instead.`); + } + } + + eachChild(fn: (Expression) => void) { + fn(this.input); + } + + possibleOutputs() { + return [undefined]; + } +} + +module.exports = Length; diff --git a/test/integration/expression-tests/length/implicit/test.json b/test/integration/expression-tests/length/implicit/test.json new file mode 100644 index 00000000000..a2708af3269 --- /dev/null +++ b/test/integration/expression-tests/length/implicit/test.json @@ -0,0 +1,24 @@ +{ + "expression": ["length", ["get", "x"]], + "inputs": [ + [{}, {"properties": {"x": "a string"}}], + [{}, {"properties": {"x": []}}], + [{}, {"properties": {"x": 0}}] + ], + "expected": { + "compiled": { + "result": "success", + "isFeatureConstant": false, + "isZoomConstant": true, + "type": "number" + }, + "outputs": [ + 8, + 0, + { + "error": "Expected value to be of type string or array, but found number instead." + } + ], + "serialized": ["length", ["get", "x"]] + } +} diff --git a/test/integration/expression-tests/length/invalid/test.json b/test/integration/expression-tests/length/invalid/test.json new file mode 100644 index 00000000000..1c7f13d5ae3 --- /dev/null +++ b/test/integration/expression-tests/length/invalid/test.json @@ -0,0 +1,16 @@ +{ + "expression": ["length", 0], + "inputs": [[{}, {}]], + "expected": { + "compiled": { + "result": "error", + "errors": [ + { + "key": "", + "error": "Expected argument of type string or array, but found number instead." + } + ] + }, + "serialized": ["length", 0] + } +}