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?

Print | posted @ Wednesday, August 06, 2008 9:47 PM

Comments on this entry:

Gravatar # re: Always use Nullables for Dates: C# and VB.NET
by Luke Smith at 8/7/2008 7:25 AM

You don't need to check HasValue before calling GetValueOrDefault().
Gravatar # re: Always use Nullables for Dates: C# and VB.NET
by Robz at 8/7/2008 10:05 AM

@Luke: I know. I put that there for the benefit of people who haven't seen the properties before. :D

Thanks for pointing that out though, I should have been more clear up above with the intentions of the code.
Gravatar # re: Always use Nullables for Dates: C# and VB.NET
by developingchris at 8/7/2008 10:23 AM

The problem with using nullables everywhere is its polluting your code, you have to ask something for itself every time you get it. Also passing in the default everywhere where you would like to get that thing, is not very good either. Why not just strive for not using nulls? When its unavoidable, whats wrong with a sentinel value?
From the example provided above, you have all the overhead of dealing with a sentinel value. Plus you have a lot of overhead of asking the thing for itself instead of just using it.
Gravatar # re: Always use Nullables for Dates: C# and VB.NET
by Robz at 8/7/2008 11:52 AM

@developingchris: What is the difference between calling a Nullable<DateTime> nullableDate and DateTime normalDate?
You call it and treat it the same way as you would just a DateTime.

Nullables are meant to be used interchangeably with the value type they are representing.

I was using it wrong before because I was calling nullableDate.Value, which is just wrong, wrong, wrong. It leads to buggy behavior. I don't understand why Microsoft even put that property in there. Just call nullableDate and you will get the DateTime or a null value. That is how it is intended to be used.

DateTime testDate = new DateTime(1899, 12, 31); Assert.AreEqual(null, DateHelper.MapDateLessThan1900ToNull(testDate));
Remember that the MapDateLessThan1900ToNull(ByVal inputDate As System.Nullable(Of DateTime)) accepts a nullable here.

The example above exists to show how to take in data from a source that may have used a sentinel value (and change it to an actual null) because some programmer thought they had to use sentinels at some point.

I would argue that sentinels pollute code, because then new people have to know that the sentinel is a bogus value that is meant for null. Why not just use null?

Nullables don't pollute your code any more than value types, so I am not sure where you were going with this. Maybe you could elaborate more?
Gravatar # re: Always use Nullables for Dates: C# and VB.NET
by developingchris at 8/7/2008 2:31 PM

ok, so after reading again, I think I may be misunderstanding.
Is DateTime? different than Nullable<DateTime>.
I thought they were the same thing, I see you are switching between them. My experience is mainly with "int?" and using its devilry and basically being required to do things like int a = i.HasValue ? i.Value : 0; in order to deal with it everywhere, when in all reality a null int should just be 0. For that matter null.ToString() should just return "" instead of throwing a null reference exception. Because null, shouldn't be some mythical absence of memory, it should be a real object that exists in memory that knows all the sentinel values that IL uses to represent itself.
Gravatar # re: Always use Nullables for Dates: C# and VB.NET
by Robz at 8/7/2008 5:59 PM

You are right, they are interchangeable (Nullable<DateTime> and DateTime?).

With nulls versus default values I guess it really depends on what you are trying to achieve. I am of the mind that there is really a difference between 0 and null (with integer) and so should both be treated as appropriate values.
An application may want to allow an update to 0 for an amount, but may not want to update to null. If you get null coming from somewhere, it may be an exception, or maybe it's expected.

As far as your code example, you could achieve the conversion to a type this way.
int? i = null;
int d = i.GetValueOrDefault();
int d2 = i.GetValueOrDefault(0);
int d3 = i.GetValueOrDefault(25);

The conversion to a nullable is an automatic.
int d = 0;
int? i= d;
int? nullableInt2 = i;

To achieve the non-error, you could call this.

[Test]
public void Verify_GetValueOrDefault_ToString_does_not_error_when_null()
{
int? nullableInt = null;
Assert.AreEqual("0", nullableInt.GetValueOrDefault().ToString());
}
Gravatar # re: Always use Nullables for Dates: C# and VB.NET
by Robz at 8/7/2008 8:21 PM

The point I am think I am trying to get across is that with dates, you either have a date or you don't. You never really have a 01/01/0001. Using 01/01/0001 or 01/01/1753 is a hack for lack of being able to say you don't have a date.

I can understand using an empty string for string (prefer string over string?) and for the most part there are arguments for and against nullables of other types (int, decimal, etc). I am not as concerned with those. Those I would say to use where appropriate (if appropriate).

But with dates it's different. There is no empty date. From the beginning of my development career I have always wondered why you can't have an "empty" date. Why do you have to check it's value differently than other types? Why is date logic a bane? Why can't you just have an empty date?

That's where nullables come in. You can have an "empty" date and the logic gets very simple.

My .02
Gravatar # re: Always use Nullables for Dates: C# and VB.NET
by Robz at 8/7/2008 8:32 PM

I take that back. Strings are nullable by themselves... :D
Gravatar # re: Always use Nullables for Dates: C# and VB.NET
by Lee Brandt at 8/8/2008 8:56 AM

@Robz: I would agree for the most part. The biggest thing is the difference between null and some arbitrary default value. In a data entered scenario it is the difference between: The user entered 0 and the user didn't enter anything.

@developingchris: not sure why it is a pain to check for null values. It should be the equivalent of checking whether the user entered anything or not, which should definitely be part of your code. IMO it's harder (if not sometimes impossible) to check for a default value than for null. It's hard to come up with some standard default value for "No Data Entered". That's basically what null is.

Just my $0.02.

~Lee

Gravatar # re: Always use Nullables for Dates: C# and VB.NET
by Troy at 8/9/2008 11:27 PM

Nice post! Nullables are like a box of chocolates, you never know what you're going to get.

tt

Your comment:

Title:
Name:
Email:
Website:
 
Italic Underline Blockquote Hyperlink
 
 
Please add 2 and 1 and type the answer here:
 
Twitter