vb.netdatetimetryparse

DateTime.TryParseExact Doesn't Work With Single-Digit Day or Month, Regardless of Format String


I'm trying to implement a TextBox validation for user input of dates. Long story short, I'm building a new application and the users are accustomed to entering dates this way, so I want to try and validate the input while not making them "learn" something new. I have the following event handler code I hook up to date fields to test the input:

Dim DateText As String = String.Empty
Dim ValidDates As New List(Of Date)
Dim DateFormats() As String = {"Mdyy", "Mddyy", "MMdyy", "MMddyy", "Mdyyyy", "Mddyyyy", "MMdyyyy", "MMddyyyy"}

If TypeOf sender Is System.Windows.Forms.TextBox Then
    Dim CurrentField As System.Windows.Forms.TextBox = CType(sender, System.Windows.Forms.TextBox)

    If CurrentField.Text IsNot Nothing AndAlso Not String.IsNullOrEmpty(CurrentField.Text.Trim) Then
        DateText = CurrentField.Text.Trim.ReplaceCharacters(CharacterType.Punctuation)
    End If

    For Each ValidFormat As String In DateFormats
        Dim DateBuff As Date

        If Date.TryParseExact(DateText, ValidFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, DateBuff) Then
            If Not ValidDates.Contains(DateBuff) Then
                ValidDates.Add(DateBuff)
            End If
        End If
    Next ValidFormat

    If ValidDates.Count > 1 Then
        CurrentField.SelectAll()
        CurrentField.HideSelection = False

        MessageBox.Show("The date you entered is ambiguous." & vbCrLf & vbCrLf &
                        "Please enter two digits for the month, two digits for the day and" & vbCrLf &
                        "two digits for the year." & vbCrLf & vbCrLf &
                        "For example, today's date should be entered as either " & Now.ToString("MMddyy") & vbCrLf &
                        " or " & Now.ToString("MM/dd/yy") & ".",
                        "AMBIGUOUS DATE ENTERED", MessageBoxButtons.OK, MessageBoxIcon.Exclamation)

        CurrentField.HideSelection = True
        e.Cancel = True
    ElseIf ValidDates.Count < 1 Then
        CurrentField.SelectAll()
        CurrentField.HideSelection = False

        MessageBox.Show("The date you entered was not valid." & vbCrLf & vbCrLf &
                        "Please enter two digits for the month, two digits for the day and" & vbCrLf &
                        "two digits for the year." & vbCrLf & vbCrLf &
                        "For example, today's date should be entered as either " & Now.ToString("MMddyy") & vbCrLf &
                        " or " & Now.ToString("MM/dd/yy") & ".",
                        "INVALID INPUT FORMAT", MessageBoxButtons.OK, MessageBoxIcon.Exclamation)

        CurrentField.HideSelection = True
        e.Cancel = True
    Else
        CurrentField.ForeColor = SystemColors.WindowText
        CurrentField.BackColor = SystemColors.Window
    End If
End If

This validation method only seems to work correctly if the format includes a two-digit month and two-digit day. If I try to use any of the single-digit formats (e.g., Mddyy, MMdyyyy, etc.), TryParseExact always returns False, and the date is never added to the List(Of Date).

Here are some "hard-coded" tests I went through trying to get to the source of the problem. I've used some intentionally ambiguous dates, as well as some definitively unambiguous ones:

If Date.TryParseExact("1223", "Mdyy", CultureInfo.InvariantCulture, DateTimeStyles.None, TempDate) Then
    Console.WriteLine($"success: 1223 -> {TempDate.ToString("M/d/yyyy")}")
Else
    Console.WriteLine("failed (1223)")
End If
'failed (1223)

If Date.TryParseExact("12123", "Mddyy", CultureInfo.InvariantCulture, DateTimeStyles.None, TempDate) Then
    Console.WriteLine($"success: 12123 -> {TempDate.ToString("M/d/yyyy")}")
Else
    Console.WriteLine("failed (12123)")
End If
'failed (12123)

If Date.TryParseExact("012123", "MMddyy", CultureInfo.InvariantCulture, DateTimeStyles.None, TempDate) Then
    Console.WriteLine($"success: 012123 -> {TempDate.ToString("M/d/yyyy")}")
Else
    Console.WriteLine("failed (012123)")
