【问题标题】:Linq select items up until next occurrenceLinq 选择项目直到下一次出现
【发布时间】:2013-07-18 01:54:15
【问题描述】:

我需要过滤以下列表以返回以“Group”开头的第一个项目开始的所有项目,直到但不包括以“Group”开头的下一个项目(或直到最后一个项目) .

List<string> text = new List<string>();
text.Add("Group Hear It:");
text.Add("    item: The Smiths");
text.Add("    item: Fernando Sor");
text.Add("Group See It:");
text.Add("    item: Longmire");
text.Add("    item: Ricky Gervais Show");
text.Add("    item: In Bruges");

过滤后,我希望在第一个分组中有以下项目:

"Group Hear It:"
"    item: The Smiths"
"    item: Fernando Sor"

以及第二组中的以下项目:

"Group See It:"
"    item: Longmire"
"    item: Ricky Gervais Show"
"    item: In Bruges"

这不起作用,因为我要在第一个排除“项目:”项目的位置过滤列表...我是用 TakeWhile 关闭还是关闭?

var group = text.Where(t => t.StartsWith("Group ")))
   .TakeWhile(t => t.ToString().Trim().StartsWith("item"));

【问题讨论】:

  • 创建适合您的域的数据结构比根据关键字过滤列表更有意义。

标签: c# .net linq


【解决方案1】:

与 Jeff Mercado 类似,但不预处理整个可枚举:

public static class Extensions
{
    public static IEnumerable<IList<T>> ChunkOn<T>(this IEnumerable<T> source, Func<T, bool> startChunk)
    {
        List<T> list = new List<T>();

        foreach (var item in source)
        {
            if(startChunk(item) && list.Count > 0)
            {
                yield return list;
                list = new List<T>();
            }

            list.Add(item);
        }

        if(list.Count > 0)
        {
            yield return list;
        }
    }
}

像这样使用:

List<string> text = new List<string>();
text.Add("Group Hear It:");
text.Add("    item: The Smiths");
text.Add("    item: Fernando Sor");
text.Add("Group See It:");
text.Add("    item: Longmire");
text.Add("    item: Ricky Gervais Show");
text.Add("    item: In Bruges");

var chunks = text.ChunkOn(t => t.StartsWith("Group"));

【讨论】:

  • 我没有尝试过提供的每种解决方案,但我非常喜欢这个。它很简单,没有很多开销,并且使用扩展程序调用时干净整洁。我以前不知道“yield return ”;这很甜蜜。
【解决方案2】:

您可以在生成器的帮助下相当干净地完成此操作。生成器将跟踪当前正在使用的密钥,这是在不引入外部变量的情况下使用传统 LINQ 查询无法做到的。您只需要在浏览集合时决定何时更改密钥。获得每个项目使用的密钥后,只需按该密钥对它们进行分组即可。

public static class Extensions
{
    public static IEnumerable<IGrouping<TKey, TResult>> ConsecutiveGroupBy<TSource, TKey, TResult>(
        this IEnumerable<TSource> source,
        Func<TSource, bool> takeNextKey,
        Func<TSource, TKey> keySelector,
        Func<TSource, TResult> resultSelector)
    {
        return
            from kvp in AssignKeys(source, takeNextKey, keySelector)
            group resultSelector(kvp.Value) by kvp.Key;
    }

    private static IEnumerable<KeyValuePair<TKey, TSource>> AssignKeys<TSource, TKey>(
        IEnumerable<TSource> source,
        Func<TSource, bool> takeNextKey,
        Func<TSource, TKey> keySelector)
    {
        var key = default(TKey);
        foreach (var item in source)
        {
            if (takeNextKey(item))
                key = keySelector(item);
            yield return new KeyValuePair<TKey, TSource>(key, item);
        }
    }
}

然后使用它:

var lines = new List<string>
{
    "Group Hear It:",
    "    item: The Smiths",
    "    item: Fernando Sor",
    "Group See It:",
    "    item: Longmire",
    "    item: Ricky Gervais Show",
    "    item: In Bruges",
};

var query = lines.ConsecutiveGroupBy(
    line => line.StartsWith("Group"),
    line => line,
    line => line);

【讨论】:

  • +1:光滑。但是,如果您再次点击相同的组名称,可能不会产生预期的结果:此代码会将这些结果集中到一个组中。
  • 在这种情况下,需要一个更合适的键选择器来区分具有相同键的不同组。例如,将在集合中合并键的索引。上面的代码不能按原样促进这种情况,但根据需要进行调整应该很简单。
  • 我喜欢对迭代器进行分组,但我经常忘记 GroupBy 需要对源中的项目进行完整的枚举和缓存,然后才能返回第一个组。
  • @Patrick:没有它我可以轻松完成。我在这里只选择使用GroupBy,因为它是获取IGrouping&lt;&gt; 对象的最简单方法,无需重新实现它。很遗憾,框架设计者没有提供组的简单公共实现,这都是内部的。
