Writing my own IsDate function in VB - vb.net

I have had problems with VB.NET IsDate function - it returns true on string values that are not a valid date, such as "367 7" and "10,600" etc. So I have decided to write my own function and wanted to hear some feedback on possible improvements. Would really appreciate valuable input.
Function IsValidDate(ByVal str As String) As Boolean
str = str.Replace(" ", "").Trim()
'If (str Is Nothing) Then
' Return False
'End If
'If IsNumeric(str) Then
' Return False
'End If
'If Regex.IsMatch(str, "^[A-Za-z ]+$") Then
' Return False
'End If
If Regex.IsMatch(str, "^(0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])[- /.](19|20)\d\d$") Then
''mm/dd/yyyy or mm-dd-yyyy
Return True
ElseIf Regex.IsMatch(str, "^(0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])[- /.]\d\d$") Then
''mm/dd/yy or mm-dd-yy
Return True
ElseIf Regex.IsMatch(str, "^(0[1-9]|[12][0-9]|3[01])[- /.](0[1-9]|1[012])[- /.](19|20)\d\d$") Then
''dd-mm-yyyy or dd/mm/yyyy
Return True
ElseIf Regex.IsMatch(str, "^(19|20)\d\d[- /.](0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])$") Then
''yyyy-mm-dd or yyyy/mm/dd
Return True
End If
Return False
End Function
I took care of a possible space in the numeric string (ex.: "367 7", then I made sure it returns false if the value is numeric or nothing. I also eliminated strings that are all alphanumeric. Please let me know what do you think. Thank you
P.S> Edit: I commented out a few lines in the beginning as I felt they could be eliminated since if the string does not match one of the regexes, it will return false anyway

Have tried something like this?
Function IsValidDate(ByVal str As String) As Boolean
Dim test As Date
If Date.TryParseExact(str, "yyyy/MM/dd HH:mm:ss", System.Globalization.CultureInfo.CurrentCulture, Globalization.DateTimeStyles.None, test) Then
Return True
Else
Return False
End If
End Function
From here you can just change the format in the quotes.
!EDIT!
I can't add comments because of my reputation, but if have your array of formats like in your comment and use my code, instead of the format I have in there, put your array formats in its' place. It worked fine for me.
Dim test As Date
Dim formats As String() = {"MM/dd/yyyy", "MM-dd-yyyy", "MM/dd/yy", "MM-dd-yy", "M-d-yyyy", "M/d/yyyy", "M/d/yyyy h:mm:ss tt", "M/d/yyyy h:mm tt", "MM/dd/yyyy hh:mm:ss", "M/d/yyyy h:mm:ss", "M/d/yyyy hh:mm tt", "M/d/yyyy hh tt", _
"M/d/yyyy h:mm", "M/d/yyyy h:mm", "MM/dd/yyyy hh:mm", "M/dd/yyyy hh:mm"}
If Date.TryParseExact(str, formats, System.Globalization.CultureInfo.CurrentCulture, Globalization.DateTimeStyles.None, test) Then
Return True
Else
Return False
End If
This won't reorganize your input, but rather just a Boolean return to see if it matches the format you specify, which is what you originally wanted correct?

Running into this situation in my own code, I found that the 'TryParseExact' function is significantly slower than using a series of 'Regex.Match' calls.
This is the function that I wrote to check the formats I am looking for and it ran about 10x faster than TryParseExact.
The downside of this over TryParseExact is that it does not check all of the edge cases, such as '31 is not a valid day in November' or '29 is not a valid day in Feb on certain years'. So as long as you are willing to give up those catches, then this is faster.
Public Shared Function ChkDate(ByVal Value As Object) As Boolean
If Value.GetType().Name = "DateTime" Then
Return True
ElseIf Not String.IsNullOrWhiteSpace(Value.ToString()) Then
Dim TempStr As String = Value.ToString()
' Please speed test before making any changes to this function, the below version is much faster than Date.TryParse or Date.TryParseExact
If InStr(TempStr, ".") <> 0 AndAlso TempStr.Split({"."c}).Length = 3 Then ' d.M.yyyy
Dim TempArray() As String = TempStr.Split({"."c})
Dim NewString As String = TempArray(1) & "/" & TempArray(0) & "/" & TempArray(2)
ElseIf InStr(TempStr, "/") <> 0 AndAlso RegularExpressions.Regex.IsMatch(TempStr, "\A[01]{0,1}[0-9]{1}/[0123]{0,1}[0-9]{1}/[0-9]{4}\z") Then ' M/d/yyyy
Return True
ElseIf InStr(TempStr, "-") <> 0 AndAlso RegularExpressions.Regex.IsMatch(TempStr, "\A[0-9]{4}-[01]{0,1}[0-9]{1}-[0123]{0,1}[0-9]{1}\z") Then ' yyyy-M-d
Return True
ElseIf InStr(TempStr, "-") <> 0 AndAlso InStr(TempStr, ":") <> 0 AndAlso RegularExpressions.Regex.IsMatch(TempStr, "\A[0-9]{4}-[01]{0,1}[0-9]{1}-[0123]{0,1}[0-9]{1} [01]{0,1}[0-9]{1}:[01]{0,1}[0-6]{1}:[01]{0,1}[0-6]{1}\z") Then ' yyyy-M-d H:mm:ss
Return True
ElseIf InStr(TempStr, "/") <> 0 AndAlso InStr(TempStr, ":") <> 0 AndAlso RegularExpressions.Regex.IsMatch(TempStr, "\A[01]{0,1}[0-9]{1}/[0123]{0,1}[0-9]{1}/[0-9]{4} [01]{0,1}[0-9]{1}:[01]{0,1}[0-6]{1}\z") Then ' M/d/yyyy h:mm
Return True
ElseIf RegularExpressions.Regex.IsMatch(TempStr, "\A[0-9]{4}[01]{1}[0-9]{1}[0123]{1}[0-9]{1}\z") Then ' yyyyMMdd
Return True
ElseIf RegularExpressions.Regex.IsMatch(TempStr, "\A[0-9]{4}[01]{0,1}[0-9]{1}[0123]{0,1}[0-9]{1}[01]{0,1}[0-9]{1}[01]{0,1}[0-6]{1}\z") Then ' yyyyMMddHHmm
Return True
Else
Return IsDate(TempStr)
End If
End If
Return False
End Function

Rather than using the Regexes, have you looked at DateTime.ParseExact? It lets you define the exact formats you support.

Related

IsDate Function not recognizing date

I've got this code
If IsDate(EndTime) = True Then
Worksheets("Control_File2").Range("D" & lrow2).Value = EndTime
Else
Worksheets("Control_File2").Range("D" & lrow2).Value = "Not Provided"
End If
and currently End Time = "25/08/2021 07:35:47.546" but the code always goes to the "Else" condition.
why is it not recognizing it as a date?
As GSerg pointed out, the milliseconds are not being handled by IsDate().
Function IsDate2(Value As Variant) As Boolean
Rem I added this line to escape empty strings
If Len(Value) = 0 Then Exit Function
IsDate2 = IsDate(Split(Value, ".")(0))
End Function

Find anything but a number or "C"

I have this formula (below) where I am trying to find a space in C1. Instead of this, I would like to update this formula to look for anything except for "C" or any number and not only find a space.
LEFT(C1, find("" "", C1, 1)-1)
For e.g.
if C1 has - "C1234 - XXX" or "C1234-XXX" or "C1234:XXX", I always want the above function to find anything except for "C" and "1234" (i.e. numbers).
P.S.: I would want to use the find function only with improvements to meet the above conditions.
Please suggest.
Perhaps this:
'To create a new string from a source string which will or will not contain the characters present within the source string
'Examples of string of characters: 0123456789 -OR- {}[]<>\/|+*=-_(),.:;?!##$%^&™®©~'" OR - combination of various characters
Public Function getNewStringFromString(ByVal strSource As Variant, ByVal strChars As Variant, Optional isInString As Boolean = True) As String
Dim strArr As Variant, iChar As Variant
getNewStringFromString = ""
If VarType(strSource) = vbString And VarType(strChars) = vbString Then
strSource = Trim(strSource): strChars = Trim(strChars)
If Len(strSource) > 0 And Len(strChars) > 0 Then
strArr = Split(StrConv(strSource, vbUnicode), vbNullChar)
For Each iChar In strArr
If (isInString Xor isInArray(iChar, strChars)) = False Then getNewStringFromString = getNewStringFromString + iChar
Next iChar
Erase strArr
End If
End If
End Function
Use as the following:
MsgBox getNewStringFromString(CStr(Range("C1")), "C0123456789")
Forgot to give you the code for the isInArray function. Here it is:
'To check if an element is within a specific Array, Object, Range, String, etc.
Public Function isInArray(ByVal itemSearched As Variant, ByVal aArray As Variant) As Boolean
Dim item As Variant
If VarType(aArray) >= vbArray Or VarType(aArray) = vbObject Or VarType(aArray) = vbDataObject Or TypeName(aArray) = "Range" Then
For Each item In aArray
If itemSearched = item Then
isInArray = True
Exit Function
End If
Next item
isInArray = False
ElseIf VarType(aArray) = vbString Then
isInArray = InStr(1, aArray, itemSearched, vbBinaryCompare) > 0 'Comparing character by character
Else
On Error Resume Next
isInArray = Not IsError(Application.Match(itemSearched, aArray, False))
Err.Clear: On Error GoTo 0
End If
End Function
Given your data format, where
C is always the first character
subsequent values are all digits
You want to return the C followed by the digits
Try:
="C" & LOOKUP(9E+307,VALUE(MID(A1,2,{1,2,3,4,5,6,7})))
If there might be more than 7 digits, you can either extend the array constant, or use a formula to create a larger array.
The formula looks for the largest integer in the string, starting with position 2. So it will stop at the last non-digit, since anything including a non-digit will return an error.
If the "non-digit" might be your decimal or thousands separator, you will need to replace it with something else, with a nested SUBSTITUTE
Replace . , and space with -
=LOOKUP(1E+307,--SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(MID(A1,2,{1,2,3,4,5,6,7}),",","-"),".","-"),".","-"))
For a VBA solution, I would use regular expressions.
Option Explicit
Function getCnum(str As String)
Dim RE As Object
Const sPat As String = "(^C\d+).*"
Set RE = CreateObject("vbscript.regexp")
With RE
.Global = False
.MultiLine = True
.ignorecase = True
.Pattern = sPat
getCnum = .Replace(str, "$1")
End With
End Function
Note that this also validates the string by checking that the first letter is, in fact, a C (or c). If you want it to be case-sensitive, make the obvious change.

