Always use Nullables for Dates: C# and VB.NET

 

Always use Nullables for dates at the least. Trust me on this. I dogged Nullables for the longest time because I thought they were buggy, then I realized today when testing that I was using them wrong.  Nullables allow you to actually have null values, which for dates is arguably a must.  Why? Keep reading...

 

Tip: Never call Nullable.Value. The nullable item will error out if it is a null value. This is where I was going wrong and thought they were buggy. Just call the nullable item and it will return the value if it exists.

C# 2.0

DateTime? returnDate = null;
DateTime? d = returnDate.Value; //Will error
DateTime? d1 = returnDate; //Will NOT error

VB2005

Dim returnDate As Nullable(Of DateTime) = Nothing 
Dim d As Nullable(Of DateTime) = returnDate.Value  'Will error 
Dim d1 As Nullable(Of DateTime) = returnDate 'Will NOT error 

The second thing to keep in mind is that you can pass the type <T> into a Nullable<T> and it will automatically use it without having to cast.  You can also set a nullable item to another nullable of the same type without calling any functions or properties of the nullable.

C# 2.0

DateTime? d = new DateTime(1900,01,01);

VB2005

Dim d As System.Nullable(Of DateTime) = New DateTime(1900, 1, 1)

You can also check to see if they have a value and even get the value or a default.

C# 2.0

DateTime? testDate = null;
if (testDate.HasValue)
{
}
testDate.GetValueOrDefault();
testDate.GetValueOrDefault(new DateTime(1753,01,01));
 

VB2005

Dim testDate As Nullable(Of DateTime) = Nothing 
If (testDate.HasValue) Then 
End If 
testDate.GetValueOrDefault() 
testDate.GetValueOrDefault(New DateTime(1753, 1, 1)) 

Nullables Play Nice With Databases and NHibernate

NHibernate is nice in that if you have a null value, it will not save it to the database. 

If you have a value of anything NHibernate will attempt to save it.  If you are rolling your own, you have to implement checks based on a date for non nullables. Who wants all of that extra code? 

Why could date checking (or lack thereof) be a problem? What date does DateTime.MinValue give you? What does MS SQLServer require a date to be?  What about Oracle? That's right kiddies, craptastic!  We're suddenly in a world of hurt and we need a bunch of code to check all of our dates. But wait, that's where Nullables come to the rescue!

Removing Dates That are Meant to be Null Values

