score:1

Perhaps this is not the best theoretical approach to work with millions of records. However, this is working and can be used as a starting point for further improvements.

class Program
{
    static void Main(string[] args)
    {
        var StartingRecords = new List<Record>()
        {
            new Record(1001930, "A", "IN"),
            new Record(1004901, "B", "IN"),
            new Record(1005192, "A", "OUT"),
            new Record(1012933, "A", "IN"),
            new Record(1014495, "B", "OUT"),
            new Record(1017891, "A", "OUT"),
        };

        var records = StartingRecords.OrderBy(x => x.BadgeId).ThenBy(x => x.Time).ToList();

        var pairs = records.Skip(1).Zip(records, (second, first) => Tuple.Create(first, second)).
        Where(x => x.Item1.BadgeId == x.Item2.BadgeId &&
        x.Item1.Direction == "IN" && x.Item2.Direction == "OUT").
        Select(x => new Pair(x.Item1.BadgeId, x.Item1.Time, x.Item2.Time)).ToList();

        foreach (var pair in pairs)
            Console.WriteLine(pair.BadgeId + "\t" + pair.TimeIn + "\t" + pair.TimeOut);

        Console.Read();
    }
}

class Record
{
    public long Time { get; set; }
    public string BadgeId { get; set; }
    public string Direction { get; set; }

    public Record(long time, string badgeId, string direction)
    {
        Time = time;
        BadgeId = badgeId;
        Direction = direction;
    }
}

class Pair
{
    public string BadgeId { get; set; }
    public long TimeIn { get; set; }
    public long TimeOut { get; set; }

    public Pair(string badgeId, long timeIn, long timeOut)
    {
        BadgeId = badgeId;
        TimeIn = timeIn;
        TimeOut = timeOut;
    }
}

Output: A 1001930 1005192 A 1012933 1017891 B 1004901 1014495

score:1

I'm not sure how efficient or performant this would be, but I think it can be translated by LINQ into SQL so if you are using a database, it may push more of the calculation to the server.

First, group the records by the badges:

var p1 = from p in punches
         group p by p.Badge into pg
         select new {
             Badge = pg.Key,
             Punches = pg.OrderBy(p => p.Time)
         };

Then, for each badge's group of records, go through all the "IN" records and match it with the "OUT" record if it exists:

var p2 = p1.SelectMany(pg => pg.Punches.Where(p => p.Dir == "IN")
                                       .Select(p => new {
                                            pg.Badge,
                                            TimeIn = p.Time,
                                            TimeOut = pg.Punches.Where(po => po.Dir == "OUT" && po.Time > p.Time)
                                                                .FirstOrDefault().Time
                                       }));

Finally, order the result:

var ans = p2.OrderBy(bio => bio.Badge).ThenBy(bio => bio.TimeIn);

Since LINQ to SQL propagates nulls automatically, I think this will handle a missing "OUT" punch for an "IN", but not orphan "OUT" punches.

Another possibility is to use the Select with two parameters to group the punch records in pairs, but that only works with LINQ to Objects so unless you are filtering the data before processing, the millions of records would all be pulled into memory.

For completeness, here is an attempt at it:

var p2 = p1.AsEnumerable()
           .SelectMany(pg => pg.Punches.Select((p, i) => (p, i))
                                       .GroupBy(pi => pi.i / 2, pi => pi.p)
                                       .Select(pp => new {
                                            pg.Badge,
                                            TimeIn = pp.Where(p => p.Dir == "IN").FirstOrDefault()?.Time,
                                            TimeOut = pp.Where(p => p.Dir == "OUT").FirstOrDefault()?.Time
                                       }));

None of this will work very well if your punches aren't well ordered, e.g. you are missing an initial "IN".


Related Articles