Slow search/filter algorithm - excel

My problem is, my current code is pretty slow right now and i would like to make it faster, but don't know how.
I have data sets in rows, which looks like this:
I need to filter/search those values like numbers (for example show all >30). But some of the entries, like 30|32,89 are not numbers. Right now i am checking each value, if it needs to be split, like 30|32,89 in 30 and 32,89 and write all the values in a sheet. So i have a column, where all the values are numbers. With a second column, which saves the original row number, Like this:
After that i use advanced filter to get the data i need. I write it in another column. Using original row numbers to write values from the same original cell only ones, if several of the numbers in that cell meet the search criteria. And to do this, i save all the original data(20 columns and many rows) in a 2D array. Then i take only the values from that array, where the 1st index matches the original row number of the filtered data and write all of the values one buy one in another sheet in a row for each 1st index (this part causes the majority of the slowness). There are 20 values for each 1st index. So at the end i get all the corresponding data for the filtered items shown in one table.
Here is my code for that:
Public Sub numberSearch(srchCol As String, srchValue As String)
Dim sValues As Variant, wRange As Variant
'temp values
cRow = archSh.Range("A1").CurrentRegion.rowS.count
Dim srchCol As String
srchCol = "B"
Dim srchValue As String
srchValue = ">2005"
'------------------
'prepare sheet
shSearch.Cells.Clear
sValues = Application.Transpose(archSh.Range(srchCol & "2", srchCol & cRow))
wRange = archSh.Range("A1").CurrentRegion
shSearch.Range("A1").Value = archSh.Range(srchCol & "1").Value
shSearch.Range("B1").Value = "tst"
shSearch.Range("D1").Value = shSearch.Range("A1").Value
shSearch.Range("E1").Value = shSearch.Range("B1").Value
shSearch.Range("G1").Value = shSearch.Range("A1").Value
shSearch.Range("H1").Value = shSearch.Range("B1").Value
shSearch.Range("D2").Value = srchValue
'----------------------------
'spilt values, make all numeric
Dim i As Long, j As Long, k As Long
Dim tst As Variant, c As Variant
Dim s
i = 2
k = 2
For Each c In sValues
If IsNumeric(c) = True Then
ReDim tst(0 To 0)
tst(0) = c
Else
tst = Split(c, sepa)
End If
For j = 0 To UBound(tst)
shSearch.Range("A" & k + j).Value = tst(j)
shSearch.Range("B" & k + j).Value = i
Next j
i = i + 1
k = k + UBound(tst) - LBound(tst) + 1
Next
'--------------------------------
'filter data
Dim rgData As Range, rgCrit As Range, rgOut As Range
Set rgData = shSearch.Range("A1").CurrentRegion
Set rgCrit = shSearch.Range("D1").CurrentRegion
Set rgOut = shSearch.Range("G1").CurrentRegion
rgData.AdvancedFilter xlFilterCopy, rgCrit, rgOut
'---------------------------------
'write searched data
Dim searchColVal As Variant
searchColVal = Application.Transpose(shSearch.Range("H1:H" & shSearch.Cells(rowS.count, 8).End(xlUp).row))
Dim tempItem As Long
tempItem = 0
k = 4
tmpSh.Range("A4").CurrentRegion.Clear
archSh.Range("A1:T1").Copy tmpSh.Range("A4")
For i = 2 To UBound(searchColVal)
If tempItem <> searchColVal(i) Then
ReDim Preserve filterRow(1 To k - 3)
filterRow(k - 3) = searchColVal(i)
k = k + 1
tempItem = searchColVal(i)
For j = 1 To UBound(wRange, 2)
tmpSh.Cells(k, j).Value = wRange(searchColVal(i), j)
Next j
End If
Next i
'----------------------------------------
End Sub
Can anybody help me with speeding up this mess please? Ty in advance.

