VBA: Add single value to single column array - excel

I do not know the size of the array.
The array is created from a column L on sheet "INPUT_MASTERDATA".
This is what I have so far:
With Worksheets("INPUT_MASTERDATA")
arrInputUniqueItems = .Range("L2", .Range("L" & Rows.Count).End(xlUp))
End With
I would like to include the value "x" in the above array.
Sample data from Range L on Worksheet "INPUT_MASTERDATA"
R83711850
1210221340
1210223342
R83711181
R83711931
These all goes into the array just fine. Now I would like to add the value "x". So the array afterwards looks like this:
R83711850
1210221340
1210223342
R83711181
R83711931
x
Any ideas or help is highly appreciated! Thank you in advance.

If you are going to be dynamically adding items to your collection you should consider a better suitable data type like Collection. Array's are not supposed by resized after their dimensions are specified.
So... consider creating an array from range in one go, then loading that into a Collection and then you can add more items to the collection. The reason you want to use the array is to load the entire range into memory is going to be faster than iterating a Range and adding each cell to the collection directly.
Sub Main()
[L1] = "header"
[L2] = "R83711850"
[L3] = "1210221340"
[L4] = "1210223342"
[L5] = "R83711181"
[L6] = "R83711931"
Dim lastRow As Long
lastRow = Range("L" & Rows.Count).End(xlUp).Row
Dim v As Variant
Dim c As New Collection
Dim arr As Variant
arr = Range("L2:L" & lastRow).Value
For Each v In arr
c.Add v
Next
' then if you ever need to add more just add it to the collection
c.Add "new value"
' print to confirm
For Each v In c
Debug.Print v
Next
End Sub
prints
R83711850
1210221340
1210223342
R83711181
R83711931
new value
in the Immediate Window CTRL+G

You can do this easily enough without resorting to further loops and/or other methods.
Simply use a range 1 cell longer than the end of the range of interest, or
Use ReDim on a 1D array (of course you can and should resize arrays)
code 1
Sub Method1_2D()
Dim arrInputUniqueItems
With Worksheets("INPUT_MASTERDATA")
arrInputUniqueItems = .Range("L2", .Range("L" & Rows.Count).End(xlUp).Offset(1, 0))
arrInputUniqueItems(UBound(arrInputUniqueItems), 1) = "X"
End With
End Sub
code 2
Sub Method2_1D()
Dim arrInputUniqueItems
With Worksheets("INPUT_MASTERDATA")
arrInputUniqueItems = Application.Transpose(.Range("L2", .Range("L" & Rows.Count).End(xlUp)))
ReDim Preserve arrInputUniqueItems(UBound(arrInputUniqueItems))
arrInputUniqueItems(UBound(arrInputUniqueItems)) = "X"
End With
End Sub

Related

Excel VBA Searching Through a Large Array

