在给定文化的字符串中查找不区分大小写的第一个差异

Find first difference in strings case insensitive given culture

有很多方法可以比较两个字符串以找到它们不同的第一个索引,但如果我需要在任何给定的文化中不区分大小写,那么这些选项就会消失。

这是我能想到的进行这种比较的唯一方法:

static int FirstDiff(string str1, string str2)
{
    for (int i = 1; i <= str1.Length && i <= str2.Length; i++)
        if (!string.Equals(str1.Substring(0, i), str2.Substring(0, i), StringComparison.CurrentCultureIgnoreCase))
            return i - 1;
    return -1; // strings are identical
}

谁能想到一个更好的方法,不涉及那么多字符串分配?

出于测试目的:

// Turkish word 'open' contains the letter 'ı' which is the lowercase of 'I' in Turkish, but not English
string lowerCase = "açık";
string upperCase = "AÇIK";

Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");
FirstDiff(lowerCase, upperCase); // Should return 2

Thread.CurrentThread.CurrentCulture = new CultureInfo("tr-TR");
FirstDiff(lowerCase, upperCase); // Should return -1

编辑: 检查每个字符的 ToUpper 和 ToLower 似乎适用于我能想到的任何示例。 但是它适用于所有文化吗?也许这个问题更适合语言学家。

您需要同时检查 ToLower 和 ToUpper。

private static int FirstDiff(string str1, string str2)
{
    int length = Math.Min(str1.Length, str2.Length);
    TextInfo textInfo = CultureInfo.CurrentCulture.TextInfo;
    for (int i = 0; i < length; ++i)
    {
        if (textInfo.ToUpper(str1[i]) != textInfo.ToUpper(str2[i]) ||
            textInfo.ToLower(str1[i]) != textInfo.ToLower(str2[i]))
        {
            return i;
        }
    }
    return str1.Length == str2.Length ? -1 : length;
}

您可以比较字符而不是字符串。这远未优化,而且相当快速和肮脏,但像这样的东西似乎有效

for (int i = 0; i < str1.Length && i < str2.Length; i++)
   if (char.ToLower(str1[i]) != char.ToLower(str2[i]))
       return i;

根据文档,这也适用于文化:https://docs.microsoft.com/en-us/dotnet/api/system.char.tolower?view=netframework-4.8

Casing rules are obtained from the current culture.

To convert a character to lowercase by using the casing conventions of the current culture, call the ToLower(Char, CultureInfo) method overload with a value of CurrentCulture for its culture parameter.

这里有一些不同的方法。字符串在技术上是 char 的数组,所以我将其与 LINQ.

一起使用
var list1 = "Hellow".ToLower().ToList();
var list2 = "HeLio".ToLower().ToList();

var diffIndex = list1.Zip(list2, (item1, item2) => item1 == item2)
                .Select((match, index) => new { Match = match, Index = index })
                .Where(a => !a.Match)
                .Select(a => a.Index).FirstOrDefault();

如果匹配,diffIndex 将为零。否则它将是第一个不匹配字符的索引。

编辑:

一个稍微改进的版本,可以随时转换为小写字母。而开头的ToList()实在是多余。

var diffIndex = list1.Zip(list2, (item1, item2) => char.ToLower(item1) == char.ToLower(item2))
                .Select((match, index) => new { Match = match, Index = index })
                .Where(a => !a.Match)
                .Select(a => a.Index).FirstOrDefault();

编辑2:

这是一个可以进一步缩短的工作版本。这是最佳答案,因为在前两个中,如果字符串匹配,您将得到 0。这里'如果字符串匹配你得到 null 否则得到索引。

var list1 = "Hellow";
var list2 = "HeLio";

var diffIndex = list1.Zip(list2, (item1, item2) => char.ToLower(item1) == char.ToLower(item2))
                .Select((match, index) => new { Match = match, Index = index })
                .FirstOrDefault(x => !x.Match)?.Index;

减少字符串分配次数的一种方法是减少进行比较的次数。在这种情况下,我们可以借鉴用于搜索数组的二进制搜索算法,并从比较长度为字符串一半的子字符串开始。然后我们继续添加或删除剩余索引的一半(取决于字符串是否相等),直到我们到达第一个不等式实例。

一般来说,这应该会加快搜索时间:

public static int FirstDiffBinarySearch(string str1, string str2)
{
    // "Fail fast" checks
    if (string.Equals(str1, str2, StringComparison.CurrentCultureIgnoreCase))
        return -1;
    if (str1 == null || str2 == null) return 0;

    int min = 0;
    int max = Math.Min(str1.Length, str2.Length);
    int mid = (min + max) / 2;

    while (min <= max)
    {               
        if (string.Equals(str1.Substring(0, mid), str2.Substring(0, mid), 
            StringComparison.CurrentCultureIgnoreCase))
        {
            min = mid + 1;                    
        }
        else
        {
            max = mid - 1;
        }

        mid = (min + max) / 2;
    }

    return mid;
}

我想起了另一个奇怪的字符(或者更确切地说是 unicode 代码点):有些字符充当代理对,它们与任何文化都无关,除非这对字符彼此相邻出现。 有关 Unicode interpretation standards see the document that 的更多信息链接在他的评论中。

在尝试不同的解决方案时,我偶然发现了这个特殊的 class,我认为它最适合我的需要:System.Globalization.StringInfoMS Doc Example 显示了它与代理对的关系)

class 将字符串分成需要彼此才有意义的部分(而不是严格按字符)。然后我可以使用 string.Equals 和 return 不同的第一个片段的索引来按文化比较每个片段:

static int FirstDiff(string str1, string str2)
{
    var si1 = StringInfo.GetTextElementEnumerator(str1);
    var si2 = StringInfo.GetTextElementEnumerator(str2);

    bool more1, more2;
    while ((more1 = si1.MoveNext()) & (more2 = si2.MoveNext())) // single & to avoid short circuiting the right counterpart
        if (!string.Equals(si1.Current as string, si2.Current as string, StringComparison.CurrentCultureIgnoreCase))
            return si1.ElementIndex;

    if (more1 || more2)
        return si1.ElementIndex;
    else
        return -1; // strings are equivalent
}