Skip to content

Commit

Permalink
feat(common): add an Intl API implementation for date formatting.
Browse files Browse the repository at this point in the history
With this commit, the Intl implementation because the default one and doesn't require the CLDR locale data imports anymore.

Opt-out is possible by invoking `useLegacyDateFormatting()` ahead of bootstrap.

BREAKING CHANGE: Some custom date format aren't supported any more (`cccccc`, `EEEEEE`, `aaaaa`, `b` to `bbbbb` and `B` to `BBBBB`)
The `DatePipe` will not support offset timezone anymore, use IANA timezones instead .
  • Loading branch information
JeanMeche committed May 8, 2024
1 parent 8fc4c5b commit 344b8d2
Show file tree
Hide file tree
Showing 9 changed files with 621 additions and 200 deletions.
8 changes: 7 additions & 1 deletion goldens/public-api/common/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ export class DecimalPipe implements PipeTransform {
export const DOCUMENT: InjectionToken<Document>;

// @public
export function formatCurrency(value: number, locale: string, display: 'code' | 'symbol' | 'symbol-narrow' | string, currencyCode?: string, digitsInfo?: string): string;
export function formatCurrency(value: number, locale: string, currency: string, currencyCode?: string, digitsInfo?: string): string;

// @public
export function formatDate(value: string | number | Date, format: string, locale: string, timezone?: string): string;
Expand Down Expand Up @@ -952,6 +952,12 @@ export class UpperCasePipe implements PipeTransform {
static ɵpipe: i0.ɵɵPipeDeclaration<UpperCasePipe, "uppercase", true>;
}

// @public (undocumented)
export const useIntlImplementation: () => void;

// @public (undocumented)
export const useLegacyImplementation: () => void;

// @public (undocumented)
export const VERSION: Version;

Expand Down
3 changes: 1 addition & 2 deletions packages/common/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ export {useIntlImplementation, useLegacyImplementation} from './i18n/implementat
export {
Plural,
NumberFormatStyle,
FormStyle,
Time,
TranslationWidth,
FormatWidth,
NumberSymbol,
WeekDay,
Expand All @@ -49,6 +47,7 @@ export {
getLocaleCurrencySymbol,
getLocaleDirection,
} from './i18n/locale_data_api';
export {FormStyle, TranslationWidth} from './i18n/format_date_interface';
export {parseCookieValue as ɵparseCookieValue} from './cookie';
export {CommonModule} from './common_module';
export {
Expand Down
158 changes: 82 additions & 76 deletions packages/common/src/i18n/format_date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,17 @@
*/

import {
FormatWidth,
DateFormatter,
DateType,
FormStyle,
TranslationType,
TranslationWidth,
ZoneWidth,
} from './format_date_interface';
import {getIntlNamedDate, intlDateStrGetter, intlPadNumber, normalizeLocale} from './intl/date';
import {
FormatWidth,
NumberSymbol,
getLocaleDateFormat,
getLocaleDateTimeFormat,
getLocaleDayNames,
Expand All @@ -20,9 +29,6 @@ import {
getLocaleMonthNames,
getLocaleNumberSymbol,
getLocaleTimeFormat,
NumberSymbol,
Time,
TranslationWidth,
} from './locale_data_api';

export const ISO8601_DATE_REGEX =
Expand All @@ -32,31 +38,6 @@ const NAMED_FORMATS: {[localeId: string]: {[format: string]: string}} = {};
const DATE_FORMATS_SPLIT =
/((?:[^BEGHLMOSWYZabcdhmswyz']+)|(?:'(?:[^']|'')*')|(?:G{1,5}|y{1,4}|Y{1,4}|M{1,5}|L{1,5}|w{1,2}|W{1}|d{1,2}|E{1,6}|c{1,6}|a{1,5}|b{1,5}|B{1,5}|h{1,2}|H{1,2}|m{1,2}|s{1,2}|S{1,3}|z{1,4}|Z{1,5}|O{1,4}))([\s\S]*)/;

enum ZoneWidth {
Short,
ShortGMT,
Long,
Extended,
}

enum DateType {
FullYear,
Month,
Date,
Hours,
Minutes,
Seconds,
FractionalSeconds,
Day,
}

enum TranslationType {
DayPeriods,
Days,
Months,
Eras,
}

/**
* @ngModule CommonModule
* @description
Expand All @@ -67,8 +48,10 @@ enum TranslationType {
* or an [ISO date-time string](https://www.w3.org/TR/NOTE-datetime).
* @param format The date-time components to include. See `DatePipe` for details.
* @param locale A locale code for the locale format rules to use.
* @param timezone The time zone. A time zone offset from GMT (such as `'+0430'`),
* @param timezone The time zone. A IANA time zone like 'America/Los_Angeles'
* or a standard UTC/GMT or continental US time zone abbreviation.
* Timezone offsets are also supported but not on Node.
*
* If not specified, uses host system settings.
*
* @returns The formatted date string.
Expand All @@ -84,12 +67,21 @@ export function formatDate(
locale: string,
timezone?: string,
): string {
locale = normalizeLocale(locale);
let date = toDate(value);
const namedFormat = getNamedFormat(locale, format);
format = namedFormat || format;

let parts: string[] = [];
let match;
if (isUsingIntlImpl()) {
const dateStr = getIntlNamedDate(date, locale, format, timezone);
if (dateStr) {
return dateStr;
}
} else {
const namedFormat = getNamedFormat(locale, format);
format = namedFormat || format;
}

while (format) {
match = DATE_FORMATS_SPLIT.exec(format);
if (match) {
Expand Down Expand Up @@ -238,13 +230,14 @@ function formatDateTime(str: string, opt_values: string[]) {
return str;
}

function padNumber(
function legacyPadNumber(
num: number,
digits: number,
minusSign = '-',
locale = 'en',
trim?: boolean,
negWrap?: boolean,
): string {
const minusSign = getLocaleNumberSymbol(locale, NumberSymbol.MinusSign);
let neg = '';
if (num < 0 || (negWrap && num <= 0)) {
if (negWrap) {
Expand Down Expand Up @@ -293,8 +286,7 @@ function dateGetter(
return formatFractionalSeconds(part, size);
}

const localeMinus = getLocaleNumberSymbol(locale, NumberSymbol.MinusSign);
return padNumber(part, size, localeMinus, trim, negWrap);
return padNumber(part, size, locale, trim, negWrap);
};
}

Expand All @@ -321,20 +313,6 @@ function getDatePart(part: DateType, date: Date): number {
}
}

/**
* Returns a date formatter that transforms a date into its locale string representation
*/
function dateStrGetter(
name: TranslationType,
width: TranslationWidth,
form: FormStyle = FormStyle.Format,
extended = false,
): DateFormatter {
return function (date: Date, locale: string): string {
return getDateTranslation(date, locale, name, width, form, extended);
};
}

/**
* Returns the locale translation of a date for a given form, type and width
*/
Expand Down Expand Up @@ -414,35 +392,34 @@ function getDateTranslation(
*/
function timeZoneGetter(width: ZoneWidth): DateFormatter {
return function (date: Date, locale: string, offset: number) {
const zone = -1 * offset;
const minusSign = getLocaleNumberSymbol(locale, NumberSymbol.MinusSign);
const zone = offset !== 0 ? -1 * offset : offset; // We don't want to handle -0
const hours = zone > 0 ? Math.floor(zone / 60) : Math.ceil(zone / 60);
switch (width) {
case ZoneWidth.Short:
return (
(zone >= 0 ? '+' : '') +
padNumber(hours, 2, minusSign) +
padNumber(Math.abs(zone % 60), 2, minusSign)
padNumber(hours, 2, locale) +
padNumber(Math.abs(zone % 60), 2, locale)
);
case ZoneWidth.ShortGMT:
return 'GMT' + (zone >= 0 ? '+' : '') + padNumber(hours, 1, minusSign);
return 'GMT' + (zone >= 0 ? '+' : '') + padNumber(hours, 1, locale);
case ZoneWidth.Long:
return (
'GMT' +
(zone >= 0 ? '+' : '') +
padNumber(hours, 2, minusSign) +
padNumber(hours, 2, locale) +
':' +
padNumber(Math.abs(zone % 60), 2, minusSign)
padNumber(Math.abs(zone % 60), 2, locale)
);
case ZoneWidth.Extended:
if (offset === 0) {
return 'Z';
} else {
return (
(zone >= 0 ? '+' : '') +
padNumber(hours, 2, minusSign) +
padNumber(hours, 2, locale) +
':' +
padNumber(Math.abs(zone % 60), 2, minusSign)
padNumber(Math.abs(zone % 60), 2, locale)
);
}
default:
Expand Down Expand Up @@ -496,7 +473,7 @@ function weekGetter(size: number, monthBased = false): DateFormatter {
result = 1 + Math.round(diff / 6.048e8); // 6.048e8 ms per week
}

return padNumber(result, size, getLocaleNumberSymbol(locale, NumberSymbol.MinusSign));
return padNumber(result, size, locale);
};
}

Expand All @@ -507,18 +484,26 @@ function weekNumberingYearGetter(size: number, trim = false): DateFormatter {
return function (date: Date, locale: string) {
const thisThurs = getThursdayThisIsoWeek(date);
const weekNumberingYear = thisThurs.getFullYear();
return padNumber(
weekNumberingYear,
size,
getLocaleNumberSymbol(locale, NumberSymbol.MinusSign),
trim,
);
return padNumber(weekNumberingYear, size, locale, trim);
};
}

type DateFormatter = (date: Date, locale: string, offset: number) => string;
/**
* Returns a date formatter that transforms a date into its locale string representation
*/
function legacyCldrDateFormatter(
name: TranslationType,
width: TranslationWidth,
form: FormStyle = FormStyle.Format,
extended = false,
): DateFormatter {
return function (date: Date, locale: string): string {
return getDateTranslation(date, locale, name, width, form, extended);
};
}

const DATE_FORMATS: {[format: string]: DateFormatter} = {};
// This cache is not a constant because we want to clear it when switching the implementation (in tests for example)
let DATE_FORMATS: {[format: string]: DateFormatter} = {};

// Based on CLDR formats:
// See complete list: http://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table
Expand Down Expand Up @@ -727,7 +712,7 @@ function getDateFormatter(format: string): DateFormatter | null {
);
break;

// Extended period of the day (midnight, night, ...), standalone
// Extended period of the day (midnight, night, ...)
case 'B':
case 'BB':
case 'BBB':
Expand Down Expand Up @@ -835,9 +820,6 @@ function getDateFormatter(format: string): DateFormatter | null {
}

function timezoneToOffset(timezone: string, fallback: number): number {
// Support: IE 11 only, Edge 13-15+
// IE/Edge do not "understand" colon (`:`) in timezone
timezone = timezone.replace(/:/g, '');
const requestedTimezoneOffset = Date.parse('Jan 01, 1970 00:00:00 ' + timezone) / 60000;
return isNaN(requestedTimezoneOffset) ? fallback : requestedTimezoneOffset;
}
Expand Down Expand Up @@ -882,8 +864,6 @@ export function toDate(value: string | number | Date): Date {
if (/^(\d{4}(-\d{1,2}(-\d{1,2})?)?)$/.test(value)) {
/* For ISO Strings without time the day, month and year must be extracted from the ISO String
before Date creation to avoid time offset and errors in the new Date.
If we only replace '-' with ',' in the ISO String ("2015,01,01"), and try to create a new
date, some browsers (e.g. IE 9) will throw an invalid Date error.
If we leave the '-' ("2015-01-01") and try to create a new Date("2015-01-01") the timeoffset
is applied.
Note: ISO months are 0 for January, 1 for February, ... */
Expand All @@ -904,8 +884,9 @@ export function toDate(value: string | number | Date): Date {
}
}

const date = new Date(value as any);
const date = new Date(value);
if (!isDate(date)) {
// TODO: create a runtime error
throw new Error(`Unable to convert "${value}" into a date`);
}
return date;
Expand All @@ -914,8 +895,11 @@ export function toDate(value: string | number | Date): Date {
/**
* Converts a date in ISO8601 to a Date.
* Used instead of `Date.parse` because of browser discrepancies.
*
* Firefox didn't support 5-digit dates until version 120
* https://bugzilla.mozilla.org/show_bug.cgi?id=1557650
*/
export function isoStringToDate(match: RegExpMatchArray): Date {
function isoStringToDate(match: RegExpMatchArray): Date {
const date = new Date(0);
let tzHour = 0;
let tzMin = 0;
Expand Down Expand Up @@ -944,3 +928,25 @@ export function isoStringToDate(match: RegExpMatchArray): Date {
export function isDate(value: any): value is Date {
return value instanceof Date && !isNaN(value.valueOf());
}

/** Delegates */

export function useDateIntlFormatting() {
DATE_FORMATS = {};
dateStrGetter = intlDateStrGetter;
padNumber = intlPadNumber;
}

export function useDateLegacyFormatting() {
DATE_FORMATS = {};
dateStrGetter = legacyCldrDateFormatter;
padNumber = legacyPadNumber;
}

// We don't reference intlDateStrGetter to make the implementation tree-shakable
export const isUsingIntlImpl = () => dateStrGetter !== legacyCldrDateFormatter;
export const isUsingLegacylImpl = () => dateStrGetter === legacyCldrDateFormatter;

// This determines the default implementation for date formatting
let dateStrGetter = legacyCldrDateFormatter;
let padNumber = legacyPadNumber;

0 comments on commit 344b8d2

Please sign in to comment.