First off, I apologize for starting a new thread but the original got confusing because I couldn't articulate my ask well (Link to original thread: Dynamic Nested Loops for Autofilter in Excel VBA). But now I have actually written the program to the way I like except using a switch statement instead of the more dynamic use of nested looping.
edit:
RSum is used to store a range and a boolean. The user selects the header cell for a column and chooses whether they want get a summation of that column or a unique count when summarizing. This allows for a collection of these objects to allow summarizing of multiple columns. This input wasn't so bad to make dynamic. The next input which starts as rtemp and ends as array1, is again the user selects the header cell for a column but this it takes the values in that column and saves a unique list to array1. With this list a for loop loops through the array using its value as criteria for an autofilter. For each step in the loop after the autofilter, the summary is calculated using the SumThisA taking the RSum object collection as an input. The data is laid out in columns where each row is a unique record.
So the question is, for the below code, I want the user to be able to select the number of categories to summarize by, have a popup to fill in those ranges (can figure this out), and then run a filter as such:
for i = 0 to UBound(array1)
Autofilter criteria1:=array1(i)
for j = 0 to UBound(array2)
Autofilter criteria1:=array2(j)
......
for x = 0 to UBound(arrayx)
Autofilter criteria1:=arrayx(x)
aSum(i,j,....x) = somefunction
Now I understand I would need to use a recursive function, but having never used one before and the somewhat complexity of this program, it is out of my understanding. Would anyone be able to help explain how to use it in this context? Plus because of the generalization of this program, it could be a useful tool for many people.
'---------Initialize Arrays---------------'
t = sMax - 1
Dim aSum()
ReDim aSum(UBound(arr1), t)
'---------------------Perform Summary----------------'
For i = LBound(arr1) To UBound(arr1)
If i = 0 Then
Data.AutoFilter field:=afield, Criteria1:=arr1, Operator:=xlFilterValues
Else
Data.AutoFilter field:=afield, Criteria1:=arr1(i)
End If
temp = SumThisA(SumValues, sMax)
For j = LBound(temp) To UBound(temp)
aSum(i, j) = temp(j)
Next j
Next i
Sum of Dollars For:
1. arrayA(1)-------100
- arrayB(1)------30
- arrayB(2)------70
2. arrayA(2)-------200
- arrayB(1)-----120
- arrayB(2)------80
3. Total-----------300
Here's a very kludgy example of recursion for what it seems you want to do. I faked up some criteria, so don't get hung up on how I'm testing for that, what's important is how the function Filter functions recursively. If I could pinpoint more exactly what you wanted I could craft it more precisely, and with less hardcoding.
Test Harness:
Public Sub Test()
Dim FilteredArray As Variant, cArray As Variant, working Array As Variant
Dim criteria As Integer
criteria = 1
ReDim criteriaArray(1 To 2)
cArray(1) = Range("C1").Value
cArray(2) = Range("C2").Value
Set workingArray = Range("A1:A7")
FilteredArray = Filter(workingArray, 7, cArray, criteria)
Range("D1") = FilteredArray
End Sub
Recursive Filter Function:
Public Function Filter(workingArray As Variant, index As Integer, _
criteriaArray As Variant, criteria) As Variant
Dim tempArray As Variant, i As Integer
ReDim tempArray(1 To 1)
For i = 1 To index
If Mid(workingArray(i), criteria, 1) = criteriaArray(criteria) Then
ReDim Preserve tempArray(1 To UBound(tempArray) + 1)
tempArray(UBound(tempArray) - 1) = workingArray(i)
End If
Next i
ReDim Preserve tempArray(1 To UBound(tempArray) - 1)
If criteria < 2 Then
Filter = Filter(tempArray, UBound(tempArray), criteriaArray, criteria + 1)
Else
Filter = tempArray
End If
End Function
Have you considered using a pivot table ? Your requirements seem very close to that functionality...
Related
This is a sample of K4 L4
[![enter image description here][2]][2] This is a sample of K63
So again I want to arrange each worksheet in the workbook based on the values in K4, L4 and than K63
Hello I'm looking for a code that will sort the worksheets in the workbook based on multiple cell values. First I will like to sort all worksheets in the workbook based on K4 (text Ascending Order) than by L4 (text Ascending Order) and finally by cell k63 (value greatest to least). I'm struggling with the logic piece on how to make it vba go in sequence. Any insight will be greatly appreciated.
I hid rows and delete sensitive data. But from the screen shot you can basically get the jist of how I would like the worksheets arranged
The following code shows how you could achieve this:
Create an array of objects that hold the information for every sheet, including the sheet name itself
Sort the array according to your needs. I have used a simple bubble sort as it is fast enough for 100 records - but if you want, feel free to look for more efficient sort algorithms, plenty around here on SO and elsewhere. The key of sorting is that you have a custom compare method that returns -1 if object 1 is "smaller" (needs to be sorted to the left) and 1 if it is "larger" - very similar to the strComp-method in VBA.
After sorting, use the sheet names of the sorted array to rearrange the sheets.
Create a class module and name it clsSheetData that holds the information needed for sorting.
Public sheetname As String
Public gmo As String
Public ovp As String
Public percent As Double
Create a regular module with the code (I assume you want to sort ThisWorkbook, else pass the workbook as parameter)
Sub SortSheets()
' Define the array
ReDim arr(1 To ThisWorkbook.Sheets.Count) As clsSheetData
' - - Step 1: Build array with data
Dim ws As Worksheet
For Each ws In ThisWorkbook.Worksheets
Set arr(ws.Index) = New clsSheetData
arr(ws.Index).sheetname = ws.Name
arr(ws.Index).gmo = ws.Range("K4")
arr(ws.Index).ovp = ws.Range("L4")
arr(ws.Index).percent = ws.Range("K63")
Next
' - - Step 2: Sort Array (Bubblesort)
Dim i As Long, j As Long
For i = 1 To UBound(arr) - 1
For j = i + 1 To UBound(arr)
If sheetCompare(arr(i), arr(j)) > 0 Then
Dim Temp As clsSheetData
Set Temp = arr(j)
Set arr(j) = arr(i)
Set arr(i) = Temp
End If
Next j
Next i
' - - Step3: Rearrange sheets
For i = 1 To UBound(arr)
With ThisWorkbook
.Sheets(arr(i).sheetname).Move before:=.Sheets(i)
End With
Next
End Sub
Function sheetCompare(o1 As clsSheetData, o2 As clsSheetData) As Integer
' Compare the data of 2 sheets.
If o1.gmo <> o2.gmo Then ' If gmo is different, use that as criteria
sheetCompare = StrComp(o1.gmo, o2.gmo, vbTextCompare)
ElseIf o1.ovp <> o2.ovp Then ' Else If ovp is different, use that as criteria
sheetCompare = StrComp(o1.ovp, o2.ovp, vbTextCompare)
Else ' Else, compare percentage
sheetCompare = IIf(o1.percent > o2.percent, -1, 1)
End If
End Function
I come to you because VBA literature online does not show many results for when dealing with Tables and list objects.
With the following code, I add the list object items to a list box in a user form. I iterate through the list object's rows. But I need to validate wether the row is hidden as sometimes there will be filters on the table in the spreadsheet:
With Main
.Clear
Dim i As Long
For i = 1 To tblDataMaster.ListRows.Count
If tblDataMaster.Row(i).Hidden = False Then
.AddItem
Dim j As Integer
For j = 0 To 9
.List(.ListCount - 1, j) = tblDataMaster.DataBodyRange(i, (j + 5))
Next j
End If
Next i
End With
As written of course, the code won't work since .Row is not a property of the list object. But just to illustrate, the If statement needs to validate if that row is hidden or not. If it is not, then it will populate the list box with it.
Something like .DataBodyRange(i,1) is not working either.
Any help, greatly appreciated.
The key is to use ListRow.Range.
Dim tblRow As ListRow
For Each tblRow In tblDataMaster.ListRows
If Not tblRow.Range.EntireRow.Hidden Then
...
End If
Next
Or if iterating by index:
For i = 1 To tblDataMaster.ListRows.Count
If Not tblDataMaster.ListRows(i).Range.EntireRow.Hidden Then
...
End If
Next
I have an excel table called AnimeList, where I have listed all the anime I have finished watching along with their info. The table has the following headers:
Name, Main Genre, Genre2, Genre3, Rating, Seasons, Episodes, Mins/Episode, Status.
I have written some VBA code that can count the distinct genres from the 3 columns as well as the number of them present.
Function CountAndSortGenre()
Dim size As Integer: size = Range("AnimeList[Main Genre]").Rows.Count
ReDim genreExtract((size * 3) - 1) As String
Dim i As Integer: i = 0
Dim cell As Range
For Each cell In Range("AnimeList[Main Genre]")
genreExtract(i) = cell.Value
i = i + 1
Next
For Each cell In Range("AnimeList[Genre - 2]")
genreExtract(i) = cell.Value
i = i + 1
Next
For Each cell In Range("AnimeList[Genre - 3]")
genreExtract(i) = cell.Value
i = i + 1
Next
Dim distinctGenres As New Dictionary
Dim genre As Variant
For Each genre In genreExtract
If distinctGenres.exists(genre) Then
distinctGenres(genre) = distinctGenres(genre) + 1
Else
distinctGenres.Add genre, 1
End If
Next
size = distinctGenres.Count
Erase genreExtract
ReDim sortedGenres(size - 1, 1) As Variant
For i = 0 To distinctGenres.Count - 1
sortedGenres(i, 0) = distinctGenres.Keys(i)
sortedGenres(i, 1) = distinctGenres.Items(i)
Next i
distinctGenres.RemoveAll
QuickSort sortedGenres, 0, size - 1 'This is done in a separate function
End Function
At the end I have what I need, i.e. the sorted genre counts in my sortedGenre array.
But I need to output it to the excel sheet now which is proving to be rather difficult task.
I tried calling the function after adding return type "As Variant" in the declaration and adding the statement CountAndSortGenre = sortedGenres at the end like so:
=CountAndSortGenre()
but the array which is returned is not spilled across multiple cells. Instead only the first element of the array is displayed on the cell where I input the formula.
I tried using Ctrl+Shift+Enter which changed the formula to:
{=CountAndSortGenre()}
but that did not change the output. It was still the first element of the array
I tried putting it in the index formula like so:
INDEX(CountAndSortGenre(), 1, 2)
trying to at least get something other than the first value of the array but that still kept returning the first value only.
Afterwards I tried using a manual approach to push the values into the cells by removing the As Variant return type and the return value in the end and adding the following code:
For i = 0 To size - 1
Application.ActiveCell.Offset(i + 1, 1) = sortedGenres(i, 0)
Application.ActiveCell.Offset(i + 1, 2) = sortedGenres(i, 1)
Next i
This approach worked when I ran the code but when I tried using the function like:
= CountAndSortGenre()
Excel gave me circular reference warning and thus it did not work.
The reason I dont want to use the macro and want to use it as a function is that I want these values to get updated as I update my source table. I am not sure that using a function will be dynamic, but it is the best bet. But right now I just want this function to start working.
I used an Array List because I'm too lazy to go look for my QuickSort routine; and I only created a single dimension output for horizontal output.
I used the range as an argument for the function so it would update dynamically when a cell in the called range is changed.
If your range may change in size, I'd suggest using either a dynamic named range, or using a Table with structured references, either of which can auto adjust the size.
If you require a vertical output, you can either Transpose before setting the output of the function; or loop into a 2D array.
Option Explicit
Option Compare Text
Function CountAndSortGenre(rg As Range) As Variant()
Dim v As Variant, w As Variant
Dim distinctGenres As Object
v = rg
Set distinctGenres = CreateObject("System.Collections.ArrayList")
With distinctGenres
For Each w In v
If w <> "" Then
If Not .contains(w) Then .Add w
End If
Next w
.Sort
CountAndSortGenre = .toarray
End With
End Function
Most people use the .Find method of a range object or use functions like If Not IsError(Application.Match([Value to Search in the Range],[Range to Search],0)) to determine whether a certain value can be found in a range. Other methods can be found in here. But if you want to match more than one criteria, it gets a little more complicated.
For instance, I want to check whether a certain Person/Date pair is present in another worksheet then write that pair if not found in the said worksheet. Referring to the example below.
The first way I would think of is use the code below:
Option Explicit
Sub Payroll()
Dim i As Long, j As Long, Present As Long
Dim Total_rows_HoursWorked As Long
Dim Total_rows_DailyTimeRecord As Long
ThisWorkbook.Worksheets("Hours Worked").Cells(1, 1) = "Person"
ThisWorkbook.Worksheets("Hours Worked").Cells(1, 2) = "Date"
Total_rows_DailyTimeRecord = ThisWorkbook.Worksheets("Daily Time Record").Range("A" & Rows.Count).End(xlUp).Row
For i = 2 To Total_rows_DailyTimeRecord
Present = 0
Total_rows_HoursWorked = ThisWorkbook.Worksheets("Hours Worked").Range("A" & Rows.Count).End(xlUp).Row
For j = 2 To Total_rows_HoursWorked
If ThisWorkbook.Worksheets("Daily Time Record").Cells(i, 1) = ThisWorkbook.Worksheets("Hours Worked").Cells(j, 1) And _
ThisWorkbook.Worksheets("Daily Time Record").Cells(i, 2) = ThisWorkbook.Worksheets("Hours Worked").Cells(j, 2) Then
Present = 1
End If
Next j
If Present = 0 Then
ThisWorkbook.Worksheets("Hours Worked").Cells(Total_rows_HoursWorked + 1, 1) = ThisWorkbook.Worksheets("Daily Time Record").Cells(i, 1)
ThisWorkbook.Worksheets("Hours Worked").Cells(Total_rows_HoursWorked + 1, 2) = ThisWorkbook.Worksheets("Daily Time Record").Cells(i, 2)
End If
Next i
End Sub
The output would be below:
But the issue with this is that it is very inefficient which will result it to run through more rows that are needed and it will be very slow especially if size of worksheets increase.
I can opt to use Arrays as well in order to speed up instead of looping through each row in a worksheet but it would still have to go through more rows than needed in order to find a match.
Another method that can be used is .Autofilter to attempt to look for matches in a certain range to minimize the looping to only those that match a certain criteria. But there is also some lag to this method but is generally faster than the first method.
What is a better way or best way of doing such tasks?
Edit:
It is not just finding the unique values but also similar to finding all values that match a certain set of criteria such as the example below:
Charles William's blog made it in a way that the ranges are resized for Application.Match and .Find but shows that Variant Array does the best but does that mean the only option is to create a nested loop and loop through each one by one, but using an array?
It does somewhat depend on the data. Ignoring the possibilities of sorting and using a hi-lo binary search then there is a comparison of using FIND vs MATCH vs variant array for a 2-column search here on my blog.
https://fastexcel.wordpress.com/2011/10/26/match-vs-find-vs-variant-array-vba-performance-shootout/
I am wondering about a simple solution for reversing the filtered values. Although it seems to me to be an easy task, I have not had a success while researching on the internet.
The situation and problem: I have a table with multiple columns, and lot of rows (exact amount does not matter obviously) and I want to see what was not filtered in exactly one column. The problem is that I normally need to do a lot of clicking
For example - in the database of projects I have filtered the ones worth over 500 000 €, which are mine and are coming from a specific country. By one click I would like to see which ones are below 500 000 €) but are still mine and coming from a specific country.
Possible solutions which came to my mind:
Create a unique list of what is filtered, unfiltered, and create an unique list of full column, AdvanceFilter by the difference. (That's my white horse - it might work in my opinion)
Go through each filtering options and check/uncheck one by one.
Screenshot filters, transfer to text, create a unique values at the column, invert the filter in advanced filtering (very crazy idea, came out of desperation)
Somewhere easily take a list of what's filtered and inverted it by easy function (that was my initial thought but not working!)
Does anybody has an idea how to approach this situation?
I am able to try the VBA on my own so I would be happy if you can point me in the right direction. Of course I would welcome your thoughts in code too.
Here's an idea to toggle a numeric filter. It won't work with all numeric filters, but most of them. For instance, it won't work with Between, because that uses Criteria1 and Criteria2. But you could expand the code to account for that.
Also, it only really works on numeric filters. It will work on some text filters, but only if one criteria is applied.
Sub InvertNumericFilter()
Dim lFilter As Long
Dim lo As ListObject
Dim fltr As Filter
Dim aOper As Variant, aOpp As Variant
Dim i As Long
'aOpp is the opposite of the corresponding
'operator in aOper
aOper = Split("<> <= >= = < >")
aOpp = Split("= > < <> >= <=")
'Find which column you're in
Set lo = ActiveCell.ListObject
lFilter = ActiveCell.Column - lo.DataBodyRange.Column + 1
Set fltr = lo.AutoFilter.Filters(lFilter)
'if the first characters of the criteria are in aOper
'then swap them for aOpp
For i = LBound(aOper) To UBound(aOper)
If Left(fltr.Criteria1, Len(aOper(i))) = aOper(i) Then
lo.DataBodyRange.AutoFilter lFilter, Replace$(fltr.Criteria1, aOper(i), aOpp(i))
Exit For
End If
Next i
End Sub
Your example happened to be inverting a number, but if you want it to be universal (apply to nonnumerics), it would get a lot more complicated.
Update
This will invert value lists, but it makes some assumptions. For one, if you only have two values, it's not a value list, it's an xlOr operator. If you're using xlOr on some other type of field, it might cause problems.
Sub InvertFilter()
Dim lFilter As Long
Dim lo As ListObject
Dim fltr As Filter
Dim aOper As Variant, aOpp As Variant
Dim i As Long, j As Long
Dim dc As Scripting.Dictionary
Dim vaValues As Variant
'Find which column you're in
Set lo = ActiveCell.ListObject
lFilter = ActiveCell.Column - lo.DataBodyRange.Column + 1
Set fltr = lo.AutoFilter.Filters(lFilter)
'lists of values or just two values
If fltr.Operator = xlFilterValues Or fltr.Operator = xlOr Then
'get all the possible values and put in a dictionary
vaValues = lo.ListColumns(lFilter).DataBodyRange.Value
Set dc = New Scripting.Dictionary
For i = LBound(vaValues, 1) To UBound(vaValues, 1)
If Not dc.Exists("=" & vaValues(i, 1)) Then
dc.Add "=" & vaValues(i, 1), "=" & vaValues(i, 1)
End If
Next i
'If it's more than two values
If IsArray(fltr.Criteria1) Then
'remove from dictionary
For i = LBound(fltr.Criteria1) To UBound(fltr.Criteria1)
If dc.Exists(fltr.Criteria1(i)) Then
dc.Remove fltr.Criteria1(i)
End If
Next i
Else
dc.Remove fltr.Criteria1
dc.Remove fltr.Criteria2
End If
'reapply filter
lo.DataBodyRange.AutoFilter lFilter, dc.Keys, xlFilterValues
ElseIf fltr.Operator = 0 Then
'aOpp is the opposite of the corresponding
'operator in aOper
aOper = Split("<> <= >= = < >")
aOpp = Split("= > < <> >= <=")
'if the first characters of the criteria are in aOper
'then swap them for aOpp
For i = LBound(aOper) To UBound(aOper)
If Left(fltr.Criteria1, Len(aOper(i))) = aOper(i) Then
lo.DataBodyRange.AutoFilter lFilter, Replace$(fltr.Criteria1, aOper(i), aOpp(i))
Exit For
End If
Next i
End If
End Sub