ฉันจะดำเนินการ "เริ่มต้นด้วย" ที่คำนึงถึงวัฒนธรรมจากตรงกลางของสตริงได้อย่างไร


106

ฉันมีข้อกำหนดที่ค่อนข้างคลุมเครือ แต่รู้สึกว่าควรเป็นไปได้โดยใช้ BCL

สำหรับบริบทผมแยกสตริงวันที่ / เวลาในNoda เวลา ฉันรักษาเคอร์เซอร์แบบลอจิคัลสำหรับตำแหน่งของฉันภายในสตริงอินพุต ดังนั้นในขณะที่สตริงที่สมบูรณ์อาจเป็น "3 มกราคม 2013" เคอร์เซอร์แบบลอจิคัลอาจอยู่ที่ "J"

ตอนนี้ฉันต้องแยกวิเคราะห์ชื่อเดือนโดยเปรียบเทียบกับชื่อเดือนที่รู้จักทั้งหมดสำหรับวัฒนธรรม:

  • วัฒนธรรมที่อ่อนไหว
  • ตัวพิมพ์เล็กและใหญ่
  • จากจุดของเคอร์เซอร์ (ไม่ใช่ในภายหลังฉันต้องการดูว่าเคอร์เซอร์กำลัง "ดู" ชื่อเดือนของผู้สมัครหรือไม่)
  • อย่างรวดเร็ว
  • ... และฉันต้องรู้ว่าหลังจากนั้นใช้อักขระกี่ตัว

รหัสปัจจุบันCompareInfo.Compareการทำเช่นนี้โดยทั่วไปจะทำงานโดยใช้ มันเป็นแบบนี้อย่างมีประสิทธิภาพ (สำหรับส่วนที่ตรงกัน - มีโค้ดมากกว่าของจริง แต่ไม่เกี่ยวข้องกับการจับคู่):

internal bool MatchCaseInsensitive(string candidate, CompareInfo compareInfo)
{
    return compareInfo.Compare(text, position, candidate.Length,
                               candidate, 0, candidate.Length, 
                               CompareOptions.IgnoreCase) == 0;
}

อย่างไรก็ตามขึ้นอยู่กับผู้สมัครและภูมิภาคที่เราเปรียบเทียบว่ามีความยาวเท่ากัน ดีเกือบตลอดเวลา แต่ไม่ดีในบางกรณี สมมติว่าเรามีสิ่งที่ต้องการ:

// U+00E9 is a single code point for e-acute
var text = "x b\u00e9d y";
int position = 2;
// e followed by U+0301 still means e-acute, but from two code points
var candidate = "be\u0301d";

ตอนนี้การเปรียบเทียบของฉันจะล้มเหลว ฉันสามารถใช้IsPrefix:

if (compareInfo.IsPrefix(text.Substring(position), candidate,
                         CompareOptions.IgnoreCase))

แต่:

  • นั่นทำให้ฉันต้องสร้างสตริงย่อยซึ่งฉันควรหลีกเลี่ยงจริงๆ (ฉันกำลังดู Noda Time เป็นไลบรารีระบบอย่างมีประสิทธิภาพการแยกวิเคราะห์ประสิทธิภาพอาจมีความสำคัญสำหรับลูกค้าบางราย)
  • มันไม่ได้บอกฉันว่าจะต้องเลื่อนเคอร์เซอร์ไปไกลแค่ไหนในภายหลัง

ในความเป็นจริงผมขอสงสัยนี้จะไม่เกิดขึ้นบ่อยมาก ... แต่ฉันจริงๆชอบที่จะทำสิ่งที่ถูกต้องที่นี่ ฉันก็อยากจะทำได้โดยไม่ต้องเป็นผู้เชี่ยวชาญ Unicode หรือลงมือทำเอง :)

(ยกเป็นบั๊ก 210ใน Noda Time เผื่อว่าใครอยากติดตามบทสรุปในท้ายที่สุด)