VBA Date stays as American

I am relatively new to VBA, and I need some help on a code I have been writing. Currently, it looks like this:
Sub RoundedRectangle1_Click()
Selection.NumberFormat = "dd mmm yy"
Range("H2").ClearContents
Dim Date1 As ValueChange
Range("H2").Value = InputBox("Enter the first date (Monday) of the week you wish to view, in the format DD/MM")
End Sub
As you can see, I have a pop-up box for the user to manually enter the date, but for some reason, once this is entered it keeps providing an answer in the US format, for instance if I type in 04/12, this will appear as "12 Apr 16", rather than "04 Dec 16"
According to my tests, InputBox returns a string. What I would do, is write the following function (just demo, no error handling in this code):
Private Function ParseDate(sInput As String) As Date
Dim sTmp() As String
sTmp = Split(sInput, "/")
ParseDate = DateTime.DateSerial(2016, sTmp(1), sTmp(0))
End Function
and then simply call it like this:
Dim sResult As String
sResult = InputBox("Enter the first date (Monday) of the week you wish to view, in the format DD/MM")
Range("H2").Value = ParseDate(sResult)
This macro, which asks for the date to be printed at the head of an attendance register works for dd/mm/yy or dd/mm in the 21st century. Could easily be adapted to include 20th cent
Sub Print_Register()
'
' Print_Register Macro
Dim MeetingDate, Answer
Sheets("Register").Select
Range("A1").Select
GetDate:
MeetingDate = DateValue(InputBox("Enter the date of the meeting." & Chr(13) & _
"Note Format" & Chr(13) & "Format DD/MM/YY or DD/MM", "Meeting Date", , 10000, 10000))
If MeetingDate = "" Then GoTo TheEnd
If MeetingDate < 36526 Then MeetingDate = MeetingDate + 36525 'If no yy add year 2000
Range("Current_Meeting_Date") = MeetingDate
Answer = MsgBox("Date OK?", 3)
If Answer = 2 Then GoTo TheEnd
If Answer = 7 Then GoTo GetDate
ExecuteExcel4Macro "PRINT(1,,,1,,,,,,,,2,,,TRUE,,FALSE)"
TheEnd:
End Sub

