Optimizing excel arrays - vba

I have a very large data set (600,000 rows) of data structured in the following format:
1) There are around 60 products. One is a Total US number, while the others are for Manufacturers and are labled as KMFs. There are also some labeled as PCKGs(but aren't relevant for this question)
2) Each product is located in 60 different markets
3) Each market has 20 different locations
4) I have 12 metrics for which I need to calculate data in the following manner: Total US number - sum(KMFs) for each metric
I have written vba code for this but it is taking too long to run(around 20 minutes) I need to run similar code on at least 20 worksheets. I have tried various methods such as setting screenUpdating etc. to false. Here is my code. I am new to vba coding so I may have missed obvious things. Please let me know anything is unclear. Please help!
Sub beforeRunningCode()
Application.ScreenUpdating = False
Application.DisplayStatusBar = False
Application.Calculation = xlCalculationManual
Application.EnableEvents = False
ActiveSheet.DisplayPageBreaks = False
End Sub
Sub returnToOriginal()
Application.ScreenUpdating = screenUpdateState
Application.DisplayStatusBar = statusBarState
Application.Calculation = calcState
Application.EnableEvents = eventsState
ActiveSheet.DisplayPageBreaks = displayPageBreaksState
End Sub
Function LastRowFunc(Sheet) As Long
LastRowFunc = ActiveWorkbook.Worksheets(Sheet).Range("A2", Worksheets(Sheet).Range("A2").End(xlDown)).Rows.Count
End Function
Function LastColFunc(Sheet) As Long
With ActiveSheet
LastColFunc = ActiveWorkbook.Sheets(Sheet).Cells(1, .Columns.Count).End(xlToLeft).Column
End With
End Function
Sub AOCalculate()
Call beforeRunningCode 'Optimize Excel
Dim LastRow As Long
Dim LastCol As Long
Dim Period As String
Dim Sheet As String
Dim Arr(1 To 16)
Dim Count As Integer
Sheet = "Energy_LS_Bottler"
Period = "2016 WAVE 1 - 3 W/E 05/07"
LastRow = LastRowFunc(Sheet) 'Calculate last row for which data exists
LastCol = LastColFunc(Sheet) 'Calculate last column for which data exists
For Each Location In ActiveWorkbook.Sheets("Locations").Range("D7:D28").Value
For Each Market In ActiveWorkbook.Sheets("Markets").Range("A5:A92").Value
Count = Count + 1
Arr(1) = Market
Arr(2) = "AO"
Arr(3) = Location
Arr(4) = Period
With ActiveWorkbook.Sheets(Sheet) 'Filtering for KMF
.AutoFilterMode = False
.Range(Cells(1, 1), Cells(LastRow, LastCol)).AutoFilter
.Range(Cells(1, 1), Cells(LastRow, LastCol)).AutoFilter field:=17, Criteria1:="=KMF"
.Range(Cells(1, 1), Cells(LastRow, LastCol)).AutoFilter field:=1, Criteria1:=Market
.Range(Cells(1, 1), Cells(LastRow, LastCol)).AutoFilter field:=3, Criteria1:=Location
End With
For k = 5 To 16
Arr(k) = Application.WorksheetFunction.Sum(ActiveWorkbook.Sheets(Sheet).Range(Cells(1, k), Cells(LastRow, k)).SpecialCells(xlCellTypeVisible))
Next k
With ActiveWorkbook.Sheets(Sheet) ' filtering for Total US
.AutoFilterMode = False
.Range(Cells(1, 1), Cells(LastRow, LastCol)).AutoFilter
.Range(Cells(1, 1), Cells(LastRow, LastCol)).AutoFilter field:=17, Criteria1:="=Total US"
.Range(Cells(1, 1), Cells(LastRow, LastCol)).AutoFilter field:=1, Criteria1:=Market
.Range(Cells(1, 1), Cells(LastRow, LastCol)).AutoFilter field:=3, Criteria1:=Location
End With
For k = 5 To 16
Arr(k) = -Arr(k) + Application.WorksheetFunction.Sum(ActiveWorkbook.Sheets(Sheet).Range(Cells(1, k), Cells(LastRow, k)).SpecialCells(xlCellTypeVisible))
Next k
For j = 1 To 16
ActiveWorkbook.Sheets(Sheet).Cells(LastRow + Count, j).Value = Arr(j)
Next j
Erase Arr
Next
Next
ActiveWorkbook.Sheets(Sheet).AutoFilterMode = False
Call returnToOriginal
End Sub
[Edit]: Here is a link to a sample data set https://drive.google.com/file/d/0B3MkGa57h6g_WGl2WWlWekd4NU0/view?usp=sharing

