Obter o índice da enésima ocorrência de uma string?

100

A menos que esteja faltando um método óbvio integrado, qual é a maneira mais rápida de obter o n º ocorrência de uma cadeia dentro de uma string?

Percebo que poderia fazer um loop no método IndexOf atualizando seu índice inicial em cada iteração do loop. Mas fazer assim parece um desperdício para mim.

PeteT
fonte
Eu usaria expressões regulares para isso, então você teria a maneira ideal de combinar a string dentro da string. Isso é uma das lindas DSLs que todos devemos usar quando possível. Um exemplo em VB.net, o código é quase o mesmo em C #.
bovium,
2
Eu colocaria um bom dinheiro na versão de expressões regulares sendo significativamente mais difícil de acertar do que "continue fazendo loop e fazendo String.IndexOf simples". As expressões regulares têm seu lugar, mas não devem ser usadas quando existem alternativas mais simples.
Jon Skeet,

Respostas:

52

Isso é basicamente o que você precisa fazer - ou pelo menos, é a solução mais fácil. Tudo o que você estaria "desperdiçando" é o custo de n invocações de método - na verdade, você não verificará nenhum caso duas vezes, se pensar sobre isso. (IndexOf retornará assim que encontrar a correspondência, e você continuará de onde parou.)

Jon Skeet
fonte
2
Suponho que você esteja certo, parece que deveria haver um método embutido, tenho certeza que é uma ocorrência comum.
PeteT,
4
Realmente? Não me lembro de ter feito isso em cerca de 13 anos de desenvolvimento em Java e C #. Isso não significa que eu realmente nunca tive que fazer isso - mas apenas não com freqüência suficiente para lembrar.
Jon Skeet,
Falando em Java, temos StringUtils.ordinalIndexOf(). C # com todo o Linq e outros recursos maravilhosos, simplesmente não tem um suporte integrado para isso. E sim, é muito importante ter seu suporte se você estiver lidando com analisadores e tokenizadores.
Annie
3
@Annie: Você diz "nós temos" - você quer dizer no Apache Commons? Nesse caso, você pode escrever sua própria biblioteca de terceiros para .NET com a mesma facilidade com que pode para Java ... então não é como se isso fosse algo que a biblioteca padrão de Java tenha e que .NET não tenha. E, claro, em C # você pode adicioná-lo como um método de extensão em string:)
Jon Skeet
108

Você realmente poderia usar a expressão regular /((s).*?){n}/para pesquisar a n-ésima ocorrência de substring s.

Em C #, pode ser assim:

public static class StringExtender
{
    public static int NthIndexOf(this string target, string value, int n)
    {
        Match m = Regex.Match(target, "((" + Regex.Escape(value) + ").*?){" + n + "}");

        if (m.Success)
            return m.Groups[2].Captures[n - 1].Index;
        else
            return -1;
    }
}

Nota: Eu adicionei Regex.Escapea solução original para permitir a pesquisa de caracteres que têm um significado especial para o mecanismo regex.

Alexander Prokofyev
fonte
2
você deve escapar do value? No meu caso, eu estava procurando um ponto msdn.microsoft.com/en-us/library/…
russau
3
Este Regex não funciona se a string de destino contém quebras de linha. Você poderia consertar isso? Obrigado.
Ignacio Soler Garcia
Parece travar se não houver uma enésima correspondência. Eu precisava limitar um valor separado por vírgula a 1000 valores, e isso travou quando o csv tinha menos. Portanto, @Yogesh - provavelmente não é uma resposta muito aceita. ;) Usando uma variante desta resposta (há uma versão string para string aqui ) e alterou o loop para parar na enésima contagem .
ruffin de
Tentando pesquisar em \, o valor passado é "\\", e a string de correspondência se parece com isto antes da função regex.match: ((). *?) {2}. Eu recebo este erro: parsing "((). *?) {2}" - Não é suficiente) 's. Qual é o formato correto para procurar barras invertidas sem erros?
RichieMN de
3
Desculpe, mas uma crítica menor: as soluções de regex são abaixo do ideal, porque então eu tenho que reaprender regexs pela enésima vez. O código é essencialmente mais difícil de ler quando regexes são usadas.
Mark Rogers
19

Isso é basicamente o que você precisa fazer - ou pelo menos, é a solução mais fácil. Tudo o que você estaria "desperdiçando" é o custo de n invocações de método - na verdade, você não verificará nenhum caso duas vezes, se pensar sobre isso. (IndexOf retornará assim que encontrar a correspondência, e você continuará de onde parou.)

Aqui está a implementação recursiva (da ideia acima ) como um método de extensão, imitando o formato do (s) método (s) de estrutura:

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);
    var idx = input.IndexOf(value, startIndex);
    if (idx == -1)
        return -1;
    return input.IndexOfNth(value, idx + 1, --nth);
}

Além disso, aqui estão alguns testes de unidade (MBUnit) que podem ajudá-lo (para provar que está correto):

using System;
using MbUnit.Framework;

namespace IndexOfNthTest
{
    [TestFixture]
    public class Tests
    {
        //has 4 instances of the 
        private const string Input = "TestTest";
        private const string Token = "Test";

