1

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.

  • Added a new List(Of String) variable where I store the possible formats for a given input string (I already limit input to numeric, -, or / only)
  • Added a Select Case to inject separators (/) into the string at specific positions based on the string's length
  • Changed the DateFormats() array to use format strings to use in TryParseExact() that include separators (/)

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
G_Hosa_Phat
  • 976
  • 2
  • 18
  • 38
  • 1
    Not an answer I know, but wouldn't it make more sense to use a date edit control over a plain text box? Thus ensure that the date is valid in the first place skipping all this complexity. – Hursey Feb 14 '23 at 23:14
  • 1
    Does this answer your question? [.Net DateTime.ParseExact not working](https://stackoverflow.com/questions/35725678/net-datetime-parseexact-not-working) – GSerg Feb 15 '23 at 00:24
  • 1
    Does this answer your question? [How to convert datetime string in format MMMdyyyyhhmmtt to datetime object?](https://stackoverflow.com/q/35229843/11683) – GSerg Feb 15 '23 at 00:25
  • 1
    Probably worth looking into something like a masking text box here. The trouble is that if they enter day January 12 2022 as 1122022, how does the compiler know it is 1/12/2022 over 11/2/2022. A masked text box can manage that by affixing the proper missing vales while the user inputs them. All this said, a date control would be the best solution that would still allow manual date typing for your users. Sometimes an extra key stroke isn't too much to ask for. – ClearlyClueless Feb 15 '23 at 03:53
  • @Hursey - Would it make more *sense*? Yes. But, as I stated, I'm trying to incorporate my users' already ingrained habits into the design. Based on the documentation link, however, it looks like I might have to push them out of their comfort zone a bit. – G_Hosa_Phat Feb 15 '23 at 15:01
  • @GSerg - The accepted answer in your first link does not, but one of the other answers on that one provides the same MSDN documentation that "explains" it a bit further. Thank you for that link because I hadn't run across that one when I was searching. The accepted answer in your second link *does*, however go into more detail that's helpful, if a bit frustrating. Apparently, this question comes up ***A LOT***. – G_Hosa_Phat Feb 15 '23 at 15:05
  • @ClearlyClueless - Yes, I've considered a few different options for input here, including using a `MonthCalendar` control but, as I initially stated, I'm trying to accommodate my users' habits and usage patterns into the design of this form. It looks like I'm not going to be able to totally achieve this the way I intended, but we'll see. – G_Hosa_Phat Feb 15 '23 at 15:10

1 Answers1

1

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".

dbasnett
  • 11,334
  • 2
  • 25
  • 33
  • While it doesn't make sense to me that the parser is unable to identify the date when using the `ParseExact` and explicitly defining the format with the single-digit day/month, at least that answers my question. I mean, I'm *telling* the parser that the format only has one digit in whichever position, so I would *think* that it would at least try to use that format and, when it succeeds, move on. Regardless, thank you for the documentation link. I must've totally overlooked that. – G_Hosa_Phat Feb 15 '23 at 14:59
  • To expand on that thought, if I were telling it to `TryParse()`, I could understand it having issues with ambiguity. However, since I'm using `TryParseExact()`, I'm explicitly stating that I "*know*" what format the string is in and I want the parser to match that ***exact*** format. If the format specifies a single digit for the day, I wouldn't expect the parser to try and cram two digits into that field under the `TryParseExact()` version. Otherwise, I would've used the simpler `TryParse()` version and saved a few keystrokes. – G_Hosa_Phat Feb 15 '23 at 15:18
  • 1
    This answer [was posted](https://stackoverflow.com/a/35725820/11683) 7 years ago on the [suggested duplicate question](https://stackoverflow.com/q/35725678/11683). – GSerg Feb 15 '23 at 15:27
  • @GSerg - This answer provides the basic information that answers the question, but if my question is going to be flagged a dupe, may I suggest an alternate duplicate, I'd probably prefer the accepted answer on [How to convert datetime string in format MMMdyyyyhhmmtt to datetime object?](https://stackoverflow.com/questions/35229843/how-to-convert-datetime-string-in-format-mmmdyyyyhhmmtt-to-datetime-object) (*which is, itself, the "duplicate" for the suggested duplicate of my question*). That answer provides more detail in regards to the reasoning *why* this documentation exists. – G_Hosa_Phat Feb 15 '23 at 15:48