Find all possible combinations of a list in vba - excel

I am trying to organise a list of data "a,b,c,d,e,...." into all possible combinations of 1,2,3....n elements.
For example:
a,b,c,d,e
a
b
c
d
e
a,b
a,c
a,d
a,e
b,c
b,d
d,e
...
and so on.
So far I have only come across people who have written code to find the combinations to two sets of data rather than one.
Would you know where to start?
In my head it would be similar to the following so it would run systematically, and stops any repeats of the permutations. So essentially I would be running a loop inside another loop 4 or 5 different times.
i
i+1
i+...n
i,j+1
.
.
.
i,j,k,l....

I asked a similar question about 10 years ago and got a great answer from John Coleman:
Gray Code
Here is his solution:
'If you run TestThis, then for example the second message box returns
'
'{}
'dog
'dog , cat
'cat
'cat , mouse
'dog , cat, mouse
'dog , mouse
'mouse
'mouse , zebra
'dog , mouse, zebra
'dog , cat, mouse, zebra
'cat , mouse, zebra
'cat , zebra
'dog , cat, zebra
'dog , zebra
'zebra
'
'Hope this helps,
'
'John Coleman
'p.s. The algorithm used to generate the Gray code comes from the
'excellent book "Combinatorial Algorithms: Generation, Enumeration and
'Search " by Kreher and Stinson."
and the code:
Sub TestThis()
Dim i As Integer
Dim A(3 To 7) As Integer
Dim B As Variant
For i = 3 To 7
A(i) = i
Next i
B = Array("dog", "cat", "mouse", "zebra")
MsgBox ListSubsets(A)
MsgBox ListSubsets(B)
End Sub
Function ListSubsets(Items As Variant) As String
Dim CodeVector() As Integer
Dim i As Integer
Dim lower As Integer, upper As Integer
Dim SubList As String
Dim NewSub As String
Dim done As Boolean
Dim OddStep As Boolean
OddStep = True
lower = LBound(Items)
upper = UBound(Items)
ReDim CodeVector(lower To upper) 'it starts all 0
Do Until done
'Add a new subset according to current contents
'of CodeVector
NewSub = ""
For i = lower To upper
If CodeVector(i) = 1 Then
If NewSub = "" Then
NewSub = Items(i)
Else
NewSub = NewSub & ", " & Items(i)
End If
End If
Next i
If NewSub = "" Then NewSub = "{}" 'empty set
SubList = SubList & vbCrLf & NewSub
'now update code vector
If OddStep Then
'just flip first bit
CodeVector(lower) = 1 - CodeVector(lower)
Else
'first locate first 1
i = lower
Do While CodeVector(i) <> 1
i = i + 1
Loop
'done if i = upper:
If i = upper Then
done = True
Else
'if not done then flip the *next* bit:
i = i + 1
CodeVector(i) = 1 - CodeVector(i)
End If
End If
OddStep = Not OddStep 'toggles between even and odd steps
Loop
ListSubsets = SubList
End Function