Allowing date entries later or equal to current date

A column in a sheet that I am working for, accepts date values. What I would like to do is to permit as valid user entries only dates that are equal or come after the current date. So, in a intent of mine a came up with the following:
Dim StageDate As date
If Target.Column = 11 Then
StageDate = InputBox("Enter a Valid Date")
If StageDate <= Date Then Target.value = StageDate
Else: MsgBox("Please enter a valid date")
End If
End If
This doesn't work very nice. Could I ask for your proposals? Thank so much!
May be a bit much. I always like to test that a proper date has been entered first. 1<date will return True as 1 is 01/01/1900 (or is it 31/12/1899)
Public Sub Test()
Dim dateRange As Range
Set dateRange = ThisWorkbook.Worksheets("Sheet1").Range("A2")
If IsDate(dateRange) Then
If dateRange < Date Then
MsgBox "Invalid date", vbInformation + vbOKOnly
dateRange = Null
End If
Else
dateRange = Null
End If
End Sub
'Check that the value entered is a date.
'Returns TRUE/FALSE.
'http://regexlib.com/DisplayPatterns.aspx?cattabindex=4&categoryId=5
'Description:
'DD.MM.YY or DD.MM.YYYY separator could be on choice '.' '/' or '-' leap years compatible, 00 is treated as year 2000.
'Matches
' 29.2.04 | 29/02-2004 | 3.4.05
'Non -Matches
' 29.2.03 | 2902.2004 | 12.31.1975
'Author: Dany Lauener
Public Function IsDate(ADate As Range) As Boolean
Dim RegX As Object
Set RegX = CreateObject("VBScript.RegExp")
RegX.Pattern = "^(((0?[1-9]|[12]\d|3[01])[\.\-\/](0?[13578]|1[02])" & _
"[\.\-\/]((1[6-9]|[2-9]\d)?\d{2}))|((0?[1-9]|[12]\d|30)" & _
"[\.\-\/](0?[13456789]|1[012])[\.\-\/]((1[6-9]|[2-9]\d)?\d{2}))" & _
"|((0?[1-9]|1\d|2[0-8])[\.\-\/]0?2[\.\-\/]((1[6-9]|[2-9]\d)?\d{2}))|" & _
"(29[\.\-\/]0?2[\.\-\/]((1[6-9]|[2-9]\d)?(0[48]|[2468][048]|[13579][26])|" & _
"((16|[2468][048]|[3579][26])00)|00)))$"
IsDate = RegX.Test(ADate)
End Function
You could shorten the `IsDate` function to something like:
Public Function IsDate(ADate As Range) As Boolean
Dim tmpDate As Date
On Error Resume Next
tmpDate = DateValue(ADate)
IsDate = (Err.Number = 0)
On Error GoTo 0
End Function
What you are looking for is DateValue();
https://support.office.com/en-us/article/DATEVALUE-function-df8b07d4-7761-4a93-bc33-b7471bbff252
With this you can compare Dates:
DateValue(TextBoxStartDate.Text) < DateValue(TextBoxEndDate.Text)