I have an array of ~5,000 unique IDs loaded from a CSV file:
Dim wb As Workbook
Dim idRng As Variant
Set wb = Workbooks.Open(Filename:=ThisWorkbook.path & "\DataSource\ID.csv")
With wb.Sheets(1)
idRng = .Range("A2:A" & .Range("A" & .Rows.Count).End(xlUp).Row).Value2
End With
wb.Close
Alongside this, I also load in ~100,000 rows of data, which contains non-unique IDs with numerous possible duplicates. My aim is to loop through the 100,000 rows and check if the corresponding rows ID is contained within the smaller array, and if so, add the rows data to a collection. Both IDs are stored as Longs. I have completed this using the below:
Dim dataRng As Variant
Set wb = Workbooks.Open(Filename:=ThisWorkbook.path & "\DataSource\data.csv")
With wb.Sheets(1)
dataRng = .Range("A2:H" & .Range("A" & .Rows.Count).End(xlUp).Row).Value2
For i = LBound(dataRng) To UBound(dataRng)
If mUtil.IsInArray(dataRng(i, 1), idRng) Then
'Add object to collection
End If
Next
End With
'mUtil
Public Function IsInArray(v As Variant, arr As Variant) As Boolean
For i = LBound(arr) To UBound(arr)
If arr(i, 1) = v Then
IsInArray = True
Exit Function
End If
Next
IsInArray = False
End Function
Despite this working, as you can imagine iterating through the 5,000 unique IDs 100,000 times can take a fair amount of time, alongside this, the larger file can end up being much bigger.
Is there a more efficient way of performing this task, with the ultimate aim to reduce the run time?
I'd suggest throwing your 5,000 records into a dictionary and then use the Exists method to check to see if it does in fact exist.
Public Sub DictionaryTest()
Dim lngKey As Long, objDict As Object
Set objDict = CreateObject("Scripting.Dictionary")
lngKey = 123456
objDict.Add lngKey, 0
Debug.Print objDict.Exists(lngKey)
End Sub
It absolves you from having to loop over the 5,000 each time AND the power of the search within the dictionary should speed up the process 10 fold.
You can try something as simple as the following. Instead of looping twice, just loop one of them and Match if the item is found in the other array. I just tested with random numbers and just looped the unique values. This would work only if you want the first match. If you want all the matches you need to simply reverse it and loop the 100k non-unique array to the unique one.
What we do is create MatchArr as a Variant and then use that variable for our Application.Match function. If the function finds a match, it returns the row it found it on. If it doesn't find a match it will error, but because we made it a variant it won't stop the code. We simply check if it's an error or not and if it is then we simply move to the next line.
This is what I tried (Change as needed):
EDIT: I've updated to do the loop of the bigger array that needs to be refined.
Sub FindValues()
Dim Arr1, Arr2, MatchArr, i As Long, Col As New Collection
Arr1 = Sheet1.Range("A1:A50").Value
Arr2 = Sheet1.Range("C1:C1000").Value
For i = LBound(Arr2, 1) To UBound(Arr2, 1)
MatchArr = Application.Match(Arr2(i, 1), Arr1, 0)
If Not IsError(MatchArr) Then
Col.Add Arr2(i, 1)
End If
Next i
For i = 1 To Col.Count
Sheet1.Range("E" & i).Value = Col(i)
Next i
End Sub

I need this matching method to skip over blank cells and not include them as a matched value