I know this question is old, but I wrote the code before finding John Coleman's option.
In order to find different rank combinations (i.e. sets of 2,3,4 items) I put the list of items in ColumnA of a clean sheet, with a header, and call it with something like:
Sub call_listcombos()
Dim sht as Worksheet, outrn As Range
Dim n As Integer, r As Integer, rto As Integer
Dim poslist()
Application.ScreenUpdating = False
Set sht = ActiveSheet
n = sht.Range("A1").CurrentRegion.Rows.Count - 1
poslist() = Application.Transpose(sht.Range("A2").Resize(n).Value2)
rto = 2
Do While Application.Combin(n, rto + 1) < 250000
DoEvents
rto = rto + 1
Loop
For r = 2 To rto
Set outrn = sht.Range("A1").Offset(sht.Range("A1").CurrentRegion.Rows.Count)
Call list_combos(poslist(), r, outrn)
Next r
Application.ScreenUpdating = True
End Sub
The code:
Private Sub list_combos(items() As Variant, r As Integer, outrange As Range)
'receives a 1-D variant array and outputs a single column with nCr combinations
'selecting r items without replacement... n > r > 1 :: integers
Dim n As Integer, i As Integer, ri As Integer, outi As Long
Dim comboindex(), comboitems()
n = UBound(items) - LBound(items) + 1
outi = Application.Combin(n, r)
'test output range
If outrange.Row + outi > 1000000 Then
MsgBox "Too many combinations! Will not fit in output range."
Exit Sub
End If
If Application.CountA(outrange.Resize(outi)) > 0 Then
MsgBox "Output range is not empty!"
Exit Sub
End If
'initialize combinations
ReDim comboindex(1 To r)
ReDim comboitems(1 To r)
For ri = 1 To r
comboindex(ri) = LBound(items) + ri - 1 'sets comboindex's base to items' base
comboitems(ri) = items(comboindex(ri))
Next ri
'loop combinations
ri = r
outi = 0
Do While comboindex(ri) <= UBound(items)
DoEvents
For i = comboindex(ri) To UBound(items)
comboindex(ri) = i
comboitems(ri) = items(comboindex(ri))
outrange.Offset(outi).Value2 = Join(comboitems, ";")
outi = outi + 1
Next i
ri = ri - 1
Do While comboindex(ri) + 1 = comboindex(ri + 1)
DoEvents
If ri = 1 Then Exit Do
ri = ri - 1
Loop
comboindex(ri) = comboindex(ri) + 1
comboitems(ri) = items(comboindex(ri))
Do While ri < r
DoEvents
ri = ri + 1
comboindex(ri) = comboindex(ri - 1) + 1
If comboindex(ri) > UBound(items) Then Exit Do
comboitems(ri) = items(comboindex(ri))
Loop
Loop
End Sub

Related

Highlighting Specific Cells in a Selected Range

The code provided below is intended to highlight cells where a negative word is within 5 words of a key word. For the sake of my benefactors privacy I will not provide the keywords I search for however you can see the function of the code. You could put any word as a key word for testing including but not limited to "Micheal", "building", "damage", etc. The code executes it's desired function to a degree however instead of highlighting the individual cells that met the requirement, it highlights the entire selection based off of one cell meeting the requirements. Is there an easy fix or work around for this problem or should I change the for loop to an integer type for loop?
Sub IdentifyCameraPresence()
Application.ScreenUpdating = False
Dim Rng As Range
Dim x As Double, y As Double, i As Double
Dim Negatives As Variant, Keys As Variant, n As Double, k As Double 'keeps track of the location of negatives and key words in a cell
Dim NWords As Variant, KWords As Variant, m As Double, j As Double 'keeps track of the words that are negative and key
Dim Temp As Variant
Set Negatives = CreateObject("System.Collections.ArrayList")
Set Keys = CreateObject("System.Collections.ArrayList")
Set NWords = CreateObject("System.Collections.ArrayList")
NWords.Add "no"
NWords.Add "not"
Debug.Print NWords(0); NWords.Count
Set KWords = CreateObject("System.Collections.ArrayList")
KWords.Add "key1"
KWords.Add "key2"
KWords.Add "key3"
KWords.Add "key4"
KWords.Add "key5"
Debug.Print KWords(3)
Debug.Print "NWords"; NWords.Count; "KWords"; KWords.Count
For Each Rng In Selection
With Application.WorksheetFunction
Temp = Split(Rng)
For i = 0 To UBound(Temp)
Debug.Print "word"; i; ":"; Temp(i)
Next i
For i = 0 To UBound(Temp)
For m = 0 To NWords.Count - 1
If LCase(Temp(i)) = LCase(NWords(m)) Then Negatives.Add i
Next m
For j = 0 To KWords.Count - 1
If InStr(1, LCase(Temp(i)), LCase(KWords(j))) Then Keys.Add i
Next j
Next i
For i = 0 To Negatives.Count - 1
Debug.Print "Negative word"; i; ":"; Negatives(i)
Next i
For i = 0 To Keys.Count - 1
Debug.Print "Key word"; i; ":"; Keys(i)
Next i
'----------------------------PROBLEM IS HERE------------------------------------------------------------
For k = 0 To Keys.Count - 1
For n = 0 To Negatives.Count - 1
Debug.Print "Key"; Keys(k); "negative"; Negatives(n)
Debug.Print "In Color Index"
If Abs(Negatives(n) - Keys(k)) < 5 Then Rng.Interior.ColorIndex = 35
Next n
Next k
'----------------------------PROBLEM IS HERE------------------------------------------------------------
End With
Next Rng
Application.ScreenUpdating = True
End Sub

