Capture specific rows of a ListObject to an array - arrays

I need help trying to figure out something I haven't done before in Excel VBA. I'm trying to assign specific (consecutive) rows of a listobject to an array. Is this possible?
Previously I was using code to copy over the entire table into the array, which worked, but it would capture unused blank rows I didn't want included. I was able to get some FUNCTIONS up and running which capture the first and low filled row numbers of the listobject for reference, but I cannot figure out how to assign ONLY this range of rows from the listobject into the array
This, for instance, works to assign only the 1st row:
varESInvoiceBreakdown = PrimeLO.ListRows(1).Range.Value
However, this doesn't work to capture the first two rows. I get an error
varESInvoiceBreakdown = PrimeLO.ListRows("1:2").Range.Value
Even though it indicates in a segment of this article that you can reference multiple rows in this manner
https://www.thespreadsheetguru.com/blog/2014/6/20/the-vba-guide-to-listobject-excel-tables
I was hoping to reference the two integer variables I have setup for the row selection.
Sincerely,
Kris

You can't index into the ListRows collection like that. Use DataBodyRange.Rows.
varESInvoiceBreakdown = PrimeLO.DataBodyRange.Rows("1:2").Value
With variables:
Dim startRow as Long
Dim endRow as Long
startRow = 1
endRow = 2
varESInvoiceBreakdown = PrimeLO.DataBodyRange.Rows(startRow & ":" & endRow).Value

Related

Create an array in VBA from an array formula in Excel (From Name Manager)

I am trying to create an array in VBA from a dynamic array formula in Excel. Ideally the Excel formula would be kept in the Name Manager, but I can accept if the formula is stored in a cell.
The ultimate goal is to use this array to match blocks of cells based on certain conditions (with a For loop) to determine which block of rows to delete from the Excel sheet. It has to be dynamic because I don't know how many rows will be in the sheet or which of those rows needs to be deleted until runtime.
A link to the Excel file here:
https://www.dropbox.com/s/rz1qej15afht6bx/BRE5560_RFQ.xlsx?dl=0
I've been trying to get this to work for days and have tried many different options but I can't get it to work. What's maddening is it works just fine for another formula which returns an array of Doubles, but this array of Strings defies me.
Function CreateRFQEmail(sRFQFileName As String)
Dim arrRows() As Variant
Dim arrRFQs2() As Variant
objExcel.Workbooks.Open (sRFQFileName)
Set sht = objExcel.ActiveWorkbook.Worksheets(2)
'This creates the array of Doubles without issue:
arrRows = sht.Evaluate(sht.Names.Item("Quote!vbaRows").Value)
'The best result I have with this line is to have an array of Strings in VBA with the
'correct number of elements, but the first element from Excel is repeated for each
'element in the array:
arrRFQs2 = sht.Evaluate(sht.Names.Item("Quote!vbaRFQs").Value)
End Function
I have also tried using:
arrRFQs2 = sht.Names.Item("Quote!vbaRFQs").Value 'Error - Can't assign to array
arrRFQs2 = sht.Names("Quote!vbaRFQs").Value 'Error - Can't assign to array
arrRFQs2 = sht.Range("BY5").Value 'Error - Type mismatch
I have tried several other options, but can't remember them all off the top of my head. (I'll keep the post updated if anyone suggests something I've already tried) The most common error was type mismatch even though I could confirm the formula side of the line (i.e. sht.Range("BY5").Value) was a String or a Range
The Excel formulas stored in the Name Manager are:
'vbaRFQs:
=TOROW(IF(Quote!vbaRows,INDEX(Quote!$A:$A,Quote!vbaRows),""))
'The first IF is needed to get VBA to return the correct number of elements,
'without it I just get an array with a single element even though in Excel the
'formula returns an array correctly
'vbaRows:
=IFERROR(TOROW(SMALL(IF(Quote!$A:$A<>"",ROW(Quote!$A:$A),""),ROW(OFFSET(Quote!$A:$A,0,0,COUNTA(Quote!$A:$A),1)))),"")
I also tried putting them in a cell instead of the Name Manager with no change.
I have tried making vbaRFQs one long formula instead of using vbaRows inside of it, but this only works if I remove the first IF (Because it will be too long for Evaluate) and then I only get a single element array in VBA.
If I use an Index formula for vbaRFQs then VBA sees the formula as a Range type (even the the result of the formula is a String) and gives me a type mismatch error.
Populate Array Using Named Formula
I think the formula is too complicated to be used with Evaluate.
Here's a workaround that uses the values from the 1st column and the information (rows) in the Rows array.
Sub CreateRFQEmailTEST()
CreateRFQEmail "C:\Test\BRE5560_RFQ.xlsx"
End Sub
Sub CreateRFQEmail(ByVal sRFQFilePath As String)
Dim swb As Workbook: Set swb = Workbooks.Open(sRFQFilePath)
Dim sws As Worksheet: Set sws = swb.Sheets("Quote")
Dim sData(): sData = sws.UsedRange.Columns(1).Value
' Get Rows.
Dim ArrRows(): ArrRows = sws.Evaluate("vbaRows")
' Get RQFs.
Dim ArrRFQs(): ReDim ArrRFQs(1 To UBound(ArrRows))
Dim rItem, dc As Long
For Each rItem In ArrRows
dc = dc + 1
ArrRFQs(dc) = sData(rItem, 1)
Next rItem
' Print both.
Debug.Print Join(ArrRows, ",")
Debug.Print Join(ArrRFQs, ",")
End Sub