This code works almost perfectly. The problem is it includes blank cells in its "matched" results. What do I need to change to make this code ignore blank cells? Below I will include an example of what is going on.
Sub MarkMatches()
Const TopLeftCell As String = "A2" ' change to match where your data are
Dim Rng As Range ' data range
Dim FirstRow As Long, FirstClm As Long
Dim Data As Variant ' original data (2-D)
Dim Arr As Variant ' data rearranged (1-D)
Dim Tmp As Variant ' working variable
Dim R As Long, R1 As Long ' row counters
Dim C As Long ' column counter
Dim Count() As String ' match counter
With Range(TopLeftCell)
FirstRow = .Row
FirstClm = .Column
End With
C = Cells(FirstRow, Columns.Count).End(xlToLeft).Column
Set Rng = Range(Cells(FirstRow, FirstClm), _
Cells(Rows.Count, FirstClm).End(xlUp).Offset(0, C - FirstClm))
Data = Rng.Value
ReDim Arr(1 To UBound(Data))
For R = 1 To UBound(Data)
ReDim Tmp(1 To UBound(Data, 2))
For C = 1 To UBound(Data, 2)
Tmp(C) = Data(R, C)
Next C
Arr(R) = Tmp
Next R
ReDim Count(1 To UBound(Arr))
For R = 1 To UBound(Arr) - 1
For R1 = R + 1 To UBound(Arr)
Tmp = 0
For C = 1 To UBound(Arr(R))
If Not IsError(Application.Match(Arr(R)(C), Arr(R1), 0)) Then
Tmp = Tmp + 1
End If
Next C
If Tmp > 0 Then ' change to suit
Tmp = Format(Tmp, "(0)") & ", "
Count(R) = Count(R) & CStr(R1 + FirstRow - 1) & Tmp
Count(R1) = Count(R1) & CStr(R + FirstRow - 1) & Tmp
End If
Next R1
Next R
For R = 1 To UBound(Count)
If Len(Count(R)) Then Count(R) = Left(Count(R), Len(Count(R)) - 2)
Next R
' set the output column here (2 columns right of the last data column)
' to avoid including this column in the evaluation
' it must be blank before a re-run
Set Rng = Rng.Resize(, 1).Offset(0, UBound(Data, 2) + 1)
Rng.Value = Application.Transpose(Count)
End Sub
Thank you #Variatus for the code and help so far!
I tried to work with your original code, but honestly I became very confused. My example below will illustrate some practices that could help (and those who may review your code later, including yourself!). So here's a list of comments:
Always use Option Explicit. Your code may already have this, but I'm listing it here for completeness sake.
Create variable names that describe what data it holds. Your code does a little of this, but some of the variable names are difficult to fit into the logic flow. My idea in coding is always to try and write self-documenting code. That way, it's nearly always clear what the code is trying to accomplish. Then I'll use comment for code blocks where it might be a bit less clear. (Don't fall into the trap of prefixing variable names with a "type" or something; it's ultimately not worth it.)
A clear description of the problem always helps. This is true not only to get help on SO, but also for yourself. My final comment to your post above, asking about the problem description really simplified everything. This includes describing what you want your output to show.
As per the problem description, you need to identify each unique item and keep track of which row you find that item so you can create a report later. A Dictionary is a perfect tool for this. Read up about how to use a Dictionary, but you should be able to follow what this block of code is doing here (even without all the previous declarations):
For Each cell In dataArea.Cells
If Not IsEmpty(cell) Then
If items.Exists(cell.Value) Then
'--- add this row to the list
rowList = items(cell.Value) & "," & cell.Row
items(cell.Value) = rowList
Else
'--- first time adding this value
items.Add cell.Value, cell.Row
End If
End If
Next cell
It's easy to see how the logic of this code follows the description of the problem. After that, it's just a matter of running through each row in the data area and checking each value on that row to see if duplicates exist on any other row. The full example solution is below for you to study and adjust to fit your situation.
Option Explicit
Sub IdentifyMatches()
Dim ws As Worksheet
Set ws = Sheet1
Dim dataArea As Range
Set dataArea = ws.Range("A1:F6")
Dim items As Dictionary
Set items = New Dictionary
'--- build the data set of all unique items, and make a note
' of which row the item appears.
' KEY = cell value
' VALUE = CSV list of row numbers
Dim rowList As String
Dim cell As Range
For Each cell In dataArea.Cells
If Not IsEmpty(cell) Then
If items.Exists(cell.Value) Then
'--- add this row to the list
rowList = items(cell.Value) & "," & cell.Row
items(cell.Value) = rowList
Else
'--- first time adding this value
items.Add cell.Value, cell.Row
End If
End If
Next cell
'--- now work through the data, row by row and make the report
Dim report As String
Dim duplicateCount As Variant
ReDim duplicateCount(1 To dataArea.Rows.Count)
Dim dataRow As Range
For Each dataRow In dataArea.Rows
Erase duplicateCount
ReDim duplicateCount(1 To dataArea.Rows.Count)
Dim rowNumber As Variant
For Each cell In dataRow.Cells
If items.Exists(cell.Value) Then
rowList = items(cell.Value)
Dim rowNumbers As Variant
rowNumbers = Split(rowList, ",")
For Each rowNumber In rowNumbers
If rowNumber <> cell.Row Then
duplicateCount(rowNumber) = duplicateCount(rowNumber) + 1
End If
Next rowNumber
End If
Next cell
report = vbNullString
For rowNumber = 1 To UBound(duplicateCount)
If duplicateCount(rowNumber) > 0 Then
report = report & rowNumber & "(" & duplicateCount(rowNumber) & ")" & ", "
End If
Next rowNumber
'--- display the report in the next column at the end of the data area
If Len(report) > 0 Then
report = Left$(report, Len(report) - 2) 'removes the trailing comma and space
dataRow.Cells(1, dataRow.Columns.Count + 1).Value = report
End If
Next dataRow
End Sub

Fetching Data from PivotTable into Array in VBA (Excel)

i'm trying to fetch the Data from a PivotTable, use it in Array in VBA and then to print it. I'm new to VBA and I've watched a few tutorials, but i can't actually get it.
I've tried with referencing the range of my column with "DataBodyRange", but i always get different errors. "Sheet4" is the sheet where my "PivotTable1" is located. And i need all the data from a column.
Public Sub ReadToArray()
'Range
Dim rg As Range
Set rg = Worksheets("Sheet4").pt("PivotTable1").DataBodyRange
'Dynamic Array
Dim Done As Variant
Done = rg.Value
'Array Values
Debug.Print "i", "Value"
Dim i As Long
For i = LBound(Done) To UBound(Done)
Debug.Print i, Done(i)
Next i
End Sub
The end result is that I want to print out the values for the whole column and use them afterwards.
So I can see a few problems that are causing this. FIrst, to reference a pivot table in a sheet, you need .pivottables() not .pt().
Next, setting an array to have the value from a range like this will give you a 2D array, so you need to loop through it in two dimensions to get all the values. I've added a nested loop using a second iterator, j:
Public Sub ReadToArray()
Dim pt As PivotTable
Dim rg As Range
Set pt = Worksheets("Sheet4").PivotTables("PivotTable1")
Set rg = pt.DataBodyRange
Dim Done As Variant
Done = rg.Value
Debug.Print "i", "Value"
Dim i As Long, j As Long
For i = LBound(Done, 1) To UBound(Done, 1)
For j = LBound(Done, 2) To UBound(Done, 2)
Debug.Print i & ", " & j & ", " & Done(i, j)
Next j
Next i
End Sub