Highlighting Specific Cells in a Selected Range Issue

The code provided below is intended to highlight cells where a negative word is within 5 words of a key word. For the sake of my benefactors privacy I will not provide the keywords I search for however, you can see the function of the code. You could put any word as a key word for testing including but not limited to "Micheal", "building", "damage", etc.
The code executes it's desired function to a degree however instead of highlighting the individual cells that met the requirement, it highlights the entire selection of cells below the first cell that meets the requirements. I feel the issue lies somewhere near the interior color index.
If anyone has any ideas to solve this issue than it'd be greatly appreciated.
Sub IdentifyCameraPresence()
Application.ScreenUpdating = False
Dim Rng As Range
Dim x As Double, y As Double, i As Double, p As Double
Dim Negatives As Variant, Keys As Variant, n As Double, k As Double 'keeps track of the location of negatives and key words in a cell
Dim NWords As Variant, KWords As Variant, m As Double, j As Double 'keeps track of the words that are negative and key
Dim Temp As Variant
Set Negatives = CreateObject("System.Collections.ArrayList")
Set Keys = CreateObject("System.Collections.ArrayList")
Set Rng = Selection
Set NWords = CreateObject("System.Collections.ArrayList")
NWords.Add "no"
NWords.Add "not"
Debug.Print NWords(0); NWords.Count
Set KWords = CreateObject("System.Collections.ArrayList")
KWords.Add "key1"
KWords.Add "key2"
KWords.Add "key3"
KWords.Add "key4"
KWords.Add "key5"
Debug.Print KWords(3)
Debug.Print "NWords"; NWords.Count; "KWords"; KWords.Count
For p = 1 To Rng.Rows.Count
' Debug.Print Rng.Cells(p, 1)
With Application.WorksheetFunction
Temp = Split(Rng(p, 1))
' For i = 0 To UBound(Temp)
' Debug.Print "word"; i; ":"; Temp(i)
' Next i
For i = 0 To UBound(Temp)
For m = 0 To NWords.Count - 1
If LCase(Temp(i)) = LCase(NWords(m)) Then Negatives.Add i
Next m
For j = 0 To KWords.Count - 1
If InStr(1, LCase(Temp(i)), LCase(KWords(j))) Then Keys.Add i
Next j
Next i
' For i = 0 To Negatives.Count - 1
' Debug.Print "Negative word"; i; ":"; Negatives(i)
' Next i
'
' For i = 0 To Keys.Count - 1
' Debug.Print "Key word"; i; ":"; Keys(i)
' Next i
'
''----------------------------PROBLEM IS HERE------------------------------------------------------------
For k = 0 To Keys.Count - 1
For n = 0 To Negatives.Count - 1
Debug.Print "Key"; Keys(k); "negative"; Negatives(n)
Debug.Print "In Color Index"
If Abs(Negatives(n) - Keys(k)) < 5 Then Rng.Cells(p, 1).Interior.ColorIndex = 35
Next n
Next k
''----------------------------PROBLEM IS HERE------------------------------------------------------------
'
End With
Next p
Application.ScreenUpdating = True
End Sub

Compare strings in a cell no matter the sequence

