All files / utils changeIndicator.ts

86.95% Statements 20/23
83.87% Branches 26/31
100% Functions 4/4
86.95% Lines 20/23

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101                                                2x 16x   2x       9x 1x   8x 8x         2x         3x     2x   1x     1x                                 2x               22x 9x     13x 3x             10x     8x         2x    
import { formatPercentage } from './formatting';
 
export interface ChangeIndicator {
	change: string | null;
	changeStatus: string;
}
 
interface ResolveChangeArgs {
 
	/** Server-calculated change percentage; null when a period has no data. */
	rateChange?: number | null;
	current?: Record<string, unknown> | null;
	previous?: Record<string, unknown> | null;
 
	/** Extracts the numeric metric value from a period's data object. */
	getValue: ( data: Record<string, unknown> ) => number;
 
	/** Set when growth is bad (e.g. abandonment rate) so statuses flip. */
	invertPolarity?: boolean;
 
	/** Returned when neither period has data. */
	noData: ChangeIndicator;
}
 
const polarity = ( grew: boolean, invert: boolean ): string =>
	grew !== invert ? 'positive' : 'negative';
 
const resolveFromRateChange = (
	rateChange: number,
	invert: boolean
): ChangeIndicator => {
	if ( 0 === rateChange ) {
		return { change: '0%', changeStatus: 'positive' };
	}
	const formatted = formatPercentage( rateChange );
	return 0 < rateChange ?
		{ change: `+${formatted}`, changeStatus: polarity( true, invert ) } :
		{ change: formatted, changeStatus: polarity( false, invert ) };
};
 
const resolveFromPeriods = (
	currentValue: number,
	previousValue: number,
	invert: boolean
): ChangeIndicator | null => {
	if ( 0 === previousValue && 0 < currentValue ) {
 
		// Positive infinity: went from 0 to something.
		return { change: '∞', changeStatus: polarity( true, invert ) };
	}
	Eif ( 0 < previousValue && 0 === currentValue ) {
 
		// Negative infinity: went from something to 0.
		return { change: '-∞', changeStatus: polarity( false, invert ) };
	}
	if ( 0 === previousValue && 0 === currentValue ) {
		return { change: '0%', changeStatus: 'positive' };
	}
 
	// Both periods have data but no rate change was provided.
	return null;
};
 
/**
 * Resolve the change label and status for a compared metric, handling the
 * infinity cases (a period at zero) that a percentage cannot express.
 *
 * Returns null when both periods have non-zero data but no rateChange was
 * provided; the caller keeps its initial values in that case.
 */
export const resolveChangeIndicator = ({
	rateChange,
	current,
	previous,
	getValue,
	invertPolarity = false,
	noData
}: ResolveChangeArgs ): ChangeIndicator | null => {
	if ( null !== rateChange && undefined !== rateChange ) {
		return resolveFromRateChange( rateChange, invertPolarity );
	}
 
	if ( current && previous ) {
		return resolveFromPeriods(
			getValue( current ),
			getValue( previous ),
			invertPolarity
		);
	}
 
	if ( current ) {
 
		// Only current data exists: treat any value as new data.
		return 0 < getValue( current ) ?
			{ change: '∞', changeStatus: polarity( true, invertPolarity ) } :
			{ change: '0%', changeStatus: 'positive' };
	}
 
	return noData;
};