End If
'success: 012123 -> 1/21/2023

If Date.TryParseExact("1122023", "MMdyyyy", CultureInfo.InvariantCulture, DateTimeStyles.None, TempDate) Then
    Console.WriteLine($"success: 1122023 -> {TempDate.ToString("M/d/yyyy")}")
Else
    Console.WriteLine("failed (1122023)")
End If
'failed (1122023)

If Date.TryParseExact("72521", "Mddyy", CultureInfo.InvariantCulture, DateTimeStyles.None, TempDate) Then
    Console.WriteLine($"success: 72521 -> {TempDate.ToString("M/d/yyyy")}")
Else
    Console.WriteLine("failed (72521)")
End If
'failed (72521)

If Date.TryParseExact("072521", "MMddyy", CultureInfo.InvariantCulture, DateTimeStyles.None, TempDate) Then
    Console.WriteLine($"success: 072521 -> {TempDate.ToString("M/d/yyyy")}")
Else
    Console.WriteLine("failed (072521)")
End If
'success: 072521 -> 7/25/2021

If Date.TryParseExact("3312019", "Mddyyyy", CultureInfo.InvariantCulture, DateTimeStyles.None, TempDate) Then
    Console.WriteLine($"success: 3312019 -> {TempDate.ToString("M/d/yyyy")}")
Else
    Console.WriteLine("failed (3312019)")
End If
'failed (3312019)

If Date.TryParseExact("05201975", "MMddyyyy", CultureInfo.InvariantCulture, DateTimeStyles.None, TempDate) Then
    Console.WriteLine($"success: 05201975 -> {TempDate.ToString("M/d/yyyy")}")
Else
    Console.WriteLine("failed (05201975)")
End If
'success: 05201975 -> 5/20/1975

If Date.TryParseExact("432013", "Mdyyyy", CultureInfo.InvariantCulture, DateTimeStyles.None, TempDate) Then
    Console.WriteLine($"success: 432013 -> {TempDate.ToString("M/d/yyyy")}")
Else
    Console.WriteLine("failed (432013)")
End If
'failed (432013)

I've seen several posts complaining of "unusual behavior" with the TryParseExact() method, but I've not been able to find anything that explains why this is actually happening. I know that I've used some of these parsing methods in the past, but I don't recall ever having this much trouble getting a simple parse to work.

I thought the whole point of using the TryParseExact() method was so that I could tell the parser specifically where the data elements were in the string and get a valid value back. Am I missing or overlooking something here?


MY "SOLUTION":

Based on the explanation from the accepted answer as well as the additional details in the accepted answer from How to convert datetime string in format MMMdyyyyhhmmtt to datetime object?, I believe I've come up with a sort of "work-around" solution that enables me to achieve my goal of allowing my users to continue doing things the way they are used to while still providing the validation I'm looking for.

With this, I can test each of those values for valid dates and make my determination from there.

Here's the updated method:

Public Sub ValidateDateField(ByVal sender As Object, ByVal e As CancelEventArgs)
    Dim DateText As String = String.Empty
    Dim ValidDates As New List(Of Date)
    Dim DateFormats() As String = {"M/d/yy", "M/dd/yy", "MM/d/yy", "MM/dd/yy", "M/d/yyyy", "M/dd/yyyy", "MM/d/yyyy", "MM/dd/yyyy"}
    Dim FormattedDates As New List(Of String)

    If TypeOf sender Is System.Windows.Forms.TextBox Then
        Dim CurrentField As System.Windows.Forms.TextBox = CType(sender, System.Windows.Forms.TextBox)

        If CurrentField.Text IsNot Nothing AndAlso Not String.IsNullOrEmpty(CurrentField.Text.Trim) Then
            'ReplaceCharacters() is a custom extension method
            DateText = CurrentField.Text.Trim.ReplaceCharacters(CharacterType.Punctuation)

            Select Case DateText.Length
                Case < 4
                    CurrentField.SelectAll()
                    CurrentField.HideSelection = False

                    MessageBox.Show("The date you entered was not valid." & vbCrLf & vbCrLf &
                                    "Please enter two digits for the month, two digits for the day and" & vbCrLf &
                                    "two digits for the year." & vbCrLf & vbCrLf &
                                    "For example, today's date should be entered as either " & Now.ToString("MMddyy") & vbCrLf &
                                    " or " & Now.ToString("MM/dd/yy") & ".",
                                    "INVALID INPUT FORMAT", MessageBoxButtons.OK, MessageBoxIcon.Exclamation)

                    CurrentField.HideSelection = True
                    e.Cancel = True
                    Exit Sub
                Case 4
                    FormattedDates.Add(DateText.Insert(1, "/"c).Insert(3, "/"c))
                Case 5
                    FormattedDates.Add(DateText.Insert(1, "/"c).Insert(4, "/"c))
                    FormattedDates.Add(DateText.Insert(2, "/"c).Insert(4, "/"c))
                Case 6
                    FormattedDates.Add(DateText.Insert(1, "/"c).Insert(3, "/"c))
                    FormattedDates.Add(DateText.Insert(2, "/"c).Insert(5, "/"c))
                Case 7
                    FormattedDates.Add(DateText.Insert(1, "/"c).Insert(4, "/"c))
                    FormattedDates.Add(DateText.Insert(2, "/"c).Insert(4, "/"c))
                Case 8
                    FormattedDates.Add(DateText.Insert(2, "/"c).Insert(5, "/"c))
                Case Else
                    CurrentField.SelectAll()
                    CurrentField.HideSelection = False

                    MessageBox.Show("The date you entered was not valid." & vbCrLf & vbCrLf &
                                    "Please enter two digits for the month, two digits for the day and" & vbCrLf &
                                    "two digits for the year." & vbCrLf & vbCrLf &
                                    "For example, today's date should be entered as either " & Now.ToString("MMddyy") & vbCrLf &
                                    " or " & Now.ToString("MM/dd/yy") & ".",
                                    "INVALID INPUT FORMAT", MessageBoxButtons.OK, MessageBoxIcon.Exclamation)

                    CurrentField.HideSelection = True
                    e.Cancel = True
                    Exit Sub
            End Select

            For Each TempDate As String In FormattedDates
                For Each ValidFormat As String In DateFormats
                    Dim DateBuff As Date

                    If DateTime.TryParseExact(TempDate, ValidFormat, System.Globalization.CultureInfo.CurrentCulture, DateTimeStyles.None, DateBuff) Then
                        If Not ValidDates.Contains(DateBuff) Then
                            ValidDates.Add(DateBuff)
                        End If
                    End If
                Next ValidFormat
            Next TempDate

            If DateText.Trim.Length > 0 Then
                If ValidDates.Count > 1 Then
                    CurrentField.SelectAll()
                    CurrentField.HideSelection = False

                    MessageBox.Show("The date you entered is ambiguous." & vbCrLf & vbCrLf &
                                    "Please enter two digits for the month, two digits for the day and" & vbCrLf &
                                    "two digits for the year." & vbCrLf & vbCrLf &
                                    "For example, today's date should be entered as either " & Now.ToString("MMddyy") & vbCrLf &
                                    " or " & Now.ToString("MM/dd/yy") & ".",
                                    "AMBIGUOUS DATE ENTERED", MessageBoxButtons.OK, MessageBoxIcon.Exclamation)

                    CurrentField.HideSelection = True
                    e.Cancel = True
                ElseIf ValidDates.Count < 1 Then
                    CurrentField.SelectAll()
                    CurrentField.HideSelection = False

                    MessageBox.Show("The date you entered was not valid." & vbCrLf & vbCrLf &
                                    "Please enter two digits for the month, two digits for the day and" & vbCrLf &
                                    "two digits for the year." & vbCrLf & vbCrLf &
                                    "For example, today's date should be entered as either " & Now.ToString("MMddyy") & vbCrLf &
                                    " or " & Now.ToString("MM/dd/yy") & ".",
                                    "INVALID INPUT FORMAT", MessageBoxButtons.OK, MessageBoxIcon.Exclamation)

                    CurrentField.HideSelection = True
                    e.Cancel = True
                Else
                    CurrentField.ForeColor = SystemColors.WindowText
                    CurrentField.BackColor = SystemColors.Window
                End If
            End If
        End If
    End If
End Sub

Solution

  • The documentation is clear about this,

    If you do not use date or time separators in a custom format pattern, use the invariant culture for the provider parameter and the widest form of each custom format specifier. For example, if you want to specify hours in the pattern, specify the wider form, "HH", instead of the narrower form, "H".