Multidimensional Matrix Transpose in Excel (Macro) - excel

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

Related

How to split cell contents from multiple columns into rows by delimeter?

The code I have takes cells containing the delimiter (; ) from a column, and creates new rows (everything except the column is duplicated) to separate those values.
What I have
I need this for multiple columns in my data, but I don't want the data to overlap (ex: for 3 columns, I want there to be only one value per row in those 3 columns). It would be ideal if I could select multiple columns instead of only one as my code does now.
What I want
Sub splitByCol()
Dim r As Range, i As Long, ar
Set r = Worksheets("Sheet").Range("J2000").End(xlUp)
Do While r.Row > 1
ar = Split(r.Value, "; ")
If UBound(ar) >= 0 Then r.Value = ar(0)
For i = UBound(ar) To 1 Step -1
r.EntireRow.Copy
r.Offset(1).EntireRow.Insert
r.Offset(1).Value = ar(i)
Next
Set r = r.Offset(-1)
Loop
End Sub
Try this code
Sub Test()
Dim a, x, e, i As Long, ii As Long, iii As Long, k As Long
a = Range("A1").CurrentRegion.Value
ReDim b(1 To 1000, 1 To UBound(a, 2))
For i = LBound(a) To UBound(a)
For ii = 2 To 3
x = Split(a(i, ii), "; ")
For Each e In x
k = k + 1
b(k, 1) = k
b(k, 2) = IIf(ii = 2, e, Empty)
b(k, 3) = IIf(ii = 3, e, Empty)
b(k, 4) = a(i, 4)
Next e
Next ii
Next i
Range("A5").Resize(UBound(b, 1), UBound(b, 2)).Value = b
End Sub
I'd go this way
Sub SplitByCol()
With Worksheets("Sheet")
With .Range("B2", .Cells(.Rows.Count, "B").End(xlUp))
Dim firstColValues As Variant
firstColValues = .Value
Dim secondColValues As Variant
secondColValues = .Offset(, 1).Value
Dim thirdColValues As Variant
thirdColValues = .Offset(, 2).Value
.Offset(, -1).Resize(, 4).ClearContents
End With
Dim iRow As Long
For iRow = LBound(firstColValues) To UBound(firstColValues)
Dim currFirstColValues As Variant
currFirstColValues = Split(firstColValues(iRow, 1), "; ")
Dim currSecondColValues As Variant
currSecondColValues = Split(secondColValues(iRow, 1), "; ")
With .Cells(.Rows.Count, "C").End(xlUp).Offset(1, -1)
With .Resize(UBound(currFirstColValues) + 1)
.Value = currFirstColValues
.Offset(, 2).Value = thirdColValues(iRow, 1)
End With
End With
With .Cells(.Rows.Count, "B").End(xlUp).Offset(1, 1)
With .Resize(UBound(currSecondColValues) + 1)
.Value = currSecondColValues
.Offset(, 1).Value = thirdColValues(iRow, 1)
End With
End With
Next
End With
End Sub
Follow the code step by step by pressing F8 while the cursor is in any code line in the VBA IDE and watch what happens in the Excel user interface
EDIT
adding edited code for a more "parametric" handling by means of a helper function
Sub SplitByCol()
With Worksheets("Sheet")
With .Range("B2", .Cells(.Rows.Count, "B").End(xlUp))
Dim firstColValues As Variant
firstColValues = .Value
Dim secondColValues As Variant
secondColValues = .Offset(, 1).Value
Dim thirdColValues As Variant
thirdColValues = .Offset(, 2).Value
.Offset(, -1).Resize(, 4).ClearContents
End With
Dim iRow As Long
For iRow = LBound(firstColValues) To UBound(firstColValues)
Dim currFirstColValues As Variant
currFirstColValues = Split(firstColValues(iRow, 1), "; ")
Dim currSecondColValues As Variant
currSecondColValues = Split(secondColValues(iRow, 1), "; ")
WriteOne .Cells(.Rows.Count, "C").End(xlUp).Offset(1), _
currFirstColValues, thirdColValues(iRow, 1), _
-1, 2
WriteOne .Cells(.Rows.Count, "B").End(xlUp).Offset(1), _
currSecondColValues, thirdColValues(iRow, 1), _
1, 1
Next
End With
End Sub
Sub WriteOne(refCel As Range, _
currMainColValues As Variant, thirdColValue As Variant, _
mainValuesOffsetFromRefCel As Long, thirdColValuesOffsetFromRefCel As Long)
With refCel.Offset(, mainValuesOffsetFromRefCel)
With .Resize(UBound(currMainColValues) + 1)
.Value = currMainColValues
.Offset(, thirdColValuesOffsetFromRefCel).Value = thirdColValue
End With
End With
End Sub
Please, use the next code. It uses arrays and should be very fast for big ranges to be processed, working mostly in memory:
Sub testSplitInsert()
Dim sh As Worksheet, lastR As Long, arr, arrSp, arrFin, i As Long, j As Long, k As Long
Set sh = ActiveSheet
lastR = sh.Range("B" & sh.rows.count).End(xlUp).row
arr = sh.Range("B1:D" & lastR).Value
ReDim arrFin(1 To UBound(arr) * 10, 1 To 3) 'maximum to keep max 10 rows per each case
k = 1 'initialize the variable to load the final array
For i = 1 To UBound(arr)
arrSp = Split(Replace(arr(i, 1)," ",""), ";") 'trim for the case when somebody used Red;Blue, instead of Red; Blue
For j = 0 To UBound(arrSp)
arrFin(k, 1) = arrSp(j): arrFin(k, 3) = arr(i, 3): k = k + 1
Next j
arrSp = Split(Replace(arr(i, 1)," ",""), ";")
For j = 0 To UBound(arrSp)
arrFin(k, 2) = arrSp(j): arrFin(k, 3) = arr(i, 3): k = k + 1
Next j
Next
sh.Range("G1").Resize(k - 1, 3).Value = arrFin
End Sub
It processes the range in columns "B:D" and returns the result in columns "G:I". It can be easily adapted to process any columns range and return even overwriting the existing range, but this should be done only after checking that it return what you need...

