Randomize a set of values without repeating value index - excel

I have a requirement to randomize or shuffle a cet of cells in column A subject to the constraint that no cell remains unchanged.
I am placing the candidate randomization in column C with this code:
Sub ShuffleCutandDeal()
Dim A As Range, C As Range
Dim B As Range, cell As Range
Set A = Range("A1:A24")
Set B = Range("B1:B24")
Set C = Range("C1")
A.Copy C
Randomize
For Each cell In B
cell.Value = Rnd()
Next cell
With ActiveSheet.Sort
.SortFields.Clear
.SortFields.Add Key:=Range("B1:B24") _
, SortOn:=xlSortOnValues, Order:=xlAscending, DataOption:=xlSortNormal
.SetRange Range("B1:C24")
.Header = xlNo
.MatchCase = False
.Orientation = xlTopToBottom
.SortMethod = xlPinYin
.Apply
End With
End Sub
The randomization works, but sometimes I get something like:
When I see that a data item has not moved, I re-run the code until all items have been moved.
It seems to me that this "If at first you don't succeed........." approach is really dumb.
Is there a better way to randomize and insure that all the items have moved in one pass ???
EDIT#1:
Based on iliketocode's comment, I attempted to adapt Tony's approach in this post to VBA:
Sub Tony()
Dim A As Range, C As Range
Dim m As Long, t As Variant, i As Long
Dim wf As WorksheetFunction
Set wf = Application.WorksheetFunction
Set A = Range("A1:A24")
Set C = Range("C1:C24")
A.Copy C
For m = 1 To 22
i = wf.RandBetween(m + 1, 24)
t = C(i)
C(i) = C(m)
C(m) = t
Next m
t = C(23)
C(23) = C(24)
C(24) = t
End Sub
I guess the idea is to:Swap C1 with a random pick between C2 and C24 then
Swap C2 with a random pick between C3 and C24 thenSwap C3 with a random pick between C4 and C24 then................Swap C22 with a random pick between C23 and C24 and finallySwap C23 and C24.
I ran this 1000 times with no unwanted matches appearing.

I had to write my own version of the worksheet's native RANK function in order to compare to the ordinal placement of the randomized value but I think this may be getting close.
Option Explicit
Sub shuffleCutDeal()
Dim i As Long, j As Long, tmp As Variant, vVALs As Variant
With Worksheets("Sheet1")
.Columns("B:D").ClearContents
'get the values from the worksheet
vVALs = .Range(.Cells(1, 1), .Cells(Rows.Count, 1).End(xlUp)).Value2
'add an extra 'column' for random index position ('helper' rank)
ReDim Preserve vVALs(LBound(vVALs, 1) To UBound(vVALs, 1), _
LBound(vVALs, 2) To UBound(vVALs, 2) + 1)
'populate the random index positions
Randomize
For i = LBound(vVALs, 1) To UBound(vVALs, 1)
vVALs(i, 2) = Rnd
Next i
'check for duplicate index postions and re-randomize
Do
Randomize
For i = LBound(vVALs, 1) To UBound(vVALs, 1)
If arrRank(vVALs(i, 2), Application.Index(vVALs, 0, 2)) = i Then
vVALs(i, 2) = Rnd
Exit For
End If
Next i
Loop Until i > UBound(vVALs, 1)
'sort the variant array
For i = LBound(vVALs, 1) + 1 To UBound(vVALs, 1)
For j = LBound(vVALs, 1) To UBound(vVALs, 1) - 1
If vVALs(i, 2) > vVALs(j, 2) Then
tmp = Array(vVALs(i, 1), vVALs(i, 2))
vVALs(i, 1) = vVALs(j, 1)
vVALs(i, 2) = vVALs(j, 2)
vVALs(j, 1) = tmp(0)
vVALs(j, 2) = tmp(1)
End If
Next j
Next i
'[optional] get rid of the 'helper' rank
'ReDim Preserve vVALs(LBound(vVALs, 1) To UBound(vVALs, 1), _
LBound(vVALs, 2) To UBound(vVALs, 2) - 1)
'return the values to the worksheet
.Cells(1, 3).Resize(UBound(vVALs, 1), UBound(vVALs, 2)) = vVALs
End With
End Sub
Function arrRank(val As Variant, vals As Variant, _
Optional ordr As Long = xlDescending)
Dim e As Long, n As Long
If ordr = xlAscending Then
For e = LBound(vals, 1) To UBound(vals, 1)
n = n - CBool(vals(e, 1) <= val)
Next e
Else
For e = LBound(vals, 1) To UBound(vals, 1)
n = n - CBool(vals(e, 1) >= val)
Next e
End If
arrRank = n
End Function
I ran it repeatedly against the original values with a CF rule that highlighted duplicates and never found one.

