Skip to content

Commit

Permalink
Add 'collator' expression for locale-aware string comparison:
Browse files Browse the repository at this point in the history
['collator',
  <case-sensitive>,
  <diacritic-sensitive>,
  (optional) <IETF language tag>]

Add 'resolved-locale' to test if requested collation locale is available:
['resolved-locale', <collator>]

'==','!=','<','<=,'>','>=' now take an optional 'collator' argument when they are used with 'string' arguments.
  • Loading branch information
ChrisLoer committed Apr 6, 2018
1 parent e9063fb commit be70589
Show file tree
Hide file tree
Showing 21 changed files with 593 additions and 24 deletions.
32 changes: 32 additions & 0 deletions docs/components/expression-metadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ const types = {
'==': [{
type: 'boolean',
parameters: ['string', 'string']
}, {
type: 'boolean',
parameters: ['string', 'string', 'collator']
}, {
type: 'boolean',
parameters: ['number', 'number']
Expand All @@ -21,6 +24,9 @@ const types = {
}, {
type: 'boolean',
parameters: ['string', 'value']
}, {
type: 'boolean',
parameters: ['string', 'value', 'collator']
}, {
type: 'boolean',
parameters: ['number', 'value']
Expand All @@ -33,6 +39,9 @@ const types = {
}, {
type: 'boolean',
parameters: ['value', 'string']
}, {
type: 'boolean',
parameters: ['value', 'string', 'collator']
}, {
type: 'boolean',
parameters: ['value', 'number']
Expand All @@ -46,6 +55,9 @@ const types = {
'!=': [{
type: 'boolean',
parameters: ['string', 'string']
}, {
type: 'boolean',
parameters: ['string', 'string', 'collator']
}, {
type: 'boolean',
parameters: ['number', 'number']
Expand All @@ -58,6 +70,9 @@ const types = {
}, {
type: 'boolean',
parameters: ['string', 'value']
}, {
type: 'boolean',
parameters: ['string', 'value', 'collator']
}, {
type: 'boolean',
parameters: ['number', 'value']
Expand All @@ -70,6 +85,9 @@ const types = {
}, {
type: 'boolean',
parameters: ['value', 'string']
}, {
type: 'boolean',
parameters: ['value', 'string', 'collator']
}, {
type: 'boolean',
parameters: ['value', 'number']
Expand Down Expand Up @@ -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'
]
}]
};

Expand Down
130 changes: 130 additions & 0 deletions src/style-spec/expression/definitions/collator.js
Original file line number Diff line number Diff line change
@@ -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<Collator>
}

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<mixed>, 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;
}
}
41 changes: 30 additions & 11 deletions src/style-spec/expression/definitions/equals.js
Original file line number Diff line number Diff line change
@@ -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' ||
Expand All @@ -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<mixed>, 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;
Expand All @@ -58,27 +59,45 @@ 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() {
return [true, false];
}

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);
27 changes: 22 additions & 5 deletions src/style-spec/expression/definitions/index.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand All @@ -33,6 +34,7 @@ const expressions: ExpressionRegistry = {
'boolean': Assertion,
'case': Case,
'coalesce': Coalesce,
'collator': CollatorExpression,
'interpolate': Interpolate,
'length': Length,
'let': Let,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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': {
Expand Down Expand Up @@ -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()
]
});

Expand Down
Loading

0 comments on commit be70589

Please sign in to comment.