How to get code to correctly count items (a variable) from one spreadsheet and successfully display this information?

I need my VBA code to count all the "x's" on a certain spreadsheet(pc) and then transfer this information to a report (rp) I am creating to display all the individuals choices. At the moment the code identifies all the ID on the sheet but however only acknowledges the first 4 options for each individual, where as some have much more than this. Throughout the course of this code I have made edits to options from Column K to Y and I assume this is the reason why the code is only acknowledging the options that haven't been altered. I have made adaptions to the code but have no idea how to correct this so that all options are successfully displayed.
Any help would be greatly appreciated!
Specific Spreadsheet Code will Read from
Code Report Results
rp.Cells(1, 1) = "Modules"
rp.Cells(1, 2) = "Student Count"
rp.Cells(1, 4) = "Students registered"
rp.Cells(1, 10) = "Students registered2" 'new
nRow = 2
For c = 2 To pc.Cells(1, Columns.Count).End(xlToLeft).Column
rp.Cells(nRow, 1) = pc.Cells(1, c)
rp.Cells(nRow, 2) = WorksheetFunction.CountIf(pc.Columns(c), "x")
nRow = nRow + 1
Next c
rp.Cells(1, 1).CurrentRegion.Borders.LineStyle = xlContinuous
If rp.Cells(2, 4).Text <> "" Then
rp.Cells(1, 4).CurrentRegion.Borders.LineStyle = xlContinuous
End If
rp.Rows(1).Font.Bold = True
rp.UsedRange.Columns.AutoFit
Although your code snippet is not sufficient to determine the cause of your problem you would definitely gain by not interacting with the sheet when manipulating data. consider the example hereunder as an alternative approach:
Option Explicit
Sub consolidate()
Dim arr, arrH
With Sheet1
arr = .Range("A1").CurrentRegion.Offset(1, 0).Value2 'get all data in memory
arrH = .Range(.Cells(1, 1), .Cells(1, UBound(arr, 2))).Value2 'get the header in an array
End With
Dim j As Long, i As Long, ii As Long: ii = 1
Dim arrC: ReDim arrC(1 To 1, 1 To UBound(arrH, 2)) '=> setup counter array
Dim arr2: ReDim arr2(1 To UBound(arr), 1 To UBound(arr, 2)) '=> setup new array to modify source data
For j = 1 To UBound(arr) 'traverse rows
For i = 1 To UBound(arr, 2) 'traverse columns
'here we can access each cell by referencing our array(<rowCounter>, <columnCounter>
'e.g. arr(j,i) => if j = 1 and i = 1 we'll have the values of Cell A1
'we can dump these values anywhere in the activesheet, other sheet, other workbook, ..
'but to limit the number of interactions with our sheet object we can also use an intermediant arrays
If arr(j, i) <> "" Then 'check if x
arr2(j, ii) = arrH(1, i) 'replace x with the value from the header
arr2(j, 1) = arr(j, 1) 'force the value in col1
ii = ii + 1 'increment consolidated counter
arrC(1, i) = arrC(1, i) + 1 'increment sum
End If
Next i
ii = 1 'reset consolidated counter for next line
Next j
'when we are ready with our data we dumb to the sheet
With Sheet2 'the with allows us the re-use the sheet name without typing it again
'the ubound function allows us to size the "range" to the same size as our array, once that's done we can just dumb it to the sheet
.Range(.Cells(1, 1), .Cells(UBound(arrH, 2), 1)).Value2 = Application.WorksheetFunction.Transpose(arrH) 'transpose to get the col's in rows
.Range(.Cells(1, 2), .Cells(UBound(arrC, 2), 2)).Value2 = Application.WorksheetFunction.Transpose(arrC)
.Range(.Cells(1, 4), .Cells(UBound(arr2), UBound(arr2, 2) + 3)).Value2 = arr2
End With
End Sub