【解决方案3】:

试试这个:

var i = 0;
var groups = text.GroupBy(t => t.StartsWith("Group") ? ++i : i);

i 保存我们看到组条件的次数。使用 i++ 而不是 ++i 会让条件完成一个组而不是启动它。

【讨论】:

  • 你应该真的避免导致这样的副作用的 LINQ 查询;这通常与他们的设计相反。
  • @Servy 在我尝试之前我不确定它是否会起作用。除了失去一点声明的纯洁性,我看不出有什么害处。是否担心声明的评估会改变?
  • 不保证选择器是按顺序调用的;您只是依赖于实现细节。它会改变吗?谁知道呢,这就是实现细节的想法。是否可以在满足方法规范的同时进行更改,是的,肯定可以。
【解决方案4】:

一种方法是使用类并使用 LINQ 从类中获取结果:

    public class MediaItem {
        public MediaItem(string action, string name) {
            this.Action = action;
            this.Name = name;
        }

        public string Action = string.Empty;

        public string Name = string.Empty;

    }

    List<MediaItem> mediaItemList = new List<MediaItem>();
    mediaItemList.Add(new MediaItem("Group: Hear It", "item: The Smiths"));
    mediaItemList.Add(new MediaItem("Group: Hear It", "item: Fernando Sor"));
    mediaItemList.Add(new MediaItem("Group: See It", "item: Longmire"));
    mediaItemList.Add(new MediaItem("Group: See It", "item: Ricky Gervais Show"));
    mediaItemList.Add(new MediaItem("Group: See It", "item: In Bruges"));

    var results = from item in mediaItemList.AsEnumerable()
                  where item.Action == "Group: Hear It"
                  select item.Name;

    foreach (string name in results) {
        MessageBox.Show(name);
    }

另一种方法是单独使用 LINQ:

    // Build the list
    List<string> text = new List<string>();
    text.Add("Group Hear It:");
    text.Add("    item: The Smiths");
    text.Add("    item: Fernando Sor");
    text.Add("Group See It:");
    text.Add("    item: Longmire");
    text.Add("    item: Ricky Gervais Show");
    text.Add("    item: In Bruges");
    text.Add("Group Buy It:");
    text.Add("    item: Apples");
    text.Add("    item: Bananas");
    text.Add("    item: Pears");

    // Query the list and create a "table" to work with
    var table = from t in text
                select new {
                    Index = text.IndexOf(t),
                    Item = t,
                    Type = t.Contains("Group") ? "Group" : "Item",
                    GroupIndex = t.Contains("Group") ? text.IndexOf(t) : -1
                };

    // Get the table in reverse order to assign the correct group index to each item
    var orderedTable = table.OrderBy(i => i.Index).Reverse();

    // Update the table to give each item the correct group index
    table = from t in table
            select new {
                Index = t.Index,
                Item = t.Item,
                Type = t.Type,
                GroupIndex = t.GroupIndex < 0 ?
                    orderedTable.Where(
                        i => i.Type == "Group" &&
                        i.Index < t.Index           
                    ).First().Index :
                    t.GroupIndex
            };

    // Get the "Hear It" items from the list
    var hearItItems = from g in table
                      from i in table
                      where i.GroupIndex == g.Index &&
                      g.Item == "Group Hear It:"
                      select i.Item;

    // Get the "See It" items from the list
    var seeItItems = from g in table
                     from i in table
                     where i.GroupIndex == g.Index &&
                     g.Item == "Group See It:"
                     select i.Item;

    // Get the "Buy It" items I added to the list
    var buyItItems = from g in table
                     from i in table
                     where i.GroupIndex == g.Index &&
                     g.Item == "Group Buy It:"
                     select i.Item;

【讨论】:

    【解决方案5】:

    您可能需要命令式代码来执行此操作,我想不出 LINQ 解决方案。

    List<List<string>> results = new List<List<string>>();
    List<string> currentGroup = null;
    
    foreach (var item in text) 
    {
        if (item.StartsWith("Group")) 
        {
            if (currentGroup != null) results.Add(currentGroup);
            currentGroup = new List<string>();
        }
        currentGroup.Add(item);
    }
    results.Add(currentGroup);
    

    【讨论】:

      【解决方案6】:

      您无法使用干净的 LINQ 和 lambda 表达式来做到这一点。您可以定义一个委托方法来访问外部范围内的布尔标志(例如在执行 this 的实例上),然后将其传递给 select 语句,但我认为普通的 for 循环会更好。如果你这样做,你可以只记下开始和结束索引然后提取范围,这可能是最干净的解决方案。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2018-11-03
        • 2010-10-18
        • 2014-01-27
        • 2021-11-21
        • 2022-07-30
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多