Nested For Loop alternatives or optimization - excel

Currently trying to append all cells in each row into the first cell of that row, and iterate through every row. Problem is I'm dealing with ~3000 rows with about 20 columns of data in each row. Is there any better way to append all cells in a row into one single cell without using a for loop? That could narrow down the code to a single for loop and may speed up the process.
Tried making a nested for loop that iterates through every row then every column per row. It works, but takes far too long when dealing with a large amount of data.
Sub AppendToSingleCell()
Dim value As String
Dim newString As String
Dim lastColumn As Long
Dim lastRow As Long
lastRow = Cells(Rows.Count, "A").End(xlUp).Row
For j = 1 To lastRow
lastColumn = Cells(j, Columns.Count).End(xlToLeft).Column
For i = 2 To lastColumn
If IsEmpty(Cells(j, i)) = False Then
value = Cells(j, i)
newString = Cells(j, 1).value & " " & value
Cells(j, 1).value = newString
Cells(j, i).Clear
End If
Next i
Next j
End Sub

Load everything into a variant array and loop that instead of the range. load the output into another variant array and then put that data as one back in the sheet.
Sub AppendToSingleCell()
With ActiveSheet
Dim lastRow As Long
lastRow = .Cells(.Rows.Count, "A").End(xlUp).row
Dim lastColumn As Long
lastColumn = .Cells.Find(What:="*", After:=.Range("a1"), LookIn:=xlValue, SearchOrder:=xlByColumns, SearchDirection:=xlPrevious).Column
Dim dtaArr() As Variant
dtaArr = .Range(.Cells(1, 2), .Cells(lastRow, lastColumn)).value
Dim otArr() As Variant
ReDim otArr(1 To lastRow, 1 To 1)
Dim i As Long
For i = LBound(dtaArr, 1) To UBound(dtaArr, 1)
For j = LBound(dtaArr, 2) To UBound(dtaArr, 2)
If dtaArr(i, j) <> "" Then otArr(i, 1) = otArr(i, 1) & dtaArr(i, j) & " "
Next j
otArr(i, 1) = Application.Trim(otArr(i, 1))
Next i
.Range(.Cells(1, 2), .Cells(lastRow, lastColumn)).Clear
.Range(.Cells(1, 1), .Cells(lastRow, 1)).value = otArr
End With
End Sub

It's a bit long, but pretty straight forward.
Explanation inside the code's comments.
Code
Option Explicit
Sub AppendToSingleCell()
Dim newString As String
Dim LastRow As Long, LastColumn As Long
Dim Sht As Worksheet
Dim FullArr As Variant, MergeCellsArr As Variant
Dim i As Long, j As Long
Set Sht = ThisWorkbook.Sheets("Sheet1") ' <-- rename "Sheet1" to your sheet's name
With Sht
LastRow = FindLastRow(Sht) ' call sub that finds last row
LastColumn = FindLastCol(Sht) ' call sub that finds last column
' populate array with enitre range contents
FullArr = .Range(.Cells(1, 1), .Cells(LastRow, LastColumn))
ReDim MergeCellsArr(1 To LastRow) ' redim 1-D array for results (same number of rows as in the 2-D array)
' looping through array is way faster than interfacing with your worksheet
For i = 1 To UBound(FullArr, 1) ' loop rows (1st dimension of 2-D array)
newString = FullArr(i, 1)
For j = 2 To UBound(FullArr, 2) ' loop columns (2nd dimension of 2-D array)
If IsEmpty(FullArr(i, j)) = False Then
newString = newString & " " & FullArr(i, j)
End If
Next j
MergeCellsArr(i) = newString ' read new appended string to new 1-D array
Next i
' paste entire array to first column
.Range("A1").Resize(UBound(MergeCellsArr)).value = MergeCellsArr
End With
End Sub
'=======================================================================
Function FindLastCol(Sht As Worksheet) As Long
' This Function finds the last col in a worksheet, and returns the column number
Dim LastCell As Range
With Sht
Set LastCell = .Cells.Find(What:="*", After:=.Cells(1), LookAt:=xlPart, LookIn:=xlFormulas, _
SearchOrder:=xlByColumns, SearchDirection:=xlPrevious, MatchCase:=False)
If Not LastCell Is Nothing Then
FindLastCol = LastCell.Column
Else
MsgBox "Error! worksheet is empty", vbCritical
Exit Function
End If
End With
End Function
'=======================================================================
Function FindLastRow(Sht As Worksheet) As Long
' This Function finds the last row in a worksheet, and returns the row number
Dim LastCell As Range
With Sht
Set LastCell = .Cells.Find(What:="*", After:=.Cells(1), LookAt:=xlPart, LookIn:=xlFormulas, _
SearchOrder:=xlByRows, SearchDirection:=xlPrevious, MatchCase:=False)
If Not LastCell Is Nothing Then
FindLastRow = LastCell.Row
Else
MsgBox "Error! worksheet is empty", vbCritical
Exit Function
End If
End With
End Function