How to chose specific rows in worksheet

I have a very big excel file and i want to transfer all information from worksheet to the variant variable.
I don't need all the rows from the file, so I want to chose rows that I am interested in.
I have tried to make complex Range variable using Union to select rows that i am interested in.
The problem is that my program doesn't increase range if useful inormation is divided by the not wanted rows.
example:
I have got table like this:
123|1|1|1
123|2|2|2
456|3|3|3
123|4|4|4
I want rows with 123 in the first column, but then i am using Union function, I got only first two rows, but not the fourth.
I need:
123|1|1|1
123|2|2|2
123|4|4|4
but recieve:
123|1|1|1
123|2|2|2
Below will be a part of my code. This part is in the cycle
r - Range
WS - Worksheet
Set r = WS.Range("A1:A1")
Can somebody help me with this. I am looking for a solution for hour already.
If WS.Cells(i, 1).Value = "123" Then
If r.Columns.Count() < 2 Then
Set r = WS.Range(WS.Cells(i, 1), WS.Cells(i, 4))
Else
Set r = Union(r, WS.Range(WS.Cells(i, 1), WS.Cells(i, 4)))
End If
End If
This works, using your approach:
Sub x()
Dim r As Range, ws As Worksheet, i As Long
Dim j As Long
Set ws = ActiveSheet
Set r = ws.Range("A1")
For i = 1 To 4
If ws.Cells(i, 1).Value = 123 Then
If r.Columns.Count < 2 Then
Set r = ws.Range(ws.Cells(i, 1), ws.Cells(i, 4))
Else
Set r = Union(r, ws.Range(ws.Cells(i, 1), ws.Cells(i, 4)))
End If
End If
Next i
For j = 1 To r.Areas.Count
Range("G" & Rows.Count).End(xlUp)(2).Resize(r.Areas(j).Rows.Count, r.Areas(j).Columns.Count).Value = r.Areas(j).Value
Next j
End Sub
Using an array approach, the results are stored in v2.
Sub x()
Dim ws As Worksheet, i As Long, j As Long, v As Variant, v2() As Variant
v = Range("A1:D4").Value
ReDim Preserve v2(1 To UBound(v, 1), 1 To UBound(v, 2))
For i = LBound(v, 1) To UBound(v, 1)
If v(i, 1) = 123 Then
j = j + 1
v2(j, 1) = v(i, 1)
v2(j, 2) = v(i, 2)
v2(j, 3) = v(i, 3)
v2(j, 4) = v(i, 4)
End If
Next i
Range("G1").Resize(j, UBound(v2, 2)).Value = v2
End Sub

Combine duplicate rows and sum the values using dictionary

