How to fix Loop Error, when using nestled If - vba

I have written a piece of code, that is meant to check if the value entered is a date or not. That part works fine, but when I added what I thought would have been some validation (such as length of the string entered and if the date was before or today) it becomes an infinite loop that I can not escape.
I have tried the code with out the loop, and it behaves as expected, however when I combine the two the infinite loop returns.
'Checks if the value entered is in a 10 digit date format, after today
Do Until IsDate(DateOfJob)
DateOfJob = InputBox("What is the date the work is to be carried out on ? DD/MM/YYYY")
If Len(DateOfJob) <> 10 Then
DateOfJob = "NotEnoughCharacters"
ElseIf DateOfJob <= Date Then
DateOfJob = "Today"
End If
Loop
I would have expected that the code would have entered the loop, collected the value DateOfJob, then run the test to see if it was
Exactly 10 characters long
Before or today's date
At any point, if it did not pass those two tests, the DateOfJob would be give a text value, which would cause the final IsDate test to fail.
However, I feel it is being passed text regardless of what is entered, and therefor failing the test completely.
Thanks in advance for any help.

The length of the input string makes no difference: either you're looking at a date, or you're not: you want the rest of your code to work with the Date value, not the String representation that the user provided.
See if this works for you:
Public Function GetValidDate() As Variant '/Date
Dim isValid As Boolean
Do While Not isValid
Dim userInput As Variant
userInput = VBA.InputBox(...)
' if user cancelled the prompt; we better not prompt again:
If VarType(userInput) = vbBoolean Then
'if we don't assign the result, we yield a Variant/Empty:
Exit Function
End If
If IsDate(userInput) Then
Dim dateValue As Date
dateValue = CDate(userInput) '<~ we know it's valid at this point
isValid = dateValue > VBA.DateTime.Date
End If
Loop
GetValidDate = dateValue
End Function
Use:
'NOTE: As Date would be a *type mismatch* error if GetValidDate is Variant/Empty.
Dim jobStartDate As Variant
jobStartDate = GetValidDate
If Not IsDate(jobStartDate) Then Exit Sub
Don't trap the user into a loop they can't get out of without providing a valid input value - an InputBox has a Cancel button, and the user will expect it to cancel the operation: don't deny them that ability - gracefully handle it instead.

Thanks for everyone's input.
I went with Mathieu Guindon solution in the end, and just modified it slightly.
Nice little bit of code he wrote :)
Do While Not isValid
Dim userInput As Variant
userInput = VBA.InputBox("What is the date the work is to be carried out on ? DD/MM/YYYY")
' if user cancelled the prompt; we better not prompt again:
If VarType(userInput) = vbBoolean Then
'if we don't assign the result, we yield a Variant/Empty:
End
End If
If IsDate(userInput) Then
Dim dateValue As Date
dateValue = CDate(userInput) '<~ we know it's valid at this point
isValid = dateValue > VBA.DateTime.Date
End If
DateOfJob = dateValue
Loop

Related

Type mismatch in for loop including tests of worksheet cell values

