How to make a simple "Sum" loop faster? - excel

I am beginning to learn how to use loops and arrays but this one has me stuck. Below is a code that loops through cells and adds them together in column P.
Sub Loop_Test()
Dim sht1 As Worksheet
Dim lr As Long
Dim i As Long
Set sht1 = Worksheets("Sheet1")
lr = Fcst.Cells(Rows.Count, "A").End(xlUp).Row
With sht1
For i = 4 To lr
.Range("P" & i).Value = Application.Sum(Range("D" & i, "O" & i))
Next
End With
End Sub
Overall, this code works but it is very slow and I need to apply it to thousands of rows. I know that in order to make this faster, I need to turn the sum range into an array but I am not entirely sure how to do this when a loop is included.
Any help would be greatly appreciated.
Thanks,
G
Disclaimer: I know there are more efficient ways to sum cells together but this is just me playing around and learning.

Just do them all at once. Looping only adds time to process individual iterations.
With sht1.Range(sht1.cells(4, "P"), sht1.cells(lr, "P"))
.formula = "=sum(D4:O4)"
.Value = .value
End With

Use a variant array to limit the number of times that the vba accesses the worksheets:
Sub Loop_Test()
Dim sht1 As Worksheet
Set sht1 = Worksheets("Sheet1")
Dim fcst As Worksheet
Set fcst = Worksheets("Sheet2")
Dim lr As Long
lr = fcst.Cells(Rows.Count, "A").End(xlUp).Row
Dim dta As Variant
dta = fcst.Range(fcst.Cells(4, "D"), fcst.Cells(lr, "O")).Value
Dim otpt As Variant
ReDim otpt(1 To UBound(dta, 1), 1 To 1)
With sht1
Dim i As Long
For i = LBound(dta, 1) To UBound(dta, 1)
otpt(i, 1) = Application.Sum(Application.Index(dta, i, 0))
Next i
.Range("P4").Resize(UBound(dta, 1), 1).Value = otpt
End With
End Sub
Edit
The SUM(INDEX()) is slow it is quicker just to add the parts individually.
Sub Loop_Test()
Dim sht1 As Worksheet
Set sht1 = Worksheets("Sheet1")
Dim fcst As Worksheet
Set fcst = Worksheets("Sheet2")
Dim lr As Long
lr = fcst.Cells(Rows.Count, "A").End(xlUp).Row
Dim dta As Variant
dta = fcst.Range(fcst.Cells(4, "D"), fcst.Cells(lr, "O")).Value
Dim otpt As Variant
ReDim otpt(1 To UBound(dta, 1), 1 To 1)
With sht1
Dim i As Long
For i = LBound(dta, 1) To UBound(dta, 1)
Dim j as Long
For j = lbound(dta,2) to ubound(dta,2)
otpt(i, 1) = otpt(i, 1) + dta(i, j)
Next j
Next i
.Range("P4").Resize(UBound(dta, 1), 1).Value = otpt
End With
End Sub
Tested on 50,000 rows and result was near instantaneous.

Rather than looping over each row you can insert a summation formula into each row of column P with a single line of code:
.Range("P4:P" & lr).Formula="=SUM(D4:O4)"
assuming 4 is the starting row, and your variable lr is the last row.

Faster With an Array
Sub Loop_Test()
Const cSheet1 As Variant = "Sheet1"
Const cSheet2 As Variant = "Sheet2"
Const fr As Integer = 4
Dim sht1 As Worksheet
Dim fcst As Worksheet
Dim lr As Long
Dim i As Long
Dim vnt As Variant
Set sht1 = Worksheets(cSheet1)
Set fcst = Worksheets(cSheet2)
With fcst
lr = .Cells(.Rows.Count, "A").End(xlUp).Row
ReDim vnt(1 To lr - fr + 1, 1 To 1)
For i = 1 To UBound(vnt)
vnt(i, 1) = WorksheetFunction.Sum( _
.Range("D" & i + fr - 1, "O" & i + fr - 1))
Next
End With
sht1.Cells(fr, "P").Resize(UBound(vnt)) = vnt
End Sub

