VBA Copying data from one table to another and rearranging columns - excel

I have 99 columns in one table called tbl_raw. I need to copy 96 of those columns into another table with the same exact header names, but they are rearranged in a different order. What is the most efficient way to do this?
The only way I knew was:
raw_data.Range("tbl_raw[EMPLOYEE]").Copy
processed_data.Range("tbl_processed[EMPLOYEE]").PasteSpecial
However, this would take a lot of code (96 * 2 = 192 lines) and I wasn't sure if there was a more efficient way to do it.
I tried to use https://www.thespreadsheetguru.com/blog/2014/6/20/the-vba-guide-to-listobject-excel-tables, but I couldn't figure out a way to do it with that information either.
Any guidance would be greatly appreciated.

Avoid dealing with copying ListObject columns and use a direct value transfer.
Option Explicit
Sub raw2processed()
Dim lc As Long, mc As Variant, x As Variant
Dim raw_data As Worksheet, processed_data As Worksheet
Dim raw_tbl As ListObject, processed_tbl As ListObject
Set raw_data = Worksheets("raw")
Set processed_data = Worksheets("processed")
Set raw_tbl = raw_data.ListObjects("tbl_raw")
Set processed_tbl = processed_data.ListObjects("tbl_processed")
With processed_tbl
'clear target table
On Error Resume Next
.DataBodyRange.Clear
.Resize .Range.Resize(raw_tbl.ListRows.Count + 1, .ListColumns.Count)
On Error GoTo 0
'loop through target header and collect columns from raw_tbl
For lc = 1 To .ListColumns.Count
Debug.Print .HeaderRowRange(lc)
mc = Application.Match(.HeaderRowRange(lc), raw_tbl.HeaderRowRange, 0)
If Not IsError(mc) Then
x = raw_tbl.ListColumns(mc).DataBodyRange.Value
.ListColumns(lc).DataBodyRange = x
End If
Next lc
End With
End Sub

Here's a basic example of copying over all but some columns from one table to another:
Dim tbl1 As ListObject, tbl2 As ListObject
Dim h As ListColumn
Set tbl1 = ActiveSheet.ListObjects("Table1")
Set tbl2 = ActiveSheet.ListObjects("Table2")
'loop over the headers from the source table
For Each h In tbl1.ListColumns
'is the column name in the "excluded" list?
If IsError(Application.Match(h.Name, Array("col10", "col11"), 0)) Then
'ok to copy...
h.DataBodyRange.Copy tbl2.ListColumns(h.Name).DataBodyRange(1)
End If
Next h

ForEach/For are the magic of working with arrays and collections.
There are ways to make the following code more efficient, but I think that may get in the way of understanding what is happening.
It has been about 6 months or so since I last worked with VBA, but I believe this should work. I suggest stepping through and watching your locals to see what is going on. If there are issues with variable assignments, there might need to be a 'Let' changed to a 'Set'.
Code follows:
'// PROBLEM:
'// Copy data from one list to a second list.
'// Both lists have the same column names and the same number of columns.
'// Copy data based on the column name.
'// Modify to return a custom source-destination association.
Private Function GetColumnTranslations(zLeftColumns As ListColumns, zRightColumns As ListColumns) As Variant
Dim zReturn(,) As Variant
ReDim zReturn(0 To zLeftColumns.Count As Long, 0 To 1 As Long)
Dim zReturnOffset As Long '// Specifies what index we are working at during our ForEach interations.
Dim zLeftVar As Variant
Dim zRightVar As Variant
ForEach zLeftVar in zLeftColumns
'// Go through each 'left' column to Find the first 'right' column that matches the name of the 'left' column.
'// Only the first 'right' column with a matching name will be used. Issue is solved with another ForEach, but beyond forum question's scope.
ForEach zRightVar in zRightColumns
If zLeftVar.Name = zRightVar.Name Then
'// Store the association and exit the nested ForEach.
Let zReturn(zReturnOffset, 0) = zLeftVar.Range.Column '// Source.
Let zReturn(zReturnOffset, 1) = zRightVar.Range.Column '// Destination.
Let zReturnOffset = zReturnOffset + 1
Exit ForEach
End If
Next zRightVar
Next zLeftVar
'// Assign return value.
Let GetColumnTranslations = zReturn
End Function
'// Take each source row and copy the value to a new destination row.
'// New rows are added to the end of the destination list.
Public Sub CopyList(zSourceList As ListObject, zDestinationList As ListObject)
Dim zColumnTranslations As Variant '// Will be 2-dimensional array.
Dim zTranslationVar As Variant '// Will be array of 2 elements.
Let zColumnTranslations = GetColumnTranslations(zSourceList.Columns, zDestinationList.Columns)
Dim zSourceRowVar As Variant '// Will translate to Range.
Dim zDestinationRow As Range
'// Every source row needs copied to a new row in destination.
ForEach zSourceRowVar in zSourceList.Rows
Set zDestinationRow = zDestinationList.Rows.Add.Range
ForEach zTranslationVar in zColumnTranslations
'// Value may copy formula.
Let zDestinationRow(0,zTranslationVar(1)).Value = zSourceRowVar(0,zTranslationVar(0)).Value
Next zTranslationVar
Next zSourceRowVar
End Sub

