Categories
javascript regex

Why does a RegExp with global flag give wrong results?

334

What is the problem with this regular expression when I use the global flag and the case insensitive flag? Query is a user generated input. The result should be [true, true].

var query = 'Foo B';
var re = new RegExp(query, 'gi');
var result = [];
result.push(re.test('Foo Bar'));
result.push(re.test('Foo Bar'));
// result will be [true, false]

var reg = /^a$/g;
for(i = 0; i++ < 10;)
   console.log(reg.test("a"));

4

  • 79

    Welcome to one of the many traps of RegExp in JavaScript. It has one of the worst interfaces to regex processing I’ve ever met, full of weird side-effects and obscure caveats. Most of the common tasks you typically want to do with regex are difficult to spell right.

    – bobince

    Oct 5, 2009 at 16:07

  • XRegExp looks like a good alternative. xregexp.com

    – about

    Oct 5, 2009 at 18:49

  • See answer here as well: stackoverflow.com/questions/604860/…

    – Prestaul

    Aug 28, 2014 at 18:40

  • One solution, if you can get away with it, is to use the regex literal directly instead of saving it to re.

    – thdoan

    Oct 10, 2018 at 17:26

421

A RegExp object with the g flag keeps track of the lastIndex where a match occurred, so on subsequent matches it will start from the last used index, instead of 0. Take a look:

var query = 'Foo B';
var re = new RegExp(query, 'gi');
console.log(re.lastIndex);

console.log(re.test('Foo Bar'));
console.log(re.lastIndex);

console.log(re.test('Foo Bar'));
console.log(re.lastIndex);

If you don’t want to manually reset lastIndex to 0 after every test, just remove the g flag.

Here’s the algorithm that the specs dictate (section 15.10.6.2):

RegExp.prototype.exec(string)

Performs
a regular expression match of string
against the regular expression and
returns an Array object containing the
results of the match, or null if the
string did not match The string
ToString(string) is searched for an
occurrence of the regular expression
pattern as follows:

  1. Let R be this RexExp object.
  2. Let S be the value of ToString(string).
  3. Let length be the length of S.
  4. Let lastIndex be the value of the lastIndex property on R.
  5. Let i be the value of ToInteger(lastIndex).
  6. If the global property is false, let i = 0.
  7. If i < 0 or i > length then set the lastIndex property of R to 0 and return null.
  8. Call [[Match]], giving it the arguments S and i. If [[Match]]
    returned failure, go to step 9;
    otherwise let r be its State result
    and go to step 10.
  9. Let i = i+1.
  10. Go to step 7.
  11. Let e be r’s endIndex value.
  12. If the global property is true, set the lastIndex property of R to e.
  13. Let n be the length of r’s captures array. (This is the same
    value as 15.10.2.1’s
    NCapturingParens.)
  14. Return a new array with the following properties:
  • The index
    property is set to the position of the
    matched substring within the complete
    string S.
  • The input property is set
    to S.
  • The length property is set to
    n + 1.
  • The 0 property is set to the
    matched substring (i.e. the portion of
    S between offset i inclusive and
    offset e exclusive).
  • For each
    integer i such that i > 0 and i ≤ n,
    set the property named ToString(i) to
    the ith element of r’s captures array.

5

  • 112

    This is like Hitchhiker’s Guide to the Galaxy API design here. “That pitfall that you fell in has been perfectly documented in the spec for several years, if you had only bothered to check”

    – Retsam

    Aug 22, 2013 at 19:54

  • 5

    Firefox’s sticky flag doesn’t do what you imply at all. Rather, it acts as if there were a ^ at the start of the regular expression, EXCEPT that this ^ matches the current string position (lastIndex) rather than the start of the string. You’re effectively testing if the regex matches “right here” instead of “anywhere after lastIndex”. See the link you provided!

    – Doin

    Jan 14, 2014 at 12:15

  • 1

    The opening statement of this answer is just not accurate. You highlighted step 3 of the spec which says nothing. The actual influence of lastIndex is in steps 5, 6 and 11. Your opening statement is only true IF THE GLOBAL FLAG IS SET.

    – Prestaul

    Aug 28, 2014 at 18:38


  • @Prestaul yes, you’re right that it doesn’t mention the global flag. It was probably (can’t remember what I thought back then) implicit due to the way the question is framed. Feel free to edit the answer or delete it and link to your answer. Also, let me reassure you that you’re better than me. Enjoy!

    Aug 29, 2014 at 0:45

  • @IonuțG.Stan, sorry if my previous comment seemed attacky, that was not my intent. I can’t edit it at this point, but I wasn’t trying to shout, just to draw attention to the essential point of my comment. My bad!

    – Prestaul

    Aug 29, 2014 at 22:11


80

You are using a single RegExp object and executing it multiple times. On each successive execution it continues on from the last match index.

You need to “reset” the regex to start from the beginning before each execution:

result.push(re.test('Foo Bar'));
re.lastIndex = 0;
result.push(re.test('Foo Bar'));
// result is now [true, true]

Having said that it may be more readable to create a new RegExp object each time (overhead is minimal as the RegExp is cached anyway):

result.push((/Foo B/gi).test(stringA));
result.push((/Foo B/gi).test(stringB));

1

  • 4

    Or simply don’t use the g flag.

    – melpomene

    Sep 20, 2019 at 6:12

39

RegExp.prototype.test updates the regular expressions’ lastIndex property so that each test will start where the last one stopped. I’d suggest using String.prototype.match since it doesn’t update the lastIndex property:

!!'Foo Bar'.match(re); // -> true
!!'Foo Bar'.match(re); // -> true

Note: !! converts it to a boolean and then inverts the boolean so it reflects the result.

Alternatively, you could just reset the lastIndex property:

result.push(re.test('Foo Bar'));
re.lastIndex = 0;
result.push(re.test('Foo Bar'));