Mam wymaganie, które jest stosunkowo niejasne, ale wydaje mi się, że powinno być możliwe przy użyciu BCL.
Dla kontekstu analizuję ciąg daty / godziny w czasie Noda . Utrzymuję logiczny kursor dla mojej pozycji w ciągu wejściowym. Tak więc, chociaż cały ciąg może mieć postać „3 stycznia 2013 r.”, Kursor logiczny może znajdować się na „J”.
Teraz muszę przeanalizować nazwę miesiąca, porównując ją ze wszystkimi znanymi nazwami miesięcy dla kultury:
- Z wrażliwością kulturową
- Bez rozróżniania wielkości liter
- Tylko z punktu kursora (nie później; chcę sprawdzić, czy kursor „patrzy” na nazwę miesiąca kandydującego)
- Szybko
- ... a potem muszę wiedzieć, ile znaków zostało użytych
Aktualny kod to zrobić ogólnie działa, używając CompareInfo.Compare
. Skutecznie działa to tak (tylko dla pasującej części - w rzeczywistości jest więcej kodu, ale nie ma to związku z dopasowaniem):
internal bool MatchCaseInsensitive(string candidate, CompareInfo compareInfo)
{
return compareInfo.Compare(text, position, candidate.Length,
candidate, 0, candidate.Length,
CompareOptions.IgnoreCase) == 0;
}
Jednak zależy to od tego, czy kandydat i region, który porównujemy, są tej samej długości. W porządku przez większość czasu, ale nie w niektórych szczególnych przypadkach. Załóżmy, że mamy coś takiego:
// 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";
Teraz moje porównanie się nie powiedzie. Mógłbym użyć IsPrefix
:
if (compareInfo.IsPrefix(text.Substring(position), candidate,
CompareOptions.IgnoreCase))
ale:
- To wymaga ode mnie stworzenia podciągu, którego naprawdę wolałbym unikać. (Uważam, że czas Noda jest efektywną biblioteką systemową; wydajność analizowania może być ważna dla niektórych klientów).
- Nie mówi mi, jak daleko później przesunąć kursor
W rzeczywistości mocno podejrzewam, że nie będzie się to pojawiać zbyt często ... ale naprawdę chciałbym zrobić tutaj właściwą rzecz. Chciałbym też móc to zrobić bez zostania ekspertem od Unicode lub samodzielnego wdrażania :)
(Zgłoszony jako błąd 210 w Czasie Nody, na wypadek gdyby ktoś chciał wyciągnąć wnioski).
Podoba mi się idea normalizacji. Muszę to szczegółowo sprawdzić pod kątem a) poprawności i b) wykonania. Zakładając, że mogę sprawić, by działał poprawnie, nadal nie jestem pewien, czy warto byłoby to zmienić w ogóle - jest to coś, co prawdopodobnie nigdy nie pojawi się w prawdziwym życiu, ale może zaszkodzić wydajności wszystkich moich użytkowników: (
Sprawdziłem również BCL - który również nie wydaje się obsługiwać tego poprawnie. Przykładowy kod:
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");
}
}
}
Zmiana niestandardowej nazwy miesiąca na „łóżko” z wartością tekstową „bEd” parsuje dobrze.
OK, jeszcze kilka punktów danych:
Koszt użytkowania
Substring
iIsPrefix
jest znaczny, ale nie straszny. Na próbce „Piątek, 12 kwietnia 2013 20:28:42” na moim laptopie deweloperskim zmienia liczbę operacji analizy składniowej, które mogę wykonać w ciągu sekundy, z około 460K do około 400K. Wolałbym raczej uniknąć tego spowolnienia, jeśli to możliwe, ale nie jest tak źle.Normalizacja jest mniej wykonalna niż myślałem - ponieważ nie jest dostępna w przenośnych bibliotekach klas. Potencjalnie mógłbym go używać tylko do kompilacji innych niż PCL, dzięki czemu kompilacje PCL są nieco mniej poprawne. Uderzenie wydajnościowe testowania normalizacji (
string.IsNormalized
) obniża wydajność do około 445 tys. Wywołań na sekundę, z czym mogę żyć. Nadal nie jestem pewien, czy robi wszystko, czego potrzebuję - na przykład nazwa miesiąca zawierająca „ß” powinna pasować do „ss” w wielu kulturach, myślę, że ... a normalizacja tego nie robi.
text
nie jest za długi, możesz to zrobićif (compareInfo.IndexOf(text, candidate, position, options) == position)
. msdn.microsoft.com/en-us/library/ms143031.aspx Ale jeślitext
trwa to bardzo długo, to zmarnuje dużo czasu na szukanie dalej niż to konieczne.String
klasy w ogóle w tym przypadku i używaćChar[]
bezpośrednio. Skończy się na pisaniu więcej kodu, ale tak właśnie się dzieje, gdy zależy Ci na wysokiej wydajności ... a może powinieneś programować w C ++ / CLI ;-)Odpowiedzi:
Najpierw rozważę problem wielu <-> jednego / wielu mapowań przypadków i oddzielnie od obsługi różnych formularzy normalizacji.
Na przykład:
Pasuje,
heisse
ale następnie przesuwa kursor o 1 za bardzo. I:Pasuje,
heiße
ale następnie przesuwa kursor o 1 za mniej.Będzie to miało zastosowanie do każdego znaku, który nie ma prostego odwzorowania jeden do jednego.
Musisz znać długość podciągu, który został faktycznie dopasowany. Ale
Compare
,IndexOf
..etc rzucić tę informację dalej. To może być możliwe z wyrażeń regularnych, ale realizacja nie robi pełnego case fałdowanie i tak nie pasujeß
doss/SS
w trybie bez uwzględniania wielkości liter choć.Compare
i.IndexOf
zrobić. I tak prawdopodobnie byłoby kosztowne tworzenie nowych wyrażeń regularnych dla każdego kandydata.Najprostszym rozwiązaniem jest po prostu wewnętrzne przechowywanie łańcuchów w formie złożonej i dokonywanie porównań binarnych z kandydatami ze złożonymi wielkościami liter. Następnie możesz poprawnie przesuwać kursor za pomocą,
.Length
ponieważ kursor służy do wewnętrznej reprezentacji. Możesz również odzyskać większość utraconej wydajności, ponieważ nie musisz jej używaćCompareOptions.IgnoreCase
.Niestety nie ma wbudowanej funkcji zwijania przypadku, a składanie przypadku dla biedaka również nie działa, ponieważ nie ma pełnego mapowania przypadków -
ToUpper
metoda nie zmienia sięß
wSS
.Na przykład działa to w Javie (a nawet w JavaScript), biorąc pod uwagę ciąg znaków w normalnej formie C:
Przyjemnie jest zauważyć, że porównanie ignorowania wielkości liter w Javie nie wykonuje pełnego zwijania wielkości liter, jak w C #
CompareOptions.IgnoreCase
. Więc pod tym względem są odwrotnie: Java wykonuje pełne mapowanie wielkości liter, ale proste składanie wielkości liter - C # wykonuje proste mapowanie wielkości liter, ale pełne składanie wielkości liter.Jest więc prawdopodobne, że potrzebujesz biblioteki innej firmy, aby złożyć struny przed ich użyciem.
Zanim cokolwiek zrobisz, upewnij się, że twoje ciągi znaków mają normalną formę C. Możesz skorzystać z tej wstępnej szybkiej kontroli zoptymalizowanej pod kątem alfabetu łacińskiego:
Daje to fałszywe alarmy, ale nie fałszywe negatywy, nie spodziewam się, że w ogóle spowolni 460 tys. Analiz / s przy użyciu znaków alfabetu łacińskiego, mimo że musi to być wykonywane na każdym ciągu. W przypadku fałszywie dodatniego wyniku należy użyć go
IsNormalized
do uzyskania prawdziwie ujemnego / pozytywnego wyniku, a dopiero potem normalizować, jeśli to konieczne.Podsumowując, przetwarzanie ma na celu zapewnienie najpierw normalnej postaci C, a następnie złożenie wielkości liter. Wykonuj binarne porównania z przetworzonymi ciągami i przesuwaj kursor w miarę przesuwania go.
źródło
æ
jest równeae
iffi
równeffi
. C-normalizacja w ogóle nie obsługuje ligatur, ponieważ pozwala tylko na mapowanie zgodności (które jest zwykle ograniczone do łączenia znaków).ffi
, ale pomija inne, takie jakæ
. Problem jest pogarszany przez rozbieżności między kulturami -æ
jest równyae
pod en-US, ale nie pod da-DK, jak omówiono w dokumentacji MSDN dla ciągów . W związku z tym normalizacja (do dowolnej postaci) i mapowanie przypadków nie są wystarczającym rozwiązaniem tego problemu.Sprawdź, czy spełnia to wymóg ..:
compareInfo.Compare
działa tylko razsource
rozpoczęte zprefix
; jeśli nie, toIsPrefix
wraca-1
; w przeciwnym razie długość znaków używanych wsource
.Jednak nie mam pojęcia, oprócz przyrostu
length2
przez1
z następującym przypadku:aktualizacja :
Próbowałem trochę poprawić perf., Ale nie zostało udowodnione, czy jest błąd w następującym kodzie:
Testowałem z konkretnym przypadkiem i porównanie do około 3.
źródło
IndexOf
operacja musi przejrzeć cały ciąg od pozycji początkowej, co byłoby problemem, gdyby ciąg wejściowy był długi.Jest to faktycznie możliwe bez normalizacji i bez użycia
IsPrefix
.Musimy porównać tę samą liczbę elementów tekstowych w przeciwieństwie do tej samej liczby znaków, ale nadal zwracamy liczbę pasujących znaków.
Utworzyłem kopię
MatchCaseInsensitive
metody z ValueCursor.cs w Noda Time i nieznacznie ją zmodyfikowałem, aby można było jej używać w kontekście statycznym:(Podany tylko w celach informacyjnych, jest to kod, który nie będzie się właściwie porównywał, jak wiesz)
Poniższy wariant tej metody używa StringInfo.GetNextTextElement, który jest udostępniany przez platformę. Chodzi o to, aby porównać element tekstowy z elementem tekstowym, aby znaleźć dopasowanie, a jeśli zostanie znaleziony, zwraca rzeczywistą liczbę pasujących znaków w ciągu źródłowym:
Ta metoda działa dobrze, przynajmniej zgodnie z moimi przypadkami testowymi (które w zasadzie testują tylko kilka wariantów podanych ciągów:
"b\u00e9d"
i"be\u0301d"
).Jednak metoda GetNextTextElement tworzy podciąg dla każdego elementu tekstowego, więc ta implementacja wymaga wielu porównań podciągów - co będzie miało wpływ na wydajność.
Stworzyłem więc inny wariant, który nie używa GetNextTextElement, ale zamiast tego pomija znaki łączące Unicode, aby znaleźć rzeczywistą długość dopasowania w znakach:
Ta metoda używa następujących dwóch pomocników:
Nie robiłem żadnego benchmarkingu, więc tak naprawdę nie wiem, czy szybsza metoda jest rzeczywiście szybsza. Nie przeprowadziłem też żadnych rozszerzonych testów.
Ale to powinno odpowiedzieć na twoje pytanie, jak przeprowadzić dopasowywanie podciągów wrażliwych na kulturę dla ciągów, które mogą zawierać znaki łączące Unicode.
Oto przypadki testowe, z których korzystałem:
Wartości krotki to:
Wykonanie tych testów trzema metodami daje taki wynik:
Ostatnie dwa testy sprawdzają przypadek, gdy ciąg źródłowy jest krótszy niż ciąg dopasowania. W tym przypadku oryginalna metoda (czas Noda) również się powiedzie.
źródło