Related

What is most efficient way to extract values from multiple non-contiguous cells from one workbook and insert them into a table on another workbook?

Little background, I am very new to VBA and just cant seem to find a solution to my problem. I am using this project as a means of learning basic VBA principles. Please bare with me.
I am currently attempting to write a macro that pulls values from non-contiguous cells (IE: F9, E15, G17, etc..) from a specific workbook and then pastes them into a table in a primary workbook. Each cell has data that needs to be added to a specific column in said table. I have hundreds of different files with the exact same layout (same important cell locations) that I want to eventually cycle through and add to a master table on the primary workbook. I would like to automate it.
My problem lies in not knowing the best method do go about this. I only need information from 12 cells per file so it is not an intense transfer. I have attempted going about it through arrays, creating variables, and messing with ranges. I was able to get to the point where I create a different variable for each cell I want data from and then, one-by-one, insert them into a specific cell in the primary workbook. This is far from automatic and doesn't include inserting each value under a specific column in my table.
Here is the most functional macro I've been able to create. It seems clunky and inefficient and does not prove to be a solution for my primary problems: automation, efficiency.
Sub data_pull()
Dim x As Workbook
Dim y As Workbook
Application.ScreenUpdating = False
Set x = Workbooks.Open("C:\Users\ - workbook that data is pulled from")
Set y = Workbooks.Open("C:\Users\ - workbook that data is put to")
'Pulling data through variables
RSS = x.Sheets(1).Range("F9").Value
RSE1_F = x.Sheets(1).Range("E13").Value
RSE1_B = x.Sheets(1).Range("F13").Value
RSE2_F = x.Sheets(1).Range("E14").Value
RSE2_B = x.Sheets(1).Range("F14").Value
TI = x.Sheets(1).Range("F20").Value
SI = x.Sheets(1).Range("F30").Value
FIBI = Split(x.Sheets(1).Range("F36").Value, "/") 'Cell has two values separated by a "/"
PEN = x.Sheets(1).Range("E40").Value
'Putting data through predefined variables
y.Sheets(1).Range("A1").Value = RSS
y.Sheets(1).Range("B1").Value = RSE1_F
y.Sheets(1).Range("C1").Value = RSE1_B
y.Sheets(1).Range("D1").Value = RSE2_F
y.Sheets(1).Range("E1").Value = RSE2_B
y.Sheets(1).Range("F1").Value = TI
y.Sheets(1).Range("G1").Value = SI
y.Sheets(1).Range("H1").Value = FIBI(0)
y.Sheets(1).Range("I1").Value = FIBI(1)
y.Sheets(1).Range("J1").Value = PEN
x.Close
Application.ScreenUpdating = True
End Sub
As you can see it is completely handled by calling for specific cell locations and does not append any data to a table specifically. I have a hunch that I could define a range with each cell location and then loop through that range, appending each cell to the desired table location.
Any and all feedback is greatly appreciated. If any more info is needed I am more than happy to elaborate!
Thanks!
One option for collecting cell values from a non-contiguous range is by defining the whole range, copying into an array and pasting in your uniform output region:
Option Explicit
Sub General_Testing()
' > Var
Dim wsInput As Worksheet
Dim wsOutput As Worksheet
Dim RG As Range
Dim CL As Range
Dim RGarr
Dim I As Long
' > Change to your workbooks/Sheets
Set wsInput = ThisWorkbook.Worksheets(1)
Set wsOutput = ThisWorkbook.Worksheets(2)
' > Source Data range
Set RG = wsInput.Range("$F$6,$E$13:$F$14,$F$20:$F$21")
ReDim RGarr(1 To RG.Cells.Count)
' > Move into array
I = 1
For Each CL In RG.Cells
RGarr(I) = CL.Value
I = I + 1
Next CL
With wsOutput
' > Array to output range
.Range("A1").Resize(1, UBound(RGarr)) = RGarr
' > last couple oddball values
.Range("H1:I1").Value = Split(wsInput.Range("F36"), "/")
.Range("J1").Value = wsInput.Range("F40").Value
End With
End Sub
If you want, you could easily do the whole thing including your split cell in the one array, just check for delimiter and increment I twice.
This is what is looks like:
Input:
Output:
Method 2:
Option Explicit
Sub General_Testing()
' > Var
Dim wsInput As Worksheet
Dim wsOutput As Worksheet
Dim RG As Range
Dim CL As Range
Dim RGarr
Dim I As Long
' > Change to your workbooks/Sheets
Set wsInput = ThisWorkbook.Worksheets(1)
Set wsOutput = ThisWorkbook.Worksheets(2)
' > Source Data range
Set RG = wsInput.Range("$F$6,$E$13:$F$14,$F$20:$F$21,$F$36,$E$40")
ReDim RGarr(1 To RG.Cells.Count)
' > Move into array
I = 1
For Each CL In RG.Cells
If InStr(1, CL.Value, "/") > 0 Then
' > String must be split
ReDim Preserve RGarr(1 To UBound(RGarr) + 1)
RGarr(I) = Split(CL.Value, "/")(0)
I = I + 1
RGarr(I) = Split(CL.Value, "/")(1)
I = I + 1
Else
' > String must not be split
RGarr(I) = CL.Value
I = I + 1
End If
Next CL
With wsOutput
' > Array to output range
.Range("A1").Resize(1, UBound(RGarr)) = RGarr
End With
End Sub

