diff --git a/packages/date/README.md b/packages/date/README.md index e45bb664c85519..954ea388ece67d 100644 --- a/packages/date/README.md +++ b/packages/date/README.md @@ -18,26 +18,40 @@ _This package assumes that your code will run in an **ES2015+** environment. If # **date** -Formats a date (like `date()` in PHP), in the site's timezone. +Formats a date (like `date()` in PHP). + +_Related_ + +- +- _Parameters_ - _dateFormat_ `string`: PHP-style formatting string. See php.net/date. - _dateValue_ `(Date|string|Moment|null)`: Date object or string, parsable by moment.js. +- _timezone_ `(string|number|null)`: Timezone to output result in or a UTC offset. Defaults to timezone from site. _Returns_ -- `string`: Formatted date. +- `string`: Formatted date in English. # **dateI18n** -Formats a date (like `date_i18n()` in PHP). +Formats a date (like `wp_date()` in PHP), translating it into site's locale. + +Backward Compatibility Notice: if `timezone` is set to `true`, the function +behaves like `gmdateI18n`. + +_Related_ + +- +- _Parameters_ - _dateFormat_ `string`: PHP-style formatting string. See php.net/date. - _dateValue_ `(Date|string|Moment|null)`: Date object or string, parsable by moment.js. -- _gmt_ `boolean`: True for GMT/UTC, false for site's timezone. +- _timezone_ `(string|number|boolean|null)`: Timezone to output result in or a UTC offset. Defaults to timezone from site. Notice: `boolean` is effectively deprecated, but still supported for backward compatibility reasons. _Returns_ @@ -79,6 +93,20 @@ _Parameters_ _Returns_ +- `string`: Formatted date in English. + +# **gmdateI18n** + +Formats a date (like `wp_date()` in PHP), translating it into site's locale +and using the UTC timezone. + +_Parameters_ + +- _dateFormat_ `string`: PHP-style formatting string. See php.net/date. +- _dateValue_ `(Date|string|Moment|null)`: Date object or string, parsable by moment.js. + +_Returns_ + - `string`: Formatted date. # **isInTheFuture** diff --git a/packages/date/src/index.js b/packages/date/src/index.js index 6251e1c82c04bf..25697d8fadd596 100644 --- a/packages/date/src/index.js +++ b/packages/date/src/index.js @@ -9,6 +9,10 @@ import 'moment-timezone/moment-timezone-utils'; const WP_ZONE = 'WP'; +// This regular expression tests positive for UTC offsets as described in ISO 8601. +// See: https://en.wikipedia.org/wiki/ISO_8601#Time_offsets_from_UTC +const VALID_UTC_OFFSET = /^[+-][0-1][0-9](:?[0-9][0-9])?$/; + // Changes made here will likely need to be made in `lib/client-assets.php` as // well because it uses the `setSettings()` function to change these settings. let settings = { @@ -318,10 +322,10 @@ const formatMap = { /** * Formats a date. Does not alter the date's timezone. * - * @param {string} dateFormat PHP-style formatting string. - * See php.net/date. - * @param {(Date|string|Moment|null)} dateValue Date object or string, - * parsable by moment.js. + * @param {string} dateFormat PHP-style formatting string. + * See php.net/date. + * @param {Date|string|Moment|null} dateValue Date object or string, + * parsable by moment.js. * * @return {string} Formatted date. */ @@ -357,30 +361,35 @@ export function format( dateFormat, dateValue = new Date() ) { } /** - * Formats a date (like `date()` in PHP), in the site's timezone. + * Formats a date (like `date()` in PHP). * - * @param {string} dateFormat PHP-style formatting string. - * See php.net/date. - * @param {(Date|string|Moment|null)} dateValue Date object or string, - * parsable by moment.js. + * @param {string} dateFormat PHP-style formatting string. + * See php.net/date. + * @param {Date|string|Moment|null} dateValue Date object or string, parsable + * by moment.js. + * @param {string|number|null} timezone Timezone to output result in or a + * UTC offset. Defaults to timezone from + * site. * - * @return {string} Formatted date. + * @see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + * @see https://en.wikipedia.org/wiki/ISO_8601#Time_offsets_from_UTC + * + * @return {string} Formatted date in English. */ -export function date( dateFormat, dateValue = new Date() ) { - const offset = settings.timezone.offset * HOUR_IN_MINUTES; - const dateMoment = momentLib( dateValue ).utcOffset( offset, true ); +export function date( dateFormat, dateValue = new Date(), timezone ) { + const dateMoment = buildMoment( dateValue, timezone ); return format( dateFormat, dateMoment ); } /** * Formats a date (like `date()` in PHP), in the UTC timezone. * - * @param {string} dateFormat PHP-style formatting string. - * See php.net/date. - * @param {(Date|string|Moment|null)} dateValue Date object or string, - * parsable by moment.js. + * @param {string} dateFormat PHP-style formatting string. + * See php.net/date. + * @param {Date|string|Moment|null} dateValue Date object or string, + * parsable by moment.js. * - * @return {string} Formatted date. + * @return {string} Formatted date in English. */ export function gmdate( dateFormat, dateValue = new Date() ) { const dateMoment = momentLib( dateValue ).utc(); @@ -388,26 +397,54 @@ export function gmdate( dateFormat, dateValue = new Date() ) { } /** - * Formats a date (like `date_i18n()` in PHP). + * Formats a date (like `wp_date()` in PHP), translating it into site's locale. + * + * Backward Compatibility Notice: if `timezone` is set to `true`, the function + * behaves like `gmdateI18n`. + * + * @param {string} dateFormat PHP-style formatting string. + * See php.net/date. + * @param {Date|string|Moment|null} dateValue Date object or string, parsable by + * moment.js. + * @param {string|number|boolean|null} timezone Timezone to output result in or a + * UTC offset. Defaults to timezone from + * site. Notice: `boolean` is effectively + * deprecated, but still supported for + * backward compatibility reasons. * - * @param {string} dateFormat PHP-style formatting string. - * See php.net/date. - * @param {(Date|string|Moment|null)} dateValue Date object or string, - * parsable by moment.js. - * @param {boolean} gmt True for GMT/UTC, false for - * site's timezone. + * @see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + * @see https://en.wikipedia.org/wiki/ISO_8601#Time_offsets_from_UTC * * @return {string} Formatted date. */ -export function dateI18n( dateFormat, dateValue = new Date(), gmt = false ) { - // Defaults. - const offset = gmt ? 0 : settings.timezone.offset * HOUR_IN_MINUTES; - // Convert to moment object. - const dateMoment = momentLib( dateValue ).utcOffset( offset, true ); +export function dateI18n( dateFormat, dateValue = new Date(), timezone ) { + if ( true === timezone ) { + return gmdateI18n( dateFormat, dateValue ); + } + + if ( false === timezone ) { + timezone = undefined; + } - // Set the locale. + const dateMoment = buildMoment( dateValue, timezone ); + dateMoment.locale( settings.l10n.locale ); + return format( dateFormat, dateMoment ); +} + +/** + * Formats a date (like `wp_date()` in PHP), translating it into site's locale + * and using the UTC timezone. + * + * @param {string} dateFormat PHP-style formatting string. + * See php.net/date. + * @param {Date|string|Moment|null} dateValue Date object or string, + * parsable by moment.js. + * + * @return {string} Formatted date. + */ +export function gmdateI18n( dateFormat, dateValue = new Date() ) { + const dateMoment = momentLib( dateValue ).utc(); dateMoment.locale( settings.l10n.locale ); - // Format and return. return format( dateFormat, dateMoment ); } @@ -440,4 +477,51 @@ export function getDate( dateString ) { return momentLib.tz( dateString, WP_ZONE ).toDate(); } +/** + * Creates a moment instance using the given timezone or, if none is provided, using global settings. + * + * @param {Date|string|Moment|null} dateValue Date object or string, parsable + * by moment.js. + * @param {string|number|null} timezone Timezone to output result in or a + * UTC offset. Defaults to timezone from + * site. + * + * @see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + * @see https://en.wikipedia.org/wiki/ISO_8601#Time_offsets_from_UTC + * + * @return {Moment} a moment instance. + */ +function buildMoment( dateValue, timezone = '' ) { + const dateMoment = momentLib( dateValue ); + + if ( timezone && ! isUTCOffset( timezone ) ) { + return dateMoment.tz( timezone ); + } + + if ( timezone && isUTCOffset( timezone ) ) { + return dateMoment.utcOffset( timezone ); + } + + if ( settings.timezone.string ) { + return dateMoment.tz( settings.timezone.string ); + } + + return dateMoment.utcOffset( settings.timezone.offset ); +} + +/** + * Returns whether a certain UTC offset is valid or not. + * + * @param {number|string} offset a UTC offset. + * + * @return {boolean} whether a certain UTC offset is valid or not. + */ +function isUTCOffset( offset ) { + if ( 'number' === typeof offset ) { + return true; + } + + return VALID_UTC_OFFSET.test( offset ); +} + setupWPTimezone(); diff --git a/packages/date/src/test/index.js b/packages/date/src/test/index.js index 69368b1fb630b5..d9b832e528ce6b 100644 --- a/packages/date/src/test/index.js +++ b/packages/date/src/test/index.js @@ -2,10 +2,14 @@ * Internal dependencies */ import { - isInTheFuture, + __experimentalGetSettings, + date as dateNoI18n, + dateI18n, getDate, + gmdate, + gmdateI18n, + isInTheFuture, setSettings, - __experimentalGetSettings, } from '../'; describe( 'isInTheFuture', () => { @@ -16,7 +20,7 @@ describe( 'isInTheFuture', () => { expect( isInTheFuture( date ) ).toBe( true ); } ); - it( 'should return true if the date is in the past', () => { + it( 'should return false if the date is in the past', () => { // Create a Date object 1 minute in the past. const date = new Date( Number( getDate() ) - 1000 * 60 ); @@ -44,6 +48,433 @@ describe( 'isInTheFuture', () => { } ); } ); +describe( 'Function date', () => { + it( 'should format date in English, ignoring locale settings', () => { + const settings = __experimentalGetSettings(); + + // Simulate different locale + const l10n = settings.l10n; + setSettings( { + ...settings, + l10n: { + ...l10n, + locale: 'es', + months: l10n.months.map( ( month ) => `es_${ month }` ), + monthsShort: l10n.monthsShort.map( + ( month ) => `es_${ month }` + ), + weekdays: l10n.weekdays.map( ( weekday ) => `es_${ weekday }` ), + weekdaysShort: l10n.weekdaysShort.map( + ( weekday ) => `es_${ weekday }` + ), + }, + } ); + + // Check + const formattedDate = dateNoI18n( + 'F M l D', + '2019-06-18T11:00:00.000Z' + ); + expect( formattedDate ).toBe( 'June Jun Tuesday Tue' ); + + // Restore default settings + setSettings( settings ); + } ); + + it( 'should format date into a date that uses site’s timezone, if no timezone was provided and there’s a site timezone set', () => { + const settings = __experimentalGetSettings(); + + // Simulate different timezone + setSettings( { + ...settings, + timezone: { offset: -4, string: 'America/New_York' }, + } ); + + // Check + const winterFormattedDate = dateNoI18n( + 'Y-m-d H:i', + '2019-01-18T11:00:00.000Z' + ); + expect( winterFormattedDate ).toBe( '2019-01-18 06:00' ); + + const summerFormattedDate = dateNoI18n( + 'Y-m-d H:i', + '2019-06-18T11:00:00.000Z' + ); + expect( summerFormattedDate ).toBe( '2019-06-18 07:00' ); + + // Restore default settings + setSettings( settings ); + } ); + + it( 'should format date into a date that uses site’s UTC offset setting, if no timezone was provided and there isn’t a timezone set in the site', () => { + const settings = __experimentalGetSettings(); + + // Simulate different timezone + setSettings( { + ...settings, + timezone: { offset: -4, string: '' }, + } ); + + // Check + const winterFormattedDate = dateNoI18n( + 'Y-m-d H:i', + '2019-01-18T11:00:00.000Z' + ); + expect( winterFormattedDate ).toBe( '2019-01-18 07:00' ); + + const summerFormattedDate = dateNoI18n( + 'Y-m-d H:i', + '2019-06-18T11:00:00.000Z' + ); + expect( summerFormattedDate ).toBe( '2019-06-18 07:00' ); + + // Restore default settings + setSettings( settings ); + } ); + + it( 'should format date into a date that uses the given timezone, if said timezone is valid', () => { + const settings = __experimentalGetSettings(); + + // Simulate different timezone + setSettings( { + ...settings, + timezone: { offset: -4, string: 'America/New_York' }, + } ); + + // Check + const formattedDate = dateNoI18n( + 'Y-m-d H:i', + '2019-06-18T11:00:00.000Z', + 'Asia/Macau' + ); + expect( formattedDate ).toBe( '2019-06-18 19:00' ); + + // Restore default settings + setSettings( settings ); + } ); + + it( 'should format date into a date that uses the given UTC offset, if given timezone is actually a UTC offset', () => { + const settings = __experimentalGetSettings(); + + // Simulate different timezone + setSettings( { + ...settings, + timezone: { offset: -4, string: 'America/New_York' }, + } ); + + // Check + let formattedDate; + formattedDate = dateNoI18n( + 'Y-m-d H:i', + '2019-06-18T11:00:00.000Z', + '+08:00' + ); + expect( formattedDate ).toBe( '2019-06-18 19:00' ); + + formattedDate = dateNoI18n( + 'Y-m-d H:i', + '2019-06-18T11:00:00.000Z', + 8 + ); + expect( formattedDate ).toBe( '2019-06-18 19:00' ); + + formattedDate = dateNoI18n( + 'Y-m-d H:i', + '2019-06-18T11:00:00.000Z', + 480 + ); + expect( formattedDate ).toBe( '2019-06-18 19:00' ); + + // Restore default settings + setSettings( settings ); + } ); +} ); + +describe( 'Function gmdate', () => { + it( 'should format date in English, ignoring locale settings', () => { + const settings = __experimentalGetSettings(); + + // Simulate different locale + const l10n = settings.l10n; + setSettings( { + ...settings, + l10n: { + ...l10n, + locale: 'es', + months: l10n.months.map( ( month ) => `es_${ month }` ), + monthsShort: l10n.monthsShort.map( + ( month ) => `es_${ month }` + ), + weekdays: l10n.weekdays.map( ( weekday ) => `es_${ weekday }` ), + weekdaysShort: l10n.weekdaysShort.map( + ( weekday ) => `es_${ weekday }` + ), + }, + } ); + + // Check + const formattedDate = gmdate( 'F M l D', '2019-06-18T11:00:00.000Z' ); + expect( formattedDate ).toBe( 'June Jun Tuesday Tue' ); + + // Restore default settings + setSettings( settings ); + } ); + + it( 'should format date into a UTC date', () => { + const settings = __experimentalGetSettings(); + + // Simulate different timezone + setSettings( { + ...settings, + timezone: { offset: -4, string: 'America/New_York' }, + } ); + + // Check + const formattedDate = gmdate( 'Y-m-d H:i', '2019-06-18T11:00:00.000Z' ); + expect( formattedDate ).toBe( '2019-06-18 11:00' ); + + // Restore default settings + setSettings( settings ); + } ); +} ); + +describe( 'Function dateI18n', () => { + it( 'should format date using locale settings', () => { + const settings = __experimentalGetSettings(); + + // Simulate different locale + const l10n = settings.l10n; + setSettings( { + ...settings, + l10n: { + ...l10n, + locale: 'es', + months: l10n.months.map( ( month ) => `es_${ month }` ), + monthsShort: l10n.monthsShort.map( + ( month ) => `es_${ month }` + ), + weekdays: l10n.weekdays.map( ( weekday ) => `es_${ weekday }` ), + weekdaysShort: l10n.weekdaysShort.map( + ( weekday ) => `es_${ weekday }` + ), + }, + } ); + + // Check + const formattedDate = dateI18n( + 'F M l D', + '2019-06-18T11:00:00.000Z', + true + ); + expect( formattedDate ).toBe( 'es_June es_Jun es_Tuesday es_Tue' ); + + // Restore default settings + setSettings( settings ); + } ); + + it( 'should format date into a date that uses site’s timezone, if no timezone was provided and there’s a site timezone set', () => { + const settings = __experimentalGetSettings(); + + // Simulate different timezone + setSettings( { + ...settings, + timezone: { offset: -4, string: 'America/New_York' }, + } ); + + // Check + const winterFormattedDate = dateI18n( + 'Y-m-d H:i', + '2019-01-18T11:00:00.000Z' + ); + expect( winterFormattedDate ).toBe( '2019-01-18 06:00' ); + + const summerFormattedDate = dateI18n( + 'Y-m-d H:i', + '2019-06-18T11:00:00.000Z' + ); + expect( summerFormattedDate ).toBe( '2019-06-18 07:00' ); + + // Restore default settings + setSettings( settings ); + } ); + + it( 'should format date into a date that uses site’s UTC offset setting, if no timezone was provided and there isn’t a timezone set in the site', () => { + const settings = __experimentalGetSettings(); + + // Simulate different timezone + setSettings( { + ...settings, + timezone: { offset: -4, string: '' }, + } ); + + // Check + const winterFormattedDate = dateI18n( + 'Y-m-d H:i', + '2019-01-18T11:00:00.000Z' + ); + expect( winterFormattedDate ).toBe( '2019-01-18 07:00' ); + + const summerFormattedDate = dateI18n( + 'Y-m-d H:i', + '2019-06-18T11:00:00.000Z' + ); + expect( summerFormattedDate ).toBe( '2019-06-18 07:00' ); + + // Restore default settings + setSettings( settings ); + } ); + + it( 'should format date into a date that uses the given timezone, if said timezone is valid', () => { + const settings = __experimentalGetSettings(); + + // Simulate different timezone + setSettings( { + ...settings, + timezone: { offset: -4, string: 'America/New_York' }, + } ); + + // Check + const formattedDate = dateI18n( + 'Y-m-d H:i', + '2019-06-18T11:00:00.000Z', + 'Asia/Macau' + ); + expect( formattedDate ).toBe( '2019-06-18 19:00' ); + + // Restore default settings + setSettings( settings ); + } ); + + it( 'should format date into a date that uses the given UTC offset, if given timezone is actually a UTC offset', () => { + const settings = __experimentalGetSettings(); + + // Simulate different timezone + setSettings( { + ...settings, + timezone: { offset: -4, string: 'America/New_York' }, + } ); + + // Check + let formattedDate; + formattedDate = dateI18n( + 'Y-m-d H:i', + '2019-06-18T11:00:00.000Z', + '+08:00' + ); + expect( formattedDate ).toBe( '2019-06-18 19:00' ); + + formattedDate = dateI18n( 'Y-m-d H:i', '2019-06-18T11:00:00.000Z', 8 ); + expect( formattedDate ).toBe( '2019-06-18 19:00' ); + + formattedDate = dateI18n( + 'Y-m-d H:i', + '2019-06-18T11:00:00.000Z', + 480 + ); + expect( formattedDate ).toBe( '2019-06-18 19:00' ); + + // Restore default settings + setSettings( settings ); + } ); + + it( 'should format date into a UTC date if `gmt` is set to `true`', () => { + const settings = __experimentalGetSettings(); + + // Simulate different timezone + setSettings( { + ...settings, + timezone: { offset: -4, string: 'America/New_York' }, + } ); + + // Check + const formattedDate = dateI18n( + 'Y-m-d H:i', + '2019-06-18T11:00:00.000Z', + true + ); + expect( formattedDate ).toBe( '2019-06-18 11:00' ); + + // Restore default settings + setSettings( settings ); + } ); + + it( 'should format date into a date that uses site’s timezone if `gmt` is set to `false`', () => { + const settings = __experimentalGetSettings(); + + // Simulate different timezone + setSettings( { + ...settings, + timezone: { offset: -4, string: 'America/New_York' }, + } ); + + // Check + const formattedDate = dateI18n( + 'Y-m-d H:i', + '2019-06-18T11:00:00.000Z', + false + ); + expect( formattedDate ).toBe( '2019-06-18 07:00' ); + + // Restore default settings + setSettings( settings ); + } ); +} ); + +describe( 'Function gmdateI18n', () => { + it( 'should format date using locale settings', () => { + const settings = __experimentalGetSettings(); + + // Simulate different locale + const l10n = settings.l10n; + setSettings( { + ...settings, + l10n: { + ...l10n, + locale: 'es', + months: l10n.months.map( ( month ) => `es_${ month }` ), + monthsShort: l10n.monthsShort.map( + ( month ) => `es_${ month }` + ), + weekdays: l10n.weekdays.map( ( weekday ) => `es_${ weekday }` ), + weekdaysShort: l10n.weekdaysShort.map( + ( weekday ) => `es_${ weekday }` + ), + }, + } ); + + // Check + const formattedDate = gmdateI18n( + 'F M l D', + '2019-06-18T11:00:00.000Z' + ); + expect( formattedDate ).toBe( 'es_June es_Jun es_Tuesday es_Tue' ); + + // Restore default settings + setSettings( settings ); + } ); + + it( 'should format date into a UTC date', () => { + const settings = __experimentalGetSettings(); + + // Simulate different timezone + setSettings( { + ...settings, + timezone: { offset: -4, string: 'America/New_York' }, + } ); + + // Check + const formattedDate = gmdateI18n( + 'Y-m-d H:i', + '2019-06-18T11:00:00.000Z' + ); + expect( formattedDate ).toBe( '2019-06-18 11:00' ); + + // Restore default settings + setSettings( settings ); + } ); +} ); + describe( 'Moment.js Localization', () => { it( 'should change the relative time strings', () => { const settings = __experimentalGetSettings();