4 Column Combinations W/ VBA - excel

I have the following code:
Sub combinations()
Range("G2:G" & Range("G2").End(xlDown).Row).ClearContents
Range("H2:G" & Range("H2").End(xlDown).Row).ClearContents
Range("I2:G" & Range("I2").End(xlDown).Row).ClearContents
Range("J2:G" & Range("J2").End(xlDown).Row).ClearContents
Dim c1() As Variant
Dim c2() As Variant
Dim c3() As Variant
Dim c4() As Variant
Dim out() As Variant
Dim j As Long, k As Long, l As Long, m As Long, n As Long
Dim col1 As Range
Dim col2 As Range
Dim col3 As Range
Dim col4 As Range
Dim out1 As Range
Set col1 = Range("A2", Range("A2").End(xlDown))
Set col2 = Range("B2", Range("B2").End(xlDown))
Set col3 = Range("C2", Range("C2").End(xlDown))
Set col4 = Range("D2", Range("D2").End(xlDown))
c1 = col1
c2 = col2
c3 = col3
c4 = col4
Set out1 = Range("G2", Range("K2").Offset(UBound(c1) * UBound(c2) * UBound(c3) * UBound(c4)))
out = out1
j = 1
k = 1
l = 1
m = 1
n = 1
Do While j <= UBound(c1)
Do While k <= UBound(c2)
Do While l <= UBound(c3)
Do While m <= UBound(c4)
out(n, 1) = c1(j, 1)
out(n, 2) = c2(k, 1)
out(n, 3) = c3(l, 1)
out(n, 4) = c4(m, 1)
n = n + 1
m = m + 1
Loop
m = 1
l = l + 1
Loop
l = 1
k = k + 1
Loop
k = 1
j = j + 1
Loop
out1.Value = out
End Sub
It creates all possible combination for values you put in A:A through D:D.
Example of a working table:
Header1 Header2 Header3 Header4
A1 B1 C1 D1
A2 B2 C2 D2
A3 B3 C3 D3
The only time it does not work is when one of the columns only has 1 value.
Example of a not working table:
Header1 Header2 Header3 Header4
A1 B1 C1 D1
B2 C2 D2
B3 C3 D3
I get a
Run-time error '1004;
Is there a way to fix this so that it would work for columns with 1 value as well?