Related

Creating variant array from union of ranges

I want to create a variant array when using a union to join ranges.
If I select one of the ranges the variant array will work.
When I union, I only receive the row dimensions and not the column dimensions.
For example,
Sub arrTest()
'Declare varbs
Dim ws As Worksheet
Dim myArr() As Variant
Dim lRow As Integer
Dim myRng As Range
'Assign varbs
Set ws = ThisWorkbook.Worksheets("Sheet1")
With ws
lRow = .Cells(Rows.count, "C").End(xlUp).row
Set myRng = Application.Union(.Range("G3:G" & lRow), .Range("J3:O" & lRow), .Range("AD3:AE" & lRow), .Range("AI3:AI" & lRow))
myArr = myRng.Value2
End With
Will return a variant of
myArr(1, 1)
myArr(2, 1)
myArr(1, 3)
However if I were to select one of the ranges within the union for example:
Sub arrTest()
'Declare varbs
Dim ws As Worksheet
Dim myArr() As Variant
Dim lRow As Integer
Dim myRng As Range
'Assign varbs
Set ws = ThisWorkbook.Worksheets("Sheet1")
With ws
lRow = .Cells(Rows.count, "C").End(xlUp).row
Set myRng = .Range("J3:O" & lRow)
myArr = myRng.Value2
End With
I properly get
myArr(1, 1)
myArr(1, 2)
myArr(1, 3)
etc.
How do I return the column dimensions as well, without looping through the sheet?
Like this:
Sub ArrayTest()
Dim ws As Worksheet
Dim arr, lrow As Long
Set ws = ThisWorkbook.Worksheets("Sheet1")
lrow = ws.Cells(Rows.Count, "C").End(xlUp).Row
arr = GetArray(ws.Range("G3:G" & lrow), ws.Range("J3:O" & lrow), _
ws.Range("AD3:AE" & lrow), ws.Range("AI3:AI" & lrow))
With ThisWorkbook.Worksheets("Sheet2").Range("B2")
.Resize(UBound(arr, 1), UBound(arr, 2)).Value = arr
End With
End Sub
'Given a number of input ranges each consisting of one or more columns (assumed all input ranges have
' the same # of rows), return a single 1-based 2D array with the data from each range
Function GetArray(ParamArray sourceCols() As Variant) As Variant
Dim arr, rng, numCols As Long, numRows As Long, r As Long, c As Long, tmp, col As Long
numRows = sourceCols(0).Rows.Count
'loop over ranges and get the total number of columns
For Each rng In sourceCols
numCols = numCols + rng.Columns.Count
Next rng
ReDim arr(1 To numRows, 1 To numCols) 'size the output array
c = 0
For Each rng In sourceCols 'loop the input ranges
tmp = As2DArray(rng) 'get range source data as array ####
For col = 1 To UBound(tmp, 2) 'each column in `rng`
c = c + 1 'increment column position in `arr`
For r = 1 To numRows 'fill the output column
arr(r, c) = tmp(r, col)
Next r
Next col
Next rng
GetArray = arr
End Function
'Get a range's value, always as a 2D array, even if only a single cell
Function As2DArray(rng)
If rng.Cells.Count > 1 Then
As2DArray = rng.Value
Else
Dim arr(1 To 1, 1 To 1)
arr(1, 1) = rng.Value
As2DArray = arr
End If
End Function

How can we compare two columns and copy differences from Sheet2 to Sheet1?

