Fervent Coder

Coding Towards Utopia...by Rob Reynolds
posts - 278 , comments - 431 , trackbacks - 0

My Links

News


Rob Reynolds

Subscribe to Fervent Coder RSS
Subscribe to Fervent Coder by Email

About Me

I manage several open source projects. Need...
   ...package management for Windows?
   ...automated builds?
   ...database change management (migrations)?
   ...your application to check email?
   ...a monitoring utility?

I also write for



Like what you are reading? Want to buy me a cup of coffee?
PayPal - The safer, easier way to pay online!

Article Categories

Archives

Post Categories

Image Galleries

Sites

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 on Wednesday, August 6, 2008 9:47 PM | Filed Under [ Code ]

Feedback

Gravatar

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

You don't need to check HasValue before calling GetValueOrDefault().
8/7/2008 7:25 AM | Luke Smith
Gravatar

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

@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.
8/7/2008 10:05 AM | Robz
Gravatar

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

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.
8/7/2008 10:23 AM | developingchris
Gravatar

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

@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?
8/7/2008 11:52 AM | Robz
Gravatar

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

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.
8/7/2008 2:31 PM | developingchris
Gravatar

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

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());
}
8/7/2008 5:59 PM | Robz
Gravatar

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

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
8/7/2008 8:21 PM | Robz
Gravatar

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

I take that back. Strings are nullable by themselves... :D
8/7/2008 8:32 PM | Robz
Gravatar

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

@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

8/8/2008 8:56 AM | Lee Brandt
Gravatar

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

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

tt
8/9/2008 11:27 PM | Troy
Gravatar

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

My problem was with whether to keep the data null or new datetime value, which is 01/01/1900..

Thanks mate.. It does help
3/11/2009 9:12 AM | Discount Codes
Gravatar

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

A thing to remember is that Nullables are performance inefficent. quote:
"One final note has to do with performance. Although a nullable type will act like the underlying value type, it will not perform like the underlying value type. Accessing or modifying the value stored in the object requires dereferencing, which will cause code that uses nullable types to execute slower than code that uses value types. In normal uses of nullable types, the difference in performance is insignificant--not even noticeable. But do not perform extensive operations on nullable types. If you need to do calculations, copy the values from the nullable types to standard value types, perform the calculations, and then copy the results back to the nullable types."

This also means allot of casting from nullable to non-nullable.
Also you should use nullable when storing data to a database when its allowed to be null and Non-nullable when storing to a not null field
9/23/2009 3:30 AM | forgot my name
Gravatar

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

@forgot: Exactly.
9/23/2009 7:08 AM | Robz
Comments have been closed on this topic.

Powered by: