Объединить значения свойства на основе другого свойства - PullRequest
4 голосов
/ 06 ноября 2019

У меня есть этот класс

public enum SubStatus { active, inactive, unset}
public class SubItem
{
    public SubStatus Status { get; set; }

    public string Value { get; set; }
}

И значения в классе могут выглядеть следующим образом

List<SubItem> list = new List<SubItem>()
{
    new SubItem() { Status = SubStatus.active, Value = "1" },
    new SubItem() { Status = SubStatus.active, Value = "2" }, //active again, add "2" to the previous
    new SubItem() { Status = SubStatus.inactive, Value = "1" },
    new SubItem() { Status = SubStatus.active, Value = "2" },
    new SubItem() { Status = SubStatus.unset, Value = "2" },
    new SubItem() { Status = SubStatus.unset, Value = "1" }, //unset again, add "1" to the previous
    new SubItem() { Status = SubStatus.unset, Value = "1" } //unset again, add "1" to the previous
};

Теперь мне нужно объединить строковые значения вместе, если Statusпредмета равен Status предыдущего предмета. поэтому в этом примере результат должен выглядеть следующим образом:

List <SubItem> output = new List<SubItem>()
{
    new SubItem() { Status = SubStatus.active, Value = "12" },
    new SubItem() { Status = SubStatus.inactive, Value = "1" },
    new SubItem() { Status = SubStatus.active, Value = "2" },
    new SubItem() { Status = SubStatus.unset, Value = "211" }
};

то, что я пробовал до сих пор, работает, но почему-то я думаю, что этот подход неправильный, и есть лучший способ с linq ...

List<SubItem> result = new List<SubItem>();
SubItem temp = list.FirstOrDefault();
if (temp != null)
{
    foreach (SubItem item in list.Skip(1))
    {
        if (temp.Status == item.Status)
        {
            temp.Value += item.Value;
        }
        else if (temp.Status != item.Status)
        {
            result.Add(temp);
            temp = item;
        }
    }
}
result.Add(temp);   

Ответы [ 3 ]

3 голосов
/ 06 ноября 2019

Стандартный Linq не обеспечивает группировки, подобной этой, но мы можем реализовать ее вручную :

  public static partial class EnumerableExtensions {
    public static IEnumerable<T[]> ToBatch<T>(this IEnumerable<T> source,
                                              Func<ICollection<T>, T, bool> addToBatch) {

      if (null == source)
        throw new ArgumentNullException(nameof(source));
      else if (null == addToBatch)
        throw new ArgumentNullException(nameof(addToBatch));

      List<T> batch = new List<T>();

      foreach (T item in source) {
        // Shall we start a new batch?
        if (batch.Count > 0 && !addToBatch(batch, item)) {
          yield return batch.ToArray();

          batch.Clear();
        }

        batch.Add(item);
      }

      if (batch.Count > 0) // do we have a last batch?
        yield return batch.ToArray();
    }
  }

Теперь мы можем использовать нашу реализацию как

  // Test Data
  List<SubItem> list = new List<SubItem>() {
    new SubItem() { Status = SubStatus.active,   Value = "1" },
    new SubItem() { Status = SubStatus.active,   Value = "2" },   
    new SubItem() { Status = SubStatus.inactive, Value = "1" },
    new SubItem() { Status = SubStatus.active,   Value = "2" },
    new SubItem() { Status = SubStatus.unset,    Value = "2" },
    new SubItem() { Status = SubStatus.unset,    Value = "1" }, 
    new SubItem() { Status = SubStatus.unset,    Value = "1" } 
  };

  ...

  List<SubItem> result = list
    .ToBatch((batch, item) => item.Status == batch.First().Status)
    .Select(batch => new SubItem() {
      Status = batch.First().Status,
      Value  = string.Concat(batch.Select(item => item.Value))
    })
    .ToList();

  // Let's have a look  
  Console.Write(string.Join(Environment.NewLine, result
    .Select(item => $"{item.Status,-8} : {item.Value}")));

Результат:

active   : 12
inactive : 1
active   : 2
unset    : 211
2 голосов
/ 06 ноября 2019

Вы можете использовать Aggregate, но IMO не лучше обычного foreach:

var result = list
    .Aggregate(
        (IEnumerable<SubItem>)Array.Empty<SubItem>(), 
        (result, item) => 
            result.LastOrDefault()?.Status == item.Status
                ? result.SkipLast(1).Concat(new [] { new SubItem { Status = item.Status, Value = result.Last().Value + item.Value } })
                : result.Concat(new [] { item })
    )
    .ToList();

Вот другая идея (с использованием TakeWhile), но она еще сложнее:

var result2 = list
    .Select((item, index) => list
        .Take(index + 1)
        .Reverse()
        .TakeWhile(x => x.Status == item.Status)
        .Select((x, i) => Tuple.Create(x, index - i))
        .Reverse()
    )
    .GroupBy(x => x.Select(y => y.Item2).Min())
    .Select(x => x.Last())
    .Select(x => new SubItem
    {
        Status = x.First().Item1.Status,
        Value = string.Join("", x.Select(y => y.Item1.Value))
    })
    .ToList();
1 голос
/ 06 ноября 2019

Вы не можете делать все это со стандартным LINQ, но вы можете использовать GroupAdjacent из MoreLINQ:

var result = list
    .GroupAdjacent(item => item.Status)
    .Select(grp => new SubItem
    {
        Status = grp.Key,
        Value = string.Concat(grp.Select(item => item.Value))
    });

foreach (var item in result)
{
    Console.WriteLine(item.ToString());
}

Какие группысмежные элементы с помощью Status выбирают новый SubItem, используя Enumerable.Select, а затем объединяют сгруппированные значения, используя String.Concat.

В приведенном выше примере выводится следующее:

Status=active,Value=12
Status=inactive,Value=1
Status=active,Value=2
Status=unset,Value=211

Примечание: Вышеуказанное значение переопределяет метод ToString() для SubItem:

public override string ToString()
{
    return $"Status={Status},Value={Value}";
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...