I have two ranges on two sheets.
I am trying to compare these two lists for differences, and copy any differences from Sheet2 to Sheet1. Here is my code. I think it's close, but something is off, because all if does is delete row 14 on Sheet1 and no different cells from Sheet2 are copied to Sheet1. What's wrong here?
Sub Compare()
Dim lastRow1 As Integer
Dim lastRow2 As Integer
Dim foundTrue As Boolean
lastRow1 = Sheets("Sheet1").Cells(Sheets("Sheet1").Rows.Count, "A").End(xlUp).Row
lastRow2 = Sheets("Sheet2").Cells(Sheets("Sheet2").Rows.Count, "E").End(xlUp).Row
For i = 2 To lastRow2
foundTrue = False
For j = 2 To lastRow1
If Sheets("Sheet2").Cells(i, 1).Value = Sheets("Sheet1").Cells(j, 1).Value Then
foundTrue = True
Exit For
End If
Next j
If Not foundTrue Then
Sheets("Sheet2").Cells(i).Copy Destination:=Sheets("Sheet1").Rows(lastRow1 - 1)
End If
Next i
Debug.Print i
End Sub
I want to end up with this.
Nothing that a debug session can't reveal.
You need to copy to lastrow + 1, not lastrow - 1.
After copying the first value, you need to somehow increase the value for lastRow1. But as you use this value as limit in your (inner) for-loop, you shouldn't modify it. So I suggest you introduce a counter variable that counts how many rows you already copied and use this as offset.
And you have some more mistakes:
Your data in sheet2 is in columns E and F, but you compare the values of column "A" (you wrote Sheets("Sheet2").Cells(i, 1).Value)
The source in your copy-command accesses is .Cells(i). In case i is 10, this would be the 10th cell of your sheet, that is J1 - not the cell E10. And even if it was the correct cell, you would copy only one cell, not two.
Obgligatory extra hints: Use Option Explicit (your variables i and j are not declared), and always use Long, not Integer.
Code could look like (I renamed foundTrue because it hurts my eyes to see True in a variable name)
Dim i As Long, j As Long
For i = 2 To lastRow2
foundValue = False
For j = 2 To lastRow1
If Sheets("Sheet2").Cells(i, 5).Value = Sheets("Sheet1").Cells(j, 1).Value Then
foundValue = True
Exit For
End If
Next j
If Not foundValue Then
addedRows = addedRows + 1
Sheets("Sheet2").Cells(i, 5).Resize(1, 2).Copy Destination:=Sheets("Sheet1").Cells(lastRow1, 1).Offset(addedRows)
End If
Next i
But this leaves a lot room for improvement. I suggest you have a look to the following, in my eyes it's much cleaner and much more easy to adapt. There is still room for optimization (for example read the data into arrays to speed up execution), but that's a different story.
Sub Compare()
Const sourceCol = "E"
Const destCol = "A"
Const colCount = 2
' Set worksheets
Dim sourceWs As Worksheet, destWs As Worksheet
Set sourceWs = ThisWorkbook.Sheets("Sheet2")
Set destWs = ThisWorkbook.Sheets("Sheet1")
' Count rows
Dim lastRowSource As Long, lastRowDest As Long
lastRowSource = sourceWs.Cells(sourceWs.Rows.Count, sourceCol).End(xlUp).Row
lastRowDest = destWs.Cells(destWs.Rows.Count, destCol).End(xlUp).Row
Dim sourceRow As Long, destRow As Long
Dim addedRows As Long
For sourceRow = 2 To lastRowSource
Dim foundValue As Boolean
foundValue = False
For destRow = 2 To lastRowDest
If sourceWs.Cells(sourceRow, sourceCol).Value = destWs.Cells(destRow, destCol).Value Then
foundValue = True
Exit For
End If
Next destRow
If Not foundValue Then
addedRows = addedRows + 1
sourceWs.Cells(sourceRow, sourceCol).Resize(1, colCount).Copy Destination:=destWs.Cells(lastRowDest, 1).Offset(addedRows)
End If
Next sourceRow
End Sub
Copy Differences (Loop)
A Quick Fix
Option Explicit
Sub Compare()
Dim wb As Workbook: Set wb = ThisWorkbook ' workbook containing this code
Dim ws1 As Worksheet: Set ws1 = wb.Worksheets("Sheet1")
Dim lRow1 As Long: lRow1 = ws1.Cells(ws1.Rows.Count, "A").End(xlUp).Row
Dim fRow1 As Long: fRow1 = lRow1
Dim ws2 As Worksheet: Set ws2 = wb.Worksheets("Sheet2")
Dim lRow2 As Long: lRow2 = ws2.Cells(ws2.Rows.Count, "E").End(xlUp).Row
Dim i As Long, j As Long
For i = 2 To lRow2
For j = 2 To lRow1
If ws2.Cells(i, "E").Value = ws1.Cells(j, "A").Value Then Exit For
Next j
' Note this possibility utilizing the behavior of the For...Next loop.
' No boolean necessary.
If j > lRow1 Then ' not found
fRow1 = fRow1 + 1
ws2.Cells(i, "E").Resize(, 2).Copy ws1.Cells(fRow1, "A")
End If
Next i
MsgBox "Found " & fRow1 - lRow1 & " differences.", vbInformation
End Sub

