From 324f0147f672b5f341d978910c6e64453cbe89e6 Mon Sep 17 00:00:00 2001 From: Andrei Tserakhau Date: Thu, 26 May 2016 22:06:29 +0300 Subject: [PATCH] feat(common/datePipe): change date formatter to use correct pattern closes #7008 (#8154) - add regular expression to parse date parts - add date part creator function - replace tokens in pattern to parsed parts --- .../common/test/pipes/date_pipe_spec.ts | 8 + modules/@angular/facade/src/intl.ts | 233 +++++++++++++----- 2 files changed, 177 insertions(+), 64 deletions(-) diff --git a/modules/@angular/common/test/pipes/date_pipe_spec.ts b/modules/@angular/common/test/pipes/date_pipe_spec.ts index fad0c0b2fd..60ab8c0afa 100644 --- a/modules/@angular/common/test/pipes/date_pipe_spec.ts +++ b/modules/@angular/common/test/pipes/date_pipe_spec.ts @@ -61,17 +61,25 @@ export function main() { }); it('should format common multi component patterns', () => { + expect(pipe.transform(date, 'E, M/d/y')).toEqual('Mon, 6/15/2015'); + expect(pipe.transform(date, 'E, M/d')).toEqual('Mon, 6/15'); + expect(pipe.transform(date, 'MMM d')).toEqual('Jun 15'); + expect(pipe.transform(date, 'dd/MM/yyyy')).toEqual('15/06/2015'); + expect(pipe.transform(date, 'MM/dd/yyyy')).toEqual('06/15/2015'); 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'); + expect(pipe.transform(date, 'jm')).toEqual('9:43'); }); 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, 'dd/MM/yyyy')).toEqual('15/06/2015'); + expect(pipe.transform(date, 'MM/dd/yyyy')).toEqual('06/15/2015'); 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'); diff --git a/modules/@angular/facade/src/intl.ts b/modules/@angular/facade/src/intl.ts index 4a0494047b..58370f7abd 100644 --- a/modules/@angular/facade/src/intl.ts +++ b/modules/@angular/facade/src/intl.ts @@ -70,78 +70,183 @@ export class NumberFormatter { return new Intl.NumberFormat(locale, intlOptions).format(num); } } +var DATE_FORMATS_SPLIT = + /((?:[^yMLdHhmsaZEwGjJ']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|J+|j+|m+|s+|a|Z|G+|w+))(.*)/; -function digitCondition(len: number): string { - return len == 2 ? '2-digit' : 'numeric'; +var PATTERN_ALIASES = { + yMMMdjms: datePartGetterFactory(combine([ + digitCondition('year', 1), + nameCondition('month', 3), + digitCondition('day', 1), + digitCondition('hour', 1), + digitCondition('minute', 1), + digitCondition('second', 1), + ])), + yMdjm: datePartGetterFactory(combine([ + digitCondition('year', 1), + digitCondition('month', 1), + digitCondition('day', 1), + digitCondition('hour', 1), + digitCondition('minute', 1) + ])), + yMMMMEEEEd: datePartGetterFactory(combine([ + digitCondition('year', 1), + nameCondition('month', 4), + nameCondition('weekday', 4), + digitCondition('day', 1) + ])), + yMMMMd: datePartGetterFactory( + combine([digitCondition('year', 1), nameCondition('month', 4), digitCondition('day', 1)])), + yMMMd: datePartGetterFactory( + combine([digitCondition('year', 1), nameCondition('month', 3), digitCondition('day', 1)])), + yMd: datePartGetterFactory( + combine([digitCondition('year', 1), digitCondition('month', 1), digitCondition('day', 1)])), + jms: datePartGetterFactory(combine( + [digitCondition('hour', 1), digitCondition('second', 1), digitCondition('minute', 1)])), + jm: datePartGetterFactory(combine([digitCondition('hour', 1), digitCondition('minute', 1)])) +}; + +var DATE_FORMATS = { + yyyy: datePartGetterFactory(digitCondition('year', 4)), + yy: datePartGetterFactory(digitCondition('year', 2)), + y: datePartGetterFactory(digitCondition('year', 1)), + MMMM: datePartGetterFactory(nameCondition('month', 4)), + MMM: datePartGetterFactory(nameCondition('month', 3)), + MM: datePartGetterFactory(digitCondition('month', 2)), + M: datePartGetterFactory(digitCondition('month', 1)), + LLLL: datePartGetterFactory(nameCondition('month', 4)), + dd: datePartGetterFactory(digitCondition('day', 2)), + d: datePartGetterFactory(digitCondition('day', 1)), + HH: hourExtracter(datePartGetterFactory(hour12Modify(digitCondition('hour', 2), false))), + H: hourExtracter(datePartGetterFactory(hour12Modify(digitCondition('hour', 1), false))), + hh: hourExtracter(datePartGetterFactory(hour12Modify(digitCondition('hour', 2), true))), + h: hourExtracter(datePartGetterFactory(hour12Modify(digitCondition('hour', 1), true))), + jj: datePartGetterFactory(digitCondition('hour', 2)), + j: datePartGetterFactory(digitCondition('hour', 1)), + mm: datePartGetterFactory(digitCondition('minute', 2)), + m: datePartGetterFactory(digitCondition('minute', 1)), + ss: datePartGetterFactory(digitCondition('second', 2)), + s: datePartGetterFactory(digitCondition('second', 1)), + // while ISO 8601 requires fractions to be prefixed with `.` or `,` + // we can be just safely rely on using `sss` since we currently don't support single or two digit + // fractions + sss: datePartGetterFactory(digitCondition('second', 3)), + EEEE: datePartGetterFactory(nameCondition('weekday', 4)), + EEE: datePartGetterFactory(nameCondition('weekday', 3)), + EE: datePartGetterFactory(nameCondition('weekday', 2)), + E: datePartGetterFactory(nameCondition('weekday', 1)), + a: hourClockExtracter(datePartGetterFactory(hour12Modify(digitCondition('hour', 1), true))), + Z: datePartGetterFactory({timeZoneName: 'long'}), + z: datePartGetterFactory({timeZoneName: 'short'}), + ww: datePartGetterFactory({}), // Week of year, padded (00-53). Week 01 is the week with the + // first Thursday of the year. not support ? + w: datePartGetterFactory({}), // Week of year (0-53). Week 1 is the week with the first Thursday + // of the year not support ? + G: datePartGetterFactory(nameCondition('era', 1)), + GG: datePartGetterFactory(nameCondition('era', 2)), + GGG: datePartGetterFactory(nameCondition('era', 3)), + GGGG: datePartGetterFactory(nameCondition('era', 4)) +}; + + +function hourClockExtracter(inner: (date: Date, locale: string) => string): ( + date: Date, locale: string) => string { + return function(date: Date, locale: string): string { + var result = inner(date, locale); + + return result.split(' ')[1]; + }; } -function nameCondition(len: number): string { - return len < 4 ? 'short' : 'long'; + +function hourExtracter(inner: (date: Date, locale: string) => string): (date: Date, + locale: string) => string { + return function(date: Date, locale: string): string { + var result = inner(date, locale); + + return result.split(' ')[0]; + }; } -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; + +function hour12Modify(options: Intl.DateTimeFormatOptions, + value: boolean): Intl.DateTimeFormatOptions { + options.hour12 = value; + return options; +} + +function digitCondition(prop: string, len: number): Intl.DateTimeFormatOptions { + var result = {}; + result[prop] = len == 2 ? '2-digit' : 'numeric'; + return result; +} +function nameCondition(prop: string, len: number): Intl.DateTimeFormatOptions { + var result = {}; + result[prop] = len < 4 ? 'short' : 'long'; + return result; +} + +function combine(options: Intl.DateTimeFormatOptions[]): Intl.DateTimeFormatOptions { + var result = {}; + + options.forEach(option => { (Object).assign(result, option); }); + + return result; +} + +function datePartGetterFactory(ret: Intl.DateTimeFormatOptions): (date: Date, locale: string) => + string { + return function(date: Date, locale: string): string { + return new Intl.DateTimeFormat(locale, ret).format(date); + }; +} + + +var datePartsFormatterCache: Map = new Map(); + +function dateFormatter(format: string, date: Date, locale: string): string { + var text = ''; + var match; + var fn; + var parts: string[] = []; + if (PATTERN_ALIASES[format]) { + return PATTERN_ALIASES[format](date, locale); } - return ret; + + + if (datePartsFormatterCache.has(format)) { + parts = datePartsFormatterCache.get(format); + } else { + var matchs = DATE_FORMATS_SPLIT.exec(format); + + while (format) { + match = DATE_FORMATS_SPLIT.exec(format); + if (match) { + parts = concat(parts, match, 1); + format = parts.pop(); + } else { + parts.push(format); + format = null; + } + } + + datePartsFormatterCache.set(format, parts); + } + + parts.forEach(part => { + fn = DATE_FORMATS[part]; + text += fn ? fn(date, locale) : + part === "''" ? "'" : part.replace(/(^'|'$)/g, '').replace(/''/g, "'"); + }); + + return text; } -var dateFormatterCache: Map = new Map(); +var slice = [].slice; +function concat(array1, array2, index): string[] { + return array1.concat(slice.call(array2, index)); +} 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); + return dateFormatter(pattern, date, locale); } }