92

I need help with creating a C# method that returns the index of the Nth occurrence of a character in a string.

For instance, the 3rd occurrence of the character 't' in the string "dtststxtu" is 5.
(Note that the string has 4 ts.)

ANeves
  • 6,219
  • 3
  • 39
  • 63
jozi
  • 2,833
  • 6
  • 28
  • 41
  • What do you have to work with so far? – Anthony Forloney Apr 03 '10 at 15:48
  • 3
    I have edited your answer to more clearly convey what you want. Hopefully you will get some answers that fit the question. Not being fluent in english is not a problem on Stack Overflow, you can always just add a line asking someone more fluent to edit your question and clean it up, but you must yourself strive to provide some examples in the question so that people understand what you need. – Lasse V. Karlsen Apr 03 '10 at 16:04

20 Answers20

106
public int GetNthIndex(string s, char t, int n)
{
    int count = 0;
    for (int i = 0; i < s.Length; i++)
    {
        if (s[i] == t)
        {
            count++;
            if (count == n)
            {
                return i;
            }
        }
    }
    return -1;
}

That could be made a lot cleaner, and there are no checks on the input.

AGuyCalledGerald
  • 7,882
  • 17
  • 73
  • 120
Mike Two
  • 44,935
  • 9
  • 80
  • 96
  • 7
    Great approach. Nice and clean, easy to read, easy to maintain, and excellent performance. – Mike Feb 22 '14 at 15:57
  • 1
    love for loops like these, not only they give excellent performance, but you cant go wrong with them since everything is crystal clear and right in front of your eyes. You write a linq and some developer puts it in a loop not understanding the cost and everyone keeps wondering where is the performance bottleneck. – user734028 Jan 08 '18 at 06:18
22

There is a minor bug in previous solution.

Here is some updated code:

s.TakeWhile(c => (n -= (c == t ? 1 : 0)) > 0).Count();
dx_over_dt
  • 13,240
  • 17
  • 54
  • 102
shalin shah
  • 221
  • 2
  • 2
15

Here's another LINQ solution:

string input = "dtststx";
char searchChar = 't';
int occurrencePosition = 3; // third occurrence of the char
var result = input.Select((c, i) => new { Char = c, Index = i })
                  .Where(item => item.Char == searchChar)
                  .Skip(occurrencePosition - 1)
                  .FirstOrDefault();

if (result != null)
{
    Console.WriteLine("Position {0} of '{1}' occurs at index: {2}",
                        occurrencePosition, searchChar, result.Index);
}
else
{
    Console.WriteLine("Position {0} of '{1}' not found!",
                        occurrencePosition, searchChar);
}

Just for fun, here's a Regex solution. I saw some people initially used Regex to count, but when the question changed no updates were made. Here is how it can be done with Regex - again, just for fun. The traditional approach is best for simplicity.

string input = "dtststx";
char searchChar = 't';
int occurrencePosition = 3; // third occurrence of the char

Match match = Regex.Matches(input, Regex.Escape(searchChar.ToString()))
                   .Cast<Match>()
                   .Skip(occurrencePosition - 1)
                   .FirstOrDefault();

if (match != null)
    Console.WriteLine("Index: " + match.Index);
else
    Console.WriteLine("Match not found!");
Ahmad Mageed
  • 94,561
  • 19
  • 163
  • 174
9

Here is a recursive implementation - as an extension method, mimicing the format of the framework method(s):

public static int IndexOfNth(
    this string input, string value, int startIndex, int nth)
{
    if (nth < 1)
        throw new NotSupportedException("Param 'nth' must be greater than 0!");
    if (nth == 1)
        return input.IndexOf(value, startIndex);

    return input.IndexOfNth(value, input.IndexOf(value, startIndex) + 1, --nth);
}

Also, here are some (MBUnit) unit tests that might help you (to prove it is correct):

[Test]
public void TestIndexOfNthWorksForNth1()
{
    const string input = "foo<br />bar<br />baz<br />";
    Assert.AreEqual(3, input.IndexOfNth("<br />", 0, 1));
}

[Test]
public void TestIndexOfNthWorksForNth2()
{
    const string input = "foo<br />whatthedeuce<br />kthxbai<br />";
    Assert.AreEqual(21, input.IndexOfNth("<br />", 0, 2));
}

[Test]
public void TestIndexOfNthWorksForNth3()
{
    const string input = "foo<br />whatthedeuce<br />kthxbai<br />";
    Assert.AreEqual(34, input.IndexOfNth("<br />", 0, 3));
}
Marc Cals
  • 2,963
  • 4
  • 30
  • 48
