Changing the RegExp flags
Asked Answered
G

4

22

So basically I wrote myself this function so as to be able to count the number of occurances of a Substring in a String:

String.prototype.numberOf = function(needle) {
  var num = 0,
      lastIndex = 0;
  if(typeof needle === "string" || needle instanceof String) {
    while((lastIndex = this.indexOf(needle, lastIndex) + 1) > 0)
      {num++;} return num;
  } else if(needle instanceof RegExp) {
    // needle.global = true;
    return this.match(needle).length;
  } return 0;
};

The method itself performs rather well and both the RegExp and String based searches are quite comparable as to the execution time (both ~2ms on the entire vast Ray Bradbury's "451 Fahrenheit" searching for all the "the"s).

What sort of bothers me, though, is the impossibility of changing the flag of the supplied RegExp instance. There is no point in calling String.prototype.match in this function without the global flag of the supplied Regular Expression set to true, as it would only note the first occurance then. You could certainly set the flag manually on each RegExp passed to the function, I'd however prefer being able to clone and then manipulate the supplied Regular Expression's flags.

Astonishingly enough, I'm not permitted to do so as the RegExp.prototype.global flag (more precisely all flags) appear to be read-only. Thence the commented-out line 8.

So my question is: Is there a nice way of changing the flags of a RegExp object?

I don't really wanna do stuff like this:

if(!expression.global)
  expression = eval(expression.toString() + "g");

Some implementations might not event support the RegExp.prototype.toString and simply inherit it from the Object.prototype, or it could be a different formatting entirely. And it just seems as a bad coding practice to begin with.

Gorrono answered 29/4, 2011 at 17:47 Comment(0)
W
18

First, your current code does not work correctly when needle is a regex which does not match. i.e. The following line:

return this.match(needle).length;

The match method returns null when there is no match. A JavaScript error is then generated when the length property of null is (unsuccessfully) accessed. This is easily fixed like so:

var m = this.match(needle);
return m ? m.length : 0;

Now to the problem at hand. You are correct when you say that global, ignoreCase and multiline are read only properties. The only option is to create a new RegExp. This is easily done since the regex source string is stored in the re.source property. Here is a tested modified version of your function which corrects the problem above and creates a new RegExp object when needle does not already have its global flag set:

String.prototype.numberOf = function(needle) {
    var num = 0,
    lastIndex = 0;
    if (typeof needle === "string" || needle instanceof String) {
        while((lastIndex = this.indexOf(needle, lastIndex) + 1) > 0)
            {num++;} return num;
    } else if(needle instanceof RegExp) {
        if (!needle.global) {
            // If global flag not set, create new one.
            var flags = "g";
            if (needle.ignoreCase) flags += "i";
            if (needle.multiline) flags += "m";
            needle = RegExp(needle.source, flags);
        }
        var m = this.match(needle);
        return m ? m.length : 0;
    }
    return 0;
};
Wuhan answered 29/4, 2011 at 18:48 Comment(3)
Thanks for pointing out the inconsistency. I like the solution, it is maybe safer not to extend the RegExp prototype with a flags function (as suggested above).Gorrono
Better yet, use myRegex.test(str) if that's all you care about. It's both shorter and faster.Jannette
Not quite, I'm trying to count all the occurances. :-)Gorrono
F
15
var globalRegex = new RegExp(needle.source, "g");

Live Demo EDIT: The m was only for the sake of demonstrating that you can set multiple modifiers

var regex = /find/;
var other = new RegExp(regex.source, "gm");
alert(other.global);
alert(other.multiline);
Freudian answered 29/4, 2011 at 17:55 Comment(0)
B
6
r = new Regexp(r.source, r.flags + (r.global ? "" : "g"));
Betelgeuse answered 14/11, 2018 at 23:14 Comment(1)
Use a note, the () around r.global ? "" : "g" is necessary because the conditional operator has a lower precedence than addition. That tripped me up so be careful.Polder
B
4

There isn't much you can do but I highly recommend you avoid using eval. You can extend the RegExp prototype to help you out.

RegExp.prototype.flags = function () {
    return (this.ignoreCase ? "i" : "")
        + (this.multiline ? "m" : "")
        + (this.global ? "g" : "");
};

var reg1 = /AAA/i;
var reg2 = new RegExp(reg1.source, reg1.flags() + 'g');
Bitolj answered 29/4, 2011 at 17:57 Comment(1)
I figured out the .source part just a few moments ago. I, however, completely forgot about the fact that the flags aren't a part of the source. :) I think I will incorporate the idea. Thanks ;)Gorrono

© 2022 - 2024 — McMap. All rights reserved.