Validating date ranges in a collection.

Some time ago, I wrote some code to handle colliding date ranges inside a collection.
As simple as it may seem, it's not as straight forward as some may think.
At that time I used some active objects features and worked very well and contemplated many different scenarios.
Since then, I thought about doing the same for CSLA classes, this time in a reusable way. So, here we go:
   
What cases does this cover?
-Two single items inside a collection can't have an overlapping date range (start/end dates).
-Items may or may nor be groupped (we'll come back to this later).

What does this mean?
Well, for the dates themselves we have 3 basic scenarios (with their inverse variations):


That's pretty obvious. So far so good.

Now, what are the "issues"? By issues I don't necessarily mean problematic issues, but more like things you need to take into account.
Issue #1 is that items inside a collection don't have references to each other.
Issue #2 is that you're adding items to the collection.
Issue #3 is that you're removing items from the collection.
Issue #4 is that the data in the items can and probably will change.

Note: For issue #1, you could access the "Parent" property, do some casting and get a reference to every other item, but that's not ideal. In this demo, I'm splitting the responsability of the validation between the items and the collection. A good reason for that is that you don’t want to revalidate a child against all others every time the rules are checked, only when the items are inserted /removed or when they actually change, and not for instance, when you call ValidationRules.CheckRules.

I’ll spare us all from the implementation details and leave that to those of you who are really interested and want to look at the code itself.

It is worth noting that the child has a state variable that tells it whether it’s conflicting or not.
There are 2 props (StartDate and EndDate) that expose the text of both smartdates. Also, for the purposes of testing, you’ll notice that I’m exposing the smart dates as readonly properties too. This is only to make testing easier, and they should be removed. They should not be used directly. (Their main purpose is to make sorting in the sample grid easier)

Now, a last feature that’s really important in all of this is grouping. Most of the times you don’t need to validate an item against every other item. Sometimes, there are categories. Let’s see a project tracker like example:
A project has resource assignments (let’s forget about roles for a sec). Resources may appear more than once in the project, as long as they don’t appear more than once in the same time frame. So, let’s say you assign “Joe” to a project from 2007-01-01 to 2007-02-28, and then from 2007-04-01 to 2007-05-31. That’s a perfectly valid scenario. You have a resource assigned to a project twice but with different start / end dates. The validation should only be run for Joe, and not against all resources.

That’s where grouping comes into play.
The child item has function called “GetGroupIdValue()” that you can override and that returns a default value for all items in case you don’t need grouping.

So, if you were to do this sort of validation by resource, you would override that function and return the assignment’s id in this scenario.
The function returns an object, so you can return anything there. In the case where you have more than one grouping key, you could create an object or structure that contains all the appropriate values and that overrides “Equals()” to correctly handle comparisons.
If your grouping key is subject to change, (For instance, you group by resource and role and the role may change in the child) there’s a delegate that takes care of it. You just need to trigger that delegate in order to notify the collection that the grouping for that item has changed. All you need to do is call “OnGrouppingKeyChanged()” in the base class from your property set and that’s it.

The collection keeps a hashtable of these groups that uses the grouping key as key and contains a List(Of T) that contains all the items in that group. The hashtable is not serialized and is recreated upon deserialization.

The sample code demoes all of these features, so if you’re interested, download the solution and try it out.

Note that I rewrote this from scratch about a month ago, so I might have missed something, If you notice anything strange, let me know.
The demo app uses the error treeview to simplify the ui and to help you find the items with problems faster. All the references are included.
Again, remember that SDate and EDate are only there to simplify sorting, they’re not editable in the grid.

Well, that’s it!! I hope I got you interested!

You can get the code here.

Andrés

posted @ Thursday, May 03, 2007 8:13 PM

Print

Comments on this entry:

# re: Validating date ranges in a collection.

Left by Benjamín Moles at 10/2/2007 11:39 AM
Gravatar
Hi,

I’m afraid you have a BUG in your code. Please try this:

1 Execute your example application.
2 Go to the last row of the grid.
3 Type “18/12/2014” in the StartDate column. See that this change makes a conflict with previous row which is marked as invalid (IsValid = False) but the last row isn’t marked as invalid. The last item is not Validated.

The problem is in “DateRangeCollection.vb“. Here you have a possible solution:

<Serializable()> _
Public Class DisjointDecimalIntervalList(Of T As DisjointDecimalIntervalList(Of T, C), C As DisjointDecimalInterval(Of C))

...

Protected Sub ValidateRange(ByVal typeRange As List(Of C), ByVal addedItem As C)
If typeRange.Count = 0 Then
'nothing to do
Exit Sub
ElseIf typeRange.Count = 1 Then
'only one item can't
typeRange(0).ResetDateRangeOverlap()
typeRange(0).ValidateRange()
Exit Sub
End If
'In order to avoid unnecesary nested loops, we make sure none are marked as overlapping first.
'This will not trigger unnecesary validation routines inside the object.
Dim adding As Boolean = addedItem IsNot Nothing
If Not adding Then
For Each i As C In typeRange
i.ResetDateRangeOverlap()
Next
Else
addedItem.ResetDateRangeOverlap()
End If
Dim total As Integer = typeRange.Count - 1
'If Not adding Then
' total -= 1
'End If
For idxi As Integer = 0 To total - 1
Dim i As C = typeRange(idxi)
If adding Then
If Not ReferenceEquals(i, addedItem) Then
If Not i.DateRangeOverlap Then
If i.IsDateRangeOverlapped(addedItem) Then
addedItem.DateRangeOverlap = True
i.DateRangeOverlap = True
End If
ElseIf Not addedItem.DateRangeOverlap Then
addedItem.DateRangeOverlap = addedItem.IsDateRangeOverlapped(i)
End If
End If
Else
For idxj As Integer = idxi + 1 To total
Dim j As C = typeRange(idxj)
If Not ReferenceEquals(i, j) Then
If Not i.DateRangeOverlap OrElse Not j.DateRangeOverlap Then
If i.IsDateRangeOverlapped(j) Then
i.DateRangeOverlap = True
j.DateRangeOverlap = True
'Exit For
End If
End If
End If
Next
End If
i.ValidateRange()
Next
If adding Then
addedItem.ValidateRange()
Else ' Init ******** Solution to the BUG! ***********
Dim lastItem As C = typeRange(total)
lastItem.ValidateRange()
' End ******** Solution to the BUG! ***********
End If
End Sub
...

Tank you very much for your contribution. It has been very helpful to me.

Benjamin
ben.m.s(at)terra.es


PS. I am from Spain so you can speak (write) in Spanish to me if you want to.

Your comment:



 (will not be displayed)


 
 
 
Please add 6 and 7 and type the answer here:
 

Live Comment Preview:

 
«July»
SunMonTueWedThuFriSat
27282930123
45678910
11121314151617
18192021222324
25262728293031
1234567