This should work for you. Please note that it will work for any number of columns, not just 4, and that it will also work if any of the columns don't have full population (though each column must have at least one populated cell).
Sub tgr()
Dim ws As Worksheet
Dim rDest As Range
Dim aHeaders() As Variant
Dim aTemp() As Variant
Dim aData() As Variant
Dim aResults() As Variant
Dim vTemp As Variant
Dim ixData As Long
Dim ixResult As Long
Dim ixRow As Long
Dim ixCol As Long
Dim lMaxRows As Long
Dim lResultsBlock As Long
Dim lOverflowResults As Long
Dim bPopulated As Boolean
'Adjust these as necessary
Set ws = ActiveWorkbook.Worksheets(1) 'The worksheet that contains the table of values
Set rDest = ws.Range("G2") 'The worksheet and cell where results should be output to
lResultsBlock = 100000 'The number of rows the results array can contain before having to output results and then continuing
'Get table of values that will be used to create combinations, assume table starts in A1 and has headers
With ws.Range("A1").CurrentRegion
If .Rows.Count = 1 Then Exit Sub 'No data
If .Cells.Count = 2 Then
ReDim aHeaders(1 To 1, 1 To 1)
aHeaders(1, 1) = .Cells(1).Value
ReDim aData(1 To 1, 1 To 1)
aData(1, 1) = .Cells(2).Value
Else
aHeaders = .Resize(1).Value
aData = .Offset(1).Resize(.Rows.Count - 1).Value
End If
lMaxRows = UBound(aData, 1) ^ UBound(aData, 2)
ReDim aResults(1 To lResultsBlock, 1 To UBound(aData, 2))
lOverflowResults = 0
End With
'Clear previous results
ClearResults rDest
'Iterate over the table of values and create every possible combination
For ixRow = 1 To lMaxRows
'Prevent Excel from looking frozen, show a % percent complete
If (ixRow - 1) Mod 10000 = 0 Then
DoEvents
Application.StatusBar = "Processing: " & Format(ixRow / lMaxRows, "0.00%") & " completed..."
End If
'Check if this combination has any empty/blank values
bPopulated = True
ReDim aTemp(1 To UBound(aResults, 2))
For ixCol = 1 To UBound(aResults, 2)
ixData = Int(((ixRow - 1) Mod (UBound(aData, 1) ^ (UBound(aData, 2) - (ixCol - 1)))) / (UBound(aData, 1) ^ (UBound(aData, 2) - ixCol))) + 1
vTemp = aData(ixData, ixCol)
If Len(vTemp) > 0 Then
aTemp(ixCol) = vTemp
Else
'Empty/blank found, skip this combination
bPopulated = False
Exit For
End If
Next ixCol
If bPopulated Then
'No empties/blanks found in this combination, add it to results
ixResult = ixResult + 1
For ixCol = 1 To UBound(aResults, 2)
aResults(ixResult, ixCol) = aTemp(ixCol)
Next ixCol
Erase aTemp
'Output results if the results array is full
If ixResult = UBound(aResults, 1) Then OutputResults ws, rDest, aResults, ixResult, lOverflowResults, aHeaders
End If
Next ixRow
'Output results if results array is at least partially populated
If ixResult > 0 Then OutputResults ws, rDest, aResults, ixResult, lOverflowResults, aHeaders
Application.StatusBar = vbNullString
End Sub
'This will clear any previous results
Sub ClearResults(ByVal arg_rDest As Range)
Dim ws As Worksheet
arg_rDest.CurrentRegion.ClearContents
Application.DisplayAlerts = False
For Each ws In arg_rDest.Worksheet.Parent.Worksheets
If ws.Name Like "Overflow Results (*)" Then ws.Delete
Next ws
Application.DisplayAlerts = True
End Sub
'This will output the current results array to the appropriate destination, accounting for if a new sheet needs to be created and whether headers need to be provided
Sub OutputResults(ByRef arg_wsDest As Worksheet, _
ByVal arg_rDest As Range, _
ByRef arg_aResults As Variant, _
ByRef arg_ixResult As Long, _
ByRef arg_lOverflowResults As Long, _
Optional ByVal arg_aHeaders As Variant)
Dim rDest As Range
Dim lHeaderRow As Long
Dim lRowCount As Long
Dim lColCount As Long
'Check if this is the first time results are being output
If arg_wsDest.Cells(arg_wsDest.Rows.Count, arg_rDest.Column).End(xlUp).Row <= arg_rDest.Row Then
'This is the first time results are being output
arg_lOverflowResults = 0
'Check if headers need to be placed
If IsArray(arg_aHeaders) Then
If arg_rDest.Row = 1 Then lHeaderRow = 1 Else lHeaderRow = arg_rDest.Row - 1
With arg_wsDest.Cells(lHeaderRow, arg_rDest.Column).Resize(, UBound(arg_aHeaders, 2))
.Value = arg_aHeaders
.Font.Bold = True
End With
Set rDest = arg_wsDest.Cells(lHeaderRow + 1, arg_rDest.Column)
Else
Set rDest = arg_rDest
End If
End If
'These are used to create a new, empty results array after results are output
lRowCount = UBound(arg_aResults, 1)
lColCount = UBound(arg_aResults, 2)
'Check if there is room left in the current destination worksheet to contain all of the results
If arg_wsDest.Cells(arg_wsDest.Rows.Count, arg_rDest.Column).End(xlUp).Row + 1 + arg_ixResult > arg_wsDest.Rows.Count Then
'Not enough room found, create a new sheet to continue outputting results on and apply headers if necessary
arg_lOverflowResults = arg_lOverflowResults + 1
Set arg_wsDest = arg_wsDest.Parent.Worksheets.Add(AFter:=arg_wsDest)
arg_wsDest.Name = "Overflow Results (" & arg_lOverflowResults & ")"
If IsArray(arg_aHeaders) Then
With arg_wsDest.Cells(1, arg_rDest.Column).Resize(, UBound(arg_aHeaders, 2))
.Value = arg_aHeaders
.Font.Bold = True
End With
Set rDest = arg_wsDest.Cells(2, arg_rDest.Column)
Else
Set rDest = arg_wsDest.Cells(1, arg_rDest.Column)
End If
Else
'Enough room found, set destination for where results should begin
If rDest Is Nothing Then Set rDest = arg_wsDest.Cells(arg_wsDest.Rows.Count, arg_rDest.Column).End(xlUp).Offset(1)
End If
'Output results
rDest.Resize(arg_ixResult, UBound(arg_aResults, 2)).Value = arg_aResults
'Clear the existing results array and create a new, empty results array
Erase arg_aResults
ReDim arg_aResults(1 To lRowCount, 1 To lColCount)
arg_ixResult = 0
End Sub

Related

Excel VBA Passing Variables

I need to pass the variables max, min, and their respective locations to another sub where it will format each max and min in their respective column. I am trying to create an array that will store the locations and the values but its not working.
I was told to first identify the number of columns used and the number of rows, which is the beginning.
Rows = wsData.UsedRange.Rows.Count
Columns = wsData.UsedRange.Col.Count
j = 1
ReDim Min(j)
With wsData.Range("A3:A19")
For j = 1 To 19 'colum
Min(j) = WorksheetFunction.Min(Range(.Offset(1, j), .Offset(Row, j)))
Max = WorksheetFunction.Max(Range(.Offset(1, j), .Offset(Row, j)))
Min(j) = Min
j = j + 1
ReDim Preserve Min(j) 'saves variables
Next 'next column
End With
The code below uses the ActiveSheet which you need to change to reference the worksheet for your data. Additionally, it assumes that your data starts with Row 1. The code looks at each column in the range and stores the minimum/maximum (it does not account for multiple cells which may share the min or max value) value found in the column as well as the cell's address, in an array and then passes the array to two different subs, one which simply displays the information in a message and one which formats the the background color of the cells. This code does not perform any kind of error handling, but should get you where you want to go.
the line Option Explicit requires that all of the variables be defined using a Dim statement
the line Option Base 1 makes the default starting point for arrays 1 instead of 0
Option Explicit
Option Base 1
Sub GatherData()
Dim iRows As Long
Dim iCols As Long
Dim j As Long
Dim iMin() As Variant
Dim iMax() As Variant
Dim R As Range
iRows = ActiveSheet.UsedRange.Rows.Count
iCols = ActiveSheet.UsedRange.Columns.Count
ReDim iMin(iCols, 2)
ReDim iMax(iCols, 2)
For j = 1 To iCols
Set R = Range(Cells(1, j), Cells(iRows, j)).Find(WorksheetFunction.Min(Range(Cells(1, j), Cells(iRows, j))), LookIn:=xlValues)
iMin(j, 1) = R.Value
iMin(j, 2) = R.Address
Set R = Range(Cells(1, j), Cells(iRows, j)).Find(WorksheetFunction.Max(Range(Cells(1, j), Cells(iRows, j))), LookIn:=xlValues)
iMax(j, 1) = R.Value
iMax(j, 2) = R.Address
Next j
ListMinMax iMax(), True
ListMinMax iMin(), False
FormatMinMax iMax, "green"
FormatMinMax iMin, "yellow"
Set R = Nothing
End Sub
Sub ListMinMax(ByRef Arr() As Variant, ByVal MinMax As Boolean)
Dim strOutput As String
Dim i As Long
If MinMax = True Then
strOutput = "Maximums:" & vbCrLf & vbCrLf
Else
strOutput = "Minimums:" & vbCrLf & vbCrLf
End If
For i = 1 To UBound(Arr, 1)
strOutput = strOutput & "Cell: " & Arr(i, 2) & " = " & Arr(i, 1) & vbCrLf
Next i
MsgBox strOutput, vbOKOnly
End Sub
Sub FormatMinMax(ByRef Arr() As Variant, ByVal BGColor As String)
Dim i As Long
Select Case UCase(BGColor)
Case "GREEN"
For i = 1 To UBound(Arr, 1)
ActiveSheet.Range(Arr(i, 2)).Interior.Color = vbGreen
Next i
Case "YELLOW"
For i = 1 To UBound(Arr, 1)
ActiveSheet.Range(Arr(i, 2)).Interior.Color = vbYellow
Next i
Case Else
MsgBox "Invalid Option", vbCritical
End Select
End Sub
======================================================================
The code below does away with the need for the arrays and formats the color of the min/max values as it finds them
Sub GatherData2()
Dim iRows As Long
Dim iCols As Long
Dim j As Long
Dim R As Range
iRows = ActiveSheet.UsedRange.Rows.Count
iCols = ActiveSheet.UsedRange.Columns.Count
For j = 1 To iCols
Set R = Range(Cells(1, j), Cells(iRows, j)).Find(WorksheetFunction.Min(Range(Cells(1, j), Cells(iRows, j))), LookIn:=xlValues)
R.Interior.Color = vbYellow
Set R = Range(Cells(1, j), Cells(iRows, j)).Find(WorksheetFunction.Max(Range(Cells(1, j), Cells(iRows, j))), LookIn:=xlValues)
R.Interior.Color = vbGreen
Next j
Set R = Nothing
End Sub

Coping Data from One Workbook To Another Based On Cell Data

I am trying to copy data from one workbook to another based on the values contained in cells in the source workbook that matches the same values in the target workbook. For example, I have a table (Table1) that has four columns say, A1:D5. One of these columns (column A) contains account numbers that match similar account numbers located on another workbook (also in column A). I am trying to find a code that looks through the table (Table1) in the source workbook via the account number column, and if the account number matches the account number in the target workbook, copy and paste the cells on that row in specific locations to the target workbook. Is this possible?
I hope that makes sense. I have looked all over on how to structure such a code, and I was not able to find anything to start the process for this logic.
Any help will be very appreciative.
Thank you
Even if your question is about doing this in VBA, I'm just going to mention that what you are trying to do seems like it could also be done with Power Query.
That being said, if you were to use VBA for this, you would have to use the Match function to find where your rows match and then copy the data from the source to the destination table.
I've adapted the code I provided to this question to better serve your specific needs. One of the things I've done is to add an optional argument called DoOverwrite and set it to false. This will make sure that the information from one row won't be overwritten by another row later down the road.
Sub TableJoinTest()
'Those table columns will have to match for the 2 lines to be a match
Dim MandatoryHeaders() As Variant
MandatoryHeaders = Array("Account Number")
Dim SourceTableAnchor As Range
Set SourceTableAnchor = Workbooks("SourceWorkbook.xlsx").Sheets("Sheet1").Range("A1")
Dim TargetTableAnchor As Range
Set TargetTableAnchor = Workbooks("TargetWorkbook.xlsx").Sheets("Sheet1").Range("A1")
TableJoin _
SourceTableAnchor:=SourceTableAnchor, _
TargetTableAnchor:=TargetTableAnchor, _
MandatoryHeaders:=MandatoryHeaders, _
AddIfMissing:=False, _
IsLogging:=False, _
DoOverwrite:=False
End Sub
Sub TableJoin( _
SourceTableAnchor As Range, _
TargetTableAnchor As Range, _
MandatoryHeaders As Variant, _
Optional OtherHeaders As Variant, _
Optional AddIfMissing As Boolean = False, _
Optional IsLogging As Boolean = False, _
Optional DoOverwrite As Boolean = True)
'''''''''''''''''''''''''''''''''''''''
'Definitions
'''''''''''''''''''''''''''''''''''''''
Dim srng As Range, trng As Range
Set srng = SourceTableAnchor.CurrentRegion
Set trng = TargetTableAnchor.CurrentRegion
Dim sHeaders As Range, tHeaders As Range
Set sHeaders = srng.Rows(1)
Set tHeaders = trng.Rows(1)
'Store in Arrays
Dim sArray() As Variant 'prefix s is for Source
sArray = ExcludeRows(srng, 1).Value2
Dim tArray() As Variant 'prefix t is for Target
tArray = ExcludeRows(trng, 1).Value2
Dim sArrayHeader As Variant
sArrayHeader = sHeaders.Value2
Dim tArrayHeader As Variant
tArrayHeader = tHeaders.Value2
'Find Column correspondance
Dim sMandatoryHeadersColumn As Variant
ReDim sMandatoryHeadersColumn(LBound(MandatoryHeaders) To UBound(MandatoryHeaders))
Dim tMandatoryHeadersColumn As Variant
ReDim tMandatoryHeadersColumn(LBound(MandatoryHeaders) To UBound(MandatoryHeaders))
Dim k As Long
For k = LBound(MandatoryHeaders) To UBound(MandatoryHeaders)
sMandatoryHeadersColumn(k) = Application.Match(MandatoryHeaders(k), sArrayHeader, 0)
tMandatoryHeadersColumn(k) = Application.Match(MandatoryHeaders(k), tArrayHeader, 0)
Next k
Dim sOtherHeadersColumn As Variant
ReDim sOtherHeadersColumn(LBound(OtherHeaders) To UBound(OtherHeaders))
Dim tOtherHeadersColumn As Variant
ReDim tOtherHeadersColumn(LBound(OtherHeaders) To UBound(OtherHeaders))
For k = LBound(OtherHeaders) To UBound(OtherHeaders)
sOtherHeadersColumn(k) = Application.Match(OtherHeaders(k), sArrayHeader, 0)
tOtherHeadersColumn(k) = Application.Match(OtherHeaders(k), tArrayHeader, 0)
Next k
'Merge mandatory headers into one column (aka the helper column method)
Dim i As Long, j As Long
Dim sHelperColumn() As Variant
ReDim sHelperColumn(LBound(sArray, 1) To UBound(sArray, 1), 1 To 1)
For i = LBound(sArray, 1) To UBound(sArray, 1)
For j = LBound(MandatoryHeaders) To UBound(MandatoryHeaders)
sHelperColumn(i, 1) = sHelperColumn(i, 1) & sArray(i, sMandatoryHeadersColumn(j))
Next j
Next i
Dim tHelperColumn() As Variant
ReDim tHelperColumn(LBound(tArray, 1) To UBound(tArray, 1), 1 To 1)
For i = LBound(tArray, 1) To UBound(tArray, 1)
For j = LBound(MandatoryHeaders) To UBound(MandatoryHeaders)
tHelperColumn(i, 1) = tHelperColumn(i, 1) & tArray(i, tMandatoryHeadersColumn(j))
Next j
Next i
'Find all matches
Dim MatchList() As Variant
Dim LoggingColumn() As String
ReDim LoggingColumn(LBound(tArray, 1) To UBound(tArray, 1), 1 To 1)
For i = LBound(sArray, 1) To UBound(sArray, 1)
ReDim MatchList(LBound(tArray, 1) To UBound(tArray, 1))
For j = LBound(tArray, 1) To UBound(tArray, 1)
If sHelperColumn(i, 1) = tHelperColumn(j, 1) Then
MatchList(j) = 1
End If
Next j
'Get the row number for the match
Dim MatchRow As Long
Select Case Application.Sum(MatchList)
Case Is > 1
'Need to do more matching
Dim MatchingScoresList() As Long
ReDim MatchingScoresList(1 To UBound(tArray, 1))
Dim m As Long
For k = LBound(OtherHeaders) To UBound(OtherHeaders)
For m = LBound(tArray, 1) To UBound(tArray, 1)
If tArray(m, sOtherHeadersColumn(k)) = sArray(i, sOtherHeadersColumn(k)) Then
MatchingScoresList(m) = MatchingScoresList(m) + 2 ^ (UBound(OtherHeaders) - k)
End If
Next m
Next k
'Get the max score position
Dim MyMax As Long
MyMax = Application.Max(MatchingScoresList)
If Application.Count(Application.Match(MatchingScoresList(), Array(MyMax), 0)) > 1 Then
MsgBox "Error: can't determine how to match row " & i & " in source table"
Exit Sub
Else
MatchRow = Application.Match(MyMax, MatchingScoresList, 0)
End If
Case Is = 1
MatchRow = Application.Match(1, MatchList, 0)
Case Else
Dim nArray() As Variant, Counter As Long
If AddIfMissing Then
MatchRow = 0
Counter = Counter + 1
ReDim nArray(1 To Counter, 1 To UBound(tArray, 2))
For k = LBound(MandatoryHeaders) To UBound(MandatoryHeaders)
nArray(Counter, tMandatoryHeadersColumn(k)) = sArray(i, sMandatoryHeadersColumn(k))
Next k
For k = LBound(OtherHeaders) To UBound(OtherHeaders)
nArray(Counter, tOtherHeadersColumn(k)) = sArray(i, sOtherHeadersColumn(k))
Next k
Else
MsgBox "Error: Couldn't find a match for data row #" & i
Exit Sub
End If
End Select
'Logging and assigning values
If MatchRow > 0 Then
For k = LBound(OtherHeaders) To UBound(OtherHeaders)
If tArray(MatchRow, tOtherHeadersColumn(k)) <> sArray(i, sOtherHeadersColumn(k)) Then
'Logging
If IsLogging And DoOverwrite Then LoggingColumn(MatchRow, 1) = LoggingColumn(MatchRow, 1) & _
IIf(LoggingColumn(MatchRow, 1) <> "", ", ", "") & _
tHeaders.Cells(1, tOtherHeadersColumn(k)) & " : " & _
tArray(MatchRow, tOtherHeadersColumn(k)) & _
" -> " & sArray(i, sOtherHeadersColumn(k))
'Assign new value
If DoOverwrite Or tArray(MatchRow, tOtherHeadersColumn(k)) = VbNullString Then
tArray(MatchRow, tOtherHeadersColumn(k)) = sArray(i, sOtherHeadersColumn(k))
End If
End If
Next k
End If
Next i
'Write arrays to sheet
ExcludeRows(trng, 1).Value2 = tArray
With trng.Parent
If IsArrayInitialised(nArray) And AddIfMissing Then
.Cells(trng.Cells(1, 1).Row + trng.Rows.Count, trng.Cells(1, 1).Column).Resize(UBound(nArray, 1), UBound(nArray, 2)).Value2 = nArray
End If
If IsLogging Then
.Cells(trng.Cells(1, 1).Row, trng.Cells(1, 1).Column + trng.Columns.Count) = "Changes"
.Cells(trng.Cells(2, 1).Row, trng.Cells(1, 1).Column + trng.Columns.Count).Resize(UBound(LoggingColumn, 1), 1).Value2 = LoggingColumn
End If
End With
End Sub
And also add these functions inside your VBA project to as they are used in the procedure above.
Function IsArrayInitialised(ByRef A() As Variant) As Boolean
On Error Resume Next
IsArrayInitialised = IsNumeric(UBound(A))
On Error GoTo 0
End Function
Function ExcludeRows(MyRng As Range, StartRow As Long, Optional EndRow As Long = -1) As Range
'PURPOSE: Exclude one or more consecutives rows from an existing range
Dim Afterpart As Range, BeforePart As Range
If StartRow < 1 Or EndRow > MyRng.Rows.Count Then Set ExcludeRows = Nothing
If StartRow = 1 And EndRow = MyRng.Rows.Count Then Set ExcludeRows = Nothing
If EndRow = -1 Then EndRow = StartRow
If EndRow < MyRng.Rows.Count Then
With MyRng.Parent
Set Afterpart = .Range(MyRng.Cells(EndRow + 1, 1), MyRng.Cells(MyRng.Rows.Count, MyRng.Columns.Count))
End With
End If
If StartRow > 1 Then
With MyRng.Parent
Set BeforePart = .Range(MyRng.Cells(1, MyRng.Cells(1, 1).Column), MyRng.Cells(StartRow - 1, MyRng.Columns.Count))
End With
End If
Set ExcludeRows = Union2(True, BeforePart, Afterpart)
End Function
Public Function Union2(IgnoreEmptyRange As Boolean, ParamArray RangeArray() As Variant) As Range
'PURPOSE: Samae as Application.Union but allows some range object to be Empty
Dim V As Variant
Dim Rng As Range
For Each V In RangeArray
Do
If VarType(V) = vbEmpty Then Exit Do
Set Rng = V
If Not Union2 Is Nothing Then
Set Union2 = Union(Union2, Rng)
ElseIf Not Rng Is Nothing Then
Set Union2 = Rng
End If
Loop While False
Next
End Function