I am looking some help with a code to compare 2 strings and rank them in accordance of their matching with the original criteria. The code should ignore the sequence, For example, A1 contains words "Jon Smith" (original value), and B1 "Smith Jon", which are the same ranking. But if C1 contains "Jon Smith Junior", this should have a lower rank than "Jon Smith" or "Smith Jon".
Any one can help?
StackOverflow is not a coding service, and you should provide your code, but in this case I was interested in the task. Here is a possible solution.
Run the checkme - it simply takes the two strings and splits them into arrays. Then it counts how many times the values in arrOne present in arrTwo. With this information, it gives some kind of a result.
Option Explicit
Public Function CompareTwo(strOne As String, strTwo As String) As Double
Dim arrOne As Variant
Dim arrTwo As Variant
Dim varOne As Variant
Dim varTwo As Variant
Dim lngCounter As Long
arrOne = Split(strOne)
arrTwo = Split(strTwo)
For Each varOne In arrOne
For Each varTwo In arrTwo
If varOne = varTwo Then
lngCounter = lngCounter + 1
End If
Next varTwo
Next varOne
CompareTwo = lngCounter / (UBound(arrOne) + 1)
End Function
Public Sub CheckMe()
Debug.Print CompareTwo("Smith Jon", "Jon Smith")
Debug.Print CompareTwo("Jon Smith Junior", "Jon Smith")
Debug.Print CompareTwo("Jon Smith Junior Ale 6", "Jon Smith Ale 6")
End Sub
I came up with this one. It creates two arrays, one where the two key names are found within a given cell in Column B, and another with how many words are in each array element of arr1. Then it sends the two arrays to a Sort2 Sub, which was written by member Gary's Student and can be found here. It presumes the multiple choice names are in column "B" and that "Jon" and "Smith" are hardcoded - but can be drawn from another column with a little change to the code.
Column B contains:
Jon Smith
Smith Jon Junior
Smith Jon
Sub create2arr()
Dim myArr() As Variant, name1 As String, name2 As String, firstMarker As Boolean, myArrayCounter As Long, myArray2Counter As Long
Dim splitArr() As String, wordCountArr() As Variant
name1 = "Jon"
name2 = "Smith"
ReDim myArr(1 To 1)
ReDim myArr2(1 To 1)
ReDim wordCountArr(1 To 1)
myArrayCounter = 1
myArray2Counter = 1
For I = 1 To 3
splitArr = Split(Sheet6.Range("B" & I))
For J = LBound(splitArr) To UBound(splitArr)
If UCase(splitArr(J)) = UCase(name1) Or UCase(splitArr(J)) = UCase(name2) Then
If firstMarker = True Then
myArr(myArrayCounter) = Sheet6.Range("B" & I)
wordCountArr(myArrayCounter) = UBound(splitArr) + 1
myArrayCounter = myArrayCounter + 1
ReDim Preserve myArr(1 To myArrayCounter)
ReDim Preserve wordCountArr(1 To myArrayCounter)
firstMarker = False
Else
firstMarker = True
End If
End If
Next J
Next I
For I = 1 To UBound(myArr)
Debug.Print myArr(I)
Next I
Call sort2(wordCountArr, myArr)
For I = 1 To UBound(myArr)
Debug.Print myArr(I)
Next I
End Sub
Sub sort2(key() As Variant, other() As Variant)
Dim I As Long, J As Long, Low As Long
Dim Hi As Long, Temp As Variant
Low = LBound(key)
Hi = UBound(key)
J = (Hi - Low + 1) \ 2
Do While J > 0
For I = Low To Hi - J
If key(I) > key(I + J) Then
Temp = key(I)
key(I) = key(I + J)
key(I + J) = Temp
Temp = other(I)
other(I) = other(I + J)
other(I + J) = Temp
End If
Next I
For I = Hi - J To Low Step -1
If key(I) > key(I + J) Then
Temp = key(I)
key(I) = key(I + J)
key(I + J) = Temp
Temp = other(I)
other(I) = other(I + J)
other(I + J) = Temp
End If
Next I
J = J \ 2
Loop
End Sub

Matching two titles by words and calculate %

