Dapatkan indeks kemunculan n dari sebuah string?

100

Kecuali saya kehilangan metode built-in yang jelas, apa cara tercepat untuk mendapatkan kemunculan n string dalam sebuah string?

Saya menyadari bahwa saya dapat mengulang metode IndexOf dengan memperbarui indeks awalnya pada setiap iterasi dari loop. Tapi melakukannya dengan cara ini tampaknya sia-sia bagi saya.

PeteT
sumber
Saya akan menggunakan ekspresi reguler untuk itu maka Anda harus cara optimal untuk mencocokkan string dalam string. Ini di salah satu DSL indah yang harus kita gunakan jika memungkinkan. Contoh di VB.net kodenya hampir sama di C #.
bovium
2
Saya akan menempatkan banyak uang pada versi ekspresi reguler yang secara signifikan lebih sulit untuk dilakukan dengan benar daripada "terus melakukan perulangan dan melakukan String.IndexOf sederhana". Ekspresi reguler memiliki tempatnya, tetapi tidak boleh digunakan jika ada alternatif yang lebih sederhana.
Jon Skeet

Jawaban:

52

Pada dasarnya itulah yang perlu Anda lakukan - atau setidaknya, itu solusi termudah. Semua yang akan Anda "sia-siakan" adalah biaya pemanggilan metode n - Anda tidak akan benar-benar memeriksa kasus apa pun dua kali, jika Anda memikirkannya. (IndexOf akan kembali segera setelah menemukan kecocokan, dan Anda akan terus melanjutkan dari tempat terakhirnya.)

Jon Skeet
sumber
2
Saya kira hak Anda, sepertinya harus ada metode bawaan, saya yakin itu kejadian umum.
PeteT
4
Betulkah? Saya tidak ingat pernah melakukannya dalam waktu sekitar 13 tahun pengembangan Java dan C #. Itu tidak berarti saya benar-benar tidak pernah melakukannya - tetapi tidak cukup sering untuk mengingatnya.
Jon Skeet
Berbicara tentang Jawa, kami punya StringUtils.ordinalIndexOf(). C # dengan semua Linq dan fitur luar biasa lainnya, hanya saja tidak memiliki dukungan bawaan untuk ini. Dan ya, sangat penting untuk mendapatkan dukungannya jika Anda berurusan dengan parser dan tokenizers.
Annie
3
@Annie: Anda mengatakan "kami memiliki" - maksud Anda di Apache Commons? Jika demikian, Anda dapat menulis pustaka pihak ketiga Anda sendiri untuk .NET semudah mungkin untuk Java ... jadi tidak seperti itu perpustakaan standar Java yang tidak dimiliki .NET. Dan tentu saja di C # Anda dapat menambahkannya sebagai metode ekstensi di string:)
Jon Skeet
108

Anda benar-benar dapat menggunakan ekspresi reguler /((s).*?){n}/untuk mencari kemunculan substring ke-n s.

Di C # mungkin terlihat seperti ini:

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

Catatan: Saya telah menambahkan Regex.Escapesolusi asli untuk memungkinkan mencari karakter yang memiliki arti khusus untuk mesin regex.

Alexander Prokofyev
sumber
2
haruskah kamu melarikan diri value? Dalam kasus saya, saya mencari titik msdn.microsoft.com/en-us/library/…
russau
3
Regex ini tidak berfungsi jika string target berisi pemutusan baris. Bisakah Anda memperbaikinya? Terima kasih.
Ignacio Soler Garcia
Tampaknya terkunci jika tidak ada pertandingan ke-N. Saya perlu membatasi nilai yang dipisahkan koma menjadi 1000 nilai, dan ini tergantung ketika csv memiliki lebih sedikit. Jadi @Yogesh - mungkin bukan jawaban yang diterima dengan baik sebagaimana adanya. ;) Menggunakan varian dari jawaban ini (ada string ke versi string di sini ) dan mengubah loop untuk berhenti pada hitungan ke-n sebagai gantinya.
ruffin
Mencoba mencari di \, nilai yang diteruskan adalah "\\", dan string yang cocok terlihat seperti ini sebelum fungsi regex.match: ((). *?) {2}. Saya mendapatkan kesalahan ini: parsing "((). *?) {2}" - Not enough). Apa format yang benar untuk mencari garis miring ke belakang tanpa kesalahan?
RichieMN
3
Maaf tapi ada kritik kecil: solusi regex kurang optimal, karena saya harus mempelajari ulang regex untuk yang kesembilan kalinya. Kode pada dasarnya lebih sulit dibaca saat ekspresi reguler digunakan.
Mark Rogers
19

Pada dasarnya itulah yang perlu Anda lakukan - atau setidaknya, itu solusi termudah. Semua yang akan Anda "sia-siakan" adalah biaya pemanggilan metode n - Anda tidak akan benar-benar memeriksa kasus apa pun dua kali, jika Anda memikirkannya. (IndexOf akan kembali segera setelah menemukan kecocokan, dan Anda akan terus melanjutkan dari tempat terakhirnya.)

Berikut adalah implementasi rekursif (dari ide di atas ) sebagai metode ekstensi, meniru format metode kerangka kerja:

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

Juga, berikut adalah beberapa unit test (MBUnit) yang mungkin membantu Anda (untuk membuktikannya benar):

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
sumber
Saya telah memperbarui pemformatan dan kasus pengujian saya berdasarkan umpan balik hebat Weston (terima kasih 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;
}

atau di C # dengan metode ekstensi

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
sumber
5
Jika saya tidak salah, metode ini gagal jika string yang cocok dimulai pada posisi 0, yang dapat diperbaiki dengan menyetel indexawalnya ke -1.
Peter Majeed
1
Anda mungkin juga ingin memeriksa null atau string kosong dan cocok atau itu akan membuang tapi itu keputusan desain.
Terima kasih @PeterMajeed - jika "BOB".IndexOf("B")mengembalikan 0, begitu juga fungsi ini untukIndexOfOccurence("BOB", "B", 1)
PeterX
2
Solusi Anda mungkin adalah solusi pamungkas karena memiliki fungsi ekstensi dan menghindari regex dan rekursi, yang keduanya membuat kode kurang dapat dibaca.
Mark Rogers
@tdyen Memang, Analisis Kode akan mengeluarkan "CA1062: Validasi argumen metode publik" jika IndexOfOccurencetidak memeriksa apakah sada null. Dan String.IndexOf (String, Int32) akan melemparkan ArgumentNullExceptionjika matchini null.
DavidRR
1

Mungkin akan menyenangkan juga untuk bekerja dengan String.Split()Metode dan memeriksa apakah kejadian yang diminta ada dalam larik, jika Anda tidak memerlukan indeks, tetapi nilai pada indeks

pengguna3227623
sumber
1

Setelah beberapa pembandingan, ini tampaknya menjadi solusi yang paling sederhana dan paling efisien

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
sumber
1

System.ValueTuple ftw:

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

menulis fungsi dari itu adalah pekerjaan rumah

Matthias
sumber
0

Jawaban Tod bisa disederhanakan.

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

Keluaran

1
3
5
-1
seron
sumber
0

Atau sesuatu seperti ini dengan do while loop

 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
sumber
-4

Ini mungkin berhasil:

Console.WriteLine(str.IndexOf((@"\")+2)+1);
Sameer Shaikh
sumber
2
Saya tidak melihat bagaimana ini akan berhasil. Bisakah Anda memasukkan penjelasan singkat tentang apa yang dilakukannya?
Bob Kaufman