使用自定义 StringComparer 的 IndexOf

IndexOf with custom StringComparer

提问人:Isaiah Shiner 提问时间:5/15/2018 最后编辑:Isaiah Shiner 更新时间:11/15/2023 访问量:443

问:

为什么需要 a 并且不允许更一般的 ,甚至只是 或 ?String.IndexOf(String, StringComparison)StringComparisonStringComparerIComparer<T>IEqualityComparer<T>

我做了一个自定义来与几个词典一起使用,我想在我项目的其他部分使用它,但如果这些方法可以工作的话,我找不到一个好的方法来做到这一点。StringComparer

这是我做的比较器。它大致基于以下建议: 使用字符串实现自定义 IComparer

另请注意,ModifyString 是一个 WIP。我希望根据我正在比较的输入在那里添加更多内容。我也知道它很贵,但我只是在寻找一个解决方案 ATM,而不是性能。

public class CustomComparer : StringComparer
{
    public override int Compare(string x, string y)
    {
        return StringComparer.Ordinal.Compare(ModifyString(x), ModifyString(y));
    }

    public override bool Equals(string x, string y)
    {
        if (ModifyString(x).Equals(ModifyString(y)))
            return true;
        else
            return false;
    }

    public override int GetHashCode(string obj)
    {
        if (obj == null)
            return 0;
        else
            return ModifyString(obj).GetHashCode();
    }

    private string ModifyString(string s)
    {
        //I know this code is expensive/naaive, your suggestions are welcome.
        s = s.ToLowerInvariant();
        s = s.Trim();
        s = Regex.Replace(s, @"\s+", " ");//replaces all whitespace characters with a single space.
        return s;
    }
}
C# 字符串 比较

评论

2赞 Dan Wilson 5/15/2018
例子总是有帮助的。
1赞 Jeppe Stig Nielsen 5/15/2018
您将如何使用完全自定义的比较器进行搜索?你对子字符串的长度一无所知。例如,要搜索 within ,您会与所有长度的所有子词进行比较吗?"x""horse""x"
0赞 Jonathon Chase 5/15/2018
我不得不假设拥有标准实现的问题在于,它可能会假设您的 StringComparer 可能不会假设的事情。使用 -1 提前退出的优化(例如,一个输入为 null 而另一个输入不为 null)在与实现一起呈现时可能无效。如果您有一个减少重复字符序列的字符序列,例如“aaabb”到“ab”以查找索引匹配,则会更加令人困惑。我不确定您将如何确认您正在比较正确的子字符串。NullMatchesWhenTheStartStringBeginsWithTwoZeroesStringComparerStringComparer
0赞 Jeppe Stig Nielsen 5/15/2018
"horse".IndexOf("x", …) 你可以试试这个。C# 源代码以 URL 编码。
0赞 Isaiah Shiner 5/15/2018
@DanWilson 添加了示例代码。

答:

2赞 NetMage 5/15/2018 #1

使用一个方便的扩展似乎应该已经存在了,您可以编写一个扩展来使用 .正如注释中所建议的,所有可能的子字符串长度都会在每个位置进行测试,因为不能对自定义做出任何假设。IEnumerableStringStringComparerStringComparer

public static class IEnumerableExt {
    public static T FirstOrDefault<T>(this IEnumerable<T> src, Func<T, bool> testFn, T defval) => src.Where(aT => testFn(aT)).DefaultIfEmpty(defval).First();
}

public static class StringExt {
    public static int IndexOf(this string source, string match, StringComparer sc) {
        return Enumerable.Range(0, source.Length) // for each position in the string
                         .FirstOrDefault(i => // find the first position where either
                             // match is Equal at this position for length of match (or to end of string) or
                             sc.Equals(source.Substring(i, Math.Min(match.Length, source.Length-i)), match) ||
                             // match is Equal to one of the substrings beginning at this position
                             Enumerable.Range(1, source.Length-i).Any(ml => sc.Equals(source.Substring(i, ml), match)),
                             -1 // else return -1 if no position matches
                          );
    }
}

注意:修改为正确处理源子字符串和匹配字符串长度可能不相等的情况。