        /* Test for 0th index */

        [Test]
        public void TestZero()
        {
            Assert.Throws<NotSupportedException>(
                () => Input.IndexOfNth(Token, 0, 0));
        }

        /* Test the two standard cases (1st and 2nd) */

        [Test]
        public void TestFirst()
        {
            Assert.AreEqual(0, Input.IndexOfNth("Test", 0, 1));
        }

        [Test]
        public void TestSecond()
        {
            Assert.AreEqual(4, Input.IndexOfNth("Test", 0, 2));
        }

        /* Test the 'out of bounds' case */

        [Test]
        public void TestThird()
        {
            Assert.AreEqual(-1, Input.IndexOfNth("Test", 0, 3));
        }

        /* Test the offset case (in and out of bounds) */

        [Test]
        public void TestFirstWithOneOffset()
        {
            Assert.AreEqual(4, Input.IndexOfNth("Test", 4, 1));
        }

        [Test]
        public void TestFirstWithTwoOffsets()
        {
            Assert.AreEqual(-1, Input.IndexOfNth("Test", 8, 1));
        }
    }
}
Tod Thomson
fonte
Atualizei minha formatação e casos de teste com base no excelente feedback de Weston (obrigado Weston).
Tod Thomson
14
private int IndexOfOccurence(string s, string match, int occurence)
{
    int i = 1;
    int index = 0;

    while (i <= occurence && (index = s.IndexOf(match, index + 1)) != -1)
    {
        if (i == occurence)
            return index;

        i++;
    }

    return -1;
}

ou em C # com métodos de extensão

public static int IndexOfOccurence(this string s, string match, int occurence)
{
    int i = 1;
    int index = 0;

    while (i <= occurence && (index = s.IndexOf(match, index + 1)) != -1)
    {
        if (i == occurence)
            return index;

        i++;
    }

    return -1;
}
Schotime
fonte
5
Se não estou enganado, esse método falha se a string correspondente começar na posição 0, o que pode ser corrigido definindo indexinicialmente como -1.
Peter Majeed
1
Você também pode querer verificar se há strings nulas ou vazias se corresponderem ou ele irá lançar, mas isso é uma decisão de design.
Obrigado @PeterMajeed - se "BOB".IndexOf("B")retorna 0, então esta função deve serIndexOfOccurence("BOB", "B", 1)
PeterX
2
A sua é provavelmente a solução definitiva, pois tem uma função de extensão e evita regexs e recursão, que tornam o código menos legível.
Mark Rogers
@tdyen De fato, a Análise de Código emitirá "CA1062: Validar argumentos de métodos públicos" se IndexOfOccurencenão verificar se sé null. E String.IndexOf (String, Int32) irá lançar ArgumentNullExceptionse matchfor null.
DavidRR
1

Talvez também seja bom trabalhar com o String.Split()Método e verificar se a ocorrência solicitada está no array, se você não precisa do índice, mas do valor do índice

user3227623
fonte
1

Após alguns benchmarking, esta parece ser a solução mais simples e eficiente

public static int IndexOfNthSB(string input,
             char value, int startIndex, int nth)
        {
            if (nth < 1)
                throw new NotSupportedException("Param 'nth' must be greater than 0!");
            var nResult = 0;
            for (int i = startIndex; i < input.Length; i++)
            {
                if (input[i] == value)
                    nResult++;
                if (nResult == nth)
                    return i;
            }
            return -1;
        }
ShadowBeast
fonte
1

System.ValueTuple ftw:

var index = line.Select((x, i) => (x, i)).Where(x => x.Item1 == '"').ElementAt(5).Item2;

escrever uma função que é lição de casa

Matthias
fonte
0

A resposta de Tod pode ser um pouco simplificada.

using System;

static class MainClass {
    private static int IndexOfNth(this string target, string substring,
                                       int seqNr, int startIdx = 0)
    {
        if (seqNr < 1)
        {
            throw new IndexOutOfRangeException("Parameter 'nth' must be greater than 0.");
        }

        var idx = target.IndexOf(substring, startIdx);

        if (idx < 0 || seqNr == 1) { return idx; }

        return target.IndexOfNth(substring, --seqNr, ++idx); // skip
    }

    static void Main () {
        Console.WriteLine ("abcbcbcd".IndexOfNth("bc", 1));
        Console.WriteLine ("abcbcbcd".IndexOfNth("bc", 2));
        Console.WriteLine ("abcbcbcd".IndexOfNth("bc", 3));
        Console.WriteLine ("abcbcbcd".IndexOfNth("bc", 4));
    }
}

Resultado

1
3
5
-1
seron
fonte
0

Ou algo assim com o loop do while

 private static int OrdinalIndexOf(string str, string substr, int n)
    {
        int pos = -1;
        do
        {
            pos = str.IndexOf(substr, pos + 1);
        } while (n-- > 0 && pos != -1);
        return pos;
    }
xFreeD
fonte
-4

Isso pode resolver:

Console.WriteLine(str.IndexOf((@"\")+2)+1);
Sameer Shaikh
fonte
2
Não vejo como isso funcionaria. Você poderia incluir uma breve explicação do que isso faz?
Bob Kaufman