change to dynamic vba

i want to have rows from one worsheet copy to another worksheet based on a specific text, i need it to run as new data will be added daily, i am using this code now but it needs to be run after inputting data.
Sub CopyYes()
Dim c As Range
Dim j As Integer
Dim Source As Worksheet
Dim Target As Worksheet
Set Source = ActiveWorkbook.Worksheets("MAINGANG")
Set Target = ActiveWorkbook.Worksheets("REPAIRS")
j = 4
For Each c In Source.Range("C4:C10000")
If c = "X" Then
Source.Rows(c.Row).Copy Target.Rows(j)
j = j + 1
End If
Next c
End Sub
I think this will achieve what you are looking for:
Sub CopyYes()
Dim myCell As Range
Dim LastColumnSource As Long 'Integer data type is outdated.
Dim LastRowTarget As Long
Dim SourceSheet As Worksheet
Dim TargetSheet As Worksheet
Dim SourceRange As Range
Dim TargetRange As Range
Dim myArray As Variant
Set SourceSheet = ActiveWorkbook.Worksheets("Sheet1") <~~ change to your sheet name
Set TargetSheet = ActiveWorkbook.Worksheets("Sheet2") <~~ change to your sheet name
'Change the 1 to whichever column you need (1 represents column A)
LastRowTarget = TargetSheet.Cells(Rows.Count, 1).End(xlUp).Row
LastRowSource = SourceSheet.Cells(Rows.Count, 3).End(xlUp).Row
Set SourceRange = SourceSheet.Range("C4:C" & LastRowSource)
j = 4
For Each myCell In SourceRange
If myCell.Value = "X" Then
LastColumnSource = SourceSheet.Cells(myCell.Row, Columns.Count).End(xlToLeft).Column
myArray = SourceSheet.Range(Cells(myCell.Row, 1), Cells(myCell.Row, LastColumnSource))
LastColumnTarget = TargetSheet.Cells(LastRowTarget, Columns.Count).End(xlToLeft).Column
Set TargetRange = TargetSheet.Range("A" & LastRowTarget)
TargetRange.Resize(1, UBound(myArray, 2)) = myArray
LastRowTarget = LastRowTarget + 1
End If
Next myCell
End Sub
To make it dynamic, the last row and last column are found for both sheets and the row is written to an array to then write back to the results sheet (which avoids the use of copy).
As I don't know enough about your project, I've left the ActiveWorkbook in but you'd be better to specify the workbook (or ThisWorkbook if it's the workbook the code is run from) - This avoids runtime errors if the code executes whilst another workbook is in focus.
Usually, I use an array to process, as follows
’--------------------------------
dim arr(),temp()
worksheets(1).activate
arr=[a1].currentregion
j=0
for i=1 to ubound(arr)
if arr(i)="x" then
j=j+1
redim preserve temp(j)
temp(j)=arr(i)
end if
next
worksheets(2).activate
range("a1:a"&ubound(temp))=temp
Try,
Sub CopyYes()
Dim Source As Worksheet
Dim Target As Worksheet
Dim vDB, vR()
Dim i As Long, n As Long, r As Long
Dim j As Integer, c As Integer
Set Source = ActiveWorkbook.Worksheets("MAINGANG")
Set Target = ActiveWorkbook.Worksheets("REPAIRS")
vDB = Source.UsedRange
r = UBound(vDB, 1)
c = UBound(vDB, 2)
For i = 4 To r
If vDB(i, 3) = "X" Then
n = n + 1
ReDim Preserve vR(1 To c, 1 To n)
For j = 1 To c
vR(j, n) = vDB(i, j)
Next j
End If
End If
Target.Range("a4").Resize(n, c) = WorksheetFunction.Transpose(vR)
End Sub

