views:

1272

answers:

7

We have a a VB6 app (in a COM component) which uses CDate() to take a string and cast it to a Date, for storing in a database.

Depending on if we want the application to talk in dd/MM/yy or MM/dd/yy for example, we have to change the regional settings of the identity user for the COM application. (Right now the only option we have is a nasty hack.)

We have a date format string which is used for formatting all output dates, and it is assumed that the date

If this were .NET we would use DateTime.ParseExact and be away laughing. Calling out to a COM object written in .NET for this sole purpose is an option. Is there a different or better option, involving some black magic around the Format command, or a long reusable function that tokenizes the date depending on the format string, etc?

A: 

You should talk with a date variable (not a formatted string) and then show formatted value in the form using the user regional settings or take the input from the user to a date variable.

Eduardo Molteni
We are taking input from the user via the web.
crb
+1  A: 

I don't know of an easy solution. You could Split the input string into sub-strings by the delimiter, and then use DateSerial to recombine the year, month and hour numbers into a native VB6 Date variable. Something like this below. If you need to support a lot of locales, this could get complicated (see Bob's answer). Mind you, so would using DateTime.ParseExact.

sInput = "1/3/71"
Dim splt() As String
splt = Split(sInput, "/")
dte = DateSerial(splt(2) + 1900, splt(1), splt(0))  ' dd/mm/yy'
MarkJ
+1  A: 

You can use the built-in Format function to do this for you.

Here is a simple test to confirm this:

Public Sub TestDateParsing()

   'On my computer, the date format is U.S. (mm/dd/yyyy)'
   'This test creates a date string in dd/mm/yyyy format to'
   'simulate user input in a different format'

    Const TEST_DATE As Date = #6/1/2009#

    Dim inputDate As String
    inputDate = Format(TEST_DATE, "dd/mm/yyyy")
    'inputDate is "1/6/2009" (June 1 in dd/mm/yyyy format)'

    Debug.Print Format(inputDate, "dd/mm/yyyy")
    'It`s magic! The above line will print 6/1/2009'
    'which is the correct format for my Regional Settings'

End Sub

It might seem like magic, but it isn't. It takes advantage of how the Format function works in conjunction with the current regional settings.

For example, suppose your Regional Settings are configured to use the "mm/dd/yyyy" format for dates.

Now, you get a date string from a user in "dd/mm/yyyy" format. If you Format this date string and tell Format to also use "dd/mm/yyy", it will swap the month and day parts of the date because your settings say dates are in "mm/dd/yyyy" format.

In other words, Format always assumes the date string from the user is formatted according to your current Regional Settings (in this case, "mm/dd/yyyy"), so when you tell it to format the date using "dd/mm/yyyy", it will force it to swap the month and day parts. If your Regional Settings use the same format as the user-provided date, this code will still work: Format will simply return the user date unchanged. Confused yet? ;)

The same thing will happen if your Regional Settings are set for "dd/mm/yyyy" and the user sends a date in "mm/dd/yyyy" format.

The catch is that you have to know ahead of time which format the user is sending dates in. They can't start mixing and matching date formats (and they shouldn't be anyway).


EDIT (by MarkJ) - just to prove that Mike's code can convert a string to Date. Mike, please roll back or change this edit if you want.

Public Sub Test()
  Dim dte As Date
  For dte = #1/1/2009# To #12/31/2009#
    Call TestDateParsing(dte)
  Next dte
End Sub

Public Sub TestDateParsing(ByVal dteIn As Date)

  'On my computer, the date format is U.S. (mm/dd/yyyy)'
  'This test creates a date string in dd/mm/yyyy format to'
  'simulate user input in a different format'

  Dim sExpected As String
  sExpected = Day(dteIn) & " / " & Month(dteIn) & " / " & Year(dteIn)
  Dim inputDate As String
  Dim dte As Date
  inputDate = Format(dteIn, "dd/mm/yyyy")
  dte = Format(inputDate, "dd/mm/yyyy")

  Debug.Assert sExpected = Day(dte) & " / " & Month(dte) & " / " & Year(dte)
  Debug.Print sExpected

End Sub
Mike Spross
You're right, this makes no sense at all. :) I have the date in a String and I have the format string in a String, and I want to get a DateTime object. Also, your code looks like VB.NET more than VB6?
crb
Yeah, it's hard to explain ;). It does work, but it will only work for your specific use case. A more general solution would unfortunately mean rolling your own DateParseExact function in VB6. There is no built-in equivalent of DateParseExact in VB6.
Mike Spross
I'll try to explain better: if the receiving computer uses "mm/dd/yy" format, and you get a string in "dd/mm/yy", then calling Format with "dd/mm/yy" for the second parameter will cause Format to format the "dd/mm/yy" in "mm/dd/yy" format, because Format assumes the "dd/mm/yy" string is actually in "mm/dd/yy" format already (it goes by the current Regional Settings), therefore to convert it to "dd/mm/yy", Format will swap the mm and dd components of the date, which gives you a "mm/dd/yyy" formatted result. Yeah, I'm not sure I'm explaining it any better ;)
Mike Spross
This does work, although I'm not sure why. I'd have thought it wouldn't like day numbers>12, but it does. Mike I've edited your answer, adding a modified version that does convert string to date. Please do change/roll back my edit if you like.
MarkJ
@MarkJ: It works with day numbers>12 because Format doesn't validate the input. Format isn't really a date-formatting function per se. It's a general string-formatting function that happens to know about date formats.
Mike Spross
+1  A: 