ฉันชอบความคิดของการทำให้เป็นมาตรฐาน ฉันต้องตรวจสอบโดยละเอียดเพื่อ a) ความถูกต้องและ b) ประสิทธิภาพ สมมติว่าฉันสามารถทำให้มันทำงานได้อย่างถูกต้องฉันก็ยังไม่แน่ใจว่ามันจะคุ้มค่ากับการเปลี่ยนแปลงทั้งหมดได้อย่างไร - มันเป็นสิ่งที่อาจไม่เคยเกิดขึ้นจริงในชีวิตจริง แต่อาจส่งผลกระทบต่อประสิทธิภาพของผู้ใช้ทั้งหมดของฉัน: (

ฉันได้ตรวจสอบ BCL ด้วยซึ่งดูเหมือนจะไม่สามารถจัดการสิ่งนี้ได้อย่างถูกต้อง โค้ดตัวอย่าง:

using System;
using System.Globalization;

class Test
{
    static void Main()
    {
        var culture = (CultureInfo) CultureInfo.InvariantCulture.Clone();
        var months = culture.DateTimeFormat.AbbreviatedMonthNames;
        months[10] = "be\u0301d";
        culture.DateTimeFormat.AbbreviatedMonthNames = months;

        var text = "25 b\u00e9d 2013";
        var pattern = "dd MMM yyyy";
        DateTime result;
        if (DateTime.TryParseExact(text, pattern, culture,
                                   DateTimeStyles.None, out result))
        {
            Console.WriteLine("Parsed! Result={0}", result);
        }
        else
        {
            Console.WriteLine("Didn't parse");
        }
    }
}

การเปลี่ยนชื่อเดือนที่กำหนดเองเป็นเพียง "bed" โดยมีค่าข้อความเป็น "bEd" จะแยกวิเคราะห์

เอาล่ะจุดข้อมูลเพิ่มเติมเล็กน้อย:

  • ค่าใช้จ่ายในการใช้งานSubstringและIsPrefixมีความสำคัญ แต่ไม่น่ากลัว ในตัวอย่าง "วันศุกร์ที่ 12 เมษายน 2556 20:28:42 น." บนแล็ปท็อปสำหรับการพัฒนาของฉันมันจะเปลี่ยนจำนวนการดำเนินการแยกวิเคราะห์ที่ฉันสามารถดำเนินการได้ในหนึ่งวินาทีจากประมาณ 460K เป็นประมาณ 400K ฉันควรหลีกเลี่ยงการชะลอตัวนั้นถ้าเป็นไปได้ แต่ก็ไม่เลวร้ายเกินไป

  • Normalization มีความเป็นไปได้น้อยกว่าที่คิด - เพราะไม่มีใน Portable Class Libraries ผมอาจจะใช้เพียงสำหรับการไม่ จำกัด สร้างอนุญาตให้บมจสร้างจะเป็นเล็ก ๆ น้อย ๆ ที่ถูกต้องน้อย การทดสอบประสิทธิภาพสำหรับการทำให้เป็นมาตรฐาน ( string.IsNormalized) ทำให้ประสิทธิภาพลดลงเหลือประมาณ 445K การโทรต่อวินาทีซึ่งฉันสามารถใช้งานได้ ฉันยังไม่แน่ใจว่ามันทำทุกอย่างที่ฉันต้องการตัวอย่างเช่นชื่อเดือนที่มี "ß" ควรตรงกับ "ss" ในหลายวัฒนธรรมฉันเชื่อว่า ... และการทำให้เป็นมาตรฐานไม่ได้ทำเช่นนั้น


ในขณะที่ฉันเข้าใจถึงความต้องการของคุณที่จะหลีกเลี่ยงการสร้างสตริงย่อย แต่มันอาจจะดีที่สุด แต่ก่อนหน้านี้ในเกมด้วยการเปลี่ยนทุกอย่างเป็นแบบฟอร์มการปรับมาตรฐานยูนิโค้ดที่เลือก FIRST แล้วรู้ว่าคุณเดินได้ "ทีละจุด ". น่าจะเป็น D-form
IDisposable

@IDisposable: ใช่ฉันสงสัยเกี่ยวกับเรื่องนั้น เห็นได้ชัดว่าฉันสามารถทำให้ชื่อเดือนเป็นปกติได้ล่วงหน้า อย่างน้อยฉันก็สามารถทำให้เป็นมาตรฐานได้เพียงครั้งเดียว ฉันสงสัยว่าขั้นตอนการปรับมาตรฐานจะตรวจสอบว่าต้องทำอะไรก่อนหรือไม่ ฉันไม่มีประสบการณ์มากนักในการทำให้เป็นมาตรฐาน - เป็นหนทางเดียวที่ต้องพิจารณา
Jon Skeet

1
หากคุณไม่ยาวเกินไปคุณสามารถทำtext msdn.microsoft.com/en-us/library/ms143031.aspx แต่ถ้านานมากก็จะเสียเวลาค้นหามากเกินกว่าที่ต้องการ if (compareInfo.IndexOf(text, candidate, position, options) == position)text
Jim Mischel

1
เพียงข้ามการใช้Stringคลาสไปเลยในอินสแตนซ์นี้และใช้ไฟล์Char[]. คุณจะเขียนโค้ดได้มากขึ้น แต่นั่นคือสิ่งที่เกิดขึ้นเมื่อคุณต้องการประสิทธิภาพสูง ... หรือบางทีคุณควรจะเขียนโปรแกรมใน C ++ / CLI ;-)
intrepidis