You can do this with the Advanced Filter and formula criteria.
We use FILTERXML (available in Excel 2013+) to split the text values.
We also is the ISNUMBER function to exclude the text values from being cast as TRUE by the comparison in the first formula.
And the Advanced Filter has an option to write the results elsewhere
For your example, the two formulas might be:
=AND(ISNUMBER(A9),A9>30)
=OR(FILTERXML("<t><s>" & SUBSTITUTE(A9,"|","</s><s>") & "</s></t>","//s")>30)
Before Filter
After Filter
Or, if you change the criteria in both formulas for >30 to <30
Depending on what you need, you could certainly use VBA to generate the relevant formulas.

This scans down the column, splits the cell value into an array then uses Evaluate to apply the search value.
Public Sub numberSearch2()
Const COL_FILTER = "B"
Const srchValue = ">2005"
Dim wb As Workbook, wsSource As Worksheet, WsTarget, t0 As Single
Dim iRow As Long, iLastRow As Long, iTargetRow As Long
Dim ar As Variant, i As Integer
t0 = timer
Set wb = ThisWorkbook
Set wsSource = wb.Sheets("Sheet2")
Set WsTarget = wb.Sheets("Sheet3")
WsTarget.Cells.Clear
wsSource.Rows(1).EntireRow.Copy WsTarget.Range("A1")
iTargetRow = 2
With wsSource
iLastRow = .Range(COL_FILTER & Rows.Count).End(xlUp).Row
For iRow = 2 To iLastRow
ar = Split(.Cells(iRow, COL_FILTER), "|")
For i = 0 To UBound(ar)
If Evaluate(ar(i) & srchValue) Then
wsSource.Rows(iRow).EntireRow.Copy WsTarget.Cells(iTargetRow, 1)
iTargetRow = iTargetRow + 1
i = UBound(ar) ' exit loop
End If
Next
Next
End With
MsgBox iLastRow - 1 & " rows read " & vbCr & _
iTargetRow - 2 & " rows written", vbInformation, "Completed in " & Int(timer - t0) & " secs"
End Sub

Related

Excel VBA: Update a cell based on conditions

