This is some VBA code I've written for Excel. I'm trying to match entries in Sheet1 with those in Sheet2. The structure of both sheets is as follows:
DATE | ID |
----- ----
Date1 ID1
Date2 ID2...
In my code, I loop through the rows of the first sheet, and set the values from each particular row as part of my MATCH() query, in hopes of finding these same values in the second sheet. When I do, I want MATCH() to return the row index it finds these values in, so I can use that same row to input further information from the first sheet. This query uses multiple criteria, as indicated by both the value and searchRange variables (I'm trying to use the multiple criteria via concatenation method, as seen in this article).
The problem is, I consistently get a WorksheetFunction.Match could not be used error. When I used one single criteria (the ID), the function worked. When I tried to use multiple ones, it failed, even though I followed the instructions seen in the previously linked article. Any suggestions or ideas to fix this would be appreciated.
Sub runComparison(Sheet1 As String, Sheet2 As String)
Dim rowCount As Variant, columnCount As Variant, information As Variant
Dim counter As Integer
Dim value As String, searchRange As String
Sheets(Sheet2).Select
'Array of the number of rows in both sheets
rowCount = Array(Sheets(Sheet1).Cells(Rows.count, "A").End(xlUp).row, Sheets(Sheet2).Cells(Rows.count, "A").End(xlUp).row)
'Array of the number of columns in both sheets
columnCount = Array(Sheets(Sheet1).Cells(1, Columns.count).End(xlToLeft).Column, Sheets(Sheet2).Cells(1, Columns.count).End(xlToLeft).Column)
'The range in which we will look for the date and the ID
searchRange = CStr(Range(Cells(2, 1), Cells(rowCount(1), 1)).Address & "&" & Range(Cells(2, 2), Cells(rowCount(1), 2)).Address)
counter = 2
Do Until counter = rowCount(0)
'Sets the search term equal to the current cell in Sheet1
value = Sheets(Sheet1).Cells(counter, 1) & "&" & Sheets(Sheet2).Cells(counter, 2)
' Attempts to set the cell in the 8th column in the same row in which the search term is found equal to a certain value from the search term's row
Cells(WorksheetFunction.Match(value, searchRange, 0), 8) = Sheets(Sheet1).Cells(counter, columnCount(0)).value
counter = counter + 1
Loop
End Sub
Edit: Here's some sample input
value = '7/14/2014&ESTUOUW1046465464'
searchRange = '$A2:$A298&$B2:B298'
UPDATED
Thanks for clarifying in comments. I removed my original answer as it pertains only to the regular "Match" function, and I see the reference/example and understand what you're trying to do now which involves an array formula.
Let's try this using Application.Evaluate which will avoid the need to put this formula in a cell. Using the example data from MS, I did this which seems to work:
Sub test()
Dim value As String
Dim srcRange As String
value = "D2&E2"
srchRange = "$A$2:$A$5&$B$2:$B$5"
Debug.Print Application.Evaluate("=MATCH(" & value & "," & srchRange & ",0)")
End Sub
Applying that in your code, I think would be like below. YOu will still want to Dim matchVal as Variant to hold the result of the formula evaluation, I think. Then do this:
Do Until counter = rowCount(0)
'Sets the search term equal to the current cell in Sheet1
value = Sheets(Sheet1).Cells(counter, 1) & "&" & Sheets(Sheet2).Cells(counter, 2)
'## Assign the result of the Match function to a variable
matchVal = Application.Evaluate("=MATCH(" & value & "," & searchRange & ",0)")
'## Check for errors, and handle as needed:
If IsError(matchVal) Then
'modify as needed, this highlight the cell with the non-matched value
' you might omit this line and simply ignore it, or you could
' display a MsgBox prompt, etc.
Sheets(Sheet1).Cells(counter, columnCount(0)).Interior.ColorIndex = 6
Else:
Cells(matchVal, 8) = Sheets(Sheet1).Cells(counter, columnCount(0)).value
End If
counter = counter + 1
Loop
Related
I am new in VBA and would like to write a code which fills cells with xlookup formula.
I have 2 tables on different sheets but in the same workbook:
on "New TS" sheet, I need to filter for the TBD-s in col H, and replace them with the exact value based on the data on the "Old TS" sheet.
formula should to be used in the filtered range: =XLOOKUP(1, ('New TS'!C4='Old TS'!C2:C35) * ('New TS'!E4='Old TS'!E2:E35),'Old TS'!G2:G35,"TBD",0)
C4 and E4 can change based on which row contains the first TBD
Last row (now 35) can change based on the table on the Old TS sheet.
I would highly appreciate if you could help me how to add that to my code.
ThisWorkbook.Worksheets("New TS").range("1:1").AutoFilter Field:=8, Criteria1:="TBD"
endrow2 = ThisWorkbook.Worksheets("Old TS").range("G" & Rows.Count).End(xlUp).Row
firstrow = ThisWorkbook.Worksheets("New TS").range("H2:H" & Rows.Count).SpecialCells(xlCellTypeVisible).Cells().Row
ThisWorkbook.Worksheets("New TS").Cells(firstrow, 8) = Application.XLookup(1, (ThisWorkbook.Worksheets("New TS").range(firstrow, 3) = ThisWorkbook.Worksheets("Old TS").range("C2:C" & endrow2)) * (ThisWorkbook.Worksheets("New TS").range(firstrow, 5) = ThisWorkbook.Worksheets("Old TS").range("E2:E" & endrow2)), ThisWorkbook.Worksheets("Old TS").range("G2:G" & endrow2), "TBD", 0)
Please let me know if you need more information about that.
I've never been able to get the XLOOKUP working in the same way as the formula when you have multiple criteria, as in your case.
My own implementation doesn't have to filter the range at all, just look for rows containing TBD.
The other "trick" to the example solution here is in how you find a "matching" row. Your criteria is essentially a combination of data from two columns. Assuming that this combination is always unique, the solution is ideal for a Dictionary. Each "key" to a dictionary entry is this unique combination of the two values.
(Another technique in the example below creates memory-based arrays from the worksheet range to speed processing.)
Option Explicit
Sub ReplaceTBDValues()
'--- capture the data into memory-based arrays
Dim newTSArea As Range
Dim newTS As Variant
Set newTSArea = ThisWorkbook.Sheets("New TS").UsedRange
newTS = newTSArea.Value
Dim oldTSArea As Range
Dim oldTS As Variant
Set oldTSArea = ThisWorkbook.Sheets("Old TS").UsedRange
oldTS = oldTSArea.Value
'--- create a Dictionary of the OldTS values for quick lookup
' the "key" for quick lookup is a combination of the values
' in columns C and E
Dim oldTSDict As Dictionary
Set oldTSDict = New Dictionary
Const NUMBER_COL As Long = 3
Const GROUP2_COL As Long = 5
Const OLD_TITLE_COL As Long = 8
Dim i As Long
For i = 2 To UBound(oldTS, 1) 'skip the header row
Dim tsKey As String
tsKey = oldTS(i, NUMBER_COL) & oldTS(i, GROUP2_COL)
If Not oldTSDict.Exists(tsKey) Then
'--- store the row number in the dictionary
oldTSDict.Add tsKey, i
Else
Debug.Print "Duplicate C/E values in row " & i & "!"
End If
Next i
'--- now run through the lines in New TS and replace the TBD data
For i = 2 To UBound(newTS, 1) 'skip the header row
If newTS(i, OLD_TITLE_COL) = "TBD" Then
Dim checkKey As String
checkKey = newTS(i, NUMBER_COL) & newTS(i, GROUP2_COL)
If oldTSDict.Exists(checkKey) Then
'--- found the values, so replace
newTS(i, OLD_TITLE_COL) = oldTS(oldTSDict(checkKey), OLD_TITLE_COL)
End If
End If
Next i
'--- finally copy the array back to the New TS sheet
newTSArea.Value = newTS
End Sub
Is it possible to find a row with 2 criteria?
I'm importing survey anwsers to a worksheet, now I want to find the answers of a specified person
I need to find the row in the worksheet(ImportLimesurvey) that has 2 specified cell values:
In that row:
the value of the C-cell has to be one of the highest value in that column (I used the function Application.WorksheetFunction.Max(rng))
This value means how much of the survey is filled in. The highest value stands in multiple answer-rows. The highest value is different for every survey. (example, if a survey has 7 pages and the participant fills in all pages :the highest value is 7 for that person, but if the person didn't complete that survey, the value could be e.g. 3), So the filter of the highest value is if the participant completed the whole survey.
the value of the L-cell has to be the same as the cell (Worksheets("Dataimport").Range("M2")
M2= accountnumber of the person I need the answers from
The correct row has to be pasted to (Worksheets("Dataimport").Range("A7")
This is my current code:
Dim g As Range
Dim rng As Range
Set rng = Worksheets("ImportLimesurvey").Range("C:C")
d = Application.WorksheetFunction.Max(rng)
With Worksheets("ImportLimesurvey").Range("L:L")
Set g = .Find(Worksheets("Dataimport").Range("M2"), LookIn:=xlValues)
g.Activate
End With
e = Range("C" & (ActiveCell.Row))
If e = d Then
ActiveCell.EntireRow.Copy _
Destination:=Worksheets("Dataimport").Range("A7")
End If
The problem here is that he finds the row with the right account number, but the answer with the C-value isn't always the highest. It picks (logically) just the first row with that accountnumber. So how can I find the row that matches those 2 criteria?
Thanks in advance
P.S. I'm new to VBA so I tried to be as specific as possible but if you need any additional info, just ask for it ;)
dmt32 forom mrexcel.com found a solution.
Link to topic: https://www.mrexcel.com/board/threads/find-row-with-2-criteria.1157983/
His code works fine:
Sub FindMaxValue()
Dim FoundCell As Range, rng As Range
Dim MaxValue As Long
Dim Search As String, FirstAddress As String
Dim wsDataImport As Worksheet, wsImportLimesurvey As Worksheet
With ThisWorkbook
Set wsDataImport = .Worksheets("Dataimport")
Set wsImportLimesurvey = .Worksheets("ImportLimesurvey")
End With
Search = wsDataImport.Range("M2").Value
If Len(Search) = 0 Then Exit Sub
With wsImportLimesurvey
Set FoundCell = .Range("L:L").Find(Search, LookIn:=xlValues, lookat:=xlWhole)
If Not FoundCell Is Nothing Then
FirstAddress = FoundCell.Address
Do
With FoundCell.Offset(, -9)
If .Value > MaxValue Then Set rng = FoundCell: MaxValue = .Value
End With
Set FoundCell = .Range("L:L").FindNext(FoundCell)
If FoundCell Is Nothing Then Exit Do
Loop Until FoundCell.Address = FirstAddress
rng.EntireRow.Copy wsDataImport.Range("A7")
MsgBox Search & Chr(10) & "Record Copied", 64, "Match Found"
Else
MsgBox Search & Chr(10) & "Record Not Found", 48, "Not Found"
End If
End With
End Sub
Still thanks for the tips.
Firstly, Visual Basic conceptual topics is a great read to help in writing 'better' code. The biggest thing I encourage is to use meaningful variable names.
It's much easier to understand your code when you have variable names like HighestCount or TargetSheet etc. rather than names like a or b etc.
The answer to your question is yes.
I would write something like this:
Option Explicit
Public Function HighestSurveyRow(ByVal TargetAccountNumber As Long) As Long
Dim ImportLimeSurveySheet As Worksheet
Set ImportLimeSurveySheet = ThisWorkbook.Sheets("ImportLimeSurvey")
Dim LastRow As Long
Dim TargetRow As Long
Dim SurveyCountArray As Variant
Dim ArrayCounter As Long
With ImportLimeSurveySheet
ArrayCounter = 1
LastRow = .Cells(.Rows.Count, 12).End(xlUp).Row
ReDim SurveyCountArray(1 To LastRow, 1 To 2)
For TargetRow = 1 To LastRow
If .Cells(TargetRow, 12).Value = TargetAccountNumber Then
SurveyCountArray(ArrayCounter, 2) = TargetRow
SurveyCountArray(ArrayCounter, 1) = .Cells(TargetRow, 3).Value
ArrayCounter = ArrayCounter + 1
End If
Next TargetRow
End With
Dim ResultArray(1 To 2) As Variant
Dim ArrayElement As Long
For ArrayElement = 1 To UBound(SurveyCountArray, 1)
If SurveyCountArray(ArrayElement, 1) > ResultArray(1) Then
ResultArray(1) = SurveyCountArray(ArrayElement, 1)
ResultArray(2) = SurveyCountArray(ArrayElement, 2)
End If
Next ArrayElement
HighestSurveyRow = ResultArray(1)
End Function
Sub FindRowForSurveyResults()
With ThisWorkbook.Sheets("DataImport")
.Range("A7").Value = HighestSurveyRow(.Range("M2").Value)
End With
End Sub
It's split into a Function and a Subroutine. The Function executes most of the code and returns the row number. The Sub calls this function and writes this returned value to cell A7 on "DataImport".
The sub can be broken down as follows;
Using a with statement helps reduce code clutter of defining the worksheet twice.
The only thing the sub is doing is assigning a value to cell A7. To get the value it calls the function and assigns the parameter TargetAccountNumber as the value from cell M2.
The function can be broken down into the following steps;
All variables are declared and the target worksheet for the function is set.
The LastRow of column L is found to establish our maximum length of the Array and search range.
The Loop searches from Row 1 to the LastRow and compares the values from column L. If it matches the TargetAccountNumber parameter then the column C value and the row number are stored into the Array.
Once the Loop is done, another Loop is run to find the highest number. The first iteration will always store the first row's data. Each iteration after that compares the values stored in the SurveyCountArray with the current value of ResultArray(1) and if the value is greater, ResultArray(1) is updated with the value, ResultArray(2) is updated with the Row number.
Once the 2nd loop is done, the Row in ResultArray(2) is assigned to the function for the Sub to write to the worksheet.
It can definately be improved and refined to work faster and more efficiently, especially if you have a very large data set, but this should help get you thinking about ways you can use loops and arrays to find data.
Note: There could be duplicate rows for the outcome (say a user submits the same survey 3 times with the same answers), which I haven't tested for - I think this code would return the highest row number that matches the required criteria but could be tweaked to throw an error or message or even write all row numbers to the sheet.
I have a spreadsheet with number of different sheets. Each sheet has a column with unique ID which is made up as follows: AD-S001, AD-S002, AD-S003 etc. The next sheet's unique ID could be AD-M001, AD-M002 etc.
I am pretty new to VBA in excel and am trying to write some code to create a new record, incrementing the unique ID. The unique ID's are sometimes not sorted so I need to find the largest in the range and then increment it by 1.
I have the other code already, just trying to add the bit that finds the largest value in the range of strings and increments it by 1.
Grateful if someone could assist me in how best to write this sub-routine.
Thanks
Use the next function, please:
Function newID(sh As Worksheet) As String
Dim lastR As Long, strID As String, arr As Variant, i As Long
lastR = sh.Range("A" & Rows.count).End(xlUp).Row
arr = sh.Range("A2:A" & lastR).Value
strID = left(sh.Range("A2").Value, 4)
For i = 1 To UBound(arr)
arr(i, 1) = CLng(Right(arr(i, 1), 3))
Next i
newID = strID & Format(WorksheetFunction.Max(arr) + 1, "000")
End Function
It can be called/used in this way:
Sub testNewID()
Dim sh As Worksheet
Set sh = ActiveSheet 'use here the necessary worksheet
MsgBox newID(sh)
End Sub
A non-VBA alternative, see if it is acceptable.
Set the format for the column as below
"AD-S"000
And then you can simply use a MAX formula to get the maximum value in preceding cells. Assuming first numbered cell is A2, formula in cell A3 would be:
=MAX($A$2:A2)+1
which can then be copied down as much needed.
I want to refer to the last cell that contains a text by its address (ex, C800) inside the MIN function. Can you help please?
Sub Set_Formula()
' -----------------------------
Dim lastRow As Long
Dim Lastcell As Range
Dim LC As String
Set Lastcell = Range("C:C").Find("*", Range("C1"), SearchDirection:=xlPrevious)
Set LC = Lastcell.Address()
'find last cell in the row
lastRow = Cells(Rows.Count, 1).End(xlUp).Row
Dim r As Long
For r = 2 To lastRow
If (Cells(r, 3).Value <> "") Then
Range("E8") _
= "=MIN(C2:LC)" 'The MIN function
End If
Next
End Sub
It's not how I would do this, but :
Range("E8") _
= "=MIN(C2:" & LC & ")" 'The MIN function
End If
and, in your code, since LC is a string and not an object, be sure to remove the Set word.
That, of course, puts a static formula into E8.
So, if column C changes (more data added below LC, for example), you'll need to rewrite the formula to the worksheet.
If you prefer a formula that automagically adjusts to changing numbers of rows in column C, you can use:
=MIN(INDEX($C$2:INDEX($C:$C,LOOKUP(9.9E+307,$C:$C,ROW($C:$C))),0,1))
Edit: (Explanation of the formula)
The formula uses a feature of the LOOKUP function to find the last row. When lookup_value is greater than any value in lookup_array, LOOKUP will match the last value in lookup_array. Since we are using the optional result_vector argument, LOOKUP returns the value in the matching position which is that row number.
By using a very large number as lookup_value, one that is close to the maximum number allowed in Excel, we assure that there will not be a larger number in lookup_array
We then use the INDEX function to create an array that begins with C2 (or whatever you put in for a starting cell) and ends and Cn where n is the row number returned by the LOOKUP function.
Use the value for lastRow: Range("E8") = "=MIN(C2:C" & lastRow & ")"
Am trying to parse an excel file using Excel VBA.
Here is the sample sata
I did some research and found you can assign ranges to array like
Arrayname = Range("A1:D200")
But am looking for some thing more dynamic, like add the below multiple ranges to a single array.
and my final array will be a single array/table with n is number of rows from all ranges and 4 columns.
Can any one please prvide me a example.
Thank you in adavance.
I think you are asking for more information about moving data between ranges and variables so that is the question I will attempt to answer.
Create a new workbook. Leave Sheet1 empty; set cell B3 of Sheet2 to "abc" and set cells C4 to F6 of Sheet3 to ="R"&ROW()&"C"&COLUMN()
Open the VB Editor, create a module and copy the follow code to it. Run macro Demo01().
Option Explicit
Sub Demo01()
Dim ColURV As Long
Dim InxWkSht As Long
Dim RowURV As Long
Dim UsedRangeValue As Variant
' For each worksheet in the workbook
For InxWkSht = 1 To Worksheets.Count
With Worksheets(InxWkSht)
Debug.Print .Name
If .UsedRange Is Nothing Then
Debug.Print " Empty sheet"
Else
Debug.Print " Row range: " & .UsedRange.Row & " to " & _
.UsedRange.Row + .UsedRange.Rows.Count - 1
Debug.Print " Col range: " & .UsedRange.Column & " to " & _
.UsedRange.Column + .UsedRange.Columns.Count - 1
End If
UsedRangeValue = .UsedRange.Value
If IsEmpty(UsedRangeValue) Then
Debug.Print " Empty sheet"
ElseIf VarType(UsedRangeValue) > vbArray Then
' More than one cell used
Debug.Print " Values:"
For RowURV = 1 To UBound(UsedRangeValue, 1)
Debug.Print " ";
For ColURV = 1 To UBound(UsedRangeValue, 2)
Debug.Print " " & UsedRangeValue(RowURV, ColURV);
Next
Debug.Print
Next
Else
' Must be single cell worksheet
Debug.Print " Value = " & UsedRangeValue
End If
End With
Next
End Sub
The following will appear in the Immediate Window:
Sheet1
Row range: 1 to 1
Col range: 1 to 1
Empty sheet
Sheet2
Row range: 3 to 3
Col range: 2 to 2
Value = abc
Sheet3
Row range: 4 to 6
Col range: 3 to 5
Values:
R4C3 R4C4 R4C5
R5C3 R5C4 R5C5
R6C3 R6C4 R6C5
If you work through the macro and study the output you will get an introduction to loading a range to a variant. The points I particularly want you to note are:
The variable to which the range is loaded is of type Variant. I have never tried loading a single range to a Variant array since the result may not be an array. Even if it works, I would find this confusing.
Sheet1 is empty but the used range tells you than cell A1 is used. However, the variant to which I have loaded the sheet is empty.
The variant only becomes an array if the range contains more than one cell. Note: the array will ALWAYS be two dimensional even if the range is a single row or a single column.
The lower bounds of the array are ALWAYS 1.
The column and row dimensions are not standard with the rows as dimension 1 and the columns as dimension 2.
If there is any doubt about the nature of the range being loaded, you must use IsEmpty and VarType to test its nature.
You may also like to look at: https://stackoverflow.com/a/16607070/973283. Skim the explanations of macros Demo01() and Demo02() which are not relevant to you but set the context. Macro Demo03() shows the advanced technique of loading multiple worksheets to a jagged array.
Now create a new worksheet and leave it with the default name of Sheet4.
Add the follow code to the module. Run macro Demo02().
Sub Demo02()
Dim ColOut As Long
Dim OutputValue() As String
Dim Rng As Range
Dim RowOut As Long
Dim Stg As String
ReDim OutputValue(5 To 10, 3 To 6)
For RowOut = LBound(OutputValue, 1) To UBound(OutputValue, 1)
For ColOut = LBound(OutputValue, 2) To UBound(OutputValue, 2)
OutputValue(RowOut, ColOut) = RowOut + ColOut
Next
Next
With Worksheets("Sheet4")
Set Rng = .Range("A1:D6")
End With
Rng.Value = OutputValue
With Worksheets("Sheet4")
Set Rng = .Range(.Cells(8, 2), .Cells(12, 4))
End With
Rng.Value = OutputValue
With Worksheets("Sheet4")
Stg = "C" & 14 & ":G" & 20
Set Rng = .Range(Stg)
End With
Rng.Value = OutputValue
End Sub
Although this macro writes an array to a worksheet, many of the points apply for the opposite direction. The points I particularly want you to note are:
For output, the array does not have to be Variant nor do the lower bounds have to be 1. I have made OutputValue a String array so the values output are strings. Change OutputValue to a Variant array and rerun the macro to see the effect.
I have used three different ways of creating the range to demonstrate some of your choices.
If you specify a range as I have, the worksheet is one of the properties of the range. That is why I can take Rng.Value = OutputValue outside the With ... End With and still have the data written to the correct worksheet.
When copying from a range to a variant, Excel sets the dimensions of the variant as appropriate. When copying from an array to a range, it is your responsibility to get the size of the range correct. With the second range, I lost data. With the third range, I gained N/As.
I hope the above gives you an idea of your options. If I understand your requirement correctly, you will have to:
Load the entire worksheet to Variant
Create a new Array of the appropriate size
Selectively copy data from the Variant to the Array.
Come back withh questions if anything is unclear.