I have a table as shown below,based on yellow highlighted column i need to sum green highlighted columns.
Expected output is here:
I have done it using the below code …
Sub test()
lrow = ActiveSheet.Cells(ActiveSheet.Cells.Rows.Count, 1).End(xlUp).Row
Set Rng = Range("A2:A" & lrow)
For Each cell In Rng
If Not IsEmpty(cell) Then
a = cell
b = cell.Offset(0, 1)
c = cell.Offset(0, 5)
r = cell.Row
cnt = Application.WorksheetFunction.CountIf(Rng, cell)
d = 0
For i = 1 To cnt
If Cells(r + i, 1) = a And Cells(r + i, 2) = b And Cells(r + i, 6) Then
Cells(r, 7) = Cells(r + i, 7) + Cells(r, 7)
Cells(r, 8) = Cells(r + i, 8) + Cells(r, 8)
d = d + 1
End If
Next
If d > 0 Then Range(Cells(r + 1, 1).Address, Cells(r + d, 1).Address).EntireRow.Delete
End If
Next
End Sub
I want to do it using scripting dictionary, which is new for me. Since I'm a beginner, I'm unable to modify the below example code found in net!!
Got it from here
Sub MG02Sep59()
Dim Rng As Range, Dn As Range, n As Long, nRng As Range
Set Rng = Range(Range("A2"), Range("A" & Rows.Count).End(xlUp))
With CreateObject("scripting.dictionary")
.CompareMode = vbTextCompare
For Each Dn In Rng
If Not .Exists(Dn.Value) Then
.Add Dn.Value, Dn
Else
If nRng Is Nothing Then Set nRng = Dn Else Set nRng = Union(nRng, Dn)
.Item(Dn.Value).Offset(, 3) = .Item(Dn.Value).Offset(, 3) + Dn.Offset(, 3)
End If
Next
If Not nRng Is Nothing Then nRng.EntireRow.Delete
End With
End Sub
Can anyone help me out? with some notes if possible.
this is how I would do it:
Option Explicit
Sub Test()
Dim ws As Worksheet
Dim arrData As Variant
Dim i As Long, ConcatenateStr As String, Sum1 As Currency, Sum2 As Currency
Dim DictSum1 As Scripting.Dictionary 'You need the Microsoft Scripting Runtime reference for this to work
Dim DictSum2 As Scripting.Dictionary
Set ws = ThisWorkbook.Sheets("SheetName") 'Change this to fit your sheet name
Set DictSum1 = New Scripting.Dictionary 'This is how you initialize your dictionary
Set DictSum2 = New Scripting.Dictionary
'Store everything on your sheet into the array
arrData = ws.UsedRange.Value 'this will get from A1 till ctrl+end cell I'd delete rows and columns that are blank
'Loop through the array to fill the dictionary
For i = 2 To UBound(arrData) '2 because row 1 are headers, UBound is the function to get the last item of your array like .count
If arrData(i, 1) = vbNullString Then Exit For 'this will end the loop once finding an empty value on column A
ConcatenateStr = arrData(i, 1) & arrData(i, 2) & arrData(i, 3) & arrData(i, 6) 'this is to work cleaner, each number is the number of the column concatenated
Sum1 = arrData(i, 7) 'column Sum 1
Sum2 = arrData(i, 8) 'column Sum 2
If Not DictSum1.Exists(ConcatenateStr) Then 'For the column Sum 1
DictSum1.Add ConcatenateStr, Sum1 'this will add the first item Key = Concatenate String and item = the money value
Else
DictSum1(ConcatenateStr) = DictSum1(ConcatenateStr) + Sum1 'this will sum the existing value on the dictionary + the current value of the loop
End If
If Not DictSum2.Exists(ConcatenateStr) Then 'For the column Sum 2
DictSum2.Add ConcatenateStr, Sum2 'this will add the first item Key = Concatenate String and item = the money value
Else
DictSum2(ConcatenateStr) = DictSum2(ConcatenateStr) + Sum2 'this will sum the existing value on the dictionary + the current value of the loop
End If
Next i
Erase arrData
With ws
.UsedRange.RemoveDuplicates Columns:=Array(1, 2, 3, 6), Header:=xlYes 'Again UsedRange will take everything, Columns as you can see are the ones highlighted in yellow
arrData = .UsedRange.Value 'Store the results of deleting all the duplicates
For i = 2 To UBound(arrData) 'Lets fill the array with the sums
ConcatenateStr = arrData(i, 1) & arrData(i, 2) & arrData(i, 3) & arrData(i, 6)
arrData(i, 8) = DictSum1(ConcatenateStr)
arrData(i, 9) = DictSum2(ConcatenateStr)
Next i
.UsedRange.Value = arrData 'Paste back the array with all the sums
End With
End Sub
I've commented the code, but to learn more about dictionaries check this awesome tutorial

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

Resources