How do I use an array of ranges to propagate data for a search

it's 3:20 am and I'm about spent, so I'm tossing this up here in hopes someone can help me. I'm not sure this is a difficult problem, but I don't honestly know how to ask this clearly.
I made a User Form a couple of weeks ago with some help here to let users store information into a table. I'm now making a Search form to allow them to search the table (namely the full name column) and as there will be multiple entries with the same name, have it propagate a combo box so that the user can choose which entry they want to view. Depending on which entry they choose in the combobox will also propagate all the fields below it.
First, I think I've got the search function working correctly and building the array of ranges right. I had originally stored the array as strings and it populated my combo box perfectly, but then I had lost the range/address to propagate other data later. So I switched it back to an array of ranges and from there I'm having problems. Currently if I use the Combobox.additem I will of course only get a range from my array, but I can't do something like LookUpTable.Range(Array(i)).Value for my AddItem either. So, I'd like to be able to figure out how to propagate the combobox with the values in those stored ranges. I think once I learn how to do that, propagating the other fields afterwards will be pretty straightforward.
I hope this makes sense. Apologies, my brain is fried and I need some sleep.
EDIT:
The combobox will be propagated with all the duplicates as well as an identifier to easily separate them (in this case the date and person who did the evaluation) so that the user can choose which evaluation they would like to view. Right now it just shows the Full Name which is the stored range. I want to be able to essentially use the stored range to grab the entire row of values in another array that can then propagate all the fields for that report. I could make an array for every result at the time of searching, but this would be inefficient I think. Instead it should be created once the user chooses which report they want to view so it's limited to only making one array. I think I can maybe figure that out, but because it happens after they choose from the combobox, I'm unable to figure out to use that one range and pull two more columns of data with it. If I try using ,Offset with it I get an "Expected Object" error. If I try using my Table and the Array value for a range, I get a different error. I hope all this makes sense.
Public Sub Search_button_Click()
Dim NameColumn As Range
Dim NameLookUp As Range
Dim SearchResultsArray() As Variant
Dim SearchResultsCounter As Integer
Dim ResultsPropagate As Integer
Dim FirstResult As String
'Sets/Resets counter to 1 each time search button is pressed
SearchResultsCounter = 1
'Converts the text box values to strings and uppercases the first character and combines them into a full name value.
FirstLookUp = StrConv(StudentFirst_textbox.Value, vbProperCase)
LastLookUp = StrConv(StudentLast_textbox.Value, vbProperCase)
FullLookUp = FirstLookUp & " " & LastLookUp
'Sets NameColumn to the Full Name column in the table
Set NameColumn = LookUpTable.Range.Columns(3)
'Sets NameLookUp to the Full Name column in the table and searches for the FullLookUp string
Set NameLookUp = LookUpTable.Range.Columns(3).Find(What:=FullLookUp, LookIn:=xlValues, _
LookAt:=xlPart, SearchDirection:=xlNext, MatchCase:=False, SearchFormat:=False)
'Saves the first result to prevent infinit looping and readjusts the array to match results size.
If Not NameLookUp Is Nothing Then
FirstResult = NameLookUp.Address
ReDim Preserve SearchResultsArray(SearchResultsCounter)
SearchResultsArray(SearchResultsCounter) = NameLookUp
Do
ReDim Preserve SearchResultsArray(SearchResultsCounter)
SearchResultsArray(SearchResultsCounter) = NameLookUp
Set NameLookUp = NameColumn.FindNext(NameLookUp)
SearchResultsCounter = SearchResultsCounter + 1
Loop Until NameLookUp Is Nothing Or NameLookUp.Address = FirstResult
SearchResults_combobox.AddItem ("Choose a result to view.")
For ResultsPropagate = LBound(SearchResultsArray) To UBound(SearchResultsArray)
SearchResults_combobox.AddItem (SearchResultsArray(ResultsPropagate)) 'Here I want to use the range stored in the array and pull the value from the table.
Next ResultsPropagate
SearchResults_combobox.ListIndex = 0
Else
MsgBox "Sorry, no entries matched your search.", vbOKOnly
End If
End Sub

