Получить индекс n-го вхождения строки?

Если я не упустил очевидный встроенный метод, каков самый быстрый способ получить n-е вхождение строки в строку?

Я понимаю, что могу зациклить IndexOf метод, обновляя его начальный индекс на каждой итерации цикла. Но делать это таким образом мне кажется расточительным.


person PeteT    schedule 09.10.2008    source источник
comment
Я бы использовал для этого регулярные выражения, тогда вам нужно найти оптимальный способ сопоставления строки внутри строки. Это в одном из красивых DSL, которые мы все должны использовать, когда это возможно. Пример в VB.net код почти такой же, как в С#.   -  person bovium    schedule 09.10.2008
comment
Я бы поставил хорошие деньги на версию с регулярными выражениями, которую значительно сложнее сделать правильно, чем продолжать зацикливаться и выполнять простой String.IndexOf. Регулярные выражения имеют свое место, но их не следует использовать, когда существуют более простые альтернативы.   -  person Jon Skeet    schedule 09.10.2008
comment
Аналогично: stackoverflow.com/a/9908392/1305911   -  person JNF    schedule 24.10.2012


Ответы (10)


Это в основном то, что вам нужно сделать, или, по крайней мере, это самое простое решение. Все, что вы «тратите впустую», - это стоимость n вызовов методов - вы на самом деле не будете проверять ни один регистр дважды, если подумать. (IndexOf вернется, как только найдет совпадение, и вы продолжите работу с того места, где остановились.)

person Jon Skeet    schedule 09.10.2008
comment
Я полагаю, вы правы, похоже, что должен быть встроенный метод, я уверен, что это обычное явление. - person PeteT; 09.10.2008
comment
Действительно? Я не могу припомнить, чтобы когда-либо приходилось делать это примерно за 13 лет разработки Java и C#. Это не значит, что мне действительно никогда не приходилось этого делать, но просто недостаточно часто, чтобы помнить. - person Jon Skeet; 09.10.2008
comment
Говоря о Java, у нас есть StringUtils.ordinalIndexOf(). C# со всеми Linq и другими замечательными функциями просто не имеет встроенной поддержки для этого. И да, очень важно иметь его поддержку, если вы имеете дело с парсерами и токенизаторами. - person Annie; 21.03.2014
comment
@Энни: Вы говорите, что у нас есть - вы имеете в виду Apache Commons? Если это так, вы можете написать свою собственную стороннюю библиотеку для .NET так же легко, как и для Java... так что это не похоже на то, что есть в стандартной библиотеке Java, чего нет в .NET. И, конечно, в C# вы можете добавить его как метод расширения на string :) - person Jon Skeet; 21.03.2014

Вы действительно можете использовать регулярное выражение /((s).*?){n}/ для поиска n-го вхождения подстроки s.

В С# это может выглядеть так:

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

Примечание. Я добавил Regex.Escape к исходному решению, чтобы разрешить поиск символов, имеющих особое значение для механизма регулярных выражений.

person Alexander Prokofyev    schedule 09.10.2008
comment
вы должны избежать value? В моем случае я искал точку msdn .microsoft.com/en-us/library/ - person russau; 06.06.2011
comment
Это регулярное выражение не работает, если целевая строка содержит разрывы строк. Не могли бы вы это исправить? Спасибо. - person Ignacio Soler Garcia; 01.09.2011
comment
Кажется, блокируется, если нет N-го совпадения. Мне нужно было ограничить значение, разделенное запятыми, до 1000 значений, и это зависало, когда в csv было меньше. Так что @Yogesh - вероятно, не лучший принятый ответ как есть. ;) Используя вариант этого ответа (есть строка для версии строки здесь) и изменили цикл так, чтобы он останавливался на n-м счёте вместо. - person ruffin; 11.10.2012
comment
При попытке поиска в \ передается значение \\, а строка совпадения перед функцией regex.match выглядит следующим образом: (().*?){2}. Я получаю эту ошибку: синтаксический анализ (().*?){2} - недостаточно )'s. Каков правильный формат для поиска обратной косой черты без ошибки? - person RichieMN; 19.02.2014
comment
@eMi, я немного изменил код, чтобы можно было искать специальные символы. - person Alexander Prokofyev; 14.01.2015
comment
Извините, но небольшая критика: решения для регулярных выражений неоптимальны, потому что тогда мне придется переучивать регулярные выражения в энный раз. Код существенно труднее читать, когда используются регулярные выражения. - person Mark Rogers; 28.05.2015

Это в основном то, что вам нужно сделать, или, по крайней мере, это самое простое решение. Все, что вы «тратите впустую», - это стоимость n вызовов методов - вы на самом деле не будете проверять ни один регистр дважды, если подумать. (IndexOf вернется, как только найдет совпадение, и вы продолжите работу с того места, где остановились.)

Вот рекурсивная реализация (вышеупомянутой идеи) в качестве метода расширения, имитирующая формат метода(ов) фреймворка:

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

Кроме того, вот несколько модульных тестов (MBUnit), которые могут вам помочь (чтобы доказать, что это правильно):

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));
        }
    }
}
person Tod Thomson    schedule 22.03.2011
comment
Я обновил свое форматирование и тестовые примеры, основываясь на замечательных отзывах Уэстона (спасибо, Уэстон). - person Tod Thomson; 07.08.2012

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

или в C# с методами расширения

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;
}
person Schotime    schedule 07.10.2009
comment
Если я не ошибаюсь, этот метод не работает, если строка для сопоставления начинается с позиции 0, что можно исправить, установив index изначально на -1. - person Peter Majeed; 27.06.2012
comment
Вы также можете проверить наличие нулевых или пустых строк и совпадение, иначе оно будет выброшено, но это дизайнерское решение. - person ; 22.01.2015
comment
Спасибо @PeterMajeed - если "BOB".IndexOf("B") возвращает 0, то и эта функция должна использоваться для IndexOfOccurence("BOB", "B", 1) - person PeterX; 17.02.2015
comment
Ваше решение, вероятно, является окончательным, поскольку оно имеет как функцию расширения, так и избегает регулярных выражений и рекурсии, которые делают код менее читаемым. - person Mark Rogers; 28.05.2015
comment
@tdyen Действительно, анализ кода выдаст CA1062: Проверить аргументы общедоступных методов если IndexOfOccurence не проверяет, является ли s null. И String.IndexOf (String, Int32) выдаст ArgumentNullException, если match равно null. - person DavidRR; 12.01.2017

После некоторого бенчмаркинга это кажется самым простым и эффективным решением.

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;
        }
person ShadowBeast    schedule 08.08.2018

Может быть, также было бы неплохо поработать с методом String.Split() и проверить, есть ли запрошенное вхождение в массиве, если вам нужен не индекс, а значение по индексу

person user3227623    schedule 13.05.2014

System.ValueTuple ftw:

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

написание функции из этого является домашним заданием

person Matthias    schedule 19.08.2018

Ответ Тода можно несколько упростить.

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

Выход

1
3
5
-1
person seron    schedule 16.08.2019

Или что-то подобное с циклом 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;
    }
person xFreeD    schedule 21.12.2019

Это может сделать это:

Console.WriteLine(str.IndexOf((@"\")+2)+1);
person Sameer Shaikh    schedule 08.07.2012
comment
Я не понимаю, как это будет работать. Не могли бы вы включить краткое объяснение того, что это делает? - person Bob Kaufman; 08.07.2012