Horizontal sort of paired entries - sorting

My VBA skills are basic. I would appreciate help with sorting numbers in a row but with moving their corresponding strings. For example, these rows:
╔═══════╦═════════╦═══════╦═════════╗
║ Name1 ║ Number1 ║ Name2 ║ Number2 ║
╠═══════╬═════════╬═══════╬═════════╣
║ Joe ║ 5 ║ John ║ 10 ║
╚═══════╩═════════╩═══════╩═════════╝
should become:
╔═══════╦═════════╦═══════╦═════════╗
║ Name1 ║ Number1 ║ Name2 ║ Number2 ║
╠═══════╬═════════╬═══════╬═════════╣
║ John ║ 10 ║ Joe ║ 5 ║
╚═══════╩═════════╩═══════╩═════════╝
The code I am trying to adjust is:
Sub hsort()
Dim lLast As Long, lLoop As Long
lLast = Cells(Rows.Count, 1).End(xlUp).Row
For lLoop = 2 To lLast
range(cells(lLoop,4),cells(lLoop,23)).Sort key1:=Cells(lLoop, 5), order1:=xlDescending,key2:=Cells(lLoop, 4), order2:=xlDescending, Header:=xlGuess, OrderCustom:=1, MatchCase:= _False, Orientation:=xlLeftToRight, DataOption1:=xlSortNormal, DataOption2:=xlSortNormal
Next
End Sub
which orders the strings first and then the numbers, rather than moving them together as wished.

This is very easy with a dictionary, but as #pnuts pointed out, it's a bit advanced. What we're going to do here is to invoke a dictionary, store your data there, transfer them to an array, bubble sort them in descending order, put them back into the dictionary, and print them out.
Whew. Anyway, credits to this site for the framework.
Anyway, code first.
'http://www.xl-central.com/sort-a-dictionary-by-item.html
Sub SortDictionaryByItem()
'Set a reference to Microsoft Scripting Runtime by using
'Tools > References in the Visual Basic Editor (Alt+F11)
'Declare the variables
Dim Dict As Scripting.Dictionary
Dim Arr() As Variant
Dim Temp1 As Variant
Dim Temp2 As Variant
Dim Txt As String
Dim i As Long
Dim j As Long
Dim LastCol As Long, Iter As Long, Iter2 As Long, Iter3 As Long
'Create an instance of the Dictionary
Set Dict = New Dictionary
'Set the comparison mode to perform a textual comparison
Dict.CompareMode = TextCompare
'''''''''''''''''BK201's Mod'''''''''''''''''
'Get the last column of the row.
LastCol = Range("A1").End(xlToRight).Column 'Modify accordingly.
'Add keys and items to the Dictionary
For Iter = 1 To (LastCol - 1) Step 2
Dict.Add Cells(1, Iter).Value, Cells(1, Iter + 1).Value
Next Iter
'''''''''''''''''BK201's Mod'''''''''''''''''
'Allocate storage space for the dynamic array
ReDim Arr(0 To Dict.Count - 1, 0 To 1)
'Fill the array with the keys and items from the Dictionary
For i = 0 To Dict.Count - 1
Arr(i, 0) = Dict.Keys(i)
Arr(i, 1) = Dict.Items(i)
Next i
'Sort the array using the bubble sort method
For i = LBound(Arr, 1) To UBound(Arr, 1) - 1
For j = i + 1 To UBound(Arr, 1)
If Arr(i, 1) < Arr(j, 1) Then
Temp1 = Arr(j, 0)
Temp2 = Arr(j, 1)
Arr(j, 0) = Arr(i, 0)
Arr(j, 1) = Arr(i, 1)
Arr(i, 0) = Temp1
Arr(i, 1) = Temp2
End If
Next j
Next i
'Clear the Dictionary
Dict.RemoveAll
'Add the sorted keys and items from the array back to the Dictionary
For i = LBound(Arr, 1) To UBound(Arr, 1)
Dict.Add Key:=Arr(i, 0), Item:=Arr(i, 1)
Next i
'''''''''''''''''BK201's Mod'''''''''''''''''
'Change Cells(2, Iter2) to Cells(1, Iter2) to overwrite.
KeyIndex = 0
For Iter2 = 1 To (LastCol - 1) Step 2
Cells(2, Iter2).Value = Dict.Keys(KeyIndex)
KeyIndex = KeyIndex + 1
Next Iter2
For Iter3 = 2 To LastCol Step 2
Cells(2, Iter3).Value = Dict.Item(Cells(2, Iter3 - 1).Value)
Next Iter3
'''''''''''''''''BK201's Mod'''''''''''''''''
Set Dict = Nothing
End Sub
Screenshots:
Set-up:
Result after running code:
Modify the ranges involved accordingly. Let us know if this helps.

