feat(pipes): add date pipe

Closes #2877
This commit is contained in:
Pouria Alimirzaei 2015-07-04 22:34:54 +04:30 committed by Tobias Bosch
parent 3143d188ae
commit b716046b97
8 changed files with 292 additions and 12 deletions

View File

@ -12,5 +12,6 @@ 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 {DatePipe} from './src/change_detection/pipes/date_pipe';
export {DecimalPipe, PercentPipe, CurrencyPipe} from './src/change_detection/pipes/number_pipe';
export {LimitToPipe} from './src/change_detection/pipes/limit_to_pipe';

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 {DatePipe} from './pipes/date_pipe';
import {DecimalPipe, PercentPipe, CurrencyPipe} from './pipes/number_pipe';
import {NullPipeFactory} from './pipes/null_pipe';
import {ChangeDetection, ProtoChangeDetector, ChangeDetectorDefinition} from './interfaces';
@ -101,6 +102,15 @@ export const percent: List<PipeFactory> =
export const currency: List<PipeFactory> =
CONST_EXPR([CONST_EXPR(new CurrencyPipe()), CONST_EXPR(new NullPipeFactory())]);
/**
* Date/time formatter.
*
* @exportedAs angular2/pipes
*/
export const date: List<PipeFactory> =
CONST_EXPR([CONST_EXPR(new DatePipe()), CONST_EXPR(new NullPipeFactory())]);
export const defaultPipes = CONST_EXPR({
"iterableDiff": iterableDiff,
"keyValDiff": keyValDiff,
@ -111,7 +121,8 @@ export const defaultPipes = CONST_EXPR({
"limitTo": limitTo,
"number": decimal,
"percent": percent,
"currency": currency
"currency": currency,
"date": date
});
/**

View File

@ -0,0 +1,102 @@
import {
isDate,
isNumber,
isPresent,
Date,
DateWrapper,
CONST,
FunctionWrapper
} from 'angular2/src/facade/lang';
import {DateFormatter} from 'angular2/src/facade/intl';
import {StringMapWrapper, ListWrapper} from 'angular2/src/facade/collection';
import {Pipe, BasePipe, PipeFactory} from './pipe';
import {ChangeDetectorRef} from '../change_detector_ref';
// TODO: move to a global configable location along with other i18n components.
var defaultLocale: string = 'en-US';
/**
* Formats a date value to a string based on the requested format.
*
* # Usage
*
* expression | date[:format]
*
* where `expression` is a date object or a number (milliseconds since UTC epoch) and
* `format` indicates which date/time components to include:
*
* | Component | Symbol | Short Form | Long Form | Numeric | 2-digit |
* |-----------|:------:|--------------|-------------------|-----------|-----------|
* | era | G | G (AD) | GGGG (Anno Domini)| - | - |
* | year | y | - | - | y (2015) | yy (15) |
* | month | M | MMM (Sep) | MMMM (September) | M (9) | MM (09) |
* | day | d | - | - | d (3) | dd (03) |
* | weekday | E | EEE (Sun) | EEEE (Sunday) | - | - |
* | hour | j | - | - | j (13) | jj (13) |
* | hour12 | h | - | - | h (1 PM) | hh (01 PM)|
* | hour24 | H | - | - | H (13) | HH (13) |
* | minute | m | - | - | m (5) | mm (05) |
* | second | s | - | - | s (9) | ss (09) |
* | timezone | z | - | z (Pacific Standard Time)| - | - |
* | timezone | Z | Z (GMT-8:00) | - | - | - |
*
* In javascript, only the components specified will be respected (not the ordering,
* punctuations, ...) and details of the the formatting will be dependent on the locale.
* On the other hand in Dart version, you can also include quoted text as well as some extra
* date/time components such as quarter. For more information see:
* https://api.dartlang.org/apidocs/channels/stable/dartdoc-viewer/intl/intl.DateFormat.
*
* `format` can also be one of the following predefined formats:
*
* - `'medium'`: equivalent to `'yMMMdjms'` (e.g. Sep 3, 2010, 12:05:08 PM for en-US)
* - `'short'`: equivalent to `'yMdjm'` (e.g. 9/3/2010, 12:05 PM for en-US)
* - `'fullDate'`: equivalent to `'yMMMMEEEEd'` (e.g. Friday, September 3, 2010 for en-US)
* - `'longDate'`: equivalent to `'yMMMMd'` (e.g. September 3, 2010)
* - `'mediumDate'`: equivalent to `'yMMMd'` (e.g. Sep 3, 2010 for en-US)
* - `'shortDate'`: equivalent to `'yMd'` (e.g. 9/3/2010 for en-US)
* - `'mediumTime'`: equivalent to `'jms'` (e.g. 12:05:08 PM for en-US)
* - `'shortTime'`: equivalent to `'jm'` (e.g. 12:05 PM for en-US)
*
* Timezone of the formatted text will be the local system timezone of the end-users machine.
*
* # Examples
*
* Assuming `dateObj` is (year: 2015, month: 6, day: 15, hour: 21, minute: 43, second: 11)
* in the _local_ time and locale is 'en-US':
*
* {{ dateObj | date }} // output is 'Jun 15, 2015'
* {{ dateObj | date:'medium' }} // output is 'Jun 15, 2015, 9:43:11 PM'
* {{ dateObj | date:'shortTime' }} // output is '9:43 PM'
* {{ dateObj | date:'mmss' }} // output is '43:11'
*
* @exportedAs angular2/pipes
*/
@CONST()
export class DatePipe extends BasePipe implements PipeFactory {
static _ALIASES = {
'medium': 'yMMMdjms',
'short': 'yMdjm',
'fullDate': 'yMMMMEEEEd',
'longDate': 'yMMMMd',
'mediumDate': 'yMMMd',
'shortDate': 'yMd',
'mediumTime': 'jms',
'shortTime': 'jm'
};
transform(value, args: List<any>): string {
var pattern: string = isPresent(args) && args.length > 0 ? args[0] : 'mediumDate';
if (isNumber(value)) {
value = DateWrapper.fromMillis(value);
}
if (StringMapWrapper.contains(DatePipe._ALIASES, pattern)) {
pattern = <string>StringMapWrapper.get(DatePipe._ALIASES, pattern);
}
return DateFormatter.format(value, defaultLocale, pattern);
}
supports(obj): boolean { return isDate(obj) || isNumber(obj); }
create(cdRef: ChangeDetectorRef): Pipe { return this }
}

View File

@ -40,3 +40,21 @@ class NumberFormatter {
return formatter.format(number);
}
}
class DateFormatter {
static RegExp _multiPartRegExp = new RegExp(r'^([yMdE]+)([Hjms]+)$');
static String format(DateTime date, String locale, String pattern) {
locale = _normalizeLocale(locale);
var formatter = new DateFormat(null, locale);
var matches = _multiPartRegExp.firstMatch(pattern);
if (matches != null) {
// Support for patterns which have known date and time components.
formatter.addPattern(matches[1]);
formatter.addPattern(matches[2], ', ');
} else {
formatter.addPattern(pattern);
}
return formatter.format(date);
}
}

View File

@ -14,9 +14,7 @@ declare module Intl {
format(value: number): string;
}
var NumberFormat: {
new (locale?: string, options?: NumberFormatOptions): NumberFormat;
}
var NumberFormat: { new (locale?: string, options?: NumberFormatOptions): NumberFormat; }
interface DateTimeFormatOptions {
localeMatcher?: string;
@ -37,9 +35,7 @@ declare module Intl {
format(date?: Date | number): string;
}
var DateTimeFormat: {
new (locale?: string, options?: DateTimeFormatOptions): DateTimeFormat;
}
var DateTimeFormat: { new (locale?: string, options?: DateTimeFormatOptions): DateTimeFormat; }
}
export enum NumberFormatStyle {
@ -71,3 +67,78 @@ export class NumberFormatter {
return new Intl.NumberFormat(locale, intlOptions).format(number);
}
}
function digitCondition(len: int): string {
return len == 2 ? '2-digit' : 'numeric';
}
function nameCondition(len: int): string {
return len < 4 ? 'short' : 'long';
}
function extractComponents(pattern: string): Intl.DateTimeFormatOptions {
var ret: Intl.DateTimeFormatOptions = {};
var i = 0, j;
while (i < pattern.length) {
j = i;
while (j < pattern.length && pattern[j] == pattern[i]) j++;
let len = j - i;
switch (pattern[i]) {
case 'G':
ret.era = nameCondition(len);
break;
case 'y':
ret.year = digitCondition(len);
break;
case 'M':
if (len >= 3)
ret.month = nameCondition(len);
else
ret.month = digitCondition(len);
break;
case 'd':
ret.day = digitCondition(len);
break;
case 'E':
ret.weekday = nameCondition(len);
break;
case 'j':
ret.hour = digitCondition(len);
break;
case 'h':
ret.hour = digitCondition(len);
ret.hour12 = true;
break;
case 'H':
ret.hour = digitCondition(len);
ret.hour12 = false;
break;
case 'm':
ret.minute = digitCondition(len);
break;
case 's':
ret.second = digitCondition(len);
break;
case 'z':
ret.timeZoneName = 'long';
break;
case 'Z':
ret.timeZoneName = 'short';
break;
}
i = j;
}
return ret;
}
var dateFormatterCache: Map<string, Intl.DateTimeFormat> = new Map<string, Intl.DateTimeFormat>();
export class DateFormatter {
static format(date: Date, locale: string, pattern: string): string {
var key = locale + pattern;
if (dateFormatterCache.has(key)) {
return dateFormatterCache.get(key).format(date);
}
var formatter = new Intl.DateTimeFormat(locale, extractComponents(pattern));
dateFormatterCache.set(key, formatter);
return formatter.format(date);
}
}

View File

@ -33,6 +33,7 @@ bool isStringMap(obj) => obj is Map;
bool isArray(obj) => obj is List;
bool isPromise(obj) => obj is Future;
bool isNumber(obj) => obj is num;
bool isDate(obj) => obj is DateTime;
String stringify(obj) => obj.toString();
@ -232,8 +233,12 @@ class Json {
}
class DateWrapper {
static DateTime create(int year, [int month = 1, int day = 1, int hour = 0,
int minutes = 0, int seconds = 0, int milliseconds = 0]) {
return new DateTime(year, month, day, hour, minutes, seconds, milliseconds);
}
static DateTime fromMillis(int ms) {
return new DateTime.fromMillisecondsSinceEpoch(ms);
return new DateTime.fromMillisecondsSinceEpoch(ms, isUtc: true);
}
static int toMillis(DateTime date) {
return date.millisecondsSinceEpoch;
@ -241,7 +246,7 @@ class DateWrapper {
static DateTime now() {
return new DateTime.now();
}
static toJson(DateTime date) {
static String toJson(DateTime date) {
return date.toUtc().toIso8601String();
}
}

View File

@ -93,6 +93,10 @@ export function isNumber(obj): boolean {
return typeof obj === 'number';
}
export function isDate(obj): boolean {
return obj instanceof Date && !isNaN(obj.valueOf());
}
export function stringify(token): string {
if (typeof token === 'string') {
return token;
@ -282,8 +286,12 @@ export class Json {
}
export class DateWrapper {
static fromMillis(ms): Date { return new Date(ms); }
static toMillis(date: Date): number { return date.getTime(); }
static now(): Date { return new Date(); }
static toJson(date): string { return date.toJSON(); }
static create(year: int, month: int = 1, day: int = 1, hour: int = 0, minutes: int = 0,
seconds: int = 0, milliseconds: int = 0): Date {
return new Date(year, month - 1, day, hour, minutes, seconds, milliseconds);
}
static fromMillis(ms: int): Date { return new Date(ms); }
static toMillis(date: Date): int { return date.getTime(); }
static now(): Date { return new Date(); }
static toJson(date: Date): string { return date.toJSON(); }
}

View File

@ -0,0 +1,64 @@
import {ddescribe, describe, it, iit, xit, expect, beforeEach, afterEach} from 'angular2/test_lib';
import {DatePipe} from 'angular2/src/change_detection/pipes/date_pipe';
import {DateWrapper} from 'angular2/src/facade/lang';
export function main() {
describe("DatePipe", () => {
var date;
var pipe;
beforeEach(() => {
date = DateWrapper.create(2015, 6, 15, 21, 43, 11);
pipe = new DatePipe();
});
describe("supports", () => {
it("should support date", () => { expect(pipe.supports(date)).toBe(true); });
it("should support int", () => { expect(pipe.supports(123456789)).toBe(true); });
it("should not support other objects", () => {
expect(pipe.supports(new Object())).toBe(false);
expect(pipe.supports(null)).toBe(false);
});
});
describe("transform", () => {
it('should format each component correctly', () => {
expect(pipe.transform(date, ['y'])).toEqual('2015');
expect(pipe.transform(date, ['yy'])).toEqual('15');
expect(pipe.transform(date, ['M'])).toEqual('6');
expect(pipe.transform(date, ['MM'])).toEqual('06');
expect(pipe.transform(date, ['MMM'])).toEqual('Jun');
expect(pipe.transform(date, ['MMMM'])).toEqual('June');
expect(pipe.transform(date, ['d'])).toEqual('15');
expect(pipe.transform(date, ['E'])).toEqual('Mon');
expect(pipe.transform(date, ['EEEE'])).toEqual('Monday');
expect(pipe.transform(date, ['H'])).toEqual('21');
expect(pipe.transform(date, ['j'])).toEqual('9 PM');
expect(pipe.transform(date, ['m'])).toEqual('43');
expect(pipe.transform(date, ['s'])).toEqual('11');
});
it('should format common multi component patterns', () => {
expect(pipe.transform(date, ['yMEd'])).toEqual('Mon, 6/15/2015');
expect(pipe.transform(date, ['MEd'])).toEqual('Mon, 6/15');
expect(pipe.transform(date, ['MMMd'])).toEqual('Jun 15');
expect(pipe.transform(date, ['yMMMMEEEEd'])).toEqual('Monday, June 15, 2015');
expect(pipe.transform(date, ['jms'])).toEqual('9:43:11 PM');
expect(pipe.transform(date, ['ms'])).toEqual('43:11');
});
it('should format with pattern aliases', () => {
expect(pipe.transform(date, ['medium'])).toEqual('Jun 15, 2015, 9:43:11 PM');
expect(pipe.transform(date, ['short'])).toEqual('6/15/2015, 9:43 PM');
expect(pipe.transform(date, ['fullDate'])).toEqual('Monday, June 15, 2015');
expect(pipe.transform(date, ['longDate'])).toEqual('June 15, 2015');
expect(pipe.transform(date, ['mediumDate'])).toEqual('Jun 15, 2015');
expect(pipe.transform(date, ['shortDate'])).toEqual('6/15/2015');
expect(pipe.transform(date, ['mediumTime'])).toEqual('9:43:11 PM');
expect(pipe.transform(date, ['shortTime'])).toEqual('9:43 PM');
});
});
});
}