VBA Macro to find rows and store in array runs slow

So I made a simple VBA macro to run over ~7000 rows of data - the idea is that .Find finds the cells which contain "1" in column G, so that it can store the row number in an array which I shall later throw back to another sub
Unfortunately the code takes too long to run - it begs the question, have I created an infinite loop in my code? Or is asking it to loop a .find operation over 7000 cells too much for vba to handle at a reasonable speed? (i.e. do I need to improve efficiency in areas?)
Option Explicit
Public Sub splittest()
Dim sheet As Object, wb As Workbook
Dim rangeofvals As Range
Dim pullrange As Variant
Dim c As Long
Dim dynarr() As Variant
Dim xvalue As Long
Dim firstaddress As Variant
Dim count As Long
Set wb = ThisWorkbook
Set sheet = wb.Sheets("imported d")
Set rangeofvals = Range("G1:G6939")
'need to set pull range at some later point
Call OptimizeCode_Begin 'sub which turns off processes like application screen updating
xvalue = 1
ReDim dynarr(3477) 'hardcoded, replace with a countif function at some point
count = 0
With wb.Sheets("imported d").Range("G1:G6939")
c = rangeofvals.Find(xvalue, LookIn:=xlFormulas).Row
If c >= 0 Then
dynarr(count) = c
' MsgBox firstaddress
Do
' MsgBox c
c = rangeofvals.FindNext(Cells(c, 7)).Row
dynarr(count) = c 'apparently redim preserve would be slower
Loop While c >= 0
End If
Call OptimizeCode_End 'sub which turns back on processes switched off before
End With
End Sub
If you know the column is G and all the data is contiguous, then just loop through the rows and check the cell value directly:
Dim rows As New Collection
Dim sheet As Worksheet
Dim lastRow, i As Integer
Set sheet = ThisWorkbook.Sheets("imported d")
lastRow = sheet.Cells(1,7).CurrentRegion.Rows.Count
For i = 1 to lastRow
If (sheet.Cells(i,7).Value = 1) Then
rows.Add i
End If
Next
Unclear how the data is being used in the other sub but collection is definitely more efficient storage object for adding iteratively when the total item count is indeterminate. If you want to convert to an array then you can do so efficiently afterward since the collection tells you the item count. I'm not really sure why you would need an array specifically but I'm not going to tell you not to without seeing the client sub. Also note that the declaration of sheet was changed from Object to Worksheet since it is better to use the specific data type if possible.
...yep it was an infinite loop.
the line:
Loop While c >= 0
caused it as there is never an occasion c is less than 0 - back to the drawing board for me!