Look, there's no easy way to say this - you're screwed. If you accept freeform input from the web, you have to live with the reality that people around the world format dates differently. That's why so many websites use popup calendars, and such, to get user input. No ambiguity there. No matter what you think, .NET's library routines can't fathom your user's intentions any better than any other library.

Fwiw, the code Mike posted is absolutely VB6. I'm not sure what about it looks like VB.NET? Once you get a date/time into a Date variable, you can display it however you want with Format(). That's the easy part.

I'd strongly suggest you either A) find a way to gather your inputs unambiguously, or B) tell your users what format you expect and live with what they input. All that said, is it possible I misunderstood the question, and you really do know what format the user is providing the data in? (Because if so, I really am having trouble understanding what's the problem interpreting it in ClassicVB - sorry.)

Karl E. Peterson
+1  A: 

You mention in the comments to one of the other answers that you are taking input from the web.

In that case you can control the date format that is being submitted by restricting user input to <select> drop lists. Make the month box list the short or long month names Jan/January Feb/February etc and then construct a date string in the format "1 Jan 2009"

Then it doesn't matter what your locale settings are, you'll get the date the user intended.

Gareth Simpson
+2  A: 

This should be close, though it hardcodes the delimiter as "/" and windows YY years at 50:

Private Function ParseDate(ByVal DateString As String, _
                           ByVal DatePattern As String) As Date
    'DateString:  i/j/k formatting.
    'DatePattern: i/j/k formatting, each to be:
    '               M or MM for month position.
    '               D or DD for day position.
    '               YY or YYYY for year position, if YY
    '                 then century windowed at 50.
    Dim strStringParts() As String
    Dim strPatternParts() As String
    Dim intPart As Integer, intScore As Integer
    Dim intMonth As Integer, intDay As Integer, intYear As Integer
    Const DELIM As String = "/"
    Const YYWINDOW As Integer = 50

    strStringParts = Split(DateString, DELIM)
    strPatternParts = Split(UCase$(DatePattern), DELIM)
    For intPart = 0 To UBound(strStringParts)
        If intPart > UBound(strPatternParts) Then
            Err.Raise 5, "ParseDate"
        End If
        Select Case strPatternParts(intPart)
            Case "M", "MM"
                intMonth = CInt(strStringParts(intPart))
                intScore = intScore Or &H1
            Case "D", "DD"
                intDay = CInt(strStringParts(intPart))
                intScore = intScore Or &H2
            Case "YY"
                intYear = CInt(strStringParts(intPart))
                If 0 > intYear Or intYear > 99 Then
                    Err.Raise 5, "ParseDate"
                End If
                intYear = intYear + IIf(intYear < YYWINDOW, 2000, 1900)
                intScore = intScore Or &H4
            Case "YYYY"
                intYear = CInt(strStringParts(intPart))
                If 100 > intYear Or intYear > 9999 Then
                    Err.Raise 5, "ParseDate"
                End If
                intScore = intScore Or &H4
            Case Else
                Err.Raise 5, "ParseDate"
        End Select
    Next
    If intScore = &H7 Then
        ParseDate = DateSerial(intYear, intMonth, intDay)
    Else
        Err.Raise 5, "ParseDate"
    End If
End Function

Validation may not be perfect, but it ought to be close. It throws "Invalid procedure call or argument (Error 5)" on bad inputs.

Bob Riemersma
+1. I've only read it, not tested it, but it looks perfect. Maybe StackOverflow needs some way to post unit tests.
MarkJ
@MarkJ: Ha. I've thought the same thing at times. Who wants to start writing SOUnit? ;)
Mike Spross
I suppose the date string parts could be tested for numeric as well. Maybe other validations would be important too if handling real-world input from users. Amazing how adding error checks balloons any code though.
Bob Riemersma
+1  A: 

Another way to go:

Public Enum abDateType
    abMDY
    abDMY
End Enum

Public Function MakeDate(ByVal dateString As String, ByVal dateType As abDateType, Optional delimiter As String = "/") As Date
    Dim strVals() As String
    Dim dtRtnVal As Date
    strVals = Split(dateString, delimiter)
    Select Case dateType
    Case abMDY
        dtRtnVal = DateSerial(strVals(2), strVals(0), strVals(1))
    Case abDMY
        dtRtnVal = DateSerial(strVals(2), strVals(1), strVals(0))
    Case Else
        Err.Raise vbObjectError, , "Unexpected date format."
    End Select
    MakeDate = dtRtnVal
End Function
Oorang