Excel VBA performance of worksheetfunction vs code, for arrays - excel

In a quest to speed up my VBA code I searched for methods online. A lot of methods pass by, and one of them that keeps returning seems to be to use worksheetfunctions in stead of code, where possible.
My experience however is contrary to that tip. I find that worksheetfunctions tend to be slower than my code. My simple test below shows that the code is about twice faster than the worksheetfunction. I found the same results with other functions, like MATCH.
My question is then, do you VBA'ers tend to use code or worksheetfunctions? Are there reasons to use the functions over code (besides the couple of lines of extra code)?
Sub testSum()
Dim testarray(0 To 10000) As Variant
Dim val As Variant
Dim valSum As Variant
Dim i As Long, j As Long
Dim t As Single
' create testarray
For i = 0 To 10000
testarray(i) = Rnd
Next
For i = 0 To 10000 Step 100
testarray(i) = "text"
Next
' measure code
t = Timer
For j = 1 To 10000
valSum = 0
For Each val In testarray
If IsNumeric(val) Then valSum = valSum + val
Next
Next
Debug.Print "Array: ", Int(valSum), Timer - t
' measure function
t = Timer
For j = 1 To 10000
valSum = 0
valSum = Application.WorksheetFunction.Sum(testarray)
Next
Debug.Print "Sum: ", Int(valSum), Timer - t
End Sub

