feat(pipes): add number (decimal, percent, currency) pipes

This commit is contained in:
Pouria Alimirzaei 2015-07-04 22:25:43 +04:30 committed by Tobias Bosch
parent b54e7214f0
commit 3143d188ae
11 changed files with 368 additions and 1 deletions

View File

@ -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';

View File

@ -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'

View File

@ -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<PipeFactory> =
export const limitTo: List<PipeFactory> =
CONST_EXPR([CONST_EXPR(new LimitToPipeFactory()), CONST_EXPR(new NullPipeFactory())]);
/**
* Number number transform.
*
* @exportedAs angular2/pipes
*/
export const decimal: List<PipeFactory> =
CONST_EXPR([CONST_EXPR(new DecimalPipe()), CONST_EXPR(new NullPipeFactory())]);
/**
* Percent number transform.
*
* @exportedAs angular2/pipes
*/
export const percent: List<PipeFactory> =
CONST_EXPR([CONST_EXPR(new PercentPipe()), CONST_EXPR(new NullPipeFactory())]);
/**
* Currency number transform.
*
* @exportedAs angular2/pipes
*/
export const currency: List<PipeFactory> =
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
});
/**

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();

View File

@ -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;

View File

@ -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');
});
});
});
}

View File

@ -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"

View File

@ -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'