How can I optimize vba code for combination of numbers - excel

I am working on a problem to find combinations equal to 100 with different vector length as input. The code is working fine for the small sequence but code takes a lot of time when the sequence of numbers increases. I need to reduce the time as much as I can because sometimes it takes an hour. The maximum value of vector length can be 6 & minimum increment can be 5 so the maximum we can get is 36 numbers and output of their combinations in a set of 6. Any help in the optimization of code to a minimum possible time would be great.
Here is the snap of sheet:
Here is the code:
Sub Combinations()
Dim rRng As Range, p As Integer
Dim vElements, lrow As Long, vresult As Variant
Range("A2:A100").Clear
Call Sequence
lrow = 25
Set rRng = Range("A2", Range("A2").End(xlDown)) ' The set of numbers
p = Range("C2").Value ' How many are picked
vElements = Application.Index(Application.Transpose(rRng), 1, 0)
ReDim vresult(1 To p)
Columns("E").Resize(, p + 5).Clear
Call CombinationsNP(vElements, p, vresult, lrow, 1, 1)
Call Delrow
Call formu
Range("C27:D15000").Clear
End Sub
Sub CombinationsNP(vElements As Variant, p As Integer, vresult As Variant, lrow As Long, iElement As Integer, iIndex As Integer)
Dim i As Integer
For i = iElement To UBound(vElements)
vresult(iIndex) = vElements(i)
If iIndex = p Then
lrow = lrow + 1
Range("E" & lrow + 1).Resize(, p) = vresult
Else
Call CombinationsNP(vElements, p, vresult, lrow, i + 1, iIndex + 1)
End If
Next i
End Sub
Sub Delrow()
Dim lrow As Long
Dim i As Long
Dim x As Integer
lrow = Cells(Rows.Count, 5).End(xlUp).Row
For i = 27 To lrow + 1
x = Cells(i, 5).Value + Cells(i, 6).Value + Cells(i, 7).Value + Cells(i, 8).Value + Cells(i, 9).Value + Cells(i, 10).Value
If x <> 100 And Cells(i, 5).Value <> "" Then
Cells(i, 5).EntireRow.Delete
i = i - 1
End If
Next i
End Sub
Sub Sequence()
Dim i As Integer
Dim x As Integer
Dim y As Integer
Dim a As Integer
Dim b As Integer
b = Cells(2, 3).Value
For i = 2 To Cells(2, 3).Value - 1
Cells(i, 1).Value = 0
Next i
For y = 0 To 100 Step Cells(8, 3).Value
a = 1
If y <> 0 Then
a = Int(100 / y)
If a > b Then
a = b
End If
End If
For x = 1 To a
Cells(i, 1).Value = y
i = i + 1
Next x
Next y
End Sub
Sub formu()
Dim lastrow As Long
lastrow = Cells(Rows.Count, 5).End(xlUp).Row
Range("D27:D" & lastrow).formula = "=E27&F27&G27&H27&I27&J27"
Range("C27:C" & lastrow).formula = "=IF(COUNTIF($D$27:$D$150000,D27)=1,FALSE,NOT(COUNTIF($D$2:D27,D27)=1))"
Range("$C$26:$C$150000").AutoFilter Field:=1, Criteria1:="TRUE"
Range("C27:C150000").EntireRow.Delete
Sheet5.ShowAllData
End Sub