I am receiving a type mismatch error in my VBA macro. Here is the essential part of my code:
Public Function CalculateSum(codes As Collection, ws As Worksheet) As Double
On Error GoTo ErrorHandler
If ws Is Nothing Then
MsgBox ("Worksheet is necessery")
Exit Function
End If
Dim balanceColumnIndex, codesCulumnIndex As Integer
Dim searchStartRow, searchEndRow As Integer
balanceColumnIndex = 17
codesColumnIndex = 4
searchStartRow = 7
searchEndRow = ws.Cells(ws.Rows.Count, codesColumnIndex).End(xlUp).Row
Dim result As Double
result = 0#
For counter = searchStartRow To searchEndRow
If Len(ws.Cells(counter, codesColumnIndex)) > 0 And Len(ws.Cells(counter, balanceColumnIndex)) > 0 And _
IsNumeric(ws.Cells(counter, codesColumnIndex).Value) And IsNumeric(ws.Cells(counter, balanceColumnIndex).Value) Then
If Contains(codes, CLng(ws.Cells(counter, codesColumnIndex).Value)) Then
result = result + ws.Cells(counter, balanceColumnIndex).Value
''' ^^^ This line throws a type-mismatch error
End If
End If
Next counter
CalculateSum = result
ErrorHandler:
Debug.Print ("counter: " & counter & "\ncode: " & ws.Cells(counter, codesColumnIndex).Value & "\namount: " & ws.Cells(counter, balanceColumnIndex).Value)
End Function
Now what happens is that a type-mismatch error occures on the line where current row balance is added to result even though:
searchEndRow equals 129, and somehow counter equals 130
cells under current address are empty, yet somehow they pass test for length and numeric values (I stopped to debug at this point, IsNumeric(ws.Cells(counter, codesColumnIndex).Value) returns true!
Now I am simply confused and I don't know what to do. Please help.
As commenters have noted, Cells(...).Value is a Variant. This means that operators may not apply to .Value the way you expect. For tests using Len or other string operations, expressly convert to a string. For example, instead of Len(ws.Cells(...)), try Len(CStr(ws.Cells(...).Value)). That way you will know that Len is giving you the result you expect.
Similarly, where you add to result, use result = result + CDbl(ws.Cells(...).Value) to make sure you are adding Double values together.
To answer your question regarding errors that happen differently on different computers, what I have most often experienced is that it is the specific data in question. As one of the commenters pointed out, Empty is indeed numeric since it implicitly converts to 0! As a result, IsNumeric(Empty) is True. Using CStr guards against that in your code because IsNumeric(CStr(Empty)) = IsNumeric("") = False. Using IsNumeric(CStr(...)) prevents you from trying to add 0# + "", which is a type mismatch. So perhaps the user has an empty cell that you don't have in your test data, and that's causing the problem. That's not the only possibility, just the one I have encountered most.

Date Validation not working correctly

I am trying to validate a date that is entered into a textbox on a user form. When I enter for example 23/7/15 the code works fine and gives me a value as the date entered is valid, when I enter 32/7/2015 I get a message that I program telling the user that they have entered a wrong date but if I enter 32/7/15 it sets the date in the code to some date in 1932 and as this is a valid date it does not throw the error. Below is the code that I am using. Is there anyway to validate 32/7/15?
Private Function errorCheckingDate(box1, message) As Boolean '<============= Returns a True or a False
If Not IsDate(box1) Then '<============================================= Checks to see if entered value is date or not
MsgBox message, vbExclamation, "Invalid Selection" '<=============== Displays a messgae box if not date
box1.SetFocus '<==================================================== Puts cursor back to the offending box
errorCheckingDate = True '<========================================= Sets function to True
End If
End Function
box1 is just the value of the textbox once it has been converted to a date. Below is the conversion
secondSelectedStr = Format(DateTextBox.value, "dd-mm-yy") '<===== Convert to correct format
Any help would be great.
In addition to the ordering issue, VBA will also try to adapt invalid date ranges to a proper date. For example, this has always been a neat technique to get the X day of the year:
d = DateSerial(2015, 1, X)
X can be really any number here (up to the limits of the data type, of course). If X is 500, it will produce the date 2016-5-14. So the mm/dd vs dd/mm vs yy/mm/dd issue plus the fact that VBA accepts numbers outside valid ranges are both troublesome.
Without range-checking each date component (where you have to consider leap years and whatnot), I think the best solution is to just have VBA create your date and then test it to ensure it's what you're expecting. For example:
' Get the date components (assuming d/m/yy)...
Dim a As Variant
a = Split(strDate, "/")
' Need 3 components...
If UBound(a) <> 2 Then MsgBox "Invalid date": Exit Sub
' Create the date. This will hardly ever fail.
' VBA will create some kind of date using whatever numbers you throw at it.
Dim d As Date
d = DateSerial(a(2), a(1), a(0))
' Make sure the date VBA created matches what we expected...
If Day(d) <> CInt(a(0)) Then MsgBox "Invalid day": Exit Sub
If Month(d) <> CInt(a(1)) Then MsgBox "Invalid month": Exit Sub
If Right$(Year(d), 2) <> a(2) Then MsgBox "Invalid year": Exit Sub
You could throw that into a subroutine (you may have noticed the Exit Sub statements) to validate the date that gets entered. The TextBox_Exit event would work well. That way, you could set Cancel = True to prevent the user from leaving the textbox with an invalid date.
Thanks for the answers guys but I need to accommodate for leap years and other things. As the isdate function works if the date is entered in the form 1/1/2015 I have just used the len() function to make sure that the date entered is of a certain length. Below is the code that I used. Thanks for you help once again. :)
Private Function errorCheckingDate(box1, message) As Boolean '<============= Returns a True or a False
If Not IsDate(box1) Or Len(box1) < 8 Or Len(box1) > 10 Then '<============================================= Checks to see if entered value is date or not
MsgBox message, vbExclamation, "Invalid Selection" '<=============== Displays a messgae box if not date
box1.SetFocus '<==================================================== Puts cursor back to the offending box
errorCheckingDate = True '<========================================= Sets function to True
End If
End Function
Maybe instead of using IsDate you could use your own custom function likened to it.
Function isValidDate(d as String) as Boolean
Dim parts as Variant
Dim months29 as variant
Dim months30 as variant
Dim months31 as variant
months29 = Array(2)
months30 = Array(4, 6, 9, 11)
months31 = Array(1, 3, 5, 7, 8, 10, 12)
parts = Split(d, "-")
if parts(2) mod 4 = 0 then
daysLeapYear = 28
else:
daysLeapYear = 29
end if
if ((IsInArray(parts(0), months29) and parts(1) <= daysLeapYear) or _
(IsInArray(parts(0), months30) and parts(1) < 31) or _
(IsInArray(parts(0), months31) and parts(1) < 32)) and _
parts(2) < 16 then
isValidDate = True
else:
isValidDate = False
end if
end function
Function IsInArray(stringToBeFound As String, arr As Variant) As Boolean
IsInArray = (UBound(Filter(arr, stringToBeFound)) > -1)
End Function
This should cover most cases. Not sure how detailed you feel you need to be. IsInArray courtesy of #JimmyPena.

String.compare returns true when strings aren't equal?

I've seen plenty of questions here about strings that are equal returning as unequal, but trust me to not get that problem.
I have this function.
Protected Sub ChkValidStockCode()
If Not (Voucher.ValidStockCode = "") Then
Dim validcount As Int32 = 0
Dim validproduct As String = Product.GetProductNameByCode(Voucher.ValidStockCode)
For Each rpi As RepeaterItem In rptCart.Items
Dim ProductID As HyperLink = CType(rpi.FindControl("hlProductID"), HyperLink)
Dim ProductName As HyperLink = CType(rpi.FindControl("hlProductName"), HyperLink)
If (String.Compare(Voucher.ValidStockCode.ToString(), ProductID.ToString())) Then
validcount = validcount + 1
End If
Next
If validcount = 0 Then
txtVoucher.Text = "Sorry, this voucher is only valid when purchasing a " & validproduct
failed = True
Exit Sub
End If
End If
End Sub
It's supposed to compare the two strings and increment a validcount integer if they're equal, and then tell you off if it gets to the end of the repeater without finding any matches.
The variables in this test are LT00004 (Voucher.ValidStockCode) and SP08076 (ProductID.ToString())
I have run the code several times, outputting the different strings as the result and can confirm they are what they should be, but when I try to compare them (and I expect validCount to be 0), they return as a match.
What did I do to screw this up?
You probably want String.Equals() and not String.Compare(). Compare is used to order things and not test for equality. What's happening is String.Compare is returning a non-zero number so the condition is being satisfied. The reason for that is because in VB "0" is False but any non-zero number evaluates to true. There's a whole history behind why that's the case but I digress.

inputBox Excel VBA Integer problems

I am new to VBA and I'm trying to create a macro that from a inputBox accepts a number between 0 and 1000 and converts it to hexadecimal. Well it works, but I am struggling to keep the program accepting that range ( 0 - 1000). This is what happens:
If I input -1 it throws a error;
If I input -1001 it throws a FFFFFFFC17;
If I input any value above 1000 it doesn't throw a MsgBox (I am not familiar with causing error on excel for now).
I've done first like this:
Sub DecToHex()
Dim inputDec As Integer
Dim outputHex As String
inputDec = InputBox("Decimal?")
If inputDec <= 1000 And inputDec >= 0 Then
outputHex = Application.WorksheetFunction.Dec2Hex(inputDec)
MsgBox ("Hex: " + outputHex)
Else
MsgBox ("Error! Please define decimal. It must be larger than zero and less than 1001")
inputDec = InputBox("Decimal?")
outputHex = Application.WorksheetFunction.Dec2Hex(inputDec)
MsgBox ("Hex: " + outputHex)
End If
End Sub
But then I thought well inputBox gives me input as string, so maybe I should accept values as string, so I changed:
Dim inputDec As Integer
'Changed to
Dim inputDec As String
Which still did a poorly control on variables ( ie. it accepts -1200, as also 1200 ). So can you point out what am I doing wrong? Maybe it's the Worksheet Function I'm not reading well. I know it's newbie mistake but it's important for me to understand how to control these input variables from inputBox.
You need to declare the inputDec As Variant
You need to Handle the Cancel Button
You need to put the code in a loop so that when user enters an invalid number, the inputbox can pop up again.
You need to use Application.InputBox with Type:=1 so that only numbers can be accepted.
Try this
Sub DecToHex()
Dim inputDec As Variant
Dim outputHex As String
Do
inputDec = Application.InputBox("Decimal?", Type:=1)
'~~> Handle Cancel
If inputDec = "False" Then Exit Do
If inputDec <= 1000 And inputDec >= 0 Then
outputHex = Application.WorksheetFunction.Dec2Hex(inputDec)
MsgBox ("Hex: " + outputHex)
Exit Do '<~~ Exit the loop
Else
MsgBox ("Error! Please define decimal. It must be larger than zero and less than 1001")
End If
Loop
End Sub

VBA 2012 - Need to return an error when a user enters text instead of a numbers

I am taking a VBA class and am completely stuck on this problem. We CANNOT use the masked text box, which would solve this problem. Instead the professor actually wants me to learn the code, can you imagine?
All kidding aside, the user needs to enter a gas price into a text box, then hit calculate to receive the total cost of the trip. There is much more to the interface but will spare you the details. If a user enters anything else number than a positive number with one decimal place, it should return an error. I have figured out 0 or 0000 as well as a negative number such as -3.45. Now I have to get any text or special characters to give me an error as well as something like 34.56.12.45. You never know, a user may feel the need to type in their IP address. The key to the assignment is that I catch all probable user errors.
Here is what I've written for the calculation as well as catch the errors. I have tried the Try/Catch statements as well. Nothing worked but I got the first two parts of the IF statement to work yet always failing on the last IF part until it gets to the calculation.
Private Sub btnCalc_Click(sender As Object, e As EventArgs) Handles btnCalc.Click
Dim Mileage As Decimal
Dim Miles As Decimal
Dim GasPrice As Decimal
Dim Cost As Decimal
If CDec(txtbxGasPrice.Text) = 0 Then
MessageBox.Show("Please enter a positive dollar amount")
txtbxGasPrice.Text = String.Empty
End If
If CDec(txtbxGasPrice.Text) < 0 Then
MessageBox.Show("Please enter a positive dollar amount")
txtbxGasPrice.Text = String.Empty
End If
If Cost = CDec((Miles / Mileage) * GasPrice) Then
Miles = CDec(lblTMiles.Text)
Mileage = CDec(lblMileage.Text)
GasPrice = CDec(txtbxGasPrice.Text)
lblTotalCost.Text = Cost.ToString("C2")
End If
If CBool(txtbxGasPrice.Text = "") Then
MsgBox("You must enter a dollar amount")
End If
*If Not IsNumeric(txtbxGasPrice.Text) Then
MessageBox.Show("Please enter a positive dollar amount")
txtbxGasPrice.Text = String.Empty*
End If
End Sub
'I have placed this at the top, in the middle, at the bottom but no luck. What am I missing?
Appreciate your thoughts - Lauren
This one seems to meet your criteria and pass David's tests:
Function IsValid(txt As String) As Boolean
If Not IsNumeric(txt) Then
Exit Function
End If
If Len(txt) < 2 Then
Exit Function
End If
If Not Mid(txt, Len(txt) - 1, 1) = "." Then
Exit Function
End If
If Not txt > 0 Then
Exit Function
End If
IsValid = True
End Function
This seems like a perfect application of regular expressions, but could be out of scope for this problem, maybe even better though vb has a Decimal.TryParse(or parse) that will take a string and try to parse it to a decimal.
http://msdn.microsoft.com/en-us/library/system.decimal.tryparse.aspx
on a side not I'm not 100% sure how it acts with xx.xx.xx but I'm betting it will fail and help your problem
I'm going to expand on CSgoose's idea to use RegEx, it seems a lot more reliable. I am very, very green when it comes to using RegEx but I try to work on Q's here at SO, so this may not be the optimal pattern to match, but a function like this seems to do the trick when I test a few value.
0/0.0/0.00 = False
-1.5 = False
1.5 = True
5.45 = False
Steve = False
Steve.6 = False
Steve6.58 = False
6.574.2 = False
A value that evaluates to 0 will return false. Negative values return false. Value must have a decimal component, be comprised of any # of digits (this can be tweaked if you want to limit it, eg., to ##.# format, etc.). Matches full text only, so things like IP addresses won't return true, etc.
NOTE This is VBA, but should be easily adaptable for your purposes)
Sub YourSub()
If Not IsMatch(CStr(txtbxGasPrice.Text)) Then
MsgBox "Please ensure that the value you enter is a positive dollar amount, to 1 decimal place!", vbCritical, "Invalid Gas Price Value!"
End IF
End Sub
This function requires enabling reference to Microsoft VBScript Regular Expressions 5.5, or you could use late-binding.
Function IsMatch(str As String) As Boolean
'Tests for a positive numeric value, formatted 0.0 with a mandatory decimal component
' exact match only
Dim re As RegExp
Dim allMatches As MatchCollection
Dim retVal As Boolean
retVal = False 'by default
If Not IsNumeric(str) Then GoTo EarlyExit 'ignore any non-numeric value
Set re = New RegExp
re.Pattern = "\d*\.[0-9]"
Set allMatches = re.Execute(str)
If allMatches.Count = 1 Then
'If there are multiple matches, then I think safe to say it's not a match,
' make sure it's a full string match
If str > 0 Then
retVal = (allMatches(0) = str)
End If
End If
EarlyExit:
Set re = Nothing
IsMatch = retVal
End Function
Update to force, for example, ##.# format, you could do
re.Pattern = "[1-9]?\d\.\d"