diff --git a/docs/components/expression-metadata.js b/docs/components/expression-metadata.js index af637bf6c4a..55d78747521 100644 --- a/docs/components/expression-metadata.js +++ b/docs/components/expression-metadata.js @@ -9,6 +9,9 @@ const types = { '==': [{ type: 'boolean', parameters: ['string', 'string'] + }, { + type: 'boolean', + parameters: ['string', 'string', 'collator'] }, { type: 'boolean', parameters: ['number', 'number'] @@ -21,6 +24,9 @@ const types = { }, { type: 'boolean', parameters: ['string', 'value'] + }, { + type: 'boolean', + parameters: ['string', 'value', 'collator'] }, { type: 'boolean', parameters: ['number', 'value'] @@ -33,6 +39,9 @@ const types = { }, { type: 'boolean', parameters: ['value', 'string'] + }, { + type: 'boolean', + parameters: ['value', 'string', 'collator'] }, { type: 'boolean', parameters: ['value', 'number'] @@ -46,6 +55,9 @@ const types = { '!=': [{ type: 'boolean', parameters: ['string', 'string'] + }, { + type: 'boolean', + parameters: ['string', 'string', 'collator'] }, { type: 'boolean', parameters: ['number', 'number'] @@ -58,6 +70,9 @@ const types = { }, { type: 'boolean', parameters: ['string', 'value'] + }, { + type: 'boolean', + parameters: ['string', 'value', 'collator'] }, { type: 'boolean', parameters: ['number', 'value'] @@ -70,6 +85,9 @@ const types = { }, { type: 'boolean', parameters: ['value', 'string'] + }, { + type: 'boolean', + parameters: ['value', 'string', 'collator'] }, { type: 'boolean', parameters: ['value', 'number'] @@ -183,6 +201,20 @@ const types = { var: [{ type: 'the type of the bound expression', parameters: ['previously bound variable name'] + }], + collator: [{ + type: 'collator', + parameters: [ + 'caseSensitive: boolean', + 'diacriticSensitive: boolean', + ] + }, { + type: 'collator', + parameters: [ + 'caseSensitive: boolean', + 'diacriticSensitive: boolean', + 'locale: string' + ] }] }; diff --git a/src/style-spec/expression/definitions/collator.js b/src/style-spec/expression/definitions/collator.js new file mode 100644 index 00000000000..c3cdd82ccfd --- /dev/null +++ b/src/style-spec/expression/definitions/collator.js @@ -0,0 +1,130 @@ +// @flow + +import { StringType, BooleanType, CollatorType } from '../types'; + +import type { Expression } from '../expression'; +import type EvaluationContext from '../evaluation_context'; +import type ParsingContext from '../parsing_context'; +import type { Type } from '../types'; + +declare var Intl: { + Collator: Class +} + +declare class Collator { + constructor ( + locales?: string | string[], + options?: CollatorOptions + ): Collator; + + static ( + locales?: string | string[], + options?: CollatorOptions + ): Collator; + + compare (a: string, b: string): number; + + resolvedOptions(): any; +} + +type CollatorOptions = { + localeMatcher?: 'lookup' | 'best fit', + usage?: 'sort' | 'search', + sensitivity?: 'base' | 'accent' | 'case' | 'variant', + ignorePunctuation?: boolean, + numeric?: boolean, + caseFirst?: 'upper' | 'lower' | 'false' +} + +export class CollatorInstantiation { + locale: string | null; + sensitivity: 'base' | 'accent' | 'case' | 'variant'; + + constructor(caseSensitive: boolean, diacriticSensitive: boolean, locale: string | null) { + if (caseSensitive) + this.sensitivity = diacriticSensitive ? 'variant' : 'case'; + else + this.sensitivity = diacriticSensitive ? 'accent' : 'base'; + + this.locale = locale; + } + + compare(lhs: string, rhs: string): number { + return new Intl.Collator(this.locale ? this.locale : [], + { sensitivity: this.sensitivity, usage: 'search' }) + .compare(lhs, rhs); + } + + resolvedLocale(): string { + return new Intl.Collator(this.locale ? this.locale : []) + .resolvedOptions().locale; + } + + serialize() { + const serialized = ["collator"]; + serialized.push(this.sensitivity === 'variant' || this.sensitivity === 'case'); + serialized.push(this.sensitivity === 'variant' || this.sensitivity === 'accent'); + if (this.locale) { + serialized.push(this.locale); + } + return serialized; + } +} + +export class CollatorExpression implements Expression { + type: Type; + caseSensitive: Expression; + diacriticSensitive: Expression; + locale: Expression | null; + + constructor(caseSensitive: Expression, diacriticSensitive: Expression, locale: Expression | null) { + this.type = CollatorType; + this.locale = locale; + this.caseSensitive = caseSensitive; + this.diacriticSensitive = diacriticSensitive; + } + + static parse(args: Array, context: ParsingContext): ?Expression { + if (args.length !== 3 && args.length !== 4) + return context.error(`Expected two or three arguments.`); + + const caseSensitive = context.parse(args[1], 1, BooleanType); + if (!caseSensitive) return null; + const diacriticSensitive = context.parse(args[2], 2, BooleanType); + if (!diacriticSensitive) return null; + + let locale = null; + if (args.length === 4) { + locale = context.parse(args[3], 3, StringType); + if (!locale) return null; + } + + return new CollatorExpression(caseSensitive, diacriticSensitive, locale); + } + + evaluate(ctx: EvaluationContext) { + return new CollatorInstantiation(this.caseSensitive.evaluate(ctx), this.diacriticSensitive.evaluate(ctx), this.locale ? this.locale.evaluate(ctx) : null); + } + + eachChild(fn: (Expression) => void) { + fn(this.caseSensitive); + fn(this.diacriticSensitive); + if (this.locale) { + fn(this.locale); + } + } + + possibleOutputs() { + // Technically the set of possible outputs is the combinatoric set of Collators produced + // by all possibleOutputs of locale/caseSensitive/diacriticSensitive + // But for the primary use of Collators in comparison operators, we ignore the Collator's + // possibleOutputs anyway, so we can get away with leaving this undefined for now. + return [undefined]; + } + + serialize() { + const serialized = ["collator"]; + this.eachChild(child => { serialized.push(child.serialize()); }); + return serialized; + } +} diff --git a/src/style-spec/expression/definitions/equals.js b/src/style-spec/expression/definitions/equals.js index 93c37e9b239..374d6f916c2 100644 --- a/src/style-spec/expression/definitions/equals.js +++ b/src/style-spec/expression/definitions/equals.js @@ -1,12 +1,11 @@ // @flow -import { toString, ValueType, BooleanType } from '../types'; +import { toString, ValueType, BooleanType, CollatorType } from '../types'; import type { Expression } from '../expression'; import type EvaluationContext from '../evaluation_context'; import type ParsingContext from '../parsing_context'; import type { Type } from '../types'; -import type { Value } from '../values'; function isComparableType(type: Type) { return type.kind === 'string' || @@ -29,21 +28,23 @@ function isComparableType(type: Type) { * * @private */ -function makeComparison(op: string, compare: (Value, Value) => boolean) { +function makeComparison(op: string, negate: boolean) { return class Comparison implements Expression { type: Type; lhs: Expression; rhs: Expression; + collator: Expression | null; - constructor(lhs: Expression, rhs: Expression) { + constructor(lhs: Expression, rhs: Expression, collator: Expression | null) { this.type = BooleanType; this.lhs = lhs; this.rhs = rhs; + this.collator = collator; } static parse(args: Array, context: ParsingContext): ?Expression { - if (args.length !== 3) - return context.error(`Expected two arguments.`); + if (args.length !== 3 && args.length !== 4) + return context.error(`Expected two or three arguments.`); const lhs = context.parse(args[1], 1, ValueType); if (!lhs) return null; @@ -58,16 +59,32 @@ function makeComparison(op: string, compare: (Value, Value) => boolean) { return context.error(`Cannot compare ${toString(lhs.type)} and ${toString(rhs.type)}.`); } - return new Comparison(lhs, rhs); + let collator = null; + if (args.length === 4) { + if (lhs.type.kind !== 'string' && rhs.type.kind !== 'string') { + return context.error(`Cannot use collator to compare non-string types.`); + } + collator = context.parse(args[3], 3, CollatorType); + if (!collator) return null; + } + + return new Comparison(lhs, rhs, collator); } evaluate(ctx: EvaluationContext) { - return compare(this.lhs.evaluate(ctx), this.rhs.evaluate(ctx)); + const equal = this.collator ? + this.collator.evaluate(ctx).compare(this.lhs.evaluate(ctx), this.rhs.evaluate(ctx)) === 0 : + this.lhs.evaluate(ctx) === this.rhs.evaluate(ctx); + + return negate ? !equal : equal; } eachChild(fn: (Expression) => void) { fn(this.lhs); fn(this.rhs); + if (this.collator) { + fn(this.collator); + } } possibleOutputs() { @@ -75,10 +92,12 @@ function makeComparison(op: string, compare: (Value, Value) => boolean) { } serialize() { - return [op, this.lhs.serialize(), this.rhs.serialize()]; + const serialized = [op]; + this.eachChild(child => { serialized.push(child.serialize()); }); + return serialized; } }; } -export const Equals = makeComparison('==', (lhs, rhs) => lhs === rhs); -export const NotEquals = makeComparison('!=', (lhs, rhs) => lhs !== rhs); +export const Equals = makeComparison('==', false); +export const NotEquals = makeComparison('!=', true); diff --git a/src/style-spec/expression/definitions/index.js b/src/style-spec/expression/definitions/index.js index e403d64f178..8d2bea004d9 100644 --- a/src/style-spec/expression/definitions/index.js +++ b/src/style-spec/expression/definitions/index.js @@ -1,6 +1,6 @@ // @flow -import { NumberType, StringType, BooleanType, ColorType, ObjectType, ValueType, ErrorType, array, toString } from '../types'; +import { NumberType, StringType, BooleanType, ColorType, ObjectType, ValueType, ErrorType, CollatorType, array, toString } from '../types'; import { typeOf, Color, validateRGBA } from '../values'; import CompoundExpression from '../compound_expression'; @@ -18,6 +18,7 @@ import Step from './step'; import Interpolate from './interpolate'; import Coalesce from './coalesce'; import { Equals, NotEquals } from './equals'; +import { CollatorExpression } from './collator'; import Length from './length'; import type { Type } from '../types'; @@ -33,6 +34,7 @@ const expressions: ExpressionRegistry = { 'boolean': Assertion, 'case': Case, 'coalesce': Coalesce, + 'collator': CollatorExpression, 'interpolate': Interpolate, 'length': Length, 'let': Let, @@ -71,6 +73,11 @@ function gt(ctx, [a, b]) { return a.evaluate(ctx) > b.evaluate(ctx); } function lteq(ctx, [a, b]) { return a.evaluate(ctx) <= b.evaluate(ctx); } function gteq(ctx, [a, b]) { return a.evaluate(ctx) >= b.evaluate(ctx); } +function ltCollate(ctx, [a, b, c]) { return c.evaluate(ctx).compare(a.evaluate(ctx), b.evaluate(ctx)) < 0; } +function gtCollate(ctx, [a, b, c]) { return c.evaluate(ctx).compare(a.evaluate(ctx), b.evaluate(ctx)) > 0; } +function lteqCollate(ctx, [a, b, c]) { return c.evaluate(ctx).compare(a.evaluate(ctx), b.evaluate(ctx)) <= 0; } +function gteqCollate(ctx, [a, b, c]) { return c.evaluate(ctx).compare(a.evaluate(ctx), b.evaluate(ctx)) >= 0; } + function binarySearch(v, a, i, j) { while (i <= j) { const m = (i + j) >> 1; @@ -432,28 +439,32 @@ CompoundExpression.register(expressions, { type: BooleanType, overloads: [ [[NumberType, NumberType], gt], - [[StringType, StringType], gt] + [[StringType, StringType], gt], + [[StringType, StringType, CollatorType], gtCollate] ] }, '<': { type: BooleanType, overloads: [ [[NumberType, NumberType], lt], - [[StringType, StringType], lt] + [[StringType, StringType], lt], + [[StringType, StringType, CollatorType], ltCollate] ] }, '>=': { type: BooleanType, overloads: [ [[NumberType, NumberType], gteq], - [[StringType, StringType], gteq] + [[StringType, StringType], gteq], + [[StringType, StringType, CollatorType], gteqCollate] ] }, '<=': { type: BooleanType, overloads: [ [[NumberType, NumberType], lteq], - [[StringType, StringType], lteq] + [[StringType, StringType], lteq], + [[StringType, StringType, CollatorType], lteqCollate] ] }, 'all': { @@ -513,6 +524,12 @@ CompoundExpression.register(expressions, { StringType, varargs(StringType), (ctx, args) => args.map(arg => arg.evaluate(ctx)).join('') + ], + 'resolved-locale': [ + // Must be added to non-featureConstant list in parsing_context.js + StringType, + [CollatorType], + (ctx, [collator]) => collator.evaluate(ctx).resolvedLocale() ] }); diff --git a/src/style-spec/expression/definitions/literal.js b/src/style-spec/expression/definitions/literal.js index 48bdde334fe..5b8110ef50e 100644 --- a/src/style-spec/expression/definitions/literal.js +++ b/src/style-spec/expression/definitions/literal.js @@ -3,6 +3,7 @@ import assert from 'assert'; import { isValue, typeOf } from '../values'; import Color from '../../util/color'; +import { CollatorInstantiation } from '../definitions/collator'; import type { Type } from '../types'; import type { Value } from '../values'; @@ -58,6 +59,8 @@ class Literal implements Expression { return ["literal", this.value]; } else if (this.value instanceof Color) { return ["rgba"].concat(this.value.toArray()); + } else if (this.value instanceof CollatorInstantiation) { + return this.value.serialize(); } else { assert(this.value === null || typeof this.value === 'string' || diff --git a/src/style-spec/expression/parsing_context.js b/src/style-spec/expression/parsing_context.js index cb981ca6f5e..43b0920906b 100644 --- a/src/style-spec/expression/parsing_context.js +++ b/src/style-spec/expression/parsing_context.js @@ -201,5 +201,5 @@ function isConstant(expression: Expression) { } return isFeatureConstant(expression) && - isGlobalPropertyConstant(expression, ['zoom', 'heatmap-density']); + isGlobalPropertyConstant(expression, ['zoom', 'heatmap-density', 'resolved-locale', 'collator']); } diff --git a/src/style-spec/expression/types.js b/src/style-spec/expression/types.js index f752fa5a0db..33324df1636 100644 --- a/src/style-spec/expression/types.js +++ b/src/style-spec/expression/types.js @@ -8,6 +8,7 @@ export type ColorTypeT = { kind: 'color' }; export type ObjectTypeT = { kind: 'object' }; export type ValueTypeT = { kind: 'value' }; export type ErrorTypeT = { kind: 'error' }; +export type CollatorTypeT = { kind: 'collator' }; export type Type = NullTypeT | @@ -18,7 +19,8 @@ export type Type = ObjectTypeT | ValueTypeT | ArrayType | // eslint-disable-line no-use-before-define - ErrorTypeT + ErrorTypeT | + CollatorTypeT export type ArrayType = { kind: 'array', @@ -34,6 +36,7 @@ export const ColorType = { kind: 'color' }; export const ObjectType = { kind: 'object' }; export const ValueType = { kind: 'value' }; export const ErrorType = { kind: 'error' }; +export const CollatorType = { kind: 'collator' }; export function array(itemType: Type, N: ?number): ArrayType { return { diff --git a/src/style-spec/reference/v8.json b/src/style-spec/reference/v8.json index b1444679c24..b664676b416 100644 --- a/src/style-spec/reference/v8.json +++ b/src/style-spec/reference/v8.json @@ -2193,6 +2193,15 @@ } } }, + "collator": { + "doc": "Returns a `collator` for use in locale dependent comparison operations. The first two arguments control case and diacritic sensitivity, and the optional third argument specifies the IETF language tag of the locale to use. If no locale is provided, the default locale is used.", + "group": "Types", + "sdk-support": { + "basic functionality": { + "js": "0.45.0" + } + } + }, "to-string": { "doc": "Converts the input value to a string. If the input is `null`, the result is `\"null\"`. If the input is a boolean, the result is `\"true\"` or `\"false\"`. If the input is a number, it is converted to a string as specified by the [\"NumberToString\" algorithm](https://tc39.github.io/ecma262/#sec-tostring-applied-to-the-number-type) of the ECMAScript Language Specification. If the input is a color, it is converted to a string of the form `\"rgba(r,g,b,a)\"`, where `r`, `g`, and `b` are numerals ranging from 0 to 255, and `a` ranges from 0 to 1. Otherwise, the input is converted to a string in the format specified by the [`JSON.stringify`](https://tc39.github.io/ecma262/#sec-json.stringify) function of the ECMAScript Language Specification.", "group": "Types", @@ -2491,7 +2500,7 @@ } }, "==": { - "doc": "Returns `true` if the input values are equal, `false` otherwise. Equality is strictly typed: values of different types are always considered not equal.", + "doc": "Returns `true` if the input values are equal, `false` otherwise. Equality is strictly typed: values of different types are always considered not equal. Accepts an optional `collator` argument to control locale-dependent string comparisons.", "group": "Decision", "sdk-support": { "basic functionality": { @@ -2500,7 +2509,7 @@ } }, "!=": { - "doc": "Returns `true` if the input values are not equal, `false` otherwise. Equality is strictly typed: values of different types are always considered not equal.", + "doc": "Returns `true` if the input values are not equal, `false` otherwise. Equality is strictly typed: values of different types are always considered not equal. Accepts an optional `collator` argument to control locale-dependent string comparisons.", "group": "Decision", "sdk-support": { "basic functionality": { @@ -2509,7 +2518,7 @@ } }, ">": { - "doc": "Returns `true` if the first input is strictly greater than the second, `false` otherwise. The inputs must be numbers or strings, and both of the same type.", + "doc": "Returns `true` if the first input is strictly greater than the second, `false` otherwise. The inputs must be numbers or strings, and both of the same type. Accepts an optional `collator` argument to control locale-dependent string comparisons.", "group": "Decision", "sdk-support": { "basic functionality": { @@ -2518,7 +2527,7 @@ } }, "<": { - "doc": "Returns `true` if the first input is strictly less than the second, `false` otherwise. The inputs must be numbers or strings, and both of the same type.", + "doc": "Returns `true` if the first input is strictly less than the second, `false` otherwise. The inputs must be numbers or strings, and both of the same type. Accepts an optional `collator` argument to control locale-dependent string comparisons.", "group": "Decision", "sdk-support": { "basic functionality": { @@ -2527,7 +2536,7 @@ } }, ">=": { - "doc": "Returns `true` if the first input is greater than or equal to the second, `false` otherwise. The inputs must be numbers or strings, and both of the same type.", + "doc": "Returns `true` if the first input is greater than or equal to the second, `false` otherwise. The inputs must be numbers or strings, and both of the same type. Accepts an optional `collator` argument to control locale-dependent string comparisons.", "group": "Decision", "sdk-support": { "basic functionality": { @@ -2536,7 +2545,7 @@ } }, "<=": { - "doc": "Returns `true` if the first input is less than or equal to the second, `false` otherwise. The inputs must be numbers or strings, and both of the same type.", + "doc": "Returns `true` if the first input is less than or equal to the second, `false` otherwise. The inputs must be numbers or strings, and both of the same type. Accepts an optional `collator` argument to control locale-dependent string comparisons.", "group": "Decision", "sdk-support": { "basic functionality": { @@ -2597,6 +2606,15 @@ "js": "0.41.0" } } + }, + "resolved-locale": { + "doc": "Returns the IETF language tag of the locale being used by the provided `collator`. This can be used to determine the default system locale, or to determine if a requested locale was successfully loaded.", + "group": "String", + "sdk-support": { + "basic functionality": { + "js": "0.45.0" + } + } } } }, diff --git a/test/integration/expression-tests/collator/accent-equals-de/test.json b/test/integration/expression-tests/collator/accent-equals-de/test.json new file mode 100644 index 00000000000..a09a80e1efc --- /dev/null +++ b/test/integration/expression-tests/collator/accent-equals-de/test.json @@ -0,0 +1,37 @@ +{ + "expression": [ + "case", + ["==", ["resolved-locale", ["collator", true, false, "de"]], "de"], + [ + "==", + ["string", ["get", "lhs"]], + ["get", "rhs"], + ["collator", true, false, "de"] + ], + ["case", ["==", ["get", "rhs"], "ue"], true, false] + ], + "inputs": [ + [{}, {"properties": {"lhs": "ü", "rhs": "ue"}}], + [{}, {"properties": {"lhs": "ü", "rhs": "u"}}] + ], + "expected": { + "compiled": { + "result": "success", + "isFeatureConstant": false, + "isZoomConstant": true, + "type": "boolean" + }, + "outputs": [true, false], + "serialized": [ + "case", + ["==", ["resolved-locale", ["collator", true, false, "de"]], "de"], + [ + "==", + ["string", ["get", "lhs"]], + ["get", "rhs"], + ["collator", true, false, "de"] + ], + ["case", ["==", ["get", "rhs"], "ue"], true, false] + ] + } +} diff --git a/test/integration/expression-tests/collator/accent-lt-en/test.json b/test/integration/expression-tests/collator/accent-lt-en/test.json new file mode 100644 index 00000000000..d0ab166898a --- /dev/null +++ b/test/integration/expression-tests/collator/accent-lt-en/test.json @@ -0,0 +1,28 @@ +{ + "expression": [ + "<", + ["string", ["get", "lhs"]], + ["get", "rhs"], + ["collator", true, false, "en"] + ], + "inputs": [ + [{}, {"properties": {"lhs": "a", "rhs": "ä"}}], + [{}, {"properties": {"lhs": "a", "rhs": "A"}}], + [{}, {"properties": {"lhs": "ä", "rhs": "b"}}] + ], + "expected": { + "compiled": { + "result": "success", + "isFeatureConstant": false, + "isZoomConstant": true, + "type": "boolean" + }, + "outputs": [false, true, true], + "serialized": [ + "<", + ["string", ["get", "lhs"]], + ["string", ["get", "rhs"]], + ["collator", true, false, "en"] + ] + } +} diff --git a/test/integration/expression-tests/collator/accent-not-equals-en/test.json b/test/integration/expression-tests/collator/accent-not-equals-en/test.json new file mode 100644 index 00000000000..d46cb887c81 --- /dev/null +++ b/test/integration/expression-tests/collator/accent-not-equals-en/test.json @@ -0,0 +1,34 @@ +{ + "expression": [ + "!", + [ + "!=", + ["string", ["get", "lhs"]], + ["get", "rhs"], + ["collator", true, false, "en"] + ] + ], + "inputs": [ + [{}, {"properties": {"lhs": "a", "rhs": "ä"}}], + [{}, {"properties": {"lhs": "a", "rhs": "A"}}], + [{}, {"properties": {"lhs": "b", "rhs": "ä"}}] + ], + "expected": { + "compiled": { + "result": "success", + "isFeatureConstant": false, + "isZoomConstant": true, + "type": "boolean" + }, + "outputs": [true, false, false], + "serialized": [ + "!", + [ + "!=", + ["string", ["get", "lhs"]], + ["get", "rhs"], + ["collator", true, false, "en"] + ] + ] + } +} diff --git a/test/integration/expression-tests/collator/base-default-locale/test.json b/test/integration/expression-tests/collator/base-default-locale/test.json new file mode 100644 index 00000000000..18b7907488e --- /dev/null +++ b/test/integration/expression-tests/collator/base-default-locale/test.json @@ -0,0 +1,28 @@ +{ + "expression": [ + "==", + ["string", ["get", "lhs"]], + ["get", "rhs"], + ["collator", false, false] + ], + "inputs": [ + [{}, {"properties": {"lhs": "a", "rhs": "a"}}], + [{}, {"properties": {"lhs": "A", "rhs": "A"}}], + [{}, {"properties": {"lhs": "b", "rhs": "a"}}] + ], + "expected": { + "compiled": { + "result": "success", + "isFeatureConstant": false, + "isZoomConstant": true, + "type": "boolean" + }, + "outputs": [true, true, false], + "serialized": [ + "==", + ["string", ["get", "lhs"]], + ["get", "rhs"], + ["collator", false, false] + ] + } +} diff --git a/test/integration/expression-tests/collator/base-equals-en/test.json b/test/integration/expression-tests/collator/base-equals-en/test.json new file mode 100644 index 00000000000..6a6bac95067 --- /dev/null +++ b/test/integration/expression-tests/collator/base-equals-en/test.json @@ -0,0 +1,28 @@ +{ + "expression": [ + "==", + ["string", ["get", "lhs"]], + ["get", "rhs"], + ["collator", false, false, "en"] + ], + "inputs": [ + [{}, {"properties": {"lhs": "a", "rhs": "ä"}}], + [{}, {"properties": {"lhs": "a", "rhs": "A"}}], + [{}, {"properties": {"lhs": "b", "rhs": "ä"}}] + ], + "expected": { + "compiled": { + "result": "success", + "isFeatureConstant": false, + "isZoomConstant": true, + "type": "boolean" + }, + "outputs": [true, true, false], + "serialized": [ + "==", + ["string", ["get", "lhs"]], + ["get", "rhs"], + ["collator", false, false, "en"] + ] + } +} diff --git a/test/integration/expression-tests/collator/base-gt-en/test.json b/test/integration/expression-tests/collator/base-gt-en/test.json new file mode 100644 index 00000000000..6c1a75a3448 --- /dev/null +++ b/test/integration/expression-tests/collator/base-gt-en/test.json @@ -0,0 +1,28 @@ +{ + "expression": [ + ">", + ["string", ["get", "lhs"]], + ["get", "rhs"], + ["collator", false, false, "en"] + ], + "inputs": [ + [{}, {"properties": {"lhs": "a", "rhs": "ä"}}], + [{}, {"properties": {"lhs": "a", "rhs": "A"}}], + [{}, {"properties": {"lhs": "b", "rhs": "ä"}}] + ], + "expected": { + "compiled": { + "result": "success", + "isFeatureConstant": false, + "isZoomConstant": true, + "type": "boolean" + }, + "outputs": [false, false, true], + "serialized": [ + ">", + ["string", ["get", "lhs"]], + ["string", ["get", "rhs"]], + ["collator", false, false, "en"] + ] + } +} diff --git a/test/integration/expression-tests/collator/case-lteq-en/test.json b/test/integration/expression-tests/collator/case-lteq-en/test.json new file mode 100644 index 00000000000..7d159668644 --- /dev/null +++ b/test/integration/expression-tests/collator/case-lteq-en/test.json @@ -0,0 +1,29 @@ +{ + "expression": [ + "<=", + ["string", ["get", "lhs"]], + ["get", "rhs"], + ["collator", false, true, "en"] + ], + "inputs": [ + [{}, {"properties": {"lhs": "ä", "rhs": "a"}}], + [{}, {"properties": {"lhs": "A", "rhs": "a"}}], + [{}, {"properties": {"lhs": "a", "rhs": "a"}}], + [{}, {"properties": {"lhs": "ä", "rhs": "b"}}] + ], + "expected": { + "compiled": { + "result": "success", + "isFeatureConstant": false, + "isZoomConstant": true, + "type": "boolean" + }, + "outputs": [false, true, true, true], + "serialized": [ + "<=", + ["string", ["get", "lhs"]], + ["string", ["get", "rhs"]], + ["collator", false, true, "en"] + ] + } +} diff --git a/test/integration/expression-tests/collator/case-not-equals-en/test.json b/test/integration/expression-tests/collator/case-not-equals-en/test.json new file mode 100644 index 00000000000..bd830c5efa5 --- /dev/null +++ b/test/integration/expression-tests/collator/case-not-equals-en/test.json @@ -0,0 +1,34 @@ +{ + "expression": [ + "!", + [ + "!=", + ["string", ["get", "lhs"]], + ["get", "rhs"], + ["collator", false, true, "en"] + ] + ], + "inputs": [ + [{}, {"properties": {"lhs": "a", "rhs": "ä"}}], + [{}, {"properties": {"lhs": "a", "rhs": "A"}}], + [{}, {"properties": {"lhs": "b", "rhs": "ä"}}] + ], + "expected": { + "compiled": { + "result": "success", + "isFeatureConstant": false, + "isZoomConstant": true, + "type": "boolean" + }, + "outputs": [false, true, false], + "serialized": [ + "!", + [ + "!=", + ["string", ["get", "lhs"]], + ["get", "rhs"], + ["collator", false, true, "en"] + ] + ] + } +} diff --git a/test/integration/expression-tests/collator/comparison-number-error/test.json b/test/integration/expression-tests/collator/comparison-number-error/test.json new file mode 100644 index 00000000000..9eed27bb59d --- /dev/null +++ b/test/integration/expression-tests/collator/comparison-number-error/test.json @@ -0,0 +1,11 @@ +{ + "expression": ["<", 1, 2, ["collator", false, false]], + "expected": { + "compiled": { + "result": "error", + "errors": [ + {"key": "[1]", "error": "Expected string but found number instead."} + ] + } + } +} diff --git a/test/integration/expression-tests/collator/equals-non-string-error/test.json b/test/integration/expression-tests/collator/equals-non-string-error/test.json new file mode 100644 index 00000000000..6f51af5ab25 --- /dev/null +++ b/test/integration/expression-tests/collator/equals-non-string-error/test.json @@ -0,0 +1,11 @@ +{ + "expression": ["==", 1, 2, ["collator", false, false]], + "expected": { + "compiled": { + "result": "error", + "errors": [ + {"key": "", "error": "Cannot use collator to compare non-string types."} + ] + } + } +} diff --git a/test/integration/expression-tests/collator/variant-equals-en/test.json b/test/integration/expression-tests/collator/variant-equals-en/test.json new file mode 100644 index 00000000000..6b858abbf43 --- /dev/null +++ b/test/integration/expression-tests/collator/variant-equals-en/test.json @@ -0,0 +1,28 @@ +{ + "expression": [ + "==", + ["string", ["get", "lhs"]], + ["get", "rhs"], + ["collator", true, true, "en"] + ], + "inputs": [ + [{}, {"properties": {"lhs": "a", "rhs": "ä"}}], + [{}, {"properties": {"lhs": "a", "rhs": "A"}}], + [{}, {"properties": {"lhs": "b", "rhs": "ä"}}] + ], + "expected": { + "compiled": { + "result": "success", + "isFeatureConstant": false, + "isZoomConstant": true, + "type": "boolean" + }, + "outputs": [false, false, false], + "serialized": [ + "==", + ["string", ["get", "lhs"]], + ["get", "rhs"], + ["collator", true, true, "en"] + ] + } +} diff --git a/test/integration/expression-tests/collator/variant-gteq-en/test.json b/test/integration/expression-tests/collator/variant-gteq-en/test.json new file mode 100644 index 00000000000..8bfcd08eab4 --- /dev/null +++ b/test/integration/expression-tests/collator/variant-gteq-en/test.json @@ -0,0 +1,29 @@ +{ + "expression": [ + ">=", + ["string", ["get", "lhs"]], + ["get", "rhs"], + ["collator", true, true, "en"] + ], + "inputs": [ + [{}, {"properties": {"lhs": "a", "rhs": "ä"}}], + [{}, {"properties": {"lhs": "a", "rhs": "A"}}], + [{}, {"properties": {"lhs": "a", "rhs": "a"}}], + [{}, {"properties": {"lhs": "b", "rhs": "ä"}}] + ], + "expected": { + "compiled": { + "result": "success", + "isFeatureConstant": false, + "isZoomConstant": true, + "type": "boolean" + }, + "outputs": [false, false, true, true], + "serialized": [ + ">=", + ["string", ["get", "lhs"]], + ["string", ["get", "rhs"]], + ["collator", true, true, "en"] + ] + } +} diff --git a/test/integration/expression-tests/resolved-locale/basic/test.json b/test/integration/expression-tests/resolved-locale/basic/test.json new file mode 100644 index 00000000000..6a65a9b81d5 --- /dev/null +++ b/test/integration/expression-tests/resolved-locale/basic/test.json @@ -0,0 +1,22 @@ +{ + "expression": [ + "==", + ["resolved-locale", ["collator", true, true, "en"]], + "en" + ], + "inputs": [[{}, {}]], + "expected": { + "compiled": { + "result": "success", + "isFeatureConstant": true, + "isZoomConstant": true, + "type": "boolean" + }, + "outputs": [true], + "serialized": [ + "==", + ["resolved-locale", ["collator", true, true, "en"]], + "en" + ] + } +}