88

For example, if I had "scissors" in variable and wanted to know the position of all occurrences of the letter "s", it should print out 1, 4, 5, 8.

How can I do this in JavaScript in most efficient way? I don't think looping through the whole is terribly efficient

Penny Liu
  • 15,447
  • 5
  • 79
  • 98
Ageis
  • 2,221
  • 4
  • 22
  • 34
  • 12
    You really don't want 1-based character indices, do you? – Phrogz May 22 '12 at 21:27
  • 3
    Unless you have a large string, or a large number of strings, or this happens fairly often (like 100's of times a second), looping through the whole string will probably suffice. What matters is not how efficient it is, but whether it is [*fast enough*](http://stackoverflow.com/a/3770194/116614). – mellamokb May 22 '12 at 21:27
  • 2
    Notice that the position of the characters start at `0` (not at `1`), this is confusing at the beginning but you will do it automatically with practice – ajax333221 May 22 '12 at 22:36
  • 1
    *"I don't think looping through the whole is terribly efficient"* - How could it be possible to test every character in a string *without* looping through the whole string? Even if there was a built in `.indexOfAll()` method it would have to loop behind the scenes... – nnnnnn Aug 09 '16 at 21:58

15 Answers15

127

A simple loop works well:

var str = "scissors";
var indices = [];
for(var i=0; i<str.length;i++) {
    if (str[i] === "s") indices.push(i);
}

Now, you indicate that you want 1,4,5,8. This will give you 0, 3, 4, 7 since indexes are zero-based. So you could add one:

if (str[i] === "s") indices.push(i+1);

and now it will give you your expected result.

A fiddle can be see here.

I don't think looping through the whole is terribly efficient

As far as performance goes, I don't think this is something that you need to be gravely worried about until you start hitting problems.

Here is a jsPerf test comparing various answers. In Safari 5.1, the IndexOf performs the best. In Chrome 19, the for loop is the fastest.

enter image description here

vcsjones
  • 138,677
  • 31
  • 291
  • 286
  • 2
    +1 *By far* the fastest solution. http://jsperf.com/javascript-string-character-finder – Tomalak May 22 '12 at 21:43
  • 3
    LOL, we three all made our own JSPerf tests ;) Note that looping is faster on Chrome, but slower on Firefox and IE (according to my test). – Phrogz May 22 '12 at 21:51
  • @Phrogz Great minds and all. ;) Looks like Chrome is a little off. – Tomalak May 22 '12 at 21:53
  • @Phrogz Add Safari to that list as well. – vcsjones May 22 '12 at 21:54
  • @vcsjones I want to, but BrowserScope has been rejecting my Safari and Opera runs for a couple of days now; not sure why ("CSRF Error - Need csrf_token in request"), but the JSPerf author blames browser scope. Safari's perf is similar to Firefox's, but with extra punishment on your loop for some reason. – Phrogz May 22 '12 at 21:57
  • 1
    @Phrogz Ah, sorry. I meant "In Safari, indexOf is the fastest. Add it to your list of browsers where indexOf is the fastest" – vcsjones May 22 '12 at 21:57
  • 3
    @Phrogz and vcsjones: you guys used `str[i]` like if it where 100% crossbrowser compatibility... `charAt()` much more reliable – ajax333221 May 22 '12 at 22:15
  • @ajax333221 All the browsers I care about support that. What browser are you hoping to support? – Phrogz May 22 '12 at 22:17
  • @Phrogz interesting, I tested it on all my browsers and it worked, I must have updated the one that failed... But I clearly remember this is not standard and might fail. edit: here, read http://stackoverflow.com/a/5943760/908879 – ajax333221 May 22 '12 at 22:23
  • Thanks I'm still a junior dev with little experience so I've not heard of jsPerf. Hmm nice little tool. – Ageis May 22 '12 at 22:26
  • @ajax333221 Bracket notation doesn't work in IE for *string literals*, like `var foo = "hello world"[0];`. It works fine in all browsers on variables of strings. – vcsjones May 22 '12 at 22:27
  • +1 for ajax333221's comment. Doesn't matter if str[i] _happens_ to work in whatever browser you're trying it in, fact is bracket notation access of strings is non-compliant with ecmascript 3 - it is an ecmascript 5 feature ([char access](http://www.google.ca/url?url=https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/String%23Character_access&rct=j&q=ecmascript+string+bracket+access&usg=AFQjCNG77YLb5ynbTHvoIzi8xNJoMRdokQ&sa=X&ei=h4q8T8uDE-qciQLBvaDNDQ&ved=0CFIQygQwAA) ) – jordancpaul May 23 '12 at 07:01
  • intersting I found indexof to faster on firefox running on ubuntu. However it's slower on windows. hmm – Ageis May 24 '12 at 10:09
  • 1
    This is how you should really test it, isolating the exact thing you are measuring: http://jsperf.com/10710345/3 – vsync Jan 16 '14 at 13:26
  • You "brute force" is also called a linear search – Ryan Aug 01 '14 at 17:11
  • function findAllIndices(needle, haystack) { var indices = []; for (i = 0; i < haystack.length; i++) { if ((haystack.substr(i, needle.length)) === needle) { indices.push(i); } } if (indices.length) { return (indices); } else { return (-1) } } – Albert James Teddy Jul 22 '16 at 15:34
  • That might be true for "scissors" but what if we are looking at something like 10,000 characters, like parsing a file. I would think that using the built in method would be faster. But i can run some test to find out. – Dustin Poissant May 24 '20 at 02:15
