William さんのプロフィール.Net Zoneブログリスト ツール ヘルプ
9月29日

Linq Tip #2 - Find overlapping date ranges


/*
* A deceptively ~simple self-join query to find all date ranges in a table that overlap.
* A date range is defined by the range between InTime and OutTime.
* The "Seq != Seq" filter is required so that each record does not match itself and
* Distinct is required to remove duplicates so we end up with only records
* that overlap date ranges.
* Which means (StartA <= EndB) And (EndA >= StartB)
*/
var rr = (
  from t1 in db.TimeLogs
  from t2 in db.TimeLogs
  where t1.Seq != t2.Seq
  where t1.InTime <= t2.OutTime && t1.OutTime >= t2.InTime
  select t1).Distinct();
ObjectDumper.Write(rr);

Data:
image

Output:

Seq=26  EmpID=10        InTime=9/20/2007        OutTime=9/20/2007       Employee={ }    TotalHours=4    Duration=04:00:00
Seq=27  EmpID=10        InTime=9/20/2007        OutTime=9/20/2007       Employee={ }    TotalHours=3    Duration=03:00:00
Seq=28  EmpID=10        InTime=9/20/2007        OutTime=9/20/2007       Employee={ }    TotalHours=2    Duration=02:00:00


CLR Objects

Here is a generic static method to do this with any IEnumerable<T>. To be generic, our function needs a some help from caller. We need to know what the Start and End dates are in the object. We could handle this by interface or by Functions. The caller can pass simple lambda expressions. We also added a filter function that allows you to pick from the list the record or records you want to restrict matches to. You could add another overload and hide the filter or just pass a lambda that returns true for anything. Example: (T t)=>true

// Takes a list and returns all records that have overlapping time ranges.
public static IEnumerable<T> GetOverlappedTimes<T>(IEnumerable<T> list, Func<T, bool> filter, Func<T,DateTime> start, Func<T, DateTime> end)
{
    // Selects all records that match filter() on left side and returns all records on right side that overlap.
    var overlap = from t1 in list
                  where filter(t1)
                  from t2 in list
                  where !object.Equals(t1, t2) // Don't match the same object on right side.
                  let in1 = start(t1)
                  let out1 = end(t1)
                  let in2 = start(t2)
                  let out2 = end(t2)
                  where in1 <= out2 && out1 >= in2
                  let totover = GetMins(in1, out1, in2, out2)
                  select t2;
    return overlap;
}

public static void TestOverlap()
{
    var tl1 = new TempTimeEntry() { ID = 1, Name = "Bill", In = "1/1/08 1:00pm".ToDate(), Out = "1/1/08 4:00pm".ToDate() };
    var tl2 = new TempTimeEntry() { ID = 2, Name = "John", In = "1/1/08 5:00pm".ToDate(), Out = "1/1/08 6:00pm".ToDate() };
    var tl3 = new TempTimeEntry() { ID = 3, Name = "Lisa", In = "1/1/08 7:00pm".ToDate(), Out = "1/1/08 9:00pm".ToDate() };
    var tl4 = new TempTimeEntry() { ID = 4, Name = "Joe", In = "1/1/08 3:00pm".ToDate(), Out = "1/1/08 8:00pm".ToDate() };
    var tl5 = new TempTimeEntry() { ID = 1, Name = "Bill", In = "1/1/08 8:01pm".ToDate(), Out = "1/1/08 8:00pm".ToDate() };
    var list = new List<TempTimeEntry>() { tl1, tl2, tl3, tl4, tl5 };
    var overlap = GetOverlappedTimes(list, (TempTimeEntry t1)=>t1.ID==1, (TempTimeEntry tIn) => tIn.In, (TempTimeEntry tOut) => tOut.Out);

    Console.WriteLine("\nRecords overlap:");
    foreach (var tl in overlap)
        Console.WriteLine("Name:{0} T1In:{1} T1Out:{2}", tl.Name, tl.In, tl.Out);
    Console.WriteLine("Done");
    /*  Output:
        Records overlap:
        Name:Joe T1In:1/1/2008 3:00:00 PM T1Out:1/1/2008 8:00:00 PM
        Name:Lisa T1In:1/1/2008 7:00:00 PM T1Out:1/1/2008 9:00:00 PM
        Done
     */
}

Linq Tip #1 - Composable queries

 

One of the cooler things about Linq (if it wasn't already cool enough) is that queries
are composable.  This comes in real handy when you want to dynamically add "where"
statements to a query based on some runtime criteria.

Data:
image

var q1 = from t1 in db.TimeLogs
         where t1.EmpID == 10
         where t1.InTime.Value.Hour == 15  // Same as using &&, but just shows multiple where statements.
         select t1;
if (true)  // If condition is true, add another where filter.
    q1 = from t1 in q1
         where t1.OutTime.Value.Hour > 12 // Remember Hour is 0-23
         select t1; 
ObjectDumper.Write(q1); 

The resulting SQL looks like so:

SELECT [t0].[Seq], [t0].[EmpID], [t0].[InTime], [t0].[OutTime]
FROM [dbo].[TimeLogs] AS [t0]
WHERE (DATEPART(Hour, [t0].[OutTime]) > @p0) AND (DATEPART(Hour, [t0].[InTime]) = @p1) AND ([t0].[EmpID] = @p2)
-- @p0: Input Int32 (Size = 0; Prec = 0; Scale = 0) [12]
-- @p1: Input Int32 (Size = 0; Prec = 0; Scale = 0) [15]
-- @p2: Input Int32 (Size = 0; Prec = 0; Scale = 0) [10]
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1

Seq=28  EmpID=10        InTime=9/20/2007        OutTime=9/20/2007

Nice feature.