Excel VBA - copying data depending on column name

I have a problem: using three separate worksheets (Source 1, Source 2, Template) I have to take data from the source files and paste it onto the Template, depending on the column names.
I.e. Source 1's first three columns' names match those on the Template, and the Template's fourth row matches the fourth row matches Source 2's fourth.
Here's what I got this far (it is incomplete).
Checking other threads I made a "derivation" of an example, but I still can not make it work. I get runtime error 9 on the line marked with a comment within Copy_Columns()
Sub MasterCopy()
Open_Files
Copy_Columns
End Sub
Sub Open_Files()
Application.Workbooks.Open Filename:="C:Source 1.xls"
Application.Workbooks.Open Filename:="C:Source 2.xls"
Application.Workbooks.Open Filename:="C:Template.xls"
End Sub
Sub Copy_Columns()
Dim Source1 As Worksheet
Source1 = Application.Workbooks("C:Source 1.xls").Worksheets("Sheet1") 'here is where the error appears
Source1.Select
Dim columnToBeCopied As Integer
columnToBeCopied = getColumnName("Source1", "columnToBeCopied")
Dim template As Worksheet
template = Application.Workbooks("C:Template.xls").Worksheets("Data")
template.Activate
Dim columnToBePasted As Integer
columnToBePasted = getColumnName("template", "columnToBePasted")
Sheets("Source1").Columns(columnToBeCopied).Copy Sheets("template").Columns(columnToBePasted)
End Sub
Public Function getColumnName(ByVal sheetName As String, ByVal columnName As String)
Dim lastColumn As Integer
lastColumn = ActiveSheet.Cells(Rows.Count, "A").End(xlUp).Column
Dim iterator As Integer
iterator = 0
While (iterator <= lastColumn)
If (LCase(Sheets(sheetName).Range(1, iterator)) = LCase(columnName)) Then
getColumnName = iterator
Else: iterator = iterator + 1
End If
Wend
If IsEmpty(getColumnName) Then getColumnName = 0
End Function
There's somewhat close examples but they are either too complex (this is my first time using VBA) or too "obscure" in terms that they use a, o, x, b as variable names...
Any help greatly appreciated :)
Change the line with the error to this:
Set Source1 = Workbooks("Source 1").Sheets("Sheet1")

Count unique entries between blanks Excel

I would like to count unique entries in a range between blanks. Please look at the attached pic for reference. The first set of data contains 3 unique entries so the highlighted box would 3. The second set of data has 3 unique entries and the highlighted box then would also display 3.
example
#Todd Nielsen
Check the below link which has different solutions for it.
https://support.office.com/en-us/article/Count-unique-values-among-duplicates-7889942d-824e-4469-893c-191d1efde950#bmcount_the_number_of_unique_values__by
Assuming you want to do this in VBA and the range containing the data sets is huge, the question is not as trivial as others which have replied to this post have made it out to be.
The following code requires that you set a reference to the Microsoft Scripting Runtime library. It inserts the number of unique values each time it finds an empty row in the search range (works best if there is only one empty row to separate the different data sets in the range):
Option Explicit
Sub uniquesEntriesInDataSets()
Dim dict As Scripting.Dictionary
Dim i As Long, startAt As Long, uCount As Long
Dim cll As Range, searchRange As Range
Dim val As Variant
' change sheetname and range to relevant parameters
Set searchRange = ThisWorkbook.Sheets("Sheet1").Range("A1:A21")
Set dict = New Dictionary
For Each cll In searchRange
val = cll.Value
If val = vbNullString Then
If dict.Count > 0 Then
cll.Value = dict.Count
dict.RemoveAll
End if
Else
If Not dict.Exists(val) Then dict.Add Key:=val, Item:=i
End If
Next cll
End Sub