34

Using the native String.prototype.indexOf method to most efficiently find each offset.

function locations(substring,string){
  var a=[],i=-1;
  while((i=string.indexOf(substring,i+1)) >= 0) a.push(i);
  return a;
}

console.log(locations("s","scissors"));
//-> [0, 3, 4, 7]

This is a micro-optimization, however. For a simple and terse loop that will be fast enough:

// Produces the indices in reverse order; throw on a .reverse() if you want
for (var a=[],i=str.length;i--;) if (str[i]=="s") a.push(i);    

In fact, a native loop is faster on chrome that using indexOf!

Graph of performance results from the link

Phrogz
  • 296,393
  • 112
  • 651
  • 745
15

benchmark

When i benchmarked everything it seemed like regular expressions performed the best, so i came up with this

function indexesOf(string, regex) {
    var match,
        indexes = {};

    regex = new RegExp(regex);

    while (match = regex.exec(string)) {
        if (!indexes[match[0]]) indexes[match[0]] = [];
        indexes[match[0]].push(match.index);
    }

    return indexes;
}

you can do this

indexesOf('ssssss', /s/g);

which would return

{s: [0,1,2,3,4,5]}

i needed a very fast way to match multiple characters against large amounts of text so for example you could do this

indexesOf('dddddssssss', /s|d/g);

and you would get this

{d:[0,1,2,3,4], s:[5,6,7,8,9,10]}

this way you can get all the indexes of your matches in one go

Chad Cache
  • 9,668
  • 3
  • 56
  • 48
  • According to the benchmark I ran on chrome, vcsjones is still the fastest http://jsperf.com/javascript-string-character-finder/6 – JackDev Nov 21 '15 at 11:01
  • Yes on a very small string, but look what happens when you increase the haystack: http://jsperf.com/javascript-string-character-finder/7 . Theres no competition, In my scenario i needed something that was performant in matching against large sets of text not a tiny string. – Chad Cache Nov 22 '15 at 00:01
  • 2
    Ah ok fair point :), perhaps you should add that graph to your answer to make it clear why your solution is in fact the most efficient. – JackDev Nov 22 '15 at 16:04
13
function charPos(str, char) {
  return str
         .split("")
         .map(function (c, i) { if (c == char) return i; })
         .filter(function (v) { return v >= 0; });
}

charPos("scissors", "s");  // [0, 3, 4, 7]

Note that JavaScript counts from 0. Add +1 to i, if you must.

Tomalak
  • 332,285
  • 67
  • 532
  • 628
  • 4
    +1 for functional fun, even if it's wicked inefficient in contrast to what the OP was asking for. – Phrogz May 22 '12 at 21:28
  • @jezternz Probably not the *fastest* one, though. -- Actually, it's very slow. http://jsperf.com/javascript-string-character-finder – Tomalak May 22 '12 at 21:42
10

In modern browsers matchAll do the job :

const string = "scissors";
const matches = [...string.matchAll(/s/g)];

You can get the values in several ways. For example :

const indexes = matches.map(match => match.index);
Joulss
  • 1,040
  • 1
  • 14
  • 26
7

More functional fun, and also more general: This finds the starting indexes of a substring of any length in a string

const length = (x) => x.length
const sum = (a, b) => a+b

const indexesOf = (substr) => ({
  in: (str) => (
    str
    .split(substr)
    .slice(0, -1)
    .map(length)
    .map((_, i, lengths) => (
      lengths
      .slice(0, i+1)
      .reduce(sum, i*substr.length)
    ))
  )  
});

console.log(indexesOf('s').in('scissors')); // [0,3,4,7]

console.log(indexesOf('and').in('a and b and c')); // [2,8]
davnicwil
  • 28,487
  • 16
  • 107
  • 123
5
indices = (c, s) => s
          .split('')
          .reduce((a, e, i) => e === c ? a.concat(i) : a, []);

indices('?', 'a?g??'); // [1, 3, 4]
tpdi
  • 34,554
  • 11
  • 80
  • 120
2

Here is a short solution using a function expression (with ES6 arrow functions). The function accepts a string and the character to find as parameters. It splits the string into an array of characters and uses a reduce function to accumulate and return the matching indices as an array.