Why does my attempt at referring to named ranges with a variant variable to which I (for-next)-store the names of ranges fail?

Apologies for the confusing title. I have multiple named ranges and I am trying to create a macro that would allow me to copy a certain row of each one of the ranges and paste it to however many following rows in each range as the user pleases. I will create 20 different macros depending on from which row the user would like to copy the selections, but for now I'm testing it with row 3.
The issue however is that I managed to store the name of the first range in a variant variable. However I have only succeeded in selecting the row with application.goto-function, but have failed in trying to copy certain rows of it. For example when I try to use the Range.(aspect(the name of my variable))-function I receive an error method 'range' of object '_Global' failed. I have also tried to create a new array based on the range that the name stored in the variable should bring but have failed in that too.
Public Sub copy_window_3()
Sheets("options").Select
ccArr = Range("ccAr").Value
copy_window rowstart:=3, copyquant:=Application.WorksheetFunction.Index(ccArr, rowstart)
Exit Sub
End Sub
Public Sub copy_window(rowstart As Double, copyquant As Variant)
Dim aspect As Variant
Dim copycounter As Integer
Dim aspcounter As Double
Dim ccArr() As Variant
Dim aspAr() As Variant
aspAr = Range("aspAr").Value
For aspcounter = 1 To UBound(aspAr, 1)
aspect = Application.WorksheetFunction.Index(aspAr, aspcounter)
Range(aspect).Rows(rowstart).Copy
For copycounter = 1 To copyquant
Range(aspect).Rows(rowstart + copyquant).PasteSpecial xlPasteValues
Next copycounter
Next aspcounter
End Sub
In case the code looks confusing: The macro first uses ccArr and rowstart variables to figure out from which row to start and how many rows to go. I intended to loop through each range name which are stored in aspAr and then copy the desired row and then loop and paste to each row that the user wishes.
Pardon me for the silly question, but I tried looking for the answer online for hours with no luck.
Thanks in advance for the help!
Cheers.

MS Excel: "MATCH()" does not find cells containing text if lookup array is too large