Pasting all values with specific text under matching header

On Sheet1, I have a set of data with column A showing names and column B marital status.
I would like to output the name based on the marital status to Sheet2 where I have a predetermined dashboard (A1 could be start of table)
The data set will be dynamic and grow each time the vba is run
what I'd like the output data to be
Would you kindly assist in the vba code for this output?
Update, here is the code I have...which works but would like input on code efficiency
Dim K As Long, r As Range, v As Variant
K = 1
Dim w1 As Worksheet, w2 As Worksheet
Set w1 = Sheets("Sheet1")
Set w2 = Sheets("Sheet2")
w1.Activate
For Each r In Intersect(Range("B:B"), ActiveSheet.UsedRange)
v = r.Value
If InStr(v, "Divorced") > 0 Then
r.Offset(, -1).Copy w2.Cells(K + 3, 2)
K = K + 1
End If
Next r
K = 1
For Each r In Intersect(Range("B:B"), ActiveSheet.UsedRange)
v = r.Value
If InStr(v, "Married") > 0 Then
r.Offset(, -1).Copy w2.Cells(K + 3, 3)
K = K + 1
End If
Next r
K = 1
For Each r In Intersect(Range("B:B"), ActiveSheet.UsedRange)
v = r.Value
If InStr(v, "Single") > 0 Then
r.Offset(, -1).Copy w2.Cells(K + 3, 4)
K = K + 1
End If
Next r
K = 1
For Each r In Intersect(Range("B:B"), ActiveSheet.UsedRange)
v = r.Value
If InStr(v, "Widowed") > 0 Then
r.Offset(, -1).Copy w2.Cells(K + 3, 5)
K = K + 1
End If
Next r
If you're looking for the best way to code it, here's how I would do it. This ran about a million rows of data in 11 seconds. Code commented for clarity. Adjust the variable values to match your actual data where necessary.
EDIT: Added variable to allow for output column on wsDest to begin at defined column instead of assuming column A. Set it to B to match OP's code.
Sub tgr()
Const lDataHeaderRow As Long = 1 'The header row of your 2-column original data worksheet
Const lDestHeaderRow As Long = 1 'The header row of your multi-column destination/output worksheet
Const sDestStartCol As String = "B" 'The column letter where the output results begin
Dim wb As Workbook
Dim wsData As Worksheet
Dim wsDest As Worksheet
Dim rDestHeaders As Range
Dim hResults As Object
Dim aData As Variant
Dim aResults() As Variant
Dim vTemp As Variant
Dim i As Long
Set wb = ActiveWorkbook
Set wsData = wb.Worksheets("Sheet1")
Set wsDest = wb.Worksheets("Sheet2")
Set rDestHeaders = wsDest.Range(wsDest.Cells(lDestHeaderRow, sDestStartCol), wsDest.Cells(lDestHeaderRow, wsDest.Columns.Count).End(xlToLeft))
Set hResults = CreateObject("Scripting.Dictionary") 'Use a dictionary to keep track of marital statuses and associated names
'Define your data range here and load it into a variant array for processing
With wsData.Range("A" & lDataHeaderRow + 1, wsData.Cells(wsData.Rows.Count, "B").End(xlUp))
If .Row <= lDataHeaderRow Then Exit Sub 'No data
ReDim aResults(1 To Evaluate("MAX(COUNTIF('" & wsData.Name & "'!B:B,'" & wsDest.Name & "'!" & rDestHeaders.Address & "))"), 1 To rDestHeaders.Cells.Count)
aData = .Value
End With
'Define which column is for which header, the "|0" is the starting count found for that marital status
For i = 1 To rDestHeaders.Cells.Count
hResults(LCase(Trim(rDestHeaders.Cells(, i).Value))) = i & "|" & 0
Next i
'Loop through the variant array, looking at column 2 for the status
For i = LBound(aData, 1) To UBound(aData, 1)
'Verify column 1 and 2 and aren't blank
If Len(Trim(aData(i, 1))) > 0 And Len(Trim(aData(i, 2))) > 0 Then
'Verify current marital status (column 2) is listed in the destination headers
If hResults.Exists(LCase(Trim(aData(i, 2)))) Then
vTemp = Split(hResults(LCase(Trim(aData(i, 2)))), "|")
vTemp(1) = vTemp(1) + 1
aResults(vTemp(1), vTemp(0)) = aData(i, 1)
hResults(LCase(Trim(aData(i, 2)))) = Join(vTemp, "|")
End If
End If
Next i
'Clear previous results
Intersect(wsDest.Cells(lDestHeaderRow, sDestStartCol).CurrentRegion, rDestHeaders.EntireColumn).Offset(1).ClearContents
'Output results
wsDest.Cells(lDestHeaderRow + 1, sDestStartCol).Resize(UBound(aResults, 1), UBound(aResults, 2)).Value = aResults
End Sub

