woocommerce/plugins/woocommerce-admin/client/lib/date/index.js

335 lines
10 KiB
JavaScript

/** @format */
/**
* External dependencies
*/
import moment from 'moment';
import { find } from 'lodash';
import { __ } from '@wordpress/i18n';
import { date as datePHPFormat, getSettings } from '@wordpress/date';
/**
* Internal dependencies
*/
import { presetValues } from 'components/date-picker/preset-periods';
import { compareValues } from 'components/date-picker/compare-periods';
/**
* DateValue Object
*
* @typedef {Object} DateValue - Describes the date range supplied by the date picker.
* @property {string} label - The translated value of the period.
* @property {string} range - The human readable value of a date range.
* @property {moment.Moment} start - Start of the date range.
* @property {moment.Moment} end - End of the date range.
*/
export const isoDateFormat = 'YYYY-MM-DD';
// This is temporary. This logic will be best addressed in Gutenberg's date package
// https://github.com/WordPress/gutenberg/blob/master/packages/date/src/index.js
// The following code is c/p from https://gist.github.com/phpmypython/f97c5f5f59f2a934599d
const formatMap = {
d: 'DD',
D: 'ddd',
j: 'D',
l: 'dddd',
N: 'E',
S: function() {
return '[' + this.format( 'Do' ).replace( /\d*/g, '' ) + ']';
},
w: 'd',
z: function() {
return this.format( 'DDD' ) - 1;
},
W: 'W',
F: 'MMMM',
m: 'MM',
M: 'MMM',
n: 'M',
t: function() {
return this.daysInMonth();
},
L: function() {
return this.isLeapYear() ? 1 : 0;
},
o: 'GGGG',
Y: 'YYYY',
y: 'YY',
a: 'a',
A: 'A',
B: function() {
const thisUTC = this.clone().utc(),
// Shamelessly stolen from http://javascript.about.com/library/blswatch.htm
swatch = ( thisUTC.hours() + 1 ) % 24 + thisUTC.minutes() / 60 + thisUTC.seconds() / 3600;
return Math.floor( swatch * 1000 / 24 );
},
g: 'h',
G: 'H',
h: 'hh',
H: 'HH',
i: 'mm',
s: 'ss',
u: '[u]', // not sure if moment has this
e: '[e]', // moment does not have this
I: function() {
return this.isDST() ? 1 : 0;
},
O: 'ZZ',
P: 'Z',
T: '[T]', // deprecated in moment
Z: function() {
return parseInt( this.format( 'ZZ' ), 10 ) * 36;
},
c: 'YYYY-MM-DD[T]HH:mm:ssZ',
r: 'ddd, DD MMM YYYY HH:mm:ss ZZ',
U: 'X',
};
const formatEx = /[dDjlNSwzWFmMntLoYyaABgGhHisueIOPTZcrU]/g;
function getMomentFormat( phpFormat ) {
return phpFormat.replace( formatEx, function( phpStr ) {
return typeof formatMap[ phpStr ] === 'function'
? formatMap[ phpStr ].call( moment )
: formatMap[ phpStr ];
} );
}
// When `wp.date.setSettings()` is called on page initialization, PHP formats
// get saved to momentjs localeData. When react-dates attempts create date strings from
// those formats, moment isn't able to handle them. So lets convert those to moment strings.
// This logic should also live in Gutenberg. A PR will be made there.
function applyMomentFormats() {
const settings = getSettings();
const longDateFormats = moment.localeData()._longDateFormat;
const momentLongDateFormats = Object.keys( longDateFormats ).reduce( ( formats, item ) => {
if ( longDateFormats[ item ] ) {
formats[ item ] = getMomentFormat( longDateFormats[ item ] );
}
return formats;
}, {} );
moment.updateLocale( settings.l10n.locale, {
longDateFormat: momentLongDateFormats,
} );
}
// i18n depends on this Gutenberg PR https://github.com/WordPress/gutenberg/pull/7542
// If it has been merged, we need to applyMomentFormats().
if ( ! moment.locale() ) {
applyMomentFormats();
}
/**
* Convert a string to Moment object
*
* @param {string} str - localized date string
* @return {Moment|null} - Moment object representing given string
*/
export function toMoment( str ) {
if ( moment.isMoment( str ) ) {
return str.isValid() ? str : null;
}
if ( 'string' === typeof str ) {
const localeFormat = getMomentFormat( getSettings().formats.date );
const date = moment( str, [ isoDateFormat, localeFormat ], true );
return date.isValid ? date : null;
}
throw new Error( 'toMoment requires a string to be passed as an argument' );
}
/**
* Given two dates, derive a string representation
*
* @param {Moment} start - start date
* @param {Moment} end - end date
* @return {string} - text value for the supplied date range
*/
function getRangeLabel( start, end ) {
const isSameDay = start.isSame( end, 'day' );
const isSameYear = start.year() === end.year();
const isSameMonth = isSameYear && start.month() === end.month();
// If the date format in settings have Full Text Month (F), replace it wtih 3 letter abbreviations (M)
const PHPFormat = getSettings().formats.date.replace( 'F', 'M' );
if ( isSameDay ) {
return datePHPFormat( PHPFormat, start );
} else if ( isSameMonth ) {
const startDate = start.date();
return datePHPFormat( PHPFormat, start ).replace(
startDate,
`${ startDate } - ${ end.date() }`
);
} else if ( isSameYear ) {
return `${ start.format( __( 'MMM D', 'woo-dash' ) ) } - ${ datePHPFormat( PHPFormat, end ) }`;
}
return `${ datePHPFormat( PHPFormat, start ) } - ${ datePHPFormat( PHPFormat, end ) }`;
}
/**
* Get a DateValue object for a period prior to the current period.
*
* @param {string} period - the chosen period
* @param {string} compare - `previous_period` or `previous_year`
* @return {DateValue} - DateValue data about the selected period
*/
function getLastPeriod( period, compare ) {
const primaryStart = moment()
.startOf( period )
.subtract( 1, period );
const primaryEnd = primaryStart.clone().endOf( period );
let secondaryStart;
let secondaryEnd;
if ( 'previous_period' === compare ) {
if ( 'year' === period ) {
// Subtract two entire periods for years to take into account leap year
secondaryStart = moment()
.startOf( period )
.subtract( 2, period );
secondaryEnd = secondaryStart.clone().endOf( period );
} else {
// Otherwise, use days in primary period to figure out how far to go back
const daysDiff = primaryEnd.diff( primaryStart, 'days' );
secondaryEnd = primaryStart.clone().subtract( 1, 'days' );
secondaryStart = secondaryEnd.clone().subtract( daysDiff, 'days' );
}
} else {
secondaryStart =
'week' === period
? primaryStart
.clone()
.subtract( 1, 'years' )
.week( primaryStart.week() )
.startOf( 'week' )
: primaryStart.clone().subtract( 1, 'years' );
secondaryEnd = secondaryStart.clone().endOf( period );
}
return {
primaryStart,
primaryEnd,
secondaryStart,
secondaryEnd,
};
}
/**
* Get a DateValue object for a curent period. The period begins on the first day of the period,
* and ends on the current day.
*
* @param {string} period - the chosen period
* @param {string} compare - `previous_period` or `previous_year`
* @return {DateValue} - DateValue data about the selected period
*/
function getCurrentPeriod( period, compare ) {
const primaryStart = moment().startOf( period );
const primaryEnd = moment();
const daysSoFar = primaryEnd.diff( primaryStart, 'days' );
let secondaryStart;
let secondaryEnd;
if ( 'previous_period' === compare ) {
secondaryStart = primaryStart.clone().subtract( 1, period );
secondaryEnd = primaryEnd.clone().subtract( 1, period );
} else {
secondaryStart =
'week' === period
? primaryStart
.clone()
.subtract( 1, 'years' )
.week( primaryStart.week() )
.startOf( 'week' )
: primaryStart.clone().subtract( 1, 'years' );
secondaryEnd = secondaryStart.clone().add( daysSoFar, 'days' );
}
return {
primaryStart,
primaryEnd,
secondaryStart,
secondaryEnd,
};
}
/**
* Get a DateValue object for a period described by a period, compare value, and start/end
* dates, for custom dates.
*
* @param {string} period - the chosen period
* @param {string} compare - `previous_period` or `previous_year`
* @param {Moment} [start] - start date if custom period
* @param {Moment} [end] - end date if custom period
* @return {DateValue} - DateValue data about the selected period
*/
function getDateValue( period, compare, start, end ) {
switch ( period ) {
case 'today':
return getCurrentPeriod( 'day', compare );
case 'yesterday':
return getLastPeriod( 'day', compare );
case 'week':
return getCurrentPeriod( 'week', compare );
case 'last_week':
return getLastPeriod( 'week', compare );
case 'month':
return getCurrentPeriod( 'month', compare );
case 'last_month':
return getLastPeriod( 'month', compare );
case 'quarter':
return getCurrentPeriod( 'quarter', compare );
case 'last_quarter':
return getLastPeriod( 'quarter', compare );
case 'year':
return getCurrentPeriod( 'year', compare );
case 'last_year':
return getLastPeriod( 'year', compare );
case 'custom':
const difference = end.diff( start, 'days' );
if ( 'previous_period' === compare ) {
const secondaryEnd = start.clone().subtract( 1, 'days' );
const secondaryStart = secondaryEnd.clone().subtract( difference, 'days' );
return {
primaryStart: start,
primaryEnd: end,
secondaryStart,
secondaryEnd,
};
}
return {
primaryStart: start,
primaryEnd: end,
secondaryStart: start.clone().subtract( 1, 'years' ),
secondaryEnd: end.clone().subtract( 1, 'years' ),
};
}
}
/**
* Get Date Value Objects for a primary and secondary date range
*
* @param {string} period - Indicates period, 'last_week', 'quarter', or 'custom'
* @param {string} compare - Indicates the period to compare against, 'previous_period', previous_year'
* @param {moment.Moment} [start] - If the period supplied is "custom", this is the start date
* @param {moment.Moment} [end] - If the period supplied is "custom", this is the end date
* @return {{primary: DateValue, secondary: DateValue}} - Primary and secondary DateValue objects
*/
export const getCurrentDates = ( { period, compare, start, end } ) => {
const { primaryStart, primaryEnd, secondaryStart, secondaryEnd } = getDateValue(
period,
compare,
start,
end
);
return {
primary: {
label: find( presetValues, item => item.value === period ).label,
range: getRangeLabel( primaryStart, primaryEnd ),
start: primaryStart,
end: primaryEnd,
},
secondary: {
label: find( compareValues, item => item.value === compare ).label,
range: getRangeLabel( secondaryStart, secondaryEnd ),
start: secondaryStart,
end: secondaryEnd,
},
};
};