I think this code is slow because of how often it touches the worksheet. There are both read and writes to worksheets in loops. There is also a recursive function that writes to the worksheet in a loop. I can't tell if you are doing this for ease of use or because you need to display the output. Avoid writing to the worksheet until output is required. Output all the data at once, instead of one cell at a time. See the example I give in the Sequence procedure.
I made the code have fully defined references so the system has to do less guessing and lookups. I doubt the performance change will be drastic.
Option Explicit
Public Sub Combinations()
Dim rRng As Range
Dim p As Long
Dim vElements As Variant
Dim lrow As Long
ActiveSheet.Range("A2:A100").Clear
Sequence
lrow = 25
Set rRng = ActiveSheet.Range("A2", ActiveSheet.Range("A2").End(xlDown)) ' The set of numbers
p = ActiveSheet.Range("C2").Value ' How many are picked
vElements = Application.WorksheetFunction.Index(Application.WorksheetFunction.Transpose(rRng), 1, 0)
ReDim vresult(1 To p)
ActiveSheet.Columns("E").Resize(, p + 5).Clear
CombinationsNP vElements, p, vresult, lrow, 1, 1
Delrow
formu
ActiveSheet.Range("C27:D15000").Clear
End Sub
Public Sub CombinationsNP(vElements As Variant, p As Long, vresult As Variant, lrow As Long, ByVal iElement As Long, iIndex As Long)
Dim i As Long
For i = iElement To UBound(vElements)
vresult(iIndex) = vElements(i)
If iIndex = p Then
lrow = lrow + 1
ActiveSheet.Range("E" & lrow + 1).Resize(, p) = vresult
Else
CombinationsNP vElements, p, vresult, lrow, i + 1, iIndex + 1
End If
Next i
End Sub
Public Sub Delrow()
Dim lrow As Long
Dim i As Long
Dim x As Long
With ActiveSheet
lrow = .Cells(.Rows.Count, 5).End(xlUp).Row
For i = 27 To lrow + 1
x = .Cells(i, 5).Value + .Cells(i, 6).Value + .Cells(i, 7).Value + .Cells(i, 8).Value + .Cells(i, 9).Value + .Cells(i, 10).Value
If x <> 100 And .Cells(i, 5).Value <> vbNullString Then
.Cells(i, 5).EntireRow.Delete
i = i - 1
End If
Next i
End With
End Sub
Public Sub Sequence()
Dim i As Long
Dim x As Long
Dim y As Long
Dim a As Long
Dim b As Long
' Example of setting all the cells at once
With ActiveSheet
b = .Cells(2, 3).Value
.Range(.Cells(2, 1), .Cells(b - 1, 1)).Value = 0
End With
For y = 0 To 100 Step ActiveSheet.Cells(8, 3).Value
a = 1
If y <> 0 Then
a = Int(100 / y)
If a > b Then
a = b
End If
End If
For x = 1 To a
ActiveSheet.Cells(i, 1).Value = y
i = i + 1
Next x
Next y
End Sub
Public Sub formu()
Dim lastrow As Long
With ActiveSheet
lastrow = .Cells(.Rows.Count, 5).End(xlUp).Row
.Range("D27:D" & lastrow).Formula = "=E27&F27&G27&H27&I27&J27"
.Range("C27:C" & lastrow).Formula = "=IF(COUNTIF($D$27:$D$150000,D27)=1,FALSE,NOT(COUNTIF($D$2:D27,D27)=1))"
.Range("$C$26:$C$150000").AutoFilter Field:=1, Criteria1:="TRUE"
.Range("C27:C150000").EntireRow.Delete
End With
Sheet5.ShowAllData
End Sub

Related

Select the First and Last Values in a Subset of String Values

