Letting VBA convert different date formats - vba

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.

Related

VBA - How to check if a String is a valid hex color code?

To prevent errors, I need to check if a String retrieved from a custom input box is not a valid hex color code. So far I found various solutions for other languages, but none for VBA.
Working on the following code, giving a not hex value input will cause a run time error. That's critical to my project, since I am working on a protected sheet.
Public Function HexWindow(MyCell As String, Description As String, Caption As String)
Dim myValue As Variant
Dim priorValue As Variant
priorValue = Range(MyCell).Value
myValue = InputBox(Description, Caption, Range(MyCell).Value)
Range(MyCell).Value = myValue
If myValue = Empty Then
Range(MyCell).Value = priorValue
End If
tHex = Mid(Range(MyCell).Text, 6, 2) & Mid(Range(MyCell).Text, 4, 2) & Mid(Range(MyCell).Text, 2, 2)
Range(MyCell).Interior.Color = WorksheetFunction.Hex2Dec(tHex)
End Function
How can I set a condition that recognizes a value not being in the format of "#" & 6 characters from 0-9 and A-F in any case?
Couple ways to do this. The easiest way is with a regular expression:
'Requires reference to Microsoft VBScript Regular Expressions x.x
Private Function IsHex(inValue As String) As Boolean
With New RegExp
.Pattern = "^#[0-9A-F]{1,6}$"
.IgnoreCase = True 'Optional depending on your requirement
IsHex = .Test(inValue)
End With
End Function
If for some reason that doesn't appeal to you, you could also take advantage of VBA's permissive casting of hex strings to numbers:
Private Function IsHex(ByVal inValue As String) As Boolean
If Left$(inValue, 1) <> "#" Then Exit Function
inValue = Replace$(inValue, "#", "&H")
On Error Resume Next
Dim hexValue As Long
hexValue = CLng(inValue) 'Type mismatch if not a number.
If Err.Number = 0 And hexValue < 16 ^ 6 Then
IsHex = True
End If
End Function
I would use regular expressions for this. First you must go to Tools-->Referencesin the VBA editor (alt-f11) and make sure this library is checked
Microsoft VBScript Regular Expressions 5.5
Then you could modify this sample code to meet your needs
Sub RegEx_Tester()
Set objRegExp_1 = CreateObject("vbscript.regexp")
objRegExp_1.Global = True
objRegExp_1.IgnoreCase = True
objRegExp_1.Pattern = "#[a-z0-9]{6}"
strToSearch = "#AAFFDD"
Set regExp_Matches = objRegExp_1.Execute(strToSearch)
If regExp_Matches.Count = 1 Then
MsgBox ("This string is a valid hex code.")
End If
End Sub
The main feature of this code is this
objRegExp_1.Pattern = "#[a-z,A-Z,0-9]{6}"
It says that you will accept a string that has a # followed by any 6 characters that are a combination of upper case or lower case strings or numbers 0-9. strToSearch is just the string you are testing to see if it is a valid color hex string. I believe this should help you.
I should credit this site. You may want to check it out if you want a crash course on regular expressions. They're great once you learn how to use them.

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)

Writing my own IsDate function in VB

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.

Run-time error "13": in my VBA excel code

