Locating Cell Values in Excel VBA - vba

Using: Excel 2007/Win 7
First, I created a subroutine to find the dynamic range of a worksheet called 'WIP':
Sub GetWIPRange()
Dim WIPrng1 As Range
Dim WIPrng2 As Range
Sheets("WIP").Activate
Set WIPrng1 = Cells.find("*", [a1], , , xlByRows, xlPrevious)
Set WIPrng2 = Cells.find("*", [a1], , , xlByColumns, xlPrevious)
If Not WIPrng1 Is Nothing Then
Set WIPrng3 = Range([a1], Cells(WIPrng1.Row, WIPrng2.Column))
Application.Goto WIPrng3
Else
MsgBox "sheet is blank", vbCritical
End If
End Sub
Now I want to find a given contract number within the range defined above:
Sub find()
Dim find As Long
find = Application.WorksheetFunction.Match("545499", Range(WIPrng3.Parent.Name & "!" & WIPrng3.Address), 0)
MsgBox "Found at row : " & find
End Sub
But the error I get from the code above is:
Run-time error '91':
Object variable With block variable not set.
How can I fix this code so that it returns the row number of the value I'm seeking?
Is there a more efficient way of finding cell values using VBA? For example, if I have many worksheets and I want to search all worksheets and return a value's specific row number and worksheet location.
Many thanks!

Where is WIPrng3 defined? Is it defined as Public? The problem is that WIPrng3 has gone out of scope by the time you run "find" and is therefore Nothing. You can check for Nothing in your "find" code and run the Get procedure if needed. Like this
Sub find()
Dim find As Long
If WIPrng3 Is Nothing Then GetWIPRange
find = Application.WorksheetFunction.Match("545499", Range(WIPrng3.Parent.Name & "!" & WIPrng3.Columns(1).Address), 0)
MsgBox "Found at row : " & find
End Sub
Two things to note: If WIPrng3 returns a multicolumn range, MATCH will fail with a 1004 error. MATCH only works on a single column or row. In the example above, I restricted WIPrng3 to the first column in the MATCH function to avoid this. You didn't have this in your code.
Another thing is that you're looking for the text string "545499", not the number 545499. If your range contains the number and not the string, you'll get an error. You can trap that error with an On Error statement and handle appropriately.
Finally, I don't see the advantage to defining WIPrng3 (but I can't see the whole of what you're doing). You could easily use
Sub Find2()
Dim lRow As Long
On Error Resume Next
lRow = Application.WorksheetFunction.Match("545499", Sheets("WIP").UsedRange.Columns(1), 0)
If lRow > 0 Then
'add where the used range starts in case it's not row 1
MsgBox "Found at row : " & lRow + Sheets("WIP").UsedRange.Cells(1).Row - 1
Else
MsgBox "Not found"
End If
End Sub
You might end up looking through a larger range, but it won't appreciably affect performance.
I added the On Error in this example so you could see how it works. Don't put the On Error in there until you've tested it because it will mask all the other errors.
Charles Williams did some nice analysis on finding efficiency here http://fastexcel.wordpress.com/2011/10/26/match-vs-find-vs-variant-array-vba-performance-shootout/

Related

Replacing hard value cells with subtotal formula - VBA