VBA Code:
Sub Example():
Dim i As Double
Dim Letter As String
Dim var1 As Long
Dim var2 As Long
Dim Row_For_Table As Integer
Row_For_Table = 1
For i = 1 To 12
If Cells(i + 1, 1).Value <> Cells(i, 1).Value Then
'MsgBox ("different")
Letter = Cells(i, 1).Value
var2 = Cells(i, 3).Value
var1 = Cells(i, 2).Value
Range("F" & Row_For_Table).Value = Letter
Range("G" & Row_For_Table).Value = var2 - var1
Row_For_Table = Row_For_Table + 1
Else
'MsgBox ("same")
End If
Next i
End Sub
I would like to create summary table of A, B, and C with the Values of (14-1), (12-5), and (4-1). I would like to write this is VBA as a template for a bigger project.
Thank you.
This uses a dictionary to do what you are looking for. It assumes your table is sorted by Column A.
Dim i As Long
Dim lr As Long
Dim dict As Object
Set dict = CreateObject("Scripting.Dictionary")
With Sheets("Sheet1") 'Change as needed
lr = .Cells(.Rows.Count, 1).End(xlUp).Row 'Lastrow
For i = 1 To lr + 1
If Not dict.exists(.Cells(i, 1).Value) Then 'Key doesn't exist
dict.Add .Cells(i, 1).Value, .Cells(i, 2).Value 'Add key and first value
If i > 1 Then 'Avoid out of range errors
dict(.Cells(i - 1, 1).Value) = .Cells(i - 1, 3).Value - dict(.Cells(i - 1, 1).Value) 'Subtract old value from new value
End If
End If
Next i
Dim key As Variant
i = 1
For Each key In dict
.Cells(i, 6).Value = key 'place values
.Cells(i, 7).Value = dict(key)
i = i + 1
Next key
End With
This also uses a dictionary and should work for multiple columns.
Option Explicit
Sub StuffDo()
Dim rng As Range
Dim arrData As Variant
Dim ky As Variant
Dim dicLetters As Object
Dim arrNumbers()
Dim cnt As Long
Dim idxCol As Long
Dim idxRow As Long
arrData = Sheets("Sheet1").Range("A1").CurrentRegion.Value
Set dicLetters = CreateObject("Scripting.Dictionary")
For idxRow = LBound(arrData, 1) To UBound(arrData, 1)
For idxCol = LBound(arrData, 2) + 1 To UBound(arrData, 2)
ky = arrData(idxRow, 1)
If Not dicLetters.exists(ky) Then
arrNumbers = Array(arrData(idxRow, idxCol))
Else
arrNumbers = dicLetters(ky)
cnt = UBound(arrNumbers) + 1
ReDim Preserve arrNumbers(cnt)
arrNumbers(cnt) = arrData(idxRow, idxCol)
End If
dicLetters(ky) = arrNumbers
Next idxCol
Next idxRow
Set rng = Range("A1").Offset(, Range("A1").CurrentRegion.Columns.Count + 2)
For Each ky In dicLetters.keys
arrNumbers = dicLetters(ky)
rng.Value = ky
rng.Offset(, 1) = arrNumbers(UBound(arrNumbers))
rng.Offset(, 2) = arrNumbers(0)
Set rng = rng.Offset(1)
Next ky
End Sub

Place value in different rows

please assist my thought below, I would like to get the value place in different rows according to the value. I believe my code is with looping issues but I can't figure out why.
The action is very simple, row A is a set of value, if range("A2").value + 50 <= 100 then place the answer on B2.value, else place value on C2.value and etc.
Sub ttest()
Dim item, lastR, itemplus As Integer
Dim i, j As Integer
i = 2
j = 2
item = Cells(i, 1).value
lastR = Cells(Rows.Count, 1).End(xlUp).row
itemplus = item + 50
For i = 2 To lastR
If itemplus <= 100 Then
Cells(i, j).value = itemplus
Else
Cells(i, j + 1).value = itemplus
End If
item = Cells(i, 1).value
Next i
End Sub
As #SJR says, you partly work outside your loop and partly inside your loop, constantly comparing to the exact same variable.
May I suggest an alternative to work through memory instead (and avoid slower calls to your worksheet too often):
Sub Test()
Dim ws As Worksheet: Set ws = ThisWorkbook.Worksheets("Sheet1")
Dim lr As Long, x As Long
Dim arr As Variant
With ws
lr = .Cells(.Rows.Count, 1).End(xlUp).Row
arr = .Range("A2:C" & lr).Value
For x = LBound(arr) To UBound(arr)
If arr(x, 1) < 50 Then
arr(x, 2) = arr(x, 1)
Else
arr(x, 3) = arr(x, 1)
End If
Next
.Range("A2:C" & lr).Value = arr
End With
End Sub
As you may notice, we don't have to add 50 to each value to compare if its <= to a 100. That would imply that the initial value must simply be below 50.
That being said, the same is achieved swiftly through native Excel functions if you would want to avoid VBA.
Just change the order of assignment:
Sub ttest()
Dim item, lastR, itemplus As Integer
Dim i, j As Integer
i = 2
j = 2
item = Cells(i, 1).Value
lastR = Cells(Rows.Count, 1).End(xlUp).Row
For i = 2 To lastR
item = Cells(i, 1).Value
itemplus = item + 50
If itemplus <= 100 Then
Cells(i, j).Value = itemplus
Else
Cells(i, j + 1).Value = itemplus
End If
Next i
End Sub
Sub test()
Set Rng = Range("A2", Range("A2").End(xlDown))
For Each cell In Rng
Sum = cell.Value + 50
If Sum <= 100 Then cell.Offset(0, 1).Value = Sum
If Sum > 100 Then cell.Offset(0, 2).Value = Sum
Next
End Sub

