首页 > 解决方案 > 如何使用 IEnumerable.GroupBy 比较元素之间的多个属性?

问题描述

如何对“相邻”站点进行分组:

给定数据:

List<Site> sites = new List<Site> {
    new Site { RouteId="A", StartMilepost=0.00m, EndMilepost=1.00m },
    new Site { RouteId="A", StartMilepost=1.00m, EndMilepost=2.00m },
    new Site { RouteId="A", StartMilepost=5.00m, EndMilepost=7.00m },
    new Site { RouteId="B", StartMilepost=3.00m, EndMilepost=5.00m },
    new Site { RouteId="B", StartMilepost=11.00m, EndMilepost=13.00m },
    new Site { RouteId="B", StartMilepost=13.00m, EndMilepost=14.00m },
};

我想要结果:

[
    [
        Site { RouteId="A", StartMilepost=0.00m, EndMilepost=1.00m },
        Site { RouteId="A", StartMilepost=1.00m, EndMilepost=2.00m }
    ],
    [
        Site { RouteId="A", StartMilepost=5.00m, EndMilepost=7.00m }
    ],
    [
        Site { RouteId="B", StartMilepost=3.00m, EndMilepost=5.00m }
    ],
    [
        Site { RouteId="B", StartMilepost=11.00m, EndMilepost=13.00m },
        Site { RouteId="B", StartMilepost=13.00m, EndMilepost=14.00m }
    ]
]

我尝试使用 GroupBy 和自定义比较器函数检查 routeIds 匹配,并且第一个站点的结束里程碑等于下一个站点开始里程碑。我的 HashKey 函数只是检查 routeId,因此路由中的所有站点都会被合并在一起,但我认为比较器会假设 A = B,B = C,然后 A = C,所以 C 不会与 A 分组,B,C 因为在我的邻接情况下,A 不等于 C。

标签: c#linq

解决方案


首先,设Site类为(用于调试/演示)

public class Site {
  public Site() { }

  public string RouteId;
  public Decimal StartMilepost;
  public Decimal EndMilepost;

  public override string ToString() => $"{RouteId} {StartMilepost}..{EndMilepost}";
}

好吧,正如你所看到的,我们必须打破规则:相等必须是传递的,即无论何时

A equals B
B equals C

然后

A equals C

在您的示例中并非如此。但是,如果我们按照我们对站点进行排序StartMilepost,从技术上讲,可以IEqualityComparer<Site>这样实现:

public class MySiteEqualityComparer : IEqualityComparer<Site> {
  public bool Equals(Site x, Site y) {
    if (ReferenceEquals(x, y))
      return true;
    else if (null == x || null == y)
      return false;
    else if (x.RouteId != y.RouteId)
      return false;
    else if (x.StartMilepost <= y.StartMilepost && x.EndMilepost >= y.StartMilepost)
      return true;
    else if (y.StartMilepost <= x.StartMilepost && y.EndMilepost >= x.StartMilepost)
      return true;

    return false;
  }

  public int GetHashCode(Site obj) {
    return obj == null
      ? 0
      : obj.RouteId == null
         ? 0
         : obj.RouteId.GetHashCode();
  }
}

然后GroupBy像往常一样;请注意这OrderBy是必需的,因为这里的比较顺序很重要。假设我们有

A = {RouteId="X", StartMilepost=0.00m, EndMilepost=1.00m}
B = {RouteId="X", StartMilepost=1.00m, EndMilepost=2.00m}
C = {RouteId="X", StartMilepost=2.00m, EndMilepost=3.00m}

在这里A == BB == C(所以如果A, B, C所有项目都在同一个组中)但是A != C(因此A, C, B最终会出现在3组中)

代码:

 List<Site> sites = new List<Site> {
    new Site { RouteId="A", StartMilepost=0.00m, EndMilepost=1.00m },
    new Site { RouteId="A", StartMilepost=1.00m, EndMilepost=2.00m },
    new Site { RouteId="A", StartMilepost=5.00m, EndMilepost=7.00m },
    new Site { RouteId="B", StartMilepost=3.00m, EndMilepost=5.00m },
    new Site { RouteId="B", StartMilepost=11.00m, EndMilepost=13.00m },
    new Site { RouteId="B", StartMilepost=13.00m, EndMilepost=14.00m },
  };

  var result = sites
    .GroupBy(item => item.RouteId)
    .Select(group => group
        // Required Here, since MySiteEqualityComparer breaks the rules
       .OrderBy(item => item.StartMilepost)  
       .GroupBy(item => item, new MySiteEqualityComparer())
       .ToArray())
    .ToArray();

  // Let's have a look
  var report = string.Join(Environment.NewLine, result
    .Select(group => string.Join(Environment.NewLine, 
                                 group.Select(g => string.Join("; ", g)))));

  Console.Write(report);

结果:

A 0.00..1.00; A 1.00..2.00
A 5.00..7.00
B 3.00..5.00
B 11.00..13.00; B 13.00..14.00

推荐阅读