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


101

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

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


Я бы использовал для этого регулярные выражения, тогда вам нужно найти оптимальный способ сопоставления строки внутри строки. Это один из прекрасных DSL, который мы все должны использовать, когда это возможно. Пример на VB.net код почти такой же на C #.
bovium

2
Я бы положил хорошие деньги на версию с регулярными выражениями, которую значительно сложнее получить правильно, чем «продолжать цикл и выполнять простой String.IndexOf». Регулярные выражения имеют свое место, но их не следует использовать, когда существуют более простые альтернативы.
Джон Скит,

Аналогично: stackoverflow.com/a/9908392/1305911
JNF

Ответы:


52

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


2
Я полагаю, что вы правы, похоже, что должен быть встроенный метод, хотя я уверен, что это обычное явление.
PeteT,

4
В самом деле? Я не припомню, чтобы мне приходилось когда-либо делать это за 13 лет разработки Java и C #. Это не значит, что мне действительно никогда не приходилось это делать - просто недостаточно часто, чтобы помнить.
Джон Скит,

Говоря о Java, у нас есть StringUtils.ordinalIndexOf(). C # со всеми Linq и другими замечательными функциями просто не имеет встроенной поддержки для этого. И да, очень важно иметь его поддержку, если вы имеете дело с парсерами и токенизаторами.
Энни

3
@Annie: Вы говорите «у нас есть» - вы имеете в виду Apache Commons? Если это так, вы можете написать свою собственную стороннюю библиотеку для .NET так же легко, как и для Java ... так что это не похоже на то, что стандартная библиотека Java имеет, чего нет в .NET. И, конечно же, в C # вы можете добавить его как метод расширения string:)
Джон Скит,

108

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

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

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к исходному решению, чтобы разрешить поиск символов, которые имеют особое значение для механизма регулярных выражений.


2
вы должны избежать value? В моем случае я искал точку msdn.microsoft.com/en-us/library/…
russau

3
Это регулярное выражение не работает, если целевая строка содержит разрывы строк. Не могли бы вы это исправить? Спасибо.
Игнасио Солер Гарсия

Кажется, блокируется, если нет N-го совпадения. Мне нужно было ограничить значение, разделенное запятыми, до 1000 значений, и это зависало, когда в csv было меньше. Итак, @Yogesh - вероятно, не самый лучший ответ как есть. ;) Используя вариант этого ответа (есть строка струнной версии здесь ) и изменила цикл для остановки на п - м счета вместо этого.
ruffin

Пытаясь выполнить поиск по \, передано значение "\\", а строка соответствия выглядит так до функции regex.match: ((). *?) {2}. Я получаю эту ошибку: синтаксический анализ "((). *?) {2}" - недостаточно). В каком формате следует искать обратную косую черту без ошибок?
RichieMN

3
Извините, но небольшая критика: решения с регулярными выражениями неоптимальны, потому что тогда мне придется переучивать регулярные выражения в n-й раз. Код значительно труднее читать при использовании регулярных выражений.
Марк Роджерс

19

Это в основном то, что вам нужно сделать - или, по крайней мере, это самое простое решение. Все, что вы «потратите впустую», - это затраты на 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));
        }
    }
}

Я обновил свое форматирование и тестовые примеры на основе отличных отзывов Уэстона (спасибо Уэстону).
Тод Томсон,

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

или в 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;
}

5
Если я не ошибаюсь, этот метод не работает, если строка для сопоставления начинается с позиции 0, что можно исправить, indexизначально установив значение -1.
Питер Маджид

1
Вы также можете проверить наличие нулевых или пустых строк и совпадений, иначе это будет бросать, но это дизайнерское решение.

Спасибо @PeterMajeed - если "BOB".IndexOf("B")возвращает 0, то должна и эта функция дляIndexOfOccurence("BOB", "B", 1)
PeterX

2
Ваш, вероятно, является окончательным решением, поскольку он имеет как функцию расширения, так и позволяет избежать регулярных выражений и рекурсии, которые делают код менее читаемым.
Марк Роджерс

@tdyen Действительно, анализ кода выдаст «CA1062: Проверить аргументы общедоступных методов» если IndexOfOccurenceне проверяет, есть ли sэто null. И String.IndexOf (String, Int32) выбросит, ArgumentNullExceptionесли matchесть null.
DavidRR

1

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


1

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

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;

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


0

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

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

0

Или что-то вроде этого с циклом 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

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

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

2
Я не понимаю, как это сработает. Не могли бы вы вкратце объяснить, что это значит?
Боб Кауфман
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.