Tod Thomson
  • 4,773
  • 2
  • 33
  • 33
8

Update: Index of Nth occurance one-liner:

int NthOccurence(string s, char t, int n)
{
    s.TakeWhile(c => n - (c == t)?1:0 > 0).Count();
}

Use these at your own risk. This looks like homework, so I left a few bugs in there for your to find:

int CountChars(string s, char t)
{
   int count = 0;
   foreach (char c in s)
      if (s.Equals(t)) count ++;
   return count;
}

.

int CountChars(string s, char t)
{
     return s.Length - s.Replace(t.ToString(), "").Length;
}

.

int CountChars(string s, char t)
{
    Regex r = new Regex("[\\" + t + "]");
    return r.Match(s).Count;
}
Joel Coehoorn
  • 399,467
  • 113
  • 570
  • 794
  • 4
    Your one-liner example doesn't work because the value of n is never changed. – Dave Neeley Jun 03 '11 at 18:17
  • 2
    Nice solution, though this isn't a true "one-liner" as a variable needs to be defined out of scope of the lambda. s.TakeWhile(c => ((n -= (c == 't')) ? 1 : 0) > 0).Count(); – nullable Oct 26 '11 at 20:15
  • 14
    −1, "so I left a few bugs in there for your to find" – Zanon Jan 09 '18 at 13:40
7

ranomore correctly commented that Joel Coehoorn's one-liner doesn't work.

Here is a two-liner that does work, a string extension method that returns the 0-based index of the nth occurrence of a character, or -1 if no nth occurrence exists:

public static class StringExtensions
{
    public static int NthIndexOf(this string s, char c, int n)
    {
        var takeCount = s.TakeWhile(x => (n -= (x == c ? 1 : 0)) > 0).Count();
        return takeCount == s.Length ? -1 : takeCount;
    }
}
Wayne Maurer
  • 12,333
  • 4
  • 33
  • 43
4

Here is a fun way to do it

     int i = 0;
     string s="asdasdasd";
     int n = 3;
     s.Where(b => (b == 'd') && (i++ == n));
     return i;
Dested
  • 6,294
  • 12
  • 51
  • 73
4

I add another answer that run pretty fast compared to others methods

private static int IndexOfNth(string str, char c, int nth, int startPosition = 0)
{
    int index = str.IndexOf(c, startPosition);
    if (index >= 0 && nth > 1)
    {
        return  IndexOfNth(str, c, nth - 1, index + 1);
    }

    return index;
}
Marc Cals
  • 2,963
  • 4
  • 30
  • 48
4

Here's another, maybe simpler implementation of string IndexOfNth() with strings implementation.

Here's the string match version:

public static int IndexOfNth(this string source, string matchString, 
                             int charInstance, 
                             StringComparison stringComparison = StringComparison.CurrentCulture)
{
    if (string.IsNullOrEmpty(source))
        return -1;

    int lastPos = 0;
    int count = 0;

    while (count < charInstance )
    {
        var len = source.Length - lastPos;
        lastPos = source.IndexOf(matchString, lastPos,len,stringComparison);
        if (lastPos == -1)
            break;

        count++;
        if (count == charInstance)
            return lastPos;

        lastPos += matchString.Length;
    }
    return -1;
}

and the char match version:

public static int IndexOfNth(string source, char matchChar, int charInstance)        
{
    if (string.IsNullOrEmpty(source))
        return -1;

    if (charInstance < 1)
        return -1;

    int count = 0;
    for (int i = 0; i < source.Length; i++)
    {
        if (source[i] == matchChar)
        {
            count++;
            if (count == charInstance)                 
                return i;                 
        }
    }
    return -1;
}

I think for such a low level implementation you'd want to stay away from using LINQ, RegEx or recursion to reduce overhead.

Rick Strahl
  • 17,302
  • 14
  • 89
  • 134
3
public int GetNthOccurrenceOfChar(string s, char c, int occ)
{
    return String.Join(c.ToString(), s.Split(new char[] { c }, StringSplitOptions.None).Take(occ)).Length;
}
AGuyCalledGerald
  • 7,882
  • 17
  • 73
  • 120
3

Joel's answer is good (and I upvoted it). Here is a LINQ-based solution:

yourString.Where(c => c == 't').Count();
Andrew Hare
  • 344,730
  • 71
  • 640
  • 635
  • 2
    @Andrew - you can shorten this by skipping the `Where` and passing the predicate to the `Count` method. Not that there is anything wrong with the way it is. – Mike Two Apr 03 '10 at 16:04
  • 11
    Won't this just find how many occurrences of a character there are rather than the index of the nth one? – dx_over_dt Jun 26 '15 at 16:21
3
string result = "i am 'bansal.vks@gmail.com'"; // string

int in1 = result.IndexOf('\''); // get the index of first quote

int in2 = result.IndexOf('\'', in1 + 1); // get the index of second

string quoted_text = result.Substring(in1 + 1, in2 - in1); // get the string between quotes
Eugene
  • 1,865
  • 3
  • 21
  • 24
Vikas Bansal
  • 10,662
  • 14
  • 58
  • 100
2

Since the built-in IndexOf function is already optimized for searching a character within a string, an even faster version would be (as extension method):

public static int NthIndexOf(this string input, char value, int n)
{
    if (n <= 0) throw new ArgumentOutOfRangeException("n", n, "n is less than zero.");

    int i = -1;
    do
    {
        i = input.IndexOf(value, i + 1);
        n--;
    }
    while (i != -1 && n > 0);

    return i;
}

Or to search from the end of the string using LastIndexOf:

public static int NthLastIndexOf(this string input, char value, int n)
{
    if (n <= 0) throw new ArgumentOutOfRangeException("n", n, "n is less than zero.");

    int i = input.Length;
    do
    {
        i = input.LastIndexOf(value, i - 1);
        n--;
    }
    while (i != -1 && n > 0);

    return i;
}

Searching for a string instead of a character is as simple as changing the parameter type from char to string and optionally add an overload to specify the StringComparison.

Ronald
  • 1,795
  • 14
  • 17
2

if your interested you can also create string extension methods like so:

     public static int Search(this string yourString, string yourMarker, int yourInst = 1, bool caseSensitive = true)
    {
        //returns the placement of a string in another string
        int num = 0;
        int currentInst = 0;
        //if optional argument, case sensitive is false convert string and marker to lowercase
        if (!caseSensitive) { yourString = yourString.ToLower(); yourMarker = yourMarker.ToLower(); }
        int myReturnValue = -1; //if nothing is found the returned integer is negative 1
        while ((num + yourMarker.Length) <= yourString.Length)
        {
            string testString = yourString.Substring(num, yourMarker.Length);

            if (testString == yourMarker)
            {
                currentInst++;
                if (currentInst == yourInst)
                {
                    myReturnValue = num;
                    break;
                }
            }
            num++;
        }           
       return myReturnValue;
    }

   public static int Search(this string yourString, char yourMarker, int yourInst = 1, bool caseSensitive = true)
    {
        //returns the placement of a string in another string
        int num = 0;
        int currentInst = 0;
        var charArray = yourString.ToArray<char>();
        int myReturnValue = -1;
        if (!caseSensitive)
        {
            yourString = yourString.ToLower();
            yourMarker = Char.ToLower(yourMarker);
        }
        while (num <= charArray.Length)
        {                
            if (charArray[num] == yourMarker)
            {
                currentInst++;
                if (currentInst == yourInst)
                {
                    myReturnValue = num;
                    break;
                }
            }
            num++;
        }
        return myReturnValue;
    }
Matt Farguson
  • 325
  • 2
  • 6
2
public static int IndexOfAny(this string str, string[] values, int startIndex, out string selectedItem)
    {
        int first = -1;
        selectedItem = null;
        foreach (string item in values)
        {
            int i = str.IndexOf(item, startIndex, StringComparison.OrdinalIgnoreCase);
            if (i >= 0)
            {
                if (first > 0)
                {
                    if (i < first)
                    {
                        first = i;
                        selectedItem = item;
                    }
                }
                else
                {
                    first = i;
                    selectedItem = item;
                }
            }
        }
        return first;
    }
2

you can do this work with Regular Expressions.

        string input = "dtststx";
        char searching_char = 't';
        int output = Regex.Matches(input, "["+ searching_char +"]")[2].Index;

best regard.

EMAI
  • 694
  • 6
  • 14
1

Hi all i have created two overload methods for finding nth occurrence of char and for text with less complexity without navigating through loop ,which increase performance of your application.