const findIndices = (str, char) =>
  str.split('').reduce((indices, letter, index) => {
    letter === char && indices.push(index);
    return indices;
  }, [])

Testing:

findIndices("Hello There!", "e");
// → [1, 8, 10]

findIndices("Looking for new letters!", "o");
// → [1, 2, 9]

Here is a compact (one-line) version:

const findIndices = (str, char) => str.split('').reduce( (indices, letter, index) => { letter === char && indices.push(index); return indices }, [] );
Richard
  • 101
  • 4
1

using while loop

let indices = [];
let array = "scissors".split('');
let element = 's';
    
let idx = array.indexOf(element);
    
while (idx !== -1) {
   indices.push(idx+1);
   idx = array.indexOf(element, idx + 1);
}
console.log(indices);
Mamunur Rashid
  • 1,095
  • 17
  • 28
1

Another alternative could be using flatMap.

var getIndices = (s, t) => {
  return [...s].flatMap((char, i) => (char === t ? i + 1 : []));
};

console.log(getIndices('scissors', 's'));
console.log(getIndices('kaios', '0'));
Penny Liu
  • 15,447
  • 5
  • 79
  • 98
0

I loved the question and thought to write my answer by using the reduce() method defined on arrays.

function getIndices(text, delimiter='.') {
    let indices = [];
    let combined;

    text.split(delimiter)
        .slice(0, -1)
        .reduce((a, b) => { 
            if(a == '') {
                combined = a + b;
            } else { 
                combined = a + delimiter + b;
            } 

            indices.push(combined.length);
            return combined; // Uncommenting this will lead to syntactical errors
        }, '');

    return indices;
}


let indices = getIndices(`Ab+Cd+Pk+Djb+Nice+One`, '+');
let indices2 = getIndices(`Program.can.be.done.in.2.ways`); // Here default delimiter will be taken as `.`

console.log(indices);  // [ 2, 5, 8, 12, 17 ]
console.log(indices2); // [ 7, 11, 14, 19, 22, 24 ]

// To get output as expected (comma separated)
console.log(`${indices}`);  // 2,5,8,12,17
console.log(`${indices2}`); // 7,11,14,19,22,24
hygull
  • 8,464
  • 2
  • 43
  • 52
0

Just for further solution, here is my solution: you can find character's indexes which exist in a string:

findIndex(str, char) {
    const strLength = str.length;
    const indexes = [];
    let newStr = str;

    while (newStr && newStr.indexOf(char) > -1) {
      indexes.push(newStr.indexOf(char) + strLength- newStr.length);
      newStr = newStr.substring(newStr.indexOf(char) + 1);
    }

    return indexes;
  }

findIndex('scissors', 's'); // [0, 3, 4, 7]
findIndex('Find "s" in this sentence', 's'); // [6, 15, 17]

alireza
  • 11
  • 2
  • While this code may solve the question, [including an explanation](https://meta.stackexchange.com/q/114762) of how and why this solves the problem would really help to improve the quality of your post, and probably result in more up-votes. Remember that you are answering the question for readers in the future, not just the person asking now. Please [edit] your answer to add explanations and give an indication of what limitations and assumptions apply. – Brian61354270 Apr 12 '20 at 19:05
0
function countClaps(str) {
     const re = new RegExp(/C/g);

    // matching the pattern
    const count = str.match(re).length;

    return count;
}
//countClaps();

console.log(countClaps("CCClaClClap!Clap!ClClClap!"));
Alexander
  • 16,091
  • 5
  • 13
  • 29
S Gabale
  • 1
  • 3
  • 1
    Please don't post only code as answer, but also provide an explanation what your code does and how it solves the problem of the question. Answers with an explanation are usually more helpful and of better quality, and are more likely to attract upvotes. – Mark Rotteveel May 04 '22 at 10:21
  • 1
    The question is not about counting anything. – General Grievance May 04 '22 at 13:23
  • If you have a new question, please ask it by clicking the [Ask Question](https://stackoverflow.com/questions/ask) button. Include a link to this question if it helps provide context. - [From Review](/review/late-answers/31681667) – cachius May 08 '22 at 15:06
0

Using recursion function:

let indcies=[];
     function findAllIndecies(str,substr,indexToStart=0) {
            if (indexToStart<str.length) {
               var index= str.indexOf(substr,indexToStart)
               indcies.push(index)
                findAllIndecies(str,substr,index+1)
            }
        }
findAllIndecies("scissors","s")
Sarl sbeer
  • 41
  • 5
-2

You could probably use the match() function of javascript as well. You can create a regular expression and then pass it as a parameter to the match().

stringName.match(/s/g);

This should return you an array of all the occurrence of the the letter 's'.

Yash Kalwani
  • 443
  • 4
  • 11