Unable to arrange evenly scattered information from one sheet to another

I'm trying to arrange some uniformly scattered information from a spreadsheet to another. The information I'm interested in is in Sheet2 and I wish to place them in a customized manner in Sheet1.
I've tried with:
Sub ArrangeInformation()
Dim ws As Worksheet, cel As Range
Set ws = ThisWorkbook.Sheets("Sheet2")
Set tws = ThisWorkbook.Sheets("Sheet1")
For Each cel In ws.Range("A1:A" & ws.Cells(Rows.Count, 1).End(xlUp).row)
tws.Range(cel(1, 1).Address) = cel
Next cel
End Sub
The above script just replicates the same information in Sheet1 how they are in Sheet2.
Data in Sheet2:
How I like to arrange them in Sheet1:
How can I arrange those information in sheet1?
we can use some math to get the correct columns and rows:
Sub ArrangeInformation()
Dim ws As Worksheet
Set ws = ThisWorkbook.Sheets("Sheet2")
Dim tws As Worksheet
Set tws = ThisWorkbook.Sheets("Sheet1")
Dim i As Long
i = 0
Dim j As Long
j = 0
Dim cel As Range
For Each cel In ws.Range("A1:A" & ws.Cells(ws.Rows.Count, 1).End(xlUp).Row)
If cel <> "" Then
tws.Cells(Int(i / 4) + 1, ((j) Mod 4) + 1) = cel
i = i + 1
j = j + 1
End If
Next cel
End Sub
In the math the 4s are the number of items in each pattern
The INT will increase by 1 every four rounds and the MOD will repeat 1,2,3,4 every 4 rounds.
Just because:
this can be done with a formula:
=INDEX(Sheet2!$A:$A,INT(ROW(1:1)-1)/5+MOD((COLUMN(A:A)-1),5)+1)
where the 2 5s are the pattern and the +1 is the starting row of the data.
Put that in sheet1 A1 and copy over and down.
Maybe use Resize to transfer your data:
Sub Test()
Dim lr As Long, x As Long, y As Long
Dim ws1 As Worksheet: Set ws1 = ThisWorkbook.Sheets("Sheet1")
Dim ws2 As Worksheet: Set ws2 = ThisWorkbook.Sheets("Sheet2")
y = 1
lr = ws1.Cells(ws1.Rows.Count, "A").End(xlUp).Row
For x = 1 To lr Step 5
ws2.Cells(y, 1).Resize(, 4) = Application.Transpose(ws1.Cells(x, 1).Resize(4))
y = y + 1
Next x
End Sub
And propbably faster, you could assign your range to an array:
Sub Test()
Dim arr As Variant
Dim lr As Long, x As Long, y As Long
Dim ws1 As Worksheet: Set ws1 = ThisWorkbook.Sheets("Sheet1")
Dim ws2 As Worksheet: Set ws2 = ThisWorkbook.Sheets("Sheet2")
y = 1
lr = ws1.Cells(ws1.Rows.Count, "A").End(xlUp).Row
arr = ws1.Range("A1:A" & lr)
For x = LBound(arr) To UBound(arr) Step 5
ws2.Cells(y, 1).Resize(, 4) = Array(arr(x, 1), arr(x + 1, 1), arr(x + 2, 1), arr(x + 3, 1))
y = y + 1
Next x
End Sub
You'll need to manage which rows and columns you write to inside your loop. Something like:
Sub ArrangeInformation()
Dim ws As Worksheet, cel As Range
'New variables to manage rows and columns to write to
Dim writecol as integer, writeRow as integer
Set ws = ThisWorkbook.Sheets("Sheet2")
Set tws = ThisWorkbook.Sheets("Sheet1")
'Start in Row 1, Column 1
writeRow = 1
writeCol = 1
For Each cel In ws.Range("A1:A" & ws.Cells(Rows.Count, 1).End(xlUp).row)
tws.Range(writeRow, writeCol) = cel
'Next Column, or reset to 1 and iterate rows
If writeCol = 4 Then
writeCol = 1
writeRow = writeRow + 1
Else
writeCol = writeCol + 1
End If
Next cel
End Sub