public static int NthIndexOf(string text, char searchChar, int nthindex)
{
   int index = -1;
   try
   {
      var takeCount = text.TakeWhile(x => (nthindex -= (x == searchChar ? 1 : 0)) > 0).Count();
      if (takeCount < text.Length) index = takeCount;
   }
   catch { }
   return index;
}
public static int NthIndexOf(string text, string searchText, int nthindex)
{
     int index = -1;
     try
     {
        Match m = Regex.Match(text, "((" + searchText + ").*?){" + nthindex + "}");
        if (m.Success) index = m.Groups[2].Captures[nthindex - 1].Index;
     }
     catch { }
     return index;
}
Hans Olsson
  • 54,199
  • 15
  • 94
  • 116
Suneel Gupta
  • 497
  • 5
  • 7
1

Another RegEx-based solution (untested):

int NthIndexOf(string s, char t, int n) {
   if(n < 0) { throw new ArgumentException(); }
   if(n==1) { return s.IndexOf(t); }
   if(t=="") { return 0; }
   string et = RegEx.Escape(t);
   string pat = "(?<="
      + Microsoft.VisualBasic.StrDup(n-1, et + @"[.\n]*") + ")"
      + et;
   Match m = RegEx.Match(s, pat);
   return m.Success ? m.Index : -1;
}

This should be slightly more optimal than requiring RegEx to create a Matches collection, only to discard all but one match.

richardtallent
  • 34,724
  • 14
  • 83
  • 123
  • In response to the Matches collection comment (since that is what I had shown in my response): I suppose a more efficient approach would be to use a while loop checking for `match.Success` and get the `NextMatch` while incrementing a counter and breaking early when the `counter == index`. – Ahmad Mageed Apr 03 '10 at 18:17
1
    public static int FindOccuranceOf(this string str,char @char, int occurance)
    {
       var result = str.Select((x, y) => new { Letter = x, Index = y })
            .Where(letter => letter.Letter == @char).ToList();
       if (occurence > result.Count || occurance <= 0)
       {
           throw new IndexOutOfRangeException("occurance");
       }
       return result[occurance-1].Index ;
    }
Islam Yahiatene
  • 1,441
  • 14
  • 27
1

Marc Cals' LINQ Extended for generic.

   using System;
   using System.Collections.Generic;
   using System.Linq;

   namespace fNns
   {
       public class indexer<T> where T : IEquatable<T>
       {
           public T t { get; set; }
           public int index { get; set; }
       }
       public static class fN
       {
           public static indexer<T> findNth<T>(IEnumerable<T> tc, T t,
               int occurrencePosition) where T : IEquatable<T>
           {
               var result = tc.Select((ti, i) => new indexer<T> { t = ti, index = i })
                      .Where(item => item.t.Equals(t))
                      .Skip(occurrencePosition - 1)
                      .FirstOrDefault();
               return result;
           }
           public static indexer<T> findNthReverse<T>(IEnumerable<T> tc, T t,
       int occurrencePosition) where T : IEquatable<T>
           {
               var result = tc.Reverse<T>().Select((ti, i) => new indexer<T> {t = ti, index = i })
                      .Where(item => item.t.Equals(t))
                      .Skip(occurrencePosition - 1)
                      .FirstOrDefault();
               return result;
           }
       }
   }

Some tests.

   using System;
   using System.Collections.Generic;
   using NUnit.Framework;
   using Newtonsoft.Json;
   namespace FindNthNamespace.Tests
   {

       public class fNTests
       {
           [TestCase("pass", "dtststx", 't', 3, Result = "{\"t\":\"t\",\"index\":5}")]
           [TestCase("pass", new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 },
        0, 2, Result="{\"t\":0,\"index\":10}")]
           public string fNMethodTest<T>(string scenario, IEnumerable<T> tc, T t, int occurrencePosition) where T : IEquatable<T>
           {
               Console.WriteLine(scenario);
               return JsonConvert.SerializeObject(fNns.fN.findNth<T>(tc, t, occurrencePosition)).ToString();
           }

           [TestCase("pass", "dtststxx", 't', 3, Result = "{\"t\":\"t\",\"index\":6}")]
           [TestCase("pass", new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 },
        0, 2, Result = "{\"t\":0,\"index\":19}")]
           public string fNMethodTestReverse<T>(string scenario, IEnumerable<T> tc, T t, int occurrencePosition) where T : IEquatable<T>
           {
               Console.WriteLine(scenario);
               return JsonConvert.SerializeObject(fNns.fN.findNthReverse<T>(tc, t, occurrencePosition)).ToString();
           }


}

}

user2584621
  • 2,305
  • 2
  • 15
  • 9