I was trying to automate an Excel file which has title in both A and B columns and I have to search each word from A within B and calculate the % by using the "no of words matched/total no of words (in column A)" formula.
I'm using the below code, however its not giving me the accurate % for which the title has repeated words (Duplicate words).
Sub percentage()
Dim a() As String, b() As String
Dim aRng As Range, cel As Range
Dim i As Integer, t As Integer
Set aRng = Range(Range("A1"), Range("A5").End(xlDown))
For Each cel In aRng
a = Split(Trim(cel), " ")
b = Split(Trim(cel.Offset(, 1)), " ")
d = 0
c = UBound(a) + 1
If cel.Value <> "" Then
If InStr(cel, cel.Offset(, 1)) Then
d = UBound(b) + 1
Else
For i = LBound(a) To UBound(a)
For t = LBound(b) To UBound(b)
If UCase(a(i)) = UCase(b(t)) Then
d = d + 1
End If
Next
Next
End If
End If
cel.Offset(0, 2).Value = (d / c)
Next
End Sub
If Title 1 : Really Nice pack with Nice print and Title 2 : Nice Print Nice pack then result should be 3/6 i.e. 67%.
But I'm getting a result as 100%.
Can anyone help me out please.
Titles are
Great job dud
Really Nice pack with Nice print
To give success and success process
Don’t eat too much. If you eat too much you will get sick
I have tried =noDuplicate(celladdress)
First, you should delete duplicate word in column B.
My function delete word and return array of word that not duplicate.
Function noDuplicate(ByVal str As String) As String()
Dim splitStr() As String
Dim result() As String
Dim i As Integer
Dim j As Integer
Dim k As Integer
Dim addFlag As Boolean
splitStr = Split(UCase(str), " ")
ReDim result(UBound(splitStr))
'
result(0) = splitStr(0)
k = 0
For i = 1 To UBound(splitStr)
addFlag = True
For j = 0 To k
If splitStr(i) = result(j) Then
addFlag = False
Exit For
End If
Next j
If addFlag Then
result(k + 1) = splitStr(i)
k = k + 1
End If
Next i
ReDim Preserve result(k)
noDuplicate = result
End Function
Then calculate the percentage of number of match word and number of word in column A.
Function percentMatch(ByVal colA As String, ByVal colB As String) As Double
Dim splitColA() As String
Dim splitColB() As String
Dim i As Integer
Dim j As Integer
Dim matchCount As Integer
splitColA = Split(UCase(colA), " ")
splitColB = noDuplicate(colB)
matchCount = 0
For i = 0 To UBound(splitColA)
For j = 0 To UBound(splitColB)
If splitColA(i) = splitColB(j) Then
matchCount = matchCount + 1
Exit For
End If
Next j
Next i
percentMatch = matchCount / (UBound(splitColA) + 1)
End Function
After add these two function, you can write your new code to below
Sub percentage()
Dim aRng As Range, cel As Range
Set aRng = Range(Range("A1"), Range("A5").End(xlDown))
For Each cel In aRng
cel.Offset(0, 2).Value = percentMatch(cel.Value, cel.Offset(0, 1).Value)
Next
End Sub
Note, I not protect for empty string in the function.
If you F8 through the code, you can see the problem.
The first Nice in column A loops through column B and counts 2 occurences.
Pack in column A loops through column B and counts 1 occurence.
The second Nice in column A loops through column B and counts 2 occurences.
Print in column A loops through column B and counts 1 occurence.
So you get a count of 6 against the 6 words in column A; 100%
If you add a random word to column A, you'll get 6 out of 7.

Unique Random Numbers using VBA