Add line breaks in cell to 1000 rows of data

I am currently using the below code to add a line break to cell data in column C and copy it to column K. I need to apply line breaks to a range of data. I have 1000 rows of data in column C.
Any help will be much appreciated.
Sub Macro
Dim Stem As Variant
Stem = ThisWorkbook.Worksheets ("Sheet1").Range("C2")
Range ("K2").Select
Range("K2").FormulaR1C1 = Stem & Chr(10) & ""
End Sub
Thanks
Try this:
Sub copyAndNewLine()
'copy column C to K
Columns("C").Copy Destination:=Columns("K")
'loop through all cells in K and add new line
For i = 2 To Cells(Rows.Count, "K").End(xlUp).Row
Cells(i, "K").Value = Cells(i, "K").Value & vbCrLf
Next i
End Sub
A couple of things:
Better to early bind your variables than late (better memory
management, take advantage of intellisense, etc.)
Usually best
practice to avoid using "select" if possible.
Your Stem variable is an object (Range Object) and thus needs to be "Set"
Try this:
Sub Macro
Dim WS As Worksheet
Dim Stem As Range
Dim R2 As Range
Dim Rng as Range
Set WS = ActiveWorkbook.Sheets("Sheet1")
Set Stem = WS.Range("C2", Cells(WS.Range("C2").End(xlDown).Row, WS.Range("C2").Column))
Set R2 = WS.Range("K2", Cells(Stem.End(xlDown).Row, WS.Range("K2").Column))
R2.Value = Stem.Value
'New Code
For Each Rng In R2
Rng.Value = Rng.Value & Chr(10) & ""
Next Rng
'Old Code: R2.End(xlDown) = R2.End(xlDown) & Chr(10) & ""
End Sub
What this does is first sets the worksheet you're using. Then, you set your working range (Stem) using the Range(cell1, cell2) format. Cell1 I defined as "C2". The next expression there is using the Cells() function. It is the VBA equivalent of being in "C2" and hitting Ctl+Down, then seeing what row you're in.
Then, I set your destination range, R2, in a similar manner, but I used the Stem range to determine how large it should be.
Finally, to get an accurate copy your destination range must be the same size as your from range. The .value to .value expression pastes the data. Then, your extra characters are added on to your new data field.
Something to keep in mind with .End(xlDown)... if you have blank rows in the middle of your data it will stop there, not go all the way down to the end. Hope this helps!
EDIT:
The For Each loop will go through every range (i.e. cell) in your destination range, R2, and add your new characters. Hope that fits better for you.
Thanks to all for your answers. I atlas was able to write my first script and got the code to add line break inside the cell.
Sub AddLineBreak()
Dim Stem As Variant
Stem = ThisWorkbook.Worksheets("Sheet1").Range("C2")
Dim i As Integer
i = 2
'this will terminate the loop if an empty cell occurs in the middle of data
Do While Cells(i, "C").Value <> ""
Cells(i, "K").Value = Cells(i, "C").Value & vbCrLf
i = i + 1
Loop
End Sub

What is the easiest way to take two columns of data and convert to dictionary?

