DEV Community

SavagePixie
SavagePixie

Posted on

Lookaheads in Javascript

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 "i-like-potatoes".

So, What about These Lookaheads?

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 "--ike-otatoes".

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 (?=) (or (?!) 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

The lookahead (?=\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

Conclusion

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.

Latest comments (8)

Collapse
 
ambroseus profile image
Eugene Samonenko

thanks Perl for regexes :)

Collapse
 
daveskull81 profile image
dAVE Inden

Great article. It’s always really interesting to see the power of regular expressions and what one can do with them.

Collapse
 
savagepixie profile image
SavagePixie

Indeed, the more I learn about them, the more I like them.

Collapse
 
gdebenito profile image
Gonzalo • Edited

Hey! Thanks to you I made something!! A regex to detect uncommented console.log()!! What do you think?

const regEx = /^(?!.*\/\/)(?=.*console\.log)/g
const examples = [
    '  // console.log("asdf")',
    '  //        console.log("asdf")',
    '  //        console.log("asdf")',
    '       console.log("asdf")   ',
    '       console.log(',
]

examples.forEach(
    (example) => {
        console.log(
            example,
            example.match(regEx) !== null
        )
    }
)

And the output:

  // console.log("asdf") false
  //        console.log("asdf") false
  //        console.log("asdf") false
       console.log("asdf")    true
       console.log( true
Collapse
 
moopet profile image
Ben Sinclair

Your next goal is to get it to work with these :)

console.log("foo"); // console.log("bar");

/*****
* console.log("baz");
*****/

console.table(['foo', 'bar']);
Collapse
 
savagepixie profile image
SavagePixie

That's pretty cool! I imagine something like this would be useful to comment console.logs once you're done debugging.

Collapse
 
avalander profile image
Avalander • Edited

Nice write-up, I didn't even know about lookaheads!

[...] to explain what spinal tap case is. I wanted to link it to a definition, but I couldn't find one.

I think it's marginally better known as kebab-case, although I've also heard it as lisp-case.

Collapse
 
savagepixie profile image
SavagePixie • Edited

Thanks!

Thanks also for the link. I'll update the post and add it.

By the way, you can also do lookbehinds in Javascript, which work the same way, except that they return a match if the expression is preceeded by the lookbehind.

const regExA = /(?<=)/ //Positive lookbehind
const regExB = /(?<!)/ //Negative lookbehind