Skip to content

Instantly share code, notes, and snippets.

@stevensacks
Last active January 3, 2019 06:13
Show Gist options
  • Save stevensacks/70786f045a02813d163a181834819bd6 to your computer and use it in GitHub Desktop.
Save stevensacks/70786f045a02813d163a181834819bd6 to your computer and use it in GitHub Desktop.
Free Code Camp - Intermediate Algorithms - Missing Letters
const onlyLetters = str => str.replace(/[^a-zA-Z]/g, '');
const alphabetize = str => str.split('').sort().join('');
const toLowerCase = str => str.toLowerCase();
const fromCharCode = code => String.fromCharCode(code);
const charCode = char => char.charCodeAt(0);
const excludes = (str, char) => !str.includes(char);
const first = str => str[0];
const last = str => str[str.length - 1];
const defined = f => f || undefined;
const range = (start, end) => 
  Array(end - start + 1)
    .fill()
    .map((_, i) => start + i);

const charRange = str => 
  range(
    charCode(first(str)),
    charCode(last(str))
  ).map(fromCharCode);

const missing = str =>
  charRange(str)
    .reduce((acc, c) =>
      excludes(str, c) ? acc + c : acc, '');

const compose = (...fns) => args => fns.reduceRight((arg, fn) => fn(arg), args);

const sanitize = compose(alphabetize, toLowerCase, onlyLetters);

const fearNotLetter = compose(defined, missing, sanitize);

console.log(fearNotLetter('abCdf'));
// e
console.log(fearNotLetter('th3 qu1ck br0wn f0x jump3d 0ve7 th3 1azy d0g'));
// ilos
console.log(fearNotLetter('what does the fox say?'));
// bcgijklmnpqruv
console.log(fearNotLetter('hifged'));
// undefined

Code Explanation:

This is obviously more code than the other solutions, however, it's more robust since it can handle any string, and also includes many helpful functions that can be used in multiple tests, not just this one.

At the top are declarative functions, some of which convert native JavaScript imperative functions to declarative.

  • onlyLetters uses regex to remove any characters from a string that are not letters.
  • alphabetize splits a string into an array, sorts it, and turns it back into a string.
  • toLowerCase is a declarative way to call String.prototype.toLowerCase()
  • fromCharCode is a declarative way to call String.fromCharCode().
  • charCode returns the charCode of a given character.
  • excludes is a helper function to return the opposite of includes().
  • first and last are declarative functions which can be used for both Strings and Arrays!
  • defined returns what is passed if it's truthy and undefined if it isn't.
  • range returns an array that consists of all the numbers between and including start to end. The next two functions leverage the above functions.
  • charRange creates a range using the first and last charCodes of an alphabetized string, and then maps them back into letters again.
  • missing reduces the charRange into a string that only contains letters excluded from the passed string.

Finally, we put it all together using compose. The tl;dr is that compose calls each function from right to left using the output of each function as the input of the next function. See the links below to learn more about function composition. Our first composed function is sanitize. It filters out non-letters, converts to lowercase, and alphabetizes. It's worth pointing out that we can change the order of these functions in compose() and we will still get the same result. Because of the way that composition works, we can compose composed functions together!

fearNotLetter takes the result of the three functions of sanitize and passes it to missing, and then the output of missing is passed to defined. Unlike the previous compose(), we cannot change the order because missing() needs a sanitized string to work properly.

You might wonder why we don't have missing return undefined, since that is what the test requires. The problem with that is that it changes the type (from String to undefined). What if, in the future, we want to do something else to the missing letters. Maybe convert them to uppercase, or format them in some other way. If that function expects a string and we pass it undefined, it will throw an error.

While we could make all our string functions safe by always checking to see if the passed value is a String, this is not ideal in functional code. There are better ways to do this, but they are out of scope of this explanation.

So, only because this test requires it, we add the defined function at the very end of our composition (on the left). Normally, we wouldn't want to change the type from String to undefined, and instead just return an empty string when no missing characters are found. We could avoid this unsafe approach by separating defined like this:

const findMissing = compose(missing, sanitize);
const fearNotLetter = compose(defined, findMissing);

This isolates the requirement of this test (fearNotLetter) to return undefined, and now we can use findMissing safely elsewhere. Notice that we're composing yet again because of the right-to-left nature of compose.

You might be asking yourself why we have to write composed functions from right-to-left. There is another function called pipe (or sequence) which works exactly like compose, but goes from left-to-right. If you're curious why compose is right-to-left, check the link below.

Imperative code can be shorter, but, it can also be less flexible and harder to debug.

Here is this solution, written imperatively:

const fearNotLetter = str => {
  const sanitized = str.replace(/[^a-zA-Z]/g, '').toLowerCase().split('').sort().join('');
  const start = sanitized.charCodeAt(0);
  const end = sanitized.charCodeAt(sanitized.length - 1);
  const range = Array(end - start + 1).fill().map((_, i) => String.fromCharCode((start + i)));
  const missing = range.reduce((acc, c) => sanitized.includes(c) ? acc : acc + c, '');
  return missing || undefined;
}

As evidenced by the other imperative solutions, there are simpler ways of accomplishing roughly the same thing. This code is also more difficult to read and thus it's harder to figure out what's going on (commonly called "reason about").

Writing pure functions declaratively is a fundamental part of functional programming. It helps you break down your logic into discrete functions that each do one thing. Even if you don't use compose, writing code this way has many benefits.

For example, we now have a bunch of functions that we can reuse in many different ways, including future tests in this course! The only function that is specific to this test is fearNotLetter (assuming we separated findMissing as explained above), and the other functions can be used in many ways and composed together.

As you can see from the tests, this version of fearNotLetter can handle any string, in any order, with any mix of letters, cases, numbers, spaces, etc. The other solutions only work if the string contains only lowercase letters and is alphabetical. If you don't pass a string that follows exactly those rules, the other solutions will fail.

That said, you could use just our sanitize function and pass its output to one of the other solutions to make them reliable.

Relevant Links

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment