From 3143d188ae2566187979a569f2d72f94d0cedd45 Mon Sep 17 00:00:00 2001 From: Pouria Alimirzaei Date: Sat, 4 Jul 2015 22:25:43 +0430 Subject: [PATCH] feat(pipes): add number (decimal, percent, currency) pipes --- modules/angular2/pipes.ts | 1 + modules/angular2/pubspec.yaml | 1 + .../src/change_detection/change_detection.ts | 30 +++- .../src/change_detection/pipes/number_pipe.ts | 132 ++++++++++++++++++ modules/angular2/src/facade/intl.dart | 42 ++++++ modules/angular2/src/facade/intl.ts | 73 ++++++++++ modules/angular2/src/facade/lang.dart | 1 + modules/angular2/src/facade/lang.ts | 4 + .../pipes/number_pipe_spec.ts | 82 +++++++++++ modules/benchmarks_external/pubspec.yaml | 2 + pubspec.yaml | 1 + 11 files changed, 368 insertions(+), 1 deletion(-) create mode 100644 modules/angular2/src/change_detection/pipes/number_pipe.ts create mode 100644 modules/angular2/src/facade/intl.dart create mode 100644 modules/angular2/src/facade/intl.ts create mode 100644 modules/angular2/test/change_detection/pipes/number_pipe_spec.ts diff --git a/modules/angular2/pipes.ts b/modules/angular2/pipes.ts index 84309712fa..fd863db924 100644 --- a/modules/angular2/pipes.ts +++ b/modules/angular2/pipes.ts @@ -12,4 +12,5 @@ export {ObservablePipe} from './src/change_detection/pipes/observable_pipe'; export {JsonPipe} from './src/change_detection/pipes/json_pipe'; export {IterableChanges} from './src/change_detection/pipes/iterable_changes'; export {KeyValueChanges} from './src/change_detection/pipes/keyvalue_changes'; +export {DecimalPipe, PercentPipe, CurrencyPipe} from './src/change_detection/pipes/number_pipe'; export {LimitToPipe} from './src/change_detection/pipes/limit_to_pipe'; diff --git a/modules/angular2/pubspec.yaml b/modules/angular2/pubspec.yaml index c0b7d76030..0632079a69 100644 --- a/modules/angular2/pubspec.yaml +++ b/modules/angular2/pubspec.yaml @@ -14,6 +14,7 @@ dependencies: code_transformers: '^0.2.8' dart_style: '^0.1.8' html: '^0.12.0' + intl: '^0.12.4' logging: '>=0.9.0 <0.12.0' source_span: '^1.0.0' stack_trace: '^1.1.1' diff --git a/modules/angular2/src/change_detection/change_detection.ts b/modules/angular2/src/change_detection/change_detection.ts index 66b104374f..1692730bc7 100644 --- a/modules/angular2/src/change_detection/change_detection.ts +++ b/modules/angular2/src/change_detection/change_detection.ts @@ -11,6 +11,7 @@ import {UpperCaseFactory} from './pipes/uppercase_pipe'; import {LowerCaseFactory} from './pipes/lowercase_pipe'; import {JsonPipe} from './pipes/json_pipe'; import {LimitToPipeFactory} from './pipes/limit_to_pipe'; +import {DecimalPipe, PercentPipe, CurrencyPipe} from './pipes/number_pipe'; import {NullPipeFactory} from './pipes/null_pipe'; import {ChangeDetection, ProtoChangeDetector, ChangeDetectorDefinition} from './interfaces'; import {Inject, Injectable, OpaqueToken, Optional} from 'angular2/di'; @@ -76,6 +77,30 @@ export const json: List = export const limitTo: List = CONST_EXPR([CONST_EXPR(new LimitToPipeFactory()), CONST_EXPR(new NullPipeFactory())]); +/** + * Number number transform. + * + * @exportedAs angular2/pipes + */ +export const decimal: List = + CONST_EXPR([CONST_EXPR(new DecimalPipe()), CONST_EXPR(new NullPipeFactory())]); + +/** + * Percent number transform. + * + * @exportedAs angular2/pipes + */ +export const percent: List = + CONST_EXPR([CONST_EXPR(new PercentPipe()), CONST_EXPR(new NullPipeFactory())]); + +/** + * Currency number transform. + * + * @exportedAs angular2/pipes + */ +export const currency: List = + CONST_EXPR([CONST_EXPR(new CurrencyPipe()), CONST_EXPR(new NullPipeFactory())]); + export const defaultPipes = CONST_EXPR({ "iterableDiff": iterableDiff, "keyValDiff": keyValDiff, @@ -83,7 +108,10 @@ export const defaultPipes = CONST_EXPR({ "uppercase": uppercase, "lowercase": lowercase, "json": json, - "limitTo": limitTo + "limitTo": limitTo, + "number": decimal, + "percent": percent, + "currency": currency }); /** diff --git a/modules/angular2/src/change_detection/pipes/number_pipe.ts b/modules/angular2/src/change_detection/pipes/number_pipe.ts new file mode 100644 index 0000000000..cbbf70d839 --- /dev/null +++ b/modules/angular2/src/change_detection/pipes/number_pipe.ts @@ -0,0 +1,132 @@ +import { + isNumber, + isPresent, + isBlank, + StringWrapper, + NumberWrapper, + RegExpWrapper, + BaseException, + CONST, + FunctionWrapper +} from 'angular2/src/facade/lang'; +import {NumberFormatter, NumberFormatStyle} from 'angular2/src/facade/intl'; +import {ListWrapper} from 'angular2/src/facade/collection'; +import {Pipe, BasePipe, PipeFactory} from './pipe'; +import {ChangeDetectorRef} from '../change_detector_ref'; + +var defaultLocale: string = 'en-US'; +var _re = RegExpWrapper.create('^(\\d+)?\\.((\\d+)(\\-(\\d+))?)?$'); + +@CONST() +export class NumberPipe extends BasePipe implements PipeFactory { + static _format(value: number, style: NumberFormatStyle, digits: string, currency: string = null, + currencyAsSymbol: boolean = false): string { + var minInt = 1, minFraction = 0, maxFraction = 3; + if (isPresent(digits)) { + var parts = RegExpWrapper.firstMatch(_re, digits); + if (isBlank(parts)) { + throw new BaseException(`${digits} is not a valid digit info for number pipes`); + } + if (isPresent(parts[1])) { // min integer digits + minInt = NumberWrapper.parseIntAutoRadix(parts[1]); + } + if (isPresent(parts[3])) { // min fraction digits + minFraction = NumberWrapper.parseIntAutoRadix(parts[3]); + } + if (isPresent(parts[5])) { // max fraction digits + maxFraction = NumberWrapper.parseIntAutoRadix(parts[5]); + } + } + return NumberFormatter.format(value, defaultLocale, style, { + minimumIntegerDigits: minInt, + minimumFractionDigits: minFraction, + maximumFractionDigits: maxFraction, + currency: currency, + currencyAsSymbol: currencyAsSymbol + }); + } + + supports(obj): boolean { return isNumber(obj); } + + create(cdRef: ChangeDetectorRef): Pipe { return this } +} + +/** + * Formats a number as local text. i.e. group sizing and seperator and other locale-specific + * configurations are based on the active locale. + * + * # Usage + * + * expression | number[:digitInfo] + * + * where `expression` is a number and `digitInfo` has the following format: + * + * {minIntegerDigits}.{minFractionDigits}-{maxFractionDigits} + * + * - minIntegerDigits is the minimum number of integer digits to use. Defaults to 1. + * - minFractionDigits is the minimum number of digits after fraction. Defaults to 0. + * - maxFractionDigits is the maximum number of digits after fraction. Defaults to 3. + * + * For more information on the acceptable range for each of these numbers and other + * details see your native internationalization library. + * + * # Examples + * + * {{ 123 | number }} // output is 123 + * {{ 123.1 | number: '.2-3' }} // output is 123.10 + * {{ 1 | number: '2.2' }} // output is 01.00 + * + * @exportedAs angular2/pipes + */ +@CONST() +export class DecimalPipe extends NumberPipe { + transform(value, args: any[]): string { + var digits: string = ListWrapper.first(args); + return NumberPipe._format(value, NumberFormatStyle.DECIMAL, digits); + } +} + +/** + * Formats a number as local percent. + * + * # Usage + * + * expression | percent[:digitInfo] + * + * For more information about `digitInfo` see {@link DecimalPipe} + * + * @exportedAs angular2/pipes + */ +@CONST() +export class PercentPipe extends NumberPipe { + transform(value, args: any[]): string { + var digits: string = ListWrapper.first(args); + return NumberPipe._format(value, NumberFormatStyle.PERCENT, digits); + } +} + +/** + * Formats a number as local currency. + * + * # Usage + * + * expression | currency[:currencyCode[:symbolDisplay[:digitInfo]]] + * + * where `currencyCode` is the ISO 4217 currency code, such as "USD" for the US dollar and + * "EUR" for the euro. `symbolDisplay` is a boolean indicating whether to use the currency + * symbol (e.g. $) or the currency code (e.g. USD) in the output. The default for this value + * is `false`. + * For more information about `digitInfo` see {@link DecimalPipe} + * + * @exportedAs angular2/pipes + */ +@CONST() +export class CurrencyPipe extends NumberPipe { + transform(value, args: any[]): string { + var currencyCode: string = isPresent(args) && args.length > 0 ? args[0] : 'USD'; + var symbolDisplay: boolean = isPresent(args) && args.length > 1 ? args[1] : false; + var digits: string = isPresent(args) && args.length > 2 ? args[2] : null; + return NumberPipe._format(value, NumberFormatStyle.CURRENCY, digits, currencyCode, + symbolDisplay); + } +} diff --git a/modules/angular2/src/facade/intl.dart b/modules/angular2/src/facade/intl.dart new file mode 100644 index 0000000000..d513569aa0 --- /dev/null +++ b/modules/angular2/src/facade/intl.dart @@ -0,0 +1,42 @@ +library facade.intl; + +import 'package:intl/intl.dart'; + +String _normalizeLocale(String locale) => locale.replaceAll('-', '_'); + +enum NumberFormatStyle { + DECIMAL, + PERCENT, + CURRENCY +} + +class NumberFormatter { + static String format(num number, String locale, NumberFormatStyle style, + {int minimumIntegerDigits: 1, + int minimumFractionDigits: 0, + int maximumFractionDigits: 3, + String currency, + bool currencyAsSymbol: false}) { + locale = _normalizeLocale(locale); + NumberFormat formatter; + switch (style) { + case NumberFormatStyle.DECIMAL: + formatter = new NumberFormat.decimalPattern(locale); + break; + case NumberFormatStyle.PERCENT: + formatter = new NumberFormat.percentPattern(locale); + break; + case NumberFormatStyle.CURRENCY: + if (currencyAsSymbol) { + // See https://github.com/dart-lang/intl/issues/59. + throw new Exception('Displaying currency as symbol is not supported.'); + } + formatter = new NumberFormat.currencyPattern(locale, currency); + break; + } + formatter.minimumIntegerDigits = minimumIntegerDigits; + formatter.minimumFractionDigits = minimumFractionDigits; + formatter.maximumFractionDigits = maximumFractionDigits; + return formatter.format(number); + } +} diff --git a/modules/angular2/src/facade/intl.ts b/modules/angular2/src/facade/intl.ts new file mode 100644 index 0000000000..238327e90d --- /dev/null +++ b/modules/angular2/src/facade/intl.ts @@ -0,0 +1,73 @@ + +// Modified version of internal Typescript intl.d.ts. +// TODO(piloopin): remove when https://github.com/Microsoft/TypeScript/issues/3521 is shipped. +declare module Intl { + interface NumberFormatOptions { + localeMatcher?: string; + style?: string; + currency?: string; + currencyDisplay?: string; + useGrouping?: boolean; + } + + interface NumberFormat { + format(value: number): string; + } + + var NumberFormat: { + new (locale?: string, options?: NumberFormatOptions): NumberFormat; + } + + interface DateTimeFormatOptions { + localeMatcher?: string; + weekday?: string; + era?: string; + year?: string; + month?: string; + day?: string; + hour?: string; + minute?: string; + second?: string; + timeZoneName?: string; + formatMatcher?: string; + hour12?: boolean; + } + + interface DateTimeFormat { + format(date?: Date | number): string; + } + + var DateTimeFormat: { + new (locale?: string, options?: DateTimeFormatOptions): DateTimeFormat; + } +} + +export enum NumberFormatStyle { + DECIMAL, + PERCENT, + CURRENCY +} + +export class NumberFormatter { + static format(number: number, locale: string, style: NumberFormatStyle, + {minimumIntegerDigits = 1, minimumFractionDigits = 0, maximumFractionDigits = 3, + currency, currencyAsSymbol = false}: { + minimumIntegerDigits?: int, + minimumFractionDigits?: int, + maximumFractionDigits?: int, + currency?: string, + currencyAsSymbol?: boolean + } = {}): string { + var intlOptions: Intl.NumberFormatOptions = { + minimumIntegerDigits: minimumIntegerDigits, + minimumFractionDigits: minimumFractionDigits, + maximumFractionDigits: maximumFractionDigits + }; + intlOptions.style = NumberFormatStyle[style].toLowerCase(); + if (style == NumberFormatStyle.CURRENCY) { + intlOptions.currency = currency; + intlOptions.currencyDisplay = currencyAsSymbol ? 'symbol' : 'code'; + } + return new Intl.NumberFormat(locale, intlOptions).format(number); + } +} diff --git a/modules/angular2/src/facade/lang.dart b/modules/angular2/src/facade/lang.dart index 5e244e57ee..9f2d7c46d2 100644 --- a/modules/angular2/src/facade/lang.dart +++ b/modules/angular2/src/facade/lang.dart @@ -32,6 +32,7 @@ bool isType(obj) => obj is Type; bool isStringMap(obj) => obj is Map; bool isArray(obj) => obj is List; bool isPromise(obj) => obj is Future; +bool isNumber(obj) => obj is num; String stringify(obj) => obj.toString(); diff --git a/modules/angular2/src/facade/lang.ts b/modules/angular2/src/facade/lang.ts index 38ecc36910..929b453533 100644 --- a/modules/angular2/src/facade/lang.ts +++ b/modules/angular2/src/facade/lang.ts @@ -89,6 +89,10 @@ export function isArray(obj): boolean { return Array.isArray(obj); } +export function isNumber(obj): boolean { + return typeof obj === 'number'; +} + export function stringify(token): string { if (typeof token === 'string') { return token; diff --git a/modules/angular2/test/change_detection/pipes/number_pipe_spec.ts b/modules/angular2/test/change_detection/pipes/number_pipe_spec.ts new file mode 100644 index 0000000000..08c246099f --- /dev/null +++ b/modules/angular2/test/change_detection/pipes/number_pipe_spec.ts @@ -0,0 +1,82 @@ +import {ddescribe, describe, it, iit, xit, expect, beforeEach, afterEach} from 'angular2/test_lib'; + +import { + DecimalPipe, + PercentPipe, + CurrencyPipe +} from 'angular2/src/change_detection/pipes/number_pipe'; + +export function main() { + describe("DecimalPipe", () => { + var pipe; + + beforeEach(() => { pipe = new DecimalPipe(); }); + + describe("supports", () => { + it("should support numbers", () => { expect(pipe.supports(123.0)).toBe(true); }); + + it("should not support other objects", () => { + expect(pipe.supports(new Object())).toBe(false); + expect(pipe.supports('str')).toBe(false); + expect(pipe.supports(null)).toBe(false); + }); + }); + + describe("transform", () => { + it('should return correct value', () => { + expect(pipe.transform(12345, [])).toEqual('12,345'); + expect(pipe.transform(123, ['.2'])).toEqual('123.00'); + expect(pipe.transform(1, ['3.'])).toEqual('001'); + expect(pipe.transform(1.1, ['3.4-5'])).toEqual('001.1000'); + expect(pipe.transform(1.123456, ['3.4-5'])).toEqual('001.12346'); + expect(pipe.transform(1.1234, [])).toEqual('1.123'); + }); + }); + }); + + describe("PercentPipe", () => { + var pipe; + + beforeEach(() => { pipe = new PercentPipe(); }); + + describe("supports", () => { + it("should support numbers", () => { expect(pipe.supports(123.0)).toBe(true); }); + + it("should not support other objects", () => { + expect(pipe.supports(new Object())).toBe(false); + expect(pipe.supports('str')).toBe(false); + expect(pipe.supports(null)).toBe(false); + }); + }); + + describe("transform", () => { + it('should return correct value', () => { + expect(pipe.transform(1.23, [])).toEqual('123%'); + expect(pipe.transform(1.2, ['.2'])).toEqual('120.00%'); + }); + }); + }); + + describe("CurrencyPipe", () => { + var pipe; + + beforeEach(() => { pipe = new CurrencyPipe(); }); + + describe("supports", () => { + it("should support numbers", () => { expect(pipe.supports(123.0)).toBe(true); }); + + it("should not support other objects", () => { + expect(pipe.supports(new Object())).toBe(false); + expect(pipe.supports('str')).toBe(false); + expect(pipe.supports(null)).toBe(false); + }); + }); + + describe("transform", () => { + it('should return correct value', () => { + expect(pipe.transform(123, [])).toEqual('USD123'); + expect(pipe.transform(12, ['EUR', false, '.2'])).toEqual('EUR12.00'); + }); + }); + }); +} diff --git a/modules/benchmarks_external/pubspec.yaml b/modules/benchmarks_external/pubspec.yaml index 9e4c2e3571..7f1fc456b6 100644 --- a/modules/benchmarks_external/pubspec.yaml +++ b/modules/benchmarks_external/pubspec.yaml @@ -7,6 +7,8 @@ dependencies: dev_dependencies: angular2: path: ../angular2 +dependency_overrides: + intl: '^0.12.4' # angular depends on an older version of intl. transformers: - angular: $exclude: "web/e2e_test" diff --git a/pubspec.yaml b/pubspec.yaml index 4f02348d21..110de16b6a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,5 +3,6 @@ environment: sdk: '>=1.9.0 <2.0.0' dev_dependencies: guinness: '^0.1.17' + intl: '^0.12.4' unittest: '^0.11.5+4' quiver: '^0.21.4'