If you're interested in a shorter solution.... It assumes your data begins in cell A1.
Public Sub CombineColumnData()
Dim arr As Variant
Dim newArr() As Variant
Dim varTemp As Variant
Dim i As Long
arr = ActiveSheet.Range("A1").CurrentRegion.Value
ReDim newArr(1 To UBound(arr, 1))
For i = LBound(arr, 1) To UBound(arr, 1)
varTemp = Application.Index(arr, i, 0)
newArr(i) = Join(varTemp, "")
Next i
With ActiveSheet.Range("A1")
.CurrentRegion.Clear
.Resize(UBound(arr, 1), 1) = Application.Transpose(newArr)
End With
End Sub

Related

Join text into one cell in various row and column

I have a spreadsheet that has values that looks similar to below :
Is there any possible way to create VBA to join all the separate data together for each ID and Class into one row? So that the ending result would look like below?
Sub JoinRowsData()
Dim lastRow As Long, i As Long, j As Long, k As Long
Application.ScreenUpdating = False
lastRow = Range("C" & Rows.Count).End(xlUp).Row
For i = 2 To lastRow
For j = i + 1 To lastRow
If Cells(i, 2) = Cells(j, 2) Then
For k = 5 To 10
If (Cells(i, k) = "" And Cells(j, k) <> "") Then
Cells(i, k) = Cells(j, k)
End If
Next
End If
Next
Next
Application.ScreenUpdating = True
End Sub
The following will do it. See the comments for an explanation how it works. It uses arrays to process the data which is much faster than process cells directly.
Option Explicit
Public Sub JoinRowsData()
Dim ws As Worksheet ' define worksheet
Set ws = ThisWorkbook.Worksheets("Sheet1")
Dim LastRow As Long ' get last used row in worksheet
LastRow = GetLastUsed(xlByRows, ws)
Dim LastCol As Long ' get last used column in worksheet
LastCol = GetLastUsed(xlByColumns, ws)
' Read data into an array for faster processing
Dim Data() As Variant
Data = ws.Range("A1", ws.Cells(LastRow, LastCol)).Value2
' define an output array with the same size
Dim Output() As Variant
ReDim Output(1 To UBound(Data, 1), 1 To UBound(Data, 2))
Dim outRow As Long ' output row index
Dim iRow As Long
For iRow = 1 To LastRow ' loop through all rows in data
' if column 1 contains data it is a new output row
If Data(iRow, 1) <> vbNullString Then
outRow = outRow + 1
End If
' loop through all columns in a data row
Dim iCol As Long
For iCol = 1 To LastCol
If Data(iRow, iCol) <> vbNullString Then ' check if current cell has data
If Output(outRow, iCol) <> vbNullString Then
' add a line break if there is already data in the output cell
Output(outRow, iCol) = Output(outRow, iCol) & vbLf
End If
' add the data to the output cell
Output(outRow, iCol) = Output(outRow, iCol) & Data(iRow, iCol)
End If
Next iCol
Next iRow
' write all the output data from the array back to the cells
ws.Range("A1", ws.Cells(LastRow, LastCol)).Value2 = Output
End Sub
' find last used row or column in worksheet
Public Function GetLastUsed(ByVal RowCol As XlSearchOrder, ByVal InWorksheet As Worksheet) As Long
With InWorksheet
If Application.WorksheetFunction.CountA(.Cells) <> 0 Then
Dim LastCell As Range
Set LastCell = .Cells.Find(What:="*", _
After:=.Range("A1"), _
Lookat:=xlPart, _
LookIn:=xlFormulas, _
SearchOrder:=RowCol, _
SearchDirection:=xlPrevious, _
MatchCase:=False)
If RowCol = xlByRows Then
GetLastUsed = LastCell.Row
Else
GetLastUsed = LastCell.Column
End If
Else
GetLastUsed = 1
End If
End With
End Function