How to split date and time

I would like to know how to split date and time into separate rows.
I would like Date in Column A and time in Column B with AM/PM included.
I tried with a delimiter of space but I get errors. On the internet people were first selecting cells but I want to do it without selecting cells.
Sub CompareTime()
Dim ws As Worksheet
Dim lastRow As Long
Dim arr As Long
Dim test As Double
Set ws = ActiveSheet
Cells(1, 2).EntireColumn.Insert
'Find last data point
With ws
lastRow = .Cells(.Rows.Count, "A").End(xlUp).Row
End With
For Count = 1 To lastRow
'split date
Cells(1, Count).Offset(0, 1) = Split(Cells(1, Count).Value, " ")
Next Count
End Sub
Date/time is a number and not a text string so to get the date you need to plit the integer from the decimal, then format them:
Sub CompareTime()
Dim ws As Worksheet
Dim lastRow As Long
Dim Count As Long
Dim test As Double
Set ws = ActiveSheet
'Find last data point
With ws
.Columns(2).Insert
lastRow = .Cells(.Rows.Count, "A").End(xlUp).Row
For Count = 5 To lastRow
'split date
test = .Cells(Count, 1).Value2
.Cells(Count, 1).Value2 = Int(test)
.Cells(Count, 1).NumberFormat = "m/d/yyyy"
.Cells(Count, 1).Offset(0, 1).Value2 = test - Int(test)
.Cells(Count, 1).Offset(0, 1).NumberFormat = "hh:mm AM/PM"
Next Count
End With
The other method. Scott's code more simple.
Sub CompareTime()
Dim ws As Worksheet
Dim lastRow As Long
Dim arr As Long
Dim test As Double
Dim vDB, vR(), n As Long, i As Long
Set ws = ActiveSheet
'Find last data point
With ws
.Cells(1, 2).EntireColumn.Insert
lastRow = .Cells(.Rows.Count, "A").End(xlUp).Row
vDB = .Range("a5", "a" & lastRow)
n = UBound(vDB, 1)
ReDim vR(1 To n, 1 To 2)
For i = 1 To n
vR(i, 1) = DateValue(Format(vDB(i, 1), "yyyy/mm/dd"))
vR(i, 2) = TimeValue(Format(vDB(i, 1), "hh:mm"))
Next i
.Range("a5").Resize(n, 2) = vR
.Columns(1).NumberFormatLocal = "mm/dd/yyyy"
.Columns(2).NumberFormatLocal = "hh:mm"
End With
End Sub
So I found a super easy way after messing with the split function. I just used a delimiter of space and split the date from time with AM/PM included.
Sub CompareTime()
Dim ws As Worksheet
Dim count As Long
Dim lastRow As Long
Dim arr As Long
Dim store As Double
Set ws = ActiveSheet
Cells(1, 2).EntireColumn.Insert
'Find last data point
With ws
lastRow = .Cells(.Rows.Count, "A").End(xlUp).Row
End With
For Count = 5 To lastRow
'split date
Cells(Count, 1).Offset(0, 1).Value = Split(Cells(Count, 1), " ")(1) & " " & Split(Cells(Count, 1), " ")(2)
Cells(Count, 1).Value = Split(Cells(Count, 1), " ")
Next Count
End Sub

Resources