Reputation: 770
I have a source workbook and destination workbook and want to map all data from source wb to destination wb but the columns go in a specific order based on a mappings. Both source and destination header can be any length but the importance one will be the destination header because I don’t want to bring back all data just want data that i need based on mapping.
I got stuck on the loops where I need to offset the columns because when I am writing to the destination wb it will write column after column which is not what I intended. So if there is blank in the mapping then leaves that column as empty and move to next one and when writing this to the workbook the array should have same number of columns but the data in the right place. Any help will be greatly appreciated as I’ve tried various ways but always end up with wrong data or not working at all. The below works but when writing the data it dose without skipping whatever is not in the mapping so then I need to manually change the columns to fit the right data in the right column might as well do copy and paste but I thought array will be a lot faster at this task as the data is large in rows over 300k rows.
The picture is put together from different sheets and will not have the yellow row this is to make it clear what data belongs to what. The data mapping the first row will have the worksheet name where I will write the data.
Option Explicit
Sub DataTransformation()
Dim wsDataMapping As Worksheet
Dim targetWS As Worksheet
Dim sourceWB As Workbook
Dim targetWB As Workbook
Dim destMapping As Range
Dim destMappingCell As Range
Dim srcData As Range
Dim destData as Range
Dim srcArr As Variant
Dim destArr() As Variant
Dim LCol As Long
Dim i As Long
Dim j As Long
Dim x As Long
Dim sourceLRow As Long
Dim targetLRow As Long
Dim remainingFiles As String
Dim time As Variant
time = Now()
Application.ScreenUpdating = False
Set targetWB = ActiveWorkbook
With targetWB
Set wsDataMapping = .Sheets("Data Mapping")
Set targetWS = .Sheets(wsDataMapping.Range("A1").value)
End With
LCol = targetWS.Cells(16, targetWS.Columns.Count).End(xlToLeft).Column
Set destMapping = wsDataMapping.Range("A2:A" & wsDataMapping.Range("A" & wsDataMapping.Rows.Count).End(xlUp).Row)
Dim filePath As String
Dim fileName As Variant
Dim allFiles As Collection
Set allFiles = New Collection
filePath = "C:\Users\" & Environ(“UserName”) & "\Desktop\Test M" & "\"
'fileName = Dir(filePath & "*.*")
Set allFiles = LoopThroughFiles(filePath, ".*")
For Each fileName In allFiles
Set sourceWB = Workbooks.Open(filePath & fileName)
Set srcData = sourceWB.Worksheets(1).Range("A1").CurrentRegion
Set destData = targetWS.Range(targetWS.Cells(13, 3), targetWS.Cells(13, LCol))
sourceLRow = sourceWB.Worksheets(1).Range("A" & sourceWB.Worksheets(1).Rows.Count).End(xlUp).Row
targetLRow = FindLastRow(targetWS, 3, LCol)
If (targetWS.Rows.Count - targetLRow) > sourceLRow Then
srcArr = srcData.value
ReDim Preserve destArr(1 To UBound(srcArr, 1), 1 To UBound(srcArr, 2))
For Each destMappingCell In destMapping
x = 0
For i = LBound(srcArr, 2) To UBound(srcArr, 2)
If destMappingCell.Offset(0, 1).value = srcArr(1, i) Then
x = x + 1
For j = LBound(srcArr, 1) To UBound(srcArr, 1)
'Debug.Print srcArr(j, i)
destArr(j, x) = srcArr(j, i)
Next j
End If
Next i
Next destMappingCell
sourceWB.Close False
targetWS.Range("C" & targetLRow).Resize(UBound(destArr, 1), UBound(destArr, 2)).value = destArr
Else
sourceWB.Close False
remainingFiles = remainingFiles & " " & fileName & vbNewLine
End If
Next fileName
Application.ScreenUpdating = True
MsgBox time & " after run " & Now() & vbNewLine & "Files remaining to process are " & vbNewLine & remainingFiles
End Sub
Function LoopThroughFiles(inputDirectoryToScanForFile, filenameCriteria) As Collection
Dim strFile As String
Dim fileNames As Collection
Set fileNames = New Collection
strFile = Dir(inputDirectoryToScanForFile & "\*" & filenameCriteria)
Do While Len(strFile) > 0
'Debug.Print strFile
fileNames.Add (strFile)
strFile = Dir 'returns a new file
Loop
Set LoopThroughFiles = fileNames
End Function
Function FindLastRow(ByVal ws As Worksheet, Optional ByVal FromCol As Long = 0, Optional ByVal ToCol As Long = 0) As Long
Dim i As Long
Dim lastRow As Long
If FromCol = 0 Then FromCol = 3
If ToCol = 0 Then ToCol = 10
For i = FromCol To ToCol
lastRow = ws.Cells(ws.Rows.Count, i).End(xlUp).Row
If FindLastRow < lastRow Then
FindLastRow = lastRow
End If
Next i
If FindLastRow < 17 Then FindLastRow = 17
End Function
Upvotes: 0
Views: 1510
Reputation: 12413
Designing and testing possible solution
I would never try to design and code a routine like this in a single go. I would divide it into steps; and I would code and test those steps separately if possible. You have a block of code which does not work. Was the error in step 1 or step 5? I also look for opportunities to write my code as subroutines or functions that I can use again.
Consider Function FindLastRow
. You have coded this function in such a way that it is unique to this project. Finding the last row and or column of a worksheet is a frequent requirement so creating a block of code you can use again and again would be useful.
You have used a technique for finding the last row of a column which is the probably the most reliable technique providing you know which column to test. You have avoided the “which column?” problem by tested every column. There are other techniques for finding the last row but none of the available techniques works in every situation. My solution was to write a routine that used several techniques and picked the ‘best’ answer. I do not worry which technique would be the best or fastest for today’s worksheet; I just use my standard routine. A routine for today’s scenario might be faster but I have no time for programmers who spend minutes writing code that shaves milliseconds off a routine that is run once a day.
This is my routine for finding the last row and column of a worksheet:
Public Sub FindLastRowCol(ByRef Wsht As Worksheet, ByRef RowLast As Long, _
ByRef ColLast As Long)
' Sets RowLast and ColLast to the last row and column with a value
' in worksheet Wsht
' The motivation for coding this routine was the discovery that Find by
' previous row found a cell formatted as Merge and Center but Find by
' previous column did not.
' I had known the Find would miss merged cells but this was new to me.
' Dec16 Coded
' 31Dec16 Corrected handling of UserRange
' 15Feb17 SpecialCells was giving a higher row number than Find for
' no reason I could determine. Added code to check for a
' value on rows and columns above those returned by Find
' 25Jun17 Found column with value about that found by Find
Dim ColCrnt As Long
Dim ColLastFind As Long
Dim ColLastOther As Long
Dim ColLastTemp As Long
Dim ColLeft As Long
Dim ColRight As Long
Dim Rng As Range
Dim RowIncludesMerged As Boolean
Dim RowBot As Long
Dim RowCrnt As Long
Dim RowLastFind As Long
Dim RowLastOther As Long
Dim RowLastTemp As Long
Dim RowTop As Long
With Wsht
Set Rng = .Cells.Find("*", .Range("A1"), xlFormulas, , xlByRows, xlPrevious)
If Rng Is Nothing Then
RowLastFind = 0
ColLastFind = 0
Else
RowLastFind = Rng.Row
ColLastFind = Rng.Column
End If
Set Rng = .Cells.Find("*", .Range("A1"), xlValues, , xlByColumns, xlPrevious)
If Rng Is Nothing Then
Else
If RowLastFind < Rng.Row Then
RowLastFind = Rng.Row
End If
If ColLastFind < Rng.Column Then
ColLastFind = Rng.Column
End If
End If
Set Rng = .Range("A1").SpecialCells(xlCellTypeLastCell)
If Rng Is Nothing Then
RowLastOther = 0
ColLastOther = 0
Else
RowLastOther = Rng.Row
ColLastOther = Rng.Column
End If
Set Rng = .UsedRange
If Rng Is Nothing Then
Else
If RowLastOther < Rng.Row + Rng.Rows.Count - 1 Then
RowLastOther = Rng.Row + Rng.Rows.Count - 1
End If
If ColLastOther < Rng.Column + Rng.Columns.Count - 1 Then
ColLastOther = Rng.Column + Rng.Columns.Count - 1
End If
End If
If RowLastFind < RowLastOther Then
' Higher row found by SpecialCells or UserRange
Do While RowLastOther > RowLastFind
ColLastTemp = .Cells(RowLastOther, .Columns.Count).End(xlToLeft).Column
If ColLastTemp > 1 Or .Cells(RowLastOther, 1).Value <> "" Then
Debug.Assert False
' Is this possible?
' Row after RowLastFind has value
RowLastFind = RowLastOther
Exit Do
End If
RowLastOther = RowLastOther - 1
Loop
ElseIf RowLastFind > RowLastOther Then
Debug.Assert False
' Is this possible?
End If
RowLast = RowLastFind
If ColLastFind < ColLastOther Then
' Higher column found by SpecialCells or UserRange
Do While ColLastOther > ColLastFind
RowLastTemp = .Cells(.Rows.Count, ColLastOther).End(xlUp).Row
If RowLastTemp > 1 Or .Cells(1, ColLastOther).Value <> "" Then
'Debug.Assert False
' Column after ColLastFind has value
' Possible causes:
' * Find does not recognise merged cells
' * Find does not examine hidden cells
ColLastFind = ColLastOther
Exit Do
End If
ColLastOther = ColLastOther - 1
Loop
ElseIf ColLastFind > ColLastOther Then
Debug.Assert False
' Is this possible
End If
ColLast = ColLastFind
End With
End Sub
This routine was written by me for me. I am happy to share it, but it was not written to share. The block of comments at the top are reasonably standard for my subroutines: what does it do, what are the parameters, why and history. The structure will vary depending on the complexity of the routine and how long it took me to get it working correctly. This routine has few comments within the body of the macro. Consider the first block in which I use Range.Find
to obtain the last row and column. What comment would help understand this block? If I had forgotten the syntax for Range.Find
, a quick online search will reveal a page that explains it. There are a few Debug.Assert False
s left. These are to catch situations I do not believe can occur but which I want to know about if they do.
I hold routines like this is PERSONAL.XLSB. I have modules with names like LibExcel, LibOutlook and LibOffice which contain routines specific to Excel (such as this one) or Outlook or routines useful across several Office packages (such as reading and writing files). Since they are in PERSONAL.XLSB, they are available to all my workbooks. Search online for PERSONAL.XLSB if you want to know more.
I would divide your problem into three steps:
You might divide your problem up differently; it will depend on how comfortable you are with programming in VBA. I have been using VBA for 18 years and I learnt my first programming language 55 years ago so I am comfortable although I have never thought of myself as a geek; for me, programming is just a means to an end.
I would not have thought of turning the Dir loop into a subroutine that returned a collection. For me, using Dir to search down a single folder is so simple that I have never bothered to create a subroutine for it. Searching subfolders makes the loop more complicated but, for me, not consistent enough to turn into a subroutine. Decoding the column mapping table depends on how it is going to be used which means I must design step 3 first.
Copying an entire range to a Variant in one go is much, much faster than copying cell by cell. However, copying from one array cell to another is not much faster than copying one worksheet cell to another. I would use Range.Copy to copy an entire column for the source to the destination worksheet. That is, I would copy source column 1 to destination column 2, 2 to 3, 6 to 6, 5 to 7, 4 to 8, 3 to 9 and 5 to 10.
To make the copying easy, I need that information in an array or perhaps a pair of arrays. My choices are limited by the need to copy source column 5 to destination columns 7 and 10. My best idea is:
Array Index | 1| 2| 3| 4| 5| 6| 7| 8| 9|10|
SrcForDest | 2| 3| 9| 8| 7| 6| 0| 0| 0| 5|
The array index identifies the destination column. A value of 0 means that column has no source. Any other value is the source column. The loop becomes:
For Inx = 1 to 10
If SrcForDest(Inx) <> 0 Then
Construct source range
Construct destination range
Copy
End If
Next
Part 2
I wanted to code and test the macro DecodeMapping()
. I created a macro-enabled workbook. Within it, I created worksheets “Data Mapping”, “Source” and “Destination”. I know “Source” and “Destination” are not the name you are using but currently this is just a testing workbook.
I entered your data mapping table into worksheet “Data Mapping”:
If I have understood correctly, you have the name of your destination worksheet as cell A1 of this worksheet. I think this is a bad idea. My assumption is that you are trying to combine a lot of existing workbooks into one new workbook. This will be a one-off conversion and you will never need to look at this macro again once the conversion is finished. I do not like anything that is not obvious because I have seen it cause catastrophes when a new programmer did not appreciate why cell A1 had this strange value. If you call the worksheet something like control data and clearly labelled the worksheet name and clearly label the mapping table then fine. But naming a worksheet “Data Mapping” but having something else in cell A1 is bad practice. I avoid bad practice even if it does not matter because it becomes a habit. However, as I will explain latter, this does not matter.
I entered the column headings into worksheet “Source”:
There is no data because I do not need any data at this stage.
Worksheet “Destination” I left empty.
I created three modules which I named: “LibExcel”, “Mod Original” and “ModNew”. I copied macro FindLastRowCol
to module “LibExcel”. I copied your code to “ModOriginal” so it was easy to reference. I wrote macros DecodeMapping
and TestDecodeMapping
within “ModNew”.
This is a technique I use frequently. To test DecodeMapping
I create a test macro that simulates the eventual setting. I do not have to worry about anything other than the needs of DecodeMapping
.
The code within module “ModNew” is:
Option Explicit
Sub TestDecodeMapping()
Const RowMapDataFirst As Long = 2
Dim ColDestCrnt As Long
Dim ColsDestName() As String
Dim ColsSrcForDest() As Long
Dim DataMap As Variant
Dim RowMapLast As Long
Dim WshtMap As Worksheet
Dim WshtSrc As Worksheet
Set WshtMap = Worksheets("Data Mapping")
Set WshtSrc = Worksheets("Source")
With WshtMap
RowMapLast = .Cells(Rows.Count, 1).End(xlUp).Row
DataMap = .Range(.Cells(RowMapDataFirst, 1), .Cells(RowMapLast, 2)).Value
' Note 1: the lower bounds of a variant loaded from a range are always one
' regardless of the location of the range within the worksheet.
' Note 2: I have loaded DataMap starting at row 2 because RowMapDataFirst
' equals 2. If you want the table to start at a different row, just
' change the value of RowMapDataFirst.
End With
Call DecodeMapping(WshtSrc, DataMap, ColsDestName, ColsSrcForDest)
' Test ColsDestName by loading it to the top row of worksheet "Destination."
With Worksheets("Destination")
.Range(.Cells(1, 1), .Cells(1, UBound(ColsDestName))).Value = ColsDestName
End With
' Test ColsSrcForDest by outoutting an anotated list of its contents.
For ColDestCrnt = 1 To UBound(ColsSrcForDest)
Debug.Print "Destination column " & ColDestCrnt & " (" & DataMap(ColDestCrnt, 1) & _
") ";
If ColsSrcForDest(ColDestCrnt) = 0 Then
Debug.Print "will be left empty"
Else
Debug.Print "will be loaded from source column " & ColsSrcForDest(ColDestCrnt) & _
" (" & DataMap(ColDestCrnt, 2) & ")"
End If
Next
End Sub
Sub DecodeMapping(ByRef WshtSrc As Worksheet, ByRef DataMap As Variant, _
ByRef ColsDestName() As String, ByRef ColsSrcForDest() As Long)
' Decodes a table mapping source column names to destination column names.
' Create an array of column headings for the destination worksheet.
' Locates the source column names within the source worksheet and creates an
' array mapping the source column numbers to the destination columns.
' WshtSrc The source worksheet
' DataMap A Variant holding a 2D table with 2 columns and 1 row per
' destination column. Table column 1 contains the names of the
' destination columns in output sequence with table row N
' holding the name of destination column N. Table column 2 of
' row N contains the name of the source column, if any, for the
' destination column whose name is in table column 1 or row N.
' ColsDestNames On exit, the names of the destination columns in an array
' ready to be loaded to the header row of the destination
' worksheet.
' ColsSrcForDest On exit, ReDimmed to (1 To M) where M is the number of columns
' in the destination worksheet. If ColsSrcForDest(P) = 0,
' destination column P is left blank. If ColsSrcForDest(P) = Q,
' source column Q is to be copied to destination column P.
' 18Apr20 Coded.
Dim ColSrcCrnt As Long
Dim ColSrcLast As Long
Dim Found As Boolean
Dim RowDataCrnt As Long
Dim RowSrcLast As Long
ReDim ColsDestName(1 To UBound(DataMap, 1))
' Build array that can be used to create heading row for destination worksheet
For RowDataCrnt = 1 To UBound(DataMap, 1)
ColsDestName(RowDataCrnt) = DataMap(RowDataCrnt, 1)
Next
Call FindLastRowCol(WshtSrc, RowSrcLast, ColSrcLast) ' Only need ColSrcLast
' Size ColsSrcForDest so there is one entry per destination column
' The entries are initialised to zeros.
ReDim ColsSrcForDest(1 To UBound(DataMap))
' There are faster methods of achieving the source to destination mapping
' than these nested loops but the VBA is more complicated. If there are
' so many source and destination columns that this is slow, I will recode.
' Match each value in column 2 of DataMap against a column heading in
' worksheet WshtSrc. When a match is found, record the match in ColsSrcForDest.
With WshtSrc
For RowDataCrnt = 1 To UBound(DataMap)
If DataMap(RowDataCrnt, 2) <> "" Then
'Debug.Assert False
Found = False
For ColSrcCrnt = 1 To ColSrcLast
If .Cells(1, ColSrcCrnt).Value = DataMap(RowDataCrnt, 2) Then
' Warning: this a case sensitive match
'Debug.Assert False
ColsSrcForDest(RowDataCrnt) = ColSrcCrnt
Found = True
Exit For
End If
Next
If Not Found Then
Debug.Assert False
Call MsgBox("Source column name """ & DataMap(RowDataCrnt, 2) & _
""" appears in the DataMap but is not a column " & _
"heading in worksheet """ & WshtSrc.Name & """", vbOKOnly)
End ' Exit this macro and calling macro.
End If
End If
Next
End With
End Sub
Notes:
TestDecodeMapping ()
does all the preparation. It loads variant DataMap
from the worksheet. DecodeMapping ()
does not know how DataMap
was created. If you want to load DataMap
from lower down worksheet “Data Mapping”, no change will be required to DecodeMapping ()
DecodeMapping
does not do any offsetting. The offsetting should be in the main routine, so it is obvious. We were taught: no hidden side-effects. If a new programmer takes over a program, everything should be obvious. If a routine is named DoX then it should do X and nothing else.DecodeMapping ()
. I want to be 100% confident that DecodeMapping ()
works correctly before I use it in my main
Routine.Part 3
The next issue is finding the source workbooks and validating the source worksheets.
You assume that every file within "C:\Users\" & Environ(“UserName”) & "\Desktop\Test M" is a source workbook. Since you control the contents of this folder, this may be a reasonable assumption unless the workbook containing the macros and the destination worksheet is also within that folder.
You assume that the source worksheet is always Sheet(1) and that every worksheet contains every source column. Again, these might be reasonable assumptions but your macro would fail if even one source workbook was not exactly as you assume. I do not know how many of these source workbooks exist since your question implies there is only one. It is function LoopThroughFiles()
that indicates there are multiple source workbooks. If you control these workbooks, you may know they are all the same. But if someone else controls them, any assumption is dangerous. It is easy to add another worksheet to a workbook by design or accident.
I have written Function FindSrcWsht()
, which makes no assumptions, and I have written Sub TestFindSrcWsht()
to test it and demonstrate how I would find and check these workbooks.
I created five workbooks that match my understanding of your source workbooks. Some have extra columns, some have the columns in a different sequence and some have missing columns. You do not need to create test workbooks since you have the real ones.
Near the beginning of TestFindSrcWsht()
you will find Path = ThisWorkbook.Path & "\"
. You need to replace this with Path = "C:\Users\" & Environ(“UserName”) & "\Desktop\Test M" & "\"
. Apart from that, I believe the macros will work without changes. The output will be something like:
Test Data 1.xlsx
Source
This is a source workbook
Test Data 2.xlsx
Sheet1
Required name "Trans qty" not found
Required name "Name" not found
Required name "Color" not found
Required name "Name" not found
Source
This is a source workbook
Test Data 3.xlsx
Sheet1
Required name "Style no" not found
Required name "Item number" not found
Required name "Trans qty" not found
Required name "Name" not found
Required name "Color" not found
Required name "Size" not found
Required name "Name" not found
Sheet2
Required name "Style no" not found
Required name "Item number" not found
Required name "Trans qty" not found
Required name "Name" not found
Required name "Color" not found
Required name "Size" not found
Required name "Name" not found
Source
This is a source workbook
Test Data 4.xlsx
Source
Required name "Name" not found
Required name "Name" not found
This is not a source workbook
Test Data 5.xlsx
Source
Required name "Style no" not found
This is not a source workbook
The new code is:
Sub TestFindSrcWsht()
Const RowMapDataFirst As Long = 2
Dim DataMap As Variant
Dim Filename As String
Dim Path As String
Dim RowMapLast As Long
Dim WbkSrc As Workbook
Dim WshtMap As Worksheet
Application.ScreenUpdating = False
Set WshtMap = Worksheets("Data Mapping")
With WshtMap
RowMapLast = .Cells(Rows.Count, 1).End(xlUp).Row
DataMap = .Range(.Cells(RowMapDataFirst, 1), .Cells(RowMapLast, 2)).Value
' Note 1: the lower bounds of a variant loaded from a range are always one
' regardless of the location of the range within the worksheet.
' Note 2: I have loaded DataMap starting at row 2 because RowMapDataFirst
' equals 2. If you want the table to start at a different row, just
' change the value of RowMapDataFirst.
End With
Path = ThisWorkbook.Path & "\"
Filename = Dir$(Path & "*.xls*")
Do While Filename <> "" And Filename <> ThisWorkbook.Name
Set WbkSrc = Workbooks.Open(Path & Filename, , True)
If FindSrcWsht(WbkSrc, DataMap) Is Nothing Then
Debug.Print " This is not a source workbook"
Else
Debug.Print " This is a source workbook"
End If
WbkSrc.Close
Filename = Dir$
Loop
Application.ScreenUpdating = True
End Sub
Function FindSrcWsht(ByRef WbkSrc As Workbook, ByRef DataMap As Variant) As Worksheet
' Return a reference to the worksheet within WbkSrc that has all the columns
' required by DataMap for a source worksheet. Return Nothing if no such
' worksheet found.
' WbkSrc A workbook that might be a source workbook
' DataMap A Variant holding a 2D table with 2 columns and 1 row per
' destination column. Table column 1 contains the names of the
' destination columns in output sequence with table row N
' holding the name of destination column N. Table column 2 of
' row N contains the name of the source column, if any, for the
' destination column whose name is in table column 1 or row N.
' Column 1 of DataMap is not used by this routine.
' Column 2 of DataMap contains column names that must exist within a source
' worksheet.
' Workbook WbkSrc can contain one or more worksheets. Match the column names
' within each worksheet against the column names in column 2 of DataMap until
' a worksheet is found with all required columns. Retun a reference to that
' worksheet. Return Nothing if no satisfactory worksheet is found.
' 19Apr20 Coded
Dim ColSrcCrnt As Long
Dim ColSrcLast As Long
Dim InxNR As Long
Dim InxWsht As Long
Dim MatchAll As Boolean
Dim MatchSingleFound As Boolean
Dim NamesRequired As Collection
Dim RowDataCrnt As Long
Dim RowSrcLast As Long
Set NamesRequired = New Collection
' Create collection of the column names required in a worksheet
For RowDataCrnt = 1 To UBound(DataMap, 1)
If DataMap(RowDataCrnt, 2) <> "" Then
NamesRequired.Add DataMap(RowDataCrnt, 2)
End If
Next
With WbkSrc
Debug.Print .Name ' Name of workbook
' For each worksheet, attempt match on every required name
For InxWsht = 1 To .Worksheets.Count
With .Worksheets(InxWsht)
Debug.Print " " & .Name ' Name of worksheet
Call FindLastRowCol(WbkSrc.Worksheets(InxWsht), RowSrcLast, ColSrcLast)
MatchAll = True ' Assume all names matched until name not found
For InxNR = 1 To NamesRequired.Count
MatchSingleFound = False ' Have not yet matched NamesRequired(InxNR)
For ColSrcCrnt = 1 To ColSrcLast
If .Cells(1, ColSrcCrnt).Value = NamesRequired(InxNR) Then
' Have a case sensitive match between required name and column heading
'Debug.Assert False
MatchSingleFound = True
Exit For
End If
Next
If Not MatchSingleFound Then
' NamesRequired(InxNR) not matched against any column heading
'Debug.Assert False
Debug.Print " Required name """ & NamesRequired(InxNR) & """ not found"
MatchAll = False
End If
Next
If MatchAll Then
' Every required name matched against this worksheet
Set FindSrcWsht = WbkSrc.Worksheets(InxWsht)
Exit Function
End If
End With
Next
End With
' If get here, none of the worksheets contains every required name
Set FindSrcWsht = Nothing
End Function
Upvotes: 1
Reputation: 12413
Solution
The code below works against my test workbooks. I believe I have handled your offsets correctly. Lines starting '### require amendment for your system. I believe the remainder of the macros will work without change. Copy this code to a new module, adjust as required and try it out. The comments within the code should explain everything. If the comments are not adequate, ask questions and I will update the comments.
Option Explicit
' * I have a naming system for my constants and variables that I have used for years.
' Having a system means I can look at code I wrote years ago and recognise the
' constants and variables. If you do not like my system, design your own but do
' code without a system if you wih to easily maintain old code.
' * Col, Inx, Row, Wsht and Wbk identify the name as relating to a column, index,
' row, worksheet or workbook.
' * Col, Row, Wsht and Wbk are followed by Xxx which is a code or abbreviation
' identifying the worksheet or workbook.
' * Values for Xxx in these macros are:
' Map for the Data mapping table.
' Src for a source workbook or worksheet
' Dest for the destination worksheet
' This for the workbook holding the macros and the destination worksheet.
' * Next, Crnt, Last destinguish different columns or rows if necessary.
' * InxX is an index for a 1D array or collection. Since the use is ArrayName(InxX),
' X is usually a single letter.
' * ColXxxDataFirst and RowXxxDataFirst define the first data column and row
' within worksheet Xxx. This allows the number of header columns or rows to be
' changed with minimum effect on the code.
' * Fldr identifie a folder. If the code accesses both Outlook and disc folders,
' the prefixes OutFldr and DscFldr are used instead.
' If the purpose of a variable does not fit within the above system, I use the
' name of the purpose for the variable. For example DataMap.
' These constants identify which worksheet within this workbook holds the
' mapping table and its position within that worksheet.
' Cells(RowMapDataFirst, ColMapDataFirst) is the top left cell of the table
' excluding any column headings.
' Cells(RowMapDataFirst, ColMapDataFirst+1)
' ### Adjust as required.
Const ColMapDataFirst As Long = 1
Const RowMapDataFirst As Long = 2
Const WshtDataName As String = "Data Mapping"
' Columns to the left of ColDestDataFirst and above RowDestDataFirst-1 are reserved.
' The data mapping table specifies the first destination column as 1. ColDestDataFirst
' specifies the true first destination column. When moving source columns to the
' destination worksheet, the destination column is adjusted for ColDestDataFirst.
' If at the start of this routine, the last row in the destination worksheet is less
' than RowDestDataFirst-1, the column headings will be written to RowDestDataFirst-1 and
' the first data will be written to RowDestDataFirst. If at the start of this routine,
' the last row in the destination worksheet is not less than RowDestDataFirst-1, the
' column headings will not be written to RowDestDataFirst-1 and the first data will be
' written to RowDestLast+1
' ### Adjust as required.
Const ColDestDataFirst As Long = 3
Const RowDestDataFirst As Long = 18
Const WshtDestName As String = "Destination"
' First data row of a source worksheet.
' Note this code does not allow for source worksheets having different
' numbers of heding rows.
Const RowSrcDataFirst As Long = 2
Sub CollectAndTransform()
' Collects data from all the source workbooks in the source folder and saves
' that data to the destination worksheet within this workbook.
' The Source folder is defined by FldrSrc.
' A source workbook is any workbook within FldrSrc that contains a worksheet which
' has all the required source columns.
' The data is saved by column with the new column sequence defined by the Data mapping
' table.
' The position of the Data mapping table is defined by ColDataDataFirst,
' RowDataDataFirst and WshtDataName. See below where the data map is loaded to
' DataMap for more information. The format of the data map is defined at the top of
' sub DecodeMapping().
' 20Apr20 Coded.
Dim ColDestCrnt As Long
Dim ColDestLast As Long
Dim ColsDestName() As String
Dim ColSrcLast As Long
Dim ColsSrcForDest() As Long
Dim DataMap As Variant
Dim Filename As String
Dim FldrSrc As String
Dim RngSrc As Range
Dim RowDestLast As Long
Dim RowMapLast As Long
Dim RowSrcLast As Long
Dim WbkSrc As Workbook
Dim WbkThis As Workbook
Dim WshtDest As Worksheet
Dim WshtMap As Worksheet
Dim WshtSrc As Worksheet
Application.ScreenUpdating = False
Set WbkThis = ThisWorkbook
' ### Replace by FldrSrc = "C:\Users\" & Environ(“UserName”) & "\Desktop\Test M" & "\"
FldrSrc = ThisWorkbook.Path & "\"
Set WshtMap = WbkThis.Worksheets("Data Mapping")
Set WshtDest = WbkThis.Worksheets(WshtDestName)
With WshtMap
RowMapLast = .Cells(Rows.Count, 1).End(xlUp).Row
DataMap = .Range(.Cells(RowMapDataFirst, 1), .Cells(RowMapLast, 2)).Value
' Note 1: the lower bounds of a variant loaded from a range are always one
' regardless of the location of the range within the worksheet.
' Note 2: I have loaded DataMap starting at row 2 because RowMapDataFirst
' equals 2. If you want the table to start at a different row, just
' change the value of RowMapDataFirst.
End With
Filename = Dir$(FldrSrc & "*.xls*")
Do While Filename <> "" And Filename <> ThisWorkbook.Name
Set WbkSrc = Workbooks.Open(FldrSrc & Filename, , True)
Set WshtSrc = FindSrcWsht(WbkSrc, DataMap)
If WshtSrc Is Nothing Then
Debug.Print WbkSrc.Name & " is is not a source workbook"
Else
' This workkbook is a source workbook
' Call DecodeMapping here in case column sequence differs between workbooks.
Call DecodeMapping(WshtSrc, DataMap, ColsDestName, ColsSrcForDest)
If RowDestLast < RowDestDataFirst - 1 Then
' This is the first source workbook so the destination worksheet
' has not been checked
Call FindLastRowCol(WshtDest, RowDestLast, ColDestLast)
If RowDestLast < RowDestDataFirst - 1 Then
' No data has been written to the destination worksheet
' Output column headings
With WshtDest
.Range(.Cells(RowDestDataFirst - 1, ColDestDataFirst), _
.Cells(RowDestDataFirst - 1, ColDestDataFirst + _
UBound(ColsDestName) - 1)).Value = ColsDestName
End With
RowDestLast = RowDestDataFirst - 1
End If
End If
Call FindLastRowCol(WshtSrc, RowSrcLast, ColSrcLast)
' For each destination column which is not to be left empty,
' copy the appropriate source column to it.
' If ColDestCrnt(N) <> 0, source column ColDestCrnt(N) is to be copied
' to destination column N + ColDestDatFirst - 1
' A source column is from RowSrcDataFirst to RowSrcLast
For ColDestCrnt = 1 To UBound(ColsSrcForDest)
If ColsSrcForDest(ColDestCrnt) <> 0 Then
With WshtSrc
Set RngSrc = .Range(.Cells(RowSrcDataFirst, ColsSrcForDest(ColDestCrnt)), _
.Cells(RowSrcLast, ColsSrcForDest(ColDestCrnt)))
End With
Debug.Print WbkSrc.Name & "." & WshtSrc.Name & "Range("; RngSrc.Address & " ) -> " & _
WshtDest.Cells(RowDestLast + 1, ColDestCrnt + ColDestDataFirst - 1).Address
RngSrc.Copy WshtDest.Cells(RowDestLast + 1, ColDestCrnt + ColDestDataFirst - 1)
End If
Next
RowDestLast = RowDestLast + RngSrc.Rows.Count ' Advance to bottom of data just copied
' ready for next source workbook
End If
WbkSrc.Close
Filename = Dir$
Loop
WshtDest.Columns.AutoFit
Application.ScreenUpdating = True
End Sub
Sub DecodeMapping(ByRef WshtSrc As Worksheet, ByRef DataMap As Variant, _
ByRef ColsDestName() As String, ByRef ColsSrcForDest() As Long)
' Decodes a table mapping source column names to destination column names.
' Create an array of column headings for the destination worksheet.
' Locates the source column names within the source worksheet and creates an
' array mapping the source column numbers to the destination columns.
' WshtSrc The source worksheet
' DataMap A Variant holding a 2D table with 2 columns and 1 row per
' destination column. Table column 1 contains the names of the
' destination columns in output sequence with table row N
' holding the name of destination column N. Table column 2 of
' row N contains the name of the source column, if any, for the
' destination column whose name is in table column 1 or row N.
' ColsDestNames On exit, the names of the destination columns in an array
' ready to be loaded to the header row of the destination
' worksheet.
' ColsSrcForDest On exit, ReDimmed to (1 To M) where M is the number of columns
' in the destination worksheet. If ColsSrcForDest(P) = 0,
' destination column P is left blank. If ColsSrcForDest(P) = Q,
' source column Q is to be copied to destination column P.
' 18Apr20 Coded.
Dim ColSrcCrnt As Long
Dim ColSrcLast As Long
Dim Found As Boolean
Dim RowMapCrnt As Long
Dim RowSrcLast As Long
ReDim ColsDestName(1 To UBound(DataMap, 1))
' Build array that can be used to create heading row for destination worksheet
For RowMapCrnt = 1 To UBound(DataMap, 1)
ColsDestName(RowMapCrnt) = DataMap(RowMapCrnt, 1)
Next
Call FindLastRowCol(WshtSrc, RowSrcLast, ColSrcLast) ' Only need ColSrcLast
' Size ColsSrcForDest so there is one entry per destination column
' The entries are initialised to zeros.
ReDim ColsSrcForDest(1 To UBound(DataMap))
' There are faster methods of achieving the source to destination mapping
' than these nested loops but the VBA is more complicated. If there are
' so many source and destination columns that this is slow, I will recode.
' Match each value in column 2 of DataMap against a column heading in
' worksheet WshtSrc. When a match is found, record the match in ColsSrcForDest.
With WshtSrc
For RowMapCrnt = 1 To UBound(DataMap)
If DataMap(RowMapCrnt, 2) <> "" Then
'Debug.Assert False
Found = False
For ColSrcCrnt = 1 To ColSrcLast
If .Cells(1, ColSrcCrnt).Value = DataMap(RowMapCrnt, 2) Then
' Warning: this a case sensitive match
'Debug.Assert False
ColsSrcForDest(RowMapCrnt) = ColSrcCrnt
Found = True
Exit For
End If
Next
If Not Found Then
Debug.Assert False
Call MsgBox("Source column name """ & DataMap(RowMapCrnt, 2) & _
""" appears in the DataMap but is not a column " & _
"heading in worksheet """ & WshtSrc.Name & """", vbOKOnly)
End ' Exit this macro and calling macro.
End If
End If
Next
End With
End Sub
Function FindSrcWsht(ByRef WbkSrc As Workbook, ByRef DataMap As Variant) As Worksheet
' Return a reference to the worksheet within WbkSrc that has all the columns
' required by DataMap for a source worksheet. Return Nothing if no such
' worksheet found.
' WbkSrc A workbook that might be a source workbook
' DataMap A Variant holding a 2D table with 2 columns and 1 row per
' destination column. Table column 1 contains the names of the
' destination columns in output sequence with table row N
' holding the name of destination column N. Table column 2 of
' row N contains the name of the source column, if any, for the
' destination column whose name is in table column 1 or row N.
' Column 1 of DataMap is not used by this routine.
' Column 2 of DataMap contains column names that must exist within a source
' worksheet.
' Workbook WbkSrc can contain one or more worksheets. Match the column names
' within each worksheet against the column names in column 2 of DataMap until
' a worksheet is found with all required columns. Retun a reference to that
' worksheet. Return Nothing if no satisfactory worksheet is found.
' 19Apr20 Coded
Dim ColSrcCrnt As Long
Dim ColSrcLast As Long
Dim InxNR As Long
Dim InxWsht As Long
Dim MatchAll As Boolean
Dim MatchSingleFound As Boolean
Dim NamesRequired As Collection
Dim RowMapCrnt As Long
Dim RowSrcLast As Long
Set NamesRequired = New Collection
' Create collection of the column names required in a worksheet
For RowMapCrnt = 1 To UBound(DataMap, 1)
If DataMap(RowMapCrnt, 2) <> "" Then
NamesRequired.Add DataMap(RowMapCrnt, 2)
End If
Next
With WbkSrc
'Debug.Print .Name ' Name of workbook
' For each worksheet, attempt match on every required name
For InxWsht = 1 To .Worksheets.Count
With .Worksheets(InxWsht)
'Debug.Print " " & .Name ' Name of worksheet
Call FindLastRowCol(WbkSrc.Worksheets(InxWsht), RowSrcLast, ColSrcLast)
MatchAll = True ' Assume all names matched until name not found
For InxNR = 1 To NamesRequired.Count
MatchSingleFound = False ' Have not yet matched NamesRequired(InxNR)
For ColSrcCrnt = 1 To ColSrcLast
If .Cells(1, ColSrcCrnt).Value = NamesRequired(InxNR) Then
' Have a case sensitive match between required name and column heading
'Debug.Assert False
MatchSingleFound = True
Exit For
End If
Next
If Not MatchSingleFound Then
' NamesRequired(InxNR) not matched against any column heading
'Debug.Assert False
'Debug.Print " Required name """ & NamesRequired(InxNR) & """ not found"
MatchAll = False
End If
Next
If MatchAll Then
' Every required name matched against this worksheet
Set FindSrcWsht = WbkSrc.Worksheets(InxWsht)
Exit Function
End If
End With
Next
End With
' If get here, none of the worksheets contains every required name
Set FindSrcWsht = Nothing
End Function
Upvotes: 1