All files / src/lib parseISODuration.ts

100% Statements 34/34
100% Branches 14/14
100% Functions 7/7
100% Lines 31/31

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 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142  14x 14x 14x   14x   98x   14x       28x 185x   185x 43x     142x 142x     517x 1x     141x 1114x   1114x   1114x               141x   141x 1114x         141x 9x     141x                       14x                                                           14x                                                                   14x   156x       156x 15x     141x    
import { Duration } from '../types';
import { ZERO, UnitKey } from './units';
import { negate } from '../negate';
import { isNegativelySigned } from './numberUtils';
 
const millisecondsPattern = '(?:[,.](\\d{1,3})\\d*)?';
 
const unitPattern = (unit: string) => `(?:(-?\\d+)${unit})?`;
 
const createDurationParser = (
	regex: RegExp,
	unitsOrder: UnitKey[],
) => {
	return (duration: string): Duration | null => {
		const match = duration.match(regex);
 
		if (!match) {
			return null;
		}
 
		const isDurationNegative = match[1] === '-';
		const unitStrings: (string | undefined)[] = match.slice(2);
 
		// Must have at least one unit match
		if (unitStrings.every(value => value === undefined)) {
			return null;
		}
 
		const unitNumbers = unitStrings.map((value, i) => {
			value = value ?? '0';
 
			const isMilliseconds = i === unitStrings.length - 1;
 
			return isMilliseconds
				// Pad the end of the millisecond values. For example, when taking the "6"
				// portion from the string "PT3.6S", we need to interpret that as "600
				// milliseconds".
				? Number(value.padEnd(3, '0'))
				: Number(value);
		});
 
		const output = { ...ZERO };
 
		unitsOrder.forEach((unit, i) => {
			output[unit] = unitNumbers[i];
		});
 
		// Milliseconds don't have their own minus symbol. It depends on the symbol
		// before the seconds value.
		if (isNegativelySigned(output.seconds)) {
			output.milliseconds *= -1;
		}
 
		return isDurationNegative
			? negate(output)
			: output;
	};
};
 
/**
 * Parse a duration string expressed in one of the following formats:
 *
 * - PYYYYMMDDThhmmss
 * - PYYYY-MM-DDThh:mm:ss
 */
const parseFullFormatISODuration = createDurationParser(
	new RegExp([
		'^(-)?P',
		'(\\d{4})', '-?',
		'(\\d{2})', '-?',
		'(\\d{2})', 'T',
		'(\\d{2})', ':?',
		'(\\d{2})', ':?',
		'(\\d{2})', millisecondsPattern,
		'$',
	].join('')),
	[
		'years',
		'months',
		'days',
		'hours',
		'minutes',
		'seconds',
		'milliseconds',
	],
);
 
/**
 * Parse a duration string expressed via number and unit character pairs. For
 * example:
 *
 * - P6D
 * - P1Y2D
 * - P2DT6H2,5S
 */
const parseUnitsISODuration = createDurationParser(
	new RegExp([
		'^(-)?P',
		unitPattern('Y'),
		unitPattern('M'),
		unitPattern('W'),
		unitPattern('D'),
		'(?:T',
		unitPattern('H'),
		unitPattern('M'),
		unitPattern(`${millisecondsPattern}S`),
		')?$',
	].join('')),
	[
		'years',
		'months',
		'weeks',
		'days',
		'hours',
		'minutes',
		'seconds',
		'milliseconds',
	],
);
 
/**
 * Parse an ISO 8601 duration string into an object.
 *
 * The units of duration are not normalized. For example, the string `"P365D"`
 * doesn't get converted to `{ years: 1 }` since not all years are the same
 * length.
 *
 * @example parseISODuration('P365D') // { days: 365 }
 */
export const parseISODuration = (duration: string): Duration => {
	const output = (
		parseUnitsISODuration(duration) ||
		parseFullFormatISODuration(duration)
	);
 
	if (output === null) {
		throw new SyntaxError(`Failed to parse duration. "${duration}" is not a valid ISO duration string.`);
	}
 
	return output;
};