Best practices for testing / checking strings in JavaScript

Viewed 224

I recently encountered this (admittedly) textbook JavaScript exercise and I'm having trouble conceptually walking through the two solutions I've been told are "best practices." I realize this is pretty basic stuff; I'm just trying to get as a solid grasp of the fundamentals here as I can.

The exercise itself is straightforward enough: write a function that checks to see if a string contains an identical number of two unique characters (in this case, 'x's and 'o's). The function must return a boolean and be case insensitive.

The first "best practice" solution, which I partially understood, was this:

function XO(string) {
let x = str.match(/x/gi);
let o = str.match(/o/gi);
return (x && x.length) === (o && o.length);
}

I understand the basic regex work being done in the first two lines of the function, but the precise role of the && logical operator in the third & final line of the function mystifies me. I'm having trouble, in plain English, explaining to myself (a) precisely what it does and (b) why something like return x === y alone is not a sufficiently robust solution in this case.

The second "best practice" solution I encountered was this:

const XO = str => {
  str = str.toLowerCase().split('');  
  return str.filter(x => x === 'x').length === str.filter(x => x === 'o').length;
}

I'm afraid I have to admit most of this solution's logic escapes me, although I do grasp that .split('') is being run to produce an array of single characters that can be run through .filter.

1 Answers

Firstly, match returns null if there are no matches at all. If you try to take the length of null, an error will be thrown.

x && x.length works, because of a) operator short-circuiting: if the former is "falsy", the latter is not executed (it wouldn't change the result) and the last truthy value is returned, b) any value is coerced as boolean in a boolean context (e.g. as an operand to &&)

function XO(string) {
  let x = str.match(/x/gi); 
  // x contains an array that has every match if any
  // e.g. ["x", "x", "x"] or null

  let o = str.match(/o/gi);
  // same for "o"

  return (x && x.length) === (o && o.length);
  // 
  // Given x === null, y === null : 
  //   (null && null.length) === (null && null.length)
  //   (false && null.length) === (false && null.length) <- boolean coercion
  //   false === false
  //   true
  // 
  // Given x === ["x", "x"], y === ["y", "y"] : 
  //   (["x", "x"] && ["x", "x"].length) === (["y", "y"] && ["y", "y"].length)
  //   (true && ["x", "x"].length) === (true && ["y", "y"].length)
  //   (true && 2) === (true && 2)
  //   2 === 2
  //   true
}

The second has a other tricks. It uses the input argument as a variable (which is not too common, but valid). Also, instead of regular functions, it uses "arrow functions". They have some differences but mostly you can think that function a(x) { return 2*x; } works as const a = (x) => 2*x;

const XO = str => {
  str = str.toLowerCase().split(''); 
  // replace the argument string with an array of characters
  //   str = "xXyY".toLowerCase().split('')
  //   str = "xxyy".split("")
  //   str = ["x", "x", "y", "y"]

  return str.filter(x => x === 'x').length === str.filter(x => x === 'o').length;
  //   ["x", "x", "y", "y"].filter(x => x === 'x').length === ["x", "x", "y", "y"].filter(x => x === 'y').length;
  //   ["x", "x"].length === ["y", "y"].length
  //   2 === 2
  //   true
}
Related