1
CompareOptions.IgnoreNonSpaceจะไม่ดูแลสิ่งนี้ให้คุณโดยอัตโนมัติหรือ? ดูเหมือนว่าสำหรับฉัน (จาก docco ไม่อยู่ในตำแหน่งที่จะทดสอบจาก iPad นี้ขออภัย!) ราวกับว่านี่อาจเป็นกรณีการใช้งาน( the ?) สำหรับตัวเลือกนั้น " บ่งชี้ว่าการเปรียบเทียบสตริงต้องละเว้นอักขระที่รวมกันแบบไม่เว้นวรรคเช่นการกำกับเสียง "
Sepster

คำตอบ:


41

ฉันจะพิจารณาปัญหาของ <-> หนึ่ง / หลาย casemappings ก่อนและแยกจากการจัดการแบบฟอร์ม Normalization ต่างๆ

ตัวอย่างเช่น:

x heiße y
  ^--- cursor

ตรงกันheisseแต่เลื่อนเคอร์เซอร์ 1 มากเกินไป และ:

x heisse y
  ^--- cursor

ตรงกันheißeแต่เลื่อนเคอร์เซอร์ 1 น้อยเกินไป

สิ่งนี้จะใช้กับอักขระใด ๆ ที่ไม่มีการแมปแบบตัวต่อตัวแบบธรรมดา

คุณจะต้องทราบความยาวของสตริงย่อยที่ตรงกันจริง แต่Compare, IndexOf..etc โยนข้อมูลที่อยู่ห่างออกไป อาจเป็นไปได้ด้วยนิพจน์ทั่วไป แต่การใช้งานไม่ได้ทำการพับแบบเต็มกรณีและไม่ตรง ßกับss/SSในโหมดไม่คำนึงถึงตัวพิมพ์เล็กและใหญ่แม้ว่า.Compareและ.IndexOfทำ และอาจมีค่าใช้จ่ายสูงในการสร้าง regexes ใหม่สำหรับผู้สมัครทุกคน

วิธีแก้ปัญหาที่ง่ายที่สุดคือเก็บสตริงไว้ภายในในกรณีที่พับและทำการเปรียบเทียบไบนารีกับตัวพิมพ์เล็ก จากนั้นคุณสามารถเลื่อนเคอร์เซอร์ได้อย่างถูกต้องเพียง.Lengthเพราะเคอร์เซอร์มีไว้สำหรับการแสดงภายใน CompareOptions.IgnoreCaseนอกจากนี้คุณยังจะได้รับมากที่สุดของประสิทธิภาพการทำงานที่หายไปกลับมาจากการไม่ต้องใช้