I'm writing a script that will count a numbers of days between few separate dates. I have a data in cell like:
1-In Progress#02-ASSIGNED TO TEAM#22/01/2013 14:54:23,4-On
Hold#02-ASSIGNED TO TEAM#18/01/2013 16:02:03,1-In Progress#02-ASSIGNED
TO TEAM#18/01/2013 16:02:03
That's the info about my transaction status. I want to count the numbers of days that this transaction was in "4-On Hold". So in this example it will be between 18/01/2013 and 22/01/2013.
I wrote something like this(sorry for ma native language words in text)
Sub Aktywnywiersz()
Dim wiersz, i, licz As Integer
Dim tekstwsadowy As String
Dim koniectekstu As String
Dim pozostalytekst As String
Dim dataztekstu As Date
Dim status4jest As Boolean
Dim status4byl As Boolean
Dim datarozpoczecia4 As Date
Dim datazakonczenia4 As Date
Dim dniw4 As Long
wiersz = 2 'I start my scrypt from second row of excel
Do Until IsEmpty(Cells(wiersz, "A")) 'this should work until there is any text in a row
status4jest = False 'is status 4-On Hold is now in a Loop
status4byl = False 'is status 4-On Hold was in las loop
dniw4 = 0 ' numbers od days in 4-On Hold status
tekstwsadowy = Cells(wiersz, "H").Value2 'grabing text
tekstwsadowy = dodanieprzecinka(tekstwsadowy) 'in some examples I had to add a coma at the end of text
For i = 1 To Len(tekstwsadowy)
If Right(Left(tekstwsadowy, i), 1) = "," Then licz = licz + 1 'count the number of comas in text that separates the changes in status
Next
For j = 1 To licz
koniectekstu = funkcjaliczeniadni(tekstwsadowy) 'take last record after coma
Cells(wiersz, "k") = koniectekstu
dataztekstu = funkcjadataztekstu(koniectekstu) 'take the date from this record
Cells(wiersz, "m") = dataztekstu
status4jest = funkcjaokreslenia4(koniectekstu) 'check if there is 4-On Hold in record
Cells(wiersz, "n") = status4jest
If (status4byl = False And staus4jest = True) Then
datarozpoczecia4 = dataztekstu
status4byl = True
ElseIf (status4byl = True And staus4jest = False) Then
datazakonczenia4 = dataztekstu
status4byl = False 'if elseif funkcion to check information about 4-On Hold
dniw4 = funkcjaobliczeniadniw4(dniw4, datazakonczenia4, datarozpoczecia4) 'count days in 4-On Hold
Else
'Else not needed...
End If
tekstwsadowy = resztatekstu(tekstwsadowy, koniectekstu) 'remove last record from main text
Next
Cells(wiersz, "L") = dniw4 ' show number of days in 4-On Hold status
wiersz = wiersz + 1
Loop
End Sub
Function funkcjaliczeniadni(tekstwsadowy As String)
Dim a, dl As Integer
dl = Len(tekstwsadowy)
a = 0
On Error GoTo errhandler:
Do Until a > dl
a = Application.WorksheetFunction.Find(",", tekstwsadowy, a + 1)
Loop
funkcjaliczeniadni = tekstwsadowy
Exit Function
errhandler:
funkcjaliczeniadni = Right(tekstwsadowy, dl - a)
End Function
Function dodanieprzecinka(tekstwsadowy As String)
If Right(tekstwsadowy, 1) = "," Then
dodanieprzecinka = Left(tekstwsadowy, Len(tekstwsadowy) - 1)
Else
dodanieprzecinka = tekstwsadowy
End If
End Function
Function resztatekstu(tekstwsadowy, koniectekstu As String)
resztatekstu = Left(tekstwsadowy, Len(tekstwsadowy) - Len(koniectekstu))
End Function
Function funkcjadataztekstu(koniectekstu As String)
funkcjadataztekstu = Right(koniectekstu, 19)
funkcjadataztekstu = Left(funkcjadataztekstu, 10)
End Function
Function funkcjaobliczeniadniw4(dniw4 As Long, datazakonczenia4 As Date, datarozpoczecia4 As Date)
Dim liczbadni As Integer
liczbadni = DateDiff(d, datarozpoczecia4, datazakonczenia4)
funkcjaobliczaniadniw4 = dniw4 + liczbadni
End Function
Function funkcjaokreslenia4(koniectekstu As String)
Dim pierwszyznak As String
pierwszyznak = "4"
If pierszyznak Like Left(koniectekstu, 1) Then
funkcjaokreslenia4 = True
Else
funkcjaokreslenia4 = False
End If
End Function
And for now I get
Run-time error "13"
in
dataztekstu = funkcjadataztekstu(koniectekstu) 'take the date from this record
I would be very grateful for any help.
You are getting that error because of Type Mismatch. dataztekstu is declared as a date and most probably the expression which is being returned by the function funkcjadataztekstu is not a date. You will have to step through it to find what value you are getting in return.
Here is a simple example to replicate that problem
This will give you that error
Option Explicit
Sub Sample()
Dim dt As String
Dim D As Date
dt = "Blah Blah"
D = getdate(dt)
Debug.Print D
End Sub
Function getdate(dd As String)
getdate = dd
End Function
This won't
Option Explicit
Sub Sample()
Dim dt As String
Dim D As Date
dt = "12/12/2014"
D = getdate(dt)
Debug.Print D
End Sub
Function getdate(dd As String)
getdate = dd
End Function
If you change your function to this
Function funkcjadataztekstu(koniectekstu As String)
Dim temp As String
temp = Right(koniectekstu, 19)
temp = Left(temp, 10)
MsgBox temp '<~~ This will tell you if you are getting a valid date in return
funkcjadataztekstu = temp
End Function
Then you can see what that function is returning.
I tried running your code, but it is a little difficult to understand just what it is that you want to do. Part of it is the code in your language, but the code is also hard to read beacuse of the lack of indentation etc. :)
Also, I do not understand how the data in the worksheet looks. I did get it running by guessing, though, and when I did I got the same error you are describing on the second run of the For loop - that was because the koniectekstu string was empty. Not sure if this is your problem, so my solution is a very general.
In order to solve this type of problem:
Use Option Explicit at the top of your code module. This will make you have to declare all variables used in the module, and you will remove many of the problems you have before you run the code. Eg you are declaring a variable status4jest but using a different variable called staus4jest and Excel will not complain unless you use Option Explicit.
Declare return types for your functions.
Format your code so it will be easier to read. Use space before and after statements. Comment everything! You have done some, but make sure a beginner can understand. I will edit you code as an example of indentation.
Debug! Step through your code using F8 and make sure all variables contain what you think they do. You will most likely solve your problem by debugging the code this way.
Ask for help here on specific problems you run into or how to solve specific problems, do not send all the code and ask why it is not working. If you break down your problems into parts and ask separately, you will learn VBA yourself a lot faster.
A specific tip regarding your code: look up the Split function. It can take a string and make an array based on a delimiter - Example: Split(tekstwsadowy, ",") will give you an array of strings, with the text between the commas.
Did I mention Option Explicit? ;)
Anyway, I hope this helps, even if I did not solve the exact error you are getting.

