All files / src toString.ts

100% Statements 39/39
100% Branches 11/11
100% Functions 4/4
100% Lines 33/33

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  1x 1x 1x 1x           1x 32x                     1x   52x   27x               27x 3x       23x 23x 23x     23x 2x     21x               168x 168x 126x     42x   42x 42x       168x 168x   168x 38x         21x   21x 11x         21x 3x     21x    
import { DurationInput } from './types';
import { isZero } from './isZero';
import { getUnitCount } from './lib/getUnitCount';
import { UNITS_META, UNITS_META_MAP } from './lib/units';
import { checkAllUnitsNegative } from './lib/checkAllUnitsNegative';
 
interface ToStringOptions {
	decimalDelimiter: string;
}
 
const joinComponents = (component: string[], delimiter: ToStringOptions['decimalDelimiter']) =>
	component
		.join('')
		.replace(/\./g, delimiter);
 
/**
 * Stringify a duration into an ISO duration string.
 *
 * @example
 * toString({ years: 1, hours: 6 }) // 'P1YT6H'
 * toString(6000) // 'PT6S'
 */
export const toString = (
	duration: DurationInput,
	options: Partial<ToStringOptions> = {},
): string => {
	const finalOptions: ToStringOptions = {
		// Commas are mentioned in the spec as the preferred decimal delimiter
		decimalDelimiter: ',',
		...options,
	};
 
	// Zero values are a special case, since "P" is not a valid value.
	// At least one unit must be specified.
	if (isZero(duration)) {
		return 'P0D';
	}
 
	const {
		maybeAbsDuration: parsed,
		isAllNegative,
	} = checkAllUnitsNegative(duration);
 
	// Weeks should not be included in the output, unless it is the only unit.
	if (getUnitCount(parsed) === 1 && parsed.weeks !== 0) {
		return `P${parsed.weeks}W`;
	}
 
	const components = {
		period: [] as string[],
		time: [] as string[],
	};
 
	// Some units should be converted before stringifying.
	// For example, weeks should not be mixed with other units, and milliseconds
	// don't exist on ISO duration strings.
	UNITS_META.forEach(({ unit: fromUnit, stringifyConvertTo: toUnit }) => {
		if (toUnit == null) {
			return;
		}
 
		const millisecondValue = parsed[fromUnit] * UNITS_META_MAP[fromUnit].milliseconds;
 
		parsed[toUnit] += millisecondValue / UNITS_META_MAP[toUnit].milliseconds;
		parsed[fromUnit] = 0;
	});
 
	// Push each non-zero unit to its relevant array
	UNITS_META.forEach(({ unit, ISOPrecision, ISOCharacter }) => {
		const value = parsed[unit];
 
		if (ISOPrecision != null && value !== 0) {
			components[ISOPrecision].push(`${value}${ISOCharacter}`);
		}
	});
 
	// Build output string
	let output = `P${joinComponents(components.period, finalOptions.decimalDelimiter)}`;
 
	if (components.time.length) {
		output += `T${joinComponents(components.time, finalOptions.decimalDelimiter)}`;
	}
 
	// Avoid "P-1DT-1H". Instead, output "-P1DT1H".
	// https://github.com/dlevs/duration-fns/issues/22
	if (isAllNegative) {
		output = `-${output}`;
	}
 
	return output;
};