SomeButNotAll() 是否有优雅的 LINQ 解决方案?

Is there an elegant LINQ solution for SomeButNotAll()?

这是我总体上要尝试做的事情。需要说明的是,这不是家庭作业,也不是比赛或其他任何东西。希望我的措辞足够清楚:

问题

Given a set of strings in the same format, but where some end in a lowercase letter and some do not, return a set of one of each string that does not end in a lowercase letter, but that has at least one identical string ending in a lowercase letter.

例子

为简单起见,假设字符串格式为 \d+[a-z]?,其中公共部分是数字。给定 {1, 4, 3a, 1b, 3, 6c},我应该得到 {1, 3} 的排列,因为 1 和 3 都有一个末尾有和没有小写字母的元素。

备用解决方案

你可以view this solution here.

我想到的一种方法是将集合划分为带和不带小写字母后缀的元素({1, 4, 3}{3a, 1b, 6c}),然后是 return withoutSuffix.Where(x => withSuffix.Any(y => y.StartsWith(x))).

我有两个问题:

  1. 我没有看到使用谓词 Regex.IsMatch(input, "[a-z]$") 划分为两个集合的好方法。我想到的两个是两个类似定义的变量,每个变量都使用 Where 子句并对每个元素进行两次正则表达式匹配,或者转换集合以存储正则表达式匹配结果,然后从中形成两个变量。 group...by 当你需要像这样访问两个集合时,似乎玩得不好,但我可能错了。

  2. 尽管大小足够小而不关心性能,但每个 withoutSuffix 元素经历一次 withSuffix 似乎不够优雅。

相关解决方案

你可以view this solution here.

想到的另一种方法是获取公共前缀和可选后缀:{1 => {"", b}, 3 => {a, ""}, 4 => {""}, 6 => {c}}。这很容易通过使用正则表达式 ((\d+)([a-z])?) 捕获前缀和后缀并将后缀按前缀分组为 grouped.

来实现

从这里开始,这样做会很棒:

where grouped.SomeButNotAll(x => x == string.Empty)
select grouped.Key

甚至:

where grouped.ContainsSomeButNotAll(string.Empty)
select grouped.Key

我当然可以创建其中任何一个,但不幸的是,我在 LINQ 中看到的最好的是:

where grouped.Contains(string.Empty) && grouped.Any(x => x != string.Empty)
select grouped.Key

感觉超级多余。 LINQ 中是否已经有比这更好的东西了?

P.S。我愿意接受更好的方法来解决整体问题,而不是将其作为 XY 问题。优雅比性能更受欢迎,但(也许只有我)简单浪费似乎仍然不优雅。

您可以按 string.IsNullOrEmpty 添加另一个分组并验证它有 2 个组(一个用于 false,一个用于 true):

return
    from str in strs
    let match = Regex.Match(str, STR_FORMAT)
    group match.Groups[2].Value by match.Groups[1].Value into parts
    where (parts.GroupBy(string.IsNullOrEmpty).Count() == 2)
    select parts.Key;

您可以将 .Any() 更改为 !All()

我更喜欢将 Count 重载与谓词一起使用并与总计数进行比较。这可能是最干净的,您不必担心空集合引起的异常。

我认为您真的不需要正则表达式。以下是我的做法:

var withEndings = new HashSet<string>();
var withoutEndings = new HashSet<string>();

foreach (var s in input)
    if(char.IsLower(s[s.Length - 1])) 
        withEndings.Add(s.Substring(0, s.Length - 1));
    else
        withoutEndings.Add(s);

var result = withEndings.Intersect(withoutEndings);

在这种情况下,我认为性能最高的解决方案在 LINQ 中不一定非常优雅。我认为这应该做你想做的事并做 O(N) 运行 时间。

values
.Aggregate(
new { HashSet1 = new HashSet<string>(), HashSet2 = new HashSet<string>() },
(a, x) =>
{
    // If the last character is a lowercase letter then put the string
    // (minus the last character) in HashSet1, otherwise, put the string
    // in HashSet2
    if(Char.IsLower(x, x.Length - 1))
    {
        a.HashSet1.Add(x.Substring(0, x.Length - 1));
    }
    else
    {
        a.HashSet2.Add(x);
    }
    return a;
},
a => 
{
    // Return all the strings that are present in both hash sets.
    return 
    a
    .HashSet1
    .Where(x => a.HashSet2.Contains(x));
});

.Where() 在每个元素上,对于每个元素,.Where() 在每个元素上 再次 ,确保至少有一个符合原始的正则表达式模式元素加上任意一个小写字母。

var input = new List<string>() { "1", "4", "3a", "1b", "3", "6c" };
var output = input.Where(
    x => input.Where(
        y => Regex.Match(y, "^" + Regex.Escape(x) + "[a-z]$").Success
    ).Any()
);

output 包含 { "1", "3" }.