Find nearest row match using VBA - vba

I have sample table as below
Type Format W D L Gauge
Roxy Rubbers 31 1 1 3
Roxy Rubbers 36 0 48 4
Roxy Rubbers 36 1 1 3
Here is my sub
Sub Calculate()
Dim Format, Gauge, Width, Depth, Length As String
Format = Sheets("inputs").Range("H26")
Gauge = Sheets("inputs").Range("I26")
Width = Sheets("inputs").Range("J26")
Depth = Sheets("inputs").Range("K26")
Length = Sheets("inputs").Range("L26")
Dim vArray() As Variant
vArray = Range("myRange")
Dim i As Long
For i = LBound(vArray, 1) To UBound(vArray, 1)
Debug.Print vArray(i, 1)
Next
End Sub
How do I find nearest row using VBA?
Gauge and format needs to be exact match and for W/D/L need to return nearest match
Example:
For Rubbers, 3 gauge, 30x12x12 input it should return first row as result

Assuming that by "nearest" you meant nearest volume (in the case of no exaxt matching), I suggest this function that returns the right range given those parameters. Note that I assumed that your "Gauge" column is next to the "Format" one (columns H and I as in your code) while your pictures shows that gauge comes last...
Option Explicit
Function findNearestRow(FindIn As Range, Format As String, _
Gauge As String, Width As Double, Depth As Double, Length As Double) As Range
Dim F As String, G As String, W As Double, D As Double, L As Double
Dim i As Long, best As Long, vol As Double, diff As Double, minDiff As Double
minDiff = 99999999
vol = Width * Depth * Length
For i = 1 To FindIn.Rows.Count
F = FindIn.Cells(i, 1).Value
G = FindIn.Cells(i, 2).Value
If F = Format And G = Gauge Then
W = FindIn.Cells(i, 3).Value
D = FindIn.Cells(i, 4).Value
L = FindIn.Cells(i, 5).Value
If W = Width And D = Depth And L = Length Then
Set findNearestRow = FindIn.Rows(i)
Exit Function
End If
diff = Abs(W * D * L - vol)
If diff < minDiff Then
minDiff = diff
best = i
End If
End If
Next
If minDiff < 1000 Then Set findNearestRow = FindIn.Rows(best)
' Else it returns null, nearest matching too far
End Function
Sub Test()
Dim r As Range
Set r = findNearestRow(FindIn:=Range("H2:L20"), _
Format:=Sheets("inputs").Range("H26"), _
Gauge:=Sheets("inputs").Range("I26"), _
Width:=Sheets("inputs").Range("J26"), _
Depth:=Sheets("inputs").Range("K26"), _
Length:=Sheets("inputs").Range("L26"))
If r Is Nothing Then
MsgBox "no matching found"
Else
r.Select
End If
End Sub

You could first AutoFilter() the exact matches and then loop through filtered cells for the nearest triple:
Option Explicit
Sub Calculate()
Dim Format As String
Dim Gauge As Long, Width As Long, Depth As Long, Length As Long
Dim nearestRate As Double
Dim nearestRng As Range, cell As Range
With Sheets("inputs")
Format = .Range("H26").Value2
Gauge = .Range("I26").Value2
Width = .Range("J26").Value2
Depth = .Range("K26").Value2
Length = .Range("L26").Value2
With .Range("F1", .Cells(.Rows.Count, "A").End(xlUp))
.AutoFilter field:=2, Criteria1:=Format
.AutoFilter field:=6, Criteria1:=Gauge
If Application.WorksheetFunction.Subtotal(103, .Resize(, 1)) > 1 Then
nearestRate = 100000000#
For Each cell In .Resize(.Rows.Count - 1, 1).Offset(1, 2).SpecialCells(xlCellTypeVisible)
UpdateNearest cell, Width, Depth, Length, nearestRate, nearestRng
Next
End If
End With
End With
End Sub
Function UpdateNearest(rng As Range, refVal1 As Long, refVal2 As Long, refVal3 As Long, nearestRate As Double, nearestRng As Range) As Long
Dim rate As Double
rate = Sqr((rng.Value - refVal1) ^ 2 + (rng.Offset(, 1).Value - refVal1) ^ 2 + (rng.Offset(, 2).Value - refVal2) ^ 2)
If rate < nearestRate Then
nearestRate = rate
Set nearestRng = rng
End If
End Function
the UpdateNearest() function assumes a criteria of minimum differences squares sum. but you can adapt to your actual (and unknown) needs