I prefer to use the worksheet function in the worksheet. Measuring the function in the macro is an unfair test as the VBA environment must switch to the Excel environment to get the results and this adds an overhead that does not exist otherwise.
As a general coding rule, my priorities are:
use Native Excel formulas, including setting up helper cells or
columns.
use VBA with arrays
use VBA with ranges
As a test:
set up 10,000 rows with random numbers
set up your formula in one cell (B1) =Sum(A1:A10000)
select B1 and calculate the cell
Set up a macro - the below is a quick and dirty based on your code above - it could be neater by using properly qualified ranges and Option Explicit.
Sub TestNativeSpeed()
Dim j As Long
Dim t As Single
t = Timer
For j = 1 To 10000
Range("B1").Dirty
Next
Debug.Print "Native Sum: ", Int(Range("B1").Value), Timer - t
End Sub
I set up my cells with the formula =Rand() and I got the results (single run only):
Native Sum: 4990 16.53125
I reset them to values only (consistent with your code) and I got the results (single run only):
Native Sum: 4990 1.765625
I ran your code and got (single run only):
Array: 4917 4.386719
Sum: 4917 27.73438
What the above shows is that Native Excel is usually the fastest and most efficient approach. But arrays (where it doesn't have to keep switching between the VBA and Excel models) are the next in line in terms of efficiency.

I almost exclusively use code.
It's probably an issue, but I hate using functions because I find that I always have to go back and edit bits and it just gets confusing, where I can just mess around with one Sub if I'm using code.
To answer your code, I would likely say just use arrays to sum - that'll shed a few picoSeconds
Try and use Code Review to get those brainiacs to improve on your speeds

Related

How Can I Re-Write this VBA Macro to be More Efficient? (Copy-Paste Range)

I currently have a full code written to copy the output of one spreadsheet, into certain columns of another spreadsheet. This is part of a project at work, but the VBA codes left to me from an employee that resigned, don't apply well. It's pretty simple in theory.
What I want it to do is pull the value in BB183 from the tab 737-10_1b28_routes in the file 737-10_1b28_routes.csv, and paste it in the tab 737-10 Scenario 1 of file Aero Sales Support Modified Att.1 Performance Data Attachment and Fill in Form_20220402.xlsx in box L30.
I then want the code to take BB184, and place it in L32. I need the code to skip a line because I want to paste different data in the other line (BB697 goes into to L31 with the same repeating pattern for BB (+1) and L (+2). I think once I have a more efficient code, I could figure out the final solution, but need some help. I'm currently running into procedure too large.
I feel like it's j=j+2 from j=30:688 for the L column and BB is like i=i+1 from i=183:512.
Then the second part of the code is j=j+2 from j=31:689 for the L column and BB is like i=i+1 from i=697:1026.
Please see code two to see how it's altered.
Sub vba_copy_data_GCD()
Workbooks("737-10_1b28_routes.csv").Worksheets("737-10_1b28_routes").Range("BB183").Copy _
Workbooks("Aero Sales Support Modified Att.1 Performance Data Attachment and Fill in Form_20220402.xlsx").Worksheets("737-10 Scenario 1").Range("L30")
Workbooks("737-10_1b28_routes.csv").Worksheets("737-10_1b28_routes").Range("BB184").Copy _
Workbooks("Aero Sales Support Modified Att.1 Performance Data Attachment and Fill in Form_20220402.xlsx").Worksheets("737-10 Scenario 1").Range("L32")
Workbooks("737-10_1b28_routes.csv").Worksheets("737-10_1b28_routes").Range("BB185").Copy _
Workbooks("Aero Sales Support Modified Att.1 Performance Data Attachment and Fill in Form_20220402.xlsx").Worksheets("737-10 Scenario 1").Range("L34")
Workbooks("737-10_1b28_routes.csv").Worksheets("737-10_1b28_routes").Range("BB186").Copy _
Workbooks("Aero Sales Support Modified Att.1 Performance Data Attachment and Fill in Form_20220402.xlsx").Worksheets("737-10 Scenario 1").Range("L36")
Workbooks("737-10_1b28_routes.csv").Worksheets("737-10_1b28_routes").Range("BB697").Copy _
Workbooks("Aero Sales Support Modified Att.1 Performance Data Attachment and Fill in Form_20220402.xlsx").Worksheets("737-10 Scenario 1").Range("L31")
Workbooks("737-10_1b28_routes.csv").Worksheets("737-10_1b28_routes").Range("BB698").Copy _
Workbooks("Aero Sales Support Modified Att.1 Performance Data Attachment and Fill in Form_20220402.xlsx").Worksheets("737-10 Scenario 1").Range("L33")
Workbooks("737-10_1b28_routes.csv").Worksheets("737-10_1b28_routes").Range("BB699").Copy _Workbooks("Aero Sales Support Modified Att.1 Performance Data Attachment and Fill in Form_20220402.xlsx").Worksheets("737-10 Scenario 1").Range("L35")
End Sub
Inspecting cell values from VBA is slow. Writing or copying values to cells from VBA is slower still. Doing these things over and over in a loop is a quick way to destroy VBA execution speed.
It is hundreds to thousands of times faster in execution speed to grab a large range and assign the values to a VBA array in one go, and then do the processing directly on the VBA array without touching any of the cells during the processing and then when done, write the entire array out to a worksheet in one go. The larger the ranges involved the greater the execution speed improvement by processing the VBA array instead of the cells directly.
Array processing is extremely fast in VBA. Worksheet cell access is extremely slow from VBA. It takes roughly the same amount of time to write a value to one cell as it does to write tens of thousands of values to a range from a VBA array. Never write individual cell values in a loop!
Using .Offset is also slow and doing so repeatedly is ill advised. This problem is avoided completely by using the array approach.
The following routine should do the trick if I understand your range descriptions adequately. vSrc and vDst are 2D VBA arrays. All the values are processed in the arrays (not on the sheets) and then when done the array values are written to the destination worksheet in one go...
Sub vba_copy_data_GCD()
Dim c&, i&, vSrc, vDst
Const SRC_GAP& = 514
Const SRC_RANGE$ = "bb183:bb1026"
Const SRC_SHEET$ = "737-10_1b28_routes"
Const SRC_WORKB$ = "737-10_1b28_routes.csv"
Const DST_RANGE$ = "l30:l688"
Const DST_SHEET$ = "737-10 Scenario 1"
Const DST_WORKB$ = "Aero Sales Support Modified Att.1 Performance Data Attachment and Fill in Form_20220402.xlsx"
vSrc = Workbooks(SRC_WORKB).Worksheets(SRC_SHEET).Range(SRC_RANGE).Value2
With Workbooks(DST_WORKB).Worksheets(DST_SHEET).Range(DST_RANGE)
vDst = .Value2
For i = 1 To UBound(vDst) \ 2 Step 2
c = c + 1
vDst(i + 0, 1) = vSrc(c, 1)
vDst(i + 1, 1) = vSrc(c + SRC_GAP, 1)
Next
.Value2 = vDst
End With
End Sub
Given how sure I am of what you want, please make sure your files are backed-up before trying this:
Sub vba_copy_data_GCD()
Dim srcWS as Worksheet
Dim destWS as Worksheet
Dim i as Long, j as Long
Set srcWS = Workbooks("737-10_1b28_routes.csv").Worksheets("737-10_1b28_routes")
Set destWS = Workbooks("Aero Sales Support Modified Att.1 Performance Data Attachment and Fill in Form_20220402.xlsx").Worksheets("737-10 Scenario 1")
For i = 0 to 329 Step 2
With destWS
.Range("L30").Offset(i,0).Value2 = srcWS.Range("BB183").Offset(j,0).Value2
.Range("L31").Offset(i,0).Value2 = srcWS.Range("BB697").Offset(j,0).Value2
End With
j = j + 1
Next i
End Sub

Summing the digits in Excel cells (long and short strings)

I'm working on a research related to frequencies.
I want to sum all the numbers in each cell and reduce them to single number only.
some cells have 2 numbers, others have 13 numbers. like these..
24.0542653897891
25.4846064424057
27
28.6055035477009
I tried several formulas to do that. the best ones have me 2 digits number, that I couldn't sum it again to get a single result.
like these Formulas:
=SUMPRODUCT(MID(SUBSTITUTE(B5,".",""),ROW(INDIRECT("1:"&LEN(B5)-1)),1)+0)
=SUMPRODUCT(1*MID(C5,ROW(INDIRECT("1:"&LEN(C5))),1))
any suggestion?
Thank you in advance.
EDIT
Based on your explanation your comments, it seems that what you want is what is called the digital root of the all the digits (excluding the decimal point). In other words, repeatedly summing the digits until you get to a single digit.
That can be calculated by a simpler formula than adding up the digits.
=1+(SUBSTITUTE(B5,".","")-1)-(INT((SUBSTITUTE(B5,".","")-1)/9)*9)
For long numbers, we can split the number in half and process each half. eg:
=1+MOD(1+MOD(LEFT(SUBSTITUTE(B5,".",""),INT(LEN(SUBSTITUTE(B5,".",""))/2))-1,9)+1+MOD(RIGHT(SUBSTITUTE(B5,".",""),LEN(SUBSTITUTE(B5,".",""))-INT(LEN(SUBSTITUTE(B5,".",""))/2))-1,9)-1,9)
However, the numbers should be stored as TEXT. When numbers are stored as numbers, what we see may not necessarily be what is stored there, and what the formula (as well as the UDF) will process.
The long formula version will correct all the errors on your worksheet EXCEPT for B104. B104 appears to have the value 5226.9332653096000 but Excel is really storing the value 5226.9333265309688. Because of Excel's precision limitations, this will get processed as 5226.93332653097. Hence there will be a disagreement.
Another method that should work would be to round all of the results in your column B to 15 digits (eg: . Combining that with using the long formula version should result in agreement for all the values you show.
Explanation
if a number is divisible by 9, its digital root will be 9, otherwise, the digital root will be n MOD 9
The general formula would be: =1+Mod(n-1,9)
In your case, since we are dealing with numbers larger than can be calculated using the MOD function, we need to both remove the dot, and also use the equivalent of mod which is n-(int(n/9)*9)
Notes:
this will work best with numbers stored as text. Since Excel may display and/or convert large numbers, or numbers with many decimal places, differently than expected, working with text strings of digits is the most stable method.
this method will not work reliably with numbers > 15 digits.
If you have numbers > 15 digits, then I suggest a VBA User Defined Function:
Option Explicit
Function digitalRoot(num As String) As Long
Dim S As String, Sum As Long, I As Long
S = num
Do While Len(S) > 1
Sum = 0
For I = 1 To Len(S)
Sum = Sum + Val(Mid(S, I, 1))
Next I
S = Trim(Str(Sum))
Loop
digitalRoot = CLng(S)
End Function
You could use a formula like:
=SUMPRODUCT(FILTERXML("<t><s>"&SUBSTITUTE(A1," ","</s><s>")&"</s></t>","//s"))
You might need an extra SUBSTITUTE for changing . to , if that's your decimal delimiter:
=SUMPRODUCT(FILTERXML("<t><s>"&SUBSTITUTE(SUBSTITUTE(A1,".",",")," ","</s><s>")&"</s></t>","//s"))
However, maybe a UDF as others proposed is also a possibility for you. Though, something tells me I might have misinterpreted your question...
I hope you are looking for something like following UDF.
Function SumToOneDigit(myNumber)
Dim temp: temp = 0
CalcLoop:
For i = 1 To Len(myNumber)
If IsNumeric(Mid(myNumber, i, 1)) Then temp = temp + Mid(myNumber, i, 1)
Next
If Len(temp) > 1 Then
myNumber = temp
temp = 0
GoTo CalcLoop
End If
SumToOneDigit = temp
End Function
UDF (User Defined Functions) are codes in VBA (visual basic for applications).
When you can not make calculations with Given Excel functions like ones in your question, you can UDFs in VBA module in Excel. See this link for UDF .. If you dont have developer tab see this link ,, Add a module in VBA in by right clicking on the workbook and paste the above code in that module. Remember, this code remains in this workbook only. So, if you want to use this UDF in some other file your will have to add module in that file and paste the code in there as well. If you are frequently using such an UDF, better to make add-in out of it like this link
In addition to using "Text to Columns" as a one-off conversion, this is relatively easy to do in VBA, by creating a user function that accepts the data as a string, splits it into an array separated by spaces, and then loops the elements to add them up.
Add the following VBA code to a new module:
Function fSumData(strData As String) As Double
On Error GoTo E_Handle
Dim aData() As String
Dim lngLoop1 As Long
aData = Split(strData, " ")
For lngLoop1 = LBound(aData) To UBound(aData)
fSumData = fSumData + CDbl(aData(lngLoop1))
Next lngLoop1
fExit:
On Error Resume Next
Exit Function
E_Handle:
MsgBox Err.Description & vbCrLf & vbCrLf & "fSumData", vbOKOnly + vbCritical, "Error: " & Err.Number
Resume fExit
End Function
Then enter this into a cell in the Excel worksheet:
=fSumData(A1)
Regards,
The UDF below will return the sum of all numbers in a cell passed to it as an argument.
Function SumCell(Cell As Range) As Double
Dim Fun As Double ' function return value
Dim Sp() As String ' helper array
Dim i As Integer ' index to helper array
Sp = Split(Cell.Cells(1).Value)
For i = 0 To UBound(Sp)
Fun = Fun + Val(Sp(i))
Next i
SumCell = Fun
End Function
Install the function in a standard code module, created with a name like Module1. Call it from the worksheet with syntax like =SumCell(A2) where A2 is the cell that contains the numbers to be summed up. Copy down as you would a built-in function.

Excel VBA get cell value after recalculation

I have a worksheet that calculates various values based on a random value and would like to use the law of large numbers to converge to an average for each calculation.
I am thinking of using VBA to execute the calculation 1000's of times and store the values in a list for averaging at the end. My current testing code only stores the original value after each iteration. ie Safety1 does not change even though the value in R36 changes.
Dim Safety1(0 To 10) As Long
For i = 0 To 10
Safety1(i) = Sheet34.Range("R36").Value
Debug.Print Safety1(i)
Next i
myAverage = Application.WorksheetFunction.Average(Safety1)
myAverage should be the converging average.
R36 contains the sum of other ranges, which contain values based on rand()
If there is a better way to do this, i am happy to listen.
Thanks in advance.
This post resolved the problem. I needed to wait until the calculation process had completed before storing the value
Please do sheet calculate like this:
Dim Safety1(0 To 10) As Long
Application.ScreenUpdating = False
For i = 0 To 10
Worksheets("Sheet34").Calculate
Safety1(i) = Sheet34.Range("R36").Value
Debug.Print Safety1(i)
Next i
Application.ScreenUpdating = True
myAverage = Application.WorksheetFunction.Average(Safety1)

Looping thru 2D array using VBA

I have several designs, and each design has several variables.
Using VBA code, I want to loop thru each design, and assign variables to each of the indices of the one design.
I'm having a hard time getting the array dim statement correct, and correctly calling the nth variable of each design.
I've shown VBA code here that uses a message box to display if I am getting the right value from each j loop, and each design.
Any help?
Dim Design(0 To 9) As Variant
Sub main
Design(1) = Array(4,6,2,1)
Design(2) = Array(8,2,6,9)
For i = 1 To 2
For j = 1 to 4
MsgBox (Design(i)(j-1))
Next j
Next i
End sub
Wow, my apologies. I've been trying for WEEKS to get this to work, and in formatting it for this question, it FINALLY worked! Code is good as is.

SumProduct over sets of cells (not contiguous)

I have a total data set that is for 4 different groupings. One of the values is the average time, the other is count. For the Total I have to multiply these and then divide by the total of the count. Currently I use:
=SUM(D32*D2,D94*D64,D156*D126,D218*D188)/SUM(D32,D94,D156,D218)
I would rather use a SumProduct if I can to make it more readable. I tried to do:
=SUMPRODUCT((D2,D64,D126,D188),(D32,D94,D156,D218))/SUM(D32,94,D156,D218)
But as you can tell by my posting here, that did not work. Is there a way to do SumProduct like I want?
I agree with the comment "It might be possible with masterful excel-fu, but even if it can be done, it's not likely to be more readable than your original solution"
A possible solution is to embed the CHOOSE() function within your SUMPRODUCT (this trick actually is pretty handy for vlookups, finding conditional maximums, etc.).
Example:
Let's say your data has eight observations and is in two columns (columns B and C) but you don't want to include some observations (exclude observations in rows 4 and 5). Then the SUMPRODUCT code looks like this...
=SUMPRODUCT(CHOOSE({1,2},A1:A3,A6:A8),CHOOSE({1,2},B1:B3,B6:B8))
I actually thought of this on the fly, so I don't know the limitations and as you can see it is not that pretty.
Hope this helps! :)
It might be possible with masterful excel-fu, but even if it can be done, it's not likely to be more readable than your original solution. The problem is that even after 20+ years, Excel still borks discontinuous ranges. Naming them won't work, array formulas won't work and as you see with SUMPRODUCT, they don't generally work in tuple-wise array functions. Your best bet here is to come up with a custom function.
UPDATE
You're question got me thinking about how to handle discontinuous ranges. It's not something I've had to deal with much in the past. I didn't have the time to give a better answer when you asked the question but now that I've got a few minutes, I've whipped up a custom function that will do what you want:
Function gvSUMPRODUCT(ParamArray rng() As Variant)
Dim sumProd As Integer
Dim valuesIndex As Integer
Dim values() As Double
For Each r In rng()
For Each c In r.Cells
On Error GoTo VBAIsSuchAPainInTheAssSometimes
valuesIndex = UBound(values) + 1
On Error GoTo 0
ReDim Preserve values(valuesIndex)
values(valuesIndex) = c.Value
Next c
Next r
If valuesIndex Mod 2 = 1 Then
For i = 0 To (valuesIndex - 1) / 2
sumProd = sumProd + values(i) * values(i + (valuesIndex + 1) / 2)
Next i
gvSUMPRODUCT = sumProd
Exit Function
Else
gvSUMPRODUCT = CVErr(xlErrValue)
Exit Function
End If
VBAIsSuchAPainInTheAssSometimes:
valuesIndex = 0
Resume Next
End Function
Some notes:
Excel enumerates ranges by column then row so if you have a continuous range where the data is organized by column, you have to select separate ranges: gvSUMPRODUCT(A1:A10,B1:B10) and not gvSUMPRODUCT(A1:B10).
The function works by pairwise multiplying the first half of cells with the second and then summing those products: gvSUMPRODUCT(A1,C3,L2,B2,G5,F4) = A1*B2 + C3*G5 + L2*F4. I.e. order matters.
You could extend the function to include n-wise multiplication by doing something like gvNSUMPRODUCT(n,ranges).
If there are an odd number of cells (not ranges), it returns the #VALUE error.
Note that sumproduct(a, b) = sumproduct(a1, b1) + sumproduct(a2, b2) where range a is split into ranges a1 and a2 (and similar for b)
It might be helpful to create an intermediate table that summarizes the data that you are using to calculate the sum product. That would also make the calculation easier to follow.

Resources