Today I faced a very interesting exercise. I had to make a function that took a string as its argument and returned the same string but written in spinal tap case (which apparently is the same as or very similar to kebab case, just-like-this). Up until here, there's nothing to fear, I knew I could just
.split().join().toLowerCase() it away. The challenge for me came when I had to turn a string such as
"ThisIsAString" into spinal tap. My first thought was how on earth do I divide a string by its capitals and keep the capitals at the same time?
The answer to my problem is lookaheads. I had learned about them a while ago, but hadn't really understood them. So this exercise was the perfect opportunity to actually learn how to use them.
The first bit of the exercise was quite simple. So I'm just going to put it here and not say much about it. This creates an array separating where there is a space or an underscore. Then joins it againt into a string separating each element with a hyphen and then turns it all to lower case. Easy peasy.
const toSpinal = str => str.split(/\s|_/).join('-').toLowerCase()
As you'll have noticed, when you turn a string into an array using
split(), it also removes the character(s) that mark the boundaries between elements. So in our example, the string
"I Like_Potatoes" would become
After this lengthy introduction, let's return to the problem at hand. My function so far works very well for anything that isn't
"ILikePotatoes". But to adecuately process such string, I know that I can't simply use a regular expression like
[A-Z], because that would also remove the capital letters. So I'd get something like
This is where lookaheads come in handy. Lookaheads allow us to check whether an expression A is followed by another expression B. It will only return A if B follows (or doesn't follow) it, but it will leave B unmatched and untouched. We write a lookahead using
(?!) if we want the expression not to be followed by the lookahead).
const regEx = /[A-Z](?=\W)/g const str = "A. Potato, B- Onion, C: All of the above" str.match(regEx) // returns A, B, C
(?=\W) tells the program to look for any non-word character after the previous expression. So all capital letters that are not followed by a non-word character, will not return a match. But notice that the
match() only returns the three capital letters A, B and C, not the character that follows them.
If we wanted the string to return a match for capital letters that are not followed by a non-word character, we could simply use a negative lookahead (
const regEx = /[A-Z](?!\W)/g const str = "A. Potato, B- Onion, C: All of the above" str.match(regEx) // returns P, O, A
If we write a regular expression with a lookahead but with no preceeding expression, it will match all that matches the lookahead but return empty. We can use this to our advantage when we want to turn the string
"ILikePotatoes" into spinal tap case:
const toSpinal = str => str.split(/\s|_|(?=[A-Z])/).join('-').toLowerCase() //Note that the lookahead comes after a | so it isn't attached to the previous expression console.log(toSpinal("I like potatoes")) //i-like-potatoes console.log(toSpinal("ILikeCarrots")) //i-like-carrots console.log(toSpinal("ThisIsA_really We1rdString")) //this-is-a-really-we1rd-string
Lookaheads allow us to define whether an expression will be matched or not based on what follows it (or doesn't). So it will only match an expression if both the expression itself and the lookahead match. But it will only return the expression, without the lookahead.