J'ai une exigence qui est relativement obscure, mais j'ai l'impression que cela devrait être possible en utilisant la BCL.
Pour le contexte, j'analyse une chaîne de date / heure dans Noda Time . Je maintiens un curseur logique pour ma position dans la chaîne d'entrée. Ainsi, alors que la chaîne complète peut être «3 janvier 2013», le curseur logique peut être sur le «J».
Maintenant, je dois analyser le nom du mois, en le comparant à tous les noms de mois connus pour la culture:
- Sensible à la culture
- Insensibilité à la casse
- Juste à partir du point du curseur (pas plus tard; je veux voir si le curseur "regarde" le nom du mois candidat)
- Rapidement
- ... et j'ai besoin de savoir par la suite combien de caractères ont été utilisés
Le code actuel pour ce faire fonctionne généralement, en utilisant CompareInfo.Compare
. C'est effectivement comme ça (juste pour la partie correspondante - il y a plus de code dans la vraie chose, mais ce n'est pas pertinent pour la correspondance):
internal bool MatchCaseInsensitive(string candidate, CompareInfo compareInfo)
{
return compareInfo.Compare(text, position, candidate.Length,
candidate, 0, candidate.Length,
CompareOptions.IgnoreCase) == 0;
}
Cependant, cela dépend du fait que le candidat et la région que nous comparons ont la même longueur. Bien la plupart du temps, mais pas très bien dans certains cas particuliers. Supposons que nous ayons quelque chose comme:
// 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";
Maintenant, ma comparaison échouera. Je pourrais utiliser IsPrefix
:
if (compareInfo.IsPrefix(text.Substring(position), candidate,
CompareOptions.IgnoreCase))
mais:
- Cela m'oblige à créer une sous-chaîne, ce que je préfère éviter. (Je considère Noda Time comme une bibliothèque système; l'analyse des performances peut très bien être importante pour certains clients.)
- Il ne me dit pas à quelle distance faire avancer le curseur par la suite
En réalité, je soupçonne fortement ce ne sera pas venu très souvent ... mais je voudrais vraiment que je fasse la bonne chose ici. J'aimerais aussi vraiment pouvoir le faire sans devenir un expert Unicode ou l'implémenter moi-même :)
(Élevé en tant que bogue 210 dans Noda Time, au cas où quelqu'un voudrait suivre une conclusion éventuelle.)
J'aime l'idée de normalisation. Je dois vérifier cela en détail pour a) l'exactitude et b) les performances. En supposant que je puisse le faire fonctionner correctement, je ne sais toujours pas comment cela vaudrait la peine d'être changé - c'est le genre de chose qui ne se produira probablement jamais dans la vraie vie, mais qui pourrait nuire aux performances de tous mes utilisateurs: (
J'ai également vérifié la BCL - qui ne semble pas non plus gérer cela correctement. Exemple de code:
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");
}
}
}
Changer le nom du mois personnalisé en juste "lit" avec une valeur de texte "bEd" analyse très bien.
D'accord, quelques points de données supplémentaires:
Le coût d'utilisation
Substring
etIsPrefix
est important mais pas horrible. Sur un échantillon de "Vendredi 12 avril 2013 20:28:42" sur mon ordinateur portable de développement, cela change le nombre d'opérations d'analyse que je peux exécuter en une seconde d'environ 460K à environ 400K. Je préfère éviter ce ralentissement si possible, mais ce n'est pas trop mal.La normalisation est moins faisable que je ne le pensais - car elle n'est pas disponible dans les bibliothèques de classes portables. Je pourrais potentiellement l'utiliser uniquement pour les versions non PCL, ce qui permettrait aux versions PCL d'être un peu moins correctes. Le succès des tests de normalisation (
string.IsNormalized
) réduit les performances à environ 445 000 appels par seconde, ce avec quoi je peux vivre. Je ne suis toujours pas sûr qu'il fasse tout ce dont j'ai besoin - par exemple, un nom de mois contenant "ß" devrait correspondre à "ss" dans de nombreuses cultures, je crois ... et la normalisation ne fait pas cela.
text
n'est pas trop long, vous pouvez le faireif (compareInfo.IndexOf(text, candidate, position, options) == position)
. msdn.microsoft.com/en-us/library/ms143031.aspx Mais sitext
c'est très long, cela va perdre beaucoup de temps à chercher au-delà de ce qu'il faut.String
classe du tout dans ce cas et d' utiliser unChar[]
directement. Vous finirez par écrire plus de code, mais c'est ce qui se passe lorsque vous voulez des performances élevées ... ou peut-être que vous devriez programmer en C ++ / CLI ;-)Réponses:
Je vais d'abord considérer le problème de plusieurs <-> un / plusieurs casemappings et séparément de la gestion de différentes formes de normalisation.
Par exemple:
Correspond
heisse
mais déplace trop le curseur 1. Et:Correspond
heiße
mais déplace le curseur 1 trop moins.Cela s'appliquera à tout caractère qui n'a pas de simple mappage un-à-un.
Vous devez connaître la longueur de la sous-chaîne qui correspond réellement. Mais
Compare
,IndexOf
..etc jeter ces informations. Cela pourrait être possible avec des expressions régulières, mais l'implémentation ne fait pas le pliage complet de la casse et ne correspond donc pasß
auss/SS
mode insensible à la casse même si.Compare
et.IndexOf
fait. De toute façon, il serait probablement coûteux de créer de nouvelles expressions régulières pour chaque candidat.La solution la plus simple à cela est de simplement stocker en interne les chaînes sous forme pliée en cas et de faire des comparaisons binaires avec les candidats pliés en cas. Ensuite, vous pouvez déplacer le curseur correctement avec juste
.Length
puisque le curseur est pour la représentation interne. Vous récupérez également la plupart des performances perdues en n'ayant pas à utiliserCompareOptions.IgnoreCase
.Malheureusement, il n'y a pas de fonction de pliage de cas intégrée et le pliage de cas du pauvre homme ne fonctionne pas non plus car il n'y a pas de mappage complet de cas - la
ToUpper
méthode ne se transforme pasß
enSS
.Par exemple, cela fonctionne en Java (et même en Javascript), avec une chaîne de caractères au format normal C:
Amusant de noter que la comparaison de casse ignorée de Java ne fait pas de pliage complet de cas comme C #
CompareOptions.IgnoreCase
. Donc, ils sont opposés à cet égard: Java fait un casemapping complet, mais un simple pliage de cas - C # fait un simple casemapping, mais un pliage complet de cas.Il est donc probable que vous ayez besoin d'une bibliothèque tierce pour plier vos chaînes avant de les utiliser.
Avant de faire quoi que ce soit, vous devez vous assurer que vos chaînes sont au format normal C. Vous pouvez utiliser cette vérification rapide préliminaire optimisée pour le script latin:
Cela donne de faux positifs mais pas de faux négatifs, je ne m'attends pas à ce qu'il ralentisse du tout 460k analyses / s lors de l'utilisation de caractères de script latin, même si cela doit être effectué sur chaque chaîne. Avec un faux positif, vous utiliseriez
IsNormalized
pour obtenir un vrai négatif / positif et seulement après cela normaliser si nécessaire.Donc, en conclusion, le traitement consiste à assurer d'abord la forme normale C, puis le pli du boîtier. Faites des comparaisons binaires avec les chaînes traitées et déplacez le curseur au fur et à mesure que vous le déplacez.
la source
æ
est égal àae
etffi
est égal àffi
. La normalisation C ne gère pas du tout les ligatures, car elle n'autorise que les mappages de compatibilité (qui sont généralement limités à la combinaison de caractères).ffi
, mais en manque d'autres, telles queæ
. Le problème est aggravé par les écarts entre les cultures -æ
est égal àae
under en-US, mais pas sous da-DK, comme indiqué dans la documentation MSDN pour les chaînes . Ainsi, la normalisation (sous n'importe quelle forme) et le mappage de cas ne sont pas une solution suffisante pour ce problème.Vérifiez si cela répond à l'exigence.:
compareInfo.Compare
ne fonctionne qu'une foissource
commencé avecprefix
; sinon,IsPrefix
revient-1
; sinon, la longueur des caractères utilisée danssource
.Cependant, je ne sais pas , sauf augmentation
length2
par1
le cas suivant:mise à jour :
J'ai essayé d'améliorer un peu les performances, mais il n'est pas prouvé qu'il y ait un bogue dans le code suivant:
J'ai testé avec le cas particulier, et la comparaison jusqu'à environ 3.
la source
IndexOf
opération initiale doit parcourir toute la chaîne à partir de la position de départ, ce qui serait gênant pour les performances si la chaîne d'entrée est longue.Ceci est en fait possible sans normalisation et sans utiliser
IsPrefix
.Nous devons comparer le même nombre d' éléments de texte par opposition au même nombre de caractères, mais toujours renvoyer le nombre de caractères correspondants.
J'ai créé une copie de la
MatchCaseInsensitive
méthode à partir de ValueCursor.cs dans Noda Time et je l'ai légèrement modifiée afin qu'elle puisse être utilisée dans un contexte statique:(Juste inclus pour référence, c'est le code qui ne se comparera pas correctement comme vous le savez)
La variante suivante de cette méthode utilise StringInfo.GetNextTextElement qui est fourni par le framework. L'idée est de comparer élément de texte par élément de texte pour trouver une correspondance et, si trouvé, retourner le nombre réel de caractères correspondants dans la chaîne source:
Cette méthode fonctionne très bien au moins selon mes cas de test (qui testent simplement quelques variantes des chaînes que vous avez fournies:
"b\u00e9d"
et"be\u0301d"
).Cependant, la méthode GetNextTextElement crée une sous-chaîne pour chaque élément de texte, de sorte que cette implémentation nécessite beaucoup de comparaisons de sous-chaînes - ce qui aura un impact sur les performances.
J'ai donc créé une autre variante qui n'utilise pas GetNextTextElement mais ignore à la place les caractères de combinaison Unicode pour trouver la longueur de correspondance réelle en caractères:
Cette méthode utilise les deux assistants suivants:
Je n'ai fait aucun benchmark, donc je ne sais pas vraiment si la méthode la plus rapide est réellement plus rapide. Je n'ai pas non plus fait de tests prolongés.
Mais cela devrait répondre à votre question sur la façon d'effectuer une correspondance de sous-chaînes sensibles à la culture pour les chaînes pouvant inclure des caractères de combinaison Unicode.
Voici les cas de test que j'ai utilisés:
Les valeurs de tuple sont:
L'exécution de ces tests sur les trois méthodes donne ce résultat:
Les deux derniers tests testent le cas où la chaîne source est plus courte que la chaîne de correspondance. Dans ce cas, la méthode originale (temps Noda) réussira également.
la source