แต่น่าเสียดายที่ไม่มีกรณีที่ฟังก์ชั่นพับในตัวและคนจนพับกรณีที่ไม่ได้ทำงานอย่างใดอย่างหนึ่งเพราะไม่มีการทำแผนที่กรณีเต็ม - ToUpperวิธีการไม่เปิดเข้าไปßSS

ตัวอย่างเช่นใช้งานได้ใน Java (และแม้กระทั่งใน Javascript) สตริงที่กำหนดให้อยู่ในรูปแบบปกติ C:

//Poor man's case folding.
//There are some edge cases where this doesn't work
public static String toCaseFold( String input, Locale cultureInfo ) {
    return input.toUpperCase(cultureInfo).toLowerCase(cultureInfo);
}

สนุกที่จะทราบว่าของ Java ละเว้นการเปรียบเทียบกรณีที่ไม่ได้ทำกรณีเต็มพับเช่น C # CompareOptions.IgnoreCase's ดังนั้นมันจึงตรงกันข้ามในเรื่องนี้: Java ทำ casemapping เต็มรูปแบบ แต่การพับเคสแบบธรรมดา - C # ทำ casemapping แบบธรรมดา แต่พับแบบเต็ม

ดังนั้นจึงเป็นไปได้ว่าคุณต้องมีไลบรารีของบุคคลที่สามเพื่อพับสตริงของคุณก่อนใช้งาน


ก่อนที่จะทำสิ่งใดคุณต้องแน่ใจว่าสตริงของคุณอยู่ในรูปแบบปกติ C คุณสามารถใช้การตรวจสอบด่วนเบื้องต้นนี้ที่ปรับให้เหมาะกับสคริปต์ละติน:

public static bool MaybeRequiresNormalizationToFormC(string input)
{
    if( input == null ) throw new ArgumentNullException("input");

    int len = input.Length;
    for (int i = 0; i < len; ++i)
    {
        if (input[i] > 0x2FF)
        {
            return true;
        }
    }

    return false;
}

สิ่งนี้ให้ผลบวกเท็จ แต่ไม่ใช่เชิงลบที่ผิดฉันไม่คาดหวังว่ามันจะชะลอการแยกวิเคราะห์ 460k / วินาทีเลยเมื่อใช้อักขระสคริปต์ละตินแม้ว่าจะต้องดำเนินการกับทุกสตริง ด้วยการบวกเท็จคุณจะใช้IsNormalizedเพื่อให้ได้ค่าลบ / บวกจริงและหลังจากนั้นจะทำให้เป็นปกติหากจำเป็นเท่านั้น


สรุปได้ว่าการประมวลผลคือต้องแน่ใจว่ามีรูปแบบปกติ C ก่อนจากนั้นจึงพับกรณี ทำการเปรียบเทียบไบนารีกับสตริงที่ประมวลผลแล้วและย้ายเคอร์เซอร์ขณะที่คุณกำลังย้าย


ขอบคุณสำหรับสิ่งนี้ - ฉันจะต้องดูรายละเอียดเพิ่มเติมในรูปแบบการทำให้เป็นมาตรฐาน C แต่นี่เป็นคำแนะนำที่ดี ฉันคิดว่าฉันสามารถอยู่ได้โดย "มันทำงานไม่ถูกต้องภายใต้ PCL" (ซึ่งไม่ได้ให้การทำให้เป็นมาตรฐาน) การใช้ไลบรารีของบุคคลที่สามสำหรับการพับเคสจะเป็นการใช้งานมากเกินไป - ขณะนี้เราไม่มีการอ้างอิงของบุคคลที่สามและการแนะนำเพียงอย่างเดียวสำหรับกรณีมุมที่แม้แต่ BCL ก็ไม่สามารถจัดการได้ก็เป็นความเจ็บปวด สันนิษฐานว่าการพับกรณีมีความอ่อนไหวต่อวัฒนธรรม btw (เช่นตุรกี)?
Jon Skeet

2
@JonSkeet ใช่ Turkic สมควรได้รับโหมดของตัวเองในการแมป casefold: P ดูส่วนรูปแบบในส่วนหัวของCaseFolding.txt
Esailija