Concatenate string (VBA) for particular columns

Goal: Add the string "Z" to a select few columns for all rows except the header. Concatenate only on select headers i.e. headers defined in the array.
Dim header As Range
ArrayCheck = Array("CarTime", "BusTime", "PlaneTime")
LastRow = desWS1.Cells.Find("*", SearchOrder:=xlByRows, SearchDirection:=xlPrevious).Row
lcol = desWS1.Cells(1, Columns.Count).End(xlToLeft).Column
For Each header In desWS1.Range(desWS1.Cells(1, 1), desWS1.Cells(1, lcol))
For i = LBound(ArrayCheck) To UBound(ArrayCheck)
If header = ArrayCheck(i) Then
desWS1.Range(desWS1.Cells(2, header.Column), desWS1.Cells(LastRow, header.Column)) & "Z"
End If
Next i
Next
all entries in these columns are of the form: yyyy-mm-ddThh:mm:ss
#SiddharthRout the current cell is: 2020-09-07T13:08:46, and the output i want is: 2020-09-07T13:08:46Z. So yep, you're right, it's a string. – Jak Carty 2 mins ago
In my below code, I will take a sample of both date and date stored as text. I have commented the code so you should not have a problem understanding it. But if you do then simply post back.
Is this what you are trying?
Code:
WAY 1
Option Explicit
Sub Sample()
Dim ws As Worksheet
Dim lRow As Long, lCol As Long
Dim ArrayCheck As Variant
Dim i As Long, j As Long
Dim rng As Range
ArrayCheck = Array("CarTime", "BusTime", "PlaneTime")
'~~> Set this to the relevant worksheet
Set ws = Sheet1
With ws
'~~> Find last row
lRow = .Cells.Find(What:="*", _
After:=.Range("A1"), _
Lookat:=xlPart, _
LookIn:=xlFormulas, _
SearchOrder:=xlByRows, _
SearchDirection:=xlPrevious, _
MatchCase:=False).Row
'~~> Find last col
lCol = .Cells(1, .Columns.Count).End(xlToLeft).Column
'~~> Loop though the cell in 1st row
For i = 1 To lCol
'~~> Loop through the array
For j = LBound(ArrayCheck) To UBound(ArrayCheck)
'~~> Check if they match
If .Cells(1, i).Value2 = ArrayCheck(j) Then
'~~> Set your range from cell 2 onwards
Set rng = .Range(.Cells(2, i), .Cells(lRow, i))
'~~> Add "Z" to the entire range in ONE GO i.e without looping
'~~> To understand this visit the url below
'https://stackoverflow.com/questions/19985895/convert-an-entire-range-to-uppercase-without-looping-through-all-the-cells
rng.Value = Evaluate("index(Concatenate(" & rng.Address & ",""Z""" & "),)")
End If
Next j
Next i
End With
End Sub
Note: For the sake of clarity, I am not joining the string ",""Z""" & "),)")
In Action
WAY 2
Introducing a 2nd way
This code writes to array and then works with it.
Option Explicit
Sub Sample()
Dim ws As Worksheet
Dim lRow As Long, lCol As Long
Dim ArrayCheck As Variant
Dim i As Long, j As Long, k As Long
Dim rng As Range
Dim tmpAr As Variant
ArrayCheck = Array("CarTime", "BusTime", "PlaneTime")
'~~> Set this to the relevant worksheet
Set ws = Sheet1
With ws
'~~> Find last row
lRow = .Cells.Find(What:="*", _
After:=.Range("A1"), _
Lookat:=xlPart, _
LookIn:=xlFormulas, _
SearchOrder:=xlByRows, _
SearchDirection:=xlPrevious, _
MatchCase:=False).Row
'~~> Find last col
lCol = .Cells(1, .Columns.Count).End(xlToLeft).Column
'~~> Loop though the cell in 1st row
For i = 1 To lCol
'~~> Loop through the array
For j = LBound(ArrayCheck) To UBound(ArrayCheck)
'~~> Check if they match
If .Cells(1, i).Value2 = ArrayCheck(j) Then
'~> Set your range
Set rng = .Range(.Cells(2, i), .Cells(lRow, i))
'~~> Store the value in array
tmpAr = rng.Value2
'~~> Work with array
For k = 1 To UBound(tmpAr)
tmpAr(k, 1) = tmpAr(k, 1) & "Z"
Next k
'~~> write the array back to worksheet
rng.Resize(UBound(tmpAr), 1).Value = tmpAr
End If
Next j
Next i
End With
End Sub
In Action