Related

Comparing numbers in an array

So the problem is more in depth than a simple comparison. Essentially im trying to model this dice roll known as the roll and keep system. Example would be 5k3. Where I would roll 5 dice and keep the 3 highest then add them together.
I've gotten my little macro program to roll the dice. Then I put them in an array in my example that would be an array with 5 indices. Now I need to take those 5 dice, and only keep the largest 3 of them.
The code is here A2 gives me the number of sides on the dice, B2 gives me how many I roll, and C2 gives me how many I keep. This rolls 10 dice, but then I transfer 5 of them into my actual dicepool. I know I could probably skip that, but I can deal with that later.
Private Sub CommandButton1_Click()
Dim i As Integer
Dim j As Integer
Dim k As Integer
Dim RandNum As Integer
Dim RollArray() As Integer
Dim KeptArray() As Integer
Dim RollArrayDummy() As Integer
Dim NumRoll As Integer
Dim Kept As Integer
Dim Largest As Integer
NumRoll = Range("B2").Value
ReDim RollArray(NumRoll)
Kept = Range("C2").Value
ReDim KeptArray(Kept)
For i = 5 To 15
Randomize
RandNum = 1 + Rnd() * (Range("A2").Value - 1)
Cells(i, 1).Value = RandNum
Next i
For j = 1 To NumRoll
RollArray(j) = Cells(4 + j, 1).Value
Cells(4 + j, 2).Value = RollArray(j)
Next j
k = 1
i = 1
m = 1
Largest = 1
For k = 1 To Kept
m = 1
KeptArray(k) = Largest
If m <= NumRoll Then
If Largest >= RollArray(m) And Largest >= KeptArray(k) Then
Largest = KeptArray(k)
Else
KeptArray(k) = Largest
Largest = RollArray(m)
End If
m = m + 1
End If
Cells(4 + k, 3).Value = KeptArray(k)
Next k
End Sub
I've tried so many things, like creating a dummy array, and comparing the variable Largest with it. And a ton of other things. My big problem is that I can't reuse any of the numbers.
If I roll 5 and keep 3. Say I roll [4,2,3,3,6] . I keep the [6,4,3]. Im sure this is incredibly simple and im overlooking it but its driving me absolutely insane.
Today I was watching some MonteCarlo simulations, so I have decided to do the whole question from the beginning. Thus, imagine that this is the input:
After the first roll, this is what you get:
The values in yellow are the top 3, which are kept. This is the result from the second roll:
And here is the whole code:
Public Sub RollMe()
Dim numberOfSides As Long: numberOfSides = Range("A2")
Dim timesToRoll As Long: timesToRoll = Range("B2")
Dim howManyToKeep As Long: howManyToKeep = Range("C2")
Dim cnt As Long
Dim rngCurrent As Range
Cells.Interior.Color = vbWhite
Set rngCurrent = Range(Cells(1, 6), Cells(1, 6 + timesToRoll - 1))
For cnt = 1 To timesToRoll
rngCurrent.Cells(1, cnt) = makeRandom(1, numberOfSides)
Next cnt
Dim myArr As Variant
With Application
myArr = .Transpose(.Transpose(rngCurrent))
End With
WriteTopN howManyToKeep, myArr, Cells(2, lastCol(rowToCheck:=2))
End Sub
Public Sub WriteTopN(N As Long, myArr As Variant, lastCell As Range)
Dim cnt As Long
For cnt = 1 To N
Set lastCell = lastCell.Offset(0, 1)
lastCell = WorksheetFunction.Large(myArr, cnt)
lastCell.Interior.Color = vbYellow
Next cnt
End Sub
The makeRandom and lastCol functions are some functions that I use for other projects as well:
Public Function makeRandom(down As Long, up As Long) As Long
makeRandom = CLng((up - down + 1) * Rnd + down)
If makeRandom > up Then makeRandom = up
If makeRandom < down Then makeRandom = down
End Function
Function lastCol(Optional strSheet As String, Optional rowToCheck As Long = 1) As Long
Dim shSheet As Worksheet
If strSheet = vbNullString Then
Set shSheet = ActiveSheet
Else
Set shSheet = Worksheets(strSheet)
End If
lastCol = shSheet.Cells(rowToCheck, shSheet.Columns.Count).End(xlToLeft).Column
End Function
Instead of looping through the array "manually", the WorksheetFunction.Large() nicely returns the Nth-largest value.
And if you are willing to color the "dice", which were used to take the top score, you may add this piece:
Public Sub ColorTopCells(howManyToKeep As Long, rngCurrent As Range, myArr As Variant)
Dim colorCell As Range
Dim myCell As Range
Dim cnt As Long
Dim lookForValue As Long
Dim cellFound As Boolean
For cnt = 1 To howManyToKeep
lookForValue = WorksheetFunction.Large(myArr, cnt)
cellFound = False
For Each myCell In rngCurrent
If Not cellFound And myCell = lookForValue Then
cellFound = True
myCell.Interior.Color = vbMagenta
End If
Next myCell
Next cnt
End Sub
It produces this, coloring the top cells in Magenta:
Edit: I have even wrote an article using the code above in my blog here:
vitoshacademy.com/vba-simulation-of-rolling-dices
Try this, changed a few things:
Edited the random bit too
Private Sub CommandButton1_Click()
Dim i As Long, j As Long, k As Long
Dim RandNum As Long
Dim RollArray() As Long
Dim KeptArray() As Long
Dim NumRoll As Long
Dim Kept As Long
NumRoll = Range("B2").Value
ReDim RollArray(1 To NumRoll)
Kept = Range("C2").Value
ReDim KeptArray(1 To Kept)
For i = 5 To 15
Randomize
'RandNum = 1 + Rnd() * (Range("A2").Value - 1)
RandNum = 1 + Int(Rnd() * Range("A2").Value)
Cells(i, 1).Value = RandNum
Next i
For j = 1 To NumRoll
RollArray(j) = Cells(4 + j, 1).Value
Cells(4 + j, 2).Value = RollArray(j)
Next j
For k = 1 To Kept
KeptArray(k) = Application.WorksheetFunction.Large(RollArray, k)
Cells(4 + k, 3).Value = KeptArray(k)
Next k
End Sub
Makes use of the Excel large function
Here is my attempt to fix this problem. I left the reading cell values and writing results to the OP as I am focused on the logic of the process.
There are three main functions. DiceRollSim(), RollDie() and GetNLargestIndex() as well as a function to test the code, named Test().
DiceRollSim() runs the particular simulation given the number of sides, and number of die and the number to keep. It prints the results in the output window. DollDie() fills in an array of random values simulating the rolling of the die. Caution is needed to make sure the interval probabilities are maintained as VBA does round values when converting the result of Rnd() into integers. Finally, GetNLargestIndex() is the meat of the answer, as it takes the die roll results, creates an array of index values (the 1st, 2nd, 3rd .. ) and then sorts the array based on the values of the die rolls.
Option Explicit
Public Sub Test()
DiceRollSim 6, 15, 3
' Example, 15k3:
' Rolling 15 die.
' x(1) = 5 *
' x(2) = 4
' x(3) = 4
' x(4) = 2
' x(5) = 4
' x(6) = 5 **
' x(7) = 6 ***
' x(8) = 1
' x(9) = 4
' x(10) = 3
' x(11) = 1
' x(12) = 3
' x(13) = 5
' x(14) = 3
' x(15) = 3
' Sorting die values.
' x(7) = 6
' x(6) = 5
' x(1) = 5
' Sum of 3 largest=16
End Sub
Public Sub DiceRollSim(ByVal n_sides As Long, ByVal n_dice As Long, ByVal n_keep As Long)
Dim die() As Long, i As Long
ReDim die(1 To n_dice)
Debug.Print "Rolling " & n_dice & " die."
Call RollDie(n_sides, n_dice, die)
For i = 1 To n_dice
Debug.Print "x(" & i & ")=" & die(i)
Next i
Dim largest() As Long
Debug.Print "Sorting die values."
Call GetNLargestIndex(die, n_keep, largest)
Dim x_sum As Long
x_sum = 0
For i = 1 To n_keep
x_sum = x_sum + die(largest(i))
Debug.Print "x(" & largest(i) & ")=" & die(largest(i))
Next i
Debug.Print "Sum of " & n_keep & " largest=" & x_sum
End Sub
Public Sub RollDie(ByVal n_sides As Long, ByVal n_dice As Long, ByRef result() As Long)
ReDim result(1 To n_dice)
Dim i As Long
For i = 1 To n_dice
' Rnd() resurns a number [0..1)
' So `Rnd()*n_sides` returns a floating point number zero or greater, but less then n_sides.
' The integer conversion `CLng(x)` rounds the number `x`, and thus will not keep equal
' probabilities for each side of the die.
' Use `CLng(Floor(x))` to return an integer between 0 and n_sides-1
result(i) = 1 + CLng(WorksheetFunction.Floor_Math(Rnd() * n_sides))
Next i
End Sub
Public Sub GetNLargestIndex(ByRef die() As Long, ByVal n_keep As Long, ByRef index() As Long)
Dim n_dice As Long, i As Long, j As Long, t As Long
n_dice = UBound(die, 1)
' Instead of sorting the die roll results `die`, we sort
' an array of index values, starting from 1..n
ReDim index(1 To n_dice)
For i = 1 To n_dice
index(i) = i
Next i
' Bubble sort the results and keep the top 'n' values
For i = 1 To n_dice - 1
For j = i + 1 To n_dice
' If a later value is larger than the current then
' swap positions to place the largest values early in the list
If die(index(j)) > die(index(i)) Then
'Swap index(i) and index(j)
t = index(i)
index(i) = index(j)
index(j) = t
End If
Next j
Next i
'Trim sorted index list to n_keep
ReDim Preserve index(1 To n_keep)
End Sub