I am not that much familiar in VBA code. I am looking to implement two scenarios using VBA code in excel.
Scenario 1: If the value in the "C" column contains specific text, then replace the corresponding values in the "A" column as below
If the value in C contains "abc" then A= "abc".
If the value in C contains "gec" then A= "GEC".
It should loop from the second row to last non-empty row
A
B
C
Two
abc-def
Thr
gec-vdg
Thr
abc-ghi
Expected Result:
A
B
C
abc
Two
abc-def
gec
Thr
gec-vdg
abc
Thr
abc-ghi
Scenario 2: If the value in the "B" column is "A", then replace all the "A" value in the B column as "Active". If the value in the "B" column is I", then replace all the I value in the B column as inactive.
It should loop from the second row to last non-empty row
A
B
C
abc
A
abc-def
gec
I
gec-vdg
abc
A
abc-ghi
Expected Result:
A
B
C
abc
Active
abc-def
gec
Inactive
gec-vdg
abc
Active
abc-ghi
I know that it is possible by using excel formulas. Wondering, how it can be implemented using vba code in excel.
Usually people on here won't just write code for you, this is more for helping you with your code when your stuck. However I've written one for you based on the information you have provided. I've assumed your cells in column C would always have the hyphen and you always want what's left of the hyphen. If there is no hyphen or the relevant cell in column C is empty then nothing will be put into the relevant cell in column A.
I've put in to turn off ScreenUpdating for the code as I don't know how many rows you have. If it's a lot and you have a lot going on, then we can also turn off Calculation and Events to speed it up more, or run it as an array if it's really slow but I suspect that it won't be an issue.
Paste this into your relevant sheet module and change the sheet name as well as the column that's finding the last row if C isn't the right one:
Sub UpdateCells()
Application.ScreenUpdating = False
Dim i As Long, lRow As Long, ws As Worksheet
Set ws = Sheets("Sheet1") 'Change Sheet1 to your sheet name
lRow = ws.Range("C" & Rows.Count).End(xlUp).Row 'Finds your last row using Column C
With ws
For i = 2 To lRow 'Loop from row 2 to last row
If .Range("B" & i) = "A" Then
.Range("B" & i) = "Active"
ElseIf .Range("B" & i) = "I" Then
.Range("B" & i) = "Inactive"
End If
If .Range("C" & i) <> "" Then
If InStr(.Range("C" & i), "-") > 0 Then 'If current row Column C contains hyphen
.Range("A" & i) = Left(.Range("C" & i), InStr(.Range("C" & i), "-") - 1)
End If
End If
Next i
End With
Application.ScreenUpdating = True
End Sub
Replace Values
Option Explicit
Sub replaceCustom()
' Define constants.
Const wsName As String = "Sheet1"
Const ColumnsAddress As String = "A:C"
Const FirstRow As Long = 2
Dim Contains As Variant: Contains = VBA.Array(3, 1) ' 0-read, 1-write
Const findContainsList As String = "abc,gec" ' read
Const replContainsList As String = "abc,gec" ' write
Dim Equals As Variant: Equals = VBA.Array(2, 2) ' 0-read, 1-write
Const findEqualsList As String = "A,I" ' read
Const replEqualsList As String = "Active,Inactive" ' write
Dim CompareMethod As VbCompareMethod: CompareMethod = vbTextCompare
' Define workbook.
Dim wb As Workbook: Set wb = ThisWorkbook ' Workbook containing this code.
' Define range.
Dim rng As Range
With wb.Worksheets(wsName).Columns(ColumnsAddress)
Set rng = .Resize(.Worksheet.Rows.Count - FirstRow + 1) _
.Offset(FirstRow - 1).Find( _
What:="*", _
LookIn:=xlFormulas, _
SearchOrder:=xlByRows, _
SearchDirection:=xlPrevious)
If rng Is Nothing Then
Exit Sub
End If
Set rng = .Resize(rng.Row - FirstRow + 1).Offset(FirstRow - 1)
End With
' Write values from range to array.
Dim Data As Variant: Data = rng.Value
' Write lists to arrays.
Dim findCo() As String: findCo = Split(findContainsList, ",")
Dim replCo() As String: replCo = Split(replContainsList, ",")
Dim findEq() As String: findEq = Split(findEqualsList, ",")
Dim replEq() As String: replEq = Split(replEqualsList, ",")
' Modify values in array.
Dim i As Long
Dim n As Long
For i = 1 To UBound(Data, 1)
For n = 0 To UBound(Contains)
If InStr(1, Data(i, Contains(0)), findCo(n), CompareMethod) > 0 Then
Data(i, Contains(1)) = replCo(n)
Exit For
End If
Next n
For n = 0 To UBound(Equals)
If StrComp(Data(i, Equals(0)), findEq(n), CompareMethod) = 0 Then
Data(i, Equals(1)) = replEq(n)
Exit For
End If
Next n
Next i
' Write values from array to range.
rng.Value = Data
End Sub

How to use each value in column 1 to add comment (NOTE) from 2 different columns?

