¿Obtener el índice de la enésima aparición de una cadena?


100

A menos que me falta una obvia incorporado en el método, ¿cuál es la forma más rápida de conseguir el n º aparición de una cadena dentro de una cadena?

Me doy cuenta de que podría repetir el método IndexOf actualizando su índice de inicio en cada iteración del ciclo. Pero hacerlo de esta manera me parece un desperdicio.


Usaría expresiones regulares para eso, entonces tienes la forma óptima de hacer coincidir la cadena dentro de la cadena. Esto en uno de los hermosos DSL que todos deberíamos usar cuando sea posible. Un ejemplo en VB.net el código es casi el mismo en C #.
Bovium

2
Yo apostaría mucho dinero a que la versión de expresiones regulares sea significativamente más difícil de acertar que "seguir haciendo bucles y haciendo String.IndexOf simple". Las expresiones regulares tienen su lugar, pero no deben usarse cuando existen alternativas más simples.
Jon Skeet

Respuestas:


52

Eso es básicamente lo que debe hacer, o al menos, es la solución más fácil. Todo lo que estaría "desperdiciando" es el costo de n invocaciones de métodos; en realidad, no revisará ningún caso dos veces, si lo piensa. (IndexOf regresará tan pronto como encuentre la coincidencia, y continuará desde donde lo dejó).


2
Supongo que tienes razón, parece que debería haber un método integrado, estoy seguro de que es algo común.
PeteT

4
De Verdad? No recuerdo haber tenido que hacerlo en unos 13 años de desarrollo en Java y C #. Eso no significa que nunca haya tenido que hacerlo, pero no con la frecuencia suficiente para recordar.
Jon Skeet

Hablando de Java, tenemos StringUtils.ordinalIndexOf(). C # con todo Linq y otras características maravillosas, simplemente no tiene un soporte incorporado para esto. Y sí, es muy imperativo contar con su soporte si se trata de analizadores y tokenizadores.
Annie

3
@Annie: Dices "tenemos", ¿te refieres a Apache Commons? Si es así, puede escribir su propia biblioteca de terceros para .NET con la misma facilidad que puede para Java ... así que no es algo que la biblioteca estándar de Java tenga y que .NET no. Y, por supuesto, en C # puede agregarlo como un método de extensión en string:)
Jon Skeet

108

Realmente podría usar la expresión regular /((s).*?){n}/para buscar enésima aparición de subcadena s.

En C # podría verse así:

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: He agregado Regex.Escapea la solución original para permitir la búsqueda de caracteres que tienen un significado especial para el motor de expresiones regulares.


2
¿Deberías escapar del value? En mi caso, estaba buscando un punto msdn.microsoft.com/en-us/library/…
russau

3
Esta expresión regular no funciona si la cadena de destino contiene saltos de línea. ¿Podrías arreglarlo? Gracias.
Ignacio Soler García

Parece bloquearse si no hay una enésima coincidencia. Necesitaba limitar un valor separado por comas a 1000 valores, y esto se colgó cuando el csv tenía menos. Entonces @Yogesh, probablemente no sea una gran respuesta aceptada como está. ;) El uso de una variante de esta respuesta (hay una cadena para la versión de cadena aquí ) y cambió el bucle de parada en el recuento enésima vez.
ruffin

Intentando buscar en \, el valor pasado es "\\", y la cadena de coincidencia se ve así antes de la función regex.match: ((). *?) {2}. Recibo este error: analizando "((). *?) {2}" - No es suficiente). ¿Cuál es el formato correcto para buscar barras invertidas sin errores?
RichieMN

3
Lo siento, pero una pequeña crítica: las soluciones de expresiones regulares no son óptimas, porque luego tengo que volver a aprender las expresiones regulares por enésima vez. El código es esencialmente más difícil de leer cuando se utilizan expresiones regulares.
Mark Rogers

19

Eso es básicamente lo que debe hacer, o al menos, es la solución más fácil. Todo lo que estaría "desperdiciando" es el costo de n invocaciones de métodos; en realidad, no revisará ningún caso dos veces, si lo piensa. (IndexOf regresará tan pronto como encuentre la coincidencia, y continuará desde donde lo dejó).

Aquí está la implementación recursiva (de la idea anterior ) como un método de extensión, imitando el formato de los métodos del marco:

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

Además, aquí hay algunas pruebas unitarias (MBUnit) que pueden ayudarlo (para demostrar que es correcto):

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

Actualicé mi formato y casos de prueba según los excelentes comentarios de Weston (gracias 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 en C # con métodos de extensión

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
Si no me equivoco, este método falla si la cadena que debe coincidir comienza en la posición 0, que se puede corregir estableciendo indexinicialmente en -1.
Peter Majeed

1
También es posible que desee verificar si hay cadenas nulas o vacías y hacer coincidir o arrojará, pero esa es una decisión de diseño.

Gracias @PeterMajeed - si "BOB".IndexOf("B")devuelve 0, también debería funcionar esta función paraIndexOfOccurence("BOB", "B", 1)
PeterX

2
La suya es probablemente la solución definitiva, ya que tiene una función de extensión y evita las expresiones regulares y la recursividad, que hacen que el código sea menos legible.
Mark Rogers

@tdyen De hecho, Code Analysis emitirá "CA1062: Validar argumentos de métodos públicos" si IndexOfOccurenceno marca si ses null. Y String.IndexOf (String, Int32) arrojará ArgumentNullExceptionsi matches null.
DavidRR

1

Quizás también sería bueno trabajar con el String.Split()Método y verificar si la ocurrencia solicitada está en la matriz, si no necesita el índice, pero el valor en el índice


1

Después de algunas evaluaciones comparativas, esta parece ser la solución más simple y 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;
        }

1

System.ValueTuple ftw:

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

escribir una función a partir de eso es tarea


0

La respuesta de Tod se puede simplificar un poco.

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

Salida

1
3
5
-1

0

O algo como esto con el bucle 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

Esto podría hacerlo:

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

2
No veo cómo funcionaría esto. ¿Podría incluir una breve explicación de lo que hace esto?
Bob Kaufman
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.