So let's settle on a date and if we are less than that, we want a null date. It's actually scary simple to implement. Let's settle on anything before 1900 is meant to be null. You  can choose any value you would like here. The naming convention of the helper function isn't quite where I want, what if the date changes? ReSharper (R#) gives me the ability to change that whenever I want. :D

C# 2.0

public class DateHelper
{
    private static readonly DateTime FIRST_GOOD_DATE = new DateTime(1900, 01, 01);
 
    public static DateTime? MapDateLessThan1900ToNull(DateTime? inputDate)
    {
        DateTime? returnDate = null;
 
        if (inputDate >= FIRST_GOOD_DATE)
        {
            returnDate = inputDate;
        }
 
        return returnDate;
    }
 
}

And more importantly, the tests to verify the helper works appropriately (MbUnit):

[TestFixture]
public class As_A_DateHelper
{
    [SetUp]
    public void I_want_to()
    {
    }
 
    [Test]
    public void Verify_MapDateLessThan1900ToNull_returns_null_date_when_passed_a_null_nullable_date()
    {
        DateTime? testDate = null;
        Assert.AreEqual(null, DateHelper.MapDateLessThan1900ToNull(testDate));
    }
 
    [Test]
    public void Verify_MapDateLessThan1900ToNull_returns_null_date_when_passed_null()
    {
        Assert.AreEqual(null, DateHelper.MapDateLessThan1900ToNull(null));
    }
 
    [Test]
    public void Verify_MapDateLessThan1900ToNull_returns_null_date_when_passed_Dec_31_1899()
    {
        DateTime testDate = new DateTime(1899, 12, 31);
        Assert.AreEqual(null, DateHelper.MapDateLessThan1900ToNull(testDate));
    }
 
    [Test]
    public void Verify_MapDateLessThan1900ToNull_returns_date_when_passed_Jan_01_1900()
    {
        DateTime testDate = new DateTime(1900, 01, 01);
        Assert.AreEqual(testDate, DateHelper.MapDateLessThan1900ToNull(testDate));
    }
 
    [Test]
    public void Verify_MapDateLessThan1900ToNull_returns_date_when_passed_normal_date()
    {
        DateTime testDate = new DateTime(2008, 08, 06);
        Assert.AreEqual(testDate, DateHelper.MapDateLessThan1900ToNull(testDate));
    }
 
    [Test]
    public void Verify_MapDateLessThan1900ToNull_returns_null_date_when_passed_nullable_Dec_31_1899()
    {
        DateTime? testDate = new DateTime(1899, 12, 31);
        Assert.AreEqual(null, DateHelper.MapDateLessThan1900ToNull(testDate));
    }
 
    [Test]
    public void Verify_MapDateLessThan1900ToNull_returns_date_when_passed_nullable_Jan_01_1900()
    {
        DateTime? testDate = new DateTime(1900, 01, 01);
        Assert.AreEqual(testDate, DateHelper.MapDateLessThan1900ToNull(testDate));
    }
 
    [Test]
    public void Verify_MapDateLessThan1900ToNull_returns_date_when_passed_normal_nullable_date()
    {
        DateTime? testDate = new DateTime(2008, 08, 06);
        Assert.AreEqual(testDate, DateHelper.MapDateLessThan1900ToNull(testDate));
    }
 
}

VB2005

Public Class DateHelper 
 
    Private Shared ReadOnly FIRST_GOOD_DATE As New DateTime(1900, 1, 1) 
    
    Public Shared Function MapDateLessThan1900ToNull(ByVal inputDate As System.Nullable(Of DateTime)) As System.Nullable(Of DateTime) 
        Dim returnDate As System.Nullable(Of DateTime) = Nothing 
        
        If inputDate >= FIRST_GOOD_DATE Then 
            returnDate = inputDate 
        End If 
        
        Return returnDate 
    End Function 
    
End Class 
 
<TestFixture()> _ 
Public Class As_A_DateHelper 
 
    <SetUp()> _ 
    Public Sub I_want_to() 
    End Sub 
    
    <Test()> _ 
    Public Sub Verify_MapDateLessThan1900ToNull_returns_null_date_when_passed_a_null_nullable_date() 
        Dim testDate As System.Nullable(Of DateTime) = Nothing 
        Assert.AreEqual(Nothing, DateHelper.MapDateLessThan1900ToNull(testDate)) 
    End Sub 
    
    <Test()> _ 
    Public Sub Verify_MapDateLessThan1900ToNull_returns_null_date_when_passed_null() 
        Assert.AreEqual(Nothing, DateHelper.MapDateLessThan1900ToNull(Nothing)) 
    End Sub 
    
    <Test()> _ 
    Public Sub Verify_MapDateLessThan1900ToNull_returns_null_date_when_passed_Dec_31_1899() 
        Dim testDate As New DateTime(1899, 12, 31) 
        Assert.AreEqual(Nothing, DateHelper.MapDateLessThan1900ToNull(testDate)) 
    End Sub 
    
    <Test()> _ 
    Public Sub Verify_MapDateLessThan1900ToNull_returns_date_when_passed_Jan_01_1900() 
        Dim testDate As New DateTime(1900, 1, 1) 
        Assert.AreEqual(testDate, DateHelper.MapDateLessThan1900ToNull(testDate)) 
    End Sub 
    
    <Test()> _ 
    Public Sub Verify_MapDateLessThan1900ToNull_returns_date_when_passed_normal_date() 
        Dim testDate As New DateTime(2008, 8, 6) 
        Assert.AreEqual(testDate, DateHelper.MapDateLessThan1900ToNull(testDate)) 
    End Sub 
    
    <Test()> _ 
    Public Sub Verify_MapDateLessThan1900ToNull_returns_null_date_when_passed_nullable_Dec_31_1899() 
        Dim testDate As System.Nullable(Of DateTime) = New DateTime(1899, 12, 31) 
        Assert.AreEqual(Nothing, DateHelper.MapDateLessThan1900ToNull(testDate)) 
    End Sub 
    
    <Test()> _ 
    Public Sub Verify_MapDateLessThan1900ToNull_returns_date_when_passed_nullable_Jan_01_1900() 
        Dim testDate As System.Nullable(Of DateTime) = New DateTime(1900, 1, 1) 
        Assert.AreEqual(testDate, DateHelper.MapDateLessThan1900ToNull(testDate)) 
    End Sub 
    
    <Test()> _ 
    Public Sub Verify_MapDateLessThan1900ToNull_returns_date_when_passed_normal_nullable_date() 
        Dim testDate As System.Nullable(Of DateTime) = New DateTime(2008, 8, 6) 
        Assert.AreEqual(testDate, DateHelper.MapDateLessThan1900ToNull(testDate)) 
    End Sub 
    
End Class 

That's more test code than actual code.  Some might say it's overkill, but not me. I know this crap works when I'm done. :D 

Notice the naming conventions of the tests. If you are looking for a quick BDD solution, you can't go wrong with using this or a similar naming convention.  With MbUnit, the execution report reads nice. 

As_A_DateHelper.I_want_to.Verify_MapDateLessThan1900ToNull_returns_null_date_when_passed_a_null_nullable_date

You know exactly what is tested without even having to look at the code. I have told you what I am testing, what I am giving it, and what I expect to get back.

Thoughts?

Twitter