Essentially, our system runs off an expenditure listing of cost headings, with a subtotal on each. The issue being we adjust the data, so need to go through and manually turn the hard value subtotals into subtotal formula in each heading; which over hundreds of different headings, with variable numbers of costs, can be tedious and time consuming.
I've built a basic test example whereby for every instance of A (Heading), where the associated B has a value (an element of data from the system for a line of expenditure), the costs (C) will be subtotalled (109,...), replacing the hard copied value.
Sub insertsubtotal()
Dim cell As Range
Dim sumrange As Range
Set cell = Cells(Cells.Rows.Count, "A")
Do
Set cell = cell.End(xlUp)
Set sumrange = cell.Offset(1, 1).CurrentRegion.Offset(1, 2).Resize(cell.Offset(1, 1).CurrentRegion.Rows.Count - 1, columnsize:=1)
If sumrange.Cells.Count > 1 Then
sumrange.End(xlDown).Offset(2, 0).Formula = "=SUBTOTAL(109," & sumrange.Address & ")"
Else
sumrange.Offset(2, 0).Formula = "=SUBTOTAL(109," & sumrange.Address & ")"
End If
Loop Until cell.Row = 1
End Sub
This works whereby the first heading is in A1, and the cost data in column C as below...
However, where I'm struggling is, I need to amend the process to have the first 5 rows ignored (first heading being on 6), and the cost data and subtotal that needs replacing being in column M.
Any help would be appreciated.
Using SpecialCells to divide the UsedRange in Columns("C") into blocks of contant values, will allow you to easily identify and subtotal your data blocks.
Sub insertsubtotal()
Dim Source As Range, rArea As Range
With Worksheets("Sheet1")
On Error Resume Next
Set Source = Intersect(.UsedRange, .Columns("C")).SpecialCells(xlCellTypeConstants)
On Error GoTo 0
If Source Is Nothing Then
MsgBox "No data found", vbInformation, "Action Cancelled"
Exit Sub
End If
For Each rArea In Source.Areas
rArea.Offset(rArea.Rows.Count).Cells(2).Formula = "=SUBTOTAL(109," & rArea.Address & ")"
Next
End With
End Sub

Setting variables VBA