Make sure that a string is exactly a 5 digit number

I want to return true if some strings = 'XXXXX'
Where every X is a number 0 through 9
I know there must be a dozen ways to do this but I would like to know the best way.
yourString Like "#####"
If you want the simplest way, you can go with this:
Function MyFunction(myString As String) As Boolean
MyFunction = ((Len(myString) = 5) And (IsNumeric(myString)))
End Function
If you want the more efficient way, you'd have to run some tests for the different methods people suggested.
Edit: The previous solution doesn't work well (see the first 2 comments) but I'm letting it there since it's what has been accepted. Here is what I would do :
Function MyFunction(myString As String) As Boolean
Dim myDouble As Double
Dim myLong As Long
myDouble = Val(myString)
myLong = Int(myDouble / 10000)
MyFunction = ((Len(myString) = 5) And (myLong > 0) And (myLong < 10))
End Function
There is no error "protection" in that function, so if you try to check a too large number like 22222222222222, it will not work.
Similar question previously asked: link text
Basically want to check
(Len(s) = 5) And IsNumeric(s)
You can also use regular expressions to solve this problem. If you include Microsoft VBScript Regular Expressions 5.5 in your VBA project, you can use RegExp and MatchCollection variables as in the function below. (This is a modification of the response to this post at ozgrid.com.)
Public Function FiveDigitString(strData As String) As Boolean
On Error GoTo HandleError
Dim RE As New RegExp
Dim REMatches As MatchCollection
With RE
.MultiLine = False
.Global = False
.IgnoreCase = True
.Pattern = "^[0-9][0-9][0-9][0-9][0-9]$"
End With
Set REMatches = RE.Execute(strData)
If REMatches.Count = 1 Then
FiveDigitString = True
Else
FiveDigitString = False
End If
Exit Function
HandleError:
Debug.Print "Error in FiveDigitString: " & Err.Description
FiveDigitString = False
End Function