评论

0赞 Isaiah Shiner 5/15/2018
完美工作。我并不完全遵循它的工作原理,LINQ 不是我的强项,但我会在评论中归功于你,也许在某个时候会弄清楚。谢谢!
0赞 NetMage 5/16/2018
@IsaiahShiner我在代码中添加了一些注释以尝试提供帮助。
0赞 Tom Bogle 9/14/2021
是否为此代码编写了任何单元测试?
0赞 Tom Bogle 9/15/2021
我为此编写了一些单元测试,使用一个比较器,即使两个字符串的长度不同,也可以认为它们“相等”。在这种情况下,此解决方案不起作用,因为 Math.Min 考虑了未调整的字符串长度,在我的比较器中,较长的字符串实际上可以被视为较短字符串的子字符串。(一个潜在的实际例子是将德国 Fluss (ß) 等于“ss”的比较器。然后 IndexOf(“Fluß”, “Fluss”, myFlussComparer) 应返回 0。
0赞 NetMage 9/16/2021
我相信@TomBogle我修改了该方法以在这种情况下工作。
1赞 Tom Bogle 9/17/2021 #2

如果有人需要一个可以与 ,以下是@NetMage发布的良好解决方案的微不足道的修改:IComparer<String>

        public static int IndexOf(this string source, string match, IComparer<String> sc) {
        return Enumerable.Range(0, source.Length) // for each position in the string
            .FirstOrDefault(i => // find the first position where either
                    // match is Equal at this position for length of match (or to end of string) or
                    sc.Compare(source.Substring(i, Math.Min(match.Length, source.Length-i)), match) == 0 ||
                    // match is Equal to one of the substrings beginning at this position
                    Enumerable.Range(1, source.Length-i).Any(ml => sc.Compare(source.Substring(i, ml), match) == 0),
                -1 // else return -1 if no position matches
            );
    }
0赞 Igor Kesler 11/15/2023 #3

基于以下基础的优化版本:ReadOnlySpan<char>

using Xunit;

namespace CustomIndexOf;

public delegate bool CharSpanEqualityComparer(ReadOnlySpan<char> a, ReadOnlySpan<char> b);

public static class TextProcessingExtensions
{
    public static int IndexOf(this string source, string match, CharSpanEqualityComparer equal)
    {
        if (string.IsNullOrEmpty(source))
        {
            if (string.IsNullOrEmpty(match)) return 0;
            return -1;
        }
        if (match.Length > source.Length) return -1;
        if (match.Length == source.Length)
        {
            if (equal(source, match)) return 0;
            return -1;
        }

        for (var i = 0; i <= source.Length - match.Length; ++i)
        {
            var span = source.AsSpan(i, match.Length);
            if (equal(span, match)) return i;
        }

        return -1;
    }
}

public class StringExtensionTests
{
    [Theory]
    [InlineData("God called the light “day”, and the darkness he called “night”.", "God", 0)]
    [InlineData("God called the light “day”, and the darkness he called “night”.", "\"day\"", 21)]
    public void IndexOfWithComparerTests(string source, string match, int expectedIndex)
    {
        Dictionary<char, char> charMap = new()
        {
            { '“', '"' },
            { '”', '"' },
        };
        bool TestComparer(ReadOnlySpan<char> a, ReadOnlySpan<char> b)
        {
            if (a.Length != b.Length) return false;
        
            for (var i = 0; i < a.Length; ++i)
            {
                var charA = charMap.GetValueOrDefault(a[i], a[i]);
                var charB = charMap.GetValueOrDefault(b[i], b[i]);
                if (charA != charB) return false;
            }
        
            return true;
        }

        var actualIndex = source.IndexOf(match, TestComparer);
        Assert.Equal(expectedIndex, actualIndex);
    }
}

这个版本也有点特定于我的自定义需求:只有当字符串具有相同的长度时,比较器才应该返回 True。但是,在其他一些自定义情况下,情况可能并非如此。例如,问题中的修整逻辑 - 此特定代码未涵盖它。但这种方法仍然有效,只是需要根据特定需求进行一些调整。