Search code examples
javascriptstringalgorithmincrementdecrement

Algorithmic Problem/Code Challenge. JS Function incrementing/decrementing Strings in the lexicographically smallest step possible


I need a js function, that increments Strings lexicographically, by incrementing or appending letters(A-Z, a-z) only, it gets passed an initial string, and the amount of increments n. It returns an array with n lexicographically ascending strings.

  1. Special characters should not be modified, they may however be appended too or removed when decrementing

  2. Increments should also be the smallest step possible, within the minimum at least required string length

  3. The total amount of increments, could be passed as an argument, since it is determined in advance.

Incrementation Examples:

"x", 2 times: [y,z]

"x", 53 times [xA, xB, xC,...,xz, y, yA]

"$y$", 1 time: ["$z$"]

"$y$", >53 times: [$y$A - $y$Z, $y$a - $y$z, $z$, $z$A,....]

I managed to create an increment function, If the question ever gets opened again I would post it as a Reply.

I also require a decrement function. Just like increment, it gets passed an initial string and n. It returns an array, just like increment.

  1. Letters may be incremented/deleted/appended, special characters only deleted.

  2. It should decrement the string with using as few additional digits as possible and never go below the empty string

  3. Minimum digits used for decrementing is the length of the initial string, it may be more if necessary, see example below.

  4. The total amount of decrements, could be passed as an argument, since it is determined in advance.

  5. Assume, that we don't pass initial strings that are empty, or only containing the smallest letter "A"

Example: (we assume our letters to only be a,b for these examples)

"ab" decrement 8 times

-> 4 digits required minimum

result: ["aabb","aaba","aab","aaaa","aaa","aa","a",""]

"b$b" decrement 10 times

-> 3 digits are enough

result: ["b$a", "b$", "b", "abb", "aba", "ab", "aaa", "aa", "a", ""]


Solution

  • You can use the replace function with regular expression argument to quickly identify the terminating sequence of "z" characters, so you can turn them all into "A".

    For decrementing, the logic is to identify a terminating sequence of "A" characters. Those must turn to "z".

    And of course, in both cases the digit that precedes that terminating sequence must increment/decrement.

    If there is a foreign character in the input, it should remain unaltered. With this logic it will not be possible to ensure that both functions mirror each other exactly. For instance, if the input is "-A-", then the output of decrementString() would remove characters from the end of the string. If that output is given as input to incrementString(), then there is no information anymore about that lost hyphen, so that you would get "-A-" again.

    To ensure that the lexical order is ensured, more letters may need to be appended at the end of the string. For instance, without such intervention, the input "B" can only be decremented twice ("A" and empty string). By appending a character we can get "Az", "Ay", ... "AA", "A", "".

    Here is a solution. The runnable snippet allows you to input some characters and the number of required results, and it will display the output of calling both functions with that input:

    const upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    const digits = upper + upper.toLowerCase();
    
    const add = (ch, unit) => digits[digits.indexOf(ch) + unit];
    
    const lettersIn = string => string.replace(/[^A-Za-z]/g, "");
    
    const valueOf = letters => Array.from(letters).reduce(
        (value, letter) => value * digits.length + digits.indexOf(letter),
        0
    );
    
    function incrementString(string, length) {
        const letters = lettersIn(string);
        return digits.length ** letters.length - 1 - valueOf(letters) < length // Not enough digits? 
                   ? [string += digits[0], ...incrementString(string, length - 1)] // Add a digit, and retry...
                   : Array.from({length}, () =>
                        string = string.replace(/([A-Za-y])([^A-Za-y]*)$/, (_, digit, zz) =>
                            add(digit, 1) + zz.replaceAll(digits.at(-1), digits[0])
                        )
                    );
    }
    
    function decrementString(string, length) {
        const letters = lettersIn(string);
        const letterValue = valueOf(letters);
        // Count also the possibility to remove characters from the end (string.length):
        return letterValue + string.length < length && letterValue // Need more digits?
                    ? decrementString(string + digits[0], length) // Add a digit, and retry...
                    : Array.from({length}, () =>
                        string = string.replace(/([B-Za-z])?([^B-Za-z]*)$/, (match, digit, aa) =>
                            digit ? add(digit, -1) + aa.replaceAll(digits[0], digits.at(-1)) : aa.slice(0, -1)
                        )
                    );
    }
    
    // I/O management
    
    const [inpString, inpCount, outPrev, outNext] = document.querySelectorAll("input, pre");
    inpString.addEventListener("input", refresh);
    inpCount.addEventListener("input", refresh);
    
    function refresh() {
        const format = (arr) => arr.map(JSON.stringify).join(", ");
        const s = inpString.value;
        const count = +inpCount.value;
        outPrev.textContent = format(decrementString(s, count));
        outNext.textContent = format(incrementString(s, count));
    }
    refresh();
    pre { white-space: pre-wrap;}
    String: <input value="_Z_"><br>
    Count: <input value="29" type="number" min="1" max="100"><br>
    <table>
    <tr><td>Prev:</td><td><pre></pre></td></tr>
    <tr><td>Next:</td><td><pre></pre></td></tr>
    </table>