How can I count the number of times different terms occur in a string string with JavaScript?

Viewed 320

I have a list of 20 phrases that I want to count in an article.

Currently, I'm doing


      let counts = phrases.map((phrase, idx) => {
        phrase.usage = (articleBody.match(new RegExp(phrase.phrase, 'gi')) || []).length
        return phrase
      })

This requires me to do an expensive search each time. Is there some way to do it faster or all at once?

4 Answers

You can use String.prototype.matchAll

const str = ['ocurrence 1, and..', 'ocurrence 2, and..', 'other phrases'];

const array = [...str.join ` `.matchAll(new RegExp('ocurrence', 'gi'))];

console.log(array.length); // 2

Edit: counting occurrences in one string;

const articleBody = 'ocurrence 1, and.. ocurrence 2, and... etc';
const array = [...articleBody.matchAll(new RegExp('ocurrence', 'gi'))];

console.log(array.length) // 2;

Edit 2:

Using a service worker

I created a text of 3000 words ("Lorem ipsum" generator).

main.js

if ("serviceWorker" in navigator) {
         document.addEventListener("DOMContentLoaded", function () {
         const phrases = [
            { text: "Lorem ipsum" },
            { text: "elementum mattis" },
          ];
          phrases.map((phrase, index) => {
            const phraseWorker = new Worker("phrase-match-sw.js");

            phraseWorker.onmessage = function (oEvent) {
              const { text, match } = oEvent.data;
              phrases[index] = { text, match };
            };

            phraseWorker.postMessage({
              article:
                "Lorem ipsum dolor sit amet consectetur adipiscing elit sapien orci, ligula leo consequat mus tellus elementum mattis lacus maecenas curabitur, ",
              phrase,
            });
          });
        });
      }

phrase-match-sw.js

onmessage = function (event) {
  const { article, phrase } = event.data;
  phrase.match = [...article.matchAll(new RegExp(phrase.text,"gi"))].length;
  postMessage(phrase);
};

Please take a look this solution.

function occurrences(string, subString, allowOverlapping) {
    string += "";
    subString += "";
    if (subString.length <= 0) return (string.length + 1);

    var n = 0,
        pos = 0,
        step = allowOverlapping ? 1 : subString.length;

    while (true) {
        pos = string.indexOf(subString, pos);
        if (pos >= 0) {
            ++n;
            pos += step;
        } else break;
    }
    return n;
}

console.time('occurrences');
occurrences("foofoofoo", "bar"); //0
occurrences("foofoofoo", "foo"); //3
occurrences("foofoofoo", "foofoo"); //1
console.timeEnd('occurrences');

console.time('RegExp');
("foofoofoo".match(new RegExp("bar", 'gi')) || []).length; // 0
("foofoofoo".match(new RegExp("foo", 'gi')) || []).length; // 3
("foofoofoo".match(new RegExp("foofoo", 'gi')) || []).length; // 1
console.timeEnd('RegExp');

This is the benchmark result: https://jsben.ch/aIqEP

I tried it on a 50KB article body and benchmarked it with your usage. The results showing the solution below is %40 faster than your solution.

The benchmark link https://jsben.ch/wWW6e

I decreased the article size for saving it you can increase it for better results.

let phrases = [{ phrase: 'placerat', usage: 0 }, { phrase: 'lorem', usage: 0 }];
let matching = article.match(new RegExp(phrases.map(phrase => phrase.phrase).join('|'), 'gi')).reduce((acc, match) => {
    match = match.toLowerCase();
    if (acc[match]) {
        acc[match].count++;
    } else {
        acc[match] = { count: 0 };
    }      

    return acc;
}, {});

You can use the | operator in your .match() call to match multiple phrases. There's an example at developer.mozilla.org for matching fox or cat, but they don't go into the details on counting the results.

But since match() returns the results, just build a hash to count the number of times each match occurs...

    // set data we'll be using
var str = "hello 1 hello 2 hello222notarealwordmatch hey 3";       // text string to search
var matchwords = ['hello', 'hey'];    // look for the phrases "hello" or "hey"

    // build regex
var fullmatch = '\\b' + matchwords.join('\\b|\\b') + '\\b';
var match = new RegExp(fullmatch, "gi");     // set "g" for greedy, to search the whole string; "i" for ignore-case, to be case insensitive

    // build match results
var matches = {};
var results = str.match(match);
if(results) {
  results.forEach((thing) => {
    matches[thing] = matches[thing] ? matches[thing] + 1 : 1;
  });
}

    // display results
console.log(matches);

In your case, to match exact whole words, you'd also want to match on word borders, for instance /\bcode\b|\bdatabase\b/ will match code and database, but not databaselike or codeesque. In my example above, you'll see we are searching for "hello", but "hello222notarealwordmatch" correctly gets ignored in the counts. If you don't care about word-boundaries, then simply remove the 4 instances of \\b from up above in defining fullmatch...

var fullmatch = matchwords.join('|');
Related