คำตอบนี้ดูเหมือนจะมีข้อบกพร่องพื้นฐานซึ่งหมายความว่าอักขระจะแมปกับอักษรควบ (และในทางกลับกัน) เฉพาะเมื่อพับตัวพิมพ์ กรณีนี้ไม่ได้; มีตัวอักษรที่ถือว่าเท่ากับอักขระโดยไม่คำนึงถึงตัวเรือน ตัวอย่างเช่นภายใต้วัฒนธรรม en-US æเท่ากับaeและจะมีค่าเท่ากับ ffiC-normalization ไม่ได้จัดการกับ ligatures เลยเนื่องจากอนุญาตเฉพาะการแมปที่เข้ากันได้เท่านั้น (ซึ่งโดยทั่วไปจะ จำกัด เฉพาะการรวมอักขระ)
ดักลาส

KC- และ KD-normalization จัดการกับการผูกมัดบางอย่างเช่นแต่คิดถึงคนอื่น ๆ เช่นæ. ปัญหานี้ทำให้แย่ลงโดยความแตกต่างระหว่างวัฒนธรรม - æเท่ากับaeภายใต้ en-US แต่ไม่ได้อยู่ภายใต้การดา-DK ตามที่กล่าวไว้ภายใต้เอกสาร MSDN สำหรับสตริง ดังนั้นการทำให้เป็นมาตรฐาน (ในรูปแบบใดก็ได้) และการแม็ปกรณีจึงไม่ใช่วิธีแก้ปัญหาที่เพียงพอสำหรับปัญหานี้
ดักลาส

การแก้ไขขนาดเล็กที่จะแสดงความคิดเห็นก่อนหน้านี้ของฉัน: C-ฟื้นฟูเพียง แต่ช่วยให้เป็นที่ยอมรับแมป (เช่นสำหรับการรวมตัวอักษร) ไม่แมปการทำงานร่วมกัน (เช่นหนังสติ๊ก)
Douglas

21

ดูว่าตรงตามข้อกำหนดหรือไม่ .. :

public static partial class GlobalizationExtensions {
    public static int IsPrefix(
        this CompareInfo compareInfo,
        String source, String prefix, int startIndex, CompareOptions options
        ) {
        if(compareInfo.IndexOf(source, prefix, startIndex, options)!=startIndex)
            return ~0;
        else
            // source is started with prefix
            // therefore the loop must exit
            for(int length2=0, length1=prefix.Length; ; )
                if(0==compareInfo.Compare(
                        prefix, 0, length1, 
                        source, startIndex, ++length2, options))
                    return length2;
    }
}

compareInfo.Compareดำเนินการเพียงครั้งเดียวที่sourceเริ่มต้นด้วยprefix; ถ้ามันไม่ได้แล้วIsPrefixจะส่งกลับ-1; sourceความยาวของตัวละครอย่างอื่นที่ใช้ในการ

อย่างไรก็ตามฉันไม่มีความคิดยกเว้นการเพิ่มขึ้นlength2โดย1ในกรณีต่อไปนี้:

var candidate="ßssß\u00E9\u0302";
var text="abcd ssßss\u0065\u0301\u0302sss";

var count=
    culture.CompareInfo.IsPrefix(text, candidate, 5, CompareOptions.IgnoreCase);

ปรับปรุง :

ฉันพยายามปรับปรุงความสมบูรณ์แบบเล็กน้อย แต่ไม่ได้รับการพิสูจน์ว่ามีข้อผิดพลาดในโค้ดต่อไปนี้หรือไม่:

public static partial class GlobalizationExtensions {
    public static int Compare(
        this CompareInfo compareInfo,
        String source, String prefix, int startIndex, ref int length2, 
        CompareOptions options) {
        int length1=prefix.Length, v2, v1;

        if(0==(v1=compareInfo.Compare(
            prefix, 0, length1, source, startIndex, length2, options))
            ) {
            return 0;
        }
        else {
            if(0==(v2=compareInfo.Compare(
                prefix, 0, length1, source, startIndex, 1+length2, options))
                ) {
                ++length2;
                return 0;
            }
            else {
                if(v1<0||v2<0) {
                    length2-=2;
                    return -1;
                }
                else {
                    length2+=2;
                    return 1;
                }
            }
        }
    }

