Tengo un requisito que es relativamente oscuro, pero parece que debería ser posible usando el BCL.
Para el contexto, estoy analizando una cadena de fecha / hora en Noda Time . Mantengo un cursor lógico para mi posición dentro de la cadena de entrada. Entonces, mientras que la cadena completa puede ser "3 de enero de 2013", el cursor lógico puede estar en la 'J'.
Ahora, necesito analizar el nombre del mes, comparándolo con todos los nombres de meses conocidos para la cultura:
- Sensible a la cultura
- No distingue entre mayúsculas y minúsculas
- Solo desde el punto del cursor (no más tarde; quiero ver si el cursor está "mirando" el nombre del mes candidato)
- Con rapidez
- ... y luego necesito saber cuántos caracteres se usaron
El código actual para hacer esto generalmente funciona, usando CompareInfo.Compare
. Es efectivamente así (solo para la parte de coincidencia; hay más código en la realidad, pero no es relevante para la coincidencia):
internal bool MatchCaseInsensitive(string candidate, CompareInfo compareInfo)
{
return compareInfo.Compare(text, position, candidate.Length,
candidate, 0, candidate.Length,
CompareOptions.IgnoreCase) == 0;
}
Sin embargo, eso depende de que el candidato y la región que comparamos tengan la misma longitud. Bien la mayor parte del tiempo, pero no bien en algunos casos especiales. Supongamos que tenemos algo como:
// 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";
Ahora mi comparación fallará. Podría usar IsPrefix
:
if (compareInfo.IsPrefix(text.Substring(position), candidate,
CompareOptions.IgnoreCase))
pero:
- Eso requiere que cree una subcadena, que realmente prefiero evitar. (Veo Noda Time efectivamente como una biblioteca del sistema; el rendimiento del análisis puede ser importante para algunos clientes).
- No me dice cuánto avanzar el cursor después
En realidad, sospecho fuertemente que esto no ocurrirá muy a menudo ... pero realmente me gustaría hacer lo correcto aquí. También me gustaría poder hacerlo sin convertirme en un experto en Unicode o implementarlo yo mismo :)
(Planteado como error 210 en Noda Time, en caso de que alguien quiera seguir alguna conclusión final).
Me gusta la idea de la normalización. Necesito verificar eso en detalle para a) corrección yb) desempeño. Suponiendo que pueda hacer que funcione correctamente, todavía no estoy seguro de si valdría la pena cambiarlo todo: es el tipo de cosas que probablemente nunca aparecerán en la vida real, pero que podrían dañar el rendimiento de todos mis usuarios: (
También verifiqué el BCL, que tampoco parece manejar esto correctamente. Código de muestra:
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");
}
}
}
Cambiar el nombre del mes personalizado a solo "cama" con un valor de texto de "bEd" se analiza bien.
Bien, algunos puntos de datos más:
El costo de usar
Substring
yIsPrefix
es significativo pero no horrible. En una muestra de "Viernes 12 de abril de 2013 20:28:42" en mi computadora portátil de desarrollo, cambia el número de operaciones de análisis que puedo ejecutar en un segundo de aproximadamente 460 K a aproximadamente 400 K. Prefiero evitar esa desaceleración si es posible, pero no está tan mal.La normalización es menos factible de lo que pensaba, porque no está disponible en las bibliotecas de clases portátiles. Potencialmente, podría usarlo solo para compilaciones que no sean PCL, lo que permite que las compilaciones PCL sean un poco menos correctas. El impacto de rendimiento de las pruebas de normalización (
string.IsNormalized
) reduce el rendimiento a aproximadamente 445K llamadas por segundo, con lo que puedo vivir. Todavía no estoy seguro de que haga todo lo que necesito, por ejemplo, un nombre de mes que contenga "ß" debería coincidir con "ss" en muchas culturas, creo ... y la normalización no hace eso.
text
no es demasiado largo, puede hacerloif (compareInfo.IndexOf(text, candidate, position, options) == position)
. msdn.microsoft.com/en-us/library/ms143031.aspx Pero sitext
es muy largo, perderá mucho tiempo buscando más allá de donde se necesita.String
clase en absoluto en este caso y utilizar unaChar[]
forma directa. Terminará escribiendo más código, pero eso es lo que sucede cuando desea un alto rendimiento ... o tal vez debería programar en C ++ / CLI ;-)Respuestas:
Consideraré el problema de muchos <-> uno / muchos mapeos de casos primero y por separado del manejo de diferentes formularios de normalización.
Por ejemplo:
Coincide
heisse
pero luego mueve el cursor 1 demasiado. Y:Coincide
heiße
pero luego mueve el cursor 1 demasiado menos.Esto se aplicará a cualquier personaje que no tenga una asignación simple uno a uno.
Necesitaría saber la longitud de la subcadena que realmente coincidió. Pero
Compare
,IndexOf
..etc tira esa información. Podría ser posible con expresiones regulares, pero la aplicación no hacer caso de plegado completo y así no coincideß
conss/SS
el modo de mayúsculas y minúsculas, aunque.Compare
y.IndexOf
hacer. Y probablemente sería costoso crear nuevas expresiones regulares para cada candidato de todos modos.La solución más simple para esto es simplemente almacenar internamente cadenas en forma de caso plegado y hacer comparaciones binarias con candidatos de caso plegado. Entonces puede mover el cursor correctamente con solo
.Length
ya que el cursor es para representación interna. También recupera la mayor parte del rendimiento perdido al no tener que usarloCompareOptions.IgnoreCase
.Desafortunadamente, no hay una función de plegado de casos incorporada y el plegado de casos del pobre tampoco funciona porque no hay un mapeo completo de casos, el
ToUpper
método no se convierteß
enSS
.Por ejemplo, esto funciona en Java (e incluso en Javascript), dada la cadena que está en forma normal C:
Es divertido notar que la comparación de mayúsculas y minúsculas de Java no hace el plegado completo de mayúsculas y minúsculas como C #
CompareOptions.IgnoreCase
. Entonces, son opuestos en este sentido: Java hace mapeo de casos completo, pero plegado de caso simple: C # hace mapeo de caso simple, pero plegado de caso completo.Por lo tanto, es probable que necesite una biblioteca de terceros para doblar sus cuerdas antes de usarlas.
Antes de hacer cualquier cosa, debe asegurarse de que sus cadenas estén en forma normal C.Puede utilizar esta verificación rápida preliminar optimizada para el alfabeto latino:
Esto da falsos positivos pero no falsos negativos, no espero que disminuya la velocidad de 460k análisis / s en absoluto cuando se usan caracteres de escritura latina a pesar de que debe realizarse en cada cadena. Con un falso positivo lo usaría
IsNormalized
para obtener un verdadero negativo / positivo y solo después de eso normalizarlo si es necesario.Entonces, en conclusión, el procesamiento es asegurar primero la forma C normal, luego el plegado de la caja. Haga comparaciones binarias con las cadenas procesadas y mueva el cursor a medida que lo mueve actualmente.
fuente
æ
es igual aae
yffi
es igual affi
. La normalización C no maneja ligaduras en absoluto, ya que solo permite asignaciones de compatibilidad (que generalmente están restringidas a la combinación de caracteres).ffi
, pero pasa por alto otras, comoæ
. El problema se agrava por las discrepancias entre culturas:æ
es igual aae
en-US, pero no a da-DK, como se explica en la documentación de MSDN para cadenas . Por lo tanto, la normalización (a cualquier forma) y el mapeo de casos no son una solución suficiente para este problema.Vea si esto cumple con el requisito ...:
compareInfo.Compare
solo se realiza una vez que sesource
inició conprefix
; si no lo hizo,IsPrefix
regresa-1
; de lo contrario, la longitud de los caracteres utilizados ensource
.Sin embargo, no tengo ni idea, excepto de la subasta
length2
por1
el siguiente caso:actualización :
Intenté mejorar un poco el rendimiento, pero no está probado si hay un error en el siguiente código:
Probé con el caso particular y la comparación bajó a aproximadamente 3.
fuente
IndexOf
operación inicial tiene que mirar a través de toda la cadena desde la posición inicial, lo que sería un problema de rendimiento si la cadena de entrada es larga.En realidad, esto es posible sin normalización y sin usar
IsPrefix
.Necesitamos comparar el mismo número de elementos de texto en contraposición al mismo número de caracteres, pero aún así devolver el número de caracteres coincidentes.
Creé una copia del
MatchCaseInsensitive
método de ValueCursor.cs en Noda Time y lo modifiqué ligeramente para que pueda usarse en un contexto estático:(Solo se incluye como referencia, es el código que no se comparará correctamente como sabe)
La siguiente variante de ese método usa StringInfo.GetNextTextElement que es proporcionado por el marco. La idea es comparar elemento de texto por elemento de texto para encontrar una coincidencia y, si se encuentra, devolver el número real de caracteres coincidentes en la cadena de origen:
Ese método funciona bien al menos de acuerdo con mis casos de prueba (que básicamente solo prueban un par de variantes de las cadenas que ha proporcionado:
"b\u00e9d"
y"be\u0301d"
).Sin embargo, el método GetNextTextElement crea una subcadena para cada elemento de texto, por lo que esta implementación requiere muchas comparaciones de subcadenas, lo que tendrá un impacto en el rendimiento.
Entonces, creé otra variante que no usa GetNextTextElement, sino que omite los caracteres de combinación Unicode para encontrar la longitud real de coincidencia en caracteres:
Ese método utiliza los siguientes dos ayudantes:
No he realizado ninguna evaluación comparativa, por lo que realmente no sé si el método más rápido es realmente más rápido. Tampoco he realizado pruebas prolongadas.
Pero esto debería responder a su pregunta sobre cómo realizar una coincidencia de subcadenas sensibles a la cultura para cadenas que pueden incluir caracteres de combinación Unicode.
Estos son los casos de prueba que he usado:
Los valores de la tupla son:
Ejecutar esas pruebas en los tres métodos produce este resultado:
Las dos últimas pruebas están probando el caso cuando la cadena de origen es más corta que la cadena de coincidencia. En este caso, el método original (tiempo de Noda) también tendrá éxito.
fuente