I am creating a large and complicated schedule, and I want one view which shows the schedule as a day-time grid, and another which allows one to look up a speaker by name from an alphabetical list. I have posted a simplified example here:
In the alphabetical list, the day and time should be populated by a function using MATCH. Just as an example, I manually typed what I would like to have happen for Jones.
I cannot get MATCH() to locate the speaker's name in the timetable correctly. There are no hidden characters: notice that in cell D15, Excel correctly recognizes that G2 and C7 are identical.
Here is what happens if I put various code in H2:
=MATCH(G2,$A$1:$D$9) results in #N/A
=MATCH(G2,$C$2:$C$9) results in #N/A
=MATCH(G2,$B$7:$D$7) results in 2 (correctly!)
=MATCH(G2,$A$7:$D$7) results in #N/A
What I would like is to put =MATCH(G2,$A$1:$D$9) into H2 and then fill cells down to H25, and have Excel indicate the column number of the day in which the adjacent name appears, then use INDIRECT or something to convert this number into the day of the week.
It may be that including column A in the search array causes problems because of the different data types. As an experiment, I made the first column into TEXT, and in this case =MATCH(G2,$A$7:$D$7) incorrectly returns 1!
And even so, I cannot understand why $B$7:$D$7 works but neither $C$2:$C$9 nor $B$7:$D$8 will.
Any workarounds or alternative strategies would be greatly appreciated, thanks.
To do this you need to add in some other logic to find the correct column and row. This AGGREGATE() Function does the job.
For Day use:
=INDEX($A$1:$D$1,AGGREGATE(15,6,COLUMN($A$2:$D$9)/(($A$2:$D$9=G2)),1))
For Hour:
=INDEX($A$1:$A$9,AGGREGATE(15,6,ROW($B$1:$D$9)/(($B$1:$D$9=G2)),1))
The AGGREGATE() Function was introduced in Excel 2010.
For other Versions:
Pre 2010, they will need to be Array Formulas:
Day:
=INDEX($A$1:$D$1,MIN(IF($A$2:$D$9=G2,COLUMN($A$2:$D$9))))
Hour:
=INDEX($A$1:$A$9,MIN(IF($B$1:$D$9=G2,ROW($B$1:$D$9))))
Being an Array Formula it must be confirmed with Ctrl-Shift-Enter when exiting Edit mode. When done correctly Excel will automatically put {} around the formula to denote an array formula.
Newest Office 360 or online:
Day:
=INDEX($A$1:$D$1,MINIFS(COLUMN($A$2:$D$9),$A$2:$D$9,G2))
Hour:
=INDEX($A$1:$A$9,MINIFS(ROW($B$1:$D$9),$B$1:$D$9,G2))
As to the reason MATCH will not work in this case:
MATCH() only works with a single row or column and not a multiple column/row range. It is set up to return a number equal to the order place found and therefore must be a 1 dimensional array.
The most efficient way to do this given your dataset is to use three MATCH queries - one for each column.
For the Day, that looks like this:
=IF(ISERROR(MATCH(G2,$B$2:$B$10,0)),"",$B$1)&IF(ISERROR(MATCH(G2,$C$2:$C$10,0)),"",$C$1)&IF(ISERROR(MATCH(G2,$D$2:$D$10,0)),"",$D$1)
For the Time, that looks like this:
=INDEX($A$2:$A$10,IFERROR(MATCH(G2,$B$2:$B$10,0),0) + IFERROR(MATCH(G2,$C$2:$C$10,0),0) + IFERROR(MATCH(G2,$D$2:$D$10,0),0))
...but truth be told, on small datasets such as this one, you won't notice any performance difference on this approach vs Scott's AGGREGATE approach. On large datasets (thousands of rows) you probably will.
Note that another reason your initial approach failed is that you did not specify the 3rd argument of MATCH, and so Excel used the default value that assumes your list data is sorted alphabetically. You almost never want to omit that argument, and you almost always want to use FALSE (or Zero, which means FALSE to Excel)
Alternative solution with vba & listobjects (you need to give the two tables the names as appear in the code below)
sheet screenshot
Public Sub makeAppointmentList()
Dim aSheet As Worksheet
Set aSheet = ThisWorkbook.Worksheets("sheet1")
Dim aSchedule As ListObject
Set aSchedule = aSheet.ListObjects("schedule")
Dim anAppointmentList As ListObject
Set anAppointmentList = aSheet.ListObjects("appointmentList")
On Error Resume Next
anAppointmentList.DataBodyRange.Delete
On Error GoTo 0
Dim c As ListColumn
Dim r As ListRow
Dim newRow As ListRow
For Each c In aSchedule.ListColumns
For Each r In aSchedule.ListRows
If c.Index > 1 And Intersect(c.Range, r.Range) <> "" Then
Set newRow = anAppointmentList.ListRows.Add
Intersect(newRow.Range, anAppointmentList.ListColumns("Name").Range).Value = Intersect(c.Range, r.Range)
Intersect(newRow.Range, anAppointmentList.ListColumns("Day").Range).Value = Intersect(c.Range, aSchedule.HeaderRowRange)
Intersect(newRow.Range, anAppointmentList.ListColumns("Time").Range).Value = Intersect(aSchedule.ListColumns(1).Range, r.Range)
End If
Next r
Next c
anAppointmentList.Sort.SortFields.Clear
anAppointmentList.Sort.SortFields.Add Key:=Intersect(anAppointmentList.HeaderRowRange, _
anAppointmentList.ListColumns("Name").Range)
anAppointmentList.Sort.SortFields.Add Key:=Intersect(anAppointmentList.HeaderRowRange, _
anAppointmentList.ListColumns("Day").Range), _
CustomOrder:="Mon,Tue,Wed,Thu,Fri,Sat,Sun"
anAppointmentList.Sort.SortFields.Add Key:=Intersect(anAppointmentList.HeaderRowRange, _
anAppointmentList.ListColumns("Time").Range)
anAppointmentList.Sort.Apply
Dim s As SortField
End Sub

How to select 2-dimensional ranges using variables and not "A1"-format?

I realize this must be a really basic question but I can't seem to get this right. This last month of trying to learn VBA always sees me stuck on problems relating to this. I have searched for the answer but still struggle. Some help would be appreciated!
So, what I want to do is to select and manipulate ranges based on their numeric order, like row1,col1 to row 15,col7. Instead of "A1:G15".
For instance, the following code should format the copy of a pivot table:
Sub layout()
Dim searchterm As String: searchterm = "Grand Total"
rad = RowIndexer(searchterm) 'finds location of last row
kolumn = ColIndexer(searchterm) 'finds location of last column
ActiveSheet.Range(Cells(15, 1), Cells(rad, kolumn)).Style = "SAPBEXfilterItem"
End Sub
I have also tried converting the range to "A1"-style, to no avail:
Start = Cells(counter, 1).Address
Finish = Cells(counter, kolumn).Address
Range("Start:Stop").Style = "SAPBEXfilterItem"
This question is very generic though so don't focus too much on the actual application. Just tell me how to work with ranges when you usually have just indices :)
In both cases I'm only able to select the first column and not the entire range. I heard someone mention that VBA is not "matrix based" and a lot of code I look at seems to overuse loops. Is the problem actually that you can only manipulate one one-dimensional array at a time? That would be really annoying...
You can indeed select ranges by using Range and using Cells(row, column). The correct usage is as follows, to select A1:G15
Dim wS as Worksheet
Set wS = ActiveSheet
Range(wS.cells(1,1), wS.cells(15,7)).select
Note that I did specify which sheet I am using INSIDE the Range method and applied to the cells object. That's the proper way to do it.

Resources