Marshal
Marshal

Reputation: 6651

Select only first record within a time interval

I have following table

CREATE TABLE [dbo].[DeviceLogs](
    [DeviceLogId] [int] IDENTITY(1,1) NOT NULL,

    [UserId] [nvarchar](50) NULL,
    [LogDate] [datetime2](0) NULL,
)
GO

Data Sample

   1     1    2013-05-29 11:05:15   //accepted (its the first occurance for userid 1)
   2     1    2013-05-29 11:05:20   //discarded (within 5 mins from 1st record)
   3     1    2013-05-29 11:07:56   //discarded (within 5 mins from 1st record)
   4     1    2013-05-29 11:11:15   //accepted (after 5 mins from 1st occurance)
   5     2    2013-05-29 11:06:05   //accepted (its the first occurance for userid 2)
   6     2    2013-05-29 11:07:18   //discarded (within 5 mins from 1st record)
   7     2    2013-05-29 11:09:38   //discarded (within 5 mins from 1st record)
   8     2    2013-05-29 11:12:15   //accepted (after 5 mins from 1st occurance)

I want to select only records which have occured after 5 mins from previous selected record and including the first record within the dataset

Desired output is

 1     1     2013-05-29 11:05:15   
 4     1     2013-05-29 11:11:15
 5     2     2013-05-29 11:06:05 
 8     2     2013-05-29 11:12:15

I am trying GroupBy but doesn't give date

db.DeviceLogs.GroupBy(g=>new {g.LogDate.Year, 
                              g.LogDate.Month, 
                              g.LogDate.Day, 
                              g.LogDate.Hour, 
                              g.LogDate.Minutes, 
                              g.UserID})
             .Select(s=>new {UserID=s.Key.UserID, s.???});

Thank you in advance.

Upvotes: 8

Views: 1169

Answers (5)

Jodrell
Jodrell

Reputation: 35706

Ok, how about.

var firstDates = db.DeviceLogs.GroupBy(d => d.UserId).ToDictionary(
    g => g.Key,
    g => g.OrderBy(d => d.LogDate).First().LogDate);

db.DeviceLogs.GroupBy(g => new
  {
    v = Math.Floor(SqlMethods.DateDiffMinute(firstDates[d.UserId], g.LogDate) / 5),
    u = g.UserID
  }).Select(s => s.OrderBy(s => s.LogDate).First());

I'm not sure you can use linq to SQL to do this in one query. There is a potential problem if the number of minutes exceeds the max value of a 32bit integer.

Upvotes: 2

Alex
Alex

Reputation: 8937

I can suggest you an SQL solution:

SELECT  [DeviceLogId],[UserId],[LogDate] FROM (
    SELECT *,
          (SELECT top 1 [LogDate] FROM DeviceLogs t2
                      WHERE datediff(minute,t2.logDate,t1.logDate)>5
                      ORDER BY [LogDate] DESC) prev,
          (SELECT TOP 1 [Logdate] FROM DeviceLogs t3 
              WHERE t3.[LogDate]=
                    (SELECT MIN([LogDate])
                     FROM DeviceLogs t4 
                     WHERE t4.[UserId]=t1.[UserId])) first
    FROM DeviceLogs t1 ) tres
WHERE prev IS NOT NULL OR first=logdate

See fiddler http://sqlfiddle.com/#!6/fa74e/50

Upvotes: 1

Jan P.
Jan P.

Reputation: 3297

var result =
    from log in db.DeviceLogs
    let byId = 
        db.DeviceLogs.Where(item => item.UserId == log.UserId)
    let first =
        byId.First(item => item.LogDate == byId.Min(min => min.LogDate))
    where 
        log.Equals(first) || (log.LogDate - first.LogDate).Minutes > 5
    select log;

Upvotes: 5

vborutenko
vborutenko

Reputation: 4443

This is my solution

class Program
{
    static void Main(string[] args)
    {
        List<DeviceLog> list = new List<DeviceLog>
            {
                new DeviceLog() { Id = 1, UserId = 1, LogDate = DateTime.Parse("2013-05-29 11:05:15") },
                new DeviceLog() { Id = 2, UserId = 1, LogDate = DateTime.Parse("2013-05-29 11:05:20") },
                new DeviceLog() { Id = 3, UserId = 1, LogDate = DateTime.Parse("2013-05-29 11:07:56") },
                new DeviceLog() { Id = 4, UserId = 1, LogDate = DateTime.Parse("2013-05-29 11:11:15") },

                new DeviceLog() { Id = 5, UserId = 2, LogDate = DateTime.Parse("2013-05-29 11:06:05") },
                new DeviceLog() { Id = 6, UserId = 2, LogDate = DateTime.Parse("2013-05-29 11:07:18") },
                new DeviceLog() { Id = 7, UserId = 2, LogDate = DateTime.Parse("2013-05-29 11:09:38") },
                new DeviceLog() { Id = 8, UserId = 2, LogDate = DateTime.Parse("2013-05-29 11:12:15") },
            };

        list = list.Where(l => (l.Id == list.Where(g => g.UserId == l.UserId).Min(h => h.Id))
            || (l.LogDate - list.Where(g => g.UserId == l.UserId).OrderBy(m => m.Id).First().LogDate).Minutes > 5 ).ToList();


    }


}

class DeviceLog
{
    public int Id { get; set; }

    public int UserId { get; set; }

    public DateTime LogDate { get; set; }

}

Upvotes: 1

Daan
Daan

Reputation: 118

I´m not sure you can do this with a single LINQ statement, as you need to remember the DateTime of the last record yielded. You can use an iterator block like so:

private static readonly TimeSpan MinimumTimeSpan = new TimeSpan(0,5,0);

IEnumerable<Record> getSparseRecords(IEnumerable<Record> allRecords)
{
    DateTime previous = DateTime.MinValue;
    foreach(var record in allRecords)
    {
        TimeSpan dif = record.DateTime - previous;
        if (dif >= MinimumTimeSpan)
        {
            previous = record.DateTime;
            yield return record;
        }
    }
}

where Record would be a class to represent a single record, which includes a DateTime property of type DateTime (see here). If your LogDate property is of a different type, you might need to change the code a bit.

Upvotes: 1

Related Questions