All files / src handleMergeError.ts

100% Statements 52/52
100% Branches 39/39
100% Functions 3/3
100% Lines 50/50
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                    8x         1x   7x           7x 11x 11x   11x             8x     3x     7x   7x               1x 19x 19x   19x 19x 19x 19x   19x 72x               75x     72x     43x     5x     43x 43x 43x       43x 29x     4x       25x       19x   19x 19x 19x   19x 19x       19x 38x         51x     38x 17x   17x 17x 17x 21x 4x   17x       19x     1x
import IBestMatch         from './Interfaces/IBestMatch';
import IMergeErrorMessage from './Interfaces/IMergeMessage';
 
function handleMergeError(err: Error, target: any, offendingKey: string, message: IMergeErrorMessage) {
    // Rethrow if:
    // - offending key already exists on target
    // - object not sealed
    // - is extensible
    // - error not a TypeError
 
    if (
        Object.hasOwnProperty.call(target, offendingKey) ||
        !Object.isSealed(target) ||
        Object.isExtensible(target) ||
        !(err instanceof TypeError)
    ) throw err;
 
    const offendingKeyLower = offendingKey.toLowerCase();
 
    // Iterate through keys in target
 
    // For each key, compare with the offending key
 
    const bestMatch: IBestMatch = Object.keys(target).reduce((currBestMatch: IBestMatch, currKey: string) => {
        const totalMatching = getTotalMatching(currKey.toLowerCase(), offendingKeyLower);
        const delta = Math.abs(currKey.length - offendingKey.length);
 
        if (
            totalMatching > currBestMatch.totalMatching ||
            (totalMatching === currBestMatch.totalMatching && delta < currBestMatch.delta)
        ) {
            // If a greater number of matching characters, or the same
            // number, but a lesser delta, usurp the best match
 
            return {key: currKey, delta, totalMatching};
        }
 
        return currBestMatch;
    }, {key: '', delta: Infinity, totalMatching: 0});
 
    const suggestion = bestMatch && bestMatch.totalMatching > 1 ? bestMatch.key : '';
 
    throw new TypeError(message(offendingKey, suggestion));
}
 
/**
 * Returns the number of common, consecutive characters
 * between two strings.
 */
 
export function getTotalMatching(possibleKey: string, offendingKey: string): number {
    const longer: string = possibleKey.length > offendingKey.length ? possibleKey : offendingKey;
    const shorter: string = longer === possibleKey ? offendingKey : possibleKey;
 
    let leftPointer = 0;
    let leftInnerPointer = 0;
    let leftTotalMatching = 0;
    let lastCommonIndex = -1;
 
    for (; leftPointer < longer.length; leftPointer++) {
        while (
            leftTotalMatching === 0 &&
            longer[leftPointer] !== shorter[leftInnerPointer] &&
            leftInnerPointer < shorter.length
        ) {
            // No match at present, move innerPointer through all possible
            // indices until a match is found
 
            leftInnerPointer++;
        }
 
        if (longer[leftPointer] === shorter[leftInnerPointer]) {
            // Match found
 
            if (lastCommonIndex !== leftPointer - 1) {
                // If beginning of a new match, reset total common
 
                leftTotalMatching = 0;
            }
 
            lastCommonIndex = leftPointer;
            leftTotalMatching++;
            leftInnerPointer++;
 
            // Whole word matched, end
 
            if (leftTotalMatching === shorter.length) break;
        } else if (leftTotalMatching > 1) {
            // No match, but at least two common characters found, end
 
            break;
        } else {
            // No match at this index, reset
 
            leftTotalMatching = leftInnerPointer = 0;
        }
    }
 
    lastCommonIndex = -1;
 
    let rightPointer = 0;
    let rightInnerPointer = 0;
    let rightTotalMatching = 0;
 
    const longerLastIndex = longer.length - 1;
    const shorterLastIndex = shorter.length - 1;
 
    // As above, but from right to left
 
    for (; rightPointer < longer.length - leftPointer; rightPointer++) {
        while (
            rightTotalMatching === 0 &&
            longer[longerLastIndex - rightPointer] !== shorter[shorterLastIndex - rightInnerPointer] &&
            rightInnerPointer < shorter.length
        ) {
            rightInnerPointer++;
        }
 
        if (longer[longerLastIndex - rightPointer] === shorter[shorterLastIndex - rightInnerPointer]) {
            if (lastCommonIndex !== rightPointer - 1) rightTotalMatching = 0;
 
            lastCommonIndex = rightPointer;
            rightTotalMatching++;
            rightInnerPointer++;
        } else if (rightTotalMatching > 1) {
            break;
        } else {
            rightTotalMatching = rightInnerPointer = 0;
        }
    }
 
    return Math.min(shorter.length, leftTotalMatching + rightTotalMatching);
}
 
export default handleMergeError;