I have a block of data which I define as a range ("ARRAY_DIM") in excel. The range includes a lot of data but also have many rows and columns with no data at all. Below is an example of the defined range. Please note that the number of data columns for each identifier varies which is why the ARRAY_DIM is defined with +100 columns (of which only few rows will contain data).
Banana 10 20 30 40 50 70
Parrot 5 1 4 30
Apple 3 3 5 6 20
Car 10 20 30 40 30
Donkey 4 12 3 0 4 5
Coconut 10 4 0 1
I am inserting all of this data into an array such that I can loop through a list of relevant identifiers and then paste the data associated with the identifiers in the adjacent cells (same row). See below for a simplified example of identifiers (first column is a range defined as "OUTPUT") and where I intend to paste the relevant data for identifiers that are included in the array.
Banana 10 20 30 40 50 70
SHARK
Apple 3 3 5 6 20
Airplane
I am having troubles with accomplishing this task based on the code below. It works fine for the first row/identifier but then I get an error "Subscript out of range" at the .Cells output line. I would appreciate if someone can review the code and maybe point out any errors.
Sub test()
Dim arr As Variant
Dim cell As Range
With ThisWorkbook.Sheets("Sheet1")
arr = .Range("ARRAY_DIM")
End With
With ThisWorkbook.Sheets("Sheet2")
For Each cell In .Range("OUTPUT")
For x = LBound(arr, 1) To UBound(arr, 1)
If arr(x, 1) = cell.Value Then
For n = LBound(arr, 1) To UBound(arr, 1)
.Cells(cell.Row, n + 2) = arr(x, n + 1)
Next n
End If
Next x
Next cell
End With
End Sub
This should handle it, assuming unique labels in the first columns:
Dim data As Object
Dim r As Range
Dim thisName As String
Dim thisData As Range
Set data = CreateObject("Scripting.Dictionary")
With ThisWorkbook.Sheets("Sheet1")
' Store each row in our Dictionary with key=item name, value=row values
For Each r In .Range("ARRAY_DIM").Rows
Set data(r.Cells(1).Value) = r.Resize(1, r.Columns.Count - 1).Offset(0, 1)
Next
End With
With ThisWorkbook.Sheets("Sheet2")
For Each r In .Range("OUTPUT").Columns(1).Cells
thisName = r.Cells(1).Value
' Check if thisName exists in our Dictionary
If data.Exists(thisName) Then
' Dump the data into the row if it exists
Set thisData = data(thisName)
r.Offset(0, 1).Resize(1, thisData.Columns.Count).Value = thisData.Value
End If
Next
End With
But I think that can be further simplified to a single loop:
Dim r As Range
Dim thisName As String
Dim thisData As Range
Dim outputRow As Variant
Dim outputRange as Range
Set outputRange = ThisWorkbook.Sheets("Sheet2").Range("OUTPUT")
With ThisWorkbook.Sheets("Sheet1").Range("ARRAY_DIM")
For Each r In .Rows
thisName = r.Cells(1).Value
' Check whether thisName exists in outputRange
outputRow = Application.Match(thisName, outputRange, False)
If Not IsError(outputRow) Then
' Dump this row's Values to the outputRange
outputRange.Rows(outputRow).Value = r.Value
End If
Next
End With
NB: Neither of the above approaches will add a new row if the thisName isn't found in the OUTPUT range.
Related
I am trying to write a VBA code where I want to populate DATA from a worksheet Range A to AQ spanning over multiple Rows. AQ contains Value "Open" or "Closed". I want to get the rows where AQ value is closed. I tried using the AutoFilter. This is working fine to an extent. But I have to use 2 For loops. One for Each Row and another for Each Column to populate Row wise, column by column into the list box
My Code as follows:
Note : Actual contents start from 6th Row where 6 contains the headers and data starts from 7th Row
Dim i As Long
Dim rowRange As Range
Dim AllData(1 To 1000, 1 To 43) As String
lstRecords.ColumnCount = 43
Set shDSR = mydata1.Sheets("DSR")
last_Row = shDSR.Cells(Rows.Count, 1).End(xlUp).Row
shDSR.AutoFilterMode = False
shDSR.Range("A6:AQ" & last_Row).AutoFilter Field:=43, Criteria1:="CLOSED"
Set rng = shDSR.Range("A6:AQ" & last_Row).SpecialCells(xlCellTypeVisible)
Dim filtrRow() As String
Dim rowCnt As Long
'Me.lstRecords.Clear
rowCnt = 0
If rng.Count > 0 Then
Me.lstRecords.Clear
Me.lstRecords.ColumnCount = rng.Columns.Count
For Each Row In rng.Rows
Me.lstRecords.AddItem
rowCnt = rowCnt +1
filterRow = Range(Row.Address)
'Me.lstRecords.List() = filterRow ''This throws error Type Mismatch so not using
For i = 1 To Row.Columns.Count
AllData(rowCnt, i) = Row.Cells(1, i).Value ''Move to Array
Me.lstRecords.List(rowCnt - 1, i - 1) = filterRow(1, i)'Buggy error when i = 11
Next
Next
'' Following segment works. Add data to Array and then populate ListBox from Array
Me.lstRecords.List() = AllData
Else
MsgBox "No data matches the filter criteria."
End If
Above Code has both approaches
a) Trying to load directly from excel Range (actually using filterRow, but can also directly use range with same issue). But, this approach stops always when i=11 with Invalid property error. I tried changing the data contents etc still same issue
Another Issue when Not taking the array based approach, only one line is added, so in affect only last line is available in the list box
b) Using the AllData array. I load all the row data (matching criteria) into the array and finally populate the listbox from array. THIS WORKS. But I do not like this approach
Can some one please point out where it is going wrong.
Thanks in advance
Problem is that filters create a non contiguous range consisting of areas which you have to iterate separately.
Option Explicit
Sub demo()
Dim mydata1 As Workbook, shDSR As Worksheet
Dim rng As Range, a As Range, r As Range
Dim last_row As Long, n As Long
Dim i As Long, rowCnt As Long
Dim ListData() As String
' change this
Set mydata1 = ThisWorkbook
Set shDSR = mydata1.Sheets("DSR")
With shDSR
.AutoFilterMode = False
last_row = .Cells(.Rows.Count, "AQ").End(xlUp).Row
.Range("A6:AQ" & last_row).AutoFilter Field:=43, Criteria1:="CLOSED"
Set rng = .Range("A6:AQ" & last_row).SpecialCells(xlCellTypeVisible)
.AutoFilterMode = False
End With
' clear listbox
With Me.lstRecords
.Clear
.ColumnCount = rng.Columns.Count
End With
'iterate areas and rows to count visible rows
For Each a In rng.Areas
n = n + a.Rows.Count
Next
rowCnt = 0
If n > 1 Then
' size array
ReDim ListData(1 To n, 1 To rng.Columns.Count)
' fill array
For Each a In rng.Areas
For Each r In a.Rows
rowCnt = rowCnt + 1
For i = 1 To UBound(ListData, 2)
ListData(rowCnt, i) = r.Cells(1, i).Value ''Move to Array
Next
Next
Next
' populate ListBox from Array
Me.lstRecords.List() = ListData
Else
MsgBox "No data matches the filter criteria."
End If
End Sub
I have a dataset that I’m trying to reorient but Excel’s transpose function won’t work for what I’m trying to do. I do want to transpose the original data from rows to columns but instead I’m hoping to stack the data from each row of the original data into a single column in a separate sheet. In other words, if I have an array made up of 3 rows and 3 columns, I want to copy and paste the data so that the 9 cells of the original array become a 1 by 9 array in a separate sheet.
My VBA skills are elementary at best and I’ve attempted to write a macro with a For Next loop but just cannot seem to get it right. Any suggestions?
This code will convert a range of cells into a unidimensional array:
Dim rngSource As Range
Dim rng As Range
Dim MiMatriz() As Integer
Dim MiPos As Integer
Dim i As Integer
Set rngSource = Range("A1").CurrentRegion
ReDim MiMatriz(1 To 1, 1 To rngSource.Cells.Count)
For Each rng In rngSource.Cells
MiPos = (((rng.Row - rngSource.Cells(1, 1).Row + 1) - 1) * rngSource.Columns.Count) + (rng.Column - rngSource.Cells(1, 1).Column + 1)
MiMatriz(1, MiPos) = rng.Value
Next rng
For i = 1 To rngSource.Cells.Count
Debug.Print "Index:(1," & i & ")", MiMatriz(1, i)
Next i
Erase MiMatriz
Set rngSource = Nothing
The output I get when executing this code is:
Index:(1,1) 1
Index:(1,2) 2
Index:(1,3) 3
Index:(1,4) 4
Index:(1,5) 5
Index:(1,6) 6
Index:(1,7) 7
Index:(1,8) 8
Index:(1,9) 9
I'm still new to VBA. This is a new post created as I was not specific enough in the previous one.
My Objective: I would like to delete my data row based on a column of data from another sheet.
I have a sheet of data name: WorkingData.
WorkingData is a list of database with ProductIDs which is under Column A
The productIDs would exist more than once because it is identified by period.
ProductIDs Date
132 30/9/2018
132 30/8/2018
132 30/7/2018
122 30/9/2018
122 30/8/2018
11 30/7/2018
11 30/6/2018
...
Sheets(ID to Exclude)
ProductID
11
23
55
34
.....
I have a external sheet name: IDs to exclude.
In the Sheets("IDs to exclude") under column A, there is a list of IDs to exclude because they are unclean data. However, every month the list will keep adding up so the range has to identify the last row.
This is my code but I can only do it row by row. There are thousands of data entry. please advise thanks!
Sub delete_Ids()
Dim c As Range, MyVals As Range, SrchRng As Range
Dim i As Long, lr1 As Long, lr2 As Long, x
'This is a range containing all the criteria to search for
lr1 = Sheets("WorkingData").Cells(Rows.Count, "A").End(xlUp).Row
Set MyVals = Sheets("ProductIDs to Exclude").Range("A2:A" & lr1)
Set SrchRng = Selection
lr2 = SrchRng.Rows.Count
For i = lr2 To 1 Step -1
For Each c In MyVals
x = InStr(SrchRng(i), c)
If x > 0 Then
SrchRng(i).EntireRow.Delete
Exit For
End If
Next c
Next i
End Sub
To make your code faster:
Read data (column A) into an array ArrIDs
Read IDs to excude into an array ArrExclude
Loop throug the data ArrIDs and
Check if it matches any of the IDs to exclude using the WorksheetFunction.Match method.
Collect all rows that match into a variable RowsToDelete
Delete them at once in the end.
This would be one of the fastest ways I think.
The issue is that every read/write action to a cell takes much time, so we try to reduce them. If you delete each row on its own then you have a lot of write actions. But if you collect them first using Union and delete them at once you only have one write action.
The same applies to reading the cell values. If we reduce it to one read action into the array instead of multiple read actions each time we need to compare a value then this would be much faster.
Option Explicit
Public Sub AlternativeDeleteIDs()
Dim wsData As Worksheet 'define datat sheet
Set wsData = ThisWorkbook.Worksheets("WorkingData")
Dim ArrIDs() As Variant 'read data into array
ArrIDs = wsData.Range("A1", wsData.Cells(wsData.Rows.Count, "A").End(xlUp)).Value
Dim ArrExclude() As Variant 'read IDs to exclude into array
With ThisWorkbook.Worksheets("IDs to exclude")
ArrExclude = .Range("A2", .Cells(.Rows.Count, "A").End(xlUp)).Value
End With
Dim RowsToDelete As Range 'we collect all rows to delete here
Dim MatchedRow As Long
Dim iRow As Long
For iRow = LBound(ArrIDs, 1) + 1 To UBound(ArrIDs, 1) '+1 because of header in data
MatchedRow = 0 'initialize
On Error Resume Next 'next line throws error if ID is not in exclude list
MatchedRow = Application.WorksheetFunction.Match(ArrIDs(iRow, 1), ArrExclude, False)
On Error GoTo 0
If MatchedRow <> 0 Then 'ID was found in exclude list
'mark ID for delete
If RowsToDelete Is Nothing Then
Set RowsToDelete = wsData.Rows(iRow)
Else
Set RowsToDelete = Union(RowsToDelete, wsData.Rows(iRow))
End If
End If
Next iRow
'delete all rows at once
RowsToDelete.Delete
End Sub
So in this code we only have 2 read actions (into array) and 1 write action (delete).
I have the following list on Sheet1:
COLUMN A COLUMNB COLUMN C
1 ADDRESS Services(s) USED VEHICLE(S) USED
2 Address1 Service4 Vehicle1, Vehicle3, Vehicle4
3 Address1 Service3 Vehicle1, Vehicle3, Vehicle4
4 Address2 Service5 Vehicle1, Vehicle2, Vehicle5
5 Address2 Service2 Vehicle1, Vehicle6
6 Address2 Service1, Service2, Service3, Service4, Service5, Service6 Vehicle2, Vehicle5, Vehicle6
7 Address1 Service1, Service2, Service3, Service4, Service5, Service6 Vehicle2, Vehicle3
On Sheet2, I would like the following output in Column B when I enter "Address1" in cell B4
COLUMN A COLUMN B
4 Address1
12 Service1
13 Service2
14 Service3
15 Service4
16 Service5
17 Service6
50 Vehicle1
51 Vehicle2
52 Vehicle3
53 Vehicle4
54 Vehicle5
56 Vehicle6
Worksheet_Change Code ("Sheet2" module)
Private Sub Worksheet_Change(ByVal Target As Range)
' call Function only if modifed cell is in Column "B"
If Not Intersect(Target, Range("B4")) Is Nothing Then
Application.EnableEvents = False
Call FilterAddress(Target.Value)
End If
Application.EnableEvents = True
End Sub
Sub FilterAddress Code (Regular module)
Option Explicit
Sub FilterAddress(FilterVal As String)
Dim LastRow As Long
Dim FilterRng As Range, cell As Range
Dim Dict As Object
'Dim ID
Dim Vehicle As Variant
Dim VehicleArr As Variant
Dim i As Long, j As Long
Dim Service As Variant
Dim ServiceArr As Variant
Dim x As Long, y As Long
Dim My_Range As Range
With Sheets("Sheet1")
' find last row with data in column "A" (Adress)
LastRow = .Cells(.Rows.Count, "A").End(xlUp).Row
Set FilterRng = .Range("A1:C" & LastRow)
.Range("A1").AutoFilter
' AutoFilter "Sheet1" according to value in "Sheet2" in Column B
FilterRng.AutoFilter Field:=1, Criteria1:=FilterVal
Set Dict = CreateObject("Scripting.Dictionary")
' create an array with size up to number of rows >> will resize it later
ReDim ServiceArr(1 To LastRow)
j = 1 ' init array counter
For Each cell In .Range("B2:B" & LastRow).SpecialCells(xlCellTypeVisible)
' read values from cell to array using the Split function
Service = Split(cell.Value, ",")
For i = LBound(Service) To UBound(Service)
Service(i) = Trim(Service(i)) ' remove extra spaces from string
If Not Dict.exists(Service(i)) Then
Dict.Add Service(i), Service(i)
' save Service Name to array >> will use it later for "Bubble-sort" and paste in "Sheet2"
ServiceArr(j) = Service(i)
j = j + 1 ' increment ServiceArr counter
End If
Next i
Next cell
' resize array up to number of actual Service
ReDim Preserve ServiceArr(1 To j - 1)
End With
Dim ServiceTmp As Variant
' Bubble-sort Service Array >> sorts the Service array from smallest to largest
For i = 1 To UBound(ServiceArr) - 1
For j = i + 1 To UBound(ServiceArr)
If ServiceArr(j) < ServiceArr(i) Then
ServiceTmp = ServiceArr(j)
ServiceArr(j) = ServiceArr(i)
ServiceArr(i) = ServiceTmp
End If
Next j
Next i
' now the "fun" part >> paste to "Sheet2"
With Sheets("Sheet2")
.Range("A1").Value = "ADDRESS"
.Range("B4").Value = FilterVal
.Range("C1").Value = "VEHICLE(S) USED"
' clear contents from previous run
.Range("B12:B17").ClearContents
.Range("B12:B" & UBound(ServiceArr) + 11) = WorksheetFunction.Transpose(ServiceArr)
End With
FilterRng.Parent.AutoFilterMode = False
With Sheets("Sheet1")
' find last row with data in column "A" (Adress)
LastRow = .Cells(.Rows.Count, "A").End(xlUp).Row
Set FilterRng = .Range("A1:C" & LastRow)
.Range("A1").AutoFilter
' AutoFilter "Sheet1" according to value in "Sheet2" in Column B
FilterRng.AutoFilter Field:=1, Criteria1:=FilterVal
Set Dict = CreateObject("Scripting.Dictionary")
' create an array with size up to number of rows >> will resize it later
ReDim VehicleArr(1 To LastRow)
y = 1 ' init array counter
For Each cell In .Range("C2:C" & LastRow).SpecialCells(xlCellTypeVisible)
' read values from cell to array using the Split function
Vehicle = Split(cell.Value, ",")
For x = LBound(Vehicle) To UBound(Vehicle)
Vehicle(x) = Trim(Vehicle(x)) ' remove extra spaces from string
If Not Dict.exists(Vehicle(x)) Then
Dict.Add Vehicle(x), Vehicle(x)
' save Vehicle Name to array >> will use it later for "Bubble-sort" and paste in "Sheet2"
VehicleArr(y) = Vehicle(x)
y = y + 1 ' increment VehicleArr counter
End If
Next x
Next cell
' resize array up to number of actual Vehicle
ReDim Preserve VehicleArr(1 To y - 1)
End With
Dim VehicleTmp As Variant
' Bubble-sort Vehicle Array >> sorts the Vehicle array from smallest to largest
For x = 1 To UBound(VehicleArr) - 1
For y = x + 1 To UBound(VehicleArr)
If VehicleArr(y) < VehicleArr(x) Then
VehicleTmp = VehicleArr(y)
VehicleArr(y) = VehicleArr(x)
VehicleArr(x) = VehicleTmp
End If
Next y
Next x
' now the "fun" part >> paste to "Sheet2"
With Sheets("Sheet2")
.Range("A1").Value = "ADDRESS"
.Range("B4").Value = FilterVal
.Range("C1").Value = "VEHICLE(S) USED"
' clear contents from previous run
.Range("B50:B55").ClearContents
.Range("B50:B" & UBound(VehicleArr) + 49) = WorksheetFunction.Transpose(VehicleArr)
End With
FilterRng.Parent.AutoFilterMode = False
End Sub
When I enter "Address1" in cell B4 on Sheet2, I receive the following error:
Runtime error '9':
Subscript out of range
However, if I save the file with B4 populated and close it, then re open the file, I am able to get the macro to work properly when I edit the cell contents to say either Address1 or Address2.
What is causing the "Subscript out of range" message to appear, and how can I change the code to avoid it? Do I need to update the code in Worksheet_Change Code?
I've also noticed that if I delete the contents of cell B4 on Sheet2 I get the following error:
Run-time error'1004':
No cells were found.
Are these two errors related?
The maximum 'j' isn't bounded by the number of rows on the sheet - it's bounded by the number of elements that you can split out of those rows. There's no way to determine before your code executes what size ServiceArr needs to be dimensioned to. That means depending on the data, you'll get intermittent subscript errors in this section:
ReDim ServiceArr(1 To LastRow) '<-- This is only a guess.
j = 1
For Each cell In .Range("B2:B" & LastRow).SpecialCells(xlCellTypeVisible)
Service = Split(cell.Value, ",")
For i = LBound(Service) To UBound(Service)
Service(i) = Trim(Service(i))
If Not Dict.exists(Service(i)) Then
Dict.Add Service(i), Service(i)
ServiceArr(j) = Service(i) '<--Subscript error here if unique elements > LastRow
j = j + 1
End If
Next i
Next cell
The solution is ridiculously easy - get rid of ServiceArr completely. It will always be exactly the same thing as both Dict.Keys and Dict.Values because you're basically keeping a 3rd identical copy of the same data here:
Dict.Add Service(i), Service(i)
ServiceArr(j) = Service(i)
This does almost exactly the same thing as your code, except it gives you a 0 based array instead of a 1 based array:
For Each cell In .Range("B2:B" & LastRow).SpecialCells(xlCellTypeVisible)
Service = Split(cell.Value, ",")
For i = LBound(Service) To UBound(Service)
Service(i) = Trim(Service(i))
If Not Dict.exists(Service(i)) Then
Dict.Add Service(i), Empty
End If
Next i
Next cell
ServiceArr = Dict.Keys
'...
'Adjust this to 0 based.
For i = LBound(ServiceArr) To UBound(ServiceArr)
See #YowE3K's comment for why you get the other error.
Well, just wildly guessing but can you try the following:
Option 1
In stead of:
For i = 1 To UBound(ServiceArr) - 1
For j = i + 1 To UBound(ServiceArr)
Write:
For i = 0 To UBound(ServiceArr) - 1
For j = i + 1 To UBound(ServiceArr)
Option 2
In stead of:
j = 1 ' init array counter
Write:
j = 0 ' init array counter
If nothing works, give information about the line of the error. E.g. once you see the error message, press debug and see on which line is colored in yellow.
example data
I have four columns of data, two of these are names (A and D). One (B) is total work hours, and one (E) is time in training.
Can I write a function which does this:
Writes the value of column E in column C in the right place, i.e. "41" in row 2, "32.8" in row 5 and "24.6" in row 8.
thank you.
i just quickly put something together, but it works, you may need to tweak it to use your sheet name etc....
Private Sub FindNames()
Dim RngArr As Variant
Dim i As Long, j As Long
Dim Rws As Long
Dim FRw As Long
'Sheet1 here is not the tab name, but the CodeName (in VBA its the name not in brackets in project explorer)
RngArr = Sheet1.UsedRange.Value 'get range array
If Not IsArray(RngArr) Then Exit Sub 'either a single cell is used or something is wrong
FRw = Sheet1.UsedRange.Row
Rws = UBound(RngArr, 1) - 1 'get total rows in range minus 1
For i = FRw To FRw + Rws 'loop for the list in D:E
If Not RngArr(i, 4) = vbnulstring Then
For j = FRw To FRw + Rws 'loop for the list in A:B (C)
'if ColD = ColA then ColC = ColE
If RngArr(i, 4) = RngArr(j, 1) Then RngArr(j, 3) = RngArr(i, 5)
Next j
Else
'you could exit the loop here if you list will never have empty spaces to save time although you wont notice
End If
Next i
Sheet1.UsedRange.Value = RngArr 'since we are resizing the original used space we can just dump the results back
End Sub
Hope this helps
Paul S.