Obtenir l'index de la nième occurrence d'une chaîne?

100

À moins que je ne manque une méthode intégrée évidente, quel est le moyen le plus rapide d'obtenir la n ième occurrence d'une chaîne dans une chaîne?

Je me rends compte que je pourrais boucler la méthode IndexOf en mettant à jour son index de démarrage à chaque itération de la boucle. Mais faire cela de cette façon me semble un gaspillage.

PeteT
la source
J'utiliserais des expressions régulières pour cela, alors vous devez trouver un moyen optimal de faire correspondre la chaîne dans la chaîne. Ceci dans l'un des magnifiques DSL que nous devrions tous utiliser lorsque cela est possible. Un exemple dans VB.net, le code est presque le même en C #.
bovium
2
Je placerais beaucoup d'argent sur la version d'expressions régulières étant beaucoup plus difficile à obtenir que "continuez à boucler et à faire de simples String.IndexOf". Les expressions régulières ont leur place, mais ne doivent pas être utilisées lorsqu'il existe des alternatives plus simples.
Jon Skeet

Réponses:

52

C'est essentiellement ce que vous devez faire - ou du moins, c'est la solution la plus simple. Tout ce que vous «gaspilleriez», c'est le coût des invocations de méthodes n - vous ne vérifierez aucun cas deux fois, si vous y réfléchissez. (IndexOf reviendra dès qu'il trouvera la correspondance, et vous continuerez à partir de là où il s'est arrêté.)

Jon Skeet
la source
2
Je suppose que vous avez raison, il semble qu'il devrait y avoir une méthode intégrée, je suis sûr que c'est une occurrence commune.
PeteT
4
Vraiment? Je ne me souviens pas avoir jamais eu à le faire en environ 13 ans de développement Java et C #. Cela ne veut pas dire que je n'ai vraiment jamais eu à le faire - mais pas assez souvent pour m'en souvenir.
Jon Skeet
En parlant de Java, nous l'avons fait StringUtils.ordinalIndexOf(). C # avec tous les Linq et d'autres fonctionnalités merveilleuses, n'a tout simplement pas de support intégré pour cela. Et oui, il est très impératif d'avoir son support si vous avez affaire à des analyseurs et des tokenizers.
Annie
3
@Annie: Vous dites "nous avons" - voulez-vous dire dans Apache Commons? Si tel est le cas, vous pouvez écrire votre propre bibliothèque tierce pour .NET aussi facilement que vous le pouvez pour Java ... donc ce n'est pas comme si c'était quelque chose que la bibliothèque standard Java a que .NET n'a pas. Et bien sûr, en C #, vous pouvez l'ajouter comme méthode d'extension sur string:)
Jon Skeet
108

Vous pouvez vraiment utiliser l'expression régulière /((s).*?){n}/pour rechercher la n-ième occurrence de sous-chaîne s.

En C #, cela pourrait ressembler à ceci:

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

Remarque: j'ai ajouté Regex.Escapeà la solution originale pour permettre la recherche de caractères qui ont une signification particulière pour le moteur regex.

Alexandre Prokofyev
la source
2
devriez-vous échapper à la value? Dans mon cas, je cherchais un point msdn.microsoft.com/en-us/library
...
3
Ce Regex ne fonctionne pas si la chaîne cible contient des sauts de ligne. Pourriez-vous le réparer? Merci.
Ignacio Soler Garcia
Semble se verrouiller s'il n'y a pas de Nième correspondance. J'avais besoin de limiter une valeur séparée par des virgules à 1000 valeurs, et cela s'est bloqué lorsque le csv en avait moins. Donc @Yogesh - probablement pas une bonne réponse acceptée telle quelle. ;) En utilisant une variante de cette réponse (il y a une version chaîne à chaîne ici ) et a changé la boucle pour s'arrêter au nième nombre à la place.
ruffin
En essayant de rechercher sur \, la valeur transmise est "\\", et la chaîne de correspondance ressemble à ceci avant la fonction regex.match: ((). *?) {2}. J'obtiens cette erreur: analyse de "((). *?) {2}" - Pas assez). Quel est le format correct pour rechercher des barres obliques inverses sans erreur?
RichieMN
3
Désolé mais une critique mineure: les solutions regex sont sous-optimales, car alors je dois réapprendre les regex pour la nième fois. Le code est essentiellement plus difficile à lire lorsque des expressions régulières sont utilisées.
Mark Rogers
19

C'est essentiellement ce que vous devez faire - ou du moins, c'est la solution la plus simple. Tout ce que vous «gaspilleriez», c'est le coût des invocations de méthodes n - vous ne vérifierez aucun cas deux fois, si vous y réfléchissez. (IndexOf reviendra dès qu'il trouvera la correspondance, et vous continuerez à partir de là où il s'est arrêté.)

Voici l'implémentation récursive (de l' idée ci-dessus ) en tant que méthode d'extension, imitant le format de la ou des méthodes du 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);
}

Aussi, voici quelques tests unitaires (MBUnit) qui pourraient vous aider (pour prouver qu'il est correct):

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
la source
J'ai mis à jour mon formatage et mes cas de test en fonction des excellents commentaires de Weston (merci 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 en C # avec des méthodes d'extension

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
la source
5
Si je ne me trompe pas, cette méthode échoue si la chaîne à rechercher commence à la position 0, ce qui peut être corrigé en définissant indexinitialement sur -1.
Peter Majeed
1
Vous voudrez peut-être également vérifier les chaînes nulles ou vides et correspondre, sinon il lancera, mais c'est une décision de conception.
Merci @PeterMajeed - si "BOB".IndexOf("B")retourne 0, cette fonction devrait donc l'être pourIndexOfOccurence("BOB", "B", 1)
PeterX
2
Le vôtre est probablement la solution ultime car il a à la fois une fonction d'extension et évite les expressions rationnelles et la récursivité, qui rendent le code moins lisible.
Mark Rogers
@tdyen En effet, l' analyse du code émettra « CA1062: Valider les arguments des méthodes publiques » si IndexOfOccurencene vérifie pas si sest null. Et String.IndexOf (String, Int32) lancera ArgumentNullExceptionsi matchest null.
DavidRR
1

Peut-être que ce serait également bien de travailler avec la String.Split()méthode et de vérifier si l'occurrence demandée est dans le tableau, si vous n'avez pas besoin de l'index, mais de la valeur à l'index

user3227623
la source
1

Après quelques analyses comparatives, cela semble être la solution la plus simple et la plus efficace

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
la source
1

System.ValueTuple ftw:

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

écrire une fonction à partir de là est un devoir

Matthias
la source
0

La réponse de Tod peut être quelque peu simplifiée.

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

Production

1
3
5
-1
seron
la source
0

Ou quelque chose comme ça avec la boucle 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
la source
-4

Cela pourrait le faire:

Console.WriteLine(str.IndexOf((@"\")+2)+1);
Sameer Shaikh
la source
2
Je ne vois pas comment cela fonctionnerait. Pourriez-vous inclure une brève explication de ce que cela fait?
Bob Kaufman