A permutation which moves everything is called a derangement. A classic result in probability is that the probability of a randomly chosen permutation being a derangement is approximately 1/e (where e = 2.71828... is the natural base). This is roughly 37%. Thus -- generating random permutations until you get a derangement is almost certain to work fairly rapidly. Doing anything otherwise risks introducing subtle biases in the distribution of the derangments generated. Of course, you should have the code itself loop until it succeeds rather than rerunning it yourself.

Related

Trying to group numbers in vba

I'm trying to group a number from E column starting with 1, the result should be like as below:
Column
E I
1 1-52
. 54-56
. 58-59
.
52
54
55
56
58
59
And I start to write like this:
Sub Group_Numbers()
Dim a As Variant, b As Variant
Dim i As Long, k As Long
Range("I1") = Range("E1")
k = 1
a = Range("E1", Range("E" & Rows.Count).End(xlUp)).Value
ReDim b(1 To UBound(a), 1 To 1)
For i = 2 To UBound(a)
If a(i, 1) <> Val(a(i - 1, 1)) + 1 Then
k = k + 1
b(k, 1) = a(i, 1)
Else
b(k, 1) = Split(b(k, 1), "-")(0) & -a(i, 1)
End If
Next i
Range("I2").Resize(l).Value = b
End Sub
However, it prompts an error 9 subscript out of range. Hope to get help right here.
Thanks a lot!
I would do the following
Option Explicit
Public Sub Example()
Dim ws As Worksheet ' define worksheet
Set ws = ThisWorkbook.Worksheets("Sheet1")
Dim LastRow As Long
LastRow = ws.Cells(ws.Rows.Count, "E").End(xlUp).Row
Dim Data() As Variant ' read input data into array
Data = ws.Range("E1", "E" & LastRow).Value2
Dim OutData() As Variant ' define output array
ReDim OutData(1 To UBound(Data, 1), 1 To 1) As Variant
Dim iOut As Long
iOut = 1
Dim StartVal As Long
StartVal = Data(1, 1) ' initialize start value of a group
Dim iRow As Long
For iRow = 2 To UBound(Data, 1) ' loop through values
' check if value is previous value +1
If Data(iRow, 1) <> Data(iRow - 1, 1) + 1 Then
' if not write output from StartVal to previos value
OutData(iOut, 1) = StartVal & "-" & Data(iRow - 1, 1)
iOut = iOut + 1
' and set curent value as new group start
StartVal = Data(iRow, 1)
End If
Next iRow
' close last group
OutData(iOut, 1) = StartVal & "-" & Data(iRow - 1, 1)
' write array back to cells
ws.Range("I1").Resize(RowSize:=iOut).NumberFormat = "#" 'format cells as text so `1-2` does not get converted into date.
ws.Range("I1").Resize(RowSize:=iOut).Value2 = OutData
End Sub
Alternative via Excel's Filter() function (vers. MS 365)
Disposing of the new dynamic array features you can profit from a worksheet-related formula evaluation via a tabular filter upon the data range rows compared with the same range shifted by 1 resulting in an array of endRows numbers. This is the base for a results array which joins start and end values.
The following code allows to define a flexible source range, as the evaluation takes care of the actual start row in the indicated data column.
Example call //edited responding to comment
Sub Grouping()
'0) get data
Dim src As Range
Set src = Sheet1.Range("E1:E59") ' change to your needs
Dim data As Variant
If src.Rows.Count > 1 Then ' is the usual case
data = src.Value2 ' get 2-dim datafield array
Else ' a one liner is the exception
ReDim data(1 To 1, 1 To 1) ' create 2-dim by ReDim
data(1, 1) = Application.Index(src, 1, 1)
End If
'1a)prepare formula evaluation of endRows
Dim EndPattern As String
EndPattern = "=LET(data,$,FILTER(ROW(OFFSET(data,1,0))-" & src.Row & ",ABS(OFFSET(data,1,0)-data)>1))"
EndPattern = Replace(EndPattern, "$", src.Address(False, False))
'1b)evaluate formula
Dim endRows: endRows = src.Parent.Evaluate(EndPattern)
'~~~~~~~~~~~~~~
'2) get results
'~~~~~~~~~~~~~~
Dim results: results = getResults(data, endRows) '<< Help function getResults
'3) write to any target
With Sheet1.Range("I1")
.Resize(UBound(results), 1) = results
End With
End Sub
Help function getResults() //added responding to comment
The usual result of an evaluation is a 1-based 2-dim array with two exceptions code has to provide for:
a) non-findings (which would result only in a returned error value),
b) only a single return value (which is a 1-dim array).
Not enough with these exceptions, the tricky comparison of identical endRows blocks - being shifted by 1 row - makes it necessary to check for the actual last row number if not comprised in endRows. - Imo this might have been the commented issue by #TecLao.
Function getResults(ByRef data, ByRef endRows)
'Purpose: combine value ranges
Dim results As Variant
Dim n As Long: n = UBound(data)
'a) no end row returned by shift-formula evaluation
If IsError(endRows) Then ReDim endRows(1 To 1): endRows(1) = n
'b) one end row found
If Application.WorksheetFunction.CountA(endRows) = 1 Then
ReDim results(1 To IIf(endRows(1) < n, 2, 1), 1 To 1)
'write results
results(1, 1) = "'" & data(1, 1) & "-" & data(endRows(1), 1)
If UBound(results) = 2 Then
results(2, 1) = _
"'" & data(endRows(1) + 1, 1) & _
"-" & _
data(n, 1)
End If
'c) several end rows found
Else
Dim increment As Long
If endRows(UBound(endRows), 1) < n Then increment = 1
'write results
ReDim results(1 To UBound(endRows) + increment, 1 To 1)
results(1, 1) = "'" & data(1, 1) & "-" & data(endRows(1, 1), 1)
Dim i As Long
For i = 2 To UBound(endRows)
results(i, 1) = _
"'" & _
data(endRows(i - 1, 1) + 1, 1) & _
"-" & _
data(endRows(i, 1), 1)
Next
If increment Then
results(i, 1) = "'" & data(endRows(i - 1, 1) + 1, 1) & "-" & data(n, 1)
End If
End If
'function return
getResults = results
End Function