Insert and highlight inserted rows

I have a code that works well for inserting multiple rows by specifying values for 2 columns. I am not sure how to add in the code to highlight the new inserted rows in some color. Here is my code -
Sub Multiplerows()
Dim rng As Integer, k As Integer
Dim attrib As String
Dim BMI As String
Dim rRange As Range
Dim salesID As Long, salesMkt As String
Set rRange = Selection
On Error Resume Next
rng = InputBox("Enter number:.")
item = InputBox("Enter name of the Item:.")
subject = InputBox("Enter name of the sub item:.")
'If rng = 0 Then Exit Sub
For k = 1 To rng
Rows(rRange.Row).Insert Shift:=xlDown
Next k
For k = rng To 1 Step -1
Cells(rRange.Row - k, 10) = item
Cells(rRange.Row - k, 8) = subject
Next k
End Sub
Depends on what exactly you want to do (e.g. highlight based on row values), but in your lower loop you could do something like
For k = rng To 1 Step -1
Cells(rRange.Row - k, 10) = item
Cells(rRange.Row - k, 8) = subject
ActiveSheet.Rows(rRange.Row - k).Interior.Color = RGB(255, 0, 0)
Next k
This would highlight the new row as bright red. Change the RGB values to whatever you like.

Calculating a Sum