Assuming Name1 is in A1, if you add temporarily a row between Name1 and John with =IF(ISEVEN(COLUMN()),A3,B3) in it copied across to suit you should then achieve the order I think you want with a normal left to right sort and the temporary row can then be deleted. Build this in to VBA if you wish.

At the end this is the solution I adopted but is really Really slow! Does anyone have any suggestion to improve this code? Dictionary seemed to me a good solution but I don't know how to use it so I ask you if it is implementable in this situation.
Sub Reorder()
With Application
.ScreenUpdating = False
.EnableEvents = False
.Calculation = xlCalculationManual
End With
Dim i, c, j As Integer
i = 7
Do
j = 5
Do
Workbooks("Ownership Full v3.xlsx").Activate
Range(Cells(i, j), Cells(i, j + 1)).Copy
Workbooks("Book1.xlsx").Activate
If Range("A2") = blank Then
Range("A2").Select
Else
Range("A1").End(xlDown).Select
Selection.Offset(1, 0).Select
End If
ActiveSheet.Paste
j = j + 2
Workbooks("Ownership Full v3.xlsx").Activate
Loop While (j <= 23)
Workbooks("Book1.xlsx").Activate
Range("B2:B11").Select
ActiveWorkbook.Worksheets("Sheet1").Sort.SortFields.Clear
ActiveWorkbook.Worksheets("Sheet1").Sort.SortFields.Add Key:=Range("B2"), _
SortOn:=xlSortOnValues, Order:=xlDescending, DataOption:= _
xlSortTextAsNumbers
With ActiveWorkbook.Worksheets("Sheet1").Sort
.SetRange Range("A1:B11")
.Header = xlYes
.MatchCase = False
.Orientation = xlTopToBottom
.SortMethod = xlPinYin
.Apply
End With
j = 5
c = 2
Do
Workbooks("Book1.xlsx").Activate
Range(Cells(c, 1), Cells(c, 2)).Cut
Workbooks("Ownership Full v3.xlsx").Activate
Cells(i, j).Select
ActiveSheet.Paste
c = c + 1
j = j + 2
Loop While (c <= 11)
i = i + 1
Loop While (Cells(i, 1) <> blank)
With Application
.ScreenUpdating = True
.EnableEvents = True
.Calculation = xlCalculationAutomatic
End With
End Sub

Related

Merge cell excel VBA time performance