I think that this will work (though I haven't had a chance to test it), and should be a lot faster:
Sub AOCalculate()
Call beforeRunningCode 'Optimize Excel
Dim LastRow As Long
Dim LastCol As Long
Dim Period As String
Dim Sheet As String
Dim Arr() '1 To 2000, 1 To 16)
Dim Count As Integer
Sheet = "Energy_LS_Bottler"
Period = "2016 WAVE 1 - 3 W/E 05/07"
LastRow = LastRowFunc(Sheet) 'Calculate last row for which data exists
LastCol = LastColFunc(Sheet) 'Calculate last column for which data exists
'copy all of the relevant cells to local arrays for speed
Dim Locations(), Markets(), data()
Markets = ActiveWorkbook.Sheets("Markets").Range("A5:A92").Value
Locations = ActiveWorkbook.Sheets("Locations").Range("D7:D28").Value
'(pretty sure the following line needs to localize the Cells() to .Cells())
data = ActiveWorkbook.Sheets(Sheet).Range(Cells(1, 1), Cells(LastRow, LastCol)).Value '**'
ReDim Arr(1 To UBound(Markets, 1) * UBound(Locations, 1), 16)
'make an index of pointers into our accumulation array
Dim counts As New Collection
Dim i As Long, l As Long, m As Long
For l = 1 To UBound(Locations, 1)
Location = Locations(l, 1) '**'
For m = 1 To UBound(Markets, 1)
Market = Markets(m, 1) '**'
i = i + 1
counts.Add i, CStr(Location) & "~" & CStr(Market)
'counts.Add NewAccumArray(Location, Market, Period), CStr(Location) & "~" & CStr(Market)
Arr(i, 1) = Market
Arr(i, 2) = "AO"
Arr(i, 3) = Location
Arr(i, 4) = Period
Next
Next
' go through each row and add it to the appropiate count in the array
Dim r As Long
Dim key As String, idx As Long
For r = 1 To UBound(data, 1)
key = CStr(data(r, 3)) & "~" & CStr(data(r, 1))
If data(r, 17) = "KMF" Then
idx = counts(key)
For k = 5 To 16
Arr(idx, k) = Arr(idx, k) - data(r, k)
Next k
Else
If data(r, 17) = "Total US" Then
idx = counts(key)
For k = 5 To 16
Arr(idx, k) = Arr(idx, k) + data(r, k)
Next k
End If
End If
Next r
' output the results
ActiveWorkbook.Sheets(Sheet).Range(Cells(LastRow + 1, 1), Cells(LastRow + Count, 16)).Value = Arr
ActiveWorkbook.Sheets(Sheet).AutoFilterMode = False
Call returnToOriginal
End Sub
Answering the query "What did I mean by this?"
'(pretty sure the following line needs to localize the Cells() to .Cells())
data = ActiveWorkbook.Sheets(Sheet).Range(Cells(1, 1), Cells(LastRow, LastCol)).Value '**'
The use of Cells(..) here is fundamentally unreliable and broken. this is because Cells(..) is really a shortcut for ActiveSheet.Cells(..) and the Active* properties are inherently slow and unreliable because they can change while the code is running. Worse, this code is assuming that ActiveSheet = Energy_LS_Blotter which is far from certain.
The correct way to write this line would be like this:
data = ActiveWorkbook.Sheets(Sheet).Range( _
ActiveWorkbook.Sheets(Sheet).Cells(1, 1), _
ActiveWorkbook.Sheets(Sheet).Cells(LastRow, LastCol) _
).Value
But that is long, ugly and inconvenient. An easier way would be to use either a Sheet variable, or a With:
With ActiveWorkbook.Sheets(Sheet)
data = .Range(.Cells(1, 1), .Cells(LastRow, LastCol)).Value
End With

Related

Speed Up Macro Extracting Rows from Data using Column to Match

I'm looking for a way to speed up this code as it takes my computer 20-30 minutes to run. It essentially runs through a list of column values in sheet "A" and if It matches a column value in sheet "B" it will pull the entire corresponding row to the sheet "Match".
Sub MatchSheets()
Dim lastRowAF As Integer
Dim lastRowL As Integer
Dim lastRowM As Integer
Dim foundTrue As Boolean
Application.ScreenUpdating = False
lastRowAF = Sheets("FHA").Cells(Sheets("FHA").Rows.Count, "AF").End(xlUp).Row
lastRowL = Sheets("New Construction").Cells(Sheets("New Construction").Rows.Count, "L").End(xlUp).Row
lastRowM = Sheets("Match").Cells(Sheets("Match").Rows.Count, "A").End(xlUp).Row
For i = 1 To lastRowAF
foundTrue = False
For j = 1 To lastRowL
If Sheets("FHA").Cells(i, 32).Value = Sheets("New Construction").Cells(j, 12).Value Then
foundTrue = True
Exit For
End If
Next j
If foundTrue Then
Sheets("FHA").Rows(i).Copy Destination:= _
Sheets("Match").Rows(lastRowM + 1)
lastRowM = lastRowM + 1
End If
Next i
Application.ScreenUpdating = True
End Sub
Collections are optimized for looking values. Using a combination of a Collection and Array is usually the best way to match two list. 20K Rows X 54 Columns (140K Values) took this code 10.87 seconds to copy over on a slow PC.
Sub NewMatchSheets()
Dim t As Double: t = Timer
Const NUM_FHA_COLUMNS As Long = 54, AF As Long = 32
Dim list As Object
Dim key As Variant, data() As Variant, results() As Variant
Dim c As Long, r As Long, count As Long
ReDim results(1 To 50000, 1 To 100)
Set list = CreateObject("System.Collections.ArrayList")
With ThisWorkbook.Worksheets("New Construction")
data = .Range("L1", .Cells(.Rows.count, "L").End(xlUp)).Value
For Each key In data
If key <> "" Then
If Not list.Contains(key) Then list.Add key
End If
Next
End With
With ThisWorkbook.Worksheets("FHA")
data = .Range(.Range("A1").Resize(1, NUM_FHA_COLUMNS), .Cells(.Rows.count, AF).End(xlUp)).Value
For r = 1 To UBound(data)
key = data(r, AF)
If list.Contains(key) Then
count = count + 1
For c = 1 To UBound(data, 2)
results(count, c) = data(r, c)
Next
End If
Next
End With
If count = 0 Then Exit Sub
Application.Calculation = xlCalculationManual
Application.ScreenUpdating = True
With ThisWorkbook.Worksheets("Match")
With .Cells(.Rows.count, "A").End(xlUp)
.Offset(1).Resize(count, NUM_FHA_COLUMNS).Value = results
End With
End With
Application.Calculation = xlCalculationAutomatic
Application.ScreenUpdating = False
Debug.Print Round(Timer - t, 2)
End Sub
use variant arrays:
Sub MatchSheets()
Dim lastRowAF As Long
Dim lastRowL As Long
Dim lastRowM As Long
Application.ScreenUpdating = False
lastRowAF = Sheets("FHA").Cells(Sheets("FHA").Rows.Count, "AF").End(xlUp).Row
lastRowL = Sheets("New Construction").Cells(Sheets("New Construction").Rows.Count, "L").End(xlUp).Row
lastRowM = Sheets("Match").Cells(Sheets("Match").Rows.Count, "A").End(xlUp).Row
Dim FHAArr As Variant
FHAArr = Sheets("FHA").Range(Sheets("FHA").Cells(1, 1), Sheets("FHA").Cells(lastRowAF, Columns.Count).End(xlToLeft)).Value
Dim NewConArr As Variant
NewConArr = Sheets("New Construction").Range(Sheets("New Construction").Cells(1, 12), Sheets("New Construction").Cells(lastRowL, 12)).Value
Dim outarr As Variant
ReDim outarr(1 To UBound(FHAArr, 1), 1 To UBound(FHAArr, 2))
Dim k As Long
k = 0
Dim l As Long
For i = 1 To lastRowAF
For j = 1 To lastRowL
If FHAArr(i, 32) = NewConArr(j, 1) Then
For l = 1 To UBound(FHAArr, 2)
k = k + 1
outarr(k, l) = FHAArr(i, l)
Next l
Exit For
End If
Next j
Next i
Sheets("Match").Cells(lastRowM + 1, 1).Resize(UBound(outarr, 1), UBound(outarr, 2)).Value = outarr
Application.ScreenUpdating = True
End Sub
FHA Worksheet: 2500 rows by 50 columnsNew Construction Worksheet: 500 rows by 1 column LMatch Worksheet: 450 transfers from FMA Elapsed time: 0.13 seconds
Get rid of all the nested loop and work with arrays.
Your narrative seemed to suggest that there might be multiple matches for any one value but your code only looks for a single match then Exit For. I'll work with the latter of the two scenarios.
Sub MatchSheets()
Dim i As Long, j As Long
Dim vFM As Variant, vNC As Variant
Debug.Print Timer
With Worksheets("New Construction")
vNC = .Range(.Cells(1, "L"), _
.Cells(.Rows.Count, "L").End(xlUp)).Value2
End With
With Worksheets("FHA")
vFM = .Range(.Cells(1, "A"), _
.Cells(.Rows.Count, _
.Cells(1, .Columns.Count).End(xlToLeft).Column).End(xlUp)).Value2
End With
ReDim vM(LBound(vFM, 2) To UBound(vFM, 2), 1 To 1)
For i = LBound(vFM, 1) To UBound(vFM, 1)
If Not IsError(Application.Match(vFM(i, 32), vNC, 0)) Then
For j = LBound(vFM, 2) To UBound(vFM, 2)
vM(j, UBound(vM, 2)) = vFM(i, j)
Next j
ReDim Preserve vM(LBound(vFM, 2) To UBound(vFM, 2), LBound(vM, 2) To UBound(vM, 2) + 1)
End If
Next i
With Worksheets("match")
.Cells(.Rows.Count, "A").End(xlUp).Offset(1, 0).Resize(UBound(vM, 2), UBound(vM, 1)) = _
Application.Transpose(vM)
End With
Application.ScreenUpdating = True
Debug.Print Timer
End Sub
Try changing this line:
Sheets("FHA").Rows(i).Copy Destination:= _
Sheets("Match").Rows(lastRowM + 1)
For the following line:
Sheets("Match").Rows(lastRowM + 1).Value for Sheets("FHA").Rows(i).value
If you really need to shave milliseconds, you could also set: lastRowM to be:
lastRowM = Sheets("Match").Cells(Sheets("Match").Rows.Count, "A").End(xlUp).Row + 1
And use:
Sheets("Match").Rows(lastRowM).Value for Sheets("FHA").Rows(i).value
Thus saving you an addition every time you go through that part of the code

Writing an 2D array to a range of cells

I've got an array of a range of cells and I need to write it back to a specific range of cells. My first 2 columns are working as desired when writing back to the new range of cells but the next 2 columns are mirroring column 2 for columns 3 and 4.
Array Range:
1,2,3,4
2,2,3,5
3,4,5,6
will write as:
1,2,2,2
2,2,2,2
3,4,4,4
What I want is:
1,2,3,4
2,2,3,5
3,4,5,6
Dim myRange As Range
Dim scriptDic As Variant
Dim arr As Variant
Dim i As Integer
Dim x As Integer
With ThisWorkbook.Sheets("AGGREGATE")
Set myRange = .Range("H4:K19")
Set scriptDic = CreateObject("Scripting.Dictionary")
arr = myRange.Value
For i = 1 To UBound(arr, 1)
If arr(i, 1) <> "" Then
scriptDic(arr(i, 1)) = scriptDic(arr(i, 1)) + arr(i, 2)
End If
Next
Application.ScreenUpdating = False
.Range("M4:P19").ClearContents
myRange.Range("F1").Resize(scriptDic.Count, 1) = Application.WorksheetFunction.Transpose(scriptDic.keys)
myRange.Range("G1").Resize(scriptDic.Count, 1) = Application.WorksheetFunction.Transpose(scriptDic.items)
myRange.Range("H1").Resize(scriptDic.Count, 1) = Application.WorksheetFunction.Transpose(scriptDic.items)
myRange.Range("I1").Resize(scriptDic.Count, 1) = Application.WorksheetFunction.Transpose(scriptDic.items)
Application.ScreenUpdating = True
End With
I am assuming that it has to do with this section but I'm not very good with dimensional arrays.
For i = 1 To UBound(arr, 1)
If arr(i, 1) <> "" Then
scriptDic(arr(i, 1)) = scriptDic(arr(i, 1)) + arr(i, 2)
End If
Any help would be much appreciated!
For this purpose, I would get rid of the Dictionary, and just use RemoveDuplicates to obtain the unique key values. Then I would use SUMIF to get the desired answers:
Sub test()
Dim numRows As Long
Application.ScreenUpdating = False
With ThisWorkbook.Sheets("AGGREGATE")
'Clear existing contents of column M:P
.Range("M4", .Cells(.Rows.Count, "M").End(xlUp).Offset(0, 3)).ClearContents
'Copy keys to column M
numRows = .Cells(.Rows.Count, "H").End(xlUp).Row - 3
.Range("M4").Resize(numRows, 1).Value = .Range("H4").Resize(numRows, 1).Value
'Generate unique list
.Range("M4").Resize(numRows, 1).RemoveDuplicates Columns:=1, Header:=xlNo
'Calculate answers in column N to P
numRows = .Cells(.Rows.Count, "M").End(xlUp).Row - 3
.Range("N4").Resize(numRows, 3).Formula = "=SUMIF($H:$H,$M4,I:I)"
'Convert formulas to values
.Range("N4").Resize(numRows, 3).Value = .Range("N4").Resize(numRows, 3).Value
End With
Application.ScreenUpdating = True
End Sub

Compare 2 sets of data and paste any missing values on another sheet

So I have a master sheet with 1000+ rows and another sheet that "should" have the same data. however, in reality sometimes some is missing from the master and sometimes some is missing from the query run.
for simplicity purposes let's say the unique ID is in column B. here's my code but it's super slow and it only does a 1-way comparison.
My ideal code would be something that runs a little smoother and gives me the missing data from both the master and the query.
Is there's something wrong with the way I'm asking the question please let me know.
Sub FindMissing()
Dim lastRowE As Integer
Dim lastRowF As Integer
Dim lastRowM As Integer
Dim foundTrue As Boolean
lastRowE = Sheets("Master").Cells(Sheets("Master").Rows.Count, "B").End(xlUp).Row
lastRowF = Sheets("Qry").Cells(Sheets("Qry").Rows.Count, "B").End(xlUp).Row
lastRowM = Sheets("Mismatch").Cells(Sheets("Mismatch").Rows.Count, "B").End(xlUp).Row
For i = 1 To lastRowE
foundTrue = False
For j = 1 To lastRowF
If Sheets("Master").Cells(i, 2).Value = Sheets("Qry").Cells(j, 2).Value Then
foundTrue = True
Exit For
End If
Next j
If Not foundTrue Then
Sheets("Master").Rows(i).Copy Destination:= _
Sheets("Mismatch").Rows(lastRowM + 1)
lastRowM = lastRowM + 1
End If
Next i
End Sub
Don't loop through the cells on the worksheet. Collect all of the values into variant arrays and process in-memory.
Option Explicit
Sub YouSuckAtVBA()
Dim i As Long, mm As Long
Dim valsM As Variant, valsQ As Variant, valsMM As Variant
With Worksheets("Master")
valsM = .Range(.Cells(1, "B"), .Cells(.Rows.Count, "B").End(xlUp)).Value2
End With
With Worksheets("Qry")
valsQ = .Range(.Cells(1, "B"), .Cells(.Rows.Count, "B").End(xlUp)).Value2
End With
ReDim valsMM(1 To (UBound(valsM, 1) + UBound(valsQ, 1)), 1 To 2)
mm = 1
valsMM(mm, 1) = "value"
valsMM(mm, 2) = "missing from"
For i = LBound(valsM, 1) To UBound(valsM, 1)
If IsError(Application.Match(valsM(i, 1), valsQ, 0)) Then
mm = mm + 1
valsMM(mm, 1) = valsM(i, 1)
valsMM(mm, 2) = "qry"
End If
Next i
For i = LBound(valsQ, 1) To UBound(valsQ, 1)
If IsError(Application.Match(valsQ(i, 1), valsM, 0)) Then
mm = mm + 1
valsMM(mm, 1) = valsQ(i, 1)
valsMM(mm, 2) = "master"
End If
Next i
valsMM = helperResizeArray(valsMM, mm)
With Worksheets("Mismatch")
With .Cells(.Rows.Count, "A").End(xlUp).Offset(1, 0)
.Resize(UBound(valsMM, 1), UBound(valsMM, 2)) = valsMM
End With
End With
End Sub
Function helperResizeArray(vals As Variant, x As Long)
Dim arr As Variant, i As Long
ReDim arr(1 To x, 1 To 2)
For i = LBound(arr, 1) To UBound(arr, 1)
arr(i, 1) = vals(i, 1)
arr(i, 2) = vals(i, 2)
Next i
helperResizeArray = arr
End Function
You cannot resize the first rank of a 2D array so I've added a helper function that will resize the results before putting them back into the Mismatch worksheet.

Autofill based on dynamic row

I'm trying to figure out how to start an autofill based on a dynamic range. For each column in the 'starting table' I need to stack them on top of one another. My current code starting at 'LastRow' does not do this. I was hoping LastRow would give me a dynamic Range to autofill from, but instead I get the error,
'Object variable or With block variable not set'
How do I change my code so that '2Move' autofills to the new size of the table, without knowing where it starts? Then repeat the process for '3Move' and '4Move'
Sub shiftingColumns()
Dim sht As Worksheet
Dim LastRow As Range
Set sht = ActiveSheet
Set copyRange = Sheets("Sheet1").Range(Range("A2:B2"), Range("A2:B2").End(xlDown))
'Insert column & add header
Columns("C:C").Insert Shift:=xlToRight, CopyOrigin:=xlFormatFromLeftOrAbove
Range("C1").Value = "Category"
'Move D1 Value to C2
Range("D1").Cut Destination:=Range("C2")
'Autofill C2 value to current table size
Range("C2").AutoFill Destination:=Range("C2:C" & Range("A" & Rows.Count).End(xlUp).Row)
'Copy copyRange below itself
copyRange.Copy Destination:=Range("A" & Rows.Count).End(xlUp).Offset(1)
'Move E1 below autofilled ranged
Range("E1").Cut Destination:=Range("C" & Rows.Count).End(xlUp).Offset(1)
'LastRow = sht.Cells(sht.Rows.Count, "C").End(xlUp).Row
'LastRow.AutoFill Destination:=Range(LastRow & Range("A" & Rows.Count).End(xlUp).Row)
End Sub
This is the starting table
This is the desired table
For the benefit of those finding this via search engine, what you're looking to do isn't anything like autofill.
This should work for you, a loop.
Sub test()
workingSheet = ActiveSheet.Name
newSheet = "New Sheet"
On Error Resume Next
Application.DisplayAlerts = False
Sheets(newSheet).Delete
Application.DisplayAlerts = True
Sheets.Add.Name = newSheet
Cells(1, 1) = "ID"
Cells(1, 2) = "Color"
Cells(1, 3) = "Category"
On Error GoTo 0
Sheets(workingSheet).Activate
'Get last column
x = Cells(1, 3).End(xlToRight).Column
y = Cells(1, 1).End(xlDown).Row
'Loop for each column from 3 (column "C") and after
For i = 3 To x
With Sheets(newSheet)
newRow = .Cells(Rows.Count, 1).End(xlUp).Row + 1
'Copy ID and Color
Range(Cells(2, 1), Cells(y, 2)).Copy .Range(.Cells(newRow, 1), .Cells(newRow + y, 2))
'Copy column header
.Range(.Cells(newRow, 3), .Cells(newRow + (y - 2), 3)) = Cells(1, i)
'Copy column values
Range(Cells(2, i), Cells(y, i)).Copy .Range(.Cells(newRow, 4), .Cells(newRow + y, 4))
End With
Next
End Sub
If your demands vary, such as adding other "fixed" columns like ID and Color then you'll have to change the cell addressing and such.
These two method will transpose the data much faster than Range.Copy and Range.Paste.
PivotTableValues - dumps the Range.Value into an array, data, then fills a second array, results, with the transposed values. Note: Transposed in this context simply means moved to a different place.
PivotTableValues2 - uses Arraylists to accomplish the OP's goals. Although it works great, it is somewhat a farcical answer. I just wanted to try this approach for esoteric reasons.
PivotTableValues Using Arrays
Sub PivotTableValues()
Const FIXED_COLUMN_COUNT As Long = 2
Dim ArrayRowCount As Long, Count As Long, ColumnCount As Long, RowCount As Long, x As Long, y As Long, y2 As Long
Dim data As Variant, results As Variant, v As Variant
With ThisWorkbook.Worksheets("Sheet1")
RowCount = .Range("A" & .Rows.Count).End(xlUp).Row
ColumnCount = .Cells(1, .Columns.Count).End(xlToLeft).Column
data = Range(.Range("A" & .Rows.Count).End(xlUp), .Cells(1, .Columns.Count).End(xlToLeft)).Value
ArrayRowCount = (ColumnCount - FIXED_COLUMN_COUNT) * (RowCount - 1) + 1
ReDim results(1 To ArrayRowCount, 1 To FIXED_COLUMN_COUNT + 2)
Count = 1
For y = FIXED_COLUMN_COUNT + 1 To ColumnCount
For x = 2 To RowCount
Count = Count + 1
results(Count, FIXED_COLUMN_COUNT + 1) = data(1, y)
results(Count, FIXED_COLUMN_COUNT + 2) = data(x, y)
For y2 = 1 To FIXED_COLUMN_COUNT
If Count = 2 Then
results(1, y2) = data(1, y2)
results(1, y2 + 1) = "Category"
results(1, y2 + 2) = "Value"
End If
results(Count, y2) = data(x, y2)
Next
Next
Next
End With
With ThisWorkbook.Worksheets.Add
.Range("A1").Resize(UBound(results), UBound(results, 2)).Value = results
.Columns.AutoFit
End With
End Sub
PivotTableValues2 Using ArrayLists
Sub PivotTableValues2()
Const FIXED_COLUMN_COUNT As Long = 2
Dim ColumnCount As Long, RowCount As Long, x As Long, y As Long
Dim valueList As Object, baseList As Object, results As Variant, v As Variant
Set valueList = CreateObject("System.Collections.ArrayList")
Set baseList = CreateObject("System.Collections.ArrayList")
With ThisWorkbook.Worksheets("Sheet1")
RowCount = .Range("A" & .Rows.Count).End(xlUp).Row
ColumnCount = .Cells(1, .Columns.Count).End(xlToLeft).Column
For x = 1 To RowCount
baseList.Add Application.Transpose(Application.Transpose(Range(.Cells(x, 1), .Cells(x, FIXED_COLUMN_COUNT))))
Next
For y = FIXED_COLUMN_COUNT + 2 To ColumnCount
baseList.AddRange baseList.getRange(1, RowCount - 1)
Next
For y = FIXED_COLUMN_COUNT + 1 To ColumnCount
For x = 2 To RowCount
valueList.Add Array(.Cells(1, y).Value, .Cells(x, y).Value)
Next
Next
End With
results = Application.Transpose(Application.Transpose(baseList.ToArray))
With ThisWorkbook.Worksheets.Add
.Range("A1").Resize(UBound(results), UBound(results, 2)).Value = results
valueList.Insert 0, Array("Category", "Value")
results = Application.Transpose(Application.Transpose(valueList.ToArray))
.Cells(1, FIXED_COLUMN_COUNT + 1).Resize(UBound(results), UBound(results, 2)).Value = results
.Columns.AutoFit
End With
End Sub

Excel Pivot Table - Formatting and Grouping Columns with Dates and Another Column

I have worked with basic pivot tables for a few years, however I am getting stuck on something that I think should be fairly simple to solve (ugh).
I would like to format my pivot table in a specific way.
For example, just say I am using the following data:
Client Name Stage Amount Paid Date Paid
Client A Start $70,000 1/10/2015
Client A Middle $50,000 1/11/2015
Client A End $30,000 1/12/2015
Client B Start $50,000 5/11/2015
Client B Middle $30,000 5/11/2015
Client B End $50,000 5/12/2015
Client C Start $10,000 10/12/2015
Client C Middle $20,000 20/12/2015
Client C End $30,000 30/12/2015
I would like to arrange the pivot table so that it looks like this:
Table Example with Correct Formatting
The only way I can almost get it to work is if it looks like this:
PivotTable Example - Not correct formatting
I really need the formatting to be exactly like picture one.
Thanks for any help you may be able to provide.
Answers in excel steps or in VBA code would be awesome :)
I think you cannot have exactly the output that you want with a pivote table. So i wrote a code which create first a pivot table as close as you want. And then other macro which will create the exact format table as you put in the picture.
1) However you have (it could be easily automated) to replace in your row data:
Start by 1
Middle by 2
End by 3
And your row data tittles should be in Sheet1 and start cell A1
Main sub to call all codes: (all codes have to be in the same module. How hope it can help you.
Sub main()
Call PivotTable
Call FinalTable
Call DeleteRow
Call FormatTable
End Sub
Here is the first code that create the pivot table:
Sub PivotTable()
Dim PTCache As PivotCache
Dim PT As PivotTable
'1.CREATE DATA STORAGE UNIT
Set PTCache = ActiveWorkbook.PivotCaches.Create( _
SourceType:=xlDatabase, _
SourceData:=Range("A1").CurrentRegion)
'2. ADD WORKSHEET
Worksheets.Add
ActiveSheet.Name = "PivotTable1"
'3.CREATE PIVOT TABLE N*1
Set PT = ActiveSheet.PivotTables.Add( _
PivotCache:=PTCache, _
TableDestination:=Range("A3"))
'4. ENUMERATE PREFERENCES FOR PIVOTE TABLE
With PT
.PivotFields("Client Name").Orientation = xlRowField
.PivotFields("Amount Paid").Orientation = xlRowField
.RowAxisLayout xlTabularRow
End With
'MODIFYING DATA FIELD CALCULATION
With PT.PivotFields("Client Name")
.Subtotals(1) = False
End With
With PT.PivotFields("Date Paid")
.Orientation = xlColumnField
.Caption = " Date Paid"
End With
With PT.PivotFields("Stage")
.Orientation = xlDataField
.Caption = " Stage"
.NumberFormat = "[=1]""Start"";[>2]""End"";""Middle"""
End With
With PT.PivotFields("Amount Paid")
.Orientation = xlDataField
.Function = xlSum
.Caption = " Amount Paid"
End With
Range("C4").Select
Selection.Group Start:=True, End:=True, Periods:=Array(False, False, False, _
False, True, False, False)
PT.DisplayErrorString = False
PT.HasAutoFormat = False
PT.PivotSelect "", xlDataAndLabel, True
Selection.Copy
Worksheets.Add
ActiveSheet.Name = "FinalTable"
Range("A1").PasteSpecial xlPasteValuesAndNumberFormats
Rows(1).Delete
Columns("B").Delete
Columns("I").Delete
Columns("H").Delete
End Sub
To format:
Sub FinalTable()
Dim Nextcell As Double
Dim j As Integer
Lastrow = Sheets("FinalTable").Range("A1").SpecialCells(xlCellTypeLastCell).Row
i = 3
Do Until i = Lastrow
NextProcess i, Nextcell, Lastrow, j
For c = 2 To 7
If j = Lastrow Then Exit Do
If IsEmpty(Cells(i, c)) Then
For j = Nextcell - 1 To i Step -1
If Not IsEmpty(Cells(j, c)) And Not IsEmpty(Cells(j - 1, c)) Then
Range(Cells(j, c), Cells(j - 1, c)).Copy Cells(i, c)
Range(Cells(j, c), Cells(j - 1, c)).ClearContents
Exit For
End If
If Not IsEmpty(Cells(j, c)) Then
Cells(j, c).Copy Cells(i, c)
Cells(j, c).ClearContents
Exit For
End If
If Not IsEmpty(Cells(j - 1, c)) Then
Cells(j - 1, c).Copy Cells(i, c)
Cells(j - 1, c).ClearContents
Exit For
End If
Next j
End If
Next c
StepB = Nextcell - i
i = StepB + i
Loop
i = 2
Do
If Application.WorksheetFunction.CountA(Rows(i)) = 0 Then
Rows(i).Delete
i = i - 1
End If
Lastrow = Sheets("FinalTable").Range("A" & Rows.Count).End(xlUp).Row
i = 1 + i
Loop Until i = Lastrow
End Sub
Code to delete the empty rows in your Final Table
Sub DeleteRow()
Dim Lastrow As Long
Dim i As Integer
Lastrow = Sheets("FinalTable").Range("A" & Rows.Count).End(xlUp).Row
i = 2
Do
If Application.WorksheetFunction.CountA(Rows(i)) = 0 Then
Rows(i).Delete
i = i - 1
End If
Lastrow = Sheets("FinalTable").Range("A" & Rows.Count).End(xlUp).Row
i = 1 + i
Loop Until i = Lastrow
End Sub
Code to put border in your final table:
Sub FormatTable()
Dim Nextcell As Double
Dim j As Integer
Lastrow = Sheets("FinalTable").Range("G" & Rows.Count).End(xlUp).Row
For i = 2 To Lastrow
If Not IsEmpty(Cells(i, 1)) Then
If Not IsEmpty(Cells(i + 1, 1)) Then
Range(Cells(i, 1), Cells(i, 7)).BorderAround
ElseIf Not IsEmpty(Cells(i + 2, 1)) Then
NextProcess i, Nextcell, Lastrow, j
Range(Cells(i, 1), Cells(Nextcell - 1, 7)).BorderAround
Else
Range(Cells(i, 1), Cells(Lastrow, 7)).BorderAround
End If
End If
Range(Cells(1, 2), Cells(Lastrow, 3)).BorderAround
Range(Cells(1, 4), Cells(Lastrow, 5)).BorderAround
Range(Cells(1, 6), Cells(Lastrow, 7)).BorderAround
Next i
End Sub
The subroutine to find the next client name:
Sub NextProcess(ByVal i As Integer, ByRef Nextcell As Double, ByVal Lastrow As Long, ByRef j As Integer)
Dim Found As Boolean
'Dim j As Integer
Found = False
j = i + 1
Do Until Found = True Or Lastrow = j
If Not IsEmpty(Range("A" & j).Value) Then
Nextcell = Cells(j, 1).Row
Found = True
End If
j = j + 1
Loop
End Sub