Building an array by skipping blank values

I'm new to VBA and was surprised that there isn't a function to insert elements in an array (my previous question). So I rethought my approach a bit.
On screen I have the following example table 'allActualWeights'. There are a lot of blanks (no weight value) that I want to get rid of (the table is different everytime). So the end result should be 'actualWeights'.
In my code I tried the following:
Option Base 1
Dim allActualWeights
allActualWeights = Range("A6:E29").Value
Dim actualWeights
actualWeights = allActualWeights
For Index = 1 To 24
If allActualWeights(Index, 2) <> 0 Then
ReDim actualWeights(Index, 5)
actualWeights(Index, 1) = allActualWeights(Index, 1)
actualWeights(Index, 2) = allActualWeights(Index, 2)
actualWeights(Index, 3) = allActualWeights(Index, 3)
actualWeights(Index, 4) = allActualWeights(Index, 4)
actualWeights(Index, 5) = allActualWeights(Index, 5)
End If
Next Index
Range("G6:K29") = actualWeights
But I'm not getting the results I hoped for.
What am I doing wrong, or is there a better approach?
Here's one approach:
Sub Tester()
Dim allActualWeights, actualweights(), i As Long, n As Long, c As Long
Dim rngSource As Range
Set rngSource = ActiveSheet.Range("A6:E29")
With rngSource
allActualWeights = .Value
'size the output array # of rows to count of values in ColB
ReDim actualweights(1 To Application.CountA(.Columns(1)), _
1 To .Columns.Count)
End With
n = 1
For i = LBound(allActualWeights, 1) To UBound(allActualWeights, 1)
If Len(allActualWeights(i, 2)) > 0 Then
For c = LBound(allActualWeights, 2) To UBound(allActualWeights, 2)
actualweights(n, c) = allActualWeights(i, c)
Next c
n = n + 1 'next output row
End If
Next i
'put the array on the sheet
Range("G6").Resize(UBound(actualweights, 1), UBound(actualweights, 2)) = actualweights
End Sub
This should do it and is easily maintainable...
Sub ActualWeights()
Dim c&, i&, j&, n&, a, b
With [a6:e29] '<-- allActualWeights
a = .Value2
n = UBound(a) - Application.CountBlank(.Offset(, 1).Resize(, 1))
ReDim b(1 To n, 1 To UBound(a, 2))
For i = 1 To UBound(a)
If a(i, 2) Then
c = c + 1
For j = 1 To UBound(a, 2)
b(c, j) = a(i, j)
Next
End If
Next
.Offset(, 6).Resize(n) = b
End With
End Sub

