I have created a VBA function to calculate a weighted average. The function has optional parameters ("criterias") that allow to restrict the weighted average calculation to a subset of rows only (all Apple rows in the example below).
The function works fine for inputs of the form :
weightedAverage([5 2 5],[2 3 4], [Apple Orange Apple], Apple)
If I change the input on the "criterias" as follows, the function returns an error:
weightedAverage([5 2 5],[2 3 4], ([Apple Orange Apple]=Apple), TRUE)
Is there a way that the function first evaluates the third input and translates it to [TRUE FALSE TRUE]?
(The workbook function sumproduct is able to handle inputs of the form sumproduct(--([Apple Orange Apple]=Apple),...), which is why I am wondering whether is also possible for my function)
Function weightedAverage(vals As Range, weight As Range, ParamArray criterias() As Variant) As Double
' Calcualted Weighted Average of "vals" weighted by "weight"
' Optionally the weighted average can be constrained to certain "vals" only by using "criterias"
Dim totalweight As Double 'sum up weights where "vals" is a number and "criterias" is true
Dim result As Double 'sum up vals(i) * weights(i)
Dim takevalue As Boolean 'temporary variable store whether "vals" is a number and "criterias" are met
' Init
result = 0
totalweight = 0
' Loop over vals
For i = 1 To vals.Count
' Check whether vals is a numeric and not empty
takevalue = IsNumeric(vals.Cells(i).Value) And Not (IsEmpty(vals.Cells(i).Value))
' Check whether criterias are satisfied
For ii = 0 To UBound(criterias) - 1 Step 2
' Exit loop if condition is not satisfied
If takevalue = False Then Exit For
' Test whether condition is true
takevalue = ((LCase(criterias(ii).Cells(i).Value) = LCase(criterias(ii + 1).Cells(1).Value)) And takevalue)
Next ii
' If all conditions are satisfied, add value to results and totalweight
If takevalue Then
result = result + vals(i) * weight(i)
totalweight = totalweight + weight(i)
End If
Next i
' Calculate weighted average
weightedAverage = result / totalweight
End Function
Thank you, Tim, for your help. The function needs to be executed using Ctrl+Shift+Enter and the fourth input needs to be dropped.
The below weightedAverage function works with the following input:
weightedAverage([5 2 5],[2 3 4], ([Apple Orange Apple]=Apple))
Function weightedAverage(vals As Range, weight As Range, ParamArray criterias() As Variant) As Double
' Calcualted Weighted Average of "vals" weighted by "weight"
' Optionally the weighted average can be constrained to certain "vals" only by using "criterias"
Dim totalweight As Double 'sum up weights where "vals" is a number and "criterias" is true
Dim result As Double 'sum up vals(i) * weights(i)
Dim takevalue As Boolean 'temporary variable store whether "vals" is a number and "criterias" are met
' Init
result = 0
totalweight = 0
' Loop over vals
For i = 1 To vals.Count
' Check whether vals is a numeric and not empty
takevalue = IsNumeric(vals.Cells(i).Value) And Not (IsEmpty(vals.Cells(i).Value))
' Check whether criterias are satisfied
For ii = 0 To UBound(criterias)
' Exit loop if condition is not satisfied
If takevalue = False Then Exit For
' Test whether condition is true
takevalue = (criterias(ii)(i, 1) And takevalue)
Next ii
' If all conditions are satisfied, add value to results and totalweight
If takevalue Then
result = result + vals(i) * weight(i)
totalweight = totalweight + weight(i)
End If
Next i
' Calculate weighted average
weightedAverage = result / totalweight
End Function
Related
I have a form that has a field with Inventory On Hand called QOH. I have 30 fields, each one contains a daily requirement and there is one field for each day. The fields are D1 through D30. What I want to do is create a loop that deducts the requirements (D1 through D30) from the QOH until the QOH falls below zero. I am counting the number of days as it is looping y = y + 1 to come up with the number of days of coverage. I then will return the counted result into a field on the form
Private Sub PDS_AfterUpdate()
Dim w As Integer 'generate the appropriate field reference'
Dim y As Integer 'My counter (The Value) that I return to the form'
Dim z As Double 'Quantity On Hand QOH'
Dim a As String
Dim strFieldName As String
z = Me.P0 ' PO is the field that contains the QOH - Quantity On Hand
If z < 0 Then ' If the QOH is already below zero return a -1
y = -1
End If
If z >= 0 Then
Do Until z < 0
w = w + 1 'This is used to get the number to add to the field to get the correct field'
a = "D" & w ' I then add the number to the field which starts with D to get the field that I want
to pull the value from'
strFieldName = "me." & a
z = z - strFieldName '<--- This is where I am stuck how do I get the value from the field I am
referencing in order to subtract it from the QOH'
y = y + 1 'This is the counter that I return to the field in my form'
Loop
End If
Me.DOH = y ' the field that the result is passed to on the form'
End Sub
Try this:
Do Until z <= 0
w = w + 1
a = "D" & CStr(w)
z = z - Me(a).Value
y = y + 1
Loop
Suppose in column Z with 200 rows, are my optimal averages.
Now I want a macro that generates n random integers between a and b inclusive (n <= 20) so that difference between the average of numbers generated with optimal average is in (-0.15,+0.15).
Example:
Z1:optimal average1=5.5
Z2:optimal average2=5.3
Z200:optimal average200=6.3
n=8
a=1; b=10
numbers of generated:
A1:H1)5-9-4-3-7-4-9-3
A2:H2)10-7-3-2-5-4-3-9
.
.
.
A200:H200)4-8-9-6-6-6-10-2
Here is a hit-or-miss approach (which is often the only viable way to get random numbers which satisfy additional constraints in an unbiased way):
Function RandIntVect(n As Long, a As Long, b As Long, mean As Double, tol As Double, Optional maxTries As Long = 1000) As Variant
'Uses a hit-or-miss approach to generate a vector of n random ints in a,b inclusive whose mean is
'within the tolerance tol of the given target mean
'The function raises an error if maxTries misses occur without a hit
Dim sum As Long, i As Long, j As Long
Dim lowTarget As Double, highTarget As Double 'targets for *sums*
Dim vect As Variant
lowTarget = n * (mean - tol)
highTarget = n * (mean + tol)
For i = 1 To maxTries
ReDim vect(1 To n)
sum = 0
j = 0
Do While j < n And sum + a * (n - j) <= highTarget And sum + b * (n - j) >= lowTarget
j = j + 1
vect(j) = Application.WorksheetFunction.RandBetween(a, b)
sum = sum + vect(j)
Loop
If j = n And lowTarget <= sum And sum <= highTarget Then
'Debug.Print i 'uncomment this line to see how many tries required
RandIntVect = vect
Exit Function
End If
Next i
'error if we get to here
RandIntVect = CVErr(xlErrValue)
End Function
This could be used as a worksheet array formula. The target means were in column I and in A2:H2 I entered =RandIntVect(8,1,10,I2,0.15) (with ctrl+shift+enter as an array formula) and then copied down:
Note that array formulas are volatile, so these numbers would be recalculated every time the worksheet is. You could use the function in VBA to place the numbers directly in the ranges rather than using the function as a worksheet formula. Something like:
Sub test()
Dim i As Long
For i = 1 To 3
Range(Cells(i + 1, 1), Cells(i + 1, 8)).Value = RandIntVect(8, 1, 10, Cells(i + 1, 9).Value, 0.15)
Next i
End Sub
enter image description here
The difference between two means is not within range (0.15+, 0.15-)
I'm trying to calculate a cross currency rate by simply summing the forex rates for A/B and B/C and multiplying the means to find the A/C rate and I keep getting 0 in return. This is the code:
Function forex(audData As Range, euData As Range)
a = Application.Count(audData)
e = Application.Count(euData)
'Counts how many values are in the data
aSum = 0
eSum = 0
aMean = 0
eMean = 0
For i = 1 To aud ' This sums the 1st forex rate and finds the mean
aSum = aSum + audData(i)
Next i
aMean = aSum / a
For i = 1 To eu ' This sums the 2nd forex rate and finds the mean
eSum = eSum + euData(i)
Next i
eMean = eSum / e
forex = (aMean * eMean)
End Function
You could use the Average() function:
Function forex(audData As Range, euData As Range)
With WorksheetFunction
forex = .Average(audData) * .Average(euData)
End With
End Function
You seem to be a victim of undeclared variables. You have two loops,
For i = 1 To aud
and (later on)
For i = 1 To eu
where neither aud nor eu are declared. Thus, they default to variants with an implicit value of 0, hence neither of these loops ever execute and all your variables stay at 0.
You really should get in the habit of using Option Explicit at the top of all of your modules. This can be done automatically by enabling the option Require Variable Declarations in the VBA editor options. In the long run, it will save you hours of debugging time.
I can't test your code, but if you declare your variables and replace aud and eu by what I think you meant you would get:
Function forex(audData As Range, euData As Range) As Double
Dim a As Long, e As Long, aSum As Double, eSum As Double, aMean As Double, eMean As Double, i As Long
a = Application.Count(audData)
e = Application.Count(euData)
'Counts how many values are in the data
For i = 1 To a ' This sums the 1st forex rate and finds the mean
aSum = aSum + audData(i)
Next i
aMean = aSum / a
For i = 1 To e ' This sums the 2nd forex rate and finds the mean
eSum = eSum + euData(i)
Next i
eMean = eSum / e
forex = (aMean * eMean)
End Function
I skipped the lines like aSum = 0 since properly declared VBA variables have reasonable default values.
I'm trying to calculate how many layers a commodity will be stacked in. I have a variable quantity (iQty), a given width for the loadbed (dRTW), a width per unit for the commodity (dWidth) and a quantity per layer (iLayerQty).
The quantity per layer is calculated as iLayerQty = Int(dRTW/dWidth)
Now I need to divide the total quantity by the quantity per layer and round up. In an Excel formula it would be easy, but I'm trying to avoid WorksheetFunction calls to minimise A1/R1C1 confusion. At the moment I'm approximating it with this:
(Number of layers) = ((Int(iQty / iLayerQty) + 1)
And that works fine most of the time - except when the numbers give an integer (a cargo width of 0.5 m, for instance, fitting onto a 2.5 m rolltrailer). In those instances, of course, adding the one ruins the result.
Is there any handy way of tweaking that formula to get a better upward rounding?
I don't see any reason to avoid WorksheetFunction; I don't see any confusion here.
Number_of_layers = WorksheetFunction.RoundUp(iQty / iLayerQty, 0)
You could also roll your own function:
Function RoundUp(ByVal Value As Double)
If Int(Value) = Value Then
RoundUp = Value
Else
RoundUp = Int(Value) + 1
End If
End Function
Call it like this:
Number_of_layers = RoundUp(iQty / iLayerQty)
If using a WorksheetFunction object to access a ROUNDUP or CEILING function is off the table then the same can be accomplished with some maths.
Number of layers = Int(iQty / iLayerQty) - CBool(Int(iQty / iLayerQty) <> Round(iQty / iLayerQty, 14))
A VBA True is the equivalent of (-1) when used mathematically. The VBA Round is there to avoid 15 digit floating point errors.
I use -int(-x) to get the ceiling.
?-int(-1.1) ' get ceil(1.1)
2
?-int(1.1) ' get ceil(-1.1)
-1
?-int(-5) ' get ceil(5)
5
These are the functions I put together for this purpose.
Function RoundUp(ByVal value As Double) as Integer
Dim intVal As Integer
Dim delta As Double
intVal = CInt(value)
delta = intVal - value
If delta < 0 Then
RoundUp = intVal + 1
Else
RoundUp = intVal
End If
End Function
Function RoundDown(ByVal value As Double) as Integer
Dim intVal As Integer
Dim delta As Double
intVal = CInt(value)
delta = intVal - value
If delta <= 0 Then
RoundDown = intVal
ElseIf delta > 0 Then
RoundDown = intVal - 1
End If
End Function
This is my Ceiling in VBA.
Function Ceiling(ByVal Number As Double, ByVal Significance As Double) As Double
Dim intVal As Long
Dim delta As Double
Dim RoundValue As Double
Dim PreReturn As Double
If Significance = 0 Then
RoundValue = 1
Else
RoundValue = 1 / Significance
End If
Number = Number * RoundValue
intVal = CLng(Number)
delta = intVal - Number
If delta < 0 Then
PreReturn = intVal + 1
Else
PreReturn = intVal
End If
Ceiling = PreReturn / RoundValue
End Function
Ok I have got this far and if you run it, it will do what you ask. Now when I type in 99999 or -99999 it will not end. Can someone tell me what I am doing wrong. I am suppose to loop until a sentinel value of -99999 is enter for previous meter reading.
Sub Main()
' program to compute a consumer’s electric bill. It will calculate the bill for one or more customers
' by looping until a sentinel value of -99999 is entered for the previous meter reading.
Dim previousReading As Integer = 0
Dim currentReading As Integer = 0
Do While (previousReading <> -99999)
Dim salesTax As Double
' prompt user to input value for previous reading then convert to integer
Console.WriteLine("Enter the value of previous meter reading")
previousReading = Convert.ToInt32(Console.ReadLine())
' prompt user to input value for current reading then convert to integer
Console.WriteLine("Enter the value of current meter reading")
currentReading = Convert.ToInt32(Console.ReadLine())
Dim kwhConsumed As Integer
Dim electricCharge, totalBill As Double
' calculate KWH consumed
kwhConsumed = currentReading - previousReading
' Use select case to determine electricCharge
Select Case kwhConsumed
Case Is < 500
electricCharge = kwhConsumed * 0.05
Case 500 To 1000
electricCharge = 25 + ((kwhConsumed - 500) * 0.055)
Case Is > 1000
electricCharge = 52.5 + ((kwhConsumed - 1000) * 0.06)
End Select
' calculate sales tax
salesTax = electricCharge * 0.085
' calculate total charges
totalBill = electricCharge + salesTax
' Output values for kwhConsumed, electricCharge, salesTax, and totalBill
Console.WriteLine("KWH consumed = " & kwhConsumed & " KWH")
Console.WriteLine("Electric charge = $" & Math.Round(electricCharge, 2))
Console.WriteLine("Sales tax = $" & Math.Round(salesTax, 2))
Console.WriteLine("Total bill = $" & Math.Round(totalBill, 2))
Loop
End Sub
You can try using string comparison instead for previousReading <> -99999. You also need to use absolute value to consider both -99999 and 99999. Do something like this
Do While (previousReading <> 99999)
//code
previousReading = Math.Abs(Convert.ToInt32(Console.ReadLine()))
//code
Loop
I'm guessing this is homework?
Instead of blurting out the answer, I wonder if you might think about inserting a Debug.Print statement and some kind of "break" statement after your previousReading = Convert.ToInt32 statement. To look for the "break" statement, search for "vb.net exit loop" and see what pops up.