Jon
Jon

Reputation: 40062

Determining value jumps in List<T>

I have a class:

public class ShipmentInformation
{
    public string OuterNo { get; set; }
    public long Start { get; set; }
    public long End { get; set; }

}

I have a List<ShipmentInformation> variable called Results.

I then do:

List<ShipmentInformation> FinalResults = new List<ShipmentInformation>();
var OuterNumbers = Results.GroupBy(x => x.OuterNo);
foreach(var item in OuterNumbers)
{
   var orderedData = item.OrderBy(x => x.Start);

   ShipmentInformation shipment = new ShipmentInformation();
   shipment.OuterNo = item.Key;
   shipment.Start = orderedData.First().Start;
   shipment.End = orderedData.Last().End;

   FinalResults.Add(shipment);
}

The issue I have now is that within each grouped item I have various ShipmentInformation but the Start number may not be sequential by x. x can be 300 or 200 based on a incoming parameter. To illustrate I could have

  1. Start = 1, End = 300
  2. Start = 301, End = 600
  3. Start = 601, End = 900
  4. Start = 1201, End = 1500
  5. Start = 1501, End = 1800

Because I have this jump I cannot use the above loop to create an instance of ShipmentInformation and take the first and last item in orderedData to use their data to populate that instance.

I would like some way of identifying a jump by 300 or 200 and creating an instance of ShipmentInformation to add to FinalResults where the data is sequnetial.

Using the above example I would have 2 instances of ShipmentInformation with a Start of 1 and an End of 900 and another with a Start of 1201 and End of 1800

Upvotes: 4

Views: 267

Answers (4)

Rich O&#39;Kelly
Rich O&#39;Kelly

Reputation: 41767

Try the following:

private static IEnumerable<ShipmentInformation> Compress(IEnumerable<ShipmentInformation> shipments) 
{
  var orderedData = shipments.OrderBy(s => s.OuterNo).ThenBy(s => s.Start);
  using (var enumerator = orderedData.GetEnumerator())
  {
    ShipmentInformation compressed = null;
    while (enumerator.MoveNext())
    {
      var current = enumerator.Current;
      if (compressed == null) 
      {
        compressed = current;
        continue;
      }
      if (compressed.OuterNo != current.OuterNo || compressed.End < current.Start - 1)
      {
        yield return compressed;
        compressed = current;
        continue;
      }
      compressed.End = current.End;
    }

    if (compressed != null)
    {
      yield return compressed;
    }
  }
}

Useable like so:

var finalResults = Results.SelectMany(Compress).ToList();

Upvotes: 4

JDB
JDB

Reputation: 25855

Another LINQ solution would be to use the Except extension method.

EDIT: Rewritten in C#, includes composing the missing points back into Ranges:

class Program
{
    static void Main(string[] args)
    {

        Range[] l_ranges = new Range[] { 
            new Range() { Start = 10, End = 19 },
            new Range() { Start = 20, End = 29 },
            new Range() { Start = 40, End = 49 },
            new Range() { Start = 50, End = 59 }
        };

        var l_flattenedRanges =
            from l_range in l_ranges
            from l_point in Enumerable.Range(l_range.Start, 1 + l_range.End - l_range.Start)
            select l_point;

        var l_min = 0;
        var l_max = l_flattenedRanges.Max();

        var l_allPoints =
            Enumerable.Range(l_min, 1 + l_max - l_min);

        var l_missingPoints =
            l_allPoints.Except(l_flattenedRanges);

        var l_lastRange = new Range() { Start = l_missingPoints.Min(), End = l_missingPoints.Min() };
        var l_missingRanges = new List<Range>();

        l_missingPoints.ToList<int>().ForEach(delegate(int i)
        {
            if (i > l_lastRange.End + 1)
            {
                l_missingRanges.Add(l_lastRange);
                l_lastRange = new Range() { Start = i, End = i };
            }
            else
            {
                l_lastRange.End = i;
            }
        });
        l_missingRanges.Add(l_lastRange);

        foreach (Range l_missingRange in l_missingRanges) {
            Console.WriteLine("Start = " + l_missingRange.Start + " End = " + l_missingRange.End);
        }

        Console.ReadKey(true);
    }
}

class Range
{

    public int Start { get; set; }
    public int End { get; set; }

}

Upvotes: 0

benjer3
benjer3

Reputation: 657

How about this?

List<ShipmentInfo> si = new List<ShipmentInfo>();
si.Add(new ShipmentInfo(orderedData.First()));
for (int index = 1; index < orderedData.Count(); ++index)
{
    if (orderedData.ElementAt(index).Start == 
        (si.ElementAt(si.Count() - 1).End + 1))
    {
        si[si.Count() - 1].End = orderedData.ElementAt(index).End;
    }
    else
    {
        si.Add(new ShipmentInfo(orderedData.ElementAt(index)));
    }
}

FinalResults.AddRange(si);

Upvotes: 1

Rawling
Rawling

Reputation: 50144

If you want something that probably has terrible performance and is impossible to understand, but only uses out-of-the box LINQ, I think this might do it.

var orderedData = item.OrderBy(x => x.Start);
orderedData
    .SelectMany(x => 
        Enumerable
            .Range(x.Start, 1 + x.End - x.Start)
            .Select(n => new { time = n, info = x))
    .Select((x, i) => new { index = i, time = x.time, info = x.info } )
    .GroupBy(t => t.time - t.info)
    .Select(g => new ShipmentInformation {
        OuterNo = g.First().Key,
        Start = g.First().Start(),
        End = g.Last().End });

My brain hurts.

(Edit for clarity: this just replaces what goes inside your foreach loop. You can make it even more horrible by putting this inside a Select statement to replace the foreach loop, like in rich's answer.)

Upvotes: 1

Related Questions