thzinc

scramble - Interview question of the week from rendezvous with cassidoo

The tests were harder than the implementation 🥴

Interview question of the week

This week’s question (thanks Tom!): If you mix up the order of letters in a word, many people can slitl raed and urenadnstd tehm. Write a function that takes an input sentence, and mixes up the insides of words (anything longer than 3 letters).

Example:

> scramble(["A quick brown fox jumped over the lazy dog."])
> "A qciuk bwron fox jmepud oevr the lzay dog."

My solution

Written as a stream of consciousness

Upon looking at the inputs, it’s interesting to note that the argument to scramble() is an array of strings, but the result is a single string. Looks like I’ll have to make a decision about how to handle an input array of arbitrary length: I’ll join the elements of the array with a space. e.g.:

> scramble([
  "A quick brown fox jumped over the lazy dog.",
  "There is a snake in my boots.",
])
> "A qciuk bwron fox jmepud oevr the lzay dog. Trehe is a sknae in my botos."

Now, I think the tests will actually be more challenging than the implementation of scramble(). I’ll need to assert the starting and ending characters match, and then the matching characters in between in any order.

I’ll base my word handling on the regular expression “word character” class (\w) and use String.prototype.matchAll() to return an iterable of results of each word in the string.

Comparing the first and last letters of a word is easy enough, but I think for the middles, I’ll sort the characters in the string and compare the sorted results.

I remembered seeing punctuation in the output, so I’ll resort to using String.prototype.replace() with the signature that uses a replacer function in my scramble() implementation.

#mocha

  

function scramble(input = []) {
  return input.join(" ").replace(/\w+/g, (word) => {
    if (word.length < 4) return word;

    const first = word.slice(0, 1);
    const middle = Array.from(word.slice(1, -1));
    middle.sort(() => Math.random() * 2 - 1);
    const last = word.slice(-1);

    return [first, ...middle, last].join("");
  });
}


mocha.setup("bdd");
const assert = chai.assert;
const expect = chai.expect;
const should = chai.should();

describe("Given valid inputs", () => {
  const expectations = [
    {
      input: ["A quick brown fox jumped over the lazy dog."],
      output: "A qciuk bwron fox jmepud oevr the lzay dog.",
    },
    {
      input: [
        "A quick brown fox jumped over the lazy dog.",
        "There is a snake in my boots.",
      ],
      output:
        "A qciuk bwron fox jmepud oevr the lzay dog. Trehe is a sknae in my botos.",
    },
  ];
  expectations.forEach(({ input, output }) => {
    describe(JSON.stringify(input), () => {
      // Arrange
      const wordPattern = /\w+/g;
      const expectedWords = Array.from(output.matchAll(wordPattern));
      const expectedNonWordCharacters = output.replace(/\w/g, "X");

      // Act
      const actual = scramble(input);
      const actualWords = Array.from(actual.matchAll(wordPattern));
      const actualNonWordCharacters = actual.replace(/\w/g, "X");

      // Assert
      it("should result in the same number of words", () =>
        actualWords.should.have.lengthOf(expectedWords.length));

      it("should preserve non-word characters", () => {
        expectedNonWordCharacters.should.equal(actualNonWordCharacters);
      });

      for (let i = 0; i < expectedWords.length; i++) {
        // Arrange
        const [expectedWord] = expectedWords[i] || [];
        const [actualWord] = actualWords[i] || [];
        describe(`${expectedWord} ~= ${actualWord}`, () => {
          it("should match first letter", () => {
            // Arrange
            const expectedFirst = expectedWord.slice(0, 1);

            // Act
            const actualFirst = actualWord.slice(0, 1);

            // Assert
            actualFirst.should.equal(expectedFirst);
          });

          it("should match last letter", () => {
            // Arrange
            const expectedLast = expectedWord.slice(-1);

            // Act
            const actualLast = actualWord.slice(-1);

            // Assert
            actualLast.should.equal(expectedLast);
          });

          it("should have the same letters in between", () => {
            // Arrange
            const expectedMiddle = Array.from(expectedWord.slice(1, -1));
            expectedMiddle.sort();

            // Act
            const actualMiddle = Array.from(actualWord.slice(1, -1));
            actualMiddle.sort();

            // Assert
            actualMiddle.should.deep.equal(expectedMiddle);
          });
        });
      }
    });
  });
});

mocha.run();

See also