Multidimensional Matrix Transpose in Excel (Macro)

I have some "multidimensional" data in an Excel spreadsheet that currently look like this below:
I'd like to transform this into rows with multiple columns:
I have tried multiple macros but still can't handle all dimensions to transpose correctly to rows, would be extremely grateful for any help :)
P.
Here's the code which works well without 3rd dimension (sales type):
Sub test()
Dim inputRange As Range, inputRRay As Variant
Dim outputRange As Range, outputRRay() As Variant
Dim outRow As Long, inCol As Long, inRow As Long
Set inputRange = ThisWorkbook.Sheets("Sheet1").Range("A1:AA150")
Set outputRange = ThisWorkbook.Sheets("Sheet2").Range("A1")
inputRRay = inputRange.Value
ReDim outputRRay(1 To (UBound(inputRRay, 1) * UBound(inputRRay, 2)), 1 To 3)
outRow = 0
For inCol = 2 To UBound(inputRRay, 2)
For inRow = 2 To UBound(inputRRay, 1)
If inputRRay(inRow, inCol) <> vbNullString And inputRRay(inRow, inCol) <> 0 Then
outRow = outRow + 1
outputRRay(outRow, 1) = inputRRay(1, inCol)
outputRRay(outRow, 2) = inputRRay(inRow, 1)
outputRRay(outRow, 3) = inputRRay(inRow, inCol)
End If
Next inRow
Next inCol
With outputRange.Resize(1, 3)
.EntireColumn.Clear
.Value = Array("Store", "Product", "QTY")
.Font.FontStyle = "Bold"
End With
With outputRange.Offset(1, 0).Resize(UBound(outputRRay, 1), UBound(outputRRay, 2))
.Value = outputRRay
End With
With outputRange.Parent
With Range(outputRange.Range("a1"), .Cells(.Rows.Count, outputRange.Column).End(xlUp)).Resize(, 3)
.Borders(xlEdgeLeft).LineStyle = xlContinuous
.Borders(xlEdgeTop).LineStyle = xlContinuous
.Borders(xlEdgeBottom).LineStyle = xlContinuous
.Borders(xlEdgeRight).LineStyle = xlContinuous
.Borders(xlInsideVertical).LineStyle = xlContinuous
.Borders(xlInsideHorizontal).LineStyle = xlContinuous
.Columns.AutoFit
End With
End With
End Sub
If you're specifically after a VBA solution, then I think you might be over-complicating your code.
Your range definition looks odd. I don't quite see why you're selecting columns "A" to "AA" when the data is only in the first 7 columns. And the data transfer should simply be a case of looping the rows and then each column to transfer into the output array. The desired code would look something like the below. I've left all the formatting bits out as you can tailor that to however you want it.
It does seem as if this code has been lifted from somewhere else and you've tried to adjust it. That's fine, but it does require you to understand what the original code is doing, and it's nor obvious to me that you have that understanding. You might get more success if you write your code from scratch so that you know where the loops are taking you.
Dim data As Variant
Dim fmt As String
Dim output() As Variant
Dim r As Long, x As Long, i As Long
'Define your range
With Sheet1
data = .Range(.Range("A1"), _
.Range("A" & .Rows.Count).End(xlUp)) _
.Resize(, 7) _
.Value2
End With
'Redim output array based on range size.
'Note the + 1 for a header.
ReDim output(1 To UBound(data, 1) * 6 + 1, 1 To 4)
'Write the header.
output(1, 1) = "Product"
output(1, 2) = "Store"
output(1, 3) = "Sales Type"
output(1, 4) = "Qty"
'Transfer the data to output array.
i = 2 'index of ouput array
For r = 3 To UBound(data, 1)
For x = 0 To 5 'loops the 5 columns in each row
output(i + x, 1) = data(r, 1) 'product
output(i + x, 2) = data(1, IIf(x < 3, 2, 5)) 'store
output(i + x, 3) = data(2, x + 2) 'type
output(i + x, 4) = data(r, x + 2) 'qty
Next
i = i + 6 'increment output index by 6 rows
Next
'Write output to sheet.
Sheet2.Range("A1") _
.Resize(UBound(output, 1), _
UBound(output, 2)) _
.Value = output