I need a dynamic way to add Note in which cell in my ID column A. However the comments need to use the information from Column B and C. ex: ON 01/13/2020, Anne.
I am not sure how to check how many times each value from column A will appear and use information from column D and B to create the comment (NOTE)..
result I need. All the time the ID number will be the same the comments need to be the same as well.
The code I am using is
Sub Cmt_test()
Sheet1.Range("A2").AddComment "On " & Sheet1.Range("D2") & ", " & Sheet1.Range("B2")
End Sub
I don't know how I can make it dynamic to get the information all the time the same ID appears. Maybe if I use Loop on column A would it be possible that all the time the loop finds the same ID to add the comment using the information from column D and B?
Write Comments to Each Cell in a Column
Option Explicit
Sub addComments()
Const wsName As String = "Sheet1"
Const FirstRow As Long = 2
Const LastRowCol As Long = 1 ' or "A"
Const str1 As String = "On "
Const str2 As String = ", "
Dim Cols As Variant: Cols = Array(1, 2, 4)
Dim wb As Workbook: Set wb = ThisWorkbook
Dim ws As Worksheet: Set ws = wb.Worksheets(wsName)
Dim LastRow: LastRow = ws.Cells(ws.Rows.Count, LastRowCol).End(xlUp).Row
Dim Vals As Variant: ReDim Vals(UBound(Cols))
' Define Source Range.
Dim rng As Range: Set rng = ws.Range(ws.Cells(FirstRow, Cols(0)), _
ws.Cells(LastRow, Cols(0)))
' Write Column Ranges to Arrays.
Dim j As Long
For j = 0 To UBound(Cols)
Vals(j) = rng.Offset(, Cols(j) - Cols(0))
Next j
' Loop through elements (rows) of Source Array
' and write comments to a dictionary.
Dim dict As Object, Curr As Variant, i As Long
Set dict = CreateObject("Scripting.Dictionary")
For i = 1 To UBound(Vals(0))
Curr = Vals(0)(i, 1)
If dict(Curr) <> "" Then
dict(Curr) = dict(Curr) & vbLf & str1 _
& Format(Vals(2)(i, 1), "mm/dd/yyyy") & str2 & Vals(1)(i, 1)
Else
dict(Curr) = str1 _
& Format(Vals(2)(i, 1), "mm/dd/yyyy") & str2 & Vals(1)(i, 1)
End If
Next i
' Write comments from the dictionary to Source Range.
rng.ClearComments
Dim cel As Range
For Each cel In rng.Cells
cel.AddComment dict(cel.Value)
Next cel
End Sub

Excel VBA Debugging

I'm running into a "run time error 1004". I suspect this has something to do with how much data I want my code to process. Currently I am running a 246 column by 30,000 row. What I'm trying to achieve is to consolidate my data into one row item because the current system export the data into individual row as a duplicate for certain data columns. As a result, the data has a ladder/stagger effect where there's duplicate row ID with blank cells in one and data below it.
Example:
Code:
Option Explicit
Sub consolidate()
Const SHEET_NAME = "Archer Search Report"
Const NO_OF_COLS = 101
Dim wb As Workbook, ws As Worksheet
Dim irow As Long, iLastRow As Long, c As Long, count As Long
Set wb = ThisWorkbook
Set ws = wb.Sheets(SHEET_NAME)
iLastRow = ws.Range("A" & Rows.count).End(xlUp).Row
' scan up sheet
For irow = iLastRow - 1 To 2 Step -1
' if same id below
If ws.Cells(irow + 1, 1) = ws.Cells(irow, 1) Then
' scan across
For c = 1 To NO_OF_COLS
' if blank copy from below
If Len(ws.Cells(irow, c)) = 0 Then
ws.Cells(irow, c) = ws.Cells(irow + 1, c)
End If
Next
ws.Rows(irow + 1).Delete
count = count + 1
End If
Next
MsgBox iLastRow - 1 & " rows scanned" & vbCr & _
count & " rows deleted from " & ws.Name, vbInformation
End Sub
I suspect it has to do with the massive amount of data it's running and wanted to see if that is the case. If so, is there an alternative approach? Appreciate the assistance.
Note: I got this awesome code from someone(CDP1802)here and have been using it for years with smaller data set.
Here's a slightly different approach which does not require sorting by id, includes some checking for error values, and does not overwrite any data in the output.
Sub consolidate()
Const SHEET_NAME = "Archer Search Report"
Const NO_OF_COLS = 10 'for example
Dim wb As Workbook, ws As Worksheet, dataIn, dataOut
Dim i As Long, c As Long
Dim dict As Object, id, rwOut As Long, idRow As Long, vIn, vOut, rngData As Range
Set wb = ThisWorkbook
Set ws = wb.Sheets(SHEET_NAME)
Set dict = CreateObject("scripting.dictionary")
Set rngData = ws.Range("A2:A" & ws.Cells(ws.Rows.count, 1).End(xlUp).Row).Resize(, NO_OF_COLS)
dataIn = rngData.Value 'input data as 2D array
ReDim dataOut(1 To UBound(dataIn, 1), 1 To NO_OF_COLS) 'resize "out" to match "in" array size
rwOut = 0 'row counter for "out" array
For i = 1 To UBound(dataIn, 1)
id = dataIn(i, 1) 'id for this "row"
If Not dict.exists(id) Then
'not seen this id before
rwOut = rwOut + 1
dict(id) = rwOut 'add id and row to dictionary
dataOut(rwOut, 1) = id 'add id to "out" array
End If
idRow = dict(id) 'row locator in the "out" array
For c = 2 To NO_OF_COLS
vIn = dataIn(i, c) 'incoming value
vOut = dataOut(idRow, c) 'existing value
'ignore error values, and don't overwrite any existing value in the "out" array
If Not IsError(vIn) Then
If Len(vIn) > 0 And Len(vOut) = 0 Then dataOut(idRow, c) = vIn
End If
Next c
Next i
rngData.Value = dataOut 'replace input data with output array
MsgBox "Got " & rwOut & " unique rows from " & UBound(dataIn, 1)
End Sub

