One of the most known and agreed upon principles in software development is DRY - don’t repeat yourself. In this short post I’ll show you how applying DRY to regular expressions in JavaScript can be tricky. The examples will be written for Node.js, but the idea applies to other environments as well.
Let’s say we want to use regular expressions to check if a string includes digits:
let string = "Hello world! 123";
if (/\d+/.test(string)) {
console.log("Digit found!");
} else {
console.log("Digit not found.");
}
Now, just for the sake of argument, assume that this regular expression might get more complex in the future. Also, assume that we’ll need it in mulitple parts of the application. The logical thing to do would be to put it in a module so that it can be reused.
exports.digits = /\d+/g;
Notice that we’ve also added the g
flag for “global”.
It will allow us to find all digits in a string instead of just the first match.
If we’re preparing for reuse we might as well support other use cases.
Now we can open up the Node.js console, import the regex and use it.
const { digits } = require("./regex");
digits.test("Hello world! 123");
// true
digits.test("321");
// false
digits.test("321");
// true
And everything works as expected… Except it doesn’t.
If you’ve noticed the humble false
in the example above, you’ve just witnessed a pecularity of the JavaScript RegExp object that caused me to beat my head against the wall for better part of a workday.
Figuratively of course.
What happened there? Well, according to MDN Web Docs:
When a regex has the global flag set,
test()
will advance thelastIndex
of the regex. (RegExp.prototype.exec()
also advances thelastIndex
property.)Further calls to test(str) will resume searching str starting from lastIndex. The lastIndex property will continue to increase each time test() returns true.
Note: As long as
test()
returnstrue
,lastIndex
will not reset—even when testing a different string!When
test()
returnsfalse
, the calling regex’slastIndex
property will reset to0
.
Why it works like that will remain a mystery to me. Good thing it’s well documented, but as one of my coworkers said: “just because it’s documented doesn’t mean it’s okay”.
Now what?
I’ve written this in hope that at least one person’s time will be saved after reading.
And as much enjoyable laughing at JavaScript might be for me, it wouldn’t be fair not to suggest a solution.
Although one could reset lastIndex
after each call to test()
:
const { digits } = require("./regex");
digits.test("Hello world! 123");
// true
digits.lastIndex = 0;
digits.test("321");
// true
digits.lastIndex = 0;
digits.test("321");
// true
But then it might be less painful to just copy-paste the regex where needed.
A better solution is to build a habit of using search
or match
functions of String
, when working with global regexes, instead of relying on test
.
const { digits } = require("./regex");
"Hello world! 123".search(digits) > -1;
// true
"321".search(digits) > -1;
// true
"321".search(digits) > -1;
// true