Pasting an array of values over a ListObject (Excel table) destroys the Listobject

In one of my worksheets, I have a
Private Sub BuggingVba()
That should replace the data in a table with an array of values
Dim MyTable As ListObject, myData() As Variant
Set MyTable = Me.ListObjects(1)
myData = collectMyData ' a function defined somewhere else in my workbook
It is probably irrelevant, but before doing so, I resize the list object (I expand line by line because if I do it at once, I overwrite what is below my table instead of schifting it.)
Dim current As Integer, required As Integer, saldo As Integer
current = MyTable.DataBodyRange.Rows.Count
required = UBound(sourceData, 1) - LBound(sourceData, 1)
' current and required are size of the body, excluding the header
saldo = required - current
If required < current Then
' reduce size
Range(DestinBody.Rows(1), DestinBody.Rows(current - required)).Delete xlShiftUp
Else
' expland size
DestinBody.Rows(1).Copy
For current = current To required - 1
DestinBody.Rows(2).Insert xlShiftDown
Next saldo
End If
If there is any data to insert, I overwrite the values
If required Then
Dim FullTableRange As Range
Set FullTableRange = MyTable.HeaderRowRange _
.Resize(1 + required, MyTable.HeaderRowRange.Columns.Count)
FullTableRange.Value = sourceData
End If
And BAM, my table/ListObject is gone! Why does this happen and how can I avoid it?
End Sub
When we paste over the entire table or clear the contents of the entire table the collateral result is that the table object (ListObject) is deleted. That’s the reason the code works when the data is changed row by row.
However, there is no need to do it row by row, not even the insertion of new rows if we work with the properties of the ListObject as demonstrated in the code below.
In these procedures we assumed that the "Target" Table and the “New Data” are, in the same workbook holding the code, located at worksheets 1 and 2 respectively:
As we will work with the HeaderRowRange and the DataBodyRange of the ListObject then we need to obtain the “New Data” to replace the data in the table in the same manner. The code below will generate two arrays with the Header and Body Arrays.
Sub Dta_Array_Set(vDtaHdr() As Variant, vDtaBdy() As Variant)
Dim vArray As Variant
With ThisWorkbook.Worksheets("Sht(1)").Range("DATA") 'Change as required
vArray = .Rows(1)
vDtaHdr = vArray
vArray = .Offset(1, 0).Resize(-1 + .Rows.Count)
vDtaBdy = vArray
End With
End Sub
Then use this code to replace the data in the table with the "New Data"
Private Sub ListObject_ReplaceData()
Dim MyTable As ListObject
Dim vDtaHdr() As Variant, vDtaBdy() As Variant
Dim lRowsAdj As Long
Set MyTable = ThisWorkbook.Worksheets(1).ListObjects(1) 'Change as required
Call Data_Array_Set(vDtaHdr, vDtaBdy)
With MyTable.DataBodyRange
Rem Get Number of Rows to Adjust
lRowsAdj = 1 + UBound(vDtaBdy, 1) - LBound(vDtaBdy, 1) - .Rows.Count
Rem Resize ListObject
If lRowsAdj < 0 Then
Rem Delete Rows
.Rows(1).Resize(Abs(lRowsAdj)).Delete xlShiftUp
ElseIf lRowsAdj > 0 Then
Rem Insert Rows
.Rows(1).Resize(lRowsAdj).Insert Shift:=xlDown
End If: End With
Rem Overwrite Table with New Data
MyTable.HeaderRowRange.Value = vDtaHdr
MyTable.DataBodyRange.Value = vDtaBdy
End Sub
Old post, but the way I paste over a listobject table is to delete the databodyrange, set a range to the array size and then set the range to the array. Similar to the solution provided above, but doesn't require resizing the table.
'Delete the rows in the table
If lo.ListRows.Count > 0 Then
lo.DataBodyRange.Delete
End If
'Assign the range to the array size then assign the array values to the range
Set rTarget = wsTemplate.Range("A2:K" & UBound(arrTarget) + 1)
rTarget = arrTarget

Resources