What I am trying to do is develop a model that takes a cell that is greater than 1 then to take the sum of the area to the first row using a cone shape, so for example cell D4, sum the area C3:C5 + B2:B6 + A1:A7.
At the moment I have this but it obviously is not working.
Dim I As Double
Dim J As Double
Dim Size As Integer
Dim x As Integer
Dim y As Integer
Dim z As Integer
'Dim Range As Integer
Dim PV1 As Integer
'MCArray = Worksheets("Data")
I = WorksheetFunction.CountA(Worksheets("Data").Rows(1))
J = WorksheetFunction.CountA(Worksheets("Data").Columns(1))
'Loop to Move down the rows
For x = 1 To J
'Loop to move acoss the columns
For y = 1 To I
'IfElse to determine if cell value is greater or equal to zero
If Cells(J, I).Value >= 0 Then
'Loop to sum the cells above
For z = 1 To J
PV1 = (ActiveCell.Value) + Worksheet.Sum(Range([J - z], [I-z:I+z]))
'IfElse to determine if final sum is greater than zero
If PV1 > 0 Then
Worksheets("MC").Range("B4").Value = PV1
Range([J - z], [I-z:I+z]).Interior.ColourIndex = 1
End If
Next z
End If
Next y
Next x
Here is a function you can use either as a UDF or from another routine. Just pass it the single cell you want to start from (D4 in your example) and this function will calculate the sum of the cone as you described.
Public Function SUMCONE(r As Range) As Double
Application.Volatile
SUMCONE = Application.Sum(r, r(-0, -0).Resize(, 3), r(-1, -1).Resize(, 5), r(-2, -2).Resize(, 7))
End Function
Here is an example of how to use the above function from your VBA routine:
Public Sub Demo()
Dim j&
For j = 5 To 10
If Cells(5, j) > 0 Then
Debug.Print SUMCONE(Cells(5, j))
End If
Next
End Sub
UPDATE
Based on your feedback I have updated the function and the demo routine to form an upward cone summation from the initial cell.
UPDATE #2
The above is for a fixed-size cone, extending upwards, that can be initiated from any cell in the worksheet.
But if you would prefer for the cone to always extend all the way up to row 1 regardless of which cell it originates in, then the following is what you are after:
Public Sub Demo()
Dim i&, j&
For j = 1 To Application.CountA(Worksheets("Data").Rows(1))
For i = 1 To Application.CountA(Worksheets("Data").Columns(1))
If Cells(i, j) > 0 Then
Debug.Print Cells(i, j).Address, SumAndColorCone(Cells(i, j))
End If
Next
Next
End Sub
Public Function SumAndColorCone(r As Range) As Double
Dim i&, k&, c As Range
Set c = r
For i = r.Row - 1 To 1 Step -1
Set c = Union(c, r(-k, -k).Resize(, (k + 1) * 2 + 1))
k = k + 1
Next
c.Interior.Color = vbRed
SumAndColorCone = Application.Sum(c)
End Function
UPDATE #3
As I suspected there was a problem if the cone was initiated too close to the left edge of the worksheet. I've added code to handle that now. Also your method for accessing the large matrix (which I had used in the Demo routine) did not work properly. I fixed that as well:
Public Sub Demo()
Dim i&, j&
For j = 1 To Cells(1, Columns.Count).End(xlToLeft).Column
For i = 1 To Cells(Rows.Count, "A").End(xlUp).Row
If Val(Cells(i, j)) > 0 Then
Debug.Print Cells(i, j).Address, SumAndColorCone(Cells(i, j))
End If
Next
Next
End Sub
Public Function SumAndColorCone(r As Range) As Double
Dim i&, k&, c As Range
Set c = r
For i = r.Row - 1 To 1 Step -1
If r.Column - k < 2 Then Exit For
Set c = Union(c, r(-k, -k).Resize(, (k + 1) * 2 + 1))
k = k + 1
Next
c.Interior.Color = vbRed
SumAndColorCone = Application.Sum(c)
End Function