How to check for equal values in cells with a for loop?

I want to check if the text value in a cell is the same as in the cell below with a for loop.
If the value in Cell(1) and Cell(2) does not match I want the value from Cell(3) written in Cell(4).
I get an error
"Overflow (Error 6)"
Dim i As Integer
For i = 1 To Rows.Count
If Cells(2 + i,21) = Cells(3 + i,21) Then
i = i + 1
Else
a = Cells(3 + i, 1)
j = j + 1
Cells(228 + j, 3) = a
End If
Next i
End Sub
I have a production output and a timeline from 6 am to 12 am and I want to create a timetable as seen below.
Screenshot:
You could use
Option Explicit
Sub test()
Dim LastRowA As Long, i As Long, j As Long, LastRowW As Long
Dim StartTime As Date, EndTime As Date, strOutPut
j = 0
With ThisWorkbook.Worksheets("Sheet1")
LastRowA = .Cells(.Rows.Count, "A").End(xlUp).Row
For i = 3 To LastRowA
If i > j - 1 Then
StartTime = .Range("A" & i).Value
strOutPut = .Range("U" & i).Value
For j = i + 1 To LastRowA + 1
If strOutPut <> .Range("U" & j).Value Then
EndTime = .Range("A" & j - 1).Value
LastRow = .Cells(.Rows.Count, "W").End(xlUp).Row
.Range("W" & LastRow + 1).Value = StartTime
.Range("X" & LastRow + 1).Value = EndTime
.Range("Y" & LastRow + 1).Value = strOutPut
Exit For
End If
Next j
End If
Next i
End With
End Sub
Result
Here I'm using a dictionary which will store every time for every product comma separated, so later will split that and take the first and last occurrence:
Sub TimeTable()
'Declare an array variable to store the data
'change MySheet for your sheet name
arr = ThisWorkbook.Sheets("MySheet").UsedRange.Value 'this will store the whole worksheet, the used area.
'Declare a dictionary object
Dim Products As Object: Set Products = CreateObject("Scripting.Dictionary")
'Loop through the array
Dim i As Long
For i = 3 To UBound(arr) 'start from row 3 because of your screenshoot
If arr(i, 21) = vbNullString Then GoTo NextRow 'if column U is empty won't add anything
If Not Products.Exists(arr(i, 21)) Then '21 is the column index for column U
Products.Add arr(i, 21), arr(i, 1)
Else
Products(arr(i, 21)) = arr(i, 21) & "," & arr(i, 1)
End If
NextRow:
Next i
Erase arr
'Redim the array to fit your final data, 4 columns and as many rows as products
ReDim arr(1 To Products.Count + 1, 1 To 4)
'Insert the headers
arr(1, 1) = "Time"
arr(1, 4) = "Product / Error"
'Now loop through the dictionary
Dim Key As Variant, MySplit As Variant
i = 2
For Each Key In Products.Keys
MySplit = Split(Products(Key), ",")
arr(i, 1) = MySplit(LBound(MySplit))
arr(i, 2) = "-"
arr(i, 3) = MySplit(UBound(MySplit))
arr(i, 4) = Key
i = i + 1
Next Key
'I don't know where are you going to paste your data, so I'm making a new worksheet at the end of your workbook
Dim ws As Worksheet
Set ws = ThisWorkbook.Sheets.Add(After:=ThisWorkbook.Sheets(ThisWorkbook.Sheets.Count))
With ws
.Range("A1").Resize(UBound(arr), UBound(arr, 2)).Value = arr
.Range("A1:C1").Merge
End With
End Sub

Excel VBA, capture first "start" value and last "end" value per group