    public static int IsPrefix(
        this CompareInfo compareInfo,
        String source, String prefix, int startIndex, CompareOptions options
        ) {
        if(compareInfo.IndexOf(source, prefix, startIndex, options)
                !=startIndex)
            return ~0;
        else
            for(int length2=
                    Math.Min(prefix.Length, source.Length-(1+startIndex)); ; )
                if(0==compareInfo.Compare(
                        source, prefix, startIndex, ref length2, options))
                    return length2;
    }
}

ฉันทดสอบกับกรณีเฉพาะและการเปรียบเทียบลงเหลือประมาณ 3


ฉันต้องการจริงๆค่อนข้างไม่ต้องห่วงเช่นนี้ เป็นที่ยอมรับในช่วงแรกจะต้องวนซ้ำหากพบบางสิ่งบางอย่าง แต่ฉันยังคงไม่จำเป็นต้องทำการเปรียบเทียบ 8 สตริงเพื่อให้ตรงกับ "กุมภาพันธ์" เช่น รู้สึกว่ามันต้องมีวิธีที่ดีกว่านี้ นอกจากนี้การIndexOfดำเนินการเริ่มต้นจะต้องมองผ่านสตริงทั้งหมดจากตำแหน่งเริ่มต้นซึ่งจะทำให้เกิดปัญหาด้านประสิทธิภาพหากสตริงอินพุตยาว
Jon Skeet

@JonSkeet: ขอบคุณครับ อาจมีบางอย่างที่สามารถเพิ่มเพื่อตรวจสอบว่าสามารถลดลูปได้หรือไม่ ฉันจะคิดเกี่ยวกับเรื่องนั้น
Ken Kin

@JonSkeet: คุณจะพิจารณาใช้การสะท้อนหรือไม่? เนื่องจากฉันตรวจสอบวิธีการพวกเขาจึงตกอยู่ในการเรียกใช้วิธีการพื้นเมืองไม่ไกล
Ken Kin

3
แน่นอน. Noda Time ไม่ต้องการเข้าสู่ธุรกิจของรายละเอียด Unicode :)
Jon Skeet

2
ฉันได้แก้ไขปัญหาที่คล้ายกันครั้งนี้แล้ว (การเน้นสตริงการค้นหาใน HTML) ฉันทำมันในทำนองเดียวกัน คุณสามารถปรับแต่งลูปและกลยุทธ์การค้นหาในลักษณะที่ทำให้เสร็จได้อย่างรวดเร็วโดยตรวจสอบกรณีที่เป็นไปได้ก่อน สิ่งที่ดีเกี่ยวกับเรื่องนี้คือดูเหมือนว่าจะถูกต้องทั้งหมดและไม่มีรายละเอียด Unicode รั่วไหลในโค้ดของคุณ
usr

9

IsPrefixนี้เป็นจริงได้โดยไม่ต้องฟื้นฟูและโดยไม่ต้องใช้

เราจำเป็นต้องเปรียบเทียบองค์ประกอบข้อความจำนวนเท่ากันเมื่อเทียบกับจำนวนอักขระเดียวกัน แต่ยังคงส่งคืนจำนวนอักขระที่ตรงกัน

ฉันได้สร้างสำเนาของMatchCaseInsensitiveวิธีการจากValueCursor.cs ใน Noda Timeและแก้ไขเล็กน้อยเพื่อให้สามารถใช้ในบริบทคงที่:

// Noda time code from MatchCaseInsensitive in ValueCursor.cs
static int IsMatch_Original(string source, int index, string match, CompareInfo compareInfo)
{
    unchecked
    {
        if (match.Length > source.Length - index)
        {
            return 0;
        }

        // TODO(V1.2): This will fail if the length in the input string is different to the length in the
        // match string for culture-specific reasons. It's not clear how to handle that...
        if (compareInfo.Compare(source, index, match.Length, match, 0, match.Length, CompareOptions.IgnoreCase) == 0)
        {
            return match.Length;
        }

        return 0;
    }
}

