as a newcomer to VBA any help would be appreciated. The basic point of my program is to loop through columns of the spreadsheet and count the number of non-blank cells in each column, within a specified range.
Here is an example of what my spreadsheet looks like.
1
2
3
1
thing
2
thing
3
thing
When all the cells in the column are blank, VBA throws out a 1004 error, no cells found. What I want to do is say, if a 1004 error occurs, set the count of the non-blank cells (nonBlank = 0) equal to zero, and if no error occurs, count normally. In something like Python, I'd use try/except. Here is my attempt.
For i = 1 To 3
On Error Resume Next
Set selec_cells = Sheet1.Range(Sheet1.Cells(FirstRow, i), Sheet1.Cells(LastRow, i)).SpecialCells(xlCellTypeVisible).Cells.SpecialCells(xlCellTypeConstants)
If Err.Number <> 1004 Then
nonBlank = 0
Else
nonBlank = selec_cells.Count
End If
On Error GoTo -1
Next i
My issue is, when I run this code, it spits out 0 every time, even though column 2 should return 3. Thank you!
Edit: selec_cells is what throws out the error.
Error Handling
There is no On Error Goto -1 in VBA, it's a VB thing (those are links to different pages). A tip would be if you google VBA stuff, just put VBA in front of what you're looking for.
When using On Error Resume Next (defer error trapping), you should 'apply' it on a line or two maximally and 'close' with On Error Goto 0 (disable error trapping) or with another error handler.
Your usage of On Error Resume Next is unacceptable because in this particular case we can test the range: 1. defer error handling, 2. try to set the range, 3. disable error handling. If there was an error the range will not be set hence If Not rg Is Nothing Then which could be translated to something like 'If rg Is Something Then' (double negation) or If a reference to a range has been created Then.
The second solution illustrates a case where the main error handler is handling all errors except the SpecialCells error which has its own error handler. Resume Next means continue with the line after the line where the error occurred. Note the Exit Sub line and note Resume ProcExit where the code is redirected to a label.
The following illustrates two ways how you could handle this. At this stage, I would suggest you use the first one and remember to use the 'closing' On Error Goto 0 whenever you use On Error Resume Next (a line or two).
The Code
Option Explicit
Sub testOnErrorResumeNext()
Const FirstRow As Long = 2
Const LastRow As Long = 11
Dim rg As Range ' ... additionally means 'Set rg = Nothing'.
Dim nonBlank As Long ' ... additionally means 'nonBlank = 0'.
Dim j As Long
For j = 1 To 3 ' Since it's a column counter, 'j' or 'c' seems preferred.
' Since you're in a loop, you need the following line.
Set rg = Nothing
On Error Resume Next
Set rg = Sheet1.Range(Sheet1.Cells(FirstRow, j), _
Sheet1.Cells(LastRow, j)).SpecialCells(xlCellTypeVisible) _
.Cells.SpecialCells(xlCellTypeConstants)
On Error GoTo 0
If Not rg Is Nothing Then
nonBlank = rg.Cells.Count
Else
' Since you're in a loop, you need the following line.
nonBlank = 0
End If
Debug.Print nonBlank
Next j
End Sub
Sub testOnError()
On Error GoTo clearError
Const FirstRow As Long = 2
Const LastRow As Long = 11
Dim rg As Range ' ... additionally means 'Set rg = Nothing'.
Dim nonBlank As Long ' ... additionally means 'nonBlank = 0'.
Dim j As Long
For j = 1 To 3 ' Since it's a column counter, 'j' or 'c' seems preferred.
' Since you're in a loop, you need the following line.
Set rg = Nothing
On Error GoTo SpecialCellsHandler
Set rg = Sheet1.Range(Sheet1.Cells(FirstRow, j), _
Sheet1.Cells(LastRow, j)).SpecialCells(xlCellTypeVisible) _
.Cells.SpecialCells(xlCellTypeConstants)
On Error GoTo clearError
If Not rg Is Nothing Then
nonBlank = rg.Cells.Count
End If
Debug.Print nonBlank
Next j
ProcExit:
Exit Sub ' Note this.
SpecialCellsHandler:
' Since you're in a loop, you need the following line.
nonBlank = 0
Resume Next
clearError:
MsgBox "Run-time error '" & Err.Number & "': " & Err.Description
Resume ProcExit
End Sub
My preference is, wherever possible, to encapsulate the line of code that may cause an error in its own function. The function returns true or false to indicate whether or not there is an error and an out parameter is used to return the value that you want.
This keeps the error testing confined within a very short well defined function.
Sub ttest()
Dim mySheet As Excel.Worksheet
Set mySheet = ThisWorkbook.Sheet1
Dim myIndex As Long
Dim myNonBlank as long
For myIndex = 1 To 3
If AllCellsAreBlank(mySheet.Range(ThisWorkbook.Sheet1.Cells(myFirstRow, myIndex), mySheet.Cells(myLastRow, myIndex)), myIndex, mySelectCells) Then
myNonBlank = 0
Else
myNonBlank = mySelectCells.Count
End If
Next
End Sub
Public Function AllCellsAreBlank(ByRef ipRange As Excel.Range, ByVal ipIndex As Long, ByRef opSelectCells As Range) As Boolean
On Error Resume Next
set opSelectCells = ipRange.SpecialCells(xlCellTypeVisible).Cells.SpecialCells(xlCellTypeConstants)
AllCellsAreBlank = Err.Number <> 0
On Error GoTo 0
End Function
For reference the prefixes I use are
ip: for an input only parameter
iop: for an input parameters that will be changed by the method
op: for a parameter only used to return a value
my: any variable declared within a Method.
I's also suggest you acquire the habit of meaningful descriptive names, myRow, myCol are much more meaningful than i,j, and of ensuring you use fully qualified references rather than the implicit use of the activesheet.
Related
I am having trouble using variables within the lookup criteria in Index Match. Some background: I use the following code to set a variable's value to the row # of whatever cell contains "Current" within column B:
Dim rowHeaderNum As Integer
rowHeaderNum = 0
On Error Resume Next
rowHeaderNum = Application.Match("Current", ActiveSheet.Range("B:B"), 0)
On Error GoTo 0
Then I use the below to store the column # of the cell within the row 'rowHeaderNum' that contains the value "CurrentActual" to another variable:
Dim currActColNum As Integer
currActColNum = 0
currActColNum = Application.Match("CurrentActual", Rows(rowHeaderNum & ":" & rowHeaderNum), 0)
Below is the Index Match line that I can't get to work:
Dim currActRev As Double
currActRev = Application.Index(Columns(currActColNum), Application.Match("Gross Operating Profit", Columns("N:N"), 0))
currActRev will store a dollar amount. The Match function will always use column N as the lookup array. When I run the Index Match line I get a
type mismatch
error in the debugger.
Using WorksheetFunction …
Application.Match and On Error Resume Next does not work, because Application.Match does not throw exceptions you need to use WorksheetFunction.Match instead.
According to the documentation the WorksheetFunction.Match method it returns a Double so you need to Dim RowHeaderNum As Double.
Dim RowHeaderNum As Double
'RowHeaderNum = 0 'not needed it is always 0 after dim
On Error Resume Next
RowHeaderNum = Application.WorksheetFunction.Match("Current", ActiveSheet.Range("B:B"), False)
On Error GoTo 0
Furthermore you need to check if RowHeaderNum is 0 and stop proceeding otherwise the following code will fail because row 0 does not exist.
If RowHeaderNum = 0 Then
MsgBox "'Current' not found."
Exit Sub
End If
You need to do exactly the same here
Dim CurrActColNum As Double
On Error Resume Next
CurrActColNum = Application.WorksheetFunction.Match("CurrentActual", Rows(RowHeaderNum), False)
On Error GoTo 0
If CurrActColNum = 0 Then
MsgBox "'CurrentActual' not found."
Exit Sub
End If
Finally the WorksheetFunction.Index method returns a Variant not a Double and you need error handling here too.
Dim currActRev As Variant
On Error Resume Next
currActRev = Application.WorksheetFunction.Index(Columns(CurrActColNum), Application.WorksheetFunction.Match("Gross Operating Profit", Columns("N:N"), False))
On Error GoTo 0
Debug.Print currActRev 'print result in immediate window
Using Application …
Note that you can also use the Application.Match and Application.Index (without WorksheetFunction) but then you cannot use On Error … and you have to check for errors using IsError(). Also your variables need to be declared as Variant then because Application.Match can either return a typo Double or a type Error.
Dim RowHeaderNum As Variant
RowHeaderNum = Application.Match("Current", ActiveSheet.Range("B:B"), False)
If IsError(RowHeaderNum) Then
MsgBox "'Current' not found."
Exit Sub
End If
Dim CurrActColNum As Variant
CurrActColNum = Application.Match("CurrentActual", Rows(RowHeaderNum), False)
If IsError(CurrActColNum) Then
MsgBox "'CurrentActual' not found."
Exit Sub
End If
Dim currActRev As Variant, currMatch As Variant
currMatch = Application.Match("Gross Operating Profit", Columns("N:N"), False)
If Not IsError(currMatch) Then
currActRev = Application.Index(Columns(CurrActColNum), currMatch)
End If
Debug.Print currActRev 'print result in immediate window
I've put together some VBA to find the last row with a certain criterion that matches the current value in my loop, then take action. This VBA code works, until I realized that the worksheet can contain the matching value multiple times but with different dates in another column. So I'm now trying to add a second search criterion to my VBA.
Here is the snippet of VBA as of now.
For Each t In trans.Cells
On Error GoTo NxtT2
If t.Value = Empty Then
On Error GoTo 0
ty = t.Offset(0, -3).Value
tx = t.Offset(0, -6).Value
Set searchTerm = .Range("E:E")
Set where = searchTerm.Find(what:=ty, after:=searchTerm(1), searchdirection:=xlPrevious)
If t.Offset(0, -3).Value = where.Value And IsError(where.Offset(0, 3).Value) Then
t.Value = "#N/A"
End If
End If
NxtT:
On Error GoTo 0
If t.Offset(1, -3).Value = "" Then Exit For
Next t
NxtT2:
Resume NxtT
Basically what I'm trying to do is make the line Set where = searchTerm.Find(what:=ty, after:=searchTerm(1), searchdirection:=xlPrevious) to also include the txvalue along with the ty that is already in there.
Something like this, if possible?
Set where = searchTerm.Find(what:=ty & tx, after:=searchTerm(1), searchdirection:=xlPrevious)
But I know that is not the correct syntax for it.
Any advice on how to approach this in the simplest way?
Not an answer to the original question, but to the issue I created with my off-the-cuff code review.
Your error handling never properly wrapped up. The code still thought it was in the error handler because you "exited" the error handler with the Next, which you really can't do - you need to leave this "instance" of error handling with a Resume.
Give this a shot instead for the cleaned up error handling.
NOTE: I declared variables because I've got Option Explicit set, which you also should also have. I've made the brash assumption that you've got your variables declared outside the code you shared. Use the variables as you've declared them, not as my quickie patched Variant declarations.
Sub foo()
Dim t As Variant
Dim ty As Variant
Dim tx As Variant
For Each t In Cells
On Error GoTo ErrorHandler
If t.Value = Empty Then
On Error GoTo 0
ty = t.Offset(0, -3).Value
tx = t.Offset(0, -6).Value
Dim searchterm As Range
Set searchterm = .Range("E:E")
Dim where As Range
Set where = searchterm.Find(what:=ty, after:=searchterm(1), SearchDirection:=xlPrevious)
If t.Offset(0, -3).Value = where.Value And IsError(where.Offset(0, 3).Value) Then
t.Value = "#N/A"
End If
End If
Continue:
Next
CleanExit:
Exit Sub
ErrorHandler:
If t.Offset(1, -3).Value = "" Then
Resume CleanExit
Else
Resume Continue
End If
End Sub
If ws.Cells(sourceSheetStartColumn, sourceSheetEndColumn - 1) = ""
Then GoTo Here '
The error means you are using one of your variable in a manner that does not fit its type. Specifically:
If ws.Cells(sourceSheetStartColumn, sourceSheetEndColumn - 1) = "" Then GoTo Here '
means that:
ws is a Worksheet
sourceSheetStartColumn and sourceSheetEndColumn are numbers
Check your code to ensure these are declared correctly. Here is an example where this should cause no issues:
Sub Foo()
Dim ws As Worksheet
Dim sourceSheetStartColumn, sourceSheetEndColumn As Integer
Set ws = ActiveWorkbook.ActiveSheet
sourceSheetStartColumn = 2
sourceSheetEndColumn = 2
If ws.Cells(sourceSheetStartColumn, sourceSheetEndColumn - 1) = "" Then GoTo Here '
MsgBox ("Checkpoint 1")
Here:
MsgBox ("Checkpoint 2")
End Sub
So, were I to change your source and end columns to a String and assign values of "A" and "B" to them, I would get your Runtime Error 13.
You will get Run-time error 13 if the cell you are trying to access has an error.
e.g. Cell A1 has formula =NA()
in the immediate window,
?cells(1,1)
gives
Error 2042
if you try to compare this value to anything, you will get Type Mismatch
Is it possible to use something with similar functionality as Iferror(value, value_if_error) or Iserror(value) in VBA?
I tried to write:
If IsError(Cells(i, c) / curr) Then
'CODE BLOCK 1
else
'CODE BLOCK 2
end if
But VBA tells me that I have division by zero error when it tries to run the if-statement. It throws me into debug. But this is just the type of thing I want to trigger CODE BLOCK 1!
The usual way to handle this would be
i = 0
On Error Resume Next
n = 1 / i
If Err.Number <> 0 Then
'Handle error - code block 1
Err.Clear
On Error GoTo 0
Else
On Error GoTo 0
' No error - code block 2
End If
You can call all worksheet functions using Application.WorksheetFunction.IsError(args)
You could also try doing the calculation in a cell directly and query it's value. For example, very hacky:
Sub asdf()
Dim ws As New Worksheet
Set ws = ActiveSheet
Dim i As Double
i = 0
ws.Range("A2").Formula = "=iserror(A1 / " & i & ")"
If ws.Range("A2").Value Then
Debug.Print "Error caught"
Else
Debug.Print "No error"
End If
End Subu
I am iterating through rows,and looking up the first column of each row(name) using a different function for finding his marks.
For each "name" there is a particular entry in a different table("marks") which can also be blank or "-"
Sub main()
On error goto errorhandler
Dim name as string
Dim marks as double
Dim source as range
Dim runs as integer
runs = 1
Set source = Sheets("input").Range("$A$2")
i=1
Do until source.offset(i,0) = "" 'iterate through rows
name = source.offset(i,0)
marks = find(name)
do until runs * marks > 100
runs = runs + 1 'since marks is not defined;runs overflows
Loop
'a lot of code which relies on marks
errorhandler:
i = i + 1
Loop
End Sub
Function find(name as string) as double
find = application.vlookup(name,Sheets("values").Range("$A$2,$C$5"),2,0)
End function
now as i said the value in column 2 of that table can also be blank or "-" and thus results in error Runtime error 13 "Type mismatch"
i even tried putting on error statement inside the loop
VBA should normally search for error handling in the calling function i.e "main" but its not doing so
TRIED AND TESTED
Sub main()
On Error GoTo errorhandler
Dim name As String
Dim marks As Double
Dim source As Range
Set source = Sheets("input").Range("$A$2")
i = 1
Do Until source.Offset(i, 0) = "" 'iterate through rows
name = source.Offset(i, 0)
marks = find(name)
Debug.Print marks
i = i + 1
Loop
Exit Sub
errorhandler:
MsgBox Err.Description
End Sub
Function find(name As String) As Double
find = Application.WorksheetFunction.VLookup(name, Sheets("values").Range("$A$2:$C$5"), 2, False)
End Function
EDIT: Kartik, Sorry, I didn't see that you have already accepted the answer.
FOLLOWUP
actually i dont want to print any error message instead straightaway skip to the next iteration – Kartik Anand 14 secs ago
In that case you are handling error in the wrong section ;)
Try this
Sub main()
Dim name As String
Dim marks As Double
Dim source As Range
Set source = Sheets("input").Range("$A$2")
i = 1
Do Until source.Offset(i, 0) = "" 'iterate through rows
name = source.Offset(i, 0)
marks = find(name)
Debug.Print marks
i = i + 1
Loop
End Sub
Function find(name As String) As Double
On Error GoTo earlyexit
find = Application.WorksheetFunction.VLookup(name, Sheets("values").Range("$A$2:$C$5"), 2, False)
Exit Function
earlyexit:
find = 0
End Function
Add an Err.Clear after errorhandler:
Also, see the Excel help on Err.Clear which reccomends On Error Resume Next
together with If Err.Number <> 0 Then
This will produce much clearer code
Something like
Sub main()
On Error Resume Next
Dim name As String
Dim marks As Double
Dim source As Range
Set source = Sheets("input").Range("$A$2")
i = 1
Do Until source.Offset(i, 0) = "" 'iterate through rows
name = source.Offset(i, 0)
marks = Find(name)
If Err.Number <> 0 Then
Err.Clear
Else
' Your other code for non-error case here
End If
i = i + 1
Loop
End Sub