I have a performance problem in my VBA-Excel code.
I have 42 rows and 55 columns (it can be increased).
My purpose is to merge the cells (in each 2 rows) that have same value using some steps (I want to make a gantt chart).
Before merge
After merge
The first step is merged on column based (for each 2 rows):
compare cell (row, col) and (row+1, col)
If it has same value, compare cell (row, col) and (row, col+1)
if it has same value, compare cell (row, col+1) and (row+1, col+1), check the next column, and go to step 1
if step 2 or 3 is false, then merge the cells from the first cell (row, col) until the last cell that have same value (cell(row + 1, col + n - 1)
if step 1 is false, then go to the next column
after that, I have to merge on row based (still for each 2 rows).
if the cell(row, col) and cell (row, col + 1) are not merged, if cell (row, col) and cell (row, col + 1) have the same value, go to next column.
if step 1 is false, then merge the cells from cell(row, col) until cell(row, col + n - 1)
I have created the code below, but I am facing a performance issue.
The time to finish this code is at least 4 minutes.
I tried to remove the merge line for checking, and the time is only 1 second.
I concluded that there is something not correct on the merge process, but I couldn't figure it out.
If you have any suggestion regarding my code, please share it.
Thank you very much...
Sub MergeCell()
Dim StartTime As Double, RunTime As Double
StartTime = Timer
Application.ScreenUpdating = False
Application.DisplayAlerts = False
Application.EnableEvents = False
Application.DisplayStatusBar = False
Application.Calculation = xlCalculationManual
Dim i As Long, j As Long, jmax1 As Long, maxRows As Long, maxCols As Long
Dim merge As Long, iMerge As Long, jMerge As Long, Jump As Long
Dim chckst As String
maxRows = 42
maxCols = 55
Dim Rng As Range, Rng3 As Range
Set Rng = Sheets("Sheet1").Range("E5").Resize(maxRows, maxCols)
Dim chk As Long
i = 1
Do While i < maxRows
j = 1
Do While j < maxCols
iMerge = 0
jMerge = 0
merge = 0
Jump = 0
If Rng.Cells(i, j).Value2 = Rng.Cells(i + 1, j).Value2 Then
jmax1 = j
iMerge = i + 1
jMerge = jmax1
merge = 1
For chk = jmax1 + 1 To maxCols - 1
If Rng.Cells(i, j).Value2 = Rng.Cells(i, chk).Value2 Then
If Rng.Cells(i, chk).Value2 = Rng.Cells(i + 1, chk).Value2 Then
jmax1 = jmax1 + 1
Else
Jump = 1
Exit For
End If
Else
Exit For
End If
Next
Else
j = j + 1
End If
If merge > 0 Then
'when I removed this merge line, the speed is good, like I said before
Range(Rng.Cells(i, j), Rng.Cells(iMerge, jmax1)).merge
j = jmax1 + 1
If Jump = 1 Then
j = j + 1
End If
End If
Loop
i = i + 2
Loop
RunTime = Round(Timer - StartTime, 2)
MsgBox "Run Time = " & RunTime & " seconds", vbInformation
Dim colId1 As Long, colId2 As Long
Dim colct As Long
i = 1
Do While i <= maxRows
j = 1
Do While j < maxCols
merge = 0
jmax1 = j
If Rng.Cells(i, jmax1).MergeCells = True Then
colct = Rng.Cells(i, jmax1).MergeArea.Columns.Count - 1
jmax1 = jmax1 + colct
j = jmax1 + 1
Else
For chk = jmax1 + 1 To maxCols
If Rng.Cells(i, j) = Rng.Cells(i, chk) And Rng.Cells(i, chk).MergeCells = False Then
merge = 1
colId1 = j
colId2 = jmax1 + 1
If chk <> maxCols Then
jmax1 = jmax1 + 1
Else
j = jmax1 + 1
Exit For
End If
Else
j = jmax1 + 1
Exit For
End If
Next
End If
If merge > 0 Then
'when I removed this merge line, the speed is good, like I said before
Range(Rng.Cells(i, colId1), Rng.Cells(i, colId2)).merge
End If
Loop
i = i + 1
Loop
Rng.HorizontalAlignment = xlCenter
Rng.VerticalAlignment = xlCenter
On Error GoTo HERE
HERE:
Application.ScreenUpdating = True
Application.DisplayAlerts = True
Application.EnableEvents = True
Application.DisplayStatusBar = True
Application.Calculation = xlCalculationAutomatic
RunTime = Round(Timer - StartTime, 2)
MsgBox "Done!" & vbNewLine & "Run Time = " & RunTime & " seconds", vbInformation
End Sub
The problem is in the conditional formatting.
I only need to remove the conditional formatting before merge, merge it, then put the conditional formatting again.
With this code, everything is fine and fast now. It's only need 2 seconds.
thank you for everyone who contributes to help..
regards,
Suggestion 1
declare variables like this:
Dim i as long, j as long, jmax1 as long, maxRows as long, maxCols as long etc. If you do not specify the type, they get declared as variant. In your line only the last one - Jump is declared as long.
If you redeclare them, it may run faster.
Suggestion 2
Do not use integers in VBA. stackoverflow.com/questions/26409117/
Suggestion 3
Do not use GoTo https://en.wikipedia.org/wiki/Spaghetti_code
Suggestion 4
In general merge is slow in VBA/Excel. But still, to see what you are doing, write this before the merge:
debug.Print Range(Rng.Cells(i, j), Rng.Cells(iMerge, jmax1)).Address
It can be that you are merging more than expected or something else.

Swimming Medley Relay Time Simulation Algorithm

I am trying to simulate the I/O of this website page
My Input sheet looks like this:
Now after taking the values from input sheet and arranging them in ascending order I got this in a temp worksheet :
This is what my results sheet looks like:
Now I have tried this after sorting process(didn't add code for sorting since it's not the problem):
Set rng = Union(wTime.Range("D6:D25"), wTime.Range("F6:F25"), wTime.Range("H6:H25"), wTime.Range("J6:J25"))
cnt1 = 1: cnt2 = 1: cnt3 = 1: cnt4 = 1
wTime.Range("A6:A25") = Empty 'Ticker
For i = 1 To 20
bckStroke(i) = wTemp.Range("A" & i + 1).Value
brstStroke(i) = wTemp.Range("C" & i + 1).Value
btrFly(i) = wTemp.Range("E" & i + 1).Value
frStyle(i) = wTemp.Range("G" & i + 1).Value
wTime.Range("A6:A25") = Empty
For Each cel In rng
If cel.Column = 4 And cel.Value = bckStroke(i) And cel.Value <> 0 And Trim(wTime.Cells(cel.Row, 1)) <> "Y" And cnt1 < 6 Then
wRes.Cells((cnt1 + 5 + (cnt1 - 1) * 2) - 1, 4) = wTime.Cells(cel.Row, 2) 'Athlete Name
wRes.Cells(cnt1 + 5 + (cnt1 - 1) * 2, 4) = bckStroke(i) 'Time
cnt1 = cnt1 + 1
wTime.Cells(cel.Row, 1) = "Y"
End If
If cel.Column = 6 And cel.Value = brstStroke(i) And cel.Value <> 0 And Trim(wTime.Cells(cel.Row, 1)) <> "Y" And cnt2 < 6 Then
wRes.Cells((cnt2 + 5 + (cnt2 - 1) * 2) - 1, 6) = wTime.Cells(cel.Row, 2) 'Athlete Name
wRes.Cells(cnt2 + 5 + (cnt2 - 1) * 2, 6) = brstStroke(i) 'Time
cnt2 = cnt2 + 1
wTime.Cells(cel.Row, 1) = "Y"
End If
If cel.Column = 8 And cel.Value = btrFly(i) And cel.Value <> 0 And Trim(wTime.Cells(cel.Row, 1)) <> "Y" And cnt3 < 6 Then
wRes.Cells((cnt3 + 5 + (cnt3 - 1) * 2) - 1, 8) = wTime.Cells(cel.Row, 2) 'Athlete Name
wRes.Cells(cnt3 + 5 + (cnt3 - 1) * 2, 8) = btrFly(i) 'Time
cnt3 = cnt3 + 1
wTime.Cells(cel.Row, 1) = "Y"
End If
If cel.Column = 10 And cel.Value = frStyle(i) And cel.Value <> 0 And Trim(wTime.Cells(cel.Row, 1)) <> "Y" And cnt4 < 6 Then
wRes.Cells((cnt4 + 5 + (cnt4 - 1) * 2) - 1, 10) = wTime.Cells(cel.Row, 2) 'Athlete Name
wRes.Cells(cnt4 + 5 + (cnt4 - 1) * 2, 10) = frStyle(i) 'Time
cnt4 = cnt4 + 1
wTime.Cells(cel.Row, 1) = "Y"
End If
Next cel
Next i
I just want to know the simplest logic to get the desired result after arranging them in ascending order (refer temp sheet) it should be easy but I can't seem to understand it.
Conditions that I know of for now:
Each team should have unique swimmers (i.e 4 Unique names in each team)
A swimmer can appear in other team as well if he has best time in other category as well. (E.g. Marcelo will appear in top 4 team since he has the best time in all 4 categories)
Teams with shortest time should be placed 1st in the list on result sheet. I think sorting in ascending order takes care of this it's matter of selecting right swimmer from the temp sheet list.
EDIT:
4. Relay Logic premise: Get all the combinations possible without 2 identical strings. And then sort them lowest to largest. I'd do the following: Get all the possible combinations and their sum with the following: *Combinations may still be buggy, since it may be variable to how many numbers you may have. This is just a guide to describe the process
Sub Combinations()
Dim i As Long, j As Long, k As Long, l As Long, m As Long, n As Long, o As Long, p As Long, q As Long
Dim CountComb As Long, lastrow As Long
Range("K2").Value = Now - 5
Application.ScreenUpdating = False
CountComb = 0: lastrow = 6
For i = 1 To 6: For j = 1 To 5
For k = 1 To 6: For l = 1 To 6
If Not (i = j Or i = k Or i = l Or j = k Or j = l Or k = l) Then
Range("K" & lastrow).Value = Range("A" & i).Value & "/" & _
Range("B" & j).Value & "/" & _
Range("C" & k).Value & "/" & _
Range("D" & l).Value
lastrow = lastrow + 1
CountComb = CountComb + 1
End If
Next: Next
Next: Next
Range("K1").Value = CountComb
Range("K3").Value = Now + 21
Application.ScreenUpdating = True
End Sub
Function TimeSum(Persons As String, Chr As String) As Double
Dim ArrayPersons() As String: ArrayPersons = Split(Persons, Chr)
Dim SumOfTime As Double
Dim ItemPerson As Variant
Dim NumberRoutines As Long: NumberRoutines = 2
Const SheetData = "Sheet1"
For Each ItemPerson In ArrayPersons
SumOfTime = Sheets(SheetData).Columns(NumberRoutines).Find(ItemPerson).Offset(0, -1).Value + SumOfTime
NumberRoutines = NumberRoutines + 2
Next ItemPerson
TimeSum = SumOfTime
End Function
Maybe you could define better the sub to do what you desire for, but, the last coding could guide you in the right path. In a second thought, you could get combinations in a dictionary instead.
[
[

iF Then Else code - how to make this run faster? VBA

I have a simple code which takes a long time to run. I was wondering if there is any way to make this run faster? Maybe this part (Cells(i, "U").Value = Cells(n, "X").Value) should not be used 2 times! Thanks!
For n = 3 To time_frame + 3
For i = 3 To 1002
If (Cells(i, "U").Value = Cells(n, "X").Value) And (Bed_in_use < 24) And Wait_L > 0 Then
Wait_L = Wait_L - (24 - Bed_in_use)
ElseIf (Cells(i, "U").Value = Cells(n, "X").Value) And (Bed_in_use < 24) And Wait_L <= 0 Then
Bed_in_use = Bed_in_use + 1
End If
Next i
Next n
MsgBox "The number of bed in use is " & Bed_in_use & ". There are " & Wait_L & " patients in the waiting list."
End Sub
Couple things will speed this up - The first was mentioned in the comments by #jcarroll, pulling the cells you need into an array and using that instead of making repeated calls to Cells.
The second is what you mentioned, structuring your If statements in a way that
you aren't making the same comparisons twice. For example, this has to be true for either condition...
Cells(i, "U").Value = Cells(n, "X").Value
...and this always has to be true:
Bed_in_use < 24
After Bed_in_use is 24 (or higher), you can exit out of the loop because you'll never satisfy either the If or the ElseIf statement. I'd re-roll it into something like this:
Dim values() As Variant
values = ActiveSheet.UsedRange '...or whatever Range you need.
For n = 3 To time_frame + 3
If Bed_in_use >= 24 Then Exit For
For i = 3 To 1002
If Bed_in_use >= 24 Then Exit For
If values(i, 21).Value = values(n, 24).Value Then
If Wait_L > 0 Then
Wait_L = Wait_L - (24 - Bed_in_use)
Else
Bed_in_use = Bed_in_use + 1
End If
End If
Next i
Next n
I'm not totally sure what your code is trying to do. But here is a sample of how you would compare two lists, and keep track of the total matches.
Sub test()
Dim arrayU() As Variant
Dim arrayX() As Variant
Dim LrowU As Integer
Dim LrowX As Integer
Dim i As Integer
Dim j As Integer
Dim bed_in_use As Integer
LrowU = Columns(21).Find(What:="*", LookIn:=xlValues, SearchOrder:=xlByRows, SearchDirection:=xlPrevious).Row
LrowX = Columns(24).Find(What:="*", LookIn:=xlValues, SearchOrder:=xlByRows, SearchDirection:=xlPrevious).Row
ReDim arrayU(1 To LrowU)
ReDim arrayX(1 To LrowX)
For i = 1 To LrowU
arrayU(i) = Cells(i, 21)
Next i
i = 1
For i = 1 To LrowX
arrayX(i) = Cells(i, 24)
Next i
i = 1
j = 1
For i = 1 To LrowX
For j = 1 To LrowU
If arrayX(i) = arrayU(j) Then bed_in_use = bed_in_use + 1
Next j
Next i
MsgBox (bed_in_use)
End Sub

How to count and total items in a column/sheet

I am running osx 10.9 and Excel:Mac2011. I have need to count how many of each item I have in an Excel spreadsheet.
For example:
Cat
Fish
Cat
Dog
Cat
Dog
I'm trying to get some output that would look like this
Cat Cat =3
Fish Dog =2
Cat Fish =1
Dog
Cat
Dog
the output does not need to be sorted/ordered. First come first counted/listed is fine, but i can sort the data if it needs to be.(or makes it easier)
If there is any more information I can provide to help you help me please let me know.
First and simplest, if you know that there aren't many different values in the column, you could just use countif():
=COUNTIF(A1:A6, "Cat")
Otherwise, if you've got tons of different items in a column and you want an automated solution, a VBA routine that scans the column, tallies the counts of each item, and deposits those counts in other columns seems reasonable.
Sub CountAll()
Dim searchCol, itemsCol, countCol, sheetName As String
Dim i, j, startRow As Integer
Dim aCounts() As Variant
Dim bAdded, bFound As Boolean
startRow = 1
searchCol = "A"
itemsCol = "B"
countCol = "C"
sheetName = "Sheet1"
ReDim aCounts(2, 1)
With Sheets(sheetName)
For i = 1 To .Range(searchCol & startRow).End(xlDown).Row
For j = 1 To UBound(aCounts, 2)
If (.Range(searchCol & i).Value) = aCounts(0, j) Then
bFound = True
Exit For
Else
bFound = False
End If
Next
If (bFound) Then
aCounts(1, j) = aCounts(1, j) + 1
Else
If (aCounts(1, UBound(aCounts, 2)) <> "") Then
ReDim Preserve aCounts(2, UBound(aCounts, 2) + 1)
End If
aCounts(0, UBound(aCounts, 2)) = .Range(searchCol & i).Value
aCounts(1, UBound(aCounts, 2)) = 1
End If
Next
i = 1
For i = 1 To UBound(aCounts, 2)
.Range(itemsCol & i).Value = aCounts(0, i)
.Range(countCol & i).Value = aCounts(1, i)
Next
End With
End Sub
If all your sheets are similar and you want it to run on each sheet, just change the With Sheets(sheetName) to For Each Sheet in Sheets, change End With to Next, add Sheet before each .Range, and reset the array with each iteration:
For Each Sheet In Sheets()
ReDim aCounts(2, 1)
For i = 1 To Sheet.Range(searchCol & startRow).End(xlDown).Row
For j = 1 To UBound(aCounts, 2)
If (Sheet.Range(searchCol & i).Value) = aCounts(0, j) Then
bFound = True
Exit For
Else
bFound = False
End If
Next
If (bFound) Then
aCounts(1, j) = aCounts(1, j) + 1
Else
If (aCounts(1, UBound(aCounts, 2)) <> "") Then
ReDim Preserve aCounts(2, UBound(aCounts, 2) + 1)
End If
aCounts(0, UBound(aCounts, 2)) = Sheet.Range(searchCol & i).Value
aCounts(1, UBound(aCounts, 2)) = 1
End If
Next
For i = 1 To UBound(aCounts, 2)
Sheet.Range(itemsCol & i).Value = aCounts(0, i)
Sheet.Range(countCol & i).Value = aCounts(1, i)
Next
Next
You could loop through all the sheets creating a pivot table for each one, and then copy and paste the data from each pivot table, back onto the source sheet. It's a weird way of doing it, but it will work. Here is the code:
Option Explicit
Sub PivotTableCreator()
Application.ScreenUpdating = False
Application.DisplayAlerts = False 'Done so excel will delete the pivot table sheet without displaying a messagebox asking you if you are sure
Dim i, WSCount, LastRow As Long
WSCount = ActiveWorkbook.Worksheets.Count
Dim PTCache As PivotCache
Dim PT As PivotTable
For i = 1 To WSCount
Worksheets(i).Activate
LastRow = Range("A1000000").End(xlUp).Row
Debug.Print LastRow
Set PTCache = ActiveWorkbook.PivotCaches.Create(xlDatabase, Range("A1", "A" & LastRow)) 'data from column A is used to create the pivot table
Worksheets.Add 'new worksheet created for pivottable
Set PT = ActiveSheet.PivotTables.Add(PivotCache:=PTCache, TableDestination:=Range("A1"))
With PT
.PivotFields("Animals").Orientation = xlRowField 'Place whatever column A's header is here (Animals is a placeholder)
.PivotFields("Animals").Orientation = xlDataField
.DisplayFieldCaptions = False
End With
ActiveCell.CurrentRegion.Copy
Worksheets(ActiveSheet.Index + 1).Range("B2").PasteSpecial Paste:=xlPasteValues 'Paste results where you want to, I use B2 in this example
ActiveSheet.Delete 'No longer need the pivot table, so this sheet is deleted
Next
End Sub

Memory and execution time reduction for algorithms

I have been asked to ask this question again and in a little different context. This is the previous post:
Filtering in VBA after finding combinations
I would like to make this code possible with 100 different variables without having excel run out of memory and reducing the execution time significantly.
The problem with the code below is that if I have 100 boxes, excel will run out of memory in the line "Result(0 To 2 ^ NumFields - 2)" ( The code works for < 10 boxes)
This is my input:
3 A B C D E ...
7.7 3 1 1 1 2 ...
5.5 2 1 2 3 3 ...
This is the code:
Function stackBox()
Dim ws As Worksheet
Dim width As Long
Dim height As Long
Dim numOfBox As Long
Dim optionsA() As Variant
Dim results() As Variant
Dim str As String
Dim outputArray As Variant
Dim i As Long, j As Long
Dim currentSymbol As String
'------------------------------------new part----------------------------------------------
Dim maxHeight As Double
Dim maxWeight As Double
Dim heightarray As Variant
Dim weightarray As Variant
Dim totalHeight As Double
Dim totalWeight As Double
'------------------------------------new part----------------------------------------------
Set ws = Worksheets("Sheet1")
With ws
'clear last time's output
height = .Cells(.Rows.Count, 1).End(xlUp).row
If height > 3 Then
.Range(.Cells(4, 1), .Cells(height, 1)).ClearContents
End If
numOfBox = .Cells(1, 1).Value
width = .Cells(1, .Columns.Count).End(xlToLeft).Column
If width < 2 Then
MsgBox "Error: There's no item, please fill your item in Cell B1,C1,..."
Exit Function
End If
'------------------------------------new part----------------------------------------------
maxHeight = .Cells(2, 1).Value
maxWeight = .Cells(3, 1).Value
ReDim heightarray(1 To 1, 1 To width - 1)
ReDim weightarray(1 To 1, 1 To width - 1)
heightarray = .Range(.Cells(2, 2), .Cells(2, width)).Value
weightarray = .Range(.Cells(3, 2), .Cells(3, width)).Value
'------------------------------------new part----------------------------------------------
ReDim optionsA(0 To width - 2)
For i = 0 To width - 2
optionsA(i) = .Cells(1, i + 2).Value
Next i
GenerateCombinations optionsA, results, numOfBox
' copy the result to sheet only once
ReDim outputArray(1 To UBound(results, 1) - LBound(results, 1) + 1, 1 To 1)
Count = 0
For i = LBound(results, 1) To UBound(results, 1)
If Not IsEmpty(results(i)) Then
'rowNum = rowNum + 1
str = ""
totalHeight = 0#
totalWeight = 0#
For j = LBound(results(i), 1) To UBound(results(i), 1)
currentSymbol = results(i)(j)
str = str & currentSymbol 'results(i)(j) is the SYMBOL e.g. A, B, C
'look up box's height and weight , increment the totalHeight/totalWeight
updateParam currentSymbol, optionsA, heightarray, weightarray, totalHeight, totalWeight
Next j
If totalHeight < maxHeight And totalWeight < maxWeight Then
Count = Count + 1
outputArray(Count, 1) = str
End If
'.Cells(rowNum, 1).Value = str
End If
Next i
.Range(.Cells(4, 1), .Cells(UBound(outputArray, 1) + 3, 1)).Value = outputArray
End With
End Function
Sub updateParam(ByRef targetSymbol As String, ByRef symbolArray As Variant, ByRef heightarray As Variant, ByRef weightarray As Variant, ByRef totalHeight As Double, ByRef totalWeight As Double)
Dim i As Long
Dim index As Long
index = -1
For i = LBound(symbolArray, 1) To UBound(symbolArray, 1)
If targetSymbol = symbolArray(i) Then
index = i
Exit For
End If
Next i
If index <> -1 Then
totalHeight = totalHeight + heightarray(1, index + 1)
totalWeight = totalWeight + weightarray(1, index + 1)
End If
End Sub
Sub GenerateCombinations(ByRef AllFields() As Variant, _
ByRef Result() As Variant, ByVal numOfBox As Long)
Dim InxResultCrnt As Integer
Dim InxField As Integer
Dim InxResult As Integer
Dim i As Integer
Dim NumFields As Integer
Dim Powers() As Integer
Dim ResultCrnt() As String
NumFields = UBound(AllFields) - LBound(AllFields) + 1
ReDim Result(0 To 2 ^ NumFields - 2) ' one entry per combination
ReDim Powers(0 To NumFields - 1) ' one entry per field name
' Generate powers used for extracting bits from InxResult
For InxField = 0 To NumFields - 1
Powers(InxField) = 2 ^ InxField
Next
For InxResult = 0 To 2 ^ NumFields - 2
' Size ResultCrnt to the max number of fields per combination
' Build this loop's combination in ResultCrnt
ReDim ResultCrnt(0 To NumFields - 1)
InxResultCrnt = -1
For InxField = 0 To NumFields - 1
If ((InxResult + 1) And Powers(InxField)) <> 0 Then
' This field required in this combination
InxResultCrnt = InxResultCrnt + 1
ResultCrnt(InxResultCrnt) = AllFields(InxField)
End If
Next
If InxResultCrnt = 0 Then
Debug.Print "testing"
End If
'additional logic here
If InxResultCrnt >= numOfBox Then
Result(InxResult) = Empty
Else
' Discard unused trailing entries
ReDim Preserve ResultCrnt(0 To InxResultCrnt)
' Store this loop's combination in return array
Result(InxResult) = ResultCrnt
End If
Next
End Sub
Here's a version that does all the heavy lifting in variant arrays
(Combinations logic based on this answer for This Answer by Joubarc)
This runs on a sample dataset of 100 boxes with > 40,000 returned, and in < 1 second
Notes:
Execution time rises quickly if the Max number of boxes increases (eg 4 from 100: approx 13s)
If the number of returned results exceeds 65535, the code to tranpose the array into the sheet fails (last line of the sub) If you need to handle this may results, you will need to change the way results are returned to the sheet
Sub Demo()
Dim rNames As Range
Dim rHeights As Range
Dim rWeights As Range
Dim aNames As Variant
Dim aHeights As Variant
Dim aWeights As Variant
Dim MaxNum As Long
Dim MaxHeight As Double
Dim MaxWeight As Double
' *** replace these six line with your data ranges
Set rNames = Range([F5], [F5].End(xlToRight))
Set rHeights = rNames.Offset(1, 0)
Set rWeights = rNames.Offset(2, 0)
MaxNum = [C5]
MaxHeight = [C6]
MaxWeight = [C7]
aNames = rNames
aHeights = rHeights
aWeights = rWeights
Dim Result() As Variant
Dim n As Long, m As Long
Dim i As Long, j As Long
Dim iRes As Long
Dim res As String
Dim TestCombin() As Long
Dim TestWeight As Double
Dim TestHeight As Double
Dim idx() As Long
' Number of boxes
ReDim TestCombin(0 To MaxNum - 1)
n = UBound(aNames, 2) - LBound(aNames, 2) + 1
' estimate size of result array = number of possible combinations
For m = 1 To MaxNum
i = i + Application.WorksheetFunction.Combin(n, m)
Next
ReDim Result(1 To 3, 1 To i)
' allow for from 1 to MaxNum of boxes
iRes = 1
For m = 1 To MaxNum
ReDim idx(0 To m - 1)
For i = 0 To m - 1
idx(i) = i
Next i
Do
'Test current combination
res = ""
TestWeight = 0#
TestHeight = 0#
For j = 0 To m - 1
'Debug.Print aNames(1, idx(j) + 1);
res = res & aNames(1, idx(j) + 1)
TestWeight = TestWeight + aWeights(1, idx(j) + 1)
TestHeight = TestHeight + aHeights(1, idx(j) + 1)
Next j
'Debug.Print
If TestWeight <= MaxWeight And TestHeight <= MaxHeight Then
Result(1, iRes) = res
' optional, include actual Height and Weight in result
Result(2, iRes) = TestHeight
Result(3, iRes) = TestWeight
iRes = iRes + 1
End If
' Locate last non-max index
i = m - 1
While (idx(i) = n - m + i)
i = i - 1
If i < 0 Then
'All indexes have reached their max, so we're done
Exit Do
End If
Wend
'Increase it and populate the following indexes accordingly
idx(i) = idx(i) + 1
For j = i To m - 1
idx(j) = idx(i) + j - i
Next j
Loop
Next
' Return Result to sheet
Dim rng As Range
ReDim Preserve Result(1 To 3, 1 To iRes)
' *** Adjust returnm range to suit
Set rng = [E10].Resize(UBound(Result, 2), UBound(Result, 1))
rng = Application.Transpose(Result)
End Sub

Resources