VBA - Vlookup Multiple Columns and Fill to End of Range

I need to do a Vlookup of an ID on the source sheet to a table in the data sheet. When the Vlookup is done, it needs to return the cell values from 6 different columns.
Here I have a function to get the range:
Function find_Col(header As String) As Range
Dim aCell As Range, rng As Range, def_Header As Range
Dim col As Long, lRow As Long, defCol As Long
Dim colName As String, defColName As String
Dim y As Workbook
Dim ws1 As Worksheet
Set y = Workbooks("Template.xlsm")
Set ws1 = y.Sheets("Results")
With ws1
Set def_Header = Cells.Find(what:="ID", LookIn:=xlValues, lookat:=xlWhole, MatchCase:=False, SearchFormat:=False)
Set aCell = .Range("B2:Z2").Find(what:=header, LookIn:=xlValues, lookat:=xlWhole, MatchCase:=False, SearchFormat:=False)
If Not aCell Is Nothing Then
defCol = def_Header.Column
defColName = Split(.Cells(, defCol).Address, "$")(1)
col = aCell.Column
colName = Split(.Cells(, col).Address, "$")(1)
lRow = Range(defColName & .Rows.count).End(xlUp).Row - 1
Set myCol = Range(colName & "2")
'This is your range
Set find_Col = Range(myCol.Address & ":" & colName & lRow).Offset(1, 0)
'If not found
Else
MsgBox "Column Not Found"
End If
End With
End Function
Then in my sub, I select the range and do a Vlookup which fills this range:
Selection.FormulaR1C1 = "=VLOOKUP(RC[-4],myTable,2,FALSE)"
And this works great.
Then I needed to return more than just one column, so I ended up with the formula:
Selection.FormulaArray = "=VLOOKUP($C3,myTable,{2,3,4,5,6},FALSE)"
Source Sheet:
Data Sheet:
So, my function returns only the range for one column, which I think I can use in terms of getting a row count then using something like this:
Set myRng = find_Col("Product")
For currentRow = myRng.Rows.count To 1 Step -1
Selection.FormulaArray = "=VLOOKUP($C3,myTable,{2,3,4,5,6},FALSE)"
Next currentRow
Then perhaps instead of C3 it could look something like this:
C & currentRow --> Selection.FormulaArray = "=VLOOKUP($C & currentRow,myTable,{2,3,4,5,6},FALSE)"
But then I have the issue that only one cell is selected (G3) and from H-L is not. And I have no idea whether this is even a plausible effort.
Ideally of course, I would have cells G3:L3 selected and fill the formula down to the last row.
My brain is just fried from all the thinking and attempts.
So this should do the trick... I've explained every instance but if you need help understanding just ask:
Option Explicit
Sub FillData1()
Dim ws As Worksheet, wsData As Worksheet, arr As Variant, arrData As Variant
Dim DictHeaders As Scripting.Dictionary, DictIds As Scripting.Dictionary, DictDataHeaders As Scripting.Dictionary, _
DictDataIds As Scripting.Dictionary
Dim LastRow As Long, LastCol As Integer, i As Long, j As Integer
With Application
.ScreenUpdating = False
.EnableEvents = False
End With
With ThisWorkbook
Set ws = .Sheets("Results")
Set wsData = .Sheets("List")
End With
'Lets suppose your data always starts on row 2 in both sheets and column B will always have the max amount of rows filled
With ws 'filling the first array
LastRow = .Cells(.Rows.Count, 2).End(xlUp).Row
LastCol = .Cells(2, .Columns.Count).End(xlToLeft).Column
arr = .Range("B2", .Cells(LastRow, LastCol)).Value
End With
With wsData 'filling the data array
LastRow = .Cells(.Rows.Count, 4).End(xlUp).Row
LastCol = .Cells(2, .Columns.Count).End(xlToLeft).Column
arrData = .Range("A2", .Cells(LastRow, LastCol)).Value
End With
'Now lets put everything into Dictionaries so if the data moves columns or rows won't matter
Set DictHeaders = New Scripting.Dictionary
Set DictIds = New Scripting.Dictionary
For i = 1 To UBound(arr, 2) 'this will fill the headers positions on the main sheet
If Not DictHeaders.Exists(arr(1, i)) Then DictHeaders.Add arr(1, i), i
Next i
For i = 2 To UBound(arr, 1) 'this will fill the IDs positions on the main sheet
If Not DictIds.Exists(arr(i, DictHeaders("KW ID"))) Then DictIds.Add arr(i, 1), i
Next i
Set DictDataHeaders = New Scripting.Dictionary
Set DictDataIds = New Scripting.Dictionary
For i = 1 To UBound(arrData, 2) 'this will fill the headers positions on the data sheet
If Not DictDataHeaders.Exists(arrData(1, i)) Then DictDataHeaders.Add arrData(1, i), i
Next i
For i = 2 To UBound(arrData, 1) 'this will fill the IDs positions on the data sheet
If Not DictDataIds.Exists(arrData(i, DictDataHeaders("KW ID"))) Then DictDataIds.Add arrData(i, DictDataHeaders("KW ID")), i
Next i
'Finally will loop through the main array to fill it with the data from the data array
On Error Resume Next
For i = 2 To UBound(arr)
For j = 6 To UBound(arr, 2) 'I'm assuming you want to avoid the first columns which are hidden
arr(i, j) = arrData(DictDataIds(arr(i, 1)), DictDataHeaders(arr(1, j)))
Next j
Next i
On Error GoTo 0
With ws 'filling the first array
LastRow = .Cells(.Rows.Count, 2).End(xlUp).Row
LastCol = .Cells(2, .Columns.Count).End(xlToLeft).Column
.Range("B2", .Cells(LastRow, LastCol)).Value = arr
End With
With Application
.ScreenUpdating = True
.EnableEvents = True
End With
End Sub
I don't know if I got the true issue of your goal. However, since your Selection parts in your code should be avoid, why don't make something like the following?
Set myRng = find_Col("Product")
For currentRow = myRng.Rows.count To 1 Step -1
Range(Cells(currentRow, 5), Cells(currentRow, 9)).FormulaArray = "=VLOOKUP(RC3,myTable,{2,3,4,5,6},FALSE)"
Next currentRow