I have a worksheet with data in columns A and B.
I am looking for a convenient way to take these columns and convert to dictionary where the cell in column A is the key and column B is the value, something like :
Dim dict as Dictionary
Set dict = CreateDictFromColumns("SheetName", "A", "B")
NOTE: I am already referencing the scripting dll.
You would need to loop, E.g.
Function CreateDictFromColumns(sheet As String, keyCol As String, valCol As String) As Dictionary
Set CreateDictFromColumns = New Dictionary
Dim rng As Range: Set rng = Sheets(sheet).Range(keyCol & ":" & valCol)
Dim i As Long
Dim lastCol As Long '// for non-adjacent ("A:ZZ")
lastCol = rng.Columns.Count
For i = 1 To rng.Rows.Count
If (rng(i, 1).Value = "") Then Exit Function
CreateDictFromColumns.Add rng(i, 1).Value, rng(i, lastCol).Value
Next
End Function
This breaks on the first empty key value cell.
I think it'd be best form to pass two ranges to a create dictionary function. This allows for the ranges to be completely separate, even different workbooks. It also allows for a 1D range to be mapped to a 2D range as demonstrated below.
Alternatively, you could also pass two arrays of range values. That may be cleaner for 1D ranges, but would result in slightly more code for 2D mapping. Notice that range elements can be looped through left to right top to bottom by index. You can use Application.Transpose(Range("A1:A5")) to effectively run top to bottom left to right.
Jagged Mapping
Sub Test()
RangeToDict Sheets(1).Range("A1:A5"), Sheets(2).Range("C1:E2")
End Sub
Function RangeToDict(ByVal KeyRng As Range, ByVal ValRng As Range) As Dictionary
Set RangeToDict = New Dictionary
For Each r In KeyRng
vi = vi + 1
'It may not be advisable to handle empty key values this way
'The handling of empty values and #N/A/Error values
'Depends on your exact usage
If r.Value2 <> "" Then
RangeToDict.Add r.Value2, ValRng(vi)
Debug.Print r.Value2 & ", " & ValRng(vi)
End If
Next
End Function
Side-By-Side (As Range)
If your target range is a single 2 column range side by side, you can simplify to passing a single range as shown below. Consequently, this also works for mapping every other element in a 1 dimensional range.
Sub Test()
RangeToDict2 Range("A1:B5")
End Sub
Function RangeToDict2(ByVal R As Range) As Dictionary
Set RangeToDict2 = New Dictionary
i = 1
Do Until i >= (R.Rows.Count * R.Columns.Count)
RangeToDict2.Add R(i), R(i + 1)
Debug.Print R(i) & ", " & R(i + 1)
i = i + 2
Loop
End Function
Two Columns (As Array)
Lastly, as an example of passing arrays as arguments, you could do something like the following. However, the following code will only work given the OP's specific scenario of mapping two columns. As is, it won't handle mapping rows or alternating elements.
Sub Test()
Dim Keys() As Variant: Keys = Range("E1:I1").Value2
Dim Values() As Variant: Values = Range("E3:I3").Value2
RangeToDict Keys, Values
End Sub
Function RangeToDict(Keys() As Variant, Values() As Variant) As Dictionary
Set RangeToDict = New Dictionary
For i = 1 To UBound(Keys)
RangeToDict.Add Keys(i, 1), Values(i, 1)
Debug.Print Keys(i, 1) & ", " & Values(i, 1)
Next
End Function
Use of Named Ranges
It may be convenient to used named ranges, in which case you can pass a Range as an argument likes this...
Sub Test()
RangeToDict Names("Keys").RefersToRange, Names("Values").RefersToRange
End Sub
The best approach to take, is to populate a variant array with the data from the worksheet. You can then loop through the array, assigning the elements of the first array column as the dictionary key; the elements of the second array column can then be used as the value.
The lrow function is used to find the last populated row from column A - allowing the code to create a dynamically sized array and dictionary.
To enable use of dictionaries within VBA, you will need to go to Tools -> References and then enable Microsoft Scripting Runtime.
Sub createDictionary()
Dim dict As Scripting.Dictionary
Dim arrData() As Variant
Dim i as Long
arrData = Range("A1", Cells(lrow(1), 2))
set dict = new Scripting.Dictionary
For i = LBound(arrData, 1) To UBound(arrData, 1)
dict(arrData(i, 1)) = arrData(i, 2)
Next i
End Sub
Function lrow(ByVal colNum As Long) As Long
lrow = Cells(Rows.Count, 1).End(xlUp).Row
End Function
This should do the trick :
Public Function test_leora(SheetName As String, _
KeyColumn As String, _
ValColumn As String) _
As Variant
Dim Dic, _
Val As String, _
Key As String, _
Ws As Worksheet, _
LastRow As Long
Set Ws = ThisWorkbook.Sheets(SheetName)
Set Dic = CreateObject("Scripting.Dictionary")
With Ws
LastRow = .Range(KeyColumn & .Rows.Count).End(xlUp).Row
For i = 1 To LastRow
Val = .Cells(i, ValColumn)
Key = .Cells(i, KeyColumn)
If Dic.exists(Key) Then
Else
Dic.Add Key, Val
End If
Next i
End With
test_leora = Dic
End Function

Resources