(รวมไว้เพื่อการอ้างอิงเป็นรหัสที่เปรียบเทียบไม่ถูกต้องตามที่คุณทราบ)

ตัวแปรต่อไปนี้ของวิธีการนั้นใช้StringInfo.GetNextTextElementซึ่งจัดเตรียมโดยกรอบงาน แนวคิดคือการเปรียบเทียบองค์ประกอบข้อความตามองค์ประกอบข้อความเพื่อค้นหารายการที่ตรงกันและหากพบว่าจะส่งคืนจำนวนอักขระที่ตรงกันในสตริงต้นทาง:

// Using StringInfo.GetNextTextElement to match by text elements instead of characters
static int IsMatch_New(string source, int index, string match, CompareInfo compareInfo)
{
    int sourceIndex = index;
    int matchIndex = 0;

    // Loop until we reach the end of source or match
    while (sourceIndex < source.Length && matchIndex < match.Length)
    {
        // Get text elements at the current positions of source and match
        // Normally that will be just one character but may be more in case of Unicode combining characters
        string sourceElem = StringInfo.GetNextTextElement(source, sourceIndex);
        string matchElem = StringInfo.GetNextTextElement(match, matchIndex);

        // Compare the current elements.
        if (compareInfo.Compare(sourceElem, matchElem, CompareOptions.IgnoreCase) != 0)
        {
            return 0; // No match
        }

        // Advance in source and match (by number of characters)
        sourceIndex += sourceElem.Length;
        matchIndex += matchElem.Length;
    }

    // Check if we reached end of source and not end of match
    if (matchIndex != match.Length)
    {
        return 0; // No match
    }

    // Found match. Return number of matching characters from source.
    return sourceIndex - index;
}

วิธีนี้ใช้ได้ผลดีอย่างน้อยก็เป็นไปตามกรณีการทดสอบของฉัน (ซึ่งโดยพื้นฐานแล้วเพียงแค่ทดสอบสายอักขระที่คุณให้มา: "b\u00e9d"และ"be\u0301d")

อย่างไรก็ตามเมธอดGetNextTextElementจะสร้างสตริงย่อยสำหรับแต่ละองค์ประกอบข้อความดังนั้นการใช้งานนี้จึงต้องมีการเปรียบเทียบสตริงย่อยจำนวนมากซึ่งจะส่งผลกระทบต่อประสิทธิภาพ

ดังนั้นฉันจึงสร้างตัวแปรอื่นที่ไม่ใช้GetNextTextElementแต่จะข้ามไปที่อักขระที่รวม Unicode เพื่อค้นหาความยาวการจับคู่จริงในอักขระ:

// This should be faster
static int IsMatch_Faster(string source, int index, string match, CompareInfo compareInfo)
{
    int sourceLength = source.Length;
    int matchLength = match.Length;
    int sourceIndex = index;
    int matchIndex = 0;

    // Loop until we reach the end of source or match
    while (sourceIndex < sourceLength && matchIndex < matchLength)
    {
        sourceIndex += GetTextElemLen(source, sourceIndex, sourceLength);
        matchIndex += GetTextElemLen(match, matchIndex, matchLength);
    }

    // Check if we reached end of source and not end of match
    if (matchIndex != matchLength)
    {
        return 0; // No match
    }

    // Check if we've found a match
    if (compareInfo.Compare(source, index, sourceIndex - index, match, 0, matchIndex, CompareOptions.IgnoreCase) != 0)
    {
        return 0; // No match
    }

    // Found match. Return number of matching characters from source.
    return sourceIndex - index;
}

วิธีนี้ใช้ตัวช่วยสองตัวต่อไปนี้:

static int GetTextElemLen(string str, int index, int strLen)
{
    bool stop = false;
    int elemLen;

    for (elemLen = 0; index < strLen && !stop; ++elemLen, ++index)
    {
        stop = !IsCombiningCharacter(str, index);
    }

    return elemLen;
}