Shift All Rows To First Common Row

I have 100K Excel file that has many employee info, I want to shift all existence data to the first row for this employee, the picture below will be louder than my words, can a VBA code do this? or there is a trick in excel that I am not aware of
Try following code.
Sub Demo()
Dim ws As Worksheet
Dim cel As Range, rng As Range
Dim lastRow As Long, lastCol As Long, i As Long
Dim fOccur As Long, lOccur As Long, colIndex As Long
Dim dict As Object, c1
Application.ScreenUpdating = False
Set ws = ThisWorkbook.Sheets("Sheet1") 'change Sheet1 to your data range
Set dict = CreateObject("Scripting.Dictionary")
With ws
lastRow = .Cells(.Rows.Count, "A").End(xlUp).Row 'last row with data in Column A
lastCol = .Cells.Find(What:="*", _
After:=.Range("A1"), _
Lookat:=xlPart, _
LookIn:=xlFormulas, _
SearchOrder:=xlByColumns, _
SearchDirection:=xlPrevious, _
MatchCase:=False).Column 'last column with data in Sheet1
Set rng = .Range("A1:A" & lastRow) 'set range in Column A
c1 = .Range("A2:A" & lastRow)
For i = 1 To UBound(c1, 1) 'using dictionary to get uniques values from Column A
dict(c1(i, 1)) = 1
Next i
colIndex = 16 'colIndex+1 is column number where data will be displayed from
For Each k In dict.keys 'loopthrough all unique values in Column A
fOccur = Application.WorksheetFunction.Match(k, rng, 0) 'get row no. of first occurrence
lOccur = Application.WorksheetFunction.CountIf(rng, k) 'get row no. of last occurrence
lOccur = lOccur + fOccur - 1
'copy range from left to right
.Range(.Cells(fOccur, 1 + colIndex), .Cells(lOccur, lastCol + colIndex)).Value = .Range(.Cells(fOccur, 1), .Cells(lOccur, lastCol)).Value
'delete blanks in range at right
.Range(.Cells(fOccur, 1 + colIndex), .Cells(lOccur, lastCol + colIndex)).SpecialCells(xlCellTypeBlanks).Delete Shift:=xlUp 'delte blank rows
Next k
End With
Application.ScreenUpdating = True
End Sub
Try the below. You can amend the below code to match where you want to move the range:
Dim oW As Worksheet: Set oW = ThisWorkbook.Worksheets("Sheet8")
With oW.UsedRange
.Cut .Offset(0, .Columns.Count + 2)
End With

