From b43478c5d5e36376a9644ef195a7cd659332dd96 Mon Sep 17 00:00:00 2001 From: Chris Loer Date: Tue, 27 Nov 2018 15:47:05 -0800 Subject: [PATCH] Add "number-format" expression for string representation of numbers.. --- docs/components/expression-metadata.js | 7 + .../expression/definitions/index.js | 2 + .../expression/definitions/number_format.js | 142 ++++++++++++++++++ src/style-spec/reference/v8.json | 9 ++ .../number-format/currency/test.json | 31 ++++ .../number-format/default/test.json | 21 +++ .../number-format/precision/test.json | 33 ++++ 7 files changed, 245 insertions(+) create mode 100644 src/style-spec/expression/definitions/number_format.js create mode 100644 test/integration/expression-tests/number-format/currency/test.json create mode 100644 test/integration/expression-tests/number-format/default/test.json create mode 100644 test/integration/expression-tests/number-format/precision/test.json diff --git a/docs/components/expression-metadata.js b/docs/components/expression-metadata.js index 02669df9bff..b9e74e3899e 100644 --- a/docs/components/expression-metadata.js +++ b/docs/components/expression-metadata.js @@ -168,6 +168,13 @@ const types = { '...', 'input_n: string, options_n: { "font-scale": number, "text-font": array }' ] + }], + 'number-format': [{ + type: 'string', + parameters: [ + 'input: number', + 'options: { "locale": string, "currency": string, "min-fraction-digits": number, "max-fraction-digits": number }' + ] }] }; diff --git a/src/style-spec/expression/definitions/index.js b/src/style-spec/expression/definitions/index.js index 003368a74b3..1e37a007fb6 100644 --- a/src/style-spec/expression/definitions/index.js +++ b/src/style-spec/expression/definitions/index.js @@ -37,6 +37,7 @@ import { GreaterThanOrEqual } from './comparison'; import CollatorExpression from './collator'; +import NumberFormat from './number_format'; import FormatExpression from './format'; import Length from './length'; @@ -66,6 +67,7 @@ const expressions: ExpressionRegistry = { 'literal': Literal, 'match': Match, 'number': Assertion, + 'number-format': NumberFormat, 'object': Assertion, 'step': Step, 'string': Assertion, diff --git a/src/style-spec/expression/definitions/number_format.js b/src/style-spec/expression/definitions/number_format.js new file mode 100644 index 00000000000..482b7bb6777 --- /dev/null +++ b/src/style-spec/expression/definitions/number_format.js @@ -0,0 +1,142 @@ +// @flow + +import { StringType, NumberType } 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: { + NumberFormat: Class +}; + +declare class Intl$NumberFormat { + constructor ( + locales?: string | string[], + options?: NumberFormatOptions + ): Intl$NumberFormat; + + static ( + locales?: string | string[], + options?: NumberFormatOptions + ): Intl$NumberFormat; + + format(a: number): string; + + resolvedOptions(): any; +} + +type NumberFormatOptions = { + style?: 'decimal' | 'currency' | 'percent'; + currency?: null | string; + minimumFractionDigits?: null | string; + maximumFractionDigits?: null | string; +}; + +export default class NumberFormat implements Expression { + type: Type; + number: Expression; + locale: Expression | null; // BCP 47 language tag + currency: Expression | null; // ISO 4217 currency code, required if style=currency + minFractionDigits: Expression | null; // Default 0 + maxFractionDigits: Expression | null; // Default 3 + + constructor(number: Expression, + locale: Expression | null, + currency: Expression | null, + minFractionDigits: Expression | null, + maxFractionDigits: Expression | null) { + this.type = StringType; + this.number = number; + this.locale = locale; + this.currency = currency; + this.minFractionDigits = minFractionDigits; + this.maxFractionDigits = maxFractionDigits; + } + + static parse(args: Array, context: ParsingContext): ?Expression { + if (args.length !== 3) + return context.error(`Expected two arguments.`); + + const number = context.parse(args[1], 1, NumberType); + if (!number) return null; + + const options = (args[2]: any); + if (typeof options !== "object" || Array.isArray(options)) + return context.error(`NumberFormat options argument must be an object.`); + + let locale = null; + if (options['locale']) { + locale = context.parse(options['locale'], 1, StringType); + if (!locale) return null; + } + + let currency = null; + if (options['currency']) { + currency = context.parse(options['currency'], 1, StringType); + if (!currency) return null; + } + + let minFractionDigits = null; + if (options['min-fraction-digits']) { + minFractionDigits = context.parse(options['min-fraction-digits'], 1, NumberType); + if (!minFractionDigits) return null; + } + + let maxFractionDigits = null; + if (options['max-fraction-digits']) { + maxFractionDigits = context.parse(options['max-fraction-digits'], 1, NumberType); + if (!maxFractionDigits) return null; + } + + return new NumberFormat(number, locale, currency, minFractionDigits, maxFractionDigits); + } + + evaluate(ctx: EvaluationContext) { + return new Intl.NumberFormat(this.locale ? this.locale.evaluate(ctx) : [], + { + style: this.currency ? "currency" : "decimal", + currency: this.currency ? this.currency.evaluate(ctx) : undefined, + minimumFractionDigits: this.minFractionDigits ? this.minFractionDigits.evaluate(ctx) : undefined, + maximumFractionDigits: this.maxFractionDigits ? this.maxFractionDigits.evaluate(ctx) : undefined, + }).format(this.number.evaluate(ctx)); + } + + eachChild(fn: (Expression) => void) { + fn(this.number); + if (this.locale) { + fn(this.locale); + } + if (this.currency) { + fn(this.currency); + } + if (this.minFractionDigits) { + fn(this.minFractionDigits); + } + if (this.maxFractionDigits) { + fn(this.maxFractionDigits); + } + } + + possibleOutputs() { + return [undefined]; + } + + serialize() { + const options = {}; + if (this.locale) { + options['locale'] = this.locale.serialize(); + } + if (this.currency) { + options['currency'] = this.currency.serialize(); + } + if (this.minFractionDigits) { + options['min-fraction-digits'] = this.minFractionDigits.serialize(); + } + if (this.maxFractionDigits) { + options['max-fraction-digits'] = this.maxFractionDigits.serialize(); + } + return ["number-format", this.number.serialize(), options]; + } +} diff --git a/src/style-spec/reference/v8.json b/src/style-spec/reference/v8.json index 0eadbdfb20a..dc1127d1cdb 100644 --- a/src/style-spec/reference/v8.json +++ b/src/style-spec/reference/v8.json @@ -2593,6 +2593,15 @@ } } }, + "number-format": { + "doc": "Converts the input number into a string representation using the providing formatting rules. If set, the `locale` argument specifies the locale to use, as a BCP 47 language tag. If set, the `currency` argument specifies an ISO 4217 code to use for currency-style formatting. If set, the `min-fraction-digits` and `max-fraction-digits` arguments specify the minimum and maximum number of fractional digits to include.", + "group": "Types", + "sdk-support": { + "basic functionality": { + "js": "0.54.0" + } + } + }, "to-string": { "doc": "Converts the input value to a string. If the input is `null`, the result is `\"\"`. 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", diff --git a/test/integration/expression-tests/number-format/currency/test.json b/test/integration/expression-tests/number-format/currency/test.json new file mode 100644 index 00000000000..7ad0292da34 --- /dev/null +++ b/test/integration/expression-tests/number-format/currency/test.json @@ -0,0 +1,31 @@ +{ + "expression": [ + "number-format", + 123456.789, + { + "locale": ["get", "locale"], + "currency": ["get", "currency"] + } + ], + "inputs": [ + [{}, {"properties": {"locale": "ja-JP", "currency": "JPY"}}], + [{}, {"properties": {"locale": "de-DE", "currency": "EUR"}}] + ], + "expected": { + "compiled": { + "result": "success", + "isFeatureConstant": false, + "isZoomConstant": true, + "type": "string" + }, + "outputs": ["JP¥ 123,457", "€ 123,456.79"], + "serialized": [ + "number-format", + 123456.789, + { + "locale": ["string", ["get", "locale"]], + "currency": ["string", ["get", "currency"]] + } + ] + } +} diff --git a/test/integration/expression-tests/number-format/default/test.json b/test/integration/expression-tests/number-format/default/test.json new file mode 100644 index 00000000000..78f2e77862a --- /dev/null +++ b/test/integration/expression-tests/number-format/default/test.json @@ -0,0 +1,21 @@ + +{ + "expression": [ + "number-format", + 123456.789, + {} + ], + "inputs": [ + [{}, {}] + ], + "expected": { + "compiled": { + "result": "success", + "isFeatureConstant": true, + "isZoomConstant": true, + "type": "string" + }, + "outputs": ["123,456.789"], + "serialized": "123,456.789" + } +} diff --git a/test/integration/expression-tests/number-format/precision/test.json b/test/integration/expression-tests/number-format/precision/test.json new file mode 100644 index 00000000000..837ffa4bedd --- /dev/null +++ b/test/integration/expression-tests/number-format/precision/test.json @@ -0,0 +1,33 @@ +{ + "expression": [ + "number-format", + 987654321.23456789, + { + "locale": ["get", "locale"], + "min-fraction-digits": ["get", "min"], + "max-fraction-digits": ["get", "max"] + } + ], + "inputs": [ + [{}, {"properties": {"locale": "en-US", "min": 15, "max": 20}}], + [{}, {"properties": {"locale": "en-US", "min": 2, "max": 4}}] + ], + "expected": { + "compiled": { + "result": "success", + "isFeatureConstant": false, + "isZoomConstant": true, + "type": "string" + }, + "outputs": ["987,654,321.234568000000000", "987,654,321.2346"], + "serialized": [ + "number-format", + 987654321.2345679, + { + "locale": ["string", ["get", "locale"]], + "min-fraction-digits": ["number", ["get", "min"]], + "max-fraction-digits": ["number", ["get", "max"]] + } + ] + } +}