Transpose multiple columns to multiple rows with VBA

This is the kind of transformation is what I am trying to perform.
For illustration I made this as table. Basically the first three columns should repeat for however many colors are available.
I searched for similar questions but could not find when I want multiple columns to repeat.
I found this code online
Sub createData()
Dim dSht As Worksheet
Dim sSht As Worksheet
Dim colCount As Long
Dim endRow As Long
Dim endRow2 As Long
Set dSht = Sheets("Sheet1") 'Where the data sits
Set sSht = Sheets("Sheet2") 'Where the transposed data goes
sSht.Range("A2:C60000").ClearContents
colCount = dSht.Range("A1").End(xlToRight).Column
'// loops through all the columns extracting data where "Thank" isn't blank
For i = 2 To colCount Step 2
endRow = dSht.Cells(1, i).End(xlDown).Row
For j = 2 To endRow
If dSht.Cells(j, i) <> "" Then
endRow2 = sSht.Range("A50000").End(xlUp).Row + 1
sSht.Range("A" & endRow2) = dSht.Range("A" & j)
sSht.Range("B" & endRow2) = dSht.Cells(j, i)
sSht.Range("C" & endRow2) = dSht.Cells(j, i).Offset(0, 1)
End If
Next j
Next i
End Sub
I tried changing step 2 to 1 and j to start from 4.
Another example with two varied sets:
Here's a generic "unpivot" approach (all "fixed" columns must appear on the left of the columns to be unpivoted)
Test sub:
Sub Tester()
Dim p
'get the unpivoted data as a 2-D array
p = UnPivotData(Sheets("Sheet1").Range("A1").CurrentRegion, _
3, False, False)
With Sheets("Sheet1").Range("H1")
.CurrentRegion.ClearContents
.Resize(UBound(p, 1), UBound(p, 2)).Value = p 'populate array to sheet
End With
'EDIT: alternative (slower) method to populate the sheet
' from the pivoted dataset. Might need to use this
' if you have a large amount of data
'Dim r As Long, c As Long
'For r = 1 To Ubound(p, 1)
'For c = 1 To Ubound(p, 2)
' Sheets("Sheet2").Cells(r, c).Value = p(r, c)
'Next c
'Next r
End Sub
UnPivot function - should not need any modifications:
Function UnPivotData(rngSrc As Range, fixedCols As Long, _
Optional AddCategoryColumn As Boolean = True, _
Optional IncludeBlanks As Boolean = True)
Dim nR As Long, nC As Long, data, dOut()
Dim r As Long, c As Long, rOut As Long, cOut As Long, cat As Long
Dim outRows As Long, outCols As Long
data = rngSrc.Value 'get the whole table as a 2-D array
nR = UBound(data, 1) 'how many rows
nC = UBound(data, 2) 'how many cols
'calculate the size of the final unpivoted table
outRows = nR * (nC - fixedCols)
outCols = fixedCols + IIf(AddCategoryColumn, 2, 1)
'resize the output array
ReDim dOut(1 To outRows, 1 To outCols)
'populate the header row
For c = 1 To fixedCols
dOut(1, c) = data(1, c)
Next c
If AddCategoryColumn Then
dOut(1, fixedCols + 1) = "Category"
dOut(1, fixedCols + 2) = "Value"
Else
dOut(1, fixedCols + 1) = "Value"
End If
'populate the data
rOut = 1
For r = 2 To nR
For cat = fixedCols + 1 To nC
If IncludeBlanks Or Len(data(r, cat)) > 0 Then
rOut = rOut + 1
'Fixed columns...
For c = 1 To fixedCols
dOut(rOut, c) = data(r, c)
Next c
'populate unpivoted values
If AddCategoryColumn Then
dOut(rOut, fixedCols + 1) = data(1, cat)
dOut(rOut, fixedCols + 2) = data(r, cat)
Else
dOut(rOut, fixedCols + 1) = data(r, cat)
End If
End If
Next cat
Next r
UnPivotData = dOut
End Function
Here is one way (fastest?) using arrays. This approach is better that the linked question as it doesn't read and write to/from range objects in a loop. I have commented the code so you shouldn't have a problem understanding it.
Option Explicit
Sub Sample()
Dim wsThis As Worksheet, wsThat As Worksheet
Dim ThisAr As Variant, ThatAr As Variant
Dim Lrow As Long, Col As Long
Dim i As Long, k As Long
Set wsThis = Sheet1: Set wsThat = Sheet2
With wsThis
'~~> Find Last Row in Col A
Lrow = .Range("A" & .Rows.Count).End(xlUp).Row
'~~> Find total value in D,E,F so that we can define output array
Col = Application.WorksheetFunction.CountA(.Range("D2:F" & Lrow))
'~~> Store the values from the range in an array
ThisAr = .Range("A2:F" & Lrow).Value
'~~> Define your new array
ReDim ThatAr(1 To Col, 1 To 4)
'~~> Loop through the array and store values in new array
For i = LBound(ThisAr) To UBound(ThisAr)
k = k + 1
ThatAr(k, 1) = ThisAr(i, 1)
ThatAr(k, 2) = ThisAr(i, 2)
ThatAr(k, 3) = ThisAr(i, 3)
'~~> Check for Color 1
If ThisAr(i, 4) <> "" Then ThatAr(k, 4) = ThisAr(i, 4)
'~~> Check for Color 2
If ThisAr(i, 5) <> "" Then
k = k + 1
ThatAr(k, 1) = ThisAr(i, 1)
ThatAr(k, 2) = ThisAr(i, 2)
ThatAr(k, 3) = ThisAr(i, 3)
ThatAr(k, 4) = ThisAr(i, 5)
End If
'~~> Check for Color 3
If ThisAr(i, 6) <> "" Then
k = k + 1
ThatAr(k, 1) = ThisAr(i, 1)
ThatAr(k, 2) = ThisAr(i, 2)
ThatAr(k, 3) = ThisAr(i, 3)
ThatAr(k, 4) = ThisAr(i, 6)
End If
Next i
End With
'~~> Create headers in Sheet2
Sheet2.Range("A1:D1").Value = Sheet1.Range("A1:D1").Value
'~~> Output the array
wsThat.Range("A2").Resize(Col, 4).Value = ThatAr
End Sub
SHEET1
SHEET2
The addition of the LET function allows for this non-VBA solution.
=LET(data,B3:F6,
dataRows,ROWS(data),
dataCols,COLUMNS(data),
rowHeaders,OFFSET(data,0,-1,dataRows,1),
colHeaders,OFFSET(data,-1,0,1,dataCols),
dataIndex,SEQUENCE(dataRows*dataCols),
rowIndex,MOD(dataIndex-1,dataRows)+1,
colIndex,INT((dataIndex-1)/dataRows)+1,
FILTER(CHOOSE({1,2,3}, INDEX(rowHeaders,rowIndex), INDEX(colHeaders,colIndex), INDEX(data,rowIndex,colIndex)), index(data,rowIndex,colIndex)<>""))
Below is a custom function I wrote for such things (demo video I posted on YouTube). A few differences from other answers:
The custom function allows for more than one axis in columns. As shown below, the column axis has Currency and Time.
Row axis does not need to be directly next to the data range.
One can specify the entire row as the column axis or the entire column to specify the row axis. See formula used as example below.
So with this data set:
And entering this as the formula:
=unPivotData(D4:G7,2:3,B:C)
an output of this:
Function unPivotData(theDataRange As Range, theColumnRange As Range, theRowRange As Range, _
Optional skipZerosAsTrue As Boolean, Optional includeBlanksAsTrue As Boolean)
'Set effecient range
Dim cleanedDataRange As Range
Set cleanedDataRange = Intersect(theDataRange, theDataRange.Worksheet.UsedRange)
'tests Data ranges
With cleanedDataRange
'Use intersect address to account for users selecting full row or column
If .EntireColumn.Address <> Intersect(.EntireColumn, theColumnRange).EntireColumn.Address Then
unPivotData = "datarange missing Column Ranges"
ElseIf .EntireRow.Address <> Intersect(.EntireRow, theRowRange).EntireRow.Address Then
unPivotData = "datarange missing row Ranges"
ElseIf Not Intersect(cleanedDataRange, theColumnRange) Is Nothing Then
unPivotData = "datarange may not intersect column range. " & Intersect(cleanedDataRange, theColumnRange).Address
ElseIf Not Intersect(cleanedDataRange, theRowRange) Is Nothing Then
unPivotData = "datarange may not intersect row range. " & Intersect(cleanedDataRange, theRowRange).Address
End If
'exits if errors were found
If Len(unPivotData) > 0 Then Exit Function
Dim dimCount As Long
dimCount = theColumnRange.Rows.Count + theRowRange.Columns.Count
Dim aCell As Range, i As Long, g As Long
ReDim newdata(dimCount, i)
End With
'loops through data ranges
For Each aCell In cleanedDataRange.Cells
With aCell
If .Value2 = "" And Not (includeBlanksAsTrue) Then
'skip
ElseIf .Value2 = 0 And skipZerosAsTrue Then
'skip
Else
ReDim Preserve newdata(dimCount, i)
g = 0
'gets DimensionMembers members
For Each gcell In Union(Intersect(.EntireColumn, theColumnRange), _
Intersect(.EntireRow, theRowRange)).Cells
newdata(g, i) = IIf(gcell.Value2 = "", "", gcell.Value)
g = g + 1
Next gcell
newdata(g, i) = IIf(.Value2 = "", "", .Value)
i = i + 1
End If
End With
Next aCell
unPivotData = WorksheetFunction.Transpose(newdata)
End Function