I am trying to create a series of unique (non-duplicating) random numbers within a user defined range. I have managed to create the random numbers, but I am getting duplicate values. How can I ensure that the random numbers will never be a duplicate?
Sub GenerateCodesUser()
Application.ScreenUpdating = False
Worksheets("Users").Activate
Dim MINNUMBER As Long
Dim MAXNUMBER As Long
MINNUMBER = 1000
MAXNUMBER = 9999999
Dim Row As Integer
Dim Number As Long
Dim high As Double
Dim Low As Double
Dim i As Integer
If (CustomCodes.CardNumberMin.Value = "") Then
MsgBox ("Fill Card Number Field!")
Exit Sub
ElseIf (CustomCodes.CardNumberMin.Value < MINNUMBER) Then
MsgBox ("Card Number Value must be equal or higher then" & MINNUMBER)
Exit Sub
End If
If (CustomCodes.CardNumberMax.Value = "") Then
MsgBox ("Fill Card Number Field!")
Exit Sub
ElseIf (CustomCodes.CardNumberMax.Value > MAXNUMBER) Then
MsgBox ("Card Number Value must be equal or higher then " & MAXNUMBER)
Exit Sub
End If
Low = CustomCodes.CardNumberMin.Value
high = CustomCodes.CardNumberMax.Value '<<< CHANGE AS DESIRED
If (Low < 1000) Then
'break
End If
For i = 1 To Cells(1, 1).End(xlToRight).Column
If InStr(Cells(1, i), "CardNumber") Then
Row = 2
While Cells(Row, 1) <> 0
Do
Number = ((high - Low + 1) * Rnd() + Low)
Loop Until Number > Low
Cells(Row, i) = Number
Row = Row + 1
Wend
End If
Next
Application.ScreenUpdating = True
End Sub
Here's a method of guaranteeing unique integer random numbers. Inline comments describe the method.
Function UniuqeRandom(Mn As Long, Mx As Long, Sample As Long) As Long()
Dim dat() As Long
Dim i As Long, j As Long
Dim tmp As Long
' Input validation checks here
If Mn > Mx Or Sample > (Mx - Mn + 1) Then
' declare error to suit your needs
Exit Function
End If
' size array to hold all possible values
ReDim dat(0 To Mx - Mn)
' Fill the array
For i = 0 To UBound(dat)
dat(i) = Mn + i
Next
' Shuffle array, unbiased
For i = UBound(dat) To 1 Step -1
tmp = dat(i)
j = Int((i + 1) * Rnd)
dat(i) = dat(j)
dat(j) = tmp
Next
'original biased shuffle
'For i = 0 To UBound(dat)
' tmp = dat(i)
' j = Int((Mx - Mn) * Rnd)
' dat(i) = dat(j)
' dat(j) = tmp
'Next
' Return sample
ReDim Preserve dat(0 To Sample - 1)
UniuqeRandom = dat
End Function
use it like this
Dim low As Long, high As Long
Dim rng As Range
Dim dat() As Long
Set rng = Range(Cells(1, 1), Cells(1, 1).End(xlToRight))
dat = UniuqeRandom(low, high, rng.Columns.Count)
rng.Offset(1, 0) = dat
Note: see this Wikipedia article regarding shuffle bias
The edit fixed one source of bias. The inherent limitations of Rnd (based on a 32 bit seed) and Modulo bias remain.
I see you have an accepted answer, but for whatever it's worth here is my stab at this question. This one uses a boolean function instead of numerical arrays. It's very simple yet fast. The advantage of it, which I'm not saying is perfect, is an effective solution for numbers in a long range because you only ever check the numbers you have already picked and saved and don't need a potentially large array to hold the values you have rejected so it won't cause memory problems because of the size of the array.
Sub UniqueRandomGenerator()
Dim N As Long, MaxNum As Long, MinNum As Long, Rand As Long, i As Long
MinNum = 1 'Put the input of minimum number here
MaxNum = 100 'Put the input of maximum number here
N = MaxNum - MinNum + 1
ReDim Unique(1 To N, 1 To 1)
For i = 1 To N
Randomize 'I put this inside the loop to make sure of generating "good" random numbers
Do
Rand = Int(MinNum + N * Rnd)
If IsUnique(Rand, Unique) Then Unique(i, 1) = Rand: Exit Do
Loop
Next
Sheet1.[A1].Resize(N) = Unique
End Sub
Function IsUnique(Num As Long, Data As Variant) As Boolean
Dim iFind As Long
On Error GoTo Unique
iFind = Application.WorksheetFunction.Match(Num, Data, 0)
If iFind > 0 Then IsUnique = False: Exit Function
Unique:
IsUnique = True
End Function
It Works perfectly:
Option Base 1
Public Function u(a As Variant, b As Variant) As Variant
Application.Volatile
Dim k%, p As Double, flag As Boolean, x() As Variant
k = 1
flag = False
ReDim x(1)
x(1) = Application.RandBetween(a, b)
Do Until k = b - a + 1
Do While flag = False
Randomize
p = Application.RandBetween(a, b)
'Debug.Assert p = 2
resultado = Application.Match(p, x, False)
If IsError(resultado) Then
k = k + 1
ReDim Preserve x(k)
x(k) = p
flag = True
Else
flag = False
End If
Loop
flag = False
Loop
u = x
End Function

Resources