VBA - Certain Data Only Visible After Second Run

This code below does the following:
Copies specific rows from "source" sheet
Pastes the rows in "target" sheet
Does a count of the "types" (Column E) and inserts the count into Column J
The issue I have is by step 3. What the macro is supposed to do is:
Column I, Rows 3 - 5 --> Insert the Column Headings "Defect",
"System", "Script"
Perform a CountIf function of Column E on each of the criteria in
Column I
Output the value (counted number) in Column J, in the respective
rows alongside Column I
For example:
Column I, Row 3 --> Defect
Column J, Row 3 --> Count of the amount of times "Defect" occurs in
Column E
However, what seems to be happening is this:
Column I is populated with the correct criteria
CountIf is performed (what appears to be correctly) and inserts
the values in Column J
As the values are inserted, the criteria in Column I is erased
and all I have left are the number values in Column J
Now if I run the macro a second time, then it performs as expected and I cannot understand why.
Also, there are no "Defect" entries in Column E, so the value is 0. But on the first run, you don't see 0, it's just blank. On the second run, it shows the value 0.
Sub Copy()
Dim xRg As Range, xCell As Range
Dim i As Long, J As Long, K As Long, x As Long, count As Long
Dim y As Workbook
Dim ws1 As Worksheet
Dim element As Variant, myarray As Variant
myarray = Array("Defect", "System", "Script")
i = Worksheets("source").UsedRange.Rows.count
J = Worksheets("target").UsedRange.Rows.count
count = 3
Set y = Workbooks("myWKBK.xlsm")
Set ws1 = y.Sheets("target")
If J = 1 Then
If Application.WorksheetFunction.CountA(Worksheets("target").UsedRange) = 0 Then J = 0
End If
lngLastRow = Cells(Rows.count, "C").End(xlUp).Row
Set xRg = Worksheets("source").Range("E3:E" & lngLastRow & i)
On Error Resume Next
Application.ScreenUpdating = False
With ws1
'Assign name to columns where values will be pasted
.Range("$B$2").Value = "ID"
.Range("$C$2").Value = "Status"
.Range("$D$2").Value = "Description"
.Range("$E$2").Value = "Type"
.Range("$F$2").Value = "Folder"
.Range("$G$2").Value = "Defect ID"
.Range("$I$2").Value = "Type"
.Range("$I$3").Value = "Defect"
.Range("$I$4").Value = "System"
.Range("$I$5").Value = "Script"
.Range("$J$2").Value = "Count"
End With
For Each element In myarray
For K = 1 To xRg.count
If CStr(xRg(K).Value) = element Then
LastRow = ws1.Cells(Rows.count, "B").End(xlUp).Row + 1
xRg(K).EntireRow.Copy Destination:=ws1.Range("A" & LastRow)
J = J + 1
End If
Next
x = Range("E" & Rows.count).End(xlUp).Row
Range("J" & count) = Application.WorksheetFunction.CountIf(Range("E3:E" & x), element)
count = count + 1
Next element
ws1.Columns("B:J").AutoFit
Application.ScreenUpdating = True
End Sub
EDIT:
It's probably well worth mentioning that the below sub on its own works just fine:
Sub CountIf()
Dim element As Variant
Dim myarray As Variant
myarray = Array("Defect", "System", "Script")
Dim count As Long
count = 3
For Each element In myarray
Dim x As Long
x = Range("E" & Rows.count).End(xlUp).Row
Range("J" & count) = Application.WorksheetFunction.CountIf(Range("E3:E" & x), element)
count = count + 1
Next element
End Sub
This function only performs the CountIf on its own and works exactly as expected.
This is a really beautiful part of your code:
Set xRg = Worksheets("source").Range("E3:E" & lngLastRow & i)
It sets correctly the parent worksheet of the Range object, thus VBA knows where to look at. However, for some reasons, it is not always like this. Take a look at these lines:
lngLastRow = Cells(Rows.count, "C").End(xlUp).Row
x = Range("E" & Rows.count).End(xlUp).Row
Range("J" & count) = Application.WorksheetFunction.CountIf(Range("E3:E" & x), element)
where the worksheet is not set. Thus, it takes either the ActiveSheet or the worksheet, in which the code is (if it is in a worksheet and not in a module). Try to rewrite it, following the beautiful part of your code, e.g., defining the worksheet:
With Worksheet("SomeName")
lngLastRow = .Cells(Rows.count, "C").End(xlUp).Row
x = .Range("E" & Rows.count).End(xlUp).Row
.Range("J" & count) = Application.WorksheetFunction.CountIf(Range("E3:E" & x), element)
End With
As a next step to debug, try to remove On Error Resume Next, because it ignores the errors in the applications and may provide false results due to this.

