-
Notifications
You must be signed in to change notification settings - Fork 2.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
"number-format" expression #7626
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -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<Intl$NumberFormat> | ||||||||||||
}; | ||||||||||||
|
||||||||||||
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 | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would we support any extensions, such as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
That sounds right to me. |
||||||||||||
currency: Expression | null; // ISO 4217 currency code, required if style=currency | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yeah, that’s the right approach. Operating systems allow you to choose the preferred currency independently of the system region. By default, the system region typically affects mechanics like decimal separators, but not semantics or content. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here’s a correspondence to other implementation languages:
In Java, use |
||||||||||||
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<mixed>, 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]; | ||||||||||||
} | ||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"]] | ||
} | ||
] | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"]] | ||
} | ||
] | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NSNumberFormatterStyle
actually supports a number of styles, includingNSNumberFormatterSpellOutStyle
andNSNumberFormatterOrdinalStyle
. However, I recognize the need to stick to JavaScript’sIntl
as a common denominator, without implementing our own custom types that could conflict with future JavaScript types.This does mean that the style author will need to hard-code some logic to implement short formats, such as the “1k” for 1,000 that Supercluster implements (only for English) in the
point_count_abbreviated
property (mapbox/supercluster#110)./ref tc39/ecma402#37 tc39/ecma402#215.