Tenho um requisito que é relativamente obscuro, mas parece que deveria ser possível usando o BCL.
Para contextualizar, estou analisando uma string de data / hora em Noda Time . Eu mantenho um cursor lógico para minha posição dentro da string de entrada. Portanto, embora a string completa possa ser "3 de janeiro de 2013", o cursor lógico pode estar em 'J'.
Agora, preciso analisar o nome do mês, comparando-o com todos os nomes de meses conhecidos da cultura:
- Sensibilidade à cultura
- Não faz distinção entre maiúsculas e minúsculas
- Apenas do ponto do cursor (não mais tarde; quero ver se o cursor está "olhando" para o nome do mês candidato)
- Rapidamente
- ... e eu preciso saber depois quantos caracteres foram usados
O código atual para fazer isso geralmente funciona, usando CompareInfo.Compare
. É efetivamente assim (apenas para a parte correspondente - há mais código na coisa real, mas não é relevante para a correspondência):
internal bool MatchCaseInsensitive(string candidate, CompareInfo compareInfo)
{
return compareInfo.Compare(text, position, candidate.Length,
candidate, 0, candidate.Length,
CompareOptions.IgnoreCase) == 0;
}
No entanto, isso depende do candidato e da região comparada ter o mesmo comprimento. Bom na maioria das vezes, mas não em alguns casos especiais. Suponha que tenhamos 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";
Agora minha comparação falhará. Eu poderia usar IsPrefix
:
if (compareInfo.IsPrefix(text.Substring(position), candidate,
CompareOptions.IgnoreCase))
mas:
- Isso exige que eu crie uma substring, o que realmente prefiro evitar. (Estou vendo o Noda Time como efetivamente uma biblioteca do sistema; o desempenho da análise pode ser importante para alguns clientes.)
- Não me diz o quanto devo avançar o cursor depois
Na realidade, tenho fortes suspeitas de que isso não acontecerá com muita frequência ... mas eu realmente gostaria de fazer a coisa certa aqui. Também gostaria muito de poder fazer isso sem me tornar um especialista em Unicode ou implementá-lo sozinho :)
(Criado como bug 210 na Hora de Noda, caso alguém queira seguir alguma conclusão eventual.)
Gosto da ideia de normalização. Preciso verificar isso em detalhes para a) correção eb) desempenho. Supondo que posso fazer funcionar corretamente, ainda não tenho certeza se valeria a pena mudar tudo - é o tipo de coisa que provavelmente nunca aparecerá na vida real, mas pode prejudicar o desempenho de todos os meus usuários: (
Também verifiquei o BCL - que também não parece lidar com isso corretamente. Código de amostra:
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");
}
}
}
Alterar o nome do mês personalizado para apenas "cama" com um valor de texto "bEd" tem uma boa análise.
Ok, mais alguns pontos de dados:
O custo de uso
Substring
eIsPrefix
é significativo, mas não horrível. Em uma amostra de "Sexta-feira, 12 de abril de 2013 20:28:42" no meu laptop de desenvolvimento, ele altera o número de operações de análise que posso executar em um segundo de cerca de 460K para cerca de 400K. Prefiro evitar essa desaceleração, se possível, mas não é tão ruim.A normalização é menos viável do que eu pensava - porque não está disponível nas Bibliotecas de Classes Portáteis. Eu poderia usá-lo potencialmente apenas para compilações não PCL, permitindo que as compilações PCL fossem um pouco menos corretas. O impacto no desempenho do teste de normalização (
string.IsNormalized
) reduz o desempenho para cerca de 445 mil chamadas por segundo, com o qual posso viver. Ainda não tenho certeza se ele faz tudo o que preciso - por exemplo, um nome de mês contendo "ß" deve corresponder a "ss" em muitas culturas, eu acredito ... e normalizar não faz isso.
text
não for muito longo, você pode fazerif (compareInfo.IndexOf(text, candidate, position, options) == position)
. msdn.microsoft.com/en-us/library/ms143031.aspx Mas setext
for muito longo, vai perder muito tempo pesquisando além de onde é necessário.String
classe em tudo neste caso e usar umChar[]
diretamente. Você acabará escrevendo mais código, mas é o que acontece quando você quer alto desempenho ... ou talvez você devesse estar programando em C ++ / CLI ;-)Respostas:
Vou considerar o problema de muitos <-> um / muitos mapeamentos de caso primeiro e separadamente do tratamento de diferentes formulários de normalização.
Por exemplo:
Corresponde,
heisse
mas move o cursor 1 demais. E:Corresponde,
heiße
mas move o cursor 1 muito menos.Isso se aplicará a qualquer caractere que não tenha um mapeamento um-para-um simples.
Você precisaria saber o comprimento da substring que foi realmente correspondida. Mas
Compare
,IndexOf
.etc jogar essa informação de distância. Poderia ser possível com expressões regulares, mas a implementação não faz a dobragem completa de maiúsculas e minúsculas e, portanto, não correspondeß
aoss/SS
modo não sensível a maiúsculas, embora.Compare
e.IndexOf
faça. E provavelmente seria caro criar novas regexes para cada candidato de qualquer maneira.A solução mais simples para isso é apenas armazenar internamente as strings em formato case-fold e fazer comparações binárias com os candidatos case-fold. Em seguida, você pode mover o cursor corretamente com apenas
.Length
uma vez que o cursor é para representação interna. Você também obtém a maior parte do desempenho perdido por não ter que usarCompareOptions.IgnoreCase
.Infelizmente, não há função de dobra de caixa embutida e a dobra de caixa do pobre homem também não funciona porque não há mapeamento completo de caixa - o
ToUpper
método não se transformaß
emSS
.Por exemplo, isso funciona em Java (e mesmo em Javascript), dada a string que está no Formulário C normal:
É divertido notar que a comparação de ignorar maiúsculas e minúsculas do Java não faz a dobra completa de maiúsculas e minúsculas como o C #
CompareOptions.IgnoreCase
. Portanto, eles são opostos a esse respeito: Java faz casemapping completo, mas simples casemapping - C # faz casemapping simples, mas full case fold.Portanto, é provável que você precise de uma biblioteca de terceiros para dobrar a caixa de suas strings antes de usá-las.
Antes de fazer qualquer coisa, você deve ter certeza de que suas strings estão na forma normal C. Você pode usar esta verificação rápida preliminar otimizada para escrita latina:
Isso fornece falsos positivos, mas não falsos negativos. Não espero que diminua a velocidade de 460 mil análises / s ao usar caracteres de script latino, embora precise ser executado em todas as strings. Com um falso positivo você usaria
IsNormalized
para obter um verdadeiro negativo / positivo e só depois normalizar se necessário.Portanto, em conclusão, o processamento é para garantir a forma normal C primeiro, depois a dobra de caixa. Faça comparações binárias com as strings processadas e mova o cursor conforme você o move atualmente.
fonte
æ
é igual aae
effi
é igual affi
. A normalização C não lida com ligaduras, uma vez que permite apenas mapeamentos de compatibilidade (que normalmente são restritos à combinação de caracteres).ffi
, mas deixa de ver outras, comoæ
. O problema é agravado pelas discrepâncias entre culturas -æ
é igual aae
em en-US, mas não em da-DK, conforme discutido na documentação do MSDN para strings . Portanto, a normalização (para qualquer forma) e o mapeamento de caso não são uma solução suficiente para este problema.Veja se isso atende ao requisito ..:
compareInfo.Compare
executa apenas uma vezsource
iniciado comprefix
; se não, entãoIsPrefix
retorna-1
; caso contrário, o comprimento dos caracteres usados emsource
.No entanto, eu não tenho idéia, exceto incremento de
length2
pelo1
com o seguinte caso:atualização :
Tentei melhorar um pouco o desempenho, mas não está provado se há bug no código a seguir:
Eu testei com o caso específico e a comparação caiu para cerca de 3.
fonte
IndexOf
operação inicial deve examinar toda a string a partir da posição inicial, o que seria um problema de desempenho se a string de entrada fosse longa.Na verdade, isso é possível sem normalização e sem uso
IsPrefix
.Precisamos comparar o mesmo número de elementos de texto em oposição ao mesmo número de caracteres, mas ainda retornar o número de caracteres correspondentes.
Eu criei uma cópia do
MatchCaseInsensitive
método de ValueCursor.cs em Noda Time e modifiquei um pouco para que possa ser usado em um contexto estático:(Incluído apenas para referência, é o código que não será comparado corretamente como você sabe)
A seguinte variante desse método usa StringInfo.GetNextTextElement que é fornecido pela estrutura. A ideia é comparar elemento de texto por elemento de texto para encontrar uma correspondência e, se encontrado, retornar o número real de caracteres correspondentes na string de origem:
Esse método funciona bem, pelo menos de acordo com meus casos de teste (que basicamente testam algumas variantes das strings que você forneceu:
"b\u00e9d"
e"be\u0301d"
).No entanto, o método GetNextTextElement cria uma substring para cada elemento de texto, portanto, essa implementação requer muitas comparações de substring - o que terá um impacto no desempenho.
Portanto, criei outra variante que não usa GetNextTextElement, mas, em vez disso, pula os caracteres de combinação Unicode para encontrar o comprimento de correspondência real em caracteres:
Esse método usa os seguintes dois auxiliares:
Eu não fiz nenhuma marcação de banco, então eu realmente não sei se o método mais rápido é realmente mais rápido. Nem fiz nenhum teste extenso.
Mas isso deve responder à sua pergunta sobre como realizar a correspondência de substring com sensibilidade cultural para strings que podem incluir caracteres de combinação Unicode.
Estes são os casos de teste que usei:
Os valores da tupla são:
Executar esses testes nos três métodos produz este resultado:
Os dois últimos testes estão testando o caso em que a string de origem é mais curta do que a string de correspondência. Nesse caso, o método original (hora Noda) também será bem-sucedido.
fonte