Normalizing Excel Grid Intersection data into a flat list

I am trying to get Excel data, which was mapped using a grid/matrix mapping into a de-normalized for so that i can enter the data into a database.
How do you copy data in a grid from one excel sheet to the other as follow illustrated below.
I was trying something like this... but as you can see, i am far off!
Sub NormaliseList(mySelection As Range)
Dim cell As Range
Dim i As Long
i = 1
For Each cell In mySelection
If cell <> "" Then
Sheets(2).Range("A" & i).Value = cell(cell.Row, 1).Value
Sheets(2).Range("B" & i).Value = cell.Value
Sheets(2).Range("C" & i).Value = cell(1, cell.Column).Value
i = i + 1
Next cell
End Sub
For Reference. I Updated my code..
Simply add the code, assign macro shortcut to the function
Select the range that contains the intersection data (not the row and column data)
Run macro (Beware, sheet 2 will have data added in normalised form)
If there are multiple headings that are needed i figured i would consolidate into one column then perform a "text to columns" after processing.
Sub NormaliseList()
' to run - assign macro shortcut to sub - Select Intersection data (not row and column headings and run)
Dim Rowname, ColumnName, IntValue As String
Dim x, cntr As Integer
Dim test As Boolean
cntr = 0
For x = 1 To Selection.Count
If Selection(x).Value <> "" Then
cntr = cntr + 1
Rowname = ActiveSheet.Cells(Selection.Cells(x).Row, Selection.Column - 1)
ColumnName = ActiveSheet.Cells(Selection.Row - 1, Selection.Cells(x).Column)
IntValue = Selection(x).Value
test = addrecord(Rowname, ColumnName, IntValue, cntr)
End If
Next x
End Sub
Function addrecord(vA, vB, vC As String, rec As Integer) As Boolean
'Make sure that you have a worksheet called "Sheet2"
Sheets("Sheet2").Cells(rec, 1) = vA
Sheets("Sheet2").Cells(rec, 2) = vB
Sheets("Sheet2").Cells(rec, 3) = vC
End Function
I've got two posts, with usable code and downloadable workbook, on doing this in Excel/VBA on my blog:
http://yoursumbuddy.com/data-normalizer
http://yoursumbuddy.com/data-normalizer-the-sql/
Here's the code:
'Arguments
'List: The range to be normalized.
'RepeatingColsCount: The number of columns, starting with the leftmost,
' whose headings remain the same.
'NormalizedColHeader: The column header for the rolled-up category.
'DataColHeader: The column header for the normalized data.
'NewWorkbook: Put the sheet with the data in a new workbook?
'
'NOTE: The data must be in a contiguous range and the
'rows that will be repeated must be to the left,
'with the rows to be normalized to the right.
Sub NormalizeList(List As Excel.Range, RepeatingColsCount As Long, _
NormalizedColHeader As String, DataColHeader As String, _
Optional NewWorkbook As Boolean = False)
Dim FirstNormalizingCol As Long, NormalizingColsCount As Long
Dim ColsToRepeat As Excel.Range, ColsToNormalize As Excel.Range
Dim NormalizedRowsCount As Long
Dim RepeatingList() As String
Dim NormalizedList() As Variant
Dim ListIndex As Long, i As Long, j As Long
Dim wbSource As Excel.Workbook, wbTarget As Excel.Workbook
Dim wsTarget As Excel.Worksheet
With List
'If the normalized list won't fit, you must quit.
If .Rows.Count * (.Columns.Count - RepeatingColsCount) > .Parent.Rows.Count Then
MsgBox "The normalized list will be too many rows.", _
vbExclamation + vbOKOnly, "Sorry"
Exit Sub
End If
'You have the range to be normalized and the count of leftmost rows to be repeated.
'This section uses those arguments to set the two ranges to parse
'and the two corresponding arrays to fill
FirstNormalizingCol = RepeatingColsCount + 1
NormalizingColsCount = .Columns.Count - RepeatingColsCount
Set ColsToRepeat = .Cells(1).Resize(.Rows.Count, RepeatingColsCount)
Set ColsToNormalize = .Cells(1, FirstNormalizingCol).Resize(.Rows.Count, NormalizingColsCount)
NormalizedRowsCount = ColsToNormalize.Columns.Count * .Rows.Count
ReDim RepeatingList(1 To NormalizedRowsCount, 1 To RepeatingColsCount)
ReDim NormalizedList(1 To NormalizedRowsCount, 1 To 2)
End With
'Fill in every i elements of the repeating array with the repeating row labels.
For i = 1 To NormalizedRowsCount Step NormalizingColsCount
ListIndex = ListIndex + 1
For j = 1 To RepeatingColsCount
RepeatingList(i, j) = List.Cells(ListIndex, j).Value2
Next j
Next i
'We stepped over most rows above, so fill in other repeating array elements.
For i = 1 To NormalizedRowsCount
For j = 1 To RepeatingColsCount
If RepeatingList(i, j) = "" Then
RepeatingList(i, j) = RepeatingList(i - 1, j)
End If
Next j
Next i
'Fill in each element of the first dimension of the normalizing array
'with the former column header (which is now another row label) and the data.
With ColsToNormalize
For i = 1 To .Rows.Count
For j = 1 To .Columns.Count
NormalizedList(((i - 1) * NormalizingColsCount) + j, 1) = .Cells(1, j)
NormalizedList(((i - 1) * NormalizingColsCount) + j, 2) = .Cells(i, j)
Next j
Next i
End With
'Put the normal data in the same workbook, or a new one.
If NewWorkbook Then
Set wbTarget = Workbooks.Add
Set wsTarget = wbTarget.Worksheets(1)
Else
Set wbSource = List.Parent.Parent
With wbSource.Worksheets
Set wsTarget = .Add(after:=.Item(.Count))
End With
End If
With wsTarget
'Put the data from the two arrays in the new worksheet.
.Range("A1").Resize(NormalizedRowsCount, RepeatingColsCount) = RepeatingList
.Cells(1, FirstNormalizingCol).Resize(NormalizedRowsCount, 2) = NormalizedList
'At this point there will be repeated header rows, so delete all but one.
.Range("1:" & NormalizingColsCount - 1).EntireRow.Delete
'Add the headers for the new label column and the data column.
.Cells(1, FirstNormalizingCol).Value = NormalizedColHeader
.Cells(1, FirstNormalizingCol + 1).Value = DataColHeader
End With
End Sub
You’d call it like this:
Sub TestIt()
NormalizeList ActiveSheet.UsedRange, 1, "Name", "Count", False
End Sub

Resources