Ottieni l'indice dell'ennesima occorrenza di una stringa?


100

A meno che non mi manchi un ovvio metodo integrato, qual è il modo più rapido per ottenere il file n ° occorrenza di una stringa all'interno di una stringa?

Mi rendo conto che potrei eseguire il ciclo del metodo IndexOf aggiornando il suo indice di inizio a ogni iterazione del ciclo. Ma farlo in questo modo mi sembra uno spreco.


Vorrei usare espressioni regolari per questo, quindi devi trovare un modo ottimale per abbinare la stringa all'interno della stringa. Questo in uno dei bellissimi DSL che dovremmo usare tutti quando possibile. Un esempio in VB.net il codice è quasi lo stesso in C #.
bovium

2
Metterei un bel po 'di soldi sulla versione delle espressioni regolari che è significativamente più difficile da ottenere rispetto a "continuare a eseguire il ciclo e fare semplici String.IndexOf". Le espressioni regolari hanno il loro posto, ma non dovrebbero essere usate quando esistono alternative più semplici.
Jon Skeet

Risposte:


52

Questo è fondamentalmente quello che devi fare o almeno è la soluzione più semplice. Tutto ciò che "sprecheresti" è il costo di n invocazioni di metodi - in realtà non controllerai due volte i casi, se ci pensi. (IndexOf tornerà non appena trova la corrispondenza e continuerai da dove era stato interrotto.)


2
Suppongo che tu abbia ragione, sembra che dovrebbe esserci un metodo integrato, sono sicuro che è un evento comune.
PeteT

4
Veramente? Non ricordo di averlo mai fatto in circa 13 anni di sviluppo Java e C #. Ciò non significa che non abbia mai dovuto farlo, ma non abbastanza spesso da ricordare.
Jon Skeet

Parlando di Java, abbiamo StringUtils.ordinalIndexOf(). C # con tutte le funzionalità di Linq e altre meravigliose, semplicemente non ha un supporto integrato per questo. E sì, è molto importante avere il suo supporto se hai a che fare con parser e tokenizer.
Annie

3
@Annie: dici "abbiamo" - intendi in Apache Commons? Se è così, puoi scrivere la tua libreria di terze parti per .NET con la stessa facilità con cui puoi per Java ... quindi non è come se fosse qualcosa che la libreria standard Java ha che .NET non ha. E ovviamente in C # puoi aggiungerlo come metodo di estensione su string:)
Jon Skeet

108

Potresti davvero usare l'espressione regolare /((s).*?){n}/per cercare l'occorrenza n-esima della sottostringa s.

In C # potrebbe essere simile a questo:

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: ho aggiunto Regex.Escapealla soluzione originale per consentire la ricerca di caratteri che hanno un significato speciale per il motore regex.


2
dovresti sfuggire al value? Nel mio caso ero alla ricerca di un punto msdn.microsoft.com/en-us/library/...
russau

3
Questo Regex non funziona se la stringa di destinazione contiene interruzioni di riga. Potresti aggiustarlo? Grazie.
Ignacio Soler Garcia

Sembra bloccarsi se non c'è un ennesimo match. Avevo bisogno di limitare un valore separato da virgole a 1000 valori e questo si bloccava quando il csv aveva meno. Quindi @Yogesh - probabilmente non è un'ottima risposta accettata così com'è. ;) Usando una variante di questa risposta (c'è una stringa in versione stringa qui ) e cambiato il ciclo per fermarsi invece all'ennesimo conteggio .
ruffin

Tentando di cercare su \, il valore passato è "\\" e la stringa di corrispondenza ha questo aspetto prima della funzione regex.match: ((). *?) {2}. Ottengo questo errore: analisi di "((). *?) {2}" - Non abbastanza). Qual è il formato corretto per cercare le barre rovesciate senza errori?
RichieMN

3
Scusate ma una piccola critica: le soluzioni regex non sono ottimali, perché poi devo imparare di nuovo le espressioni regolari per l'ennesima volta. Il codice è essenzialmente più difficile da leggere quando vengono utilizzate le regex.
Mark Rogers

19

Questo è fondamentalmente quello che devi fare o almeno è la soluzione più semplice. Tutto ciò che "sprecheresti" è il costo di n invocazioni di metodi - in realtà non controllerai due volte i casi, se ci pensi. (IndexOf tornerà non appena trova la corrispondenza e continuerai da dove era stato interrotto.)

Ecco l'implementazione ricorsiva ( dell'idea sopra ) come metodo di estensione, che imita il formato del metodo (i) framework:

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

Inoltre, ecco alcuni unit test (MBUnit) che potrebbero aiutarti (per dimostrare che è corretto):

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

Ho aggiornato la mia formattazione e i casi di test in base all'ottimo feedback di Weston (grazie 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;
}

o in C # con metodi di estensione

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

5
Se non sbaglio, questo metodo fallisce se la stringa da abbinare inizia dalla posizione 0, che può essere corretta impostando indexinizialmente a -1.
Peter Majeed,

1
Potresti anche voler verificare la presenza di stringhe nulle o vuote e corrispondere o verrà generata, ma questa è una decisione di progettazione.

Grazie @PeterMajeed - se "BOB".IndexOf("B")restituisce 0, anche questa funzione dovrebbe essereIndexOfOccurence("BOB", "B", 1)
PeterX

2
La tua è probabilmente la soluzione definitiva poiché ha sia una funzione di estensione che evita le regex e la ricorsione, che rendono il codice meno leggibile.
Mark Rogers

@tdyen In effetti, Code Analysis emetterà "CA1062: Validate arguments of public methods" se IndexOfOccurencenon controlla se lo sè null. E String.IndexOf (String, Int32) lancerà ArgumentNullExceptionif matchis null.
DavidRR

1

Forse sarebbe anche bello lavorare con il String.Split()Metodo e controllare se l'occorrenza richiesta è nell'array, se non è necessario l'indice, ma il valore nell'indice


1

Dopo alcuni benchmark, questa sembra essere la soluzione più semplice ed efficiente

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

1

System.ValueTuple ftw:

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

scrivere una funzione da quello è compito


0

La risposta di Tod's può essere un po 'semplificata.

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

Produzione

1
3
5
-1

0

O qualcosa di simile con il ciclo 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;
    }

-4

Questo potrebbe farlo:

Console.WriteLine(str.IndexOf((@"\")+2)+1);

2
Non vedo come funzionerebbe. Potresti includere una breve spiegazione di ciò che fa?
Bob Kaufman
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.