Using Excel VBA, I'm trying to capture the first value in a column "Start" and the last value in a column "End", per group.
Data is already sorted.
Example:
I want to capture the first value for Start_open and the last value for Start_end per company.
So for Company A code should put B2 in Start_Open and put C5 in Start_end.
Capturing the last value works fine using this code:
Sub First_last()
Dim i, j As Integer
Dim LastRow, LastCol As Long
LastRow = Cells(Rows.Count, "A").End(xlUp).Row
LastCol = Cells(1, Columns.Count).End(xlToLeft).Column
For i = 2 To LastRow
If Cells(i + 1, "A").Value <> Cells(i, "A").Value Then
MsgBox i
Cells(j + 2, "E").Value = Cells(i, "C").Value
j = j + 1
End If
Next
End Sub
What I'm struggling with is capturing Start_open per group.
I think I need to use above condition and use a counter to capture Start_open per group but I can't find the right code.
Please advise, thanks!
You can use variables a and b to find the start and end of each section:
Dim a as Long, b as Long, i as Long, lr as Long
lr = cells(rows.count,1).end(xlup).row
For i = 2 to lr
If cells(i,1).value = cells(i+1,1).value then
If a = 0 then
a = i + 1
End If
Else
If a > 0 AND b = 0 then
b = i + 1
End If
End If
If b > 0 AND a > 0 Then
'perform max(range(cells(a,2),cells(b,2))), etc.
a = 0 'resets for next grouping
b = 0 'resets for next grouping
End If
Next i
a = 0
b = 0
To add another method into the mix.
Sub x()
Dim r As Range, oDic As Object, r1 As Range, r2 As Range, r3 As Range, v(), i As Long
Set oDic = CreateObject("Scripting.Dictionary")
Set r = Range("A2", Range("A" & Rows.Count).End(xlUp))
ReDim v(1 To r.Count, 1 To 3)
For Each r3 In r
If Not oDic.Exists(r3.Text) Then
Set r1 = r.Find(What:=r3, After:=r(r.Count), LookAt:=xlWhole, _
SearchDirection:=xlNext, MatchCase:=False, SearchFormat:=False)
Set r2 = r.Find(r3, r(1), , , , xlPrevious)
i = i + 1
v(i, 1) = r3
v(i, 2) = r1.Offset(, 1).Value
v(i, 3) = r2.Offset(, 2).Value
oDic.Add r3.Text, Nothing
End If
Next r3
Range("D2").Resize(oDic.Count, 3) = v
End Sub
This will do what you want:
Sub First_Last()
With ActiveSheet
Dim LastRow As Long
LastRow = .Cells(.Rows.Count, "A").End(xlUp).Row
Dim numUnique
numUnique = .Evaluate("SUM(1/COUNTIF(A:A,A2:A" & LastRow & "))")
Dim outarr As Variant
ReDim outarr(1 To numUnique, 1 To 2)
Dim clmc As Variant
clmc = .Range(.Cells(1, 3), .Cells(LastRow, 3)).Value
Dim clmb As Variant
clmb = .Range(.Cells(1, 2), .Cells(LastRow, 2)).Value
Dim j As Long
j = 1
Dim i As Long
For i = 2 To LastRow
outarr(j, 1) = clmb(i, 1)
Dim k As Long
k = .Evaluate("AGGREGATE(14,6,ROW(A2:A" & LastRow & ")/(A2:A" & LastRow & " = " & .Cells(i, 1).Address & "),1)")
outarr(j, 2) = clmc(k, 1)
j = j + 1
i = k
Next i
.Range("D2").Resize(UBound(outarr, 1), UBound(outarr, 2)).Value = outarr
End With
End Sub

Excel VBA - Finding the beginning and end of coloured rows