static bool IsCombiningCharacter(string str, int index)
{
    switch (CharUnicodeInfo.GetUnicodeCategory(str, index))
    {
        case UnicodeCategory.NonSpacingMark:
        case UnicodeCategory.SpacingCombiningMark:
        case UnicodeCategory.EnclosingMark:
            return true;

        default:
            return false;
    }
}

ฉันยังไม่ได้ทำ Bench marking เลยไม่รู้จริงๆว่าวิธีที่เร็วกว่านั้นเร็วกว่าจริงหรือเปล่า ฉันไม่ได้ทำการทดสอบเพิ่มเติมใด ๆ

แต่สิ่งนี้ควรตอบคำถามของคุณเกี่ยวกับวิธีดำเนินการจับคู่สตริงย่อยที่ละเอียดอ่อนทางวัฒนธรรมสำหรับสตริงที่อาจมีอักขระที่รวม Unicode เข้าด้วยกัน

นี่คือกรณีทดสอบที่ฉันเคยใช้:

static Tuple<string, int, string, int>[] tests = new []
{
    Tuple.Create("x b\u00e9d y", 2, "be\u0301d", 3),
    Tuple.Create("x be\u0301d y", 2, "b\u00e9d", 4),

    Tuple.Create("x b\u00e9d", 2, "be\u0301d", 3),
    Tuple.Create("x be\u0301d", 2, "b\u00e9d", 4),

    Tuple.Create("b\u00e9d y", 0, "be\u0301d", 3),
    Tuple.Create("be\u0301d y", 0, "b\u00e9d", 4),

    Tuple.Create("b\u00e9d", 0, "be\u0301d", 3),
    Tuple.Create("be\u0301d", 0, "b\u00e9d", 4),

    Tuple.Create("b\u00e9", 0, "be\u0301d", 0),
    Tuple.Create("be\u0301", 0, "b\u00e9d", 0),
};

ค่าทูเพิลคือ:

  1. สตริงต้นทาง (กองหญ้า)
  2. ตำแหน่งเริ่มต้นในแหล่งที่มา
  3. สตริงตรงกัน (เข็ม)
  4. ความยาวการจับคู่ที่คาดไว้

การเรียกใช้การทดสอบเหล่านั้นในสามวิธีให้ผลลัพธ์ดังนี้:

Test #0: Orignal=BAD; New=OK; Faster=OK
Test #1: Orignal=BAD; New=OK; Faster=OK
Test #2: Orignal=BAD; New=OK; Faster=OK
Test #3: Orignal=BAD; New=OK; Faster=OK
Test #4: Orignal=BAD; New=OK; Faster=OK
Test #5: Orignal=BAD; New=OK; Faster=OK
Test #6: Orignal=BAD; New=OK; Faster=OK
Test #7: Orignal=BAD; New=OK; Faster=OK
Test #8: Orignal=OK; New=OK; Faster=OK
Test #9: Orignal=OK; New=OK; Faster=OK

การทดสอบสองครั้งล่าสุดกำลังทดสอบกรณีที่สตริงต้นทางสั้นกว่าสตริงที่ตรงกัน ในกรณีนี้วิธีเดิม (เวลา Noda) จะประสบความสำเร็จเช่นกัน


ขอบคุณมากสำหรับเรื่องนี้. ฉันจะต้องพิจารณารายละเอียดเพื่อดูว่ามันทำงานได้ดีเพียงใด แต่ดูเหมือนว่าจะเป็นจุดเริ่มต้นที่ดี มีความรู้เกี่ยวกับ Unicode มากขึ้น (ในโค้ดเอง) มากกว่าที่ฉันคาดหวังไว้ แต่ถ้าแพลตฟอร์มไม่ทำตามที่ต้องการฉันก็ทำได้ไม่มาก :(
Jon Skeet

@JonSkeet: ดีใจที่ได้รับความช่วยเหลือ! และใช่การจับคู่สตริงย่อยกับการสนับสนุน Unicode ควรรวมอยู่ในเฟรมเวิร์กอย่างแน่นอน ...
MårtenWikström
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.