Letting VBA convert different date formats

I've got a VBA macro which reads Excel files and needs to process the data in there, including some dates. However, depending on the user who exported these files, the dates in those sheets might be written YYYYMMDD, MM/DD/YYYY, DD.MM.YYYY, M/D/YYYY and so on. All are only formatted as text.
So far, I've tried parsing the string and creating a new date. As I encounter more exotic dates, like M/DD/YYYY or D.MM.YY, my code gets very large and doesn't seem very elegant. I've searched, but I couldn't find any standardised way or function to detect these several date formats and convert them accordingly.
Am I missing something or is simply parsing the string the only reliable way for doing this?
Try this code - it'll convert any of the following formats: DD.MM.YYYY,DD.MM.YY,YYYYMMDD,MM/DD/YYYY,MM/DD/YY,M/D/YYYY, M/D/YY.
You can easily add additional formats, just add more conversion rules to the If...ElseIf... statement.
Option Explicit
Private mStrLastPattern As String
Private mStrSourceDate As String
Private mDatResult As Date
Public Function fctDateFromString(strDate As String) As Date
mStrSourceDate = strDate
mDatResult = 0
If TryConvert("(^\d{2})\.(\d{2})\.(\d{4})$", "$2/$1/$3") Then 'DD.MM.YYYY
ElseIf TryConvert("(^\d{2})\.(\d{2})\.(\d{2})$", "$2/$1/20$3") Then 'DD.MM.YY
ElseIf TryConvert("(^\d{4})(\d{2})\.(\d{2})$", "$2/$3/$1") Then 'YYYYMMDD
ElseIf TryConvert("(^\d{2})/(\d{2})/(\d{4})$", "$1/$2/$3") Then 'MM/DD/YYYY
ElseIf TryConvert("(^\d{2})/(\d{2})/(\d{2})$", "$1/$2/20$3") Then 'MM/DD/YY
ElseIf TryConvert("(^\d{1})/(\d{1})/(\d{4})$", "0$1/0$2/$3") Then 'M/D/YYYY
ElseIf TryConvert("(^\d{1})/(\d{1})/(\d{2})$", "0$1/0$2/20$3") Then 'M/D/YY
End If
If mDatResult = 0 Then Debug.Print "Cannot find matching format for " & strDate
fctDateFromString = mDatResult
End Function
Private Function TryConvert(strFrom As String, strTo As String) As Boolean
If RegExMatch(strFrom) Then
mDatResult = RegExConvert("$1/$2/$3")
TryConvert = (mDatResult <> 0)
End If
End Function
Private Function RegExMatch(strPattern As String) As Boolean
mStrLastPattern = strPattern
With CreateObject("VBScript.RegExp")
.Pattern = strPattern
.IgnoreCase = True
.MultiLine = False
RegExMatch = .Test(mStrSourceDate)
End With
End Function
Private Function RegExConvert(strReplacePattern As String) As Date
On Error Resume Next
With CreateObject("VBScript.RegExp")
.Pattern = mStrLastPattern
.IgnoreCase = True
.MultiLine = False
RegExConvert = CDate(.Replace(mStrSourceDate, strReplacePattern))
If Err.Number Then
Err.Clear
RegExConvert = 0
End If
End With
End Function
Note, that this code will interpret MM.DD.YYYY as DD.MM.YYYY and so on as long as the number of digits matches and the resulting date is valid.