Excel VBA: Faster way of finding the column letter of a header?

I have this code for finding the column letter of a given header:
Public Function GetColumnLetter(ByRef in_cells As Range, ByVal column_header As String, Optional look_at As Excel.XlLookAt = xlPart) As String
GetColumnLetter = Split(in_cells.Find(what:=column_header, LookAt:=look_at, SearchOrder:=xlByRows).Address(ColumnAbsolute:=False), "$")(0)
End Function
But it seems to be a little slow. It seems to take a few seconds on some worksheets when it seems like it shouldn't take more than a second. I have to do this for a number of different columns in a number of different worksheets.
Is there a faster way of doing this?
EDIT: I just made a helper function that calls this function, but w/just the first row (in_cells.Range("1:1)), and also changed xlByRows to xlByColumns, and this has sped things enough.
For what it's worth, here is an extremely fast function that does what you want without invoking Find. According to my (very old) notes, it came from here. The parameter c represents the index of the column in question, as in the property Selection.Column.
Public Function GetColumnLetter(ByVal c As Long) As String
Dim p As Long
While c
p = 1 + (c - 1) Mod 26
c = (c - p) \ 26
GetColumnLetter = Chr$(64 + p) & GetColumnLetter
Wend
End Function
EDIT: Given clarifications in comments, here is a setup for testing .Find vs .Match, which appears to be faster. Using the variant values of the array might be faster still, but I'll leave it here.
Set up a first row of data:
Public Sub MakeUglyFirstRow()
Dim rng As Excel.Range
Dim i As Long, p As Long
Dim strChar As String
Dim initialLength As Integer
Set rng = ActiveSheet.Rows(1)
initialLength = 5
For i = 1 To rng.Cells.Count
p = 1 + (i - 1) Mod 26
strChar = String(initialLength, Chr$(64 + p))
rng.Cells(i).Value = strChar
If i Mod 26 = 0 Then initialLength = initialLength + 1
Next i
End Sub
The original function (plus error handling for a value not found), and a match version calling the function above:
Public Function GetColumnLetter_ByFind(ByRef in_cells As Range, ByVal column_header As String, Optional look_at As Excel.XlLookAt = xlPart) As String
Dim rngFound As Excel.Range
Set rngFound = in_cells.Find(what:=column_header, LookAt:=look_at, SearchOrder:=xlByRows)
If Not (rngFound Is Nothing) Then
GetColumnLetter_ByFind = Split(rngFound.Address(ColumnAbsolute:=False), "$")(0)
End If
End Function
Public Function GetColumnLetter_ByMatch(in_cells As Range, text_to_find As String, Optional look_at As Excel.XlLookAt = XlLookAt.xlPart) As String
On Error Resume Next
Dim rngFirstRow As Excel.Range
Dim result As Variant
Dim col As Long
Dim r As Long
Set rngFirstRow = in_cells.Rows(1)
col = 0
With Application.WorksheetFunction
If look_at = xlPart Then
result = .Match("*" + text_to_find + "*", rngFirstRow, 0)
Else
result = .Match(text_to_find, rngFirstRow, 0)
End If
If .IsError(result) = False Then
col = CLng(result) 'will need an offset if the range's first column is not 1
End If
End With
If col > 0 Then
GetColumnLetter_ByMatch = GetColumnLetter(col)
End If
End Function
The (very crude) test method (some parameter explanation below):
Public Sub Test_ColumnFinding(Optional testString As String = "yyy", _
Optional numberOfTests As Long = 1000, _
Optional printResults As Boolean = True, _
Optional printEvery As Integer = 10)
Dim rng As Excel.Range
Dim timStart1 As Single, timEnd1 As Single, timTotal1 As Single
Dim timStart2 As Single, timEnd2 As Single, timTotal2 As Single
Dim strTest1 As String, strTest2 As String
Dim i As Long
Set rng = ActiveSheet.Rows(1)
For i = 1 To numberOfTests
timStart1 = Timer
strTest1 = GetColumnLetter_ByFind(rng, testString, IIf(i Mod 3 = 0, XlLookAt.xlPart, XlLookAt.xlWhole))
timEnd1 = Timer
timTotal1 = timTotal1 + (timEnd1 - timStart1)
timStart2 = Timer
strTest2 = GetColumnLetter_ByMatch(rng, testString, IIf(i Mod 3 = 0, XlLookAt.xlPart, XlLookAt.xlWhole))
timEnd2 = Timer
timTotal2 = timTotal2 + (timEnd2 - timStart2)
If printResults Then
If i Mod printEvery = 0 Then
Debug.Print i, "GetColumnLetter_ByFind", strTest1, timEnd1 - timStart1
Debug.Print i, "GetColumnLetter_ByMatch", strTest2, timEnd2 - timStart2
End If
End If
Next i
Debug.Print "GetColumnLetter_ByFind took " & timTotal1 / numberOfTests & " seconds on avg to execute"
Debug.Print "GetColumnLetter_ByMatch took " & timTotal2 / numberOfTests & " seconds on avg to execute"
End Sub
Where testString controls distance to match, numberOfTests the repetition, printResults whether or not to see debug output, and printEvery how often to check in on that output.
My results, for 1000 tests but no result debug output:
GetColumnLetter_ByFind took 0.003546875 seconds on avg to execute
GetColumnLetter_ByMatch took 0.00134375 seconds on avg to execute

Excel vba Create combinations in same row each one

I need help with a macro that exports all combinations of a range in same row each one ( I mean horizontal exports).
Every combination I want to be in one cell each time.
I want to change any time the number of strings in the range and also the number of strings combinations (In the example below 4 strings in the range and 3 for combinations)
1. A B C D -------------ABC --ABD--ACD--BCD
2. E F G H--------------EFG---EFH--EGH--FGH
3. I G K L----------------IGK----IGL---IKL---GKL
Below its a module that I found in web that is very close to what I need.
I am very new to Vba macros and I cannot achieve what I am looking for with the below code
Private NextRow As Long
Sub Test()
Dim V() As Variant, SetSize As Integer, i As Integer
SetSize = Cells(2, Columns.count).End(xlToLeft).Column
ReDim V(1 To SetSize)
For i = 1 To SetSize
V(i) = Cells(2, i).Value
Next i
NextRow = 4
CreateCombinations V, 3, 3
End Sub
Sub CreateCombinations( _
OriginalSet() As Variant, _
MinSubset As Integer, MaxSubset As Integer)
Dim SubSet() As Variant, SubSetIndex As Long
Dim SubSetCount As Integer, Bit As Integer
Dim k As Integer, hBit As Integer
Dim MaxIndex As Long
hBit = UBound(OriginalSet) - 1
ReDim SubSet(1 To UBound(OriginalSet))
MaxIndex = 2 ^ UBound(OriginalSet) - 1
For SubSetIndex = 1 To MaxIndex
SubSetCount = BitCount(SubSetIndex)
If SubSetCount >= MinSubset And SubSetCount <= MaxSubset Then
k = 1
For Bit = 0 To hBit
If 2 ^ Bit And SubSetIndex Then
SubSet(k) = OriginalSet(Bit + 1)
k = k + 1
End If
Next Bit
DoSomethingWith SubSet, SubSetCount
End If
Next SubSetIndex
End Sub
Sub DoSomethingWith(SubSet() As Variant, ItemCount As Integer)
Dim i As Integer
For i = 1 To ItemCount
Cells(NextRow, i) = SubSet(i)
Next i
NextRow = NextRow + 1
End Sub
Function BitCount(ByVal Pattern As Long) As Integer
BitCount = 0
While Pattern
If Pattern And 1 Then BitCount = BitCount + 1
Pattern = Int(Pattern / 2)
Wend
End Function
Here is a way to do it:
In your excel sheet, add an array formula like this:
A B C D E
1
2 A B C D {=k_combinations(CONCATENATE(A2;B2;C2;D2);3)}
3 E F G H {=k_combinations(CONCATENATE(A3;B3;C3;D3);3)}
Note that you should extend the array formula to columns F, G, H and so on so that you get all results. (The { and } are not to be inserted manually, they are the mark of the array formula) :
Select cells E2, F2, G2, H2, and so on to Z2
Type the formula
To validate input, press Ctrl+Shift+Enter
Put the following code into a code module.
Public Function k_combinations(ByVal chLetters As String, ByVal k As Long) As Variant
Dim chCombinations() As String
Dim uCount As Long
Dim vReturn() As Variant
Dim i As Long
uCount = Get_k_combinations(chLetters, chCombinations, k)
ReDim vReturn(0 To uCount - 1) As Variant
For i = 0 To uCount - 1
vReturn(i) = chCombinations(i)
Next i
k_combinations = vReturn
End Function
Private Function Get_k_combinations(chLetters As String, chCombinations() As String, ByVal k As Long) As Long
Dim i As Long
Dim M As Long
M = Len(chLetters)
If k > 1 Then
Get_k_combinations = 0
For i = 1 To M - (k - 1)
Dim chLetter As String
Dim uNewCombinations As Long
Dim chSubCombinations() As String
Dim j As Long
chLetter = Mid$(chLetters, i, 1)
uNewCombinations = Get_k_combinations(Right$(chLetters, M - i), chSubCombinations, k - 1)
ReDim Preserve chCombinations(0 To Get_k_combinations + uNewCombinations) As String
For j = 0 To uNewCombinations - 1
chCombinations(Get_k_combinations + j) = chLetter & chSubCombinations(j)
Next j
Get_k_combinations = Get_k_combinations + uNewCombinations
Next i
Else
ReDim chCombinations(0 To M - 1) As String
For i = 1 To M
chCombinations(i - 1) = Mid$(chLetters, i, 1)
Next i
Get_k_combinations = M
End If
End Function
Get_k_combinations is called recursively. The performance of this method is quite poor (because it uses string arrays and makes a lot of reallocations). If you consider bigger data sets, you will have to optimize it.