如何从string中间执行文化敏感的“开始”操作?

我有一个比较模糊的要求,但是觉得应该可以使用BCL。

对于上下文,我正在parsing日野时间的date/时间string。 我为inputstring中的位置维护一个逻辑光标。 所以虽然完整的string可能是“2013年1月3日”,但逻辑光标可能在“J”处。

现在,我需要parsing月份名称,并将其与文化的所有已知月份名称进行比较:

  • 文化敏感
  • 不区分大小写
  • 只是从光标的angular度来看(不会晚;我想看看光标是否在“看着”候选人的月份名称)
  • 很快
  • …之后我需要知道使用了多less个字符

当前的代码通常使用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 = "xb\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)) 

但:

  • 这需要我创build一个子string,我真的很想避免。 (我将Noda Time视为一个系统库,parsing性能对于某些客户来说可能非常重要。)
  • 它并没有告诉我在多大程度上提前光标

实际上,我非常怀疑这个问题不会经常出现,但我真的很想在这里做正确的事情。 我也很想能够做到这一点,而不必成为一个Unicode专家或自己实现它:)

(在野田时间出现210错误 ,如果有人想要遵循任何最终结论。)

我喜欢正常化的想法。 我需要详细检查a)正确性和b)性能。 假设我可以使它正常工作,我还不确定这是否值得改变 – 这是在现实生活中可能永远不会实际发生的事情,但可能会伤害所有用户的performance: (

我也检查了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”即可parsing。

好的,还有几个数据点:

  • 使用SubstringIsPrefix的成本是显着的,但并不可怕。 在我的开发笔记本电脑上的“2013年4月12日星期五20:28:42”的示例中,它改变了我可以在一秒钟内从约460K到约400K执行的parsing操作次数。 如果可能的话,我宁愿避免这种放缓,但不是糟糕。

  • 规范化比我想象的更不可行 – 因为它在可移植类库中不可用。 我可以使用它仅仅用于非PCL构build,从而使PCL构build不太正确。 正常化testing( string.IsNormalized )的性能降低使性能下降到每秒大约445K次,这是我可以忍受的。 我仍然不确定它是否满足我需要的一切 – 例如,包含“ß”的月份名称应该与许多文化中的“ss”相匹配,我相信…并且正常化不会这样做。

我将首先考虑许多</>一个/多个casemappings的问题,并分别处理不同的Normalizationforms。

例如:

 x heiße y ^--- cursor 

匹配heisse但随后移动光标1太多。 和:

 x heisse y ^--- cursor 

匹配heiße但是随后移动光标1的次数也减less了。

这将适用于没有简单的一对一映射的任何字符。

您需要知道实际匹配的子string的长度。 但CompareIndexOf ..等将这些信息扔掉。 正则expression式可能是可能的,但是实现不会执行全部大小写的折叠,所以在大小写不敏感的模式下不匹配ßss/SS ,即使.Compare.IndexOf做。 无论如何,为每个候选人创build新的正则expression式可能是昂贵的。

最简单的解决scheme是在内部存储string以防大小写折叠forms,并与大小写折叠候选进行二进制比较。 然后你可以用.Length正确地移动游标,因为游标是用于内部表示的。 您还可以从不必使用CompareOptions.IgnoreCase获得大部分的性能CompareOptions.IgnoreCase

不幸的是,没有内置折叠function,而穷人的情况下折叠也不起作用,因为没有完整的大小写映射 – ToUpper方法不会将ß转换为SS

例如,这个工作在Java(甚至在Javascript),给定的string是正常的formsC:

 //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那样进行全部大小写的折叠。 所以他们在这方面是相反的:Java做全面的casemapping,但简单的案例折叠 – C#做简单的casemapping,但全案折叠。

所以很有可能你需要第三方库才能在使用前折叠你的string。


在做任何事之前,你必须确保你的string是正常的formsC.你可以使用这个初步的快速检查优化的拉丁脚本:

 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; } 

这给出了错误的肯定,但不是错误的否定,我不希望它使用拉丁脚本字符,即使需要在每个string上执行,都减缓了460k parse / s。 如果出现误报,您可以使用IsNormalized获得真正的负值/正值,并且只有在必要时才正常化。


所以总的来说,处理是先确保C的正常forms,然后是折叠。 与处理的string进行二进制比较,并在当前移动光标时移动光标。

看看这是否符合要求..:

 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只执行一次以prefix开始的source ; 如果没有,则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); 

更新

我试图改进一点点,但是没有certificate下面的代码是否有bug:

 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; } } 

我testing了特定的情况下,比较下降到3左右。

这实际上可能没有规范化,也没有使用IsPrefix

我们需要比较相同数量的文本元素 ,而不是相同数量的字符,但仍然返回匹配字符的数量。

我已经在Noda Time中创build了一个来自ValueCursor.cs的MatchCaseInsensitive方法的副本,并对其稍加修改,以便可以在静态上下文中使用它:

 // 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 。 这个想法是通过文本元素来比较文本元素以find匹配,并且如果find,则返回源string中匹配字符的实际数目:

 // 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; } 

这个方法工作得很好,至less根据我的testing用例(基本上只是testing你提供的string的几个变体: "b\u00e9d""be\u0301d" )。

但是, GetNextTextElement方法为每个文本元素创build一个子string,所以这个实现需要大量的子string比较 – 这会影响性能。

所以,我创build了另一个不使用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; } } 

我还没有做过任何基准testing,所以我不知道更快的方法是否更快。 我也没有做任何扩展testing。

但是,这应该回答你的问题,如何执行文字敏感string匹配string,可能包括Unicode字符组合。

这些是我用过的testing用例:

 static Tuple<string, int, string, int>[] tests = new [] { Tuple.Create("xb\u00e9d y", 2, "be\u0301d", 3), Tuple.Create("x be\u0301d y", 2, "b\u00e9d", 4), Tuple.Create("xb\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. 源string(haystack)
  2. 源头的起始位置。
  3. 匹配string(针)。
  4. 预期的匹配长度。

在这三种方法上运行这些testing会得到如下结果:

 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 

最后两个testing是testing源string比匹配string短的情况。 在这种情况下,原始(Noda时间)方法也会成功。