VBA Excel - Range in Variant split by content criteria

I have a very large data block in an excel spreadsheet (100,000 rows by 30 columns).
The first column can have one of only six different values (CAT1..CAT6).
I need to split the content in 6 spreadsheets in the same book.
I load the source range in a source variant and split it in target variant, which I write in target sheets.
Code is along this lines:
Sub TestVariant()
Dim a, b, c As Variant
Dim i, j, k As Variant
Worksheets("Sheet1").Activate
a = Worksheets("Sheet1").Range("A1:AD100000").Value
ReDim b(UBound(a, 1), UBound(a, 2))
ReDim c(UBound(a, 1), UBound(a, 2))
j = 1
k = 1
For i = 1 To UBound(a, 1)
Select Case a(i, 1)
Case "CAT01"
b(j, 1) = a(i, 1)
'..
b(j, 30) = a(i, 30)
j = j + 1
Case Else
c(k, 1) = a(i, 1)
'..
c(k, 30) = a(i, 30)
k = k + 1
End Select
Next i
Worksheets("Sheet2").Range("A1").Resize(UBound(b, 1), UBound(b, 2)) = b
Worksheets("Sheet3").Range("A1").Resize(UBound(c, 1), UBound(c, 2)) = c
End Sub
Now for the questions:
Is there a way to copy one "row" at a time from the source variant to the target variant? Something like
b(j,) = a(i,)
Is there a way to simply redim the target variants to the data content (initially I just DIM to match the source but each target variant will obiously have less content than the source
Is there any other approach to the split problem more efficient? (collections? keys?)
Any suggestions will be most appreciated.
Thanks for reading
Cris
a combination of Sort() and Autofilter() methods of Range object should be quite fast:
Option Explicit
Sub TestVariant()
Dim iCat As Long
With Worksheets("Sheet1")
With .Range("AD1", .Cells(.Rows.COUNT, 1).End(xlUp))
.Sort key1:=Range("A1"), order1:=xlAscending, Header:=xlYes ', SortMethod:=xlPinYin, DataOption1:=xlSortNormal, MatchCase:=False, Orientation:=xlTopToBottom
For iCat = 1 To 6
.AutoFilter Field:=1, Criteria1:="CAT0" & iCat '<--| filter its columns A on current "CAT"
If Application.WorksheetFunction.Subtotal(103, .Columns(1).Cells) > 1 Then '<--| if any cell filtered other than header
With .Offset(1).Resize(.Rows.COUNT - 1).SpecialCells(xlCellTypeVisible)
GetWorkSheet("CAT0" & iCat).Range("A1").Resize(.Rows.COUNT, .Columns.COUNT).Value = .Value
End With
End If
Next iCat
End With
.AutoFilterMode = False
End With
End Sub
Function GetWorkSheet(shtName As String) As Worksheet
On Error Resume Next
Set GetWorkSheet = Worksheets(shtName)
If GetWorkSheet Is Nothing Then
Set GetWorkSheet = Worksheets.Add
GetWorkSheet.name = shtName
End If
End Function

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

Resources