I realised that the need to cater for a variable number of variables would make this quite complicated, so I had a go at it in my lunch break.
The following is a complete solution - but it is not tidy!! It also cannot be re-run - if you want to re-run it you have to convert all the tables back to Ranges and delete the Power Queries that have been created.
I've included a couple of my standard helper functions too.
Public Sub SetUpTablesForCrossJoin()
Dim lastColumn As Long, thisColumn As Long, lastRowNo As Long
Dim rng As Range, rng1 As Range
lastColumn = lastCol(shtUserData)
'I used your data, so 1st Column just says "Values" and 1st Row is "Variable Name"
For thisColumn = 2 To lastColumn
Set rng = shtUserData.Columns(thisColumn)
lastRowNo = lastRow(rng)
Set rng1 = Range(rng.Cells(1), rng.Cells(lastRowNo))
'Turn each of the data sets into Excel Tables
shtUserData.ListObjects.Add(xlSrcRange, rng1, , xlYes).Name = "Table" & CStr(thisColumn - 1)
Next
'Now turn each of those tables into a Power Query
Dim PQName As String, colHeader As String
For thisColumn = 2 To lastColumn
PQName = "Table" & CStr(thisColumn - 1)
colHeader = shtUserData.ListObjects(PQName).HeaderRowRange(1, 1)
ThisWorkbook.Queries.Add Name:=PQName, Formula:= _
"let Source = Excel.CurrentWorkbook(){[Name=" & Chr(34) & PQName & Chr(34) & "]}[Content], #""Changed Type"" = " & _
"Table.TransformColumnTypes(Source,{{""" & colHeader & """, type text}}) in #""Changed Type"""
ThisWorkbook.Connections.Add2 "Query - " & PQName, _
"Connection to the '" & PQName & "' query in the workbook.", _
"OLEDB;Provider=Microsoft.Mashup.OleDb.1;Data Source=$Workbook$;Location=" & PQName & ";Extended Properties=""""" _
, "SELECT * FROM [" & PQName & "]", 2
Next
'Now use Power Query to Cross join them all
Dim lfcr As String: lfcr = lfcr
Dim queryFormula As String, addTable As String, expandTable As String
queryFormula = "let" & lfcr & " Source = Table1" & lfcr
addTable = " ,#""Added Custom%s"" = Table.AddColumn(%s, ""Custom"", each %s)"
expandTable = " ,#""Expanded Custom%s"" = Table.ExpandTableColumn(#""%s"", ""Custom"", {""%s""}, {""%s""})"
Dim prevstep As String, prevstep1 As String
For thisColumn = 2 To lastColumn - 1 'N.B. We've already captured Table1
PQName = "Table" & CStr(thisColumn)
colHeader = shtUserData.ListObjects(PQName).HeaderRowRange(1, 1)
prevstep = IIf(thisColumn = 2, "Source", sPrintf("#""Expanded Custom%s""", thisColumn - 2))
prevstep1 = sPrintf("Added Custom%s", thisColumn - 1)
queryFormula = queryFormula & sPrintf(addTable, thisColumn - 1, prevstep, PQName) & _
lfcr & sPrintf(expandTable, thisColumn - 1, prevstep1, colHeader, colHeader) & lfcr
Next
queryFormula = queryFormula & " in" & lfcr & sPrintf(" #""Expanded Custom%s""", thisColumn - 2)
ThisWorkbook.Queries.Add Name:="CrossJoin", Formula:=queryFormula
With shtCrossJoin.ListObjects.Add(SourceType:=0, Source:= _
"OLEDB;Provider=Microsoft.Mashup.OleDb.1;Data Source=$Workbook$;Location=CrossJoin;Extended Properties=""""" _
, Destination:=Range("CrossJoin!$A$1")).QueryTable
.CommandType = xlCmdSql
.CommandText = Array("SELECT * FROM [CrossJoin]")
.ListObject.DisplayName = "CrossJoin"
.Refresh BackgroundQuery:=False
End With
End Sub
Public Function lastCol(rg As Variant) As Long
'Find the last column used in a worksheet - regardless of where the data starts and ends
'and regardless of whether the data is contiguous or jagged
'Note use of Variant for the input range parameter
'This allows both worksheets and ranges to passed into the function
On Error Resume Next
Dim lc As Long
lc = rg.Cells.Find(What:="*" _
, After:=rg.Cells(1, 1) _
, LookAt:=xlPart _
, LookIn:=xlFormulas _
, SearchOrder:=xlByColumns _
, SearchDirection:=xlPrevious).Column
If Err.Number <> 0 Then
lc = 1 'cater for a completely empty sheet
End If
On Error GoTo 0
lastCol = lc
End Function
Public Function lastRow(rg As Variant) As Long
'Find the last row used in a worksheet - regardless of where the data starts and ends
'and regardless of whether the data is contiguous or jagged
'Note use of Variant for the input range parameter
'This allows both worksheets and ranges to passed into the function
On Error Resume Next
Dim lr As Long
lr = rg.Cells.Find(What:="*" _
, LookAt:=xlPart _
, LookIn:=xlFormulas _
, SearchOrder:=xlByRows _
, SearchDirection:=xlPrevious).Row
If Err.Number <> 0 Then
lr = 1 'cater for a completely empty sheet
End If
On Error GoTo 0
lastRow = lr
End Function
Public Function sPrintf(ByVal inputStr As String, ParamArray replaceStrs() As Variant)
'Mimic the C++ sprintf function, replace place holders in a string with an array of values
'This version can handle input strings with %s - the array must be in the correct order
'It also handles the more flexible %1, %2 etc in which case the correct array cell will be used
'Don't be tempted to call this with sPrintf(inputStr, Array("a", "b")) as this will fail.
'Use a single dimension array with sPrintf(inputStr, "a", "b")
Dim i As Long, res As String, bOld As Boolean, sRep As String
bOld = IIf(InStr(1, inputStr, "%s", vbBinaryCompare) > 0, True, False)
res = inputStr
For i = LBound(replaceStrs) To UBound(replaceStrs)
sRep = IIf(bOld, "%s", "%" & CStr(i + 1))
res = Replace(res, sRep, replaceStrs(i), 1, 1, vbBinaryCompare)
Next
sPrintf = res
End Function
Basically the code is converting your data into Excel tables so they can be easily queried with Power Query.
It then creates one Connection-only Power Query for each of those tables (aka Variables)
Then it builds a Cross Join power query "on the fly" to merge all of the table queries into one big cartesian product. That bit could definitely do with a tidy up!