How to receive all combinations of all columns?

I am trying to get all row combinations of all columns (say 8 columns). The following vba macro can do that but I get an error that says data overload:
Option Explicit
Const sTitle As String = "shg Cartesian Product"
Sub CartesianProduct()
' shg 2012, 2013
' Choose one from col A, one from col B, ...
Dim rInp As Range
Dim avInp As Variant ' ragged input list
Dim nCol As Long ' # columns in list
Dim rOut As Range ' output range
Dim iCol As Long ' column index
Dim iRow As Long ' row index
Dim aiCum() As Long ' cum count of arrangements from right to left
Dim aiCnt() As Long ' count of items in each column
Dim iArr As Long ' arrangement number
Dim avOut As Variant ' output buffer
Application.ScreenUpdating = False
Set rInp = Range("rgnInp")
If VarType(rInp.Value) = vbEmpty Then
MsgBox Prompt:="No input!", _
Buttons:=vbOKOnly, _
Title:=sTitle
Exit Sub
End If
Set rInp = rInp.CurrentRegion
If rInp.Columns.Count < 2 Or rInp.Rows.Count < 2 Then
MsgBox Prompt:="Must have more than one row and more than one columns!", _
Buttons:=vbOKOnly, _
Title:=sTitle
Exit Sub
End If
With rInp
.Style = "Input"
avInp = .Value
nCol = .Columns.Count
Set rOut = .Resize(1).Offset(.Rows.Count + 1)
Range(rOut.Offset(-1, -1), Cells(Rows.Count, Columns.Count)).Clear
End With
ReDim aiCum(1 To nCol + 1)
ReDim aiCnt(1 To nCol)
aiCum(nCol + 1) = 1
For iCol = nCol To 1 Step -1
For iRow = 1 To UBound(avInp, 1)
If IsEmpty(avInp(iRow, iCol)) Then Exit For
aiCnt(iCol) = aiCnt(iCol) + 1
Next iRow
aiCum(iCol) = aiCnt(iCol) * aiCum(iCol + 1) <------ This is where it says error is
Next iCol
If aiCum(1) > Rows.Count - rOut.Row + 1 Then
MsgBox Prompt:=Format(aiCum(1), "#,##0") & _
" is too many rows!", _
Buttons:=vbOKOnly, Title:=sTitle
Exit Sub
End If
ReDim avOut(1 To aiCum(1), 1 To nCol)
For iArr = 1 To aiCum(1)
For iCol = 1 To nCol
avOut(iArr, iCol) = avInp((Int((iArr - 1) * aiCnt(iCol) / aiCum(iCol))) Mod aiCnt(iCol) + 1, iCol)
Next iCol
Next iArr
With rOut.Resize(aiCum(1), nCol)
.NumberFormat = "#"
.Value = avOut
.Style = "Code"
.Cells(1, 0).Value = 1
.Cells(2, 0).Value = 2
.Cells(1, 0).Resize(2).AutoFill .Columns(0)
End With
ActiveWindow.FreezePanes = False
rOut.EntireColumn.AutoFit
ActiveSheet.UsedRange
Beep
End Sub
Is there away to adjust for this? I also want it to not bring back the same values for a row. So lets say that two columns had the exact same data. If column A has lets say Ice cream, cake, and cookies and so does Column B, I don't want Row 1 to have cookies in column B if it is already picked in Column A.

Resources