Root Mean Square (rms) function in VBA? - excel

So I'm calculating basic statistics in my worksheet and it includes code such as:
xxx = Application.worksheetfunction.average(etc etc etc
yyy = Application.worksheetfunction.min(etc etc etc
zzz = Application.worksheetfunction.max(etc etc etc
My question: Is there an RMS equivalent function where I can simply plug it in place of where I have 'average, min, max' functions in that code? And if there isn't then what would be the most efficient means to code in to find RMS solutions?
I hope I've stated the goal clearly enough. I'm curious as to whether or not there is a predefined RMS function for VBA or whether or not I've got to create some sort of user defined function? ~ That of which I'm fairly new to as well so if there isn't a simple line of code to write for this, I'll have to do more reading on UDF's.
EDIT:
I've got around 30,000 rows, and for simplicity's sake: imagine two columns. Column A has the year i.e. 1941 or anything else through 2008. Column B is a numeric value. I'm just trying to put code together that gives decade summaries of Average, Min, Max, and the RMS values.

You can do the average with
=SQRT(SUMSQ(A:A)/COUNTA(range))
or in VBA:
r = (Application.WorksheetFunction.SumSq(Range("A:A")) / Range("A:A").Count) ^ (1 / 2)

A VBA function that accepts arrays (any rank) and ranges with multiple areas (a discontinuous range like A4:B6,C11:D15), or even a union of ranges in a formula. It skips non number datatypes (including dates, boolean, blanks etc).
You can use it in VBA code, or as a UDF in a worksheet formula such as:
"=RMS(A1:A10)" (basic usage)
"=RMS(A1:A10,C1:C10)" (multiple ranges (or arrays for that matter))
"{=RMS({1,2,3,4})}" (array formula entered with Ctrl+shift+enter)
Function RMS(ParamArray args()) As Double
Dim arg, arr, area As Range, ss As Double, n As Long
For Each arg In args
If TypeOf arg Is Range Then
For Each area In arg.Areas
arr = area.value
If VarType(arr) < vbArray Then
queryRmsElements Array(arr), ss, n
Else
queryRmsElements arr, ss, n
End If
Next area
ElseIf VarType(arg) > vbArray Then
queryRmsElements arg, ss, n
Else
Err.Raise 1, "RMS", "Invalid Argument"
End If
Next arg
RMS = (ss / n) ^ 0.5
End Function
Private Sub queryRmsElements(ByRef elements, ByRef ss As Double, ByRef n As Long)
Static element As Variant
'Enumerate to cover rank > 1 (vs. Iterate)
For Each element In elements
Select Case VarType(element)
Case VbVarType.vbByte, _
VbVarType.vbCurrency, _
VbVarType.vbDecimal, _
VbVarType.vbDouble, _
VbVarType.vbInteger, _
VbVarType.vbLong, _
VbVarType.vbSingle
ss = element ^ 2 + ss
n = n + 1
Case Else
End Select
Next element
End Sub

This one worked for me:
Function RMS(Intervalo As Range)
Dim SomaQ As Double
Dim Tamanho As Integer
SomaQ = 0
Tamanho = Intervalo.Count
SomaQ = Application.WorksheetFunction.SumSq(Intervalo)
RMS = Sqr(SomaQ / Tamanho)
End Function

Related

Using a FOR loop within an Excel VBA Function

I created a simple function in MATLAB, and am trying to convert the function into Excel VBA function. My goal is to create an Excel formula =RT('range of dB levels', 'delta-time') and output the estimated reverberation time. The math is simple, see MATLAB code below:
function rr=RT(lvl_broad, dt)
n=12; %number of samples within slope calc
slope=zeros(length(lvl_broad), 1);
for i=1:length(lvl_broad)
if i<((n/2)+1) | i>length(lvl_broad)-(n/2)-1
slope(i)=0;
else
slope(i)=(lvl_broad(i+(n/2))-lvl_broad(i-(n/2)))/n;
end
end
min_slope=min(slope);
rr=abs(dt./min_slope)*60;
end
In excel, I modified/simplified this until I no longer got errors, however, the cell that I enter my 'RT' function in returns #VALUE and I do not know why. Does anything stand out in the code below? (note I changed the input range from lvl_broad to InterruptedNZ)
Function RT(InterruptedNZ, dt)
Dim Slope As Double
Slope = Slope(InterruptedNZ.Height, 1)
For i = 1 To InterruptedNZ.Height
If i < ((6) + 1) Or i > (InterruptedNZ.Height - (6) - 1) Then
Slope(i) = 0
Else
Slope(i) = (InterruptedNZ(i + (6)) - InterruptedNZ(i - (6))) / 12
End If
Next
End
min_slope = Application.WorksheetFunction.Min(Slope)
RT = Abs((dt / min_slope) * 60)
End Function
Here are some tips to translate MATLAB code into VBA code:
length()
If you are trying to get the dimensions of a range, you'll need to use the .Rows.Count or .Columns.Count properties on the range you are working with.
PERFORMANCE NOTE:
When you have a large enough range, it's a good idea to store the values of the range inside an array since you will reduce the number of times you access data from the sheets which can comme with lot of overhead. If so, you'll have to use ubound() and lbound().
zeros()
In VBA, there is no exact equivalent to the zeros() function in MATLAB. The way we would initialize an array of zeros would simply be by initializing an array of doubles (or another numerical type). And since the default value of a double is zero, we don't need to do anything else :
Dim Slope() As Double
ReDim Slope(1 To InterruptedNZ.Rows.Count)
Note that you cannot pass the dimensions in the Dim statement since it only accepts constants as arguments, so we need to create Slope as a dynamic array of doubles and then redimension it to the desired size.
Putting these two principles together, it seems like your code would look something like this:
Function RT(ByRef InterruptedNZ As Range, ByVal dt As Double)
Dim Slope() As Double
ReDim Slope(1 To InterruptedNZ.Rows.Count)
Dim i As Long
For i = 1 To InterruptedNZ.Rows.Count
If i < ((6) + 1) Or i > (InterruptedNZ.Rows.Count - (6) - 1) Then
Slope(i) = 0
Else
Slope(i) = (InterruptedNZ(i + (6)) - InterruptedNZ(i - (6))) / 12
End If
Next
Dim min_slope As Double
min_slope = Application.WorksheetFunction.Min(Slope)
RT = Abs((dt / min_slope) * 60)
End Function
Addtionnal notes:
Refering to cells from a range like this InterruptedNZ(i) works but it is good practice to be more specific like this (assuming column range) :
InterruptedNZ.Cells(i,1)
During my tests, I had a division by zero error since min_slope was zero. You might want to account for that in your code.

Qualifier errors when attempting to debug, along with final lines -- help pls

Below is the code i have put together from various examples to try achieve my goal. Concept is to be dynamic and retrieve from survey sheet within my workbook, to be able to obtain the corresponding TVD for the MD
--Use while loop only to run code if there is a depth in Column B Present. Nested loop uses the difference between depths to calculate a gradient.
---The issue i'm having is getting past my first debug error "Invalid Qualifier".
----Lastly, any suggestions for how i would then return the TVD to Column A, relevant to the looked up MD, within the nested loop to maintain the row in which the MD was grabbed. Sorry for making this so wordy, been working on this for over 10hrs while at work.
http://www.wellog.com/tvd.htm
Sub MdtoTVD()
Dim MD1 As String, MD2 As Integer
Dim TVD1 As String, TVD2 As Integer
Dim Srng As Range 'Survey MD column
Dim MDrng As Range 'MdtoTVD MD column as range
Dim MDdiff As Integer ' Var to calculate difference of MD end from MD start
Dim TVDdiff As Integer ' Var to calculate difference of TVD end from TVD start
Dim TVDincr As Double ' var to use for stepping TVD
Dim MDrow As Integer
Dim i As Long
MDrng = Range("Surveys!B27:B215") 'range from the survey sheet within my report book
Srng = Range("Surveys!G27:G215") 'range from the survey sheet within my report book
Dim X As Integer
X = 2
While Not (IsEmpty(Sheets("MDtoTVD").Cells(X, 2).Value)) 'runs loop as long as there a MD depth to be looked up
Cells(X, 2) = MDrow 'assigns current row value to for MD input
MD1.Value = Application.WorksheetFunction.Index(Srng, Application.WorksheetFunction.Match(MDrow, MDrng, 1)) ' retrieves Start point for MD
MD2.Value = Application.WorksheetFunction.Index(Srng, Application.WorksheetFunction.Match(MDrow, MDrng, 1) + 1) 'retrieves end point for MD
TVD1.Value = Application.WorksheetFunction.Index(MDrng, Application.WorksheetFunction.Match(MDrow, Srng, 1)) 'retrieves start point for TVD
TVD2.Value = Application.WorksheetFunction.Index(MDrng, Application.WorksheetFunction.Match(MDrow, Srng, 1) + 1) 'retrieves end point for TVD
MDdiff.Value = (MD2 - MD1) 'assigns and calculates difference of MD end from MD start
TVDdiff.Value = (TVD2 - TD1) 'assigns and calculates difference of TVD end from TVD start
TVDincr.Value = MDdiff / TVDdiff 'Divides MD by TVD to get increment per foot
For i = 1 To MDdiff Step TVDincr 'set max loop run to amount of feet between survey points
Cells(X, 1).Value = TVD1 + i 'uses the loop to increment the TVD from start point
Next i
Wend
End Sub
I can see a number of problems with your code:
MD1, MD2, TVD1, TVD2 are all of type String. Also, MDdiff, TVDdiff and TVDIncr are all of type Integer. The property Value is not defined for a string or integer variable. Just remove the .Value from all of them and you won't get the "Invalid Qualifier" error.
After you do the above, the following lines will give another error about type mismatch:
MDdiff = (MD2 - MD1)
TVDdiff = (TVD2 - TD1)
because you're trying to subtract a string from another string and assign the result to an integer. Not sure what to advise there, you have to consider what you're trying to achieve and act accordingly. Maybe they shouldn't be strings in the first place? I don't know, up to you to determine that.
At the very least, you can cast strings to integers if you're really sure they're string representations of integers by doing CInt(string_var) or use CLng to convert to long. If the strings are not string representations of integers and you try to cast them to integers, you'll get a type mismatch error.
When you assign a value to a Range object, you need to use Set. So do:
Set MDrng = Range("Surveys!B27:B215")
Set Srng = Range("Surveys!G27:G215")
to correctly set the ranges.
Another problem is that you haven't assign a value to X but you use it as a cell index. By default, uninitialised numeric variables in VBA get assigned the value of 0, so doing .Cells(X, 2) will fail because row 0 is not a valid row index.
In this line:
TVDincr = MDdiff / TVDdiff
you're dividing two integers and you assign the result to another integer. Note that if the result of the division happens to be a decimal (like 3 / 2 = 1.5), your TVDincr integer will actually contain just 1, i.e. you lose some precision. I don't understand your code to know if it's ok or not, you have to judge for yourself, I'm pointing it out just in case you're not aware of that.
Also, if TVDdiff happens to be 0, then you'll get a "division by zero" error.
This line in your For loop:
Cells(X, 1).Value = TVD1 + i
will also generate an error, because you're trying to numerically add TVD1 (a string) and i (a long). Perhaps you're trying to concatenate the two, in which case you should replace + with &.
There's also a problem when calling the WorksheetFunctions, but I haven't been able to determine the cause. Probably if you fix the other errors then it'll be easier to understand what's going on, not sure though. You just have to investigate things a little bit too.

Simple VBA function that returns product of range value

I want the function to take a range of cells as an argument and return their product.
Let's assume the following value for cells:
A1=5
A2=2
A3=3
Let's call the function Multiply.
=Multiply(A1:A3) will return 30 (=5×2×3).
What is the code for this? I'm just trying to familiarize myself with the syntax and this will help out a lot.
Edit: figured it out:
Function multiply(rng As Range)
multiplied = 1
For Each cell In rng
multiplied = multiplied * cell.Value
Next
multiply = multiplied
End Function
You can use the VBA version of PRODUCT directly, ie
MsgBox WorksheetFunction.Product([a1:a3])
You can use excel worksheet functions.
The default behaviour of SUMPRODUCT if there is only a single range provided is to return the sum so you can just pass the range to SUMPRODUCT this way:
WorksheetFunction.Sumproduct(**your range goes here**)
OK for =PRODUCT, which does the job (SUMPRODUCT will NOT do what the initial fellow asked).
But, just for fun, using the properties of the math functions EXP and LN we have that if
X = A1*B1*C1 then
LN(X) = LN(A1)+LN(B1)+LN(C1)
and
X = EXP(LN(X)) or
X = EXP(LN(A1)+LN(B1)+LN(C1))
so just put in a cel: =EXP(SUM(LN(A1:C1))) and voilà! (you must use CTRL+SHIFT+ENTER)
If you insist of writing your own function then try the following. It loads all the values into memory first and thus should work faster.
Public Function RngProduct(ByRef r as Range) as Double
Dim vals() as Variant, res as Double
vals = r.Value
Dim i as Long, N as Long
N = r.Rows.Count
res = #1
For i=1 to N
res = res * vals(i,1)
Next i
RngProduct = res
End Function

Counting rows in VBA excel

I'm designing a function in VBA of the form myFunction(x,y,z) where z is a table, and x can take the values of the column headings. As part of the function I need to find the number of rows in z.
I'm having problems with this, as everywhere I look suggests using length = z.Rows.Count, but when I try and output this value (as in, set myFunction = length), it produces a VALUE error. However, when I output myFunction = a which doesn't directly use length (it will eventually form part of an IF statement once I get it working), the function works fine. My code is below:
Public Function myFunction(x As String, y As Double, z As Range) As Double
Dim upper_threshold As Double
Dim lower_threshold As Double
Dim a As Double
Dim rates As Variant
Dim u As Byte
Dim l As Byte
Dim r As Byte
Dim length As Byte
a = 0
u = 2
l = 1
rates = Application.WorksheetFunction.Index(z, 1, 0)
r = Application.WorksheetFunction.Match(x, rates, 0)
length = z.rows.Count
upper_threshold = z(u, 1)
Do While y > upper_threshold
u = u + 1
l = l + 1
upper_threshold = z(u, 1)
lower_threshold = z(l, 1)
If y < upper_threshold Then
a = a + z(l, r) * (y - lower_threshold)
Else
a = a + z(l, r) * (upper_threshold - lower_threshold)
End If
Loop
myFunction = a
End Function
To test it out I also created another function:
Public Function myRows(myTable As Range) As Double
myRows = myTable.rows.Count
End Function
This one works fine on its own, but when I try to use it within the other function, I still get a VALUE error. I've tried declaring length as every type I can think of and it doesn't seem to help.
Can anyone see what's going on?
EDIT: I'm obviously not making myself very clear. The function without the two lines referring to length works as I intended. However, I need to add a bit of code to increase its functionality and this involves calculating the number of rows in the table z. When I add the two lines shown here into the function it continues to work, since it doesn't affect the output. However, if I then set the output to show length, i.e. change the penultimate line to myFunction = length it gives me a VALUE error. This leaves me with two options as far as I can see: either something else in the program is impacting on these two lines (some clashes of syntax or something), or I'm making a mistake in just assuming I can output length like that.
Your problem is with:
rates = Application.WorksheetFunction.Index(z, 1, 0)
Index only accepts a single row or column, otherwise you get a VALUE error.

EXCEL XOR multiple bits

I have two cells with a string of bits 0111010 and 0101011. I want to XOR the two together so that the resulting cell would be 0010001.
I know you can use this for boolean values
=OR(AND(A1,NOT(A2)),AND(A2,NOT(A1)))
but it doesn't work for a string of bits.
You need to use VBA to do this. If you open VBA, create a new Module and enter the function
Public Function BITXOR(x As Long, y As Long)
BITXOR = x Xor y
End Function
You can then use the DEC2BIN and BIN2DEC to convert from binary to decimal to run this function. For example:
Cell A1 = 0111010
Cell A2 = 0101011
=DEC2BIN(BITXOR(BIN2DEC(A1),BIN2DEC(A2)))
You can do this with VBA:
Public Function XOR_binary(b1, b2) As String
Dim len_b1
Dim len_b2
Dim len_diff
Dim i
Dim bit1
Dim bit2
' see if the two string are the same length. If not, add 0's to
' the beginning of the shorter string
len_b1 = Len(b1)
len_b2 = Len(b2)
len_diff = len_b1 - len_b2
Select Case len_diff
Case Is < 0
' b2 is longer
b1 = String(Abs(len_diff), "0") & b1
Case Is = 0
' they're the same length
Case Is > 0
' b1 is longer
b2 = String(len_diff, "0") & b2
End Select
XOR_binary = ""
For i = Len(b2) To 1 Step -1
bit1 = CInt(Mid(b1, i, 1))
bit2 = CInt(Mid(b2, i, 1))
XOR_binary = CInt(bit1 Xor bit2) & XOR_binary
Next i
End Function
Probably not the best implementation, but it works.
Using your example, A3 contains:
=XOR_Binary(A1,A2)
The resulting string will have the same number of bits as the longest string you pass in.
Here is a solution without using VBA:
=TEXT(SUMPRODUCT(MOD(INT(MID(A1,{1,2,3,4,5,6,7},1))+INT(MID(A2,{1,2,3,4,5,6,7},1)),2),{1000000,100000,10000,1000,100,10,1}),"0000000")
This calculates the bitwise XOR using SUMPRODUCT and TEXT to turn it into a string of bits.
Note: this formula requires both input values to have length 7 (as per your own example) and the output will also have length 7. To allow for different input lengths, simply implement the necessary truncation and/or padding.
You can choose to use some shorthand definitions:
define BitPositions as ={1,2,3,4,5,6,7} (7-bit),
define BitStrings as ={1000000,100000,10000,1000,100,10,1} (7-bit),
define BitFormat as ="0000000" (7-bit),
then your formula can be made a bit more legible/shorter/cleaner:
=TEXT(SUMPRODUCT(MOD(INT(MID(A1,BitPositions,1))+INT(MID(A2,BitPositions,1)),2),BitStrings),BitFormat)
This also makes it easier to work with larger strings of bits, e.g.:
define BitPositions as =ROW(INDIRECT("1:32")) (32-bit),
define BitStrings as =10^(32-ROW(INDIRECT("1:32"))) (32-bit),
define BitFormat as =REPT("0",32) (32-bit)
Should you wish to implement NOT/OR/AND/etc. then you can get your inspiration from these formulas for the decimal counterparts; here are some more in-depth explanations for XOR with SUMPRODUCT though it also uses decimal inputs.
=1-(A1<>0)+(A2<>0) for each bit.
You can split it into individual columns for the above formula using this:
=MID(A1|7|1)
=MID(A1|6|1)
=MID(A1|5|1)
=MID(A1|4|1)
=MID(A1|3|1)
=MID(A1|2|1)
=MID(A1|1|1)
...
' this VBA returns a double that has to be formatted on the worksheet.
Option Explicit
Public Function MYXOR(r1 As Range, r2 As Range) As Double
'r1 and r2 are expected as HEX; for example,
'DEC2HEX(CODE("B")) returns ASCII of "B" as HEX
On Error GoTo ErrHandler
MYXOR = "&H" & r1.Value Xor "&H" & r2.Value
GoTo CleanUp
ErrHandler:
MYXOR = Err.Number
Resume CleanUp
CleanUp:
' format the double being returned in MYXOR with TEXT(DEC2HEX(MYXOR(C9,F9)),"00000")
' number of leading zeroes according to the size of the HEX in r1 and r2
End Function

Resources