complete novice here
I started some VBA a few days ago, I have simple question but cant seem to find what I am doing wrong.
I am trying to make a button which will take the coordinates of the active cell and compare them to another worksheet to retrieve a specific value from another table.
I set variables to the active cell column and row, I want to do this so I can later compare these locations to another worksheet and get the value at a specified position on another worksheet.
So far I have written simply what I could find on the internet as I have no formal training.
The msgbox at the end is just to test whether or not it actually picks up the reference.
Sub CommandButton1_Click()
Dim Arow As Range
Dim Acol As Range
Set Arow = Worksheets("Sheet1").Range(ActiveCell.Row)
Set Acol = Worksheets("Sheet1").Range(ActiveCell.Column)
MsgBox (Arow)
End Sub
So far I have error run-time error '1004' Application defined or object defined error highlighting the 4th Row. If anyone could help me solve this or redirect me to some help it would be much appreciated.
I think this won't work, you should put there
Set arow = Worksheets("Sheet1").Range(ActiveCell.Row & ":" & ActiveCell.Row)
Putting there simply number won't work. For the column, you should put there somethong like C:C. For getting letter of column, see this qestion: Function to convert column number to letter?
For more information about Range property, please see official documentation https://msdn.microsoft.com/en-us/library/office/ff836512.aspx.
The thing is, that you have to supply either the address in so called A1 reference, which is "A1", or "$A$1" or name of cell, etc, or you have to supply two Range objects, such as two cells Worksheets("Sheet1").Range(Worksheets("Sheet1").Cells(1,1), Worksheets("Sheet1").Cells(2,2)), which defines area starting with upper-left corner in first parameter and lower right in second parameter.
ActiveCell.Row and ActiveCell.Column returns you some Integer value representing number of row and column, i.e. if you point cell B4, ActiveCell.Row would return 4, and ActiveCell.Column gonna return 2. An Range() property need as an argument whole adress for some range, i.e. Range("C6") or Range("G3:J8").
When you have your column as a number, you can use Cells() property for pointing first and last cell in your range, i.e. Range(Cells(2, 4), Cells(6, 8) would be the same range as Range("D2:H6").
Following this, one of the ways that you can do what you have described is:
Sub CommandButton1_Click()
Dim Rng As Range
Set Rng = Worksheets("Sheet1").Cells(ActiveCell.Row, ActiveCell.Column)
End Sub
Now you have under variable Rng an Range of the same coordinates as ActiveCell, but in Sheet1. You can pass some value into i.e Rng.Value = "Hello World", paste something with Rng.PasteSpecial xlPasteAll etc.
if you want the value from other sheet at the same location as activeCell, use this code,
Private Sub CommandButton1_Click()
valueFromOtherSheet = Sheets("Sheet2").Range(ActiveCell.Address)
MsgBox (valueFromOtherSheet)
End Sub
Like the others have said, it's just about knowing your variable types. This is another way you could achieve what you want
Sub CommandButton1_Click()
Dim Acell As Range
Set Acell = Worksheets("Sheet2").Range(ActiveCell.Address)
MsgBox "Value on ActiveSheet: " & ActiveCell.Value & vbNewLine & _
"Value on Sheet2: " & Acell.Value
End Sub
Thank you everyone for the help and clarification, In the end I was able to come up with some code that seems to do what I need it to.
Private Sub CommandButton1_Click()
Dim cabDate As Range
Dim searchCol As Integer
Dim newindex As Range
Set cabDate = WorksheetFunction.Index(Range("A1:O9999"), ActiveCell.Row, 2)
searchCol = ActiveCell.Column
Set newindex = WorksheetFunction.Index(Worksheets("Deadlines").Range("A1:O9999"), cabDate.Row, searchCol)
MsgBox (newindex)
End Sub
I wasn't aware about conflicting data types so thank you all for the assistance.

Look up values in sheet(x) column(x), match to values in sheet(y) column(y), if they match paste row

Dealing with an issue that seems simple enough, but for some reason I cannot get this to work.
I have a data input sheet I am trying to match values across to another sheet, the values are both in column E, and all the values in column E are unique.
The values will always be stored in rows 8 though to 2500.
My code is as below, however is throwing the ever useful 1004 error (Application-Defined or object-defined error), on line
If Sheets("Target Inputs").Range("E" & CStr(LSearchRow)).Value = searchTerm Then
any help would be greatly appreciated:
Sub LOAD_BUID_Lookup()
Dim i As Integer
Dim LSearchRow As Integer
Dim LCopyToRow As Integer
Dim searchTerm As String
On Error GoTo Err_Execute
For i = 8 To 2500
searchTerm = Range("E" & i).Text
If Sheets("Target Inputs").Range("E" & CStr(LSearchRow)).Value = searchTerm Then
'Select row in Sheet1 to copy
Rows(CStr(LSearchRow) & ":" & CStr(LSearchRow)).Select
Selection.Copy
'Paste row into Sheet2 in next row
Sheets("LOAD").Select
Rows(CStr(LCopyToRow) & ":" & CStr(LCopyToRow)).Select
ActiveSheet.Paste
'Move counter to next row
LCopyToRow = LCopyToRow + 1
'Go back to Sheet1 to continue searching
Sheets("Target Inputs").Select
End If
Next i
Application.CutCopyMode = False
Range("A3").Select
MsgBox "All matching data has been copied."
Exit Sub
Err_Execute:
MsgBox "An error occurred."
End Sub
LSearchRow is not being set to any value, which means it is 0. This in turn throws the exception, since the row number cannot be 0. And there is no reason whatsoever to convert to string with CStr, since the concatenation casts the entire range parameter to a string anyway.
Usually when comparing two different columns in two different sheet you would see a double loop the first to loop through sheet1 and the second to take every value of sheet1 and loop through sheet2 to find a match. In reading your description I think this is what you want.
Double loops can be time intensive. There is another way, Worksheetfunction.match!!
I also noticed your code selecting/activating sheets multiple times. Typically selecting/activating sheets is not required if you declare and instantiate the variables you need.
Below is an example code I tried to make it as plug and play as possible, but I wasn't sure of the name of the sheet you are looping through. I've tested the code on dummy data and it seems to work, but again I'm not quite positive on the application. I've commented the code to explain as much of the process as possible. Hopefully it helps. Cheers!
Option Explicit 'keeps simple errors from happening
Sub LOAD_BUID_Lookup()
'Declare variables
Dim wb As Workbook
Dim wsInputs As Worksheet
Dim wsTarget As Worksheet
Dim wsLoad As Worksheet
Dim searchTerm As String
Dim matchRng As Range
Dim res
Dim i As Integer
'instantiate variables
Set wb = Application.ThisWorkbook
Set wsInputs = wb.Worksheets("Inputs") 'unsure of the name of this sheet
Set wsTarget = wb.Worksheets("Target Inputs")
Set wsLoad = wb.Worksheets("LOAD")
Set matchRng = wsTarget.Range("E:E")
On Error GoTo Err_Execute
For i = 8 To 2500
searchTerm = wsInputs.Range("E" & i).Text 'can use sheet variable to refer exactly to the sheet you want without selecting
'get match if one exists
On Error Resume Next
res = Application.WorksheetFunction.Match(searchTerm, matchRng, 0) 'will return a row number if there is a match
If Err.Number > 0 Then 'the above command will throw an error if there is no match
'MsgBox "No Match!", vbCritical
Err.Clear ' we clear the error for next time around
On Error GoTo 0 'return to previous error handeling
Else
On Error GoTo 0 'return to previous error handeling
wsInputs.Range("A" & i).EntireRow.Copy Destination:=wsLoad.Range("A" & wsLoad.Range("E50000").End(xlUp).Row + 1) 'gets last row and comes up to last used row ... offset goes one down from that to the next empty row
End If
Next i
'Application.CutCopyMode = False -- there is no need for this when we use "Destination"
MsgBox "All matching data has been copied."
Exit Sub
Err_Execute:
MsgBox "An error occurred."
End Sub

Find and FindNext for Excel VBA

I've been stuck trying to figure out what to do with this, but basically I want a way to print out the value in column B given a specific value that matches column A. So for example:
Column A Column B
1 ABC
2 DEF
3 GHI
1 JKL
I want to, after using find/findnext or whatever it is, to print out this string:
ABC JKL
I tried using
Set cellFound = ActiveWorkbook.Worksheets("sheet1").Range("F1:F1000000").Find("1")
string = cellFound.Offset(0, 1).value
And I have a loop to loop through as many time as it needs to get all the rows taken care of. But with find it keeps returning me the same first string ("ABC") and the string ends up being ABC ABC instead of ABC JKL
I tried using FindNext instead of find, but what I got is a 1004 Error. So I'm not really sure where I'm doing this wrong. Anyone has any idea?
You don't need FindNext if you start each Find after the previous one:
Sub qwerty()
Dim rFirst As Range, r As Range
Dim A As Range
Set A = Range("A:A")
Do
If rFirst Is Nothing Then
Set rFirst = A.Find(What:=1, After:=A(1))
Set r = rFirst
Else
Set r = A.Find(What:=1, After:=r)
If r.Address = rFirst.Address Then Exit Do
End If
MyString = MyString & " " & r.Offset(0, 1)
Loop
MsgBox MyString
End Sub
You need to call Find once, and then successively FindNext. But there are a couple of non-obvious things:
Each time you call FindNext, the search will start again from the upper-left corner of the range; unless you pass in the current found cell.
The search will wrap around (up or down, depending on your search direction. You need to write code that handles this possibility.
The minimal working code would look something like this:
Dim rng As Excel.Range
Set rng = ActiveWorkbook.Worksheets("sheet1").Range("F1:F1000000")
Dim lastRow as Integer
Set cellFound = rng.Find("1")
Do While Not cellFound Is Nothing
' handles wraparound
If cellFound.Row < lastRow Then Exit Do
string = cellFound.Offset(0, 1).Value
' do something here with string
Set cellFound = rng.FindNext(cellFound)
Loop
Reference:
Find method
FindNext method
When using the Range.FindNext method, one need just include some reference to the initial find position. For example, I recorded this macro using excel; while I'm not a fan of using selection and activate, I think it helps to understand how the method functions:
Sub Using_Find()
Selection.Find(What:="my search string here", After:=ActiveCell _
, LookIn:=xlFormulas, LookAt:=xlPart, SearchOrder:=xlByRows, _
SearchDirection:=xlNext, MatchCase:=False, SearchFormat:=False).Activate
Selection.FindNext(After:=ActiveCell).Activate
Selection.FindNext(After:=ActiveCell).Activate
Selection.FindNext(After:=ActiveCell).Activate
Selection.FindNext(After:=ActiveCell).Activate
Selection.FindNext(After:=ActiveCell).Activate
Selection.FindNext(After:=ActiveCell).Activate
End Sub
To generate this subroutine, I used the record > macro in excel, then selected Home > Find & Select > Find.
The way I see this subroutine working is:
Step #1: Find the first location of the string, activate it;
Step #2: FindNext looks after the active cell that we just activated, finds the next location of the string, then activates it;
Etc. etc. So, the observation here is that the .FindNext method needs some reference to the prior find cell (which the first answer accomplishes by manually identifying it as a unique reference). I'm not saying anything to that answer, it works just as well. My goal was to help provide some insight into the Range.FindNext method.
Some other points worth mentioning:
Range.FindNext will return a Range object. (Microsoft)
The After parameter is described as:
"The cell after which you want to search. This corresponds to the position of the active cell when a search is done from the user interface. Be aware that After must be a single cell in the range. Remember that the search begins after this cell; the specified cell is not searched until the method wraps back around to this cell. If this argument is not specified, the search starts after the cell in the upper-left corner of the range." (Microsoft)
...and
Under the Remarks section, Microsoft notes that, "The search will wrap around to the beginning of the range." They suggest to save the first address and do a check against it for each subsequent .FindNext. This way, once the method does wrap around, it will check the address against the first and end the check.
So, modeling the Range.FindNext Method provided by Microsoft, I wrote this introductory subroutine for review:
Sub USING_FIND()
'this line sets the range to our used range on the active sheet
With ActiveSheet.UsedRange
'setting c variable to .Find method, where the first value is what we're looking for,
'i.e. "1"; LookIn:= can be changed to our needs but set currently to xlValues
Set c = .Find(1, LookIn:=xlValues)
'begin first conditional; this conditional checks c (our .Find method) to see if it has
'some reference, then sets the address to a constant 'firstAddress' so we can check it
'against the .FindNext returns later to prevent endless loop
If Not c Is Nothing Then
firstAddress = c.Address
'Do...is where we place our "work"; this can be a redirect to another function/sub, etc
'for now I've just tossed a msgbox as a placeholder that returns the offset 1 column over
Do
MsgBox c.Offset(0, 1)
'Now we set c to the .FindNext method, using the original .Find method as the 'after'
Set c = .FindNext(c)
'Another empty reference check/exit as a conditional
If c Is Nothing Then
GoTo DoneFinding
'ends the empty reference conditional
End If
'using our .FindNext method that we replaced 'c' with earlier, we can now loop through
'the remainder of the value returns. The Loop While 'c.Address <> firstAddress' sentence
'is checking that each subsequent .FindNext address IS NOT the first address;
'-our loop will return to the 'Do' sentence to repeat the loop, starting on the
'MsgBox c.Offset(0,1) sentence with the next string occurence
'-the characters '<>' means 'does not equal'; i.e. the opposite of '='
Loop While c.Address <> firstAddress
'this ends the address check loop
End If
DoneFinding:
End With
End Sub
To adjust this code to your specific needs, we can change the sentence after the Do line: 'MsgBox c.Offset(0,1)' to our specific needs.
Depending on how complex your output needs are, you can add all occurrences to an array, then have the array output the values in order of how you want to see them. This can be done by redim array and preserve each return. Once the .Find loop completes, open a new workbook with the Workbooks.Open method, and run a quick loop that takes each array value and places it in the order that you prefer.
Another option is to 'print' to .txt. Open a new .txt as #1, then 'print' accordingly. This can also be done as a second subroutine via the array option suggested previously.
Hope this helps add some context to your initial question with respect to the .FindNext method, as well as provides some ideas for future direction/implementation. Good luck!
Microsoft page on Range.FindNext Method:
https://msdn.microsoft.com/en-us/VBA/Excel-VBA/articles/range-findnext-method-excel
Function FindMultiResut(ByRef What As String, _
ByRef FindRng As Range, _
ByRef OutputRng As Range, _
ByRef Delimite As String)
Dim fRng As Range
Dim Rng1 As Range
Dim Rng2 As Range
Dim temp As String
Set fRng = FindRng
Do
If Rng1 Is Nothing Then
Set Rng1 = fRng.Find(What:=What, After:=fRng(1))
Set Rng2 = Rng1
Else
Set Rng2 = fRng.Find(What:=What, After:=Rng2)
If Rng2.Address = Rng1.Address Then Exit Do
End If
If OutputRng.Worksheet.Cells(Rng2.Row, OutputRng.Column) <> Empty Then
temp = temp & OutputRng.Worksheet.Cells(Rng2.Row, OutputRng.Column) & Delimite
End If
Loop
FindMultiResut = Left(temp, Len(temp) - 1)
End Function
Here is an implementation of the suggestion I made in my comment under your question.
Function RowBeforeLast(ByVal What As Variant) As Long
Dim Fnd As Range
Set Fnd = Range("E:E").Find(What:=What, After:=Range("E1"), _
LookAt:=xlWhole, _
Searchdirection:=xlPrevious)
If Not Fnd Is Nothing Then
Set Fnd = Range("E:E").Find(What:=What, After:=Fnd, _
LookAt:=xlWhole, _
Searchdirection:=xlPrevious)
If Not Fnd Is Nothing Then RowBeforeLast = Fnd.Row
End If
End Function
It's designed as a UDF so that you can call it from the worksheet with a worksheet function like =RowBeforeLast(E5). You can also call it with code like
Private Sub TestGet()
RowBeforeLast "GR 3"
End Sub
Either way it will return the row number in which the search criterium was found for the second time from the bottom of the column. If there is only one or no occurrance the function will return zero.

Type Mismatch Run Time error 13 for excel VBA

I have requirement to write some function which will accept Range as input and I need to return value of first non empty cell. I have tried in one excel sheet and finding non empty cell was working fine. When I try with my project excel file it was not working. Basically for Find method of Range I am getting runtime error 13. Check below code and let me know what is the issue. I have noticed even in when I put Range.Row property it make "Row" as row in code ( in below code see Target.row).
Sub Btn_GenerateChartClicked()
If Range("E9") = "Scatter" Then
MsgBox "Scatter is selected"
Dim str As String
Dim rng As Range
Set rng = Range("B12:I12")
str = FindNonEmptyCellFromRange(rng)
' MsgBox str
Else
MsgBox "Bar is selected"
End If
End Sub
Function FindNonEmptyCellFromRange(Target As Range) As String
Dim ws As Worksheet
Set ws = Sheets("Benchmarking_Project")
Dim foundRange As Range
Set foundRange = Target.Find("*", Cells(Target.row, 1), xlFormulas, , xlByColumns, xlPrevious)
'Dim cellValue As String
'cellValue = foundRange.Value
FindNonEmptyCellFromRange = "Test"
'cellValue
End Function
You can't find a target.
use Cell.Find and then once you have the cell selected use Target.Address to get the address of the cell
So your CellValue would become:
CellValue = FoundRange.Address
Although, your question is a little vague as your not doing anything practicle with this UDF anyway
Your question does not provide enough details and the function call does not return the non empty cell. Whatever happens your function will return only Test.
Anyway when going through the code, your range has a single row in it.
Issue seems to be with the following code
Set foundRange = Target.Find("*", Cells(Target.row, 1), xlFormulas, , xlByColumns, xlPrevious)
There is no need to specify the After Parameter Cells(Target.row, 1)
After parameters corresponds to the position of the active cell when a search is done from the user interface. Notice that After must be a single cell in the range. Remember that the search begins after this cell; the specified cell isn’t searched until the method wraps back around to this cell. If you do no specify this argument, the search starts after the cell in the upper-left corner of the range.
Try to change that code to
Set foundRange = Target.Find("*", , xlFormulas, , xlByColumns, xlPrevious)
The following code may work for you
Sub Btn_GenerateChartClicked()
If Range("E9") = "Scatter" Then
MsgBox "Scatter is selected"
Dim str As String
Dim rng As Range
Set rng = Range("B12:I12")
str = GetFirstNonEmptyCell(rng)
' MsgBox str
Else
MsgBox "Bar is selected"
End If
End Sub
Public Function GetFirstNonEmptyCell(Target As Range)
Dim startCell As Range, firstNonEmptyCell As Range
For Each c In Target.Cells
If Trim(c.Value) <> "" Then
Found_Address = c.Address
Exit For
End If
Next
GetFirstNonEmptyCell = Found_Address
End Function
Ian your suggestion about not to use Cells(Target.Row,1) in Find method is right. I got my mistake. In that I have put column index as 1 but it should be 2 because my selected range is from Column B which means column index 2. So I got actually error because there is no column index 1 in that range. So if I put 2 instead of 1 in above mentioned call then it is working fine. Yes your right that I was not returning actually value of last non empty cell as that was my R&D code I kept changing it. So while posting it I forgot to change it. Thank you all for your reply