VBA Collection issue

I an trying towrite a simple VBA code where some cell values are combined.
Problem with code bellow is that Cell Object in the loop keeps selecting whole row, not just one cell in Row Collection
Dim Cell As Range
Dim Row As Range
Set Row = Rows(ActiveCell.Row)
Set Cell = ActiveCell
For Each Cell In Row
With Cell
If IsNumeric(InStr(1, Right(.Value, 1), "/")) Then
.Value = .Value & .Offset(0, 1).Value
.Offset(0, 1).Delete (xlShiftToLeft)
End If
End With
Next Cell
Try this. For this example, assumptions made about data being on Sheet1 and the start row (stRow) and start col (testCol) of the data. Amend these to suit your conditions.
Option Explicit
Sub combine()
Dim ws As Worksheet
Dim stRow As Long, endRow As Long, testCol As Long, endCol As Long
Dim rnum As Long, cnum As Long
Dim cl As Range
Set ws = Sheets("Sheet1")
stRow = 1
testCol = 1
With ws
endRow = .Cells(Rows.Count, testCol).End(xlUp).Row
For rnum = stRow To endRow
endCol = .Cells(rnum, Columns.Count).End(xlToLeft).Column
For cnum = testCol To endCol - 1
Set cl = .Cells(rnum, cnum)
If Right(cl, 1) = "/" And Right(cl.Offset(0, 1), 1) <> "/" Then
If IsNumeric(Left(cl.Value, Len(cl.Value) - 1)) Then
cl.Value = cl.Value & cl.Offset(0, 1).Value
cl.Offset(0, 1).Delete (xlShiftToLeft)
End If
End If
Next cnum
Next rnum
End With
End Sub
Although not specified by you, this code does not combine an adjacent cell which also has a trailing "/". This on the basis that we shouldn't 'remove' a 'test' value. If this condition not required it is easily changed.

Resources