查找字符串中字符的第 N 个匹配项

我需要帮助来创建一个 C # 方法,该方法返回字符串中第 N 个出现的字符的索引。

例如,字符串 "dtststxtu"中字符 't'的第三个匹配项是5。
(注意字符串有4个 ts。)

117220 次浏览

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's answer is good (and I upvoted it). Here is a LINQ-based solution:

yourString.Where(c => c == 't').Count();
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.

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!");

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;

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.

    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 ;
}

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));
}

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;
}
}

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.

There is a minor bug in previous solution.

Here is some updated code:

s.TakeWhile(c => (n -= (c == t ? 1 : 0)) > 0).Count();

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;
}

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.

public int GetNthOccurrenceOfChar(string s, char c, int occ)
{
return String.Join(c.ToString(), s.Split(new char[] { c }, StringSplitOptions.None).Take(occ)).Length;
}
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

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' 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();
}




}

}

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;
}

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.

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;
}