Search code examples
javascriptstate-machine

How to write a state machine to solve the Count the smiley faces?


I have solved the problem Count the smiley faces:

Given an array (arr) as an argument complete the function countSmileys that should return the total number of smiling faces.

Rules for a smiling face:

  • Each smiley face must contain a valid pair of eyes. Eyes can be marked as : or ;
  • A smiley face can have a nose but it does not have to. Valid characters for a nose are - or ~
  • Every smiling face must have a smiling mouth that should be marked with either ) or D

No additional characters are allowed except for those mentioned.

Valid smiley face examples: :) :D ;-D :~) Invalid smiley faces: ;( :> :} :]

Example

countSmileys([':)', ';(', ';}', ':-D']);       // should return 2;
countSmileys([';D', ':-(', ':-)', ';~)']);     // should return 3;
countSmileys([';]', ':[', ';*', ':$', ';-D']); // should return 1;

Note

In case of an empty array return 0. You will not be tested with invalid input (input will always be an array). Order of the face (eyes, nose, mouth) elements will always be the same.

Then when I look through the solutions I find that many people use regexp. Then I want write a state machine to implement regexp and solve this problem. But I failed. This is my code:

function countSmileys(smileys) {
  let state = smileyHasValidEye;
  return smileys.filter(smiley => {
    for (let s of [...smiley]) {
      state = state(s);
    }
    return state === true;
  }).length;
}

function smileyHasValidEye(s) {
  if (s === ':' || s === ';') {
    return smileyHasValidNose;
  }
  return smileyHasValidEye;
}

function smileyHasValidNose(s) {
  if (s === '-' || s === '~') {
    return smileyHasValidMouth;
  }
  return smileyHasValidMouth(s);
}

function smileyHasValidMouth(s) {
  if (s === ')' || s === 'D') {
    return true;
  }
  return;
}

console.log(countSmileys([':)', ';(', ';}', ':-D']));

And the error I get is:

state = state(s);
              ^

TypeError: state is not a function

Then I debugged my code I found the procedure doesn't enter the smileyHasValidNose function. Then I don't know the reason.


Solution

  • The problem is you don't really reset state in between smileys. So the next smiley state will be true which you can't call (it's not a function).

    You could use a local variable for state that resets it to the first function (the first step).

    function countSmileys(smileys) {
      let firstStep = smileyHasValidEye;
      return smileys.filter(smiley => {
        let state = firstStep;
        for (let s of [...smiley]) {
          state = state(s);
        }
        return state === true;
      }).length;
    }
    
    function smileyHasValidEye(s) {
      if (s === ':' || s === ';') {
        return smileyHasValidNose;
      }
      return smileyHasValidEye;
    }
    
    function smileyHasValidNose(s) {
      if (s === '-' || s === '~') {
        return smileyHasValidMouth;
      }
      return smileyHasValidMouth(s);
    }
    
    function smileyHasValidMouth(s) {
      if (s === ')' || s === 'D') {
        return true;
      }
      return;
    }
    
    console.log(countSmileys([':)', ';(', ';}', ':-D']));

    This code however, will error if there's more on the string besides the smiley (or a partial of the smiley).

    I would change smileyHasValidMouth to return false if it doesn't detect a smiley. Just to be more consistent here...

    function smileyHasValidMouth(s) {
      if (s === ')' || s === 'D') {
        return true;
      }
      return false;
    }
    

    And adjust your loop to exit early if it finds a value that is not a function.

        for (let s of [...smiley]) {
          state = state(s);
          if(typeof state !== 'function') return state;
        }
    

    function countSmileys(smileys) {
      let firstStep = smileyHasValidEye;
      return smileys.filter(smiley => {
        let state = firstStep;
        for (let s of [...smiley]) {
          state = state(s);
          if (typeof state !== 'function') return state;
        }
      }).length;
    }
    
    function smileyHasValidEye(s) {
      if (s === ':' || s === ';') {
        return smileyHasValidNose;
      }
      return smileyHasValidEye;
    }
    
    function smileyHasValidNose(s) {
      if (s === '-' || s === '~') {
        return smileyHasValidMouth;
      }
      return smileyHasValidMouth(s);
    }
    
    function smileyHasValidMouth(s) {
      if (s === ')' || s === 'D') {
        return true;
      }
      return false;
    }
    
    console.log(countSmileys([':~(', ':>', ':D', ':(', ':o>', ';)', ':)']));