Отримайте індекс n-го появи рядка?


100

Якщо я не пропускаю очевидний вбудований метод, який найшвидший спосіб отримати n- е поява рядка в рядку?

Я усвідомлюю, що міг би циклічити метод IndexOf , оновивши його початковий індекс на кожній ітерації циклу. Але робити це таким чином здається мені марнотратним.


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

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

Відповіді:


52

Це в основному те, що вам потрібно зробити - або, принаймні, це найпростіше рішення. Все, що ви б "витрачали" - це вартість викликів методу n - ви насправді не перевіряєте жоден випадок двічі, якщо задумаєтесь. (IndexOf повернеться, як тільки знайде відповідність, і ви продовжуватимете місце, де воно закінчилося.)


2
Я думаю, що ваше право, здається, що має бути вбудований метод, хоча я впевнений, що це звичайна подія.
PeteT

4
Дійсно? Я не можу пригадати, щоб коли-небудь доводилося це робити приблизно за 13 років розвитку Java та C #. Це не означає, що мені насправді ніколи цього не доводилося робити, але просто не часто, щоб згадати.
Джон Скіт

Говорячи про Яву, у нас є 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
Цей Regex не працює, якщо цільовий рядок містить рядкові перерви. Ви могли б це виправити? Дякую.
Ігнасіо Солер Гарсія

Здається, що заблоковано, якщо немає N-го збігу. Мені потрібно було обмежити значення, розділене комами, на 1000 значень, і це висіло, коли у CSV було менше. Тож @Yogesh - напевно, не дуже прийнята відповідь, як є. ;) Використовуючи варіант цієї відповіді (тобто рядок струнної версії тут ) і змінила цикл для зупинки на п - м рахунки замість цього.
ruffin

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

3
Вибачте, але незначна критика: рішення регулярних виразів є неоптимальними, тому що мені доведеться повторно навчатись регулярним виразам. Код по суті складніше читати, коли використовуються регулярні вирази.
Марк Роджерс

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

Дякуємо @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.