I am trying to create a code in Excel VBA, to locate the beginning (Cell Address) and the end (Cell Address) of coloured rows in a table. The table is a timeline(Horizontal axis- Dates, Vertical axis - General Text). The coloured rows all do not start in the first column, but start in different columns.
Any help?
How's this?
Sub findColoredRows()
Dim startCol As Integer, endCol As Integer, o As Integer
Dim ws As Worksheet
Dim i As Integer, k As Integer
Dim startRow As Long, endRow As Long
Dim cellColor As String, noColor As String
Dim cel As Range
noColor = -4142 ' this is the color index of NO coloring
k = 3
Set ws = ActiveSheet
With ws
startRow = .Cells(1, 3).End(xlDown).Row
startCol = .Cells(1, 3).Column
Do While startRow > 100 ' I assume your table starts before row 100. So, if there's no data before row 100, check next column
k = k + 1
startRow = .Cells(1, k).End(xlDown).Row
startCol = k
Loop
'Now, we have our starting row - get end row.
endRow = .Cells(startRow, k).End(xlDown).Row
endCol = .Cells(startRow, startCol).End(xlToRight).Column
Debug.Print "Start row: " & startRow & ", start column: " & startCol
' How many non colored cells is there in our range?
Dim noColorCells As Integer
For Each cel In .Range(.Cells(startRow, startCol), .Cells(endRow, endCol))
If cel.Interior.ColorIndex = noColor Then
noColorCells = noColorCells + 1
End If
Next cel
Debug.Print "There are " & noColorCells & " non colored cells."
.Cells(startRow - 1, endCol + 2).Value = "Start Date"
.Cells(startRow - 1, endCol + 3).Value = "End Date"
'reDim the array to fit the colored cells
ReDim tDates(1 To noColorCells + 1)
i = 1 'index starts at 1, so set this to 1
For k = startRow To endRow
For o = startCol To endCol
If .Cells(k, o).Interior.ColorIndex = noColor And .Cells(k, endCol + 2) = "" Then
.Cells(k, endCol + 2).Value = .Cells(k, o).Value
ElseIf .Cells(k, o).Interior.ColorIndex = noColor And .Cells(k, endCol + 2) Then
i = i + i
.Cells(k, endCol + 3).Value = .Cells(k, o).Value
End If
' i = i + 1
Next o
i = i + 1
Next k
End With
MsgBox ("Done!")
End Sub
This sub will find the addresses of any colored cells. If you can explain more what you mean by "locate the beginning and the end of coloured rows in a table." I can tweak this. Can you post an image of a sample table maybe?
Edit: Per discussion below, try this in case there's not always data in the table, but you want the columns of the colored cells:
Sub findColoredBGCells()
Dim startRow As Integer, endRow As Integer, i As Integer, k As Integer, startCol As Integer, endCol As Integer
Dim cellColor As String, noColor As String
Dim ws As Worksheet
Set ws = ActiveSheet
noColor = -4142
With ws
'Get the starting row
startRow = .Cells(1, 1).End(xlDown).Row
endRow = .Cells(startRow, 1).End(xlDown).Row
' Since we know where the names start and end (less ONE for the "Names" part), let's count how many names we have
Dim noNames As Integer
noNames = endRow - startRow
If Not IsEmpty(.Cells(1, 1)) Then ' Get the first used column with data
startCol = 1
ElseIf IsEmpty(.Cells(1, 1)) Then
startCol = .Cells(1, 1).End(xlToRight).Column
End If
endCol = .Cells(1, startCol).End(xlToRight).Column
'Now we have our range, let's use it to loop for blank cells, and add those to an array
Dim coloredCells() As Variant
ReDim coloredCells(1 To noNames, 2)
Dim rng As Range, cel As Range
Set rng = .Range(.Cells(startRow, startCol), .Cells(endRow, endCol))
'rng.Select
'Now, count how many cells are not blank background
Dim cnt As Integer, celRow As Integer, lastCelRow As Integer
i = 1
lastCelRow = 2
For Each cel In rng
cel.Select
celRow = cel.Row
If cel.Row <> lastCelRow Then 'This is so we can change the first dimension in the array
k = k + 1
coloredCells(k, 0) = .Cells(cel.Row, 1).Value
i = 1
' i = i + 1
End If
If cel.Interior.ColorIndex <> noColor Then
cnt = cnt + 1
If i > 2 Then i = 2 'Since it's only two dimensions we need, only go up to '1'
' ReDim Preserve coloredCells(noNames, i) 'resize the array to hold the new column
coloredCells(k, i) = .Cells(1, cel.Column).Value
i = i + 1
End If
lastCelRow = celRow
Next cel
For k = 1 To UBound(coloredCells)
Debug.Print coloredCells(k, 0) & " Start Date: " & coloredCells(k, 1) & ", end date: " & coloredCells(k, 2) & "."
.Cells(2 + k, 2).Value = coloredCells(k, 1)
.Cells(2 + k, 3).Value = coloredCells(k, 2)
Next k
End With
MsgBox ("Done!")
End Sub

Resources