I would like to copy data from one sheet to another.
I put the range that I want to copy into an array (LookupSource) because it's faster to work on arrays than looping through cells.
After filling my two dimensional array (LookupSource), I would like to keep only some records based on critieria (Column A = 10000), so I am trying to copy from LookupSource, the rows that fetch this criteria to the two dimensional array (DataToCopy) which will be copied to the destination sheet.
My problem is that I am not able to do that because as it seems I am not able to make a dynamic resize for the first dimension (rows) of the second array (DataToCopy).
Any Idea how to fill DataToCopy from LookupSource based on my condition ?
The error "index out of range" that I am getting is at the Line : ReDim Preserve DataToCopy(1 to j, 1 to 6)
not at first time, but on second time that I enter the For loop after the Next I
I suppose it's because the J is variable and I am not allowed to change the first dimension of the array.
How to deal with that ?
Any better Idea from what I am doing ?
to give you an example here is a small part of the sheet that I want to copy (I took only 8 rows, but in real there thousands). I want to copy only the rows that have 10000 in column A.
Here is my code
Dim LookupSource as Variant
Dim DataToCopy() As Variant
Dim i As Long
Dim j As Long
With MySheet
'MyRange is a defined name that reprensent column A, B, C, D, E, F
LookupSource = .Range(.Range("MyRange")(1, 1), .Range("MyRange")(8, 6)).Value2
j = 1
For i = LBound(LookupSource) To UBound(LookupSource)
If LookupSource(i, 1) = 10073 Then
ReDim Preserve DataToCopy(1 to j, 1 to 6)
DataToCopy(j, 1) = LookupSource(i, 1)
DataToCopy(j, 2) = LookupSource(i, 2)
DataToCopy(j, 3) = LookupSource(i, 3)
DataToCopy(j, 4) = LookupSource(i, 4)
DataToCopy(j, 5) = LookupSource(i, 5)
DataToCopy(j, 6) = LookupSource(i, 6)
j = j + 1
End If
Next i
end with
How to overcome the restrictions of ReDim Preserve in multidimensional arrays
As mentioned by #ScottCraner, a ReDim Preserve can change only the last dimension of a given (datafield) array.
Therefore trying to resize a 2-dimensional array's first dimension (="rows") will fail.
However you can overcome this inconvenience applying the relatively unknown filtering capability of Application.Index() (c.f. section [2]) and profit from the additional bonus of less loops.
Further reading: see Some pecularities of the Application.Index() function
Sub GetRowsEqual10000()
With Sheet1
Dim lastRow As Long: lastRow = .Range("A" & .Rows.Count).End(xlUp).Row
Dim rng As Range: Set rng = .Range("A2:F" & lastRow)
End With
'[1] get data
Dim data: data = rng
'[2] rearrange data via Application.Index() instead ReDim Preserve plus loops
data = Application.Index(data, ValidRows(data, Condition:=10000), Array(1, 2, 3, 4, 5, 6))
End Sub
Help function ValidRows()
Function ValidRows(arr, Condition) As Variant
'Purpose: a) check condition (e.g. values equalling 10000) and b) get valid row numbers c) in a 2-dim vertical array
ReDim tmp(1 To UBound(arr)) ' provide for 1-based 2-dim array
Dim i As Long, ii As Long
For i = 1 To UBound(arr) ' loop through 1st "column"
If arr(i, 1) = Condition Then ' a) check condition
ii = ii + 1: tmp(ii) = i ' b) collect valid row numbers
End If
Next i
ReDim Preserve tmp(1 To ii) ' resize tmp array (here the 1st dimension is also the last one:)
ValidRows = Application.Transpose(tmp) ' c) return transposed result as 2-dim array
End Function
Edit due to comment (2020-04-22)
Short hints to the most frequent use of Application.Index():
Frequently the Application.Index() function is used to
get an entire row or column array out of a 2-dim array without need to loop.
Accessing your 1-based 2-dimensional datafield array like that requires to
indicate a single row or column number and
to set the neighbour argument column or row number to 0 (zero), respectively which might result in e.g.
Dim horizontal, vertical, RowNumber As Long, ColumnNumber As Long
RowNumber = 17: ColumnNumber = 4
horizontal = Application.Index(data, RowNumber, 0)
vertical = Application.Index(data, 0, ColumnNumber)
(Addressing a single array element will be done directly, however via data(i,j)
instead of a theoretical Application.Index(data, i, j))
How to use Application.Index() for restructuring/filtering purposes:
In order to profit from the advanced possibilities of Application.Index() you
need to pass not only the array name (e.g. data), but the row|column arguments as Arrays, e.g.
data = Application.Index(data, Application.Transpose(Array(15,8,10)), Array(1, 2, 3, 4, 5, 6))
Note that the rows parameter becomes a "vertical" 2-dim array by transposition, where Array(15,8,10)
would even change the existing row order
(in the example code above this is done in the last code line within the ValidRows() function).
The columns argument Array(1,2,3,4,5,6) on the other hand remains "flat" or "horizontal" and
allows to get all existing column values as they are.
So you eventually you are receiving any data elements within the given element indices
(think them as coordinates in a graphic).
Range Lookup Function
The Code
Option Explicit
'START ****************************************************************** START'
' Purpose: Filters a range by a value in a column and returns the result '
' in an array ready to be copied to a worksheet. '
'******************************************************************************'
Function RangeLookup(LookUpValue As Variant, LookupRange As Range, _
Optional LookupColumn As Long = 1) As Variant
Dim LookUpArray As Variant ' LookUp Array
Dim DataToCopy As Variant ' DataToCopy (RangeLookup) Array
Dim countMatch As Long ' DataToCopy (RangeLookUp) Rows Counter
Dim r As Long, c As Long ' Row and Column Counters
' Check the arguments.
Select Case VarType(LookUpValue)
Case 2 To 8, 11, 17
Case Else: Exit Function
End Select
If LookupRange Is Nothing Then Exit Function
If LookupColumn < 1 Or LookupColumn > LookupRange.Columns.Count _
Then Exit Function
' Copy values of Lookup Range to Lookup Array.
LookUpArray = LookupRange
' Task: Count the number of values containing LookUp Value
' in LookUp Column of LookUp Array which will be
' the number of rows in DataToCopy Array.
' The number of columns in both arrays will be the same.
' Either:
' Count the number of values containing LookUp Value.
countMatch = Application.WorksheetFunction _
.CountIf(LookupRange.Columns(LookupColumn), LookUpValue)
' Although the previous looks more efficient, it should be tested.
' ' Or:
' ' Loop through rows of LookUpArray.
' For r = 1 To UBound(LookUpArray)
' ' Check if the value in current row in LookUp Column
' ' is equal to LookUp Value.
' If LookUpArray(r, LookupColumn) = LookUpValue Then
' ' Increase DataCopy Rows Counter.
' countMatch = countMatch + 1
' End If
' Next r
' Check if no match was found.
If countMatch = 0 Then Exit Function
' Task: Write the matching rows in LookUp Array to DataToCopy Array.
' Resize DataToCopy Array to DataToCopy Rows counted in the previous
' For Next loop and the number of columns in Lookup Array.
ReDim DataToCopy(1 To countMatch, 1 To UBound(LookUpArray, 2))
' Reset DataToCopy Rows Counter.
countMatch = 0
' Loop through rows of LookUp Array.
For r = 1 To UBound(LookUpArray)
' Check if the value in current row in LookUp Column
' is equal to LookUp Value.
If LookUpArray(r, LookupColumn) = LookUpValue Then
' Increase DataCopy Rows Counter.
countMatch = countMatch + 1
' Loop through columns of LookUp (DataToCopy) Array.
For c = 1 To UBound(LookUpArray, 2)
' Write the current value of LookUp Array to DataToCopy Array.
DataToCopy(countMatch, c) = LookUpArray(r, c)
Next c
End If
Next r
' Write values from DataToCopy Array to RangeLookup Array.
RangeLookup = DataToCopy
End Function
'END ********************************************************************** END'
You should use it e.g. like this:
Sub TryRangeLookup()
Dim LookupRange As Range
Dim DataToCopy As Variant
With MySheet
'MyRange is a defined name that reprensent column A, B, C, D, E, F
Set LookupRange = .Range(.Range("MyRange")(1, 1), _
.Range("MyRange")(8, 6)).Value2
End With
RangeLookUp 10073, DataCopy
If Not IsArray(DataToCopy) Then
MsgBox "No data found.": Exit Sub ' or whatever...
Endif
' Continue with code...
End Sub
Related
I am new to VBA and am trying to copy the column from Row 2 onwards where the column header (in Row 1) contains a certain word- "Unique ID".
Currently what I have is:
Dim lastRow As Long
lastRow = ActiveWorkbook.Worksheets("Sheets1").Range("A" & Rows.Count).End(xlUp).Row
Sheets("Sheets1").Range("D2:D" & lastRow).Copy
But the "Unique ID" is not always in Column D
You can try following code, it loops through first row looking for a specified header:
Sub CopyColumnWithHeader()
Dim i As Long
Dim lastRow As Long
For i = 1 To Columns.Count
If Cells(1, i) = "Unique ID" Then
lastRow = Cells(Rows.Count, i).End(xlUp).Row
Range(Cells(2, i), Cells(lastRow, i)).Copy Range("A2")
Exit For
End If
Next
End Sub
When you want to match info in VBA you should use a dictionary. Additionally, when manipulating data in VBA you should use arrays. Although it will require some learning, below code will do what you want with minor changes. Happy learning and don't hesitate to ask questions if you get stuck:
Option Explicit
'always add this to your code
'it will help you to identify non declared (dim) variables
'if you don't dim a var in vba it will be set as variant wich will sooner than you think give you a lot of headaches
Sub DictMatch()
'Example of match using dictionary late binding
'Sourcesheet = sheet1
'Targetsheet = sheet2
'colA of sh1 is compared with colA of sh2
'if we find a match, we copy colB of sh1 to the end of sh2
'''''''''''''''''
'Set some vars and get data from sheets in arrays
'''''''''''''''''
'as the default is variant I don't need to add "as variant"
Dim arr, arr2, arr3, j As Long, i As Long, dict As Object
'when creating a dictionary we can use early and late binding
'early binding has the advantage to give you "intellisense"
'late binding on the other hand has the advantage you don't need to add a reference (tools>references)
Set dict = CreateObject("Scripting.Dictionary") 'create dictionary lateB
dict.CompareMode = 1 'textcompare
arr = Sheet1.Range("A1").CurrentRegion.Value2 'load source, assuming we have data as of A1
arr2 = Sheet2.Range("A1").CurrentRegion.Value2 'load source2, assuming we have data as of A1
'''''''''''''''''
'Loop trough source, calculate and save to target array
'''''''''''''''''
'here we can access each cell by referencing our array(<rowCounter>, <columnCounter>
'e.g. arr(j,i) => if j = 1 and i = 1 we'll have the values of Cell A1
'we can write these values anywhere in the activesheet, other sheet, other workbook, .. but to limit the number of interactions with our sheet object we can also create new, intermediant arrays
'e.g. we could now copy cel by cel to the new sheet => Sheets(arr(j,1).Range(... but this would create significant overhead
'so we'll use an intermediate array (arr3) to store the results
'We use a "dictionary" to match values in vba because this allows to easily check the existence of a value
'Together with arrays and collections these are probably the most important features to learn in vba!
For j = 1 To UBound(arr) 'traverse source, ubound allows to find the "lastrow" of the array
If Not dict.Exists(arr(j, 1)) Then 'Check if value to lookup already exists in dictionary
dict.Add Key:=arr(j, 1), Item:=arr(j, 1) 'set key if I don't have it yet in dictionary
End If
Next j 'go to next row. in this simple example we don't travers multiple columns so we don't need a second counter (i)
'Before I can add values to a variant array I need to redim it. arr3 is a temp array to store matching col
'1 To UBound(arr2) = the number of rows, as in this example we'll add the match as a col we just keep the existing nr of rows
'1 to 1 => I just want to add 1 column but you can basically retrieve as much cols as you want
ReDim arr3(1 To UBound(arr2), 1 To 1)
For j = 1 To UBound(arr2) 'now that we have all values to match in our dictionary, we traverse the second source
If dict.Exists(arr2(j, 1)) Then 'matching happens here, for each value in col 1 we check if it exists in the dictionary
arr3(j, 1) = arr(j, 2) 'If a match is found, we add the value to find back, in this example col. 2, and add it to our temp array (arr3).
'arr3(j, 2) = arr(j, 3) 'As explained above, we could retrieve as many columns as we want, if you only have a few you would add them manually like in this example but if you have many we could even add an additional counter (i) to do this.
End If
Next j 'go to the next row
'''''''''''''''''
'Write to sheet only at the end, you could add formatting here
'''''''''''''''''
With Sheet2 'sheet on which I want to write the matching result
'UBound(arr2, 2) => ubound (arr2) was the lastrow, the ubound of the second dimension of my array is the lastcolumn
'.Cells(1, UBound(arr2, 2) + 1) = The startcel => row = 1, col = nr of existing cols + 1
'.Cells(UBound(arr2), UBound(arr2, 2) + 1)) = The lastcel => row = number of existing rows, col = nr of existing cols + 1
.Range(.Cells(1, UBound(arr2, 2) + 1), .Cells(UBound(arr2), UBound(arr2, 2) + 1)).Value2 = arr3 'write target array to sheet
End With
End Sub
Example data set
A B c D E F G H I J K L M N O P
-10 5 16 23 8 2 6 3162625 -10 5 16 23 8 2 6 3162626
Desired output
A B C D E F G H I J K L M N O P
-10 5 16 23 8 2 6 3162625
-10 5 16 23 8 2 6 3162626
Constant is -10 and i need the 7 columns after it
using VBA I can transfer column A to H to another sheet, but i can't get the VBA to move to Column I, Q etc etc
The VBA I have is
Sub search_and_extract_singlecriteria()
'1.
'2.
'3.
Dim datasheet As Worksheet
Dim reportsheet As Worksheet
Dim recordid As String
Dim finalrow As Integer
Dim i As Integer
Set datasheet = Sheet1
Set reportsheet = Sheet2
recordid = "-46" 'reportsheet.Range("B2").Value
'reportsheet.Range("A1:L100").ClearContents
datasheet.Select
finalrow = Cells(Rows.Count, 1).End(xlUp).Row
For i = 1 To finalrow
If Cells(i, 1) = recordid Then
Range(Cells(i, 9), Cells(i, 17)).Copy
reportsheet.Select
Range("A200").End(xlUp).Offset(1, 0).PasteSpecial xlPasteFormulasAndNumberFormats
datasheet.Select
End If
Next i
reportsheet.Select
Range("B2").Select
End Sub
Simple copy and paste is not an option as on one row, the 8 column sets repeat over a 1000 columns. each row, has varying column lengths. i will end up with 300k plus rows across 8 columns A:H if this can be done
Any suggestions would be greatly appreciated.
Try this. Have added some comments to explain.
If it's slow, more efficient to use arrays.
Sub x()
Dim r As Range
application.screenupdating=false
Set r = Sheet1.Range("A1").Resize(, 8) 'set starting range 1 x 8
Do Until IsEmpty(r(1)) 'keep doing this until first cell is empty
r.Copy Sheet2.Range("A" & Rows.Count).End(xlUp)(2) 'copy to first blank cell in A sheet2
Set r = r.Offset(, 8) 'move copy range along by 8 cells to the right
Loop
application.screenupdating=true
End Sub
Array vs Range
Option Explicit
'START ****************************************************************** START'
' Title: Search and Extract Single Criteria '
' Purpose: In a specified Data Worksheet, each non-empty row contains '
' an unknown number of consecutive data sets of a specified '
' size (columns). '
' By looping through each row of Data Worksheet, copies each '
' data set to another specified Report Worksheet one below '
' another, starting from a specified cell range. '
'******************************************************************************'
Sub search_and_extract_singlecriteria()
' 10s for 1280 cols and 3000 rows = 480000 rows in Report Sheet
Const Noc As Long = 8 ' Size of Data Set (Number of Columns)
' = Number of Columns in Report Array
Const strRR As String = "B2" ' Report First Cell Range Address
Dim wsD As Worksheet: Set wsD = Sheet1 ' Data Sheet
Dim wsR As Worksheet: Set wsR = Sheet2 ' Report Sheet
Dim rng As Range ' Last Non-Empty Cell in the Last Non-Empty Row,
' Non-Empty Range (both in Data Sheet)
Dim vntD As Variant ' Data Array (2D 1-based)
Dim vntC As Variant ' Count Array (1D 1-based)
Dim vntR As Variant ' Report Array (2D 1-based)
Dim Nor As Long ' Number of Data Sets
' = Number of Rows in Report Array
Dim i As Long ' Data/Count Array Rows Counter
Dim j As Long ' Data Array Columns Counter
Dim k As Long ' Count Array Values Counter
Dim m As Long ' Report Array Rows Counter
' IN DATA SHEET
' Note: It is assumed that Data Sheet contains ONLY Data Sets.
' By defining the Last Non-Empty Cell in the Last Non-Empty Row
' using the Find method, check if the sheet is not empty.
Set rng = wsD.Cells.Find("*", wsD.Cells(wsD.Rows.Count, wsD.Columns.Count), _
xlFormulas, , xlByRows)
If rng Is Nothing Then Exit Sub
' Define Non-Empty Range on Data Sheet.
Set rng = wsD.Range(wsD.Cells(rng.Row, wsD.Cells.Find("*", _
wsD.Cells(wsD.Rows.Count, wsD.Columns.Count), , , xlByColumns).Column), _
wsD.Cells(wsD.Cells.Find("*", , , , xlByRows, xlPrevious).Row, _
wsD.Cells.Find("*", , , , xlByColumns, xlPrevious).Column))
' Write values of Non-Empty Range on Data Sheet to Data Array.
vntD = rng
' Release object variables. Necessary data is in Data Array (vntD).
Set rng = Nothing
Set wsD = Nothing
' IN ARRAYS
' Task: Calculate Number of Rows in Report Array and populate Count Array.
' Resize Count Array (vntC) to number of rows of Data Array (vntD).
ReDim vntC(1 To UBound(vntD))
' Loop through rows (1st dimension) of Data Array (vntD).
For i = 1 To UBound(vntD)
' Loop through every Noc-th column (2nd dimension) of Data Array (vntD).
For j = 1 To UBound(vntD, 2) Step Noc
' Check if value of current element in Data Array (vntD) is <> "".
If vntD(i, j) <> "" Then
' Value of current element in Data Array (vntD) is <> "".
' Increase Count Array Value (Count of Data Sets in current row
' of Data Array).
k = k + 1
' Increase Number of Rows in Report Array
' (Total Count of Data Sets).
Nor = Nor + 1
Else
' Value of current element in Data Array (vntD) is = "".
' The following will leave the current element in Count Array
' empty, i.e. 0 which becomes obvious only later in:
' "If vntC(i) > 0 Then...".
Exit For
End If
Next
' Write current Count Array Value (k) to current element
' of Count Array (vntC).
' Note: The i-th row in Data Array contains k Data Sets.
vntC(i) = k
' Reset Count Array Values Counter.
k = 0
Next
' Remarks: Count Array (vntC) has the same number of elemnts
' as Data Array (vntD) has rows. Each value in Count Array (vntC)
' respresents the number of Data Sets per row of Data Array (vntD).
' The implementation of Count Array (vntC) makes it possible
' to write the last loop as a For Next loop:
' "For j = (k - 1) * Noc + 1 To (k - 1) * Noc + Noc...",
' without checking if there are "" values, because it has
' already been checked previously in:
' "If vntD(i, j) <> "" Then)...".
' Task: Define and populate Report Array.
' Resize Report Array (vntR) to rows defined by Number of Data Sets (Nor)
' and columns specified by (Column) Size of Data Set (Noc).
ReDim vntR(1 To Nor, 1 To Noc)
' Loop through rows (1st dimension) of Data Array (vntD).
For i = 1 To UBound(vntD)
' Check if the value in the same row (i) in Count Array (vntC) is > 0.
If vntC(i) > 0 Then
' Value in the same row (i) in Count Array (vntC) is > 0.
' Loop through Data Sets from Data Array.
For k = 1 To vntC(i)
' Increase Report Array Rows Counter (m).
m = m + 1
' Loop through columns (j) of current Data Set.
For j = (k - 1) * Noc + 1 To (k - 1) * Noc + Noc
' Write value of current element of Data Array (Set) to
' current element of Report Array.
vntR(m, j - (k - 1) * Noc) = vntD(i, j)
Next
Next
'Else
' Value in the same row (i) in Count Array (vntC) is NOT > 0 i.e.
' skipping (No Data Set in) current row of Data Array (vntD).
End If
Next
' IN REPORT SHEET
' Copy values of Report Array to Report Range defined by the specified
' Report First Cell Range Address (strRR) in specified Report Sheet (wsR)
' and the size (rows and columns) of Report Array (vntR).
wsR.Range(strRR).Resize(UBound(vntR), UBound(vntR, 2)) = vntR
End Sub
'END ********************************************************************** END'
I have the first name in column A and the last name in Column B and I need to combine them into just column A. Also not sure if I need to check this in the code but some of the cells are empty with now names. I have tried many things but they all want me to pull the two and enter them into a different or 3rd column. But I need to put them into column A.
This is the code I have and it keeps giving me the merge error.
With Worksheet
For Counter = LastRow To FirstRow Step -1
Range("BD2:BE1000").Merge Across:=True
Next Counter
End With
You can just use string concatenation here (assuming that lastrow (1000) and firstrow (2) have been set up properly in your sample code).
With Worksheet
For Counter = LastRow To FirstRow Step -1
.Range("BD" & counter).Value = .Range("BD" & counter).value & .Range("BE" & counter).value
Next Counter
End With
Concatenate (non-empty) names into one column
[1] In a first step you can assign your data range (object variable e.g. rng) to a variant 1-based 2-dim datafield array by a one liner v = rng or v = rng.Value2.
[2] In a second step you loop through all array rows and check for non-empty names concatenating these findings in the array's first columns (overwriting the original single name part).
[3] Resizing the receiving range to 1 column only (and the number of non-empty rows allows you to write the results back to sheet.
Code example
Option Explicit ' declaration head of your code module enforces declaration of variables/objects
Sub ConcatenateNames()
Dim v As Variant, rng As Range
With ThisWorkbook.Worksheets("MySheet") ' <<~~ change to your sheet name
' [1] assign names to 2-dim datafield array v
Set rng = .Range("BD2:BE1000") ' set user defined range to memory
v = rng.Value2 ' get data
' [2] loop through data
Dim i As Long, ii As Long
For i = 1 To UBound(v)
' [2a] check for non empty names
If Len(Trim(v(i, 1)) & Trim(v(i, 2))) > 0 Then
' [2b] concatenate first and last names in array v
ii = ii + 1 ' increment counter
v(ii, 1) = v(i, 1) & " " & v(i, 2)
End If
Next i
' [3] write back to sheet and resize receiving range to ii rows and 1 column
rng.Clear ' clear original data
rng.Resize(ii, 1) = v ' write names back to sheet
End With
End Sub
Further hint
Take care of the leading point . before "Range" referring to your worksheet object: Set rng = .Range("BD2:BE1000")
I have tried below code to fill a two dimensional array in Excel VBA and I was able to get the desired results. I would like to know if there is a better way of doing this or if you foresee any technical issue once I have a significantly large size of data in real case situations. Any ideas or suggestions would be appreciated for improvement.
Sub test_selection()
' My below array is based on values contained within
' selected cells
' The purpose of using two dimensional array is to
' keep values in one column of array
' while retaining cell addresses in 2nd
' dimension to print some info in relevant cells
' offset to the selected cells
Dim anArray() As String
firstRow = Selection.Range("A1").Row
LastRow = Selection.Rows(Selection.Rows.Count).Row
colum = Selection.Columns.Column
arrSize = LastRow - firstRow
ReDim anArray(0 To arrSize, 1)
cnt = 0
For i = firstRow To LastRow
anArray(cnt, 0) = CStr(Cells(i, colum).Value2)
anArray(cnt, 1) = Cells(i, colum).Address
cnt = cnt + 1
Next i
Call TestGetFileList(anArray)
End Sub
When you have a significantly large size of data, that loop through the worksheet is going to be slow. Probably better to grab all of the data at once and reprocess it in memory.
Option Explicit
Sub test_selection()
' My below array is based on values contained within
' selected cells
' The purpose of using two dimensional array is to
' keep values in one column of array
' while retaining cell addresses in 2nd
' dimension to print some info in relevant cells
' offset to the selected cells
Dim i As Long, r As Long, c As String, anArray As Variant
With Selection
c = Split(.Cells(1).Address, "$")(1)
r = Split(.Cells(1).Address, "$")(2) - 1
anArray = .Columns(1).Cells.Resize(.Rows.Count, 2).Value2
End With
For i = LBound(anArray, 1) To UBound(anArray, 1)
anArray(i, 1) = CStr(anArray(i, 1))
anArray(i, 2) = "$" & c & "$" & i + r
Next i
TestGetFileList anArray
End Sub
I have a problem I need help with involving Excel and VBA. I know next to nothing about Excel/VBA, and I need a coding solution to help me avoid performing the extremely tedious task of doing this manually (think hundreds of lines that need to be parsed where one row could become multiple rows in a new sheet). I've been searching the web for solutions, but I just keep getting confused by the answers (because I don't know anything about VB and using it to program a macro in Excel), so I figured I'd seek help for my specific problem.
Here is the rundown: I have a spreadsheet where I need to copy rows from a source sheet to a target sheet. The source sheet has 2 columns (A & B) that can be thought of as a key/value pair where col A contains the key and col B contains the value. The problem lies with the values in col B. The values can either be a single line of text or a numbered list of different texts
What I want to do is for each row in the source:
split the values in col B to get an array of each individual value (if the value is in the form of a numbered list)
create new rows in the target sheet by looping over the split array of values such that a new row will be created where:
new row col A = source row col A key and new row col B = current iteration index from the array of split values.
if no numbered list, just copy the source row into target sheet
Source
A B
key1 1. text1
2. text2
key2 1. text3
Target
A B
key1 text1
key1 text2
key2 text3
The numbered list in a cell will be multiple lines where each line of text is prepended by a decimal and a dot. This applies to single line cells as well.
(Update) Bear in mind that the values in either col A or B are not simple text values. These are full on sentences. So, I'm not sure a simple formula is going to work.
Split Multi Line
It is unclear which line separator occurs in the multi line cells. Choose one, vbLf worked for me.
Adjust the values in the constants section to fit your needs.
The Code
Sub SplitMultiLine()
Const cSheet1 As Variant = "Sheet1" ' Source Worksheet Name/Index
Const cFirstR As Integer = 1 ' Source First Row Number
Const cFirstC As Variant = "A" ' Source First Column Letter/Number
Const cLastC As Variant = "C" ' Source Last Column Letter/Number
Const cMulti As Integer = 2 ' Multi Column
Const cSplit As String = vbLf ' Split Char(vbLf, vbCrLf, vbCr)
Const cDot As String = "." ' Dot Char (Delimiter)
Const cSheet2 As Variant = "Sheet1" ' Target Worksheet Name/Index
Const cTarget As String = "E1" ' Target First Cell Address
Dim vntS As Variant ' Source Array
Dim vntSplit As Variant ' Split Array
Dim vntT As Variant ' Target Array
Dim lastR As Long ' Source Last Row
Dim i As Long ' Source Array Row Counter
Dim j As Integer ' Source/Target Array Column Counter
Dim k As Long ' Target Array Row Counter
Dim m As Integer ' Split Array Row Counter
' Paste Source Range into Source Array.
With Worksheets(cSheet1)
lastR = .Cells(.Rows.Count, cFirstC).End(xlUp).Row
vntS = .Range(.Cells(cFirstR, cFirstC), .Cells(lastR, cLastC))
End With
' Count the number of rows in target array.
For i = 1 To UBound(vntS)
k = k + UBound(Split(vntS(i, cMulti), cSplit)) + 1
Next
' Write from Source to Target Array.
ReDim vntT(1 To k, 1 To UBound(vntS, 2))
k = 0
For i = 1 To UBound(vntS)
k = k + 1
vntSplit = Split(vntS(i, cMulti), cSplit)
For m = 0 To UBound(vntSplit)
If InStr(vntSplit(m), cDot) > 0 Then
vntT(k, cMulti) = Trim(Right(vntSplit(m), Len(vntSplit(m)) _
- InStr(vntSplit(m), cDot)))
Else
vntT(k, cMulti) = vntSplit(m)
End If
For j = 1 To UBound(vntS, 2)
If j <> cMulti Then
vntT(k, j) = vntS(i, j)
End If
Next
k = k + 1
Next
k = k - 1
Next
' Paste Target Array into Target Range calculated from Target Frist Cell.
With Worksheets(cSheet2).Range(cTarget)
.Resize(UBound(vntT), UBound(vntT, 2)) = vntT
End With
End Sub
An Over-Commenting
Sub SplitMultiLineOverCommented()
Const cSheet1 As Variant = "Sheet1" ' Source Worksheet Name/Index
Const cFirstR As Integer = 1 ' Source First Row Number
Const cFirstC As Variant = "A" ' Source First Column Letter/Number
Const cLastC As Variant = "C" ' Source Last Column Letter/Number
Const cMulti As Integer = 2 ' Multi Column
Const cSplit As String = vbLf ' Split Char(vbLf, vbCrLf, vbCr)
Const cDot As String = "." ' Dot Char (Delimiter)
Const cSheet2 As Variant = "Sheet1" ' Target Worksheet Name/Index
Const cTarget As String = "E1" ' Target First Cell Address
Dim vntS As Variant ' Source Array
Dim vntSplit As Variant ' Split Array
Dim vntT As Variant ' Target Array
Dim lastR As Long ' Source Last Row
Dim i As Long ' Source Array Row Counter
Dim j As Integer ' Source/Target Array Column Counter
Dim k As Long ' Target Array Row Counter
Dim m As Integer ' Split Array Row Counter
' Paste Source Range into Source Array.
With Worksheets(cSheet1)
' The last row of data is usually calculated going from the bottom up,
' it is like selecting the last cell and pressing CTRL UP and returning
' =ROW() in Excel.
lastR = .Cells(.Rows.Count, cFirstC).End(xlUp).Row
' Paste a range into an array actually means copying it. The array
' created is a 1-based 2-dimensional array which has the same number
' of rows and columns as the Source Range.
vntS = .Range(.Cells(cFirstR, cFirstC), .Cells(lastR, cLastC))
End With
' Count the number of rows in Target Array.
' You refer to the last row of the array with UBound(vntS) which is short
' for UBound(vntS, 1) which reveals that it is referring to the first
' dimension (rows).
For i = 1 To UBound(vntS)
' We are splitting the string by cSplit which is the line
' separator (delimiter). When you enter something into a cell and
' hold left Alt and press ENTER, the vbLf character is set in place
' of the line separator. But the data may have been imported from
' another system that uses another line separator. When splitting the
' string, a 0-based array is 'created' and its UBound is the last
' row, but since it is 0-based we have to add 1.
k = k + UBound(Split(vntS(i, cMulti), cSplit)) + 1
Next
' Write from Source to Target Array.
' After we have calculated the number of rows, we have to resize the
' Target Array. To avoid confusion, I always use '1 To' to be certain that
' it is a 1-based array. Since the number columns of the Source Array and
' the Target Array is the same, we use the UBound of the Source Array to
' resize the second dimension of the Target Array - UBound(vntS, 2) where
' 2 is indicating the second dimension, columns.
ReDim vntT(1 To k, 1 To UBound(vntS, 2))
' We will use again k as the row counter since its value is no more
' needed. This is what I have many times forgotten, so maybe it is
' better to use a different variable.
k = 0
' Loop through the columns of Source Array.
For i = 1 To UBound(vntS)
' Increase the row of Target Array or e.g. align it for writing.
k = k + 1
' Split the string (lines) in the Multi Column into the 0-based
' Split Array.
vntSplit = Split(vntS(i, cMulti), cSplit)
' Loop through the values of the Split Array
For m = 0 To UBound(vntSplit)
' Check if value contains cDot. The Instr function returns 0 if
' a string has not been found, it's like =FIND(".",A1) in Excel,
' except that Excel would return an error if not found.
If InStr(vntSplit(m), cDot) > 0 Then
' If cDot was found then write the right part after cDot
' to the current row of column cMulti but trim the result
' (remove space before and after.
' It's like =TRIM(RIGHT(A1,LEN(A1)-FIND(".",A1))) in Excel.
vntT(k, cMulti) = Trim(Right(vntSplit(m), Len(vntSplit(m)) _
- InStr(vntSplit(m), cDot)))
Else
' If cDot was not found then just write the value to the
' current row.
vntT(k, cMulti) = vntSplit(m)
End If
' Loop through all columns.
For j = 1 To UBound(vntS, 2)
If j <> cMulti Then
' Write to other columns (Not cMulti)
vntT(k, j) = vntS(i, j)
End If
Next ' Next Source/Target Array Column
' Increase the current row of Target Array before going to next
' value in Split Array.
k = k + 1
Next ' Next Split Array Row
' Since we have increased the last current row but haven't written to
' it, we have to decrease one row because of the "k = k + 1" right below
' "For i = 1 To UBound(vntS)" which increases the row of Target Array
' for each next row in Source Array.
k = k - 1
Next ' Next Source Array Row
' Paste Target Array into Target Range calculated from Target Frist Cell.
' Like we pasted a range into an array, we can also paste an array into
' a range, but it has to be the same size as the array, so by using
' the Resize method we adjust the Target Range First Cell to the Target
' Range, using the last row and column of the Target Array. Again,
' remember UBound(vntT) is short for UBound(vntT, 1) (rows).
With Worksheets(cSheet2).Range(cTarget)
.Resize(UBound(vntT), UBound(vntT, 2)) = vntT
End With
End Sub
You can do this with two formulas.
I'm assuming your data is in Sheet1.
For the first columns, use the following formula:
=IF(ISBLANK(Sheet1!A2),A1,Sheet1!A2)
For the second one use:
=IFERROR(RIGHT(Sheet1!B2,LEN(Sheet1!B2)-FIND(". ",Sheet1!B2)-1),Sheet1!B2)
And populate down.
edit:
The first formula will look at the corresponding cell in Sheet1, column A. If it is blank, it will take the value of the cell above where the formula is. If it isn't blank, it will take the value of the cell in Sheet1, column A that it just checked.
The second formula looks for the string ". " in the cells in Sheet1 column B and removes it and everything to the left of it from the text. If the string in question (". ") is not found (meaning there is no numbering in that given cell) it would return an error, so the whole thing is wrapped in an IFERROR statement which returns the value of the cell in Sheet1 column B if it is triggered.